Verify data came from a Browser device
You'll mint a key once, keep its public half on your server, and from then on check every request the client signs with it. The installation needs to be enrolled first.
Ask for a key challenge
On your server, use issueKeyChallenge with purpose: "sign" 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: "sign",
expectedDevices: [deviceId],
});
res.json({ nonce, keyChallenge });
});In a browser, keys go through a browser extension you build: the page signs every request, and a link handler would open your app each time.
Mint the key
In the page, send the key challenge to your extension as a mint message. Your app, the extension's native messaging host, answers it with RootHeraldMintKey, the attestation key blob and a buffer for the new key, keeps the key blob itself, and returns the certification. Post the certification and its nonce to your server.
const { nonce, keyChallenge } = await postJSON("/key/challenge", {});
const { certification } = await chrome.runtime.sendMessage(EXTENSION_ID, {
type: "mint",
keyChallenge,
});
await postJSON("/key/certify", { nonce, certification });Keep the public key
On your server, pass the certification and nonce to certifyKey. It returns the key: keep jwk, its public half, with the account that deviceId belongs to.
app.post("/key/certify", async (req, res) => {
const { nonce, certification } = req.body;
const key = await rh.certifyKey(nonce, certification);
await saveKey(accountOf(req), key.deviceId, key.jwk);
res.sendStatus(204);
});Sign what you send
In the page, send each request body to your extension as a sign message, and send the signature it returns along with the body. Your app loads the blob with RootHeraldLoadKey and signs with RootHeraldSign: no network, no challenge, no prompt.
const { signature } = await chrome.runtime.sendMessage(EXTENSION_ID, {
type: "sign",
body,
});
await fetch("/orders", {
method: "POST",
body,
headers: { "x-signature": signature },
});If the TPM has been cleared, every blob is gone, the attestation key's included: your app gets RH_ERR_KEY_UNLOADABLE. Have it enroll again, then mint a new key. The device ID stays the same; the key ID is new.
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));
});Root Herald bridges are in development and coming soon: an extension and host your pages will be able to use directly, instead of building your own.