Skip to content
Reference

Windows elevation patterns

First-time enrollment on Windows creates the attestation key and runs TPM2_ActivateCredential. The OS permits those operations only for an elevated (administrator) process. Everything afterwards — answering a challenge (RootHeraldRespond), minting an app key for a key ask, signing with it (RootHeraldSign) — is unprivileged, forever. The native SDK never elevates on your behalf. It returns RH_ERR_ELEVATION_REQUIRED and lets you choose how (and whether) to elevate. The common strategies for acquiring that one elevation are below.

Attest first, so you elevate at most once

Trigger enrollment only on a genuine miss — see Re-attestation and step-up. If you ship an installer, Pattern 1 below means you never prompt at all.

The SDK reports; you decide

RootHeraldEnrollBegin and RootHeraldEnrollComplete return RH_ERR_ELEVATION_REQUIRED (value 7) when the calling process is not elevated. A process that is already elevated (for example a Windows service) never sees that code; enrollment just runs in-process, no prompt. RootHeraldRespond, RootHeraldLoadKey and RootHeraldSign never return it — a key ask creates the app key under the storage parent without elevation, and signing with it is one unprivileged TPM command. Acquiring elevation is a policy decision that belongs to your application, not to a linked library, so the SDK stops and tells you rather than silently spawning a UAC.

The resident elevated worker (every strategy needs it)

Whatever gets you into an elevated context performs the enrollment ceremony there. Enrollment is two legs that share one open session, so the elevated worker must stay resident across the round-trip: it calls EnrollBegin, relays the emitted blob — together with the id of the challenge the device was answering — through your backend (which holds the rh_sk_ secret), then calls EnrollComplete with the returned credential. Admission runs under that challenge's policy, so a TPM class it refuses comes back 422 admission_refused on the first leg — which is why the worker should check RootHeraldPreCheck and relay leg one before prompting where it can. The usual way in is to relaunch your own executable with an argv hook, routed before any normal startup:

main.cpp: argv hookcpp
int main(int argc, char** argv) {
    // Runs ELEVATED (your strategy relaunches the exe with this arg). Creates the
    // AK + activates it, relaying the two blobs through your backend, and stays
    // resident across the relay round-trip. See the device-enrollment guide.
    if (argc >= 2 && strcmp(argv[1], "--enroll-worker") == 0)
        return RunEnrollWorker(/* IPC channel to your unprivileged process */);
    /* ... normal startup ... */
}

// Inside the elevated worker, on ONE session h:
//   RootHeraldEnrollBegin(h, buf, cap, &len);                       // -> relay + challengeId to backend
//   /* backend returns the sealed credential (or 422 admission_refused) */
//   RootHeraldEnrollComplete(h, cred, cred_len, buf, cap, &len);   // -> relay
// Keep 'h' open across both calls (the EK+AK context must persist).
Same tenant on both sides

The elevated worker and your unprivileged client must relay to the same RootHerald tenant, or the two halves bind against different servers. Enrollment is keyless on the device: the rh_sk_ secret lives only in your backend, never in the client or the elevated worker.

Pattern 1: Enroll during installation (best, if you ship an installer)

Your installer already runs elevated and the user has already approved that prompt, so enrollment there is free: no second UAC, no relaunch, no IPC, and nothing for your application to do at runtime. Enrollment is device-scoped, not user-scoped: it binds a key to the chip, not to an account, so it needs no logged-in user and can run long before anyone signs in.

Run the ceremony from a custom action (MSI), an install step (NSIS / Inno), or your post-install task. It is the same two calls every other pattern makes; the only requirement is that the installer can reach your backend to relay them, and that the process stays alive across that one round-trip.

installer-custom-action.cppcpp
// Runs inside your ALREADY-ELEVATED installer. ELEVATION_REQUIRED never appears here.
// challengeId names the challenge your backend minted for this install; admission
// runs under its policy.
static bool EnrollDeviceAtInstall(RH_HANDLE h, const std::string& challengeId) {
    char buf[8192]; size_t len = 0;
    if (RootHeraldEnrollBegin(h, buf, sizeof buf, &len) != RH_OK)
        return false;                       // no TPM, or TPM unavailable -- see below

    // Relay to YOUR backend, which holds the rh_sk_ secret and calls
    // POST /api/v1/attest/enroll on your behalf. Blocks for one round-trip.
    // An empty result here includes 422 admission_refused: the policy says no.
    std::string credential = MyBackend_RelayEnroll(std::string(buf, len - 1), challengeId);
    if (credential.empty()) return false;

    if (RootHeraldEnrollComplete(h, credential.data(), credential.size(),
                                 buf, sizeof buf, &len) != RH_OK)
        return false;

    return MyBackend_RelayActivate(std::string(buf, len - 1));   // POST /api/v1/attest/activate
    // device is now enrolled, for the life of the machine; nothing to free
}
Do not fail the install if enrollment fails

