From 0990a6eb56ca5142a75d0814626143c35150a94f Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Wed, 4 Dec 2024 10:07:37 -0500 Subject: [PATCH] fsverity: Use thiserror for measurement, add a compare API Change the API to use thiserror instead of anyhow, as we want in some cases more precise error mappings. Also by default change the API to return `Option` for callers that want to not have missing verity be a fatal error. Finally, add a high level compare API that most of the callers really wanted. Note I didn't try to add a wrapper error enum for the verity *enablement* part yet. Signed-off-by: Colin Walters --- Cargo.toml | 1 + src/fsverity/ioctl.rs | 69 +++++++++++++++++++++++++++++++++++++------ src/fsverity/mod.rs | 62 +++++++++++++++++++++++++++++++++++++- src/mount.rs | 9 ++---- src/repository.rs | 15 ++++------ 5 files changed, 131 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ac8a8f0..6f33deb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ rustix = { version = "0.38.37", features = ["fs", "mount", "process"] } sha2 = "0.10.8" tar = { version = "0.4.42", default-features = false } tempfile = "3.13.0" +thiserror = "2.0.4" tokio = "1.41.0" zstd = "0.13.2" diff --git a/src/fsverity/ioctl.rs b/src/fsverity/ioctl.rs index f4955af..f0b957a 100644 --- a/src/fsverity/ioctl.rs +++ b/src/fsverity/ioctl.rs @@ -1,10 +1,22 @@ use std::os::fd::AsFd; -use anyhow::Result; +use rustix::io::Errno; use rustix::ioctl; +use thiserror::Error; use super::FsVerityHashValue; +/// Measuring fsverity failed. +#[derive(Error, Debug)] +pub enum MeasureVerityError { + #[error("i/o error")] + Io(#[from] std::io::Error), + #[error("Expected algorithm {expected}, found {found}")] + InvalidDigestAlgorithm { expected: u16, found: u16 }, + #[error("Expected digest size {expected}")] + InvalidDigestSize { expected: u16 }, +} + // See /usr/include/linux/fsverity.h #[repr(C)] pub struct FsVerityEnableArg { @@ -22,7 +34,10 @@ pub struct FsVerityEnableArg { // #define FS_IOC_ENABLE_VERITY _IOW('f', 133, struct fsverity_enable_arg) type FsIocEnableVerity = ioctl::WriteOpcode; -pub fn fs_ioc_enable_verity(fd: F) -> Result<()> { +/// Enable fsverity on the target file. This is a thin safe wrapper for the underlying base `ioctl` +/// and hence all constraints apply such as requiring the file descriptor to already be `O_RDONLY` +/// etc. +pub fn fs_ioc_enable_verity(fd: F) -> std::io::Result<()> { unsafe { ioctl::ioctl( fd, @@ -43,6 +58,7 @@ pub fn fs_ioc_enable_verity(fd: F) -> Result<()> Ok(()) } +/// Core definition of a fsverity digest. #[repr(C)] pub struct FsVerityDigest { digest_algorithm: u16, @@ -53,7 +69,10 @@ pub struct FsVerityDigest { // #define FS_IOC_MEASURE_VERITY _IORW('f', 134, struct fsverity_digest) type FsIocMeasureVerity = ioctl::ReadWriteOpcode>; -pub fn fs_ioc_measure_verity(fd: F) -> Result { +/// Measure the fsverity digest of the provided file descriptor. +pub fn fs_ioc_measure_verity( + fd: F, +) -> Result, MeasureVerityError> { let digest_size = std::mem::size_of::() as u16; let digest_algorithm = H::ALGORITHM as u16; @@ -63,16 +82,48 @@ pub fn fs_ioc_measure_verity(fd: F) -> Result digest: H::EMPTY, }; - unsafe { + let r = unsafe { ioctl::ioctl( fd, ioctl::Updater::>::new(&mut digest), - )?; + ) + }; + match r { + Ok(()) => { + if digest.digest_algorithm != digest_algorithm { + return Err(MeasureVerityError::InvalidDigestAlgorithm { + expected: digest.digest_algorithm, + found: digest_algorithm, + }); + } + if digest.digest_size != digest_size { + return Err(MeasureVerityError::InvalidDigestSize { + expected: digest.digest_size, + }); + } + Ok(Some(digest.digest)) + } + // This function returns Ok(None) if there's no verity digest found. + Err(Errno::NODATA | Errno::NOTTY) => Ok(None), + Err(Errno::OVERFLOW) => Err(MeasureVerityError::InvalidDigestSize { + expected: digest.digest_size, + }), + Err(e) => Err(std::io::Error::from(e).into()), } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fsverity::Sha256HashValue; - if digest.digest_algorithm != digest_algorithm || digest.digest_size != digest_size { - Err(std::io::Error::from(std::io::ErrorKind::InvalidData))? - } else { - Ok(digest.digest) + #[test] + fn test_measure_verity_opt() -> anyhow::Result<()> { + let tf = tempfile::tempfile()?; + assert_eq!( + fs_ioc_measure_verity::<_, Sha256HashValue>(&tf).unwrap(), + None + ); + Ok(()) } } diff --git a/src/fsverity/mod.rs b/src/fsverity/mod.rs index e030fff..3af2d4e 100644 --- a/src/fsverity/mod.rs +++ b/src/fsverity/mod.rs @@ -1,7 +1,13 @@ +use std::os::fd::AsFd; + +use anyhow::Result; +use ioctl::MeasureVerityError; +use thiserror::Error; + pub mod digest; pub mod ioctl; -pub trait FsVerityHashValue { +pub trait FsVerityHashValue: Eq + AsRef<[u8]> { const ALGORITHM: u8; const EMPTY: Self; } @@ -19,3 +25,57 @@ impl FsVerityHashValue for Sha512HashValue { const ALGORITHM: u8 = 2; const EMPTY: Self = [0; 64]; } + +pub fn sha256_from_str(s: &str) -> Result { + let mut r: Sha256HashValue = Default::default(); + hex::decode_to_slice(s, r.as_mut_slice())?; + Ok(r) +} + +/// A verity comparison failed. +#[derive(Error, Debug)] +pub enum CompareVerityError { + #[error("failed to read verity")] + Measure(#[from] MeasureVerityError), + #[error("fsverity is not enabled on target file")] + VerityMissing, + #[error("Expected digest {expected} but found {found}")] + DigestMismatch { expected: String, found: String }, +} + +/// Compare the fsverity digest of the file versus the expected digest. +pub fn compare_verity( + fd: F, + expected: &H, +) -> Result<(), CompareVerityError> { + match ioctl::fs_ioc_measure_verity::<_, H>(fd)? { + Some(ref found) => { + if expected == found { + Ok(()) + } else { + Err(CompareVerityError::DigestMismatch { + expected: hex::encode(expected), + found: hex::encode(found.as_ref()), + }) + } + } + None => Err(CompareVerityError::VerityMissing), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fsverity::Sha256HashValue; + + #[test] + fn test_compare_verity() -> anyhow::Result<()> { + let tf = tempfile::tempfile()?; + let h = Sha256HashValue::default(); + match compare_verity(&tf, &h) { + Err(CompareVerityError::VerityMissing) => {} + o => panic!("Unexpected {o:?}"), + } + Ok(()) + } +} diff --git a/src/mount.rs b/src/mount.rs index 6721d03..190f8be 100644 --- a/src/mount.rs +++ b/src/mount.rs @@ -14,7 +14,7 @@ use rustix::{ }, }; -use crate::fsverity; +use crate::fsverity::{self, sha256_from_str}; struct FsHandle { pub fd: OwnedFd, @@ -192,11 +192,8 @@ impl<'a> MountOptions<'a> { let image = std::fs::File::open(self.image)?; if let Some(expected) = self.digest { - let measured: fsverity::Sha256HashValue = - fsverity::ioctl::fs_ioc_measure_verity(&image)?; - if expected != hex::encode(measured) { - panic!("expected {:?} measured {:?}", expected, measured); - } + let expected = sha256_from_str(expected)?; + fsverity::compare_verity(&image, &expected)?; } mount_fd(image, self.basedir, mountpoint) diff --git a/src/repository.rs b/src/repository.rs index 42a3114..61a6dbb 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -20,6 +20,7 @@ use sha2::{Digest, Sha256}; use crate::{ fsverity::{ + self, digest::FsVerityHasher, ioctl::{fs_ioc_enable_verity, fs_ioc_measure_verity}, FsVerityHashValue, Sha256HashValue, @@ -98,8 +99,7 @@ impl Repository { fs_ioc_enable_verity::<&OwnedFd, Sha256HashValue>(&ro_fd)?; // double-check - let measured_digest: Sha256HashValue = fs_ioc_measure_verity(&ro_fd)?; - assert!(measured_digest == digest); + fsverity::compare_verity(&ro_fd, &digest)?; if let Err(err) = linkat( CWD, @@ -123,12 +123,8 @@ impl Repository { expected_verity: &Sha256HashValue, ) -> Result { let fd = self.openat(filename, OFlags::RDONLY)?; - let measured_verity: Sha256HashValue = fs_ioc_measure_verity(&fd)?; - if measured_verity != *expected_verity { - bail!("bad verity!") - } else { - Ok(fd) - } + fsverity::compare_verity(&fd, expected_verity)?; + Ok(fd) } /// Creates a SplitStreamWriter for writing a split stream. @@ -194,7 +190,8 @@ impl Repository { pub fn check_stream(&self, sha256: &Sha256HashValue) -> Result> { match self.openat(&format!("streams/{}", hex::encode(sha256)), OFlags::RDONLY) { Ok(stream) => { - let measured_verity: Sha256HashValue = fs_ioc_measure_verity(&stream)?; + let measured_verity: Sha256HashValue = fs_ioc_measure_verity(&stream)? + .ok_or(fsverity::CompareVerityError::VerityMissing)?; let mut context = Sha256::new(); let mut split_stream = SplitStreamReader::new(File::from(stream))?;