Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

net.Conn for pageant with example and tests #16

Open
wants to merge 3 commits into
base: feat/ssh-auth-sock
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions example/pageant_ssh.go
Original file line number Diff line number Diff line change
@@ -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()
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
35 changes: 35 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
4 changes: 1 addition & 3 deletions pageant_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions sshagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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") != ""
}
55 changes: 48 additions & 7 deletions sshagent_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -50,29 +57,46 @@ 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the point of returning a fake net.Conn here instead of just returning nil to indicate to a user there is no underlying net.Conn connection to be managed/closed?

I think we should omit this and also remove the methods you added for similarity with net.Conn.

I think

}

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 {
sync.Mutex
buf []byte
}

func (c *conn) Close() {
func (c *conn) Close() error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this is also done to mimic net.Conn? Inline with my other comment I suggest we do not mask this conn type so we can return it as some kind a fake net.Conn.

c.Lock()
defer c.Unlock()
c.buf = nil
return nil
}

func (c *conn) Write(p []byte) (int, error) {
Expand Down Expand Up @@ -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
}
40 changes: 40 additions & 0 deletions sshagent_windows_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}