Conversation
meta-avocado gates the /var key provider at three tiers, and none of them runs on the path a device actually receives. Two read cryptsetup-var's own datastore, which a bbappend can clear; the third hangs off a Yocto image recipe's ROOTFS_POSTPROCESS_COMMAND, and this tool composes a sysroot from the feed's RPMs without building an image. The tree already knew: avocado-security-capabilities.bb exists precisely because the image-scope artifact never reached a jetson-orin-nano. So a machine could declare encrypted-var, ship the unusable placeholder provider, and have every build-time check pass or never run. On first boot cryptsetup-var.sh reads the declaration, runs the provider, gets exit 1, and /var never unlocks - or worse, derives a constant and every device in the fleet shares one key. What does survive into the RPM is var-key.sh and the var-key.sh.sha256 that cryptsetup-var's do_install writes after every deliverability check passes. Confirmed on the built package rather than assumed: `rpm -qpl` lists the attestation beside the provider, and the extracted digest matches the extracted script. Checking that pair here re-arms the gate for this path. The absent-provider rule mirrors the build side rather than inventing one: only the initramfs must carry a provider, a rootfs legitimately ships the udev and posture packages without one, and a rootfs that does ship one is still validated. Getting that backwards would fail every rootfs build on a declaring machine, which is what the unit test pins. Ordered before the install stamp and propagated with `?`, unlike the stamp write beside it. A stamp failure leaves a correct sysroot merely unrecorded; this failure leaves a sysroot that must not ship, and stamping it first would record it fresh and skip the check on the next run. Verified end to end, in the real SDK container against a sysroot composed from a real cryptsetup-var RPM: as shipped rc=0, attestation removed rc=1, provider swapped after validation rc=1 with both digests printed. All nine branches of the generated script were exercised separately against fixtures. sha256sum, readlink, grep, cat and cut all resolve in avocadolinux/sdk:2024-edge and sha256sum emits the field shape the script parses. 1758 lib tests pass. Rollout order is a hard constraint and is documented at the function: a feed predating the attestation is refused, not warned, so meta-avocado ships first and feeds rebuild before this goes live.
The composed-sysroot check verified var-key.sh alone, and everything it asserted about itself was asserted against the script's text rather than its behaviour. Both halves were wrong in ways only execution showed. Verify every component meta-avocado attests: the unlock script, the provider, the optional hardware backend, and the unit that decides whether any of them runs. The table mirrors avocado_var_key_attested_components() in avocado-security-capabilities.bbclass. That list is a hand copy across a repo boundary and nothing binds the two, which the doc comment says plainly; the alternative, shipping a manifest of what to check beside the digests, puts the set in the same attacker-writable directory as the digests and weakens the gate. Decide on the SET rather than on the provider. The old shape exited before the first component check whenever var-key.sh was absent, so on a rootfs the higher-value target was checkable only at the attacker's discretion: a sysroot with a substituted cryptsetup-var.sh, a stale digest and no var-key.sh was measured passing with rc=0 and no stderr, and restoring an untouched provider flipped the same tree to rc=1. Require the initrd-root-fs.target.wants symlink when the unit is present. Deleting it reaches the same outcome as repointing ExecStart for one fewer edit, with every digest still matching. Canonicalise the sysroot once and compare resolved against resolved. The parent-directory check compared readlink -f output against the raw path, so any non-canonical AVOCADO_PREFIX refused a clean sysroot with "resolves outside the sysroot": measured rc=1 on an untampered tree reached through a symlinked component, and again with a trailing slash. A shop that hits that on a legitimate build learns to route around the check, which costs more than the case it was guarding. The build-side half never had this bug because it realpaths both sides. Testing that the sysroot directory exists comes first, because readlink -f exits non-zero on a dangling path and under set -e that killed the script with rc=1 and an empty stderr, which the caller renders as a failed attestation with no reason to show for it. Refusals exit rather than return. A return leaves the refusal depending on set -e still being armed at the call site, and a later "|| true", a wrapping if, or a caller that drops errexit would turn every refusal into an unread stderr line while the script still exited 0, because the last component returns 0 on every machine shipping no hardware backend. An exit inside a function terminates the script however it was called, which is the property a gate needs; the earlier comment argued the opposite and had the semantics backwards. Add tests that run the generated script under sh against real fixture trees, one case per decision branch. Replacing the whole check_component body with "return 0" previously left both text tests green, so the gate could be gutted without failing anything. It now fails two. Every defect listed above was found by executing this script and none was visible in its text, which is the argument for the fixtures over more assertions on the emitted string. The caller's error names the unlock path rather than the provider: there are now seven distinct refusal paths across four files, and the summary line is what survives the TUI. Signed-off-by: Javier Tia <javier@peridio.com>
The attestation check sat below the install-stamp short-circuit, so it ran on the first install of a sysroot and never again. A stamp records install INPUTS - the package set and the config that produced them - and stays current whenever nothing about the request changed. It says nothing about the sysroot's CONTENTS, which is the only thing this check reads. That gap is the threat model, not an edge of it. The check exists to catch a script substituted after the sysroot was composed, and an overlay does not change any stamp input, so the first install verified the sysroot and every run after it returned early above the check. Measured against a real composed initramfs sysroot on a local feed, with cryptsetup-var.sh substituted in place: rc=0 and no diagnostic before this change, rc=1 naming the file and both digests after it, and rc=0 again once the original bytes were restored. None of the existing tests could see this. They exercise generate_var_key_attestation_script directly, so they cover what the script decides and not whether the caller asks it. This costs one container exec on an otherwise no-op install, which is a real regression against the short-circuit's "pays nothing" promise and is taken deliberately: a gate that runs only on the first install is not a gate. The cheaper alternatives were both rejected. Hashing the sysroot in -process would duplicate the digest logic outside the container the rest of the check runs in, and gating on a content stamp would need that same hash to decide whether to hash. Signed-off-by: Javier Tia <javier@peridio.com>
The Windows compile check failed: three tests use std::os::unix::fs::symlink directly, and their two shared helpers create a real symlink and shell out to `sh`, which does not exist on that target. Neither is incidental - the tests exist to run the generated attestation script against real fixture trees rather than assert on its text, and a POSIX shell running against real filesystem symlinks is the whole point of that exercise. Gate the three executing tests and their two helpers behind #[cfg(unix)], matching the crate's existing cfg(unix) convention elsewhere. The two tests that only assert on the generated script's text stay ungated - they were never the problem. All 1762 lib tests still pass on Linux, including the five gated ones, which the gate does not skip on a unix host by construction. Signed-off-by: Javier Tia <javier@peridio.com>
This branch pinned rustls 0.23.39, and a new advisory published against that exact version blocks every push through the cargo-audit pre-push hook. main already carries 0.23.45; this branch just never picked it up since it branched. cargo update -p rustls --precise 0.23.45, which also carries aws-lc-rs and rustls-webpki forward to satisfy it. 1762 lib tests still pass. Signed-off-by: Javier Tia <javier@peridio.com>
|
While fixing the Windows compile check, two other build legs went red on this branch ( Not caused by either commit on this branch. The two things I added - Not caused by anything else on this branch either, as far as I can tell. Every other branch that ran this same CI workflow today passed, including Where I looked and came up empty. The error text is I don't have a fix. The remaining candidate is the SDK container's own internal tooling ( |
Problem
meta-avocado gates the
/varkey provider at three build-time tiers, and none ofthem runs on the path a device actually receives. Two read
cryptsetup-var's owndatastore, which a
.bbappendcan clear; the third hangs off a Yocto imagerecipe's
ROOTFS_POSTPROCESS_COMMAND, and this tool composes a sysroot from thefeed's RPMs without building an image.
The tree already knew this:
avocado-security-capabilities.bbexists preciselybecause the image-scope artifact never reached a jetson-orin-nano.
So a machine can declare
encrypted-var, ship a substituted unlock path, andhave every build-time check pass or never run. On first boot
cryptsetup-var.shreads the declaration, runs the provider, and either
/varnever unlocks or itopens on a key every device in the fleet shares.
Solution
What survives into the RPM is each script on the unlock path plus the
.sha256that
cryptsetup-var'sdo_installwrites beside it after its deliverabilitychecks pass. Verifying those pairs on the composed sysroot re-arms the gate for
this path.
Key changes
generate_var_key_attestation_scriptemits the check;verify_var_key_attestationruns it in the SDK container against the composed sysroot.cryptsetup-var.sh,var-key.sh, the optionalvar-hwkey.sh, andcryptsetup-var.service. The unit is included because itsExecStart=names which script performs the unlock and itsConditionPathExists=gates the unit, so editing it substitutes or skips the whole path without changing a script digest. The check also requires theinitrd-root-fs.target.wantssymlink, since deleting that disables the unit while every digest still matches.var-key.shwas absent, so on a rootfs the higher-value target was checkable only at the attacker's discretion.AVOCADO_PREFIXno longer refuses a clean sysroot.exits rather thanreturns, so a refusal cannot degrade to an unread warning ifset -eis ever disarmed at the call site.Reviewer notes
The gate this verifies is no longer in
meta-avocado. It now lives inmeta-avocado-nonpublicPR 3, along with the rest of the security-capabilitywork, because those features are becoming enterprise. The rollout order is
unchanged in shape and changed in target:
Nothing about that ordering softened. A feed built before the attestation
exists has no
.sha256, and this refuses it rather than warning.The limitation that arrangement creates, stated plainly. This tool is
public and the contract it enforces is not. Three consequences, and none of
them is hypothetical:
meta-avocadocarries no attestations at all. Itscryptsetup-var.bbis 117 lines with nosha256anywhere, against 1141 inthe private version, and its
avocado-security-capabilities.bbclassis 189lines with none of the gate. So this check fails closed against every
public-built sysroot that declares
encrypted-var. That is the correctbehaviour and it is also a hard coupling:
avocado sdk installsucceeds orfails depending on which repository the feed came from.
avocado_var_key_attested_components(), and that function is now in aprivate repository. Before the move the drift was at least checkable by
anyone reading both trees. Now a contributor to this repo cannot see the
source of truth, cannot tell whether the list is current, and will get no
signal if it goes stale. The alternative previously considered and rejected -
shipping a manifest of what to check beside the digests - remains rejected
for the same reason: it puts the set of things-to-check in the same
attacker-writable directory as the digests.
the silent pass the check exists to remove, and a default-off flag is a
silent pass with extra steps. The consequence is that the behaviour cannot be
varied per build, only per feed.
Follow-up, not resolved here: how enterprise features should be applied at
all. This PR is the first place the public/private boundary produces a
user-visible behaviour difference, and it does so by accident of where a file
happens to live rather than by any deliberate mechanism. There is no notion in
this tool of a feature being enterprise, no way for a public build to say "this
capability is not available to you" rather than failing an attestation check,
and no way for the private side to declare what it provides. Worth designing
before more capabilities move, because the number of places that need the
answer grows with each one.
Related and still undecided: ten machine configurations in public
meta-avocadostill declareencrypted-var. Until that changes, a publicbuild can request a capability whose implementation is private, which is the
shape this check turns into a hard failure.
Verified end to end on a real feed, not only against fixtures. A local
package feed was built from this branch's meta-avocado tree, served over HTTP,
and installed with
avocado sdk installfollowed byavocado initramfs installinto a composed sysroot carrying
encrypted-var tpm2and all four attestedcomponents with their digests. Substituting
cryptsetup-var.shin that sysrootand re-running the install returns
rc=1naming the file and both digests;restoring the original bytes returns
rc=0.That E2E is what found the last defect here. The check previously sat below the
install-stamp short-circuit, so it ran once on first install and never again,
and a tampered sysroot passed with
rc=0and no diagnostic. No unit or fixturetest could see it: they exercise the generated script directly, so they cover
what the script decides and not whether the caller asks it.
Twelve fixture cases run the generated script under
shagainst real sysroottrees in
avocadolinux/sdk:2026-edge, one per decision branch. 1762 lib testspass. Mutation-tested: replacing the whole
check_componentbody withreturn 0previously left every test green and now fails two.Draft until meta-avocado-nonpublic PR 3 lands and feeds rebuild. Undrafting this before then would merge a check with nothing to check against.