Skip to content
Reference · Windows elevation

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. Attestation afterwards (CollectEvidence) is unprivileged, forever. The native SDK never elevates on your behalf. It returns ROOTHERALD_ERR_ELEVATION_REQUIRED and lets you choose how (and whether) to elevate. The common strategies for acquiring that one elevation are below.

First read: attest-first, so you elevate at most once

Elevation is a one-time cost. Before wiring any of the strategies below, make sure you only trigger enrollment on a genuine miss; see Re-attest without a prompt for the attest-first pattern and how to handle a server that no longer recognizes the device. A correct integration shows the UAC once per device, never on repeat attestations.

The SDK reports; you decide

RootHeraldClient_EnrollBegin and RootHeraldClient_EnrollComplete return ROOTHERALD_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. CollectEvidence never returns it. 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 TPM context, so the elevated worker must stay resident across the round-trip: it calls EnrollBegin, relays the emitted blob through your backend (which holds the rh_sk_ secret), then calls EnrollComplete with the returned challenge. 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:
//   RootHeraldClient_EnrollBegin(client, &enroll_json);            // -> relay to backend
//   /* backend returns the MakeCredential challenge */
//   RootHeraldClient_EnrollComplete(client, challenge, &act_json); // -> relay
// Keep 'client' alive 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: Self-elevation shim (most desktop apps)

On ROOTHERALD_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 == ROOTHERALD_ERR_NOT_ENROLLED /* from CollectEvidence */)
    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 highintegrity, 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 2: 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 3: 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* enrollJson = NULL;
RootHeraldClient_EnrollBegin(client, &enrollJson);                 // -> relay to your backend
/* backend returns the MakeCredential challenge */
char* activateJson = NULL;
RootHeraldClient_EnrollComplete(client, challenge, &activateJson); // -> relay
// The device's attestation key is now persisted; the key context is evicted to a
// persistent handle, so your unprivileged app can CollectEvidence 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 4: 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* enrollJson = NULL;
RootHeraldStatus st = RootHeraldClient_EnrollBegin(client, &enrollJson);
if (st == ROOTHERALD_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

ROOTHERALD_ERR_ELEVATION_REQUIRED (value 7) is returned only on Windows, only by EnrollBegin / EnrollComplete, and only when the process is not elevated, never by attestation. ROOTHERALD_ERR_NOT_ENROLLED (value 6) from CollectEvidencemeans “no key yet, run enrollment.” RootHerald_ErrorString()renders both as human-readable hints. Treat them as actionable (“elevate / enroll, then retry”), not as hard failures.