Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Proposal: Certificates, TLS, and Certificate Transparency

Capability-native abstractions for X.509 certificates, trust stores, chain verification, Certificate Transparency (CT), revocation, pinning, automated issuance (ACME), and the TLS contexts built from all of these.

Why a Separate Proposal

Keys and certificates are related but different concerns. Keys are secret material whose contract is “compute with me.” Certificates are public assertions whose contract is “believe this identity, if the chain and CT/revocation evidence pass policy.” The two failure modes (key compromise vs. mis-issuance, revocation vs. renewal, HSM custody vs. CA trust) barely overlap.

cryptography-and-key-management-proposal.md already covers SymmetricKey, PrivateKey, PublicKey, KeySource, and KeyVault. This proposal covers everything on top: certificates, trust anchors, CT logs, OCSP, CRLs, pinning, ACME, and TLS configuration. A TLS server is composed from a PrivateKey cap (from the key proposal) plus the certificate/verification/revocation caps defined here.

Problem

capOS will need certificate and TLS infrastructure for:

Without a shared abstraction each consumer invents its own “where do trust anchors live”, its own CT policy (or skips CT silently), its own revocation story (or skips revocation silently), and its own config surface for rustls. That is how the Linux ecosystem ended up with /etc/ssl/certs, NSS, GnuTLS’ own store, OpenSSL’s SSL_CTX, update-ca-certificates, and per-language HTTPS clients with divergent trust policies. capOS is young enough to avoid that.

Design Principle: Certificates Are Typed Capabilities

A certificate in capOS is a Certificate CapObject, not an opaque byte blob flowing between services. Trust evaluation, CT and revocation policy, and TLS configuration are expressed as cap compositions — never as well-known paths (/etc/ssl/certs) or library singletons (rustls::RootCertStore::load_native_certs()).

Consequences mirror the key-cap case:

  • Attenuation by scope. A service that only needs to verify one signer receives a TrustStore cap containing that one anchor, not the full Mozilla root bundle. A service that must not bypass CT receives a CertVerifier whose policy has minScts >= 2; no method on that cap lets the caller lower the bar.
  • Revocation is a cap drop. A compromised anchor is removed from the TrustStore it lives in; holders of a stale restricted view that still trusts it keep trusting it until they pick up the new version. No library’s “just reload the roots” ambient step.
  • Audit is intrinsic. Every verifyChain, every addAnchor, every OCSP query flows through the audit cap. A service that bypasses revocation shows up in the audit log as a service that stopped calling OcspResponder.status.
  • Rotation without restart. A TLS server holds a CertificateStore.watch subscription; when an ACME renewal lands a fresh chain under the server’s handle, the TLS stack swaps chains on the next handshake. No filesystem signaling, no SIGHUP, no “reloaded 0 of 1 certs” log lines.
  • Composition, not configuration. A TlsServerConfig is a cap that encapsulates the key, chain source, stapler, client-auth verifier, and cipher policy. Building a TLS server means acquiring those caps and composing them, not filling in a struct with raw bytes.

Schemas

Certificates and chains

interface Certificate {
    # Raw DER encoding — for logging, CT submission, export.
    der             @0 () -> (encoded :Data);

    # Structured fields — callers should prefer these over re-parsing.
    subject         @1 () -> (name :DistinguishedName);
    issuer          @2 () -> (name :DistinguishedName);
    serial          @3 () -> (bytes :Data);
    notBefore       @4 () -> (epochSeconds :Int64);
    notAfter        @5 () -> (epochSeconds :Int64);
    subjectAltNames @6 () -> (names :List(GeneralName));

    # Public key as a cap — callers verify signatures through this.
    publicKey       @7 () -> (pk :PublicKey);

    # Extensions the platform cares about. Returning typed views
    # forces the implementation to parse once.
    keyUsage        @8 () -> (usage :KeyUsageFlags);
    extendedKeyUsage @9 () -> (ekus :List(ExtendedKeyUsage));
    basicConstraints @10 () -> (ca :Bool, pathLenConstraint :Int32);
    nameConstraints  @11 () -> (constraints :NameConstraints);