A machine with no TPM, a disabled TPM, or no network at install time must still install your product. Treat enrollment as best-effort here and fall back to Pattern 2 on first launch for the devices that missed it — your application should be checking attest-first anyway, so the fallback costs you no extra code.

Pattern 2: Self-elevation shim (desktop apps with no installer)

On RH_ERR_ELEVATION_REQUIRED, relaunch your own exe elevated (one UAC) with --enroll-worker; the argv hook above routes it, and it talks back to your unprivileged process over an IPC channel (a named pipe) for the relay.

self-shim.cppcpp
// Returns the elevated child's exit code (0 = enrolled, non-zero = UAC declined / failed).
static int RunSelfElevationShim(const wchar_t* pipeName) {
    wchar_t exe[MAX_PATH] = {0};
    GetModuleFileNameW(NULL, exe, MAX_PATH);

    wchar_t args[512] = {0};
    swprintf_s(args, 512, L"--enroll-worker \"%s\"", pipeName);

    SHELLEXECUTEINFOW sei = { sizeof(sei) };
    sei.fMask  = SEE_MASK_NOCLOSEPROCESS;
    sei.lpVerb = L"runas";          // the UAC consent prompt
    sei.lpFile = exe;
    sei.lpParameters = args;
    sei.nShow  = SW_HIDE;
    if (!ShellExecuteExW(&sei) || !sei.hProcess) return 1; // declined / no desktop

    WaitForSingleObject(sei.hProcess, INFINITE);
    DWORD code = 1; GetExitCodeProcess(sei.hProcess, &code);
    CloseHandle(sei.hProcess);
    return (int)code;
}

// Use it: only when the device has no key yet (attest-first — see re-attestation).
if (st == RH_ERR_NOT_ENROLLED /* from RootHeraldRespond */)
    RunSelfElevationShim(pipeName);
If the worker creates the IPC pipe, label it for the medium-integrity client

When an elevated worker creates the named pipe and your unprivileged (medium-integrity) process connects to it, create the pipe with an explicit security descriptor carrying a medium integrity label (S:(ML;;NW;;;ME)). A pipe made with default security inherits the worker’s high integrity, and Mandatory Integrity Control blocks the medium client from opening it: the connect fails and enrollment hangs. RootHerald’s own native host does this for you; this only matters if you build the worker yourself.

Pattern 3: Use the RootHerald host (browser / native-messaging)

If your end users reach attestation through a browser, ship our rootherald_host.exe (the Native Messaging Host). It already implements the resident elevated worker, the single “Establish hardware key” UAC, and the medium-integrity pipe handling. Your application writes no native code; the @rootherald/browser SDK drives it, and enrollment is triggered attest-first (only on a real miss). This is the lowest-effort path for web-integrated products.

Pattern 4: Privileged helper or service

If your app already runs elevated, or you can install a small signed helper / Windows service, run the enroll ceremony directly from that elevated context, with no relaunch and no UAC at attestation time:

privileged-helper.cppcpp
// Runs inside YOUR already-elevated helper / service. No ELEVATION_REQUIRED here.
char buf[8192]; size_t len = 0;
RootHeraldEnrollBegin(h, buf, sizeof buf, &len);                     // -> relay + challengeId to your backend
/* backend returns the sealed credential */
RootHeraldEnrollComplete(h, cred, cred_len, buf, sizeof buf, &len); // -> relay
// The device's attestation key is now persisted; the key context is evicted to a
// persistent handle, so your unprivileged app can Respond and Sign with no prompt.

A long-running elevated service is also the cleanest fit for unattended fleets where no interactive user is present to approve a UAC.

Pattern 5: Check-then-skip (sandboxed / locked-down apps)

Microsoft Store (MSIX) apps, tightly sandboxed processes, and some enterprise-locked environments cannot elevate at all. Detect that and degrade gracefully; never spin on a prompt you can't show:

degrade.cppcpp
char buf[8192]; size_t len = 0;
RH_STATUS st = RootHeraldEnrollBegin(h, buf, sizeof buf, &len);
if (st == RH_ERR_ELEVATION_REQUIRED) {
    // We can't elevate here. Don't prompt; record it and continue without
    // hardware attestation (or defer enrollment to a context that can elevate).
    log_warn("device not hardware-attested: enrollment needs elevation");
    return CONTINUE_WITHOUT_ATTESTATION;
}
Error codes

RH_ERR_ELEVATION_REQUIRED (value 7) is returned only on Windows, only by EnrollBegin / EnrollComplete, and only when the process is not elevated — never by Respond, LoadKey or Sign. RH_ERR_NOT_ENROLLED (value 6) from Respond means “no key yet, run enrollment.” RootHeraldErrorString() renders both as human-readable hints. Treat them as actionable (“elevate / enroll, then retry”), not as hard failures. The full table is on the status-codes reference.