forked from kbolino/pageant
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add pageant.go, tests, and README.md
- Loading branch information
Showing
4 changed files
with
375 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
# Go Pageant client | ||
|
||
This repository contains a library for Go that provides a native | ||
[PuTTY][putty] Pageant SSH agent implementation compatible with the | ||
[golang.org/x/crypto/ssh/agent][go-ssh-agent] package. | ||
|
||
This page, rather unsuprisingly, only works with Windows. | ||
See below for alternatives on Unix/Linux platforms. | ||
|
||
[putty]: https://www.chiark.greenend.org.uk/~sgtatham/ | ||
[go-ssh-agent]: https://godoc.org/golang.org/x/crypto/ssh/agent | ||
|
||
## Usage | ||
|
||
```golang | ||
import ( | ||
"golang.org/x/crypto/ssh" | ||
"golang.org/x/crypto/ssh/agent" | ||
"github.com/kbolino/pageant" | ||
) | ||
|
||
func main() { | ||
agentConn, err := pageant.NewConn() | ||
if err != nil { | ||
// failed to connect to Pageant | ||
} | ||
defer agentConn.Close() | ||
sshAgent := agent.NewClient(agentConn) | ||
signers, err := sshAgent.Signers() | ||
if err != nil { | ||
// failed to get signers from Pageant | ||
} | ||
config := ssh.ClientConfig{ | ||
Auth: []ssh.AuthMethod{ssh.PublicKeys(signers...)}, | ||
HostKeyCallback: ssh.InsecureIgnoreHostKey(), | ||
User: "somebody", | ||
} | ||
sshConn, err := ssh.Dial("tcp", "someserver:22", &config) | ||
if err != nil { | ||
// failed to connect to SSH | ||
} | ||
defer sshConn.Close() | ||
// now connected to SSH with public key auth from Pageant | ||
// ... | ||
} | ||
``` | ||
|
||
## Unix/Linux Alternatives | ||
|
||
The `ssh-agent` command implements the same [SSH agent protocol][ssh-agent] | ||
as Pageant, but over a Unix domain socket instead of shared memory. | ||
The path to this socket is exposed through the environment variable | ||
`SSH_AUTH_SOCK`. | ||
|
||
Replace the connection to Pageant with one to the socket: | ||
```golang | ||
// instead of this: | ||
agentConn, err := pageant.NewConn() | ||
// do this: | ||
agentConn, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")) | ||
``` | ||
|
||
[ssh-agent]: https://tools.ietf.org/html/draft-miller-ssh-agent-02 | ||
|
||
## Testing | ||
|
||
The standard tests require Pageant to be running and to have at least 1 | ||
key loaded. | ||
To test connecting to an SSH server, set the `sshtest` build flag and | ||
see the comments in `pageant_ssh_test.go` for how to set up the test. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
// Package pageant provides native Go support for using PuTTY Pageant as an | ||
// SSH agent with the golang.org/x/crypto/ssh/agent package. | ||
// Based loosely on the Java JNA package jsch-agent-proxy-pageant. | ||
package pageant | ||
|
||
import ( | ||
"encoding/binary" | ||
"fmt" | ||
"io" | ||
"reflect" | ||
"syscall" | ||
"unsafe" | ||
|
||
"golang.org/x/sys/windows" | ||
) | ||
|
||
const ( | ||
agentCopydataID = 0x804e50ba | ||
agentMaxMsglen = 8192 | ||
noError = syscall.Errno(0) | ||
wmCopyData = 0x004a | ||
) | ||
|
||
var ( | ||
pageantWindowName = utf16Ptr("Pageant") | ||
user32 = windows.NewLazySystemDLL("user32.dll") | ||
findWindow = user32.NewProc("FindWindowW") | ||
sendMessage = user32.NewProc("SendMessageW") | ||
) | ||
|
||
// Conn is a shared-memory connection to Pageant. | ||
// Conn implements io.Reader, io.Writer, and io.Closer. | ||
// It is not safe to use Conn in multiple concurrent goroutines. | ||
type Conn struct { | ||
window windows.Handle | ||
sharedFile windows.Handle | ||
sharedMem uintptr | ||
readOffset int | ||
readLimit int | ||
mapName string | ||
} | ||
|
||
var _ io.ReadWriteCloser = &Conn{} | ||
|
||
// NewConn creates a new connection to Pageant. | ||
// Ensure Close gets called on the returned Conn when it is no longer needed. | ||
func NewConn() (*Conn, error) { | ||
return &Conn{}, nil | ||
} | ||
|
||
// Close frees resources used by Conn. | ||
func (c *Conn) Close() error { | ||
if c.sharedMem == 0 { | ||
return nil | ||
} | ||
errUnmap := windows.UnmapViewOfFile(c.sharedMem) | ||
errClose := windows.CloseHandle(c.sharedFile) | ||
if errUnmap != nil { | ||
return errUnmap | ||
} else if errClose != nil { | ||
return errClose | ||
} | ||
c.sharedMem = 0 | ||
c.sharedFile = windows.InvalidHandle | ||
return nil | ||
} | ||
|
||
func (c *Conn) Read(p []byte) (n int, err error) { | ||
if c.sharedMem == 0 { | ||
return 0, fmt.Errorf("not connected to Pageant") | ||
} else if c.readLimit == 0 { | ||
return 0, fmt.Errorf("must send request to Pageant before reading response") | ||
} else if c.readOffset == c.readLimit { | ||
return 0, io.EOF | ||
} | ||
bytesToRead := minInt(len(p), int(c.readLimit-c.readOffset)) | ||
src := toSlice(c.sharedMem+uintptr(c.readOffset), bytesToRead) | ||
copy(p, src) | ||
c.readOffset += bytesToRead | ||
return bytesToRead, nil | ||
} | ||
|
||
func (c *Conn) Write(p []byte) (n int, err error) { | ||
if len(p) > agentMaxMsglen { | ||
return 0, fmt.Errorf("size of message to send (%d) exceeds max length (%d)", len(p), agentMaxMsglen) | ||
} else if len(p) == 0 { | ||
return 0, fmt.Errorf("message to send is empty") | ||
} | ||
if c.sharedMem != 0 { | ||
err := c.Close() | ||
if c.sharedMem != 0 { | ||
return 0, fmt.Errorf("failed to close previous connection: %s", err) | ||
} | ||
} | ||
if err := c.establishConn(); err != nil { | ||
return 0, fmt.Errorf("failed to connect to Pageant: %s", err) | ||
} | ||
dst := toSlice(c.sharedMem, len(p)) | ||
copy(dst, p) | ||
data := make([]byte, len(c.mapName)+1) | ||
copy(data, c.mapName) | ||
result, err := c.sendMessage(data) | ||
if result == 0 { | ||
if err != nil { | ||
return 0, fmt.Errorf("failed to send request to Pageant: %s", err) | ||
} else { | ||
return 0, fmt.Errorf("request refused by Pageant") | ||
} | ||
} | ||
messageSize := binary.BigEndian.Uint32(toSlice(c.sharedMem, 4)) | ||
c.readOffset = 0 | ||
c.readLimit = 4 + int(messageSize) | ||
return len(p), nil | ||
} | ||
|
||
// establishConn creates a new connection to Pageant. | ||
func (c *Conn) establishConn() error { | ||
window, _, err := findWindow.Call( | ||
uintptr(unsafe.Pointer(pageantWindowName)), | ||
uintptr(unsafe.Pointer(pageantWindowName)), | ||
) | ||
if window == 0 { | ||
if err != nil && err != noError { | ||
return fmt.Errorf("cannot find Pageant window: %s", err) | ||
} else { | ||
return fmt.Errorf("cannot find Pageant window, ensure Pageant is running") | ||
} | ||
} | ||
mapName := fmt.Sprintf("PageantRequest%08x", windows.GetCurrentThreadId()) | ||
mapNameUTF16 := utf16Ptr(mapName) | ||
sharedFile, err := windows.CreateFileMapping( | ||
windows.InvalidHandle, | ||
nil, | ||
windows.PAGE_READWRITE, | ||
0, | ||
agentMaxMsglen, | ||
mapNameUTF16, | ||
) | ||
if err != nil { | ||
return fmt.Errorf("failed to create shared file: %s", err) | ||
} | ||
sharedMem, err := windows.MapViewOfFile( | ||
sharedFile, | ||
windows.FILE_MAP_WRITE, | ||
0, | ||
0, | ||
0, | ||
) | ||
if err != nil { | ||
return fmt.Errorf("failed to map file into shared memory: %s", err) | ||
} | ||
*c = Conn{ | ||
window: windows.Handle(window), | ||
sharedFile: sharedFile, | ||
sharedMem: sharedMem, | ||
mapName: mapName, | ||
} | ||
return nil | ||
} | ||
|
||
// sendMessage invokes user32.SendMessage to alert Pageant that data | ||
// is available for it to read. | ||
func (c *Conn) sendMessage(data []byte) (uintptr, error) { | ||
cds := copyData{ | ||
dwData: agentCopydataID, | ||
cbData: uintptr(len(data)), | ||
lpData: uintptr(unsafe.Pointer(&data[0])), | ||
} | ||
result, _, err := sendMessage.Call( | ||
uintptr(c.window), | ||
wmCopyData, | ||
0, | ||
uintptr(unsafe.Pointer(&cds)), | ||
) | ||
if err == noError { | ||
return result, nil | ||
} | ||
return result, err | ||
} | ||
|
||
// copyData is equivalent to COPYDATASTRUCT. | ||
// Unlike Java, Go has a native type that matches the bit width of the | ||
// platform, so there is no need for separate 32-bit and 64-bit versions. | ||
// Curiously, the MSDN definition of COPYDATASTRUCT says dwData is ULONG_PTR | ||
// and cbData is DWORD, which seems to be backwards. | ||
type copyData struct { | ||
dwData uint32 | ||
cbData uintptr | ||
lpData uintptr | ||
} | ||
|
||
// minInt returns the lesser of x and y. | ||
func minInt(x, y int) int { | ||
if x < y { | ||
return x | ||
} else { | ||
return y | ||
} | ||
} | ||
|
||
// toSlice creates a fake slice header that allows copying to/from the block | ||
// of memory from addr to addr+size. | ||
func toSlice(addr uintptr, size int) []byte { | ||
header := reflect.SliceHeader{ | ||
Len: size, | ||
Cap: size, | ||
Data: addr, | ||
} | ||
return *(*[]byte)(unsafe.Pointer(&header)) | ||
} | ||
|
||
// utf16Ptr converts a static string not containing any zero bytes to a | ||
// sequence of UTF-16 code units, represented as a pointer to the first one. | ||
func utf16Ptr(s string) *uint16 { | ||
result, err := windows.UTF16PtrFromString(s) | ||
if err != nil { | ||
panic(err) | ||
} | ||
return result | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
// +build sshtest | ||
|
||
package pageant | ||
|
||
import ( | ||
"os" | ||
"testing" | ||
|
||
"golang.org/x/crypto/ssh" | ||
"golang.org/x/crypto/ssh/agent" | ||
) | ||
|
||
// This test requires all of the following to work: | ||
// - build tag sshtest is active | ||
// - 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 TestSSHConnect(t *testing.T) { | ||
pageantConn, err := NewConn() | ||
if err != nil { | ||
t.Fatalf("error on NewConn: %s", err) | ||
} | ||
defer pageantConn.Close() | ||
sshAgent := agent.NewClient(pageantConn) | ||
signers, err := sshAgent.Signers() | ||
if err != nil { | ||
t.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 { | ||
t.Fatalf("failed to connect to %s@%s due to error: %s", sshUser, sshAddr, err) | ||
} | ||
sshConn.Close() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package pageant | ||
|
||
import ( | ||
"testing" | ||
|
||
"golang.org/x/crypto/ssh/agent" | ||
) | ||
|
||
// Pageant must be running for this test to work. | ||
func TestNewConn(t *testing.T) { | ||
conn, err := NewConn() | ||
if err != nil { | ||
t.Fatalf("error on NewConn: %s", err) | ||
} else if conn == nil { | ||
t.Fatalf("NewConn 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) { | ||
conn, err := NewConn() | ||
if err != nil { | ||
t.Fatalf("error on NewConn: %s", err) | ||
} | ||
defer conn.Close() | ||
sshAgent := agent.NewClient(conn) | ||
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) | ||
} | ||
} |