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:
docker push atcr.io/alice/myapp:latest
The following happens:
- AppView stores manifest as an
io.atcr.manifestrecord in alice's PDS - PDS creates repository commit containing the manifest record
- PDS signs the commit with alice's ATProto signing key (ECDSA K-256)
- 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:
- Serialize commit data as DAG-CBOR
- Hash with SHA-256
- Sign hash with ECDSA K-256 private key
- 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
zprefix) - Codec: Multicodec
0xE701for 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#
# Get manifest digest
DIGEST=$(crane digest atcr.io/alice/myapp:latest)
# Result: sha256:abc123...
Step 2: Fetch Manifest Record from PDS#
# 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:
{
"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#
# 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#
# Resolve DID document
curl "https://plc.directory/did:plc:alice123" | jq -r '.verificationMethod[0].publicKeyMultibase'
# Result: zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDdo1Ko4Z
Step 5: Verify Signature#
// 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#
# 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#
{
"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:
- artifactType:
application/vnd.atproto.signature.v1+json- identifies this as an ATProto signature - subject: Links to the image manifest being signed
- layers: Contains signature metadata blob
- annotations: Quick-access metadata for verification
Signature Metadata Blob#
The layer blob contains detailed verification information:
{
"$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:
# Query for signature artifacts
curl "https://atcr.io/v2/alice/myapp/referrers/sha256:abc123?\
artifactType=application/vnd.atproto.signature.v1+json"
Response:
{
"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#
-
DID Resolution: Must correctly resolve DID to public key
- Mitigation: Use multiple resolvers, cache DID documents
-
PDS Availability: Must query PDS to verify signatures
- Mitigation: Embed signature bytes in ORAS blob for offline verification
-
PDS Honesty: PDS could serve fake/unsigned records
- Mitigation: Signature verification prevents this (can't forge signature)
-
Key Security: User's private key could be compromised
- Mitigation: Key rotation via DID document updates, short-lived credentials
-
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:
- Store manifest record (existing behavior)
- Get commit response with commit CID and revision
- 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)
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.mdfor 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#
- Offline verification: Embed signature bytes in ORAS blob
- Multi-PDS verification: Check signature across multiple PDSs
- Key rotation support: Handle historical key validity
Medium-term#
- Timestamp service: RFC 3161 timestamps for long-term validity
- Multi-signature: Require N signatures from M DIDs
- Transparency log integration: Record verifications in public log
Long-term#
- IANA registration: Register
application/vnd.atproto.signature.v1+json - Standards proposal: ATProto signature spec to ORAS/OCI
- 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#
OCI/ORAS Specifications#
Cryptography#
Related Documentation#
- SBOM Scanning - Similar ORAS artifact pattern
- Signature Integration - Practical integration examples