    # Embedded SCTs (RFC 6962 §3.3). Callers that only allow
    # CT-qualified certs filter on this.
    embeddedScts    @12 () -> (scts :List(SignedCertificateTimestamp));

    # Must-staple marker (RFC 7633).
    mustStaple      @13 () -> (required :Bool);

    # Fingerprint used for pinning, logging, and human display.
    fingerprint     @14 (hash :HashAlgorithm) -> (digest :Data);

    info            @15 () -> (kind :CertificateKind,
                               algorithm :AsymmetricAlgorithm);
}

interface CertificateChain {
    # Leaf first, root (or closest-to-root) last. Length-one chains are
    # permitted (self-signed leaf).
    certificates    @0 () -> (chain :List(Certificate));
    leaf            @1 () -> (cert :Certificate);

    # Convenience: verify this chain against a trust store using a
    # given verifier. Shortcuts the CertVerifier flow for simple cases.
    verify          @2 (against :TrustStore,
                        verifier :CertVerifier,
                        atEpochSeconds :Int64,
                        hostname :Text)
                    -> (outcome :VerificationOutcome);
}

enum CertificateKind {
    endEntity    @0;
    intermediate @1;
    trustAnchor  @2;
    crossSigned  @3;
}

GeneralName, DistinguishedName, KeyUsageFlags, ExtendedKeyUsage, and NameConstraints are plain struct/enum definitions mirroring RFC 5280 (omitted here for brevity).

Trust stores

interface TrustStore {
    # List anchors as certs. Consumers typically only need metadata,
    # but anchors themselves are Certificate caps so the full
    # structured surface is available.
    anchors         @0 () -> (certs :List(Certificate));

    # Attenuate to a subset (e.g. only WebPKI roots, only corporate
    # CAs, only a specific CA). The resulting cap is a fresh
    # TrustStore that no longer references anchors outside the filter.
    restrict        @1 (filter :TrustFilter) -> (subset :TrustStore);

    # Add a trusted anchor. Only holders with write authority succeed;
    # read-only TrustStore caps reject this method.
    addAnchor       @2 (cert :Certificate, pin :AnchorPin) -> ();

    # Remove an anchor. Matches either fingerprint or subject DN.
    removeAnchor    @3 (selector :AnchorSelector) -> ();

    # Monotonic version bumped on every mutation; consumers cache by
    # version to avoid revalidating unchanged trust chains.
    version         @4 () -> (n :UInt64);
}

struct TrustFilter {
    purposes        @0 :List(CertPurpose);     # Only anchors usable for these
    fingerprints    @1 :List(Data);            # Allow-list by SHA-256
    subjects        @2 :List(Data);            # Allow-list by subject DN
    excludeFingerprints @3 :List(Data);        # Deny-list
}

struct AnchorPin {
    spkiHash        @0 :Data;                  # SHA-256 of SPKI
    hashAlgorithm   @1 :HashAlgorithm;
}

struct AnchorSelector {
    union {
        fingerprint @0 :Data;
        subject     @1 :DistinguishedName;
    }
}

enum CertPurpose {
    tlsServerAuth   @0;
    tlsClientAuth   @1;
    codeSigning     @2;
    emailSmime      @3;
    clientIdentity  @4;
    ctLog           @5;   # TrustStore of CT log public keys
    ocspSigning     @6;
    webauthnRoot    @7;   # FIDO metadata / attestation roots
}

Verifier

interface CertVerifier {
    verifyChain     @0 (chain :CertificateChain,
                        trust :TrustStore,
                        purpose :CertPurpose,
                        atEpochSeconds :Int64,
                        hostname :Text)
                    -> (outcome :VerificationOutcome);

