Skip to content
Tenbri pangolinTenbri

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.

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:

Terminal window
tenbri init audit/support
created stream tnb1:5db31441baa2d080326d9f6c83c95319 at audit/support
stream key fingerprint (pin it): sha256:5db31441baa2d080326d9f6c83c95319c6e64dd5c33a49257cc54d231cc36484

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:

Terminal window
tenbri verify audit/support --key sha256:5db31441baa2d080326d9f6c83c95319c6e64dd5c33a49257cc54d231cc36484

If 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.

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).

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.

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.

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.

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.secret

Files 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:e2d9b038a3a00790e40737bf798d1f0c2d468932e2610d6d64cfd05b4eeb9e63

The same directory holds data/, the data keys for encrypted bodies and input digests.

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 other

For a stream in an object store, the keys stay on the machine that writes.

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”.

  • Integrity: what the signatures cover.
  • Privacy: data keys, which are separate from signing keys.