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.

Hold-as-Certificate-Authority Architecture#

⚠️ Important Notice#

This document describes an optional enterprise feature for X.509 PKI compliance. The hold-as-CA approach introduces centralization trade-offs that contradict ATProto's decentralized philosophy.

Default Recommendation: Use plugin-based integration instead. Only implement hold-as-CA if your organization has specific X.509 PKI compliance requirements.

Overview#

The hold-as-CA architecture allows ATCR to generate Notation/Notary v2-compatible signatures by having hold services act as Certificate Authorities that issue X.509 certificates for users.

The Problem#

  • ATProto signatures use K-256 (secp256k1) elliptic curve
  • Notation only supports P-256, P-384, P-521 elliptic curves
  • Cannot convert K-256 signatures to P-256 (different cryptographic curves)
  • Must re-sign with P-256 keys for Notation compatibility

The Solution#

Hold services act as trusted Certificate Authorities (CAs):

  1. User pushes image → Manifest signed by PDS with K-256 (ATProto)
  2. Hold verifies ATProto signature is valid
  3. Hold generates ephemeral P-256 key pair for user
  4. Hold issues X.509 certificate to user's DID
  5. Hold signs manifest with P-256 key
  6. Hold creates Notation signature envelope (JWS format)
  7. Stores both ATProto and Notation signatures

Result: Images have two signatures:

  • ATProto signature (K-256) - Decentralized, DID-based
  • Notation signature (P-256) - Centralized, X.509 PKI

Architecture#

Certificate Chain#

Hold Root CA Certificate (self-signed, P-256)
  └── User Certificate (issued to DID, P-256)
      └── Image Manifest Signature

Hold Root CA:

Subject: CN=ATCR Hold CA - did:web:hold01.atcr.io
Issuer: Self (self-signed)
Key Usage: Digital Signature, Certificate Sign
Basic Constraints: CA=true, pathLen=1
Algorithm: ECDSA P-256
Validity: 10 years

User Certificate:

Subject: CN=did:plc:alice123
SAN: URI:did:plc:alice123
Issuer: Hold Root CA
Key Usage: Digital Signature
Extended Key Usage: Code Signing
Algorithm: ECDSA P-256
Validity: 24 hours (short-lived)

Push Flow#

┌──────────────────────────────────────────────────────┐
│ 1. User: docker push atcr.io/alice/myapp:latest      │
└────────────────────┬─────────────────────────────────┘
                     ↓
┌──────────────────────────────────────────────────────┐
│ 2. AppView stores manifest in alice's PDS            │
│    - PDS signs with K-256 (ATProto standard)         │
│    - Signature stored in repository commit           │
└────────────────────┬─────────────────────────────────┘
                     ↓
┌──────────────────────────────────────────────────────┐
│ 3. AppView requests hold to co-sign                  │
│    POST /xrpc/io.atcr.hold.coSignManifest            │
│    {                                                 │
│      "userDid": "did:plc:alice123",                  │
│      "manifestDigest": "sha256:abc123...",           │
│      "atprotoSignature": {...}                       │
│    }                                                 │
└────────────────────┬─────────────────────────────────┘
                     ↓
┌──────────────────────────────────────────────────────┐
│ 4. Hold verifies ATProto signature                   │
│    a. Resolve alice's DID → public key               │
│    b. Fetch commit from alice's PDS                  │
│    c. Verify K-256 signature                         │
│    d. Ensure signature is valid                      │
│                                                      │
│    If verification fails → REJECT                    │
└────────────────────┬─────────────────────────────────┘
                     ↓
┌──────────────────────────────────────────────────────┐
│ 5. Hold generates ephemeral P-256 key pair           │
│    privateKey := ecdsa.GenerateKey(elliptic.P256())  │
└────────────────────┬─────────────────────────────────┘
                     ↓
┌──────────────────────────────────────────────────────┐
│ 6. Hold issues X.509 certificate                     │
│    Subject: CN=did:plc:alice123                      │
│    SAN: URI:did:plc:alice123                         │
│    Issuer: Hold CA                                   │
│    NotBefore: now                                    │
│    NotAfter: now + 24 hours                          │
│    KeyUsage: Digital Signature                       │
│    ExtKeyUsage: Code Signing                         │
│                                                      │
│    Sign certificate with hold's CA private key       │
└────────────────────┬─────────────────────────────────┘
                     ↓
┌──────────────────────────────────────────────────────┐
│ 7. Hold signs manifest digest                        │
│    hash := SHA256(manifestBytes)                     │
│    signature := ECDSA_P256(hash, privateKey)         │
└────────────────────┬─────────────────────────────────┘
                     ↓
┌──────────────────────────────────────────────────────┐
│ 8. Hold creates Notation JWS envelope                │
│    {                                                 │
│      "protected": {...},                             │
│      "payload": "base64(manifestDigest)",            │
│      "signature": "base64(p256Signature)",           │
│      "header": {                                     │
│        "x5c": [                                      │
│          "base64(userCert)",                         │
│          "base64(holdCACert)"                        │
│        ]                                             │
│      }                                               │
│    }                                                 │
└────────────────────┬─────────────────────────────────┘
                     ↓
┌──────────────────────────────────────────────────────┐
│ 9. Hold returns signature to AppView                 │
└────────────────────┬─────────────────────────────────┘
                     ↓
┌──────────────────────────────────────────────────────┐
│ 10. AppView stores Notation signature                │
│     - Create ORAS artifact manifest                  │
│     - Upload JWS envelope as layer blob              │
│     - Link to image via subject field                │
│     - artifactType: application/vnd.cncf.notary...   │
└──────────────────────────────────────────────────────┘

Verification Flow#

┌──────────────────────────────────────────────────────┐
│ User: notation verify atcr.io/alice/myapp:latest     │
└────────────────────┬─────────────────────────────────┘
                     ↓
┌──────────────────────────────────────────────────────┐
│ 1. Notation queries Referrers API                    │
│    GET /v2/alice/myapp/referrers/sha256:abc123       │
│    → Discovers Notation signature artifact           │
└────────────────────┬─────────────────────────────────┘
                     ↓
┌──────────────────────────────────────────────────────┐
│ 2. Notation downloads JWS envelope                    │
│    - Parses JSON Web Signature                       │
│    - Extracts certificate chain from x5c header      │
└────────────────────┬─────────────────────────────────┘
                     ↓
┌──────────────────────────────────────────────────────┐
│ 3. Notation validates certificate chain              │
│    a. User cert issued by Hold CA? ✓                 │
│    b. Hold CA cert in trust store? ✓                 │
│    c. Certificate not expired? ✓                     │
│    d. Key usage correct? ✓                           │
│    e. Subject matches policy? ✓                      │
└────────────────────┬─────────────────────────────────┘
                     ↓
┌──────────────────────────────────────────────────────┐
│ 4. Notation verifies signature                       │
│    a. Extract public key from user certificate       │
│    b. Compute manifest hash: SHA256(manifest)        │
│    c. Verify: ECDSA_P256(hash, sig, pubKey) ✓        │
└────────────────────┬─────────────────────────────────┘
                     ↓
┌──────────────────────────────────────────────────────┐
│ 5. Success: Image verified ✓                         │
│    Signed by: did:plc:alice123 (via Hold CA)         │
└──────────────────────────────────────────────────────┘

Implementation#

Hold CA Certificate Generation#

// cmd/hold/main.go - CA initialization
func (h *Hold) initializeCA(ctx context.Context) error {
    caKeyPath := filepath.Join(h.config.DataDir, "ca-private-key.pem")
    caCertPath := filepath.Join(h.config.DataDir, "ca-certificate.pem")

    // Load existing CA or generate new one
    if exists(caKeyPath) && exists(caCertPath) {
        h.caKey = loadPrivateKey(caKeyPath)
        h.caCert = loadCertificate(caCertPath)
        return nil
    }

    // Generate P-256 key pair for CA
    caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    if err != nil {
        return fmt.Errorf("failed to generate CA key: %w", err)
    }

    // Create CA certificate template
    serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))

    template := &x509.Certificate{
        SerialNumber: serialNumber,
        Subject: pkix.Name{
            CommonName: fmt.Sprintf("ATCR Hold CA - %s", h.DID),
        },
        NotBefore: time.Now(),
        NotAfter:  time.Now().AddDate(10, 0, 0), // 10 years

        KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
        BasicConstraintsValid: true,
        IsCA: true,
        MaxPathLen: 1, // Can only issue end-entity certificates
    }

    // Self-sign
    certDER, err := x509.CreateCertificate(
        rand.Reader,
        template,
        template, // Self-signed: issuer = subject
        &caKey.PublicKey,
        caKey,
    )
    if err != nil {
        return fmt.Errorf("failed to create CA certificate: %w", err)
    }

    caCert, _ := x509.ParseCertificate(certDER)

    // Save to disk (0600 permissions)
    savePrivateKey(caKeyPath, caKey)
    saveCertificate(caCertPath, caCert)

    h.caKey = caKey
    h.caCert = caCert

    log.Info("Generated new CA certificate", "did", h.DID, "expires", caCert.NotAfter)
    return nil
}

