diff --git a/pkg/build/signing/gpg.go b/pkg/build/signing/gpg.go new file mode 100644 index 00000000..80f48fb7 --- /dev/null +++ b/pkg/build/signing/gpg.go @@ -0,0 +1,57 @@ +package signing + +import ( + "context" + "fmt" + "os/exec" +) + +// GPGSigner signs files using GPG. +type GPGSigner struct { + KeyID string +} + +// Compile-time interface check. +var _ Signer = (*GPGSigner)(nil) + +// NewGPGSigner creates a new GPG signer. +func NewGPGSigner(keyID string) *GPGSigner { + return &GPGSigner{KeyID: keyID} +} + +// Name returns "gpg". +func (s *GPGSigner) Name() string { + return "gpg" +} + +// Available checks if gpg is installed and key is configured. +func (s *GPGSigner) Available() bool { + if s.KeyID == "" { + return false + } + _, err := exec.LookPath("gpg") + return err == nil +} + +// Sign creates a detached ASCII-armored signature. +// For file.txt, creates file.txt.asc +func (s *GPGSigner) Sign(ctx context.Context, file string) error { + if !s.Available() { + return fmt.Errorf("gpg.Sign: gpg not available or key not configured") + } + + cmd := exec.CommandContext(ctx, "gpg", + "--detach-sign", + "--armor", + "--local-user", s.KeyID, + "--output", file+".asc", + file, + ) + + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("gpg.Sign: %w\nOutput: %s", err, string(output)) + } + + return nil +} diff --git a/pkg/build/signing/gpg_test.go b/pkg/build/signing/gpg_test.go new file mode 100644 index 00000000..cf40cc60 --- /dev/null +++ b/pkg/build/signing/gpg_test.go @@ -0,0 +1,25 @@ +package signing + +import ( + "testing" +) + +func TestGPGSigner_Good_Name(t *testing.T) { + s := NewGPGSigner("ABCD1234") + if s.Name() != "gpg" { + t.Errorf("expected name 'gpg', got %q", s.Name()) + } +} + +func TestGPGSigner_Good_Available(t *testing.T) { + s := NewGPGSigner("ABCD1234") + // Available depends on gpg being installed + _ = s.Available() +} + +func TestGPGSigner_Bad_NoKey(t *testing.T) { + s := NewGPGSigner("") + if s.Available() { + t.Error("expected Available() to be false when key is empty") + } +}