Skip to content

Commit db7d769

Browse files
committed
Various composefs enhancements
- Add build infrastructure to toplevel to seal - Change the install logic to detect UKIs and automatically enable composefs Signed-off-by: Colin Walters <[email protected]>
1 parent 66e66e4 commit db7d769

File tree

7 files changed

+193
-11
lines changed

7 files changed

+193
-11
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Dockerfile.cfsuki

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Override via --build-arg=base=<image> to use a different base
2+
ARG base=localhost/bootc
3+
# This is where we get the tools to build the UKI
4+
ARG buildroot=quay.io/fedora/fedora:42
5+
FROM $base AS base
6+
7+
FROM $buildroot as buildroot-base
8+
RUN <<EORUN
9+
set -xeuo pipefail
10+
dnf install -y systemd-ukify sbsigntools systemd-boot-unsigned
11+
dnf clean all
12+
EORUN
13+
14+
# This must be provided and computed via cfs oci compute-id
15+
ARG COMPOSEFS_FSVERITY
16+
17+
FROM buildroot-base as kernel
18+
RUN --mount=type=secret,id=key \
19+
--mount=type=secret,id=cert \
20+
--mount=type=bind,from=base,target=/target \
21+
<<EOF
22+
set -eux
23+
24+
# Inject the composefs kernel argument and specify a root with the x86_64 DPS UUID.
25+
# TODO: Discoverable partition fleshed out, or drop root UUID as systemd-stub extension
26+
cmdline="composefs=${COMPOSEFS_FSVERITY} root=UUID=4f68bce3-e8cd-4db1-96e7-fbcaf984b709 rw"
27+
28+
kver=$(cd /target/usr/lib/modules && echo *)
29+
ukify build \
30+
--linux "/target/usr/lib/modules/$kver/vmlinuz" \
31+
--initrd "/target/usr/lib/modules/$kver/initramfs.img" \
32+
--uname="${kver}" \
33+
--cmdline "${cmdline}" \
34+
--os-release "@/target/usr/lib/os-release" \
35+
--signtool sbsign \
36+
--secureboot-private-key "/run/secrets/key" \
37+
--secureboot-certificate "/run/secrets/cert" \
38+
--measure \
39+
--json pretty \
40+
--output "/boot/$kver.efi"
41+
sbsign \
42+
--key "/run/secrets/key" \
43+
--cert "/run/secrets/cert" \
44+
"/usr/lib/systemd/boot/efi/systemd-bootx64.efi" \
45+
--output "/boot/systemd-bootx64.efi"
46+
47+
rm -vf /tmp/cmdline
48+
EOF
49+
50+
FROM base as final
51+
52+
RUN --mount=type=bind,from=kernel,target=/run/kernel <<EOF
53+
kver=$(cd /usr/lib/modules && echo *)
54+
mkdir -p /boot/EFI/Linux
55+
# We put the UKI in /boot for now due to composefs verity not being the
56+
# same due to mtime of /usr/lib/modules being changed
57+
cp /run/kernel/boot/$kver.efi /boot/EFI/Linux/$kver.efi
58+
EOF
59+
60+
FROM base as final-final
61+
COPY --from=final /boot /boot
62+
# Override the default
63+
LABEL containers.bootc=sealed

Justfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
build *ARGS:
1414
podman build --jobs=4 -t localhost/bootc {{ARGS}} .
1515

16+
build-sealed *ARGS:
17+
podman build --jobs=4 -t localhost/bootc-unsealed {{ARGS}} .
18+
cargo xtask build-sealed localhost/bootc-unsealed localhost/bootc
19+
1620
# This container image has additional testing content and utilities
1721
build-integration-test-image *ARGS:
1822
cd hack && podman build --jobs=4 -t localhost/bootc-integration -f Containerfile {{ARGS}} .

crates/lib/src/bootc_composefs/boot.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
use std::ffi::OsStr;
21
use std::fs::create_dir_all;
32
use std::io::Write;
43
use std::path::Path;
4+
use std::ffi::OsStr;
55

66
use anyhow::{anyhow, Context, Result};
77
use bootc_blockdev::find_parent_devices;
@@ -129,6 +129,23 @@ fi
129129
)
130130
}
131131

