|
| 1 | +//! Low-level ioctl interfaces for fs-verity kernel operations. |
| 2 | +//! |
| 3 | +//! This module provides safe wrappers around the Linux fs-verity ioctls |
| 4 | +//! for enabling and measuring fs-verity on files. |
| 5 | +
|
| 6 | +#![allow(unsafe_code)] |
| 7 | + |
| 8 | +use std::{io::Error, os::fd::AsFd}; |
| 9 | + |
| 10 | +use rustix::{ |
| 11 | + io::Errno, |
| 12 | + ioctl::{ioctl, opcode, Opcode, Setter, Updater}, |
| 13 | +}; |
| 14 | +use thiserror::Error; |
| 15 | + |
| 16 | +/// Enabling fsverity failed. |
| 17 | +#[derive(Error, Debug)] |
| 18 | +pub enum EnableVerityError { |
| 19 | + /// I/O operation failed. |
| 20 | + #[error("{0}")] |
| 21 | + Io(#[from] Error), |
| 22 | + /// The filesystem does not support fs-verity. |
| 23 | + #[error("Filesystem does not support fs-verity")] |
| 24 | + FilesystemNotSupported, |
| 25 | + /// fs-verity is already enabled on the file. |
| 26 | + #[error("fs-verity is already enabled on file")] |
| 27 | + AlreadyEnabled, |
| 28 | + /// The file has an open writable file descriptor. |
| 29 | + #[error("File is opened for writing")] |
| 30 | + FileOpenedForWrite, |
| 31 | + /// Signature verification failed (when using kernel signatures). |
| 32 | + #[error("Signature verification failed")] |
| 33 | + SignatureVerificationFailed, |
| 34 | +} |
| 35 | + |
| 36 | +/// Measuring fsverity failed. |
| 37 | +#[derive(Error, Debug)] |
| 38 | +pub enum MeasureVerityError { |
| 39 | + /// I/O operation failed. |
| 40 | + #[error("{0}")] |
| 41 | + Io(#[from] Error), |
| 42 | + /// fs-verity is not enabled on the file. |
| 43 | + #[error("fs-verity is not enabled on file")] |
| 44 | + VerityMissing, |
| 45 | + /// The filesystem does not support fs-verity. |
| 46 | + #[error("fs-verity is not supported by filesystem")] |
| 47 | + FilesystemNotSupported, |
| 48 | + /// The hash algorithm does not match the expected algorithm. |
| 49 | + #[error("Expected algorithm {expected}, found {found}")] |
| 50 | + InvalidDigestAlgorithm { |
| 51 | + /// The expected algorithm identifier. |
| 52 | + expected: u16, |
| 53 | + /// The actual algorithm identifier found. |
| 54 | + found: u16, |
| 55 | + }, |
| 56 | + /// The digest size does not match the expected size. |
| 57 | + #[error("Expected digest size {expected}")] |
| 58 | + InvalidDigestSize { |
| 59 | + /// The expected digest size in bytes. |
| 60 | + expected: u16, |
| 61 | + }, |
| 62 | +} |
| 63 | + |
| 64 | +// See /usr/include/linux/fsverity.h |
| 65 | +#[repr(C)] |
| 66 | +#[derive(Debug)] |
| 67 | +struct FsVerityEnableArg { |
| 68 | + version: u32, |
| 69 | + hash_algorithm: u32, |
| 70 | + block_size: u32, |
| 71 | + salt_size: u32, |
| 72 | + salt_ptr: u64, |
| 73 | + sig_size: u32, |
| 74 | + __reserved1: u32, |
| 75 | + sig_ptr: u64, |
| 76 | + __reserved2: [u64; 11], |
| 77 | +} |
| 78 | + |
| 79 | +// #define FS_IOC_ENABLE_VERITY _IOW('f', 133, struct fsverity_enable_arg) |
| 80 | +const FS_IOC_ENABLE_VERITY: Opcode = opcode::write::<FsVerityEnableArg>(b'f', 133); |
| 81 | + |
| 82 | +/// Enable fs-verity on the target file without a signature. |
| 83 | +/// |
| 84 | +/// This is a thin safe wrapper for the `FS_IOC_ENABLE_VERITY` ioctl. |
| 85 | +/// The file descriptor must be opened `O_RDONLY` and there must be no |
| 86 | +/// other writable file descriptors or mappings for the file. |
| 87 | +/// |
| 88 | +/// # Arguments |
| 89 | +/// * `fd` - File descriptor opened O_RDONLY |
| 90 | +/// * `hash_algorithm` - Algorithm ID (1 = SHA-256, 2 = SHA-512) |
| 91 | +/// * `block_size` - Block size (typically 4096) |
| 92 | +pub fn fs_ioc_enable_verity( |
| 93 | + fd: impl AsFd, |
| 94 | + hash_algorithm: u8, |
| 95 | + block_size: u32, |
| 96 | +) -> Result<(), EnableVerityError> { |
| 97 | + fs_ioc_enable_verity_with_sig(fd, hash_algorithm, block_size, None) |
| 98 | +} |
| 99 | + |
| 100 | +/// Enable fs-verity on the target file with an optional PKCS#7 signature. |
| 101 | +/// |
| 102 | +/// When a signature is provided, the kernel will verify it against keys |
| 103 | +/// in the `.fs-verity` keyring before enabling verity. |
| 104 | +/// |
| 105 | +/// # Arguments |
| 106 | +/// * `fd` - File descriptor opened O_RDONLY |
| 107 | +/// * `hash_algorithm` - Algorithm ID (1 = SHA-256, 2 = SHA-512) |
| 108 | +/// * `block_size` - Block size (typically 4096) |
| 109 | +/// * `signature` - Optional PKCS#7 DER-encoded signature |
| 110 | +pub fn fs_ioc_enable_verity_with_sig( |
| 111 | + fd: impl AsFd, |
| 112 | + hash_algorithm: u8, |
| 113 | + block_size: u32, |
| 114 | + signature: Option<&[u8]>, |
| 115 | +) -> Result<(), EnableVerityError> { |
| 116 | + let (sig_size, sig_ptr) = match signature { |
| 117 | + Some(sig) => (sig.len() as u32, sig.as_ptr() as u64), |
| 118 | + None => (0, 0), |
| 119 | + }; |
| 120 | + |
| 121 | + unsafe { |
| 122 | + match ioctl( |
| 123 | + fd, |
| 124 | + Setter::<{ FS_IOC_ENABLE_VERITY }, FsVerityEnableArg>::new(FsVerityEnableArg { |
| 125 | + version: 1, |
| 126 | + hash_algorithm: hash_algorithm as u32, |
| 127 | + block_size, |
| 128 | + salt_size: 0, |
| 129 | + salt_ptr: 0, |
| 130 | + sig_size, |
| 131 | + __reserved1: 0, |
| 132 | + sig_ptr, |
| 133 | + __reserved2: [0; 11], |
| 134 | + }), |
| 135 | + ) { |
| 136 | + Err(Errno::NOTTY) | Err(Errno::OPNOTSUPP) => { |
| 137 | + Err(EnableVerityError::FilesystemNotSupported) |
| 138 | + } |
| 139 | + Err(Errno::EXIST) => Err(EnableVerityError::AlreadyEnabled), |
| 140 | + Err(Errno::TXTBSY) => Err(EnableVerityError::FileOpenedForWrite), |
| 141 | + Err(Errno::KEYREJECTED) => Err(EnableVerityError::SignatureVerificationFailed), |
| 142 | + Err(e) => Err(Error::from(e).into()), |
| 143 | + Ok(_) => Ok(()), |
| 144 | + } |
| 145 | + } |
| 146 | +} |
| 147 | + |
| 148 | +/// Core definition of a fsverity digest returned by the kernel. |
| 149 | +#[repr(C)] |
| 150 | +#[derive(Debug)] |
| 151 | +struct FsVerityDigest<const N: usize> { |
| 152 | + digest_algorithm: u16, |
| 153 | + digest_size: u16, |
| 154 | + digest: [u8; N], |
| 155 | +} |
| 156 | + |
| 157 | +// #define FS_IOC_MEASURE_VERITY _IORW('f', 134, struct fsverity_digest) |
| 158 | +const FS_IOC_MEASURE_VERITY: Opcode = opcode::read_write::<FsVerityDigest<0>>(b'f', 134); |
| 159 | + |
| 160 | +/// Measure the fs-verity digest of a file. |
| 161 | +/// |
| 162 | +/// Returns the raw digest bytes if successful. The generic parameter `N` |
| 163 | +/// specifies the expected digest size (32 for SHA-256, 64 for SHA-512). |
| 164 | +/// |
| 165 | +/// # Arguments |
| 166 | +/// * `fd` - File descriptor to measure |
| 167 | +/// * `expected_algorithm` - Expected algorithm ID (1 = SHA-256, 2 = SHA-512) |
| 168 | +/// |
| 169 | +/// # Returns |
| 170 | +/// The digest bytes on success. |
| 171 | +pub fn fs_ioc_measure_verity<const N: usize>( |
| 172 | + fd: impl AsFd, |
| 173 | + expected_algorithm: u8, |
| 174 | +) -> Result<[u8; N], MeasureVerityError> { |
| 175 | + let digest_size = N as u16; |
| 176 | + let digest_algorithm = expected_algorithm as u16; |
| 177 | + |
| 178 | + let mut digest = FsVerityDigest::<N> { |
| 179 | + digest_algorithm, |
| 180 | + digest_size, |
| 181 | + digest: [0u8; N], |
| 182 | + }; |
| 183 | + |
| 184 | + let r = unsafe { |
| 185 | + ioctl( |
| 186 | + fd, |
| 187 | + Updater::<{ FS_IOC_MEASURE_VERITY }, FsVerityDigest<N>>::new(&mut digest), |
| 188 | + ) |
| 189 | + }; |
| 190 | + |
| 191 | + match r { |
| 192 | + Ok(()) => { |
| 193 | + if digest.digest_algorithm != digest_algorithm { |
| 194 | + return Err(MeasureVerityError::InvalidDigestAlgorithm { |
| 195 | + expected: digest_algorithm, |
| 196 | + found: digest.digest_algorithm, |
| 197 | + }); |
| 198 | + } |
| 199 | + if digest.digest_size != digest_size { |
| 200 | + return Err(MeasureVerityError::InvalidDigestSize { |
| 201 | + expected: digest_size, |
| 202 | + }); |
| 203 | + } |
| 204 | + Ok(digest.digest) |
| 205 | + } |
| 206 | + Err(Errno::NODATA) => Err(MeasureVerityError::VerityMissing), |
| 207 | + Err(Errno::NOTTY | Errno::OPNOTSUPP) => Err(MeasureVerityError::FilesystemNotSupported), |
| 208 | + Err(Errno::OVERFLOW) => Err(MeasureVerityError::InvalidDigestSize { |
| 209 | + expected: digest.digest_size, |
| 210 | + }), |
| 211 | + Err(e) => Err(Error::from(e).into()), |
| 212 | + } |
| 213 | +} |
| 214 | + |
| 215 | +#[cfg(test)] |
| 216 | +mod tests { |
| 217 | + use std::io::Write; |
| 218 | + |
| 219 | + use tempfile::tempfile_in; |
| 220 | + |
| 221 | + use super::*; |
| 222 | + |
| 223 | + fn get_test_tmpdir() -> std::ffi::OsString { |
| 224 | + if let Some(path) = std::env::var_os("CFS_TEST_TMPDIR") { |
| 225 | + path |
| 226 | + } else { |
| 227 | + let home = std::env::var("HOME").expect("$HOME must be set when running tests"); |
| 228 | + let tmp = std::path::PathBuf::from(home).join(".var/tmp"); |
| 229 | + std::fs::create_dir_all(&tmp).expect("can't create ~/.var/tmp"); |
| 230 | + tmp.into() |
| 231 | + } |
| 232 | + } |
| 233 | + |
| 234 | + fn test_tempfile() -> std::fs::File { |
| 235 | + tempfile_in(get_test_tmpdir()).unwrap() |
| 236 | + } |
| 237 | + |
| 238 | + #[test] |
| 239 | + fn test_measure_verity_missing() { |
| 240 | + let mut tf = test_tempfile(); |
| 241 | + tf.write_all(b"test").unwrap(); |
| 242 | + tf.sync_all().unwrap(); |
| 243 | + |
| 244 | + // Re-open read-only |
| 245 | + let path = format!("/proc/self/fd/{}", std::os::fd::AsRawFd::as_raw_fd(&tf)); |
| 246 | + let ro_fd = |
| 247 | + rustix::fs::open(&path, rustix::fs::OFlags::RDONLY, rustix::fs::Mode::empty()).unwrap(); |
| 248 | + |
| 249 | + assert!(matches!( |
| 250 | + fs_ioc_measure_verity::<32>(&ro_fd, 1), |
| 251 | + Err(MeasureVerityError::VerityMissing) |
| 252 | + )); |
| 253 | + } |
| 254 | + |
| 255 | + #[test_with::path(/dev/shm)] |
| 256 | + #[test] |
| 257 | + fn test_measure_verity_not_supported() { |
| 258 | + let tf = tempfile_in("/dev/shm").unwrap(); |
| 259 | + assert!(matches!( |
| 260 | + fs_ioc_measure_verity::<32>(&tf, 1), |
| 261 | + Err(MeasureVerityError::FilesystemNotSupported) |
| 262 | + )); |
| 263 | + } |
| 264 | + |
| 265 | + #[test_with::path(/dev/shm)] |
| 266 | + #[test] |
| 267 | + fn test_enable_verity_wrong_fs() { |
| 268 | + let file = tempfile_in("/dev/shm").unwrap(); |
| 269 | + let err = fs_ioc_enable_verity(&file, 1, 4096).unwrap_err(); |
| 270 | + assert!(matches!(err, EnableVerityError::FilesystemNotSupported)); |
| 271 | + } |
| 272 | +} |
0 commit comments