Challenges and verdicts
A challenge names what the device must prove and the policy that judges it. The device answers; verify returns the verdict on the same call.
const { challengeId, challenge } = await rh.issueChallenge({
ask: ["identity", "posture"], // "identity" | "posture" | "key"; omitted = identity + posture
policy: "rootherald:builtin:strict-hardware",
keyPurpose: "sign", // only with a key ask
});
// relay `challenge` verbatim → device → `evidence`
const result = await rh.verify(evidence, {
challengeId,
policy: undefined, // omit, or name one at least as strict (else 422 policy_downgrade)
requestedDisclosureClass: "pseudonymous",
});
if (result.enrollmentRequired) { /* enrol, retry */ }
if (result.device.verdict === "pass" && result.assuranceClaimsMet.includes("rootherald:assurance:secure-boot")) { /* … */ }The ask
| ask | Device proves | Collected | Verdict adds |
|---|---|---|---|
| identity | Same enrolled chip as before. | Quote over PCR 7. No event log. | ueid, quoteVerified, real-device. |
| posture | How it booted. | PCRs 0–7, event log, Secure Boot variables. | Boot booleans, trustworthinessVector, cohort block, secure-boot / oem-keyed. |
| key | Holds a fresh key that cannot leave the chip. | Everything posture collects, plus TPM2_Certify of a new P-256 key. | key block on pass. |
Asks nest. Not every platform serves every ask: macOS answers identity only, iOS identity and key. The device refuses what it cannot serve (AskUnsupportedError / RH_ERR_ASK_UNSUPPORTED) before touching the network; the fix is a smaller ask. Matrix on the app keys page.
The challenge string
rhc1.<base64url nonce>.<base64url ask-json> single-use, five-minute TTLRelay it verbatim and never parse it. The client reads the ask from it; the server binds the ask and the policy to the challenge row, so a client lied to about the ask produces evidence the server refuses. A string without the rhc1. prefix is refused by every client (TypeError in the browser, RH_ERR_INVALID_ARG in C).
Respond
const { evidence, key } = await respond(challenge); // @rootherald/browser; key only for a key ask
RootHeraldRespond(h, challenge, NULL, ev, cap, &len, blob, 512, &blen); // C; blob written only for a key ask
let evidence = try await client.respond(to: challenge) // Swift; evidence.jsonData() is the blobUnprivileged everywhere. On a device with no attestation key it fails with a not-enrolled error; see Enrolment.
The verdict
Fields are present or omitted, never null. A smaller ask produces a smaller verdict. A policy that requires a field the ask did not collect fails the verdict.
| Field | Type | Meaning |
|---|---|---|
| assuranceClaimsMet | string[] | rootherald:assurance:* URNs the device satisfied. Gate with required ⊆ met. |
| enrollmentRequired | boolean | The device is not enrolled. Enrol and retry; never re-enrol on a policy failure. |
| key | object | Certified public key. Present only for a key ask on a passing verdict. |
| verdict.acr | URN | urn:rootherald:device:any or urn:rootherald:device:high. The tier satisfied, never a weaker substitute. |
| verdict.amr | string[] | Method, RFC 8176. TPM path: ["hwk_tpm"]. |
| verdict.userId | uuid | The device ueid; the device is the principal on this path. |
| verdict.authTime / expiresAt | ISO-8601 | When appraised, and the advisory freshness deadline (about five minutes). |
| device.ueid | uuid | Stable per-tenant device id. Survives OS reinstall and key rotation. No PII. |
| device.verdict | pass | warn | fail | The coarse verdict. |
| device.earStatus | affirming | warning | contraindicated | The IETF EAR tier. Allow, step up, deny. |
| device.attestationType | tpm20 | apple-se | android-ka | ios-appattest | Which root of trust produced the evidence. |
| device.quoteVerified | boolean | The quote signature verified against the enrolled AK. |
| device.secureBootVerified | boolean | A quote-bound event log established Secure Boot. Posture ask only. |
| device.eventLogVerified | boolean | The event log replayed to the signed PCRs. Posture ask only. |
| device.platform | windows | linux | macos | android | ios | Omitted when unresolved. |
| device.hardwareModel | string | Finer model when resolved; usually omitted today. |
| device.trustworthinessVector | Record<string, int> | AR4SI tiers per dimension: instance-identity, configuration, executables, hardware. |
| device.cohort* | block | cohortPrevalence, cohortPrevalencePerPcr, novelProfile, cohortScope, cohortKey, cohortSampleSize. Advisory; see Policies. |
{
"verdict": {
"acr": "urn:rootherald:device:high",
"amr": ["hwk_tpm"],
"authTime": "2026-07-03T17:08:13Z",
"expiresAt": "2026-07-03T17:13:13Z",
"userId": "1ade0832-6a16-cf50-a2bd-2a5c4cb498a5",
"device": {
"ueid": "1ade0832-6a16-cf50-a2bd-2a5c4cb498a5",
"verdict": "pass",
"earStatus": "affirming",
"attestationType": "tpm20",
"platform": "windows",
"quoteVerified": true,
"secureBootVerified": true,
"eventLogVerified": true,
"trustworthinessVector": { "instance-identity": 2, "configuration": 2, "executables": 2, "hardware": 2 }
}
},
"assuranceClaimsMet": ["rootherald:assurance:real-device", "rootherald:assurance:secure-boot", "rootherald:assurance:oem-keyed"],
"enrollmentRequired": false
}Assurance claims
rootherald:assurance:real-device— a hardware, firmware or mobile-hardware class, chain-validated.rootherald:assurance:secure-boot— Secure Boot on, proven by a quote-bound event log.rootherald:assurance:oem-keyed— the Secure Boot platform key is a known OEM's.rootherald:assurance:hardware-bound-key— the certified app key is inside the chip.rootherald:assurance:cloud-vtpm— a cloud virtual TPM, admitted by a permissive policy.
Write gates as claims, not platforms: required ⊆ met ⇒ allow.
Disclosure classes
requestedDisclosureClass on verify caps what the verdict reveals: verdict (booleans and enums, no identifier), pseudonymous (adds ueid; the default), derived (lifecycle facts), full. The policy's default applies when omitted. Webhook endpoints carry their own class.
Policy failures, rejected classes and banned devices return a normal response with device.verdict of fail or warn. Only auth, challenge, evidence, policy and quota problems are 4xx; codes are on the API reference.