Skip to content

feat(ext): image.verity - dm-verity hash tree and root hash for extension images - #220

Open
mobileoverlord wants to merge 4 commits into
mainfrom
jschneck/ext-verity
Open

feat(ext): image.verity - dm-verity hash tree and root hash for extension images#220
mobileoverlord wants to merge 4 commits into
mainfrom
jschneck/ext-verity

Conversation

@mobileoverlord

@mobileoverlord mobileoverlord commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Adds image.verity: true for extensions and for the rootfs: avocado ext image / the rootfs build produce a dm-verity hash tree; the runtime manifest carries the extension root hashes; the boot FIT carries the rootfs root hash. Consumer sides: avocado-control #22 (extensions), meta-avocado #316 (kernel/U-Boot capability, hash partitions, env).

Scope: provisioning. avocado provision carries the hash trees (whole var-staging tree + hash partition). avocado deploy and avocado connect upload do not publish <image_id>.verity yet and therefore refuse a runtime whose manifest carries root_hash; publishing the trees as targets is a follow-up.

Config

extensions:
  app:
    image:
      type: kab        # existing: raw | kab
      verity: true     # new
rootfs:
  image:
    verity: true       # same key, same meaning

Sibling of type because verity is orthogonal to the container format (avocadoctl applies --root-hash/--verity-data at its single dissect call for raw and KAB alike). A bool because the consumers fix every parameter; it can grow to bool | { … } later. Under image: so every image kind spells it the same way and stamps.rs already hashes it.

How

Extensions — after mkfs (before any KAB wrap): veritysetup format --salt=0…0<stem>.verity, <stem>.roothash. The runtime build copies both next to the image, writes root_hash into the manifest entry, stores the tree as <image_id>.verity (the path ManifestExtension::resolve_verity_path derives). Fixed zero salt: the manifest is the trust anchor, so a public salt buys reproducible root hashes.

Rootfs — same producer; the tree is written as <rootfs>.verity for the stone manifest's per-slot hash partition (systemd's veritysetup generator wants a block device, no hash-offset option), and AVOCADO_ROOTFS_ROOTHASH is recorded on the manifest's rootfs entry.

Boot FIT — the runtime build now assembles fitImage from the feed's fit-image.its template on machines that ship one: the template's ramdisk node is pointed at this runtime's initramfs (until now a FIT machine booted the feed's initramfs, so initramfs: config never reached the device), the rootfs root hash is added to every configuration node as avocado,roothash (read back by the U-Boot env after verification into roothash= on the cmdline), and the FIT is signed with the project key from AVOCADO_FIT_KEY_DIR (host dir with FIT.key/FIT.crt, bind-mounted read-only). The rebuild happens only when that key is set or AVOCADO_FIT_UNSIGNED=1 explicitly asks for an unsigned FIT (a verified-boot distro embeds its key in U-Boot, where an unsigned FIT would not boot); with neither, the feed's FIT is left untouched and a warning notes the project initramfs is not in it. Rootfs verity needs the rebuild, so it errors out in that case. No template → no-op. No verity field and no FIT env → nothing changes.

Verified on imx8mp-evk

Case Result
extension with verity: true mounted via loop*-verity, root hash = manifest, status: verified
extension image, one byte flipped, reboot dm-verity: data block 512 is corrupted, extension refused, system boots
extension .verity sidecar removed not merged (fail closed)
rootfs with verity: true, project-signed FIT root=/dev/mapper/root roothash=…, veritysetup status root → verified over rootfs-a + rootfs-a-hash, / erofs
rootfs, one byte flipped at 32 MiB, reboot dm-verity: 179:5: data block 8192 is corrupted, reads of that block return EIO

Regression tests: test_verity_step_emitted_only_when_requested (incl. KAB ordering), test_rootfs_verity_step_is_opt_in_and_exports_root_hash, fit_assembly_script_is_gated_and_handles_signing_both_ways, image_verity_is_strict_bool; generated scripts are sh -n checked.

Review follow-ups landed in 6488ee4 and ccf2bb3: strict bool config, fixed --uuid, verity flag carried explicitly through AVOCADO_EXT_PAIRS, no stale sidecars (image build, staging, ext clean, sweep), fail-closed veritysetup/mkimage, checked template rewrites, deploy/upload refusal, CHANGELOG.

…sion images

`image: { verity: true }` on an extension makes `avocado ext image` run
`veritysetup format` over the finished filesystem image, producing a hash-tree
sidecar (<stem>.verity) and the hex root hash (<stem>.roothash) next to it.
The runtime build carries both alongside the image and writes the root hash
into the manifest entry as `root_hash`, with the sidecar landing as
<image_id>.verity - exactly what avocadoctl's ManifestExtension::
resolve_verity_path and VeritySpec (avocado-control #22) consume through the
single systemd-dissect call site, for raw and KAB alike.

Shape: a sibling of image.type, not a type value - verity is orthogonal to the
container format (the KAB layer is the same bytes, so one tree serves both).
A bool for now because avocadoctl's contract fixes every parameter (sha256,
manifest-carried hash); it can grow to `bool | { sign: .., hash: .. }` without
breaking `verity: true`. Lives under image: so rootfs/initramfs later use the
same key, and stamps.rs already folds image: into the build hash.

Salt is a fixed zero: the manifest, not the sidecar, is the trust anchor, so a
public salt costs nothing and buys reproducible root hashes for identical
image bytes. Inert for every existing config (field absent -> no step, no
sidecars, manifest unchanged).
Copilot AI lite review requested due to automatic review settings August 26, 2026 17:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds producer-side support in avocado ext image and runtime manifest/build tooling to generate and propagate dm-verity artifacts (hash tree + manifest-carried root hash) for extension images, controlled via a new extensions.<name>.image.verity: true config flag.