User Certificate Issuance#

// pkg/hold/cosign.go
func (h *Hold) issueUserCertificate(userDID string) (*x509.Certificate, *ecdsa.PrivateKey, error) {
    // Generate ephemeral P-256 key for user
    userKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    if err != nil {
        return nil, nil, fmt.Errorf("failed to generate user key: %w", err)
    }

    serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))

    // Parse DID for SAN
    sanURI, _ := url.Parse(userDID)

    template := &x509.Certificate{
        SerialNumber: serialNumber,
        Subject: pkix.Name{
            CommonName: userDID,
        },
        URIs: []*url.URL{sanURI}, // Subject Alternative Name

        NotBefore: time.Now(),
        NotAfter:  time.Now().Add(24 * time.Hour), // Short-lived: 24 hours

        KeyUsage: x509.KeyUsageDigitalSignature,
        ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
        BasicConstraintsValid: true,
        IsCA: false,
    }

    // Sign with hold's CA key
    certDER, err := x509.CreateCertificate(
        rand.Reader,
        template,
        h.caCert, // Issuer: Hold CA
        &userKey.PublicKey,
        h.caKey, // Sign with CA private key
    )
    if err != nil {
        return nil, nil, fmt.Errorf("failed to create user certificate: %w", err)
    }

    userCert, _ := x509.ParseCertificate(certDER)

    return userCert, userKey, nil
}

Co-Signing XRPC Endpoint#

// pkg/hold/oci/xrpc.go
func (s *Server) handleCoSignManifest(ctx context.Context, req *CoSignRequest) (*CoSignResponse, error) {
    // 1. Verify caller is authenticated
    did, err := s.auth.VerifyToken(ctx, req.Token)
    if err != nil {
        return nil, fmt.Errorf("authentication failed: %w", err)
    }

    // 2. Verify ATProto signature
    valid, err := s.verifyATProtoSignature(ctx, req.UserDID, req.ManifestDigest, req.ATProtoSignature)
    if err != nil || !valid {
        return nil, fmt.Errorf("ATProto signature verification failed: %w", err)
    }

    // 3. Issue certificate for user
    userCert, userKey, err := s.hold.issueUserCertificate(req.UserDID)
    if err != nil {
        return nil, fmt.Errorf("failed to issue certificate: %w", err)
    }

    // 4. Sign manifest with user's key
    manifestHash := sha256.Sum256([]byte(req.ManifestDigest))
    signature, err := ecdsa.SignASN1(rand.Reader, userKey, manifestHash[:])
    if err != nil {
        return nil, fmt.Errorf("failed to sign manifest: %w", err)
    }

    // 5. Create JWS envelope
    jws, err := s.createJWSEnvelope(signature, userCert, s.hold.caCert, req.ManifestDigest)
    if err != nil {
        return nil, fmt.Errorf("failed to create JWS: %w", err)
    }

    return &CoSignResponse{
        JWS: jws,
        Certificate: encodeCertificate(userCert),
        CACertificate: encodeCertificate(s.hold.caCert),
    }, nil
}

Trust Model#

Centralization Analysis#

ATProto Model (Decentralized):

  • Each PDS is independent
  • User controls which PDS to use
  • Trust user's DID, not specific infrastructure
  • PDS compromise affects only that PDS's users
  • Multiple PDSs provide redundancy

Hold-as-CA Model (Centralized):

  • Hold acts as single Certificate Authority
  • All users must trust hold's CA certificate
  • Hold compromise = attacker can issue certificates for ANY user
  • Hold becomes single point of failure
  • Users depend on hold operator honesty

What Hold Vouches For#

When hold issues a certificate, it attests:

"I verified that [DID] signed this manifest with ATProto"

  • Hold validated ATProto signature
  • Hold confirmed signature matches user's DID
  • Hold checked signature at specific time

"This image is safe"

  • Hold does NOT audit image contents
  • Certificate ≠ vulnerability scan
  • Signature ≠ security guarantee

"I control this DID"

  • Hold does NOT control user's DID
  • DID ownership is independent
  • Hold cannot revoke DIDs

Threat Model#

Scenario 1: Hold Private Key Compromise

Attack:

  • Attacker steals hold's CA private key
  • Can issue certificates for any DID
  • Can sign malicious images as any user

Impact:

  • CRITICAL - All users affected
  • Attacker can impersonate any user
  • All signatures become untrustworthy

