From fd7c15c75323e198a898c49f5c4d4210a3d2bd02 Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 29 Jan 2026 02:41:18 +0000 Subject: [PATCH] docs: add code signing design (S3.3) GPG signs checksums.txt by default. macOS codesign + notarization. Windows signtool deferred. Co-Authored-By: Claude Opus 4.5 --- docs/plans/2026-01-29-code-signing-design.md | 236 +++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 docs/plans/2026-01-29-code-signing-design.md diff --git a/docs/plans/2026-01-29-code-signing-design.md b/docs/plans/2026-01-29-code-signing-design.md new file mode 100644 index 0000000..cedf738 --- /dev/null +++ b/docs/plans/2026-01-29-code-signing-design.md @@ -0,0 +1,236 @@ +# Code Signing Design (S3.3) + +## Summary + +Integrate standard code signing tools into the build pipeline. GPG signs checksums by default. macOS codesign + notarization for Apple binaries. Windows signtool deferred. + +## Design Decisions + +- **Sign during build**: Signing happens in `pkg/build/signing/` after compilation, before archiving +- **Config location**: `.core/build.yaml` with environment variable fallbacks for secrets +- **GPG scope**: Signs `checksums.txt` only (standard pattern like Go, Terraform) +- **macOS flow**: Codesign always when identity configured, notarize optional with flag/config +- **Windows**: Placeholder for later implementation + +## Package Structure + +``` +pkg/build/signing/ +├── signer.go # Signer interface + SignConfig +├── gpg.go # GPG checksums signing +├── codesign.go # macOS codesign + notarize +└── signtool.go # Windows placeholder +``` + +## Signer Interface + +```go +// pkg/build/signing/signer.go +type Signer interface { + Name() string + Available() bool + Sign(ctx context.Context, artifact string) error +} + +type SignConfig struct { + Enabled bool `yaml:"enabled"` + GPG GPGConfig `yaml:"gpg,omitempty"` + MacOS MacOSConfig `yaml:"macos,omitempty"` + Windows WindowsConfig `yaml:"windows,omitempty"` +} + +type GPGConfig struct { + Key string `yaml:"key"` // Key ID or fingerprint, supports $ENV +} + +type MacOSConfig struct { + Identity string `yaml:"identity"` // Developer ID Application: ... + Notarize bool `yaml:"notarize"` // Submit to Apple + AppleID string `yaml:"apple_id"` // Apple account email + TeamID string `yaml:"team_id"` // Team ID + AppPassword string `yaml:"app_password"` // App-specific password +} + +type WindowsConfig struct { + Certificate string `yaml:"certificate"` // Path to .pfx + Password string `yaml:"password"` // Certificate password +} +``` + +## Config Schema + +In `.core/build.yaml`: + +```yaml +sign: + enabled: true + + gpg: + key: $GPG_KEY_ID + + macos: + identity: "Developer ID Application: Your Name (TEAM_ID)" + notarize: false + apple_id: $APPLE_ID + team_id: $APPLE_TEAM_ID + app_password: $APPLE_APP_PASSWORD + + # windows: (deferred) + # certificate: $WINDOWS_CERT_PATH + # password: $WINDOWS_CERT_PASSWORD +``` + +## Build Pipeline Integration + +``` +Build() in pkg/build/builders/go.go + ↓ +compile binaries + ↓ +Sign macOS binaries (codesign) ← NEW + ↓ +Notarize if enabled (wait) ← NEW + ↓ +Create archives (tar.gz, zip) + ↓ +Generate checksums.txt + ↓ +GPG sign checksums.txt ← NEW + ↓ +Return artifacts +``` + +## GPG Signer + +```go +// pkg/build/signing/gpg.go +type GPGSigner struct { + KeyID string +} + +func (s *GPGSigner) Name() string { return "gpg" } + +func (s *GPGSigner) Available() bool { + _, err := exec.LookPath("gpg") + return err == nil && s.KeyID != "" +} + +func (s *GPGSigner) Sign(ctx context.Context, file string) error { + cmd := exec.CommandContext(ctx, "gpg", + "--detach-sign", + "--armor", + "--local-user", s.KeyID, + "--output", file+".asc", + file, + ) + return cmd.Run() +} +``` + +**Output:** `checksums.txt.asc` (ASCII armored detached signature) + +**User verification:** +```bash +gpg --verify checksums.txt.asc checksums.txt +sha256sum -c checksums.txt +``` + +## macOS Codesign + +```go +// pkg/build/signing/codesign.go +type MacOSSigner struct { + Identity string + Notarize bool + AppleID string + TeamID string + AppPassword string +} + +func (s *MacOSSigner) Name() string { return "codesign" } + +func (s *MacOSSigner) Available() bool { + if runtime.GOOS != "darwin" { + return false + } + _, err := exec.LookPath("codesign") + return err == nil && s.Identity != "" +} + +func (s *MacOSSigner) Sign(ctx context.Context, binary string) error { + cmd := exec.CommandContext(ctx, "codesign", + "--sign", s.Identity, + "--timestamp", + "--options", "runtime", + "--force", + binary, + ) + return cmd.Run() +} + +func (s *MacOSSigner) NotarizeAndStaple(ctx context.Context, binary string) error { + // 1. Create ZIP for submission + zipPath := binary + ".zip" + exec.CommandContext(ctx, "zip", "-j", zipPath, binary).Run() + defer os.Remove(zipPath) + + // 2. Submit and wait + cmd := exec.CommandContext(ctx, "xcrun", "notarytool", "submit", + zipPath, + "--apple-id", s.AppleID, + "--team-id", s.TeamID, + "--password", s.AppPassword, + "--wait", + ) + if err := cmd.Run(); err != nil { + return fmt.Errorf("notarization failed: %w", err) + } + + // 3. Staple ticket + return exec.CommandContext(ctx, "xcrun", "stapler", "staple", binary).Run() +} +``` + +## CLI Flags + +```bash +core build # Sign with defaults (GPG + codesign if configured) +core build --no-sign # Skip all signing +core build --notarize # Enable macOS notarization (overrides config) +``` + +## Environment Variables + +| Variable | Purpose | +|----------|---------| +| `GPG_KEY_ID` | GPG key ID or fingerprint | +| `CODESIGN_IDENTITY` | macOS Developer ID (fallback) | +| `APPLE_ID` | Apple account email | +| `APPLE_TEAM_ID` | Apple Developer Team ID | +| `APPLE_APP_PASSWORD` | App-specific password for notarization | + +## Deferred + +- **Windows signtool**: Placeholder implementation returning nil +- **Sigstore/keyless signing**: Future consideration +- **Binary-level GPG signatures**: Only checksums.txt signed + +## Implementation Steps + +1. Create `pkg/build/signing/` package structure +2. Implement Signer interface and SignConfig +3. Implement GPGSigner +4. Implement MacOSSigner with codesign +5. Add notarization support to MacOSSigner +6. Add SignConfig to build.Config +7. Integrate signing into build pipeline +8. Add CLI flags (--no-sign, --notarize) +9. Add Windows placeholder +10. Tests with mocked exec + +## Dependencies + +- `gpg` CLI (system) +- `codesign` CLI (macOS Xcode Command Line Tools) +- `xcrun notarytool` (macOS Xcode Command Line Tools) +- `xcrun stapler` (macOS Xcode Command Line Tools)