Mint a key
The ceremony that creates a key inside the chip and hands your server its public half.
backend rh.issueKeyChallenge({ purpose, expectedDevices }) → { nonce, keyChallenge: "rhk1c.<nonce>.<purpose>", expiresAt }
device RootHeraldMintKey(keyChallenge, ak) → certification, key blob
backend rh.certifyKey(nonce, certification) → { deviceId, keyId, purpose, alg, format?, jwk, hardwareBound, certifiedAt }Ask for a key challenge
POST /api/v1/keys/challenge with { purpose: "sign" } or { purpose: "decrypt" } and the device IDs you expect. It returns a nonce, your handle for the challenge, and the keyChallenge string, which starts with rhk1c. and carries the purpose. Relay the string to the device as it is.
const { nonce, keyChallenge } = await rh.issueKeyChallenge({
purpose: "sign",
expectedDevices: [deviceId],
});Mint the key on the device
RootHeraldMintKey takes the string and the attestation key blob, creates the key under the TPM's storage parent, has the attestation key certify it over the nonce, and writes the certification into one buffer and the key blob into the other. No elevation, no network. On a Mac the blob selects an enclave key and holds no key material.
st = RootHeraldMintKey(rh, key_challenge, ak, ak_len,
cert, sizeof cert, &cert_len,
blob, sizeof blob, &blob_len);Certify it
POST /api/v1/keys/certify with the nonce and the certification. Root Herald resolves the installation from the certification, checks the attestation key's signature and the key's template, evaluates your key's identity policy against the device, and returns the key: its deviceId, keyId, alg and jwk. Keep the JWK with the account the device belongs to.
const key = await rh.certifyKey(nonce, certification);
await saveKey(accountOf(req), key.deviceId, key.jwk);A decrypt key's response also carries format, the envelope it opens. Keep the whole result; encryptToDevice reads it.
{
"deviceId": "2f9c4a1c-8b…",
"keyId": "7d1eQx2ZkPq0sT9vB4nLmA",
"purpose": "decrypt",
"alg": "ECDH-ES",
"format": "jwe",
"jwk": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" },
"hardwareBound": true,
"certifiedAt": "2026-06-18T18:09:14Z"
}Facts about a key
- One live key per installation per purpose. Minting again rotates it under the same keyId; replace the JWK you kept.
- keyId is new after a re-enrollment. Bind accounts to the device ID, never to a key ID.
- A key challenge works once and expires after five minutes.
- Minting checks no posture. To mint only on a device that booted clean, verify a posture challenge first and mint on a pass.
- alg is ES256 or RS256 on a sign key and ECDH-ES or RSA-OAEP-256 on a decrypt key; the SDK picks the family the TPM supports.
- Any process on the machine that holds the blob can sign with the key, or open everything ever encrypted to it. A copy on another machine does not load.
- An API key whose disclosure ceiling is below pseudonymous cannot mint: issueKeyChallenge fails with 422 key_disclosure_too_low.
Coverage
- Windows and Linux: a TPM key for either purpose,
hardwareBound: true. A decrypt key opensjwe. - macOS: the enrolled Secure Enclave key signs; a second enclave key, which the enrolled one signs for, decrypts with
format: "apple-ecies". BothhardwareBound: false. Not attested. - iOS: the App Attest key signs;
certifyKeyreturns its JWK and later signatures are assertions. No decrypt key.