@@ -3,12 +3,14 @@ package gitsshsigning
33import (
44 "bytes"
55 "fmt"
6+ "os"
67 "os/exec"
78)
89
910type GitSSHSignatureRequest struct {
10- Content string
11- KeyPath string
11+ Content string
12+ KeyPath string
13+ PublicKey string // Public key content; when set, written to a temp file for ssh-keygen
1214}
1315
1416type GitSSHSignatureResponse struct {
@@ -18,22 +20,38 @@ type GitSSHSignatureResponse struct {
1820// Sign signs the content using the private key and returns the signature.
1921// This is intended to be a drop-in replacement for gpg.ssh.program for git,
2022// so we simply execute ssh-keygen in the same way as git would do locally.
23+ //
24+ // When PublicKey is set, it is written to a temporary file that ssh-keygen
25+ // can read. This is necessary because the original KeyPath comes from
26+ // inside the container and does not exist on the host where Sign() runs.
2127func (req * GitSSHSignatureRequest ) Sign () (* GitSSHSignatureResponse , error ) {
22- // Create a buffer to store the commit content
28+ keyFile , cleanup , err := req .resolveKeyFile ()
29+ if err != nil {
30+ return nil , fmt .Errorf ("resolve signing key: %w" , err )
31+ }
32+ defer cleanup ()
33+
2334 var commitBuffer bytes.Buffer
2435 commitBuffer .WriteString (req .Content )
2536
26- // Create the command to run ssh-keygen
27- cmd := exec .Command ("ssh-keygen" , "-Y" , "sign" , "-f" , req .KeyPath , "-n" , "git" )
37+ //nolint:gosec // keyFile is a controlled temp path or validated KeyPath
38+ cmd := exec .Command (
39+ "ssh-keygen" ,
40+ "-Y" ,
41+ "sign" ,
42+ "-f" ,
43+ keyFile ,
44+ "-n" ,
45+ "git" ,
46+ )
2847 cmd .Stdin = & commitBuffer
2948
30- // Capture the output of the command
3149 var out bytes.Buffer
3250 var stderr bytes.Buffer
3351 cmd .Stdout = & out
3452 cmd .Stderr = & stderr
3553
36- err : = cmd .Run ()
54+ err = cmd .Run ()
3755 if err != nil {
3856 return nil , fmt .Errorf ("failed to sign commit: %w, stderr: %s" , err , stderr .String ())
3957 }
@@ -42,3 +60,35 @@ func (req *GitSSHSignatureRequest) Sign() (*GitSSHSignatureResponse, error) {
4260 Signature : out .Bytes (),
4361 }, nil
4462}
63+
64+ // resolveKeyFile returns the path to use for ssh-keygen -f and a cleanup function.
65+ // When PublicKey content is available, it writes a temp file. Otherwise falls back to KeyPath.
66+ func (req * GitSSHSignatureRequest ) resolveKeyFile () (string , func (), error ) {
67+ noop := func () {}
68+
69+ if req .PublicKey == "" {
70+ return req .KeyPath , noop , nil
71+ }
72+
73+ // ssh-keygen -Y sign -f <path> reads the public key directly from <path>
74+ // to identify which SSH agent key to use for signing. We write the public
75+ // key content to a temp file and pass that path to -f.
76+ tmpFile , err := os .CreateTemp ("" , ".git_signing_key_*" )
77+ if err != nil {
78+ return "" , noop , fmt .Errorf ("create temp key file: %w" , err )
79+ }
80+ keyPath := tmpFile .Name ()
81+
82+ if _ , err := tmpFile .WriteString (req .PublicKey ); err != nil {
83+ _ = tmpFile .Close ()
84+ _ = os .Remove (keyPath )
85+ return "" , noop , fmt .Errorf ("write public key: %w" , err )
86+ }
87+ _ = tmpFile .Close ()
88+
89+ cleanup := func () {
90+ _ = os .Remove (keyPath )
91+ }
92+
93+ return keyPath , cleanup , nil
94+ }
0 commit comments