    # Thin wrapper over a single signature check against a cert's
    # public key. Useful for WebAuthn attestation, signed manifests,
    # signed audit records.
    verifySignature @1 (cert :Certificate,
                        message :Data,
                        signature :Data,
                        scheme :SignatureScheme)
                    -> (ok :Bool);

    policy          @2 () -> (policy :VerificationPolicy);
}

struct VerificationPolicy {
    minScts                 @0 :UInt8;
    ctLogs                  @1 :TrustStore;  # which logs count
    allowedAlgorithms       @2 :List(AsymmetricAlgorithm);
    allowedSignatureSchemes @3 :List(SignatureScheme);
    requireOcsp             @4 :Bool;
    maxChainLength          @5 :UInt8;
    permitNameConstraints   @6 :Bool;
    clockSkewSeconds        @7 :UInt32;
    # When set, certificates not carrying the must-staple extension
    # are still required to deliver a stapled OCSP response.
    staplingRequired        @8 :Bool;
}

struct VerificationOutcome {
    union {
        valid   @0 :ValidChain;
        invalid @1 :VerificationFailure;
    }
}

struct ValidChain {
    anchor      @0 :Certificate;
    sctCount    @1 :UInt8;
    ocspStatus  @2 :OcspStatus;
    notAfter    @3 :Int64;     # min notAfter across the chain
}

struct VerificationFailure {
    reason      @0 :FailureReason;
    detail      @1 :Text;
}

enum FailureReason {
    unknownAnchor           @0;
    expired                 @1;
    notYetValid             @2;
    signatureMismatch       @3;
    nameMismatch            @4;
    insufficientScts        @5;
    revoked                 @6;
    ocspUnavailable         @7;
    weakAlgorithm           @8;
    policyViolation         @9;
    badEku                  @10;
    chainTooLong            @11;
    nameConstraintViolation @12;
    mustStapleMissing       @13;
    pinMismatch             @14;
}

Default VerificationPolicy presets:

  • webPkiStrictminScts = 2, requireOcsp = true, allowed algorithms and schemes drawn from Mozilla’s “modern” profile.
  • webPkiLenientminScts = 0, requireOcsp = false. Used by low-value clients where misrouting is acceptable.
  • privateMtlsminScts = 0, requireOcsp = true, maxChainLength = 3. Used between capOS services holding CA-issued identity certs.
  • codeSigningminScts = 0, long notAfter tolerances, narrow allowed EKU set.

Certificate Transparency

capOS treats CT as a first-class verification input, not an add-on. Consumers that need WebPKI trust configure a CertVerifier with minScts >= 2 and a ctLogs trust store; verification fails closed if the leaf lacks that many valid SCTs signed by logs the policy accepts.

struct SignedCertificateTimestamp {
    logId              @0 :Data;    # SHA-256 of the log's public key
    timestamp          @1 :UInt64;  # ms since epoch
    extensions         @2 :Data;
    signature          @3 :Data;
    hashAlgorithm      @4 :HashAlgorithm;
    signatureAlgorithm @5 :SignatureScheme;
    origin             @6 :SctOrigin;
}

enum SctOrigin {
    embedded      @0;   # X.509 extension (RFC 6962 §3.3)
    ocspStapled   @1;   # OCSP response extension
    tlsExtension  @2;   # TLS handshake extension
}

interface CtLog {
    # Submission — used by ACME responders and capOS-internal CAs to
    # obtain SCTs before serving newly issued certs.
    addChain          @0 (chain :CertificateChain)
                      -> (sct :SignedCertificateTimestamp);
    addPreChain       @1 (precert :CertificateChain)
                      -> (sct :SignedCertificateTimestamp);

    # Monitoring — STH, entries, consistency proofs.
    signedTreeHead    @2 () -> (sth :SignedTreeHead);
    entries           @3 (start :UInt64, count :UInt32)
                      -> (entries :List(LogEntry));
    consistencyProof  @4 (first :UInt64, second :UInt64)
                      -> (proof :List(Data));

