Skip to content

Commit 2738ef0

Browse files
authored
fix(agent/git_ssh_signature): remove agent-forwarding and start-services flags (#662)
1 parent 9c397fe commit 2738ef0

5 files changed

Lines changed: 238 additions & 114 deletions

File tree

cmd/agent/git_ssh_signature.go

Lines changed: 106 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,17 @@
11
package agent
22

33
import (
4-
"errors"
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"strings"
58

69
"github.com/skevetter/devpod/cmd/flags"
710
"github.com/skevetter/devpod/pkg/gitsshsigning"
811
"github.com/skevetter/log"
912
"github.com/spf13/cobra"
1013
)
1114

12-
type GitSSHSignatureCmd struct {
13-
*flags.GlobalFlags
14-
15-
CertPath string
16-
Namespace string
17-
BufferFile string
18-
Command string
19-
}
20-
2115
// NewGitSSHSignatureCmd creates new git-ssh-signature command
2216
// This agent command can be used as git ssh program by setting
2317
//
@@ -28,41 +22,119 @@ type GitSSHSignatureCmd struct {
2822
//
2923
// custom-ssh-signature-handler -Y sign -n git -f /Users/johndoe/.ssh/my-key.pub /tmp/.git_signing_buffer_tmp4Euk6d
3024
func NewGitSSHSignatureCmd(flags *flags.GlobalFlags) *cobra.Command {
31-
cmd := &GitSSHSignatureCmd{
32-
GlobalFlags: flags,
33-
}
34-
35-
gitSSHSignatureCmd := &cobra.Command{
25+
return &cobra.Command{
3626
Use: "git-ssh-signature",
37-
// Allow unknown flags so that git can pass any ssh-keygen flags
38-
// (e.g. -U for stdin input) without cobra rejecting them.
39-
FParseErrWhitelist: cobra.FParseErrWhitelist{
40-
UnknownFlags: true,
41-
},
27+
// This command implements the ssh-keygen protocol used by git for commit
28+
// signing. Disable cobra's flag parsing so we can handle the ssh-keygen
29+
// argument format directly, including boolean flags like -U (ssh-agent
30+
// mode) that cobra cannot distinguish from flags that take a value.
31+
DisableFlagParsing: true,
4232
RunE: func(_ *cobra.Command, args []string) error {
4333
logger := log.GetInstance()
4434

45-
if len(args) < 1 {
46-
logger.Fatalf("Buffer file is required")
47-
}
35+
parsed := parseSSHKeygenArgs(args)
4836

49-
// Check if the required -Y sign flags are present
50-
if cmd.Command != "sign" {
51-
return errors.New("must include '-Y sign' arguments")
37+
// For non-sign operations (verify, find-principals, check-novalidate),
38+
// delegate command to system ssh-keygen since op does not require the tunnel.
39+
if parsed.command != "sign" {
40+
return delegateToSSHKeygen(args, logger)
5241
}
5342

54-
// The last argument is the buffer file
55-
cmd.BufferFile = args[len(args)-1]
43+
if parsed.certPath == "" {
44+
return fmt.Errorf("key file (-f) is required")
45+
}
46+
if parsed.namespace == "" {
47+
return fmt.Errorf("namespace (-n) is required")
48+
}
49+
if parsed.bufferFile == "" {
50+
return fmt.Errorf("buffer file is required")
51+
}
5652

5753
return gitsshsigning.HandleGitSSHProgramCall(
58-
cmd.CertPath, cmd.Namespace, cmd.BufferFile, logger)
54+
parsed.certPath, parsed.namespace, parsed.bufferFile, logger)
5955
},
6056
}
57+
}
58+
59+
// sshKeygenArgs holds the parsed result of a git ssh-keygen invocation.
60+
type sshKeygenArgs struct {
61+
command string // -Y value (sign, verify, find-principals, etc.)
62+
certPath string // -f value (path to public key)
63+
namespace string // -n value (always "git" for commit signing)
64+
bufferFile string // last non-flag argument
65+
}
66+
67+
// parseSSHKeygenArgs parses the ssh-keygen argument format used by git:
68+
//
69+
// -Y <command> -n <namespace> -f <key> [flags...] <buffer-file>
70+
//
71+
// The buffer file is always the last argument and is never a flag.
72+
// Unknown flags (e.g. -U for ssh-agent mode) are ignored.
73+
func parseSSHKeygenArgs(args []string) sshKeygenArgs {
74+
result := sshKeygenArgs{
75+
command: "sign",
76+
} // git only ever calls with sign, but default defensively
77+
consumed := make(map[int]bool)
78+
for i := 0; i < len(args); i++ {
79+
if next := consumeFlag(&result, args, i); next > i {
80+
consumed[i] = true
81+
consumed[next] = true
82+
i = next
83+
}
84+
}
85+
// The buffer file is always the last argument and is never a flag.
86+
lastIdx := len(args) - 1
87+
if lastIdx >= 0 && !consumed[lastIdx] && !strings.HasPrefix(args[lastIdx], "-") {
88+
result.bufferFile = args[lastIdx]
89+
}
90+
return result
91+
}
92+
93+
// consumeFlag processes a single known flag from args at position i.
94+
// Returns the index of the consumed value if a known flag-value pair is found,
95+
// or i if no value was consumed.
96+
func consumeFlag(result *sshKeygenArgs, args []string, i int) int {
97+
if i+1 >= len(args) {
98+
return i
99+
}
100+
next := args[i+1]
101+
102+
switch args[i] {
103+
case "-Y":
104+
if strings.HasPrefix(next, "-") {
105+
return i
106+
}
107+
result.command = next
108+
return i + 1
109+
case "-f":
110+
if strings.HasPrefix(next, "-") {
111+
return i
112+
}
113+
result.certPath = next
114+
return i + 1
115+
case "-n":
116+
if strings.HasPrefix(next, "-") {
117+
return i
118+
}
119+
result.namespace = next
120+
return i + 1
121+
}
122+
return i
123+
}
124+
125+
// delegateToSSHKeygen forwards args to the system ssh-keygen binary.
126+
func delegateToSSHKeygen(args []string, logger log.Logger) error {
127+
sshKeygen, err := exec.LookPath("ssh-keygen")
128+
if err != nil {
129+
return fmt.Errorf("find ssh-keygen: %w", err)
130+
}
131+
132+
logger.Debugf("delegating to ssh-keygen: %s %v", sshKeygen, args)
61133

62-
gitSSHSignatureCmd.Flags().StringVarP(&cmd.CertPath, "file", "f", "", "Path to the private key")
63-
gitSSHSignatureCmd.Flags().StringVarP(&cmd.Namespace, "namespace", "n", "", "Namespace")
64-
gitSSHSignatureCmd.Flags().
65-
StringVarP(&cmd.Command, "command", "Y", "sign", "Command - should be 'sign'")
134+
c := exec.Command(sshKeygen, args...) // #nosec G204,G304,G702
135+
c.Stdin = os.Stdin
136+
c.Stdout = os.Stdout
137+
c.Stderr = os.Stderr
66138

67-
return gitSSHSignatureCmd
139+
return c.Run()
68140
}

cmd/agent/git_ssh_signature_helper.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ type GitSSHSignatureHelperCmd struct {
2929
// The signing key path is a required argument for this command. It should be what equal to what you
3030
// would have set as user.signingkey git config.
3131
func NewGitSSHSignatureHelperCmd(flags *flags.GlobalFlags) *cobra.Command {
32-
cmd := &GitSSHSignatureCmd{
32+
cmd := &GitSSHSignatureHelperCmd{
3333
GlobalFlags: flags,
3434
}
3535

cmd/agent/git_ssh_signature_test.go

Lines changed: 33 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package agent
33
import (
44
"testing"
55

6-
"github.com/skevetter/devpod/cmd/flags"
76
"github.com/stretchr/testify/assert"
87
"github.com/stretchr/testify/suite"
98
)
@@ -16,61 +15,43 @@ func TestGitSSHSignatureSuite(t *testing.T) {
1615
suite.Run(t, new(GitSSHSignatureTestSuite))
1716
}
1817

19-
func (s *GitSSHSignatureTestSuite) TestAcceptsUnknownFlags() {
20-
cmd := NewGitSSHSignatureCmd(&flags.GlobalFlags{})
21-
22-
// Git passes: -Y sign -n git -f /path/to/key -U /dev/stdin /tmp/buffer
23-
// -U is an unknown flag that consumes /dev/stdin as its value.
24-
// /tmp/buffer remains as a positional argument.
25-
err := cmd.ParseFlags(
26-
[]string{
27-
"-Y",
28-
"sign",
29-
"-n",
30-
"git",
31-
"-f",
32-
"/path/to/key",
33-
"-U",
34-
"/dev/stdin",
35-
"/tmp/buffer",
36-
},
37-
)
38-
assert.NoError(s.T(), err, "flag parsing should succeed with unknown flag -U")
39-
40-
args := cmd.Flags().Args()
41-
s.Require().NotEmpty(args, "should have positional args")
42-
assert.Equal(s.T(), "/tmp/buffer", args[len(args)-1],
43-
"buffer file should be preserved as last positional arg")
18+
func (s *GitSSHSignatureTestSuite) TestParseBasicSignArgs() {
19+
args := []string{"-Y", "sign", "-n", "git", "-f", "/path/to/key.pub", "/tmp/buffer"}
20+
result := parseSSHKeygenArgs(args)
21+
assert.Equal(s.T(), "sign", result.command)
22+
assert.Equal(s.T(), "git", result.namespace)
23+
assert.Equal(s.T(), "/path/to/key.pub", result.certPath)
24+
assert.Equal(s.T(), "/tmp/buffer", result.bufferFile)
4425
}
4526

46-
func (s *GitSSHSignatureTestSuite) TestBufferFileAsPositionalArg() {
47-
cmd := NewGitSSHSignatureCmd(&flags.GlobalFlags{})
48-
49-
err := cmd.ParseFlags(
50-
[]string{"-Y", "sign", "-n", "git", "-f", "/path/to/key", "/tmp/buffer"},
51-
)
52-
assert.NoError(s.T(), err)
53-
54-
args := cmd.Flags().Args()
55-
s.Require().NotEmpty(args, "should have positional args")
56-
assert.Equal(s.T(), "/tmp/buffer", args[len(args)-1],
57-
"last positional arg should be the buffer file")
27+
func (s *GitSSHSignatureTestSuite) TestParseWithAgentFlag() {
28+
// When the signing key is loaded in the ssh-agent, git passes -U (a boolean
29+
// "use agent" flag) immediately before the buffer file. The buffer file must
30+
// still be recognised as the last non-flag argument.
31+
args := []string{"-Y", "sign", "-n", "git", "-f", "/path/to/key.pub", "-U", "/tmp/buffer"}
32+
result := parseSSHKeygenArgs(args)
33+
assert.Equal(s.T(), "sign", result.command)
34+
assert.Equal(s.T(), "/path/to/key.pub", result.certPath)
35+
assert.Equal(s.T(), "/tmp/buffer", result.bufferFile)
5836
}
5937

60-
func (s *GitSSHSignatureTestSuite) TestKnownFlagsParsed() {
61-
cmd := NewGitSSHSignatureCmd(&flags.GlobalFlags{})
62-
63-
err := cmd.ParseFlags(
64-
[]string{"-Y", "sign", "-n", "git", "-f", "/path/to/key", "/tmp/buffer"},
65-
)
66-
assert.NoError(s.T(), err)
38+
func (s *GitSSHSignatureTestSuite) TestParseNonSignCommand() {
39+
args := []string{"-Y", "verify", "-n", "git", "-f", "/path/to/key.pub", "/tmp/buffer"}
40+
result := parseSSHKeygenArgs(args)
41+
assert.Equal(s.T(), "verify", result.command)
42+
}
6743

68-
val, err := cmd.Flags().GetString("command")
69-
assert.NoError(s.T(), err)
70-
assert.Equal(s.T(), "sign", val, "command flag should be 'sign'")
44+
func (s *GitSSHSignatureTestSuite) TestParseMissingBufferFile() {
45+
// All args end in a flag — no buffer file present.
46+
args := []string{"-Y", "sign", "-n", "git", "-f", "/path/to/key.pub", "-U"}
47+
result := parseSSHKeygenArgs(args)
48+
assert.Equal(s.T(), "", result.bufferFile)
49+
}
7150

72-
args := cmd.Flags().Args()
73-
s.Require().NotEmpty(args, "should have positional args")
74-
assert.Equal(s.T(), "/tmp/buffer", args[len(args)-1],
75-
"last positional arg should be the buffer file")
51+
func (s *GitSSHSignatureTestSuite) TestParseDefaultsToSign() {
52+
// If -Y is absent the command defaults to "sign".
53+
args := []string{"-n", "git", "-f", "/path/to/key.pub", "/tmp/buffer"}
54+
result := parseSSHKeygenArgs(args)
55+
assert.Equal(s.T(), "sign", result.command)
56+
assert.Equal(s.T(), "/tmp/buffer", result.bufferFile)
7657
}

cmd/up.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1572,8 +1572,6 @@ func setupGitSSHSignature(
15721572
out, err := exec.Command(
15731573
execPath,
15741574
"ssh",
1575-
"--agent-forwarding=true",
1576-
"--start-services=true",
15771575
"--user",
15781576
remoteUser,
15791577
"--context",

0 commit comments

Comments
 (0)