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

Commit 5f9515c

Browse files
committed
Adding XShell/XAgent support
1 parent 52aca48 commit 5f9515c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

102 files changed

+18908
-74
lines changed

CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ endif()
5656

5757
# Project version number
5858
set(PRJ_VERSION_Major "1")
59-
set(PRJ_VERSION_Minor "4")
60-
set(PRJ_VERSION_Patch "3")
59+
set(PRJ_VERSION_Minor "5")
60+
set(PRJ_VERSION_Patch "0")
6161

6262
if (EXISTS "${PROJECT_SOURCE_DIR}/.git" AND IS_DIRECTORY "${PROJECT_SOURCE_DIR}/.git")
6363
execute_process(COMMAND ${CMAKE_SOURCE_DIR}/cmake/githash.sh ${GIT_EXECUTABLE}

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ your gpg-agent.* This is a fundamental feature of WSL; if you are not sure of wh
2828
**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.
2929

3030
**BREAKING CHANGES:**
31-
* 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.
31+
* 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.
3232

3333
## Installation
3434

@@ -138,6 +138,7 @@ gui:
138138
openssh: native
139139
ignore_session_lock: false
140140
deadline: 1m
141+
xagent_cookie_size: 16
141142
pipe_name: "\\\\.\\pipe\\openssh-ssh-agent"
142143
homedir: "${LOCALAPPDATA}\\gnupg\\agent-gui"
143144
gclpr:
@@ -156,6 +157,7 @@ Full list of configuration keys:
156157
* `gui.setenv` - automatically prepare environment variables
157158
* `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.
158159
* `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
160+
* `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.
159161
* `gui.ignore_session_lock` - continue to serve requests even if user session is locked
160162
* `gui.pipe_name` - full name of pipe for Windows OpenSSH
161163
* `gui.homedir` - directory to be used by agent-gui to create sockets in

agent/agent.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ func NewAgent(cfg *config.Config) (*Agent, error) {
7878
// 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
7979
a.conns[ConnectorExtraPort] = NewConnector(ConnectorExtraPort, sdir, fmt.Sprintf("localhost:%d", a.Cfg.GUI.ExtraPort), util.SocketAgentExtraName, locked, &a.wg)
8080
}
81+
if a.Cfg.GUI.XAgentCookieSize > 0 {
82+
a.conns[ConnectorXShell] = NewConnector(ConnectorXShell, "", "", util.XAgentCookieString(a.Cfg.GUI.XAgentCookieSize), locked, &a.wg)
83+
}
8184

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

109115
return buf.String()
110116
}

agent/connector.go

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const (
3636
ConnectorPipeSSH
3737
ConnectorSockAgentCygwinSSH
3838
ConnectorExtraPort
39+
ConnectorXShell
3940
maxConnector
4041
)
4142

@@ -55,6 +56,8 @@ func (ct ConnectorType) String() string {
5556
return "ssh-agent cygwin socket"
5657
case ConnectorExtraPort:
5758
return "gpg-agent extra socket on local port"
59+
case ConnectorXShell:
60+
return "xagent protocol socket"
5861
default:
5962
}
6063
return fmt.Sprintf("unknown connector type %d", ct)
@@ -69,6 +72,7 @@ type Connector struct {
6972
locked *int32
7073
wg *sync.WaitGroup
7174
listener net.Listener
75+
xa io.Closer
7276
}
7377

7478
// NewConnector initializes Connector of particular ConnectorType.
@@ -93,7 +97,13 @@ func (c *Connector) Close() {
9397
log.Printf("Error closing listener on connector for %s: %s", c.index, err)
9498
}
9599
}
96-
if c.index != ConnectorPipeSSH && c.index != ConnectorExtraPort && len(c.PathGUI()) != 0 {
100+
101+
if c.index == ConnectorXShell && c.xa != nil {
102+
if err := c.xa.Close(); err != nil {
103+
log.Printf("Error closing connector for %s: %s", c.index, err)
104+
}
105+
}
106+
if c.index != ConnectorPipeSSH && c.index != ConnectorExtraPort && c.index != ConnectorXShell && len(c.PathGUI()) != 0 {
97107
if err := os.Remove(c.PathGUI()); err != nil {
98108
log.Printf("Error closing connector for %s: %s", c.index, err.Error())
99109
}
@@ -115,6 +125,16 @@ func (c *Connector) Name() string {
115125
return c.name
116126
}
117127

128+
// Port returns TCP local port of our listener or negative value.
129+
func (c *Connector) Port() int {
130+
if c.listener != nil {
131+
if a, ok := c.listener.Addr().(*net.TCPAddr); ok {
132+
return a.Port
133+
}
134+
}
135+
return -1
136+
}
137+
118138
// Serve serves requests on Connector.
119139
func (c *Connector) Serve(deadline time.Duration) error {
120140
switch c.index {
@@ -130,6 +150,8 @@ func (c *Connector) Serve(deadline time.Duration) error {
130150
return c.serveSSHCygwinSocket()
131151
case ConnectorExtraPort:
132152
return c.serveExtraPortSocket(deadline)
153+
case ConnectorXShell:
154+
return c.serveXAgentSocket()
133155
default:
134156
}
135157
log.Printf("Connector for %s is not supported", c.index)
@@ -390,7 +412,7 @@ func (c *Connector) serveSSHCygwinSocket() error {
390412
}
391413

392414
go func() {
393-
log.Printf("Serving %s on %s:%d with nonce: %s)", c.index, socketName, port, util.CygwinNonceString(nonce))
415+
log.Printf("Serving %s on %s:%d with nonce: %s", c.index, socketName, port, util.CygwinNonceString(nonce))
394416
for {
395417
conn, err := c.listener.Accept()
396418
if err != nil {
@@ -417,6 +439,53 @@ func (c *Connector) serveSSHCygwinSocket() error {
417439
return nil
418440
}
419441

442+
func (c *Connector) serveXAgentSocket() error {
443+
444+
if c == nil {
445+
return fmt.Errorf("gpg agent has not been initialized properly")
446+
}
447+
448+
var err error
449+
c.listener, err = net.Listen("tcp", "localhost:0")
450+
if err != nil {
451+
return fmt.Errorf("could not open xagent socket: %w", err)
452+
}
453+
454+
cookie := c.Name()
455+
port := c.listener.Addr().(*net.TCPAddr).Port
456+
c.xa, err = util.AdvertiseXAgent(cookie, port)
457+
if err != nil {
458+
return err
459+
}
460+
461+
go func() {
462+
log.Printf("Serving %s on :%d with cookie: %s", c.index, port, cookie)
463+
for {
464+
conn, err := c.listener.Accept()
465+
if err != nil {
466+
if !util.IsNetClosing(err) {
467+
log.Printf("Quiting - unable to serve on xagent socket: %s", err)
468+
}
469+
return
470+
}
471+
if err = util.XAgentPerformHandshake(conn, cookie); err != nil {
472+
log.Printf("Unable to perform handshake on xagent socket: %s", err)
473+
}
474+
c.wg.Add(1)
475+
go func() {
476+
defer c.wg.Done()
477+
defer conn.Close()
478+
id := time.Now().UnixNano() // create unique id for debug tracing
479+
log.Printf("[%d] Accepted request from %s", id, cookie)
480+
if err := serveSSH(id, conn, c.locked); err != nil {
481+
log.Printf("[%d] SSH handler returned error: %s", id, err.Error())
482+
}
483+
}()
484+
}
485+
}()
486+
return nil
487+
}
488+
420489
func makeInheritSaWithSid() *windows.SecurityAttributes {
421490
var sa windows.SecurityAttributes
422491
u, err := user.Current()

cmd/agent/main.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,14 @@ func run() error {
149149
// 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
150150
// since AF_UNIX interop is not (yet? ever?) implemented.
151151

152+
// Transact on local TCP socket for XAgent protocol
153+
if gpgAgent.Cfg.GUI.XAgentCookieSize > 0 {
154+
if err := gpgAgent.Serve(agent.ConnectorXShell); err != nil {
155+
return err
156+
}
157+
defer gpgAgent.Close(agent.ConnectorXShell)
158+
}
159+
152160
// Transact on Cygwin socket for ssh Cygwin/MSYS ports
153161
if err := gpgAgent.Serve(agent.ConnectorSockAgentCygwinSSH); err != nil {
154162
return err
@@ -167,7 +175,7 @@ func run() error {
167175
}
168176
defer gpgAgent.Close(agent.ConnectorSockAgentSSH)
169177

170-
// Transact on local tcp ocket for gpg agent
178+
// Transact on local tcp cocket for gpg agent
171179
if gpgAgent.Cfg.GUI.ExtraPort != 0 {
172180
if err := gpgAgent.Serve(agent.ConnectorExtraPort); err != nil {
173181
return err

config/cfg.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type GUIConfig struct {
4747
ExtraPort int `yaml:"extra_port,omitempty"`
4848
Home string `yaml:"homedir,omitempty"`
4949
Deadline time.Duration `yaml:"deadline,omitempty"`
50+
XAgentCookieSize int `yaml:"xagent_cookie_size,omitempty"`
5051
PinDlg util.DlgDetails `yaml:"pin_dialog,omitempty"`
5152
Clp CLPConfig `yaml:"gclpr,omitempty"`
5253
}
@@ -58,6 +59,7 @@ gui:
5859
openssh: windows
5960
ignore_session_lock: false
6061
deadline: 1m
62+
xagent_cookie_size: 16
6163
pipe_name: %s
6264
homedir: "${LOCALAPPDATA}\\gnupg\\%s"
6365
gclpr:
@@ -100,6 +102,13 @@ func Load(fnames ...string) (*Config, error) {
100102
return nil, err
101103
}
102104

105+
if cfg.GUI.XAgentCookieSize < 0 {
106+
cfg.GUI.XAgentCookieSize = 0
107+
}
108+
if cfg.GUI.XAgentCookieSize > 32 {
109+
cfg.GUI.XAgentCookieSize = 32
110+
}
111+
103112
if filepath.Clean(cfg.GPG.Sockets) == filepath.Clean(cfg.GUI.Home) {
104113
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))
105114
}

docs/pic2.png

64.8 KB
Loading

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/stretchr/testify v1.7.0
1313
go.uber.org/config v1.4.0
1414
go.uber.org/multierr v1.7.0
15+
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
1516
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c
1617
honnef.co/go/tools v0.2.1
1718
)
@@ -24,7 +25,6 @@ require (
2425
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
2526
github.com/stretchr/objx v0.1.0 // indirect
2627
go.uber.org/atomic v1.7.0 // indirect
27-
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
2828
golang.org/x/lint v0.0.0-20190930215403-16217165b5de // indirect
2929
golang.org/x/mod v0.3.0 // indirect
3030
golang.org/x/text v0.3.3 // indirect

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w
7474
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
7575
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c h1:taxlMj0D/1sOAuv/CbSD+MMDof2vbyPTqz5FNYKpXt8=
7676
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
77+
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM=
7778
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
7879
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
7980
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=

util/cygwin.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package util
2+
3+
import (
4+
"bytes"
5+
"crypto/rand"
6+
"encoding/binary"
7+
"encoding/hex"
8+
"fmt"
9+
"io"
10+
"io/ioutil"
11+
"os"
12+
13+
"golang.org/x/sys/windows"
14+
)
15+
16+
// CygwinNonceString converts binary nonce to printable string in net order.
17+
func CygwinNonceString(nonce [16]byte) string {
18+
var buf [35]byte
19+
dst := buf[:]
20+
for i := 0; i < 4; i++ {
21+
b := nonce[i*4 : i*4+4]
22+
hex.Encode(dst[i*9:i*9+8], []byte{b[3], b[2], b[1], b[0]})
23+
if i != 3 {
24+
dst[9*i+8] = '-'
25+
}
26+
}
27+
return string(buf[:])
28+
}
29+
30+
// CygwinCreateSocketFile creates CygWin socket file with proper content and attributes.
31+
func CygwinCreateSocketFile(fname string, port int) (nonce [16]byte, err error) {
32+
if _, err = rand.Read(nonce[:]); err != nil {
33+
return
34+
}
35+
if err = ioutil.WriteFile(fname, []byte(fmt.Sprintf("!<socket >%d s %s", port, CygwinNonceString(nonce))), 0600); err != nil {
36+
return
37+
}
38+
var cpath *uint16
39+
if cpath, err = windows.UTF16PtrFromString(fname); err != nil {
40+
return
41+
}
42+
err = windows.SetFileAttributes(cpath, windows.FILE_ATTRIBUTE_SYSTEM|windows.FILE_ATTRIBUTE_READONLY)
43+
return
44+
}
45+
46+
// CygwinPerformHandshake exchanges handshake data.
47+
func CygwinPerformHandshake(conn io.ReadWriter, nonce [16]byte) error {
48+
49+
var nonceR [16]byte
50+
if _, err := conn.Read(nonceR[:]); err != nil {
51+
return err
52+
}
53+
if !bytes.Equal(nonce[:], nonceR[:]) {
54+
return fmt.Errorf("invalid nonce received - expecting %x but got %x", nonce[:], nonceR[:])
55+
}
56+
if _, err := conn.Write(nonce[:]); err != nil {
57+
return err
58+
}
59+
60+
// read client pid:uid:gid
61+
buf := make([]byte, 12)
62+
if _, err := conn.Read(buf); err != nil {
63+
return err
64+
}
65+
66+
// Send back our info, making sure that gid:uid are the same as received
67+
binary.LittleEndian.PutUint32(buf, uint32(os.Getpid()))
68+
if _, err := conn.Write(buf); err != nil {
69+
return err
70+
}
71+
return nil
72+
}

0 commit comments

Comments
 (0)