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::util;
31 use dm::verity::{DmVerityHashAlgorithm, DmVerityTargetBuilder};
32 use itertools::Itertools;
33 use std::fmt::Debug;
34 use std::fs;
35 use std::os::unix::fs::FileTypeExt;
36 use std::path::{Path, PathBuf};
37
38 #[cfg(not(test))]
main() -> Result<()>39 fn main() -> Result<()> {
40 let matches = clap_command().get_matches();
41
42 let apks = matches.get_many::<String>("apk").unwrap();
43 assert!(apks.len() % 4 == 0);
44
45 let verbose = matches.get_flag("verbose");
46
47 for (apk, idsig, name, roothash) in apks.tuples() {
48 let roothash = if roothash != "none" {
49 Some(hex::decode(roothash).expect("failed to parse roothash"))
50 } else {
51 None
52 };
53 let ret = enable_verity(apk, idsig, name, roothash.as_deref())?;
54 if verbose {
55 println!(
56 "data_device: {:?}, hash_device: {:?}, mapper_device: {:?}",
57 ret.data_device, ret.hash_device, ret.mapper_device
58 );
59 }
60 }
61 Ok(())
62 }
63
clap_command() -> Command64 fn clap_command() -> Command {
65 Command::new("apkdmverity")
66 .about("Creates a dm-verity block device out of APK signed with APK signature scheme V4.")
67 .arg(
68 arg!(--apk ...
69 "Input APK file, idsig file, name of the block device, and root hash. \
70 The APK file must be signed using the APK signature scheme 4. The \
71 block device is created at \"/dev/mapper/<name>\".' root_hash is \
72 optional; idsig file's root hash will be used if specified as \"none\"."
73 )
74 .action(ArgAction::Append)
75 .value_names(["apk_path", "idsig_path", "name", "root_hash"]),
76 )
77 .arg(
78 Arg::new("verbose")
79 .short('v')
80 .long("verbose")
81 .action(ArgAction::SetTrue)
82 .help("Shows verbose output"),
83 )
84 }
85
86 struct VerityResult {
87 data_device: PathBuf,
88 hash_device: PathBuf,
89 mapper_device: PathBuf,
90 }
91
92 const BLOCK_SIZE: u64 = 4096;
93
94 // 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>95 fn enable_verity<P: AsRef<Path> + Debug>(
96 apk: P,
97 idsig: P,
98 name: &str,
99 roothash: Option<&[u8]>,
100 ) -> Result<VerityResult> {
101 // Attach the apk file to a loop device if the apk file is a regular file. If not (i.e. block
102 // device), we only need to get the size and use the block device as it is.
103 let (data_device, apk_size) = if fs::metadata(&apk)?.file_type().is_block_device() {
104 (apk.as_ref().to_path_buf(), util::blkgetsize64(apk.as_ref())?)
105 } else {
106 let apk_size = fs::metadata(&apk)?.len();
107 if apk_size % BLOCK_SIZE != 0 {
108 bail!("The size of {:?} is not multiple of {}.", &apk, BLOCK_SIZE)
109 }
110 (
111 loopdevice::attach(
112 &apk, 0, apk_size, /* direct_io */ true, /* writable */ false,
113 )
114 .context("Failed to attach APK to a loop device")?,
115 apk_size,
116 )
117 };
118
119 // Parse the idsig file to locate the merkle tree in it, then attach the file to a loop device
120 // with the offset so that the start of the merkle tree becomes the beginning of the loop
121 // device.
122 let sig = V4Signature::from_idsig_path(&idsig)?;
123 let offset = sig.merkle_tree_offset;
124 let size = sig.merkle_tree_size as u64;
125 // Due to unknown reason(b/191344832), we can't enable "direct IO" for the IDSIG file (backing
126 // the hash). For now we don't use "direct IO" but it seems OK since the IDSIG file is very
127 // small and the benefit of direct-IO would be negliable.
128 let hash_device = loopdevice::attach(
129 &idsig, offset, size, /* direct_io */ false, /* writable */ false,
130 )
131 .context("Failed to attach idsig to a loop device")?;
132
133 // Build a dm-verity target spec from the information from the idsig file. The apk and the
134 // idsig files are used as the data device and the hash device, respectively.
135 let target = DmVerityTargetBuilder::default()
136 .data_device(&data_device, apk_size)
137 .hash_device(&hash_device)
138 .root_digest(if let Some(roothash) = roothash {
139 roothash
140 } else {
141 &sig.hashing_info.raw_root_hash
142 })
143 .hash_algorithm(match sig.hashing_info.hash_algorithm {
144 HashAlgorithm::SHA256 => DmVerityHashAlgorithm::SHA256,
145 })
146 .salt(&sig.hashing_info.salt)
147 .build()
148 .context(format!("Merkle tree in {:?} is not compatible with dm-verity", &idsig))?;
149
150 // Actually create a dm-verity block device using the spec.
151 let dm = dm::DeviceMapper::new()?;
152 let mapper_device =
153 dm.create_verity_device(name, &target).context("Failed to create dm-verity device")?;
154
155 Ok(VerityResult { data_device, hash_device, mapper_device })
156 }
157
158 #[cfg(test)]
159 rdroidtest::test_main!();
160
161 #[cfg(test)]
162 mod tests {
163 use crate::*;
164 use rdroidtest::{ignore_if, rdroidtest};
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 #[rdroidtest]
236 #[ignore_if(should_skip())]
correct_inputs()237 fn correct_inputs() {
238 let apk = include_bytes!("../testdata/test.apk");
239 let idsig = include_bytes!("../testdata/test.apk.idsig");
240 run_test(apk.as_ref(), idsig.as_ref(), "correct", |ctx| {
241 let verity = fs::read(&ctx.result.mapper_device).unwrap();
242 let original = fs::read(&ctx.result.data_device).unwrap();
243 assert_eq!(verity.len(), original.len()); // fail fast
244 assert_eq!(verity.as_slice(), original.as_slice());
245 });
246 }
247
248 // A single byte change in the APK file causes an IO error
249 #[rdroidtest]
250 #[ignore_if(should_skip())]
incorrect_apk()251 fn incorrect_apk() {
252 let apk = include_bytes!("../testdata/test.apk");
253 let idsig = include_bytes!("../testdata/test.apk.idsig");
254
255 let mut modified_apk = Vec::new();
256 modified_apk.extend_from_slice(apk);
257 if let Some(byte) = modified_apk.get_mut(100) {
258 *byte = 1;
259 }
260
261 run_test(modified_apk.as_slice(), idsig.as_ref(), "incorrect_apk", |ctx| {
262 fs::read(&ctx.result.mapper_device).expect_err("Should fail");
263 });
264 }
265
266 // A single byte change in the merkle tree also causes an IO error
267 #[rdroidtest]
268 #[ignore_if(should_skip())]
incorrect_merkle_tree()269 fn incorrect_merkle_tree() {
270 let apk = include_bytes!("../testdata/test.apk");
271 let idsig = include_bytes!("../testdata/test.apk.idsig");
272
273 // Make a single-byte change to the merkle tree
274 let offset = V4Signature::from_idsig_path("testdata/test.apk.idsig")
275 .unwrap()
276 .merkle_tree_offset as usize;
277
278 let mut modified_idsig = Vec::new();
279 modified_idsig.extend_from_slice(idsig);
280 if let Some(byte) = modified_idsig.get_mut(offset + 10) {
281 *byte = 1;
282 }
283
284 run_test(apk.as_ref(), modified_idsig.as_slice(), "incorrect_merkle_tree", |ctx| {
285 fs::read(&ctx.result.mapper_device).expect_err("Should fail");
286 });
287 }
288
289 // APK is not altered when the verity device is created, but later modified. IO error should
290 // occur when trying to read the data around the modified location. This is the main scenario
291 // that we'd like to protect.
292 #[rdroidtest]
293 #[ignore_if(should_skip())]
tampered_apk()294 fn tampered_apk() {
295 let apk = include_bytes!("../testdata/test.apk");
296 let idsig = include_bytes!("../testdata/test.apk.idsig");
297
298 run_test(apk.as_ref(), idsig.as_ref(), "tampered_apk", |ctx| {
299 // At this moment, the verity device is created. Then let's change 10 bytes in the
300 // backing data file.
301 const MODIFIED_OFFSET: u64 = 10000;
302 let f = OpenOptions::new().read(true).write(true).open(ctx.data_backing_file).unwrap();
303 f.write_at(&[0, 1], MODIFIED_OFFSET).unwrap();
304
305 // Read around the modified location causes an error
306 let f = File::open(&ctx.result.mapper_device).unwrap();
307 let mut buf = vec![0; 10]; // just read 10 bytes
308 f.read_at(&mut buf, MODIFIED_OFFSET).expect_err("Should fail");
309 });
310 }
311
312 // idsig file is not alread when the verity device is created, but later modified. Unlike to
313 // the APK case, this doesn't occur IO error because the merkle tree is already cached.
314 #[rdroidtest]
315 #[ignore_if(should_skip())]
tampered_idsig()316 fn tampered_idsig() {
317 let apk = include_bytes!("../testdata/test.apk");
318 let idsig = include_bytes!("../testdata/test.apk.idsig");
319 run_test(apk.as_ref(), idsig.as_ref(), "tampered_idsig", |ctx| {
320 // Change 10 bytes in the merkle tree.
321 let f = OpenOptions::new().read(true).write(true).open(ctx.hash_backing_file).unwrap();
322 f.write_at(&[0, 10], 100).unwrap();
323
324 let verity = fs::read(&ctx.result.mapper_device).unwrap();
325 let original = fs::read(&ctx.result.data_device).unwrap();
326 assert_eq!(verity.len(), original.len());
327 assert_eq!(verity.as_slice(), original.as_slice());
328 });
329 }
330
331 // test if both files are already block devices
332 #[rdroidtest]
333 #[ignore_if(should_skip())]
inputs_are_block_devices()334 fn inputs_are_block_devices() {
335 let apk = include_bytes!("../testdata/test.apk");
336 let idsig = include_bytes!("../testdata/test.apk.idsig");
337
338 let test_dir = tempfile::TempDir::new().unwrap();
339 let (apk_path, idsig_path) = prepare_inputs(test_dir.path(), apk, idsig);
340
341 // attach the files to loop devices to make them block devices
342 let apk_size = fs::metadata(&apk_path).unwrap().len();
343 let idsig_size = fs::metadata(&idsig_path).unwrap().len();
344
345 // Note that apk_loop_device is not detatched. This is because, when the apk file is
346 // already a block device, `enable_verity` uses the block device as it is. The detatching
347 // of the data device is done in the scopeguard for the return value of `enable_verity`
348 // below. Only the idsig_loop_device needs detatching.
349 let apk_loop_device = loopdevice::attach(
350 &apk_path, 0, apk_size, /* direct_io */ true, /* writable */ false,
351 )
352 .unwrap();
353 let idsig_loop_device = scopeguard::guard(
354 loopdevice::attach(
355 &idsig_path,
356 0,
357 idsig_size,
358 /* direct_io */ false,
359 /* writable */ false,
360 )
361 .unwrap(),
362 |dev| loopdevice::detach(dev).unwrap(),
363 );
364
365 let name = "loop_as_input";
366 // Run the program WITH the loop devices, not the regular files.
367 let ret =
368 enable_verity(apk_loop_device.deref(), idsig_loop_device.deref(), name, None).unwrap();
369 let ret = scopeguard::guard(ret, |ret| {
370 loopdevice::detach(ret.data_device).unwrap();
371 loopdevice::detach(ret.hash_device).unwrap();
372 let dm = dm::DeviceMapper::new().unwrap();
373 dm.delete_device_deferred(name).unwrap();
374 });
375
376 let verity = fs::read(&ret.mapper_device).unwrap();
377 let original = fs::read(&apk_path).unwrap();
378 assert_eq!(verity.len(), original.len()); // fail fast
379 assert_eq!(verity.as_slice(), original.as_slice());
380 }
381
382 // test with custom roothash
383 #[rdroidtest]
384 #[ignore_if(should_skip())]
correct_custom_roothash()385 fn correct_custom_roothash() {
386 let apk = include_bytes!("../testdata/test.apk");
387 let idsig = include_bytes!("../testdata/test.apk.idsig");
388 let roothash = V4Signature::from_idsig_path("testdata/test.apk.idsig")
389 .unwrap()
390 .hashing_info
391 .raw_root_hash;
392 run_test_with_hash(
393 apk.as_ref(),
394 idsig.as_ref(),
395 "correct_custom_roothash",
396 Some(&roothash),
397 |ctx| {
398 let verity = fs::read(&ctx.result.mapper_device).unwrap();
399 let original = fs::read(&ctx.result.data_device).unwrap();
400 assert_eq!(verity.len(), original.len()); // fail fast
401 assert_eq!(verity.as_slice(), original.as_slice());
402 },
403 );
404 }
405
406 #[rdroidtest]
verify_command()407 fn verify_command() {
408 // Check that the command parsing has been configured in a valid way.
409 clap_command().debug_assert();
410 }
411 }
412