前回、goでssh-agentをForwardingする処理について書いたことがあったけど、認証自体は普通にパスワードで処理するように書いていた。で、ssh-agentで認証させることもできるようなので、サンプルコードを書いてみた。

以下、サンプル(一応 こちら にもあげている)。

package main

import (
    "fmt"
    "io/ioutil"
    "net"
    "os"
    "os/signal"
    "syscall"

    "golang.org/x/crypto/ssh"
    "golang.org/x/crypto/ssh/agent"
    "golang.org/x/crypto/ssh/terminal"
)

var (
    host          = "target.host"
    port          = "22"
    user          = "user"
    ssh_agent_key = "/path/to/.ssh/id_rsa" // SshAgentに新たに追加する鍵のPATH(フルパス)
)

func main() {
    // SSH_AUTH_SOCKのSocketへ接続し、SshAgentとしてセッションを開始する
    sock, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK"))
    if err != nil {
        panic(err)
    }
    agentsock := agent.NewClient(sock)

    // ssh agentに読み込ませる鍵ファイルを読み込む
    buf, err := ioutil.ReadFile(ssh_agent_key)
    if err != nil {
        panic(err)
    }

    // ssh agentに読み込ませる鍵ファイルをパースする(interface形式)
    key, err := ssh.ParseRawPrivateKey(buf)
    if err != nil {
        panic(err)
    }

    // SshAgentに鍵を追加する前のリストを出力
    fmt.Println(agentsock.List())

    // 鍵を追加する
    err = agentsock.Add(agent.AddedKey{
        PrivateKey:       key,
        ConfirmBeforeUse: true,
        LifetimeSecs:     300, // SshAgentに追加する鍵の利用可能時間(秒)
    })

    if err != nil {
        fmt.Println("add keyring error: %s", err)
    }

    // SshAgentに鍵追加後のリストを出力
    fmt.Println(agentsock.List())

    // Create sshClientConfig
    signers, err := agentsock.Signers()
    if err != nil {
        fmt.Println("create signers error: %s", err)
    }

    sshConfig := &ssh.ClientConfig{
        User: user,
        Auth: []ssh.AuthMethod{
            ssh.PublicKeys(signers...),
        },
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }

    // SSH connect.
    client, err := ssh.Dial("tcp", host+":"+port, sshConfig)

    // Create Session
    session, err := client.NewSession()
    defer session.Close()

    // ssh agentに鍵を転送する
    agent.ForwardToAgent(client, agentsock)
    agent.RequestAgentForwarding(session)

    // キー入力を接続先が認識できる形式に変換する(ここがキモ)
    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)
    }
}

Golangはssh周りの機能が多いので楽でいいなあ。 できれば、x11ForwardingとPKCS#11の接続も実装してくれると嬉しい(´・ω・`)。