c8s-verify (JavaScript)
The client-side library that verifies a c8s Load Balancer's attestation from a browser or Node and opens the post-quantum over-encrypted channel — installation, the C8sClient options, the session object, what connect() checks, and every error code.
c8s-verify is the client-side library
that implements everything above: it verifies a c8s Load Balancer's attestation from a browser or
Node, then opens the post-quantum over-encrypted channel that terminates inside the LB's enclave.
Evidence is verified in your browser by the
attestation-rs verifier compiled to
WebAssembly, with the AMD roots bundled — no network calls during verification. Its only runtime
dependency is mlkem-wasm for ML-KEM-768.
Use a build that speaks c8s-verify/v1.
The wire protocol is versioned and the LB serves exactly one binding: the identity-bound
transcript above. A client that computes the older session-key-only report_data fails closed
on a binding mismatch. Check the library's PROTOCOL.md against the version string the
attestation bundle carries before pinning a release.
Prerequisites
- Runtime: Node ≥ 20, or a modern browser — the library uses WebCrypto and WebAssembly.
- Two values pinned out of band. Without both you cannot prove you reached your cluster,
and the library refuses to run:
- the LB measurement allowlist — one or more SHA-384 launch digests (see Obtaining launch measurements);
- the mesh CA certificate (PEM) your cluster's CDS issued — the cluster-unique anchor (see why cluster identity is pinned).
- A reachable LB serving the
c8s-verify/v1endpoints under/.well-known/c8s/.
The browser verifier covers the SEV-SNP family only — bare snp and Azure vTPM az-snp.
Intel TDX (tdx, az-tdx) is carried by the protocol and served by the LB, but the WASM verifier
does not implement it; verify TDX deployments from the
operator path instead.
Installation
The package name is c8s-verify and it is not on the public npm registry yet, so consume it from
the c8s-verify-js repo — vendor it into
your app or add it as a git submodule — and install its one runtime dependency:
npm install mlkem-wasmThe prebuilt WASM verifier ships in the repo, so verification itself makes no network calls.
Usage
The high-level C8sClient runs the entire flow — fetch the attestation, verify it, run the
handshake, derive the channel — and returns a Session whose fetch is end-to-end encrypted to
the enclave:
import { C8sClient } from "c8s-verify";
const client = new C8sClient({
baseUrl: "https://lb.example.com",
measurements: ["<SHA384_LAUNCH_DIGEST>"], // pinned out of band; empty is refused
meshCaPem: pinnedMeshCaPem, // pinned cluster anchor; required
});
// Generates a 32-byte nonce, fetches the LB attestation, verifies the hardware
// evidence, the measurement, the identity transcript, the leaf's chain to the
// pinned mesh CA, and the proof signature — then runs the X25519 + ML-KEM-768
// handshake and derives the AES-256-GCM channel.
const session = await client.connect();
console.log(session.attestation.measurement, session.attestation.cert.sha256);
// Every request on session.fetch is sealed end-to-end to the LB enclave —
// method, path, headers and body — underneath whatever TLS terminator is in front.
const res = await session.fetch("/v1/chat", { method: "POST", body: prompt });
console.log(res.text());If any check fails, connect() throws a typed C8sVerifyError and no channel is established
(fail-closed) — see Errors.
Configuration
new C8sClient(options):
| Option | Type | Default | Description |
|---|---|---|---|
baseUrl | string | — (required) | LB origin, e.g. https://lb.example.com. |
measurements | string[] | — (required) | Accepted launch digests (hex SHA-384). An empty list is rejected. |
meshCaPem | string | — (required) | Pinned mesh CA (PEM) the LB's leaf must chain to. |
platform | string | "snp" | Expected TEE platform; "az-snp" for Azure vTPM evidence. |
requireFreshness | bool | true | Require report_data to bind the identity transcript. false downgrades the check to a warnings[] entry the embedding app must inspect (UNSAFE for a live channel; for recorded-evidence demos). |
fetch | function | globalThis.fetch | Custom fetch implementation (e.g. for Node without a global fetch, or to inject headers). |
wellKnownPrefix | string | /.well-known/c8s | Base path for the protocol endpoints. |
at | Date | now | Validity reference time for certificate checks. |
The session object
client.connect() resolves to a Session:
session.attestation— the verification result:measurement,platform,certinfo (incl.cert.sha256), and anywarnings.session.fetch(path, init?)— an over-encrypted request. The entire request — method, path, headers, and body — is sealed with AES-256-GCM and POSTed to the tunnel endpoint, so a proxy in front of the LB sees only ciphertext. Resolves to{ status, headers, bytes, text() }.
What it verifies
connect() (via verifyAttestation) performs these checks, failing closed on the first failure:
- nonce echo — the response carries back the exact nonce the client sent.
- hardware signature + vendor chain — verified in WASM against the bundled AMD roots.
- measurement ∈ allowlist — the report's launch digest matches a pinned measurement (case-insensitive hex).
- identity transcript —
report_dataequalsSHA-384over the length-prefixed(version, mesh-CA digest, mesh-leaf digest, X25519 pubkey, ML-KEM-768 pubkey, nonce). One check proves the evidence is fresh, that the session key was generated inside the attested TEE, and which cluster identity it belongs to. - leaf chains to the pinned mesh CA — cluster identity.
- proof of possession — the ECDSA-SHA384 signature over the transcript verifies under the leaf's public key.
The verified transcript is then the HKDF salt, so a channel cannot be derived from checks that did not pass.
The browser's bare-snp WASM path enforces fewer checks than the Go and Rust verifiers the
cluster runs: it omits the VMPL-0 requirement, debug-policy rejection, the minimum-TCB floor, and
VEK validity/CRL. A browser client would accept a debug-enabled or non-VMPL-0 guest whose
measurement is allowlisted. See Limitations.
Errors
Every failure throws a typed C8sVerifyError carrying a .code (and .details). The codes mirror
the c8s server error envelope where they overlap:
| Code | Meaning |
|---|---|
invalid_request | Bad client arguments (e.g. missing baseUrl, empty measurement allowlist). |
nonce_mismatch | The response did not echo the nonce that was sent. |
verification_failed | Hardware signature / vendor chain or platform check failed. |
report_data_mismatch | report_data did not match the expected transcript. |
measurement_denied | The launch digest is not in the allowlist. |
invalid_cert / cert_chain | The leaf is malformed, or did not chain to the mesh CA. |
identity_binding | meshCaPem is missing, or the proof of possession failed. |
key_binding | The session key is not bound to the attested report. |
channel_error | The handshake or an over-encrypted request failed. |
unsupported | Unsupported platform or protocol feature. |
The server side answers in the same envelope, with binding_unavailable when the LB has no mesh
identity credentials configured and attestation_unavailable when it cannot reach its
attestation-api.
Lower-level: verifying bare evidence
If you obtain SNP evidence through your own transport (e.g. a
discovery document) rather than the c8s-verify/v1
challenge/response bundle, use verifyEvidence. It runs the same hardware verification and
measurement / platform checks, plus a report_data binding when you pass expectedReportData —
with no bundle, nonce, session key, or certificate required (do any mesh-CA chaining yourself):
import { verifyEvidence } from "c8s-verify";
const r = await verifyEvidence(evidence /* { attestation_report, cert_chain:{ vcek } } */, {
generation: "genoa", // "milan" | "genoa" | "turin" (required for bare snp)
measurements: ["<SHA384_LAUNCH_DIGEST>"],
expectedReportData, // optional Uint8Array, exactly as the producer bound it
});
console.log(r.measurement, r.reportDataMatch, r.claims);Pass expectedReportData unpadded — the anchor the producer bound (48 bytes for c8s
bindings), not the zero-padded 64-byte hardware field. The raw WASM entrypoints verifySnp and
verifyAzSnp are also exported for full control.
Try it offline
The repo ships a self-contained mock LB so you can run the whole flow with no TEE:
npm install
npm run gen-fixtures # openssl mesh CA + leaf, copies recorded SNP evidence
npm run demo # serves the mock LB + browser demo on http://localhost:8799Open the page and click Run verification; each step shows green or red, and a Tamper with
evidence toggle flips a byte of the signed report to demonstrate failing closed. The recorded
evidence is real hardware-signed SNP evidence, so the signature, measurement, certificate chain,
and post-quantum channel are all genuine — only the live report_data binding is simulated, which
is the same downgrade cds-attest --evidence-fixture serves and why that flag is dev only.
See also
- The verification model — the design this library implements, and why it pins two values.
- Verified chat over confidential vLLM — the library end to end against a real cluster.
- Verify a deployment — the operator-side path, for TDX and for CI.