Skip to content
Closed
55 changes: 40 additions & 15 deletions cmd/entire/cli/objectsigner.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"net"
"os"
"path"
"strings"
"sync"

"github.com/entireio/cli/cmd/entire/cli/logging"
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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")
}
Comment thread
pfleidi marked this conversation as resolved.

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 {
Expand Down
148 changes: 145 additions & 3 deletions cmd/entire/cli/objectsigner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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")
}
}
Loading