Skip to content
Guides · App keys

App keys on Browser

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.

1

Ask for a key

On your server, issue a challenge that asks for a key, and send it to the client with its nonce.

server (Node)ts
app.post("/key/challenge", async (_req, res) => {
  const { nonce, challenge } = await rh.issueChallenge({
    ask: ["key"],
    keyPurpose: "sign",
  });

  res.json({ nonce, challenge });
});

In a browser, app keys go through a browser extension you build: the page signs every request, and a link handler would open your app each time.

2

Make the key

In the page, send the challenge to your extension as a mint message. Your app, the extension's native messaging host, answers it with RootHeraldRespond and a buffer for the key, keeps the key blob itself, and returns the evidence. Post the evidence and its nonce to your server.

page.tsts
const { nonce, challenge } = await postJSON("/key/challenge", {});

const { evidence } = await chrome.runtime.sendMessage(EXTENSION_ID, {
  type: "mint",
  challenge,
});

await postJSON("/key/bind", { nonce, evidence });
3

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.

server (Node)ts
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);
});
4

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.

page.tsts
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, your app gets RH_ERR_KEY_UNLOADABLE. Have it throw the blob away and mint a new key from step 1. The device is still enrolled, so don't enroll it again.

5

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.

server (Node)ts
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. Send your extension another mint message; your app passes the key it already holds to RootHeraldRespond instead of NULL.

yourapp_host.cc
st = RootHeraldRespond(rh, challenge, key,
                       evidence, sizeof evidence, &ev_len,
                       NULL, 0, NULL);
Coming soon

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.