blob: f09af79b7b1dc4b07ecf6bccff42d9e78955dee2 [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
24mod apksigv4;
25mod dm;
26mod loopdevice;
27mod util;
28
29use crate::apksigv4::*;
30
31use anyhow::{bail, Context, Result};
32use clap::{App, Arg};
33use 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.")
42 .arg(
43 Arg::with_name("apk")
44 .help("Input APK file. Must be signed using the APK signature scheme V4.")
45 .required(true),
46 )
47 .arg(
48 Arg::with_name("idsig")
49 .help("The idsig file having the merkle tree and the signing info.")
50 .required(true),
51 )
52 .arg(
53 Arg::with_name("name")
54 .help(
55 "Name of the dm-verity block device. The block device is created at \
56 \"/dev/mapper/<name>\".",
57 )
58 .required(true),
59 )
Jiyong Park99a35b82021-06-07 10:13:44 +090060 .arg(Arg::with_name("verbose").short("v").long("verbose").help("Shows verbose output"))
Jiyong Park86c9b082021-06-04 19:03:48 +090061 .get_matches();
62
63 let apk = matches.value_of("apk").unwrap();
64 let idsig = matches.value_of("idsig").unwrap();
65 let name = matches.value_of("name").unwrap();
Jiyong Park99a35b82021-06-07 10:13:44 +090066 let ret = enable_verity(apk, idsig, name)?;
67 if matches.is_present("verbose") {
68 println!(
69 "data_device: {:?}, hash_device: {:?}, mapper_device: {:?}",
70 ret.data_device, ret.hash_device, ret.mapper_device
71 );
72 }
Jiyong Park86c9b082021-06-04 19:03:48 +090073 Ok(())
74}
75
76struct VerityResult {
77 data_device: PathBuf,
78 hash_device: PathBuf,
79 mapper_device: PathBuf,
80}
81
82const BLOCK_SIZE: u64 = 4096;
83
84// Makes a dm-verity block device out of `apk` and its accompanying `idsig` files.
85fn enable_verity<P: AsRef<Path> + Debug>(apk: P, idsig: P, name: &str) -> Result<VerityResult> {
86 // 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 }
95 (loopdevice::attach(&apk, 0, apk_size)?, apk_size)
96 };
97
98 // Parse the idsig file to locate the merkle tree in it, then attach the file to a loop device
99 // with the offset so that the start of the merkle tree becomes the beginning of the loop
100 // device.
Jiyong Park0553ff22021-07-15 12:25:36 +0900101 let sig = V4Signature::from(
102 File::open(&idsig).context(format!("Failed to open idsig file {:?}", &idsig))?,
103 )?;
Jiyong Park86c9b082021-06-04 19:03:48 +0900104 let offset = sig.merkle_tree_offset;
105 let size = sig.merkle_tree_size as u64;
106 let hash_device = loopdevice::attach(&idsig, offset, size)?;
107
108 // Build a dm-verity target spec from the information from the idsig file. The apk and the
109 // idsig files are used as the data device and the hash device, respectively.
110 let target = dm::DmVerityTargetBuilder::default()
111 .data_device(&data_device, apk_size)
112 .hash_device(&hash_device)
113 .root_digest(&sig.hashing_info.raw_root_hash)
114 .hash_algorithm(match sig.hashing_info.hash_algorithm {
115 apksigv4::HashAlgorithm::SHA256 => dm::DmVerityHashAlgorithm::SHA256,
116 })
117 .salt(&sig.hashing_info.salt)
118 .build()
119 .context(format!("Merkle tree in {:?} is not compatible with dm-verity", &idsig))?;
120
121 // Actually create a dm-verity block device using the spec.
122 let dm = dm::DeviceMapper::new()?;
123 let mapper_device =
124 dm.create_device(&name, &target).context("Failed to create dm-verity device")?;
125
126 Ok(VerityResult { data_device, hash_device, mapper_device })
127}
128
129#[cfg(test)]
130mod tests {
131 use crate::*;
132 use std::fs::OpenOptions;
133 use std::io::{Cursor, Write};
134 use std::os::unix::fs::FileExt;
135
136 struct TestContext<'a> {
137 data_backing_file: &'a Path,
138 hash_backing_file: &'a Path,
139 result: &'a VerityResult,
140 }
141
Jiyong Parkd17ff4b2021-07-15 12:32:25 +0900142 // On Android, skip the test on devices that doesn't have the virt APEX
143 // (b/193612136)
144 #[cfg(target_os = "android")]
145 fn should_skip() -> bool {
146 !Path::new("/apex/com.android.virt").exists()
147 }
148 #[cfg(not(target_os = "android"))]
149 fn should_skip() -> bool {
150 false
151 }
152
Jiyong Park86c9b082021-06-04 19:03:48 +0900153 fn create_block_aligned_file(path: &Path, data: &[u8]) {
154 let mut f = File::create(&path).unwrap();
155 f.write_all(data).unwrap();
156
157 // Add padding so that the size of the file is multiple of 4096.
158 let aligned_size = (data.len() as u64 + BLOCK_SIZE - 1) & !(BLOCK_SIZE - 1);
159 let padding = aligned_size - data.len() as u64;
160 f.write_all(vec![0; padding as usize].as_slice()).unwrap();
161 }
162
163 fn prepare_inputs(test_dir: &Path, apk: &[u8], idsig: &[u8]) -> (PathBuf, PathBuf) {
164 let apk_path = test_dir.join("test.apk");
165 let idsig_path = test_dir.join("test.apk.idsig");
166 create_block_aligned_file(&apk_path, apk);
167 create_block_aligned_file(&idsig_path, idsig);
168 (apk_path, idsig_path)
169 }
170
171 fn run_test(apk: &[u8], idsig: &[u8], name: &str, check: fn(TestContext)) {
Jiyong Parkd17ff4b2021-07-15 12:32:25 +0900172 if should_skip() {
173 return;
174 }
Jiyong Park86c9b082021-06-04 19:03:48 +0900175 let test_dir = tempfile::TempDir::new().unwrap();
176 let (apk_path, idsig_path) = prepare_inputs(&test_dir.path(), apk, idsig);
177
178 // Run the program and register clean-ups.
179 let ret = enable_verity(&apk_path, &idsig_path, name).unwrap();
180 let ret = scopeguard::guard(ret, |ret| {
181 loopdevice::detach(ret.data_device).unwrap();
182 loopdevice::detach(ret.hash_device).unwrap();
183 let dm = dm::DeviceMapper::new().unwrap();
184 dm.delete_device_deferred(name).unwrap();
185 });
186
187 check(TestContext {
188 data_backing_file: &apk_path,
189 hash_backing_file: &idsig_path,
190 result: &ret,
191 });
192 }
193
194 #[test]
195 fn correct_inputs() {
196 let apk = include_bytes!("../testdata/test.apk");
197 let idsig = include_bytes!("../testdata/test.apk.idsig");
198 run_test(apk.as_ref(), idsig.as_ref(), "correct", |ctx| {
199 let verity = fs::read(&ctx.result.mapper_device).unwrap();
200 let original = fs::read(&ctx.result.data_device).unwrap();
201 assert_eq!(verity.len(), original.len()); // fail fast
202 assert_eq!(verity.as_slice(), original.as_slice());
203 });
204 }
205
206 // A single byte change in the APK file causes an IO error
207 #[test]
208 fn incorrect_apk() {
209 let apk = include_bytes!("../testdata/test.apk");
210 let idsig = include_bytes!("../testdata/test.apk.idsig");
211
212 let mut modified_apk = Vec::new();
213 modified_apk.extend_from_slice(apk);
214 if let Some(byte) = modified_apk.get_mut(100) {
215 *byte = 1;
216 }
217
218 run_test(modified_apk.as_slice(), idsig.as_ref(), "incorrect_apk", |ctx| {
219 let ret = fs::read(&ctx.result.mapper_device).map_err(|e| e.kind());
220 assert_eq!(ret, Err(std::io::ErrorKind::Other));
221 });
222 }
223
224 // A single byte change in the merkle tree also causes an IO error
225 #[test]
226 fn incorrect_merkle_tree() {
227 let apk = include_bytes!("../testdata/test.apk");
228 let idsig = include_bytes!("../testdata/test.apk.idsig");
229
230 // Make a single-byte change to the merkle tree
231 let offset = V4Signature::from(Cursor::new(&idsig)).unwrap().merkle_tree_offset as usize;
232
233 let mut modified_idsig = Vec::new();
234 modified_idsig.extend_from_slice(idsig);
235 if let Some(byte) = modified_idsig.get_mut(offset + 10) {
236 *byte = 1;
237 }
238
239 run_test(apk.as_ref(), modified_idsig.as_slice(), "incorrect_merkle_tree", |ctx| {
240 let ret = fs::read(&ctx.result.mapper_device).map_err(|e| e.kind());
241 assert_eq!(ret, Err(std::io::ErrorKind::Other));
242 });
243 }
244
245 // APK is not altered when the verity device is created, but later modified. IO error should
246 // occur when trying to read the data around the modified location. This is the main scenario
247 // that we'd like to protect.
248 #[test]
249 fn tampered_apk() {
250 let apk = include_bytes!("../testdata/test.apk");
251 let idsig = include_bytes!("../testdata/test.apk.idsig");
252
253 run_test(apk.as_ref(), idsig.as_ref(), "tampered_apk", |ctx| {
254 // At this moment, the verity device is created. Then let's change 10 bytes in the
255 // backing data file.
256 const MODIFIED_OFFSET: u64 = 10000;
257 let f = OpenOptions::new().read(true).write(true).open(ctx.data_backing_file).unwrap();
258 f.write_at(&[0, 1], MODIFIED_OFFSET).unwrap();
259
260 // Read around the modified location causes an error
261 let f = File::open(&ctx.result.mapper_device).unwrap();
262 let mut buf = vec![0; 10]; // just read 10 bytes
263 let ret = f.read_at(&mut buf, MODIFIED_OFFSET).map_err(|e| e.kind());
264 assert!(ret.is_err());
265 assert_eq!(ret, Err(std::io::ErrorKind::Other));
266 });
267 }
268
269 // idsig file is not alread when the verity device is created, but later modified. Unlike to
270 // the APK case, this doesn't occur IO error because the merkle tree is already cached.
271 #[test]
272 fn tampered_idsig() {
273 let apk = include_bytes!("../testdata/test.apk");
274 let idsig = include_bytes!("../testdata/test.apk.idsig");
275 run_test(apk.as_ref(), idsig.as_ref(), "tampered_idsig", |ctx| {
276 // Change 10 bytes in the merkle tree.
277 let f = OpenOptions::new().read(true).write(true).open(ctx.hash_backing_file).unwrap();
278 f.write_at(&[0, 10], 100).unwrap();
279
280 let verity = fs::read(&ctx.result.mapper_device).unwrap();
281 let original = fs::read(&ctx.result.data_device).unwrap();
282 assert_eq!(verity.len(), original.len());
283 assert_eq!(verity.as_slice(), original.as_slice());
284 });
285 }
286
287 // test if both files are already block devices
288 #[test]
289 fn inputs_are_block_devices() {
Jiyong Parkd17ff4b2021-07-15 12:32:25 +0900290 if should_skip() {
291 return;
292 }
293
Jiyong Park86c9b082021-06-04 19:03:48 +0900294 use std::ops::Deref;
295 let apk = include_bytes!("../testdata/test.apk");
296 let idsig = include_bytes!("../testdata/test.apk.idsig");
297
298 let test_dir = tempfile::TempDir::new().unwrap();
299 let (apk_path, idsig_path) = prepare_inputs(&test_dir.path(), apk, idsig);
300
301 // attach the files to loop devices to make them block devices
302 let apk_size = fs::metadata(&apk_path).unwrap().len();
303 let idsig_size = fs::metadata(&idsig_path).unwrap().len();
304
305 // Note that apk_loop_device is not detatched. This is because, when the apk file is
306 // already a block device, `enable_verity` uses the block device as it is. The detatching
307 // of the data device is done in the scopeguard for the return value of `enable_verity`
308 // below. Only the idsig_loop_device needs detatching.
309 let apk_loop_device = loopdevice::attach(&apk_path, 0, apk_size).unwrap();
310 let idsig_loop_device =
311 scopeguard::guard(loopdevice::attach(&idsig_path, 0, idsig_size).unwrap(), |dev| {
312 loopdevice::detach(dev).unwrap()
313 });
314
315 let name = "loop_as_input";
316 // Run the program WITH the loop devices, not the regular files.
317 let ret = enable_verity(apk_loop_device.deref(), idsig_loop_device.deref(), &name).unwrap();
318 let ret = scopeguard::guard(ret, |ret| {
319 loopdevice::detach(ret.data_device).unwrap();
320 loopdevice::detach(ret.hash_device).unwrap();
321 let dm = dm::DeviceMapper::new().unwrap();
322 dm.delete_device_deferred(name).unwrap();
323 });
324
325 let verity = fs::read(&ret.mapper_device).unwrap();
326 let original = fs::read(&apk_path).unwrap();
327 assert_eq!(verity.len(), original.len()); // fail fast
328 assert_eq!(verity.as_slice(), original.as_slice());
329 }
330}