tests: Add an integration test for composefs signatures
authorColin Walters <walters@verbum.org>
Tue, 29 Aug 2023 18:51:38 +0000 (14:51 -0400)
committerColin Walters <walters@verbum.org>
Thu, 21 Sep 2023 11:53:19 +0000 (07:53 -0400)
Ensure we have some automated test coverage for this.

tests/inst/src/composefs.rs
tests/inst/src/test.rs

index caa00c0cac4f1e09722f1083f66f8b255f362575..4e0ad66efeb9129b62013da16148451980970ce2 100644 (file)
+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<u8>,
+    private: Vec<u8>,
+}
+
+fn generate_raw_ed25519_keypair(sh: &xshell::Shell) -> Result<Keypair> {
+    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::<Vec<u8>>();
+    Ok(Keypair { public, private })
+}
+
+fn read_booted_metadata() -> Result<glib::VariantDict> {
+    let metadata = std::fs::read("/run/ostree-booted")?;
+    let metadata = glib::Variant::from_bytes::<glib::VariantDict>(&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::<bool>("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 target_commit = &pending_deployment.csum();
+
+    // 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::<String>("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 +150,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::VariantDict>(&glib::Bytes::from(&metadata));
-    let metadata = glib::VariantDict::new(Some(&metadata));
-
-    assert_eq!(metadata.lookup::<bool>("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(())
 }
index 7508db9d7c45c9443925e4d8ae2f48c01c046daa..9fe042f307732b5bd047a7ef40add25a98aec4a5 100644 (file)
@@ -169,12 +169,19 @@ pub(crate) fn get_reboot_mark() -> Result<Option<String>> {
 
 /// Initiate a clean reboot; on next boot get_reboot_mark() will return `mark`.
 #[allow(dead_code)]
-pub(crate) fn reboot<M: AsRef<str>>(mark: M) -> std::io::Error {
+pub(crate) fn reboot<M: AsRef<str>>(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.