# ATProto Signatures for Container Images ## Overview ATCR container images are **already cryptographically signed** through ATProto's repository commit system. Every manifest stored in a user's PDS is signed with the user's ATProto signing key, providing cryptographic proof of authorship and integrity. This document explains: - How ATProto signing works - Why additional signing tools aren't needed - How to bridge ATProto signatures to the OCI/ORAS ecosystem - Trust model and security considerations ## Key Insight: Manifests Are Already Signed When you push an image to ATCR: ```bash docker push atcr.io/alice/myapp:latest ``` The following happens: 1. **AppView stores manifest** as an `io.atcr.manifest` record in alice's PDS 2. **PDS creates repository commit** containing the manifest record 3. **PDS signs the commit** with alice's ATProto signing key (ECDSA K-256) 4. **Signature is stored** in the repository commit object **Result:** The manifest is cryptographically signed with alice's private key, and anyone can verify it using alice's public key from her DID document. ## ATProto Signing Mechanism ### Repository Commit Signing ATProto uses a Merkle Search Tree (MST) to store records, and every modification creates a signed commit: ``` ┌─────────────────────────────────────────────┐ │ Repository Commit │ ├─────────────────────────────────────────────┤ │ DID: did:plc:alice123 │ │ Version: 3jzfkjqwdwa2a │ │ Previous: bafyreig7... (parent commit) │ │ Data CID: bafyreih8... (MST root) │ │ ┌───────────────────────────────────────┐ │ │ │ Signature (ECDSA K-256 + SHA-256) │ │ │ │ Signed with: alice's private key │ │ │ │ Value: 0x3045022100... (DER format) │ │ │ └───────────────────────────────────────┘ │ └─────────────────────────────────────────────┘ │ ↓ ┌─────────────────────┐ │ Merkle Search Tree │ │ (contains records) │ └─────────────────────┘ │ ↓ ┌────────────────────────────┐ │ io.atcr.manifest record │ │ Repository: myapp │ │ Digest: sha256:abc123... │ │ Layers: [...] │ └────────────────────────────┘ ``` ### Signature Algorithm **Algorithm:** ECDSA with K-256 (secp256k1) curve + SHA-256 hash - **Curve:** secp256k1 (same as Bitcoin, Ethereum) - **Hash:** SHA-256 - **Format:** DER-encoded signature bytes - **Variant:** "low-S" signatures (per BIP-0062) **Signing process:** 1. Serialize commit data as DAG-CBOR 2. Hash with SHA-256 3. Sign hash with ECDSA K-256 private key 4. Store signature in commit object ### Public Key Distribution Public keys are distributed via DID documents, accessible through DID resolution: **DID Resolution Flow:** ``` did:plc:alice123 ↓ Query PLC directory: https://plc.directory/did:plc:alice123 ↓ DID Document: { "@context": ["https://www.w3.org/ns/did/v1"], "id": "did:plc:alice123", "verificationMethod": [{ "id": "did:plc:alice123#atproto", "type": "Multikey", "controller": "did:plc:alice123", "publicKeyMultibase": "zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDdo1Ko4Z" }], "service": [{ "id": "#atproto_pds", "type": "AtprotoPersonalDataServer", "serviceEndpoint": "https://bsky.social" }] } ``` **Public key format:** - **Encoding:** Multibase (base58btc with `z` prefix) - **Codec:** Multicodec `0xE701` for K-256 keys - **Example:** `zQ3sh...` decodes to 33-byte compressed public key ## Verification Process To verify a manifest's signature: ### Step 1: Resolve Image to Manifest Digest ```bash # Get manifest digest DIGEST=$(crane digest atcr.io/alice/myapp:latest) # Result: sha256:abc123... ``` ### Step 2: Fetch Manifest Record from PDS ```bash # Extract repository name from image reference REPO="myapp" # Query PDS for manifest record curl "https://bsky.social/xrpc/com.atproto.repo.listRecords?\ repo=did:plc:alice123&\ collection=io.atcr.manifest&\ limit=100" | jq -r '.records[] | select(.value.digest == "sha256:abc123...")' ``` Response includes: ```json { "uri": "at://did:plc:alice123/io.atcr.manifest/abc123", "cid": "bafyreig7...", "value": { "$type": "io.atcr.manifest", "repository": "myapp", "digest": "sha256:abc123...", ... } } ``` ### Step 3: Fetch Repository Commit ```bash # Get current repository state curl "https://bsky.social/xrpc/com.atproto.sync.getRepo?\ did=did:plc:alice123" --output repo.car # Extract commit from CAR file (requires ATProto tools) # Commit includes signature over repository state ``` ### Step 4: Resolve DID to Public Key ```bash # Resolve DID document curl "https://plc.directory/did:plc:alice123" | jq -r '.verificationMethod[0].publicKeyMultibase' # Result: zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDdo1Ko4Z ``` ### Step 5: Verify Signature ```go // Pseudocode for verification import "github.com/bluesky-social/indigo/atproto/crypto" // 1. Parse commit commit := parseCommitFromCAR(repoCAR) // 2. Extract signature bytes signature := commit.Sig // 3. Get bytes that were signed bytesToVerify := commit.Unsigned().BytesForSigning() // 4. Decode public key from multibase pubKey := decodeMultibasePublicKey(publicKeyMultibase) // 5. Verify ECDSA signature valid := crypto.VerifySignature(pubKey, bytesToVerify, signature) ``` ### Step 6: Verify Manifest Integrity ```bash # Verify the manifest record's CID matches the content # CID is content-addressed, so tampering changes the CID ``` ## Bridging to OCI/ORAS Ecosystem While ATProto signatures are cryptographically sound, the OCI ecosystem doesn't understand ATProto records. To make signatures discoverable, we create **ORAS signature artifacts** that reference the ATProto signature. ### ORAS Signature Artifact Format ```json { "schemaVersion": 2, "mediaType": "application/vnd.oci.image.manifest.v1+json", "artifactType": "application/vnd.atproto.signature.v1+json", "config": { "mediaType": "application/vnd.oci.empty.v1+json", "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", "size": 2 }, "subject": { "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "sha256:abc123...", "size": 1234 }, "layers": [ { "mediaType": "application/vnd.atproto.signature.v1+json", "digest": "sha256:sig789...", "size": 512, "annotations": { "org.opencontainers.image.title": "atproto-signature.json" } } ], "annotations": { "io.atcr.atproto.did": "did:plc:alice123", "io.atcr.atproto.pds": "https://bsky.social", "io.atcr.atproto.recordUri": "at://did:plc:alice123/io.atcr.manifest/abc123", "io.atcr.atproto.commitCid": "bafyreih8...", "io.atcr.atproto.signedAt": "2025-10-31T12:34:56.789Z", "io.atcr.atproto.keyId": "did:plc:alice123#atproto" } } ``` **Key elements:** 1. **artifactType**: `application/vnd.atproto.signature.v1+json` - identifies this as an ATProto signature 2. **subject**: Links to the image manifest being signed 3. **layers**: Contains signature metadata blob 4. **annotations**: Quick-access metadata for verification ### Signature Metadata Blob The layer blob contains detailed verification information: ```json { "$type": "io.atcr.atproto.signature", "version": "1.0", "subject": { "digest": "sha256:abc123...", "mediaType": "application/vnd.oci.image.manifest.v1+json" }, "atproto": { "did": "did:plc:alice123", "handle": "alice.bsky.social", "pdsEndpoint": "https://bsky.social", "recordUri": "at://did:plc:alice123/io.atcr.manifest/abc123", "recordCid": "bafyreig7...", "commitCid": "bafyreih8...", "commitRev": "3jzfkjqwdwa2a", "signedAt": "2025-10-31T12:34:56.789Z" }, "signature": { "algorithm": "ECDSA-K256-SHA256", "keyId": "did:plc:alice123#atproto", "publicKeyMultibase": "zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDdo1Ko4Z" }, "verification": { "method": "atproto-repo-commit", "instructions": "Fetch repository commit from PDS and verify signature using public key from DID document" } } ``` ### Discovery via Referrers API ORAS artifacts are discoverable via the OCI Referrers API: ```bash # Query for signature artifacts curl "https://atcr.io/v2/alice/myapp/referrers/sha256:abc123?\ artifactType=application/vnd.atproto.signature.v1+json" ``` Response: ```json { "schemaVersion": 2, "mediaType": "application/vnd.oci.image.index.v1+json", "manifests": [ { "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "sha256:sig789...", "size": 1234, "artifactType": "application/vnd.atproto.signature.v1+json", "annotations": { "io.atcr.atproto.did": "did:plc:alice123", "io.atcr.atproto.signedAt": "2025-10-31T12:34:56.789Z" } } ] } ``` ## Trust Model ### What ATProto Signatures Prove ✅ **Authenticity**: Image was published by the DID owner ✅ **Integrity**: Image manifest hasn't been tampered with since signing ✅ **Non-repudiation**: Only the DID owner could have created this signature ✅ **Timestamp**: When the image was signed (commit timestamp) ### What ATProto Signatures Don't Prove ❌ **Safety**: Image doesn't contain vulnerabilities (use vulnerability scanning) ❌ **DID trustworthiness**: Whether the DID owner is trustworthy (trust policy decision) ❌ **Key security**: Private key wasn't compromised (same limitation as all PKI) ❌ **PDS honesty**: PDS operator serves correct data (verify across multiple sources) ### Trust Dependencies 1. **DID Resolution**: Must correctly resolve DID to public key - **Mitigation**: Use multiple resolvers, cache DID documents 2. **PDS Availability**: Must query PDS to verify signatures - **Mitigation**: Embed signature bytes in ORAS blob for offline verification 3. **PDS Honesty**: PDS could serve fake/unsigned records - **Mitigation**: Signature verification prevents this (can't forge signature) 4. **Key Security**: User's private key could be compromised - **Mitigation**: Key rotation via DID document updates, short-lived credentials 5. **Algorithm Security**: ECDSA K-256 must remain secure - **Status**: Well-studied, same as Bitcoin/Ethereum (widely trusted) ### Comparison with Other Signing Systems | Aspect | ATProto Signatures | Cosign (Keyless) | Notary v2 | |--------|-------------------|------------------|-----------| | **Identity** | DID (decentralized) | OIDC (federated) | X.509 (PKI) | | **Key Management** | PDS signing keys | Ephemeral (Fulcio) | User-managed | | **Trust Anchor** | DID resolution | Fulcio CA + Rekor | Certificate chain | | **Transparency Log** | ATProto firehose | Rekor | Optional | | **Offline Verification** | Limited* | No | Yes | | **Decentralization** | High | Medium | Low | | **Complexity** | Low | High | Medium | *Can be improved by embedding signature bytes in ORAS blob ### Security Considerations **Threat: Man-in-the-Middle Attack** - **Attack**: Intercept PDS queries, serve fake records - **Defense**: TLS for PDS communication, verify signature with public key from DID document - **Result**: Attacker can't forge signature without private key **Threat: Compromised PDS** - **Attack**: PDS operator serves unsigned/fake manifests - **Defense**: Signature verification fails (PDS can't sign without user's private key) - **Result**: Protected **Threat: Key Compromise** - **Attack**: Attacker steals user's ATProto signing key - **Defense**: Key rotation via DID document, revoke old keys - **Result**: Same as any PKI system (rotate keys quickly) **Threat: Replay Attack** - **Attack**: Replay old signed manifest to rollback to vulnerable version - **Defense**: Check commit timestamp, verify commit is in current repository DAG - **Result**: Protected (commits form immutable chain) **Threat: DID Takeover** - **Attack**: Attacker gains control of user's DID (rotation keys) - **Defense**: Monitor DID document changes, verify key history - **Result**: Serious but requires compromising rotation keys (harder than signing keys) ## Implementation Strategy ### Automatic Signature Artifact Creation When AppView stores a manifest in a user's PDS: 1. **Store manifest record** (existing behavior) 2. **Get commit response** with commit CID and revision 3. **Create ORAS signature artifact**: - Build metadata blob (JSON) - Upload blob to hold storage - Create ORAS manifest with subject = image manifest - Store ORAS manifest (creates referrer link) ### Storage Location Signature artifacts follow the same pattern as SBOMs: - **Metadata blobs**: Stored in hold's blob storage - **ORAS manifests**: Stored in hold's embedded PDS - **Discovery**: Via OCI Referrers API ### Verification Tools **Option 1: Custom CLI tool (`atcr-verify`)** ```bash atcr-verify atcr.io/alice/myapp:latest # → Queries referrers API # → Fetches signature metadata # → Resolves DID → public key # → Queries PDS for commit # → Verifies signature ``` **Option 2: Shell script (curl + jq)** - See `docs/SIGNATURE_INTEGRATION.md` for examples **Option 3: Kubernetes admission controller** - Custom webhook that runs verification - Rejects pods with unsigned/invalid signatures ## Benefits of ATProto Signatures ### Compared to No Signing ✅ **Cryptographic proof** of image authorship ✅ **Tamper detection** for manifests ✅ **Identity binding** via DIDs ✅ **Audit trail** via ATProto repository history ### Compared to Cosign/Notary ✅ **No additional signing required** (already signed by PDS) ✅ **Decentralized identity** (DIDs, not CAs) ✅ **Simpler infrastructure** (no Fulcio, no Rekor, no TUF) ✅ **Consistent with ATCR's architecture** (ATProto-native) ✅ **Lower operational overhead** (reuse existing PDS infrastructure) ### Trade-offs ⚠️ **Custom verification tools required** (standard tools won't work) ⚠️ **Online verification preferred** (need to query PDS) ⚠️ **Different trust model** (trust DIDs, not CAs) ⚠️ **Ecosystem maturity** (newer approach, less tooling) ## Future Enhancements ### Short-term 1. **Offline verification**: Embed signature bytes in ORAS blob 2. **Multi-PDS verification**: Check signature across multiple PDSs 3. **Key rotation support**: Handle historical key validity ### Medium-term 4. **Timestamp service**: RFC 3161 timestamps for long-term validity 5. **Multi-signature**: Require N signatures from M DIDs 6. **Transparency log integration**: Record verifications in public log ### Long-term 7. **IANA registration**: Register `application/vnd.atproto.signature.v1+json` 8. **Standards proposal**: ATProto signature spec to ORAS/OCI 9. **Cross-ecosystem bridges**: Convert to Cosign/Notary formats ## Conclusion ATCR images are already cryptographically signed through ATProto's repository commit system. By creating ORAS signature artifacts that reference these existing signatures, we can: - ✅ Make signatures discoverable to OCI tooling - ✅ Maintain ATProto as the source of truth - ✅ Provide verification tools for users and clusters - ✅ Avoid duplicating signing infrastructure This approach leverages ATProto's strengths (decentralized identity, built-in signing) while bridging to the OCI ecosystem through standard ORAS artifacts. ## References ### ATProto Specifications - [ATProto Repository Specification](https://atproto.com/specs/repository) - [ATProto Data Model](https://atproto.com/specs/data-model) - [ATProto DID Methods](https://atproto.com/specs/did) ### OCI/ORAS Specifications - [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec) - [OCI Referrers API](https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers) - [ORAS Artifacts](https://oras.land/docs/) ### Cryptography - [ECDSA (secp256k1)](https://en.bitcoin.it/wiki/Secp256k1) - [Multibase Encoding](https://github.com/multiformats/multibase) - [Multicodec](https://github.com/multiformats/multicodec) ### Related Documentation - [SBOM Scanning](./SBOM_SCANNING.md) - Similar ORAS artifact pattern - [Signature Integration](./SIGNATURE_INTEGRATION.md) - Practical integration examples