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