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:
- TLS termination in the web text shell gateway (boot-to-shell-proposal.md).
- mTLS between services on a multi-host capability graph (networking-proposal.md).
- WebAuthn attestation statement verification (boot-to-shell-proposal.md).
- Code signing verification for binaries, boot manifests, update bundles (storage-and-naming-proposal.md Open Question #5).
- Cloud KMS HTTPS API clients
(cryptography-and-key-management-proposal.md
CloudKmsKeySource). - Attestation report verification chains
(cryptography-and-key-management-proposal.md
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
TrustStorecap containing that one anchor, not the full Mozilla root bundle. A service that must not bypass CT receives aCertVerifierwhose policy hasminScts >= 2; no method on that cap lets the caller lower the bar. - Revocation is a cap drop. A compromised anchor is removed from
the
TrustStoreit 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, everyaddAnchor, every OCSP query flows through the audit cap. A service that bypasses revocation shows up in the audit log as a service that stopped callingOcspResponder.status. - Rotation without restart. A TLS server holds a
CertificateStore.watchsubscription; 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
TlsServerConfigis 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:
webPkiStrict—minScts = 2,requireOcsp = true, allowed algorithms and schemes drawn from Mozilla’s “modern” profile.webPkiLenient—minScts = 0,requireOcsp = false. Used by low-value clients where misrouting is acceptable.privateMtls—minScts = 0,requireOcsp = true,maxChainLength = 3. Used between capOS services holding CA-issued identity certs.codeSigning—minScts = 0, longnotAftertolerances, 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 = truemeans OCSP-unreachable is a hard verification failure. Default forCertPurpose.tlsClientAuthon services facing untrusted networks; soft-fail otherwise.- A certificate carrying the
id-pe-tlsfeaturemust-staple extension fails verification if no stapled response is present, regardless ofrequireOcsp. VerificationPolicy.staplingRequired = trueextends 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.
addAnchoron the systemTrustStoreis 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.restrictandaddAnchoron 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
| Consumer | Uses |
|---|---|
| Web text shell gateway | TlsServerConfig + OcspStapler; cert from AcmeClient |
| Inter-service mTLS | TlsServerConfig + TlsClientConfig with private-PKI TrustStore |
| Outbound HTTPS clients (KMS, IMDS) | TlsClientConfig with WebPKI-strict verifier |
| WebAuthn attestation verification | CertVerifier.verifySignature with FIDO MDS TrustStore |
| Code signing verification | CertVerifier with codeSigning trust store + OCSP |
| Signed manifest verification | CertVerifier.verifySignature + pinned build-time root |
| CT mis-issuance monitoring | CtMonitor.watchSubject on capOS-owned domains |
Threat Model
Specific to this subsystem, independent of the crypto/key threat model:
- 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; enableCtMonitorfor capOS-owned subject patterns. - 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. - 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
nextUpdatewindows limit stale “good” responses; fail-closed whenrequireOcspis set. - 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
staplingRequiredpolicy extends this to all certs. - Name-constraint bypass. An intermediate CA issues for names
outside its constrained scope. Mitigations:
permitNameConstraintsalways on; verifier enforces name constraints before reporting success. - Pin brittleness. A pin prevents legitimate rotation,
locking out users. Mitigations: short pin expiries,
reportOnlymode for rollout, pins bound to SPKI (not to full certificates). - 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.
- 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.
- Clock skew attacks. A client with a wrong clock accepts
expired certs or rejects valid ones. Mitigations:
clockSkewSecondshas 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
CertVerifierusingrustls-webpkifor 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
TlsServerConfigandTlsClientConfigschemas. - Wire rustls into the networking stack as a
TlsSocketoverTcpSocket; defined in networking-proposal.md. CertificateStorewith in-memory backing for the web shell gateway.
Phase 3 — ACME client and challenge solvers
AcmeClientspeaking RFC 8555 (Let’s Encrypt-compatible).ChallengeSolverimplementations forhttp-01(against the web shell gateway’s HTTP listener) andtls-alpn-01.dns-01follows once a DNS cap exists.CertificateStore.watchsubscription drives TLS rotation without gateway restart.
Phase 4 — OCSP stapling
OcspResponder+OcspStaplerservices.- 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). CtLogclient 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
CtMonitorservice withwatchSubjectsubscriptions.- 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
PinSetservice with enforce and report-only modes.- Per-consumer pin policy plumbing.
- Audit records on mismatch.
Phase 8 — CRL fallback and legacy compat
CrlStoreimplementation for code-signing flows that require CRL.- Policy knob to enable CRL fallback for OCSP-unreachable cases.
Phase 9 — Private CA
CertificateAuthoritycap for capOS-internal issuance (mTLS fleet bootstrapping without an external ACME dependency).- CA keys live in
KeyVaultwith 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/KeySourceprimitives this proposal consumes. A TLS server’s key cap, an ACME account key, and an internal CA signing key all live in aKeyVaultsealed under aKeySource. - networking-proposal.md — defines the
TcpSocketthis proposal wraps and theTlsSocketinterface that consumesTlsServerConfig/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 usesCertVerifier.verifySignature. - cloud-metadata-proposal.md —
InstanceIdentityis often expressed as a signed JWT or X.509 certificate; verifying attestation statements usesCertVerifier. - 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-webpkiare 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
TlsClientConfigwith a per-sessionPrivateKeycap.
Open Questions
- Canonical default policy. Should
webPkiStrictrequire `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.
- 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.
- Private CA surface. A
CertificateAuthoritycap withissue,revoke, andlistIssuedmethods is straightforward, but the policy for issuance (SAN constraints, lifetime caps) deserves its own schema pass. Deferred to Phase 9. - 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.
- OCSP nonce support. Nonces prevent replay but most responders do not honor them. Ship without and revisit if a deployment needs replay-resistance.
webpki-rootscrate 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.- Stapling cache persistence. Must the
OcspStaplercache 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 beforenextUpdate. - Client-cert private key reuse. If a client uses one mTLS
identity across many outbound connections, does each
TlsClientConfighold its ownPrivateKeycap (wasteful) or share one (safe, since the cap’ssignmethod is the only surface)? Probably share by default; make duplication explicit if needed. - Integration with
CredentialStore. Some WebAuthn authenticators return attestation certs that must be chain-verified against FIDO MDS. The verification usesCertVerifier; the MDS trust store is aTrustStoremaintained 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. - GOST trust chains. The
formal-mac-mic-proposal.md GOST
track implies GOST-signed certificate chains. The
CertVerifieralgorithm enum is already open-ended; the work is algorithm implementation, not schema evolution.