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.

Implementation Status

The schemas and Phase 1-9 ordering below are design beyond the landed Phase 1 subset: vendored WebPKI roots, capos-tls host verifier logic, and the Certificate / CertificateChain / TrustStore / CertVerifier schema surface. The remaining near-term work is decomposed into a bounded slice chain owned by Certificates / TLS and the Certificates / TLS track in docs/tasks/README.md. The cut lands the lowest-risk real logic first. The Phase 2 client local proof landed on 2026-06-08: a userspace TLS 1.3 client completes one handshake over a userspace-served TcpSocket cap with a vendored embedded-tls state machine while validating the peer chain with capos-tls. The key-management proposal now has the minimal PrivateKey / PublicKey ABI, RAM signing core, and RAM-only KeyVault custody plus a development-only software KeySource for local TLS/ACME proofs, but no production custody source yet. Production/public server-side TLS remains blocked on reviewed custody and a server cert source:

  • Phase 1 deps [DONE 2026-06-03]. vendor rustls-webpki + webpki-roots as no_std+alloc snapshots with provenance: cloud-tls-vendor-rustls-webpki-roots-no-std-provenance.
  • Phase 1 [DONE 2026-06-03]. Certificate / CertificateChain / TrustStore / CertVerifier schema + host-tested verify logic over a RAM-only webpki-roots store: cloud-tls-cert-truststore-certverifier-phase1-host-proof.
  • Phase 2 (client) [DONE 2026-06-08]. One userspace TLS client handshake over the Phase C userspace TcpSocket cap, validating the peer chain with the Phase 1 verifier and a vendored embedded-tls TLS 1.3 state machine: cloud-tls-client-handshake-over-tcpsocket-local-proof.
  • Phase 2 (server consumer) – capOS-terminated TLS for the self-hosted Web UI (the direct-termination successor to the provider-terminated bootstrap below, not the closeout path for the first public proof), blocked additionally on a sealed PrivateKey cap and a server cert source: cloud-tls-self-hosted-webui-terminated-endpoint.
  • Minimal TLS/ACME key custody [DONE for local proofs]. The TLS server key and ACME account key need a PrivateKey / KeyVault / KeySource subset. The minimal PrivateKey / PublicKey ABI and RAM signing proof landed 2026-06-04; RAM KeyVault custody landed 2026-06-05; development-only software KeySource bootstrap landed 2026-06-05.
  • Phase 3 (ACME successor chain) [PARTIAL]. The local ACME account/order core landed on 2026-06-08: capos-tls signs ES256 JWS requests through an AcmeAccount PrivateKey cap, submits a CSR signed by a TLS-purpose key cap, and parses a returned local test certificate chain. Remaining Phase 3 work is scoped http-01 challenge solving, CertificateStore.watch renewal/rotation, and then a public GCE capOS-terminated direct-termination proof. These are successor tasks after the provider-managed first public proof, not replacements for it: cloud-tls-acme-account-order-local-proof [DONE 2026-06-08], cloud-tls-acme-http01-challenge-solver-local-proof, cloud-tls-acme-renewal-certstore-rotation-local-proof, and cloud-gce-public-webui-letsencrypt-direct-termination-proof.

Phases 4-9 (OCSP, CT, pinning, CRL, private CA) remain undecomposed design.

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

Two adjacent proposals draw their own trust boundaries instead of extending this one:

  • OIDC and OAuth2 tokens are not X.509. OIDC and OAuth2 covers short-lived bearer tokens (ID tokens, access tokens, DPoP proofs, client assertions) signed by JWKS-published keys, not by X.509 trust chains. Where an OIDC issuer’s private_key_jwt client assertion or workload-identity federation flow does need an X.509 cert, the signing key is a PrivateKey cap from the key proposal and the cert is a Certificate cap from this one. The token capability objects, JWKS verifier, and DPoP machinery live in the OIDC proposal; this proposal only supplies the verifier when an OIDC flow happens to land on an X.509 binding.
  • SSH host keys are not X.509 certs. SSH Shell Gateway uses raw SSH host-key signatures (SshHostKey.signExchangeHash) and TOFU/authorized-key trust, not WebPKI chains. The host key is a narrow wrapper around a PrivateKey cap from the key proposal, constrained to SSH host-key signing; this proposal’s Certificate, TrustStore, CertVerifier, and ACME flow are not consumed by the SSH transport. SSH and TLS/mTLS are intentional siblings — SSH for raw operator/agent access without a CA, TLS for PKI-integrated services.

