blob: a8a8f1582c40b8ea7e68012dcd420a1d7e9feaa3 [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 +090024mod dm;
25mod loopdevice;
26mod util;
27
Jiyong Park86c9b082021-06-04 19:03:48 +090028use anyhow::{bail, Context, Result};
29use clap::{App, Arg};
Jiyong Parkbde94ab2021-08-11 18:32:01 +090030use idsig::{HashAlgorithm, V4Signature};
Inseob Kim217038e2021-11-25 11:15:06 +090031use itertools::Itertools;
Jiyong Parkbb4a9872021-09-06 15:59:21 +090032use rustutils::system_properties;
Jiyong Park86c9b082021-06-04 19:03:48 +090033use std::fmt::Debug;
34use std::fs;
35use std::fs::File;
36use std::os::unix::fs::FileTypeExt;
37use std::path::{Path, PathBuf};
38
39fn main() -> Result<()> {
Jooyung Han7ce2e532021-06-16 16:52:02 +090040 let matches = App::new("apkdmverity")
Jiyong Park86c9b082021-06-04 19:03:48 +090041 .about("Creates a dm-verity block device out of APK signed with APK signature scheme V4.")
Inseob Kim217038e2021-11-25 11:15:06 +090042 .arg(Arg::from_usage(
43 "--apk... <apk_path> <idsig_path> <name> \
44 'Input APK file, idsig file, and the name of the block device. The APK \
45 file must be signed using the APK signature scheme 4. The block device \
46 is created at \"/dev/mapper/<name>\".'",
47 ))
Jiyong Park99a35b82021-06-07 10:13:44 +090048 .arg(Arg::with_name("verbose").short("v").long("verbose").help("Shows verbose output"))
Jiyong Park86c9b082021-06-04 19:03:48 +090049 .get_matches();
50
Inseob Kim217038e2021-11-25 11:15:06 +090051 let apks = matches.values_of("apk").unwrap();
52 assert!(apks.len() % 3 == 0);
53
Jooyung Han6606ce32021-09-08 14:31:39 +090054 let roothash = if let Ok(val) = system_properties::read("microdroid_manager.apk_root_hash") {
Jiyong Parkbb4a9872021-09-06 15:59:21 +090055 Some(util::parse_hexstring(&val)?)
56 } else {
57 // This failure is not an error. We will use the roothash read from the idsig file.
58 None
59 };
Inseob Kim217038e2021-11-25 11:15:06 +090060
61 let verbose = matches.is_present("verbose");
62
63 for (apk, idsig, name) in apks.tuples() {
64 let ret = enable_verity(apk, idsig, name, roothash.as_deref())?;
65 if verbose {
66 println!(
67 "data_device: {:?}, hash_device: {:?}, mapper_device: {:?}",
68 ret.data_device, ret.hash_device, ret.mapper_device
69 );
70 }
Jiyong Park99a35b82021-06-07 10:13:44 +090071 }
Jiyong Park86c9b082021-06-04 19:03:48 +090072 Ok(())
73}
74
75struct VerityResult {
76 data_device: PathBuf,
77 hash_device: PathBuf,
78 mapper_device: PathBuf,
79}
80
81const BLOCK_SIZE: u64 = 4096;
82
83// Makes a dm-verity block device out of `apk` and its accompanying `idsig` files.
Jiyong Parkbb4a9872021-09-06 15:59:21 +090084fn enable_verity<P: AsRef<Path> + Debug>(
85 apk: P,
86 idsig: P,
87 name: &str,
88 roothash: Option<&[u8]>,
89) -> Result<VerityResult> {
Jiyong Park86c9b082021-06-04 19:03:48 +090090 // Attach the apk file to a loop device if the apk file is a regular file. If not (i.e. block
91 // device), we only need to get the size and use the block device as it is.
92 let (data_device, apk_size) = if fs::metadata(&apk)?.file_type().is_block_device() {
93 (apk.as_ref().to_path_buf(), util::blkgetsize64(apk.as_ref())?)
94 } else {
95 let apk_size = fs::metadata(&apk)?.len();
96 if apk_size % BLOCK_SIZE != 0 {
97 bail!("The size of {:?} is not multiple of {}.", &apk, BLOCK_SIZE)
98 }
99 (loopdevice::attach(&apk, 0, apk_size)?, apk_size)
100 };
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.
Jiyong Park0553ff22021-07-15 12:25:36 +0900105 let sig = V4Signature::from(
106 File::open(&idsig).context(format!("Failed to open idsig file {:?}", &idsig))?,
107 )?;
Jiyong Park86c9b082021-06-04 19:03:48 +0900108 let offset = sig.merkle_tree_offset;
109 let size = sig.merkle_tree_size as u64;
110 let hash_device = loopdevice::attach(&idsig, offset, size)?;
111
112 // Build a dm-verity target spec from the information from the idsig file. The apk and the
113 // idsig files are used as the data device and the hash device, respectively.
114 let target = dm::DmVerityTargetBuilder::default()
115 .data_device(&data_device, apk_size)
116 .hash_device(&hash_device)
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900117 .root_digest(if let Some(roothash) = roothash {
118 roothash
119 } else {
120 &sig.hashing_info.raw_root_hash
121 })
Jiyong Park86c9b082021-06-04 19:03:48 +0900122 .hash_algorithm(match sig.hashing_info.hash_algorithm {
Jiyong Parkbde94ab2021-08-11 18:32:01 +0900123 HashAlgorithm::SHA256 => dm::DmVerityHashAlgorithm::SHA256,
Jiyong Park86c9b082021-06-04 19:03:48 +0900124 })
125 .salt(&sig.hashing_info.salt)
126 .build()
127 .context(format!("Merkle tree in {:?} is not compatible with dm-verity", &idsig))?;
128
129 // Actually create a dm-verity block device using the spec.
130 let dm = dm::DeviceMapper::new()?;
131 let mapper_device =
Chris Wailes68c39f82021-07-27 16:03:44 -0700132 dm.create_device(name, &target).context("Failed to create dm-verity device")?;
Jiyong Park86c9b082021-06-04 19:03:48 +0900133
134 Ok(VerityResult { data_device, hash_device, mapper_device })
135}
136
137#[cfg(test)]
138mod tests {
139 use crate::*;
140 use std::fs::OpenOptions;
141 use std::io::{Cursor, Write};
142 use std::os::unix::fs::FileExt;
143
144 struct TestContext<'a> {
145 data_backing_file: &'a Path,
146 hash_backing_file: &'a Path,
147 result: &'a VerityResult,
148 }
149
Jiyong Parkd17ff4b2021-07-15 12:32:25 +0900150 // On Android, skip the test on devices that doesn't have the virt APEX
151 // (b/193612136)
152 #[cfg(target_os = "android")]
153 fn should_skip() -> bool {
154 !Path::new("/apex/com.android.virt").exists()
155 }
156 #[cfg(not(target_os = "android"))]
157 fn should_skip() -> bool {
158 false
159 }
160
Jiyong Park86c9b082021-06-04 19:03:48 +0900161 fn create_block_aligned_file(path: &Path, data: &[u8]) {
162 let mut f = File::create(&path).unwrap();
163 f.write_all(data).unwrap();
164
165 // Add padding so that the size of the file is multiple of 4096.
166 let aligned_size = (data.len() as u64 + BLOCK_SIZE - 1) & !(BLOCK_SIZE - 1);
167 let padding = aligned_size - data.len() as u64;
168 f.write_all(vec![0; padding as usize].as_slice()).unwrap();
169 }
170
171 fn prepare_inputs(test_dir: &Path, apk: &[u8], idsig: &[u8]) -> (PathBuf, PathBuf) {
172 let apk_path = test_dir.join("test.apk");
173 let idsig_path = test_dir.join("test.apk.idsig");
174 create_block_aligned_file(&apk_path, apk);
175 create_block_aligned_file(&idsig_path, idsig);
176 (apk_path, idsig_path)
177 }
178
179 fn run_test(apk: &[u8], idsig: &[u8], name: &str, check: fn(TestContext)) {
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900180 run_test_with_hash(apk, idsig, name, None, check);
181 }
182
183 fn run_test_with_hash(
184 apk: &[u8],
185 idsig: &[u8],
186 name: &str,
187 roothash: Option<&[u8]>,
188 check: fn(TestContext),
189 ) {
Jiyong Parkd17ff4b2021-07-15 12:32:25 +0900190 if should_skip() {
191 return;
192 }
Jiyong Park86c9b082021-06-04 19:03:48 +0900193 let test_dir = tempfile::TempDir::new().unwrap();
Chris Wailes68c39f82021-07-27 16:03:44 -0700194 let (apk_path, idsig_path) = prepare_inputs(test_dir.path(), apk, idsig);
Jiyong Park86c9b082021-06-04 19:03:48 +0900195
196 // Run the program and register clean-ups.
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900197 let ret = enable_verity(&apk_path, &idsig_path, name, roothash).unwrap();
Jiyong Park86c9b082021-06-04 19:03:48 +0900198 let ret = scopeguard::guard(ret, |ret| {
199 loopdevice::detach(ret.data_device).unwrap();
200 loopdevice::detach(ret.hash_device).unwrap();
201 let dm = dm::DeviceMapper::new().unwrap();
202 dm.delete_device_deferred(name).unwrap();
203 });
204
205 check(TestContext {
206 data_backing_file: &apk_path,
207 hash_backing_file: &idsig_path,
208 result: &ret,
209 });
210 }
211
212 #[test]
213 fn correct_inputs() {
214 let apk = include_bytes!("../testdata/test.apk");
215 let idsig = include_bytes!("../testdata/test.apk.idsig");
216 run_test(apk.as_ref(), idsig.as_ref(), "correct", |ctx| {
217 let verity = fs::read(&ctx.result.mapper_device).unwrap();
218 let original = fs::read(&ctx.result.data_device).unwrap();
219 assert_eq!(verity.len(), original.len()); // fail fast
220 assert_eq!(verity.as_slice(), original.as_slice());
221 });
222 }
223
224 // A single byte change in the APK file causes an IO error
225 #[test]
226 fn incorrect_apk() {
227 let apk = include_bytes!("../testdata/test.apk");
228 let idsig = include_bytes!("../testdata/test.apk.idsig");
229
230 let mut modified_apk = Vec::new();
231 modified_apk.extend_from_slice(apk);
232 if let Some(byte) = modified_apk.get_mut(100) {
233 *byte = 1;
234 }
235
236 run_test(modified_apk.as_slice(), idsig.as_ref(), "incorrect_apk", |ctx| {
Jiyong Park7b08c572021-09-14 07:28:56 +0900237 fs::read(&ctx.result.mapper_device).expect_err("Should fail");
Jiyong Park86c9b082021-06-04 19:03:48 +0900238 });
239 }
240
241 // A single byte change in the merkle tree also causes an IO error
242 #[test]
243 fn incorrect_merkle_tree() {
244 let apk = include_bytes!("../testdata/test.apk");
245 let idsig = include_bytes!("../testdata/test.apk.idsig");
246
247 // Make a single-byte change to the merkle tree
248 let offset = V4Signature::from(Cursor::new(&idsig)).unwrap().merkle_tree_offset as usize;
249
250 let mut modified_idsig = Vec::new();
251 modified_idsig.extend_from_slice(idsig);
252 if let Some(byte) = modified_idsig.get_mut(offset + 10) {
253 *byte = 1;
254 }
255
256 run_test(apk.as_ref(), modified_idsig.as_slice(), "incorrect_merkle_tree", |ctx| {
Jiyong Park7b08c572021-09-14 07:28:56 +0900257 fs::read(&ctx.result.mapper_device).expect_err("Should fail");
Jiyong Park86c9b082021-06-04 19:03:48 +0900258 });
259 }
260
261 // APK is not altered when the verity device is created, but later modified. IO error should
262 // occur when trying to read the data around the modified location. This is the main scenario
263 // that we'd like to protect.
264 #[test]
265 fn tampered_apk() {
266 let apk = include_bytes!("../testdata/test.apk");
267 let idsig = include_bytes!("../testdata/test.apk.idsig");
268
269 run_test(apk.as_ref(), idsig.as_ref(), "tampered_apk", |ctx| {
270 // At this moment, the verity device is created. Then let's change 10 bytes in the
271 // backing data file.
272 const MODIFIED_OFFSET: u64 = 10000;
273 let f = OpenOptions::new().read(true).write(true).open(ctx.data_backing_file).unwrap();
274 f.write_at(&[0, 1], MODIFIED_OFFSET).unwrap();
275
276 // Read around the modified location causes an error
277 let f = File::open(&ctx.result.mapper_device).unwrap();
278 let mut buf = vec![0; 10]; // just read 10 bytes
Jiyong Park7b08c572021-09-14 07:28:56 +0900279 f.read_at(&mut buf, MODIFIED_OFFSET).expect_err("Should fail");
Jiyong Park86c9b082021-06-04 19:03:48 +0900280 });
281 }
282
283 // idsig file is not alread when the verity device is created, but later modified. Unlike to
284 // the APK case, this doesn't occur IO error because the merkle tree is already cached.
285 #[test]
286 fn tampered_idsig() {
287 let apk = include_bytes!("../testdata/test.apk");
288 let idsig = include_bytes!("../testdata/test.apk.idsig");
289 run_test(apk.as_ref(), idsig.as_ref(), "tampered_idsig", |ctx| {
290 // Change 10 bytes in the merkle tree.
291 let f = OpenOptions::new().read(true).write(true).open(ctx.hash_backing_file).unwrap();
292 f.write_at(&[0, 10], 100).unwrap();
293
294 let verity = fs::read(&ctx.result.mapper_device).unwrap();
295 let original = fs::read(&ctx.result.data_device).unwrap();
296 assert_eq!(verity.len(), original.len());
297 assert_eq!(verity.as_slice(), original.as_slice());
298 });
299 }
300
301 // test if both files are already block devices
302 #[test]
303 fn inputs_are_block_devices() {
Jiyong Parkd17ff4b2021-07-15 12:32:25 +0900304 if should_skip() {
305 return;
306 }
307
Jiyong Park86c9b082021-06-04 19:03:48 +0900308 use std::ops::Deref;
309 let apk = include_bytes!("../testdata/test.apk");
310 let idsig = include_bytes!("../testdata/test.apk.idsig");
311
312 let test_dir = tempfile::TempDir::new().unwrap();
Chris Wailes68c39f82021-07-27 16:03:44 -0700313 let (apk_path, idsig_path) = prepare_inputs(test_dir.path(), apk, idsig);
Jiyong Park86c9b082021-06-04 19:03:48 +0900314
315 // attach the files to loop devices to make them block devices
316 let apk_size = fs::metadata(&apk_path).unwrap().len();
317 let idsig_size = fs::metadata(&idsig_path).unwrap().len();
318
319 // Note that apk_loop_device is not detatched. This is because, when the apk file is
320 // already a block device, `enable_verity` uses the block device as it is. The detatching
321 // of the data device is done in the scopeguard for the return value of `enable_verity`
322 // below. Only the idsig_loop_device needs detatching.
323 let apk_loop_device = loopdevice::attach(&apk_path, 0, apk_size).unwrap();
324 let idsig_loop_device =
325 scopeguard::guard(loopdevice::attach(&idsig_path, 0, idsig_size).unwrap(), |dev| {
326 loopdevice::detach(dev).unwrap()
327 });
328
329 let name = "loop_as_input";
330 // Run the program WITH the loop devices, not the regular files.
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900331 let ret =
332 enable_verity(apk_loop_device.deref(), idsig_loop_device.deref(), name, None).unwrap();
Jiyong Park86c9b082021-06-04 19:03:48 +0900333 let ret = scopeguard::guard(ret, |ret| {
334 loopdevice::detach(ret.data_device).unwrap();
335 loopdevice::detach(ret.hash_device).unwrap();
336 let dm = dm::DeviceMapper::new().unwrap();
337 dm.delete_device_deferred(name).unwrap();
338 });
339
340 let verity = fs::read(&ret.mapper_device).unwrap();
341 let original = fs::read(&apk_path).unwrap();
342 assert_eq!(verity.len(), original.len()); // fail fast
343 assert_eq!(verity.as_slice(), original.as_slice());
344 }
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900345
346 // test with custom roothash
347 #[test]
348 fn correct_custom_roothash() {
349 let apk = include_bytes!("../testdata/test.apk");
350 let idsig = include_bytes!("../testdata/test.apk.idsig");
351 let roothash = V4Signature::from(Cursor::new(&idsig)).unwrap().hashing_info.raw_root_hash;
Jiyong Parka204c762021-09-14 17:27:12 +0900352 run_test_with_hash(
353 apk.as_ref(),
354 idsig.as_ref(),
355 "correct_custom_roothash",
356 Some(&roothash),
357 |ctx| {
358 let verity = fs::read(&ctx.result.mapper_device).unwrap();
359 let original = fs::read(&ctx.result.data_device).unwrap();
360 assert_eq!(verity.len(), original.len()); // fail fast
361 assert_eq!(verity.as_slice(), original.as_slice());
362 },
363 );
Jiyong Parkbb4a9872021-09-06 15:59:21 +0900364 }
Jiyong Park86c9b082021-06-04 19:03:48 +0900365}