Certificates / TLS Backlog
Bounded implementation slice chain for the certificates/TLS track. It decomposes Certificates and TLS into dispatchable slices and is owned by the Certificates / TLS track on the loopyard task board. The dispatchable records live in loopyard; this file is the long-form decomposition and sequencing rationale.
Grounding
- Certificates and TLS
– the schema surface and Phase 1-9 ordering. Phases 1-2 are the near-term
target; Phase 1 is
Certificate/CertificateChain/TrustStore/CertVerifierover a RAM-onlywebpki-rootsstore and arustls-webpkiverifier. The Phase 2 client local proof completes a TLS 1.3 handshake over a userspace-servedTcpSocketcap withembedded-tls, and the Phase 2 server core now terminates real traffic in the local QEMU Web UI proof (slice 4). The capnpTlsServerConfig/ cert-source service surface remains future Phase 2 work: the landed consumer composes its chain and key in-process rather than through a served config cap. - Cryptography and Key Management
– partial implementation. The minimal
SymmetricKey,PrivateKey, andPublicKeyABI, RAM-only XChaCha20+HMAC/P-256 key cores, RAM-onlyKeyVaulthandle custody, and development-only softwareKeySourcebootstrap exist for local proofs. There is still no persistence or production custody source, so production/public TLS and ACME remain blocked on a reviewed source that can mint key handles without exposing raw private-key material. - Time and Clock Authority
–
WallClockPhase 1 landed (88cf4b5d): a read cap withwallTimeand aClockProvenancelabel, but the fixed-boot-base source reportsUntrusted. Cert-validity (notBefore/notAfter) and OIDCexp/iatcompare against it. Host-tested verify logic passes an explicitatEpochSecondsand needs no live clock; security-grade validity against an adversarial clock wants the trusted-provenance upgrade (WallClock Phase 2). Recorded as a sequencing dependency on the live consumer slices, not on the host verifier slice. - Phase C Userspace NIC Driver Relocation
– the userspace
TcpSocketcap the TLS stack wraps arrives via Phase C slice-7 (cloud-prod-userspace-network-stack-smoltcp-local-proof). The TLS stack is a userspace consumer of that cap and must not move into the kernel.
Sequencing Rationale
The proposal’s suggested shape (library -> handshake -> cert caps -> consumer) is reordered to land the lowest-risk real logic first, grounded in what exists:
- The verifier path (TrustStore + CertVerifier over webpki-roots) needs
no socket and no private key – it is pure
no_std + allochost-testable logic. It lands before the handshake. - A TLS client handshake needs a
TcpSocketcap but no private key. - A TLS server needs a
KeyVault-issuedPrivateKeyhandle and a server cert source. The 2026-07-16 audit split this: the server handshake core is pure host-testable logic that needs no socket and no production custody (it reaches the key only through a signer seam), so it landed host-first like the verifier. The local QEMU consumer now terminates real traffic; the public direct-termination consumer remains dependency-blocked by the on-hold provider-terminated public proof, with production key custody and a real certificate source also required.
Slice Chain
-
Vendor the Phase-1 verifier crates.
rustls-webpki+webpki-rootsas static-pinned no_std+alloc snapshots withVENDORED_FROM.mdprovenance, recorded underdocs/trusted-build-inputs.md, proved to build for the bare-metal target. Slice 3 later selectedembedded-tlsfor the local client proof’s no_std TLS state machine; the broader server/config service stack remains future work. -
Certificate/TrustStore/CertVerifier(Phase 1). Schema additions plus host-tested verify logic overrustls-webpkiseeded bywebpki-roots, with chain verification proved against committed good/bad vectors and explicitatEpochSeconds. No running cap service, no socket, no key – the lowest-risk real cert logic. -
Client TLS handshake over
TcpSocket(Phase 2, client-only). Done 2026-06-08. A userspace process completes one TLS 1.3 client handshake over the Phase C userspaceTcpSocketcap, validating the peer chain with the slice-2 verifier, with an observable local QEMU proof. The no_std determination selected a vendoredembedded-tls 0.19.0client state machine for this local proof rather than fullrustls. 3s. Server-side handshake core (host proof). Done 2026-07-16. Split out of slice 4 by the 2026-07-16 backlog audit, which found that the local server-side path had no remaining technical prerequisite: only the server-side TLS state machine itself was missing, because the vendoredembedded-tlsis client-only.capos-tlsnow carries ano_std + allocTLS 1.3 server handshake core (tls13/), bounded to TLS 1.3,TLS_AES_128_GCM_SHA256,secp256r1, andecdsa_secp256r1_sha256, with no mTLS, resumption, 0-RTT, or HelloRetryRequest. Server key authority stays behind aTlsServerSignerseam whose in-tree implementations are the landedPrivateKeycap types; the core has no raw-key path. The host proof completes a full handshake and application-data round trip against the vendoredembedded-tlsclient over an in-memory transport (cargo test-tls). It owns no socket and no served cap, so it grants no production or public TLS authority.The RFC 8446 §6 alert path landed 2026-07-17 10:38 UTC as a follow-up (
cloud-tls-server-alert-path): the core sent no alerts, so a peer saw a bare TCP close where ahandshake_failureor aclose_notifywas owed. The core now builds the fatal alert a failedfeedowes and emitsclose_notifyon clean shutdown, protected under whatever epoch the server currently writes and plaintext only before the ServerHello reaches the peer. The error-to-description map is a disclosure decision, not a formatting one, so the bounded-surface refusals collapse onto onehandshake_failureand deprotection failure says onlybad_record_mac(RFC 8446 §5.2). Sending the bytes is the consumer’s move – the core owns no transport – so a consumer that drops them still shows a peer the old bare close.The in-tree consumer was wired to that path 2026-07-17 (
cloud-tls-webui-alert-path-consumer-wiring), which is what carries it to the wire:remote-session-web-uiwritestake_alert()before abandoning a failed handshake, andclose_notify()when it closes. Both writes are best-effort and share the connection deadline, so neither can extend a connection past the bound it already had or turn an abort into another outcome.make run-cloud-tls-webui-terminatedproves both from the host client – a refused hello reports a fatalhandshake_failure, and a completed exchange closes cleanly – each verified against the unwired code.close_notifyis owed wider than the served response: RFC 8446 §6.1 requires it before every close of the write side except after an error alert, so a connection abandoned mid-request (read deadline expired, oversized or unparseable request) is owed one too – it sent no alert. The consumer’s abort path therefore offers it and the core answers, refusing for any connection that never completed a handshake or already sent its fatal alert, which is precisely §6.1’s exception. The attempt is one bounded write, never a retry loop: an abandoned connection’s expired deadline discards whatever of it did not leave, so the alert cannot extend the bound that deadline already put on the slot. Transport and ring faults do not attempt it at all – there the write is already known to be impossible. What the QEMU proof (make run-cloud-tls-webui-terminated) checks is that the peer receives it on the refused-request and read-deadline aborts, both verified against the unwired code; delivery after a peer has already stopped reading is attempted, not claimed.The ClientHello parser was made to enforce
supported_groups2026-07-18 13:05 UTC (cloud-tls-client-hello-supported-groups-conformance), closing the one case the alert-path slice recorded that did not abort at all: the extension was never parsed, so a hello that omitted it entirely – which RFC 8446 §9.2 forbids of any client offering ECDHE, and requires a server to abort on – silently completed a handshake off thekey_sharealone. The parser now requires the extension to namesecp256r1, treating “absent” and “names only groups we do not implement” as one verdict, since the bounded surface has no HelloRetryRequest to answer either with. The refusal joins the existing collapse ontohandshake_failurerather than sending themissing_extension§9.2 names, which would report which bound refused the hello; that trade was already made and reviewed for thekey_shareandsignature_algorithmsabsent cases. Those cases, and now this one, still abort under a description §9.2 does not name, so the parser is not §9.2-alert conformant and is not claimed to be – the disclosure collapse is the deliberate reason. Both real clients the repo proves against – vendoredembedded-tlsand OpenSSL – send the extension, so enforcement cost no interop. -
capOS-terminated Web UI endpoint (local QEMU consumer). Done 2026-07-16.
remote-session-web-uiserves the Web UI over a TLS 1.3 endpoint it terminates itself, wiring slice 3s’s core to the Phase C userspaceTcpSocketcap. Termination is a boot-manifest decision: atls_terminated_endpointmarker makes the network stack forward the marker and grant anEntropySource. The service refuses to boot if it ever sees one without the other, or a request for termination in a build that links no TLS stack. Those three refusals, and the cap-id ordering contract they rest on (entropyat the slot immediately after the required service caps, every policy marker strictly after it), are decided byevaluate_service_capset_policyin the web-ui’s host-testable policy library and covered by the refusal matrix inmake webui-login-peer-logic-test. The spawning network stack asserts the grant order it builds against the same shared constants, so a reorder there fails at its source rather than only as a guest boot refusal under whichever posture a smoke happens to boot. Proofmake run-cloud-tls-webui-terminated: a host OpenSSL self-test client over user-net hostfwd completes the handshake, pins the served leaf against the fingerprint the guest logged, fetches a Web UI response over the terminated connection, and confirms plaintext HTTP is refused on the same port. The plain-HTTP L4 path and its proofs are unchanged; the TLS stack is a default-offtls-endpointfeature, so that build links none of it.Two bounds this proof does not clear. The chain is a boot-provisioned self-signed development certificate over a runtime-minted key (
issue_development_self_signed_certificate): a vault-mintedPrivateKeyrefuses export, so no certificate provisioned ahead of time could match it, and nothing here chains to a public root. Its validity window is a fixed constant rather than a trusted-clock read, becauseWallClockprovenance is still Phase 1Untrusted. Public trust and real validity come from ACME issuance (slices 5-7), not from this path.
4p. Public direct-termination Web UI endpoint (terminal consumer). Serves the
Web UI over capOS-held TLS as a direct-termination successor after the first
GCE public Web UI proof closes through provider-terminated HTTPS. Deeply
blocked: needs the local proof above plus a production key-custody source and
a real cert source (ACME / provisioned).
5. Minimal key-custody decomposition. Done. It decomposes the missing
PrivateKey / KeyVault / KeySource subset into the three implementation
records below, keeping production hardware/cloud custody out of the local
TLS/ACME bootstrap.
6. PrivateKey / PublicKey RAM signing proof. Done 2026-06-04. Adds the
minimal asymmetric-key ABI and host-tested RAM signing core:
sign/public/info, public verify/export/info, purpose metadata, and no raw
private-key export.
7. RAM KeyVault custody. Done 2026-06-05. Adds handle-based key
generation/open/list/destroy and a local QEMU proof for TLS and ACME account
key handles, still RAM-only and not production custody.
8. Development-only KeySource bootstrap. Done 2026-06-05. Grants local proofs a
development software key source that mints key handles without putting raw
private keys in manifests, images, logs, task records, or evidence, and is
rejected for production/public profiles.
9. ACME account/order local proof. Done 2026-06-08. capos-tls now has a
no_std+alloc ACME account/new-order/CSR-finalize/certificate-retrieval core,
with ES256 JWS signing through AcmeAccount PrivateKey caps and CSR
signing through a separate TLS-purpose PrivateKey cap. Challenge
validation stays fake or pre-authorized here; the proof does not call Let’s
Encrypt staging or production.
10. Scoped http-01 solver. Done 2026-06-09. capos-tls adds a bounded,
token-scoped Http01ChallengeSolver and the RFC 8555 http-01 authorization
flow (pending order, authorization fetch, key-authorization derivation via the
RFC 7638 account-key thumbprint, challenge response, out-of-band validation,
and cleanup). remote-session-web-ui serves only
/.well-known/acme-challenge/<token> for currently-published tokens through
that same solver; retired, unknown, sub-path, and traversal tokens fail closed
(404). The host ACME http-01 test proves the protocol and cleanup; the Web UI
L4 QEMU proof fetches the challenge through the served origin. It grants no
generic route, static-file, DNS, or Web UI authority and adds no public CA
call.
11. CertificateStore.watch renewal and rotation. Done 2026-07-17.
capos-tls adds the bounded RAM CertificateStore (a stable handle names the
current chain; put replaces it in one step and notifies watch subscribers;
get resolves only inside the stored chain’s validity window, so unknown,
deleted, expired, and not-yet-valid handles fail closed) and RenewalPolicy,
the pure “is it due” decision that fires a lead before notAfter and
distinguishes Due from Expired. A store entry holds public certificate
bytes only, which is what keeps a chain rotation from being a key rotation.
The host proof (cargo test-tls) drives a full local ACME order into the
store, reaches the renewal threshold, orders a second distinct chain, and
resolves it back out with a watcher observing the rotation and the same key
still signing. The Web UI TLS endpoint serves in epochs – one resolution per
epoch, re-resolved periodically and again at each admission, latched into
draining by the first check that finds the chain no longer current or the
clock unreadable – and renews at the halfway mark of a short-lived window
once the table has drained. The QEMU proof
(make run-cloud-tls-webui-terminated) pins the provisioned leaf before the
threshold and a different leaf after it, on a service whose provisioning
lines occur exactly once, drives that rotation under overlapping load (two
staggered holder connections plus the polling client), and requires the
endpoint to settle on the one renewed chain over further polls rather than
stopping at the first leaf that differs.
An accept that completes after its epoch has stopped being fit to serve it is
carried into the next epoch rather than refused inside the stale one. The
epoch that latched into draining – or that found its chain no longer current
at the moment of admission – hands the completed socket to a boot-scoped
holder outside the epoch loop, keeping one stable accept id and the original
absolute read deadline, and stops submitting accepts while it is occupied, so
at most one is ever held. The next epoch renews and re-resolves first, then
admits that same socket, or closes it fail-closed if its original deadline has
expired, the newly resolved chain is still not servable, or – the case
re-resolution alone does not catch – the newly resolved generation is not
strictly greater than the one the socket was deferred under. An unreadable
clock is owned by the first of those paths: without a clock the loop cannot
prove any deadline remains, so it consumes the carried socket through the
bounded expiry close before asking a chain-freshness question. That last
generation condition is the one that makes the crossing mean something: a
renewal that fails leaves the old generation still resolvable and still
current, so a rule that asked only “is this chain the one the store would hand
out” would readmit the socket onto the very chain it was carried away from.
The deferral generation is therefore stored on the carried socket and compared
at readmission, in runtime code rather than in an after-the-fact log assertion.
This is deliberately availability-conservative for every deferral reason: an
EpochDraining carry that reaches the next epoch without a renewal is closed
for an unadvanced generation even when that generation remains otherwise
current. A carried socket is never carried twice: the second unservable epoch
closes it once, through the same bounded refusal path every other refusal
takes, which is what makes this a single boundary crossing rather than a retry
loop. No TLS state is created under the old chain, since the decision happens
before any handshake starts.
The QEMU proof for that crossing (make test-cloud-tls-webui-deferred-accept)
uses a default-off deferred-accept-proof build with two levers. Each is
explicitly classified as an unauthenticated LoginPost, whose Host / Origin /
Referer / principal-hint / JSON-content-type chain matches a state-changing
request without claiming the handler will call require_active. The client
sends the loopback TLS authority it actually dialed – Host with the port and
https:// Origin and Referer – and the QEMU SLIRP gateway is accepted only by
the peer-gated loopback posture it already holds; no guard is relaxed for the
proof. /api/probe/defer-next-accept arms one admission cutoff and forces the
paired renewal; the harness then asserts an ordered record tied to one accept
id and one carried deadline: deferred under generation N, a renewal strictly
between, readmitted under a generation strictly greater than N with the same
id, the same deadline, and the same recorded source generation, and a
handshake completed by that same carried socket naming all four – with no
refusal in between. The handshake proof context owns that original deadline;
it never re-reads the connection field that changes to a send deadline once a
response begins.
/api/probe/defer-next-accept-no-renew arms the same cutoff and withholds the
renewal, which is what makes the strictly-greater comparison a gated invariant
rather than a reviewed one. Every other path in the proof advances the
generation, so a non-strict comparison would behave identically there and pass;
under this lever the next epoch resolves the same generation, and the harness
requires exactly one bounded close naming that accept id, that unchanged
deadline, and reason=generation-not-advanced, with no admission, no
handshake, and no second deferral for it. Mutating the comparison to accept
equality fails this assertion.
Neither lever changes a certificate lifetime or a renewal policy; they only choose when an already-implemented cutoff fires and whether the renewal it would have triggered happens. Production builds contain neither route, neither static, nor the per-connection field the handshake marker uses.
The real-expiry trigger is covered separately by
make test-cloud-tls-webui-epoch-drain-latch. Its default-off
tls-epoch-drain-latch-proof profile leaves the ordinary 120-second lifetime,
one-half renewal fraction, and 5-second chain-check interval unchanged. For
this focused run it selects a 30-second lifetime, a 1/31 renewal fraction,
and a 40-second periodic check. Integer arithmetic makes the renewal lead
zero; provisioning also refuses to start unless RenewalPolicy::due_at
equals the stored chain’s notAfter. A policy arithmetic change therefore
cannot silently turn the proof back into an early-renewal run.
The harness starts after the initial leaf is announced, keeps staggered TLS
holders and completed /healthz admissions overlapping through expiry, and
waits for the exact chain-not-current-at-admission latch rather than a
prefix shared with another drain cause. It then releases the old epoch’s
holders and requires one ordered crossing tied to the same accept id,
absolute deadline, and source generation: admission-time deferral, one
renewal strictly after the latch and before readmission, readmission on a
strictly newer generation, and handshake completion by that carried socket.
The host observes a different leaf on the carried connection, a 200 health
response, and close_notify. A readmission close for that accept, renewal
before the latch, an unreadable clock, or a second provisioning fails the
proof.
This closes the local trigger-and-consequence gap without claiming a trusted validity clock or public trust. The clock remains the monotonic-offset proof clock. The later in-VM issuance proof executes the local ACME order path at this renewal seam; both the initial and locally issued chains remain development-signed and grant no public trust. 12. Public GCE Let’s Encrypt direct-termination proof. A separately reviewed successor after the provider-managed first public proof. It requires a public DNS name controlled for the run, explicit billable/public-ingress authorization, and explicit authorization before any Let’s Encrypt production call; staging remains the default external CA target.
Let’s Encrypt / ACME Public TLS Decomposition
Let’s Encrypt support is implementable for the public TLS milestone only as the capability-native, capOS-terminated successor path. It is not the already selected closeout path for the first public GCE Web UI proof. That first proof continues to terminate HTTPS at the GCP external load balancer with a provider-managed certificate, no capOS private-key custody, and no raw public HTTP closeout.
The missing prerequisites are represented as named task records:
- Minimal
PrivateKey/PublicKeyABI and RAM signing proof for TLS server keys and ACME account keys (crypto-privatekey-publickey-ram-signing-local-proof). - RAM-only
KeyVaultcustody for TLS and ACME private-key handles (crypto-keyvault-ram-privatekey-custody-local-proof). - Development-only software
KeySourcebootstrap for local TLS/ACME proofs, rejected for production/public profiles (crypto-development-keysource-tls-acme-bootstrap-local-proof). - A TLS client over the userspace
TcpSocketcap (cloud-tls-client-handshake-over-tcpsocket-local-proof). - Server-side TLS and
TlsServerConfigforremote-session-web-ui(cloud-tls-self-hosted-webui-terminated-endpoint). - An RFC 8555
AcmeClientaccount/order/finalize path against a local Let’s Encrypt-compatible directory (cloud-tls-acme-account-order-local-proof). - A scoped
http-01challenge solver under the Web UI service boundary (cloud-tls-acme-http01-challenge-solver-local-proof). CertificateStore.watchrenewal and TLS-chain rotation without a Web UI restart (cloud-tls-acme-renewal-certstore-rotation-local-proof).- In-VM local ACME account/order/
http-01/finalize execution through the Web UI solver and into the liveCertificateStore(cloud-tls-acme-in-vm-order-issuance-certstore-local-proof). - Public DNS/name control plus explicit billable/public-ingress authorization
before a real GCE run, and explicit CA authorization before any Let’s Encrypt
staging or production request
(
cloud-gce-public-webui-letsencrypt-direct-termination-proof).
Local proofs and public CA/cloud proofs stay distinct. The ACME account/order, challenge, and renewal slices use a local RFC 8555-compatible directory and local QEMU/cloudboot paths. A public GCE/Let’s Encrypt run requires a separately authorized harness mode, a controlled public DNS name, public-ingress teardown evidence, and no private key material in manifests, images, logs, task records, or evidence directories.
Next Gap
Slices 1 and 2 landed on 2026-06-03: rustls-webpki and webpki-roots are
vendored as static-pinned no_std+alloc snapshots, and capos-tls contains the
Phase 1 Certificate / TrustStore / CertVerifier host verifier proof over
those crates. K1 landed on 2026-06-04: capos-tls also contains the minimal
RAM-only P-256 PrivateKey / PublicKey signing core. K2 landed on
2026-06-05: RAM-only KeyVault generation/open/list/destroy handle custody for
those keys. K3 landed on 2026-06-05: local development software KeySource
bootstrap now mints TLS and ACME account key handles without raw private-key
material in manifests or evidence and rejects production/public profiles.
Capability-infrastructure key-cap reconciliation landed on 2026-06-06: the
minimal RAM-only SymmetricKey ABI and local AEAD/MAC proof now exist. Slice 3
landed on 2026-06-08: the local QEMU proof now completes one TLS 1.3 client
handshake over a userspace-served TcpSocket cap and validates the peer chain
with capos-tls. ACME slice 5 landed on 2026-06-08: capos-tls now proves
account registration, order creation, CSR finalize, and returned-chain parsing
against a local RFC 8555-style directory using purpose-scoped key caps. ACME
slice 6 (proposal item 10) landed on 2026-06-09: the scoped http-01 solver
now serves bounded
/.well-known/acme-challenge/<token> responses through remote-session-web-ui,
with the http-01 authorization/validation/cleanup flow proven host-side and the
served route proven in the Web UI L4 QEMU proof. The renewal and
certificate-store rotation slice (proposal item 11; table slice 7) landed on
2026-07-17 02:40 UTC with the bounded RAM CertificateStore, RenewalPolicy,
epoch-serving Web UI endpoint, deferred-accept crossing, and epoch-drain latch.
The in-VM issuance path proven in QEMU on 2026-07-26 12:02 UTC: the Web UI process
keeps the boot-time self-signed chain as generation 1, then drives account
registration, order creation, account-derived http-01 solver publication and
local validation, CSR finalize, returned-chain parsing, and atomic store
installation against the local RFC 8555-compatible directory. The scoped HTTP
route remains separately proven with its boot fixture; the local directory
validates the order authorization directly through the same solver while the
single-threaded service is at its quiescent renewal boundary, so this proof does
not claim an external CA HTTP fetch. The QEMU proof ties generation 2 to the
leaf observed on the wire and, in its default-off failed-order mode, separately
drives a complete order whose malformed returned chain fails parsing, observes
the generation-1 leaf still served over TLS, and then retries successfully. Its
clock remains the monotonic-offset proof clock, both chains remain
development-signed, key custody remains RAM-only, and no public CA, DNS,
ingress, provider action, or public-trust authority is involved.
The next ACME gap is the public GCE Let’s Encrypt direct-termination proof
(proposal item 12; table slice 8), which remains on hold behind the
provider-terminated public Web UI proof, the public direct-termination consumer,
controlled public DNS, and explicit public-ingress and CA authorization.
The local QEMU Web UI TLS consumer (slice 4) is landed. It starts with a
development self-signed chain over a runtime-minted key and rotates to the
local-directory order result, still over a fixed proof-clock validity window
because WallClock provenance remains Phase 1 Untrusted; it grants no
production or public-trust authority. The remaining server-side TLS consumer
gap is the public direct-termination successor (slice 4p), blocked on
cloud-gce-public-self-hosted-webui-ingress-tls
closing with provider-terminated HTTPS, production key custody, and a real
certificate source. The key-custody local-proof precursor is complete for
PrivateKey / PublicKey, RAM KeyVault, and development KeySource;
production custody remains future.