Policies
A policy decides which chips count and how strictly. It is named on the challenge; verify may tighten it and never loosen it.
const { challengeId, challenge } = await rh.issueChallenge({
ask: ["posture"],
policy: "rootherald:builtin:strict-hardware", // or the id of a policy you created
});
const result = await rh.verify(evidence, { challengeId }); // judged under the challenge's policy
await rh.verify(evidence, { challengeId, policy: "rootherald:builtin:dev" }); // 422 policy_downgrade
await rh.relayEnroll(enrollRequestBlob, { challengeId }); // admission under the same policy: 422 admission_refused for a rejected classThe five built-ins
| rootherald:builtin:… | Accepts | Gates |
|---|---|---|
| strict-hardware | hardware, firmware-tpm, mobile-hardware | Default. Event log + signed quote, EAR affirming, device:high. Rejects every virtual and emulated TPM. |
| strict-hardware-permissive-mobile | same, plus Android TEE and macOS Secure Enclave | No event-log or device:high floor; EAR warning. For consumer surfaces where StrongBox is not universal. |
| cloud-permissive | + cloud-vtpm, cross-validated | EAR warning, permissive mode. Endpoints and your own cloud workloads. |
| enterprise-managed-only | hardware, firmware-tpm | Like strict-hardware without mobile. Pair with your own enrolment gate. |
| dev | everything, including emulated-swtpm | No signed quote required. Development only; never affirming. |
Built-ins are immutable. Omit policy and the project default applies (strict-hardware unless you changed it in the dashboard). Classes and groups are on the TPM classes page.
What each knob gates
| Field | Type | Gates |
|---|---|---|
| acceptedClassGroups | string[] | Groups admitted: hardware, firmware-tpm, cloud-vtpm, emulated, mobile-hardware, mobile-software. Expands to new vendors automatically. |
| acceptedSpecificClasses | string[] | Individual classes admitted in addition, e.g. cloud-vtpm-aws-nitro. |
| unacceptedBehavior | "reject" | "warn" | What an out-of-set class gets: a fail verdict, or a warn verdict with a degraded trustworthiness vector. |
| requireSignedQuote | boolean | The evidence must carry a quote signed by the enrolled AK. |
| requireEventLog | boolean | A quote-bound event log must replay to the PCRs. Needs a posture ask. |
| requiredPcrs | int[] | PCRs the quote must cover. Built-ins use [0,1,2,3,4,7]. |
| minimumEarStatus | "affirming" | "warning" | The EAR floor a device must reach to pass. |
| minAcr | URN | urn:rootherald:device:high requires the verified boot chain. user:* tiers are unreachable on this path and always reject. |
| mode | "strict" | "permissive" | Whether a missing optional check fails the verdict or degrades it. |
| requireCloudCrossValidation | boolean | A cloud-vtpm class must also present provider instance-identity evidence binding the vTPM to one instance. |
| allowMacosReduced | boolean | Admit macOS Secure Enclave evidence at reduced assurance. |
| minCohortPrevalence / cohortScope / onNovelProfile | see below | The additive cohort-prevalence constraint. |
Custom policies
Dashboard → Policies → New, or the admin API. The response carries the id you name on challenges or set as the project default. PATCH the same path with only the fields to change.
curl -X POST https://rootherald.io/api/v1/admin/policies \
-H "Authorization: Bearer rh_sk_…" -H "Content-Type: application/json" -d '{
"name": "oem-fleet",
"acceptedClassGroups": ["hardware", "firmware-tpm"],
"acceptedSpecificClasses": [],
"unacceptedBehavior": "reject",
"requireSignedQuote": true,
"requireEventLog": true,
"requiredPcrs": [0, 1, 2, 3, 4, 7],
"minimumEarStatus": "affirming",
"minAcr": "urn:rootherald:device:high",
"mode": "strict"
}'
# 201 { "id": "8f3a…", "name": "oem-fleet", "isBuiltIn": false, "version": 1, … }Two shapes worth knowing. unacceptedBehavior: "warn" with the strict class set lets everything through with a warn verdict, so you can observe before enforcing. Listing the four hardware-discrete-* classes in acceptedSpecificClasses with an empty group list admits discrete silicon only.
Cloud cross-validation
A cloud-vtpm class under requireCloudCrossValidation must also submit the provider's instance-identity evidence, and the verifier binds the vTPM's EK to that instance: AWS Nitro attestation document plus EC2 instance identity, Azure IMDS attested data, GCP instance-identity JWT. Evidence relayed from a different instance fails. The client bundles it into the same opaque blob.
Cohort prevalence
How common this device's boot configuration is among distinct real devices of the same TPM family, firmware build and Secure Boot platform key. Evaluated after every explicit check passes; it can add a reason to reject, never remove one. Off by default.
{
"cohortKey": "c0a7f31e…", // opaque id for this exact boot configuration
"cohortScope": "global", // "global" | "tenant-fleet"
"cohortSampleSize": 48211, // distinct devices in the denominator
"cohortPrevalence": 0.034, // min over PCR 0, 4, 7; omitted below the sample floor
"cohortPrevalencePerPcr": { "0": 0.91, "4": 0.88, "7": 0.034 },
"novelProfile": false // true on cold start or below the floor
}{
"minCohortPrevalence": 0.05, // null = off; claims are still emitted
"cohortScope": "tenant-fleet", // "global": every device of the same kind; "tenant-fleet": your own
"onNovelProfile": "warn" // "warn" | "step-up" | "reject"
}A new firmware build is rare until adoption grows, so warn is the default and cohortPrevalence is omitted while the cohort is small. Counters are distinct-device cardinality estimates over authentic evidence only, so re-attesting one device moves nothing. Dashboard → Cohort allowlist approves a known-good cohortKey for your tenant so a rare-but-trusted image stops being flagged.
An unknown, deleted or foreign policy id is 422 unknown_policy; nothing is substituted. A policy whose minAcr is a user:* tier rejects every device on this path.