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 <noreply@anthropic.com>
This commit is contained in:
parent
acd0f90424
commit
fd7c15c753
1 changed files with 236 additions and 0 deletions
236
docs/plans/2026-01-29-code-signing-design.md
Normal file
236
docs/plans/2026-01-29-code-signing-design.md
Normal file
|
|
@ -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)
|
||||||
Loading…
Add table
Reference in a new issue