blob: 23457c472d5dddc55b210e4dba3c35ea872b80f1 [file] [log] [blame]
Jiyong Park86c9b082021-06-04 19:03:48 +09001/*
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
Jiyong Park86c9b082021-06-04 19:03:48 +090024use anyhow::{bail, Context, Result};
Alice Wang1bf3d782022-09-28 07:56:36 +000025use apkverify::{HashAlgorithm, V4Signature};
Jiyong Park86c9b082021-06-04 19:03:48 +090026use clap::{App, Arg};
Shikha Panwarb278b1c2022-10-14 12:38:32 +000027use dm::loopdevice;
Shikha Panwar414ea892022-10-12 13:45:52 +000028use dm::util;
29use dm::verity::{DmVerityHashAlgorithm, DmVerityTargetBuilder};
Inseob Kim217038e2021-11-25 11:15:06 +090030use itertools::Itertools;
Jiyong Park86c9b082021-06-04 19:03:48 +090031use std::fmt::Debug;
32use std::fs;
Jiyong Park86c9b082021-06-04 19:03:48 +090033use std::os::unix::fs::FileTypeExt;
34use std::path::{Path, PathBuf};
35
36fn main() -> Result<()> {
Jooyung Han7ce2e532021-06-16 16:52:02 +090037 let matches = App::new("apkdmverity")
Jiyong Park86c9b082021-06-04 19:03:48 +090038 .about("Creates a dm-verity block device out of APK signed with APK signature scheme V4.")
Inseob Kim217038e2021-11-25 11:15:06 +090039 .arg(Arg::from_usage(
Inseob Kim197748b2021-12-01 19:49:00 +090040 "--apk... <apk_path> <idsig_path> <name> <root_hash> \
41 'Input APK file, idsig file, name of the block device, and root hash. \
42 The APK file must be signed using the APK signature scheme 4. The \
43 block device is created at \"/dev/mapper/<name>\".' root_hash is \
44 optional; idsig file's root hash will be used if specified as \"none\"."
45 ))
Jeff Vander Stoepa8dc2712022-07-29 02:33:45 +020046 .arg(Arg::with_name("verbose").short('v').long("verbose").help("Shows verbose output"))
Jiyong Park86c9b082021-06-04 19:03:48 +090047 .get_matches();
48
Inseob Kim217038e2021-11-25 11:15:06 +090049 let apks = matches.values_of("apk").unwrap();
Inseob Kim197748b2021-12-01 19:49:00 +090050 assert!(apks.len() % 4 == 0);
Inseob Kim217038e2021-11-25 11:15:06 +090051
52 let verbose = matches.is_present("verbose");
53
Inseob Kim197748b2021-12-01 19:49:00 +090054 for (apk, idsig, name, roothash) in apks.tuples() {
55 let roothash = if roothash != "none" {
56 Some(util::parse_hexstring(roothash).expect("failed to parse roothash"))
57 } else {
58 None
59 };
Inseob Kim217038e2021-11-25 11:15:06 +090060 let ret = enable_verity(apk, idsig, name, roothash.as_deref())?;
61 if verbose {
62 println!(
63 "data_device: {:?}, hash_device: {:?}, mapper_device: {:?}",
64 ret.data_device, ret.hash_device, ret.mapper_device
65 );
66 }
Jiyong Park99a35b82021-06-07 10:13:44 +090067 }
Jiyong Park86c9b082021-06-04 19:03:48 +090068 Ok(())
69}
70
71struct VerityResult {
72 data_device: PathBuf,
73 hash_device: PathBuf,
74 mapper_device: PathBuf,
75}
76
77const BLOCK_SIZE: u64 = 4096;
78
79// Makes a dm-verity block device out of `apk` and its accompanying `idsig` files.
Jiyong Parkbb4a9872021-09-06 15:59:21 +090080fn enable_verity<P: AsRef<Path> + Debug>(
81 apk: P,
82 idsig: P,
83 name: &str,
84 roothash: Option<&[u8]>,
85) -> Result<VerityResult> {
Jiyong Park86c9b082021-06-04 19:03:48 +090086 // Attach the apk file to a loop device if the apk file is a regular file. If not (i.e. block
87 // device), we only need to get the size and use the block device as it is.
88 let (data_device, apk_size) = if fs::metadata(&apk)?.file_type().is_block_device() {
89 (apk.as_ref().to_path_buf(), util::blkgetsize64(apk.as_ref())?)
90 } else {
91 let apk_size = fs::metadata(&apk)?.len();
92 if apk_size % BLOCK_SIZE != 0 {
93 bail!("The size of {:?} is not multiple of {}.", &apk, BLOCK_SIZE)
94 }
Jooyung Han1b00bd22022-04-15 15:29:25 +090095 (
96 loopdevice::attach(&apk, 0, apk_size, /*direct_io*/ true)
97 .context("Failed to attach APK to a loop device")?,
98 apk_size,
99 )
Jiyong Park86c9b082021-06-04 19:03:48 +0900100 };
101
102 // Parse the idsig file to locate the merkle tree in it, then attach the file to a loop device
103 // with the offset so that the start of the merkle tree becomes the beginning of the loop
104 // device.
Alice Wang89cff012022-09-26 10:05:16 +0000105 let sig = V4Signature::from_idsig_path(&idsig)?;
Jiyong Park86c9b082021-06-04 19:03:48 +0900106 let offset = sig.merkle_tree_offset;
107 let size = sig.merkle_tree_size as u64;
Jooyung Han1b00bd22022-04-15 15:29:25 +0900108 // Due to unknown reason(b/191344832), we can't enable "direct IO" for the IDSIG file (backing
109 // the hash). For now we don't use "direct IO" but it seems OK since the IDSIG file is very
110 // small and the benefit of direct-IO would be negliable.
111 let hash_device = loopdevice::attach(&idsig, offset, size, /*direct_io*/ false)
112 .context("Failed to attach idsig to a loop device")?;
Jiyong Park86c9b082021-06-04 19:03:48 +0900113
114 // Build a dm-verity target spec from the information from the idsig file. The apk and the
115 // idsig files are used as the data device and the hash device, respectively.
Shikha Panwar414ea892022-10-12 13:45:52 +0000116 let target = DmVerityTargetBuilder::default()
Jiyong Park86c9b082021-06-04 19:03:48 +0900117 .data_device(&data_device, apk_size)
118 .hash_device(&hash_device)
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900119 .root_digest(if let Some(roothash) = roothash {
120 roothash
121 } else {
122 &sig.hashing_info.raw_root_hash
123 })
Jiyong Park86c9b082021-06-04 19:03:48 +0900124 .hash_algorithm(match sig.hashing_info.hash_algorithm {
Shikha Panwar414ea892022-10-12 13:45:52 +0000125 HashAlgorithm::SHA256 => DmVerityHashAlgorithm::SHA256,
Jiyong Park86c9b082021-06-04 19:03:48 +0900126 })
127 .salt(&sig.hashing_info.salt)
128 .build()
129 .context(format!("Merkle tree in {:?} is not compatible with dm-verity", &idsig))?;
130
131 // Actually create a dm-verity block device using the spec.
132 let dm = dm::DeviceMapper::new()?;
133 let mapper_device =
Shikha Panwar414ea892022-10-12 13:45:52 +0000134 dm.create_verity_device(name, &target).context("Failed to create dm-verity device")?;
Jiyong Park86c9b082021-06-04 19:03:48 +0900135
136 Ok(VerityResult { data_device, hash_device, mapper_device })
137}
138
139#[cfg(test)]
140mod tests {
141 use crate::*;
Alice Wang89cff012022-09-26 10:05:16 +0000142 use std::fs::{File, OpenOptions};
143 use std::io::Write;
Jiyong Park86c9b082021-06-04 19:03:48 +0900144 use std::os::unix::fs::FileExt;
145
146 struct TestContext<'a> {
147 data_backing_file: &'a Path,
148 hash_backing_file: &'a Path,
149 result: &'a VerityResult,
150 }
151
Jiyong Parkd17ff4b2021-07-15 12:32:25 +0900152 // On Android, skip the test on devices that doesn't have the virt APEX
153 // (b/193612136)
154 #[cfg(target_os = "android")]
155 fn should_skip() -> bool {
156 !Path::new("/apex/com.android.virt").exists()
157 }
158 #[cfg(not(target_os = "android"))]
159 fn should_skip() -> bool {
160 false
161 }
162
Jiyong Park86c9b082021-06-04 19:03:48 +0900163 fn create_block_aligned_file(path: &Path, data: &[u8]) {
164 let mut f = File::create(&path).unwrap();
165 f.write_all(data).unwrap();
166
167 // Add padding so that the size of the file is multiple of 4096.
168 let aligned_size = (data.len() as u64 + BLOCK_SIZE - 1) & !(BLOCK_SIZE - 1);
169 let padding = aligned_size - data.len() as u64;
170 f.write_all(vec![0; padding as usize].as_slice()).unwrap();
171 }
172
173 fn prepare_inputs(test_dir: &Path, apk: &[u8], idsig: &[u8]) -> (PathBuf, PathBuf) {
174 let apk_path = test_dir.join("test.apk");
175 let idsig_path = test_dir.join("test.apk.idsig");
176 create_block_aligned_file(&apk_path, apk);
177 create_block_aligned_file(&idsig_path, idsig);
178 (apk_path, idsig_path)
179 }
180
181 fn run_test(apk: &[u8], idsig: &[u8], name: &str, check: fn(TestContext)) {
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900182 run_test_with_hash(apk, idsig, name, None, check);
183 }
184
185 fn run_test_with_hash(
186 apk: &[u8],
187 idsig: &[u8],
188 name: &str,
189 roothash: Option<&[u8]>,
190 check: fn(TestContext),
191 ) {
Jiyong Parkd17ff4b2021-07-15 12:32:25 +0900192 if should_skip() {
193 return;
194 }
Jiyong Park86c9b082021-06-04 19:03:48 +0900195 let test_dir = tempfile::TempDir::new().unwrap();
Chris Wailes68c39f82021-07-27 16:03:44 -0700196 let (apk_path, idsig_path) = prepare_inputs(test_dir.path(), apk, idsig);
Jiyong Park86c9b082021-06-04 19:03:48 +0900197
198 // Run the program and register clean-ups.
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900199 let ret = enable_verity(&apk_path, &idsig_path, name, roothash).unwrap();
Jiyong Park86c9b082021-06-04 19:03:48 +0900200 let ret = scopeguard::guard(ret, |ret| {
201 loopdevice::detach(ret.data_device).unwrap();
202 loopdevice::detach(ret.hash_device).unwrap();
203 let dm = dm::DeviceMapper::new().unwrap();
204 dm.delete_device_deferred(name).unwrap();
205 });
206
207 check(TestContext {
208 data_backing_file: &apk_path,
209 hash_backing_file: &idsig_path,
210 result: &ret,
211 });
212 }
213
214 #[test]
215 fn correct_inputs() {
216 let apk = include_bytes!("../testdata/test.apk");
217 let idsig = include_bytes!("../testdata/test.apk.idsig");
218 run_test(apk.as_ref(), idsig.as_ref(), "correct", |ctx| {
219 let verity = fs::read(&ctx.result.mapper_device).unwrap();
220 let original = fs::read(&ctx.result.data_device).unwrap();
221 assert_eq!(verity.len(), original.len()); // fail fast
222 assert_eq!(verity.as_slice(), original.as_slice());
223 });
224 }
225
226 // A single byte change in the APK file causes an IO error
227 #[test]
228 fn incorrect_apk() {
229 let apk = include_bytes!("../testdata/test.apk");
230 let idsig = include_bytes!("../testdata/test.apk.idsig");
231
232 let mut modified_apk = Vec::new();
233 modified_apk.extend_from_slice(apk);
234 if let Some(byte) = modified_apk.get_mut(100) {
235 *byte = 1;
236 }
237
238 run_test(modified_apk.as_slice(), idsig.as_ref(), "incorrect_apk", |ctx| {
Jiyong Park7b08c572021-09-14 07:28:56 +0900239 fs::read(&ctx.result.mapper_device).expect_err("Should fail");
Jiyong Park86c9b082021-06-04 19:03:48 +0900240 });
241 }
242
243 // A single byte change in the merkle tree also causes an IO error
244 #[test]
245 fn incorrect_merkle_tree() {
246 let apk = include_bytes!("../testdata/test.apk");
247 let idsig = include_bytes!("../testdata/test.apk.idsig");
248
249 // Make a single-byte change to the merkle tree
Alice Wang89cff012022-09-26 10:05:16 +0000250 let offset = V4Signature::from_idsig_path("testdata/test.apk.idsig")
251 .unwrap()
252 .merkle_tree_offset as usize;
Jiyong Park86c9b082021-06-04 19:03:48 +0900253
254 let mut modified_idsig = Vec::new();
255 modified_idsig.extend_from_slice(idsig);
256 if let Some(byte) = modified_idsig.get_mut(offset + 10) {
257 *byte = 1;
258 }
259
260 run_test(apk.as_ref(), modified_idsig.as_slice(), "incorrect_merkle_tree", |ctx| {
Jiyong Park7b08c572021-09-14 07:28:56 +0900261 fs::read(&ctx.result.mapper_device).expect_err("Should fail");
Jiyong Park86c9b082021-06-04 19:03:48 +0900262 });
263 }
264
265 // APK is not altered when the verity device is created, but later modified. IO error should
266 // occur when trying to read the data around the modified location. This is the main scenario
267 // that we'd like to protect.
268 #[test]
269 fn tampered_apk() {
270 let apk = include_bytes!("../testdata/test.apk");
271 let idsig = include_bytes!("../testdata/test.apk.idsig");
272
273 run_test(apk.as_ref(), idsig.as_ref(), "tampered_apk", |ctx| {
274 // At this moment, the verity device is created. Then let's change 10 bytes in the
275 // backing data file.
276 const MODIFIED_OFFSET: u64 = 10000;
277 let f = OpenOptions::new().read(true).write(true).open(ctx.data_backing_file).unwrap();
278 f.write_at(&[0, 1], MODIFIED_OFFSET).unwrap();
279
280 // Read around the modified location causes an error
281 let f = File::open(&ctx.result.mapper_device).unwrap();
282 let mut buf = vec![0; 10]; // just read 10 bytes
Jiyong Park7b08c572021-09-14 07:28:56 +0900283 f.read_at(&mut buf, MODIFIED_OFFSET).expect_err("Should fail");
Jiyong Park86c9b082021-06-04 19:03:48 +0900284 });
285 }
286
287 // idsig file is not alread when the verity device is created, but later modified. Unlike to
288 // the APK case, this doesn't occur IO error because the merkle tree is already cached.
289 #[test]
290 fn tampered_idsig() {
291 let apk = include_bytes!("../testdata/test.apk");
292 let idsig = include_bytes!("../testdata/test.apk.idsig");
293 run_test(apk.as_ref(), idsig.as_ref(), "tampered_idsig", |ctx| {
294 // Change 10 bytes in the merkle tree.
295 let f = OpenOptions::new().read(true).write(true).open(ctx.hash_backing_file).unwrap();
296 f.write_at(&[0, 10], 100).unwrap();
297
298 let verity = fs::read(&ctx.result.mapper_device).unwrap();
299 let original = fs::read(&ctx.result.data_device).unwrap();
300 assert_eq!(verity.len(), original.len());
301 assert_eq!(verity.as_slice(), original.as_slice());
302 });
303 }
304
305 // test if both files are already block devices
306 #[test]
307 fn inputs_are_block_devices() {
Jiyong Parkd17ff4b2021-07-15 12:32:25 +0900308 if should_skip() {
309 return;
310 }
311
Jiyong Park86c9b082021-06-04 19:03:48 +0900312 use std::ops::Deref;
313 let apk = include_bytes!("../testdata/test.apk");
314 let idsig = include_bytes!("../testdata/test.apk.idsig");
315
316 let test_dir = tempfile::TempDir::new().unwrap();
Chris Wailes68c39f82021-07-27 16:03:44 -0700317 let (apk_path, idsig_path) = prepare_inputs(test_dir.path(), apk, idsig);
Jiyong Park86c9b082021-06-04 19:03:48 +0900318
319 // attach the files to loop devices to make them block devices
320 let apk_size = fs::metadata(&apk_path).unwrap().len();
321 let idsig_size = fs::metadata(&idsig_path).unwrap().len();
322
323 // Note that apk_loop_device is not detatched. This is because, when the apk file is
324 // already a block device, `enable_verity` uses the block device as it is. The detatching
325 // of the data device is done in the scopeguard for the return value of `enable_verity`
326 // below. Only the idsig_loop_device needs detatching.
Jooyung Han1b00bd22022-04-15 15:29:25 +0900327 let apk_loop_device = loopdevice::attach(&apk_path, 0, apk_size, true).unwrap();
328 let idsig_loop_device = scopeguard::guard(
329 loopdevice::attach(&idsig_path, 0, idsig_size, false).unwrap(),
330 |dev| loopdevice::detach(dev).unwrap(),
331 );
Jiyong Park86c9b082021-06-04 19:03:48 +0900332
333 let name = "loop_as_input";
334 // Run the program WITH the loop devices, not the regular files.
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900335 let ret =
336 enable_verity(apk_loop_device.deref(), idsig_loop_device.deref(), name, None).unwrap();
Jiyong Park86c9b082021-06-04 19:03:48 +0900337 let ret = scopeguard::guard(ret, |ret| {
338 loopdevice::detach(ret.data_device).unwrap();
339 loopdevice::detach(ret.hash_device).unwrap();
340 let dm = dm::DeviceMapper::new().unwrap();
341 dm.delete_device_deferred(name).unwrap();
342 });
343
344 let verity = fs::read(&ret.mapper_device).unwrap();
345 let original = fs::read(&apk_path).unwrap();
346 assert_eq!(verity.len(), original.len()); // fail fast
347 assert_eq!(verity.as_slice(), original.as_slice());
348 }
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900349
350 // test with custom roothash
351 #[test]
352 fn correct_custom_roothash() {
353 let apk = include_bytes!("../testdata/test.apk");
354 let idsig = include_bytes!("../testdata/test.apk.idsig");
Alice Wang89cff012022-09-26 10:05:16 +0000355 let roothash = V4Signature::from_idsig_path("testdata/test.apk.idsig")
356 .unwrap()
357 .hashing_info
358 .raw_root_hash;
Jiyong Parka204c762021-09-14 17:27:12 +0900359 run_test_with_hash(
360 apk.as_ref(),
361 idsig.as_ref(),
362 "correct_custom_roothash",
363 Some(&roothash),
364 |ctx| {
365 let verity = fs::read(&ctx.result.mapper_device).unwrap();
366 let original = fs::read(&ctx.result.data_device).unwrap();
367 assert_eq!(verity.len(), original.len()); // fail fast
368 assert_eq!(verity.as_slice(), original.as_slice());
369 },
370 );
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900371 }
Jiyong Park86c9b082021-06-04 19:03:48 +0900372}