ここ最近、仕事が変わったのと Rustでこさえてたツール の改修に手間取っててあまり触ってたなかったのだけど、ある程度一段落したので Goのsshクライアントの改修 に着手した。 で、sshでの接続方式というといくつかあると思うが、最近はYubikeyをPIVカードとして利用して、Yubikey内の秘密鍵を使ってssh接続するというやり方もあるので、それをGolangで実装しようと思いやってみた。

これ、実はかなり手間取ってしまい、結構時間がかかってしまった…。 Yubikey等のPIVカードに入ってる秘密鍵を使ってssh接続する場合、OpenSCなどのPKCS11を扱えるライブラリが必要になるのだが、それがいまいちよくわからなかった(実装できた今もさっぱりわからない…)。

Golangは結構いろんな人がいろんなライブラリを提供してくれてて、その中にpkcs11を扱うための、OpenSCなどのラッパーとして扱えるライブラリもあるのだが、それらを利用する。 ssh接続をする場合はYubikeyのKeyPairを使って、Signerとして扱える必要があるので、今回はcrypto11というライブラリを主に使っている(ココいくまでも右往左往してしまった…)。

以下、サンプルコード。 一応 こっち にも上げてある。

ssh_term_pkcs11.go
package main import ( "crypto" "fmt" "os" "os/signal" "syscall" "github.com/ThalesIgnite/crypto11" "github.com/miekg/pkcs11" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/terminal" ) var ( // ↓この2つは指定が必要 pkcs11Path = "/usr/local/lib/opensc-pkcs11.so" tokenLabel = "tokenLabel" // 鍵のIDは決め打ち。(p11toolでの出力だと「01」に該当) // 自動取得にする場合は、"pkcs11.NewAttribute"で指定して取得することで対応すればいい。 // ↓ サンプル // ------ // findTemplate := []*pkcs11.Attribute{ // pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY), // pkcs11.NewAttribute(pkcs11.CKA_PRIVATE, true), // pkcs11.NewAttribute(pkcs11.CKA_ID, true), // pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_RSA), // } // p.FindObjectsInit(session, findTemplate) // ------ keyID = []byte{'\x01'} // sshの接続情報 host = "target.host" port = "22" user = "user" ) func main() { // PINコードの入力を受け付け fmt.Printf("PIN: ") tokenPin, err := terminal.ReadPassword(int(syscall.Stdin)) if err != nil { fmt.Println("err: {}", err) os.Exit(1) } // pkcs11のConfigを作成 config := &crypto11.PKCS11Config{ Path: pkcs11Path, TokenLabel: tokenLabel, Pin: string(tokenPin), } // pkcs11のConfigを使いctxを作成 ctx, err := crypto11.Configure(config) if err != nil { fmt.Println("err: {}", err) os.Exit(1) } // pkcs11のセッションを作成 keysession, err := ctx.OpenSession(0, pkcs11.CKF_SERIAL_SESSION) if err != nil { fmt.Println("err: {}", err) os.Exit(1) } pkcs11Session := &crypto11.PKCS11Session{ctx, keysession} // PIVのSignerを作成する(slot,idは決め打ち) prv, err := crypto11.FindKeyPairOnSession(pkcs11Session, 0, keyID, nil) if err != nil { fmt.Println("err: {}", err) os.Exit(1) } signer := prv.(crypto.Signer) // PIVのSignerからssh用のSignerを作成する sshSigner, err := ssh.NewSignerFromSigner(signer) if err != nil { fmt.Println("err: {}", err) os.Exit(1) } auth := []ssh.AuthMethod{} auth = append(auth, ssh.PublicKeys(sshSigner)) // --------------------- // // こっから下はコピペのやつ // // --------------------- // Create sshClientConfig sshConfig := &ssh.ClientConfig{ User: user, Auth: auth, HostKeyCallback: ssh.InsecureIgnoreHostKey(), BannerCallback: ssh.BannerDisplayStderr(), } // SSH connect. client, err := ssh.Dial("tcp", host+":"+port, sshConfig) if err != nil { fmt.Println("client: {}", err) os.Exit(0) } // Create Session session, err := client.NewSession() defer session.Close() // キー入力を接続先が認識できる形式に変換する(ここがキモ) fd := int(os.Stdin.Fd()) state, err := terminal.MakeRaw(fd) if err != nil { fmt.Println(err) } defer terminal.Restore(fd, state) // ターミナルサイズの取得 w, h, err := terminal.GetSize(fd) if err != nil { fmt.Println(err) } modes := ssh.TerminalModes{ ssh.ECHO: 1, ssh.TTY_OP_ISPEED: 14400, ssh.TTY_OP_OSPEED: 14400, } err = session.RequestPty("xterm", h, w, modes) if err != nil { fmt.Println(err) } session.Stdout = os.Stdout session.Stderr = os.Stderr session.Stdin = os.Stdin err = session.Shell() if err != nil { fmt.Println(err) } // ターミナルサイズの変更検知・処理 signal_chan := make(chan os.Signal, 1) signal.Notify(signal_chan, syscall.SIGWINCH) go func() { for { s := <-signal_chan switch s { case syscall.SIGWINCH: fd := int(os.Stdout.Fd()) w, h, _ = terminal.GetSize(fd) session.WindowChange(h, w) } } }() err = session.Wait() if err != nil { fmt.Println(err) } }

ひとまず、これでPIVカードに入ってる秘密鍵を利用してssh接続できるはず…(自分の環境(Mac + OpenSC + Yubikey)ではできた)。 あまり一般的な接続方式ではないけど、今後は物理トークンを用いた接続も使われていくことがあるだろうし、うまいこと付き合っていきたいところだなぁと(´・ω・`)。

PKCS11、ちゃんと扱える様になりたいのだけど、自分が理解できるような情報がいまいち見つけられず、未だによくわかってない…。 誰か詳しい人いたら教えてください(´;ω;`)。