1#![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#[derive(Error, Debug)]
18pub enum EnableVerityError {
19 #[error("{0}")]
21 Io(#[from] Error),
22 #[error("Filesystem does not support fs-verity")]
24 FilesystemNotSupported,
25 #[error("fs-verity is already enabled on file")]
27 AlreadyEnabled,
28 #[error("File is opened for writing")]
30 FileOpenedForWrite,
31 #[error("Signature verification failed")]
33 SignatureVerificationFailed,
34}
35
36#[derive(Error, Debug)]
38pub enum MeasureVerityError {
39 #[error("{0}")]
41 Io(#[from] Error),
42 #[error("fs-verity is not enabled on file")]
44 VerityMissing,
45 #[error("fs-verity is not supported by filesystem")]
47 FilesystemNotSupported,
48 #[error("Expected algorithm {expected}, found {found}")]
50 InvalidDigestAlgorithm {
51 expected: u16,
53 found: u16,
55 },
56 #[error("Expected digest size {expected}")]
58 InvalidDigestSize {
59 expected: u16,
61 },
62}
63
64#[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
79const FS_IOC_ENABLE_VERITY: Opcode = opcode::write::<FsVerityEnableArg>(b'f', 133);
81
82pub 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
100pub 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#[repr(C)]
150#[derive(Debug)]
151struct FsVerityDigest<const N: usize> {
152 digest_algorithm: u16,
153 digest_size: u16,
154 digest: [u8; N],
155}
156
157const FS_IOC_MEASURE_VERITY: Opcode = opcode::read_write::<FsVerityDigest<0>>(b'f', 134);
159
160pub 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 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}