Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d85dc66

Browse files
committed
feat: working GPG agent forwarding test
1 parent 9ebf840 commit d85dc66

File tree

6 files changed

+54
-31
lines changed

6 files changed

+54
-31
lines changed

agent/agent.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ func (a *agent) init(ctx context.Context) {
483483

484484
sshLogger := a.logger.Named("ssh-server")
485485
forwardHandler := &ssh.ForwardedTCPHandler{}
486-
unixForwardHandler := &forwardedUnixHandler{}
486+
unixForwardHandler := &forwardedUnixHandler{log: a.logger}
487487

488488
a.sshServer = &ssh.Server{
489489
ChannelHandlers: map[string]ssh.ChannelHandler{
@@ -531,8 +531,8 @@ func (a *agent) init(ctx context.Context) {
531531
RequestHandlers: map[string]ssh.RequestHandler{
532532
"tcpip-forward": forwardHandler.HandleSSHRequest,
533533
"cancel-tcpip-forward": forwardHandler.HandleSSHRequest,
534-
"[email protected]": unixForwardHandler.HandleSSHRequest, // TODO: this
535-
"[email protected]": unixForwardHandler.HandleSSHRequest, // TODO: this
534+
"[email protected]": unixForwardHandler.HandleSSHRequest,
535+
"[email protected]": unixForwardHandler.HandleSSHRequest,
536536
},
537537
ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
538538
return &gossh.ServerConfig{

agent/agent_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -495,16 +495,16 @@ func TestAgent_UnixRemoteForwarding(t *testing.T) {
495495
}
496496
}()
497497

498-
cmd := setupSSHCommand(t, []string{"-R", fmt.Sprintf("%s:%s", remoteSocketPath, localSocketPath)}, []string{"echo", "test"})
498+
cmd := setupSSHCommand(t, []string{"-R", fmt.Sprintf("%s:%s", remoteSocketPath, localSocketPath)}, []string{"sleep", "10"})
499499
err = cmd.Start()
500500
require.NoError(t, err)
501501

502502
require.Eventually(t, func() bool {
503-
_, err := os.Stat(localSocketPath)
503+
_, err := os.Stat(remoteSocketPath)
504504
return err == nil
505505
}, testutil.WaitLong, testutil.IntervalFast)
506506

507-
conn, err := net.Dial("unix", localSocketPath)
507+
conn, err := net.Dial("unix", remoteSocketPath)
508508
require.NoError(t, err)
509509
_, err = conn.Write([]byte("test"))
510510
require.NoError(t, err)

agent/ssh.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/gliderlabs/ssh"
1111
gossh "golang.org/x/crypto/ssh"
12+
"golang.org/x/xerrors"
1213

1314
"cdr.dev/slog"
1415
)
@@ -23,6 +24,7 @@ type streamLocalForwardPayload struct {
2324
// channel request when a Unix connection is accepted by the listener.
2425
type forwardedStreamLocalPayload struct {
2526
SocketPath string
27+
Reserved uint32
2628
}
2729

2830
// forwardedUnixHandler is a clone of ssh.ForwardedTCPHandler that does
@@ -101,18 +103,20 @@ func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server,
101103
for {
102104
c, err := ln.Accept()
103105
if err != nil {
104-
h.log.Warn(ctx, "accept on local Unix socket for SSH unix forward request",
105-
slog.F("socket_path", addr),
106-
slog.Error(err),
107-
)
106+
if !xerrors.Is(err, net.ErrClosed) {
107+
h.log.Warn(ctx, "accept on local Unix socket for SSH unix forward request",
108+
slog.F("socket_path", addr),
109+
slog.Error(err),
110+
)
111+
}
108112
break
109113
}
110114
payload := gossh.Marshal(&forwardedStreamLocalPayload{
111115
SocketPath: addr,
112116
})
113117

114118
go func() {
115-
ch, reqs, err := conn.OpenChannel("forwardedTCPChannelType", payload)
119+
ch, reqs, err := conn.OpenChannel("[email protected]", payload)
116120
if err != nil {
117121
h.log.Warn(ctx, "open SSH channel to forward Unix connection to client",
118122
slog.F("socket_path", addr),

cli/ssh.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ func ssh() *cobra.Command {
152152
}
153153

154154
if forwardGPG {
155+
if workspaceAgent.OperatingSystem == "windows" {
156+
return xerrors.New("GPG forwarding is not supported for Windows workspaces")
157+
}
158+
155159
err = uploadGPGKeys(ctx, sshClient)
156160
if err != nil {
157161
return xerrors.Errorf("upload GPG public keys and ownertrust to workspace: %w", err)
@@ -449,6 +453,18 @@ func runRemoteSSH(sshClient *gossh.Client, stdin io.Reader, cmd string) ([]byte,
449453
}
450454

451455
func uploadGPGKeys(ctx context.Context, sshClient *gossh.Client) error {
456+
// Check if the agent is running in the workspace already.
457+
// Note: we don't support windows in the workspace for GPG forwarding so
458+
// using shell commands is fine.
459+
agentSocketBytes, err := runRemoteSSH(sshClient, nil, "set -eux; agent_socket=$(gpgconf --list-dir agent-socket); echo $agent_socket; test ! -S $agent_socket")
460+
agentSocket := strings.TrimSpace(string(agentSocketBytes))
461+
if err != nil {
462+
return xerrors.Errorf("check if agent socket is running (check if %q exists): %w", agentSocket, err)
463+
}
464+
if agentSocket == "" {
465+
return xerrors.Errorf("agent socket path is empty, check the output of `gpgconf --list-dir agent-socket`")
466+
}
467+
452468
// Read the user's public keys and ownertrust from GPG.
453469
pubKeyExport, err := runLocal(ctx, nil, "gpg", "--armor", "--export")
454470
if err != nil {
@@ -469,6 +485,13 @@ func uploadGPGKeys(ctx context.Context, sshClient *gossh.Client) error {
469485
return xerrors.Errorf("import ownertrust into workspace: %w", err)
470486
}
471487

488+
// Kill the agent in the workspace if it was started by one of the above
489+
// commands.
490+
_, err = runRemoteSSH(sshClient, nil, fmt.Sprintf("gpgconf --kill gpg-agent && rm -f %q", agentSocket))
491+
if err != nil {
492+
return xerrors.Errorf("kill existing agent in workspace: %w", err)
493+
}
494+
472495
return nil
473496
}
474497

@@ -497,11 +520,11 @@ type cookieAddr struct {
497520
cookie []byte
498521
}
499522

500-
// sshForward starts forwarding connections from a remote listener to a local
501-
// address via SSH in a goroutine.
523+
// sshForwardRemote starts forwarding connections from a remote listener to a
524+
// local address via SSH in a goroutine.
502525
//
503526
// Accepts a `cookieAddr` as the local address.
504-
func sshForward(ctx context.Context, stderr io.Writer, sshClient *gossh.Client, localAddr, remoteAddr net.Addr) (io.Closer, error) {
527+
func sshForwardRemote(ctx context.Context, stderr io.Writer, sshClient *gossh.Client, localAddr, remoteAddr net.Addr) (io.Closer, error) {
505528
listener, err := sshClient.Listen(remoteAddr.Network(), remoteAddr.String())
506529
if err != nil {
507530
return nil, xerrors.Errorf("listen on remote SSH address %s: %w", remoteAddr.String(), err)

cli/ssh_other.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,5 @@ func forwardGPGAgent(ctx context.Context, stderr io.Writer, sshClient *gossh.Cli
4444
Net: "unix",
4545
}
4646

47-
return sshForward(ctx, stderr, sshClient, localAddr, remoteAddr)
47+
return sshForwardRemote(ctx, stderr, sshClient, localAddr, remoteAddr)
4848
}

cli/ssh_test.go

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,6 @@ Expire-Date: 0
416416
require.NoError(t, err, "import ownertrust failed: %s", out)
417417

418418
// Start the GPG agent.
419-
// gpg-connect-agent reloadagent /bye
420419
agentCmd := exec.CommandContext(ctx, gpgAgentPath, "--no-detach", "--extra-socket", extraSocketPath)
421420
agentCmd.Env = append(agentCmd.Env, "GNUPGHOME="+gnupgHomeClient)
422421
agentPTY, agentProc, err := pty.Start(agentCmd, pty.WithPTYOption(pty.WithGPGTTY()))
@@ -426,24 +425,19 @@ Expire-Date: 0
426425
_ = agentPTY.Close()
427426
}()
428427

429-
// Initialize the GPG database in the "workspace".
428+
// Get the agent socket path in the "workspace".
430429
gnupgHomeWorkspace := t.TempDir()
431-
c = exec.CommandContext(ctx, gpgPath, "--list-keys")
432-
c.Env = append(c.Env, "GNUPGHOME="+gnupgHomeWorkspace)
433-
out, err = c.CombinedOutput()
434-
require.NoError(t, err, "list keys in workspace failed: %s", out)
435430

436-
// Get the agent extra socket path in the "workspace".
437431
stdout = bytes.NewBuffer(nil)
438432
stderr = bytes.NewBuffer(nil)
439-
c = exec.CommandContext(ctx, gpgConfPath, "--list-dir", "agent-extra-socket")
433+
c = exec.CommandContext(ctx, gpgConfPath, "--list-dir", "agent-socket")
440434
c.Env = append(c.Env, "GNUPGHOME="+gnupgHomeWorkspace)
441435
c.Stdout = stdout
442436
c.Stderr = stderr
443437
err = c.Run()
444-
require.NoError(t, err, "get extra socket path in workspace failed: %s", stderr.String())
445-
workspaceExtraSocketPath := strings.TrimSpace(stdout.String())
446-
require.NotEqual(t, extraSocketPath, workspaceExtraSocketPath, "extra socket path should be different")
438+
require.NoError(t, err, "get agent socket path in workspace failed: %s", stderr.String())
439+
workspaceAgentSocketPath := strings.TrimSpace(stdout.String())
440+
require.NotEqual(t, extraSocketPath, workspaceAgentSocketPath, "socket path should be different")
447441

448442
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
449443

@@ -484,20 +478,22 @@ Expire-Date: 0
484478
pty.ExpectMatch("env--command-done")
485479

486480
// Get the agent extra socket path in the "workspace" via shell.
487-
pty.WriteLine("gpgconf --list-dir agent-extra-socket && echo gpgconf-''-extrasocket-command-done")
488-
pty.ExpectMatch(workspaceExtraSocketPath)
489-
pty.ExpectMatch("gpgconf--extrasocket-command-done")
481+
pty.WriteLine("gpgconf --list-dir agent-socket && echo gpgconf-''-agentsocket-command-done")
482+
pty.ExpectMatch(workspaceAgentSocketPath)
483+
pty.ExpectMatch("gpgconf--agentsocket-command-done")
490484

491485
// List the keys in the "workspace".
492486
pty.WriteLine("gpg --list-keys && echo gpg-''-listkeys-command-done")
493487
listKeysOutput := pty.ExpectMatch("gpg--listkeys-command-done")
494488
require.Contains(t, listKeysOutput, "[ultimate] Coder Test <[email protected]>")
495489
require.Contains(t, listKeysOutput, "[ultimate] Dean Sheather (work key) <[email protected]>")
496490

497-
// Try to sign something.
491+
// Try to sign something. This demonstrates that the forwarding is
492+
// working as expected, since the workspace doesn't have access to the
493+
// private key directly and must use the forwarded agent.
498494
pty.WriteLine("echo 'hello world' | gpg --clearsign && echo gpg-''-sign-command-done")
499495
pty.ExpectMatch("BEGIN PGP SIGNED MESSAGE")
500-
pty.ExpectMatch("Hash: SHA256")
496+
pty.ExpectMatch("Hash:")
501497
pty.ExpectMatch("hello world")
502498
pty.ExpectMatch("gpg--sign-command-done")
503499

0 commit comments

Comments
 (0)