A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
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└────────────────────┬────────────────────────────────┘
112 ↓
113┌─────────────────────────────────────────────────────┐
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└────────────────────┬────────────────────────────────┘
124 ↓
125┌─────────────────────────────────────────────────────┐
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
222 ↓
2232. Docker daemon calls credential helper:
224 docker-credential-atcr get atcr.io
225 ↓
2263. 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)
233 ↓
2344. 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 }
244 ↓
2455. Return registry JWT to Docker
246 ↓
2476. 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
399 ↓
400GET /v2/alice/myapp/referrers/sha256:abc123
401 ↓
402AppView:
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
407 ↓
408Cosign receives OCI referrer manifest
409 ↓
410Verifies 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/)