How it works
One call, five steps, cryptographically grounded.
Your integration surface is one dumb client-side evidence collect and one server→server appraisal. The cryptographic surface beneath it (chain validation, AIA chasing, cloud cross-validation, policy resolution) is what makes the answer trustworthy. Here is every step.
Step 1
The client collects device evidence
On the device — via the native client or the Root Herald browser extension — the SDK collects a fresh block of device evidence (a TPM quote plus EK certificate chain) tied to your request's action string (signup, claim, vote, game-launch, or your own). The client holds no Root Herald keys and renders no verdict — it hands the opaque evidence back to your app, which forwards it to your backend with the rest of the request. The server appraises it.
// Collect fresh device evidence (TPM quote + EK chain) over a
// server nonce. Keyless: no key is consulted, no verdict is rendered.
char* evidence = nullptr;
RootHeraldClient_CollectEvidence(nonce, &evidence);
// Hand the opaque `evidence` blob to your backend to appraise (step 5).
RootHeraldClient_FreeEvidence(evidence);
// On the web, the extension collects the same opaque evidence; your
// backend appraises it with @rootherald/node (see step 5).Step 2
The device attests under a fresh nonce
On Windows / Linux, the SDK talks to the local native host over the extension's native-messaging API. The host issues a TPM2_Quote over PCRs 0–7 under the server nonce, reads the EK public key and certificate from the chip, and pulls intermediates from the Windows TPM store, Intel ODCA NV handles (PTT), or the AMD AIA endpoint (fTPM) as needed.
Windows and Linux ship today. Mobile and macOS collectors are forthcoming: on Android, Hardware Key Attestation will produce the chain (StrongBox or TEE); on iOS, App Attest an assertion; on macOS, Secure Enclave a signed assertion at reduced assurance.
Everything is signed under the server's nonce. A quote captured yesterday carries the wrong nonce and is rejected at step 5.
Step 3
Root Herald validates the EK cert chain
EkCertificateValidator walks the chain. When intermediates are missing, the AiaChaser follows AIA URLs (up to 16 levels) backed by a Postgres + in-memory cache with stale-while-revalidate. The chain must terminate at one of our 40 pinned manufacturer roots.
Operational failures (EkChainFetchFailed) are distinguished from cryptographic ones (EkNotTrusted, EkKeyMismatch) via ChainFailureKind, so the former are retry-eligible and the latter never are.
Step 4
Root Herald applies your policy
PolicyResolver picks the policy by three-rung resolution: per-call → per-session → per-project default → strict-hardware fallback. Five built-ins cover most needs (strict-hardware, strict-hardware-permissive-mobile, cloud-permissive, enterprise-managed-only, dev), or you author your own through the admin API.
For cloud-permissive policies, the cross-validators run here: NitroTPM + EC2 instance identity bind the vTPM to a specific instance; Azure IMDS-attested data and the GCP instance-identity JWT do the same for their VMs. A mismatch rejects with the audit event cloud_cross_validation_failed.
Step 5
Your backend gets the verdict
In the default RATS Background-Checkflow your server submits the client's opaque evidence to Root Herald server→server (with its rh_sk_ secret key) and gets the typed AttestationVerdict back directly — no token to verify. The verdict carries a tenant-scoped device.ueid: stable per chip per tenant, uncorrelated across tenants. (If you opt into a portable token, or run the backend-less badge tier, Root Herald instead signs an EAT/EAR JWT against the tenant JWKS at /<tenant>/.well-known/jwks.json that you verify offline with @rootherald/node or RootHerald.AspNetCore.)
const rh = new RootHerald({ secretKey: process.env.RH_SECRET_KEY }); // rh_sk_…
const { challengeId, nonce } = await rh.issueChallenge(); // relay nonce to client
const verdict = await rh.verify(evidence, { // appraise server→server
challengeId,
policy: "rootherald:builtin:strict-hardware",
});
// verdict ≈ AttestationVerdict
{
"acr": "urn:rootherald:device:high",
"device": {
"ueid": "2f9c8a14…", // stable per-tenant device id (no PII)
"verdict": "pass", // pass | warn | fail
"earStatus": "affirming", // affirming | warning | contraindicated
"attestationType": "tpm20",
"secureBootVerified": true,
"trustworthinessVector": { "hardware": 2, "configuration": 2 }
}
}What you get out
The contract.
Whatever your branching logic (allow / friction / deny / step-up / rate-limit / grant-credits), the contract is the same: pass device.ueid into your existing business logic, store it as the per-tenant device pseudonym, and you have a hardware-rooted identity layer under whatever you build next.
See the whole flow run on your own device.
Free up to 10K attestations a month, no card.