Problem

capOS will need certificate and TLS infrastructure for:

  • TLS termination in the web text shell gateway (Boot to Shell).
  • mTLS between services on a multi-host capability graph (Networking). TLS wraps the TcpSocket cap defined there; in Phase A-B that socket state is kernel-resident smoltcp, and TLS sees it through the same cap boundary after Phase C migrates the stack to userspace.
  • WebAuthn attestation statement verification (Boot to Shell).
  • Code signing verification for binaries, boot manifests, update bundles (Storage and Naming Open Question #5).
  • Cloud KMS HTTPS API clients (Cryptography and Key Management CloudKmsKeySource).
  • Attestation report verification chains (Cryptography and Key Management AttestationKeySource).
  • Any outbound HTTPS client invoked from a service.

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 WebPKI trust-anchor records. Mozilla/WebPKI roots may not
    # be representable as full Certificate caps.
    anchors         @0 () -> (anchors :List(TrustAnchorInfo));

    # 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 :TrustAnchorInfo;
    sctCount    @1 :UInt8;
    ocspStatus  @2 :OcspStatus;
    notAfter    @3 :Int64;     # min notAfter across the verified path
}

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 consumes a TlsServerConfig or TlsClientConfig cap plus a raw TcpSocket and produces a TlsSocket. The first landed local client proof uses embedded-tls directly over a userspace-served TcpSocket; the broader config-cap service surface remains the Phase 2 TLS-service design. The TlsSocket draft interface lives in the “TLS Layering” section of Networking; this proposal only defines the configuration surface. While TcpSocket state remains kernel-resident through Phase A-B of the networking proposal, the TLS stack itself is a userspace consumer of that cap and does not move into the kernel — the certificate parser, path builder, and TLS state machine all run in the userspace TLS service.

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

Bootstrap TLS for the First Public GCE Web UI

The schemas above are no longer entirely pre-implementation design: the Phase 1 verifier, Phase 2 client handshake over a userspace TcpSocket, local key-custody precursors, and the local ACME account/order/finalize core have landed. The server-side TlsServerConfig / TlsSocket consumer, scoped http-01, CertificateStore.watch renewal, production key custody, and later CT/OCSP/pinning surfaces remain future or blocked work. The first time the self-served capOS Web UI (remote-session-web-ui) is exposed to a public operator browser on GCE, capOS therefore still does not terminate TLS itself. The reviewed first ingress terminates HTTPS at the GCP external load balancer’s Google front end against a provider-managed certificate; capOS serves only plain HTTP/1.1 on a backend port reachable solely from the load balancer and health-check source ranges. The full posture (firewall scope, browser session rules, evidence, teardown) is recorded in the “Public Web UI Ingress Policy” section of Cloud Deployment and the on-hold public Web UI ingress task; this note records only where TLS terminates and who holds the key.

Bootstrap consequences specific to this proposal:

  • No capOS private-key custody in the first proof. The TLS private key stays on the provider side. No PrivateKey cap, KeyVault, or KeySource from Cryptography and Key Management is consumed for the first public Web UI endpoint, and no key material is written into the disk image, manifest, or evidence directory.
  • No capability-native verification on the public hop. Because the Google front end performs TLS, the Certificate, TrustStore, CertVerifier, OcspStapler, and ACME flows defined here are not exercised by the first public Web UI proof. Provider-managed certificate lifecycle (issuance, renewal, revocation) is the provider’s, not capOS’s.
  • Successor path is the direct-termination shape. When this proposal’s TlsServerConfig plus an AcmeClient / ChallengeSolver (Phases 2-3) ship over the userspace TLS stack, a direct-external-IP, capOS-terminated ingress becomes a separately reviewed second option. At that point the certificate is a CertificateChain cap, the key is a sealed PrivateKey cap, CertificateStore.watch drives rotation, and the load-balancer-terminated path becomes one deployment choice rather than the only buildable one. The bootstrap step does not foreclose the capability-native model; it precedes it.
  • Let’s Encrypt is successor-only until the remaining prerequisites land. The Certificates / TLS backlog now names the landed local key-custody precursor (PrivateKey / KeyVault / development KeySource), landed TLS client over the userspace TcpSocket, and landed local ACME account/order/finalize core. Remaining successor prerequisites are the capOS-terminated Web UI TLS endpoint, scoped http-01 solver, CertificateStore.watch renewal and rotation, and then the on-hold Let’s Encrypt direct-termination GCE proof. Local ACME proofs use a local Let’s Encrypt-compatible directory. A real GCE or Let’s Encrypt staging/production run additionally needs a controlled public DNS name and explicit billable/public-ingress and CA authorization. Raw key material must not be written to manifests, images, logs, or evidence.

This mirrors the trust-anchor bootstrap above: capOS ships a pragmatic, reviewed interim posture (here, provider-terminated TLS) and migrates to the capability-native model as the implementing subsystems land, rather than blocking the first public proof on the full stack.

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

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 a userspace TLS state machine into the networking stack as a TlsSocket over TcpSocket; defined in Networking.
  • CertificateStore with in-memory backing for the web shell gateway.

Phase 3 — ACME client and challenge solvers

  • AcmeClient core speaking the local RFC 8555 account/order/finalize flow has landed in capos-tls; served capability wiring and public CA transport remain future.
  • 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).
  • 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 — supplies the key primitives this proposal consumes. Its minimal PrivateKey / PublicKey ABI, RAM signing core, RAM-only KeyVault handle custody, and development-only software KeySource bootstrap exist for the local TLS/ACME precursor. Persistence and production custody remain future. 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 (typical choices: Tpm2KeySource for fleet mTLS identities, PasskeyPrfKeySource or PassphraseKeySource for operator client-auth, CloudKmsKeySource for cloud-anchored CAs, development-only software sources for local ACME accounts). TLS certificate keys and ACME account JWS keys remain purpose-separated; the key proposal names that split as KeyPurpose.tls and KeyPurpose.acmeAccount.
  • Networking — defines the TcpSocket this proposal wraps and the draft TlsSocket interface that consumes TlsServerConfig / TlsClientConfig. In the proposal’s Phase A-B the socket state is kernel-resident smoltcp; the TLS stack consumes that cap from userspace and does not move into the kernel even before Phase C. mTLS between services uses this proposal’s verifier and trust store on top of that same cap.
  • OIDC and OAuth2 — separate trust model (JWKS-signed bearer tokens, not X.509 chains). The two proposals meet only at the corners where OIDC flows do bind to an X.509 cert: private_key_jwt client assertions and tls_client_auth/self_signed_tls_client_auth OAuth2 client authentication consume a PrivateKey from the key proposal plus a Certificate / CertificateChain from this one; workload-identity federation (RFC 8693) and outbound HTTPS to IdP/JWKS endpoints consume a TlsClientConfig with a webPkiStrict verifier. Inbound bearer-token verification stays in the OIDC proposal.
  • SSH Shell Gateway — explicitly a non-consumer. SSH uses raw host-key signatures and TOFU/authorized-key trust, not WebPKI; the host key wraps a PrivateKey from the key proposal directly, not a Certificate from this one. SSH and TLS/mTLS coexist as the two operator-facing remote-shell paths: SSH for CA-free operator/agent access, TLS/mTLS (the web text shell gateway plus future Telnet-over-TLS paths) for PKI-integrated environments.
  • Boot to Shell — web text shell gateway consumes TlsServerConfig; ACME via this proposal provides the cert. WebAuthn attestation verification uses CertVerifier.verifySignature.
  • Cloud MetadataInstanceIdentity is often expressed as a signed JWT or X.509 certificate; verifying attestation statements uses CertVerifier.
  • Storage and Naming — 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 — every verifyChain, addAnchor, OCSP query, CT observation, and pin mismatch flows through the audit cap.
  • Security and Verification — the certificate parser, path builder, and policy engine are top targets for fuzzing and property testing. The landed client path uses embedded-tls plus rustls-webpki-backed capos-tls verification; capOS-specific policy glue (CT, stapling, pinning) gets its own tier of tooling.
  • User Identity and Policy — 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 GOST track implies GOST-signed certificate chains. The CertVerifier algorithm enum is already open-ended; the work is algorithm implementation, not schema evolution.