Guarantee only a Linux device can decrypt
You'll mint a key once, keep its public half on your server, and from then on encrypt to it whatever only that device may read. The installation needs to be enrolled first.
Ask for a key challenge
On your server, use issueKeyChallenge with purpose: "decrypt" and the device ID you expect, and send the keyChallenge string to the client with its nonce.
app.post("/key/challenge", async (req, res) => {
const deviceId = await deviceOf(accountOf(req));
const { nonce, keyChallenge } = await rh.issueKeyChallenge({
purpose: "decrypt",
expectedDevices: [deviceId],
});
res.json({ nonce, keyChallenge });
});Mint the key
On the client, answer the key challenge with RootHeraldMintKey, passing the attestation key blob and a second buffer for the new key. The chip makes the key, the attestation key certifies it, and the certification lands in one buffer and the key blob in the other. Post the certification and its nonce to your server, and once it has kept the key, save the blob wherever your app keeps its files.
size_t cert_len, blob_len;
if (!load_ak_blob(ak, sizeof ak, &ak_len)) {
return enroll_then_mint();
}
RH_STATUS st = RootHeraldMintKey(rh, key_challenge, ak, ak_len,
cert, sizeof cert, &cert_len,
blob, sizeof blob, &blob_len);
if (st != RH_OK) {
return 0;
}
post_certification("/key/certify", nonce, cert);
save_key_blob(blob, blob_len);Keep the key
On your server, pass the certification and nonce to certifyKey. It returns the key: keep the whole result with the account that deviceId belongs to, since encryptToDevice reads its alg and format as well as the jwk.
app.post("/key/certify", async (req, res) => {
const { nonce, certification } = req.body;
const key = await rh.certifyKey(nonce, certification);
await saveDecryptKey(accountOf(req), key.deviceId, key);
res.sendStatus(204);
});Encrypt to the key
On your server, use encryptToDevice with the key you kept and the bytes only this device may read. It runs locally and returns the JWE string; send that to the client.
import { encryptToDevice } from "@rootherald/node";
app.post("/session", async (req, res) => {
const key = await decryptKeyOf(accountOf(req));
const token = await issueSessionToken(accountOf(req));
res.json({ envelope: encryptToDevice(key, token) });
});Open it on the device
On the client, load the blob once with RootHeraldLoadKey, then use RootHeraldDecrypt on each envelope. Decrypting only touches the chip: no network, no challenge, no prompt. The plaintext is never longer than the envelope, so a buffer of the envelope's size always fits. Close the key with RootHeraldCloseKey when you're done with it.
RH_KEY_HANDLE key;
st = RootHeraldLoadKey(rh, blob, blob_len, &key);
if (st != RH_OK) {
return 0;
}
size_t jwe_len = strlen(jwe);
uint8_t* token = allocate(jwe_len);
size_t token_len;
st = RootHeraldDecrypt(key, jwe, jwe_len,
token, jwe_len, &token_len);RH_ERR_DECRYPT_FAILED means the envelope did not open under this key: ask your server for a fresh one. Retrying the same bytes cannot succeed.
If the TPM has been cleared, every blob is gone, the attestation key's included: RootHeraldLoadKey returns RH_ERR_KEY_UNLOADABLE. Enroll again, then mint a new key. The device ID stays the same; the key ID is new.