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/go.mod b/go.mod index 701b3bb..098fa97 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,6 @@ go 1.16 require ( github.com/Microsoft/go-winio v0.5.2 - golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 + golang.org/x/crypto v0.19.0 + golang.org/x/sys v0.17.0 ) diff --git a/go.sum b/go.sum index 4012158..a7b2298 100644 --- a/go.sum +++ b/go.sum @@ -4,17 +4,52 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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) + } +}