Detection:

  • Certificate Transparency logs (if implemented)
  • Unusual certificate issuance patterns
  • Users report unexpected signatures

Mitigation:

  • Store CA key in Hardware Security Module (HSM)
  • Strict access controls
  • Audit logging
  • Regular key rotation

Recovery:

  • Revoke compromised CA certificate
  • Generate new CA certificate
  • Re-issue all active certificates
  • Notify all users
  • Update trust stores

Scenario 2: Malicious Hold Operator

Attack:

  • Hold operator issues certificates without verifying ATProto signatures
  • Hold operator signs malicious images
  • Hold operator backdates certificates

Impact:

  • HIGH - Trust model broken
  • Users receive signed malicious images
  • Difficult to detect without ATProto cross-check

Detection:

  • Compare Notation signature timestamp with ATProto commit time
  • Verify ATProto signature exists independently
  • Monitor hold's signing patterns

Mitigation:

  • Audit trail linking certificates to ATProto signatures
  • Public transparency logs
  • Multi-signature requirements
  • Periodically verify ATProto signatures

Recovery:

  • Identify malicious certificates
  • Revoke hold's CA trust
  • Switch to different hold
  • Re-verify all images

Scenario 3: Certificate Theft

Attack:

  • Attacker steals issued user certificate + private key
  • Uses it to sign malicious images

Impact:

  • LOW-MEDIUM - Limited scope
  • Affects only specific user/image
  • Short validity period (24 hours)

Detection:

  • Unexpected signature timestamps
  • Images signed from unknown locations

Mitigation:

  • Short certificate validity (24 hours)
  • Ephemeral keys (not stored long-term)
  • Certificate revocation if detected

Recovery:

  • Wait for certificate expiration (24 hours)
  • Revoke specific certificate
  • Investigate compromise source

Certificate Management#

Expiration Strategy#

Short-Lived Certificates (24 hours):

Pros:

  • ✅ Minimal revocation infrastructure needed
  • ✅ Compromise window is tiny
  • ✅ Automatic cleanup
  • ✅ Lower CRL/OCSP overhead

Cons:

  • ❌ Old images become unverifiable quickly
  • ❌ Requires re-signing for historical verification
  • ❌ Storage: multiple signatures for same image

Solution: On-Demand Re-Signing

User pulls old image → Notation verification fails (expired cert)
→ User requests re-signing: POST /xrpc/io.atcr.hold.reSignManifest
→ Hold verifies ATProto signature still valid
→ Hold issues new certificate (24 hours)
→ Hold creates new Notation signature
→ User can verify with fresh certificate

Revocation#

Certificate Revocation List (CRL):

Hold publishes CRL at: https://hold01.atcr.io/ca.crl

Notation configured to check CRL:
{
  "trustPolicies": [{
    "name": "atcr-images",
    "signatureVerification": {
      "verificationLevel": "strict",
      "override": {
        "revocationValidation": "strict"
      }
    }
  }]
}

OCSP (Online Certificate Status Protocol):

  • Hold runs OCSP responder: https://hold01.atcr.io/ocsp
  • Real-time certificate status checks
  • Lower overhead than CRL downloads

Revocation Triggers:

  • Key compromise detected
  • Malicious signing detected
  • User request
  • DID ownership change

CA Key Rotation#

Rotation Procedure:

  1. Generate new CA key pair
  2. Create new CA certificate
  3. Cross-sign old CA with new CA (transition period)
  4. Distribute new CA certificate to all users
  5. Begin issuing with new CA for new signatures
  6. Grace period (30 days): Accept both old and new CA
  7. Retire old CA after grace period

Frequency: Every 2-3 years (longer than short-lived certs)

Trust Store Distribution#

Problem#

Users must add hold's CA certificate to their Notation trust store for verification to work.

Manual Distribution#

# 1. Download hold's CA certificate
curl https://hold01.atcr.io/ca.crt -o hold01-ca.crt

# 2. Verify fingerprint (out-of-band)
openssl x509 -in hold01-ca.crt -fingerprint -noout
# Compare with published fingerprint

# 3. Add to Notation trust store
notation cert add --type ca --store atcr-holds hold01-ca.crt

Automated Distribution#

ATCR CLI tool:

atcr trust add hold01.atcr.io
# → Fetches CA certificate
# → Verifies via HTTPS + DNSSEC
# → Adds to Notation trust store
# → Configures trust policy

atcr trust list
# → Shows trusted holds with fingerprints

System-Wide Trust#

For enterprise deployments:

Debian/Ubuntu:

