From 439ac377f9f21ea7d935da7037c4e3093fa8487b Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Tue, 29 Aug 2023 14:51:38 -0400 Subject: [PATCH] tests: Add an integration test for composefs signatures Ensure we have some automated test coverage for this. --- tests/inst/src/composefs.rs | 166 +++++++++++++++++++++++++++++++----- tests/inst/src/test.rs | 9 +- 2 files changed, 152 insertions(+), 23 deletions(-) diff --git a/tests/inst/src/composefs.rs b/tests/inst/src/composefs.rs index caa00c0cac..203d32441f 100644 --- a/tests/inst/src/composefs.rs +++ b/tests/inst/src/composefs.rs @@ -1,12 +1,137 @@ +use std::io::Write; use std::os::unix::fs::MetadataExt; use std::path::Path; use anyhow::Result; -use ostree_ext::glib; +use ostree_ext::{gio, glib}; use xshell::cmd; +use crate::test::reboot; + +const BINDING_KEYPATH: &str = "/etc/ostree/initramfs-root-binding.key"; +const PREPARE_ROOT_PATH: &str = "/etc/ostree/prepare-root.conf"; + +struct Keypair { + public: Vec, + private: Vec, +} + +fn generate_raw_ed25519_keypair(sh: &xshell::Shell) -> Result { + let keydata = cmd!(sh, "openssl genpkey -algorithm ed25519 -outform PEM") + .output()? + .stdout; + let mut public = cmd!(sh, "openssl pkey -outform DER -pubout") + .stdin(&keydata) + .output()? + .stdout; + assert_eq!(public.len(), 44); + let _ = public.drain(..12); + let mut seed = cmd!(sh, "openssl pkey -outform DER") + .stdin(&keydata) + .stdin(&keydata) + .output()? + .stdout; + assert_eq!(seed.len(), 48); + let _ = seed.drain(..16); + assert_eq!(seed.len(), 32); + let private = seed.iter().chain(&public).copied().collect::>(); + Ok(Keypair { public, private }) +} + +fn read_booted_metadata() -> Result { + let metadata = std::fs::read("/run/ostree-booted")?; + let metadata = glib::Variant::from_bytes::(&glib::Bytes::from(&metadata)); + Ok(glib::VariantDict::new(Some(&metadata))) +} + +fn verify_composefs_sanity(sh: &xshell::Shell, metadata: &glib::VariantDict) -> Result<()> { + let fstype = cmd!(sh, "findmnt -n -o FSTYPE /").read()?; + assert_eq!(fstype.as_str(), "overlay"); + + assert_eq!(metadata.lookup::("composefs").unwrap(), Some(true)); + + let private_dir = Path::new("/run/ostree/.private"); + assert_eq!( + std::fs::symlink_metadata(private_dir)?.mode() & !libc::S_IFMT, + 0 + ); + assert!(std::fs::read_dir(private_dir.join("cfsroot-lower"))? + .next() + .is_none()); + + Ok(()) +} + +fn prepare_composefs_signed(sh: &xshell::Shell) -> Result<()> { + let sysroot = ostree_ext::ostree::Sysroot::new_default(); + sysroot.load(gio::Cancellable::NONE)?; + + // Generate a keypair, writing the public half to /etc and the private stays in memory + let keypair = generate_raw_ed25519_keypair(sh)?; + let mut pubkey = base64::encode(keypair.public); + pubkey.push_str("\n"); + std::fs::write(BINDING_KEYPATH, pubkey)?; + let mut tmp_privkey = tempfile::NamedTempFile::new()?; + let priv_base64 = base64::encode(keypair.private); + tmp_privkey + .as_file_mut() + .write_all(priv_base64.as_bytes())?; + + // Note rpm-ostree initramfs-etc changes the final commit hash + std::fs::create_dir_all("/etc/ostree")?; + std::fs::write( + PREPARE_ROOT_PATH, + r##"[composefs] +enabled=signed +"##, + )?; + cmd!( + sh, + "rpm-ostree initramfs-etc --track {BINDING_KEYPATH} --track {PREPARE_ROOT_PATH}" + ) + .run()?; + + sysroot.load_if_changed(gio::Cancellable::NONE)?; + let pending_deployment = sysroot.staged_deployment().expect("staged deployment"); + let pending_commit = &pending_deployment.csum(); + + // Temporarily re-commit with composefs metadata, since older rpm-ostree don't do it by default + cmd!(sh, "ostree commit --generate-composefs-metadata --tree=ref={pending_commit} --bootable -b test").run()?; + let target_commit = &sysroot.repo().require_rev("test")?; + cmd!(sh, "rpm-ostree cleanup -p").run()?; + cmd!(sh, "ostree admin deploy --stage {target_commit}").run()?; + + // Sign + let tmp_privkey_path = tmp_privkey.path(); + cmd!( + sh, + "ostree sign -s ed25519 --keys-file {tmp_privkey_path} {target_commit}" + ) + .run()?; + println!("Signed commit"); + // And verify + cmd!( + sh, + "ostree sign --verify --keys-file {BINDING_KEYPATH} {target_commit}" + ) + .run()?; + + // We explicitly throw away the private key now + tmp_privkey.close()?; + + Ok(()) +} + +fn verify_composefs_signed(sh: &xshell::Shell, metadata: &glib::VariantDict) -> Result<()> { + verify_composefs_sanity(sh, metadata)?; + // Verify signature + assert!(metadata.lookup::("composefs.signed").unwrap().is_some()); + cmd!(sh, "journalctl -u ostree-prepare-root --grep='Validated commit signature'").run()?; + Ok(()) +} + pub(crate) fn itest_composefs() -> Result<()> { - let sh = xshell::Shell::new()?; + let sh = &xshell::Shell::new()?; if !cmd!(sh, "ostree --version").read()?.contains("- composefs") { println!("SKIP no composefs support"); return Ok(()); @@ -24,27 +149,24 @@ pub(crate) fn itest_composefs() -> Result<()> { } Some(v) => v, }; - if mark != "1" { - anyhow::bail!("Invalid reboot mark: {mark}") + let metadata = read_booted_metadata()?; + match mark.as_str() { + "1" => { + verify_composefs_sanity(sh, &metadata)?; + prepare_composefs_signed(sh)?; + Err(reboot("2"))?; + Ok(()) + } + "2" => verify_composefs_signed(sh, &metadata), + o => anyhow::bail!("Unrecognized reboot mark {o}"), } +} - let fstype = cmd!(sh, "findmnt -n -o FSTYPE /").read()?; - assert_eq!(fstype.as_str(), "overlay"); - - let metadata = std::fs::read("/run/ostree-booted")?; - let metadata = glib::Variant::from_bytes::(&glib::Bytes::from(&metadata)); - let metadata = glib::VariantDict::new(Some(&metadata)); - - assert_eq!(metadata.lookup::("composefs").unwrap(), Some(true)); - - let private_dir = Path::new("/run/ostree/.private"); - assert_eq!( - std::fs::symlink_metadata(private_dir)?.mode() & !libc::S_IFMT, - 0 - ); - assert!(std::fs::read_dir(private_dir.join("cfsroot-lower"))? - .next() - .is_none()); - +#[test] +fn gen_keypair() -> Result<()> { + let sh = &xshell::Shell::new()?; + let keypair = generate_raw_ed25519_keypair(sh).unwrap(); + assert_eq!(keypair.public.len(), 32); + assert_eq!(keypair.private.len(), 64); Ok(()) } diff --git a/tests/inst/src/test.rs b/tests/inst/src/test.rs index 7508db9d7c..9fe042f307 100644 --- a/tests/inst/src/test.rs +++ b/tests/inst/src/test.rs @@ -169,12 +169,19 @@ pub(crate) fn get_reboot_mark() -> Result> { /// Initiate a clean reboot; on next boot get_reboot_mark() will return `mark`. #[allow(dead_code)] -pub(crate) fn reboot>(mark: M) -> std::io::Error { +pub(crate) fn reboot>(mark: M) -> anyhow::Error { let mark = mark.as_ref(); use std::os::unix::process::CommandExt; + if let Err(e) = std::io::stderr().flush() { + return e.into(); + } + if let Err(e) = std::io::stdout().flush() { + return e.into(); + } std::process::Command::new("/tmp/autopkgtest-reboot") .arg(mark) .exec() + .into() } /// Prepare a reboot - you should then initiate a reboot however you like.