Skip to main content

composefs_ioctls/
fsverity.rs

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
8use std::{io::Error, os::fd::AsFd};
9
10use rustix::{
11    io::Errno,
12    ioctl::{Opcode, Setter, Updater, ioctl, opcode},
13};
14use thiserror::Error;
15
16/// Enabling fsverity failed.
17#[derive(Error, Debug)]
18pub 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)]
38pub 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)]
67struct 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)
80const 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)
92pub 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
110pub 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)]
151struct 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)
158const 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.
171pub 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)]
216mod 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}