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

Commit

Permalink
Adding XShell/XAgent support
Browse files Browse the repository at this point in the history
  • Loading branch information
rupor-github committed Jan 4, 2022
1 parent 52aca48 commit 5f9515c
Show file tree
Hide file tree
Showing 102 changed files with 18,908 additions and 74 deletions.
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ endif()

# Project version number
set(PRJ_VERSION_Major "1")
set(PRJ_VERSION_Minor "4")
set(PRJ_VERSION_Patch "3")
set(PRJ_VERSION_Minor "5")
set(PRJ_VERSION_Patch "0")

if (EXISTS "${PROJECT_SOURCE_DIR}/.git" AND IS_DIRECTORY "${PROJECT_SOURCE_DIR}/.git")
execute_process(COMMAND ${CMAKE_SOURCE_DIR}/cmake/githash.sh ${GIT_EXECUTABLE}
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ your gpg-agent.* This is a fundamental feature of WSL; if you are not sure of wh
**COMPATIBILITY NOTICE:** tools from this project were tested on Windows 10 and Windows 11 with multiple distributions and should work on anything starting with build 1809 - beginning with insider build 17063 and would not work on older versions of Windows 10, because it requires [AF_UNIX socket support](https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/) feature. I started testing everything with "official" GnuPG LTS Windows build 2.2.27.

**BREAKING CHANGES:**
* v1.4.0 changes default configuration values to support installation of 2.3+ GnuPG in non-portable mode. This requred changing default `gui.homedir` and introducing `gpg.socketdir` to avoid `gpg-agent` sockets being overwritten by `agent-gui` due to name conflict. This change may require adjusting your configuration and usage scripts.
* v1.4.0 changes default configuration values to support installation of 2.3+ GnuPG in non-portable mode. This required changing default `gui.homedir` and introducing `gpg.socketdir` to avoid `gpg-agent` sockets being overwritten by `agent-gui` due to name conflict. This change may require adjusting your configuration and usage scripts.

## Installation

Expand Down Expand Up @@ -138,6 +138,7 @@ gui:
openssh: native
ignore_session_lock: false
deadline: 1m
xagent_cookie_size: 16
pipe_name: "\\\\.\\pipe\\openssh-ssh-agent"
homedir: "${LOCALAPPDATA}\\gnupg\\agent-gui"
gclpr:
Expand All @@ -156,6 +157,7 @@ Full list of configuration keys:
* `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 by default instead of what comes with Windows.
* `gui.extra_port` - Win32-OpenSSH does not know how to redirect unix sockets yet, so if you want to use windows native ssh to remote "S.gpg-agent.extra" specify some non-zero port here. Program will open this port on localhost and you can use socat on the other side to recreate domain socket. By default it is disabled
* `gui.xagent_cookie_size` - Size of the cookie used to perform XAgent protocol handshake. If set to 0 XAgent server would not be started at all. See [XShell](https://netsarang.atlassian.net/wiki/spaces/ENSUP/pages/419957237/Using+Xagent) for details.
* `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
6 changes: 6 additions & 0 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ func NewAgent(cfg *config.Config) (*Agent, error) {
// Since OpenSSH-Win32 does not yet know how to redirect unix sockets we have no choice but to make available this additional port on local host only
a.conns[ConnectorExtraPort] = NewConnector(ConnectorExtraPort, sdir, fmt.Sprintf("localhost:%d", a.Cfg.GUI.ExtraPort), util.SocketAgentExtraName, locked, &a.wg)
}
if a.Cfg.GUI.XAgentCookieSize > 0 {
a.conns[ConnectorXShell] = NewConnector(ConnectorXShell, "", "", util.XAgentCookieString(a.Cfg.GUI.XAgentCookieSize), locked, &a.wg)
}

util.WaitForFileDeparture(time.Second*5,
a.conns[ConnectorSockAgent].PathGPG(),
Expand Down Expand Up @@ -105,6 +108,9 @@ func (a *Agent) Status() string {
}
fmt.Fprintf(&buf, "\n\n---------------------------\nagent-gui AF_UNIX and Cygwin sockets directory:\n---------------------------\n%s", a.Cfg.GUI.Home)
fmt.Fprintf(&buf, "\n\n---------------------------\nagent-gui SSH named pipe:\n---------------------------\n%s", a.Cfg.GUI.PipeName)
if a.Cfg.GUI.XAgentCookieSize > 0 {
fmt.Fprintf(&buf, "\n\n---------------------------\ngpg-agent XAgent protocol socket on TCP:\n---------------------------\nlocalhost:%d", a.conns[ConnectorXShell].Port())
}

return buf.String()
}
Expand Down
73 changes: 71 additions & 2 deletions agent/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const (
ConnectorPipeSSH
ConnectorSockAgentCygwinSSH
ConnectorExtraPort
ConnectorXShell
maxConnector
)

Expand All @@ -55,6 +56,8 @@ func (ct ConnectorType) String() string {
return "ssh-agent cygwin socket"
case ConnectorExtraPort:
return "gpg-agent extra socket on local port"
case ConnectorXShell:
return "xagent protocol socket"
default:
}
return fmt.Sprintf("unknown connector type %d", ct)
Expand All @@ -69,6 +72,7 @@ type Connector struct {
locked *int32
wg *sync.WaitGroup
listener net.Listener
xa io.Closer
}

// NewConnector initializes Connector of particular ConnectorType.
Expand All @@ -93,7 +97,13 @@ func (c *Connector) Close() {
log.Printf("Error closing listener on connector for %s: %s", c.index, err)
}
}
if c.index != ConnectorPipeSSH && c.index != ConnectorExtraPort && len(c.PathGUI()) != 0 {

if c.index == ConnectorXShell && c.xa != nil {
if err := c.xa.Close(); err != nil {
log.Printf("Error closing connector for %s: %s", c.index, err)
}
}
if c.index != ConnectorPipeSSH && c.index != ConnectorExtraPort && c.index != ConnectorXShell && len(c.PathGUI()) != 0 {
if err := os.Remove(c.PathGUI()); err != nil {
log.Printf("Error closing connector for %s: %s", c.index, err.Error())
}
Expand All @@ -115,6 +125,16 @@ func (c *Connector) Name() string {
return c.name
}

// Port returns TCP local port of our listener or negative value.
func (c *Connector) Port() int {
if c.listener != nil {
if a, ok := c.listener.Addr().(*net.TCPAddr); ok {
return a.Port
}
}
return -1
}

// Serve serves requests on Connector.
func (c *Connector) Serve(deadline time.Duration) error {
switch c.index {
Expand All @@ -130,6 +150,8 @@ func (c *Connector) Serve(deadline time.Duration) error {
return c.serveSSHCygwinSocket()
case ConnectorExtraPort:
return c.serveExtraPortSocket(deadline)
case ConnectorXShell:
return c.serveXAgentSocket()
default:
}
log.Printf("Connector for %s is not supported", c.index)
Expand Down Expand Up @@ -390,7 +412,7 @@ func (c *Connector) serveSSHCygwinSocket() error {
}

go func() {
log.Printf("Serving %s on %s:%d with nonce: %s)", c.index, socketName, port, util.CygwinNonceString(nonce))
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 {
Expand All @@ -417,6 +439,53 @@ func (c *Connector) serveSSHCygwinSocket() error {
return nil
}

func (c *Connector) serveXAgentSocket() error {

if c == nil {
return fmt.Errorf("gpg agent has not been initialized properly")
}

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

cookie := c.Name()
port := c.listener.Addr().(*net.TCPAddr).Port
c.xa, err = util.AdvertiseXAgent(cookie, port)
if err != nil {
return err
}

go func() {
log.Printf("Serving %s on :%d with cookie: %s", c.index, port, cookie)
for {
conn, err := c.listener.Accept()
if err != nil {
if !util.IsNetClosing(err) {
log.Printf("Quiting - unable to serve on xagent socket: %s", err)
}
return
}
if err = util.XAgentPerformHandshake(conn, cookie); err != nil {
log.Printf("Unable to perform handshake on xagent 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, cookie)
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
10 changes: 9 additions & 1 deletion cmd/agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ 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 local TCP socket for XAgent protocol
if gpgAgent.Cfg.GUI.XAgentCookieSize > 0 {
if err := gpgAgent.Serve(agent.ConnectorXShell); err != nil {
return err
}
defer gpgAgent.Close(agent.ConnectorXShell)
}

// Transact on Cygwin socket for ssh Cygwin/MSYS ports
if err := gpgAgent.Serve(agent.ConnectorSockAgentCygwinSSH); err != nil {
return err
Expand All @@ -167,7 +175,7 @@ func run() error {
}
defer gpgAgent.Close(agent.ConnectorSockAgentSSH)

// Transact on local tcp ocket for gpg agent
// Transact on local tcp cocket for gpg agent
if gpgAgent.Cfg.GUI.ExtraPort != 0 {
if err := gpgAgent.Serve(agent.ConnectorExtraPort); err != nil {
return err
Expand Down
9 changes: 9 additions & 0 deletions config/cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type GUIConfig struct {
ExtraPort int `yaml:"extra_port,omitempty"`
Home string `yaml:"homedir,omitempty"`
Deadline time.Duration `yaml:"deadline,omitempty"`
XAgentCookieSize int `yaml:"xagent_cookie_size,omitempty"`
PinDlg util.DlgDetails `yaml:"pin_dialog,omitempty"`
Clp CLPConfig `yaml:"gclpr,omitempty"`
}
Expand All @@ -58,6 +59,7 @@ gui:
openssh: windows
ignore_session_lock: false
deadline: 1m
xagent_cookie_size: 16
pipe_name: %s
homedir: "${LOCALAPPDATA}\\gnupg\\%s"
gclpr:
Expand Down Expand Up @@ -100,6 +102,13 @@ func Load(fnames ...string) (*Config, error) {
return nil, err
}

if cfg.GUI.XAgentCookieSize < 0 {
cfg.GUI.XAgentCookieSize = 0
}
if cfg.GUI.XAgentCookieSize > 32 {
cfg.GUI.XAgentCookieSize = 32
}

if filepath.Clean(cfg.GPG.Sockets) == filepath.Clean(cfg.GUI.Home) {
return nil, fmt.Errorf("potential conflict as gpg.socketdir=[%s] and gui.homedir=[%s] are pointing to the same location", filepath.Clean(cfg.GPG.Sockets), filepath.Clean(cfg.GUI.Home))
}
Expand Down
Binary file modified docs/pic2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/stretchr/testify v1.7.0
go.uber.org/config v1.4.0
go.uber.org/multierr v1.7.0
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c
honnef.co/go/tools v0.2.1
)
Expand All @@ -24,7 +25,6 @@ require (
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/stretchr/objx v0.1.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
golang.org/x/lint v0.0.0-20190930215403-16217165b5de // indirect
golang.org/x/mod v0.3.0 // indirect
golang.org/x/text v0.3.3 // indirect
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c h1:taxlMj0D/1sOAuv/CbSD+MMDof2vbyPTqz5FNYKpXt8=
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
Expand Down
72 changes: 72 additions & 0 deletions util/cygwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package util

import (
"bytes"
"crypto/rand"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"os"

"golang.org/x/sys/windows"
)

// 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[:]) {
return fmt.Errorf("invalid nonce received - expecting %x but got %x", nonce[:], nonceR[:])
}
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
}
Loading

0 comments on commit 5f9515c

Please sign in to comment.