A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
74
fork

Configure Feed

Select the types of activity you want to include in your feed.

1# Image Signing with ATProto 2 3ATCR supports cryptographic signing of container images to ensure authenticity and integrity. Users have two options: 4 51. **Automatic signing (recommended)**: Credential helper signs images automatically on every push 62. **Manual signing**: Use standard Cosign tools yourself 7 8Both approaches use the OCI Referrers API bridge for verification with standard tools (Cosign, Notary, Kubernetes admission controllers). 9 10## Design Constraints 11 12### Why Server-Side Signing Doesn't Work 13 14It's tempting to implement automatic signing on the AppView or hold (like GitHub's automatic Cosign signing), but this breaks the fundamental trust model: 15 16**The problem: Signing "on behalf of" isn't real signing** 17 18``` 19❌ AppView signs image → Proves "AppView vouches for this" 20❌ Hold signs image → Proves "Hold vouches for this" 21❌ PDS signs image → Proves "PDS vouches for this" 22✅ Alice signs image → Proves "Alice created/approved this" 23``` 24 25**Why GitHub can do it:** 26- GitHub Actions runs with your GitHub identity 27- OIDC token proves "this workflow runs as alice on GitHub" 28- Fulcio certificate authority issues cert based on that proof 29- Still "alice" signing, just via GitHub's infrastructure 30 31**Why ATCR can't replicate this:** 32- ATProto doesn't have OIDC/Fulcio equivalent 33- AppView can't sign "as alice" - only alice can 34- No secure server-side storage for user private keys 35 - ATProto doesn't have encrypted record storage yet 36 - Storing keys in AppView database = AppView controls keys, not alice 37- Hold's PDS has its own private key, but signing with it proves hold ownership, not user ownership 38 39**Conclusion:** Signing must happen **client-side with user-controlled keys**. 40 41### Why ATProto Record Signatures Aren't Sufficient 42 43ATProto already signs all records stored in PDSs. When a manifest is stored as an `io.atcr.manifest` record, it includes: 44 45```json 46{ 47 "uri": "at://did:plc:alice123/io.atcr.manifest/abc123", 48 "cid": "bafyrei...", 49 "value": { /* manifest data */ }, 50 "sig": "..." // ← PDS signature over record 51} 52``` 53 54**What this proves:** 55- ✅ Alice's PDS created and signed this record 56- ✅ Record hasn't been tampered with since signing 57- ✅ CID correctly represents the record content 58 59**What this doesn't prove:** 60- ❌ Alice personally approved this image 61- ❌ Alice's private key was involved (only PDS key) 62 63**The gap:** 64- A compromised or malicious PDS could create fake manifest records and sign them validly 65- PDS operator could sign manifests without user's knowledge 66- No proof that the *user* (not just their PDS) approved the image 67 68**For true image signing, we need:** 69- User-controlled private keys (not PDS keys) 70- Client-side signing (where user has key access) 71- Separate signature records proving user approval 72 73**Important nuance - PDS Trust Spectrum:** 74 75While ATProto records are always signed by the PDS, this doesn't provide user-level signing for image verification: 76 771. **Self-hosted PDS with user-controlled keys:** 78 - User runs their own PDS and controls PDS rotation keys 79 - PDS signature ≈ user signature (trusted operator) 80 - Still doesn't work with standard tools (Cosign/Notary) 81 822. **Shared/managed PDS (e.g., Bluesky):** 83 - PDS operated by third party (bsky.social) 84 - Auto-generated keys controlled by operator 85 - User doesn't have access to PDS rotation keys 86 - PDS signature ≠ user signature 87 88**For ATCR:** 89- Credential helper signing works for all users (self-hosted or shared PDS) 90- Provides user-controlled keys separate from PDS keys 91- Works with standard verification tools via OCI Referrers API bridge 92 93## Signing Options 94 95### Option 1: Automatic Signing (Recommended) 96 97The credential helper automatically signs images on every push - no extra commands needed. 98 99**How it works:** 100- Credential helper runs on every `docker push` for authentication 101- Extended to also sign the manifest digest with user's private key 102- Private key stored securely in OS keychain 103- Signature sent to AppView and stored in ATProto 104- Completely transparent to the user 105 106### Architecture 107 108``` 109┌─────────────────────────────────────────────────────┐ 110│ docker push atcr.io/alice/myapp:latest │ 111└────────────────────┬────────────────────────────────┘ 112113┌─────────────────────────────────────────────────────┐ 114│ docker-credential-atcr (runs automatically) │ 115│ │ 116│ 1. Authenticate to AppView (OAuth) │ 117│ 2. Get registry JWT │ 118│ 3. Sign manifest digest with local private key ← NEW 119│ 4. Send signature to AppView ← NEW 120│ │ 121│ Private key stored in OS keychain │ 122│ (macOS Keychain, Windows Credential Manager, etc.) │ 123└────────────────────┬────────────────────────────────┘ 124125┌─────────────────────────────────────────────────────┐ 126│ AppView │ 127│ │ 128│ 1. Receives signature from credential helper │ 129│ 2. Stores in user's PDS (io.atcr.signature) │ 130│ │ 131│ OR stores in hold's PDS for BYOS scenarios │ 132└─────────────────────────────────────────────────────┘ 133``` 134 135**User experience:** 136 137```bash 138# One-time setup 139docker login atcr.io 140# → Credential helper generates ECDSA key pair 141# → Private key stored in OS keychain 142# → Public key published to user's PDS 143 144# Every push (automatic signing) 145docker push atcr.io/alice/myapp:latest 146# → Image pushed 147# → Automatically signed by credential helper 148# → No extra commands! 149 150# Verification (standard Cosign) 151cosign verify atcr.io/alice/myapp:latest --key alice.pub 152``` 153 154### Option 2: Manual Signing (DIY) 155 156Use standard Cosign tools yourself if you prefer manual control. 157 158**How it works:** 159- You manage your own signing keys 160- You run `cosign sign` manually after pushing 161- Signatures stored in ATProto via OCI Referrers API 162- Full control over signing workflow 163 164**User experience:** 165 166```bash 167# Push image 168docker push atcr.io/alice/myapp:latest 169 170# Sign manually with Cosign 171cosign sign atcr.io/alice/myapp:latest --key cosign.key 172 173# Cosign stores signature via registry's OCI API 174# AppView receives signature and stores in ATProto 175 176# Verification (same as automatic) 177cosign verify atcr.io/alice/myapp:latest --key cosign.pub 178``` 179 180**When to use:** 181- Need specific signing workflows (e.g., CI/CD pipelines) 182- Want to use hardware tokens (YubiKey) 183- Prefer manual control over automatic signing 184- Already using Cosign in your organization 185 186### Key Management 187 188**Key generation (first run):** 1891. Credential helper checks for existing signing key in OS keychain 1902. If not found, generates new ECDSA P-256 key pair (or Ed25519) 1913. Stores private key in OS keychain with access control 1924. Derives public key for publishing 193 194**Public key publishing:** 195```json 196{ 197 "$type": "io.atcr.signing.key", 198 "keyId": "credential-helper-default", 199 "keyType": "ecdsa-p256", 200 "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkw...", 201 "validFrom": "2025-10-20T12:00:00Z", 202 "expiresAt": null, 203 "revoked": false, 204 "purpose": ["image-signing"], 205 "deviceId": "alice-macbook-pro", 206 "createdAt": "2025-10-20T12:00:00Z" 207} 208``` 209 210**Record stored in:** User's PDS at `io.atcr.signing.key/credential-helper-default` 211 212**Key storage locations:** 213- **macOS:** Keychain Access (secure enclave on modern Macs) 214- **Windows:** Credential Manager / Windows Data Protection API 215- **Linux:** Secret Service API (gnome-keyring, kwallet) 216- **Fallback:** Encrypted file with restrictive permissions (0600) 217 218### Signing Flow 219 220``` 2211. docker push atcr.io/alice/myapp:latest 2222232. Docker daemon calls credential helper: 224 docker-credential-atcr get atcr.io 2252263. Credential helper flow: 227 a. Authenticate via OAuth (existing) 228 b. Receive registry JWT from AppView (existing) 229 c. Fetch manifest digest from registry (NEW) 230 d. Load private key from OS keychain (NEW) 231 e. Sign manifest digest (NEW) 232 f. Send signature to AppView via XRPC (NEW) 2332344. AppView stores signature: 235 { 236 "$type": "io.atcr.signature", 237 "repository": "alice/myapp", 238 "digest": "sha256:abc123...", 239 "signature": "MEUCIQDx...", 240 "keyId": "credential-helper-default", 241 "signatureAlgorithm": "ecdsa-p256-sha256", 242 "signedAt": "2025-10-20T12:34:56Z" 243 } 2442455. Return registry JWT to Docker 2462476. Docker proceeds with push 248``` 249 250### Signature Storage 251 252**Option 1: User's PDS (Default)** 253- Signature stored in alice's PDS 254- Collection: `io.atcr.signature` 255- Discoverable via alice's ATProto repo 256- User owns all signing metadata 257 258**Option 2: Hold's PDS (BYOS)** 259- Signature stored in hold's embedded PDS 260- Useful for shared holds with multiple users 261- Hold acts as signature repository 262- Parallel to SBOM storage model 263 264**Decision logic:** 265```go 266// In AppView signature handler 267if manifest.HoldDid != "" && manifest.HoldDid != appview.DefaultHoldDid { 268 // BYOS scenario - store in hold's PDS 269 storeSignatureInHold(manifest.HoldDid, signature) 270} else { 271 // Default - store in user's PDS 272 storeSignatureInUserPDS(userDid, signature) 273} 274``` 275 276## Signature Format 277 278Signatures are stored in a simple format in ATProto and transformed to Cosign-compatible format when served via the OCI Referrers API: 279 280**ATProto storage format:** 281```json 282{ 283 "$type": "io.atcr.signature", 284 "repository": "alice/myapp", 285 "digest": "sha256:abc123...", 286 "signature": "base64-encoded-signature-bytes", 287 "keyId": "credential-helper-default", 288 "signatureAlgorithm": "ecdsa-p256-sha256", 289 "signedAt": "2025-10-20T12:34:56Z", 290 "format": "simple" 291} 292``` 293 294**OCI Referrers format (served by AppView):** 295```json 296{ 297 "schemaVersion": 2, 298 "mediaType": "application/vnd.oci.image.index.v1+json", 299 "manifests": [{ 300 "mediaType": "application/vnd.oci.image.manifest.v1+json", 301 "digest": "sha256:...", 302 "artifactType": "application/vnd.dev.cosign.simplesigning.v1+json", 303 "annotations": { 304 "dev.sigstore.cosign.signature": "MEUCIQDx...", 305 "io.atcr.keyId": "credential-helper-default", 306 "io.atcr.signedAt": "2025-10-20T12:34:56Z" 307 } 308 }] 309} 310``` 311 312This allows: 313- Simple storage in ATProto 314- Compatible with Cosign verification 315- No duplicate storage needed 316 317## ATProto Records 318 319### io.atcr.signing.key - Public Signing Keys 320 321```json 322{ 323 "$type": "io.atcr.signing.key", 324 "keyId": "credential-helper-default", 325 "keyType": "ecdsa-p256", 326 "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZI...", 327 "validFrom": "2025-10-20T12:00:00Z", 328 "expiresAt": "2026-10-20T12:00:00Z", 329 "revoked": false, 330 "purpose": ["image-signing"], 331 "deviceId": "alice-macbook-pro", 332 "comment": "Generated by docker-credential-atcr", 333 "createdAt": "2025-10-20T12:00:00Z" 334} 335``` 336 337**Record key:** `keyId` (user-chosen identifier) 338 339**Fields:** 340- `keyId`: Unique identifier (e.g., `credential-helper-default`, `ci-key-1`) 341- `keyType`: Algorithm (ecdsa-p256, ed25519, rsa-2048, rsa-4096) 342- `publicKey`: PEM-encoded public key 343- `validFrom`: Key becomes valid at this time 344- `expiresAt`: Key expires (null = no expiry) 345- `revoked`: Revocation status 346- `purpose`: Key purposes (image-signing, sbom-signing, etc.) 347- `deviceId`: Optional device identifier 348- `comment`: Optional human-readable comment 349 350### io.atcr.signature - Image Signatures 351 352```json 353{ 354 "$type": "io.atcr.signature", 355 "repository": "alice/myapp", 356 "digest": "sha256:abc123...", 357 "signature": "MEUCIQDxH7...", 358 "keyId": "credential-helper-default", 359 "signatureAlgorithm": "ecdsa-p256-sha256", 360 "signedAt": "2025-10-20T12:34:56Z", 361 "format": "simple", 362 "createdAt": "2025-10-20T12:34:56Z" 363} 364``` 365 366**Record key:** SHA256 hash of `(digest || keyId)` for deduplication 367 368**Fields:** 369- `repository`: Image repository (alice/myapp) 370- `digest`: Manifest digest being signed (sha256:...) 371- `signature`: Base64-encoded signature bytes 372- `keyId`: Reference to signing key record 373- `signatureAlgorithm`: Algorithm used 374- `signedAt`: Timestamp of signature creation 375- `format`: Signature format (simple, cosign, notary) 376 377## Verification 378 379Image signatures are verified using standard tools (Cosign, Notary) via the OCI Referrers API bridge. AppView transparently serves ATProto signatures as OCI artifacts, so verification "just works" with existing tooling. 380 381### Integration with Docker/Kubernetes Workflows 382 383**The challenge:** Cosign and Notary plugins are for **key management** (custom KMS, HSMs), not **signature storage**. Both tools expect signatures stored as OCI artifacts in the registry itself. 384 385**Reality check:** 386- Cosign looks for signatures as OCI referrers or attached manifests 387- Notary looks for signatures in registry's `_notary` endpoint 388- Kubernetes admission controllers (Sigstore Policy Controller, Ratify) use these tools 389- They won't find signatures stored only in ATProto 390 391**The solution:** AppView implements the **OCI Referrers API** and serves ATProto signatures as OCI artifacts on-demand. 392 393### How It Works: OCI Referrers API Bridge 394 395When Cosign/Notary verify an image, they call the OCI Referrers API: 396 397``` 398cosign verify atcr.io/alice/myapp:latest 399400GET /v2/alice/myapp/referrers/sha256:abc123 401402AppView: 403 1. Queries alice's PDS for io.atcr.signature records 404 2. Filters signatures matching digest sha256:abc123 405 3. Transforms to OCI referrers format 406 4. Returns as JSON 407408Cosign receives OCI referrer manifest 409410Verifies signature (works normally) 411``` 412 413**AppView endpoint implementation:** 414 415```go 416// GET /v2/{owner}/{repo}/referrers/{digest} 417func (h *Handler) GetReferrers(w http.ResponseWriter, r *http.Request) { 418 owner := mux.Vars(r)["owner"] 419 digest := mux.Vars(r)["digest"] 420 421 // 1. Resolve owner → DID → PDS 422 did, pds, err := h.resolver.ResolveIdentity(owner) 423 424 // 2. Query PDS for signatures matching digest 425 signatures, err := h.atproto.ListRecords(pds, did, "io.atcr.signature") 426 filtered := filterByDigest(signatures, digest) 427 428 // 3. Transform to OCI Index format 429 index := &ocispec.Index{ 430 SchemaVersion: 2, 431 MediaType: ocispec.MediaTypeImageIndex, 432 Manifests: []ocispec.Descriptor{}, 433 } 434 435 for _, sig := range filtered { 436 index.Manifests = append(index.Manifests, ocispec.Descriptor{ 437 MediaType: "application/vnd.oci.image.manifest.v1+json", 438 Digest: sig.Digest, 439 Size: sig.Size, 440 ArtifactType: "application/vnd.dev.cosign.simplesigning.v1+json", 441 Annotations: map[string]string{ 442 "dev.sigstore.cosign.signature": sig.Signature, 443 "io.atcr.keyId": sig.KeyId, 444 "io.atcr.signedAt": sig.SignedAt, 445 "io.atcr.source": fmt.Sprintf("at://%s/io.atcr.signature/%s", did, sig.Rkey), 446 }, 447 }) 448 } 449 450 // 4. Return as JSON 451 w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex) 452 json.NewEncoder(w).Encode(index) 453} 454``` 455 456**Benefits:** 457-**No dual storage** - signatures only in ATProto 458-**Standard tools work** - Cosign, Notary, Kubernetes admission controllers 459-**Single source of truth** - ATProto PDS 460-**On-demand transformation** - only when needed 461-**Offline verification** - can cache public keys 462 463**Trade-offs:** 464- ⚠️ AppView must be reachable during verification (but already required for image pulls) 465- ⚠️ Transformation overhead (minimal - just JSON formatting) 466 467### Alternative Approaches 468 469#### Option 1: Dual Storage (Not Recommended) 470 471Store signatures in BOTH ATProto AND OCI registry: 472 473```go 474// In credential helper or AppView 475func StoreSignature(sig Signature) error { 476 // 1. Store in ATProto (user's PDS or hold's PDS) 477 err := storeInATProto(sig) 478 479 // 2. ALSO store as OCI artifact in registry 480 err = storeAsOCIReferrer(sig) 481 482 return err 483} 484``` 485 486**OCI Referrer format:** 487```json 488{ 489 "schemaVersion": 2, 490 "mediaType": "application/vnd.oci.image.manifest.v1+json", 491 "artifactType": "application/vnd.dev.cosign.simplesigning.v1+json", 492 "subject": { 493 "digest": "sha256:abc123...", 494 "mediaType": "application/vnd.oci.image.manifest.v1+json" 495 }, 496 "layers": [{ 497 "mediaType": "application/vnd.dev.cosign.simplesigning.v1+json", 498 "digest": "sha256:sig...", 499 "annotations": { 500 "dev.sigstore.cosign.signature": "MEUCIQDx...", 501 "io.atcr.source": "atproto://did:plc:alice123/io.atcr.signature/..." 502 } 503 }] 504} 505``` 506 507**Benefits:** 508- ✅ Works with standard Cosign verification 509- ✅ Kubernetes admission controllers work out of box 510- ✅ ATProto signatures still available for discovery 511- ✅ Cross-reference via `io.atcr.source` annotation 512 513**Trade-offs:** 514- ❌ Duplicate storage (ATProto + OCI) 515- ❌ Consistency issues (what if one write fails?) 516- ❌ Signatures tied to specific registry 517 518#### Option 2: Custom Admission Controller 519 520Write Kubernetes admission controller that understands ATProto: 521 522```yaml 523# admission-controller deployment 524apiVersion: v1 525kind: ConfigMap 526metadata: 527 name: atcr-policy 528data: 529 policy.yaml: | 530 policies: 531 - name: require-atcr-signatures 532 images: 533 - "atcr.io/*/*" 534 verification: 535 method: atproto 536 requireSignature: true 537``` 538 539**Benefits:** 540- ✅ Native ATProto support 541- ✅ No OCI conversion needed 542- ✅ Can enforce ATCR-specific policies 543 544**Trade-offs:** 545- ❌ Doesn't work with standard tools (Cosign, Notary) 546- ❌ Additional infrastructure to maintain 547- ❌ Limited ecosystem integration 548 549#### Recommendation 550 551**Primary approach: OCI Referrers API Bridge** 552- Implement `/v2/{owner}/{repo}/referrers/{digest}` in AppView 553- Query ATProto on-demand and transform to OCI format 554- Works with Cosign, Notary, Kubernetes admission controllers 555- No duplicate storage, single source of truth 556 557**Why this works:** 558- Cosign/Notary just make HTTP GET requests to the registry 559- AppView is already the registry - just add one endpoint 560- Transformation is simple (ATProto record → OCI descriptor) 561- Signatures stay in ATProto where they belong 562 563### Cosign Verification (OCI Referrers API) 564 565```bash 566# Standard Cosign works out of the box: 567cosign verify atcr.io/alice/myapp:latest \ 568 --key <(atcr-cli key export alice credential-helper-default) 569 570# What happens: 571# 1. Cosign queries: GET /v2/alice/myapp/referrers/sha256:abc123 572# 2. AppView fetches signatures from alice's PDS 573# 3. AppView returns OCI referrers index 574# 4. Cosign downloads signature artifact 575# 5. Cosign verifies with public key 576# 6. Success! 577 578# Or with public key inline: 579cosign verify atcr.io/alice/myapp:latest --key '-----BEGIN PUBLIC KEY----- 580MFkwEwYHKoZI... 581-----END PUBLIC KEY-----' 582``` 583 584**Fetching public keys from ATProto:** 585 586Public keys are stored in ATProto records and can be fetched via standard XRPC: 587 588```bash 589# Query for public keys 590curl "https://atcr.io/xrpc/com.atproto.repo.listRecords?\ 591 repo=did:plc:alice123&\ 592 collection=io.atcr.signing.key" 593 594# Extract public key and save as PEM 595# Then use in Cosign: 596cosign verify atcr.io/alice/myapp:latest --key alice.pub 597``` 598 599### Kubernetes Policy Example (OCI Referrers API) 600 601```yaml 602# Sigstore Policy Controller 603apiVersion: policy.sigstore.dev/v1beta1 604kind: ClusterImagePolicy 605metadata: 606 name: atcr-images-must-be-signed 607spec: 608 images: 609 - glob: "atcr.io/*/*" 610 authorities: 611 - key: 612 # Public key from ATProto record 613 data: | 614 -----BEGIN PUBLIC KEY----- 615 MFkwEwYHKoZI... 616 -----END PUBLIC KEY----- 617``` 618 619**How it works:** 6201. Pod tries to run `atcr.io/alice/myapp:latest` 6212. Policy Controller intercepts 6223. Queries registry for OCI referrers (finds signature) 6234. Verifies signature with public key 6245. Allows pod if valid 625 626### Trust Policies 627 628Define what signatures are required for image execution: 629 630```yaml 631# ~/.atcr/trust-policy.yaml 632policies: 633 - name: production-images 634 scope: "atcr.io/alice/prod-*" 635 require: 636 - signature: true 637 - keyIds: ["ci-key-1", "alice-release-key"] 638 action: enforce # block, audit, or allow 639 640 - name: dev-images 641 scope: "atcr.io/alice/dev-*" 642 require: 643 - signature: false 644 action: audit 645``` 646 647**Integration points:** 648- Kubernetes admission controller 649- Docker Content Trust equivalent 650- CI/CD pipeline gates 651 652## Security Considerations 653 654### Key Storage Security 655 656**OS keychain benefits:** 657- ✅ Encrypted storage 658- ✅ Access control (requires user password/biometric) 659- ✅ Auditing (macOS logs keychain access) 660- ✅ Hardware-backed on modern systems (Secure Enclave, TPM) 661 662**Best practices:** 663- Generate keys on device (never transmitted) 664- Use hardware-backed storage when available 665- Require user approval for key access (biometric/password) 666- Rotate keys periodically (e.g., annually) 667 668### Trust Model 669 670**What signatures prove:** 671- ✅ User had access to private key at signing time 672- ✅ Manifest digest matches what was signed 673- ✅ Signature created by specific key ID 674- ✅ Timestamp of signature creation 675 676**What signatures don't prove:** 677- ❌ Image is free of vulnerabilities 678- ❌ Image contents are safe to run 679- ❌ User's identity is verified (depends on DID trust) 680- ❌ Private key wasn't compromised 681 682**Trust dependencies:** 683- User protects their private key 684- OS keychain security 685- DID resolution accuracy (PLC directory, did:web) 686- PDS serves correct public key records 687- Signature algorithms remain secure 688 689### Multi-Device Support 690 691**Challenge:** User has multiple devices (laptop, desktop, CI/CD) 692 693**Options:** 694 6951. **Separate keys per device:** 696 ```json 697 { 698 "keyId": "alice-macbook-pro", 699 "deviceId": "macbook-pro" 700 }, 701 { 702 "keyId": "alice-desktop", 703 "deviceId": "desktop" 704 } 705 ``` 706 - Pros: Best security (key compromise limited to one device) 707 - Cons: Need to trust signatures from any device 708 7092. **Shared key via secure sync:** 710 - Export key from primary device 711 - Import to secondary devices 712 - Stored in each device's keychain 713 - Pros: Single key ID to trust 714 - Cons: More attack surface (key on multiple devices) 715 7163. **Primary + secondary model:** 717 - Primary key on main device 718 - Secondary keys on other devices 719 - Trust policy requires primary key signature 720 - Pros: Flexible + secure 721 - Cons: More complex setup 722 723**Recommendation:** Separate keys per device (Option 1) for security, with trust policy accepting any of user's keys. 724 725### Key Compromise Response 726 727If a device is lost or private key is compromised: 728 7291. **Revoke the key** via AppView web UI or XRPC API 730 - Updates `io.atcr.signing.key` record: `"revoked": true` 731 - Revocation is atomic and immediate 732 7332. **Generate new key** on new/existing device 734 - Automatic on next `docker login` from secure device 735 - Credential helper generates new key pair 736 7373. **Old signatures still exist but fail verification** 738 - Revoked key = untrusted 739 - No certificate revocation list (CRL) delays 740 - Globally visible within seconds 741 742### CI/CD Signing 743 744For automated builds, use standard Cosign in your CI pipeline: 745 746```yaml 747# .github/workflows/build.yml 748steps: 749 - name: Push image 750 run: docker push atcr.io/alice/myapp:latest 751 752 - name: Sign with Cosign 753 run: cosign sign atcr.io/alice/myapp:latest --key ${{ secrets.COSIGN_KEY }} 754``` 755 756**Key management:** 757- Generate Cosign key pair: `cosign generate-key-pair` 758- Store private key in CI secrets (GitHub Actions, GitLab CI, etc.) 759- Publish public key to PDS via XRPC or AppView web UI 760- Cosign stores signature via registry's OCI API 761- AppView automatically stores in ATProto 762 763**Or use automatic signing:** 764- Configure credential helper in CI environment 765- Signatures happen automatically on push 766- No explicit signing step needed 767 768## Implementation Roadmap 769 770### Phase 1: Core Signing (2-3 weeks) 771 772**Week 1: Credential helper key management** 773- Generate ECDSA key pair on first run 774- Store private key in OS keychain 775- Create `io.atcr.signing.key` record in PDS 776- Handle key rotation 777 778**Week 2: Signing integration** 779- Sign manifest digest after authentication 780- Send signature to AppView via XRPC 781- AppView stores in user's PDS or hold's PDS 782- Error handling and retries 783 784**Week 3: OCI Referrers API** 785- Implement `GET /v2/{owner}/{repo}/referrers/{digest}` in AppView 786- Query ATProto for signatures 787- Transform to OCI Index format 788- Return Cosign-compatible artifacts 789- Test with `cosign verify` 790 791### Phase 2: Enhanced Features (2-3 weeks) 792 793**Key management (credential helper):** 794- Key rotation support 795- Revocation handling 796- Device identification 797- Key expiration 798 799**Signature storage:** 800- Handle manual Cosign signing (via OCI API) 801- Store signatures from both automatic and manual flows 802- Signature deduplication 803- Signature audit logs 804 805**AppView endpoints:** 806- XRPC endpoints for key/signature queries 807- Web UI for viewing keys and signatures 808- Key revocation via web interface 809 810### Phase 3: Kubernetes Integration (2-3 weeks) 811 812**Admission controller setup:** 813- Documentation for Sigstore Policy Controller 814- Example policies for ATCR images 815- Public key management (fetch from ATProto) 816- Integration testing with real clusters 817 818**Advanced features:** 819- Signature caching in AppView (reduce PDS queries) 820- Multi-signature support (require N signatures) 821- Timestamp verification 822- Signature expiration policies 823 824### Phase 4: UI Integration (1-2 weeks) 825 826**AppView web UI:** 827- Show signature status on repository pages 828- List signing keys for users 829- Revoke keys via web interface 830- Signature verification badges 831 832## Comparison: Automatic vs Manual Signing 833 834| Feature | Automatic (Credential Helper) | Manual (Standard Cosign) | 835|---------|-------------------------------|--------------------------| 836| **User action** | Zero - happens on push | `cosign sign` after push | 837| **Key management** | Automatic generation/storage | User manages keys | 838| **Consistency** | Every image signed | Easy to forget | 839| **Setup** | Works with credential helper | Install Cosign, generate keys | 840| **CI/CD** | Automatic if cred helper configured | Explicit signing step | 841| **Flexibility** | Opinionated defaults | Full control over workflow | 842| **Use case** | Most users, simple workflows | Advanced users, custom workflows | 843 844**Recommendation:** 845- **Start with automatic**: Best UX, works for most users 846- **Use manual** for: CI/CD pipelines, hardware tokens, custom signing workflows 847 848## Complete Workflow Summary 849 850### Option 1: Automatic Signing (Recommended) 851 852```bash 853# Setup (one time) 854docker login atcr.io 855# → Credential helper generates ECDSA key pair 856# → Private key in OS keychain 857# → Public key published to PDS 858 859# Push (automatic signing) 860docker push atcr.io/alice/myapp:latest 861# → Image pushed and signed automatically 862# → No extra commands! 863 864# Verify (standard Cosign) 865cosign verify atcr.io/alice/myapp:latest --key alice.pub 866# → Cosign queries OCI Referrers API 867# → AppView returns ATProto signatures as OCI artifacts 868# → Verification succeeds ✓ 869``` 870 871### Option 2: Manual Signing (DIY) 872 873```bash 874# Push image 875docker push atcr.io/alice/myapp:latest 876 877# Sign with Cosign 878cosign sign atcr.io/alice/myapp:latest --key cosign.key 879# → Cosign stores via OCI API 880# → AppView stores in ATProto 881 882# Verify (same as automatic) 883cosign verify atcr.io/alice/myapp:latest --key cosign.pub 884``` 885 886### Kubernetes (Standard Admission Controller) 887 888```yaml 889# Sigstore Policy Controller (standard) 890apiVersion: policy.sigstore.dev/v1beta1 891kind: ClusterImagePolicy 892metadata: 893 name: atcr-signed-only 894spec: 895 images: 896 - glob: "atcr.io/*/*" 897 authorities: 898 - key: 899 data: | 900 -----BEGIN PUBLIC KEY----- 901 [Alice's public key from ATProto] 902 -----END PUBLIC KEY----- 903``` 904 905**How admission control works:** 9061. Pod tries to start with `atcr.io/alice/myapp:latest` 9072. Policy Controller intercepts 9083. Calls `GET /v2/alice/myapp/referrers/sha256:abc123` 9094. AppView returns signatures from ATProto 9105. Policy Controller verifies with public key 9116. Pod allowed to start ✓ 912 913### Key Design Points 914 915**User experience:** 916- ✅ Two options: automatic (credential helper) or manual (standard Cosign) 917- ✅ Standard verification tools work (Cosign, Notary, Kubernetes) 918- ✅ No custom ATCR-specific signing commands 919- ✅ User-controlled keys (OS keychain or self-managed) 920 921**Architecture:** 922- **Signing**: Client-side only (credential helper or Cosign) 923- **Storage**: ATProto (user's PDS or hold's PDS via `io.atcr.signature`) 924- **Verification**: Standard tools via OCI Referrers API bridge 925- **Bridge**: AppView transforms ATProto → OCI format on-demand 926 927**Why this works:** 928- ✅ No server-side signing needed (impossible with ATProto constraints) 929- ✅ Signatures discoverable via ATProto 930- ✅ No duplicate storage (single source of truth) 931- ✅ Standard OCI compliance for verification 932 933## References 934 935### Signing & Verification 936- [Sigstore Cosign](https://github.com/sigstore/cosign) 937- [Notary v2 Specification](https://notaryproject.dev/) 938- [Cosign Signature Specification](https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md) 939 940### OCI & Registry 941- [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec) 942- [OCI Referrers API](https://github.com/oras-project/artifacts-spec/blob/main/manifest-referrers-api.md) 943- [OCI Artifacts](https://github.com/opencontainers/artifacts) 944 945### ATProto 946- [ATProto Specification](https://atproto.com/) 947- [ATProto Repository Specification](https://atproto.com/specs/repository) 948 949### Key Management 950- [Docker Credential Helpers](https://docs.docker.com/engine/reference/commandline/login/#credential-helpers) 951- [macOS Keychain Services](https://developer.apple.com/documentation/security/keychain_services) 952- [Windows Credential Manager](https://docs.microsoft.com/en-us/windows/security/identity-protection/credential-guard/) 953- [Linux Secret Service API](https://specifications.freedesktop.org/secret-service/) 954 955### Kubernetes Integration 956- [Sigstore Policy Controller](https://docs.sigstore.dev/policy-controller/overview/) 957- [Ratify (Notary verification for Kubernetes)](https://ratify.dev/)