    info              @5 () -> (name :Text,
                                publicKey :PublicKey,
                                url :Text);
}

interface CtMonitor {
    # Watch for certificates issued under a subject-name pattern (for
    # phishing / mis-issuance detection). Events flow to the audit cap.
    watchSubject      @0 (pattern :Text) -> (subscription :CtSubscription);
    listWatched       @1 () -> (subscriptions :List(CtSubscription));
}

interface CtSubscription {
    events            @0 () -> (events :List(CtEvent));  # since last call
    cancel            @1 () -> ();
}

struct SignedTreeHead {
    treeSize       @0 :UInt64;
    timestamp      @1 :UInt64;
    rootHash       @2 :Data;
    signature      @3 :Data;
}

struct LogEntry {
    index          @0 :UInt64;
    timestamp      @1 :UInt64;
    entryType      @2 :CtEntryType;
    certificate    @3 :Data;   # ASN.1 TimestampedEntry payload
}

enum CtEntryType {
    x509Entry      @0;
    precertEntry   @1;
}

struct CtEvent {
    union {
        observed   @0 :CtObservation;
        error      @1 :CtWatchError;
    }
}

struct CtObservation {
    log          @0 :Text;           # log name or URL
    index        @1 :UInt64;
    certificate  @2 :Certificate;
    matched      @3 :Text;           # matched pattern
}

CT integration depends on networking and audit being available. A capOS build without networking falls back to minScts = 0 and skips monitoring. The CtMonitor service is optional — its absence means capOS does not detect mis-issuance against its own domains but does not affect leaf verification, which uses only embeddedScts and any SCTs delivered in the TLS handshake.

The log trust store (the ctLogs field of VerificationPolicy) is itself a TrustStore cap, populated from Chrome’s CT log list with the same bundling and signing approach used for WebPKI roots. CT logs are rotated regularly; the log list is the first place a deployment without fresh updates starts failing in a visible way, which is the intended failure mode.

Revocation

interface OcspResponder {
    # Query an OCSP responder for status. `issuer` supplies the cert
    # used to verify the responder signature chain back to a trust
    # anchor.
    status    @0 (cert :Certificate,
                  issuer :Certificate,
                  atEpochSeconds :Int64)
              -> (response :OcspResponse);
}

interface OcspStapler {
    # TLS server side: fetch and cache an OCSP response for the
    # server's own certificate. The TLS stack staples the cached
    # response into every handshake.
    currentResponse  @0 () -> (response :OcspResponse);
    refresh          @1 () -> ();
    setCertificate   @2 (chain :CertificateChain,
                         responder :OcspResponder) -> ();
}

interface CrlStore {
    # Look up a CRL for a given issuer DN; fallback when OCSP is
    # unavailable. Discouraged; CRLs do not scale.
    crlFor    @0 (issuer :DistinguishedName) -> (crl :Data);
    contains  @1 (issuer :DistinguishedName, serial :Data)
              -> (revoked :Bool);
}

struct OcspResponse {
    der         @0 :Data;        # RFC 6960 DER-encoded response
    status      @1 :OcspStatus;
    thisUpdate  @2 :Int64;
    nextUpdate  @3 :Int64;
}

enum OcspStatus {
    good                 @0;
    revoked              @1;
    unknown              @2;
    stapledAbsent        @3;   # handshake carried no stapled response
    responderUnreachable @4;
}

Policy choices capOS bakes into the defaults:

  • VerificationPolicy.requireOcsp = true means OCSP-unreachable is a hard verification failure. Default for CertPurpose.tlsClientAuth on services facing untrusted networks; soft-fail otherwise.
  • A certificate carrying the id-pe-tlsfeature must-staple extension fails verification if no stapled response is present, regardless of requireOcsp.
  • VerificationPolicy.staplingRequired = true extends must-staple behavior to all certs checked under that verifier, not only the ones that set the extension.
  • CRL support exists for legacy compatibility and explicit code-signing fallback. Services that can choose prefer OCSP stapling, which pulls revocation latency to handshake time without leaking the client’s identity to the responder.

