- plans/ → tasks/plans/ (planning documents) - framework/ → core-gui/docs/framework/ (GUI framework docs) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
6 KiB
6 KiB
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.yamlwith environment variable fallbacks for secrets - GPG scope: Signs
checksums.txtonly (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
// 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:
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
// 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:
gpg --verify checksums.txt.asc checksums.txt
sha256sum -c checksums.txt
macOS Codesign
// 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
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
- Create
pkg/build/signing/package structure - Implement Signer interface and SignConfig
- Implement GPGSigner
- Implement MacOSSigner with codesign
- Add notarization support to MacOSSigner
- Add SignConfig to build.Config
- Integrate signing into build pipeline
- Add CLI flags (--no-sign, --notarize)
- Add Windows placeholder
- Tests with mocked exec
Dependencies
gpgCLI (system)codesignCLI (macOS Xcode Command Line Tools)xcrun notarytool(macOS Xcode Command Line Tools)xcrun stapler(macOS Xcode Command Line Tools)