mod digest;
mod hashvalue;
mod ioctl;
use std::{io::Error, os::fd::AsFd};
use thiserror::Error;
pub use hashvalue::{FsVerityHashValue, Sha256HashValue, Sha512HashValue};
#[derive(Error, Debug)] pub enum MeasureVerityError {
#[error("{0}")]
Io(#[from] Error),
#[error("fs-verity is not enabled on file")]
VerityMissing,
#[error("Expected algorithm {expected}, found {found}")]
InvalidDigestAlgorithm { expected: u16, found: u16 },
#[error("Expected digest size {expected}")]
InvalidDigestSize { expected: u16 },
}
#[derive(Error, Debug)]
pub enum EnableVerityError {
#[error("{0}")]
Io(#[from] Error),
#[error("Filesystem does not support fs-verity")]
FilesystemNotSupported,
#[error("fs-verity is already enabled on file")]
AlreadyEnabled,
}
#[derive(Error, Debug)]
pub enum CompareVerityError {
#[error("failed to read verity")]
Measure(#[from] MeasureVerityError),
#[error("Expected digest {expected} but found {found}")]
DigestMismatch { expected: String, found: String },
}
pub fn compute_verity<H: FsVerityHashValue>(data: &[u8]) -> H {
digest::FsVerityHasher::<H, 12>::hash(data)
}
pub fn enable_verity<H: FsVerityHashValue>(fd: impl AsFd) -> Result<(), EnableVerityError> {
ioctl::fs_ioc_enable_verity::<H>(fd)
}
pub fn measure_verity<H: FsVerityHashValue>(fd: impl AsFd) -> Result<H, MeasureVerityError> {
ioctl::fs_ioc_measure_verity(fd)
}
pub fn measure_verity_opt<H: FsVerityHashValue>(
fd: impl AsFd,
) -> Result<Option<H>, MeasureVerityError> {
match ioctl::fs_ioc_measure_verity(fd) {
Ok(result) => Ok(Some(result)),
Err(MeasureVerityError::VerityMissing) => Ok(None),
Err(other) => Err(other),
}
}
pub fn ensure_verity_equal(
fd: impl AsFd,
expected: &impl FsVerityHashValue,
) -> Result<(), CompareVerityError> {
let found = measure_verity(fd)?;
if expected == &found {
Ok(())
} else {
Err(CompareVerityError::DigestMismatch {
expected: expected.to_hex(),
found: found.to_hex(),
})
}
}
#[cfg(test)]
mod tests {
use std::{collections::BTreeSet, io::Write};
use rustix::{
fd::OwnedFd,
fs::{open, Mode, OFlags},
};
use tempfile::tempfile_in;
use crate::{test::tempfile, util::proc_self_fd};
use super::*;
fn rdonly_file_with(data: &[u8]) -> OwnedFd {
let mut file = tempfile();
file.write_all(data).unwrap();
file.sync_data().unwrap();
let fd = open(
proc_self_fd(&file),
OFlags::RDONLY | OFlags::CLOEXEC,
Mode::empty(),
)
.unwrap();
drop(file); fd
}
#[test]
fn test_verity_missing() {
let tf = rdonly_file_with(b"");
assert!(matches!(
measure_verity::<Sha256HashValue>(&tf).unwrap_err(),
MeasureVerityError::VerityMissing
));
assert!(measure_verity_opt::<Sha256HashValue>(&tf)
.unwrap()
.is_none());
assert!(matches!(
ensure_verity_equal(&tf, &Sha256HashValue::EMPTY).unwrap_err(),
CompareVerityError::Measure(MeasureVerityError::VerityMissing)
));
}
#[test]
fn test_verity_simple() {
let tf = rdonly_file_with(b"hello world");
enable_verity::<Sha256HashValue>(&tf).unwrap();
assert!(matches!(
enable_verity::<Sha256HashValue>(&tf).unwrap_err(),
EnableVerityError::AlreadyEnabled
));
assert_eq!(
measure_verity::<Sha256HashValue>(&tf).unwrap().to_hex(),
"1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7d5f56362ba64"
);
assert_eq!(
measure_verity_opt::<Sha256HashValue>(&tf)
.unwrap()
.unwrap()
.to_hex(),
"1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7d5f56362ba64"
);
ensure_verity_equal(
&tf,
&Sha256HashValue::from_hex(
"1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7d5f56362ba64",
)
.unwrap(),
)
.unwrap();
let Err(CompareVerityError::DigestMismatch { expected, found }) = ensure_verity_equal(
&tf,
&Sha256HashValue::from_hex(
"1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7000000000000",
)
.unwrap(),
) else {
panic!("Didn't fail with expected error");
};
assert_eq!(
expected,
"1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7000000000000"
);
assert_eq!(
found,
"1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7d5f56362ba64"
);
}
#[test_with::path(/dev/shm)]
#[test]
fn test_verity_error_noverity() {
let tf = tempfile_in("/dev/shm").unwrap();
assert!(matches!(
enable_verity::<Sha256HashValue>(&tf).unwrap_err(),
EnableVerityError::FilesystemNotSupported
));
assert!(matches!(
measure_verity::<Sha256HashValue>(&tf).unwrap_err(),
MeasureVerityError::VerityMissing
));
assert!(measure_verity_opt::<Sha256HashValue>(&tf)
.unwrap()
.is_none());
assert!(matches!(
ensure_verity_equal(&tf, &Sha256HashValue::EMPTY).unwrap_err(),
CompareVerityError::Measure(MeasureVerityError::VerityMissing)
));
}
#[test]
fn test_verity_wrongdigest_sha512_sha256() {
let tf = rdonly_file_with(b"hello world");
enable_verity::<Sha512HashValue>(&tf).unwrap();
assert!(matches!(
measure_verity::<Sha256HashValue>(&tf).unwrap_err(),
MeasureVerityError::InvalidDigestSize { .. }
));
assert!(matches!(
measure_verity_opt::<Sha256HashValue>(&tf).unwrap_err(),
MeasureVerityError::InvalidDigestSize { .. }
));
assert!(matches!(
ensure_verity_equal(&tf, &Sha256HashValue::EMPTY).unwrap_err(),
CompareVerityError::Measure(MeasureVerityError::InvalidDigestSize { .. })
));
}
#[test]
fn test_verity_wrongdigest_sha256_sha512() {
let tf = rdonly_file_with(b"hello world");
enable_verity::<Sha256HashValue>(&tf).unwrap();
assert!(matches!(
measure_verity::<Sha512HashValue>(&tf).unwrap_err(),
MeasureVerityError::InvalidDigestAlgorithm { .. }
));
assert!(matches!(
measure_verity_opt::<Sha512HashValue>(&tf).unwrap_err(),
MeasureVerityError::InvalidDigestAlgorithm { .. }
));
assert!(matches!(
ensure_verity_equal(&tf, &Sha512HashValue::EMPTY).unwrap_err(),
CompareVerityError::Measure(MeasureVerityError::InvalidDigestAlgorithm { .. })
));
}
#[test]
fn crosscheck_interesting_cases() {
let mut cases = BTreeSet::new();
for arity in [32, 64] {
for layer4 in [ 0 ] {
for layer3 in [-1, 0, 1] {
for layer2 in [-1, 0, 1] {
for layer1 in [-1, 0, 1] {
for layer0 in [-1, 0, 1] {
let candidate = layer4 * (arity * arity * arity * arity)
+ layer3 * (arity * arity * arity)
+ layer2 * (arity * arity)
+ layer1 * arity
+ layer0;
if let Ok(size) = usize::try_from(candidate) {
cases.insert(size);
}
}
}
}
}
}
}
fn assert_kernel_equal<H: FsVerityHashValue>(data: &[u8], expected: H) {
let fd = rdonly_file_with(data);
enable_verity::<H>(&fd).unwrap();
ensure_verity_equal(&fd, &expected).unwrap();
}
for size in cases {
let data = vec![0x5a; size];
assert_kernel_equal(&data, compute_verity::<Sha256HashValue>(&data));
assert_kernel_equal(&data, compute_verity::<Sha512HashValue>(&data));
}
}
}