diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ae021397..96362b1b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -154,11 +154,24 @@ jobs: matrix: test_os: [fedora-42, fedora-43, fedora-44, centos-9, centos-10] variant: [ostree, composefs-sealeduki-sdboot] + gating: [true] exclude: # centos-9 UKI is experimental/broken (https://github.com/bootc-dev/bootc/issues/1812) - test_os: centos-9 variant: composefs-sealeduki-sdboot - + - test_os: fedora-44 + gating: true + include: + # fedora-44 non-gating due to grub2 regression + # https://bugzilla.redhat.com/show_bug.cgi?id=2429501 + - test_os: fedora-44 + gating: false + variant: ostree + - test_os: fedora-44 + gating: false + variant: composefs-sealeduki-sdboot + # Non-gating jobs are allowed to fail without blocking the PR + continue-on-error: ${{ !matrix.gating }} runs-on: ubuntu-24.04 steps: @@ -197,6 +210,10 @@ jobs: - name: Unit and container integration tests run: just test-container + - name: Validate composefs digest (sealed UKI only) + if: matrix.variant == 'composefs-sealeduki-sdboot' + run: just validate-composefs-digest + - name: Run TMT integration tests run: | if [ "${{ matrix.variant }}" = "composefs-sealeduki-sdboot" ]; then diff --git a/Cargo.lock b/Cargo.lock index 30b0dd4d7..f3d0019c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler2" @@ -573,7 +573,7 @@ checksum = "55b672471b4e9f9e95499ea597ff64941a309b2cdbffcc46f2cc5e2d971fd335" [[package]] name = "composefs" version = "0.3.0" -source = "git+https://github.com/containers/composefs-rs?rev=e9008489375044022e90d26656960725a76f4620#e9008489375044022e90d26656960725a76f4620" +source = "git+https://github.com/cgwalters/composefs-rs?rev=c7257e95b80704adc7e2a4a37e5a10b27cd5237d#c7257e95b80704adc7e2a4a37e5a10b27cd5237d" dependencies = [ "anyhow", "hex", @@ -593,7 +593,7 @@ dependencies = [ [[package]] name = "composefs-boot" version = "0.3.0" -source = "git+https://github.com/containers/composefs-rs?rev=e9008489375044022e90d26656960725a76f4620#e9008489375044022e90d26656960725a76f4620" +source = "git+https://github.com/cgwalters/composefs-rs?rev=c7257e95b80704adc7e2a4a37e5a10b27cd5237d#c7257e95b80704adc7e2a4a37e5a10b27cd5237d" dependencies = [ "anyhow", "composefs", @@ -606,7 +606,7 @@ dependencies = [ [[package]] name = "composefs-oci" version = "0.3.0" -source = "git+https://github.com/containers/composefs-rs?rev=e9008489375044022e90d26656960725a76f4620#e9008489375044022e90d26656960725a76f4620" +source = "git+https://github.com/cgwalters/composefs-rs?rev=c7257e95b80704adc7e2a4a37e5a10b27cd5237d#c7257e95b80704adc7e2a4a37e5a10b27cd5237d" dependencies = [ "anyhow", "async-compression", diff --git a/Cargo.toml b/Cargo.toml index df45ce27f..ea13bd49e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,9 +42,17 @@ clap = "4.5.4" clap_mangen = { version = "0.2.20" } # Reviewers (including AI tools): The composefs-rs git revision is duplicated for each crate. # If adding/removing crates here, also update docs/Dockerfile.mdbook and docs/src/internals.md. -composefs = { git = "https://github.com/containers/composefs-rs", rev = "e9008489375044022e90d26656960725a76f4620", package = "composefs", features = ["rhel9"] } -composefs-boot = { git = "https://github.com/containers/composefs-rs", rev = "e9008489375044022e90d26656960725a76f4620", package = "composefs-boot" } -composefs-oci = { git = "https://github.com/containers/composefs-rs", rev = "e9008489375044022e90d26656960725a76f4620", package = "composefs-oci" } +# +# To develop against a local composefs-rs checkout: +# 1. Set BOOTC_extra_src to your composefs-rs path when building: +# BOOTC_extra_src=$HOME/src/composefs-rs just build +# 2. Comment out the git refs below and uncomment the path refs: +composefs = { git = "https://github.com/cgwalters/composefs-rs", rev = "c7257e95b80704adc7e2a4a37e5a10b27cd5237d", package = "composefs", features = ["rhel9"] } +composefs-boot = { git = "https://github.com/cgwalters/composefs-rs", rev = "c7257e95b80704adc7e2a4a37e5a10b27cd5237d", package = "composefs-boot" } +composefs-oci = { git = "https://github.com/cgwalters/composefs-rs", rev = "c7257e95b80704adc7e2a4a37e5a10b27cd5237d", package = "composefs-oci" } +# composefs = { path = "/run/extra-src/crates/composefs", package = "composefs", features = ["rhel9"] } +# composefs-boot = { path = "/run/extra-src/crates/composefs-boot", package = "composefs-boot" } +# composefs-oci = { path = "/run/extra-src/crates/composefs-oci", package = "composefs-oci" } fn-error-context = "0.2.1" hex = "0.4.3" indicatif = "0.18.0" diff --git a/Dockerfile b/Dockerfile index ca821407f..48a06ac11 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,12 +30,9 @@ WORKDIR /src # See https://www.reddit.com/r/rust/comments/126xeyx/exploring_the_problem_of_faster_cargo_docker/ # We aren't using the full recommendations there, just the simple bits. # First we download all of our Rust dependencies +# Note: /run/extra-src is optionally bind-mounted via BOOTC_extra_src for local composefs-rs development RUN --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome cargo fetch -FROM buildroot as sdboot-content -# Writes to /out -RUN /src/contrib/packaging/configure-systemdboot download - # We always do a "from scratch" build # https://docs.fedoraproject.org/en-US/bootc/building-from-scratch/ # because this fixes https://github.com/containers/composefs-rs/issues/132 @@ -65,6 +62,11 @@ ENV container=oci STOPSIGNAL SIGRTMIN+3 CMD ["/sbin/init"] +# This layer contains things which aren't in the default image and may +# be used for sealing images in particular. +FROM base as tools +RUN --mount=type=bind,from=packaging,target=/run/packaging /run/packaging/initialize-sealing-tools + # ------------- # external dependency cutoff point: # NOTE: Every RUN instruction past this point should use `--network=none`; we want to ensure @@ -81,14 +83,35 @@ ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} # Build RPM directly from source, using cached target directory RUN --network=none --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome RPM_VERSION="${pkgversion}" /src/contrib/packaging/build-rpm -FROM buildroot as sdboot-signed +# This image signs systemd-boot using our key, and writes the resulting binary into /out +FROM tools as sdboot-signed # The secureboot key and cert are passed via Justfile # We write the signed binary into /out +# Note: /out already contains systemd-boot-unsigned RPM from initialize-sealing-tools RUN --network=none \ - --mount=type=bind,from=sdboot-content,target=/run/sdboot-package \ --mount=type=secret,id=secureboot_key \ - --mount=type=secret,id=secureboot_cert \ - /src/contrib/packaging/configure-systemdboot sign + --mount=type=secret,id=secureboot_cert < to use a different base -ARG base=localhost/bootc -FROM $base AS base - -FROM base as kernel -RUN <&2; exit 1 - ;; -esac diff --git a/contrib/packaging/configure-variant b/contrib/packaging/configure-variant index 487ea3076..8940286ef 100755 --- a/contrib/packaging/configure-variant +++ b/contrib/packaging/configure-variant @@ -14,20 +14,7 @@ fi # Handle variant-specific configuration case "${VARIANT}" in *-sdboot) - # Install systemd-boot and remove bootupd; - # We downloaded this in an earlier phase - sdboot="usr/lib/systemd/boot/efi/systemd-bootx64.efi" - sdboot_bn=$(basename ${sdboot}) - rpm -Uvh /run/sdboot-content/out/*.rpm - # And override with our signed binary - install -m 0644 /run/sdboot-signed/out/${sdboot_bn} /${sdboot} - # Uninstall bootupd - rpm -e bootupd - rm -rf /usr/lib/bootupd/updates - # Clean up package manager caches - dnf clean all - rm -rf /var/cache /var/lib/{dnf,rhsm} /var/log/* ;; # Future variants can be added here # For Debian support, this could check package manager type and use apt instead diff --git a/contrib/packaging/fedora-extra.txt b/contrib/packaging/fedora-extra.txt index 50bc48f0b..a9f66c015 100644 --- a/contrib/packaging/fedora-extra.txt +++ b/contrib/packaging/fedora-extra.txt @@ -7,5 +7,3 @@ git-core jq # We now always build a package in the container build rpm-build -# Used for signing -sbsigntools diff --git a/contrib/packaging/finalize-uki b/contrib/packaging/finalize-uki new file mode 100755 index 000000000..6de60c2cc --- /dev/null +++ b/contrib/packaging/finalize-uki @@ -0,0 +1,49 @@ +#!/bin/bash +# Finalize UKI installation: copy to /boot, remove raw kernel/initramfs, create symlinks +# +# For sealed UKI images, the kernel and initramfs are embedded inside the signed +# UKI PE binary. We remove the standalone vmlinuz/initramfs.img to: +# - Avoid duplication (they're inside the UKI) +# - Ensure tools use the UKI path +# - Make it clear this is a UKI-only boot configuration +# +# NOTE: The old Dockerfile.cfsuki had a bug where the final-final stage started +# FROM base instead of FROM final, then only copied /boot. This meant the +# vmlinuz/initramfs removal in the final stage was lost. Running this script +# in the actual final image stage fixes that issue. +# +# IMPORTANT: bcvk needs to be updated to find .efi files inside kernel version +# subdirectories (e.g., /usr/lib/modules//.efi) rather than at the +# top level of /usr/lib/modules/. See https://github.com/bootc-dev/bcvk/pull/144 +set -xeuo pipefail + +# Path to directory containing the generated UKI +uki_src=$1 +shift + +# Find the kernel version from the current system +kver=$(cd /usr/lib/modules && echo *) +if [ -z "$kver" ] || [ "$kver" = "*" ]; then + echo "Error: No kernel found" >&2 + exit 1 +fi + +# Create the EFI directory structure +mkdir -p /boot/EFI/Linux + +# The UKI in /boot is outside the composefs-verified tree, which is fine +# because the UKI itself is signed and verified by Secure Boot +target=/boot/EFI/Linux/${kver}.efi +cp "${uki_src}/${kver}.efi" "${target}" + +# Remove the raw kernel and initramfs since we're using a UKI now. +# NOTE: We intentionally keep these for now until bcvk is updated to extract +# kernel/initramfs from UKIs in subdirectories. Once bcvk PR #144 is fixed +# to look for .efi files in /usr/lib/modules//, we can uncomment this. +# rm -v "/usr/lib/modules/${kver}/vmlinuz" "/usr/lib/modules/${kver}/initramfs.img" + +# NOTE: We used to create a symlink from /usr/lib/modules/${kver}/${kver}.efi to the UKI +# for tooling compatibility. However, composefs-boot's find_uki_components() doesn't +# handle symlinks correctly and fails with "is not a regular file". The UKI is already +# found in /boot/EFI/Linux/, so the symlink is not needed. +# See: https://github.com/containers/composefs-rs/issues/XXX diff --git a/contrib/packaging/initialize-sealing-tools b/contrib/packaging/initialize-sealing-tools new file mode 100755 index 000000000..b1e037bed --- /dev/null +++ b/contrib/packaging/initialize-sealing-tools @@ -0,0 +1,15 @@ +#!/bin/bash +set -xeuo pipefail +. /usr/lib/os-release +case "${ID}${ID_LIKE:-}" in + *centos*|*rhel*) + # Enable EPEL for sbsigntools + dnf -y install epel-release + ;; +esac +dnf -y install systemd-ukify sbsigntools +# And in the sealing case, we're going to inject and sign systemd-boot +# into the target image. +mkdir -p /out +cd /out +dnf -y download systemd-boot-unsigned diff --git a/contrib/packaging/install-buildroot b/contrib/packaging/install-buildroot index cc133e970..1bde1a2d2 100755 --- a/contrib/packaging/install-buildroot +++ b/contrib/packaging/install-buildroot @@ -3,14 +3,18 @@ set -xeuo pipefail cd $(dirname $0) . /usr/lib/os-release -case $ID in - centos|rhel) +case "${ID}${ID_LIKE:-}" in + *centos*|*rhel*) + # We'll use crb at build time dnf config-manager --set-enabled crb - # Enable EPEL for sbsigntools - dnf -y install epel-release ;; - fedora) dnf -y install dnf-utils 'dnf5-command(builddep)';; esac +# Deal with dnf4 vs dnf5 +if test -x /usr/bin/dnf5; then + dnf -y install 'dnf5-command(builddep)' +else + dnf -y install 'dnf-command(builddep)' +fi # Handle version skew, xref https://gitlab.com/redhat/centos-stream/containers/bootc/-/issues/1174 dnf -y distro-sync ostree{,-libs} systemd # Install base build requirements diff --git a/contrib/packaging/seal-uki b/contrib/packaging/seal-uki new file mode 100755 index 000000000..253278212 --- /dev/null +++ b/contrib/packaging/seal-uki @@ -0,0 +1,43 @@ +#!/bin/bash +# Generate a sealed UKI with embedded composefs digest +set -xeuo pipefail + +# Path to the desired root filesystem +target=$1 +shift +# Write to this directory +output=$1 +shift +# Path to secrets directory +secrets=$1 +shift + +# Compute the composefs digest from the target rootfs +composefs_digest=$(bootc container compute-composefs-digest "${target}") + +# Build the kernel command line +# enforcing=0: https://github.com/bootc-dev/bootc/issues/1826 +# TODO: pick up kargs from /usr/lib/bootc/kargs.d +cmdline="composefs=${composefs_digest} console=ttyS0,115200n8 console=hvc0 enforcing=0 rw" + +# Find the kernel version +kver=$(cd "${target}/usr/lib/modules" && echo *) +if [ -z "$kver" ] || [ "$kver" = "*" ]; then + echo "Error: No kernel found" >&2 + exit 1 +fi + +mkdir -p "${output}" + +ukify build \ + --linux "${target}/usr/lib/modules/${kver}/vmlinuz" \ + --initrd "${target}/usr/lib/modules/${kver}/initramfs.img" \ + --uname="${kver}" \ + --cmdline "${cmdline}" \ + --os-release "@${target}/usr/lib/os-release" \ + --signtool sbsign \ + --secureboot-private-key "${secrets}/secureboot_key" \ + --secureboot-certificate "${secrets}/secureboot_cert" \ + --measure \ + --json pretty \ + --output "${output}/${kver}.efi" diff --git a/contrib/packaging/switch-to-sdboot b/contrib/packaging/switch-to-sdboot new file mode 100755 index 000000000..be030ec9f --- /dev/null +++ b/contrib/packaging/switch-to-sdboot @@ -0,0 +1,24 @@ +#!/bin/bash +# Switch the target root to use systemd-boot, using the content from SRC +# SRC should contain an "out" subdirectory with: +# - systemd-boot-unsigned RPM (*.rpm) +# - signed systemd-boot binary (systemd-boot*.efi) +set -xeuo pipefail + +src=$1/out +shift + +# Uninstall bootupd if present (we're switching to sd-boot managed differently) +if rpm -q bootupd &>/dev/null; then + rpm -e bootupd + rm -vrf /usr/lib/bootupd/updates +fi + +# First install the unsigned systemd-boot RPM to get the package in place +rpm -Uvh "${src}"/*.rpm + +# Now find where it installed the binary and override with our signed version +sdboot=$(ls /usr/lib/systemd/boot/efi/systemd-boot*.efi) +sdboot_bn=$(basename "${sdboot}") +# Override with our signed binary +install -m 0644 "${src}/${sdboot_bn}" "${sdboot}" diff --git a/crates/etc-merge/src/lib.rs b/crates/etc-merge/src/lib.rs index 9f687fc6d..80b0637e6 100644 --- a/crates/etc-merge/src/lib.rs +++ b/crates/etc-merge/src/lib.rs @@ -316,17 +316,17 @@ pub fn traverse_etc( Directory, Option>, )> { - let mut pristine_etc_files = Directory::default(); + let mut pristine_etc_files = Directory::new(Stat::uninitialized()); recurse_dir(pristine_etc, &mut pristine_etc_files) .context(format!("Recursing {pristine_etc:?}"))?; - let mut current_etc_files = Directory::default(); + let mut current_etc_files = Directory::new(Stat::uninitialized()); recurse_dir(current_etc, &mut current_etc_files) .context(format!("Recursing {current_etc:?}"))?; let new_etc_files = match new_etc { Some(new_etc) => { - let mut new_etc_files = Directory::default(); + let mut new_etc_files = Directory::new(Stat::uninitialized()); recurse_dir(new_etc, &mut new_etc_files).context(format!("Recursing {new_etc:?}"))?; Some(new_etc_files) diff --git a/crates/lib/src/bootc_composefs/digest.rs b/crates/lib/src/bootc_composefs/digest.rs index d031d5277..0ddef9631 100644 --- a/crates/lib/src/bootc_composefs/digest.rs +++ b/crates/lib/src/bootc_composefs/digest.rs @@ -61,7 +61,7 @@ pub(crate) fn compute_composefs_digest( // Read filesystem from path, transform for boot, compute digest let mut fs = - composefs::fs::read_filesystem(rustix::fs::CWD, path.as_std_path(), Some(&repo), false)?; + composefs::fs::read_container_root(rustix::fs::CWD, path.as_std_path(), Some(&repo))?; fs.transform_for_boot(&repo).context("Preparing for boot")?; let id = fs.compute_image_id(); let digest = id.to_hex(); diff --git a/crates/lib/src/cfsctl.rs b/crates/lib/src/cfsctl.rs index 60e50beb8..07337dfaf 100644 --- a/crates/lib/src/cfsctl.rs +++ b/crates/lib/src/cfsctl.rs @@ -123,14 +123,17 @@ enum Command { /// the mountpoint mountpoint: String, }, + /// Creates a composefs image from a filesystem CreateImage { path: PathBuf, #[clap(long)] bootable: bool, + /// Don't copy /usr metadata to root directory (use if root already has well-defined metadata) #[clap(long)] - stat_root: bool, + no_propagate_usr_to_root: bool, image_name: Option, }, + /// Computes the composefs image ID for a filesystem ComputeId { path: PathBuf, /// Write the dumpfile to the provided target @@ -138,15 +141,18 @@ enum Command { write_dumpfile_to: Option, #[clap(long)] bootable: bool, + /// Don't copy /usr metadata to root directory (use if root already has well-defined metadata) #[clap(long)] - stat_root: bool, + no_propagate_usr_to_root: bool, }, + /// Outputs the composefs dumpfile format for a filesystem CreateDumpfile { path: PathBuf, #[clap(long)] bootable: bool, + /// Don't copy /usr metadata to root directory (use if root already has well-defined metadata) #[clap(long)] - stat_root: bool, + no_propagate_usr_to_root: bool, }, ImageObjects { name: String, @@ -215,7 +221,7 @@ where ref config_verity, } => { let verity = verity_opt(config_verity)?; - let mut fs = + let fs = composefs_oci::image::create_filesystem(repo, config_name, verity.as_ref())?; fs.print_dumpfile()?; } @@ -316,9 +322,13 @@ where ref path, write_dumpfile_to, bootable, - stat_root, + no_propagate_usr_to_root, } => { - let mut fs = composefs::fs::read_filesystem(CWD, path, Some(repo.as_ref()), stat_root)?; + let mut fs = if no_propagate_usr_to_root { + composefs::fs::read_filesystem(CWD, path, Some(repo.as_ref()))? + } else { + composefs::fs::read_container_root(CWD, path, Some(repo.as_ref()))? + }; if bootable { fs.transform_for_boot(repo)?; } @@ -334,10 +344,14 @@ where Command::CreateImage { ref path, bootable, - stat_root, + no_propagate_usr_to_root, ref image_name, } => { - let mut fs = composefs::fs::read_filesystem(CWD, path, Some(repo.as_ref()), stat_root)?; + let mut fs = if no_propagate_usr_to_root { + composefs::fs::read_filesystem(CWD, path, Some(repo.as_ref()))? + } else { + composefs::fs::read_container_root(CWD, path, Some(repo.as_ref()))? + }; if bootable { fs.transform_for_boot(repo)?; } @@ -347,9 +361,13 @@ where Command::CreateDumpfile { ref path, bootable, - stat_root, + no_propagate_usr_to_root, } => { - let mut fs = composefs::fs::read_filesystem(CWD, path, Some(repo.as_ref()), stat_root)?; + let mut fs = if no_propagate_usr_to_root { + composefs::fs::read_filesystem(CWD, path, Some(repo.as_ref()))? + } else { + composefs::fs::read_container_root(CWD, path, Some(repo.as_ref()))? + }; if bootable { fs.transform_for_boot(repo)?; } diff --git a/crates/xtask/src/xtask.rs b/crates/xtask/src/xtask.rs index 034a06b12..1a8b93580 100644 --- a/crates/xtask/src/xtask.rs +++ b/crates/xtask/src/xtask.rs @@ -54,6 +54,20 @@ enum Commands { TmtProvision(TmtProvisionArgs), /// Check build system properties (e.g., reproducible builds) CheckBuildsys, + /// Validate composefs digests match between build-time and install-time views + ValidateComposefsDigest(ValidateComposefsDigestArgs), +} + +/// Arguments for validate-composefs-digest command +#[derive(Debug, Args)] +pub(crate) struct ValidateComposefsDigestArgs { + /// Force a clean build (no podman cache) + #[arg(long)] + pub(crate) no_cache: bool, + + /// Base image to use + #[arg(long, default_value = "quay.io/centos-bootc/centos-bootc:stream10")] + pub(crate) base: String, } /// Arguments for run-tmt command @@ -139,6 +153,7 @@ fn try_main() -> Result<()> { Commands::RunTmt(args) => tmt::run_tmt(&sh, &args), Commands::TmtProvision(args) => tmt::tmt_provision(&sh, &args), Commands::CheckBuildsys => buildsys::check_buildsys(&sh, "Dockerfile".into()), + Commands::ValidateComposefsDigest(args) => validate_composefs_digest(&sh, &args), } } @@ -406,3 +421,110 @@ fn update_generated(sh: &Shell) -> Result<()> { Ok(()) } + +/// Validate that composefs digests match between build-time and install-time views. +/// +/// This builds a sealed image and compares dumpfiles generated from: +/// 1. The mounted filesystem (what seal-uki sees at build time) +/// 2. The OCI tar layers in containers-storage (what bootc upgrade sees) +/// +/// This helps debug mtime and metadata discrepancies that cause sealed boot failures. +#[context("Validating composefs digest")] +fn validate_composefs_digest(sh: &Shell, args: &ValidateComposefsDigestArgs) -> Result<()> { + let variant = "composefs-sealeduki-sdboot"; + let out_dir = Utf8Path::new("target/validate-digest"); + + // Ensure packages are built + if !Utf8Path::new("target/packages").exists() { + anyhow::bail!("target/packages not found. Run 'just package' first."); + } + + // Ensure secureboot keys exist + if !Utf8Path::new("target/test-secureboot/db.key").exists() { + cmd!(sh, "./hack/generate-secureboot-keys").run()?; + } + + std::fs::create_dir_all(out_dir)?; + let pkg_path = std::fs::canonicalize("target/packages")?; + let out_dir_abs = std::fs::canonicalize(out_dir)?; + + // Build the base-penultimate stage + println!("=== Building base-penultimate stage ==="); + let no_cache = args.no_cache.then_some("--no-cache"); + let base = &args.base; + let pkg_vol = format!("{}:/run/packages:ro,z", pkg_path.display()); + cmd!( + sh, + "podman build {no_cache...} + -v {pkg_vol} + --build-arg=base={base} + --build-arg=variant={variant} + --cap-add=all + --security-opt=label=type:container_runtime_t + --device=/dev/fuse + --secret=id=secureboot_key,src=target/test-secureboot/db.key + --secret=id=secureboot_cert,src=target/test-secureboot/db.crt + --target=base-penultimate + -t localhost/bootc-penultimate + ." + ) + .run()?; + + // Generate dumpfile from mounted filesystem (build-time view) + println!("=== Generating build-time dumpfile (from mounted filesystem) ==="); + let build_dumpfile = out_dir_abs.join("build.dumpfile"); + let out_vol = format!("{}:/out:z", out_dir_abs.display()); + cmd!( + sh, + "podman run --rm --privileged + -v {out_vol} + --mount=type=image,source=localhost/bootc-penultimate,target=/target + localhost/bootc-penultimate + bootc container compute-composefs-digest /target + --write-dumpfile-to /out/build.dumpfile" + ) + .run()?; + println!("Build-time dumpfile: {}", build_dumpfile.display()); + + // Generate dumpfile from containers-storage (install-time view) + println!("=== Generating install-time dumpfile (from containers-storage) ==="); + let format_arg = "{{.Store.GraphRoot}}"; + let graphroot = cmd!(sh, "podman system info -f {format_arg}").read()?; + let graphroot = graphroot.trim(); + let storage_dumpfile = out_dir_abs.join("storage.dumpfile"); + let storage_vol = format!("{graphroot}:/run/host-container-storage:ro"); + cmd!( + sh, + "podman run --rm --privileged --security-opt=label=disable + -v {out_vol} + -v {storage_vol} + -v /sys:/sys:ro + --tmpfs=/var + localhost/bootc-penultimate + bootc container compute-composefs-digest-from-storage + --write-dumpfile-to /out/storage.dumpfile" + ) + .run()?; + println!("Install-time dumpfile: {}", storage_dumpfile.display()); + + // Compare dumpfiles + println!("=== Comparing dumpfiles ==="); + let diff_result = cmd!(sh, "diff -u {build_dumpfile} {storage_dumpfile}") + .ignore_status() + .output()?; + + if diff_result.status.success() { + println!("SUCCESS: Dumpfiles match!"); + Ok(()) + } else { + let diff_file = out_dir_abs.join("diff.txt"); + std::fs::write(&diff_file, &diff_result.stdout)?; + println!("MISMATCH: Dumpfiles differ! See {}", diff_file.display()); + println!("First 50 lines of diff:"); + let stdout = String::from_utf8_lossy(&diff_result.stdout); + for line in stdout.lines().take(50) { + println!("{line}"); + } + anyhow::bail!("Composefs digest mismatch"); + } +} diff --git a/docs/src/experimental-composefs.md b/docs/src/experimental-composefs.md index 37d94d7cd..7d2b87673 100644 --- a/docs/src/experimental-composefs.md +++ b/docs/src/experimental-composefs.md @@ -11,60 +11,30 @@ The composefs backend is an experimental alternative storage backend that uses [ **Status**: Experimental. The composefs backend is under active development and not yet suitable for production use. The feature is always compiled in as of bootc v1.10.1. -## Key Benefits +A key goal is custom "sealed" images, signed with your own Secure Boot keys. +This is based on [Unified Kernel Images](https://uapi-group.org/specifications/specs/unified_kernel_image/) +that embed a digest of the target container root filesystem, typically alongside a bootloader (such +as systemd-boot) also signed with your key. -- **Native container integration**: Direct use of container image formats without the ostree layer -- **UKI support**: First-class support for Unified Kernel Images (UKIs) and systemd-boot -- **Sealed images**: Enables building cryptographically sealed, securely-bootable images -- **Simpler architecture**: Reduces dependency on ostree as an implementation detail +### UKIs in bootc containers -## Building Sealed Images +There must be exactly one UKI placed in `/boot/EFI/Linux/.efi`. -### Using `just build-sealed` +### Bootloader support -This is an entrypoint focused on *bootc development* itself - it builds bootc -from source. +To use sealed images, ensure that the target container image has systemd-boot, +and does not have `bootupd`. -```bash -just build-sealed -``` +### Installation -We are working on documenting individual steps to build a sealed image outside of -this tooling. +There is a `--composefs-backend` option for `bootc install`; however, if +a UKI and systemd-boot are detected, it will automatically be used. -## How Sealed Images Work +### Developing and testing bootc with sealed composefs -A sealed image includes: -- A Unified Kernel Image (UKI) that combines kernel, initramfs, and boot parameters -- The composefs fsverity digest embedded in the kernel command line -- Secure Boot signatures on both the UKI and systemd-boot loader - -The UKI is placed in `/boot/EFI/Linux/` and includes the composefs digest in its command line: -``` -composefs=${COMPOSEFS_FSVERITY} root=UUID=... -``` - -This enables the boot chain to verify the integrity of the root filesystem. - -## Installation - -When installing a composefs-backend system, use: - -```bash -bootc install to-disk /dev/sdX -``` - -**Note**: Sealed images will require fsverity support on the target filesystem by default. - -## Testing Composefs - -To run the composefs integration tests: - -```bash -just test-composefs -``` - -This builds a sealed image and runs the composefs test suite using `bcvk` (bootc VM tooling). +Use `just variant=composefs-sealeduki-sdboot build` to build a local sealed +UKI, using Secure Boot keys generated in `target/test-secureboot`. This is +not a production path. ## Current Limitations diff --git a/hack/build-sealed b/hack/build-sealed deleted file mode 100755 index 22b668312..000000000 --- a/hack/build-sealed +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -set -euo pipefail -# This should turn into https://github.com/bootc-dev/bootc/issues/1498 - -dn=$(cd $(dirname $0) && pwd) - -variant=$1 -shift -# The un-sealed container image we want to use -input_image=$1 -shift -# The output container image -output_image=$1 -shift - -runv() { - set -x - "$@" -} - -case $variant in - ostree) - # Nothing to do - echo "Not building a sealed image; forwarding tag" - runv podman tag $input_image $output_image - exit 0 - ;; - composefs-sealeduki*) - ;; - *) - echo "Unknown variant=$variant" 1>&2; exit 1 - ;; -esac - -cfs_digest=$(${dn}/compute-composefs-digest $input_image) -runv podman build -t $output_image \ - --build-arg=COMPOSEFS_FSVERITY=${cfs_digest} --build-arg=base=${input_image} "$@" -f Dockerfile.cfsuki . diff --git a/usr/bin/bootc b/usr/bin/bootc new file mode 100755 index 000000000..86226efb7 Binary files /dev/null and b/usr/bin/bootc differ