Skip to content

Commit 0b879fe

Browse files
committed
fix: use DisableFlagParsing to correctly handle ssh-keygen argument format
Replace the cobra flag-parsing approach with DisableFlagParsing:true and a dedicated parseSSHKeygenArgs parser. This command implements the ssh-keygen protocol used by git, not a standard CLI; cobra's flag parser cannot handle boolean flags like -U (ssh-agent mode) that git passes before the buffer file because pflag consumes the following argument as the flag's value, leaving positional args empty. With DisableFlagParsing, RunE receives the raw args and parseSSHKeygenArgs parses -Y/-f/-n by position, treating the last non-flag argument as the buffer file regardless of what flags precede it. delegateToSSHKeygen now takes args directly instead of parsing os.Args.
1 parent 749307e commit 0b879fe

File tree

2 files changed

+95
-96
lines changed

2 files changed

+95
-96
lines changed

cmd/agent/git_ssh_signature.go

Lines changed: 62 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -31,73 +31,91 @@ type GitSSHSignatureCmd struct {
3131
//
3232
// custom-ssh-signature-handler -Y sign -n git -f /Users/johndoe/.ssh/my-key.pub /tmp/.git_signing_buffer_tmp4Euk6d
3333
func NewGitSSHSignatureCmd(flags *flags.GlobalFlags) *cobra.Command {
34-
cmd := &GitSSHSignatureCmd{
35-
GlobalFlags: flags,
36-
}
37-
38-
gitSSHSignatureCmd := &cobra.Command{
34+
return &cobra.Command{
3935
Use: "git-ssh-signature",
40-
// Allow unknown flags so that git can pass any ssh-keygen flags
41-
// (e.g. -U for stdin input) without cobra rejecting them.
42-
FParseErrWhitelist: cobra.FParseErrWhitelist{
43-
UnknownFlags: true,
44-
},
36+
// This command implements the ssh-keygen protocol used by git for commit
37+
// signing. Disable cobra's flag parsing so we can handle the ssh-keygen
38+
// argument format directly, including boolean flags like -U (ssh-agent
39+
// mode) that cobra cannot distinguish from flags that take a value.
40+
DisableFlagParsing: true,
4541
RunE: func(_ *cobra.Command, args []string) error {
4642
logger := log.GetInstance()
4743

44+
parsed := parseSSHKeygenArgs(args)
45+
4846
// For non-sign operations (verify, find-principals, check-novalidate),
4947
// delegate command to system ssh-keygen since op does not require the tunnel.
50-
if cmd.Command != "sign" {
51-
return delegateToSSHKeygen(logger)
48+
if parsed.command != "sign" {
49+
return delegateToSSHKeygen(args, logger)
5250
}
5351

54-
// Sign operation requires a buffer file
55-
if len(args) < 1 {
56-
return fmt.Errorf(
57-
"buffer file is required (received %d positional args: %v)",
58-
len(args), args,
59-
)
52+
if parsed.bufferFile == "" {
53+
return fmt.Errorf("buffer file is required")
6054
}
6155

62-
// The last argument is the buffer file
63-
cmd.BufferFile = args[len(args)-1]
64-
6556
return gitsshsigning.HandleGitSSHProgramCall(
66-
cmd.CertPath, cmd.Namespace, cmd.BufferFile, logger)
57+
parsed.certPath, parsed.namespace, parsed.bufferFile, logger)
6758
},
6859
}
60+
}
61+
62+
// sshKeygenArgs holds the parsed result of a git ssh-keygen invocation.
63+
type sshKeygenArgs struct {
64+
command string // -Y value (sign, verify, find-principals, etc.)
65+
certPath string // -f value (path to public key)
66+
namespace string // -n value (always "git" for commit signing)
67+
bufferFile string // last non-flag argument
68+
}
6969

70-
gitSSHSignatureCmd.Flags().StringVarP(&cmd.CertPath, "file", "f", "", "Path to the private key")
71-
gitSSHSignatureCmd.Flags().StringVarP(&cmd.Namespace, "namespace", "n", "", "Namespace")
72-
gitSSHSignatureCmd.Flags().
73-
StringVarP(&cmd.Command, "command", "Y", "sign", "Command - should be 'sign'")
70+
// parseSSHKeygenArgs parses the ssh-keygen argument format used by git:
71+
//
72+
// -Y <command> -n <namespace> -f <key> [flags...] <buffer-file>
73+
//
74+
// The buffer file is always the last argument and is never a flag.
75+
// Unknown flags (e.g. -U for ssh-agent mode) are ignored.
76+
func parseSSHKeygenArgs(args []string) sshKeygenArgs {
77+
result := sshKeygenArgs{
78+
command: "sign",
79+
} // git only ever calls with sign, but default defensively
80+
for i := 0; i < len(args); i++ {
81+
consumeFlag(&result, args, &i)
82+
}
83+
// The buffer file is always the last argument and is never a flag.
84+
if len(args) > 0 && !strings.HasPrefix(args[len(args)-1], "-") {
85+
result.bufferFile = args[len(args)-1]
86+
}
87+
return result
88+
}
7489

75-
return gitSSHSignatureCmd
90+
// consumeFlag processes a single known flag from args at position i, advancing
91+
// i past the flag's value when present.
92+
func consumeFlag(result *sshKeygenArgs, args []string, i *int) {
93+
if *i+1 >= len(args) {
94+
return
95+
}
96+
switch args[*i] {
97+
case "-Y":
98+
result.command = args[*i+1]
99+
*i++
100+
case "-f":
101+
result.certPath = args[*i+1]
102+
*i++
103+
case "-n":
104+
result.namespace = args[*i+1]
105+
*i++
106+
}
76107
}
77108

78-
// delegateToSSHKeygen forwards the original arguments to the system ssh-keygen binary.
79-
func delegateToSSHKeygen(logger log.Logger) error {
109+
// delegateToSSHKeygen forwards args to the system ssh-keygen binary.
110+
func delegateToSSHKeygen(args []string, logger log.Logger) error {
80111
sshKeygen, err := exec.LookPath("ssh-keygen")
81112
if err != nil {
82113
return fmt.Errorf("find ssh-keygen: %w", err)
83114
}
84115

85-
// Extract the arguments that were originally passed to this command.
86-
// Find "git-ssh-signature" in os.Args and take everything after it.
87-
var sshArgs []string
88-
for i, arg := range os.Args {
89-
if strings.HasSuffix(arg, "git-ssh-signature") {
90-
sshArgs = os.Args[i+1:]
91-
break
92-
}
93-
}
94-
if sshArgs == nil {
95-
return fmt.Errorf("git-ssh-signature not found in process arguments")
96-
}
97-
98-
logger.Debugf("delegating to ssh-keygen: %s %v", sshKeygen, sshArgs)
116+
logger.Debugf("delegating to ssh-keygen: %s %v", sshKeygen, args)
99117

100-
c := exec.Command(sshKeygen, sshArgs...) // #nosec G204,G304,G702
118+
c := exec.Command(sshKeygen, args...) // #nosec G204,G304,G702
101119
c.Stdin = os.Stdin
102120
c.Stdout = os.Stdout
103121
c.Stderr = os.Stderr

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
}

0 commit comments

Comments
 (0)