• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 //! `apkdmverity` is a program that protects a signed APK file using dm-verity. The APK is assumed
18 //! to be signed using APK signature scheme V4. The idsig file generated by the signing scheme is
19 //! also used as an input to provide the merkle tree. This program is currently intended to be used
20 //! to securely mount the APK inside Microdroid. Since the APK is physically stored in the file
21 //! system managed by the host Android which is assumed to be compromisable, it is important to
22 //! keep the integrity of the file "inside" Microdroid.
23 
24 #![cfg_attr(test, allow(unused))]
25 
26 use anyhow::{bail, Context, Result};
27 use apkverify::{HashAlgorithm, V4Signature};
28 use clap::{arg, Arg, ArgAction, Command};
29 use dm::loopdevice;
30 use dm::loopdevice::LoopConfigOptions;
31 use dm::util;
32 use dm::verity::{DmVerityHashAlgorithm, DmVerityTargetBuilder};
33 use itertools::Itertools;
34 use std::fmt::Debug;
35 use std::fs;
36 use std::os::unix::fs::FileTypeExt;
37 use std::path::{Path, PathBuf};
38 
39 #[cfg(not(test))]
main() -> Result<()>40 fn main() -> Result<()> {
41     let matches = clap_command().get_matches();
42 
43     let apks = matches.get_many::<String>("apk").unwrap();
44     assert!(apks.len() % 4 == 0);
45 
46     let verbose = matches.get_flag("verbose");
47 
48     for (apk, idsig, name, roothash) in apks.tuples() {
49         let roothash = if roothash != "none" {
50             Some(hex::decode(roothash).expect("failed to parse roothash"))
51         } else {
52             None
53         };
54         let ret = enable_verity(apk, idsig, name, roothash.as_deref())?;
55         if verbose {
56             println!(
57                 "data_device: {:?}, hash_device: {:?}, mapper_device: {:?}",
58                 ret.data_device, ret.hash_device, ret.mapper_device
59             );
60         }
61     }
62     Ok(())
63 }
64 
clap_command() -> Command65 fn clap_command() -> Command {
66     Command::new("apkdmverity")
67         .about("Creates a dm-verity block device out of APK signed with APK signature scheme V4.")
68         .arg(
69             arg!(--apk ...
70                 "Input APK file, idsig file, name of the block device, and root hash. \
71                 The APK file must be signed using the APK signature scheme 4. The \
72                 block device is created at \"/dev/mapper/<name>\".' root_hash is \
73                 optional; idsig file's root hash will be used if specified as \"none\"."
74             )
75             .action(ArgAction::Append)
76             .value_names(["apk_path", "idsig_path", "name", "root_hash"]),
77         )
78         .arg(
79             Arg::new("verbose")
80                 .short('v')
81                 .long("verbose")
82                 .action(ArgAction::SetTrue)
83                 .help("Shows verbose output"),
84         )
85 }
86 
87 struct VerityResult {
88     data_device: PathBuf,
89     hash_device: PathBuf,
90     mapper_device: PathBuf,
91 }
92 
93 const BLOCK_SIZE: u64 = 4096;
94 
95 // Makes a dm-verity block device out of `apk` and its accompanying `idsig` files.
enable_verity<P: AsRef<Path> + Debug>( apk: P, idsig: P, name: &str, roothash: Option<&[u8]>, ) -> Result<VerityResult>96 fn enable_verity<P: AsRef<Path> + Debug>(
97     apk: P,
98     idsig: P,
99     name: &str,
100     roothash: Option<&[u8]>,
101 ) -> Result<VerityResult> {
102     // Attach the apk file to a loop device if the apk file is a regular file. If not (i.e. block
103     // device), we only need to get the size and use the block device as it is.
104     let (data_device, apk_size) = if fs::metadata(&apk)?.file_type().is_block_device() {
105         (apk.as_ref().to_path_buf(), util::blkgetsize64(apk.as_ref())?)
106     } else {
107         let apk_size = fs::metadata(&apk)?.len();
108         if apk_size % BLOCK_SIZE != 0 {
109             bail!("The size of {:?} is not multiple of {}.", &apk, BLOCK_SIZE)
110         }
111         (
112             loopdevice::attach(
113                 &apk,
114                 0,
115                 apk_size,
116                 &LoopConfigOptions { direct_io: true, ..Default::default() },
117             )
118             .context("Failed to attach APK to a loop device")?
119             .path,
120             apk_size,
121         )
122     };
123 
124     // Parse the idsig file to locate the merkle tree in it, then attach the file to a loop device
125     // with the offset so that the start of the merkle tree becomes the beginning of the loop
126     // device.
127     let sig = V4Signature::from_idsig_path(&idsig)?;
128     let offset = sig.merkle_tree_offset;
129     let size = sig.merkle_tree_size as u64;
130     // Due to unknown reason(b/191344832), we can't enable "direct IO" for the IDSIG file (backing
131     // the hash). For now we don't use "direct IO" but it seems OK since the IDSIG file is very
132     // small and the benefit of direct-IO would be negliable.
133     let hash_device = loopdevice::attach(&idsig, offset, size, &LoopConfigOptions::default())
134         .context("Failed to attach idsig to a loop device")?
135         .path;
136 
137     // Build a dm-verity target spec from the information from the idsig file. The apk and the
138     // idsig files are used as the data device and the hash device, respectively.
139     let target = DmVerityTargetBuilder::default()
140         .data_device(&data_device, apk_size)
141         .hash_device(&hash_device)
142         .root_digest(if let Some(roothash) = roothash {
143             roothash
144         } else {
145             &sig.hashing_info.raw_root_hash
146         })
147         .hash_algorithm(match sig.hashing_info.hash_algorithm {
148             HashAlgorithm::SHA256 => DmVerityHashAlgorithm::SHA256,
149         })
150         .salt(&sig.hashing_info.salt)
151         .build()
152         .context(format!("Merkle tree in {:?} is not compatible with dm-verity", &idsig))?;
153 
154     // Actually create a dm-verity block device using the spec.
155     let dm = dm::DeviceMapper::new()?;
156     let mapper_device =
157         dm.create_verity_device(name, &target).context("Failed to create dm-verity device")?;
158 
159     Ok(VerityResult { data_device, hash_device, mapper_device })
160 }
161 
162 #[cfg(test)]
163 mod tests {
164     use crate::*;
165     use std::fs::{File, OpenOptions};
166     use std::io::Write;
167     use std::ops::Deref;
168     use std::os::unix::fs::FileExt;
169 
170     struct TestContext<'a> {
171         data_backing_file: &'a Path,
172         hash_backing_file: &'a Path,
173         result: &'a VerityResult,
174     }
175 
176     // On Android, skip the test on devices that doesn't have the virt APEX
177     // (b/193612136)
178     #[cfg(target_os = "android")]
should_skip() -> bool179     fn should_skip() -> bool {
180         !Path::new("/apex/com.android.virt").exists()
181     }
182     #[cfg(not(target_os = "android"))]
should_skip() -> bool183     fn should_skip() -> bool {
184         false
185     }
186 
create_block_aligned_file(path: &Path, data: &[u8])187     fn create_block_aligned_file(path: &Path, data: &[u8]) {
188         let mut f = File::create(path).unwrap();
189         f.write_all(data).unwrap();
190 
191         // Add padding so that the size of the file is multiple of 4096.
192         let aligned_size = (data.len() as u64 + BLOCK_SIZE - 1) & !(BLOCK_SIZE - 1);
193         let padding = aligned_size - data.len() as u64;
194         f.write_all(vec![0; padding as usize].as_slice()).unwrap();
195     }
196 
prepare_inputs(test_dir: &Path, apk: &[u8], idsig: &[u8]) -> (PathBuf, PathBuf)197     fn prepare_inputs(test_dir: &Path, apk: &[u8], idsig: &[u8]) -> (PathBuf, PathBuf) {
198         let apk_path = test_dir.join("test.apk");
199         let idsig_path = test_dir.join("test.apk.idsig");
200         create_block_aligned_file(&apk_path, apk);
201         create_block_aligned_file(&idsig_path, idsig);
202         (apk_path, idsig_path)
203     }
204 
run_test(apk: &[u8], idsig: &[u8], name: &str, check: fn(TestContext))205     fn run_test(apk: &[u8], idsig: &[u8], name: &str, check: fn(TestContext)) {
206         run_test_with_hash(apk, idsig, name, None, check);
207     }
208 
run_test_with_hash( apk: &[u8], idsig: &[u8], name: &str, roothash: Option<&[u8]>, check: fn(TestContext), )209     fn run_test_with_hash(
210         apk: &[u8],
211         idsig: &[u8],
212         name: &str,
213         roothash: Option<&[u8]>,
214         check: fn(TestContext),
215     ) {
216         let test_dir = tempfile::TempDir::new().unwrap();
217         let (apk_path, idsig_path) = prepare_inputs(test_dir.path(), apk, idsig);
218 
219         // Run the program and register clean-ups.
220         let ret = enable_verity(&apk_path, &idsig_path, name, roothash).unwrap();
221         let ret = scopeguard::guard(ret, |ret| {
222             loopdevice::detach(ret.data_device).unwrap();
223             loopdevice::detach(ret.hash_device).unwrap();
224             let dm = dm::DeviceMapper::new().unwrap();
225             dm.delete_device_deferred(name).unwrap();
226         });
227 
228         check(TestContext {
229             data_backing_file: &apk_path,
230             hash_backing_file: &idsig_path,
231             result: &ret,
232         });
233     }
234 
235     #[test]
correct_inputs()236     fn correct_inputs() {
237         if should_skip() {
238             return;
239         }
240         let apk = include_bytes!("../testdata/test.apk");
241         let idsig = include_bytes!("../testdata/test.apk.idsig");
242         run_test(apk.as_ref(), idsig.as_ref(), "correct", |ctx| {
243             let verity = fs::read(&ctx.result.mapper_device).unwrap();
244             let original = fs::read(&ctx.result.data_device).unwrap();
245             assert_eq!(verity.len(), original.len()); // fail fast
246             assert_eq!(verity.as_slice(), original.as_slice());
247         });
248     }
249 
250     // A single byte change in the APK file causes an IO error
251     #[test]
incorrect_apk()252     fn incorrect_apk() {
253         if should_skip() {
254             return;
255         }
256         let apk = include_bytes!("../testdata/test.apk");
257         let idsig = include_bytes!("../testdata/test.apk.idsig");
258 
259         let mut modified_apk = Vec::new();
260         modified_apk.extend_from_slice(apk);
261         if let Some(byte) = modified_apk.get_mut(100) {
262             *byte = 1;
263         }
264 
265         run_test(modified_apk.as_slice(), idsig.as_ref(), "incorrect_apk", |ctx| {
266             fs::read(&ctx.result.mapper_device).expect_err("Should fail");
267         });
268     }
269 
270     // A single byte change in the merkle tree also causes an IO error
271     #[test]
incorrect_merkle_tree()272     fn incorrect_merkle_tree() {
273         if should_skip() {
274             return;
275         }
276         let apk = include_bytes!("../testdata/test.apk");
277         let idsig = include_bytes!("../testdata/test.apk.idsig");
278 
279         // Make a single-byte change to the merkle tree
280         let offset = V4Signature::from_idsig_path("testdata/test.apk.idsig")
281             .unwrap()
282             .merkle_tree_offset as usize;
283 
284         let mut modified_idsig = Vec::new();
285         modified_idsig.extend_from_slice(idsig);
286         if let Some(byte) = modified_idsig.get_mut(offset + 10) {
287             *byte = 1;
288         }
289 
290         run_test(apk.as_ref(), modified_idsig.as_slice(), "incorrect_merkle_tree", |ctx| {
291             fs::read(&ctx.result.mapper_device).expect_err("Should fail");
292         });
293     }
294 
295     // APK is not altered when the verity device is created, but later modified. IO error should
296     // occur when trying to read the data around the modified location. This is the main scenario
297     // that we'd like to protect.
298     #[test]
tampered_apk()299     fn tampered_apk() {
300         if should_skip() {
301             return;
302         }
303         let apk = include_bytes!("../testdata/test.apk");
304         let idsig = include_bytes!("../testdata/test.apk.idsig");
305 
306         run_test(apk.as_ref(), idsig.as_ref(), "tampered_apk", |ctx| {
307             // At this moment, the verity device is created. Then let's change 10 bytes in the
308             // backing data file.
309             const MODIFIED_OFFSET: u64 = 10000;
310             let f = OpenOptions::new().read(true).write(true).open(ctx.data_backing_file).unwrap();
311             f.write_at(&[0, 1], MODIFIED_OFFSET).unwrap();
312 
313             // Read around the modified location causes an error
314             let f = File::open(&ctx.result.mapper_device).unwrap();
315             let mut buf = vec![0; 10]; // just read 10 bytes
316             f.read_at(&mut buf, MODIFIED_OFFSET).expect_err("Should fail");
317         });
318     }
319 
320     // idsig file is not alread when the verity device is created, but later modified. Unlike to
321     // the APK case, this doesn't occur IO error because the merkle tree is already cached.
322     #[test]
tampered_idsig()323     fn tampered_idsig() {
324         if should_skip() {
325             return;
326         }
327         let apk = include_bytes!("../testdata/test.apk");
328         let idsig = include_bytes!("../testdata/test.apk.idsig");
329         run_test(apk.as_ref(), idsig.as_ref(), "tampered_idsig", |ctx| {
330             // Change 10 bytes in the merkle tree.
331             let f = OpenOptions::new().read(true).write(true).open(ctx.hash_backing_file).unwrap();
332             f.write_at(&[0, 10], 100).unwrap();
333 
334             let verity = fs::read(&ctx.result.mapper_device).unwrap();
335             let original = fs::read(&ctx.result.data_device).unwrap();
336             assert_eq!(verity.len(), original.len());
337             assert_eq!(verity.as_slice(), original.as_slice());
338         });
339     }
340 
341     // test if both files are already block devices
342     #[test]
inputs_are_block_devices()343     fn inputs_are_block_devices() {
344         if should_skip() {
345             return;
346         }
347         let apk = include_bytes!("../testdata/test.apk");
348         let idsig = include_bytes!("../testdata/test.apk.idsig");
349 
350         let test_dir = tempfile::TempDir::new().unwrap();
351         let (apk_path, idsig_path) = prepare_inputs(test_dir.path(), apk, idsig);
352 
353         // attach the files to loop devices to make them block devices
354         let apk_size = fs::metadata(&apk_path).unwrap().len();
355         let idsig_size = fs::metadata(&idsig_path).unwrap().len();
356 
357         // Note that apk_loop_device is not detatched. This is because, when the apk file is
358         // already a block device, `enable_verity` uses the block device as it is. The detatching
359         // of the data device is done in the scopeguard for the return value of `enable_verity`
360         // below. Only the idsig_loop_device needs detatching.
361         let apk_loop_device = loopdevice::attach(
362             &apk_path,
363             0,
364             apk_size,
365             &LoopConfigOptions { direct_io: true, ..Default::default() },
366         )
367         .unwrap()
368         .path;
369         let idsig_loop_device = scopeguard::guard(
370             loopdevice::attach(&idsig_path, 0, idsig_size, &LoopConfigOptions::default())
371                 .unwrap()
372                 .path,
373             |dev| loopdevice::detach(dev).unwrap(),
374         );
375 
376         let name = "loop_as_input";
377         // Run the program WITH the loop devices, not the regular files.
378         let ret =
379             enable_verity(apk_loop_device.deref(), idsig_loop_device.deref(), name, None).unwrap();
380         let ret = scopeguard::guard(ret, |ret| {
381             loopdevice::detach(ret.data_device).unwrap();
382             loopdevice::detach(ret.hash_device).unwrap();
383             let dm = dm::DeviceMapper::new().unwrap();
384             dm.delete_device_deferred(name).unwrap();
385         });
386 
387         let verity = fs::read(&ret.mapper_device).unwrap();
388         let original = fs::read(&apk_path).unwrap();
389         assert_eq!(verity.len(), original.len()); // fail fast
390         assert_eq!(verity.as_slice(), original.as_slice());
391     }
392 
393     // test with custom roothash
394     #[test]
correct_custom_roothash()395     fn correct_custom_roothash() {
396         if should_skip() {
397             return;
398         }
399         let apk = include_bytes!("../testdata/test.apk");
400         let idsig = include_bytes!("../testdata/test.apk.idsig");
401         let roothash = V4Signature::from_idsig_path("testdata/test.apk.idsig")
402             .unwrap()
403             .hashing_info
404             .raw_root_hash;
405         run_test_with_hash(
406             apk.as_ref(),
407             idsig.as_ref(),
408             "correct_custom_roothash",
409             Some(&roothash),
410             |ctx| {
411                 let verity = fs::read(&ctx.result.mapper_device).unwrap();
412                 let original = fs::read(&ctx.result.data_device).unwrap();
413                 assert_eq!(verity.len(), original.len()); // fail fast
414                 assert_eq!(verity.as_slice(), original.as_slice());
415             },
416         );
417     }
418 
419     #[test]
verify_command()420     fn verify_command() {
421         // Check that the command parsing has been configured in a valid way.
422         clap_command().debug_assert();
423     }
424 }
425