A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
1# ATProto Signatures for Container Images
2
3## Overview
4
5ATCR 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.
6
7This document explains:
8- How ATProto signing works
9- Why additional signing tools aren't needed
10- How to bridge ATProto signatures to the OCI/ORAS ecosystem
11- Trust model and security considerations
12
13## Key Insight: Manifests Are Already Signed
14
15When you push an image to ATCR:
16
17```bash
18docker push atcr.io/alice/myapp:latest
19```
20
21The following happens:
22
231. **AppView stores manifest** as an `io.atcr.manifest` record in alice's PDS
242. **PDS creates repository commit** containing the manifest record
253. **PDS signs the commit** with alice's ATProto signing key (ECDSA K-256)
264. **Signature is stored** in the repository commit object
27
28**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.
29
30## ATProto Signing Mechanism
31
32### Repository Commit Signing
33
34ATProto uses a Merkle Search Tree (MST) to store records, and every modification creates a signed commit:
35
36```
37┌─────────────────────────────────────────────┐
38│ Repository Commit │
39├─────────────────────────────────────────────┤
40│ DID: did:plc:alice123 │
41│ Version: 3jzfkjqwdwa2a │
42│ Previous: bafyreig7... (parent commit) │
43│ Data CID: bafyreih8... (MST root) │
44│ ┌───────────────────────────────────────┐ │
45│ │ Signature (ECDSA K-256 + SHA-256) │ │
46│ │ Signed with: alice's private key │ │
47│ │ Value: 0x3045022100... (DER format) │ │
48│ └───────────────────────────────────────┘ │
49└─────────────────────────────────────────────┘
50 │
51 ↓
52 ┌─────────────────────┐
53 │ Merkle Search Tree │
54 │ (contains records) │
55 └─────────────────────┘
56 │
57 ↓
58 ┌────────────────────────────┐
59 │ io.atcr.manifest record │
60 │ Repository: myapp │
61 │ Digest: sha256:abc123... │
62 │ Layers: [...] │
63 └────────────────────────────┘
64```
65
66### Signature Algorithm
67
68**Algorithm:** ECDSA with K-256 (secp256k1) curve + SHA-256 hash
69- **Curve:** secp256k1 (same as Bitcoin, Ethereum)
70- **Hash:** SHA-256
71- **Format:** DER-encoded signature bytes
72- **Variant:** "low-S" signatures (per BIP-0062)
73
74**Signing process:**
751. Serialize commit data as DAG-CBOR
762. Hash with SHA-256
773. Sign hash with ECDSA K-256 private key
784. Store signature in commit object
79
80### Public Key Distribution
81
82Public keys are distributed via DID documents, accessible through DID resolution:
83
84**DID Resolution Flow:**
85```
86did:plc:alice123
87 ↓
88Query PLC directory: https://plc.directory/did:plc:alice123
89 ↓
90DID Document:
91{
92 "@context": ["https://www.w3.org/ns/did/v1"],
93 "id": "did:plc:alice123",
94 "verificationMethod": [{
95 "id": "did:plc:alice123#atproto",
96 "type": "Multikey",
97 "controller": "did:plc:alice123",
98 "publicKeyMultibase": "zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDdo1Ko4Z"
99 }],
100 "service": [{
101 "id": "#atproto_pds",
102 "type": "AtprotoPersonalDataServer",
103 "serviceEndpoint": "https://bsky.social"
104 }]
105}
106```
107
108**Public key format:**
109- **Encoding:** Multibase (base58btc with `z` prefix)
110- **Codec:** Multicodec `0xE701` for K-256 keys
111- **Example:** `zQ3sh...` decodes to 33-byte compressed public key
112
113## Verification Process
114
115To verify a manifest's signature:
116
117### Step 1: Resolve Image to Manifest Digest
118
119```bash
120# Get manifest digest
121DIGEST=$(crane digest atcr.io/alice/myapp:latest)
122# Result: sha256:abc123...
123```
124
125### Step 2: Fetch Manifest Record from PDS
126
127```bash
128# Extract repository name from image reference
129REPO="myapp"
130
131# Query PDS for manifest record
132curl "https://bsky.social/xrpc/com.atproto.repo.listRecords?\
133 repo=did:plc:alice123&\
134 collection=io.atcr.manifest&\
135 limit=100" | jq -r '.records[] | select(.value.digest == "sha256:abc123...")'
136```
137
138Response includes:
139```json
140{
141 "uri": "at://did:plc:alice123/io.atcr.manifest/abc123",
142 "cid": "bafyreig7...",
143 "value": {
144 "$type": "io.atcr.manifest",
145 "repository": "myapp",
146 "digest": "sha256:abc123...",
147 ...
148 }
149}
150```
151
152### Step 3: Fetch Repository Commit
153
154```bash
155# Get current repository state
156curl "https://bsky.social/xrpc/com.atproto.sync.getRepo?\
157 did=did:plc:alice123" --output repo.car
158
159# Extract commit from CAR file (requires ATProto tools)
160# Commit includes signature over repository state
161```
162
163### Step 4: Resolve DID to Public Key
164
165```bash
166# Resolve DID document
167curl "https://plc.directory/did:plc:alice123" | jq -r '.verificationMethod[0].publicKeyMultibase'
168# Result: zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDdo1Ko4Z
169```
170
171### Step 5: Verify Signature
172
173```go
174// Pseudocode for verification
175import "github.com/bluesky-social/indigo/atproto/crypto"
176
177// 1. Parse commit
178commit := parseCommitFromCAR(repoCAR)
179
180// 2. Extract signature bytes
181signature := commit.Sig
182
183// 3. Get bytes that were signed
184bytesToVerify := commit.Unsigned().BytesForSigning()
185
186// 4. Decode public key from multibase
187pubKey := decodeMultibasePublicKey(publicKeyMultibase)
188
189// 5. Verify ECDSA signature
190valid := crypto.VerifySignature(pubKey, bytesToVerify, signature)
191```
192
193### Step 6: Verify Manifest Integrity
194
195```bash
196# Verify the manifest record's CID matches the content
197# CID is content-addressed, so tampering changes the CID
198```
199
200## Bridging to OCI/ORAS Ecosystem
201
202While 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.
203
204### ORAS Signature Artifact Format
205
206```json
207{
208 "schemaVersion": 2,
209 "mediaType": "application/vnd.oci.image.manifest.v1+json",
210 "artifactType": "application/vnd.atproto.signature.v1+json",
211 "config": {
212 "mediaType": "application/vnd.oci.empty.v1+json",
213 "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
214 "size": 2
215 },
216 "subject": {
217 "mediaType": "application/vnd.oci.image.manifest.v1+json",
218 "digest": "sha256:abc123...",
219 "size": 1234
220 },
221 "layers": [
222 {
223 "mediaType": "application/vnd.atproto.signature.v1+json",
224 "digest": "sha256:sig789...",
225 "size": 512,
226 "annotations": {
227 "org.opencontainers.image.title": "atproto-signature.json"
228 }
229 }
230 ],
231 "annotations": {
232 "io.atcr.atproto.did": "did:plc:alice123",
233 "io.atcr.atproto.pds": "https://bsky.social",
234 "io.atcr.atproto.recordUri": "at://did:plc:alice123/io.atcr.manifest/abc123",
235 "io.atcr.atproto.commitCid": "bafyreih8...",
236 "io.atcr.atproto.signedAt": "2025-10-31T12:34:56.789Z",
237 "io.atcr.atproto.keyId": "did:plc:alice123#atproto"
238 }
239}
240```
241
242**Key elements:**
243
2441. **artifactType**: `application/vnd.atproto.signature.v1+json` - identifies this as an ATProto signature
2452. **subject**: Links to the image manifest being signed
2463. **layers**: Contains signature metadata blob
2474. **annotations**: Quick-access metadata for verification
248
249### Signature Metadata Blob
250
251The layer blob contains detailed verification information:
252
253```json
254{
255 "$type": "io.atcr.atproto.signature",
256 "version": "1.0",
257 "subject": {
258 "digest": "sha256:abc123...",
259 "mediaType": "application/vnd.oci.image.manifest.v1+json"
260 },
261 "atproto": {
262 "did": "did:plc:alice123",
263 "handle": "alice.bsky.social",
264 "pdsEndpoint": "https://bsky.social",
265 "recordUri": "at://did:plc:alice123/io.atcr.manifest/abc123",
266 "recordCid": "bafyreig7...",
267 "commitCid": "bafyreih8...",
268 "commitRev": "3jzfkjqwdwa2a",
269 "signedAt": "2025-10-31T12:34:56.789Z"
270 },
271 "signature": {
272 "algorithm": "ECDSA-K256-SHA256",
273 "keyId": "did:plc:alice123#atproto",
274 "publicKeyMultibase": "zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDdo1Ko4Z"
275 },
276 "verification": {
277 "method": "atproto-repo-commit",
278 "instructions": "Fetch repository commit from PDS and verify signature using public key from DID document"
279 }
280}
281```
282
283### Discovery via Referrers API
284
285ORAS artifacts are discoverable via the OCI Referrers API:
286
287```bash
288# Query for signature artifacts
289curl "https://atcr.io/v2/alice/myapp/referrers/sha256:abc123?\
290 artifactType=application/vnd.atproto.signature.v1+json"
291```
292
293Response:
294```json
295{
296 "schemaVersion": 2,
297 "mediaType": "application/vnd.oci.image.index.v1+json",
298 "manifests": [
299 {
300 "mediaType": "application/vnd.oci.image.manifest.v1+json",
301 "digest": "sha256:sig789...",
302 "size": 1234,
303 "artifactType": "application/vnd.atproto.signature.v1+json",
304 "annotations": {
305 "io.atcr.atproto.did": "did:plc:alice123",
306 "io.atcr.atproto.signedAt": "2025-10-31T12:34:56.789Z"
307 }
308 }
309 ]
310}
311```
312
313## Trust Model
314
315### What ATProto Signatures Prove
316
317✅ **Authenticity**: Image was published by the DID owner
318✅ **Integrity**: Image manifest hasn't been tampered with since signing
319✅ **Non-repudiation**: Only the DID owner could have created this signature
320✅ **Timestamp**: When the image was signed (commit timestamp)
321
322### What ATProto Signatures Don't Prove
323
324❌ **Safety**: Image doesn't contain vulnerabilities (use vulnerability scanning)
325❌ **DID trustworthiness**: Whether the DID owner is trustworthy (trust policy decision)
326❌ **Key security**: Private key wasn't compromised (same limitation as all PKI)
327❌ **PDS honesty**: PDS operator serves correct data (verify across multiple sources)
328
329### Trust Dependencies
330
3311. **DID Resolution**: Must correctly resolve DID to public key
332 - **Mitigation**: Use multiple resolvers, cache DID documents
333
3342. **PDS Availability**: Must query PDS to verify signatures
335 - **Mitigation**: Embed signature bytes in ORAS blob for offline verification
336
3373. **PDS Honesty**: PDS could serve fake/unsigned records
338 - **Mitigation**: Signature verification prevents this (can't forge signature)
339
3404. **Key Security**: User's private key could be compromised
341 - **Mitigation**: Key rotation via DID document updates, short-lived credentials
342
3435. **Algorithm Security**: ECDSA K-256 must remain secure
344 - **Status**: Well-studied, same as Bitcoin/Ethereum (widely trusted)
345
346### Comparison with Other Signing Systems
347
348| Aspect | ATProto Signatures | Cosign (Keyless) | Notary v2 |
349|--------|-------------------|------------------|-----------|
350| **Identity** | DID (decentralized) | OIDC (federated) | X.509 (PKI) |
351| **Key Management** | PDS signing keys | Ephemeral (Fulcio) | User-managed |
352| **Trust Anchor** | DID resolution | Fulcio CA + Rekor | Certificate chain |
353| **Transparency Log** | ATProto firehose | Rekor | Optional |
354| **Offline Verification** | Limited* | No | Yes |
355| **Decentralization** | High | Medium | Low |
356| **Complexity** | Low | High | Medium |
357
358*Can be improved by embedding signature bytes in ORAS blob
359
360### Security Considerations
361
362**Threat: Man-in-the-Middle Attack**
363- **Attack**: Intercept PDS queries, serve fake records
364- **Defense**: TLS for PDS communication, verify signature with public key from DID document
365- **Result**: Attacker can't forge signature without private key
366
367**Threat: Compromised PDS**
368- **Attack**: PDS operator serves unsigned/fake manifests
369- **Defense**: Signature verification fails (PDS can't sign without user's private key)
370- **Result**: Protected
371
372**Threat: Key Compromise**
373- **Attack**: Attacker steals user's ATProto signing key
374- **Defense**: Key rotation via DID document, revoke old keys
375- **Result**: Same as any PKI system (rotate keys quickly)
376
377**Threat: Replay Attack**
378- **Attack**: Replay old signed manifest to rollback to vulnerable version
379- **Defense**: Check commit timestamp, verify commit is in current repository DAG
380- **Result**: Protected (commits form immutable chain)
381
382**Threat: DID Takeover**
383- **Attack**: Attacker gains control of user's DID (rotation keys)
384- **Defense**: Monitor DID document changes, verify key history
385- **Result**: Serious but requires compromising rotation keys (harder than signing keys)
386
387## Implementation Strategy
388
389### Automatic Signature Artifact Creation
390
391When AppView stores a manifest in a user's PDS:
392
3931. **Store manifest record** (existing behavior)
3942. **Get commit response** with commit CID and revision
3953. **Create ORAS signature artifact**:
396 - Build metadata blob (JSON)
397 - Upload blob to hold storage
398 - Create ORAS manifest with subject = image manifest
399 - Store ORAS manifest (creates referrer link)
400
401### Storage Location
402
403Signature artifacts follow the same pattern as SBOMs:
404- **Metadata blobs**: Stored in hold's blob storage
405- **ORAS manifests**: Stored in hold's embedded PDS
406- **Discovery**: Via OCI Referrers API
407
408### Verification Tools
409
410**Option 1: Custom CLI tool (`atcr-verify`)**
411```bash
412atcr-verify atcr.io/alice/myapp:latest
413# → Queries referrers API
414# → Fetches signature metadata
415# → Resolves DID → public key
416# → Queries PDS for commit
417# → Verifies signature
418```
419
420**Option 2: Shell script (curl + jq)**
421- See `docs/SIGNATURE_INTEGRATION.md` for examples
422
423**Option 3: Kubernetes admission controller**
424- Custom webhook that runs verification
425- Rejects pods with unsigned/invalid signatures
426
427## Benefits of ATProto Signatures
428
429### Compared to No Signing
430
431✅ **Cryptographic proof** of image authorship
432✅ **Tamper detection** for manifests
433✅ **Identity binding** via DIDs
434✅ **Audit trail** via ATProto repository history
435
436### Compared to Cosign/Notary
437
438✅ **No additional signing required** (already signed by PDS)
439✅ **Decentralized identity** (DIDs, not CAs)
440✅ **Simpler infrastructure** (no Fulcio, no Rekor, no TUF)
441✅ **Consistent with ATCR's architecture** (ATProto-native)
442✅ **Lower operational overhead** (reuse existing PDS infrastructure)
443
444### Trade-offs
445
446⚠️ **Custom verification tools required** (standard tools won't work)
447⚠️ **Online verification preferred** (need to query PDS)
448⚠️ **Different trust model** (trust DIDs, not CAs)
449⚠️ **Ecosystem maturity** (newer approach, less tooling)
450
451## Future Enhancements
452
453### Short-term
454
4551. **Offline verification**: Embed signature bytes in ORAS blob
4562. **Multi-PDS verification**: Check signature across multiple PDSs
4573. **Key rotation support**: Handle historical key validity
458
459### Medium-term
460
4614. **Timestamp service**: RFC 3161 timestamps for long-term validity
4625. **Multi-signature**: Require N signatures from M DIDs
4636. **Transparency log integration**: Record verifications in public log
464
465### Long-term
466
4677. **IANA registration**: Register `application/vnd.atproto.signature.v1+json`
4688. **Standards proposal**: ATProto signature spec to ORAS/OCI
4699. **Cross-ecosystem bridges**: Convert to Cosign/Notary formats
470
471## Conclusion
472
473ATCR images are already cryptographically signed through ATProto's repository commit system. By creating ORAS signature artifacts that reference these existing signatures, we can:
474
475- ✅ Make signatures discoverable to OCI tooling
476- ✅ Maintain ATProto as the source of truth
477- ✅ Provide verification tools for users and clusters
478- ✅ Avoid duplicating signing infrastructure
479
480This approach leverages ATProto's strengths (decentralized identity, built-in signing) while bridging to the OCI ecosystem through standard ORAS artifacts.
481
482## References
483
484### ATProto Specifications
485- [ATProto Repository Specification](https://atproto.com/specs/repository)
486- [ATProto Data Model](https://atproto.com/specs/data-model)
487- [ATProto DID Methods](https://atproto.com/specs/did)
488
489### OCI/ORAS Specifications
490- [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec)
491- [OCI Referrers API](https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers)
492- [ORAS Artifacts](https://oras.land/docs/)
493
494### Cryptography
495- [ECDSA (secp256k1)](https://en.bitcoin.it/wiki/Secp256k1)
496- [Multibase Encoding](https://github.com/multiformats/multibase)
497- [Multicodec](https://github.com/multiformats/multicodec)
498
499### Related Documentation
500- [SBOM Scanning](./SBOM_SCANNING.md) - Similar ORAS artifact pattern
501- [Signature Integration](./SIGNATURE_INTEGRATION.md) - Practical integration examples