[payload][slot records][ext blocks…][v2 signing ext?][trailer header (166 B, at EOF)]
v2 signing extension — 268 B fixed + variable signature (all-new numerics big-endian):
offset size field
0 256 slot sha256 digests: 8 × 32 B — digest of slot i's bytes
at i*32; entries beyond slot_count are zeroed
256 8 signer keyid (low 64 bits of the OpenPGP fingerprint, BE)
264 sig_len OpenPGP detached signature (binary packets)
264+sig_len 4 u32be signature length (1..=65536)
canonical signed bytes = slot table ‖ digest array ‖ keyid ‖ trailer header
(everything except the signature and its length field)
ARCHITECTURE · 19
The chain of trust.
Package parts travel over mirrors and are shared between machines, so the platform must establish that they were not altered in transit. The answer is a chain: one root key, one signature mechanism, and verification at every hand-off, together with an honest account of what a checksum can and cannot prove.
|
Note
|
The distinction is stated plainly: crc32 in the tpkg trailer is not authenticity. It is an accident-integrity check that catches a truncated download or a flipped byte, and nothing more. Likewise, HTTPS plus a sha256 from the same channel as the download protects the transport, not the object: whoever swaps the artifact swaps the manifest too. Authenticity starts at the OpenPGP signature, anything short of it is a corruption check, and these docs call it that. |
The flow, from release page to run
Verification happens at fetch and install, never per run. What lands in the store carries its proof with it, and what runs has already been believed, exactly once.
Figure 1 — The trust flow: a published artifact plus its sha256 sidecar, verified at install, then runs with no re-verification; unsigned is first-class and loud.
The chain, link by link
1. Root of trust
|
Note
|
This link is shipped. |
The whole project uses one signature mechanism: OpenPGP via rnp-rs over
vendored botan, which is post-quantum cryptography (PQC) capable. The root
fingerprint of the tamatebako release keypair is published on tebako.org and
embedded in the loader at release time (EMBEDDED_ROOT_FINGERPRINT, which
the root ceremony filled when it minted the classical root;
TEBAKO_TRUSTED_ROOT overrides it for development). Rotation is a signed
successor-key statement
(TEBAKO-ROOT-SUCCESSOR-V1; the machinery is shipped in tebako-signer), so
machines forward trust with no out-of-band step. Revocation
(TEBAKO-ROOT-REVOCATION-V1) kills a key that has no successor: the format
is locked, and loader consumption is planned. The one-time ceremony that
mints the roots has its own runbook, below.
2. Production (releases)
|
Note
|
This link is shipped. |
Every released part, the runtime packages, the bootstrap, the src tarballs, and the libraries, ships a detached .asc per artifact plus a signed release manifest, which gives one verify path for all. The release index (manifest.json) is signed too; resolvers verify the index signature before trusting its hashes, which closes the swap-the-manifest gap. The release side shipped with v2.6.0: the release workflows produce the signatures.
3. tpkg trailer v2
|
Note
|
This link is shipped. |
The trailer gains a per-slot sha256 array, the signer keyid, and an OpenPGP signature block over the canonical trailer bytes. crc32 stays as the cheap corruption check and is documented as non-authentic. Trailers of v1 still parse, and the version field stays 1.
4. Sign/verify tooling
|
Note
|
This link is shipped. |
tebako-pkg sign emits a detached .asc per artifact plus a signed
SHA256SUMS, and tebako-pkg verify classifies each artifact as Trusted,
Untrusted, or Invalid. Factory release flows sign in CI from secret-held key
material, and the private key never leaves CI secrets or hardware.
5. Bundle-time signing: opt-in
|
Note
|
This link is shipped. |
The tebako-pkg bundle --sign[=keyid] invocation signs; without it the
package carries no v2 extension at all, with no key, no prompt, and no
ceremony. A press-local Ed25519 key is generated under $TEBAKO_HOME/keys/
only on the first explicit --sign, and it is registered locally
automatically, so development iteration never produces or accepts unsigned
artifacts.
6. First-run verification
|
Note
|
This link is planned. |
The target model is the following: the loader verifies the trailer signature
against the trust store and then each slot’s sha256 before mounting, in one
streaming pass at install time; the .sha256 sidecar is the trust anchor, so
nothing re-hashes per run. Today’s phase-1 posture is unverified-first: the
shipped bootstrap is built without OpenPGP verification, so a signed package
runs with a loud UNVERIFIED warning plus an audit journal entry while the
per-slot sha256 is still enforced as an integrity check against corruption,
and TEBAKO_REQUIRE_SIGNED=1 fails closed with exit 71, naming the missing
capability. OpenPGP verification returns via the tebako-crypto toolkit
payload, which the bootstrap itself verifies with Ed25519.
7. Fail-closed behavior everywhere
|
Note
|
This link is shipped. |
No insecure-skip flag exists. Verification of signed packages is always strict; only the presence of a signature is optional.
8. The v1-legacy rule
|
Note
|
This link is shipped. |
Unsigned v1 packages exist in the wild and must not become unrunnable: a
v2-verifying bootstrap accepts them as LEGACY with a loud stderr warning
(recorded in its audit journal), while v2-signed packages receive full
verification. Rejecting legacy packages is a separate explicit opt-in,
TEBAKO_REQUIRE_SIGNED=1, for hardened environments, and it is never the
default.
"Shipped" means implemented and tested in tamatebako/tebako — whose five binaries (tebako, tfs, tebako-pkg, tebako-shim, tebako-bootstrap) are downloadable from the project’s release pages today. "Planned" covers the loader’s OpenPGP verification; see "What is still planned" below.
The root-key ceremony
|
Note
|
The ceremony ran, and its runbook is the record. |
The top link of the chain is made once, offline, by the project owner, with a witness, and nothing about it is improvised. The runbook is docs/root-ceremony.md in the product repository; it produces, in order:
-
the classical root, an Ed25519 OpenPGP keypair that anchors the chain from day one;
-
the PQC root, an ML-DSA-65 keypair generated the same day, so that the successor exists before it is needed;
-
a pre-made, unpublished successor statement from the classical root to the PQC root, by which a quantum break becomes a publish operation rather than a crisis;
-
pre-made revocation certificates for both roots;
-
an ML-KEM-768 encryption subkey on the classical root, so that encryption exists from day one and a year-one encrypted image never needs a year-three key.
The private keys move to hardware tokens from two vendors and to two offline
backups, and the factories sign with a subkey held in CI secrets, never with
the root primary. The only public outputs are the fingerprints and the
armored public keys, which are wired into EMBEDDED_ROOT_FINGERPRINT (filled
when the ceremony minted the classical root on 2026-09-09),
published on this site, and mirrored to the org’s .github repository and
the keyservers. Rotation and revocation are rehearsed on a scratch keyring
before the first signed release: the runbook publishes the pre-made
successor and observes trust forward, then publishes a revocation and
observes the revoked fingerprint fail closed (RevokedKey, exit 72) even on
previously-valid artifacts.
tpkg v2: the wire layout
|
Note
|
The wire layout is shipped in crates/tpkg. |
Version 2 puts the integrity material between the slot table and the
header, keeping the header exactly where readers of v1 expect it, at EOF
with the version field still 1. Signing is flagged by TPKG_FLAG_SIGNED_V2
(bit 1 of package_flags), which old readers pass through untouched: runtimes
of the v1 era keep reading v2 packages exactly as before, and they simply do
not verify. The same gap also carries typed extension blocks (a type-2 block
holds the package manifest), and the signing extension, when present, stays
last before the header, unchanged. The authoritative reference is
crates/tpkg/src/lib.rs.
Detection works as follows: the reader parses the v1 header at EOF as
always, and if the signed flag is set, the extension, exactly
256 + 8 + sig_len + 4 bytes, sits immediately before the header,
self-delimiting from the tail through its sig_len field, with the digest
tail zeroed. Typed ext blocks before it walk forward from the end of the
slot table (type + length self-delimit); readers aware of the v1 era skip
unknown block types (forward-compat), while tebako-pkg validate rejects
them with a named error. A trailer with the flag clear has no extension and
parses exactly as v1, which is the legacy-unsigned case below.
The signer and the trust store
|
Note
|
The signer is shipped in crates/tebako-signer. |
crates/tebako-signer
is the one OpenPGP half of the chain:
rnp-rs over librnp with vendored botan
(PQC-capable; #![deny(unsafe_code)] with one documented FFI exception).
The crate owns detached sign/verify, which classifies into Trusted /
Untrusted / Invalid, the outcomes onto which the named trust errors map, and
its rewrite operations (insert-image / remove-image / set-runtime /
reassemble) preserve the input’s signing state. The store has two homes
under $TEBAKO_HOME:
-
keys/holds the user’s keypairs. The press-local Ed25519 key lives here (press-local.pub/press-local.key, mode 0600 on unix), generated once per machine on the first explicit--sign; see keys.rs. -
trust/holds pinned signer keys. Each confirmed identity is pinned one file per fingerprint attrust/<fingerprint>.pub, keyed by the place where the reader learned it. Today’s signer still implements the earlier single concatenated keyring,keyring/trusted.pgp(keyring.rs, trust-on-first-use (TOFU) registration with named outcomes), with rotation statements underkeyring/successors/; the per-fingerprint pin layout is where the store converges.
Third-party identity: three channels, two must agree
|
Note
|
This mechanism is planned, and part of it is shipped. |
First-party slices verify against the embedded tamatebako root, with zero interaction, ever. A third-party author’s slices verify against the author’s own key, and everything turns on how a machine learns the right fingerprint. No channel is trusted alone, and two channels agreeing is the trust event:
-
The registry is the primary channel. A registry ref pins an authenticated location (
tfs:github:metanorma/metanormais GitHub’s guarantee that this is the metanorma org). The index carries asigning:block with the armored public key, the fingerprint, and an optional canonical key URL.tebako add-registrydisplays the fingerprint and the cross-check URLs and asks for confirmation, which is a trust-on-first-use (TOFU) step; on confirmation the key is pinned to the trust store. A non-interactive--yesflow must supply the expected fingerprint out of band, and a blind yes is never acceptable. -
Out-of-band confirmation is never auto-trusted. Authors publish the same fingerprint where their audience already trusts them:
https://<author-domain>/.well-known/tebako-key.asc, their docs, or the org’s.githubrepository. The CLI prints these URLs so that the user can compare them, and it never upgrades trust from the second channel silently. -
Keyservers serve as directories and need no trust. The reader fetches by fingerprint and authenticates the fetched key against the channels above; a swapped key has the wrong fingerprint by construction, so the directory itself needs no trust.
Continuity is the security property. After the first confirmation, every
artifact from that identity must verify against the pinned key. A mismatch
fails closed (SignerKeyChanged, exit 72) and displays both fingerprints; a
valid successor chain forwards automatically after displaying the chain
proof and re-pins the new key. The management verbs
(tebako trust list|show|remove) and re-verification of installed artifacts
by tebako doctor are planned.
Three kinds of crypto, three jobs
|
Note
|
This decision is planned, and it is proposed. |
Tebako touches cryptography for three different questions, and the crypto consolidation decision gives each question exactly one home, never two overlapping mechanisms at the same layer:
| QUESTION | MECHANISM | ROLE |
|---|---|---|
who signed it |
OpenPGP (rnp-rs / botan), covering trailers, release indexes, third-party identity, and the root ceremony |
trust |
who may read it |
the ENC transform, which is per-image opt-in and stacks over any backend |
confidentiality |
how the bytes are sealed |
limnifs format-native crypto: per-drop AEAD, Ed25519 over the Merkle ManifestRoot |
evidence |
The OpenPGP chain is the only authenticity decision point, for every
backend; a slice author manages exactly one keypair, and format-native keys
never enter keys/ or trust/. ENC is the only tebako decryption path, and
a self-encrypted limnifs image (aead ≠ 0) fails ENOTSUP at mount. A
limnifs in-image signature, when admitted, may be verified only as
non-authoritative integrity evidence, after the OpenPGP chain has decided
and against a key that the signed metadata binds to the author; absence is
never an error, and a mismatch warns and journals. That evidence role is the
planned part: its binding addendum is not committed, and today tebako
ignores embedded bundles.
Named failure exits
|
Note
|
The named failure exits are shipped in the Rust bootstrap. |
Verification failures are named exit codes with full message bodies, never silent and never a crash. The trust codes (70/71/72) sit in the loader’s full named set:
| CODE | MEANING |
|---|---|
65 |
trailer missing, corrupt, or structurally invalid |
66 |
launcher ABI mismatch (package requires a newer bootstrap) |
67 |
runtime_ref unparseable or unsupported |
68 |
overlay/decrypt binding failure — unbound store, missing key material, malformed TEBAKO_OVERLAYS / TEBAKO_DECRYPT |
69 |
runtime unresolvable — offline miss or download failure |
70 |
sha256 mismatch — the artifact is refused; nothing enters the store |
71 |
an invalid signature; an unsigned package under TEBAKO_REQUIRE_SIGNED=1; or a strict-mode request asked of a bootstrap built without OpenPGP verification |
72 |
signer key not trusted — including a revoked key (RevokedKey) or a changed signer key (SignerKeyChanged) |
73 |
jail policy could not be applied — malformed TEBAKO_JAIL; fail closed |
74 |
filesystem / lock / install failure |
75 |
the runtime declares a contract_version the loader does not speak — refused before any checksum acceptance |
76 |
--tebako-install refused (TPKG_FLAG_NO_INSTALL) or needs the CLI |
77 |
era mismatch — the package belongs to another tebako era, in either direction |
78 |
the env image layout contradicts the runtime exe, or a TEBAKO_MOUNT_ROOT override that the image does not grant |
79 |
a payload check FAILed (the tebako check aggregate) |
The end-to-end behavior is the following: a tampered runtime download is refused with the sha error; a tampered image slot names its sha256 mismatch at first run; an unregistered signer key is exit 72, a trust error rather than a crash.
What is still planned
|
Note
|
Only the loader’s signature check remains, and it is planned. |
The package-side machinery is shipped, and so is the release side: since
v2.6.0 every released artifact carries its detached .asc signature beside
the sha256 sidecar, the release manifest and the checksum index are
object-signed, and the root-key ceremony has minted the classical root that
this site publishes and the tools embed. What remains is the loader’s
signature check: loader-side revocation consumption (the format is locked),
and the return of the OpenPGP verification feature to the shipped bootstrap
via the tebako-crypto toolkit payload. Until that lands, signed packages run
under the loud unverified-first posture described above, with
TEBAKO_REQUIRE_SIGNED=1 failing closed for hardened environments.
See also: Verify integrity · The store