A client-only log

This guide shows how a browser keeps a tamper-evident log of its own, such as what an app did on the device or what a user consented to; read it when the log should live on the user’s device.

Example: examples/client-only

With the safe API it takes two calls, and the log is durable, shared safely between tabs, and signed by a key that no script can export:

import { openBrowserLog, openDeviceKey, verifyReceipt } from "webtessera/browser";

const key = await openDeviceKey(origin);     // generated once; non-extractable; kept in IndexedDB
const log = await openBrowserLog({ key });   // kept in IndexedDB, shared by every tab of the page

const receipt = await log.append(entry);     // resolves once a signed checkpoint covers it
verifyReceipt(receipt.text, { vkey: log.vkey, data: entry });   // anyone, offline

Choices to make

  • The origin names the log, its key and its IndexedDB database. Choose one per device and per purpose (the example uses <host>/device/<random>), and remember it: it is public.

  • Several tabs write the same log through Web Locks (log.lockScope === "origin"). Outside a secure context there are no Web Locks, and openBrowserLog refuses to open rather than let two tabs fork the log; choosing storage explains the way out.

  • Persistence. Ask for navigator.storage.persist(): a log the browser evicts can no longer prove what it signed (receipts already handed out still verify).

  • Reading entries back: log.entries(from?, to?) streams them in order, each checked against the leaf hash the log’s tiles hold for it, and log.entry(index) reads one. The example’s history.ts lists the newest twenty, proving each with log.prove(index):

    const { size } = await log.latestCheckpoint();
    for await (const { index, data } of log.entries(size > 20n ? size - 20n : 0n)) { … }

What it guarantees, and what it does not

The log is tamper-evident: once a receipt or checkpoint has left the page, the device cannot rewrite what came before without every holder noticing. It is not tamper-proof: the device holds its own key, and could sign a different history for someone who has seen nothing yet. Witnessing closes that gap: session receipts.

The example’s Chromium tests cover the guardrails: a tampered receipt, a receipt for other data and one checked with another key all fail; two tabs appending at once make one log; the log refuses to open without Web Locks unless told otherwise. See also the safe API.

This page is generated from docs/guides/client-only-log.md . Edit it on GitHub.