Skip to content
Guides · Verify data came from a device

Verify data came from a macOS 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.

Not attested

A Mac's key proves possession of the enclave key it enrolled, and certifyKey returns it with hardwareBound: false.

1

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.

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

Mint the key

On the client, answer the key challenge with RootHeraldMintKey. The enclave key signs the nonce, and the certification lands in one buffer and a selector for the key 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.

client.cc
size_t cert_len, blob_len;

RH_STATUS st = RootHeraldMintKey(rh, key_challenge, NULL, 0,
                                 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);
3

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.

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

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. RootHeraldKeyInfo reports the signature size. Send the signature along with the body, and close the key with RootHeraldCloseKey when you're done with it.

client.cc
RH_KEY_HANDLE key;

st = RootHeraldLoadKey(rh, blob, blob_len, &key);

if (st != RH_OK) {
    return 0;
}

uint8_t sig[256];

size_t sig_len;

st = RootHeraldSign(key, body, body_len,
                    sig, sizeof sig, &sig_len);
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));
});