Skip to content

Commit e7fc238

Browse files
authored
Merge pull request #7 from go-git/auto-binary
objectsigner: Allow program to be a path to an executable
2 parents 8252d81 + 3fb9831 commit e7fc238

2 files changed

Lines changed: 46 additions & 23 deletions

File tree

plugin/objectsigner/program/program.go

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
// prefix or the deprecated raw ssh-* form, are written to a temporary key
1313
// file and passed with -U so ssh-keygen signs via ssh-agent.
1414
//
15-
// The program must be a bare binary name resolvable on $PATH (e.g. "gpg",
16-
// not "/usr/bin/gpg" or "./gpg").
15+
// The program may be a bare binary name resolved on $PATH (e.g. "gpg") or
16+
// a path to an executable (e.g. "/usr/bin/gpg" or "./gpg").
1717
package program
1818

1919
import (
@@ -49,11 +49,9 @@ var (
4949
ErrUnsupportedFormat = errors.New("unsupported signing format")
5050
// ErrEmptyProgram is returned when no program name was provided.
5151
ErrEmptyProgram = errors.New("program is empty")
52-
// ErrInvalidProgram is returned when program is not a bare binary name
53-
// (i.e. contains a path separator).
54-
ErrInvalidProgram = errors.New("program must be a bare binary name")
55-
// ErrProgramNotFound is returned when program cannot be resolved on $PATH.
56-
ErrProgramNotFound = errors.New("program not found in PATH")
52+
// ErrProgramNotFound is returned when program cannot be resolved as
53+
// either a path to an existing executable or a bare name on $PATH.
54+
ErrProgramNotFound = errors.New("program not found")
5755
// ErrEmptySigningKey is returned when no signing key was provided.
5856
ErrEmptySigningKey = errors.New("signing key is empty")
5957
// ErrNilMessage is returned when a nil message is passed to Sign.
@@ -69,8 +67,8 @@ var (
6967
// New returns a signer that invokes program to produce a signature in the
7068
// given format using the provided signing key.
7169
//
72-
// program must be a bare binary name (no path separators) and must resolve
73-
// on $PATH at the time of this call; otherwise [ErrInvalidProgram] or
70+
// program is either a bare binary name resolved on $PATH or a path to an
71+
// executable; it must resolve at the time of this call, otherwise
7472
// [ErrProgramNotFound] is returned.
7573
//
7674
// For OpenPGP and X.509, signingKey is the key ID or fingerprint passed to
@@ -125,25 +123,30 @@ func newSigner(
125123
}, nil
126124
}
127125

128-
// resolveProgram validates that program is a bare binary name and returns
129-
// the absolute path that lookPath resolves it to. The resolved path is
130-
// captured at New() time and reused by Sign() so a $PATH change between the
131-
// two calls cannot redirect execution to a different binary.
126+
// resolveProgram returns the absolute path that lookPath resolves program
127+
// to. When program contains a path separator, lookPath (exec.LookPath in
128+
// production) verifies the file exists and is executable and returns the
129+
// path as given; otherwise it is searched for on $PATH. Relative results
130+
// (e.g. from a "./gpg" input) are anchored to the current working directory
131+
// via filepath.Abs so that the path captured at New() time cannot be
132+
// redirected by a $PATH change or a working-directory change before Sign()
133+
// runs.
132134
func resolveProgram(program string, lookPath func(string) (string, error)) (string, error) {
133135
if program == "" {
134136
return "", ErrEmptyProgram
135137
}
136138

137-
if filepath.Base(program) != program {
138-
return "", fmt.Errorf("%w: %q", ErrInvalidProgram, program)
139-
}
140-
141139
resolved, err := lookPath(program)
142140
if err != nil {
143141
return "", fmt.Errorf("%w: %q: %w", ErrProgramNotFound, program, err)
144142
}
145143

146-
return resolved, nil
144+
absolute, err := filepath.Abs(resolved)
145+
if err != nil {
146+
return "", fmt.Errorf("resolving absolute path for %q: %w", program, err)
147+
}
148+
149+
return absolute, nil
147150
}
148151

149152
// Sign reads message and returns the signature produced by the external

plugin/objectsigner/program/program_test.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@ var (
2424
)
2525

2626
func resolvedTestProgram() string {
27-
return filepath.Join(string(os.PathSeparator), "mock", testProgram)
27+
abs, err := filepath.Abs(filepath.Join(string(os.PathSeparator), "mock", testProgram))
28+
if err != nil {
29+
panic(err)
30+
}
31+
32+
return abs
2833
}
2934

3035
//nolint:funlen // table test enumerates each validation branch.
@@ -70,20 +75,20 @@ func TestNew(t *testing.T) {
7075
{
7176
name: "absolute path program", format: FormatOpenPGP,
7277
program: filepath.Join(string(os.PathSeparator), "usr", "bin", testProgram),
73-
signingKey: "k", wantErr: ErrInvalidProgram,
78+
signingKey: "k", wantErr: nil,
7479
},
7580
{
7681
name: "relative path program", format: FormatOpenPGP,
7782
program: "." + string(os.PathSeparator) + testProgram,
78-
signingKey: "k", wantErr: ErrInvalidProgram,
83+
signingKey: "k", wantErr: nil,
7984
},
8085
{
8186
name: "subdir program", format: FormatOpenPGP,
8287
program: filepath.Join("bin", testProgram),
83-
signingKey: "k", wantErr: ErrInvalidProgram,
88+
signingKey: "k", wantErr: nil,
8489
},
8590
{
86-
name: "program not in PATH", format: FormatOpenPGP,
91+
name: "program not found", format: FormatOpenPGP,
8792
program: "missing", signingKey: "k", wantErr: ErrProgramNotFound,
8893
},
8994
{
@@ -110,6 +115,21 @@ func TestNew(t *testing.T) {
110115
}
111116
}
112117

118+
func TestNew_RelativeResolvedPathBecomesAbsolute(t *testing.T) {
119+
t.Parallel()
120+
121+
lookPath := func(string) (string, error) {
122+
return filepath.Join("relative", "to", "cwd", testProgram), nil
123+
}
124+
commandContext, _ := stubCommand(nil)
125+
126+
signer, err := newSigner(FormatOpenPGP, "./"+testProgram, "k", lookPath, commandContext)
127+
require.NoError(t, err)
128+
require.NotNil(t, signer)
129+
assert.True(t, filepath.IsAbs(signer.program),
130+
"program should be anchored to an absolute path, got %q", signer.program)
131+
}
132+
113133
func TestSign_NilMessage(t *testing.T) {
114134
t.Parallel()
115135

0 commit comments

Comments
 (0)