Skip to content

Commit 5a429f1

Browse files
authored
Merge pull request #1128 from entireio/sign-program
Use go-git program signer for custom checkpoint signing
2 parents 536322d + 97a0d06 commit 5a429f1

7 files changed

Lines changed: 371 additions & 295 deletions

File tree

cmd/entire/cli/checkpoint/committed.go

Lines changed: 0 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"fmt"
1010
"io"
1111
"log/slog"
12-
"net"
1312
"os"
1413
"path/filepath"
1514
"sort"
@@ -36,12 +35,8 @@ import (
3635
"github.com/go-git/go-git/v6/config"
3736
"github.com/go-git/go-git/v6/plumbing"
3837
"github.com/go-git/go-git/v6/plumbing/filemode"
39-
format "github.com/go-git/go-git/v6/plumbing/format/config"
4038
"github.com/go-git/go-git/v6/plumbing/object"
4139
"github.com/go-git/go-git/v6/utils/binary"
42-
"github.com/go-git/go-git/v6/x/plugin"
43-
"github.com/go-git/x/plugin/objectsigner/auto"
44-
sshagent "golang.org/x/crypto/ssh/agent"
4540
)
4641

4742
// errStopIteration is used to stop commit iteration early in GetCheckpointAuthor.
@@ -53,14 +48,6 @@ var errStopIteration = errors.New("stop iteration")
5348
// unwrapped function.
5449
var chunkTranscript = agent.ChunkTranscript
5550

56-
var (
57-
objectSignerLoader = loadObjectSigner
58-
scopeName = map[config.Scope]string{
59-
config.GlobalScope: "global",
60-
config.SystemScope: "system",
61-
}
62-
)
63-
6451
// WriteCommitted writes a committed checkpoint to the entire/checkpoints/v1 branch.
6552
// Checkpoints are stored at sharded paths: <id[:2]>/<id[2:]>/
6653
//
@@ -1914,92 +1901,6 @@ func SignCommitBestEffort(ctx context.Context, commit *object.Commit) {
19141901
commit.Signature = string(sig)
19151902
}
19161903

