bootstrap: verify signatures

This commit is contained in:
Mikaël Cluseau
2025-07-06 15:43:42 +02:00
parent 8fcd2d6684
commit f45fbe116e
9 changed files with 127 additions and 24 deletions

9
Cargo.lock generated
View File

@ -91,6 +91,12 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.9.1" version = "2.9.1"
@ -225,8 +231,9 @@ dependencies = [
[[package]] [[package]]
name = "init" name = "init"
version = "2.4.0" version = "2.4.1"
dependencies = [ dependencies = [
"base64",
"cpio", "cpio",
"env_logger", "env_logger",
"eyre", "eyre",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "init" name = "init"
version = "2.4.0" version = "2.4.1"
edition = "2024" edition = "2024"
[profile.release] [profile.release]
@ -28,3 +28,4 @@ zstd = "0.13.3"
unix_mode = "0.1.4" unix_mode = "0.1.4"
cpio = "0.4.1" cpio = "0.4.1"
lz4 = "1.28.1" lz4 = "1.28.1"
base64 = "0.22.1"

View File

@ -19,7 +19,7 @@ run . /etc/os-release \
run apk add --no-cache --update -p . musl coreutils \ run apk add --no-cache --update -p . musl coreutils \
lvm2 lvm2-extra lvm2-dmeventd udev cryptsetup \ 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 && rm -rf usr/share/apk var/cache/apk etc/motd
copy etc/sshd_config etc/ssh/sshd_config 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 # check viability
run chroot . init-version run chroot . init-version
run find |cpio -H newc -o >/initrd run find * |cpio -H newc -oF /initrd
# ------------------------------------------------------------------------ # ------------------------------------------------------------------------
from alpine:3.22.0 from alpine:3.22.0

21
build-test-initrd Executable file
View File

@ -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

View File

@ -1,9 +1,6 @@
modd.test.conf {} modd.test.conf {}
dist/initrd test-initrd/**/* { dist/initrd build-test-initrd test-initrd/**/* {
prep: cp -f dist/initrd test-initrd.cpio prep: ./build-test-initrd
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
prep: kill $(<qemu.pid) prep: kill $(<qemu.pid)
} }

View File

@ -30,6 +30,9 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub crypt: Vec<CryptDev>, pub crypt: Vec<CryptDev>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signer_public_key: Option<String>,
pub bootstrap: Bootstrap, pub bootstrap: Bootstrap,
} }

View File

@ -257,10 +257,19 @@ async fn mount(src: Option<&str>, dst: &str, fstype: &str, opts: Option<&str>) {
error!("failed to create dir {dst}: {e}"); error!("failed to create dir {dst}: {e}");
} }
let mut is_file = false;
if let Some(src) = src { 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 { match fstype {
"ext4" => { "ext4" => {
exec("fsck", &["-p", src]).await; exec("fsck.ext4", &["-p", src]).await;
} }
_ => {} _ => {}
} }
@ -272,13 +281,11 @@ async fn mount(src: Option<&str>, dst: &str, fstype: &str, opts: Option<&str>) {
} }
retry_or_ignore(async || { retry_or_ignore(async || {
// check source, use a loopdev if needed // if it's a file, we need to use a loopdev
if let Some(src) = src { if is_file {
if fs::metadata(src).await?.is_file() {
// loopdev crate has annoying dependencies, just use the normal mount program // loopdev crate has annoying dependencies, just use the normal mount program
return try_exec("mount", &args).await; return try_exec("mount", &args).await;
} }
}
let (cmd_str, _) = cmd_str("mount", &args); let (cmd_str, _) = cmd_str("mount", &args);
let flags = nix::mount::MsFlags::empty(); let flags = nix::mount::MsFlags::empty();

View File

@ -11,10 +11,10 @@ use crate::bootstrap::config::Config;
use crate::{dkl, utils}; use crate::{dkl, utils};
pub async fn bootstrap(cfg: Config) { pub async fn bootstrap(cfg: Config) {
let verifier = retry(async || Verifier::from_config(&cfg)).await;
let bs = cfg.bootstrap; let bs = cfg.bootstrap;
retry_or_ignore(async || { retry_or_ignore(async || {
fs::create_dir_all("/boostrap").await?;
mount(Some(&bs.dev), "/bootstrap", "ext4", None).await; mount(Some(&bs.dev), "/bootstrap", "ext4", None).await;
Ok(()) Ok(())
}) })
@ -33,12 +33,12 @@ pub async fn bootstrap(cfg: Config) {
.await; .await;
let sys_cfg: dkl::Config = retry(async || { 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)?) Ok(serde_yaml::from_slice(&sys_cfg_bytes)?)
}) })
.await; .await;
mount_system(&sys_cfg, base_dir).await; mount_system(&sys_cfg, base_dir, &verifier).await;
retry_or_ignore(async || { retry_or_ignore(async || {
let path = "/etc/resolv.conf"; let path = "/etc/resolv.conf";
@ -68,7 +68,67 @@ pub async fn bootstrap(cfg: Config) {
exec("chroot", &["/system", "update-ca-certificates"]).await exec("chroot", &["/system", "update-ca-certificates"]).await
} }
async fn seed_config(base_dir: &str, seed_url: &Option<String>) -> Result<Vec<u8>> { struct Verifier {
pubkey: Option<Vec<u8>>,
}
impl Verifier {
fn from_config(cfg: &Config) -> Result<Self> {
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<String>,
verifier: &Verifier,
) -> Result<Vec<u8>> {
let cfg_path = &format!("{base_dir}/config.yaml"); let cfg_path = &format!("{base_dir}/config.yaml");
if fs::try_exists(cfg_path).await? { if fs::try_exists(cfg_path).await? {
@ -92,6 +152,8 @@ async fn seed_config(base_dir: &str, seed_url: &Option<String>) -> Result<Vec<u8
return Err(format_err!("{cfg_path} does not exist after seeding")); return Err(format_err!("{cfg_path} does not exist after seeding"));
} }
verifier.verify_path(&cfg_path).await?;
Ok(fs::read(cfg_path).await?) Ok(fs::read(cfg_path).await?)
} }
@ -107,7 +169,7 @@ async fn fetch_bootstrap(seed_url: &str, output_file: &str) -> Result<()> {
Ok(()) 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"; let mem_dir = "/mem";
mount(None, mem_dir, "tmpfs", Some("size=512m")).await; 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 { for layer in &cfg.layers {
let src = if layer == "modules" { let src = if layer == "modules" {
"/modules.sqfs" "/modules.sqfs".to_string()
} else { } 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"); let tgt = &format!("{mem_dir}/{layer}.fs");
retry(async || { retry(async || {
info!("copying layer {layer} from {src}"); info!("copying layer {layer} from {src}");
fs::copy(src, tgt).await?; fs::copy(&src, tgt).await?;
Ok(()) Ok(())
}) })
.await; .await;

View File

@ -21,6 +21,8 @@ auths:
sshKey: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICkpbU6sf4t0f6XAv9DuW3XH5iLM0AI5rc8PT2jwea1N sshKey: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICkpbU6sf4t0f6XAv9DuW3XH5iLM0AI5rc8PT2jwea1N
password: bXlzZWVk:HMSxrg1cYphaPuUYUbtbl/htep/tVYYIQAuvkNMVpw0 # mypass password: bXlzZWVk:HMSxrg1cYphaPuUYUbtbl/htep/tVYYIQAuvkNMVpw0 # mypass
signer_public_key: MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQA29glSqk7MqoUIjD+UQG+b4v59pTFkn8rYtNhOftTe7uiLUvGFsjNdzP3tW64t/c6YD2p6dtI3oQXGOVQO1vIWPEBc6Sq++BRpQ0FVna+dgNQx8/kLXN9Na0ZYbK7q0haCI7/EHWOX79JFFxJE9HJ67AOMmXwGJ2jrfa1CUnWvfCmT+E=
ssh: ssh:
listen: "[::]:22" listen: "[::]:22"
user_ca: /user_ca.pub user_ca: /user_ca.pub