diff --git a/example/pageant_ssh.go b/example/pageant_ssh.go new file mode 100644 index 0000000..43fd056 --- /dev/null +++ b/example/pageant_ssh.go @@ -0,0 +1,52 @@ +// based on [pageant](https://github.com/kbolino/pageant) of Kristian Bolino +package main + +import ( + "log" + "os" + + sshagent "github.com/xanzy/ssh-agent" + "golang.org/x/crypto/ssh" +) + +// This example requires all of the following to work: +// - environment variable PAGEANT_TEST_SSH_ADDR is set to a valid SSH +// server address (host:port) +// - environment variable PAGEANT_TEST_SSH_USER is set to a user name +// that the SSH server recognizes +// - Pageant is running on the local machine +// - Pageant has a key that is authorized for the user on the server +func main() { + sshAgent, pageantConn, err := sshagent.New() + if err != nil { + log.Fatalf("error on New: %s", err) + } + defer pageantConn.Close() + keys, err := sshAgent.List() + if err != nil { + log.Fatalf("error on agent.List: %s", err) + } + if len(keys) == 0 { + log.Fatalf("no keys listed by Pagent") + } + for i, key := range keys { + log.Printf("key %d: %s %s\n", i, key.Comment, ssh.FingerprintSHA256(key)) + } + + signers, err := sshAgent.Signers() + if err != nil { + log.Fatalf("cannot obtain signers from SSH agent: %s", err) + } + sshUser := os.Getenv("PAGEANT_TEST_SSH_USER") + config := ssh.ClientConfig{ + Auth: []ssh.AuthMethod{ssh.PublicKeys(signers...)}, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + User: sshUser, + } + sshAddr := os.Getenv("PAGEANT_TEST_SSH_ADDR") + sshConn, err := ssh.Dial("tcp", sshAddr, &config) + if err != nil { + log.Fatalf("failed to connect to %s@%s due to error: %s", sshUser, sshAddr, err) + } + sshConn.Close() +} diff --git a/pageant_windows.go b/pageant_windows.go index 1608e54..072b81b 100644 --- a/pageant_windows.go +++ b/pageant_windows.go @@ -36,7 +36,7 @@ import ( "golang.org/x/sys/windows" ) -// Maximum size of message can be sent to pageant +// Maximum size of message can be sent to pageant. const MaxMessageLen = 8192 var ( @@ -76,8 +76,6 @@ func winAPI(dll *windows.LazyDLL, funcName string) func(...uintptr) (uintptr, ui } // Query sends message msg to Pageant and returns response or error. -// 'msg' is raw agent request with length prefix -// Response is raw agent response with length prefix func query(msg []byte) ([]byte, error) { if len(msg) > MaxMessageLen { return nil, ErrMessageTooLong diff --git a/sshagent.go b/sshagent.go index 4a4ee30..f3bc509 100644 --- a/sshagent.go +++ b/sshagent.go @@ -28,7 +28,7 @@ import ( "golang.org/x/crypto/ssh/agent" ) -// New returns a new agent.Agent that uses a unix socket +// New returns a new agent.Agent that uses a unix socket. func New() (agent.Agent, net.Conn, error) { if !Available() { return nil, nil, errors.New("SSH agent requested but SSH_AUTH_SOCK not-specified") @@ -44,7 +44,7 @@ func New() (agent.Agent, net.Conn, error) { return agent.NewClient(conn), conn, nil } -// Available returns true is a auth socket is defined +// Available returns true if an auth socket is defined. func Available() bool { return os.Getenv("SSH_AUTH_SOCK") != "" } diff --git a/sshagent_windows.go b/sshagent_windows.go index 175d161..48535f7 100644 --- a/sshagent_windows.go +++ b/sshagent_windows.go @@ -26,22 +26,29 @@ import ( "errors" "io" "net" + "os" + "strings" "sync" + "time" "github.com/Microsoft/go-winio" "golang.org/x/crypto/ssh/agent" ) const ( - sshAgentPipe = `\\.\pipe\openssh-ssh-agent` + pipe = `\\.\pipe\` + openSSHAgentPipe = pipe + "openssh-ssh-agent" ) -// Available returns true if Pageant is running +// Available returns true if Pageant is running. func Available() bool { if pageantWindow() != 0 { return true } - conn, err := winio.DialPipe(sshAgentPipe, nil) + if sshAuthSock := os.Getenv("SSH_AUTH_SOCK"); sshAuthSock != "" { + return true + } + conn, err := winio.DialPipe(openSSHAgentPipe, nil) if err != nil { return false } @@ -50,18 +57,34 @@ func Available() bool { } // New returns a new agent.Agent and the (custom) connection it uses -// to communicate with a running pagent.exe instance (see README.md) +// to communicate with a running pagent.exe instance (see README.md). func New() (agent.Agent, net.Conn, error) { if pageantWindow() != 0 { - return agent.NewClient(&conn{}), nil, nil + return agent.NewClient(&conn{}), &conn{}, nil + } + + sshAgentPipe := openSSHAgentPipe + if sshAuthSock := os.Getenv("SSH_AUTH_SOCK"); sshAuthSock != "" { + conn, err := net.Dial("unix", sshAuthSock) + if err == nil { + return agent.NewClient(conn), conn, nil + } + + if !strings.HasPrefix(sshAuthSock, pipe) { + sshAuthSock = pipe + sshAuthSock + } + + sshAgentPipe = sshAuthSock } + conn, err := winio.DialPipe(sshAgentPipe, nil) if err != nil { return nil, nil, errors.New( "SSH agent requested, but could not detect Pageant or Windows native SSH agent", ) } - return agent.NewClient(conn), nil, nil + + return agent.NewClient(conn), conn, nil } type conn struct { @@ -69,10 +92,11 @@ type conn struct { buf []byte } -func (c *conn) Close() { +func (c *conn) Close() error { c.Lock() defer c.Unlock() c.buf = nil + return nil } func (c *conn) Write(p []byte) (int, error) { @@ -102,3 +126,20 @@ func (c *conn) Read(p []byte) (int, error) { return n, nil } + +// for similarity with net.Conn +func (c *conn) LocalAddr() net.Addr { + return nil +} +func (c *conn) RemoteAddr() net.Addr { + return nil +} +func (c *conn) SetDeadline(_ time.Time) error { + return nil +} +func (c *conn) SetReadDeadline(_ time.Time) error { + return nil +} +func (c *conn) SetWriteDeadline(_ time.Time) error { + return nil +} diff --git a/sshagent_windows_test.go b/sshagent_windows_test.go new file mode 100644 index 0000000..1978fa9 --- /dev/null +++ b/sshagent_windows_test.go @@ -0,0 +1,40 @@ +// based on [pageant](https://github.com/kbolino/pageant) of Kristian Bolino + +package sshagent + +import ( + "testing" +) + +// Pageant must be running for this test to work. +func TestNew(t *testing.T) { + _, conn, err := New() + if err != nil { + t.Fatalf("error on New: %s", err) + } else if conn == nil { + t.Fatalf("New returned nil") + } + err = conn.Close() + if err != nil { + t.Fatalf("error on Conn.Close: %s", err) + } +} + +// Pageant must be running and have at least 1 key loaded for this test to work. +func TestSSHAgentList(t *testing.T) { + sshAgent, conn, err := New() + if err != nil { + t.Fatalf("error on New: %s", err) + } + defer conn.Close() + keys, err := sshAgent.List() + if err != nil { + t.Fatalf("error on agent.List: %s", err) + } + if len(keys) == 0 { + t.Fatalf("no keys listed by Pagent") + } + for i, key := range keys { + t.Logf("key %d: %s", i, key.Comment) + } +}