From f45fbe116ee30319672937255ac5964b805cf846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Cluseau?= Date: Sun, 6 Jul 2025 15:43:42 +0200 Subject: [PATCH] bootstrap: verify signatures --- Cargo.lock | 9 ++++- Cargo.toml | 3 +- Dockerfile | 4 +- build-test-initrd | 21 ++++++++++ modd.test.conf | 7 +--- src/bootstrap/config.rs | 3 ++ src/cmd/init.rs | 21 ++++++---- src/cmd/init/bootstrap.rs | 81 +++++++++++++++++++++++++++++++++++---- test-initrd/config.yaml | 2 + 9 files changed, 127 insertions(+), 24 deletions(-) create mode 100755 build-test-initrd diff --git a/Cargo.lock b/Cargo.lock index 2251147..08f1451 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.9.1" @@ -225,8 +231,9 @@ dependencies = [ [[package]] name = "init" -version = "2.4.0" +version = "2.4.1" dependencies = [ + "base64", "cpio", "env_logger", "eyre", diff --git a/Cargo.toml b/Cargo.toml index 407196f..45bf2d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "init" -version = "2.4.0" +version = "2.4.1" edition = "2024" [profile.release] @@ -28,3 +28,4 @@ zstd = "0.13.3" unix_mode = "0.1.4" cpio = "0.4.1" lz4 = "1.28.1" +base64 = "0.22.1" diff --git a/Dockerfile b/Dockerfile index 9093adf..481bfc4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,7 @@ run . /etc/os-release \ run apk add --no-cache --update -p . musl coreutils \ lvm2 lvm2-extra lvm2-dmeventd udev cryptsetup \ - e2fsprogs lsblk openssh-server wireguard-tools-wg-quick \ + e2fsprogs lsblk openssl openssh-server wireguard-tools-wg-quick \ && rm -rf usr/share/apk var/cache/apk etc/motd copy etc/sshd_config etc/ssh/sshd_config @@ -36,7 +36,7 @@ run mkdir -p bin run var/log; cd bin && for cmd in init-version init-connect boo # check viability run chroot . init-version -run find |cpio -H newc -o >/initrd +run find * |cpio -H newc -oF /initrd # ------------------------------------------------------------------------ from alpine:3.22.0 diff --git a/build-test-initrd b/build-test-initrd new file mode 100755 index 0000000..b57437e --- /dev/null +++ b/build-test-initrd @@ -0,0 +1,21 @@ +#! /bin/bash + +dir=tmp/test-initrd +base_initrd=dist/initrd +test_initrd=test-initrd + +set -ex + +rm -fr $dir +mkdir $dir + +cpio --quiet --extract --file $base_initrd --directory $dir +(cd $test_initrd && find * |cpio --quiet --create -H newc) |cpio --quiet --extract --directory $dir + +(cd $dir && find * |cpio --create -H newc -R 0:0) >test-initrd.cpio + +cpio --quiet -tF test-initrd.cpio +if cpio -tF test-initrd.cpio 2>&1 |grep bytes.of.junk; then echo "bad cpio archive"; exit 1; fi + +lz4 -l9v test-initrd.cpio && mv test-initrd.cpio.lz4 test-initrd.cpio + diff --git a/modd.test.conf b/modd.test.conf index bbebe24..58027f7 100644 --- a/modd.test.conf +++ b/modd.test.conf @@ -1,9 +1,6 @@ modd.test.conf {} -dist/initrd test-initrd/**/* { - prep: cp -f dist/initrd test-initrd.cpio - prep: cd test-initrd && find |cpio -oAv -H newc -F ../test-initrd.cpio - prep: lz4 -l9v test-initrd.cpio && mv test-initrd.cpio.lz4 test-initrd.cpio - prep: if lz4cat test-initrd.cpio | cpio -t 2>&1 |grep bytes.of.junk; then echo "bad cpio archive"; exit 1; fi +dist/initrd build-test-initrd test-initrd/**/* { + prep: ./build-test-initrd prep: kill $(, + #[serde(skip_serializing_if = "Option::is_none")] + pub signer_public_key: Option, + pub bootstrap: Bootstrap, } diff --git a/src/cmd/init.rs b/src/cmd/init.rs index fdd7601..a09d6cd 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -257,10 +257,19 @@ async fn mount(src: Option<&str>, dst: &str, fstype: &str, opts: Option<&str>) { error!("failed to create dir {dst}: {e}"); } + let mut is_file = false; + if let Some(src) = src { + retry_or_ignore(async || { + is_file = (fs::metadata(src).await) + .map_err(|e| format_err!("stat {src} failed: {e}"))? + .is_file(); + Ok(()) + }) + .await; match fstype { "ext4" => { - exec("fsck", &["-p", src]).await; + exec("fsck.ext4", &["-p", src]).await; } _ => {} } @@ -272,12 +281,10 @@ async fn mount(src: Option<&str>, dst: &str, fstype: &str, opts: Option<&str>) { } retry_or_ignore(async || { - // check source, use a loopdev if needed - if let Some(src) = src { - if fs::metadata(src).await?.is_file() { - // loopdev crate has annoying dependencies, just use the normal mount program - return try_exec("mount", &args).await; - } + // if it's a file, we need to use a loopdev + if is_file { + // loopdev crate has annoying dependencies, just use the normal mount program + return try_exec("mount", &args).await; } let (cmd_str, _) = cmd_str("mount", &args); diff --git a/src/cmd/init/bootstrap.rs b/src/cmd/init/bootstrap.rs index 8a01993..b68d1f8 100644 --- a/src/cmd/init/bootstrap.rs +++ b/src/cmd/init/bootstrap.rs @@ -11,10 +11,10 @@ use crate::bootstrap::config::Config; use crate::{dkl, utils}; pub async fn bootstrap(cfg: Config) { + let verifier = retry(async || Verifier::from_config(&cfg)).await; let bs = cfg.bootstrap; retry_or_ignore(async || { - fs::create_dir_all("/boostrap").await?; mount(Some(&bs.dev), "/bootstrap", "ext4", None).await; Ok(()) }) @@ -33,12 +33,12 @@ pub async fn bootstrap(cfg: Config) { .await; let sys_cfg: dkl::Config = retry(async || { - let sys_cfg_bytes = seed_config(base_dir, &bs.seed).await?; + let sys_cfg_bytes = seed_config(base_dir, &bs.seed, &verifier).await?; Ok(serde_yaml::from_slice(&sys_cfg_bytes)?) }) .await; - mount_system(&sys_cfg, base_dir).await; + mount_system(&sys_cfg, base_dir, &verifier).await; retry_or_ignore(async || { let path = "/etc/resolv.conf"; @@ -68,7 +68,67 @@ pub async fn bootstrap(cfg: Config) { exec("chroot", &["/system", "update-ca-certificates"]).await } -async fn seed_config(base_dir: &str, seed_url: &Option) -> Result> { +struct Verifier { + pubkey: Option>, +} +impl Verifier { + fn from_config(cfg: &Config) -> Result { + let Some(ref pubkey) = cfg.signer_public_key else { + return Ok(Self { pubkey: None }); + }; + + use base64::{prelude::BASE64_STANDARD, Engine}; + let pubkey = BASE64_STANDARD.decode(pubkey)?; + let pubkey = Some(pubkey); + + return Ok(Self { pubkey }); + } + + async fn verify_path(&self, path: &str) -> Result<()> { + let Some(ref pubkey) = self.pubkey else { + return Ok(()); + }; + + info!("verifying {path}"); + + let mut pubkey = std::io::Cursor::new(pubkey); + + let sig = format!("{path}.sig"); + + use std::process::Stdio; + use tokio::process::Command; + + let mut openssl = Command::new("openssl") + .stdin(Stdio::piped()) + .args(&[ + "dgst", + "-sha512", + "-verify", + "/dev/stdin", + "-signature", + &sig, + path, + ]) + .spawn()?; + + tokio::io::copy(&mut pubkey, openssl.stdin.as_mut().unwrap()).await?; + + let status = openssl.wait().await?; + if status.success() { + Ok(()) + } else { + Err(format_err!( + "signature verification failed for {path}: {status}" + )) + } + } +} + +async fn seed_config( + base_dir: &str, + seed_url: &Option, + verifier: &Verifier, +) -> Result> { let cfg_path = &format!("{base_dir}/config.yaml"); if fs::try_exists(cfg_path).await? { @@ -92,6 +152,8 @@ async fn seed_config(base_dir: &str, seed_url: &Option) -> Result Result<()> { Ok(()) } -async fn mount_system(cfg: &dkl::Config, bs_dir: &str) { +async fn mount_system(cfg: &dkl::Config, bs_dir: &str, verifier: &Verifier) { let mem_dir = "/mem"; mount(None, mem_dir, "tmpfs", Some("size=512m")).await; @@ -116,14 +178,17 @@ async fn mount_system(cfg: &dkl::Config, bs_dir: &str) { for layer in &cfg.layers { let src = if layer == "modules" { - "/modules.sqfs" + "/modules.sqfs".to_string() } else { - &format!("{bs_dir}/{layer}.fs") + let p = format!("{bs_dir}/{layer}.fs"); + retry(async || verifier.verify_path(&p).await).await; + p }; + let tgt = &format!("{mem_dir}/{layer}.fs"); retry(async || { info!("copying layer {layer} from {src}"); - fs::copy(src, tgt).await?; + fs::copy(&src, tgt).await?; Ok(()) }) .await; diff --git a/test-initrd/config.yaml b/test-initrd/config.yaml index eb44f2a..e52fdb9 100644 --- a/test-initrd/config.yaml +++ b/test-initrd/config.yaml @@ -21,6 +21,8 @@ auths: sshKey: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICkpbU6sf4t0f6XAv9DuW3XH5iLM0AI5rc8PT2jwea1N password: bXlzZWVk:HMSxrg1cYphaPuUYUbtbl/htep/tVYYIQAuvkNMVpw0 # mypass +signer_public_key: MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQA29glSqk7MqoUIjD+UQG+b4v59pTFkn8rYtNhOftTe7uiLUvGFsjNdzP3tW64t/c6YD2p6dtI3oQXGOVQO1vIWPEBc6Sq++BRpQ0FVna+dgNQx8/kLXN9Na0ZYbK7q0haCI7/EHWOX79JFFxJE9HJ67AOMmXwGJ2jrfa1CUnWvfCmT+E= + ssh: listen: "[::]:22" user_ca: /user_ca.pub