Verify a device from a native app
The full client + server flow for a desktop app, game launcher, or embedded agent: your backend issues a challenge, your binary collects a fresh TPM quote through the native SDK, and your backend appraises the evidence. The static library is keyless — your rh_sk_ secret never ships to the device.
The server half — @rootherald/node — is available on npm. The client half, sdk-native, is in development and Windows-only today (the macOS Secure Enclave and Linux tpm2-tss collectors are declared in the ABI but return ROOTHERALD_ERR_INTERNAL until they land). It links as a static library and is keyless — it holds no Root Herald key and opens no socket to Root Herald. You can drive the equivalent chain end to end today at /try.
[server] Set up the server SDK
What to do → create a project in the dashboard, copy its rh_sk_… secret key, and install the server SDK on your backend.
npm install @rootherald/nodeWhat you get → the RootHerald client that issues challenges and appraises evidence. Why → the secret and the verdict live only on your server; the native binary you ship to users is untrusted and keyless.
import { RootHerald } from "@rootherald/node";
const rh = new RootHerald({ secretKey: process.env.RH_SECRET_KEY! }); // rh_sk_…[server] Issue a challenge and relay the nonce
What to do → mint a single-use challenge and send the nonce down your own client↔backend channel to the native app.
app.post("/api/challenge", async (_req, res) => {
const { challengeId, nonce } = await rh.issueChallenge();
res.json({ challengeId, nonce });
});What you get → { challengeId, nonce }. Why → the TPM quote is taken over nonce, so freshness and anti-replay are bound inside the signature.
[client] Collect evidence with the native SDK
What to do → link RootHerald.lib (Windows) into your binary and call the C ABI: create a client, collect over the relayed nonce, then free the buffer.
#include "rootherald.h"
// nonce_b64 came from YOUR backend's /api/challenge (relayed to this process).
char* collect_evidence(const char* nonce_b64) {
RootHeraldClient* client = RootHeraldClient_Create(); // keyless: no key, no endpoint
if (!client) return NULL;
char* evidence_json = NULL;
RootHeraldStatus rc =
RootHeraldClient_CollectEvidence(nonce_b64, &evidence_json);
if (rc == ROOTHERALD_ERR_NOT_ENROLLED) {
// First run on this device — bootstrap the key first (see note below),
// then retry CollectEvidence.
}
RootHeraldClient_Destroy(client);
// Caller owns evidence_json; send it, then:
// RootHeraldClient_FreeEvidence(evidence_json);
return (rc == ROOTHERALD_OK) ? evidence_json : NULL;
}If CollectEvidence returns ROOTHERALD_ERR_NOT_ENROLLED, bootstrap the device key with the keyless two-leg handshake: RootHeraldClient_EnrollBegin emits the /devices/enroll request body, your backend relays it (via @rootherald/node) and hands back the challenge, then RootHeraldClient_EnrollComplete emits the /devices/activate body. Both legs run under a single elevation and never open a Root Herald socket. Full API in the Native C++ SDK reference.
What you get → a newly-allocated, opaque evidence_json blob you own and must free with RootHeraldClient_FreeEvidence. Why → the blob is exactly what /attestations/verify expects; the client forwards it verbatim and never inspects a verdict.
[wire] Send the blob to your backend
What to do → ship { challengeId, evidence }from the native app to your backend over your own transport (HTTPS, your game's existing session channel, whatever you already use). Why → only your backend holds rh_sk_; the client's job ends at handing over the blob.
[server] Appraise the evidence and act
What to do → submit the blob and branch on the verdict — identical to every other client track.
import { QuotaExceededError } from "@rootherald/node";
app.post("/api/verify", async (req, res) => {
const { challengeId, evidence } = req.body;
let verdict;
try {
verdict = await rh.verify(evidence, {
challengeId,
policy: "rootherald:builtin:strict-hardware",
});
} catch (err) {
if (err instanceof QuotaExceededError) return res.status(429).end();
throw err;
}
if (verdict.device.verdict !== "pass") {
return res.status(403).json({ error: "device_check_failed" });
}
res.json({ ok: true, deviceId: verdict.device.ueid });
});What you get → a verdict with device.verdict, device.ueid, device.earStatus, and acr. Why → the enforcement decision is yours, made server-side where the key and the trust anchors live.
A worked example, and building your own collector
For a native collector living inside a game launcher — the canonical embedded case — the embedded game SDK guide walks a launcher that gates match entry on a device verdict. If you need a collector on a platform the SDK doesn't cover yet, or your own native messaging host, the Native C++ SDK reference documents the complete C ABI (enroll, collect, posture, lifecycle) you build against.