#![allow(unsafe_code)]
use std::{io::Error, os::fd::AsFd};
use rustix::{
io::Errno,
ioctl::{Opcode, Setter, Updater, ioctl, opcode},
};
use thiserror::Error;
#[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,
#[error("File is opened for writing")]
FileOpenedForWrite,
#[error("Signature verification failed")]
SignatureVerificationFailed,
}
#[derive(Error, Debug)]
pub enum MeasureVerityError {
#[error("{0}")]
Io(#[from] Error),
#[error("fs-verity is not enabled on file")]
VerityMissing,
#[error("fs-verity is not supported by filesystem")]
FilesystemNotSupported,
#[error("Expected algorithm {expected}, found {found}")]
InvalidDigestAlgorithm {
expected: u16,
found: u16,
},
#[error("Expected digest size {expected}")]
InvalidDigestSize {
expected: u16,
},
}
#[repr(C)]
#[derive(Debug)]
struct FsVerityEnableArg {
version: u32,
hash_algorithm: u32,
block_size: u32,
salt_size: u32,
salt_ptr: u64,
sig_size: u32,
__reserved1: u32,
sig_ptr: u64,
__reserved2: [u64; 11],
}
const FS_IOC_ENABLE_VERITY: Opcode = opcode::write::<FsVerityEnableArg>(b'f', 133);
pub fn fs_ioc_enable_verity(
fd: impl AsFd,
hash_algorithm: u8,
block_size: u32,
) -> Result<(), EnableVerityError> {
fs_ioc_enable_verity_with_sig(fd, hash_algorithm, block_size, None)
}
pub fn fs_ioc_enable_verity_with_sig(
fd: impl AsFd,
hash_algorithm: u8,
block_size: u32,
signature: Option<&[u8]>,
) -> Result<(), EnableVerityError> {
let (sig_size, sig_ptr) = match signature {
Some(sig) => (sig.len() as u32, sig.as_ptr() as u64),
None => (0, 0),
};
unsafe {
match ioctl(
fd,
Setter::<{ FS_IOC_ENABLE_VERITY }, FsVerityEnableArg>::new(FsVerityEnableArg {
version: 1,
hash_algorithm: hash_algorithm as u32,
block_size,
salt_size: 0,
salt_ptr: 0,
sig_size,
__reserved1: 0,
sig_ptr,
__reserved2: [0; 11],
}),
) {
Err(Errno::NOTTY) | Err(Errno::OPNOTSUPP) => {
Err(EnableVerityError::FilesystemNotSupported)
}
Err(Errno::EXIST) => Err(EnableVerityError::AlreadyEnabled),
Err(Errno::TXTBSY) => Err(EnableVerityError::FileOpenedForWrite),
Err(Errno::KEYREJECTED) => Err(EnableVerityError::SignatureVerificationFailed),
Err(e) => Err(Error::from(e).into()),
Ok(_) => Ok(()),
}
}
}
#[repr(C)]
#[derive(Debug)]
struct FsVerityDigest<const N: usize> {
digest_algorithm: u16,
digest_size: u16,
digest: [u8; N],
}
const FS_IOC_MEASURE_VERITY: Opcode = opcode::read_write::<FsVerityDigest<0>>(b'f', 134);
pub fn fs_ioc_measure_verity<const N: usize>(
fd: impl AsFd,
expected_algorithm: u8,
) -> Result<[u8; N], MeasureVerityError> {
let digest_size = N as u16;
let digest_algorithm = expected_algorithm as u16;
let mut digest = FsVerityDigest::<N> {
digest_algorithm,
digest_size,
digest: [0u8; N],
};
let r = unsafe {
ioctl(
fd,
Updater::<{ FS_IOC_MEASURE_VERITY }, FsVerityDigest<N>>::new(&mut digest),
)
};
match r {
Ok(()) => {
if digest.digest_algorithm != digest_algorithm {
return Err(MeasureVerityError::InvalidDigestAlgorithm {
expected: digest_algorithm,
found: digest.digest_algorithm,
});
}
if digest.digest_size != digest_size {
return Err(MeasureVerityError::InvalidDigestSize {
expected: digest_size,
});
}
Ok(digest.digest)
}
Err(Errno::NODATA) => Err(MeasureVerityError::VerityMissing),
Err(Errno::NOTTY | Errno::OPNOTSUPP) => Err(MeasureVerityError::FilesystemNotSupported),
Err(Errno::OVERFLOW) => Err(MeasureVerityError::InvalidDigestSize {
expected: digest.digest_size,
}),
Err(e) => Err(Error::from(e).into()),
}
}
#[cfg(test)]
mod tests {
use std::io::Write;
use tempfile::tempfile_in;
use super::*;
fn get_test_tmpdir() -> std::ffi::OsString {
if let Some(path) = std::env::var_os("CFS_TEST_TMPDIR") {
path
} else {
let home = std::env::var("HOME").expect("$HOME must be set when running tests");
let tmp = std::path::PathBuf::from(home).join(".var/tmp");
std::fs::create_dir_all(&tmp).expect("can't create ~/.var/tmp");
tmp.into()
}
}
fn test_tempfile() -> std::fs::File {
tempfile_in(get_test_tmpdir()).unwrap()
}
#[test]
fn test_measure_verity_missing() {
let mut tf = test_tempfile();
tf.write_all(b"test").unwrap();
tf.sync_all().unwrap();
let path = format!("/proc/self/fd/{}", std::os::fd::AsRawFd::as_raw_fd(&tf));
let ro_fd =
rustix::fs::open(&path, rustix::fs::OFlags::RDONLY, rustix::fs::Mode::empty()).unwrap();
assert!(matches!(
fs_ioc_measure_verity::<32>(&ro_fd, 1),
Err(MeasureVerityError::VerityMissing)
));
}
#[test_with::path(/dev/shm)]
#[test]
fn test_measure_verity_not_supported() {
let tf = tempfile_in("/dev/shm").unwrap();
assert!(matches!(
fs_ioc_measure_verity::<32>(&tf, 1),
Err(MeasureVerityError::FilesystemNotSupported)
));
}
#[test_with::path(/dev/shm)]
#[test]
fn test_enable_verity_wrong_fs() {
let file = tempfile_in("/dev/shm").unwrap();
let err = fs_ioc_enable_verity(&file, 1, 4096).unwrap_err();
assert!(matches!(err, EnableVerityError::FilesystemNotSupported));
}
}