App keys on Linux
You'll ask for a key once, keep its public half on your server, and from then on check every request the client signs with it. The device needs to be enrolled first.
Ask for a key
On your server, issue a challenge that asks for a key, and send it to the client with its nonce.
app.post("/key/challenge", async (_req, res) => {
const { nonce, challenge } = await rh.issueChallenge({
ask: ["key"],
keyPurpose: "sign",
});
res.json({ nonce, challenge });
});Make the key
On the client, answer the challenge with RootHeraldRespond and give it a second buffer for the key. The chip makes a new key, certifies it, and writes the evidence into one buffer and the key blob into the other; 512 bytes is always enough for the blob. Post the evidence and its nonce to your server, and once it has kept the key, save the blob wherever your app keeps its files.
size_t ev_len, blob_len;
RH_STATUS st = RootHeraldRespond(rh, challenge, NULL,
evidence, sizeof evidence, &ev_len,
blob, sizeof blob, &blob_len);
if (st != RH_OK) {
return 0;
}
post_evidence("/key/bind", nonce, evidence);
save_key_blob(blob, blob_len);Keep the public key
On your server, pass the evidence and nonce to verify. On a pass, the verdict carries the key: keep key.jwk, its public half, with whatever the device belongs to. No pass, no key.
app.post("/key/bind", async (req, res) => {
const { nonce, evidence } = req.body;
const result = await rh.verify(evidence, { nonce });
if (result.device.verdict !== "pass" || !result.key) {
return res.sendStatus(403);
}
await saveKey(accountOf(req), result.key.jwk);
res.sendStatus(204);
});Sign what you send
On the client, load the blob once with RootHeraldLoadKey, then use RootHeraldSign on each request body. Signing only touches the chip: no network, no challenge, no prompt. Send the signature along with the body, and 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;
}
uint8_t sig[64];
size_t sig_len;
st = RootHeraldSign(key, body, body_len,
sig, sizeof sig, &sig_len);If the TPM has been cleared, RootHeraldLoadKey returns RH_ERR_KEY_UNLOADABLE. Throw the blob away and start again from step 1 for a new key. The device is still enrolled, so don't enroll it again.
Check each signature
On your server, use verifyKeySignature to check the signature against the JWK you kept, over the exact bytes the client signed. It runs locally and returns false rather than throwing, so treat anything but true as a refusal.
import { verifyKeySignature } from "@rootherald/node";
app.post("/orders", async (req, res) => {
const jwk = await keyOf(accountOf(req));
const signature = req.header("x-signature") ?? "";
if (!verifyKeySignature(jwk, rawBody(req), signature)) {
return res.sendStatus(401);
}
res.json(await placeOrder(req.body));
});Re-certify the key
To certify the key you already have under today's policy, answer a fresh key challenge and pass the key along. Give RootHeraldRespond the loaded key handle instead of NULL.
st = RootHeraldRespond(rh, challenge, key,
evidence, sizeof evidence, &ev_len,
NULL, 0, NULL);