Real & secure hardware
Some features should run onlyon genuine physical hardware that booted securely: not a cloud VM’s virtual TPM, not a software emulator. This tutorial builds that gate end to end. Require a real hardware/firmware TPM, a verified Secure Boot chain, and a quote-bound measured-boot event log, and reject everything else. Every check is server-validated and bound to a signed TPM quote, so a client can’t forge its way past it.
You don’t hand-roll the boot checks. The built-in rootherald:builtin:strict-hardware policy (the conservative default) accepts only hardware and firmware-tpm TPM classes (so cloud vTPMs and emulators are rejected), requires a signed quote, requires a quote-bound event log, requires Secure Boot to be enabled, and floors the verdict at affirming. You pass the policy name to verify(); the server returns an affirming verdict only when all of that holds.
1. Set up the server client
import { RootHerald } from "@rootherald/node";
export const rh = new RootHerald({ secretKey: process.env.RH_SECRET_KEY! });Enrollment is a prerequisite (a device must have an attestation key before it can quote) — see the hardware-ID tracking tutorial for the relay wiring. This guide is about the appraisal.
2. Attest against the strict-hardware policy
Issue a challenge, let the page collect a fresh quote over the nonce, then verify with the policy. The policy is what turns “a valid quote” into “a valid quote from real, secure hardware.”
// 1) mint a single-use challenge
const { challengeId, nonce } = await rh.issueChallenge();
// 2) hand `nonce` to the page; it calls attest(nonce) and posts `evidence` back
// 3) verify under the hardware-only, secure-boot policy
const verdict = await rh.verify(evidence, {
challengeId,
policy: "rootherald:builtin:strict-hardware", // (also the default if omitted)
});import { attest, enroll, NotEnrolledError } from "@rootherald/browser";
async function collect(nonce, relay) {
try {
return await attest(nonce); // unprivileged quote, no prompt
} catch (e) {
if (e instanceof NotEnrolledError) { // first run on this device
await enroll(relay); // the one-time elevation
return await attest(nonce);
}
throw e;
}
}3. Read the verdict
For real-and-secure, an affirming verdict is your green light: the policy already guaranteed hardware class + Secure Boot + measured boot. The device booleans let you display or log the specifics.
if (verdict.device.earStatus !== "affirming") {
// Contraindicated / warning: not genuine secure hardware under this policy.
return res.status(403).json({ error: "requires_secure_hardware" });
}
// Green light. What you now KNOW (all server-validated, quote-bound):
const d = verdict.device;
d.attestationType; // "tpm20" — a TPM device (vs apple-se / android-ka)
d.quoteVerified; // true — the quote signature checked against the enrolled AK
d.secureBootVerified; // true — Secure Boot established from the quote-bound event log
d.eventLogVerified; // true — the event log replayed and bound to the signed quote
d.platform; // "windows" | "linux" | ...
grantSecureHardwareFeature(verdict.device.ueid);secureBootVerified and eventLogVerified are computed on the server from an event log that had to reconstruct the exact PCR values the TPM signed. A client can’t set them: an event log that doesn’t match the signed quote is rejected before Secure Boot is even read. This is the difference between “the client told us Secure Boot is on” and “the hardware proved it.”
4. What gets rejected, and why
Under strict-hardware, each of these returns a fail (contraindicated) verdict:
| Device / condition | TPM class | Why it fails |
|---|---|---|
| AWS / Azure / GCP / Hyper-V VM | cloud-vtpm | Class not in the policy’s accepted set (hardware + firmware-tpm only). |
| swtpm / software emulator | emulated | No hardware root of trust; class rejected. |
| Real TPM, Secure Boot OFF | hardware / firmware-tpm | Quote-bound event log shows Secure Boot disabled → contraindicated. |
| Revoked boot component (dbx) | hardware / firmware-tpm | A forbidden signature in the boot chain → contraindicated. |
| No event log / PCR-only | any | Policy requires a quote-bound event log; a bare quote can’t prove boot posture. |
An unrecognized EK (no vendor chain) classifies as Unknown and is rejected too. See the TPM-class taxonomy for how the class is derived.
5. Tune it (optional): a custom policy
If strict-hardware is close but not exact (say you also accept mobile hardware, require a specific ACR, or want to reject rare boot configurations), author a custom policy in the dashboard, or via the admin API, and pass its name to verify(). Configurable knobs include the accepted TPM-class groups, whether an event log and signed quote are required, the required PCR set, a minimum ACR, and an opt-in cohort-prevalence floor (reject boot configs that are novel / too rare).
POST /api/v1/admin/policies
{
"name": "my-secure-hw",
"acceptedClassGroups": ["hardware", "firmware-tpm"], // no cloud-vtpm / emulated
"requireEventLog": true,
"requireSignedQuote": true,
"minAcr": "urn:rootherald:device:high"
}Then: rh.verify(evidence, { challengeId, policy: "my-secure-hw" }). Built-in policies are immutable; custom ones are tenant-scoped and versioned.
6. Gate a whole route (token path)
If you issue signed attestation tokens (returnToken: true) and want to gate an API route on device assurance, the requireAttestation middleware verifies the token and enforces an ACR — its device track additionally demands quoteVerified && secureBootVerified && eventLogVerified for device:high:
import { requireAttestation } from "@rootherald/node";
app.post(
"/api/secure-action",
requireAttestation({
issuer: "https://api.rootherald.io",
audience: "your-app",
acrValues: ["urn:rootherald:device:high"], // real + secure-boot + measured boot
maxAgeSeconds: 300, // require a fresh attestation
}),
(req, res) => {
// reaches here only for genuine, securely-booted hardware
res.json({ ok: true });
},
);Nothing here trusts the client. The TPM class comes from a vendor-chained EK; Secure Boot and the boot chain come from an event log that had to reproduce the signed PCRs; the policy decision runs on our side. The client collects evidence; it never renders the verdict.