A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
73
fork

Configure Feed

Select the types of activity you want to include in your feed.

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:

{
  "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#

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:

# 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:

# 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:

{
  "$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:

// 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:

{
  "$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):

{
  "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#

{
  "$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#

{
  "$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:

// 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#

Store signatures in BOTH ATProto AND OCI registry:

// 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:

{
  "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:

# 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)#

# 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:

# 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)#

# 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:

# ~/.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:

    {
      "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:

# .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#

# 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)#

# 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)#

# 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#

OCI & Registry#

ATProto#

Key Management#

Kubernetes Integration#