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 2 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: 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)
}
}