Skip to content

feat(bundle): Publish the digests of the kernel and initrd - #13

Merged
ananos merged 4 commits into
mainfrom
feat/boot-asset-digests
Sep 30, 2026
Merged

ananos merged 4 commits into
mainfrom
feat/boot-asset-digests

Conversation

@ananos

@ananos ananos commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

brig-sh/brig#392 makes brig refuse to boot a kernel or initrd whose digest does not match what it was told to expect. On Linux, the kernel and initrd come from this bundle: the launcher hands share/guest to brig as BRIG_BOOT_ASSETS. Nothing recorded what those two files hash to, so brig had nothing to compare them with. Under BRIG_VERIFY=require, every default Linux run would refuse. The amd64 kernel also came from bunny's linux-kernel-cloud-hypervisor:latest, a moving tag on an image nothing signs, while arm64 took its kernel from hull-assets.

This patch takes the kernel from hull-assets on both arches, makes the bundle publish the digests, and keeps a signed record of them on the host:

  • both arches take the kernel from ghcr.io/nofireai/hull-assets, the artifact hull and brig boot from. The build resolves the version tag once, checks the signature on that digest with the bundled cosign against the identity brig checks (the build-assets.yml workflow in NOFireAI/hull-assets), and pulls that digest. The kernel's sha256 is its layer digest in the signed manifest, and on arm64 it is the file hull boots on macOS;
  • the build writes share/guest/SHA256SUMS with the sha256 of the kernel and of container-initrd, and publishes the same bytes as <bundle>.boot-assets.sha256. The release job checksums every asset, so the cosign-signed checksums.txt covers the record with no change to release.yml;
  • the installer keeps the release's checksums.txt, checksums.txt.sig and checksums.txt.pem in share/guest, beside SHA256SUMS, when checksums.txt lists that record. It fetches the signature even without cosign at install time, since brig checks it before a boot with the bundle's own cosign;
  • pins.env names the kernel by digest.

brig's side is brig-sh/brig#403, stacked on #392. Once this and #14 land, cut v0.1.0-rc12 from main by pushing the tag: the certificate has to name release.yml@refs/tags/…, which both this installer and brig check for. brig's install.sh then moves its runtime pin to rc12 in #403.

