Skip to content
This repository has been archived by the owner on Dec 12, 2022. It is now read-only.

Commit

Permalink
Adding support for Cygwin socket
Browse files Browse the repository at this point in the history
  • Loading branch information
rupor-github committed Jan 26, 2021
1 parent b6367b7 commit a40b0f7
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 19 deletions.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ endif()

# Project version number
set(PRJ_VERSION_Major "1")
set(PRJ_VERSION_Minor "1")
set(PRJ_VERSION_Minor "2")
set(PRJ_VERSION_Patch "0")

if (EXISTS "${PROJECT_SOURCE_DIR}/.git" AND IS_DIRECTORY "${PROJECT_SOURCE_DIR}/.git")
Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

Windows 10 has `ssh-agent` service (with support for persistence and Windows security) and I have been using it [successfully](https:/github.com/rupor-github/wsl-ssh-agent) for a while. However there is another set of tools entirely - [GnuPG](https://gnupg.org/). It implements `ssh-agent` functionality (with somewhat more flexibility than original), supports smart cards, attempts to handle identity aspects of security and sometimes *must* be used (for example to sign git commits on some projects). All of that works [reasonably well](https://eklitzke.org/using-gpg-agent-effectively) on Linux.

Windows usage is a bit more problematic as we have to deal with various non-cooperating pieces: GnuPG win32 binaries are somewhat deficient, OpenSSH port integrated into Windows 10 (console, terminal and all), WSL1 and WSL2 add challenges with specific binaries and different lifetime management requirements. Ideally we need to have Windows host to handle single set of secured keys (SSH and GPG) while transparently providing necessary interfaces to all other environments. This project aims to create simple set of tools to be combined with GnuPG binaries for Windows to do exactly that.
Windows usage is a bit more problematic as we have to deal with various non-cooperating pieces: GnuPG win32 binaries are somewhat deficient, OpenSSH port integrated into Windows 10 (console, terminal and all), Cygwin/MSYS2 ssh tools and WSL1 and WSL2 add challenges with specific binaries and different lifetime management requirements. Ideally we need to have Windows host to handle single set of secured keys (SSH and GPG) while transparently providing necessary interfaces to all other environments. This project aims to create simple set of tools to be combined with GnuPG binaries for Windows to do exactly that.

**DISCLAIMER** When using term `GnuPG` I am **not referring** to [GPG4Win](https://gpg4win.org), but rather to basic GnuPG tools built from code base common for all platforms. GPG4Win includes this set (which could be extracted), but normally it is available from GnuPG ftp site [ftp://ftp.gnupg.org](ftp://ftp.gnupg.org/gcrypt/binary/). It also could be installed by using [chocolatey](https://chocolatey.org/) command `choco install gnupg`. So no wonderful KDE GUIs ported to Windows.

Expand Down Expand Up @@ -41,7 +41,10 @@ Download from the [releases page](https://github.com/rupor-github/win-gpg-agent/
Set-Service -StartupType Disabled ssh-agent
```

3. Run `agent-gui.exe`
3. If you would like to use Cygwin/MSYS2 ssh tools (as is the case by default with [Git4Windows](https://gitforwindows.org/)) you may want to consider placing `gui.openssh: cygwin` in agent-gui.conf file. **NOTE** that in any case you need to manage `SSH_AUTH_SOCK` environment variable value on Windows side. It has to point to named pipe for Windows OpenSSH to work and to Cygwin socket file for Cygwin/MSYS2 tools and __both sets are using the same variable name__.

4. Run `agent-gui.exe`


Here is a diagram to show simplified relationship between parts: ![protocol](docs/pic1.png)

Expand All @@ -68,8 +71,9 @@ Is is a simple "notification tray" applet which does `gpg-agent.exe` lifetime ma
- make sure that gpg-agent will use `pinentry.exe` from the same directory where agent-gui.exe is.
- make sure that it functions by communicating with it.
- create AF_UNIX socket counterparts for Assuan sockets from gpg-agent (except "browser" and "ssh" ones) and handle translation. I have no use for "browser" and S.gpg-agent.ssh presently does not work on Windows.
- create and service named pipe for Windows native OpenSSH. Note, that both ssh AF_UNIX socket and named pipe are using pageant protocol to talk to gpg-agent.
- set environment variable `SSH_AUTH_SOCK` on Windows side and set it with proper pipe name so native OpenSSH tools know where to go.
- create and service named pipe for Windows native OpenSSH. Note, that OpenSSH (native and Cygwin) and AF_UNIX socket and named pipe are using pageant protocol to talk to gpg-agent.
- create and service Cygwin socket for Cygwin/MSYS2 build of OpenSSH. Note, that OpenSSH (native and Cygwin) and AF_UNIX socket and named pipe are using pageant protocol to talk to gpg-agent.
- set environment variable `SSH_AUTH_SOCK` on Windows side to point either to pipe name so native OpenSSH tools know where to go or to Cygwin socket file to be used with Cygwin/MSYS2 ssh binaries.
- create `WIN_GNUPG_HOME`, `WSL_GNUPG_HOME`, `WIN_AGENT_HOME`, `WSL_AGENT_HOME` environment variables, setting them to point to directories with Assuan sockets and AF_UNIX sockets and register those environment variables with WSLENV for path translation. Basically WSL_* would be paths on the Linux side and WIN_* are Windows ones. This way every WSL environment started after will have proper "unix" and "windows" paths available for easy scripting.
- serve as a backend for [gclpr](https://github.com/rupor-github/gclpr) remote clipboard tool (NOTE: starting with v1.1.0 gclpr server backend enforces protocol versioning and may require upgrade of gclpr).

Expand All @@ -86,6 +90,7 @@ gpg:
gui:
debug: false
setenv: true
openssh: native
ignore_session_lock: false
deadline: 1m
pipe_name: \\\\.\\pipe\\openssh-ssh-agent
Expand All @@ -102,6 +107,7 @@ Full list of configuration keys:
* `gpg.gpg_agent_args` - array of additional arguments to be passed to gpg-agent on start. No checking is performed.
* `gui.debug` - turn on debug logging. Uses `OutputDebugStringW` - use Sysinternals [debugview](https://docs.microsoft.com/en-us/sysinternals/downloads/debugview) to see
* `gui.setenv` - automatically prepare environment variables
* `gui.openssh` - when value is `cygwin` set environment `SSH_AUTH_SOCK` on Windows side to point to Cygwin socket file rather then named pipe, so Cygwin and MSYS2 ssh build could be used instead of what comes with Windows 10.
* `gui.ignore_session_lock` - continue to serve requests even if user session is locked
* `gui.pipe_name` - full name of pipe for Windows OpenSSH
* `gui.homedir` - directory to be used by agent-gui to create sockets in
Expand Down
8 changes: 8 additions & 0 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func NewAgent(cfg *config.Config) (*Agent, error) {
a.conns[ConnectorSockAgentBrowser] = NewConnector(ConnectorSockAgentBrowser, a.Cfg.GPG.Home, a.Cfg.GUI.Home, util.SocketAgentBrowserName, locked, &a.wg)
a.conns[ConnectorSockAgentSSH] = NewConnector(ConnectorSockAgentSSH, a.Cfg.GPG.Home, a.Cfg.GUI.Home, util.SocketAgentSSHName, locked, &a.wg)
a.conns[ConnectorPipeSSH] = NewConnector(ConnectorPipeSSH, "", "", a.Cfg.GUI.PipeName, locked, &a.wg)
a.conns[ConnectorSockAgentCygwinSSH] = NewConnector(ConnectorSockAgentCygwinSSH, "", a.Cfg.GUI.Home, util.SocketAgentSSHCygwinName, locked, &a.wg)

util.WaitForFileDeparture(time.Second*5,
a.conns[ConnectorSockAgent].PathGPG(),
Expand Down Expand Up @@ -194,6 +195,13 @@ func (a *Agent) Start() error {
return nil
}

func (a *Agent) GetConnector(ct ConnectorType) *Connector {
if a == nil || ct > maxConnector {
return nil
}
return a.conns[ct]
}

// Serve handles serving requests for a particular ConnectorType.
func (a *Agent) Serve(ct ConnectorType) error {
if a == nil || ct > maxConnector {
Expand Down
73 changes: 67 additions & 6 deletions agent/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
ConnectorSockAgentBrowser
ConnectorSockAgentSSH
ConnectorPipeSSH
ConnectorSockAgentCygwinSSH
maxConnector
)

Expand All @@ -49,6 +50,8 @@ func (ct ConnectorType) String() string {
return "ssh-agent socket"
case ConnectorPipeSSH:
return "ssh-agent named pipe"
case ConnectorSockAgentCygwinSSH:
return "ssh-agent cygwin socket"
default:
}
return fmt.Sprintf("unknown connector type %d", ct)
Expand Down Expand Up @@ -84,11 +87,13 @@ func (c *Connector) Close() {
}
if err := c.listener.Close(); err != nil {
if !util.IsNetClosing(err) && !errors.Is(err, winio.ErrPipeListenerClosed) {
log.Printf("Error closing connector for %s: %s", c.index, err)
log.Printf("Error closing listener on connector for %s: %s", c.index, err)
}
}
if c.index != ConnectorPipeSSH && len(c.PathGUI()) != 0 {
_ = windows.Unlink(c.PathGUI())
if err := os.Remove(c.PathGUI()); err != nil {
log.Printf("Error closing connector for %s: %s", c.index, err.Error())
}
}
}

Expand Down Expand Up @@ -118,6 +123,8 @@ func (c *Connector) Serve(deadline time.Duration) error {
return c.serveSSHSocket()
case ConnectorPipeSSH:
return c.serveSSHPipe()
case ConnectorSockAgentCygwinSSH:
return c.serveSSHCygwinSocket()
default:
}
log.Printf("Connector for %s is not supported", c.index)
Expand Down Expand Up @@ -206,8 +213,7 @@ func (c *Connector) serveAssuanSocket(deadline time.Duration) error {

_, err := os.Stat(socketName)
if err == nil || !os.IsNotExist(err) {
err = windows.Unlink(socketName)
if err != nil {
if err = os.Remove(socketName); err != nil {
return fmt.Errorf("failed to unlink socket %s: %w", socketName, err)
}
}
Expand Down Expand Up @@ -284,8 +290,7 @@ func (c *Connector) serveSSHSocket() error {

_, err := os.Stat(socketName)
if err == nil || !os.IsNotExist(err) {
err = windows.Unlink(socketName)
if err != nil {
if err = os.Remove(socketName); err != nil {
return fmt.Errorf("failed to unlink socket %s: %w", socketName, err)
}
}
Expand Down Expand Up @@ -320,6 +325,62 @@ func (c *Connector) serveSSHSocket() error {
return nil
}

func (c *Connector) serveSSHCygwinSocket() error {

if c == nil {
return fmt.Errorf("gpg agent has not been initialized properly")
}
socketName := c.PathGUI()
if len(socketName) > util.MaxNameLen {
return fmt.Errorf("socket name is too long: %d, max allowed: %d", len(socketName), util.MaxNameLen)
}

_, err := os.Stat(socketName)
if err == nil || !os.IsNotExist(err) {
if err = os.Remove(socketName); err != nil {
return fmt.Errorf("failed to unlink socket %s: %w", socketName, err)
}
}

c.listener, err = net.Listen("tcp", "localhost:0")
if err != nil {
return fmt.Errorf("could not open cygwin socket: %w", err)
}

port := c.listener.Addr().(*net.TCPAddr).Port
nonce, err := util.CygwinCreateSocketFile(socketName, port)
if err != nil {
return err
}

go func() {
log.Printf("Serving %s on %s:%d with nonce: %s)", c.index, socketName, port, util.CygwinNonceString(nonce))
for {
conn, err := c.listener.Accept()
if err != nil {
if !util.IsNetClosing(err) {
log.Printf("Quiting - unable to serve on Cygwin socket: %s", err)
}
return
}
if err = util.CygwinPerformHandshake(conn, nonce); err != nil {
log.Printf("Unable to perform handshake on Cygwin socket: %s", err)
}
c.wg.Add(1)
go func() {
defer c.wg.Done()
defer conn.Close()
id := time.Now().UnixNano() // create unique id for debug tracing
log.Printf("[%d] Accepted request from %s", id, socketName)
if err := serveSSH(id, conn, c.locked); err != nil {
log.Printf("[%d] SSH handler returned error: %s", id, err.Error())
}
}()
}
}()
return nil
}

func makeInheritSaWithSid() *windows.SecurityAttributes {
var sa windows.SecurityAttributes
u, err := user.Current()
Expand Down
17 changes: 14 additions & 3 deletions cmd/agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,23 @@ func onSession(e systray.SessionEvent) {
}
}

func setVars() (func(), error) {
func setVars(native bool) (func(), error) {

vars := []struct {
initialized bool
name, value string
register, translate bool
}{
{name: envPipeName, value: gpgAgent.Cfg.GUI.PipeName, register: false, translate: false},
{name: "WSL_" + envGPGHomeName, value: gpgAgent.Cfg.GPG.Home, register: true, translate: true},
{name: "WIN_" + envGPGHomeName, value: util.PrepareWindowsPath(gpgAgent.Cfg.GPG.Home), register: true, translate: false},
{name: "WSL_" + envGUIHomeName, value: gpgAgent.Cfg.GUI.Home, register: true, translate: true},
{name: "WIN_" + envGUIHomeName, value: util.PrepareWindowsPath(gpgAgent.Cfg.GUI.Home), register: true, translate: false},
{name: envPipeName, value: gpgAgent.Cfg.GUI.PipeName, register: false, translate: false},
}

if !native {
// set variable for Cygwin OpenSSH rather then for Windows OpenSSH
vars[0].value = gpgAgent.GetConnector(agent.ConnectorSockAgentCygwinSSH).PathGUI()
}

cleaner := func() {
Expand Down Expand Up @@ -138,6 +143,12 @@ func run() error {
// socket (WSL). NOTE: WSL2 requires additional layer of translation using socat on Linux side and either HYPER-V socket server or helper on Windows end
// since AF_UNIX interop is not (yet? ever?) implemented.

// Transact on Cygwin socket for ssh Cygwin/MSYS ports
if err := gpgAgent.Serve(agent.ConnectorSockAgentCygwinSSH); err != nil {
return err
}
defer gpgAgent.Close(agent.ConnectorSockAgentCygwinSSH)

// Transact on pipe for Windows openssh
if err := gpgAgent.Serve(agent.ConnectorPipeSSH); err != nil {
return err
Expand All @@ -163,7 +174,7 @@ func run() error {
defer gpgAgent.Close(agent.ConnectorSockAgentExtra)

if gpgAgent.Cfg.GUI.SetEnv {
cleaner, err := setVars()
cleaner, err := setVars(!strings.EqualFold(gpgAgent.Cfg.GUI.SSH, "cygwin"))
if err != nil {
return err
}
Expand Down
2 changes: 2 additions & 0 deletions config/cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type GUIConfig struct {
Debug bool `yaml:"debug,omitempty"`
SetEnv bool `yaml:"setenv,omitempty"`
IgnoreSessionLock bool `yaml:"ignore_session_lock,omitempty"`
SSH string `yaml:"openssh,omitempty"`
PipeName string `yaml:"pipe_name,omitempty"`
Home string `yaml:"homedir,omitempty"`
Deadline time.Duration `yaml:"deadline,omitempty"`
Expand All @@ -49,6 +50,7 @@ var defaultGUIConfig = `
gui:
debug: false
setenv: true
openssh: windows
ignore_session_lock: false
deadline: 1m
pipe_name: %s
Expand Down
Binary file modified docs/pic1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 74 additions & 5 deletions util/names.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
package util

import (
"bytes"
"crypto/rand"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
Expand All @@ -19,11 +28,12 @@ const (
// Putty seems to have it at 8 + 1024.
MaxAgentMsgLen = 256 * 1024

GPGAgentName = "gpg-agent"
SocketAgentName = "S." + GPGAgentName
SocketAgentBrowserName = "S." + GPGAgentName + ".browser"
SocketAgentExtraName = "S." + GPGAgentName + ".extra"
SocketAgentSSHName = "S." + GPGAgentName + ".ssh"
GPGAgentName = "gpg-agent"
SocketAgentName = "S." + GPGAgentName
SocketAgentBrowserName = "S." + GPGAgentName + ".browser"
SocketAgentExtraName = "S." + GPGAgentName + ".extra"
SocketAgentSSHName = "S." + GPGAgentName + ".ssh"
SocketAgentSSHCygwinName = "S." + GPGAgentName + ".ssh.cyg"
)

// PrepareWindowsPath prepares Windows path for use on unix shell line without quoting.
Expand Down Expand Up @@ -105,3 +115,62 @@ func WaitForFileDeparture(period time.Duration, filenames ...string) {
func IsNetClosing(err error) bool {
return strings.Contains(err.Error(), "use of closed network connection")
}

// CygwinNonceString converts binary nonce to printable string in net order.
func CygwinNonceString(nonce [16]byte) string {
var buf [35]byte
dst := buf[:]
for i := 0; i < 4; i++ {
b := nonce[i*4 : i*4+4]
hex.Encode(dst[i*9:i*9+8], []byte{b[3], b[2], b[1], b[0]})
if i != 3 {
dst[9*i+8] = '-'
}
}
return string(buf[:])
}

// CygwinCreateSocketFile creates CygWin socket file with proper content and attributes.
func CygwinCreateSocketFile(fname string, port int) (nonce [16]byte, err error) {
if _, err = rand.Read(nonce[:]); err != nil {
return
}
if err = ioutil.WriteFile(fname, []byte(fmt.Sprintf("!<socket >%d s %s", port, CygwinNonceString(nonce))), 0600); err != nil {
return
}
var cpath *uint16
if cpath, err = windows.UTF16PtrFromString(fname); err != nil {
return
}
err = windows.SetFileAttributes(cpath, windows.FILE_ATTRIBUTE_SYSTEM|windows.FILE_ATTRIBUTE_READONLY)
return
}

// CygwinPerformHandshake exchanges handshake data.
func CygwinPerformHandshake(conn io.ReadWriter, nonce [16]byte) error {

var nonceR [16]byte
if _, err := conn.Read(nonceR[:]); err != nil {
return err
}
if !bytes.Equal(nonce[:], nonceR[:]) {
log.Printf("Wrong nonce received - expecting %x but got %x", nonce[:], nonceR[:])
return errors.New("invalid nonce received")
}
if _, err := conn.Write(nonce[:]); err != nil {
return err
}

// read client pid:uid:gid
buf := make([]byte, 12)
if _, err := conn.Read(buf); err != nil {
return err
}

// Send back our info, making sure that gid:uid are the same as received
binary.LittleEndian.PutUint32(buf, uint32(os.Getpid()))
if _, err := conn.Write(buf); err != nil {
return err
}
return nil
}

0 comments on commit a40b0f7

Please sign in to comment.