Skip to main content

dm_verity/
lib.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5pub use fsverity_merkle::MerkleVerifier;
6use fsverity_merkle::{FsVerityHasher, FsVerityHasherOptions};
7use zx_status::Status;
8
9#[derive(thiserror::Error, Debug, PartialEq, Eq)]
10pub enum DmVerityError {
11    #[error("Unsupported hash algorithm: {0}")]
12    UnsupportedAlgorithm(String),
13    #[error("Root hash mismatch")]
14    RootHashMismatch,
15    #[error("Invalid verifier arguments")]
16    InvalidVerifierArgs,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum HashAlgorithm {
21    Sha256,
22    Sha512,
23}
24
25impl std::str::FromStr for HashAlgorithm {
26    type Err = DmVerityError;
27
28    fn from_str(s: &str) -> Result<Self, Self::Err> {
29        match s {
30            "sha256" => Ok(HashAlgorithm::Sha256),
31            "sha512" => Ok(HashAlgorithm::Sha512),
32            _ => Err(DmVerityError::UnsupportedAlgorithm(s.to_string())),
33        }
34    }
35}
36
37impl HashAlgorithm {
38    pub fn as_str(&self) -> &'static str {
39        match self {
40            HashAlgorithm::Sha256 => "sha256",
41            HashAlgorithm::Sha512 => "sha512",
42        }
43    }
44}
45
46/// Optional construction parameters for a dm-verity target.
47///
48/// These correspond to the optional target parameters in Linux dm-verity. They default to false if
49/// all features are disabled.
50///
51/// See https://docs.kernel.org/admin-guide/device-mapper/verity.html
52#[derive(Debug, Clone, Default, PartialEq, Eq)]
53pub struct DmVerityTargetOptionalParams {
54    /// If set, blocks containing only zeroes will bypass verification and always return zeroes
55    /// instead.
56    // TODO(https://fxbug.dev/338125341): Add support for ignore_zero_blocks.
57    pub ignore_zero_blocks: bool,
58    /// If set, the system will restart if data corruption is detected.
59    // TODO(https://fxbug.dev/338243823): Add support for restart on corruption.
60    pub restart_on_corruption: bool,
61}
62
63/// Construction parameters for a dm-verity target.
64///
65/// Mirrors the mandatory parameters passed to the dm-verity target in Linux, along with optional
66/// `DmVerityTargetOptionalParams`.
67///
68/// See https://docs.kernel.org/admin-guide/device-mapper/verity.html
69#[derive(Debug, Clone)]
70pub struct DmVerityTargetParams {
71    /// The version of the dm-verity target.
72    pub version: String,
73    /// The path to the data block device to be verified.
74    pub block_device_path: String,
75    /// The path to the block device containing the Merkle tree hashes.
76    pub hash_device_path: String,
77    /// The size of a data block in bytes.
78    pub data_block_size: u64,
79    /// The size of a hash block in bytes.
80    pub hash_block_size: u64,
81    /// The total number of data blocks on the data device.
82    pub num_data_blocks: u64,
83    /// The root block index on the hash device where the Merkle tree begins.
84    pub hash_start_block: u64,
85    /// The cryptographic hash algorithm used (e.g., SHA256 or SHA512).
86    pub hash_algorithm: HashAlgorithm,
87    /// The hexadecimal encoded root digest of the Merkle tree. This hash should be trusted.
88    pub root_digest: String,
89    /// The hexadecimal encoded salt used when hashing blocks.
90    pub salt: String,
91    /// Optional target parameters.
92    pub optional_params: DmVerityTargetOptionalParams,
93}
94
95/// Creates a new dm-verity Merkle tree verifier and verifies the leaf hashes against the trusted
96/// root digest found in `params`.
97pub fn create_verifier(
98    params: &DmVerityTargetParams,
99    leaf_hashes: Box<[u8]>,
100) -> Result<MerkleVerifier, DmVerityError> {
101    let salt_bytes = hex::decode(&params.salt).map_err(|_| DmVerityError::InvalidVerifierArgs)?;
102
103    let options = FsVerityHasherOptions::new_dmverity(salt_bytes, params.hash_block_size as usize);
104
105    let hasher = match params.hash_algorithm {
106        HashAlgorithm::Sha256 => FsVerityHasher::Sha256(options),
107        HashAlgorithm::Sha512 => FsVerityHasher::Sha512(options),
108    };
109
110    let expected_root_bytes =
111        hex::decode(&params.root_digest).map_err(|_| DmVerityError::InvalidVerifierArgs)?;
112
113    let verifier =
114        MerkleVerifier::new(&expected_root_bytes, leaf_hashes, hasher).map_err(|status| {
115            match status {
116                Status::IO_DATA_INTEGRITY => DmVerityError::RootHashMismatch,
117                _ => DmVerityError::InvalidVerifierArgs,
118            }
119        })?;
120
121    Ok(verifier)
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use fsverity_merkle::MerkleTree;
128    use std::str::FromStr;
129    use test_case::test_case;
130
131    const TEST_BLOCK_SIZE: u64 = 128;
132
133    fn create_verifier_test(
134        params: &DmVerityTargetParams,
135        leaf_hashes: &[u8],
136    ) -> Result<MerkleVerifier, DmVerityError> {
137        create_verifier(params, leaf_hashes.to_vec().into_boxed_slice())
138    }
139
140    fn create_data(size: usize) -> Vec<u8> {
141        let mut data = vec![0xABu8; size];
142        for (i, block) in data.chunks_mut(TEST_BLOCK_SIZE as usize).enumerate() {
143            let index_bytes = (i as u64).to_le_bytes();
144            let len = std::cmp::min(block.len(), index_bytes.len());
145            block[0..len].copy_from_slice(&index_bytes[0..len]);
146        }
147        data
148    }
149
150    fn build_test_verifier(
151        data: &[u8],
152        hash_algorithm: HashAlgorithm,
153        salt: &str,
154    ) -> (MerkleVerifier, DmVerityTargetParams) {
155        let salt_bytes = hex::decode(salt).expect("Failed to decode salt");
156        let hasher = match hash_algorithm {
157            HashAlgorithm::Sha256 => FsVerityHasher::Sha256(FsVerityHasherOptions::new_dmverity(
158                salt_bytes.clone(),
159                TEST_BLOCK_SIZE as usize,
160            )),
161            HashAlgorithm::Sha512 => FsVerityHasher::Sha512(FsVerityHasherOptions::new_dmverity(
162                salt_bytes.clone(),
163                TEST_BLOCK_SIZE as usize,
164            )),
165        };
166
167        let tree = MerkleTree::from_data(data, hasher);
168        let root_digest = hex::encode(tree.root());
169        let leaf_hashes = tree.leaf_hashes();
170
171        let params = DmVerityTargetParams {
172            version: "1".to_string(),
173            block_device_path: "mock".to_string(),
174            hash_device_path: "mock".to_string(),
175            data_block_size: TEST_BLOCK_SIZE,
176            hash_block_size: TEST_BLOCK_SIZE,
177            num_data_blocks: (data.len() as u64).div_ceil(TEST_BLOCK_SIZE),
178            hash_start_block: 0,
179            hash_algorithm,
180            root_digest,
181            salt: salt.to_string(),
182            optional_params: DmVerityTargetOptionalParams::default(),
183        };
184
185        let verifier =
186            create_verifier_test(&params, &leaf_hashes).expect("Failed to create verifier");
187        (verifier, params)
188    }
189
190    #[test_case(HashAlgorithm::Sha256; "sha256")]
191    #[test_case(HashAlgorithm::Sha512; "sha512")]
192    fn test_verify_read_success(hash_algorithm: HashAlgorithm) {
193        let data = create_data(512);
194        let (verifier, _) = build_test_verifier(&data, hash_algorithm, "aabbcc");
195
196        verifier.verify(0, &data).expect("Failed to verify read");
197    }
198
199    #[test_case(HashAlgorithm::Sha256; "sha256")]
200    #[test_case(HashAlgorithm::Sha512; "sha512")]
201    fn test_verify_read_corrupted_data(hash_algorithm: HashAlgorithm) {
202        let mut data = create_data(512);
203        let (verifier, _) = build_test_verifier(&data, hash_algorithm, "aabbcc");
204
205        // Corrupt the first byte of data
206        data[0] ^= 0xFF;
207
208        verifier
209            .verify(0, &data)
210            .expect_err("Read verification should have failed for corrupted data");
211    }
212
213    #[test_case(HashAlgorithm::Sha256; "sha256")]
214    #[test_case(HashAlgorithm::Sha512; "sha512")]
215    fn test_verify_root_mismatch(hash_algorithm: HashAlgorithm) {
216        let data = create_data(512);
217        let salt = "aabbcc";
218        let salt_bytes = hex::decode(salt).expect("Failed to decode salt");
219        let hasher = match hash_algorithm {
220            HashAlgorithm::Sha256 => FsVerityHasher::Sha256(FsVerityHasherOptions::new_dmverity(
221                salt_bytes.clone(),
222                TEST_BLOCK_SIZE as usize,
223            )),
224            HashAlgorithm::Sha512 => FsVerityHasher::Sha512(FsVerityHasherOptions::new_dmverity(
225                salt_bytes.clone(),
226                TEST_BLOCK_SIZE as usize,
227            )),
228        };
229
230        let tree = MerkleTree::from_data(&data, hasher);
231        let leaf_hashes = tree.leaf_hashes();
232
233        let digest_size = match hash_algorithm {
234            HashAlgorithm::Sha256 => 32,
235            HashAlgorithm::Sha512 => 64,
236        };
237        let wrong_root_digest = "00".repeat(digest_size);
238
239        let params = DmVerityTargetParams {
240            version: "1".to_string(),
241            block_device_path: "mock".to_string(),
242            hash_device_path: "mock".to_string(),
243            data_block_size: TEST_BLOCK_SIZE,
244            hash_block_size: TEST_BLOCK_SIZE,
245            num_data_blocks: (data.len() as u64).div_ceil(TEST_BLOCK_SIZE),
246            hash_start_block: 0,
247            hash_algorithm,
248            root_digest: wrong_root_digest,
249            salt: salt.to_string(),
250            optional_params: DmVerityTargetOptionalParams::default(),
251        };
252
253        assert_eq!(
254            create_verifier_test(&params, &leaf_hashes)
255                .expect_err("create_verifier passed unexpectedly with root mismatch"),
256            DmVerityError::RootHashMismatch
257        );
258    }
259
260    #[test_case(HashAlgorithm::Sha256; "sha256")]
261    #[test_case(HashAlgorithm::Sha512; "sha512")]
262    fn test_verify_read_non_zero_offset(hash_algorithm: HashAlgorithm) {
263        let data = create_data(1024);
264        let (verifier, _) = build_test_verifier(&data, hash_algorithm, "aabbcc");
265
266        let block_index = 2;
267        let offset = block_index * TEST_BLOCK_SIZE as usize;
268        let block_data = &data[offset..(offset + TEST_BLOCK_SIZE as usize)];
269
270        verifier.verify(offset, block_data).expect("Failed to verify read at non-zero offset");
271    }
272
273    #[test_case(HashAlgorithm::Sha256; "sha256")]
274    #[test_case(HashAlgorithm::Sha512; "sha512")]
275    fn test_verify_read_non_zero_offset_corrupted(hash_algorithm: HashAlgorithm) {
276        let data = create_data(1024);
277        let (verifier, _) = build_test_verifier(&data, hash_algorithm, "aabbcc");
278
279        let block_index = 2;
280        let offset = block_index * TEST_BLOCK_SIZE as usize;
281
282        let mut block_data = data[offset..(offset + TEST_BLOCK_SIZE as usize)].to_vec();
283        block_data[0] ^= 0xFF;
284
285        verifier.verify(offset, &block_data).expect_err(
286            "Read verification should have failed for corrupted data at non-zero offset",
287        );
288    }
289
290    #[test]
291    fn test_invalid_root_digest() {
292        let data = create_data(512);
293        let (_, mut params) = build_test_verifier(&data, HashAlgorithm::Sha256, "aabbcc");
294        let leaf_hashes = vec![0u8; 32];
295
296        // Non-hex characters
297        params.root_digest = "invalid_hex".to_string();
298        assert_eq!(
299            create_verifier_test(&params, &leaf_hashes)
300                .expect_err("create_verifier passed unexpectedly with invalid root digest"),
301            DmVerityError::InvalidVerifierArgs
302        );
303
304        // Odd length
305        params.root_digest = "a".to_string();
306        assert_eq!(
307            create_verifier_test(&params, &leaf_hashes)
308                .expect_err("create_verifier passed unexpectedly with odd-length root digest"),
309            DmVerityError::InvalidVerifierArgs
310        );
311
312        // Wrong size (expect 32 bytes for SHA-256)
313        params.root_digest = "00".repeat(31);
314        assert_eq!(
315            create_verifier_test(&params, &leaf_hashes)
316                .expect_err("create_verifier passed unexpectedly with wrong-sized root digest"),
317            DmVerityError::InvalidVerifierArgs
318        );
319    }
320
321    #[test]
322    fn test_invalid_salt() {
323        let data = create_data(512);
324        let (_, mut params) = build_test_verifier(&data, HashAlgorithm::Sha256, "aabbcc");
325        let leaf_hashes = vec![0u8; 32];
326
327        // Non-hex characters
328        params.salt = "invalid_hex".to_string();
329        assert_eq!(
330            create_verifier_test(&params, &leaf_hashes)
331                .expect_err("create_verifier passed unexpectedly with invalid salt"),
332            DmVerityError::InvalidVerifierArgs
333        );
334
335        // Odd length
336        params.salt = "a".to_string();
337        assert_eq!(
338            create_verifier_test(&params, &leaf_hashes)
339                .expect_err("create_verifier passed unexpectedly with odd-length salt"),
340            DmVerityError::InvalidVerifierArgs
341        );
342    }
343
344    #[test]
345    fn test_invalid_leaf_hashes_length() {
346        let data = create_data(512);
347        let (_, params) = build_test_verifier(&data, HashAlgorithm::Sha256, "aabbcc");
348
349        // SHA-256 expects multiples of 32 bytes.
350        let bad_leaf_hashes = vec![0u8; 31];
351        assert_eq!(
352            create_verifier_test(&params, &bad_leaf_hashes)
353                .expect_err("create_verifier passed unexpectedly with short leaf hashes"),
354            DmVerityError::InvalidVerifierArgs
355        );
356
357        let bad_leaf_hashes = vec![0u8; 33];
358        assert_eq!(
359            create_verifier_test(&params, &bad_leaf_hashes)
360                .expect_err("create_verifier passed unexpectedly with long leaf hashes"),
361            DmVerityError::InvalidVerifierArgs
362        );
363    }
364
365    #[test]
366    fn test_unsupported_algorithm() {
367        assert_eq!(
368            HashAlgorithm::from_str("md5")
369                .expect_err("HashAlgorithm::from_str passed unexpectedly for md5"),
370            DmVerityError::UnsupportedAlgorithm("md5".to_string())
371        );
372        HashAlgorithm::from_str("sha256")
373            .expect("HashAlgorithm::from_str failed unexpectedly for sha256");
374        HashAlgorithm::from_str("sha512")
375            .expect("HashAlgorithm::from_str failed unexpectedly for sha512");
376    }
377}