Every status the native SDKs return
RH_STATUS is the whole diagnostic surface of the native client SDKs — they emit no log messages. Branch on the symbol; each one names a different next action.
This is a different surface from the API's errors
These are library return values, handed back to your process by code you linked. They are not what the Shield API returns to your backend over HTTP — that is a string error value, documented on the API reference. Both are stable contracts; they just live at different layers.
In both cases the stable identifier is the symbol, not the number. The integers below are an artifact of a C enum — useful when you are staring at a debugger, but the symbol is what you branch on and what support will ask you for.
The SDK does not log
There is no log callback and no log level. A static archive gets linked into your signed binary, and shipping our prose inside it helps nobody: you cannot route it, filter it, or correlate it with your own events. The status code is the diagnosis, and it is yours to log in your own stack.
char evidence[8192];
size_t evidence_len = 0;
RH_STATUS st = RootHeraldRespond(h, challenge, NULL,
evidence, sizeof evidence, &evidence_len,
NULL, 0, NULL);
if (st != RH_OK) {
/* Log the SYMBOL, not the number, and not a message we wrote for you. */
my_logger.warn("rootherald respond failed", {{"status", st}});
return;
}RootHeraldErrorString(status) exists and returns a short static English string. It is a convenience for a printf while you are integrating and for a CI harness's failure output — not a substitute for this page, and not something to show an end user.
Codes
| Code | Means | Do |
|---|---|---|
RH_OK= 0 | The call succeeded. | For the buffer-filling calls, *out_len bytes are now in your buffer: relay evidence to your backend, keep a key blob for as long as you want the key. There is nothing to free. |
RH_ERR_INVALID_ARG= 1 | An argument was null, empty, or not the shape the call expects — an empty challenge string, a null out-pointer, a key handle where a session was expected. | Fix the call site. Retrying with the same arguments cannot succeed. |
RH_ERR_TPM_UNAVAILABLE= 2 | No TPM 2.0 or Secure Enclave is reachable on this host at all. A hardware ceiling, not a transient fault. | Gate the feature off for this device and fall back to whatever you do for machines that cannot attest. Do not retry in a loop. |
RH_ERR_NOT_ENROLLED= 6 | No attestation key is enrolled on this device. From EnrollComplete it means something narrower: there is no in-flight EnrollBegin state on this session. | Run the enrollment ceremony. If you saw it from EnrollComplete, the elevated worker that ran EnrollBegin did not stay resident — start again from EnrollBegin on a session that will. |
RH_ERR_ELEVATION_REQUIRED= 7 · Windows only · EnrollBegin, EnrollComplete | Enrollment needs an elevated process and this one is not elevated. The SDK never elevates on your behalf. | Run EnrollBegin and EnrollComplete in an elevated resident worker — one elevation spans both legs. See the Windows elevation patterns reference. Respond and Sign never return this. |
RH_ERR_EK_READ_FAILED= 8 | A TPM is present, but its endorsement key could not be read. Deliberately distinct from TPM_UNAVAILABLE: the chip is there. | Usually fixable on the machine. Check that a resource manager is running (tpm2-abrmd on Linux), that the process can open the TPM device, and that the driver is healthy. |
RH_ERR_AK_FAILED= 9 | Creating, loading, reading or persisting the attestation key failed. The device's TPM state is the problem, not your arguments. | Retry enrollment. If it persists, the persistent handle may be occupied or the TPM may be out of NV space — clearing the TPM resolves both, at the cost of every key on it. |
RH_ERR_ACTIVATION_FAILED= 10 · EnrollComplete | The enrollment challenge could not be consumed — TPM2_ActivateCredential failed, or the enclave could not sign it. | Restart from EnrollBegin. The credential is single-use and is now spent; retrying EnrollComplete with it will always fail. |
RH_ERR_QUOTE_FAILED= 11 · Respond | Reading PCRs or producing the quote failed on a device that is enrolled. | Retry the challenge. Do not re-enroll — the key is fine, and re-enrolling would rotate a device identity your backend has already recorded. |
RH_ERR_ENTITLEMENT_MISSING= 12 · macOS / iOS only | The host executable is not signed with a provisioning profile granting data-protection keychain access, so the Secure Enclave is unreachable from that binary (OSStatus -34018). | Fix your build, not the device. Add the keychain-access-group entitlement and re-sign. An unsigned or ad-hoc-signed binary always fails this way, however healthy the hardware. |
RH_ERR_ASK_UNSUPPORTED= 13 · Respond | The challenge asks for something this platform cannot serve: 'key' or 'posture' on macOS, 'key' with a returned blob on iOS. A platform ceiling, not a fault. | Tell your backend, which will mint a challenge that asks for less. Check RH_POSTURE.app_keys_supported and platform_name before requesting a key challenge and you will never see this. |
RH_ERR_KEY_UNLOADABLE= 14 · LoadKey | The blob no longer loads under its storage parent: the TPM was cleared, or the parent changed. The key is gone for good. | Discard the blob and respond to a fresh 'key' challenge with key = NULL to mint a replacement. Do NOT re-enroll — the attestation key is fine. |
RH_ERR_KEY_CREATE_FAILED= 15 · Respond | TPM2_Create or TPM2_Certify failed for the app key on a device that can otherwise attest. | Retry the challenge once; then report it. The attestation key and the enrollment are unaffected. |
RH_ERR_SIGN_FAILED= 16 · Sign | The key is loaded but TPM2_Sign failed. | Retry. A second failure means the TPM is wedged, not the key — surface it to the operator rather than discarding the blob. |
RH_ERR_BUFFER_TOO_SMALL= 17 | Your buffer is NULL or smaller than the output. *out_len holds the size required. Nothing was written. | Not a fault. Allocate *out_len bytes and call again — the two-call size query is the intended way to size every buffer. |
RH_ERR_INTERNAL= 99 | Nothing above fits — a condition the SDK does not have a name for. | Genuinely unexpected. Capture the call and the platform and open an issue on the SDK repository. |
Two that are not failures
RH_ERR_BUFFER_TOO_SMALL is how you size a buffer. Every function that produces bytes takes (buf, cap, out_len); pass NULL, 0 and it returns this code with *out_len set to what it needs. Allocate that, call again. RootHeraldRespond sets both lengths when either buffer is short, and writes nothing until both fit.
RH_ERR_ASK_UNSUPPORTED is a platform saying what it cannot prove. It is the backend's move, not the device's: mint a challenge with a smaller ask. A challenge that is not an rhc1. string is RH_ERR_INVALID_ARG: there is one challenge format, and relaying the bare nonce instead of the challenge string is a caller bug.
Retired codes
ABI 5.0 removed three codes, and they are never reused — a consumer built against an older header fails to compile rather than silently re-mapping onto a new meaning. ABI 6.0 renamed the prefix from ROOTHERALD_ to RH_ and kept every surviving value.
ROOTHERALD_ERR_NETWORK(3) andROOTHERALD_ERR_QUOTA_EXCEEDED(5) — neither was ever returned by any platform. They described a direct-POST client removed in ABI 3.0. The client holds no key and opens no socket to RootHerald, so it cannot observe either condition.ROOTHERALD_ERR_SERVER(4) — reachable, but misleading. It reported local TPM and crypto failures as "the server returned an error", which sent people to debug the wrong system. Its real causes are nowACTIVATION_FAILEDandQUOTE_FAILED.
Which platforms return which
The enum is shared, and the header is byte-identical across sdk-windows, sdk-linux, sdk-macos and sdk-ios. Not every code is reachable everywhere: ELEVATION_REQUIRED is Windows-only, ENTITLEMENT_MISSING is Apple-only, EK_READ_FAILED and ACTIVATION_FAILED apply to the TPM platforms (macOS proves possession by signing rather than by credential activation), and ASK_UNSUPPORTED is what macOS says to a posture or key ask and iOS says to a key ask that wants a blob back. KEY_UNLOADABLE, KEY_CREATE_FAILED and SIGN_FAILED exist only where app keys do — Windows and Linux. Handle the ones your target platforms can produce; a default arm covers the rest.