Skip to content

Commit

Permalink
fsverity: Use thiserror for measurement, add a compare API
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
cgwalters committed Dec 4, 2024
1 parent e10babb commit 0990a6e
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 25 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
69 changes: 60 additions & 9 deletions src/fsverity/ioctl.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -22,7 +34,10 @@ pub struct FsVerityEnableArg {
// #define FS_IOC_ENABLE_VERITY _IOW('f', 133, struct fsverity_enable_arg)
type FsIocEnableVerity = ioctl::WriteOpcode<b'f', 133, FsVerityEnableArg>;

pub fn fs_ioc_enable_verity<F: AsFd, H: FsVerityHashValue>(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<F: AsFd, H: FsVerityHashValue>(fd: F) -> std::io::Result<()> {
unsafe {
ioctl::ioctl(
fd,
Expand All @@ -43,6 +58,7 @@ pub fn fs_ioc_enable_verity<F: AsFd, H: FsVerityHashValue>(fd: F) -> Result<()>
Ok(())
}

/// Core definition of a fsverity digest.
#[repr(C)]
pub struct FsVerityDigest<F> {
digest_algorithm: u16,
Expand All @@ -53,7 +69,10 @@ pub struct FsVerityDigest<F> {
// #define FS_IOC_MEASURE_VERITY _IORW('f', 134, struct fsverity_digest)
type FsIocMeasureVerity = ioctl::ReadWriteOpcode<b'f', 134, FsVerityDigest<()>>;

pub fn fs_ioc_measure_verity<F: AsFd, H: FsVerityHashValue>(fd: F) -> Result<H> {
/// Measure the fsverity digest of the provided file descriptor.
pub fn fs_ioc_measure_verity<F: AsFd, H: FsVerityHashValue>(
fd: F,
) -> Result<Option<H>, MeasureVerityError> {
let digest_size = std::mem::size_of::<H>() as u16;
let digest_algorithm = H::ALGORITHM as u16;

Expand All @@ -63,16 +82,48 @@ pub fn fs_ioc_measure_verity<F: AsFd, H: FsVerityHashValue>(fd: F) -> Result<H>
digest: H::EMPTY,
};

unsafe {
let r = unsafe {
ioctl::ioctl(
fd,
ioctl::Updater::<FsIocMeasureVerity, FsVerityDigest<H>>::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(())
}
}
62 changes: 61 additions & 1 deletion src/fsverity/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
}
Expand All @@ -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<Sha256HashValue> {
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<F: AsFd, H: FsVerityHashValue>(
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(())
}
}
9 changes: 3 additions & 6 deletions src/mount.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use rustix::{
},
};

use crate::fsverity;
use crate::fsverity::{self, sha256_from_str};

struct FsHandle {
pub fd: OwnedFd,
Expand Down Expand Up @@ -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)
Expand Down
15 changes: 6 additions & 9 deletions src/repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -123,12 +123,8 @@ impl Repository {
expected_verity: &Sha256HashValue,
) -> Result<OwnedFd> {
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.
Expand Down Expand Up @@ -194,7 +190,8 @@ impl Repository {
pub fn check_stream(&self, sha256: &Sha256HashValue) -> Result<Option<Sha256HashValue>> {
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))?;

Expand Down

0 comments on commit 0990a6e

Please sign in to comment.