Pinning

interface PinSet {
    # A pin set is a list of (SPKI-hash, algorithm) pairs. Verification
    # succeeds only if at least one cert in the chain has an SPKI hash
    # matching a pin.
    pins        @0 () -> (entries :List(Pin));
    enforce     @1 (chain :CertificateChain) -> (outcome :VerificationOutcome);
    addPin      @2 (pin :Pin) -> ();
    removePin   @3 (pin :Pin) -> ();
    info        @4 () -> (mode :PinMode, expires :Int64);
}

struct Pin {
    spkiHash        @0 :Data;
    hashAlgorithm   @1 :HashAlgorithm;
}

enum PinMode {
    enforce     @0;   # fail closed on mismatch
    reportOnly  @1;   # succeed; emit audit event
}

A PinSet restricts an already-trusted chain; it does not add trust. Composition is intersection: trust + CT + OCSP + pins must all pass for verification to succeed. Pin sets are per-consumer; the web shell gateway’s client-side ACME challenge fetches do not share a pin set with the fleet mTLS layer.

Issuance and renewal

ACME is the only supported issuance protocol for v1. Challenge solvers are caps so the ACME client has no ambient authority over DNS or the HTTP server. Self-signing and internal-CA use cases are covered by a separate CertificateAuthority cap (future work, see Open Questions).

interface AcmeClient {
    # Register or rediscover an account using an account key cap.
    register    @0 (accountKey :PrivateKey, contact :List(Text))
                -> (account :AcmeAccount);

    # Order a certificate for a list of identifiers.
    order       @1 (account :AcmeAccount,
                    identifiers :List(AcmeIdentifier),
                    certKey :PrivateKey,
                    solver :ChallengeSolver)
                -> (chain :CertificateChain);

    # Renew a previously-issued chain when notAfter is near.
    renew       @2 (chain :CertificateChain,
                    certKey :PrivateKey,
                    solver :ChallengeSolver)
                -> (chain :CertificateChain);

    # Revoke a cert.
    revoke      @3 (cert :Certificate, reason :RevocationReason) -> ();

    directory   @4 () -> (url :Text, meta :AcmeDirectoryMeta);
}

interface ChallengeSolver {
    # Publish a challenge token and wait for the ACME server to
    # validate. The solver owns whatever authority is required —
    # DNS record write, HTTP server handler registration, TLS-ALPN
    # responder slot — and nothing more.
    solve       @0 (challenge :AcmeChallenge) -> (ok :Bool);
    cleanup     @1 (challenge :AcmeChallenge) -> ();
    supports    @2 () -> (types :List(AcmeChallengeType));
}

enum AcmeChallengeType {
    http01      @0;
    dns01       @1;
    tlsAlpn01   @2;
}

struct AcmeIdentifier {
    type        @0 :Text;    # "dns", "ip", ...
    value       @1 :Text;
}

interface CertificateStore {
    # Store a certificate chain under a stable handle; used by TLS
    # servers to retrieve the current chain on handshake.
    put         @0 (handle :Text, chain :CertificateChain) -> ();
    get         @1 (handle :Text) -> (chain :CertificateChain);
    list        @2 () -> (handles :List(Text));
    delete      @3 (handle :Text) -> ();
    watch       @4 (handle :Text) -> (subscription :CertSubscription);
}

interface CertSubscription {
    events      @0 () -> (events :List(CertRotationEvent));
    cancel      @1 () -> ();
}

struct CertRotationEvent {
    handle      @0 :Text;
    newChain    @1 :CertificateChain;
    rotatedAt   @2 :Int64;
}

The CertificateStore.watch subscription is the point at which an ACME renewal service notifies a TLS server to rotate its chain. The TLS server does not poll files, no filesystem signaling is involved, and rotation is atomic from a handshake’s perspective.

TLS configuration

