Skip to content

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.

the release page payload-1.2.3.tfs + .sha256 — the anchor + .asc — opt-in signature on the publisher's OWN host fetch verify at install sha256 of the bytes vs the anchor; signature if present mismatch → named error (70/71/72), nothing enters the store rename the store entry ~/.tebako/payloads/…/1.2.3.tfs + .sha256 sidecar = "verified" read-only 0444 · byte-identical with the published artifact every run no re-verification — that is what makes startup fast UNSIGNED IS FIRST-CLASS — AND LOUD an unsigned artifact installs and runs — with a loud stderr warning and a line in the audit journal, every time. Signing is per-package opt-in; verification of anything signed is always strict. TEBAKO_REQUIRE_SIGNED=1 — unsigned fails closed (exit 71). Never the default. Trust survives redistribution: the anchor travels with the object, so a mirror, a CDN, or a USB stick is untrusted infrastructure — the verification never touches the network again.

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.

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.

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

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 at trust/<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 under keyring/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:

  1. The registry is the primary channel. A registry ref pins an authenticated location (tfs:github:metanorma/metanorma is GitHub’s guarantee that this is the metanorma org). The index carries a signing: block with the armored public key, the fingerprint, and an optional canonical key URL. tebako add-registry displays 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 --yes flow must supply the expected fingerprint out of band, and a blind yes is never acceptable.

  2. 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 .github repository. The CLI prints these URLs so that the user can compare them, and it never upgrades trust from the second channel silently.

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