Keys
Every entry is signed with Ed25519. A stream has two kinds of keys.
| Key | Lifetime | Signs | Id |
|---|---|---|---|
| Stream key | Long-lived | Key events only (key entries) |
stream:<12 hex> |
| Session key | Short-lived, 24 hours by default | Every other entry, HEAD and checkpoints | sess:<date>:<12 hex> |
The stream key identifies the stream: the stream id is derived from its fingerprint. Because it signs only key events, it can stay offline most of the time.
Fingerprints and public keys
Section titled “Fingerprints and public keys”| Value | Encoding |
|---|---|
| Public key | ed25519: and the 32-byte key as 64 lowercase hex digits |
| Fingerprint | sha256: and the hex SHA-256 of the 32-byte public key |
| Stream id | tnb1: and the first 32 hex digits of the genesis stream key’s fingerprint |
Creating a stream makes its stream key and gives you the fingerprint to pin:
tenbri init audit/supportcreated stream tnb1:5db31441baa2d080326d9f6c83c95319 at audit/supportstream key fingerprint (pin it): sha256:5db31441baa2d080326d9f6c83c95319c6e64dd5c33a49257cc54d231cc36484from tenbri import Tenbri
# create=True makes the stream and its keys when there is none.log = Tenbri.open("audit/support", create=True)print("pin this:", log.fingerprint)import { Tenbri } from 'tenbri'
// create: true makes the stream and its keys when there is none.const log = await Tenbri.open('audit/support', { create: true })console.log('pin this:', log.fingerprint)Pinning
Section titled “Pinning”A log cannot vouch for itself: anyone can create a stream that agrees with its own key. The verifier must hold the trust root before reading the stream. In format v1 that is the fingerprint of the genesis stream key.
Pass it to --key, as a fingerprint or as the public key:
tenbri verify audit/support --key sha256:5db31441baa2d080326d9f6c83c95319c6e64dd5c33a49257cc54d231cc36484If the genesis key differs, verification fails at seq 1 with untrusted_root. Without --key,
verify exits 3 and says the stream is not pinned. query, tail, receipt create and
receipt verify, tenbrid (the key setting of a stream) and the Python and TypeScript bindings
(the key option of open, or the argument of verify) take the same pin.
Store the fingerprint where the stream’s operator cannot change it: in the auditor’s own records, in your verifier’s configuration, in a contract.
Certification
Section titled “Certification”Key events are key entries whose key member has an op:
op |
Members | Effect |
|---|---|---|
certify |
id, pub, role (stream or session), not_after (sessions only) |
Adds a key |
rotate |
id, pub |
The new key becomes the current stream key |
revoke |
id, from (a sequence number, 1 or more) |
Entries signed by key id at from or later fail |
All three are signed by the current stream key.
Entry 1 of every stream (the genesis entry) is a certify with role: stream, signed by that
same key. Its stream must be the id derived from its pub; otherwise bad_genesis. Entry 2,
written by tenbri init, certifies the first session key:
{ "id": "01M5CT5F7SAM4CQ1A4M1N01P9J", "key": { "id": "sess:2026-10-20:db131c41f8ad", "not_after": "2026-10-21T14:06:39.830Z", "op": "certify", "pub": "ed25519:af896119b95db85a6e2fa213c8969cf63b746c3d49b3c13766bfabd286d2f509", "role": "session" }, "kind": "key", "prev": "sha256:785feea9b766ff7493156628f6524d220b8abeea9a8ab86fc97ba301ef2a79b2", "seq": 2, "sig": "ed25519:c507ddc7…", "signer": "stream:5db31441baa2", "stream": "tnb1:5db31441baa2d080326d9f6c83c95319", "ts": "2026-10-20T14:06:39.865Z", "v": 1}Each certify or rotate must introduce a key id and a public key not used before in the stream
(bad_key_event).
Session lifetime and renewal
Section titled “Session lifetime and renewal”A session key’s not_after is a timestamp. An entry signed by it with a later ts fails with
key_expired. Writers certify sessions for 24 hours by default.
The writer renews its session key on its own. When the current one has less than one minute left, it uses another stored session key that is still valid, or makes a new one, certifies it with the stream key (if the keys directory holds it) and saves it. A machine that holds only session keys keeps writing until they expire.
Rotation
Section titled “Rotation”A rotate entry, signed by the current stream key, makes a new key the stream key. The old one
signs nothing more. Session keys certified by the old stream key stay valid until they expire or
are revoked.
A thief holding the stream key can sign a competing rotation. Only checkpoints held outside the operator reveal that fork.
Revocation reaches back
Section titled “Revocation reaches back”A revoke names a key certified earlier, other than the current stream key, and a sequence
number from. Entries signed by that key at from or later fail with key_revoked, including
entries written before the revocation. A verifier reports the earliest such entry as the first
broken one.
This is how an operator marks the window in which a stolen session key was used: revoke it from
the first seq the thief could have signed. Revocation applies to checkpoints the same way.
Where keys are kept
Section titled “Where keys are kept”Keys live in a keys directory: --keys <DIR>, else $TENBRI_KEYS, else
~/.config/tenbri/keys (with $XDG_CONFIG_HOME honoured, %APPDATA%\tenbri\keys on Windows).
Each stream has a subdirectory named after its id, with : written as -:
keys/tnb1-5db31441baa2d080326d9f6c83c95319/ 5db31441baa2.json stream key: id, role, pub, backend 5db31441baa2.secret its secret, hex (0600) db131c41f8ad.json session key: id, role, pub, not_after, backend db131c41f8ad.secretFiles are named by the first 12 hex digits of the key’s fingerprint. The .json file never holds
the secret:
{ "id": "sess:2026-10-20:db131c41f8ad", "role": "session", "pub": "ed25519:af896119b95db85a6e2fa213c8969cf63b746c3d49b3c13766bfabd286d2f509", "not_after": "2026-10-21T14:06:39.830Z", "backend": "file"}On Unix the directories are created mode 0700 and secret files 0600. A key file is never
replaced.
tenbri keys list <stream> prints the signing keys held for a stream (and its data keys), never a
secret:
signing keys of tnb1:dc736478ac6b04bad25e3a3a6f8d71aa (<keys dir>/tnb1-dc736478ac6b04bad25e3a3a6f8d71aa): sess:2026-10-20:adb9599f29d0 session ed25519:641a6cdf885136243e096b3227edc4e0306e0467759a36a1a2ef0b6b58755da5 until 2026-10-21T14:46:33.240Z stream:dc736478ac6b stream ed25519:e2d9b038a3a00790e40737bf798d1f0c2d468932e2610d6d64cfd05b4eeb9e63The same directory holds data/, the data keys for
encrypted bodies and input digests.
Keep keys apart from the stream
Section titled “Keep keys apart from the stream”Keys never live under the stream’s directory or bucket prefix. Whoever can read or copy the log
must not be able to sign as it. For a local stream, tenbri refuses a keys directory inside the
stream directory, or the other way round:
keys directory … and stream … must not contain each otherFor a stream in an object store, the keys stay on the machine that writes.
OS keychain
Section titled “OS keychain”With the keychain feature (on by default in the CLI), tenbri init --keychain keeps the secret
halves in the OS keychain instead of .secret files: macOS Keychain or Windows Credential
Manager, under the service name tenbri. The .json descriptions stay in the keys directory.
New session keys use the same backend as the stream key.
On Linux and other systems the keychain backend fails with “the OS keychain backend supports macOS and Windows only for now; use key files”.