# Install CA certificate system-wide
cp hold01-ca.crt /usr/local/share/ca-certificates/atcr-hold01.crt
update-ca-certificates

RHEL/CentOS:

cp hold01-ca.crt /etc/pki/ca-trust/source/anchors/
update-ca-trust

Container images:

FROM ubuntu:22.04
COPY hold01-ca.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates

Configuration#

Hold Service#

Environment variables:

# Enable co-signing feature
HOLD_COSIGN_ENABLED=true

# CA certificate and key paths
HOLD_CA_CERT_PATH=/var/lib/atcr/hold/ca-certificate.pem
HOLD_CA_KEY_PATH=/var/lib/atcr/hold/ca-private-key.pem

# Certificate validity
HOLD_CERT_VALIDITY_HOURS=24

# OCSP responder
HOLD_OCSP_ENABLED=true
HOLD_OCSP_URL=https://hold01.atcr.io/ocsp

# CRL distribution
HOLD_CRL_ENABLED=true
HOLD_CRL_URL=https://hold01.atcr.io/ca.crl

Notation Trust Policy#

{
  "version": "1.0",
  "trustPolicies": [{
    "name": "atcr-images",
    "registryScopes": ["atcr.io/*/*"],
    "signatureVerification": {
      "level": "strict",
      "override": {
        "revocationValidation": "strict"
      }
    },
    "trustStores": ["ca:atcr-holds"],
    "trustedIdentities": [
      "x509.subject: CN=did:plc:*",
      "x509.subject: CN=did:web:*"
    ]
  }]
}

When to Use Hold-as-CA#

✅ Use When#

Enterprise X.509 PKI Compliance:

  • Organization requires standard X.509 certificates
  • Existing security policies mandate PKI
  • Audit requirements for certificate chains
  • Integration with existing CA infrastructure

Tool Compatibility:

  • Must use standard Notation without plugins
  • Cannot deploy custom verification tools
  • Existing tooling expects X.509 signatures

Centralized Trust Acceptable:

  • Organization already uses centralized trust model
  • Hold operator is internal/trusted team
  • Centralization risk is acceptable trade-off

❌ Don't Use When#

Default Deployment:

  • Most users should use plugin-based approach
  • Plugins maintain decentralization
  • Plugins reuse existing ATProto signatures

Small Teams / Startups:

  • Certificate management overhead too high
  • Don't need X.509 compliance
  • Prefer simpler architecture

Maximum Decentralization Required:

  • Cannot accept hold as single trust point
  • Must maintain pure ATProto model
  • Centralization contradicts project goals

Comparison: Hold-as-CA vs. Plugins#

Aspect Hold-as-CA Plugin Approach
Standard compliance ✅ Full X.509/PKI ⚠️ Custom verification
Tool compatibility ✅ Notation works unchanged ❌ Requires plugin install
Decentralization ❌ Centralized (hold CA) ✅ Decentralized (DIDs)
ATProto alignment ❌ Against philosophy ✅ ATProto-native
Signature reuse ❌ Must re-sign (P-256) ✅ Reuses ATProto (K-256)
Certificate mgmt 🔴 High overhead 🟢 None
Trust distribution 🔴 Must distribute CA cert 🟢 DID resolution
Hold compromise 🔴 All users affected 🟢 Metadata only
Operational cost 🔴 High 🟢 Low
Use case Enterprise PKI General purpose

Recommendations#

Default Approach: Plugins#

For most deployments, use plugin-based verification:

  • Ratify plugin for Kubernetes
  • OPA Gatekeeper provider for policy enforcement
  • Containerd verifier for runtime checks
  • atcr-verify CLI for general purpose

See Integration Strategy for details.

Optional: Hold-as-CA for Enterprise#

Only implement hold-as-CA if you have specific requirements:

  • Enterprise X.509 PKI mandates
  • Cannot use plugins (restricted environments)
  • Accept centralization trade-off

Implement as opt-in feature:

# Users explicitly enable co-signing
docker push atcr.io/alice/myapp:latest --sign=notation

# Or via environment variable
export ATCR_ENABLE_COSIGN=true
docker push atcr.io/alice/myapp:latest

Security Best Practices#

If implementing hold-as-CA:

  1. Store CA key in HSM - Never on filesystem
  2. Audit all certificate issuance - Log every cert
  3. Public transparency log - Publish all certificates
  4. Short certificate validity - 24 hours max
  5. Monitor unusual patterns - Alert on anomalies
  6. Regular CA key rotation - Every 2-3 years
  7. Cross-check ATProto - Verify both signatures match
  8. Incident response plan - Prepare for compromise

See Also#