Build a small RISC-V verified-boot lab from source, then run seven QEMU experiments that make each trust decision visible. Docker prepares and validates the pinned U-Boot v2026.04 source, builds three firmware lanes, and generates the FIT, environment, device-tree, and legacy-image inputs inside a local image. The runner commands assert their console evidence for successful hand-offs and expected refusals.
The curriculum takes you from an unsigned baseline to a signed FIT policy and its useful differentials:
- an unsigned baseline that demonstrates integrity-only boot;
- a signed FIT and control DTB carrying the development public key;
- wrong-key, unsigned, keyless, and one-byte-tamper comparisons;
- a rollback ratchet, stale-image refusal, and valid-state replay;
- a read-only pflash storage experiment;
- a legacy parser and policy comparison.
By the end, you can distinguish integrity, authenticity, freshness, storage, and the root-of-trust assumptions that connect them.
Bring a recent Docker engine with BuildKit enabled and a shell. The image supplies QEMU, the
RISC-V cross-compiler, OpenSSL, mkimage, device-tree tools, and the other build tools. The runner
checks for QEMU >= 8.2.2; the observed versions recorded in VERSIONS are 8.2.2,
10.0.11, and 11.0.2.
BuildKit is required, not merely recommended: the Dockerfile uses heredoc RUN blocks, which only
BuildKit supports. Confirm it with docker buildx version, which should print a version. A Docker
CLI without the buildx plugin, as with Homebrew's docker formula and default Colima setups,
silently falls back to the legacy builder; there the build fails immediately with a clear
--mount option requires BuildKit message rather than several minutes into the build.
The supported execution environment is this repository's Linux/amd64 Docker image.
Run the build from the source root, the directory containing docker/, run/, and src/:
docker build --platform linux/amd64 -t secure-boot-tutorial -f docker/Dockerfile .The first build uses network access for the pinned Debian package snapshot and U-Boot archive. It compiles each of the three firmware lanes twice, then generates the demo outputs twice so the independent results can be compared. Allow several GB of temporary Docker storage; BuildKit cache and the final local image use additional space.
Read Part 0 - Concepts and Part 1 - Boot it, unverified before starting. Run the first lab with the recommended hardened container contract:
docker run --rm --platform linux/amd64 \
--read-only \
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=1g \
--network none \
--cap-drop ALL \
--security-opt no-new-privileges \
-e SBT_NO_SKIP=1 \
secure-boot-tutorial run/01-boot-unverified.shPart 1 gives you a positive hand-off to observe. Starting kernel shows the unsigned baseline
reached the payload boundary, and the 16 KiB synthetic payload then raises Illegal instruction.
The virtual board requests a reset; -no-reboot ends QEMU instead of restarting it, so that trace
is the expected result. The trap frame's TVAL: 000000005f544253 contains the first four payload
bytes (SBT_), fetched as the payload's first instruction: the hand-off reached the payload's own
bytes. The runner exits zero when these observations match.
Read each Part before its runner. A dash means that the Part is explanatory rather than a separate lab.
| Part | Read | Run |
|---|---|---|
| 0 | Concepts | - |
| 1 | Boot it, unverified | run/01-boot-unverified.sh |
| 2 | Keys and signing | run/02-keys-and-signing.sh |
| 3 | Enforce the signed configuration | run/03-enforce.sh |
| 4 | Tamper with a valid FIT | run/04-tamper.sh |
| 4.5 | Anti-rollback and availability lockout | run/45-rollback-brick.sh |
| 4.6 | Read-only first-stage storage | run/46-chain-and-immutable.sh |
| 4.7 | Failure taxonomy | run/47-failure-taxonomy.sh |
| 5 | From toy to real | - |
Each runner is self-contained. To run all seven in fresh hardened containers, define the helper and loop from the source root:
run_lab() {
docker run --rm --platform linux/amd64 \
--read-only \
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=1g \
--network none \
--cap-drop ALL \
--security-opt no-new-privileges \
-e SBT_NO_SKIP=1 \
secure-boot-tutorial "$@"
}
for lab in \
run/01-boot-unverified.sh \
run/02-keys-and-signing.sh \
run/03-enforce.sh \
run/04-tamper.sh \
run/45-rollback-brick.sh \
run/46-chain-and-immutable.sh \
run/47-failure-taxonomy.sh
do
run_lab "$lab"
doneThe legacy-format experiment is described with its evidence in Part 4.7. After all labs, remove the local image when you are finished:
docker image rm secure-boot-tutorialThe generated runtime set is local to the image. It contains these 13 files under /work/images:
control-nokey.dtb control.dtb env.img
kernel-legacy.uImage kernel-unsigned.itb kernel-v1-forgedver.itb
kernel-v1.itb kernel-v2.itb kernel-wrongkey.itb
mkimage u-boot-nosign u-boot-pflash.bin
u-boot-signed
src/configs/build-contract.tsv defines the expected output names and modes. Each staged generation
contains those 13 outputs plus its SHA256SUMS manifest: 14 entries in all. The builder compares
the exact five outputs from two independent three-lane firmware builds, compares two complete
14-entry demo generations byte- and mode-for-byte, promotes one generation, and validates the
promoted set with verify-hashes.sh before the image build succeeds.
Each runner verifies that manifest before its first boot and caches the successful per-process
verdict for later boots.
The manifest is a same-build integrity gate for the generated outputs; the acquisition and upstream metadata boundary is in Tutorial scope.
This is a hands-on teaching tutorial, not production deployment guidance. Its compact scope is:
- The Docker acquisition uses
https://ftp.denx.de/pub/u-boot/u-boot-2026.04.tar.bz2with SHA-256ac7c04b8b7004923b00a4e5d6699c5df4d21233bac9fda690d8cfbc209fff2fd, validates archive paths and the expected root before extraction, checks the extracted Makefile version, and records tag object32750a1d473aa4932de6303e62afc5306aee2b1fplus commit88dc2788777babfd6322fa655df549a019aa1e69; the build performs no independent upstream detached signature or signed-tag verification. - The two independent firmware builds and two complete demo generations compare the exact outputs described above within this pinned environment. The source distribution contains the tutorial inputs, and Docker outputs stay local.
- Flow A demonstrates one U-Boot proper → FIT verification link. Part 4.5, Part 4.6, and Part 4.7 keep the freshness, storage, and parser boundaries at the point where each experiment uses them.
- The public teaching credentials and their safety boundary are documented in
keys/README.md.
Project-owned code, scripts, and configuration are Apache-2.0 under LICENSE, except
the local U-Boot-context patch, which declares GPL-2.0-or-later. Tutorial prose is CC-BY-SA-4.0
under LICENSE-docs. U-Boot attribution and the applicable GPL notice are in
LICENSE-3RDPARTY/UPSTREAM.md and
LICENSE-3RDPARTY/gpl-2.0.txt. Cite the project with
CITATION.cff, and use SECURITY.md for privacy-safe reports.
Contribution sign-off guidance is in DCO.