# 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](./INTEGRATION_STRATEGY.md) 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 ```go // 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 ```go // 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 ```go // 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 ```bash # 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:** ```bash 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:** ```bash # Install CA certificate system-wide cp hold01-ca.crt /usr/local/share/ca-certificates/atcr-hold01.crt update-ca-certificates ``` **RHEL/CentOS:** ```bash cp hold01-ca.crt /etc/pki/ca-trust/source/anchors/ update-ca-trust ``` **Container images:** ```dockerfile FROM ubuntu:22.04 COPY hold01-ca.crt /usr/local/share/ca-certificates/ RUN update-ca-certificates ``` ## Configuration ### Hold Service **Environment variables:** ```bash # 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 ```json { "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](./INTEGRATION_STRATEGY.md) - 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](./INTEGRATION_STRATEGY.md) 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:** ```bash # 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 - [ATProto Signatures](./ATPROTO_SIGNATURES.md) - How ATProto signing works - [Integration Strategy](./INTEGRATION_STRATEGY.md) - Overview of integration approaches - [Signature Integration](./SIGNATURE_INTEGRATION.md) - Tool-specific integration guides