diff --git a/cmd/entire/cli/objectsigner.go b/cmd/entire/cli/objectsigner.go index e322c87430..3292ec3236 100644 --- a/cmd/entire/cli/objectsigner.go +++ b/cmd/entire/cli/objectsigner.go @@ -5,6 +5,8 @@ import ( "fmt" "net" "os" + "path" + "strings" "sync" "github.com/entireio/cli/cmd/entire/cli/logging" @@ -27,11 +29,7 @@ func RegisterObjectSigner() { return nil } - sysCfg := loadScopedConfig(cfgSource, config.SystemScope) - globalCfg := loadScopedConfig(cfgSource, config.GlobalScope) - - // Merge system then global so that global settings take precedence. - merged := config.Merge(sysCfg, globalCfg) + merged, sshSignProgram := loadObjectSignerConfig(cfgSource) if !merged.Commit.GpgSign.IsTrue() { return nil @@ -43,7 +41,7 @@ func RegisterObjectSigner() { // will be unsigned, which is acceptable since signing is best-effort. // The default program is "ssh-keygen", which works with go-git's // native SSH agent signing and does not need to be skipped. - if auto.Format(merged.GPG.Format) == auto.FormatSSH && hasCustomSSHSignProgram(merged.Raw) { + if auto.Format(merged.GPG.Format) == auto.FormatSSH && isCustomSSHSignProgram(sshSignProgram) { logging.Debug(context.Background(), "skipping native SSH commit signing: custom gpg.ssh.program is configured") return nil } @@ -65,6 +63,14 @@ func RegisterObjectSigner() { }) } +func loadObjectSignerConfig(source plugin.ConfigSource) (*config.Config, string) { + sysCfg := loadScopedConfig(source, config.SystemScope) + globalCfg := loadScopedConfig(source, config.GlobalScope) + sshSignProgram := effectiveSSHSignProgram(sysCfg.Raw, globalCfg.Raw) + merged := config.Merge(sysCfg, globalCfg) + return &merged, sshSignProgram +} + // connectSSHAgent connects to the SSH agent via SSH_AUTH_SOCK. // Returns nil if the agent is unavailable. func connectSSHAgent() agent.Agent { //nolint:ireturn // must return the ssh agent interface @@ -87,20 +93,39 @@ var scopeName = map[config.Scope]string{ config.SystemScope: "system", } -// hasCustomSSHSignProgram checks whether gpg.ssh.program is set to a -// non-default value in the raw config. The git default is "ssh-keygen", -// which works with go-git's native SSH agent signing. Custom programs -// (e.g. 1Password's op-ssh-sign) use a separate signing mechanism that -// go-git cannot invoke. -// go-git's Config struct does not expose this field, so we read it directly. -func hasCustomSSHSignProgram(raw *format.Config) bool { +func rawSSHSignProgram(raw *format.Config) string { if raw == nil { + return "" + } + + return raw.Section("gpg").Subsection("ssh").Option("program") +} + +func isCustomSSHSignProgram(program string) bool { + program = strings.TrimSpace(program) + program = strings.Trim(program, `"'`) + if program == "" { return false } - program := raw.Section("gpg").Subsection("ssh").Option("program") + name := path.Base(strings.ReplaceAll(program, `\`, "/")) + return !strings.EqualFold(name, "ssh-keygen") && !strings.EqualFold(name, "ssh-keygen.exe") +} + +func effectiveSSHSignProgram(raws ...*format.Config) string { + for i := len(raws) - 1; i >= 0; i-- { + raw := raws[i] + if raw == nil { + continue + } + + program := rawSSHSignProgram(raw) + if program != "" { + return program + } + } - return program != "" && program != "ssh-keygen" + return "" } func loadScopedConfig(source plugin.ConfigSource, scope config.Scope) *config.Config { diff --git a/cmd/entire/cli/objectsigner_test.go b/cmd/entire/cli/objectsigner_test.go index 891e7af4e4..7fcb661d64 100644 --- a/cmd/entire/cli/objectsigner_test.go +++ b/cmd/entire/cli/objectsigner_test.go @@ -3,10 +3,12 @@ package cli import ( "testing" + "github.com/go-git/go-git/v6/config" format "github.com/go-git/go-git/v6/plumbing/format/config" + "github.com/go-git/x/plugin/objectsigner/auto" ) -func TestHasCustomSSHSignProgram(t *testing.T) { +func TestIsCustomSSHSignProgram(t *testing.T) { t.Parallel() tests := []struct { @@ -42,6 +44,33 @@ func TestHasCustomSSHSignProgram(t *testing.T) { }(), want: false, }, + { + name: "absolute ssh-keygen path is not custom", + raw: func() *format.Config { + c := format.New() + c.Section("gpg").Subsection("ssh").SetOption("program", "/usr/bin/ssh-keygen") + return c + }(), + want: false, + }, + { + name: "quoted ssh-keygen path is not custom", + raw: func() *format.Config { + c := format.New() + c.Section("gpg").Subsection("ssh").SetOption("program", "\"/usr/bin/ssh-keygen\"") + return c + }(), + want: false, + }, + { + name: "windows ssh-keygen exe is not custom", + raw: func() *format.Config { + c := format.New() + c.Section("gpg").Subsection("ssh").SetOption("program", `C:\Windows\System32\OpenSSH\ssh-keygen.exe`) + return c + }(), + want: false, + }, { name: "gpg section exists but no ssh.program", raw: func() *format.Config { @@ -57,10 +86,123 @@ func TestHasCustomSSHSignProgram(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - got := hasCustomSSHSignProgram(tt.raw) + got := isCustomSSHSignProgram(rawSSHSignProgram(tt.raw)) if got != tt.want { - t.Errorf("hasCustomSSHSignProgram() = %v, want %v", got, tt.want) + t.Errorf("isCustomSSHSignProgram(rawSSHSignProgram()) = %v, want %v", got, tt.want) } }) } } + +func TestConfigMerge_DropsCustomSSHSignProgramFromRaw(t *testing.T) { + t.Parallel() + + sysCfg := config.NewConfig() + sysCfg.GPG.Format = string(auto.FormatSSH) + sysCfg.Raw.Section("gpg").Subsection("ssh").SetOption("program", "/Applications/1Password.app/Contents/MacOS/op-ssh-sign") + + merged := config.Merge(sysCfg, config.NewConfig()) + + if !isCustomSSHSignProgram(rawSSHSignProgram(sysCfg.Raw)) { + t.Fatal("expected scoped raw config to report custom gpg.ssh.program") + } + + if isCustomSSHSignProgram(rawSSHSignProgram(merged.Raw)) { + t.Fatal("expected merged raw config to lose custom gpg.ssh.program") + } +} + +func TestCustomSSHSignProgramDetection_UsesScopedRawBeforeMerge(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + sysRaw *format.Config + globalRaw *format.Config + want bool + }{ + { + name: "system scope custom program", + sysRaw: func() *format.Config { + raw := format.New() + raw.Section("gpg").Subsection("ssh").SetOption("program", "/Applications/1Password.app/Contents/MacOS/op-ssh-sign") + return raw + }(), + globalRaw: format.New(), + want: true, + }, + { + name: "global scope custom program", + sysRaw: format.New(), + globalRaw: func() *format.Config { + raw := format.New() + raw.Section("gpg").Subsection("ssh").SetOption("program", "/Applications/1Password.app/Contents/MacOS/op-ssh-sign") + return raw + }(), + want: true, + }, + { + name: "default ssh-keygen in both scopes", + sysRaw: func() *format.Config { + raw := format.New() + raw.Section("gpg").Subsection("ssh").SetOption("program", "ssh-keygen") + return raw + }(), + globalRaw: func() *format.Config { + raw := format.New() + raw.Section("gpg").Subsection("ssh").SetOption("program", "ssh-keygen") + return raw + }(), + want: false, + }, + { + name: "no custom program configured", + sysRaw: format.New(), + globalRaw: format.New(), + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + sysCfg := config.NewConfig() + sysCfg.GPG.Format = string(auto.FormatSSH) + sysCfg.Raw = tt.sysRaw + + globalCfg := config.NewConfig() + globalCfg.GPG.Format = string(auto.FormatSSH) + globalCfg.Raw = tt.globalRaw + + got := isCustomSSHSignProgram(rawSSHSignProgram(sysCfg.Raw)) || isCustomSSHSignProgram(rawSSHSignProgram(globalCfg.Raw)) + if got != tt.want { + t.Fatalf("scoped raw detection = %v, want %v", got, tt.want) + } + + merged := config.Merge(sysCfg, globalCfg) + if got && isCustomSSHSignProgram(rawSSHSignProgram(merged.Raw)) { + t.Fatal("expected merged raw config not to preserve custom gpg.ssh.program") + } + }) + } +} + +func TestEffectiveSSHSignProgram_UsesHighestPrecedenceScope(t *testing.T) { + t.Parallel() + + sysRaw := format.New() + sysRaw.Section("gpg").Subsection("ssh").SetOption("program", "/usr/local/bin/custom-system-signer") + + globalRaw := format.New() + globalRaw.Section("gpg").Subsection("ssh").SetOption("program", "/Applications/1Password.app/Contents/MacOS/op-ssh-sign") + + got := effectiveSSHSignProgram(sysRaw, globalRaw) + if got != "/Applications/1Password.app/Contents/MacOS/op-ssh-sign" { + t.Fatalf("effectiveSSHSignProgram() = %q, want %q", got, "/Applications/1Password.app/Contents/MacOS/op-ssh-sign") + } + + if !isCustomSSHSignProgram(got) { + t.Fatal("expected highest-precedence global override to be treated as custom") + } +}