Changes:

  • Introduces image.verity config parsing for extensions.
  • Ext image build script can optionally run veritysetup format to emit *.verity and *.roothash sidecars.
  • Runtime build copies verity sidecars and injects root_hash into the generated extension manifest entries (when sidecars are present), with a unit test covering script emission.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/utils/config.rs Adds get_ext_image_verity() to read extensions.*.image.verity (default false).
src/commands/ext/image.rs Threads verity into the image build script and conditionally emits a veritysetup format step; updates tests for the new argument.
src/commands/runtime/build.rs Copies *.verity/*.roothash sidecars into runtime staging and records root_hash + sidecar path in the manifest generation logic.
Suppressed comments (2)

src/commands/runtime/build.rs:1459

  • Same stale-sidecar risk as above for versioned external extensions: the destination .verity/.roothash files can linger across rebuilds and then be incorrectly included in the manifest. Remove the destination sidecars before the conditional copy loop.
    for sc in verity roothash; do
        [ -f "$AVOCADO_PREFIX/output/extensions/{versioned_name}.$sc" ] && cp -f "$AVOCADO_PREFIX/output/extensions/{versioned_name}.$sc" "$RUNTIME_EXT_DIR/{versioned_name}.$sc"
    done

src/commands/runtime/build.rs:1475

  • Same stale-sidecar risk as above for the wildcard .raw extension copy: if $RUNTIME_EXT_DIR/${EXT_BASENAME%.raw}.verity / .roothash already exist, they’ll remain when the source sidecars are absent and can be incorrectly consumed later. Remove destination sidecars before conditionally copying.
    for sc in verity roothash; do
        [ -f "${{EXT_FILE%.raw}}.$sc" ] && cp -f "${{EXT_FILE%.raw}}.$sc" "$RUNTIME_EXT_DIR/${{EXT_BASENAME%.raw}}.$sc"
    done

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/commands/ext/image.rs
Comment thread src/commands/runtime/build.rs Outdated
Comment thread src/commands/runtime/build.rs
Comment thread src/commands/runtime/build.rs Outdated
Comment thread src/commands/ext/image.rs Outdated
Comment thread src/utils/config.rs Outdated
Comment thread src/commands/ext/image.rs
Comment thread src/utils/config.rs Outdated
…in the runtime build

`rootfs: { image: { verity: true } }` makes the rootfs build run `veritysetup
format` over the finished image: `<image>.verity` (hash tree, consumed by the
stone manifest as a per-slot hash partition image) and `<image>.roothash`,
exported as AVOCADO_ROOTFS_ROOTHASH and recorded as `root_hash` on the
manifest's rootfs entry.

The runtime build now assembles `fitImage` itself on machines whose feed ships
a `fit-image.its` template: the template's ramdisk node is pointed at this
runtime's initramfs (until now a FIT machine booted the feed's initramfs, so
`initramfs:` config never reached the device), the rootfs root hash is added to
every configuration node as `avocado,roothash` so the U-Boot env can read it
back after verification and put it on the kernel cmdline, and the result is
signed with the project's key when AVOCADO_FIT_KEY_DIR (host dir holding
FIT.key/FIT.crt, bind-mounted read-only) is set - otherwise the signature
nodes are stripped and the FIT is unsigned. No template: no-op.
Comment thread src/commands/runtime/build.rs Outdated
Comment thread src/commands/rootfs/image.rs Outdated
Comment thread src/commands/runtime/build.rs Outdated
Comment thread src/commands/runtime/build.rs Outdated
…ble tree, no stale sidecars, guarded publish paths

- image.verity must be a boolean; a present non-bool value is an error rather
  than silently reading as off (config test added).
- veritysetup format gets a fixed --uuid alongside the fixed salt, so the tree
  itself is reproducible, not only the root hash.
- The runtime manifest step takes the verity flag from config through
  AVOCADO_EXT_PAIRS (name:version:type:verity|plain) instead of inferring it
  from sidecar presence: with the flag set both sidecars are required, without
  it they are ignored.
- Stale sidecars cannot survive: the image build removes them when verity is
  off, the staging copies remove the destination before the conditional copy,
  ext clean removes them, and the stale-image sweep covers <image_id>.verity.
- avocado deploy and avocado connect upload refuse a runtime whose manifest
  carries root_hash: neither publishes the hash trees yet, and a device given
  a root hash without its tree refuses the extension. Provisioning carries
  everything.
- KAB ordering test: the verity step precedes the KAB wrap.
- CHANGELOG entry under Unreleased / Added.
…build an unsigned FIT by default

The rootfs verity block and the FIT assembly run without set -e in the runtime
build, so: veritysetup failure and an empty root hash are fatal; mkimage
failure and a missing fitImage are fatal; both sed rewrites of the template
(initramfs path, avocado,roothash) are verified to have matched, and the no-op
ramdisk substitution is gone.

The FIT is rebuilt only when it can be signed (AVOCADO_FIT_KEY_DIR) or an
unsigned FIT is explicitly requested (AVOCADO_FIT_UNSIGNED=1): a distro built
with verified-boot embeds its key in U-Boot, where an unsigned FIT would not
boot. With neither, the feed's FIT is left in place (with a warning that this
runtime's initramfs is not in it), and rootfs verity - which needs the rebuild -
is an error rather than a silently unverified boot. Tests sh -n the generated
scripts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants