Skip to content
Merged
115 changes: 81 additions & 34 deletions cmd/agent/git_ssh_signature.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
package agent

import (
"errors"
"fmt"
"os"
"os/exec"
"strings"

"github.com/skevetter/devpod/cmd/flags"
"github.com/skevetter/devpod/pkg/gitsshsigning"
"github.com/skevetter/log"
"github.com/spf13/cobra"
)

type GitSSHSignatureCmd struct {
*flags.GlobalFlags

CertPath string
Namespace string
BufferFile string
Command string
}

// NewGitSSHSignatureCmd creates new git-ssh-signature command
// This agent command can be used as git ssh program by setting
//
Expand All @@ -28,41 +22,94 @@ type GitSSHSignatureCmd struct {
//
// custom-ssh-signature-handler -Y sign -n git -f /Users/johndoe/.ssh/my-key.pub /tmp/.git_signing_buffer_tmp4Euk6d
func NewGitSSHSignatureCmd(flags *flags.GlobalFlags) *cobra.Command {
cmd := &GitSSHSignatureCmd{
GlobalFlags: flags,
}

gitSSHSignatureCmd := &cobra.Command{
return &cobra.Command{
Use: "git-ssh-signature",
// Allow unknown flags so that git can pass any ssh-keygen flags
// (e.g. -U for stdin input) without cobra rejecting them.
FParseErrWhitelist: cobra.FParseErrWhitelist{
UnknownFlags: true,
},
// This command implements the ssh-keygen protocol used by git for commit
// signing. Disable cobra's flag parsing so we can handle the ssh-keygen
// argument format directly, including boolean flags like -U (ssh-agent
// mode) that cobra cannot distinguish from flags that take a value.
DisableFlagParsing: true,
RunE: func(_ *cobra.Command, args []string) error {
logger := log.GetInstance()

if len(args) < 1 {
logger.Fatalf("Buffer file is required")
}
parsed := parseSSHKeygenArgs(args)

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

// The last argument is the buffer file
cmd.BufferFile = args[len(args)-1]
if parsed.bufferFile == "" {
return fmt.Errorf("buffer file is required")
}

return gitsshsigning.HandleGitSSHProgramCall(
cmd.CertPath, cmd.Namespace, cmd.BufferFile, logger)
parsed.certPath, parsed.namespace, parsed.bufferFile, logger)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
}
}

// sshKeygenArgs holds the parsed result of a git ssh-keygen invocation.
type sshKeygenArgs struct {
command string // -Y value (sign, verify, find-principals, etc.)
certPath string // -f value (path to public key)
namespace string // -n value (always "git" for commit signing)
bufferFile string // last non-flag argument
}

// parseSSHKeygenArgs parses the ssh-keygen argument format used by git:
//
// -Y <command> -n <namespace> -f <key> [flags...] <buffer-file>
//
// The buffer file is always the last argument and is never a flag.
// Unknown flags (e.g. -U for ssh-agent mode) are ignored.
func parseSSHKeygenArgs(args []string) sshKeygenArgs {
result := sshKeygenArgs{
command: "sign",
} // git only ever calls with sign, but default defensively
for i := 0; i < len(args); i++ {
consumeFlag(&result, args, &i)
}
// The buffer file is always the last argument and is never a flag.
if len(args) > 0 && !strings.HasPrefix(args[len(args)-1], "-") {
result.bufferFile = args[len(args)-1]
}
return result
}

// consumeFlag processes a single known flag from args at position i, advancing
// i past the flag's value when present.
func consumeFlag(result *sshKeygenArgs, args []string, i *int) {
if *i+1 >= len(args) {
return
}
switch args[*i] {
case "-Y":
result.command = args[*i+1]
*i++
case "-f":
result.certPath = args[*i+1]
*i++
case "-n":
result.namespace = args[*i+1]
*i++
}
}

// delegateToSSHKeygen forwards args to the system ssh-keygen binary.
func delegateToSSHKeygen(args []string, logger log.Logger) error {
sshKeygen, err := exec.LookPath("ssh-keygen")
if err != nil {
return fmt.Errorf("find ssh-keygen: %w", err)
}

logger.Debugf("delegating to ssh-keygen: %s %v", sshKeygen, args)

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

return gitSSHSignatureCmd
return c.Run()
}
2 changes: 1 addition & 1 deletion cmd/agent/git_ssh_signature_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type GitSSHSignatureHelperCmd struct {
// The signing key path is a required argument for this command. It should be what equal to what you
// would have set as user.signingkey git config.
func NewGitSSHSignatureHelperCmd(flags *flags.GlobalFlags) *cobra.Command {
cmd := &GitSSHSignatureCmd{
cmd := &GitSSHSignatureHelperCmd{
GlobalFlags: flags,
}

Expand Down
85 changes: 33 additions & 52 deletions cmd/agent/git_ssh_signature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package agent
import (
"testing"

"github.com/skevetter/devpod/cmd/flags"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
Expand All @@ -16,61 +15,43 @@ func TestGitSSHSignatureSuite(t *testing.T) {
suite.Run(t, new(GitSSHSignatureTestSuite))
}

func (s *GitSSHSignatureTestSuite) TestAcceptsUnknownFlags() {
cmd := NewGitSSHSignatureCmd(&flags.GlobalFlags{})

// Git passes: -Y sign -n git -f /path/to/key -U /dev/stdin /tmp/buffer
// -U is an unknown flag that consumes /dev/stdin as its value.
// /tmp/buffer remains as a positional argument.
err := cmd.ParseFlags(
[]string{
"-Y",
"sign",
"-n",
"git",
"-f",
"/path/to/key",
"-U",
"/dev/stdin",
"/tmp/buffer",
},
)
assert.NoError(s.T(), err, "flag parsing should succeed with unknown flag -U")

args := cmd.Flags().Args()
s.Require().NotEmpty(args, "should have positional args")
assert.Equal(s.T(), "/tmp/buffer", args[len(args)-1],
"buffer file should be preserved as last positional arg")
func (s *GitSSHSignatureTestSuite) TestParseBasicSignArgs() {
args := []string{"-Y", "sign", "-n", "git", "-f", "/path/to/key.pub", "/tmp/buffer"}
result := parseSSHKeygenArgs(args)
assert.Equal(s.T(), "sign", result.command)
assert.Equal(s.T(), "git", result.namespace)
assert.Equal(s.T(), "/path/to/key.pub", result.certPath)
assert.Equal(s.T(), "/tmp/buffer", result.bufferFile)
}

func (s *GitSSHSignatureTestSuite) TestBufferFileAsPositionalArg() {
cmd := NewGitSSHSignatureCmd(&flags.GlobalFlags{})

err := cmd.ParseFlags(
[]string{"-Y", "sign", "-n", "git", "-f", "/path/to/key", "/tmp/buffer"},
)
assert.NoError(s.T(), err)

args := cmd.Flags().Args()
s.Require().NotEmpty(args, "should have positional args")
assert.Equal(s.T(), "/tmp/buffer", args[len(args)-1],
"last positional arg should be the buffer file")
func (s *GitSSHSignatureTestSuite) TestParseWithAgentFlag() {
// When the signing key is loaded in the ssh-agent, git passes -U (a boolean
// "use agent" flag) immediately before the buffer file. The buffer file must
// still be recognised as the last non-flag argument.
args := []string{"-Y", "sign", "-n", "git", "-f", "/path/to/key.pub", "-U", "/tmp/buffer"}
result := parseSSHKeygenArgs(args)
assert.Equal(s.T(), "sign", result.command)
assert.Equal(s.T(), "/path/to/key.pub", result.certPath)
assert.Equal(s.T(), "/tmp/buffer", result.bufferFile)
}

func (s *GitSSHSignatureTestSuite) TestKnownFlagsParsed() {
cmd := NewGitSSHSignatureCmd(&flags.GlobalFlags{})

err := cmd.ParseFlags(
[]string{"-Y", "sign", "-n", "git", "-f", "/path/to/key", "/tmp/buffer"},
)
assert.NoError(s.T(), err)
func (s *GitSSHSignatureTestSuite) TestParseNonSignCommand() {
args := []string{"-Y", "verify", "-n", "git", "-f", "/path/to/key.pub", "/tmp/buffer"}
result := parseSSHKeygenArgs(args)
assert.Equal(s.T(), "verify", result.command)
}

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

args := cmd.Flags().Args()
s.Require().NotEmpty(args, "should have positional args")
assert.Equal(s.T(), "/tmp/buffer", args[len(args)-1],
"last positional arg should be the buffer file")
func (s *GitSSHSignatureTestSuite) TestParseDefaultsToSign() {
// If -Y is absent the command defaults to "sign".
args := []string{"-n", "git", "-f", "/path/to/key.pub", "/tmp/buffer"}
result := parseSSHKeygenArgs(args)
assert.Equal(s.T(), "sign", result.command)
assert.Equal(s.T(), "/tmp/buffer", result.bufferFile)
}
2 changes: 0 additions & 2 deletions cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -1565,8 +1565,6 @@ func setupGitSSHSignature(
out, err := exec.Command(
execPath,
"ssh",
"--agent-forwarding=true",
"--start-services=true",
"--user",
remoteUser,
"--context",
Expand Down
Loading
Loading