From 169bddbc74331be0f87beeb1c6572684bd300675 Mon Sep 17 00:00:00 2001 From: Javier Tia Date: Mon, 10 Aug 2026 16:40:07 -0600 Subject: [PATCH 01/10] field-notes/hardened-boot-imx93: correct the verification claims The Coverage table said every control was Verified on i.MX93 FRDM hardware. None of it was. The implementing PRs (meta-avocado #232, #236) are still open, the hardware validation task has never been run, and Avocado could not boot on this board at all on the BSP those PRs target until the meta-imx pin moved - the 6.6.36 SPL stalls before Normal Boot on Rev1.2 silicon, from any medium. The page asserted a finished hardware pass for a board that could not run the OS when it was written. The proxy column was wrong in the other direction. It named five harness assertions; three exist and only on the #232 branch, so they gate nothing on scarthgap, and the two for IMA and module signing do not exist anywhere. Both columns now say what actually holds, and the two proxy results the original omitted are stated as the open questions they are: TPM2 unseal fails on reboot and falls back to Argon2, and dm-verity is not active on that target. The unseal failure is the one that matters, because it is plausibly a QEMU artifact and equally plausibly the design sealing to a PCR that will not be stable - only the board settles it. The design content is unchanged and still correct; it was the verification status that was false. Verify-it and Reproduce-it are reframed from results into the checks the hardware pass owes, and the first-boot wedge found on the proxy is recorded, since it strands a device in emergency mode permanently and re-provisioning does not clear it. Signed-off-by: Javier Tia --- .../2026-06-24-hardened-boot-imx93.mdx | 50 +++++++++++++++---- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx index c7f53897..f659e61c 100644 --- a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx +++ b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx @@ -8,7 +8,7 @@ category: 'Security & Compliance' featured: false # Featured selection is Kristen/Bill's call image: /img/field-notes/2026-06-24-hardened-boot-imx93-thumb.png image_source: local:hardened-boot-imx93.jpg -tested_against: 'Avocado 0.41.2, avocado-imx93-frdm (AHAB Secure Boot + OP-TEE fTPM), avocado-qemuarm64 (fTPM via QEMU)' +tested_against: 'Avocado 0.41.2, avocado-qemuarm64 (fTPM via QEMU), on the meta-avocado #232/#236 branches. NOT yet validated on avocado-imx93-frdm hardware.' poster: jtia lift_for_blog: 'CRA Annex I and IEC 62443-4-2 both require demonstrable data-at-rest protection and integrity; this is the boot-chain half a compliance mapping builds on.' promote_to_track: 'Secure-boot / storage-encryption reference candidate once the i.MX93 hardware pass is fully signed off.' @@ -18,7 +18,7 @@ import TestStatus from '@site/src/components/TestStatus' import PullQuote from '@site/src/components/PullQuote' Date: Wed, 12 Aug 2026 16:28:07 -0600 Subject: [PATCH 02/10] field-notes/hardened-boot-imx93: correct the proxy TPM backend The note contradicted itself on which TPM the qemuarm64 proxy runs: the Coverage paragraph said QEMU swtpm while the open-questions bullet said OP-TEE fTPM. The layer settles which half is wrong. meta-avocado-qemu/conf/machine/avocado-qemuarm64.conf sets MACHINE_FEATURES += " optee-ftpm", and its own comment on the /var mapper change says the key is sealed to the OP-TEE fTPM rather than swtpm - swtpm is the qemux86-64 path. So the Coverage paragraph was the wrong half, and reconciling the bullet against it would have replaced a correct statement with an incorrect one. Correcting that cost the bullet its stated cause, which had blamed unstable PCR 7 measurement under the emulator. That was a guess, and optee-ftpm-init.bb already documents the mechanism: the QEMU virt machine has no RPMB, so OP-TEE cannot commit its secure-storage rollback counter, does not trust the fTPM's saved state, and the fTPM re-derives its seed on the next boot. Using the documented cause instead of the guess also sharpens the note's own thesis, because it means the proxy structurally cannot demonstrate reboot survival rather than merely having failed to. The admonition moves to the space-delimited title form the other 22 admonitions on the site use, so the one bracketed instance stops being a second convention nobody else follows. The Verify it lead-in becomes two sentences because "not output it has produced" parsed two ways as one. Prettier's italic and table-padding normalisation lands here too. Those lines are this branch's own additions, not a drive-by: scripts/checks.sh was already failing on them before this commit. Signed-off-by: Javier Tia --- .../2026-06-24-hardened-boot-imx93.mdx | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx index f659e61c..50d400a4 100644 --- a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx +++ b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx @@ -30,7 +30,7 @@ import PullQuote from '@site/src/components/PullQuote' {/* truncate */} -:::caution[Verification status: proxy only, the hardware pass is still open] +:::caution Verification status: proxy only, the hardware pass is still open This note went out reading as a finished hardware result. It is not one, and the table near the end of it has been corrected to match. @@ -42,7 +42,7 @@ What is not verified: any of it on the i.MX93 FRDM board. The implementation is in review ([meta-avocado #232](https://github.com/avocado-linux/meta-avocado/pull/232) and [#236](https://github.com/avocado-linux/meta-avocado/pull/236) are open), and two results from the proxy run are open questions rather than details: TPM2 unseal on -*reboot* fails there and falls back to the Argon2 slot, and dm-verity is not active on +_reboot_ fails there and falls back to the Argon2 slot, and dm-verity is not active on that target at all. The design below is worth reading and unchanged. Treat the command output as what you @@ -177,7 +177,7 @@ SRC_URI:append = " \ ## Verify it -These are the checks the hardware pass has to produce, not output it has produced. Run them on the board after boot: +These are the checks the hardware pass still has to run. The output below is what they should produce, not output a board has already produced. Run them on the board after boot: ```bash # /var is LUKS2 and mounted from the mapped device @@ -207,17 +207,17 @@ tpm2_pcrread sha256:10 ### Coverage -| Control | `avocado-qemuarm64` (proxy) | `avocado-imx93-frdm` (hardware) | -| :------------- | :------------------------------------------------- | :------------------------------ | -| LUKS2 `/var` | Verified on first boot | Not yet run | -| Phase-2 TPM2 | Enrollment verified; unseal on reboot **fails** | Not yet run | -| dm-verity | **Not active** on this target | Not yet run | -| IMA/EVM | No harness case exists | Not yet run | -| Module signing | No harness case exists | Not yet run | +| Control | `avocado-qemuarm64` (proxy) | `avocado-imx93-frdm` (hardware) | +| :------------- | :---------------------------------------------- | :------------------------------ | +| LUKS2 `/var` | Verified on first boot | Not yet run | +| Phase-2 TPM2 | Enrollment verified; unseal on reboot **fails** | Not yet run | +| dm-verity | **Not active** on this target | Not yet run | +| IMA/EVM | No harness case exists | Not yet run | +| Module signing | No harness case exists | Not yet run | Three harness cases back the proxy column (`luks-var`, `luks-var-tpm`, `dm-verity`), and they live on the #232 branch, so they are not gating anything on `scarthgap` yet. There are no IMA or module-signing cases at all; an earlier version of this table named two that do not exist. -The intent still holds: both paths run the same recipes, kernel fragments and initramfs logic, only the fTPM backend differs (QEMU swtpm against OP-TEE on the board), so a cheap proxy gate should guard the expensive hardware path. It is an intent rather than a seal until the cases merge and the board pass runs. +The intent still holds: both paths run the same recipes, kernel fragments and initramfs logic, and both seal to the same OP-TEE fTPM (`MACHINE_FEATURES += " optee-ftpm"` on `avocado-qemuarm64`, per `meta-avocado-qemu/conf/machine/avocado-qemuarm64.conf`) - what differs is the platform underneath it, QEMU `virt` against i.MX93 silicon. So a cheap proxy gate should guard the expensive hardware path. It is an intent rather than a seal until the cases merge and the board pass runs. ## Security properties @@ -232,7 +232,7 @@ The LUKS key is sealed to PCR 7 (UEFI Secure Boot state). A tampered bootloader ## Issues and open questions -- **Unseal fails on reboot on the proxy.** The two-phase enrollment lands both slots correctly, and then the very next boot cannot unseal from the TPM2 slot and falls back to Argon2. The likely cause is that PCR 7 is not stably measured under QEMU's OP-TEE fTPM, since there is no real UEFI Secure Boot state going into it - which would make this proxy-specific and fine on a board with AHAB. That is a hypothesis, and it is exactly what the hardware pass has to settle. If PCR 7 turns out to be unstable on the board too, sealing to it is the wrong design and not a bug to patch. +- **Unseal fails on reboot on the proxy.** The two-phase enrollment lands both slots correctly, and then the very next boot cannot unseal from the TPM2 slot and falls back to Argon2. The layer already documents why, and it is a property of the emulator rather than of the design: the QEMU `virt` machine has no RPMB, so OP-TEE cannot commit the rollback counter its secure storage depends on, will not trust the fTPM's saved state on the next boot, and the fTPM re-derives its seed. `meta-avocado-qemu/recipes-security/optee-ftpm-init/optee-ftpm-init.bb` states this and calls it a QEMU limitation, not a defect in the layer. What follows for this note is narrower than the symptom suggests but still unresolved: the proxy structurally cannot demonstrate reboot survival, so "the TPM2 slot reopens `/var` on every later boot" rests entirely on the hardware pass, and nothing here has shown it yet. - **An interrupted first boot wedges the device permanently.** `cryptsetup-var.sh` only runs `mkfs.btrfs` in its first-boot branch. A first boot that dies after `luksFormat` but before `mkfs.btrfs` leaves a LUKS2 header with no filesystem inside, so every later boot takes the open-existing branch, never creates the filesystem, and drops to emergency forever. Re-provisioning does not clear it, because `fwup -t complete` does not touch the `var` partition; recovery means zeroing the LUKS header by hand. The fix is either to detect "header present, no filesystem on the opened mapper" and re-`mkfs`, or to make format-plus-mkfs atomic behind a completion marker. - **`luksKillSlot` guard.** The `|| true` on slot 0 removal is intentional but means a failed kill leaves both slots active. A monitoring check after enrollment should confirm slot 0 is gone; the current implementation does not warn if the kill is skipped. - **IMA/EVM raises a labeling and update cost.** Every appraised file must carry a valid `security.ima` signature, so the build signs the rootfs and shipped mutable content, and every OS update re-signs and shifts the measured hashes. That makes the attestation server's known-good list an ongoing ops dependency. First boot also runs roughly 5-15% slower while IMA labels and measures, which is why IMA is gated on `ima` rather than always on. @@ -244,7 +244,7 @@ The LUKS key is sealed to PCR 7 (UEFI Secure Boot state). A tampered bootloader Three adversarial tests, stated as predictions the hardware pass should confirm or break. None has been run on a board yet, and each one failing would say something specific about which control is not doing its job: - Boot the FRDM board with a rootfs where one block has been flipped with `dd`. dm-verity should halt the boot before any userspace code runs. The proxy cannot answer this one at all, since dm-verity is not active there. -- Enroll phase-2 TPM2 keys on one board, then clone the eMMC to a second board with a different TPM. PCR 7 on the clone should not match what was sealed, so the clone should not decrypt `/var`. This is the test that matters most right now, because the proxy's unseal already fails on a plain reboot of the *same* machine. +- Enroll phase-2 TPM2 keys on one board, then clone the eMMC to a second board with a different TPM. PCR 7 on the clone should not match what was sealed, so the clone should not decrypt `/var`. This is the test that matters most right now, because the proxy's unseal already fails on a plain reboot of the _same_ machine. - Copy a stock binary into `/var/tmp` and run it. IMA appraisal should deny the exec for want of a valid `security.ima` signature, while the same binary in its signed rootfs location runs normally. The controls live in `meta-avocado` and `meta-avocado-nxp` in the [avocado-linux organization on GitHub](https://github.com/avocado-linux). It composes cleanly with `hardened-security` (runtime posture), and the boot-chain controls satisfy CRA Annex I Part I items (b), (d), and (f) that a compliance mapping can build on. From 477a0f94aa6eb286e0a0a00d27d7b52f7f1cac3e Mon Sep 17 00:00:00 2001 From: Javier Tia Date: Wed, 12 Aug 2026 18:08:37 -0600 Subject: [PATCH 03/10] field-notes/hardened-boot-imx93: record the board pass and what it did not cover The note has been carrying "not yet run" against every hardware row since the claims correction. The board pass ran on 2026-08-12, so the LUKS2 row can be filled in - but only that row, and the reason the rest stay open is worth more to a reader than the tick mark. Three defects had to be fixed before the board could unlock /var at all, and all three were invisible on the qemuarm64 proxy because that machine carries an override imx93 lacked: DM_CRYPT silently demoted to =m by kconfig, AVOCADO_VAR_PART_DEV unset so fstab pointed at the raw partition, and a key provider reading a secret no recipe creates. Each is recorded with what it looked like rather than just that it was fixed, because the pattern - proxy passes, board cannot - is the transferable part. Phase-2 TPM2 changes from "not yet run" to "not possible on this board yet", which is a different claim. There is no TPM on the FRDM-IMX93 to seal to: no discrete part on the board, and the i.MX9 route to a TPM 2.0 interface is a firmware TPM on OP-TEE that is currently built for qemuarm64 only. So the open question about the TPM2 slot reopening /var is now pinned precisely: the proxy cannot show it for want of RPMB, and the board cannot show it for want of a TPM. Half of it - that /var reopens at all from an existing container across a reboot - the board did show. The security-properties section needed the sharpest correction. It claims two independent factors; on this board today there is one, and it is weaker than "passphrase" implies. The key derives from the SoC UID, which binds the volume to one board but is not a secret - anyone who can read it from a running system can rederive the key. Device binding is a real property and worth claiming; confidentiality against an attacker who has had code execution on the device is not, and the note said otherwise. Adds a reproduce section, since none of this is reachable without knowing that the image ships root login disabled, that the board boots from eMMC by default so an SD image needs the switches moved, and that the default flash task rewrites var. Verified: npm run build succeeds, static files generated. Signed-off-by: Javier Tia --- .../2026-06-24-hardened-boot-imx93.mdx | 73 ++++++++++++++++--- 1 file changed, 63 insertions(+), 10 deletions(-) diff --git a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx index 50d400a4..d10f4541 100644 --- a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx +++ b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx @@ -8,7 +8,7 @@ category: 'Security & Compliance' featured: false # Featured selection is Kristen/Bill's call image: /img/field-notes/2026-06-24-hardened-boot-imx93-thumb.png image_source: local:hardened-boot-imx93.jpg -tested_against: 'Avocado 0.41.2, avocado-qemuarm64 (fTPM via QEMU), on the meta-avocado #232/#236 branches. NOT yet validated on avocado-imx93-frdm hardware.' +tested_against: 'Avocado 0.41.2, avocado-qemuarm64 (fTPM via QEMU). LUKS2 /var validated on avocado-imx93-frdm hardware 2026-08-12 (wrynose BSP, meta-avocado #271), first boot and reboot. Phase-2 TPM2, dm-verity, IMA/EVM and module signing NOT yet validated on that hardware.' poster: jtia lift_for_blog: 'CRA Annex I and IEC 62443-4-2 both require demonstrable data-at-rest protection and integrity; this is the boot-chain half a compliance mapping builds on.' promote_to_track: 'Secure-boot / storage-encryption reference candidate once the i.MX93 hardware pass is fully signed off.' @@ -175,9 +175,33 @@ SRC_URI:append = " \ " ``` +## Reproduce it + +`encrypted-var` is a kas feature, so composing it onto a board's machine YAML is the whole build step: + +```bash +bakar build \ + meta-avocado/kas/machine/imx93-frdm.yml:meta-avocado/kas/feature/encrypted-var.yml +``` + +The stock image gives you no way in, on console or over SSH: `avocado-users` ships `/etc/shadow` with `root:*:`, and a literal `*` means no password can ever match. Stack the dev-access feature when you need a shell to check any of this - dev boards only, it accepts root with an empty password: + +```bash +bakar build \ + meta-avocado/kas/machine/imx93-frdm.yml:meta-avocado/kas/feature/encrypted-var.yml:meta-avocado/kas/feature/ssh-dev.yml +``` + +Then write it to the card. `avocado-flash` selects the backend from the medium and refuses anything that is not an unmounted whole disk on a removable bus: + +```bash +meta-avocado/scripts/avocado-flash sd /dev/sdX +``` + +Two things that will otherwise cost you an afternoon. The FRDM-IMX93 boots from **eMMC by default** (`SW1[3:0] = 0010` per UM12181), so booting an SD image needs the switches at `0011` - leave them and the board quietly boots the old eMMC contents, which looks like a successful boot of the wrong thing. And the default `-t complete` rewrites the whole medium **including `var`**, which is what you want for a first-boot format and what you do not want on a device whose data must survive; `-t upgrade` writes only the inactive A/B slots and leaves `var` alone. + ## Verify it -These are the checks the hardware pass still has to run. The output below is what they should produce, not output a board has already produced. Run them on the board after boot: +Everything under LUKS2 below is output a board has produced. The TPM2, dm-verity, IMA/EVM and module-signing checks are what they should produce, not output anything has produced yet - see the coverage table. ```bash # /var is LUKS2 and mounted from the mapped device @@ -207,22 +231,51 @@ tpm2_pcrread sha256:10 ### Coverage -| Control | `avocado-qemuarm64` (proxy) | `avocado-imx93-frdm` (hardware) | -| :------------- | :---------------------------------------------- | :------------------------------ | -| LUKS2 `/var` | Verified on first boot | Not yet run | -| Phase-2 TPM2 | Enrollment verified; unseal on reboot **fails** | Not yet run | -| dm-verity | **Not active** on this target | Not yet run | -| IMA/EVM | No harness case exists | Not yet run | -| Module signing | No harness case exists | Not yet run | +| Control | `avocado-qemuarm64` (proxy) | `avocado-imx93-frdm` (hardware) | +| :------------- | :---------------------------------------------- | :------------------------------------------------- | +| LUKS2 `/var` | Verified on first boot | **Verified**, first boot and reboot (2026-08-12) | +| Phase-2 TPM2 | Enrollment verified; unseal on reboot **fails** | **Not possible on this board yet** - see below | +| dm-verity | **Not active** on this target | Not yet run | +| IMA/EVM | No harness case exists | Not yet run | +| Module signing | No harness case exists | Not yet run | Three harness cases back the proxy column (`luks-var`, `luks-var-tpm`, `dm-verity`), and they live on the #232 branch, so they are not gating anything on `scarthgap` yet. There are no IMA or module-signing cases at all; an earlier version of this table named two that do not exist. +### The hardware pass (2026-08-12) + +The board pass ran on an FRDM-IMX93 against the wrynose BSP, and it closed the LUKS2 row. Three defects had to be fixed first, and every one of them was invisible on the proxy because `avocado-qemuarm64` carries a machine override that `avocado-imx93-frdm` did not: + +- **`CONFIG_DM_CRYPT` was silently `=m`.** The fragment asks for `=y`, but kconfig caps a tristate at its weakest parent and the i.MX defconfig left both `BLK_DEV_DM` and `TRUSTED_KEYS` at `=m`. The initramfs ships `cryptsetup` with no module and nothing that loads one, so the unlock had no dm-crypt to call. The task check greps the fragment *text*, so it passed while the built kernel disagreed - verify against the produced `.config`, never the fragment. +- **`AVOCADO_VAR_PART_DEV` was unset.** Both qemu machines point `/var` at `/dev/mapper/var` when `encrypted-var` is on; imx93 fell through to the base-files raw-PARTUUID default, so the container would be created and opened in the initrd while fstab mounted `/var` from the raw partition underneath it. +- **The key provider read a secret that does not exist.** The shared `var-key.sh` reads `/var/private/var-key-secret` - on the volume being unlocked, not mounted when the provider runs in the initramfs, and created by nothing in the tree. The Argon2id path had therefore never once succeeded on this board. + +What the pass showed, on the board: + +```text +# cryptsetup status var +/dev/mapper/var is active and is in use. + type: LUKS2 cipher: aes-xts-plain64 keysize: 512 [bits] + device: /dev/mmcblk1p8 + +# findmnt /var +/var /dev/mapper/var btrfs rw,relatime,ssd,space_cache=v2 + +# dmsetup targets | grep crypt +crypt v1.28.0 +``` + +First boot ran the whole path - open, no filesystem found, filesystem created at 29.01 GiB, `Finished LUKS2 /var unlock and first-boot format` - and the partition expanded 112M to 29G. A second boot then exercised the **unlock** path rather than first-boot format: `/var` reopened with the same btrfs UUID across a reboot confirmed by a changed `boot_id`. That is the reboot survival the proxy structurally cannot show, though only for the Argon2id slot. + +**Phase-2 TPM2 remains unshown, and not for want of running it.** There is no TPM on this board to seal to. The FRDM-IMX93 has no discrete TPM (confirmed against UM12181 and, for its i.MX95 siblings, the schematic BOMs), and the i.MX9 route to a TPM 2.0 interface is a firmware TPM on OP-TEE, which `optee-ftpm-init` currently builds for `avocado-qemuarm64` only. Nothing seals to PCR 7 on an i.MX93 today. The board does have what that route needs - OP-TEE enabled, 32 GB eMMC so RPMB exists, and a `recovery` partition for the TEE store - so this is a porting task rather than a hardware limitation, and it needs no AHAB work. Worth knowing when reading NXP material: on i.MX, "TPM" in a datasheet or device tree usually means the **Timer/PWM Module** peripheral, not a trusted platform module. + The intent still holds: both paths run the same recipes, kernel fragments and initramfs logic, and both seal to the same OP-TEE fTPM (`MACHINE_FEATURES += " optee-ftpm"` on `avocado-qemuarm64`, per `meta-avocado-qemu/conf/machine/avocado-qemuarm64.conf`) - what differs is the platform underneath it, QEMU `virt` against i.MX93 silicon. So a cheap proxy gate should guard the expensive hardware path. It is an intent rather than a seal until the cases merge and the board pass runs. ## Security properties Pulling the eMMC off the board gives an attacker AES-256-XTS ciphertext. Without the Argon2id recovery passphrase (device-bound, not in the image) or the TPM2 (physically in the device), they cannot decrypt it, and the two factors are independent. +**On the i.MX93 today that is one factor, not two, and the remaining one is weaker than "passphrase" suggests.** With no TPM to seal to, the Argon2id slot is not a recovery baseline behind a sealed primary - it is the only slot. And the key it derives from is the SoC's 128-bit OCOTP UID, read from `/sys/devices/soc0/serial_number`: that binds the volume to *this* board, so a cloned eMMC will not open elsewhere, but it is not a secret. Anyone who can read the UID from a running system can rederive the key. The design's stronger secret-plus-hardware-id scheme has no provisioning path in the tree yet, which is why the UID provider exists at all. So the honest claim for this board right now is device binding, not confidentiality against an attacker who has already had code execution on it. Sealing to an OP-TEE fTPM restores the two-factor property and is the next step. + The storage chip alone is worthless: the key is sealed to the Secure Boot state, and the TPM is not on the chip. @@ -232,7 +285,7 @@ The LUKS key is sealed to PCR 7 (UEFI Secure Boot state). A tampered bootloader ## Issues and open questions -- **Unseal fails on reboot on the proxy.** The two-phase enrollment lands both slots correctly, and then the very next boot cannot unseal from the TPM2 slot and falls back to Argon2. The layer already documents why, and it is a property of the emulator rather than of the design: the QEMU `virt` machine has no RPMB, so OP-TEE cannot commit the rollback counter its secure storage depends on, will not trust the fTPM's saved state on the next boot, and the fTPM re-derives its seed. `meta-avocado-qemu/recipes-security/optee-ftpm-init/optee-ftpm-init.bb` states this and calls it a QEMU limitation, not a defect in the layer. What follows for this note is narrower than the symptom suggests but still unresolved: the proxy structurally cannot demonstrate reboot survival, so "the TPM2 slot reopens `/var` on every later boot" rests entirely on the hardware pass, and nothing here has shown it yet. +- **Unseal fails on reboot on the proxy.** The two-phase enrollment lands both slots correctly, and then the very next boot cannot unseal from the TPM2 slot and falls back to Argon2. The layer already documents why, and it is a property of the emulator rather than of the design: the QEMU `virt` machine has no RPMB, so OP-TEE cannot commit the rollback counter its secure storage depends on, will not trust the fTPM's saved state on the next boot, and the fTPM re-derives its seed. `meta-avocado-qemu/recipes-security/optee-ftpm-init/optee-ftpm-init.bb` states this and calls it a QEMU limitation, not a defect in the layer. What follows for this note is narrower than the symptom suggests but still unresolved: the proxy structurally cannot demonstrate reboot survival, so "the TPM2 slot reopens `/var` on every later boot" rests entirely on the hardware pass. **The 2026-08-12 board pass answered half of it.** `/var` did reopen across a reboot on real hardware, from an existing LUKS2 container, with the same filesystem UUID either side - so the unlock-on-later-boot path is shown. What it was shown for is the Argon2id slot, because the board has no TPM to seal to and the fTPM route is not yet ported to it, so the specific claim about the *TPM2* slot is still unevidenced on any platform: the proxy cannot show it for want of RPMB, and the board cannot show it for want of a TPM. - **An interrupted first boot wedges the device permanently.** `cryptsetup-var.sh` only runs `mkfs.btrfs` in its first-boot branch. A first boot that dies after `luksFormat` but before `mkfs.btrfs` leaves a LUKS2 header with no filesystem inside, so every later boot takes the open-existing branch, never creates the filesystem, and drops to emergency forever. Re-provisioning does not clear it, because `fwup -t complete` does not touch the `var` partition; recovery means zeroing the LUKS header by hand. The fix is either to detect "header present, no filesystem on the opened mapper" and re-`mkfs`, or to make format-plus-mkfs atomic behind a completion marker. - **`luksKillSlot` guard.** The `|| true` on slot 0 removal is intentional but means a failed kill leaves both slots active. A monitoring check after enrollment should confirm slot 0 is gone; the current implementation does not warn if the kill is skipped. - **IMA/EVM raises a labeling and update cost.** Every appraised file must carry a valid `security.ima` signature, so the build signs the rootfs and shipped mutable content, and every OS update re-signs and shifts the measured hashes. That makes the attestation server's known-good list an ongoing ops dependency. First boot also runs roughly 5-15% slower while IMA labels and measures, which is why IMA is gated on `ima` rather than always on. From 17815f0be67167ce1c7f78551ad8485464c29f16 Mon Sep 17 00:00:00 2001 From: Javier Tia Date: Wed, 12 Aug 2026 18:26:57 -0600 Subject: [PATCH 04/10] field-notes/hardened-boot-imx93: stop documenting IMA wiring that was never written The build-flow snippet showed a fourth SRC_URI gate pulling file://ima.cfg on the ima DISTRO_FEATURE, and the source-layout tree listed an ima-policy recipe with a policy file and a public cert. None of it exists: ima.cfg is absent from scarthgap and from both secure-boot branches, no recipe or kas feature references ima, and the board confirms it - no /sys/kernel/security/ima, no ima_policy on the cmdline. A reader following the snippet greps for a file nobody ever wrote and concludes the docs are stale rather than the control is missing. The earlier pass on this note corrected the coverage table while leaving the snippet asserting the wiring exists, so the two halves disagreed. Replace the snippet with the three gates the bbappend actually carries - including the encrypted-var one it omitted - and say plainly that the IMA gate is design. Correcting the snippet meant reading the surrounding block against the tree, which surfaced the same defect in the fragment paths and packagegroup names; those are fixed here rather than left knowingly wrong in a block being rewritten. Signed-off-by: Javier Tia --- .../2026-06-24-hardened-boot-imx93.mdx | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx index d10f4541..53f0a03c 100644 --- a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx +++ b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx @@ -8,7 +8,7 @@ category: 'Security & Compliance' featured: false # Featured selection is Kristen/Bill's call image: /img/field-notes/2026-06-24-hardened-boot-imx93-thumb.png image_source: local:hardened-boot-imx93.jpg -tested_against: 'Avocado 0.41.2, avocado-qemuarm64 (fTPM via QEMU). LUKS2 /var validated on avocado-imx93-frdm hardware 2026-08-12 (wrynose BSP, meta-avocado #271), first boot and reboot. Phase-2 TPM2, dm-verity, IMA/EVM and module signing NOT yet validated on that hardware.' +tested_against: 'Avocado 0.41.2, avocado-qemuarm64 (fTPM via QEMU). LUKS2 /var validated on avocado-imx93-frdm hardware 2026-08-12 (wrynose BSP, meta-avocado #271), first boot and reboot. Phase-2 TPM2, dm-verity and module signing NOT yet validated on that hardware; IMA/EVM is design only and not implemented in the layer.' poster: jtia lift_for_blog: 'CRA Annex I and IEC 62443-4-2 both require demonstrable data-at-rest protection and integrity; this is the boot-chain half a compliance mapping builds on.' promote_to_track: 'Secure-boot / storage-encryption reference candidate once the i.MX93 hardware pass is fully signed off.' @@ -66,7 +66,7 @@ The threat is concrete in regulated industries. CRA Annex I Part I and IEC 62443 - LUKS2 `/var` formatted on first boot by the initramfs, with no pre-provisioned key material shipped in the image - Two-phase TPM2 enrollment: phase 1 uses a software Argon2id passphrase as a recovery baseline; phase 2 seals a new key to TPM2 PCR 7 (Secure Boot state) via `systemd-cryptenroll` and kills the Argon2id slot only after a `--token-only` unseal confirms the TPM path works - dm-verity rootfs integrity gated on the `secureboot` DISTRO_FEATURE, with every rootfs block cryptographically verified on read -- IMA/EVM runtime appraisal gated on the `ima` DISTRO_FEATURE: the kernel refuses to open or execute a mutable file whose IMA signature does not verify, with EVM sealing the security xattrs and IMA measurements logged to PCR 10 +- IMA/EVM runtime appraisal - designed, not built. The intent is that the kernel refuses to open or execute a mutable file whose IMA signature does not verify, with EVM sealing the security xattrs and measurements logged to PCR 10. No kernel fragment, policy recipe, or `ima` gate exists in the layer today - Build-time UEFI Secure Boot key generation (PK/KEK/db/dbx) via an idempotent recipe that regenerates keys only when absent and never ships private `.key` files to the target - Kernel module signing with `MODULE_SIG_FORCE` gated on the `security` DISTRO_FEATURE - Device-side TPM2 userspace (`tpm2-tss`, `tpm2-tools`, `tpm2-openssl`) with `tpm2-abrmd` disabled and excluded from the initramfs entirely (the unseal path is D-Bus-free) @@ -81,7 +81,7 @@ The threat is concrete in regulated industries. CRA Annex I Part I and IEC 62443 | Key derivation (phase 1) | Argon2id via OpenSSL 3 KDF | Software fallback; combines a provisioned per-device secret with a hardware serial so the key is device-bound without a TPM | | Key sealing (phase 2) | TPM2 PCR 7 via `systemd-cryptenroll` | PCR 7 captures UEFI Secure Boot state; a tampered boot chain makes unseal fail | | Rootfs integrity | dm-verity over EROFS | Hash tree computed at build time; verified on every block read; gated on `secureboot` | -| Mutable-file integrity | IMA appraisal + EVM | Appraises a per-file signature before open/exec; EVM protects `security.ima`; gated on `ima` | +| Mutable-file integrity | IMA appraisal + EVM | Designed only - appraises a per-file signature before open/exec and EVM protects `security.ima`, but nothing implements it in the layer yet | | Secure Boot keys | `gen-sbkeys.sh` (build-time, idempotent) | Generates PK/KEK/db/dbx; public certs to `/usr/share/secureboot-keys`; private `.key` never in `FILES` | | Module signing | `MODULE_SIG_FORCE` kernel config | Gated on `security`; prevents unsigned kernel modules from loading | | TPM2 userspace | tpm2-tss + tpm2-tools + tpm2-openssl | `tpm2-tss` is the ESAPI `systemd-cryptenroll` needs; `tpm2-abrmd` is delivered but auto-start disabled, so the initramfs unseal path is D-Bus-free | @@ -103,22 +103,26 @@ meta-avocado/ sb-keys.bb # do_install[nostamp]; BB_BASEHASH_IGNORE_VARS key-store.bb # installs public certs to /usr/share/secureboot-keys files/gen-sbkeys.sh # PK/KEK/db/dbx generation script - recipes-security/ima-policy/ - ima-policy.bb # installs the appraisal policy + public IMA cert - files/ima-policy # appraise func=BPRM/FILE_MMAP/MODULE_CHECK - files/x509_ima.der # public IMA cert only; private key never in FILES + recipes-kernel/linux/files/ + dm-crypt.cfg # CONFIG_DM_CRYPT + AES + XTS + SHA256 + HW_RANDOM_TPM (arch-agnostic) + dm-verity.cfg # dm-verity rootfs integrity chain + modsign.cfg # MODULE_SIG_FORCE + +meta-avocado-tpm/ recipes-avocado/packagegroups/ - packagegroup-avocado-tpm2.bb # tpm2-tss + tpm2-abrmd + tpm2-tools + tpm2-openssl - packagegroup-avocado-initramfs-security.bb # tpm2-tools subset; excludes abrmd + packagegroup-avocado-tpm2.bb # tpm2-tss + tpm2-abrmd + tpm2-tools + tpm2-openssl + packagegroup-avocado-tpm2-initramfs.bb # tpm2-tools subset; excludes abrmd meta-avocado-nxp/ + recipes-core/cryptsetup-var/ + cryptsetup-var.bbappend # FILESEXTRAPATHS:prepend:avocado-imx93-frdm + files/var-key.sh # i.MX93 key provider; reads the SoC OCOTP UID recipes-kernel/linux/files/imx93-frdm/ - dm-crypt.cfg # CONFIG_DM_CRYPT + AES + XTS + SHA256 + HW_RANDOM_TPM + CAAM (i.MX93-specific) - dm-verity.cfg # dm-verity rootfs integrity chain - modsign.cfg # MODULE_SIG_FORCE - ima.cfg # CONFIG_IMA + IMA_APPRAISE + EVM + TRUSTED_KEYRING + dm-crypt.cfg # forces BLK_DEV_DM and TRUSTED_KEYS to y, adds the CAAM job rings ``` +There is no IMA recipe and no `ima.cfg` in the layer. The appraisal policy, the public cert recipe, and the kernel fragment described below are design, not code - see the coverage table. + ### The unlock script `cryptsetup-var.sh` takes the raw block device and calls `var-key.sh` for the passphrase. The contract is strict: emit exactly 64 raw bytes to stdout, exit non-zero on any failure. The key never touches the filesystem; it passes through a `mktemp` file cleaned on `EXIT`. @@ -168,13 +172,14 @@ do_install[nostamp] = "1" Kernel config fragments are wired via `SRC_URI:append`, each conditionally included by `DISTRO_FEATURES`, so a target only pays for the controls it opts into: ```bash -SRC_URI:append = " \ - ${@bb.utils.contains('DISTRO_FEATURES', 'secureboot', 'file://dm-verity.cfg', '', d)} \ - ${@bb.utils.contains('DISTRO_FEATURES', 'security', 'file://modsign.cfg', '', d)} \ - ${@bb.utils.contains('DISTRO_FEATURES', 'ima', 'file://ima.cfg', '', d)} \ -" +# meta-avocado/recipes-kernel/linux/linux-yocto_%.bbappend +SRC_URI:append = "${@bb.utils.contains('DISTRO_FEATURES', 'secureboot', ' file://dm-verity.cfg', '', d)}" +SRC_URI:append = "${@bb.utils.contains('DISTRO_FEATURES', 'security', ' file://modsign.cfg', '', d)}" +SRC_URI:append = "${@bb.utils.contains('DISTRO_FEATURES', 'encrypted-var', ' file://dm-crypt.cfg', '', d)}" ``` +Three gates, not four. An `ima` gate belongs in that list by design and is not in it: nothing in the layer defines `ima.cfg`, so putting `ima` in `DISTRO_FEATURES` today changes nothing about the built kernel. The i.MX93 gets a second, machine-scoped `dm-crypt.cfg` through `meta-avocado-nxp/recipes-kernel/linux/linux-imx_%.bbappend` because its defconfig ships `BLK_DEV_DM=m`, and kconfig caps a tristate at its weakest parent - `DM_CRYPT=y` silently demotes to `=m` unless `BLK_DEV_DM` and `TRUSTED_KEYS` are both forced to `y` first. + ## Reproduce it `encrypted-var` is a kas feature, so composing it onto a board's machine YAML is the whole build step: @@ -236,11 +241,13 @@ tpm2_pcrread sha256:10 | LUKS2 `/var` | Verified on first boot | **Verified**, first boot and reboot (2026-08-12) | | Phase-2 TPM2 | Enrollment verified; unseal on reboot **fails** | **Not possible on this board yet** - see below | | dm-verity | **Not active** on this target | Not yet run | -| IMA/EVM | No harness case exists | Not yet run | +| IMA/EVM | **Not implemented** - nothing to test | **Not implemented** - nothing to test | | Module signing | No harness case exists | Not yet run | Three harness cases back the proxy column (`luks-var`, `luks-var-tpm`, `dm-verity`), and they live on the #232 branch, so they are not gating anything on `scarthgap` yet. There are no IMA or module-signing cases at all; an earlier version of this table named two that do not exist. +IMA/EVM is a step further back than "not yet run". Checked on the running board, `/sys/kernel/security/ima/` and `/sys/kernel/security/evm` do not exist and `/proc/cmdline` carries no `ima_policy` or `ima_appraise`; checked in the layer, `ima.cfg` is absent from `scarthgap` and from both secure-boot branches, and no recipe, kas feature, or `DISTRO_FEATURES` gate references `ima` at all. Everything this note says about IMA and EVM is the design, and building it is its own piece of work - a fragment, a signing policy, and a labelled rootfs - not a check someone can run today. + ### The hardware pass (2026-08-12) The board pass ran on an FRDM-IMX93 against the wrynose BSP, and it closed the LUKS2 row. Three defects had to be fixed first, and every one of them was invisible on the proxy because `avocado-qemuarm64` carries a machine override that `avocado-imx93-frdm` did not: From 8abbdef3cce4dcc3cae361da5294855419d2a549 Mon Sep 17 00:00:00 2001 From: Javier Tia Date: Thu, 13 Aug 2026 12:41:21 -0600 Subject: [PATCH 05/10] field-notes/hardened-boot-imx93: record the signed-boot pass The note described a designed secure boot; the chain now actually boots on the board, so the coverage table gains a row it can back with output rather than intent. Three things are worth a reader's time more than the walkthrough. Signing is SPSDK rather than CST, because cst_signer refuses AHAB without it and its own README puts i.MX 9 on that path - which also means there are no container offsets to scrape out of a build log. Enabling AHAB changes what booti means, so a bare Image no longer boots and a container staged at its own payload destination is destroyed by its own authentication. And ahab_status cannot report zero events before the fuses are burned, which inverts the order this note previously implied: burn while open, then check, then close. Also states plainly what is still not covered - the initramfs is passed outside the container and is therefore unauthenticated, on an image where it is what unlocks LUKS /var, and an open part logs a signature failure rather than refusing to boot. Signed-off-by: Javier Tia --- .../2026-06-24-hardened-boot-imx93.mdx | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx index 53f0a03c..caf85ebf 100644 --- a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx +++ b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx @@ -239,6 +239,7 @@ tpm2_pcrread sha256:10 | Control | `avocado-qemuarm64` (proxy) | `avocado-imx93-frdm` (hardware) | | :------------- | :---------------------------------------------- | :------------------------------------------------- | | LUKS2 `/var` | Verified on first boot | **Verified**, first boot and reboot (2026-08-12) | +| AHAB signed boot | No harness case exists | **Verified** on an open part (2026-08-13) - see below | | Phase-2 TPM2 | Enrollment verified; unseal on reboot **fails** | **Not possible on this board yet** - see below | | dm-verity | **Not active** on this target | Not yet run | | IMA/EVM | **Not implemented** - nothing to test | **Not implemented** - nothing to test | @@ -248,6 +249,64 @@ Three harness cases back the proxy column (`luks-var`, `luks-var-tpm`, `dm-verit IMA/EVM is a step further back than "not yet run". Checked on the running board, `/sys/kernel/security/ima/` and `/sys/kernel/security/evm` do not exist and `/proc/cmdline` carries no `ima_policy` or `ima_appraise`; checked in the layer, `ima.cfg` is absent from `scarthgap` and from both secure-boot branches, and no recipe, kas feature, or `DISTRO_FEATURES` gate references `ima` at all. Everything this note says about IMA and EVM is the design, and building it is its own piece of work - a fragment, a signing policy, and a labelled rootfs - not a check someone can run today. +### The signed-boot pass (2026-08-13) + +The whole chain now boots on the board: NXP-signed ELE container, OEM-signed +`imx-boot`, OEM-signed OS container, Linux, and LUKS2 `/var` underneath it. + +```text +Booting A +35519488 bytes read # os_cntr_signed.bin +44262937 bytes read # initramfs +Authenticate OS container at 0x88000000 +## Flattened Device Tree blob at 83000000 + Loading Ramdisk to fa469000, end fce9f619 ... OK +Starting kernel ... +[ 0.000000] Machine model: NXP FRDM-IMX93 +[ OK ] Found device /dev/mapper/var. +``` + +Signing is SPSDK, not CST. `cst_signer`'s own source refuses AHAB without SPSDK, +and its README says i.MX 8x/8ULP/9 go through SPSDK while only 6/7/8M stay on +CST - so `nxpimage ahab sign` takes the finished container and locates the +header and signature block itself. There are no offsets to scrape out of a build +log. + +**Enabling `CONFIG_AHAB_BOOT` changes what `booti` means, and that is the part +that bites.** Its first argument stops being a kernel and becomes a container: +`cmd/booti.c` authenticates it, then calls `container_get_image_dst()` on the +same address to find where the kernel landed. But authentication has already +copied each image to the destination recorded inside the container - so if the +container is staged at the kernel's own destination, authentication overwrites +what the next line parses. It surfaces as `Parse kernel and fdt address failed +-1`, or as a bad-container report, depending on how far the copy got. + +That hides behind `auth_cntr`, which calls the same function and passes every +time because nothing reads the container afterwards. Four separate experiments +exonerated the container while `booti` kept failing on identical input. Stage the +container somewhere it does not overlap its own payload. + +**`ahab_status` cannot report zero events before fusing.** On an unfused part the +ELE has no `SRK_HASH` to compare the container's SRK table against, so it logs +`ELE_BAD_KEY_HASH_FAILURE_IND (0xFA)` and returns `ELE_SUCCESS_IND` - the open +part tolerates it and boots: + +```text +Lifecycle: 0x00000008, OEM Open +fuse read 16 0 8 -> all zeros +``` + +So the order is burn `SRK_HASH` while still open, *then* check for zero events, +*then* close. A plan that validates zero events before the burn cannot be +satisfied - which is what this note used to imply. + +Worth being plain about two limits. The initramfs is loaded outside the container +and passed to `booti` as a separate argument, so it is **not** authenticated - +and on this image it is what unlocks LUKS `/var`. And nothing here is enforced +yet: the part is open, so a signature failure is logged rather than fatal. +Enforcement is what closing the device buys, and closing is the step that can +leave a board unable to boot anything you hold keys for. + ### The hardware pass (2026-08-12) The board pass ran on an FRDM-IMX93 against the wrynose BSP, and it closed the LUKS2 row. Three defects had to be fixed first, and every one of them was invisible on the proxy because `avocado-qemuarm64` carries a machine override that `avocado-imx93-frdm` did not: From 2f3158f518cc02b9115f0f3df800413af1a9408f Mon Sep 17 00:00:00 2001 From: Javier Tia Date: Fri, 14 Aug 2026 09:57:10 -0600 Subject: [PATCH 06/10] field-notes/hardened-boot-imx93: close the initramfs gap the log showed The signed-boot pass recorded here has `Loading Ramdisk` in its own output, and that line was the hole: AHAB covered the kernel and device tree, while booti takes its ramdisk from argv[1] and never authenticates it. On this image that initrd derives the LUKS key, so the only unsigned component sat in front of the storage control the note is about. Record what closed it and why the obvious route does not. A second container payload is authenticated and then never located, because the lookup stops at the first exec and first data image; keeping the separate load for its size leaves the length under an attacker's control, which a concatenated cpio turns into file replacement. Bundling into the kernel removes the argument entirely. The size consequences are worth a reader's time as much as the fix, since they are what the next person hits: an uncompressed embed that sextuples the kernel, boot partitions that have to double, and a memory map that breaks twice - once on the device tree, once on a DRAM hole this board has and a flat 2 GB assumption does not survive. Verified with `make build`. Signed-off-by: Javier Tia --- .../2026-06-24-hardened-boot-imx93.mdx | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx index caf85ebf..e4ae50b5 100644 --- a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx +++ b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx @@ -239,7 +239,7 @@ tpm2_pcrread sha256:10 | Control | `avocado-qemuarm64` (proxy) | `avocado-imx93-frdm` (hardware) | | :------------- | :---------------------------------------------- | :------------------------------------------------- | | LUKS2 `/var` | Verified on first boot | **Verified**, first boot and reboot (2026-08-12) | -| AHAB signed boot | No harness case exists | **Verified** on an open part (2026-08-13) - see below | +| AHAB signed boot | No harness case exists | **Verified** on an open part - bootloader and OS container (2026-08-13), initramfs folded into the signed container (2026-08-14). See below | | Phase-2 TPM2 | Enrollment verified; unseal on reboot **fails** | **Not possible on this board yet** - see below | | dm-verity | **Not active** on this target | Not yet run | | IMA/EVM | **Not implemented** - nothing to test | **Not implemented** - nothing to test | @@ -300,6 +300,65 @@ So the order is burn `SRK_HASH` while still open, *then* check for zero events, *then* close. A plan that validates zero events before the burn cannot be satisfied - which is what this note used to imply. +### The gap in that log (2026-08-14) + +Read the pass above again and one line gives it away: `Loading Ramdisk`. AHAB +covered the kernel and the device tree. The initramfs was loaded separately and +handed to `booti` as `argv[1]`, which `booti` never authenticates - and on this +image that initrd is what derives the LUKS key and opens `/var`. The single +unsigned component stood directly in front of the storage control. + +The obvious fix is to add it as a second `--data` payload, and it fails +quietly. `container_get_image_dst()` returns the first exec image and the first +data image and stops, so a second data payload is authenticated, placed, and +then never located; `booti` still reads `argv[1]`. Keeping the separate load +just to obtain its size is worse: `${filesize}` comes from the unauthenticated +copy on disk, so a larger file leaves unverified bytes past the authenticated +region - and appending a cpio archive is the documented way to overlay files in +an initrd. + +Bundling it into the kernel closes it by construction. The initramfs rides +inside the exec payload the container already signs, and `booti` is handed no +ramdisk at all: + +```text +Booting A +176028672 bytes read # os_cntr_signed.bin - kernel, dtb and initramfs +Authenticate OS container at 0xa8000000 +Starting kernel ... +[ OK ] Found device /dev/mapper/var. + +# findmnt -no SOURCE,FSTYPE /var +/dev/mapper/var btrfs +``` + +No `Loading Ramdisk` line. That absence is the whole result. + +**The cost is size, and it lands in three places.** Bundling embeds the archive +uncompressed - the build decompresses the `.cpio.zst` and the kernel stores what +it is handed - so the kernel goes from 33 MB to about 200 MB. Re-compressing is +not reachable from a config fragment, because the compression choice depends on +`INITRAMFS_SOURCE`, which only reaches the kernel as a make argument during the +bundling step and so is unset when the config is written. + +That forces the boot partitions from 128 to 256 MiB, `recovery` included since +it holds a boot filesystem too. It also breaks the memory map, twice. The +device tree ended up inside the kernel's unpack range (`FDT image overlaps OS +image`). Then the container, moved out of the way, landed across a hole: this +board's DRAM is not contiguous - `bdinfo` reports `0x80000000+0x16000000` and +`0x98000000+0x68000000` with 32 MiB missing between them - and a load spanning +it is refused with `Reading file would overwrite reserved memory`, which names +neither the hole nor the size. The container now stages in the second bank, +above the carve-out Linux later claims as `ele-reserved`. + +The guard that was supposed to catch this compared two addresses for equality, +which is true of any layout that fails this way and false of every layout that +did. It now measures the built kernel and refuses when it reaches either the +device tree or the container. It earned that immediately: turning on encrypted +`/var` grew the initramfs and pushed the kernel past a device tree that had +already been moved once, and the build stopped with both addresses named rather +than the board stopping at a prompt. + Worth being plain about two limits. The initramfs is loaded outside the container and passed to `booti` as a separate argument, so it is **not** authenticated - and on this image it is what unlocks LUKS `/var`. And nothing here is enforced From 213de611c754d6487dded5ea45ec4b91155659cc Mon Sep 17 00:00:00 2001 From: Javier Tia Date: Wed, 19 Aug 2026 17:51:16 -0600 Subject: [PATCH 07/10] field-notes/hardened-boot-imx93: record the fTPM hardware pass Phase-2 TPM2 sealing was documented as unverifiable on this board - no discrete TPM, and the OP-TEE fTPM route existed only on avocado-qemuarm64. That port landed and ran on FRDM-IMX93 hardware: a PCR-7 keyslot enrolls automatically on first boot alongside a permanent Argon2id recovery slot, confirmed via cryptsetup luksDump and the boot journal. The note's "Phase-2 TPM2 enrollment" section described a different design than what shipped - a manual, one-time enrollment that killed the recovery slot once the TPM path proved itself. The actual cryptsetup-var.sh runs enrollment unattended on every boot and never kills the Argon2id slot, on purpose: a legitimate PCR-7 change (a Secure Boot policy update) would otherwise cause a permanent lockout. Corrected that section along with the coverage table, the top admonition, and the security-properties framing, which still called two-factor sealing "the next step" after it was verified. Also folds in two findings adjacent to this pass but distinct from it, since they change what a reader following this note's own reproduce steps should expect: a u-boot bug forced AHAB authentication onto every imx93-frdm/imx91-frdm build regardless of the ahab feature (fixed in meta-avocado#279, the same PR this fTPM work landed on), and this specific board's SRK fuse was burned byte-swapped out of band, so it can no longer validate AHAB at all - unrelated to fTPM, which needs no AHAB, but relevant to anyone reading the AHAB row of the coverage table and wondering why it can't be re-run here. npm run build exits 0 against this branch; verified the caution admonition, corrected coverage table, and new dated section render as expected. Signed-off-by: Javier Tia --- .../2026-06-24-hardened-boot-imx93.mdx | 130 +++++++++++++----- 1 file changed, 96 insertions(+), 34 deletions(-) diff --git a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx index e4ae50b5..42f1279f 100644 --- a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx +++ b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx @@ -8,7 +8,7 @@ category: 'Security & Compliance' featured: false # Featured selection is Kristen/Bill's call image: /img/field-notes/2026-06-24-hardened-boot-imx93-thumb.png image_source: local:hardened-boot-imx93.jpg -tested_against: 'Avocado 0.41.2, avocado-qemuarm64 (fTPM via QEMU). LUKS2 /var validated on avocado-imx93-frdm hardware 2026-08-12 (wrynose BSP, meta-avocado #271), first boot and reboot. Phase-2 TPM2, dm-verity and module signing NOT yet validated on that hardware; IMA/EVM is design only and not implemented in the layer.' +tested_against: 'Avocado 0.41.2, avocado-qemuarm64 (fTPM via QEMU). LUKS2 /var and phase-2 OP-TEE fTPM PCR-7 sealing both validated on avocado-imx93-frdm hardware (2026-08-12 and 2026-08-19, meta-avocado #279). dm-verity and module signing NOT yet validated on that hardware; IMA/EVM is design only and not implemented in the layer.' poster: jtia lift_for_blog: 'CRA Annex I and IEC 62443-4-2 both require demonstrable data-at-rest protection and integrity; this is the boot-chain half a compliance mapping builds on.' promote_to_track: 'Secure-boot / storage-encryption reference candidate once the i.MX93 hardware pass is fully signed off.' @@ -18,7 +18,7 @@ import TestStatus from '@site/src/components/TestStatus' import PullQuote from '@site/src/components/PullQuote' The storage chip alone is worthless: the key is sealed to the Secure Boot state, and the TPM is @@ -410,7 +472,7 @@ The LUKS key is sealed to PCR 7 (UEFI Secure Boot state). A tampered bootloader ## Issues and open questions -- **Unseal fails on reboot on the proxy.** The two-phase enrollment lands both slots correctly, and then the very next boot cannot unseal from the TPM2 slot and falls back to Argon2. The layer already documents why, and it is a property of the emulator rather than of the design: the QEMU `virt` machine has no RPMB, so OP-TEE cannot commit the rollback counter its secure storage depends on, will not trust the fTPM's saved state on the next boot, and the fTPM re-derives its seed. `meta-avocado-qemu/recipes-security/optee-ftpm-init/optee-ftpm-init.bb` states this and calls it a QEMU limitation, not a defect in the layer. What follows for this note is narrower than the symptom suggests but still unresolved: the proxy structurally cannot demonstrate reboot survival, so "the TPM2 slot reopens `/var` on every later boot" rests entirely on the hardware pass. **The 2026-08-12 board pass answered half of it.** `/var` did reopen across a reboot on real hardware, from an existing LUKS2 container, with the same filesystem UUID either side - so the unlock-on-later-boot path is shown. What it was shown for is the Argon2id slot, because the board has no TPM to seal to and the fTPM route is not yet ported to it, so the specific claim about the *TPM2* slot is still unevidenced on any platform: the proxy cannot show it for want of RPMB, and the board cannot show it for want of a TPM. +- **Unseal fails on reboot on the proxy; the TPM2-specific reboot claim is still open even on hardware.** The two-phase enrollment lands both slots correctly on QEMU, and then the very next boot cannot unseal from the TPM2 slot and falls back to Argon2. The layer already documents why, and it is a property of the emulator rather than of the design: the QEMU `virt` machine has no RPMB, so OP-TEE cannot commit the rollback counter its secure storage depends on, will not trust the fTPM's saved state on the next boot, and the fTPM re-derives its seed. **The 2026-08-12 board pass showed reboot survival for the Argon2id slot** - `/var` reopened across a reboot from an existing LUKS2 container, same filesystem UUID either side - and **the 2026-08-19 pass showed the eMMC-backed board *does* have RPMB and a working fTPM, with the PCR-7 keyslot enrolling cleanly on first boot.** What remains genuinely untested on any platform is the specific case that matters: reopen `/var` via the TPM2 token specifically (not the Argon2id fallback) on a *second* boot of an *already-enrolled* image on real hardware. `cryptsetup-var.sh` is written to prefer the TPM2 token on open and fall back to Argon2id only on failure, so this should work - it just has not been independently re-tested this way yet. - **An interrupted first boot wedges the device permanently.** `cryptsetup-var.sh` only runs `mkfs.btrfs` in its first-boot branch. A first boot that dies after `luksFormat` but before `mkfs.btrfs` leaves a LUKS2 header with no filesystem inside, so every later boot takes the open-existing branch, never creates the filesystem, and drops to emergency forever. Re-provisioning does not clear it, because `fwup -t complete` does not touch the `var` partition; recovery means zeroing the LUKS header by hand. The fix is either to detect "header present, no filesystem on the opened mapper" and re-`mkfs`, or to make format-plus-mkfs atomic behind a completion marker. - **`luksKillSlot` guard.** The `|| true` on slot 0 removal is intentional but means a failed kill leaves both slots active. A monitoring check after enrollment should confirm slot 0 is gone; the current implementation does not warn if the kill is skipped. - **IMA/EVM raises a labeling and update cost.** Every appraised file must carry a valid `security.ima` signature, so the build signs the rootfs and shipped mutable content, and every OS update re-signs and shifts the measured hashes. That makes the attestation server's known-good list an ongoing ops dependency. First boot also runs roughly 5-15% slower while IMA labels and measures, which is why IMA is gated on `ima` rather than always on. From b5d9e277b61fd017ce849bef26d1a7c3c97ebc8b Mon Sep 17 00:00:00 2001 From: Javier Tia Date: Wed, 19 Aug 2026 17:54:56 -0600 Subject: [PATCH 08/10] field-notes/hardened-boot-imx93: use plain kas, not bakar Both reproduce commands used bakar, an internal dev-tool wrapper around kas that a reader following this public field note would not have installed. meta-avocado/overview.md already documents the real external entry point: source init-build $KAS_YML, then kas build $KAS_YML. Match it. npm run build exits 0. Signed-off-by: Javier Tia --- src/field-notes/2026-06-24-hardened-boot-imx93.mdx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx index 42f1279f..d43eb4e7 100644 --- a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx +++ b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx @@ -188,8 +188,9 @@ Three gates, not four. An `ima` gate belongs in that list by design and is not i `encrypted-var` is a kas feature, so composing it onto a board's machine YAML is the whole build step. Add `ftpm.yml` for phase-2 PCR-7 sealing (verified 2026-08-19; see below) - it needs no `ahab`: ```bash -bakar build \ - meta-avocado/kas/machine/imx93-frdm.yml:meta-avocado/kas/feature/encrypted-var.yml:meta-avocado/kas/feature/ftpm.yml +KAS_YML="meta-avocado/kas/machine/imx93-frdm.yml:meta-avocado/kas/feature/encrypted-var.yml:meta-avocado/kas/feature/ftpm.yml" +source init-build "$KAS_YML" +kas build "$KAS_YML" ``` A checkout older than meta-avocado#279 hits `Error: Wrong container header` on this @@ -199,8 +200,9 @@ build regardless of the `ahab` feature. Fixed 2026-08-19; see below. The stock image gives you no way in, on console or over SSH: `avocado-users` ships `/etc/shadow` with `root:*:`, and a literal `*` means no password can ever match. Stack the dev-access feature when you need a shell to check any of this - dev boards only, it accepts root with an empty password: ```bash -bakar build \ - meta-avocado/kas/machine/imx93-frdm.yml:meta-avocado/kas/feature/encrypted-var.yml:meta-avocado/kas/feature/ssh-dev.yml +KAS_YML="meta-avocado/kas/machine/imx93-frdm.yml:meta-avocado/kas/feature/encrypted-var.yml:meta-avocado/kas/feature/ssh-dev.yml" +source init-build "$KAS_YML" +kas build "$KAS_YML" ``` Then write it to the card. `avocado-flash` selects the backend from the medium and refuses anything that is not an unmounted whole disk on a removable bus: From f11ec1dcb3f967924bbb9bafda00c1c8d84bc152 Mon Sep 17 00:00:00 2001 From: Javier Tia Date: Thu, 20 Aug 2026 10:50:18 -0600 Subject: [PATCH 09/10] field-notes/hardened-boot-imx93: satisfy the prettier gate Rebasing onto main picked up a `prettier --check .` step that scripts/checks.sh did not run when this branch was written, and the note's new coverage table and `*emphasis*` spans fail it. Formatting only - no prose or claim changes. Signed-off-by: Javier Tia --- .../2026-06-24-hardened-boot-imx93.mdx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx index d43eb4e7..c06e7946 100644 --- a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx +++ b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx @@ -245,14 +245,14 @@ tpm2_pcrread sha256:10 ### Coverage -| Control | `avocado-qemuarm64` (proxy) | `avocado-imx93-frdm` (hardware) | -| :------------- | :---------------------------------------------- | :------------------------------------------------- | -| LUKS2 `/var` | Verified on first boot | **Verified**, first boot and reboot (2026-08-12) | -| AHAB signed boot | No harness case exists | **Verified** on an open part - bootloader and OS container (2026-08-13), initramfs folded into the signed container (2026-08-14). This exact board can no longer re-run this check - see below | -| Phase-2 TPM2 | Enrollment verified; unseal on reboot **fails** | **Verified** - PCR-7 keyslot enrolled via the OP-TEE fTPM on first boot (2026-08-19); reboot-survival of this specific TPM2 slot not yet independently re-tested on hardware. See below | -| dm-verity | **Not active** on this target | Not yet run | -| IMA/EVM | **Not implemented** - nothing to test | **Not implemented** - nothing to test | -| Module signing | No harness case exists | Not yet run | +| Control | `avocado-qemuarm64` (proxy) | `avocado-imx93-frdm` (hardware) | +| :--------------- | :---------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| LUKS2 `/var` | Verified on first boot | **Verified**, first boot and reboot (2026-08-12) | +| AHAB signed boot | No harness case exists | **Verified** on an open part - bootloader and OS container (2026-08-13), initramfs folded into the signed container (2026-08-14). This exact board can no longer re-run this check - see below | +| Phase-2 TPM2 | Enrollment verified; unseal on reboot **fails** | **Verified** - PCR-7 keyslot enrolled via the OP-TEE fTPM on first boot (2026-08-19); reboot-survival of this specific TPM2 slot not yet independently re-tested on hardware. See below | +| dm-verity | **Not active** on this target | Not yet run | +| IMA/EVM | **Not implemented** - nothing to test | **Not implemented** - nothing to test | +| Module signing | No harness case exists | Not yet run | Three harness cases back the proxy column (`luks-var`, `luks-var-tpm`, `dm-verity`), and they live on the #232 branch, so they are not gating anything on `scarthgap` yet. There are no IMA or module-signing cases at all; an earlier version of this table named two that do not exist. @@ -305,8 +305,8 @@ Lifecycle: 0x00000008, OEM Open fuse read 16 0 8 -> all zeros ``` -So the order is burn `SRK_HASH` while still open, *then* check for zero events, -*then* close. A plan that validates zero events before the burn cannot be +So the order is burn `SRK_HASH` while still open, _then_ check for zero events, +_then_ close. A plan that validates zero events before the burn cannot be satisfied - which is what this note used to imply. ### The gap in that log (2026-08-14) @@ -379,7 +379,7 @@ leave a board unable to boot anything you hold keys for. The board pass ran on an FRDM-IMX93 against the wrynose BSP, and it closed the LUKS2 row. Three defects had to be fixed first, and every one of them was invisible on the proxy because `avocado-qemuarm64` carries a machine override that `avocado-imx93-frdm` did not: -- **`CONFIG_DM_CRYPT` was silently `=m`.** The fragment asks for `=y`, but kconfig caps a tristate at its weakest parent and the i.MX defconfig left both `BLK_DEV_DM` and `TRUSTED_KEYS` at `=m`. The initramfs ships `cryptsetup` with no module and nothing that loads one, so the unlock had no dm-crypt to call. The task check greps the fragment *text*, so it passed while the built kernel disagreed - verify against the produced `.config`, never the fragment. +- **`CONFIG_DM_CRYPT` was silently `=m`.** The fragment asks for `=y`, but kconfig caps a tristate at its weakest parent and the i.MX defconfig left both `BLK_DEV_DM` and `TRUSTED_KEYS` at `=m`. The initramfs ships `cryptsetup` with no module and nothing that loads one, so the unlock had no dm-crypt to call. The task check greps the fragment _text_, so it passed while the built kernel disagreed - verify against the produced `.config`, never the fragment. - **`AVOCADO_VAR_PART_DEV` was unset.** Both qemu machines point `/var` at `/dev/mapper/var` when `encrypted-var` is on; imx93 fell through to the base-files raw-PARTUUID default, so the container would be created and opened in the initrd while fstab mounted `/var` from the raw partition underneath it. - **The key provider read a secret that does not exist.** The shared `var-key.sh` reads `/var/private/var-key-secret` - on the volume being unlocked, not mounted when the provider runs in the initramfs, and created by nothing in the tree. The Argon2id path had therefore never once succeeded on this board. @@ -474,7 +474,7 @@ The LUKS key is sealed to PCR 7 (UEFI Secure Boot state). A tampered bootloader ## Issues and open questions -- **Unseal fails on reboot on the proxy; the TPM2-specific reboot claim is still open even on hardware.** The two-phase enrollment lands both slots correctly on QEMU, and then the very next boot cannot unseal from the TPM2 slot and falls back to Argon2. The layer already documents why, and it is a property of the emulator rather than of the design: the QEMU `virt` machine has no RPMB, so OP-TEE cannot commit the rollback counter its secure storage depends on, will not trust the fTPM's saved state on the next boot, and the fTPM re-derives its seed. **The 2026-08-12 board pass showed reboot survival for the Argon2id slot** - `/var` reopened across a reboot from an existing LUKS2 container, same filesystem UUID either side - and **the 2026-08-19 pass showed the eMMC-backed board *does* have RPMB and a working fTPM, with the PCR-7 keyslot enrolling cleanly on first boot.** What remains genuinely untested on any platform is the specific case that matters: reopen `/var` via the TPM2 token specifically (not the Argon2id fallback) on a *second* boot of an *already-enrolled* image on real hardware. `cryptsetup-var.sh` is written to prefer the TPM2 token on open and fall back to Argon2id only on failure, so this should work - it just has not been independently re-tested this way yet. +- **Unseal fails on reboot on the proxy; the TPM2-specific reboot claim is still open even on hardware.** The two-phase enrollment lands both slots correctly on QEMU, and then the very next boot cannot unseal from the TPM2 slot and falls back to Argon2. The layer already documents why, and it is a property of the emulator rather than of the design: the QEMU `virt` machine has no RPMB, so OP-TEE cannot commit the rollback counter its secure storage depends on, will not trust the fTPM's saved state on the next boot, and the fTPM re-derives its seed. **The 2026-08-12 board pass showed reboot survival for the Argon2id slot** - `/var` reopened across a reboot from an existing LUKS2 container, same filesystem UUID either side - and **the 2026-08-19 pass showed the eMMC-backed board _does_ have RPMB and a working fTPM, with the PCR-7 keyslot enrolling cleanly on first boot.** What remains genuinely untested on any platform is the specific case that matters: reopen `/var` via the TPM2 token specifically (not the Argon2id fallback) on a _second_ boot of an _already-enrolled_ image on real hardware. `cryptsetup-var.sh` is written to prefer the TPM2 token on open and fall back to Argon2id only on failure, so this should work - it just has not been independently re-tested this way yet. - **An interrupted first boot wedges the device permanently.** `cryptsetup-var.sh` only runs `mkfs.btrfs` in its first-boot branch. A first boot that dies after `luksFormat` but before `mkfs.btrfs` leaves a LUKS2 header with no filesystem inside, so every later boot takes the open-existing branch, never creates the filesystem, and drops to emergency forever. Re-provisioning does not clear it, because `fwup -t complete` does not touch the `var` partition; recovery means zeroing the LUKS header by hand. The fix is either to detect "header present, no filesystem on the opened mapper" and re-`mkfs`, or to make format-plus-mkfs atomic behind a completion marker. - **`luksKillSlot` guard.** The `|| true` on slot 0 removal is intentional but means a failed kill leaves both slots active. A monitoring check after enrollment should confirm slot 0 is gone; the current implementation does not warn if the kill is skipped. - **IMA/EVM raises a labeling and update cost.** Every appraised file must carry a valid `security.ima` signature, so the build signs the rootfs and shipped mutable content, and every OS update re-signs and shifts the measured hashes. That makes the attestation server's known-good list an ongoing ops dependency. First boot also runs roughly 5-15% slower while IMA labels and measures, which is why IMA is gated on `ima` rather than always on. From 3251a7c98a7db91e615a46ddbb25d8e51cdf4980 Mon Sep 17 00:00:00 2001 From: Javier Tia Date: Sun, 23 Aug 2026 09:05:54 -0600 Subject: [PATCH 10/10] Say dm-verity is kernel support only, not a working hash tree The note's caution block and coverage table already say dm-verity is unverified on this board, but three passages still describe it as built. The capability table claims a hash tree is computed at build time and verified on every block read, the demonstrates list says every rootfs block is cryptographically verified, and the threat-model paragraph asserts an offline patch is caught before the patched code executes. "Unverified" and "does not exist" are different claims, and only the weaker one was being made: nothing in meta-avocado generates a hash tree, signs a roothash, or opens a verity device. The lone artifact is `dm-verity.cfg`, which sets CONFIG_DM_VERITY=y and then disables CONFIG_PKCS7_MESSAGE_PARSER, so a signed roothash could not verify even if a tree existed. Adopt the idiom the note already uses for IMA/EVM two rows down ("Designed only", "nothing implements it in the layer yet") so the reader can tell the two unimplemented controls apart from the one that is built. The threat-model paragraph splits for the same reason: PCR-7 sealing is verified on this board and belongs in its own sentence, while dm-verity, IMA/EVM and module signing are stated as intent with a pointer to the coverage table. The customer-facing security pages carried the same claim and are corrected separately, since they are undated present-tense product docs rather than a dated note. Signed-off-by: Javier Tia --- src/field-notes/2026-06-24-hardened-boot-imx93.mdx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx index c06e7946..525049b7 100644 --- a/src/field-notes/2026-06-24-hardened-boot-imx93.mdx +++ b/src/field-notes/2026-06-24-hardened-boot-imx93.mdx @@ -59,7 +59,7 @@ Peridio makes Avocado OS, an immutable embedded Linux runtime: a read-only A/B r An attacker who removes the eMMC or NVMe chip gets plaintext data, can patch the rootfs, can drop a modified config or binary into a mutable path, and can load an unsigned kernel module, unless four controls are in place: encrypted storage, rootfs integrity verification, runtime appraisal of mutable files (IMA/EVM), and a boot chain that gates decryption on the signature state of the firmware. Each control alone is weaker; together they require the attacker to compromise all four simultaneously. -dm-verity and IMA/EVM cover complementary halves of the integrity problem. dm-verity protects the immutable read-only rootfs with a build-time hash tree; it cannot say anything about files that are not in that tree. IMA/EVM closes that gap by appraising mutable files (writable config, dropped binaries, extension content) against a signature before the kernel opens or executes them, with EVM protecting the security xattr that holds the signature. The two together mean neither the immutable nor the mutable surface accepts tampered code. +dm-verity and IMA/EVM cover complementary halves of the integrity problem. dm-verity protects an immutable read-only rootfs with a build-time hash tree; it cannot say anything about files that are not in that tree. IMA/EVM closes that gap by appraising mutable files (writable config, dropped binaries, extension content) against a signature before the kernel opens or executes them, with EVM protecting the security xattr that holds the signature. The two together mean neither the immutable nor the mutable surface accepts tampered code. The threat is concrete in regulated industries. CRA Annex I Part I and IEC 62443-4-2 SL-2 both require demonstrable protection of data at rest; LUKS2 with TPM2 PCR 7 sealing satisfies that, and dm-verity plus IMA/EVM cover the integrity half. @@ -67,7 +67,7 @@ The threat is concrete in regulated industries. CRA Annex I Part I and IEC 62443 - LUKS2 `/var` formatted on first boot by the initramfs, with no pre-provisioned key material shipped in the image - Two-phase TPM2 enrollment: phase 1 uses a software Argon2id passphrase as a recovery baseline; phase 2 seals a new key to TPM2 PCR 7 (Secure Boot state) via `systemd-cryptenroll` and kills the Argon2id slot only after a `--token-only` unseal confirms the TPM path works -- dm-verity rootfs integrity gated on the `secureboot` DISTRO_FEATURE, with every rootfs block cryptographically verified on read +- dm-verity rootfs integrity - kernel support only. `dm-verity.cfg` sets `CONFIG_DM_VERITY=y` on `secureboot` builds, but nothing in the layer generates a hash tree, signs a roothash, or opens a verity device, and the same fragment disables `CONFIG_PKCS7_MESSAGE_PARSER`, so a signed roothash could not be verified even if one existed. The intent is that every rootfs block is cryptographically verified on read - IMA/EVM runtime appraisal - designed, not built. The intent is that the kernel refuses to open or execute a mutable file whose IMA signature does not verify, with EVM sealing the security xattrs and measurements logged to PCR 10. No kernel fragment, policy recipe, or `ima` gate exists in the layer today - Build-time UEFI Secure Boot key generation (PK/KEK/db/dbx) via an idempotent recipe that regenerates keys only when absent and never ships private `.key` files to the target - Kernel module signing with `MODULE_SIG_FORCE` gated on the `security` DISTRO_FEATURE @@ -82,7 +82,7 @@ The threat is concrete in regulated industries. CRA Annex I Part I and IEC 62443 | Data encryption | LUKS2 AES-256-XTS 512-bit key | XTS is the standard for block-device encryption; 512-bit key matches the AES-256 keyslot | | Key derivation (phase 1) | Argon2id via OpenSSL 3 KDF | Software fallback; combines a provisioned per-device secret with a hardware serial so the key is device-bound without a TPM | | Key sealing (phase 2) | TPM2 PCR 7 via `systemd-cryptenroll` | PCR 7 captures UEFI Secure Boot state; a tampered boot chain makes unseal fail | -| Rootfs integrity | dm-verity over EROFS | Hash tree computed at build time; verified on every block read; gated on `secureboot` | +| Rootfs integrity | dm-verity over EROFS | Kernel support only - `CONFIG_DM_VERITY=y` on `secureboot` builds, but no hash tree is generated, no roothash signed, and no verity device opened | | Mutable-file integrity | IMA appraisal + EVM | Designed only - appraises a per-file signature before open/exec and EVM protects `security.ima`, but nothing implements it in the layer yet | | Secure Boot keys | `gen-sbkeys.sh` (build-time, idempotent) | Generates PK/KEK/db/dbx; public certs to `/usr/share/secureboot-keys`; private `.key` never in `FILES` | | Module signing | `MODULE_SIG_FORCE` kernel config | Gated on `security`; prevents unsigned kernel modules from loading | @@ -470,7 +470,9 @@ Pulling the eMMC off the board gives an attacker AES-256-XTS ciphertext. Without not on the chip. -The LUKS key is sealed to PCR 7 (UEFI Secure Boot state). A tampered bootloader or disabled Secure Boot changes PCR 7 before the initramfs runs, so the TPM refuses to unseal, and you cannot clone the eMMC to another board and boot it. dm-verity verifies every rootfs block against a build-time hash tree, so an offline patch is caught before the patched code executes. IMA/EVM extends that to mutable files: a binary dropped into `/var` or an overlay has no valid `security.ima` signature, so the kernel refuses to execute it. Module signing closes the in-band injection path: even with a shell, the attacker cannot load a custom kernel module. +The LUKS key is sealed to PCR 7 (UEFI Secure Boot state). A tampered bootloader or disabled Secure Boot changes PCR 7 before the initramfs runs, so the TPM refuses to unseal, and you cannot clone the eMMC to another board and boot it. That half is built, and verified on this board. + +The integrity half is design rather than delivery, and the coverage table above says which is which. The intent: dm-verity verifies every rootfs block against a build-time hash tree, so an offline patch is caught before the patched code executes; IMA/EVM extends that to mutable files, so a binary dropped into `/var` or an overlay has no valid `security.ima` signature and the kernel refuses to execute it; and module signing closes the in-band injection path, so even with a shell an attacker cannot load a custom kernel module. None of those three is implemented in the layer today. ## Issues and open questions