NAME
elebake - compiler for a verified-boot trust chain: build, sign, attest, deploy, and the tamper-detection loader’s decisions
SYNOPSIS
elebake command [args ...]
elebake help [command | group | topic]
DESCRIPTION
elebake maintains the trust chain of a locally built FreeBSD boot loader: checked-out source, measured expectations, Authenticode signature, OpenPGP-attested manifest, deployment to removable boot media, and tamper witnesses read back at boot. Detection, not enforcement: the design assumes the boot medium can be tampered with and concentrates on noticing.
Every elebake command is a generator: it emits shell text describing an action. Whether that text is displayed, piped, or executed is decided by an interpreter the operator controls per function (see ENVIRONMENT). The shipped default displays (cat): inspect first, then pin an executing interpreter. Batch commands emit an unconditional sequence of further elebake commands; checking is itself a command in the sequence, and the batch machinery stops at the first failure.
The command reference below is generated from the tool’s own help corpus and is therefore always current; elebake help renders the same corpus in the terminal.
THE FREEBSD SOURCES
elebake is nothing without the loader it compiles for. The measurement, claim, gate and policy engine that reads the compiled decisions at boot is a FreeBSD patch series – platform trust gates – and the catalogs elebake offers (stage measure, stage action, stage when, stage phase show) are parsed from the headers of that series. Against a stock FreeBSD tree the catalogs are empty and no foundation can be built.
Get the tree that carries the series:
-
git clone -b platform-trust-gates-15.1 \
https://github.com/johannes-bruegmann/freebsd-src.git ~/git/freebsd-src elebake setenv ELEBAKE_FREEBSD_SRC ~/git/freebsd-src elebake freebsd prerequisites
The branch is based on releng/15.1 (15.1-RELEASE); the loader-side engine lives under stand/efi/loader/local/ and its README points back at this project. Parts of the series are on their way upstream. stage checkout adds a git worktree of that repository per stage, so the source stays one repository and every stage builds from a named ref.
COMMANDS
Every command, grouped as elebake help groups it. elebake help <command> shows parameters, environment and examples.
Without a database
The only commands that run without an existing database. Start here.
- bootstrap <name> <profile>
- Create the database ~/.elebake/<name> (under ELEBAKE_ROOT) with the named profile and point the active-DB symlink db at it. <name> is a plain name, not a path: a slash is refused, db is reserved
- environment init <profile>
- Install (or re-sync after an update) the profile’s template variables into .env/default: the profile is shipped, then `environment install'
- init <profile>
- Initialize the database at ELEBAKE_BASE: the layout (database init), then the profile’s environment (environment init)
- setenv <VAR> <value>
- Set a machine override: the name is a variable name, the value is written to .env/local/<VAR> (0640, owner of the database when possible), the environment cache refreshed when one is on. Effective with the next command
- getenv <VAR>
- Show the effective value of a variable and the layer that answers (.env/local override, .env/default, the shipped template): the variable resolves: rewrite to `getenv read <VAR> <layer> <path>', else an error line
- unsetenv <VAR>
- Remove the machine override .env/local/<VAR> when there is one (else say so), and refresh the environment cache when one is on
- help [<command|group|topic>]
- Show the grouped overview, or detail for a command, group or topic
- help environment
- The concept topic: the environment system and the safety-first design (interpreters, pins, the three-layer cascade, profiles, logs) – template/manual/topic-environment.md
- help manual
- Emit the complete manual as Pandoc Markdown, part by part: the title block, the prose building blocks from template/manual/, COMMANDS from the help corpus, ENVIRONMENT from the variable templates – one truth per content kind (make man renders elebake.8 from it)
- help manual title
- The Pandoc title block of the manual: name, section, author, today’s date
- help manual commands
- The COMMANDS section of the manual, generated from the help corpus and grouped as `elebake help' groups it
- help manual environment
- The ENVIRONMENT section of the manual: one entry per shipped variable template, its @summary as the body – the templates ARE the corpus (help env reads the same files)
- Inspect-by-default
- Pipeline commands PRINT their shell: review, then append `| sh' to run (`| sudo sh' where the ESP or root-only keys are involved). Bookkeeping (add/bind/setenv/filter) is pinned to sh and acts directly. Experienced users may flip the safety default so EVERY terminal acts: elebake setenv ELEBAKE_TERMINAL_INTERPRETER sh (inspect any single command via setintp <family> cat, or per run under an explicit interpreter).
Configuration
Read and change elebake environment variables and interpreter pins.
- environment cache [on|off|status]
- Manage the cached-environment optimisation
- environment cache on
- Build the environment cache NOW (a fresh scan of .env/, written at generation time) so every later invocation skips the layered lookup
- environment cache off
- Remove the environment cache; later invocations read the layered store again
- environment cache status
- Is the environment cache on, and how many variables does it hold?
- printenv [<VAR>]
- Show all effective environment variables, or just one
- help env [<name> [<location>]]
- Text terminal: the documented variables (templates, and the copies under .env); show one with `help env <name>' – the name resolves literal, then ELEBAKE_<name>, then ELEBAKE_INTERPRETER_<name>
- setintp <function> <interpreter>
- Pin a function’s interpreter: rewrite to `setenv <variable> <interpreter>' (the variable from the token)
- getintp <function>
- Show a function’s pinned interpreter: rewrite to `getenv <variable>'
- help intp <function> [<location>]
- Show the documentation of an interpreter variable (class default or per-function): rewrite to `help env <variable>'
Database lifecycle
Back up, restore and batch-drive the database itself.
- attest <file> <key>
- Sign a file with a registered openpgp key (armored detached signature -> <file>.asc): the file exists, the record exists and carries its keyid; then `attest sign'
- attest verify <file> <key>
- Check a detached signature at generation time – signed by the PINNED key, key neither expired nor revoked: the file exists, the record exists and is complete; then `attest signer pinned' (a finding fails the command)
- dump env prologue
- Emit the prologue of a dump: the SOURCE’s .env/local overrides that the restore itself depends on (the PROLOGUE profile’s list – deliberately WITHOUT function-specific interpreter pins, which could deactivate replayed commands), plus the cache switch when the source had it on
- dump env epilogue
- Emit the epilogue of a dump: EVERY .env/local override of the source, including function-specific interpreter pins – restored at the very end, when the replay is done
- destroy <name>
- Remove this database and everything it owns, IRRECOVERABLY, as three steps: the worktrees and their registration in the source repo, the records, the scaffolding (bundle handover area, active-DB symlink, empty root). The name IS the confirmation, checked before a line runs
- destroy worktrees <name>
- Step 1 of destroy: the name is this database’s: rewrite to `destroy worktrees remove' (every stage worktree AND its registration in the source repo – a plain rm -rf would leave “missing but already registered worktree” behind; the ids are only knowable while the database exists), else an error line
- destroy records <name>
- Step 2 of destroy: the name is this database’s: rewrite to `destroy records remove' (the database directory itself, resolved through the active-DB symlink first – rm -rf on a symlink removes the LINK and orphans the database), else an error line
- destroy scaffold <name>
- Step 3 of destroy: the name is this database’s: rewrite to `destroy scaffold remove' (the bundle handover area, the active-DB symlink if it points here, the empty scaffolding), else an error line
- version
- Print the version of this elebake: the dump format it writes and restores (ELEBAKE_FORMAT) and the commit of the checkout it runs from (git; `unknown' without one) – what a dump carries in its header and what a report names
- dump [<strategy> [<stage>|all]]
- Export the database as an executable shell script; the bare form is the COMPLETE description of every stage (strategy `complete', scope `all'). The strategy selects the vocabulary of the body: complete is everything the database holds, minimized is binary management only (loaders, kernel modules, loader.conf, backups, media – what a rescue system needs to roll back). The dump is an EXECUTABLE description: keep it in git and replaying a committed dump reproduces the database. Import lines reference base elements against “$ELEBAKE_ARCHIVE_BASE”; `export' pairs the dump with the bundle that carries them
- dump complete <stage>|all
- The complete dump: the prologue (header, environment, keys, provenance), the foundation arsenal as CLI replays, the full description of the stage (or every stage), the epilogue
- dump minimized <stage>|all
- The minimized dump, the rescue vocabulary: the prologue (header, environment, keys, provenance), per stage only what binary management needs (record, media, backups, the loader/kernel/loader.conf subset of boot/), the epilogue. No foundation, no worktree, no bindings, no rebuild: a rescue system swaps binaries, it does not build them
- restore <dump> [<base>]
- Replay a dump into the current database – only a dump signed by the PINNED attest key, at or above the lineage’s serial; <base> binds where its base elements come from (default: this database). The dump’s format (its `# Version:' header) selects the restore that speaks it: `restore v<format> <dump> <base>'
- batch <file>
- Execute a file of elebake commands line by line (ELEBAKE_BATCH_KEEP_GOING: 0 = stop at the first failing line, 1 = keep going): the file is readable: rewrite to the runner `batch <file> false' (no cleanup), else an error line
- collect [<stage>|all]
- The complete list of files an archive must carry: every key class, the provenance, the foundation – and with a stage its records and boot artifacts. The bare form is `collect all'
- filter <collection> <filtered>
- Apply the default strategy to a collection (delegates: default -> redacted)
- filter redacted <collection> <filtered>
- Drop what is a machine secret in spirit: marker values, site.mk baselines, backups – and always the operational directories. What a bug report needs, without the reporter’s fingerprints. The collection is readable: rewrite to `filter write redacted ...', else an error line
- filter full <collection> <filtered>
- Keep everything except the operational directories. For one’s own disaster recovery, never for sending. The collection is readable: rewrite to `filter write full ...', else an error line
- filter minimized <collection> <filtered>
- Keep binary management only: per stage the loader (built and signed), loader.conf, kernel and modules, the manifest pair, media and backups; keys and receipts. What a rescue system needs to roll back or repair, nothing to build with. The collection is readable: rewrite to `filter write minimized ...', else an error line
- bundle <collection> <archive>
- Pack the collected files with ELEBAKE_ARCHIVER: the collection is readable, the archiver set, the archive path absolute, the manifest pair (MANIFEST + MANIFEST.asc) lies beside the collection – an archive that carries no tamper detection is not an artifact this tool produces – and the collection lives inside the database (the pair is packed by its database-relative path); then `bundle pack'
- seal <dump> <bundle>
- Append the bundle’s sha256 and size to the dump as its seal line (`# Bundle: sha256=... bytes=...'): both files readable, the dump not yet sealed (a dump names ONE bundle) and not yet attested (sealing after signing would invalidate the signature); then `seal write'. Attest the dump AFTER sealing so one signature covers both
- seal verify <dump> <bundle>
- Check that the dump’s seal line names THIS bundle (sha256 and size): both files readable, the dump sealed, the seal matching – a mismatch fails the command before anything is extracted
- incoming clear <directory>
- Empty one scratch directory under $ELEBAKE_ROOT/incoming/ – import’s own unpacking area, cleared before each extract so a re-import never trips over the last one. The path lies below incoming/ and carries no `..': rewrite to `incoming remove', else an error line (only import’s own scratch is cleared here, never a database)
- extract <archive> <destination>
- Unpack an archive into a scratch directory with ELEBAKE_EXTRACTOR: the archive readable, the extractor set, the destination absolute and empty (or not yet there); then `extract unpack'
- export <strategy> <dump> <bundle> [<stage>|all]
- Write the two artifacts that travel apart, bound and signed: the DUMP to a path of your choosing (commit it), the BUNDLE to its store. The strategy is the first word: redacted (no machine secrets, for sending) | full (own disaster recovery) | minimized (rescue: loaders, kernel, loader.conf, backups, media). redacted and full describe the database completely and govern the payload only; minimized describes and carries binary management only. The bare form covers every stage
- import <dump> <bundle>
- Replay an exported pair into the current database, checked end to end BEFORE anything lands, cheapest first: the pinned signer, the dump’s signature, the seal (this bundle is the one the dump names), then – unpacked into import’s own scratch under incoming/ – the bundle’s MANIFEST (pinned signer, every hash). The receipt is filed BEFORE the replay (restore applies its own admissibility: signer, serial floor; the replay runs keep-going, so a redacted pair’s withheld elements cost no receipt). restore re-checks the dump as every restore does
- manifest <collection> <manifest>
- Hash every entry of a collection into a manifest, same format as the boot manifest (LC_ALL=C-sorted `path sha256=hash', symlinks as `path symlink=target'): the collection is readable, the manifest is named MANIFEST (import looks for it by name, next to the collection), every entry is hashable; then `manifest write' – everything hashed at generation, the emission is one concrete heredoc write
- manifest attest <collection> <key>
- Write the manifest for a collection and sign it, side by side with the collection: manifest + attest
- manifest verify <manifest> <base> <key>
- Verify a manifest, both judgments at generation time: the signature (attest verify: signed by the PINNED key, key neither expired nor revoked) and the tree (manifest match: every listed entry present and identical) – a finding fails the batch before restore replays anything. The key is the RECEIVER’s openpgp record naming the signer it expects
- manifest match <manifest> <base>
- Does the tree under <base> match the manifest? The manifest is readable and lists entries, the base exists; then `manifest tree matches' – every listed file hashes identically, every listed symlink points where recorded (MISSING, CHANGED, RETARGETED fail). Files present but unlisted are not findings: the base is an extraction directory
- provenance serial
- Act terminal: advance the export serial by one (export does this before writing the dump); the number lands in the dump header
- provenance add <dump> <bundle>
- File the receipt of an admitted import: the dump readable, numbered, signed by the pinned signer and not a downgrade (restore’s own checks), the bundle readable or `-'; then `provenance receipt' (files it, or says it is filed, or refuses a differing one) and the export serial raised to the imported one
- provenance import <id> <absfile>
- Copy ONE file of a receipt record from another database (dump/restore base element; the record directory is created on first file): the id is a record name, the source exists; then `provenance import place'
- provenance dump
- Emit the provenance portion of a database dump: one `provenance import' line per receipt file (cat-pinned: dump TEXT, replayed by restore); none is a comment line
- provenance collect
- Text terminal: the receipt record files that belong into an archive, by their real path against “$ELEBAKE_ARCHIVE_BASE”
- provenance list
- Text terminal: the export serial and every receipt – serial, when, signer, dump and bundle hashes, into which database, by whom; none is said so
Keys
Key registries. Records hold REFERENCES (paths, URIs, key ids) – never private material; custody stays with the token, keyring, or root file.
- openpgp add <name> <keyid> [<gnupghome>]
- Register a GnuPG attest key; gnupghome for a keyring living elsewhere: the name is a record name, the keyid is hex; then `openpgp record'
- openpgp dump
- Emit the openpgp portion of a database dump: one `openpgp add' replay line per registered key, in the arity of the record (cat-pinned: dump TEXT, replayed by `restore'), plus one `openpgp import' per extra file; none is a comment line
- openpgp import <name> <absfile>
- Copy ONE extra file into the key record – dump/restore base element (the schema files travel as the `openpgp add' replay): rewrite to `key import openpgp <name> <absfile>'
- openpgp collect
- List the files of the openpgp key records that belong into an archive – public material only; private key material stays a path promise into the world: one `openpgp collect <key>' per record, none is a comment line
- pem add <name> <keyfile> <certfile>
- Register a file-based signing key by its paths (for sites without a token): the name is a record name; then `pem record'
- pem dump
- Emit the pem portion of a database dump: one `pem add' replay line per registered key (cat-pinned: dump TEXT, replayed by `restore'), plus one `pem import' per extra file; none is a comment line
- pem import <name> <absfile>
- Copy ONE extra file into the key record – dump/restore base element (the schema files travel as the `pem add' replay): rewrite to `key import pem <name> <absfile>'
- pem collect
- List the files of the pem key records that belong into an archive – public material only; private key material stays a path promise into the world: one `pem collect <key>' per record, none is a comment line
- pkcs11 add <name> <uri> <certfile>
- Register a PKCS#11 token key, its URI and certificate path: the name is a record name; then `pkcs11 record'
- pkcs11 dump
- Emit the pkcs11 portion of a database dump: one `pkcs11 add' replay line per registered key (cat-pinned: dump TEXT, replayed by `restore'), plus one `pkcs11 import' per extra file; none is a comment line
- pkcs11 import <name> <absfile>
- Copy ONE extra file into the key record – dump/restore base element (the schema files travel as the `pkcs11 add' replay): rewrite to `key import pkcs11 <name> <absfile>'
- pkcs11 collect
- List the files of the pkcs11 key records that belong into an archive – public material only; private key material stays a path promise into the world: one `pkcs11 collect <key>' per record, none is a comment line
Stage pipeline
A stage is one named workspace for one boot tree: bind keys, check out a worktree, embed trust, build isolated, curate, sign, attest.
- stage list
- List all stages with their derived state (one line each): name, id, populated, signed (STALE = loader changed after signing), sign-key/attest-key
- stage add <stage>
- Create a named stage (workspace for one boot tree): the name is a record name; then `stage add new' – IDEMPOTENT, an existing stage is left untouched (a replayed dump must never mint a fresh id and bend the name symlink)
- stage edit <stage> <relpath>
- Edit a file of the stage’s boot tree (the single source of truth; publish with stage push): the stage exists, the file is in boot/, the editor is pinned; then `stage edit open'
- stage sign key <stage> <backend> <key>
- Bind the loader-signing key slot (backend: pem | pkcs11): the stage exists, the key record is registered; then `stage sign key bind'
- stage attest key <stage> <backend> <key>
- Bind the attest key slot (openpgp) – required BEFORE build, its anchor is embedded: the stage exists, the key record is registered; then `stage attest key bind'
- stage unkey <stage>
- Clear both key slots: the stage exists; then `stage unkey clear'
- stage prerequisites add <stage> <exist|verify> <path|->
- Add one absolute bootfs path to the stage’s EXIST or VERIFY prerequisites (idempotent append); `-' reads paths from stdin, one per line, each validated before anything is emitted. `-': rewrite to `stage prerequisites add stdin <stage> <kind>', else to `stage prerequisites add entry <stage> <kind> <path>'
- stage prerequisites drop <stage> <exist|verify> <path>
- Remove one path from the stage’s EXIST or VERIFY prerequisites: the stage exists, the kind is valid, the path is listed; then `stage prerequisites remove'
- stage prerequisites show <stage> <exist|verify>
- Show one of the stage’s prerequisites lists: the stage exists, the kind is valid; then `stage prerequisites table'
- stage prerequisites exist add <stage> <path|->
- Add one absolute bootfs path to the stage’s EXIST prerequisites; `-' reads paths from stdin – rewrites to `stage prerequisites add <stage> exist <path|->'
- stage prerequisites exist drop <stage> <path>
- Remove one path from the stage’s EXIST prerequisites – rewrites to `stage prerequisites drop <stage> exist <path>'
- stage prerequisites exist show <stage>
- Show the stage’s EXIST prerequisites – rewrites to `stage prerequisites show <stage> exist'
- stage prerequisites verify add <stage> <path|->
- Add one absolute bootfs path to the stage’s VERIFY prerequisites; `-' reads paths from stdin – rewrites to `stage prerequisites add <stage> verify <path|->'
- stage prerequisites verify drop <stage> <path>
- Remove one path from the stage’s VERIFY prerequisites – rewrites to `stage prerequisites drop <stage> verify <path>'
- stage prerequisites verify show <stage>
- Show the stage’s VERIFY prerequisites – rewrites to `stage prerequisites show <stage> verify'
- stage status <stage>
- Show the stage’s derived state (nothing cached – read from the artifacts): the stage exists; then the title and one line per aspect, each its own terminal
- stage sign <stage>
- Authenticode-sign boot/loader.efi with the bound sign-key: the stage exists, the slot is bound; then `stage sign backend' lifts the record’s backend into the command (pem: uefisign, pkcs11: osslsigncode)
- stage sign pem <stage>
- Authenticode-sign boot/loader.efi with the bound file-based key: the backend’s tools, the stage exists and is populated, the record carries key and cert; then `stage sign pem run' (uefisign)
- stage sign pkcs11 <stage>
- Authenticode-sign boot/loader.efi with the bound pkcs11 key: the backend’s tools, the stage exists and is populated, the record carries uri and cert; then `stage sign pkcs11 run' (osslsigncode through the engine or provider bridge, PIN prompted hidden on the tty and handed over via a transient file – never argv, never env)
- stage attest <stage>
- Detach-sign the stage manifest with the bound attest key: gpg is there, the stage exists, the attest-key is bound and complete, the manifest is there; then `stage detachsign' (armored detached signature boot/manifest.asc, exactly what the loader verifies against the anchor)
- stage checkout <stage> <ref>
- Add a DETACHED git worktree of ELEBAKE_FREEBSD_SRC at <ref> (a snapshot, never a branch to commit on) and point the stage at it: the toolchain is there, the stage exists, the source repo is set; then `stage worktree'
- stage recheckout <stage> <ref>
- Replace the stage’s worktree by a fresh DETACHED one at <ref>: the toolchain is there, the stage exists and is checked out, the source repo is set; then `stage worktree remove' (git worktree remove –force and prune, the work link), then `stage worktree'. The old worktree holds generated files only – site.mk, secret.mk, foundation.c – which stage site mk and stage foundation make write again; the build outputs live in destdir/ and obj/ of the stage
- stage clean <stage>
- cleandir the worktree’s stand/ build and reset the stage’s build outputs: the stage exists and is checked out; then `stage clean dir' and `stage reset'
- stage build <stage>
- The isolated stand/ build: the toolchain, the trust anchor in the worktree (stage trust), the build gate (keys bound), clean, then one `stage build stand <stage> <component>' per curated component
- stage make <stage>
- BUILD the stage: build (gate, clean, stand), install, include, sign – publishing is stage push. Needs executing interpreter pins on the terminals underneath, otherwise every step only displays (see the environment topic)
- stage install <stage>
- Unprivileged stand/ DESTDIR install into destdir/: one `stage install <stage> <component>' per component of ELEBAKE_STAND_INSTALL_SUBDIRS (curated policy); an unset list is an error line
- stage build kernel <stage>
- Build the kernel from the stage’s checkout – the source delivers EVERYTHING, the filter selects: the stage exists and is checked out, ELEBAKE_KERNCONF is set (no implicit default: the guarding kernel is a decision) and exists in the checkout; then `stage build kernel run' (isolated per-stage obj)
- stage install kernel <stage>
- Unprivileged installkernel into the stage’s destdir (destdir/boot/kernel becomes selectable by the filter): the stage exists and is checked out, ELEBAKE_KERNCONF is set, the obj tree exists; then `stage install kernel run'
- stage loader <stage> <loader.efi>
- Ingest an ALREADY-BUILT external loader as boot/loader.efi (sign it without building): the stage exists, the source file exists; then `stage loader ingest'
- stage manifest <stage>
- Generate the REAL veriexec manifest over boot/ (path sha256=hash, LC_ALL=C-sorted, every file except the manifest pair; loader.efi included on purpose – it is the disaster reserve, and an unverified reserve is not a reserve): the stage exists and is populated; then `stage manifest write' – the WHOLE manifest is computed at generation, the emission is one concrete heredoc write
- stage verify <stage>
- Manifest consistency BOTH ways, checked at generation time: the stage exists, the manifest is there; then `stage verify listed' (every listed entry exists and hashes identically) and `stage verify unlisted' (every file in boot/ is listed)
- stage trust <stage>
- Root of trust into the worktree, in one step: `stage trust anchor' exports the bound attest key (its public key and a self-test signature) into lib/libsecureboot, `stage trust mk' writes site.trust.mk – the veriexec anchor the loader is BUILT with, so this runs after `stage attest key' and before `stage build'. Assumes: the attest key is RSA (the loader’s BearSSL verifies RSA only) and its keyring is readable: the shipped profiles pin the anchor export to sudo sh (the keyring lives under /root), so the batch asks for the password by itself
- stage trust anchor <stage>
- Export the attest key + self-test signature into the worktree’s libsecureboot: the stage exists and is checked out, the attest key is bound and complete; then `stage trust anchor export'
- stage trust mk <stage>
- Write site.trust.mk (elevated veriexec config) into the worktree: the stage exists and is checked out; then `stage trust mk write'
- stage filter list <stage>
- The curated list as it stands: the stage exists; then `stage filter table'
- stage filter uncurated <stage> <srcdir>
- What the source directory holds at top level that the filter does not cover: the stage exists, the source is absolute; then `stage filter uncurated table'
- stage filter orphaned <stage>
- What lies in boot/ that the filter does not cover and no step generates: the stage exists; then `stage filter orphaned table'
- stage filter prune <stage>
- Emit the removal of every orphaned entry of boot/ (what `stage filter orphaned' lists: neither curated nor generated) – one rm per entry, displayed by default so the owner reads the list before piping it to sh. After `stage adopt' this is how a medium’s stale files (BIOS boot blocks, forth, old loader variants) leave the stage: the curation says what belongs, prune removes the rest. Nothing curated or generated is ever touched. The stage exists; then `stage filter prune rm'
- stage filter add <stage> <rel|->
- Curate one destdir/boot-relative path into the boot/ list (idempotent append); `-' reads a FROZEN snapshot from stdin, one path per line – a directory entry is a LIVING curation (new files under it travel along), the snapshot a frozen one. `-': rewrite to `stage filter add stdin <stage>', else to `stage filter add entry <stage> <rel>'
- stage filter drop <stage> <rel>
- Remove one entry from the boot/ curation list: the stage exists, the entry is listed; then `stage filter remove'
- stage filter show <stage> [<srcdir>]
- ONE view, the whole truth, as three tables: the curated list, what sits uncurated in the source (default: the stage’s own destdir/boot), what lies orphaned in boot/
- stage include <stage> [<srcdir>]
- Work the curated filter off from a chosen SOURCE directory (default: the stage’s own destdir/boot; e.g. /boot takes the same selection from the host’s binaries): the stage exists, the source is absolute and present, the filter is not empty, every entry is in the source or already in boot/; then `stage include copy'. Per entry: present in the source -> copied into boot/ (directories replaced whole); absent from the source but present in boot/ -> kept and noted (an adopted file the build does not deliver, e.g. loader.conf after stage adopt); absent from both -> refused before anything ran
- stage import <stage> <reldir> [<absfile>]
- Import ONE base element from another database: two args declare a record-tree DIRECTORY, three copy a file/symlink into it – an unconditional check-then-act sequence; the logic lives in the dump that emits these lines. Three args: the stage exists, the subdir is record-relative and declared, the source an absolute existing file or symlink; then `stage import file'
- stage collect [<stage>]
- Fan out to every stage: one `stage collect <stage>' per name symlink; none is a comment line
- stage adopt <stage> <medium>
- One-time adoption of a RUNNING boot tree: copies the medium’s WHOLE boot/ (cp -a, existing files in the stage’s boot/ are overwritten) into the stage – boot/ becomes the single source of truth, configuration (loader.conf & Co.) included. A batch: pool import (read-only), adopt copy, pool export – the export runs even when the copy failed (keep-going wrapper), so the pool never stays imported. Compiled parts the medium brings (kernel, loader, lua, defaults) are replaced by the stage’s own build the next time `stage include' runs, so adopt BEFORE include or run include again after it; `stage filter orphaned' then lists what the medium brought that nothing curates – the owner decides about each such file
- stage push <stage> <medium>
- Publish the stage: manifest, attest, verify, tree onto the medium, loader onto the ESP
- stage dump [<strategy>] [<stage>|all]
- The stage part of a dump: every stage in the complete vocabulary. The strategy selects the anchor that knows its blocks (dispatch): `stage dump complete <stage>' is the full description, `stage dump minimized <stage>' the rescue subset
Provisioning
The machine-bound trust expectations: the NVRAM boot marker and the compiled-in baselines (local/site.mk). What turns a generic loader into THIS machine’s tamper detector.
- stage baseline add <stage> <MACRO> <digest|string|int> <value>
- Store one build-provided value of the stage: a LOADER_TRUST_* macro the loader sources consume, rendered into site.mk by `stage site mk' (digest: 64 hex -> byte list; string: C string; int: bare). Immutable: an identical re-add is a no-op, a differing one is refused (drop first). This is where gate slots (gate add) and provider windows get their values; the dump replays every record
- stage baseline drop <stage> <MACRO>
- Remove one build-provided value of the stage: the stage and the record exist, then it is erased
- stage baseline show <stage> [<MACRO>]
- Show the stage’s build-provided values (or one) as the -D lines site mk would render: one `stage baseline show <stage> <MACRO>' per record; none is a note
- stage baseline learn <stage> <MACRO> <kenv-variable>
- Take a digest baseline from THIS machine’s kenv when the batch runs – the value a trusted boot’s loader published (loader.trust.<gate>.<leaf>, 64 hex) – and record it as `stage baseline add <stage> <MACRO> digest <value>'. For the witnesses userland cannot compute (loaded EFI images, ACPI tables, EFI variables, PCI devices, PCR bank, soft PCR, guarded kenv). Assumes: the boot that published the value was the owner’s own, on the intended firmware – learning on a tampered platform bakes the tampering in
- stage disks add <stage> <partition>
- Record one internal GPT partition (nda0p1) whose GELI metadata sector and whose disk’s partition table the loader measures (measure_geli, measure_gpt); `stage site mk' hashes them when it runs. Order of the records = order of hashing. Assumes: the partitions are GELI providers on GPT disks the firmware exposes via Block I/O; a legitimate setkey/delkey or repartitioning moves the digest – re-run site mk afterwards
- stage disks drop <stage> <partition>
- Remove one partition from the stage’s disks record: the stage exists and lists it, then the line is removed
- stage disks show <stage>
- List the stage’s recorded partitions as notes; none says so
- stage kenv add <stage> <key> <value>
- Store one loader.trust.* kenv value of the stage (dumb store, immutable: identical re-add is a no-op, a differing one is refused); `stage loaderconf mk' writes every record into boot/loader.trust.conf, `stage require' says which the bound actions demand. Values that are secrets (hashes) belong here only as HASHES; the medium carries this file in clear, covered by the manifest
- stage password set <stage>
- Set the loader prompt password of the stage: the word is read HIDDEN from the terminal when the batch runs – twice, both must match – and its sha256 becomes the kenv record password_sha256, which stage loaderconf mk writes into boot/loader.trust.conf (the loader reads that file like loader.conf). With it set, Lua boots at once and asks the word only after a key during the autoboot; the compiled-in loaderlock secret guards the prompt in C besides, so the word is the second lock, not the only one. The word reaches neither argv, history nor the batch text; the hash is what the card carries anyway. The record is immutable: stage kenv drop <stage> password_sha256 first to change it
- stage kenv drop <stage> <key>
- Remove one loader.trust.* value of the stage: the stage and the record exist, then it is erased
- stage kenv show <stage> [<key>]
- Show the stage’s loader.trust.* values (or one) as the lines loaderconf mk would write: one `stage kenv show <stage> <key>' per record; none is a note
- stage kenv learn <stage> <key> <kenv-variable>
- Take a value from THIS machine’s kenv when the batch runs – what a trusted boot’s loader published – and record it as `stage kenv add <stage> <key> <value>': the way a key expectation (expectation add ... key ...) gets its value, e.g. loader.trust.kernellock.pcr.expected from loader.trust.kernellock.pcr.sha256. The loader reads it from loader.trust.conf, so learning changes the conf (stage loaderconf mk, include, sign, push), never the binary. Assumes: the boot that published the value was the owner’s own, on the intended firmware – learning on a tampered platform bakes the tampering in
- stage require <stage>
- Report what the stage’s BOUND loader policies demand: the loader.trust.<gate>.<leaf> keys the actions read at boot (parsed from the kenv(a, ...) calls in the checkout’s action.c, attributed per gate), each with its kenv record or MISSING; then the LOADER_TRUST_* baselines the measurements and actions consume without a code default, each ok or MISSING. With a container argument: the demands of that container’s bound measurements on the boot tree
- stage loaderconf mk <stage>
- Write boot/loader.trust.conf of the stage from its kenv records: the stage and its checkout exist, boot/loader.conf names the file in loader_conf_files (stage edit adds it), every leaf a bound action reads has its value (stage require), then the file is written. It rides on the medium in clear, the manifest covers it (stage manifest after this)
- stage loaderconf check <stage>
- Regenerate loader.trust.conf from the kenv records and compare with the stage’s boot/loader.trust.conf: drift per key is reported, agreement is one line – the conf under tamper detection
- stage inventory import <stage>
- Fetch the per-boot lists elvbootd filed under /var/db/elvboot/inventory (root’s, written by inventory_record_act in STARTUP) into the stage’s inventory/records/, owned by the database’s owner: the stage exists, then the copy (pinned sudo sh). Run it after every boot whose lists you want to compare
- stage inventory show <stage> <kind>
- Lay the imported records side by side for one kind (acpi | efivars): per item its identity, size, for efivars the attributes in words, in how many boots it appeared, whether every boot saw the same digest (same) or not (MOVES), whether it is in the set (+), and the digest of each boot, oldest first. What moves is what the firmware rewrites: leave it out of the set
- stage inventory add <stage> <kind> <entry>
- Take one item into the set of a kind: the stage exists, the kind is acpi or efivars, the entry has the identity form (<a>/<b>, letters, digits, _ . -), the newest imported record lists it (the loader saw it – stage inventory import first), then it is appended to inventory/<kind> unless already there. The set is what AcpiTables / EfiVariables measure; after a change: stage site mk, build, boot, then learn the digest
- stage inventory drop <stage> <kind> <entry>
- Take one item out of the set of a kind: the stage exists, the kind is valid, the entry is in the set, then its line is removed from inventory/<kind>
- stage inventory list <stage> <kind>
- The set of a kind as the stage has it, one entry per line: the stage exists, the kind is valid, then the lines of inventory/<kind> (a note when empty)
- stage inventory make <stage>
- Render the sets of the stage as site.mk lines, one `stage inventory make set <stage> <kind>' per kind: LOADER_TRUST_ACPI_SET and LOADER_TRUST_EFIVARS_SET, the entries sorted and comma-joined – the order the loader hashes the members in. A part of stage site mk; an empty set renders nothing, the claim is then skipped
- stage marker record <stage> <BootXXXX> <filepath>
- Record which load option carries the marker and where its value file lives (bookkeeping only – no NVRAM): the stage exists, the load option and the absolute path are well-formed; then `stage marker store'
- stage marker write <stage> <restore|new>
- Write the marker into the recorded load option: `restore' puts the value from the recorded file back (the file is read when the privileged act runs; empty or unreadable is an error there), `new' always generates. The marker is recorded: rewrite to `stage marker value <restore|new> <stage>', else an error line
- stage marker <stage> <BootXXXX> [<filepath>]
- Write the boot-entry marker into that load option: a batch of two – the RECORD (bootvar + value-file path, bookkeeping, acts) and the WRITE (NVRAM byte surgery, inspect-by-default – pipe the output to sudo sh). The value comes from the file, or is generated when the file is empty
- stage marker rotate <stage>
- Rotate the marker: a NEW value into the recorded load option and value file. The marker is recorded: rewrite to `stage marker write <stage> new' (inspect-by-default, pipe to sudo sh), else an error line
- stage site mk <stage>
- Write local/site.mk of the stage’s checkout: the stage and its checkout exist; the header, then the measured parts (board, keys, marker, origin, disks) and the baselines appended in file order to site.mk.new; the install (rendered, clean, placed) and the report. Run under sudo: the key store, the marker and the origin are EFI variables
- stage site mk report <stage> [<file>]
- Show the written site.mk of the stage (default: local/site.mk of its checkout) as comment lines
Medium
The physical boot medium: bind it, back up its loader, swap, roll back.
- stage tree sync <stage> <medium>
- Write the stage’s boot/ tree onto the medium’s dataset: pool import, then snapshot + copy + verify (fail-fast), then close (rollback if the tree is not the manifest) and pool export – close and export ALWAYS run (keep-going wrapper), so a failed sync never leaves the pool imported or the medium half-written
- stage pool import <stage> <medium> <rw|ro>
- Import the medium’s pool from its GPT label (confined: no device scan), rooted under $ELEBAKE_ROOT/mnt/<stage>-<medium>, and mount the boot dataset: the medium ready, the mode rw or ro, the pool not imported yet; then `stage pool mount'
- stage pool export <stage> <medium>
- Unmount and export the medium’s pool and remove the altroot: the medium ready, then `stage pool release' (a pool that is not imported is a note, not an error, so the closing line of a batch is always safe). Its pin forces fail-fast inside, whatever the enclosing batch keeps going
- stage tree snapshot <stage> <medium>
- Snapshot the medium’s boot dataset (elebake-<stamp>) before it is rewritten: the medium ready, the pool imported; then `stage tree snap'
- stage tree copy <stage> <medium>
- Replace the boot/ tree on the MOUNTED dataset with the stage’s boot/: the medium ready, the stage’s manifest attested, the dataset mounted and a boot dataset; then `stage tree write'
- stage tree verify <stage> <medium>
- Inspect the boot tree ON the mounted medium against the stage’s manifest at generation time, both directions: the medium ready, the manifest there, the dataset mounted; then `stage tree matches' – a finding fails the command
- stage tree close <stage> <medium>
- Leave the medium consistent, decided at generation time: the medium ready, then `stage tree settle' – a tree that IS the manifest stays (snapshot kept as history), a tree that is not goes back to the newest elebake snapshot; not imported = nothing to do. Its pin forces fail-fast inside, whatever the enclosing batch keeps going
- stage device <stage> <medium> </dev/node> [<mountpoint>]
- Register a NAMED deploy medium (its backups sort under backup/<medium>): the stage exists, the medium name and the device node are well-formed; then `stage medium record'
- stage boot tree <stage> <medium> <gpt-label> <pool/dataset>
- Register where the medium’s boot tree lives (used by stage push/tree sync, adopt, pool import): the stage and the medium exist, the label and the dataset name are well-formed, the label – when the medium is inserted – names a freebsd-zfs partition of the medium’s disk, never its EFI system partition; then `stage boot tree record'
- stage backup <stage> <medium> [<label> [<description>]]
- Save the loader currently ON that medium as a backup RECORD backup/<medium>/<label>/ (loader.efi, description, sha256, created, source, by): the stage and the medium exist, the label is a record name and new (a label is unique per medium and immutable), the description given, the device present; then `stage backup take'. Label defaults to the UTC stamp, description to medium/stage/user
- stage backup list <stage> <medium>
- Show the backup records of that medium, oldest first (label, created, sha256, description – the view for a rollback decision): the stage and the medium exist; then `stage backup table'
- stage rollback <stage> <medium> [<label>]
- Put a backup back onto the medium – but FIRST save what is on the medium now as record suspect-<stamp> (quietly: the loader about to be overwritten may be the evidence), then write the named (or newest) backup and verify its hash on the medium
- stage deploy <stage> <medium>
- The loader swap onto the NAMED medium: the stage and the medium exist, the device is present, the signed loader is there and newer than the built one; then `stage deploy write' (mount, backup record of what is there, copy, hash check, umount)
Trust foundation (catalogs and, soon, claims/gates/policies)
Toolchain readiness checks; each backend answers for itself.
- stage measure <stage> [<container>]
- List the measurements the containers’ catalogs offer in the stage’s checkout, with their descriptions: one `stage measure list <container> <stage>' per container of ELEBAKE_CONTAINERS, or the one container given
- stage action <stage> [<container>]
- List the actions the containers’ catalogs offer in the stage’s checkout, with their descriptions: one `stage action list <container> <stage>' per container of ELEBAKE_CONTAINERS, or the one container given
- stage when <stage> [<container>]
- List the firing predicates the containers’ catalogs offer in the stage’s checkout, with their descriptions: one `stage when list <container> <stage>' per container of ELEBAKE_CONTAINERS, or the one container given
- catalog doc <file> <name>
- Print the entry’s name and the description its source carries (template/awk/catalog-doc.awk over the catalog file); an entry without one says so
- stage phase policy add <stage> <phase> <policy> [<position>]
- Bind a policy into a phase of the stage: ensure its prerequisites in the container that hosts the phase, then append it to the phase’s policy list (a position inserts instead)
- stage phase policy ensure <stage> <phase> <policy>
- Unroll the containers: one `stage phase policy ensure <container> <stage> <phase> <policy>' per container of ELEBAKE_CONTAINERS; the container that hosts the phase ensures the policy, the others say so in a comment line
- stage policy ensure <stage> <container> <policy>
- The policy’s prerequisites in the container: every line `<kind> <name>' of the record (`gate <gate>', `trigger <trigger>') is one `stage <kind> ensure <stage> <container> <name>'
- stage foundation check <stage>
- The stage exists; every bound policy has its prerequisites (stage phase policy ensure, per phase and policy); every baseline the bound policies demand is reported when missing (gate slots, macro expectations, and what the catalog names demand per template/tbl/baseline-demands.tbl); then the summary
- stage phase show <stage> [<phase>]
- List the phases the stage’s checkout offers per container (ELEBAKE_CONTAINERS, in that order), each with its bound policies: one `stage phase show <container> <stage>' per container; one phase in detail with the 2-arg form
- stage phase policy drop <stage> <phase> <policy>
- Unbind a policy from a stage’s phase (the policy itself survives): the stage exists and the phase binds the policy, then the line is removed
- macro add <MACRO> <type> <label> [<else>] [<defined>]
- Store a macro record for a compiled baseline: <MACRO> is the C macro stem (BOARD_DIGEST), <type> sha256 | byte, <label> the measurement label. The 4-arg form names the #else alternative (a verbatim C expression, `-' derives MEASUREMENT_NONE), the 5-arg form also the defined name (`-' derives <MACRO> minus _DIGEST plus _EXPECTED). No arsenal record is referenced: the chain is write only
- macro drop <MACRO>
- Remove a macro record: it exists, no macro expectation names the macro it defines (a referenced record is never dropped), then it is erased
- macro show [<macro>]
- Show every macro record (or one) as the C that WOULD be emitted: one `macro show <macro>' per record; none is a note
- expectation add <expectation> <type> <label> <value>
- Store a named, reusable expectation: <type> byte | sha256 | string | macro | key; a macro expectation names a macro an arsenal macro record defines (the reference never dangles; the type word dispatches the check); a key expectation names the leaf of a kenv record the loader reads at run time, loader.trust.<gate>.<key> – for a value that includes the loader itself (PcrBank, LoadedImages) and so cannot be compiled into it; stage kenv learn records it, no build follows. Then the record is written – an identical re-add is a no-op, a different one is refused (immutable; drop first)
- expectation drop <expectation>
- Remove a expectation: it exists, no claim names it as its expectation (a referenced record is never dropped), then it is erased
- expectation show [<expectation>]
- Show every expectation record (or one) as the C that WOULD be emitted: one `expectation show <expectation>' per record; none is a note
- claim add <claim> <measurement> <diagnose|-> <publish|-> <expectation>
- Store a named, reusable claim: its expectation must exist in the arsenal (the reference never dangles), then the record is written – an identical re-add is a no-op, a different one is refused (immutable; drop first)
- claim drop <claim>
- Remove a claim: it exists, no gate lists it (a referenced record is never dropped), then it is erased
- claim show [<claim>]
- Show every claim record (or one) as the C that WOULD be emitted: one `claim show <claim>' per record; none is a note
- trigger add <trigger> <when> <action>
- Store a named, reusable FIRE(<when>, <action>) pair. Each field is a catalog name or a composition without whitespace: the when may be and(a,b,...), or(a,b,...) or not(a), nested at will; the action may be compose(a,b,...) to run several in order. The loader sees AND/OR/NOT and COMPOSE, the sh containers { a && b; }, { a || b; }, ! a and a; b. The leaves are checked against the container’s catalog when a policy binds: no arsenal record is referenced, the chain is write only – an identical re-add is a no-op, a different one is refused (immutable; drop first)
- trigger drop <trigger>
- Remove a trigger: it exists, no policy lists it (a referenced record is never dropped), then it is erased
- trigger show [<trigger>]
- Show every trigger record (or one) as the C that WOULD be emitted: one `trigger show <trigger>' per record; none is a note
- gate add <gate> [<secret-slot>] [<duress-slot>]
- Create a gate: <gate> is a C identifier (it lands in foundation.c); the slots are the -D macros carrying the unlock and duress hashes (LOADER_TRUST_BOOTLOCK_SECRET, ..._DURESS), `-' or absent for none. No arsenal record is referenced: the chain is write only – an identical re-add is a no-op, different slots are refused (immutable; drop first)
- gate drop <gate>
- Remove a gate: it exists, no policy names it (a referenced record is never dropped), then it is erased
- gate claim add <gate> <claim> [<position>]
- Append a claim reference to a gate (order = evaluation order): the gate and the claim exist (the reference never dangles), then the claim is appended – a claim already listed is a no-op; the 3-arg form inserts at a 1-based position instead
- gate claim drop <gate> <claim>
- Unlink a claim reference from a gate (the claim itself survives): the gate exists and lists the claim, then the line is removed
- gate show [<gate>]
- Show every gate record (or one) as the C that WOULD be emitted: one `gate show <gate>' per record; none is a note
- policy add <policy> <gate>
- Create a named policy for a gate (triggers are appended separately): the gate exists (the reference never dangles), then the record is written – an identical re-add is a no-op, a different gate is refused (immutable; drop first)
- policy drop <policy>
- Remove a policy: it exists, no phase of any stage binds it (a referenced record is never dropped), then it is erased
- policy trigger add <policy> <trigger> [<position>]
- Append a trigger (FIRE) reference to a policy: the policy and the trigger exist (the reference never dangles), then the trigger line is appended – a trigger already listed is a no-op; the 3-arg form inserts at a 1-based position among the trigger lines instead
- policy trigger drop <policy> <trigger>
- Remove a trigger reference from a policy (the trigger itself survives): the policy exists and lists the trigger, then the line is removed
- policy show [<policy>]
- Show every policy record (or one) as the C that WOULD be emitted: one `policy show <policy>' per record; none is a note
- foundation dump
- Emit the foundation portion of a database dump: one block per family in dependency order (macros, expectations, claims, triggers, gates, policies); adds are idempotent-immutable, so replays are safe
- foundation collect
- List the arsenal record files (macros, expectations, claims, triggers, gates, policies) that belong into an archive
- answer hash add <stage> <phase> <loader-gate> <name> <hash> <policy-template>
- One sentinel answer class in one batch: the hash is 64 hex digits and the template policy exists; then expectation <name> (string, label <loader-gate>, value <hash> = sha256(salt+word) exactly as the loader publishes loader.trust.<loader-gate>.answer), claim <name> over measure_answer, gate <name> with `-' and `.' as ’_’, policy <name> with the triggers of the template, and the binding to <phase> of <stage>. Assumes the sentinel of <loader-gate> is bound in the loader (sentinel_act, leafs question/salt/display) and the container of <phase> has measure_answer. Names stay neutral (fish-3, not fish-halt): whoever reads the database must not learn the class from the name
- answer add <stage> <phase> <loader-gate> <name> <policy-template>
- Add one sentinel answer class with the word read HIDDEN from the terminal when the batch runs – twice, both must match – and hashed with the stage’s salt (stage kenv add <stage> loader.trust.<loader-gate>.salt <hex>). The word reaches neither argv, trace, history, the batch text nor the generator: the running script leaves only sha256(salt+word) in a file of the database, the next line lifts it into `answer hash add' and removes the file. Write the word down BEFORE typing it (inventory, paper): the database keeps only the hash
- answer drop <stage> <phase> <name>
- Remove one answer class in reverse order: the binding in <phase>, the policy, the gate, the claim, the expectation
- answer file add <stage> <phase> <loader-gate> <file>
- Roll out every answer class of a table file, one line per class `<name> <policy-template> <word...>' (the word is the rest of the line, spaces kept; no word = the empty answer; `#' lines and blank lines skipped): the file is readable by its owner only and the salt exists; then per class line `answer line hash' (the word hashed when the batch runs, into .tmp/answer/<name>) and `answer hashed add' (the hash lifted into `answer hash add') – the words reach only the running script, never argv, trace, the batch text or the generator. The file IS the written place of the words: keep it on a tmpfs while typing, encrypt and print it, then remove it (rm -P)
- answer file drop <stage> <phase> <file>
- Remove every answer class named in a table file (first field of each class line; the words are not read): one `answer drop' per line
- answer catchall add <stage> <phase> <loader-gate> <name> <policy-template>
- The catch-all answer class: expectation <name> (byte, label <loader-gate>, 1), claim over measure_answer_matched, gate, policy with the triggers of the template – a when_fail template (react-miss: spool-fail, shutdown-fail) – and the binding. It fails whenever no word class matched: empty and wrong alike. Bind it AFTER the word classes (the phase runs in binding order; a class added later must be followed by `answer drop' + re-add of the catch-all), and give every class template the trigger for answer_matched_act
- answer show <stage>
- List the answer classes seen from the stage, in policy order: every policy whose gate’s first claim measures measure_answer (a word class) or measure_answer_matched (the catch-all) – name, the phase of <stage> that binds it or `unbound', its triggers. The words are not here and never were: the inventory has them
- stage earlboot mk <stage>
- Generate the earlboot rc.d script of the stage into its hooks/earlboot: the stage and its checkout exist, the bound measurements’ demands on the boot tree are met (stage require <stage> earlboot), then header, constants, state, tools, the catalog functions, the prologue, the phases SYSINIT and MOUNTED in that order, the footer, and the install. Assumes: the record LOADER_TRUST_WORD_SECRET is present when a word claim or handover is bound; mac_bootlock keeps loader.trust.* immutable; the script is installed with `stage earlboot install' and covered by mac_veriexec
- stage earlboot test <stage> <dump>
- Replay a captured boot through the CURRENT earlboot of the stage, without root and without side effects: the same rendering as `stage earlboot mk' but with mocks for state and tools – kenv/sysctl/kldstat answer from <dump>, shutdown and logger become notes on stderr, the state lives under hooks/ and is removed after – then the run: exit code, every file the run left (appraisal, book, spool, marks). Never run the INSTALLED earlboot by hand: book_act appends to the book. Capture a dump after a boot with: { kenv; sysctl kern.osrelease kern.ident kern.securelevel | sed `s/^/sysctl./'; kldstat -q -m mac_bootlock && echo `kldstat.mac_bootlock=1'; } > /tmp/ram/boot.kenv
- stage earlboot install <stage>
- Install the generated earlboot script as /etc/rc.d/earlboot (root:wheel 0500; the root dataset, so the earliest userland does not depend on a mounted /usr/local) and enable it in /etc/rc.conf.d/earlboot; refused while none was generated. Run as root
- stage elvbootd mk <stage>
- Generate the elvbootd hooks of the stage into its hooks/: the stage and its checkout exist, the bound measurements’ demands on the boot tree are met, at least one runtime phase is bound; then one self-contained hook.<phase>.sh per BOUND runtime phase (STARTUP rc.d start, PERIODIC periodic/security, RESUME rc.resume, MEDIA devd with $1 = the cdev, SHUTDOWN rc.d stop), each with header, constants, state, tools, the catalog functions (earlboot’s palette inherited), the prologue and its phase; plus the glue that plugs a hook into its mechanism: hooks/elvbootd (rc.d) when STARTUP or SHUTDOWN is bound, hooks/elvboot.devd.conf (devd) when MEDIA is bound
- stage elvbootd install <stage>
- Install the generated hooks under /usr/local/etc/elvboot/ (0500) and wire each into its mechanism with the generated glue: STARTUP as /usr/local/etc/rc.d/elvbootd (enabled), PERIODIC as /usr/local/etc/periodic/security/900.elvboot, RESUME as a line in /etc/rc.resume, MEDIA via /usr/local/etc/devd/elvboot.conf (devd restarted). Refused while nothing was generated. Run as root
- stage foundation make <stage>
- Generate foundation.c from the bound policies into the worktree: the stage and its checkout exist, the foundation/ directory is there, then the sections rendered in file order into foundation.c.new (header, macros, secrets, prerequisites, gates, phases, dispatch) and installed as one complete file
- stage foundation <stage>
- The foundation batch: check, make, report
- stage foundation report <stage>
- Summarize the foundation of a stage: the checkout, per enum phase its bound policies, the referenced gates and the emission target – notes for the caller
- stage report <stage> [<container>]
- The one reading view of a stage: every container, every phase, the bound policies, each policy’s gate with its claims (measurement, diagnose, publish, expectation) and its triggers in firing order (when -> action). Rewrites to `stage phase show <stage>'; with a container to `stage phase show <container> <stage>'
ENVIRONMENT
All configuration lives in ELEBAKE_* variables, layered as .env/local (override, written by setenv), .env/default (the installed profile) and the shipped templates. Interpreter pins ELEBAKE_INTERPRETER_<function> decide per function whether emitted shell is displayed or executed: arity-specific before arity-agnostic before the class default (ELEBAKE_TERMINAL_INTERPRETER, default cat). Every variable answers elebake help env <VAR>.
- ELEBAKE_ARCHIVE_ATTEST_KEY
- The openpgp record that signs what this database exports – and the signer a receiver expects
- ELEBAKE_CONTAINERS
- The containers a stage binds policies into, in this order: loader (the EFI loader, phases PHASE_BOOT/LOADER/KERNEL, emitted as foundation.c), earlboot (rc.d at mountcritlocal, SYSINIT/MOUNTED, sh), elvbootd (runtime hooks, STARTUP/PERIODIC/RESUME/MEDIA, sh). Each name is a catalog directory under stand/efi/loader/local of the checkout (the loader in the headers there, the others as <name>/{policy,measure,action}.sh with a PHASES= line). A fourth name here is a fourth container: its own catalog directory and phases, no code change in elebake (JB 08.09.: extensible, an intermediate layer must be possible)
- ELEBAKE_EDITOR
- The editor stage edit opens on a file of the boot tree (stdin from /dev/tty, so it works under ... \| sh); edit = FreeBSD base ee(1). No fallback to the login environment: an unset pin is a refusal
- ELEBAKE_FREEBSD_PREREQUISITES
- Curated tool list freebsd prerequisites probes at generation time
- ELEBAKE_FREEBSD_SRC
- Path to the FreeBSD source repository the stages build from
- ELEBAKE_INCLUDES
- Modules preloaded at startup (engine.sh is always loaded first)
- ELEBAKE_INTERPRETER_catalog_doc
- Interpreter for catalog doc – runs template/catalog-doc.awk over the catalog file and prints the entry with its description
- ELEBAKE_INTERPRETER_catalog_title
- Interpreter for catalog title – the heading of one catalog section (display)
- ELEBAKE_INTERPRETER_claim_dump
- Interpreter for the dump block claim dump – dump TEXT
- ELEBAKE_INTERPRETER_dump_env_epilogue
- Interpreter for dump env epilogue – dump TEXT
- ELEBAKE_INTERPRETER_dump_env_prologue
- Interpreter for dump env prologue – dump TEXT
- ELEBAKE_INTERPRETER_environment_cache_off
- Interpreter for environment cache off – removes the cache file
- ELEBAKE_INTERPRETER_environment_cache_on
- Interpreter for environment cache on – writes the cache at generation time, reports
- ELEBAKE_INTERPRETER_environment_cache_status
- Interpreter for environment cache status – one report line
- ELEBAKE_INTERPRETER_expectation_dump
- Interpreter for the dump block expectation dump – dump TEXT
- ELEBAKE_INTERPRETER_gate_dump
- Interpreter for the dump block gate dump – dump TEXT
- ELEBAKE_INTERPRETER_help_manual_commands
- Interpreter for help manual commands – one part of the manual as TEXT
- ELEBAKE_INTERPRETER_help_manual_environment
- Interpreter for help manual environment – one part of the manual as TEXT
- ELEBAKE_INTERPRETER_help_manual_title
- Interpreter for help manual title – one part of the manual as TEXT
- ELEBAKE_INTERPRETER_macro_dump
- Interpreter for the dump block macro dump – dump TEXT
- ELEBAKE_INTERPRETER_policy_dump
- Interpreter for the dump block policy dump – dump TEXT
- ELEBAKE_INTERPRETER_provenance_collect
- Interpreter for provenance collect – receipt files for an archive
- ELEBAKE_INTERPRETER_provenance_dump
- Interpreter for provenance dump – dump TEXT
- ELEBAKE_INTERPRETER_provenance_file
- Interpreter for provenance file – files the receipt of an import
- ELEBAKE_INTERPRETER_provenance_list
- Interpreter for provenance list – the lineage, readable
- ELEBAKE_INTERPRETER_provenance_serial
- Interpreter for provenance serial – advances export/serial
- ELEBAKE_INTERPRETER_stage_adopt
- Interpreter for the batch stage adopt – KEEP-GOING wrapper
- ELEBAKE_INTERPRETER_stage_container_prepare
- Interpreter for stage container prepare – the hooks/ directory
- ELEBAKE_INTERPRETER_stage_dump_add
- Interpreter for the dump block stage dump add – dump TEXT
- ELEBAKE_INTERPRETER_stage_dump_baselines
- Interpreter for the dump block stage dump baselines – dump TEXT
- ELEBAKE_INTERPRETER_stage_dump_checkout
- Interpreter for the dump block stage dump checkout – dump TEXT
- ELEBAKE_INTERPRETER_stage_dump_disks
- Interpreter for the dump block stage dump disks – dump TEXT
- ELEBAKE_INTERPRETER_stage_dump_filter
- Interpreter for the dump block stage dump filter – dump TEXT
- ELEBAKE_INTERPRETER_stage_dump_hooks
- Interpreter for the dump block stage dump hooks – dump TEXT (a note:
- ELEBAKE_INTERPRETER_stage_dump_kenv
- Interpreter for the dump block stage dump kenv – dump TEXT
- ELEBAKE_INTERPRETER_stage_dump_keys
- Interpreter for the dump block stage dump keys – dump TEXT
- ELEBAKE_INTERPRETER_stage_dump_media
- Interpreter for the dump block stage dump media – dump TEXT
- ELEBAKE_INTERPRETER_stage_dump_phases
- Interpreter for the dump block stage dump phases – dump TEXT
- ELEBAKE_INTERPRETER_stage_dump_prereqs
- Interpreter for the dump block stage dump prereqs – dump TEXT
- ELEBAKE_INTERPRETER_stage_dump_work
- Interpreter for the dump block stage dump work – dump TEXT
- ELEBAKE_INTERPRETER_stage_elvbootd_glue_devd
- Interpreter for stage elvbootd glue devd – the devd configuration as TEXT
- ELEBAKE_INTERPRETER_stage_elvbootd_glue_rcd
- Interpreter for stage elvbootd glue rcd – the rc.d script as TEXT
- ELEBAKE_INTERPRETER_stage_status_attestkey
- Interpreter for stage status attestkey – one derived-state line
- ELEBAKE_INTERPRETER_stage_status_filter
- Interpreter for stage status filter – one derived-state line
- ELEBAKE_INTERPRETER_stage_status_marker
- Interpreter for stage status marker – one derived-state line
- ELEBAKE_INTERPRETER_stage_status_media
- Interpreter for stage status media – one derived-state line
- ELEBAKE_INTERPRETER_stage_status_populated
- Interpreter for stage status populated – one derived-state line
- ELEBAKE_INTERPRETER_stage_status_signed
- Interpreter for stage status signed – one derived-state line
- ELEBAKE_INTERPRETER_stage_status_signkey
- Interpreter for stage status signkey – one derived-state line
- ELEBAKE_INTERPRETER_stage_status_sitemk
- Interpreter for stage status sitemk – one derived-state line
- ELEBAKE_INTERPRETER_stage_tree_sync
- Interpreter for the batch stage tree sync – KEEP-GOING wrapper
- ELEBAKE_INTERPRETER_stage_tree_work
- Interpreter for the batch stage tree work – fail-fast core of tree sync
- ELEBAKE_INTERPRETER_trigger_dump
- Interpreter for the dump block trigger dump – dump TEXT
- ELEBAKE_MAKEARGS
- Extra make(1) arguments appended to every stand/ component build (stage
- ELEBAKE_PAGER
- The pager last \<n\> \| sh opens on a trace (stdin from /dev/tty, so it works under ... \| sh). No fallback to the login environment: an unset pin is a refusal
- ELEBAKE_STATE_DB
- The state directory of the generated earlboot/elvbootd scripts on the target (ELV_STATE in the scripts: appraisal, book, heartbeat, spool, marks, smart counters); created 0700 root by the install steps
FILES
- ~/.elebake/db
- the active database (symlink)
- ~/.elebake/<name>/.env/{local,default}/
- the layered variable store: overrides (setenv) over the installed profile over the shipped templates
- ~/.elebake/<name>/.log/YYYY-MM-DD/
- per-invocation traces – the debugging ground truth (elebake last)
- ~/.elebake/<name>/stage/<stage>/
- a stage record: boot/ (the built tree), backup/<medium>/<label>/ (backup records), media/, marker/
- ~/.elebake/<name>/provenance/
- receipts of every admitted import (the lineage)
- ~/.elebake/worktree/
- git worktrees of the FreeBSD source; build trees live outside the database
- ~/.elebake/bundle/, ~/.elebake/incoming/
- where an exported bundle is handed to the user, and import’s scratch area
EXAMPLES
Create a database, connect the sources, register keys, build and publish a stage (every command that changes the database acts by itself; the pins decide what runs as root):
-
elebake bootstrap current minimal elebake setenv ELEBAKE_FREEBSD_SRC ~/git/freebsd-src elebake pkcs11 add db 'pkcs11:token=...;object=db' /root/sb/db.crt elebake openpgp add manifest 4E1F0A2B7C9D8E6F5A4B3C2D1E0F9A8B7C6D5E4F /root/sb/.gnupg elebake stage add smoke1 elebake stage sign key smoke1 pkcs11 db elebake stage attest key smoke1 openpgp manifest elebake stage checkout smoke1 platform-trust-gates-15.1^0 elebake stage make smoke1 elebake stage push smoke1 b
Take the database elsewhere – one signed pair, pinned on arrival:
-
elebake setenv ELEBAKE_ARCHIVE_ATTEST_KEY manifest elebake export full ~/git/config/dump.sh ~/.elebake/bundle/a1b2c3d.tar.gz # on the receiving machine elebake openpgp add manifest 4E1F0A2B7C9D8E6F5A4B3C2D1E0F9A8B7C6D5E4F elebake setenv ELEBAKE_ARCHIVE_ATTEST_KEY manifest elebake import ~/git/config/dump.sh ~/.elebake/bundle/a1b2c3d.tar.gz elebake provenance list
The rescue pair, and a rollback that keeps the evidence:
-
elebake export minimized ~/rescue/dump.sh ~/rescue/bundle.tar.gz elebake stage backup list smoke1 a elebake stage rollback smoke1 a known-good-p2
Inspect instead of act (any command): pin its act terminal to cat (elebake setintp stage_deploy_write cat), read the emitted shell, then pin it back. elebake is the wrapper make install places in $PREFIX/bin; from a checkout the same commands read ./elebake.sh ....
A trigger whose when is a composition, and one that runs two actions:
-
elebake trigger add unlock-measured 'and(when_fail,not(when_skipped))' unlock_act elebake trigger add silence-duress when_duress 'compose(taint_act,silence_act)' elebake trigger show unlock-measured # unlock-measured: FIRE(AND(when_fail, NOT(when_skipped)), unlock_act)
A value the loader itself is part of – the PCR bank, the loaded images – is expected from the conf, not the binary; learned after a trusted boot, carried by loaderconf mk, include, sign, push, no build:
-
elebake expectation add pcr-expected key PcrBank pcr.expected elebake claim add pcr measure_pcr - pcr.sha256 pcr-expected elebake stage require daily-v1 # loader.trust.kernellock.pcr.expected (claim pcr) MISSING -- stage kenv learn ... elebake stage kenv learn daily-v1 loader.trust.kernellock.pcr.expected loader.trust.kernellock.pcr.sha256
The platform sets, add semantics: import what elvbootd filed per boot, see what moves, take in what holds, render, build, learn:
-
elebake stage inventory import daily-v1 elebake stage inventory show daily-v1 efivars elebake stage inventory add daily-v1 efivars 8be4df61/BootOrder elebake stage inventory add daily-v1 acpi FACP/- elebake stage inventory list daily-v1 acpi elebake stage site mk daily-v1
SEE ALSO
uefisign(8), veriexec(8), gpg(1)
docs/QUICKSTART.md, docs/TUTORIAL.md, docs/ARCHITECTURE.md, docs/DESIGN_DUMP_ARCHIVE.md in the source tree; https://github.com/enk-ode/elvboot; the FreeBSD sources with the platform-trust-gates series: https://github.com/johannes-bruegmann/freebsd-src (branch platform-trust-gates-15.1).
AUTHORS
Dr. Johannes Brügmann
AUTHORS
Dr. Johannes Brügmann.