Security
The safe API is built so that the obvious call is the safe one, and a divergence from Tessera that changes a verification outcome is treated as a security bug. To report one, use a private security advisory on GitHub.
Safe defaults
Each entry point of the safe API refuses the mistakes that are easy to make with a transparency log. Its module documentation lists them:
webtessera/server
Runs in your server's private environment: Node.js 22.18+, Deno, Bun, Cloudflare Workers, Vercel Edge and other edge runtimes. A bundle built under the “react-native” and “browser” conditions gets a module that fails the build.
- Run in a browser: a browser bundle resolves this module to one that fails the build, and a browser that loads it anyway gets an error at import.
- Keep a log in memory by accident: storage is a required choice, and memory must be asked for by name.
- Fork a log across processes: SQLite storage uses lease locking for every database another process could reach, unless told the process is the only writer; and if that declaration is wrong, the earlier writer stops as soon as a second one starts.
- Lose track of publication: `append` resolves only once a checkpoint commits to the entry, and hands back a receipt it has verified.
- Change a log's key: a log refuses to open with a key that did not sign its checkpoint.
- Leak the key: keys are non-extractable WebCrypto keys wherever the runtime supports Ed25519, and no error, `toString` or JSON ever contains key material.
webtessera/browser
Runs in the browser's public environment: windows, workers, service workers.
- Hand it a private key string: a browser log's key is generated on the device with `openDeviceKey`, or imported as a `CryptoKey` with `fromCryptoKey`.
- Keep a persistent log with a key that does not persist with it.
- Change a log's key, or open a log another key created.
- Lose track of publication: `append` resolves with a receipt it has verified.
Reviews
The safe API and the storage drivers went through security reviews. These decision records state what the reviews required, and how each finding was fixed:
- ADR-0152: Lock SQLite stores in memory or with fenced leases in the database, per store
- ADR-0154: `webtessera/storage/sqlite` exports `newSqliteDriver`, the store, and one adapter per engine
- ADR-0155: Test the SQLite backend on every engine it claims, under production limits, with a live rqlite
- ADR-0170: Serve the tlog-tiles read API from any LogReader as a fetch-style handler
- ADR-0171: Add a C2SP tlog-witness server, `webtessera/witness`
- ADR-0176: Verify what a mirror copies before writing it, outside the faithful port
- ADR-0210: Default SQLite stores to lease locking unless the database is provably private
- ADR-0211: Fence lease-mode writes on a NOT NULL column, at schema version 2
- ADR-0212: Take only origin-form request-targets, validate every numeric limit, and keep 500 bodies generic
- ADR-0213: rqlite and S3 requests omit credentials and refuse redirects; rqlite reads only at linearizable or strong
Hardening beyond Tessera
Hardening beyond Tessera, from input validation that upstream lacks. None of it changes the bytes of a valid log.
-
No error repeats a signer key: a signer key passed where a verifier key, an origin or a name belongs, in the safe API or in
webtessera/witness, is refused with the codeSIGNER_KEY_MISUSE, and no message, cause or stack contains it (ADR-0243). -
Leases wait out a busy database: taking or renewing a lease waits on
SQLITE_BUSY/SQLITE_LOCKEDinstead of failing. A libSQLfile:database shared by several processes needs a client with a busy timeout, andfromLibsql’s busy error says so (ADR-0210). Checkpoint publication’s refusal writes nothing to the store (ADR-0205). -
SQLite locking fails closed: stores default to lease locking unless the adapter shows the database is private, and an explicit
locking: "local"declares a single writer. Otherwise two connections or processes on one file could assign one index twice, and two witnesses could roll a log back (ADR-0210). The lease fence is a NOT NULL column thatPRAGMA ignore_check_constraintscannot turn off (schema version 2, ADR-0211). A database whose text encoding is not UTF-8 is refused at open (ADR-0151). -
Mirroring: the S3 sink accepts a 412 only over identical bytes, and otherwise fails before the checkpoint is written.
newSinkTarget(s3, { prefix })keeps metadata and conditional writes. A verified mirror verifies afresh on every run, refuses overlapping runs and copies what it verifies (ADR-0175, ADR-0176). -
HTTP and witness inputs:
toNodeListeneraccepts only origin-form request-targets; every size cap and count must be a positive integer;addErrorResponseanswers 500 without the error’s text unless asked for{ detail: true }(ADR-0212).newWitnessServerrequires cosignature/v1 signers, and caches its key checks forlookupLog(ADR-0171). -
rqlite and S3 requests omit credentials; rqlite refuses redirects unless
followRedirects: true, and reads only atlinearizableorstrong(ADR-0213). -
Entries:
newEntrythrows for data longer than 65535 bytes, which an entry bundle cannot encode, where Go silently truncates the length prefix (ADR-0182). Big-endian length prefixes reject values their width cannot hold (ADR-0200). -
Checkpoint publication fails closed: a checkpoint is published only if it parses and commits to exactly the size and root requested, and a new tree is never started over a published checkpoint when the tree state is missing (ADR-0205). When a witness fails and
failOpenis set, the checkpoint published after an error other than an unmet policy is the log-signed one, not an empty one (ADR-0183). -
Witness policies and URLs: a policy that names the same child twice, gives one Ed25519 key two witness names, or sets a threshold of 0 is rejected (ADR-0184); witness URLs must use
https, orhttpto a loopback address (ADR-0185); witness requests do not follow redirects, and an update retries a stale tree size at most three times (ADR-0197, ADR-0198). -
Verification: every hash in a Merkle proof, and every root passed to the verifiers, must be a node hash of the hasher’s size (ADR-0202); a parsed checkpoint’s root must be 32 bytes, and an origin must be valid UTF-8 (ADR-0202, ADR-0203); Ed25519 verification follows Go’s rules exactly, and a verifier configured with a small-order or non-canonical key is refused (ADR-0206);
LogStateTracker.updatethrowsErrInconsistencyfor a checkpoint of the tracked size with a different root, or a smaller one that is not consistent with it (ADR-0196). -
Tiles, bundles and responses: a tile or an entry bundle holding more than the tlog-tiles maximum of 256 hashes or entries is rejected, response bodies are read with a size cap, and the log fetcher and the witness client omit credentials on every request (ADR-0194, ADR-0195, ADR-0197).
-
Locks: opening an IndexedDB log without Web Locks throws, unless the caller passes
singleWriter: trueto promise that only one tab or worker writes the log;newIndexedDBDriverreturns anIndexedDBDriverwhoselockScopesays which guarantee the log has (ADR-0201). -
Numbers: the exported
uint64entry points of the Merkle, compact-range and checkpoint code throwRangeErrorfor a value outside theuint64range instead of computing with it (ADR-0207).parseUint, the hex decoder andquoteare transcriptions of Go’s, so malformed input is accepted or rejected as Go does, in time linear in its length (ADR-0204). -
Worker counts:
entryBundles,entriesandfsckreject a worker count below one with aRangeErrorinstead of hanging (ADR-0192).
Report a vulnerability
Please do not open a public issue, pull request or discussion for a security problem.
Report it privately through GitHub Security Advisories:
- Go to https://github.com/diagnos-tech/webtessera/security/advisories/new (the repository’s Security tab, then Report a vulnerability).
- Describe the problem and how to reproduce it. A failing test or a minimal script is the most
useful thing you can attach. For a divergence from Tessera, include the Go behaviour and the
pinned upstream commit you compared against (
scripts/upstream.json). - Say whether you intend to disclose publicly, and by when.
We aim to acknowledge a report within five working days, to tell you whether we consider it a
vulnerability within ten, and to keep you informed until a fix is released. Fixes are developed in a
private advisory fork where possible, released with an advisory and a CHANGELOG.md entry, and
credited to the reporter unless you ask us not to.
The full policy, with what is in scope and which versions are supported, is
SECURITY.md.