132+
/// Returns `true` if detect the target rootfs carries a UKI.
133+
pub(crate) fn container_root_has_uki(root: &Dir) -> Result<bool> {
134+
let Some(efi_linux) = root.open_dir_optional(EFI_LINUX)? else {
135+
return Ok(false);
136+
};
137+
for entry in efi_linux.entries()? {
138+
let entry = entry?;
139+
let name = entry.file_name();
140+
let name = Path::new(&name);
141+
let extension = name.extension().and_then(|v| v.to_str());
142+
if extension == Some("efi") {
143+
return Ok(true);
144+
}
145+
}
146+
Ok(false)
147+
}
148+
132149
pub fn get_esp_partition(device: &str) -> Result<(String, Option<String>)> {
133150
let device_info = bootc_blockdev::partitions_of(Utf8Path::new(device))?;
134151
let esp = device_info

crates/lib/src/generator.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,6 @@ ExecStart=bootc internals fixup-etc-fstab\n\
139139
#[cfg(test)]
140140
mod tests {
141141
use camino::Utf8Path;
142-
use cap_std_ext::cmdext::CapStdExtCommandExt as _;
143142

144143
use super::*;
145144

crates/lib/src/install.rs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ pub(crate) struct InstallConfigOpts {
247247
pub(crate) stateroot: Option<String>,
248248
}
249249

250-
#[derive(Debug, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)]
250+
#[derive(Debug, Default, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)]
251251
pub(crate) struct InstallComposefsOpts {
252252
#[clap(long, default_value_t)]
253253
#[serde(default)]
@@ -438,6 +438,10 @@ pub(crate) struct State {
438438
pub(crate) container_root: Dir,
439439
pub(crate) tempdir: TempDir,
440440

441+
/// Set if we have determined that composefs is required
442+
#[allow(dead_code)]
443+
pub(crate) composefs_required: bool,
444+
441445
// If Some, then --composefs_native is passed
442446
#[cfg(feature = "composefs-backend")]
443447
pub(crate) composefs_options: Option<InstallComposefsOpts>,
@@ -793,6 +797,7 @@ async fn install_container(
793797
let sepolicy = sepolicy.as_ref();
794798
let stateroot = state.stateroot();
795799

800+
// TODO factor out this
796801
let (src_imageref, proxy_cfg) = if !state.source.in_host_mountns {
797802
(state.source.imageref.clone(), None)
798803
} else {
@@ -1221,20 +1226,28 @@ async fn verify_target_fetch(
12211226
Ok(())
12221227
}
12231228

1229+
fn root_has_uki(root: &Dir) -> Result<bool> {
1230+
#[cfg(feature = "composefs-backend")]
1231+
return crate::bootc_composefs::boot::container_root_has_uki(root);
1232+
1233+
#[cfg(not(feature = "composefs-backend"))]
1234+
Ok(false)
1235+
}
1236+
12241237
/// Preparation for an install; validates and prepares some (thereafter immutable) global state.
12251238
async fn prepare_install(
12261239
config_opts: InstallConfigOpts,
12271240
source_opts: InstallSourceOpts,
12281241
target_opts: InstallTargetOpts,
1229-
_composefs_opts: Option<InstallComposefsOpts>,
1242+
composefs_options: Option<InstallComposefsOpts>,
12301243
) -> Result<Arc<State>> {
12311244
tracing::trace!("Preparing install");
12321245
let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())
12331246
.context("Opening /")?;
12341247

12351248
let host_is_container = crate::containerenv::is_container(&rootfs);
12361249
let external_source = source_opts.source_imgref.is_some();
1237-
let source = match source_opts.source_imgref {
1250+
let (source, target_rootfs) = match source_opts.source_imgref {
12381251
None => {
12391252
ensure!(host_is_container, "Either --source-imgref must be defined or this command must be executed inside a podman container.");
12401253

@@ -1259,11 +1272,13 @@ async fn prepare_install(
12591272
};
12601273
tracing::trace!("Read container engine info {:?}", container_info);
12611274

1262-
SourceInfo::from_container(&rootfs, &container_info)?
1275+
let source = SourceInfo::from_container(&rootfs, &container_info)?;
1276+
(source, Some(rootfs.try_clone()?))
12631277
}
12641278
Some(source) => {
12651279
crate::cli::require_root(false)?;
1266-
SourceInfo::from_imageref(&source, &rootfs)?
1280+
let source = SourceInfo::from_imageref(&source, &rootfs)?;
1281+
(source, None)
12671282
}
12681283
};
12691284

@@ -1291,6 +1306,15 @@ async fn prepare_install(
12911306
};
12921307
tracing::debug!("Target image reference: {target_imgref}");
12931308

1309+
let composefs_required = if let Some(root) = target_rootfs.as_ref() {
1310+
root_has_uki(root)?
1311+
} else {
1312+
false
1313+
};
1314+
tracing::debug!("Composefs required: {composefs_required}");
1315+
let composefs_options =
1316+
composefs_options.or_else(|| composefs_required.then_some(InstallComposefsOpts::default()));
1317+
12941318
// We need to access devices that are set up by the host udev
12951319
bootc_mount::ensure_mirrored_host_mount("/dev")?;
12961320
// We need to read our own container image (and any logically bound images)
@@ -1371,8 +1395,9 @@ async fn prepare_install(
13711395
container_root: rootfs,
13721396
tempdir,
13731397
host_is_container,
1398+
composefs_required,
13741399
#[cfg(feature = "composefs-backend")]
1375-
composefs_options: _composefs_opts,
1400+
composefs_options,
13761401
});
13771402

13781403
Ok(state)

crates/xtask/src/xtask.rs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ use std::fs::File;
88
use std::io::{BufRead, BufReader, BufWriter, Write};
99
use std::process::Command;
1010

11-
use anyhow::{Context, Result};
11+
use anyhow::{anyhow, Context, Result};
1212
use camino::{Utf8Path, Utf8PathBuf};
1313
use fn_error_context::context;
14+
use serde::Deserialize;
1415
use xshell::{cmd, Shell};
1516

1617
mod man;
@@ -43,6 +44,7 @@ const TASKS: &[(&str, fn(&Shell) -> Result<()>)] = &[
4344
("package", package),
4445
("package-srpm", package_srpm),
4546
("spec", spec),
47+
("build-sealed", build_sealed),
4648
];
4749

4850
fn try_main() -> Result<()> {
@@ -237,6 +239,78 @@ fn spec(sh: &Shell) -> Result<()> {
237239
Ok(())
238240
}
239241

242+
#[derive(Debug, Deserialize)]
243+
#[allow(dead_code)]
244+
#[serde(rename_all = "PascalCase")]
245+
struct ImageInspect {
246+
pub id: String,
247+
pub digest: String,
248+
}
249+
250+
fn build_sealed(sh: &Shell) -> Result<()> {
251+
let args = &std::env::args().collect::<Vec<_>>()[2..];
252+
let input_image = args.get(0).ok_or_else(|| anyhow!("Missing arg: IMAGE"))?;
253+
let output_image = args.get(1).ok_or_else(|| anyhow!("Missing arg: IMAGE"))?;
254+
// If present this should be a path to a directory with secureboot keys.
255+
// If not provided, one will be generated in target/test-secureboot-keys
256+
let secureboot_default = Utf8Path::new("target/test-secureboot");
257+
let secureboot = args.get(2);
258+
let output = cmd!(sh, "podman image inspect {input_image}").output()?;
259+
let inspect: Vec<ImageInspect> = serde_json::from_slice(&output.stdout)?;
260+
let inspect = inspect
261+
.first()
262+
.ok_or_else(|| anyhow!("Failed to get array from inspect"))?;
263+
let digest = inspect.id.as_str();
264+
265+
let tmpdir = sh.create_temp_dir()?;
266+
let tmpdir = tmpdir.path();
267+
let tmpdir: &Utf8Path = tmpdir.try_into().unwrap();
268+
let repoarg = format!("--repo={tmpdir}");
269+
// Inject --insecure so we handle systems without fsverity on the build host
270+
let cfsargs = ["internals", "cfs", "--insecure", repoarg.as_str()];
271+
cmd!(
272+
sh,
273+
"bootc {cfsargs...} oci pull containers-storage:{digest}"
274+
)
275+
.run()?;
276+
let output = cmd!(sh, "bootc {cfsargs...} oci compute-id --bootable {digest}").output()?;
277+
let cfs_digest = String::from_utf8(output.stdout)?;
278+
let cfs_digest = cfs_digest.trim();
279+
280+
let secureboot = if let Some(d) = secureboot.as_deref() {
281+
d.to_owned().into()
282+
} else {
283+
sh.create_dir(secureboot_default)?;
284+
let _g = sh.push_dir(secureboot_default);
285+
if !sh.path_exists("db.cer") {
286+
cmd!(sh, "openssl req -newkey rsa:4096 -nodes -keyout PK.key -new -x509 -sha256 -days 3650 -subj '/CN=Test Platform Key/' -out PK.crt").run()?;
287+
cmd!(sh, "openssl x509 -outform DER -in PK.crt -out PK.cer").run()?;
288+
cmd!(sh, "openssl req -newkey rsa:4096 -nodes -keyout KEK.key -new -x509 -sha256 -days 3650 -subj '/CN=Test Key Exchange Key/' -out KEK.crt").run()?;
289+
cmd!(sh, "openssl x509 -outform DER -in KEK.crt -out KEK.cer").run()?;
290+
cmd!(sh, "openssl req -newkey rsa:4096 -nodes -keyout db.key -new -x509 -sha256 -days 3650 -subj '/CN=Test Signature Database key/' -out db.crt").run()?;
291+
cmd!(sh, "openssl x509 -outform DER -in db.crt -out db.cer").run()?;
292+
}
293+
secureboot_default.to_owned()
294+
};
295+
296+
cmd!(sh, "podman build -t {output_image} --build-arg=COMPOSEFS_FSVERITY={cfs_digest} --build-arg=base={input_image} --secret=id=key,src={secureboot}/db.key --secret=id=cert,src={secureboot}/db.crt -f Dockerfile.cfsuki .").run()?;
297+
298+
sh.create_dir("efi")?;
299+
cmd!(
300+
sh,
301+
"bootc {cfsargs...} oci pull containers-storage:{digest}"
302+
)
303+
.run()?;
304+
cmd!(sh, "bootc {cfsargs...} oci compute-id --bootable {digest}").run()?;
305+
cmd!(
306+
sh,
307+
"bootc {cfsargs...} oci prepare-boot --bootdir tmp/efi {digest}"
308+
)
309+
.run()?;
310+
311+
Ok(())
312+
}
313+
240314
fn impl_srpm(sh: &Shell) -> Result<Utf8PathBuf> {
241315
{
242316
let _g = sh.push_dir("target");

0 commit comments

Comments
 (0)