interface TlsServerConfig {
    key             @0 () -> (k :PrivateKey);
    chainSource     @1 () -> (store :CertificateStore, handle :Text);
    stapler         @2 () -> (s :OcspStapler);

    # Optional: require client auth against these verifier + trust
    # caps. If unset, the server accepts any client or no client.
    clientVerifier  @3 () -> (v :CertVerifier, trust :TrustStore);

    alpn            @4 () -> (protocols :List(Text));
    minVersion      @5 () -> (v :TlsVersion);
    cipherPolicy    @6 () -> (policy :CipherPolicy);
}

interface TlsClientConfig {
    verifier        @0 () -> (v :CertVerifier);
    trust           @1 () -> (t :TrustStore);
    pins            @2 () -> (p :PinSet);     # null for no pinning
    clientAuth      @3 () -> (k :PrivateKey, chain :CertificateChain);
    alpn            @4 () -> (protocols :List(Text));
    minVersion      @5 () -> (v :TlsVersion);
    serverNameOverride @6 () -> (host :Text);
}

enum TlsVersion {
    tls12           @0;
    tls13           @1;
}

enum CipherPolicy {
    modern          @0;  # TLS 1.3 + AEAD only; Mozilla "modern"
    intermediate    @1;  # TLS 1.2 + 1.3; Mozilla "intermediate"
    legacy          @2;  # Explicit opt-in for ancient peers
}

The TLS stack (rustls in the first implementation) consumes a TlsServerConfig or TlsClientConfig cap plus a raw TcpSocket and produces a TlsSocket. The TlsSocket interface lives in networking-proposal.md; this proposal only defines the configuration surface.

Trust Anchor Bootstrap

The v1 trust anchor bundle is Mozilla’s NSS store, synthesized from the webpki-roots crate data embedded in the boot manifest. Rationale: the bundle is well-curated, auditable (Mozilla’s CA Certificate Program publishes policy and meeting minutes), and already the de facto default for every Rust TLS stack. capOS does not invent a new root program.

CT log lists follow the same pattern, drawn from Chrome’s published CT log list.

