A notary

This guide shows how to build a notary, a service that proves a document existed and that someone vouched for it at a point in a log’s history; read it when you need receipts that anyone can verify offline.

Example: examples/notary

The document never leaves its owner: the submitter sends its SHA-256 digest and an Ed25519 signature over it, the notary checks the signature and appends a record of the two to its log, and answers with a receipt that anyone can verify offline, with the document and the notary’s vkey.

The entry

Choose an encoding the verifier can rebuild exactly, since the receipt proves the entry’s exact bytes. Fixed-length fields need no parser that could disagree with another implementation’s; the example’s record is 137 bytes:

version (1) ‖ notarizedAt (8, ms, big-endian) ‖ sha256 (32) ‖ public key (32) ‖ signature (64)

The submitter signs a domain-separated statement (webtessera-example-notary/v1\n ‖ digest), so a signature made for the notary cannot be passed off as one over anything else. The notary appends only records whose signature verifies: a record is evidence against its key, and nobody else must be able to make it.

The receipt

log.append(record) from webtessera/server resolves to a verified receipt, a C2SP tlog-proof. The example puts the record in the proof’s extra line too, so one .tlog-proof file carries everything a verifier needs besides the document:

const receipt = await log.append(record, { extraData: record });

The format does not authenticate extra data, so the verifier proves it first. dataInExtra takes the entry from the extra line, checks the receipt against it, and returns it only once the inclusion proof has bound it to the signed checkpoint:

const { data } = verifyReceipt(receiptText, { vkey: notaryVkey, dataInExtra: true }); // exactly these bytes are in the log
const record = decodeRecord(data);                                                     // only now read its fields
// then: record.digest === sha256(document), and the signature verifies with record.publicKey

A receipt without the extra line fails with reason extra. A verifier that keeps the record elsewhere passes data (or the leaf hash, leafHash) as usual, and may add dataInExtra: true to check that the receipt’s extra line holds that same record.

What fails, and how

The example’s CLI (scripts/verify.ts) and tests show each failure with its reason: another document (document), an altered record, proof, index or checkpoint (receipt: inclusion), a receipt from a notary with another key (receipt: signature), a receipt without its record (receipt: extra), and an unexpected signer (signer).

Trust

The notary’s key signs the log, so NOTARY_VKEY is all a verifier needs. The time is the notary’s claim; witnesses and monitors are what make a notary’s history, and its timing, hard to dispute. Because the log is public and append-only (the server serves the tlog-tiles read API next to /notarize), a notary cannot quietly withdraw a notarization: a monitor would see the rewritten history.

This page is generated from docs/guides/notary.md . Edit it on GitHub.