Skip to content

Commit

Permalink
Go version of wsl-ssh-pageant
Browse files Browse the repository at this point in the history
  • Loading branch information
benpye committed Feb 6, 2019
0 parents commit f8eaf10
Show file tree
Hide file tree
Showing 6 changed files with 341 additions and 0 deletions.
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Visual Studio Code
.vscode/*

# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out
9 changes: 9 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Copyright 2019 Ben Pye and contributors

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
54 changes: 54 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# wsl-ssh-pageant

## Why
I use a Yubikey to store a GPG key pair and I like to use this key pair as my SSH key too. GPG on Windows exposes a Pageant style SSH agent and I wanted a way to use this key within WSL. I have rewritten this in Go as it means the release is a single simple binary, and I like Go.

## How to use with WSL

1. On the Windows side start Pageant (or compatible agent such as gpg4win).

2. Run `wsl-ssh-pageant.exe --wsl C:\wsl-ssh-pageant\ssh-agent.sock` (or any other path, max ~100 characters)

3. In WSL export the `SSH_AUTH_SOCK` environment variable to point at the socket, for example, if you have `ssh-agent.sock` in `C:\wsl-ssh-pageant`
```
$ export SSH_AUTH_SOCK=/mnt/c/wsl-ssh-pageant/ssh-agent.sock
```

4. The SSH keys from Pageant should now be usable by `ssh`

## How to use with Windows 10 native OpenSSH client

1. On the Windows side start Pageant (or compatible agent such as gpg4win).

2. Run `wsl-ssh-pageant.exe --winssh ssh-pageant` (or any other name)

3. In `cmd` export the `SSH_AUTH_SOCK` environment variable or define it in your Environment Variables on Windows. Use the name you gave the pipe, for example:

```
$ set SSH_AUTH_SOCK=\\.\pipe\ssh-pageant
```

4. The SSH keys from Pageant should now be usable by the native Windows SSH client, try using `ssh` in `cmd.exe`

## Note

You can use both `--winssh` and `--wsl` parameters at the same time with the same process to proxy for both

# Frequently asked questions

## How do I download it?
Grab the latest release on the [releases page](https://github.com/benpye/wsl-ssh-pageant/releases).

## How do I build this?
For WSL support you will need Go 1.12 or later,. Go 1.12 added support for `AF_UNIX` sockets on Windows.

## What version of Windows do I need?
You need Windows 10 1803 or later for WSL support as it is the first version supporting `AF_UNIX` sockets. You can still use this with the native [Windows SSH client](https://github.com/PowerShell/Win32-OpenSSH/releases) on earlier builds.

## You didn't answer my question!
Please open an issue, I do try and keep on top of them, promise.

# Credit

* Thanks to [John Starks](https://github.com/jstarks/) for [npiperelay](https://github.com/jstarks/npiperelay/) for an example of a more secure way to create a stream between WSL and Linux before `AF_UNIX` sockets were available.
* Thanks for [Mark Dietzer](https://github.com/Doridian) for several contributions to the old .NET implementation.
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module github.com/benpye/wsl-ssh-pageant

go 1.12

require (
github.com/Microsoft/go-winio v0.4.11
github.com/lxn/win v0.0.0-20181015143721-a7f87360b10e
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
github.com/Microsoft/go-winio v0.4.11 h1:zoIOcVf0xPN1tnMVbTtEdI+P8OofVk3NObnwOQ6nK2Q=
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/lxn/win v0.0.0-20181015143721-a7f87360b10e h1:dz4TzIsrPe4XtUyhLkOLdCS8UkVwJKQu4WY8VcIwo3I=
github.com/lxn/win v0.0.0-20181015143721-a7f87360b10e/go.mod h1:jACzEp9RV7NhfPJQkiCNTteU4nkZZVlvkNpYtVOZPfE=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952 h1:FDfvYgoVsA7TTZSbgiqjAbfPbK47CNHdWl3h/PJtii0=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
248 changes: 248 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
package main

import (
"bufio"
"encoding/binary"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"os"
"os/signal"
"reflect"
"sync"
"syscall"
"unsafe"

"github.com/Microsoft/go-winio"
"github.com/lxn/win"
"golang.org/x/sys/windows"
)

var (
unixSocket = flag.String("wsl", "", "Path to Unix socket for passthrough to WSL")
namedPipe = flag.String("winssh", "", "Named pipe for use with Win32 OpenSSH")
verbose = flag.Bool("verbose", false, "Enable verbose logging")
)

const (
// Windows constats
invalidHandleValue = ^windows.Handle(0)
pageReadWrite = 0x4
fileMapWrite = 0x2

// ssh-agent/Pageant constants
agentMaxMessageLength = 8192
agentCopyDataID = 0x804e50ba
)

// copyDataStruct is used to pass data in the WM_COPYDATA message.
// We directly pass a pointer to our copyDataStruct type, we need to be
// careful that it matches the Windows type exactly
type copyDataStruct struct {
dwData uintptr
cbData uint32
lpData uintptr
}

var queryPageantMutex sync.Mutex

func queryPageant(buf []byte) (result []byte, err error) {
if len(buf) > agentMaxMessageLength {
err = errors.New("Message too long")
return
}

hwnd := win.FindWindow(syscall.StringToUTF16Ptr("Pageant"), syscall.StringToUTF16Ptr("Pageant"))

if hwnd == 0 {
err = errors.New("Could not find Pageant window")
return
}

// Typically you'd add thread ID here but thread ID isn't useful in Go
// We would need goroutine ID but Go hides this and provides no good way of
// accessing it, instead we serialise calls to queryPageant and treat it
// as not being goroutine safe
mapName := fmt.Sprintf("WSLPageantRequest")
queryPageantMutex.Lock()

fileMap, err := windows.CreateFileMapping(invalidHandleValue, nil, pageReadWrite, 0, agentMaxMessageLength, syscall.StringToUTF16Ptr(mapName))
if err != nil {
queryPageantMutex.Unlock()
return
}
defer func() {
windows.CloseHandle(fileMap)
queryPageantMutex.Unlock()
}()

sharedMemory, err := windows.MapViewOfFile(fileMap, fileMapWrite, 0, 0, 0)
if err != nil {
return
}
defer windows.UnmapViewOfFile(sharedMemory)

sharedMemoryArray := (*[agentMaxMessageLength]byte)(unsafe.Pointer(sharedMemory))
copy(sharedMemoryArray[:], buf)

mapNameWithNul := mapName + "\000"

// We use our knowledge of Go strings to get the length and pointer to the
// data and the length directly
cds := copyDataStruct{
dwData: agentCopyDataID,
cbData: uint32(((*reflect.StringHeader)(unsafe.Pointer(&mapNameWithNul))).Len),
lpData: ((*reflect.StringHeader)(unsafe.Pointer(&mapNameWithNul))).Data,
}

ret := win.SendMessage(hwnd, win.WM_COPYDATA, 0, uintptr(unsafe.Pointer(&cds)))
if ret == 0 {
err = errors.New("WM_COPYDATA failed")
return
}

len := binary.BigEndian.Uint32(sharedMemoryArray[:4])
len += 4

if len > agentMaxMessageLength {
err = errors.New("Return message too long")
return
}

result = make([]byte, len)
copy(result, sharedMemoryArray[:len])

return
}

var failureMessage = [...]byte{0, 0, 0, 1, 5}

func handleConnection(conn net.Conn) {
defer conn.Close()

reader := bufio.NewReader(conn)

for {
lenBuf := make([]byte, 4)
_, err := io.ReadFull(reader, lenBuf)
if err != nil {
if *verbose {
log.Printf("io.ReadFull error '%s'", err)
}
return
}

len := binary.BigEndian.Uint32(lenBuf)
buf := make([]byte, len)
_, err = io.ReadFull(reader, buf)
if err != nil {
if *verbose {
log.Printf("io.ReadFull error '%s'", err)
}
return
}

result, err := queryPageant(append(lenBuf, buf...))
if err != nil {
// If for some reason talking to Pageant fails we fall back to
// sending an agent error to the client
if *verbose {
log.Printf("Pageant query error '%s'", err)
}
result = failureMessage[:]
}

_, err = conn.Write(result)
if err != nil {
if *verbose {
log.Printf("net.Conn.Write error '%s'", err)
}
return
}
}
}

func listenLoop(ln net.Listener) {
defer ln.Close()

for {
conn, err := ln.Accept()
if err != nil {
log.Printf("net.Listener.Accept error '%s'", err)
return
}

if *verbose {
log.Printf("New connection: %v\n", conn)
}

go handleConnection(conn)
}
}

func main() {
flag.Parse()

var unix, pipe net.Listener
var err error

done := make(chan bool, 1)

sigs := make(chan os.Signal, 1)
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
go func() {
sig := <-sigs
switch sig {
case os.Interrupt:
log.Printf("Caught signal")
done <- true
}
}()

if *unixSocket != "" {
unix, err = net.Listen("unix", *unixSocket)

if err != nil {
log.Fatalf("Could not open socket %s, error '%s'\n", *unixSocket, err)
}

defer unix.Close()
log.Printf("Listening on Unix socket: %s\n", *unixSocket)
go func() {
listenLoop(unix)
// If for some reason our listener breaks, kill the program
done <- true
}()
}

if *namedPipe != "" {
namedPipeFullName := "\\\\.\\pipe\\" + *namedPipe
var cfg = &winio.PipeConfig{}
pipe, err = winio.ListenPipe(namedPipeFullName, cfg)

if err != nil {
log.Fatalf("Could not open named pipe %s, error '%s'\n", namedPipeFullName, err)
}

defer pipe.Close()
log.Printf("Listening on named pipe: %s\n", namedPipeFullName)
go func() {
listenLoop(pipe)
// If for some reason our listener breaks, kill the program
done <- true
}()
}

if *namedPipe == "" && *unixSocket == "" {
flag.PrintDefaults()
os.Exit(1)
}

// Wait until we are signalled as finished
<-done

log.Printf("Exiting...")
}

0 comments on commit f8eaf10

Please sign in to comment.