Changes

  • scripts/build-bundle.sh:
    • both arches: oras resolve the hull-assets tag, check the answer is a sha256 digest, cosign verify that digest with the bundled cosign against ASSETS_SIGNER and ASSETS_ISSUER (default: brig's identity for hull-assets), pull hull-assets@<digest>, and record …:<tag>@<digest> as KERNEL_SOURCE.
    • ASSETS_VERSION defaults to 0.1.6, the first hull-assets release with the minimal kernel profile: the bzImage is 11.3 MB on amd64 (was 16.6) and the Image 23.5 MB on arm64 (was 52.4).
    • KERNEL_IMAGE_AMD64 and the docker path are gone.
    • Write share/guest/SHA256SUMS next to bundle.json, and copy it to $OUT/<bundle>.boot-assets.sha256. Remove a record an earlier build left in --out first, so a --variant stock build (no guest record) cannot pass off an old one.
    • --help prints the whole header, output list included.
  • .github/workflows/ci.yml: a new step, "Check the boot-asset digests":
    • unpack the built tarball;
    • check the published record is byte-identical to share/guest/SHA256SUMS;
    • check it lists exactly the kernel and container-initrd;
    • run sha256sum -c against both files;
    • check pins.env names the kernel by digest.
  • install.sh:
    • fetch checksums.txt.sig and .pem whether or not cosign is present;
    • keep_release_record keeps the three files when checksums.txt lists this SHA256SUMS under a *.boot-assets.sha256 name. If the signature or the listing is missing, it keeps none and warns, because brig would then warn before every run;
    • a local tarball picks up the three files when they were copied across beside it. The tarball itself is still taken as given;
    • the --help line for INSTALL_BRIG_SKIP_SIGCHECK says that option keeps no record.
  • tests/install-record.sh (new) serves a fake release over http and covers eight cases:
    • a release install with cosign keeps the three files;
    • so does one without cosign, which still fetches the signature;
    • a local tarball with nothing beside it keeps none;
    • a local tarball with the release files beside it keeps them;
    • a checksums.txt that does not list the record keeps none and warns;
    • so does one that lists the record under a name other than *.boot-assets.sha256;
    • a release with no signature keeps none and warns;
    • a bundle with no SHA256SUMS keeps none.
  • README.md, DESIGN.md and docs/variants.md:
    • one kernel source, hull-assets, checked against its signature;
    • SHA256SUMS and the kept files in the tree;
    • the record and its asset;
    • copying the release files across for an airgapped install, and what brig says without them.

Testing

  • shellcheck -s sh is clean on install.sh, scripts/build-bundle.sh and tests/*.sh at every commit, and ci.yml parses.
  • The signature check, run with cosign v3.1.3 (the bundled version):
    • brig's identity verifies hull-assets:0.1.6-linux-amd64 (sha256:e95a433d…) and 0.1.6-linux-arm64 (sha256:f86d1ca9…);
    • the identity of this repo's release.yml is refused: "no matching signatures: none of the expected identities matched".
  • The CI name check passes a record of the two files. It fails a record that lists only the kernel, and one that lists an extra file.
  • tests/*.sh pass in an Ubuntu 24.04 container as a normal user with a subuid range (rootless-setup.sh skips: no /dev/kvm). Each new install-record.sh case was revert-checked:
    • with main's verify_tarball, "without cosign the signature was not fetched";
    • with the listing check removed, "unlisted record: checksums.txt was kept";
    • with the name check removed, "record under another name: checksums.txt was kept";
    • with the local pick-up removed, the local case fails;
    • with warn turned back into say, "a release with no signature was dropped without a warning".
  • The rootless bundles were built natively from this branch plus fix(bundle): Pin urunc at the commit that boots arm64 on cloud-hypervisor #14, amd64 on nofire and arm64 on jetson6. Both logs say (…, signature verified), and pins.env names …:0.1.6-linux-amd64@sha256:e95a433d… and …:0.1.6-linux-arm64@sha256:f86d1ca9…. Installed under a scratch HOME with brig v0.3.0:
    • sha256sum -c SHA256SUMS in share/guest prints OK for the kernel and the initrd;
    • brig sh ubuntu@x uname -r on a fresh sandbox returns 6.12.95, rc 0, three times on each host, after "boot assets verified";
    • ten boots each under cloud-hypervisor answered the first exec: 648-725 ms on amd64, 719-792 ms on arm64.
  • The cost: on amd64 under cloud-hypervisor, a sandbox still starts later than with bunny's kernel. brig sh ubuntu@x true on 10 fresh sandboxes takes 3024 ms (median) against 2882 ms on nofire, in separate runs; the hull-assets kernel before 0.1.6 took 3090 ms. hull-assets ships a gzip bzImage and bunny an uncompressed vmlinux, and most of the gap is the bzImage decompressing itself.
  • On arm64 the minimal kernel is faster than the one before it: 3591 against 3718 ms (median, 10 fresh sandboxes each, jetson6).
  • feat(verify): Check the runtime bundle's record, fetch by digest brig-sh/brig#403 checks the record with the real cosign against the real v0.1.0-rc11 checksums.txt. An end-to-end check against a published record waits for rc12.

@ananos
ananos marked this pull request as ready for review September 29, 2026 07:04
@ananos

ananos commented Sep 29, 2026

Copy link
Copy Markdown
Contributor Author

@claude-fleet review

@ananos
ananos force-pushed the feat/boot-asset-digests branch 2 times, most recently from 6603b9b to ae6d1e2 Compare September 29, 2026 07:49
@ananos

ananos commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Review round on this PR: no must-fix, six should-fix and a handful of nits. All of them are addressed in the current head, 26943eb, squashed into three commits. CI is green, including the four real bundle builds.

Build (6fd90db)

  • A docker.io/… kernel override failed the build. The digest lookup filtered RepoDigests by the literal image name, and Docker lists a Docker Hub image by its short name. The build now takes the digest from docker pull's own output and copies the kernel out of that digest. All seven spellings we tried work, docker.io/library/… included.
  • The amd64 kernel came from bunny's :latest, so two builds of one commit could differ. KERNEL_IMAGE_AMD64 now defaults to the digest :latest names today, sha256:a9638a1d….
  • Nits:
    • oras resolve's answer is checked to be a sha256 digest.
    • The CI step requires the record to list exactly the kernel and the initrd.
    • A record left in --out by an earlier build is removed.
    • --help prints the whole header.
    • docs/variants.md describes both kernels by digest.

Install (26943eb)

  • The new test stubbed cosign, so it never ran the no-cosign path this commit is about. tests/install-record.sh now has eight cases, one of them an install with no cosign. The five new cases each fail when the change they cover is reverted.
  • A record was kept even when checksums.txt did not list it, and brig would refuse that record before every boot. The installer now checks the listing. When it is missing, it keeps nothing and warns.
  • A release with no .sig/.pem dropped the record without a word. That is a warn now.
  • The README and --help now say what an install without the record costs: brig warns before every run, and refuses under BRIG_VERIFY=require. A local tarball picks up checksums.txt, .sig and .pem when they are copied across beside it.

Elsewhere

  • brig accepted only this repo's release workflow as the signer, so a bundle released from a fork would refuse every boot. feat(verify): Check the runtime bundle's record, fetch by digest brig-sh/brig#403 adds BRIG_VERIFY_RUNTIME_IDENTITY and BRIG_VERIFY_RUNTIME_ISSUER, and the refusal names them. Nothing changes here: the installer already requires a tag-signed release, so we cut rc12 by pushing the tag.
  • A failing docker cp of the kernel stopped the build under set -e before the container was removed. That predates this PR, and 42406ae fixes it.

@ananos
ananos force-pushed the feat/boot-asset-digests branch from ae6d1e2 to 26943eb Compare September 29, 2026 17:14
The amd64 kernel came from bunny's linux-kernel-cloud-hypervisor:latest,
a moving tag on an image nothing signs, copied out with docker. arm64
already took its kernel from hull-assets, the artifact hull and brig
boot from, so the two arches drew from different places.

Both arches now take the kernel from hull-assets. The build resolves the
version tag once, checks the signature on that digest with the bundled
cosign against the identity brig checks (the build-assets.yml workflow
in NOFireAI/hull-assets), and pulls that digest. pins.env names it as
KERNEL_SOURCE, <repo>:<tag>@<digest>. KERNEL_IMAGE_AMD64 and the docker
path are gone; ASSETS_SIGNER and ASSETS_ISSUER set the identity for a
fork. On arm64 the kernel is the file hull boots on macOS.

On amd64 under cloud-hypervisor, a sandbox's agent answers about 0.25 s
later than with bunny's kernel (20 boots each on two hosts).

Refs: brig-sh/brig#234
Signed-off-by: Anastassios Nanos <ananos@nofire.ai>
On Linux the kernel and initrd that boot come from this bundle, and
nothing recorded what they hash to, so brig had nothing to compare them
with before a boot.

The build now writes share/guest/SHA256SUMS with the sha256 of the
kernel and of the initrd, and publishes the same bytes as
<bundle>.boot-assets.sha256. The release job checksums every asset, so
the signed checksums.txt covers the record. The kernel's line is its
layer digest in the signed hull-assets manifest.

CI checks that the published record is the one in the tree, that it
lists the kernel and the initrd, that it matches them, and that pins.env
names the kernel by digest. --help prints the whole header, which now
lists the record.

Refs: brig-sh/brig#234
Signed-off-by: Anastassios Nanos <ananos@nofire.ai>
The release's checksums.txt lists <bundle>.boot-assets.sha256, which is
share/guest/SHA256SUMS. The installer checked the tarball against it,
and its signature too when cosign was present, then dropped all of it,
so nothing on the host tied the kernel and initrd to the release.

A release install now keeps checksums.txt, checksums.txt.sig and
checksums.txt.pem in share/guest, beside SHA256SUMS, when checksums.txt
lists that SHA256SUMS. The signature is fetched even when cosign is
missing at install time, because brig checks it with the bundle's own
cosign before a boot. A local tarball keeps the three files when they
were copied across beside it.

A remote install under INSTALL_BRIG_SKIP_SIGCHECK, a bundle with no
SHA256SUMS, and a local tarball with nothing beside it keep none. A
release whose signature is missing, or whose checksums.txt does not list
the record, keeps none and says so, because brig would warn before every
run.

Refs: brig-sh/brig#234
Signed-off-by: Anastassios Nanos <ananos@nofire.ai>
@ananos
ananos force-pushed the feat/boot-asset-digests branch from 26943eb to b93e44b Compare September 29, 2026 19:42
hull-assets 0.1.6 carries the minimal kernel profile, which drops the
drivers for hardware no VMM presents and builds in the memory cgroup
controller, cgroup BPF and virtio-rng:

- amd64: bzImage 11.3 MB (was 16.6 MB)
- arm64: Image 23.5 MB (was 52.4 MB)

The bundle builds its own initrd, so the kernel is the only part it
takes from 0.1.6.

Signed-off-by: Anastassios Nanos <ananos@nofire.ai>
@ananos
ananos merged commit 68e8ed5 into main Sep 30, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant