blob: 167f5d4c565892bd2e9b9fadf4218ecde5f702b3 [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
Andrew Walbranf714eeb2022-11-30 11:15:56 +000024#![cfg_attr(test, allow(unused))]
25
Jiyong Park86c9b082021-06-04 19:03:48 +090026use anyhow::{bail, Context, Result};
Alice Wang1bf3d782022-09-28 07:56:36 +000027use apkverify::{HashAlgorithm, V4Signature};
Andrew Walbranaa1efc42022-08-10 13:33:57 +000028use clap::{arg, Arg, ArgAction, Command};
Shikha Panwarb278b1c2022-10-14 12:38:32 +000029use dm::loopdevice;
Hung Nguyen109cdfa2024-12-06 11:02:44 -080030use dm::loopdevice::LoopConfigOptions;
Shikha Panwar414ea892022-10-12 13:45:52 +000031use dm::util;
32use dm::verity::{DmVerityHashAlgorithm, DmVerityTargetBuilder};
Inseob Kim217038e2021-11-25 11:15:06 +090033use itertools::Itertools;
Jiyong Park86c9b082021-06-04 19:03:48 +090034use std::fmt::Debug;
35use std::fs;
Jiyong Park86c9b082021-06-04 19:03:48 +090036use std::os::unix::fs::FileTypeExt;
37use std::path::{Path, PathBuf};
38
Andrew Walbranf714eeb2022-11-30 11:15:56 +000039#[cfg(not(test))]
Jiyong Park86c9b082021-06-04 19:03:48 +090040fn main() -> Result<()> {
Andrew Walbranaa1efc42022-08-10 13:33:57 +000041 let matches = clap_command().get_matches();
Jiyong Park86c9b082021-06-04 19:03:48 +090042
Andrew Walbranaa1efc42022-08-10 13:33:57 +000043 let apks = matches.get_many::<String>("apk").unwrap();
Inseob Kim197748b2021-12-01 19:49:00 +090044 assert!(apks.len() % 4 == 0);
Inseob Kim217038e2021-11-25 11:15:06 +090045
Andrew Walbranaa1efc42022-08-10 13:33:57 +000046 let verbose = matches.get_flag("verbose");
Inseob Kim217038e2021-11-25 11:15:06 +090047
Inseob Kim197748b2021-12-01 19:49:00 +090048 for (apk, idsig, name, roothash) in apks.tuples() {
49 let roothash = if roothash != "none" {
Alice Wang8ed29ce2023-12-01 08:33:12 +000050 Some(hex::decode(roothash).expect("failed to parse roothash"))
Inseob Kim197748b2021-12-01 19:49:00 +090051 } else {
52 None
53 };
Inseob Kim217038e2021-11-25 11:15:06 +090054 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 }
Jiyong Park99a35b82021-06-07 10:13:44 +090061 }
Jiyong Park86c9b082021-06-04 19:03:48 +090062 Ok(())
63}
64
Andrew Walbranaa1efc42022-08-10 13:33:57 +000065fn 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)
Chris Wailes75269622022-12-05 23:01:44 -080076 .value_names(["apk_path", "idsig_path", "name", "root_hash"]),
Andrew Walbranaa1efc42022-08-10 13:33:57 +000077 )
78 .arg(
79 Arg::new("verbose")
80 .short('v')
81 .long("verbose")
82 .action(ArgAction::SetTrue)
83 .help("Shows verbose output"),
84 )
85}
86
Jiyong Park86c9b082021-06-04 19:03:48 +090087struct VerityResult {
88 data_device: PathBuf,
89 hash_device: PathBuf,
90 mapper_device: PathBuf,
91}
92
93const BLOCK_SIZE: u64 = 4096;
94
95// Makes a dm-verity block device out of `apk` and its accompanying `idsig` files.
Jiyong Parkbb4a9872021-09-06 15:59:21 +090096fn enable_verity<P: AsRef<Path> + Debug>(
97 apk: P,
98 idsig: P,
99 name: &str,
100 roothash: Option<&[u8]>,
101) -> Result<VerityResult> {
Jiyong Park86c9b082021-06-04 19:03:48 +0900102 // 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 }
Jooyung Han1b00bd22022-04-15 15:29:25 +0900111 (
Alice Wang46211912023-12-07 14:30:37 +0000112 loopdevice::attach(
Hung Nguyen109cdfa2024-12-06 11:02:44 -0800113 &apk,
114 0,
115 apk_size,
116 &LoopConfigOptions { direct_io: true, ..Default::default() },
Alice Wang46211912023-12-07 14:30:37 +0000117 )
Hung Nguyen109cdfa2024-12-06 11:02:44 -0800118 .context("Failed to attach APK to a loop device")?
119 .path,
Jooyung Han1b00bd22022-04-15 15:29:25 +0900120 apk_size,
121 )
Jiyong Park86c9b082021-06-04 19:03:48 +0900122 };
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.
Alice Wang89cff012022-09-26 10:05:16 +0000127 let sig = V4Signature::from_idsig_path(&idsig)?;
Jiyong Park86c9b082021-06-04 19:03:48 +0900128 let offset = sig.merkle_tree_offset;
129 let size = sig.merkle_tree_size as u64;
Jooyung Han1b00bd22022-04-15 15:29:25 +0900130 // 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.
Hung Nguyen109cdfa2024-12-06 11:02:44 -0800133 let hash_device = loopdevice::attach(&idsig, offset, size, &LoopConfigOptions::default())
134 .context("Failed to attach idsig to a loop device")?
135 .path;
Jiyong Park86c9b082021-06-04 19:03:48 +0900136
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.
Shikha Panwar414ea892022-10-12 13:45:52 +0000139 let target = DmVerityTargetBuilder::default()
Jiyong Park86c9b082021-06-04 19:03:48 +0900140 .data_device(&data_device, apk_size)
141 .hash_device(&hash_device)
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900142 .root_digest(if let Some(roothash) = roothash {
143 roothash
144 } else {
145 &sig.hashing_info.raw_root_hash
146 })
Jiyong Park86c9b082021-06-04 19:03:48 +0900147 .hash_algorithm(match sig.hashing_info.hash_algorithm {
Shikha Panwar414ea892022-10-12 13:45:52 +0000148 HashAlgorithm::SHA256 => DmVerityHashAlgorithm::SHA256,
Jiyong Park86c9b082021-06-04 19:03:48 +0900149 })
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 =
Shikha Panwar414ea892022-10-12 13:45:52 +0000157 dm.create_verity_device(name, &target).context("Failed to create dm-verity device")?;
Jiyong Park86c9b082021-06-04 19:03:48 +0900158
159 Ok(VerityResult { data_device, hash_device, mapper_device })
160}
161
162#[cfg(test)]
163mod tests {
164 use crate::*;
Alice Wang89cff012022-09-26 10:05:16 +0000165 use std::fs::{File, OpenOptions};
166 use std::io::Write;
Andrew Walbranf714eeb2022-11-30 11:15:56 +0000167 use std::ops::Deref;
Jiyong Park86c9b082021-06-04 19:03:48 +0900168 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
Jiyong Parkd17ff4b2021-07-15 12:32:25 +0900176 // On Android, skip the test on devices that doesn't have the virt APEX
177 // (b/193612136)
178 #[cfg(target_os = "android")]
179 fn should_skip() -> bool {
180 !Path::new("/apex/com.android.virt").exists()
181 }
182 #[cfg(not(target_os = "android"))]
183 fn should_skip() -> bool {
184 false
185 }
186
Jiyong Park86c9b082021-06-04 19:03:48 +0900187 fn create_block_aligned_file(path: &Path, data: &[u8]) {
Chris Wailes9b866f02022-11-16 15:17:16 -0800188 let mut f = File::create(path).unwrap();
Jiyong Park86c9b082021-06-04 19:03:48 +0900189 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
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
205 fn run_test(apk: &[u8], idsig: &[u8], name: &str, check: fn(TestContext)) {
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900206 run_test_with_hash(apk, idsig, name, None, check);
207 }
208
209 fn run_test_with_hash(
210 apk: &[u8],
211 idsig: &[u8],
212 name: &str,
213 roothash: Option<&[u8]>,
214 check: fn(TestContext),
215 ) {
Jiyong Park86c9b082021-06-04 19:03:48 +0900216 let test_dir = tempfile::TempDir::new().unwrap();
Chris Wailes68c39f82021-07-27 16:03:44 -0700217 let (apk_path, idsig_path) = prepare_inputs(test_dir.path(), apk, idsig);
Jiyong Park86c9b082021-06-04 19:03:48 +0900218
219 // Run the program and register clean-ups.
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900220 let ret = enable_verity(&apk_path, &idsig_path, name, roothash).unwrap();
Jiyong Park86c9b082021-06-04 19:03:48 +0900221 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
Frederick Mayleafc347b2025-02-25 12:51:14 -0800235 #[test]
Jiyong Park86c9b082021-06-04 19:03:48 +0900236 fn correct_inputs() {
Frederick Mayleafc347b2025-02-25 12:51:14 -0800237 if should_skip() {
238 return;
239 }
Jiyong Park86c9b082021-06-04 19:03:48 +0900240 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
Frederick Mayleafc347b2025-02-25 12:51:14 -0800251 #[test]
Jiyong Park86c9b082021-06-04 19:03:48 +0900252 fn incorrect_apk() {
Frederick Mayleafc347b2025-02-25 12:51:14 -0800253 if should_skip() {
254 return;
255 }
Jiyong Park86c9b082021-06-04 19:03:48 +0900256 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| {
Jiyong Park7b08c572021-09-14 07:28:56 +0900266 fs::read(&ctx.result.mapper_device).expect_err("Should fail");
Jiyong Park86c9b082021-06-04 19:03:48 +0900267 });
268 }
269
270 // A single byte change in the merkle tree also causes an IO error
Frederick Mayleafc347b2025-02-25 12:51:14 -0800271 #[test]
Jiyong Park86c9b082021-06-04 19:03:48 +0900272 fn incorrect_merkle_tree() {
Frederick Mayleafc347b2025-02-25 12:51:14 -0800273 if should_skip() {
274 return;
275 }
Jiyong Park86c9b082021-06-04 19:03:48 +0900276 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
Alice Wang89cff012022-09-26 10:05:16 +0000280 let offset = V4Signature::from_idsig_path("testdata/test.apk.idsig")
281 .unwrap()
282 .merkle_tree_offset as usize;
Jiyong Park86c9b082021-06-04 19:03:48 +0900283
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| {
Jiyong Park7b08c572021-09-14 07:28:56 +0900291 fs::read(&ctx.result.mapper_device).expect_err("Should fail");
Jiyong Park86c9b082021-06-04 19:03:48 +0900292 });
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.
Frederick Mayleafc347b2025-02-25 12:51:14 -0800298 #[test]
Jiyong Park86c9b082021-06-04 19:03:48 +0900299 fn tampered_apk() {
Frederick Mayleafc347b2025-02-25 12:51:14 -0800300 if should_skip() {
301 return;
302 }
Jiyong Park86c9b082021-06-04 19:03:48 +0900303 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
Jiyong Park7b08c572021-09-14 07:28:56 +0900316 f.read_at(&mut buf, MODIFIED_OFFSET).expect_err("Should fail");
Jiyong Park86c9b082021-06-04 19:03:48 +0900317 });
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.
Frederick Mayleafc347b2025-02-25 12:51:14 -0800322 #[test]
Jiyong Park86c9b082021-06-04 19:03:48 +0900323 fn tampered_idsig() {
Frederick Mayleafc347b2025-02-25 12:51:14 -0800324 if should_skip() {
325 return;
326 }
Jiyong Park86c9b082021-06-04 19:03:48 +0900327 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
Frederick Mayleafc347b2025-02-25 12:51:14 -0800342 #[test]
Jiyong Park86c9b082021-06-04 19:03:48 +0900343 fn inputs_are_block_devices() {
Frederick Mayleafc347b2025-02-25 12:51:14 -0800344 if should_skip() {
345 return;
346 }
Jiyong Park86c9b082021-06-04 19:03:48 +0900347 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();
Chris Wailes68c39f82021-07-27 16:03:44 -0700351 let (apk_path, idsig_path) = prepare_inputs(test_dir.path(), apk, idsig);
Jiyong Park86c9b082021-06-04 19:03:48 +0900352
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.
Shikha Panwar743454c2022-10-18 12:50:30 +0000361 let apk_loop_device = loopdevice::attach(
Hung Nguyen109cdfa2024-12-06 11:02:44 -0800362 &apk_path,
363 0,
364 apk_size,
365 &LoopConfigOptions { direct_io: true, ..Default::default() },
Shikha Panwar743454c2022-10-18 12:50:30 +0000366 )
Hung Nguyen109cdfa2024-12-06 11:02:44 -0800367 .unwrap()
368 .path;
Jooyung Han1b00bd22022-04-15 15:29:25 +0900369 let idsig_loop_device = scopeguard::guard(
Hung Nguyen109cdfa2024-12-06 11:02:44 -0800370 loopdevice::attach(&idsig_path, 0, idsig_size, &LoopConfigOptions::default())
371 .unwrap()
372 .path,
Jooyung Han1b00bd22022-04-15 15:29:25 +0900373 |dev| loopdevice::detach(dev).unwrap(),
374 );
Jiyong Park86c9b082021-06-04 19:03:48 +0900375
376 let name = "loop_as_input";
377 // Run the program WITH the loop devices, not the regular files.
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900378 let ret =
379 enable_verity(apk_loop_device.deref(), idsig_loop_device.deref(), name, None).unwrap();
Jiyong Park86c9b082021-06-04 19:03:48 +0900380 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 }
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900392
393 // test with custom roothash
Frederick Mayleafc347b2025-02-25 12:51:14 -0800394 #[test]
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900395 fn correct_custom_roothash() {
Frederick Mayleafc347b2025-02-25 12:51:14 -0800396 if should_skip() {
397 return;
398 }
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900399 let apk = include_bytes!("../testdata/test.apk");
400 let idsig = include_bytes!("../testdata/test.apk.idsig");
Alice Wang89cff012022-09-26 10:05:16 +0000401 let roothash = V4Signature::from_idsig_path("testdata/test.apk.idsig")
402 .unwrap()
403 .hashing_info
404 .raw_root_hash;
Jiyong Parka204c762021-09-14 17:27:12 +0900405 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 );
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900417 }
Andrew Walbranaa1efc42022-08-10 13:33:57 +0000418
Frederick Mayleafc347b2025-02-25 12:51:14 -0800419 #[test]
Andrew Walbranaa1efc42022-08-10 13:33:57 +0000420 fn verify_command() {
421 // Check that the command parsing has been configured in a valid way.
422 clap_command().debug_assert();
423 }
Jiyong Park86c9b082021-06-04 19:03:48 +0900424}