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

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#