# Image Signing with ATProto ATCR supports cryptographic signing of container images to ensure authenticity and integrity. Users have two options: 1. **Automatic signing (recommended)**: Credential helper signs images automatically on every push 2. **Manual signing**: Use standard Cosign tools yourself Both approaches use the OCI Referrers API bridge for verification with standard tools (Cosign, Notary, Kubernetes admission controllers). ## Design Constraints ### Why Server-Side Signing Doesn't Work It's tempting to implement automatic signing on the AppView or hold (like GitHub's automatic Cosign signing), but this breaks the fundamental trust model: **The problem: Signing "on behalf of" isn't real signing** ``` ❌ AppView signs image → Proves "AppView vouches for this" ❌ Hold signs image → Proves "Hold vouches for this" ❌ PDS signs image → Proves "PDS vouches for this" ✅ Alice signs image → Proves "Alice created/approved this" ``` **Why GitHub can do it:** - GitHub Actions runs with your GitHub identity - OIDC token proves "this workflow runs as alice on GitHub" - Fulcio certificate authority issues cert based on that proof - Still "alice" signing, just via GitHub's infrastructure **Why ATCR can't replicate this:** - ATProto doesn't have OIDC/Fulcio equivalent - AppView can't sign "as alice" - only alice can - No secure server-side storage for user private keys - ATProto doesn't have encrypted record storage yet - Storing keys in AppView database = AppView controls keys, not alice - Hold's PDS has its own private key, but signing with it proves hold ownership, not user ownership **Conclusion:** Signing must happen **client-side with user-controlled keys**. ### Why ATProto Record Signatures Aren't Sufficient ATProto already signs all records stored in PDSs. When a manifest is stored as an `io.atcr.manifest` record, it includes: ```json { "uri": "at://did:plc:alice123/io.atcr.manifest/abc123", "cid": "bafyrei...", "value": { /* manifest data */ }, "sig": "..." // ← PDS signature over record } ``` **What this proves:** - ✅ Alice's PDS created and signed this record - ✅ Record hasn't been tampered with since signing - ✅ CID correctly represents the record content **What this doesn't prove:** - ❌ Alice personally approved this image - ❌ Alice's private key was involved (only PDS key) **The gap:** - A compromised or malicious PDS could create fake manifest records and sign them validly - PDS operator could sign manifests without user's knowledge - No proof that the *user* (not just their PDS) approved the image **For true image signing, we need:** - User-controlled private keys (not PDS keys) - Client-side signing (where user has key access) - Separate signature records proving user approval **Important nuance - PDS Trust Spectrum:** While ATProto records are always signed by the PDS, this doesn't provide user-level signing for image verification: 1. **Self-hosted PDS with user-controlled keys:** - User runs their own PDS and controls PDS rotation keys - PDS signature ≈ user signature (trusted operator) - Still doesn't work with standard tools (Cosign/Notary) 2. **Shared/managed PDS (e.g., Bluesky):** - PDS operated by third party (bsky.social) - Auto-generated keys controlled by operator - User doesn't have access to PDS rotation keys - PDS signature ≠ user signature **For ATCR:** - Credential helper signing works for all users (self-hosted or shared PDS) - Provides user-controlled keys separate from PDS keys - Works with standard verification tools via OCI Referrers API bridge ## Signing Options ### Option 1: Automatic Signing (Recommended) The credential helper automatically signs images on every push - no extra commands needed. **How it works:** - Credential helper runs on every `docker push` for authentication - Extended to also sign the manifest digest with user's private key - Private key stored securely in OS keychain - Signature sent to AppView and stored in ATProto - Completely transparent to the user ### Architecture ``` ┌─────────────────────────────────────────────────────┐ │ docker push atcr.io/alice/myapp:latest │ └────────────────────┬────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────┐ │ docker-credential-atcr (runs automatically) │ │ │ │ 1. Authenticate to AppView (OAuth) │ │ 2. Get registry JWT │ │ 3. Sign manifest digest with local private key ← NEW │ 4. Send signature to AppView ← NEW │ │ │ Private key stored in OS keychain │ │ (macOS Keychain, Windows Credential Manager, etc.) │ └────────────────────┬────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────┐ │ AppView │ │ │ │ 1. Receives signature from credential helper │ │ 2. Stores in user's PDS (io.atcr.signature) │ │ │ │ OR stores in hold's PDS for BYOS scenarios │ └─────────────────────────────────────────────────────┘ ``` **User experience:** ```bash # One-time setup docker login atcr.io # → Credential helper generates ECDSA key pair # → Private key stored in OS keychain # → Public key published to user's PDS # Every push (automatic signing) docker push atcr.io/alice/myapp:latest # → Image pushed # → Automatically signed by credential helper # → No extra commands! # Verification (standard Cosign) cosign verify atcr.io/alice/myapp:latest --key alice.pub ``` ### Option 2: Manual Signing (DIY) Use standard Cosign tools yourself if you prefer manual control. **How it works:** - You manage your own signing keys - You run `cosign sign` manually after pushing - Signatures stored in ATProto via OCI Referrers API - Full control over signing workflow **User experience:** ```bash # Push image docker push atcr.io/alice/myapp:latest # Sign manually with Cosign cosign sign atcr.io/alice/myapp:latest --key cosign.key # Cosign stores signature via registry's OCI API # AppView receives signature and stores in ATProto # Verification (same as automatic) cosign verify atcr.io/alice/myapp:latest --key cosign.pub ``` **When to use:** - Need specific signing workflows (e.g., CI/CD pipelines) - Want to use hardware tokens (YubiKey) - Prefer manual control over automatic signing - Already using Cosign in your organization ### Key Management **Key generation (first run):** 1. Credential helper checks for existing signing key in OS keychain 2. If not found, generates new ECDSA P-256 key pair (or Ed25519) 3. Stores private key in OS keychain with access control 4. Derives public key for publishing **Public key publishing:** ```json { "$type": "io.atcr.signing.key", "keyId": "credential-helper-default", "keyType": "ecdsa-p256", "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkw...", "validFrom": "2025-10-20T12:00:00Z", "expiresAt": null, "revoked": false, "purpose": ["image-signing"], "deviceId": "alice-macbook-pro", "createdAt": "2025-10-20T12:00:00Z" } ``` **Record stored in:** User's PDS at `io.atcr.signing.key/credential-helper-default` **Key storage locations:** - **macOS:** Keychain Access (secure enclave on modern Macs) - **Windows:** Credential Manager / Windows Data Protection API - **Linux:** Secret Service API (gnome-keyring, kwallet) - **Fallback:** Encrypted file with restrictive permissions (0600) ### Signing Flow ``` 1. docker push atcr.io/alice/myapp:latest ↓ 2. Docker daemon calls credential helper: docker-credential-atcr get atcr.io ↓ 3. Credential helper flow: a. Authenticate via OAuth (existing) b. Receive registry JWT from AppView (existing) c. Fetch manifest digest from registry (NEW) d. Load private key from OS keychain (NEW) e. Sign manifest digest (NEW) f. Send signature to AppView via XRPC (NEW) ↓ 4. AppView stores signature: { "$type": "io.atcr.signature", "repository": "alice/myapp", "digest": "sha256:abc123...", "signature": "MEUCIQDx...", "keyId": "credential-helper-default", "signatureAlgorithm": "ecdsa-p256-sha256", "signedAt": "2025-10-20T12:34:56Z" } ↓ 5. Return registry JWT to Docker ↓ 6. Docker proceeds with push ``` ### Signature Storage **Option 1: User's PDS (Default)** - Signature stored in alice's PDS - Collection: `io.atcr.signature` - Discoverable via alice's ATProto repo - User owns all signing metadata **Option 2: Hold's PDS (BYOS)** - Signature stored in hold's embedded PDS - Useful for shared holds with multiple users - Hold acts as signature repository - Parallel to SBOM storage model **Decision logic:** ```go // In AppView signature handler if manifest.HoldDid != "" && manifest.HoldDid != appview.DefaultHoldDid { // BYOS scenario - store in hold's PDS storeSignatureInHold(manifest.HoldDid, signature) } else { // Default - store in user's PDS storeSignatureInUserPDS(userDid, signature) } ``` ## Signature Format Signatures are stored in a simple format in ATProto and transformed to Cosign-compatible format when served via the OCI Referrers API: **ATProto storage format:** ```json { "$type": "io.atcr.signature", "repository": "alice/myapp", "digest": "sha256:abc123...", "signature": "base64-encoded-signature-bytes", "keyId": "credential-helper-default", "signatureAlgorithm": "ecdsa-p256-sha256", "signedAt": "2025-10-20T12:34:56Z", "format": "simple" } ``` **OCI Referrers format (served by AppView):** ```json { "schemaVersion": 2, "mediaType": "application/vnd.oci.image.index.v1+json", "manifests": [{ "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "sha256:...", "artifactType": "application/vnd.dev.cosign.simplesigning.v1+json", "annotations": { "dev.sigstore.cosign.signature": "MEUCIQDx...", "io.atcr.keyId": "credential-helper-default", "io.atcr.signedAt": "2025-10-20T12:34:56Z" } }] } ``` This allows: - Simple storage in ATProto - Compatible with Cosign verification - No duplicate storage needed ## ATProto Records ### io.atcr.signing.key - Public Signing Keys ```json { "$type": "io.atcr.signing.key", "keyId": "credential-helper-default", "keyType": "ecdsa-p256", "publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZI...", "validFrom": "2025-10-20T12:00:00Z", "expiresAt": "2026-10-20T12:00:00Z", "revoked": false, "purpose": ["image-signing"], "deviceId": "alice-macbook-pro", "comment": "Generated by docker-credential-atcr", "createdAt": "2025-10-20T12:00:00Z" } ``` **Record key:** `keyId` (user-chosen identifier) **Fields:** - `keyId`: Unique identifier (e.g., `credential-helper-default`, `ci-key-1`) - `keyType`: Algorithm (ecdsa-p256, ed25519, rsa-2048, rsa-4096) - `publicKey`: PEM-encoded public key - `validFrom`: Key becomes valid at this time - `expiresAt`: Key expires (null = no expiry) - `revoked`: Revocation status - `purpose`: Key purposes (image-signing, sbom-signing, etc.) - `deviceId`: Optional device identifier - `comment`: Optional human-readable comment ### io.atcr.signature - Image Signatures ```json { "$type": "io.atcr.signature", "repository": "alice/myapp", "digest": "sha256:abc123...", "signature": "MEUCIQDxH7...", "keyId": "credential-helper-default", "signatureAlgorithm": "ecdsa-p256-sha256", "signedAt": "2025-10-20T12:34:56Z", "format": "simple", "createdAt": "2025-10-20T12:34:56Z" } ``` **Record key:** SHA256 hash of `(digest || keyId)` for deduplication **Fields:** - `repository`: Image repository (alice/myapp) - `digest`: Manifest digest being signed (sha256:...) - `signature`: Base64-encoded signature bytes - `keyId`: Reference to signing key record - `signatureAlgorithm`: Algorithm used - `signedAt`: Timestamp of signature creation - `format`: Signature format (simple, cosign, notary) ## Verification Image 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. ### Integration with Docker/Kubernetes Workflows **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. **Reality check:** - Cosign looks for signatures as OCI referrers or attached manifests - Notary looks for signatures in registry's `_notary` endpoint - Kubernetes admission controllers (Sigstore Policy Controller, Ratify) use these tools - They won't find signatures stored only in ATProto **The solution:** AppView implements the **OCI Referrers API** and serves ATProto signatures as OCI artifacts on-demand. ### How It Works: OCI Referrers API Bridge When Cosign/Notary verify an image, they call the OCI Referrers API: ``` cosign verify atcr.io/alice/myapp:latest ↓ GET /v2/alice/myapp/referrers/sha256:abc123 ↓ AppView: 1. Queries alice's PDS for io.atcr.signature records 2. Filters signatures matching digest sha256:abc123 3. Transforms to OCI referrers format 4. Returns as JSON ↓ Cosign receives OCI referrer manifest ↓ Verifies signature (works normally) ``` **AppView endpoint implementation:** ```go // GET /v2/{owner}/{repo}/referrers/{digest} func (h *Handler) GetReferrers(w http.ResponseWriter, r *http.Request) { owner := mux.Vars(r)["owner"] digest := mux.Vars(r)["digest"] // 1. Resolve owner → DID → PDS did, pds, err := h.resolver.ResolveIdentity(owner) // 2. Query PDS for signatures matching digest signatures, err := h.atproto.ListRecords(pds, did, "io.atcr.signature") filtered := filterByDigest(signatures, digest) // 3. Transform to OCI Index format index := &ocispec.Index{ SchemaVersion: 2, MediaType: ocispec.MediaTypeImageIndex, Manifests: []ocispec.Descriptor{}, } for _, sig := range filtered { index.Manifests = append(index.Manifests, ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: sig.Digest, Size: sig.Size, ArtifactType: "application/vnd.dev.cosign.simplesigning.v1+json", Annotations: map[string]string{ "dev.sigstore.cosign.signature": sig.Signature, "io.atcr.keyId": sig.KeyId, "io.atcr.signedAt": sig.SignedAt, "io.atcr.source": fmt.Sprintf("at://%s/io.atcr.signature/%s", did, sig.Rkey), }, }) } // 4. Return as JSON w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex) json.NewEncoder(w).Encode(index) } ``` **Benefits:** - ✅ **No dual storage** - signatures only in ATProto - ✅ **Standard tools work** - Cosign, Notary, Kubernetes admission controllers - ✅ **Single source of truth** - ATProto PDS - ✅ **On-demand transformation** - only when needed - ✅ **Offline verification** - can cache public keys **Trade-offs:** - ⚠️ AppView must be reachable during verification (but already required for image pulls) - ⚠️ Transformation overhead (minimal - just JSON formatting) ### Alternative Approaches #### Option 1: Dual Storage (Not Recommended) Store signatures in BOTH ATProto AND OCI registry: ```go // In credential helper or AppView func StoreSignature(sig Signature) error { // 1. Store in ATProto (user's PDS or hold's PDS) err := storeInATProto(sig) // 2. ALSO store as OCI artifact in registry err = storeAsOCIReferrer(sig) return err } ``` **OCI Referrer format:** ```json { "schemaVersion": 2, "mediaType": "application/vnd.oci.image.manifest.v1+json", "artifactType": "application/vnd.dev.cosign.simplesigning.v1+json", "subject": { "digest": "sha256:abc123...", "mediaType": "application/vnd.oci.image.manifest.v1+json" }, "layers": [{ "mediaType": "application/vnd.dev.cosign.simplesigning.v1+json", "digest": "sha256:sig...", "annotations": { "dev.sigstore.cosign.signature": "MEUCIQDx...", "io.atcr.source": "atproto://did:plc:alice123/io.atcr.signature/..." } }] } ``` **Benefits:** - ✅ Works with standard Cosign verification - ✅ Kubernetes admission controllers work out of box - ✅ ATProto signatures still available for discovery - ✅ Cross-reference via `io.atcr.source` annotation **Trade-offs:** - ❌ Duplicate storage (ATProto + OCI) - ❌ Consistency issues (what if one write fails?) - ❌ Signatures tied to specific registry #### Option 2: Custom Admission Controller Write Kubernetes admission controller that understands ATProto: ```yaml # admission-controller deployment apiVersion: v1 kind: ConfigMap metadata: name: atcr-policy data: policy.yaml: | policies: - name: require-atcr-signatures images: - "atcr.io/*/*" verification: method: atproto requireSignature: true ``` **Benefits:** - ✅ Native ATProto support - ✅ No OCI conversion needed - ✅ Can enforce ATCR-specific policies **Trade-offs:** - ❌ Doesn't work with standard tools (Cosign, Notary) - ❌ Additional infrastructure to maintain - ❌ Limited ecosystem integration #### Recommendation **Primary approach: OCI Referrers API Bridge** - Implement `/v2/{owner}/{repo}/referrers/{digest}` in AppView - Query ATProto on-demand and transform to OCI format - Works with Cosign, Notary, Kubernetes admission controllers - No duplicate storage, single source of truth **Why this works:** - Cosign/Notary just make HTTP GET requests to the registry - AppView is already the registry - just add one endpoint - Transformation is simple (ATProto record → OCI descriptor) - Signatures stay in ATProto where they belong ### Cosign Verification (OCI Referrers API) ```bash # Standard Cosign works out of the box: cosign verify atcr.io/alice/myapp:latest \ --key <(atcr-cli key export alice credential-helper-default) # What happens: # 1. Cosign queries: GET /v2/alice/myapp/referrers/sha256:abc123 # 2. AppView fetches signatures from alice's PDS # 3. AppView returns OCI referrers index # 4. Cosign downloads signature artifact # 5. Cosign verifies with public key # 6. Success! # Or with public key inline: cosign verify atcr.io/alice/myapp:latest --key '-----BEGIN PUBLIC KEY----- MFkwEwYHKoZI... -----END PUBLIC KEY-----' ``` **Fetching public keys from ATProto:** Public keys are stored in ATProto records and can be fetched via standard XRPC: ```bash # Query for public keys curl "https://atcr.io/xrpc/com.atproto.repo.listRecords?\ repo=did:plc:alice123&\ collection=io.atcr.signing.key" # Extract public key and save as PEM # Then use in Cosign: cosign verify atcr.io/alice/myapp:latest --key alice.pub ``` ### Kubernetes Policy Example (OCI Referrers API) ```yaml # Sigstore Policy Controller apiVersion: policy.sigstore.dev/v1beta1 kind: ClusterImagePolicy metadata: name: atcr-images-must-be-signed spec: images: - glob: "atcr.io/*/*" authorities: - key: # Public key from ATProto record data: | -----BEGIN PUBLIC KEY----- MFkwEwYHKoZI... -----END PUBLIC KEY----- ``` **How it works:** 1. Pod tries to run `atcr.io/alice/myapp:latest` 2. Policy Controller intercepts 3. Queries registry for OCI referrers (finds signature) 4. Verifies signature with public key 5. Allows pod if valid ### Trust Policies Define what signatures are required for image execution: ```yaml # ~/.atcr/trust-policy.yaml policies: - name: production-images scope: "atcr.io/alice/prod-*" require: - signature: true - keyIds: ["ci-key-1", "alice-release-key"] action: enforce # block, audit, or allow - name: dev-images scope: "atcr.io/alice/dev-*" require: - signature: false action: audit ``` **Integration points:** - Kubernetes admission controller - Docker Content Trust equivalent - CI/CD pipeline gates ## Security Considerations ### Key Storage Security **OS keychain benefits:** - ✅ Encrypted storage - ✅ Access control (requires user password/biometric) - ✅ Auditing (macOS logs keychain access) - ✅ Hardware-backed on modern systems (Secure Enclave, TPM) **Best practices:** - Generate keys on device (never transmitted) - Use hardware-backed storage when available - Require user approval for key access (biometric/password) - Rotate keys periodically (e.g., annually) ### Trust Model **What signatures prove:** - ✅ User had access to private key at signing time - ✅ Manifest digest matches what was signed - ✅ Signature created by specific key ID - ✅ Timestamp of signature creation **What signatures don't prove:** - ❌ Image is free of vulnerabilities - ❌ Image contents are safe to run - ❌ User's identity is verified (depends on DID trust) - ❌ Private key wasn't compromised **Trust dependencies:** - User protects their private key - OS keychain security - DID resolution accuracy (PLC directory, did:web) - PDS serves correct public key records - Signature algorithms remain secure ### Multi-Device Support **Challenge:** User has multiple devices (laptop, desktop, CI/CD) **Options:** 1. **Separate keys per device:** ```json { "keyId": "alice-macbook-pro", "deviceId": "macbook-pro" }, { "keyId": "alice-desktop", "deviceId": "desktop" } ``` - Pros: Best security (key compromise limited to one device) - Cons: Need to trust signatures from any device 2. **Shared key via secure sync:** - Export key from primary device - Import to secondary devices - Stored in each device's keychain - Pros: Single key ID to trust - Cons: More attack surface (key on multiple devices) 3. **Primary + secondary model:** - Primary key on main device - Secondary keys on other devices - Trust policy requires primary key signature - Pros: Flexible + secure - Cons: More complex setup **Recommendation:** Separate keys per device (Option 1) for security, with trust policy accepting any of user's keys. ### Key Compromise Response If a device is lost or private key is compromised: 1. **Revoke the key** via AppView web UI or XRPC API - Updates `io.atcr.signing.key` record: `"revoked": true` - Revocation is atomic and immediate 2. **Generate new key** on new/existing device - Automatic on next `docker login` from secure device - Credential helper generates new key pair 3. **Old signatures still exist but fail verification** - Revoked key = untrusted - No certificate revocation list (CRL) delays - Globally visible within seconds ### CI/CD Signing For automated builds, use standard Cosign in your CI pipeline: ```yaml # .github/workflows/build.yml steps: - name: Push image run: docker push atcr.io/alice/myapp:latest - name: Sign with Cosign run: cosign sign atcr.io/alice/myapp:latest --key ${{ secrets.COSIGN_KEY }} ``` **Key management:** - Generate Cosign key pair: `cosign generate-key-pair` - Store private key in CI secrets (GitHub Actions, GitLab CI, etc.) - Publish public key to PDS via XRPC or AppView web UI - Cosign stores signature via registry's OCI API - AppView automatically stores in ATProto **Or use automatic signing:** - Configure credential helper in CI environment - Signatures happen automatically on push - No explicit signing step needed ## Implementation Roadmap ### Phase 1: Core Signing (2-3 weeks) **Week 1: Credential helper key management** - Generate ECDSA key pair on first run - Store private key in OS keychain - Create `io.atcr.signing.key` record in PDS - Handle key rotation **Week 2: Signing integration** - Sign manifest digest after authentication - Send signature to AppView via XRPC - AppView stores in user's PDS or hold's PDS - Error handling and retries **Week 3: OCI Referrers API** - Implement `GET /v2/{owner}/{repo}/referrers/{digest}` in AppView - Query ATProto for signatures - Transform to OCI Index format - Return Cosign-compatible artifacts - Test with `cosign verify` ### Phase 2: Enhanced Features (2-3 weeks) **Key management (credential helper):** - Key rotation support - Revocation handling - Device identification - Key expiration **Signature storage:** - Handle manual Cosign signing (via OCI API) - Store signatures from both automatic and manual flows - Signature deduplication - Signature audit logs **AppView endpoints:** - XRPC endpoints for key/signature queries - Web UI for viewing keys and signatures - Key revocation via web interface ### Phase 3: Kubernetes Integration (2-3 weeks) **Admission controller setup:** - Documentation for Sigstore Policy Controller - Example policies for ATCR images - Public key management (fetch from ATProto) - Integration testing with real clusters **Advanced features:** - Signature caching in AppView (reduce PDS queries) - Multi-signature support (require N signatures) - Timestamp verification - Signature expiration policies ### Phase 4: UI Integration (1-2 weeks) **AppView web UI:** - Show signature status on repository pages - List signing keys for users - Revoke keys via web interface - Signature verification badges ## Comparison: Automatic vs Manual Signing | Feature | Automatic (Credential Helper) | Manual (Standard Cosign) | |---------|-------------------------------|--------------------------| | **User action** | Zero - happens on push | `cosign sign` after push | | **Key management** | Automatic generation/storage | User manages keys | | **Consistency** | Every image signed | Easy to forget | | **Setup** | Works with credential helper | Install Cosign, generate keys | | **CI/CD** | Automatic if cred helper configured | Explicit signing step | | **Flexibility** | Opinionated defaults | Full control over workflow | | **Use case** | Most users, simple workflows | Advanced users, custom workflows | **Recommendation:** - **Start with automatic**: Best UX, works for most users - **Use manual** for: CI/CD pipelines, hardware tokens, custom signing workflows ## Complete Workflow Summary ### Option 1: Automatic Signing (Recommended) ```bash # Setup (one time) docker login atcr.io # → Credential helper generates ECDSA key pair # → Private key in OS keychain # → Public key published to PDS # Push (automatic signing) docker push atcr.io/alice/myapp:latest # → Image pushed and signed automatically # → No extra commands! # Verify (standard Cosign) cosign verify atcr.io/alice/myapp:latest --key alice.pub # → Cosign queries OCI Referrers API # → AppView returns ATProto signatures as OCI artifacts # → Verification succeeds ✓ ``` ### Option 2: Manual Signing (DIY) ```bash # Push image docker push atcr.io/alice/myapp:latest # Sign with Cosign cosign sign atcr.io/alice/myapp:latest --key cosign.key # → Cosign stores via OCI API # → AppView stores in ATProto # Verify (same as automatic) cosign verify atcr.io/alice/myapp:latest --key cosign.pub ``` ### Kubernetes (Standard Admission Controller) ```yaml # Sigstore Policy Controller (standard) apiVersion: policy.sigstore.dev/v1beta1 kind: ClusterImagePolicy metadata: name: atcr-signed-only spec: images: - glob: "atcr.io/*/*" authorities: - key: data: | -----BEGIN PUBLIC KEY----- [Alice's public key from ATProto] -----END PUBLIC KEY----- ``` **How admission control works:** 1. Pod tries to start with `atcr.io/alice/myapp:latest` 2. Policy Controller intercepts 3. Calls `GET /v2/alice/myapp/referrers/sha256:abc123` 4. AppView returns signatures from ATProto 5. Policy Controller verifies with public key 6. Pod allowed to start ✓ ### Key Design Points **User experience:** - ✅ Two options: automatic (credential helper) or manual (standard Cosign) - ✅ Standard verification tools work (Cosign, Notary, Kubernetes) - ✅ No custom ATCR-specific signing commands - ✅ User-controlled keys (OS keychain or self-managed) **Architecture:** - **Signing**: Client-side only (credential helper or Cosign) - **Storage**: ATProto (user's PDS or hold's PDS via `io.atcr.signature`) - **Verification**: Standard tools via OCI Referrers API bridge - **Bridge**: AppView transforms ATProto → OCI format on-demand **Why this works:** - ✅ No server-side signing needed (impossible with ATProto constraints) - ✅ Signatures discoverable via ATProto - ✅ No duplicate storage (single source of truth) - ✅ Standard OCI compliance for verification ## References ### Signing & Verification - [Sigstore Cosign](https://github.com/sigstore/cosign) - [Notary v2 Specification](https://notaryproject.dev/) - [Cosign Signature Specification](https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md) ### OCI & Registry - [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec) - [OCI Referrers API](https://github.com/oras-project/artifacts-spec/blob/main/manifest-referrers-api.md) - [OCI Artifacts](https://github.com/opencontainers/artifacts) ### ATProto - [ATProto Specification](https://atproto.com/) - [ATProto Repository Specification](https://atproto.com/specs/repository) ### Key Management - [Docker Credential Helpers](https://docs.docker.com/engine/reference/commandline/login/#credential-helpers) - [macOS Keychain Services](https://developer.apple.com/documentation/security/keychain_services) - [Windows Credential Manager](https://docs.microsoft.com/en-us/windows/security/identity-protection/credential-guard/) - [Linux Secret Service API](https://specifications.freedesktop.org/secret-service/) ### Kubernetes Integration - [Sigstore Policy Controller](https://docs.sigstore.dev/policy-controller/overview/) - [Ratify (Notary verification for Kubernetes)](https://ratify.dev/)