Skip to content

Commit

Permalink
tests: Add an integration test for composefs signatures
Browse files Browse the repository at this point in the history
Ensure we have some automated test coverage for this.
  • Loading branch information
cgwalters committed Aug 30, 2023
1 parent 03a1988 commit 439ac37
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 23 deletions.
166 changes: 144 additions & 22 deletions tests/inst/src/composefs.rs
Original file line number Diff line number Diff line change
@@ -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<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 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::<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(());
Expand All @@ -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::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(())
}
9 changes: 8 additions & 1 deletion tests/inst/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 439ac37

Please sign in to comment.