Update policy:

  • Root-store bundles are versioned and signed. addAnchor on the system TrustStore is restricted to the trust-admin service, which accepts bundles whose signature chains to a build-time key embedded in the boot manifest.
  • Deployment overrides (corporate CAs, explicit Mozilla-root removal) compose with the Mozilla bundle via TrustStore.restrict and addAnchor on an override store. Overrides are themselves signed and manifest-addressable.
  • Replacement ships as a manifest update (see storage-and-naming-proposal.md Open Question #5 on manifest signing).

The manifest-embedded root store has no background network update path by design. A compromised root requires a new signed manifest, which requires the measured-boot chain. Root updates are a deliberate operational event, not a silent refresh. This is a deliberate trade-off against the Linux-style ca-certificates package that updates on every apt run.

Consumers

ConsumerUses
Web text shell gatewayTlsServerConfig + OcspStapler; cert from AcmeClient
Inter-service mTLSTlsServerConfig + TlsClientConfig with private-PKI TrustStore
Outbound HTTPS clients (KMS, IMDS)TlsClientConfig with WebPKI-strict verifier
WebAuthn attestation verificationCertVerifier.verifySignature with FIDO MDS TrustStore
Code signing verificationCertVerifier with codeSigning trust store + OCSP
Signed manifest verificationCertVerifier.verifySignature + pinned build-time root
CT mis-issuance monitoringCtMonitor.watchSubject on capOS-owned domains

Threat Model

Specific to this subsystem, independent of the crypto/key threat model:

  1. Bogus CA in the trust store. Compromise of any CA in the trust store compromises every cert the verifier accepts. Mitigations: restrict the trust store as narrowly as each consumer permits (private-PKI services use a private-PKI-only store, not WebPKI); require CT for tlsServerAuth; enable CtMonitor for capOS-owned subject patterns.
  2. CT log compromise or collusion. A log signs a non-existent certificate. Mitigations: require SCTs from multiple independent logs (minScts >= 2); enforce log list freshness (policy rejects SCTs from retired or disqualified logs); monitor STH inclusion proofs for capOS-issued certs.
  3. OCSP responder compromise. The responder signs “good” for a revoked cert. Mitigations: OCSP response signature chains back to a trust anchor via the OCSP-signing EKU; short nextUpdate windows limit stale “good” responses; fail-closed when requireOcsp is set.
  4. Stapling stripping. A MITM strips OCSP staples between a compliant server and the client. Mitigations: must-staple extension on the server cert forces closed-fail; client-side staplingRequired policy extends this to all certs.
  5. Name-constraint bypass. An intermediate CA issues for names outside its constrained scope. Mitigations: permitNameConstraints always on; verifier enforces name constraints before reporting success.
  6. Pin brittleness. A pin prevents legitimate rotation, locking out users. Mitigations: short pin expiries, reportOnly mode for rollout, pins bound to SPKI (not to full certificates).
  7. ACME challenge hijack. A challenge solver with excessive authority forges validation tokens. Mitigations: each solver is a scoped cap (one DNS zone, one HTTP path prefix, one ALPN slot); solvers are per-consumer, not shared.
  8. Revocation denial-of-service. An attacker saturates the OCSP responder, forcing soft-fail everywhere. Mitigations: OCSP stapling (server-side caching takes the responder off the hot path); CRL fallback under deployment policy only.
  9. Clock skew attacks. A client with a wrong clock accepts expired certs or rejects valid ones. Mitigations: clockSkewSeconds has a tight default; consumers requiring hard-fail use an attested time source (cloud-metadata-proposal.md).

Phases

Phases follow the consumers that need this infrastructure.

Phase 1 — Certificate, CertificateChain, TrustStore, CertVerifier

  • Add the schemas above to schema/capos.capnp.
  • Implement a RAM-only trust store seeded from webpki-roots.
  • Implement a CertVerifier using rustls-webpki for path building and signature verification.
  • Host tests: chain verification against known-good and known-bad samples, name constraints, algorithm gating.

Phase 2 — TLS server and client configs

  • Add TlsServerConfig and TlsClientConfig schemas.
  • Wire rustls into the networking stack as a TlsSocket over TcpSocket; defined in networking-proposal.md.
  • CertificateStore with in-memory backing for the web shell gateway.

Phase 3 — ACME client and challenge solvers

  • AcmeClient speaking RFC 8555 (Let’s Encrypt-compatible).
  • ChallengeSolver implementations for http-01 (against the web shell gateway’s HTTP listener) and tls-alpn-01. dns-01 follows once a DNS cap exists.
  • CertificateStore.watch subscription drives TLS rotation without gateway restart.

Phase 4 — OCSP stapling

  • OcspResponder + OcspStapler services.
  • Must-staple enforcement in CertVerifier.
  • Cached stapled responses refresh in the background.

Phase 5 — Certificate Transparency (submission + verification)

  • SCT verification in CertVerifier (both embedded and TLS-extension SCTs).
  • CtLog client for submission; ACME flows submit precertificates to required logs before handing the cert to the caller.
  • Chrome CT log list bundled and signed like the WebPKI bundle.

Phase 6 — CT monitoring

  • CtMonitor service with watchSubject subscriptions.
  • Observations flow to the audit cap (system-monitoring-proposal.md).
  • Proof verification: STH signatures, inclusion proofs for capOS-issued certs, consistency proofs across STHs.

Phase 7 — Pinning

  • PinSet service with enforce and report-only modes.
  • Per-consumer pin policy plumbing.
  • Audit records on mismatch.

Phase 8 — CRL fallback and legacy compat

  • CrlStore implementation for code-signing flows that require CRL.
  • Policy knob to enable CRL fallback for OCSP-unreachable cases.

Phase 9 — Private CA

  • CertificateAuthority cap for capOS-internal issuance (mTLS fleet bootstrapping without an external ACME dependency).
  • CA keys live in KeyVault with strict seal policy.
  • Internal CT log (optional) for mis-issuance detection within a private fleet.

Relationship to Other Proposals

  • cryptography-and-key-management-proposal.md — supplies the PrivateKey / PublicKey / KeyVault / KeySource primitives this proposal consumes. A TLS server’s key cap, an ACME account key, and an internal CA signing key all live in a KeyVault sealed under a KeySource.
  • networking-proposal.md — defines the TcpSocket this proposal wraps and the TlsSocket interface that consumes TlsServerConfig / TlsClientConfig. mTLS between services uses this proposal’s verifier and trust store.
  • boot-to-shell-proposal.md — web text shell gateway consumes TlsServerConfig; ACME via this proposal provides the cert. WebAuthn attestation verification uses CertVerifier.verifySignature.
  • cloud-metadata-proposal.mdInstanceIdentity is often expressed as a signed JWT or X.509 certificate; verifying attestation statements uses CertVerifier.
  • storage-and-naming-proposal.md — Open Question #5 (manifest trust, secure boot) is the source of the build-time key that signs root-store and CT-log bundles.
  • system-monitoring-proposal.md — every verifyChain, addAnchor, OCSP query, CT observation, and pin mismatch flows through the audit cap.
  • security-and-verification-proposal.md — the certificate parser, path builder, and policy engine are top targets for fuzzing and property testing. rustls and rustls-webpki are the first-layer implementation; capOS-specific policy glue (CT, stapling, pinning) gets its own tier of tooling.
  • user-identity-and-policy-proposal.md — client-auth certs and per-user mTLS identity consume TlsClientConfig with a per-session PrivateKey cap.

Open Questions

  1. Canonical default policy. Should webPkiStrict require `minScts

    = 2` from day one, or is that too aggressive before CT log list curation ships? Chrome requires 2; Apple requires varying counts by cert lifetime. Probably match Chrome initially and revisit.

  2. CRL scope. Is CRL support worth the footprint at all, or should capOS ship OCSP-only and refuse to verify against CRL-only CAs? Leaning “CRL for code signing only”, not for TLS.
  3. Private CA surface. A CertificateAuthority cap with issue, revoke, and listIssued methods is straightforward, but the policy for issuance (SAN constraints, lifetime caps) deserves its own schema pass. Deferred to Phase 9.
  4. Trust-store delta signing. Signing every bundle replacement is expensive. A delta format (add/remove anchors with signed manifest patches) would be lighter; worth it only once bundle churn becomes a real operational cost.
  5. OCSP nonce support. Nonces prevent replay but most responders do not honor them. Ship without and revisit if a deployment needs replay-resistance.
  6. webpki-roots crate churn. The crate publishes a new version on Mozilla NSS changes, which is frequent. capOS needs a clean bump story — probably “new release triggers a trust-store bundle rebuild”, automated in CI.
  7. Stapling cache persistence. Must the OcspStapler cache survive reboot? Surviving reboot avoids a refresh storm at startup but risks serving very stale responses. Probably: cache is per-boot, with a short pre-refresh window before nextUpdate.
  8. Client-cert private key reuse. If a client uses one mTLS identity across many outbound connections, does each TlsClientConfig hold its own PrivateKey cap (wasteful) or share one (safe, since the cap’s sign method is the only surface)? Probably share by default; make duplication explicit if needed.
  9. Integration with CredentialStore. Some WebAuthn authenticators return attestation certs that must be chain-verified against FIDO MDS. The verification uses CertVerifier; the MDS trust store is a TrustStore maintained separately from the WebPKI bundle. How does MDS update cadence fit the no-background-update policy? Probably: MDS updates ride manifest updates, same as root bundles.
  10. GOST trust chains. The formal-mac-mic-proposal.md GOST track implies GOST-signed certificate chains. The CertVerifier algorithm enum is already open-ended; the work is algorithm implementation, not schema evolution.