Verify as an auditor
You are the auditor. The operator gives you the stream’s fingerprint through a channel they cannot rewrite later (a signed email, a ticket, a contract annex). You verify the stream against it, keep a checkpoint, then play the attacker: edit one byte of a batch file on disk and watch verification fail with the first broken sequence number. Verifying needs only read access to the stream and the fingerprint, never the operator’s keys.
Prerequisites
Section titled “Prerequisites”-
A copy of the stream, or read access to where it lives, and its fingerprint. Step 1 makes one.
-
The
tenbriCLI to set up the stream and seal a checkpoint, plusjq:Terminal window cargo install tenbri-cli --version 0.1.0-beta.1 --locked -
The tool you verify with:
The tenbri CLI above is all you need.
Python 3.10 or later.
pip install --pre tenbriuv add --prerelease allow tenbripoetry add --allow-prereleases tenbriNode 22.18 or later, which runs .ts files directly, in a project with "type": "module"
(npm pkg set type=module).
npm install tenbri@nextpnpm add tenbri@nextyarn add tenbri@nextbun add tenbri@nextcurl and a tenbrid that serves the stream, with a token for you:
cargo install tenbri-server --version 0.1.0-beta.1 --locked-
Set up a stream to audit.
The operator side: a stream with one intent and its outcome.
Terminal window export TENBRI_KEYS=./keystenbri init audit/supportINTENT_ID=$(tenbri append audit/support <<'EOF' | jq -r .id{"kind":"decision","phase":"intent","source":"sdk","principal":{"kind":"agent","id":"support-assistant"},"action":{"type":"reply_ticket","target":"zendesk:ticket/4821"}}EOF)tenbri append audit/support <<EOF > /dev/null{"kind":"decision","phase":"outcome","source":"sdk","intent":"$INTENT_ID","status":"ok","principal":{"kind":"agent","id":"support-assistant"},"action":{"type":"reply_ticket","target":"zendesk:ticket/4821"}}EOFcreated stream tnb1:db888dbab715e39d31a8b0c7b6bd418a at audit/supportstream key fingerprint (pin it): sha256:db888dbab715e39d31a8b0c7b6bd418acfe8482508fd345ffcb6d5da23adb6fekeys: ./keys/tnb1-db888dbab715e39d31a8b0c7b6bd418averify with: tenbri verify audit/support --key sha256:db888dbab715e39d31a8b0c7b6bd418acfe8482508fd345ffcb6d5da23adb6feThe fingerprint is what the operator sends you. Keep it in
PIN:Terminal window export PIN=sha256:db888dbab715e39d31a8b0c7b6bd418acfe8482508fd345ffcb6d5da23adb6fe -
Verify without a pin, then with it.
The fingerprint is your trust root. Without it, verification can only say the stream agrees with its own genesis key, which anyone who rewrites the whole stream can also produce. The Python and TypeScript scripts below exit with the CLI’s codes: 0 verified, 1 broken, 3 not pinned.
Terminal window tenbri verify audit/support; echo "exit $?"tenbri verify audit/support --key $PIN; echo "exit $?"consistent: 4 entries, stream tnb1:db888dbab715e39d31a8b0c7b6bd418aNOT pinned: this only shows the stream agrees with its own genesis key; pass --key <fingerprint>exit 3verified 4 entries, stream tnb1:db888dbab715e39d31a8b0c7b6bd418a, pinned to sha256:db888dbab715e39d31a8b0c7b6bd418acfe8482508fd345ffcb6d5da23adb6feexit 0report.okis true only when nothing is broken and the stream was checked against a pinned key.verify.py import osimport sysfrom tenbri import Tenbriwith Tenbri.open("./audit/support") as log:report = log.verify(os.environ.get("PIN"))print(report)if report.broken:sys.exit(1) # the stream does not verifyif not report.ok:sys.exit(3) # consistent, but not checked against a pinned keyTerminal window env -u PIN python verify.py; echo "exit $?"python verify.py; echo "exit $?"VerifyReport(stream='tnb1:db888dbab715e39d31a8b0c7b6bd418a', entries=4, pinned_key=None, broken=None, checkpoint=None)exit 3VerifyReport(stream='tnb1:db888dbab715e39d31a8b0c7b6bd418a', entries=4, pinned_key='sha256:db888dbab715e39d31a8b0c7b6bd418acfe8482508fd345ffcb6d5da23adb6fe', broken=None, checkpoint=None)exit 0report.okis true only when nothing is broken and the stream was checked against a pinned key.verify.ts import { Tenbri } from 'tenbri'const log = await Tenbri.open('./audit/support')const report = await log.verify(process.env.PIN)await log.close()console.log(report)if (report.broken) process.exit(1) // the stream does not verifyif (!report.ok) process.exit(3) // consistent, but not checked against a pinned keyTerminal window env -u PIN node verify.ts; echo "exit $?"node verify.ts; echo "exit $?"{ok: false,stream: 'tnb1:db888dbab715e39d31a8b0c7b6bd418a',entries: 4,pinnedKey: null,broken: null,checkpoint: null}exit 3{ok: true,stream: 'tnb1:db888dbab715e39d31a8b0c7b6bd418a',entries: 4,pinnedKey: 'sha256:db888dbab715e39d31a8b0c7b6bd418acfe8482508fd345ffcb6d5da23adb6fe',broken: null,checkpoint: null}exit 0tenbridalways verifies against thekeypinned in its configuration, so ask for that pin intenbrid.tomland check that the server’s answer names your fingerprint. The operator makes you a token withtenbrid tokenand adds its hash:tenbrid.toml keys = "keys"[streams.support]path = "audit/support"key = "sha256:db888dbab715e39d31a8b0c7b6bd418acfe8482508fd345ffcb6d5da23adb6fe"[[tokens]]sha256 = "sha256:3d19cdaba826019fd319346560190e1269ccc88f762fa4dc2cdcfbd7b4b8767b"principal = { kind = "person", id = "auditor" }Terminal window tenbrid --config tenbrid.tomlIn another terminal:
Terminal window export TENBRI_URL=http://127.0.0.1:7070export TOKEN=tnbt_d0ae9132… # the token tenbrid printedcurl -s $TENBRI_URL/v1/streams/support/verify -H "Authorization: Bearer $TOKEN"curl -s $TENBRI_URL/v1/streams/support/verify -H "Authorization: Bearer $TOKEN" \| jq -e --arg pin "$PIN" '.pinned_key == $pin and .broken == null'; echo "exit $?"{"stream":"tnb1:db888dbab715e39d31a8b0c7b6bd418a","entries":4,"pinned_key":"sha256:db888dbab715e39d31a8b0c7b6bd418acfe8482508fd345ffcb6d5da23adb6fe"}trueexit 0 -
Keep a checkpoint.
A checkpoint is a signed note of the stream’s size and Merkle root. The operator seals one; you keep a copy outside the operator’s reach.
Terminal window tenbri checkpoint audit/supportcp audit/support/checkpoints/000000000004.note seen.notehead -3 seen.notecheckpoint at size 4: checkpoints/000000000004.notetnb1:db888dbab715e39d31a8b0c7b6bd418a40SREoiNR1ENpnImgPcXhjwWKjv1yTiX3aR+6YMRp57s=The three lines are the origin (the stream id), the size and the root hash. A blank line and the signature line follow.
Next time, verify against the checkpoint you kept. The stream must still hold exactly the entries it covers:
Terminal window tenbri verify audit/support --key $PIN --checkpoint seen.note; echo "exit $?"matches checkpoint tnb1:db888dbab715e39d31a8b0c7b6bd418a at size 4matches its latest checkpoint, at size 4verified 4 entries, stream tnb1:db888dbab715e39d31a8b0c7b6bd418a, pinned to sha256:db888dbab715e39d31a8b0c7b6bd418acfe8482508fd345ffcb6d5da23adb6feexit 0verifychecks the stream against its latest checkpoint and reports its size. To compare with the copy you kept, usetenbri verify --checkpoint(the CLI tab).Terminal window python verify.pyVerifyReport(stream='tnb1:db888dbab715e39d31a8b0c7b6bd418a', entries=4, pinned_key='sha256:db888dbab715e39d31a8b0c7b6bd418acfe8482508fd345ffcb6d5da23adb6fe', broken=None, checkpoint=4)verifychecks the stream against its latest checkpoint and reports its size. To compare with the copy you kept, usetenbri verify --checkpoint(the CLI tab).Terminal window node verify.ts | grep checkpointcheckpoint: 4 -
Tamper with one byte.
Each batch file holds consecutive entries, one canonical JSON line each. Here every append made its own batch, so entry 3 (the intent) is alone in
000000000003.jsonl.Terminal window ls audit/support/batches000000000001.jsonl000000000002.jsonl000000000003.jsonl000000000004.jsonlChange the ticket number in the intent from
4821to4822, one byte:Terminal window perl -pi -e 's#ticket/4821#ticket/4822#' audit/support/batches/000000000003.jsonl -
Verify again.
Terminal window tenbri verify audit/support --key $PIN --checkpoint seen.note; echo "exit $?"broken at seq 3: bad_signature: the signature does not verify2 entries verified before it, stream tnb1:db888dbab715e39d31a8b0c7b6bd418aexit 1Reads that check the stream refuse it too.
--verifymakesqueryread the stream itself rather than trust a query index built earlier:Terminal window tenbri query audit/support --action reply_ticket --key $PIN --verify; echo "exit $?"tenbri: the stream does not verify: broken at seq 3: bad_signature: the signature does not verifyexit 1Terminal window python verify.py; echo "exit $?"VerifyReport(stream='tnb1:db888dbab715e39d31a8b0c7b6bd418a', entries=2, pinned_key='sha256:db888dbab715e39d31a8b0c7b6bd418acfe8482508fd345ffcb6d5da23adb6fe', broken=Broken(seq=3, code='bad_signature', message='the signature does not verify'), checkpoint=None)exit 1A stream opened with
key=PINrefuses reads the same way:log.query(...)raisestenbri.errors.StreamBrokenError: the stream does not verify: broken at seq 3: bad_signature: the signature does not verify.Terminal window node verify.ts; echo "exit $?"{ok: false,stream: 'tnb1:db888dbab715e39d31a8b0c7b6bd418a',entries: 2,pinnedKey: 'sha256:db888dbab715e39d31a8b0c7b6bd418acfe8482508fd345ffcb6d5da23adb6fe',broken: {code: 'bad_signature',message: 'the signature does not verify',seq: 3},checkpoint: null}exit 1A stream opened with
{ key: PIN }refuses reads the same way:log.query(...)rejects withStreamBrokenError: the stream does not verify: broken at seq 3: bad_signature: the signature does not verify./verifyreads the whole stream from storage again:Terminal window curl -s $TENBRI_URL/v1/streams/support/verify -H "Authorization: Bearer $TOKEN"curl -s $TENBRI_URL/v1/streams/support/verify -H "Authorization: Bearer $TOKEN" \| jq -e --arg pin "$PIN" '.pinned_key == $pin and .broken == null'; echo "exit $?"{"stream":"tnb1:db888dbab715e39d31a8b0c7b6bd418a","entries":2,"pinned_key":"sha256:db888dbab715e39d31a8b0c7b6bd418acfe8482508fd345ffcb6d5da23adb6fe","broken":{"seq":3,"code":"bad_signature","message":"the signature does not verify"}}falseexit 1
What happened
Section titled “What happened”The edit changed the bytes the session key signed, so the signature on entry 3 no longer verifies
and every verifier reports the first broken entry. A stream signed with another genesis key fails
your pin with untrusted_root. An operator who holds the keys could still truncate or re-sign a
rewritten stream; the checkpoint you kept catches that for the entries it covers (truncated,
fork), which a pin alone cannot.
