Integrate the native collector into a game client
A walkthrough for game studios wiring the native SDK into a launcher or matchmaker. Targets Windows (NCrypt PCP), macOS (Secure Enclave), and Linux dedicated servers. Sidesteps the browser and works inside your existing anti-cheat / DRM envelope. Following the Background-Check default, the native client is a dumb collector: it gathers a hardware quote and hands the opaque evidence to your matchmaker, which appraises it server→server with rh.verify().
Where the integration lives
Three places in a typical game stack make sense:
- Launcher (recommended). Before the launcher hands off to the game executable, collect evidence and call your matchmaker (which appraises it server→server). If the matchmaker rejects, refuse to launch. Lowest friction; highest assurance.
- First-tick of the game.Boot into a minimal lobby scene, collect, post, refuse to load any map until your matchmaker accepts. Useful if you don't control the launcher (e.g. Steam-distributed without a custom launcher).
- Matchmaker handshake.Server-side: when the client connects, demand a fresh attestation as part of the join handshake. Works even with mod-supporting / private-server scenarios where launcher attestation doesn't flow through.
Link the native SDK
The SDK ships as a static archive (RootHerald.lib on Windows, librootherald.a on Linux/macOS) behind one C99 header, rootherald.h. Link it into your launcher; no runtime DLL/.so to ship.
find_package(RootHerald REQUIRED)
add_executable(launcher
src/main.cpp
src/herald_gate.cpp)
target_link_libraries(launcher PRIVATE
RootHerald::RootHerald
YourGameCore::Core)Collect evidence inside the launcher
RootHeraldClient_CollectEvidence gathers a fresh hardware quote over a nonce your matchmaker minted (via POST /api/v1/attestations/challenge) and writes the opaque evidence JSON into a caller-owned buffer; it consults no API key and never contacts Root Herald. You POST that blob to yourmatchmaker, which appraises it server→server (Step 3) and returns the decision. Following Background-Check, the PASS/WARN/FAIL verdict is your server's decision under your acceptance policy, never the client's.
#include <rootherald.h>
#include <stdexcept>
#include <string>
// Collects the opaque evidence blob over a server-issued nonce. Holds no key
// and never contacts Root Herald — the caller POSTs the blob to your matchmaker,
// which appraises it server→server. Throws on a TPM / collection failure.
std::string CollectEvidenceForLaunch(const char* nonceB64)
{
char* evidenceJson = nullptr;
RootHeraldStatus status =
RootHeraldClient_CollectEvidence(nonceB64, &evidenceJson);
if (status != ROOTHERALD_OK)
{
throw std::runtime_error(
std::string("rh_collect_failed: ") + RootHerald_ErrorString(status));
}
std::string evidence = evidenceJson; // self-contained evidence blob
RootHeraldClient_FreeEvidence(evidenceJson); // caller owns + frees the buffer
return evidence; // POST verbatim to your matchmaker -> rh.verify()
}On Windows firmware that rejects the unprivileged NCrypt/PCP path, the SDK falls back to spawning your executable elevated once with --establish-key <url> <path>. Route that argv to RootHerald_RunElevatedEstablishKey at the very top of main() before any normal startup, as shown in the native SDK's INTEGRATING.md.
Appraise the evidence server-side
Your matchmaker mints the challenge, relays the nonce to the launcher (Step 2), receives the opaque evidence, and appraises it server→server with rh.verify() — the verdict comes back directly. Use the stable device.ueid to enforce one-account-per-device, ban lists, or session tickets.
import { RootHerald } from "@rootherald/node";
const rh = new RootHerald({ secretKey: process.env.RH_SECRET_KEY! }); // rh_sk_…
// (a) the launcher first asks for a challenge; relay the nonce to it.
app.post("/matchmake/challenge", async (_req, res) => {
const { challengeId, nonce } = await rh.issueChallenge();
res.json({ challengeId, nonce });
});
// (b) the launcher posts back the opaque evidence; appraise it server→server.
app.post("/matchmake/join", async (req, res) => {
const verdict = await rh.verify(req.body.evidence, {
challengeId: req.body.challengeId,
policy: "rootherald:builtin:strict-hardware",
});
if (verdict.device.verdict !== "pass")
return res.status(403).json({ error: "device_rejected" });
// verdict.device.ueid is the stable per-tenant device id. Enforce
// one-account-per-device or check a ban list.
if (await usersRepo.deviceBanned(verdict.device.ueid))
return res.status(403).json({ reason: "device_banned" });
res.json({ sessionTicket: makeSessionTicket(verdict.device.ueid) });
});Ban from anti-cheat hits
When your anti-cheat detects cheating, ban the device, not just the account. That's the structural change a hardware-rooted device id enables: ban-evasion requires the cheater to buy a new physical device, not just a new email.
async function banCheater(accountId: string) {
const user = await usersRepo.find(accountId);
// user.deviceUeid was persisted from verdict.device.ueid at join time.
await usersRepo.banDevice({
ueid: user.deviceUeid,
reason: `anti-cheat hit: ${user.lastCheatSignal}`,
});
await usersRepo.suspend(accountId);
}Anti-cheat coexistence
Root Herald is complementary to BattlEye / EAC / Vanguard / Ricochet. They attest the process; we attest the device. The right composition:
- Anti-cheat rejects players whose process looks tampered.
- Root Herald rejects players whose deviceisn't a real, distinct, ban-list-clean machine.
Together they close both halves of the smurfing problem: smurfs running fresh accounts on the same PC, and smurfs running anti-detect rigs that fake a process to look clean.
If you ship on Unity or Unreal, the embedded-SDK doc shows the C++ surface, but you probably want the engine-specific bindings instead: Unity plugin (Wave 3), Unreal plugin (Wave 3).