1917-
func loadObjectSigner(ctx context.Context) (plugin.Signer, bool) {
1918-
cfgSource, err := plugin.Get(plugin.ConfigLoader())
1919-
if err != nil {
1920-
// No config loader registered; signing not possible.
1921-
return nil, false
1922-
}
1923-
1924-
sysCfg := loadScopedConfig(cfgSource, config.SystemScope)
1925-
globalCfg := loadScopedConfig(cfgSource, config.GlobalScope)
1926-
1927-
// Merge system then global so that global settings take precedence.
1928-
merged := config.Merge(sysCfg, globalCfg)
1929-
1930-
if !merged.Commit.GpgSign.IsTrue() {
1931-
return nil, false
1932-
}
1933-
1934-
// Custom gpg.ssh.program values use an external signer flow that go-git
1935-
// cannot invoke, so fall back to unsigned checkpoint commits.
1936-
if auto.Format(merged.GPG.Format) == auto.FormatSSH && hasCustomSSHSignProgram(merged.Raw) {
1937-
logging.Debug(ctx, "skipping native SSH commit signing: custom gpg.ssh.program is configured")
1938-
return nil, false
1939-
}
1940-
1941-
signer, err := auto.FromConfig(auto.Config{
1942-
SigningKey: merged.User.SigningKey,
1943-
Format: auto.Format(merged.GPG.Format),
1944-
SSHAgent: connectSSHAgent(ctx),
1945-
})
1946-
if err != nil {
1947-
logging.Debug(ctx, "failed to create object signer", "error", err.Error())
1948-
return nil, false
1949-
}
1950-
1951-
return signer, true
1952-
}
1953-
1954-
// connectSSHAgent connects to the SSH agent via SSH_AUTH_SOCK.
1955-
// Returns nil if the agent is unavailable.
1956-
func connectSSHAgent(ctx context.Context) sshagent.Agent {
1957-
sock := os.Getenv("SSH_AUTH_SOCK")
1958-
if sock == "" {
1959-
return nil
1960-
}
1961-
1962-
var d net.Dialer
1963-
conn, err := d.DialContext(ctx, "unix", sock)
1964-
if err != nil {
1965-
return nil
1966-
}
1967-
1968-
return sshagent.NewClient(conn)
1969-
}
1970-
1971-
// hasCustomSSHSignProgram checks whether gpg.ssh.program is set to a
1972-
// non-default value in the raw config. The git default is "ssh-keygen",
1973-
// which works with go-git's native SSH agent signing. Custom programs use
1974-
// a separate signing mechanism that go-git cannot invoke.
1975-
func hasCustomSSHSignProgram(raw *format.Config) bool {
1976-
if raw == nil {
1977-
return false
1978-
}
1979-
1980-
program := raw.Section("gpg").Subsection("ssh").Option("program")
1981-
1982-
return program != "" && program != "ssh-keygen"
1983-
}
1984-
1985-
func loadScopedConfig(source plugin.ConfigSource, scope config.Scope) *config.Config {
1986-
name := scopeName[scope]
1987-
1988-
storer, err := source.Load(scope)
1989-
if err != nil {
1990-
fmt.Fprintf(os.Stderr, "warning: failed to load %s git config: %v\n", name, err)
1991-
return config.NewConfig()
1992-
}
1993-
1994-
cfg, err := storer.Config()
1995-
if err != nil {
1996-
fmt.Fprintf(os.Stderr, "warning: failed to parse %s git config: %v\n", name, err)
1997-
return config.NewConfig()
1998-
}
1999-
2000-
return cfg
2001-
}
2002-
20031904
// readTranscriptFromTree reads a transcript from a git tree, handling both chunked and non-chunked formats.
20041905
// It checks for chunk files first (.001, .002, etc.), then falls back to the base file.
20051906
// The agentType is used for reassembling chunks in the correct format.
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package checkpoint
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log/slog"
7+
"net"
8+
"os"
9+
10+
"github.com/entireio/cli/cmd/entire/cli/logging"
11+
"github.com/go-git/go-git/v6/config"
12+
format "github.com/go-git/go-git/v6/plumbing/format/config"
13+
"github.com/go-git/go-git/v6/x/plugin"
14+
"github.com/go-git/x/plugin/objectsigner/auto"
15+
programsigner "github.com/go-git/x/plugin/objectsigner/program"
16+
sshagent "golang.org/x/crypto/ssh/agent"
17+
)
18+
19+
var (
20+
objectSignerLoader = loadObjectSigner
21+
scopeName = map[config.Scope]string{
22+
config.GlobalScope: "global",
23+
config.SystemScope: "system",
24+
}
25+
)
26+
27+
func loadObjectSigner(ctx context.Context) (plugin.Signer, bool) {
28+
cfgSource, err := plugin.Get(plugin.ConfigLoader())
29+
if err != nil {
30+
// No config loader registered; signing not possible.
31+
return nil, false
32+
}
33+
34+
sysCfg := loadScopedConfig(cfgSource, config.SystemScope)
35+
globalCfg := loadScopedConfig(cfgSource, config.GlobalScope)
36+
37+
return loadObjectSignerFromConfigs(ctx, sysCfg, globalCfg)
38+
}
39+
40+
func loadObjectSignerFromConfigs(ctx context.Context, sysCfg, globalCfg *config.Config) (plugin.Signer, bool) {
41+
// Merge system then global so that global settings take precedence.
42+
merged := config.Merge(sysCfg, globalCfg)
43+
44+
if !merged.Commit.GpgSign.IsTrue() {
45+
return nil, false
46+
}
47+
48+
if signer, ok := loadCustomProgramSigner(ctx, sysCfg, globalCfg, merged); ok {
49+
return signer, true
50+
}
51+
52+
signer, err := auto.FromConfig(auto.Config{
53+
SigningKey: merged.User.SigningKey,
54+
Format: auto.Format(merged.GPG.Format),
55+
SSHAgent: connectSSHAgent(ctx),
56+
})
57+
if err != nil {
58+
logging.Debug(ctx, "failed to create object signer", slog.String("error", err.Error()))
59+
return nil, false
60+
}
61+
62+
return signer, true
63+
}
64+
65+
func loadCustomProgramSigner(ctx context.Context, sysCfg, globalCfg *config.Config, merged config.Config) (plugin.Signer, bool) {
66+
signFormat := normalizeProgramFormat(merged.GPG.Format)
67+
68+
// TODO: Replace with merged.GPG.Program once that is surfaced by go-git.
69+
programName, ok := customSignProgram(signFormat, rawConfig(sysCfg), rawConfig(globalCfg))
70+
if !ok {
71+
return nil, false
72+
}
73+
74+
signer, err := programsigner.New(signFormat, programName, merged.User.SigningKey)
75+
if err != nil {
76+
logging.Debug(ctx, "failed to create object signer from custom program", slog.String("error", err.Error()))
77+
return nil, false
78+
}
79+
80+
logging.Debug(
81+
ctx,
82+
"using custom object signer program",
83+
slog.String("format", string(signFormat)),
84+
slog.String("program", programName),
85+
)
86+
87+
return signer, true
88+
}
89+
90+
func rawConfig(cfg *config.Config) *format.Config {
91+
if cfg == nil {
92+
return nil
93+
}
94+
95+
return cfg.Raw
96+
}
97+
98+
func normalizeProgramFormat(gitFormat string) programsigner.Format {
99+
switch auto.Format(gitFormat) {
100+
case "", auto.FormatOpenPGP:
101+
return programsigner.FormatOpenPGP
102+
case auto.FormatSSH:
103+
return programsigner.FormatSSH
104+
case auto.Format("x509"):
105+
return programsigner.FormatX509
106+
default:
107+
return programsigner.Format(gitFormat)
108+
}
109+
}
110+
111+
// customSignProgram returns the effective custom signer program for signFormat.
112+
// Git supports both legacy OpenPGP gpg.program and format-specific
113+
// gpg.<format>.program settings; format-specific values override gpg.program
114+
// within the same scope, and later scopes override earlier scopes.
115+
func customSignProgram(signFormat programsigner.Format, raws ...*format.Config) (string, bool) {
116+
var programName string
117+
for _, raw := range raws {
118+
if raw == nil {
119+
continue
120+
}
121+
122+
if scopedProgram := signProgramFromRaw(signFormat, raw); scopedProgram != "" {
123+
programName = scopedProgram
124+
}
125+
}
126+
127+
if programName == "" || programName == defaultSignProgram(signFormat) {
128+
return "", false
129+
}
130+
131+
return programName, true
132+
}
133+
134+
func signProgramFromRaw(signFormat programsigner.Format, raw *format.Config) string {
135+
if raw == nil {
136+
return ""
137+
}
138+
139+
gpgSection := raw.Section("gpg")
140+
var programName string
141+
if signFormat == programsigner.FormatOpenPGP {
142+
programName = gpgSection.Option("program")
143+
}
144+
if formatProgram := gpgSection.Subsection(string(signFormat)).Option("program"); formatProgram != "" {
145+
programName = formatProgram
146+
}
147+
148+
return programName
149+
}
150+
151+
func defaultSignProgram(signFormat programsigner.Format) string {
152+
switch signFormat {
153+
case programsigner.FormatOpenPGP:
154+
return "gpg"
155+
case programsigner.FormatSSH:
156+
return "ssh-keygen"
157+
case programsigner.FormatX509:
158+
return "gpgsm"
159+
default:
160+
return ""
161+
}
162+
}
163+
164+
// connectSSHAgent connects to the SSH agent via SSH_AUTH_SOCK.
165+
// Returns nil if the agent is unavailable.
166+
func connectSSHAgent(ctx context.Context) sshagent.Agent {
167+
sock := os.Getenv("SSH_AUTH_SOCK")
168+
if sock == "" {
169+
return nil
170+
}
171+
172+
var d net.Dialer
173+
conn, err := d.DialContext(ctx, "unix", sock)
174+
if err != nil {
175+
return nil
176+
}
177+
178+
return sshagent.NewClient(conn)
179+
}
180+
181+
func loadScopedConfig(source plugin.ConfigSource, scope config.Scope) *config.Config {
182+
name := scopeName[scope]
183+
184+
storer, err := source.Load(scope)
185+
if err != nil {
186+
fmt.Fprintf(os.Stderr, "warning: failed to load %s git config: %v\n", name, err)
187+
return config.NewConfig()
188+
}
189+
190+
cfg, err := storer.Config()
191+
if err != nil {
192+
fmt.Fprintf(os.Stderr, "warning: failed to parse %s git config: %v\n", name, err)
193+
return config.NewConfig()
194+
}
195+
196+
return cfg
197+
}

0 commit comments

Comments
 (0)