Skip to content

Repository files navigation

secure-boot-tutorial

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.

What you will build and observe

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.

Requirements

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.

Build the tutorial 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.

Run the first lab

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

Part 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 and run the curriculum

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"
done

The 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-tutorial

What Docker builds and checks

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

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.bz2 with SHA-256 ac7c04b8b7004923b00a4e5d6699c5df4d21233bac9fda690d8cfbc209fff2fd, validates archive paths and the expected root before extraction, checks the extracted Makefile version, and records tag object 32750a1d473aa4932de6303e62afc5306aee2b1f plus commit 88dc2788777babfd6322fa655df549a019aa1e69; 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.

Licensing, citation, and reports

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.

About

No description, website, or topics provided.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages