apkdmverity: dm-verity over apk
apkdmverity is a program that create a dm-verity block device over an
apk that is signed with the APK signature scheme V4. The merkle tree
comes from the *.idsig file generated as part of the signing scheme.
In the context of Microdroid, this program will be used to keep the
integrity of APK inside Microdroid because the APK is stored in a
filesystem that is served by the host Android. Any tampering happening
outside of Microdroid is manifested as an IO error inside Microdroid.
The dm-verity block device will then be mounted at /mnt/apk by zipfuse.
It is not yet decided to merge apkdmverity into zipfuse. It might be
good for saving storage and memory, but right now let's keep them
separate for easy testing.
This CL doesn't have Android.bp. Building and and testing for Android
will be the next step once this lands.
Bug: 189785765
Test: cargo test
Change-Id: I482028a7350162dc55d1cdb35183cd34ea8c18fe
diff --git a/apkverity/.cargo/config.toml b/apkverity/.cargo/config.toml
new file mode 100644
index 0000000..fac6997
--- /dev/null
+++ b/apkverity/.cargo/config.toml
@@ -0,0 +1,3 @@
+# Configuring loop devices and device mampper require root privilege.
+[target.x86_64-unknown-linux-gnu]
+runner = 'sudo -E'
diff --git a/apkverity/Cargo.toml b/apkverity/Cargo.toml
new file mode 100644
index 0000000..bd0ab01
--- /dev/null
+++ b/apkverity/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "apkdmverity"
+version = "0.1.0"
+authors = ["Jiyong Park <jiyong@google.com>"]
+edition = "2018"
+
+[dependencies]
+anyhow = "1.0"
+scopeguard = "1.1"
+bitflags = "1.2"
+clap = "2.33"
+libc = "0.2"
+nix = "0.21"
+num-derive = "0.3"
+num-traits = "0.2"
+uuid = { version = "0.8", features = ["v4"] }
+
+[dev-dependencies]
+tempfile = "3.2"
diff --git a/apkverity/src/apksigv4.rs b/apkverity/src/apksigv4.rs
new file mode 100644
index 0000000..f1ee0a4
--- /dev/null
+++ b/apkverity/src/apksigv4.rs
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use anyhow::{anyhow, Context, Result};
+use num_derive::FromPrimitive;
+use num_traits::FromPrimitive;
+use std::io::{Read, Seek};
+
+// `apksigv4` module provides routines to decode the idsig file as defined in [APK signature
+// scheme v4] (https://source.android.com/security/apksigning/v4).
+
+#[derive(Debug)]
+pub struct V4Signature {
+ pub version: Version,
+ pub hashing_info: HashingInfo,
+ pub signing_info: SigningInfo,
+ pub merkle_tree_size: u32,
+ pub merkle_tree_offset: u64,
+}
+
+#[derive(Debug)]
+pub struct HashingInfo {
+ pub hash_algorithm: HashAlgorithm,
+ pub log2_blocksize: u8,
+ pub salt: Box<[u8]>,
+ pub raw_root_hash: Box<[u8]>,
+}
+
+#[derive(Debug)]
+pub struct SigningInfo {
+ pub apk_digest: Box<[u8]>,
+ pub x509_certificate: Box<[u8]>,
+ pub additional_data: Box<[u8]>,
+ pub public_key: Box<[u8]>,
+ pub signature_algorithm_id: SignatureAlgorithmId,
+ pub signature: Box<[u8]>,
+}
+
+#[derive(Debug, PartialEq, FromPrimitive)]
+#[repr(u32)]
+pub enum Version {
+ V2 = 2,
+}
+
+impl Version {
+ fn from(val: u32) -> Result<Version> {
+ Self::from_u32(val).ok_or(anyhow!("{} is an unsupported version", val))
+ }
+}
+
+#[derive(Debug, PartialEq, FromPrimitive)]
+#[repr(u32)]
+pub enum HashAlgorithm {
+ SHA256 = 1,
+}
+
+impl HashAlgorithm {
+ fn from(val: u32) -> Result<HashAlgorithm> {
+ Self::from_u32(val).ok_or(anyhow!("{} is an unsupported hash algorithm", val))
+ }
+}
+
+#[derive(Debug, PartialEq, FromPrimitive)]
+#[allow(non_camel_case_types)]
+#[repr(u32)]
+pub enum SignatureAlgorithmId {
+ RSASSA_PSS_SHA2_256 = 0x0101,
+ RSASSA_PSS_SHA2_512 = 0x0102,
+ RSASSA_PKCS1_SHA2_256 = 0x0103,
+ RSASSA_PKCS1_SHA2_512 = 0x0104,
+ ECDSA_SHA2_256 = 0x0201,
+ ECDSA_SHA2_512 = 0x0202,
+ DSA_SHA2_256 = 0x0301,
+}
+
+impl SignatureAlgorithmId {
+ fn from(val: u32) -> Result<SignatureAlgorithmId> {
+ Self::from_u32(val)
+ .with_context(|| format!("{:#06x} is an unsupported signature algorithm", val))
+ }
+}
+
+impl V4Signature {
+ /// Reads a stream from `r` and then parses it into a `V4Signature` struct.
+ pub fn from<T: Read + Seek>(mut r: T) -> Result<V4Signature> {
+ Ok(V4Signature {
+ version: Version::from(read_le_u32(&mut r)?)?,
+ hashing_info: HashingInfo::from(&mut r)?,
+ signing_info: SigningInfo::from(&mut r)?,
+ merkle_tree_size: read_le_u32(&mut r)?,
+ merkle_tree_offset: r.stream_position()?,
+ })
+ }
+}
+
+impl HashingInfo {
+ fn from(mut r: &mut dyn Read) -> Result<HashingInfo> {
+ read_le_u32(&mut r)?;
+ Ok(HashingInfo {
+ hash_algorithm: HashAlgorithm::from(read_le_u32(&mut r)?)?,
+ log2_blocksize: read_u8(&mut r)?,
+ salt: read_sized_array(&mut r)?,
+ raw_root_hash: read_sized_array(&mut r)?,
+ })
+ }
+}
+
+impl SigningInfo {
+ fn from(mut r: &mut dyn Read) -> Result<SigningInfo> {
+ read_le_u32(&mut r)?;
+ Ok(SigningInfo {
+ apk_digest: read_sized_array(&mut r)?,
+ x509_certificate: read_sized_array(&mut r)?,
+ additional_data: read_sized_array(&mut r)?,
+ public_key: read_sized_array(&mut r)?,
+ signature_algorithm_id: SignatureAlgorithmId::from(read_le_u32(&mut r)?)?,
+ signature: read_sized_array(&mut r)?,
+ })
+ }
+}
+
+fn read_u8(r: &mut dyn Read) -> Result<u8> {
+ let mut byte = [0; 1];
+ r.read_exact(&mut byte)?;
+ Ok(byte[0])
+}
+
+fn read_le_u32(r: &mut dyn Read) -> Result<u32> {
+ let mut bytes = [0; 4];
+ r.read_exact(&mut bytes)?;
+ Ok(u32::from_le_bytes(bytes))
+}
+
+fn read_sized_array(r: &mut dyn Read) -> Result<Box<[u8]>> {
+ let size = read_le_u32(r)?;
+ let mut data = vec![0; size as usize];
+ r.read_exact(&mut data)?;
+ Ok(data.into_boxed_slice())
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::*;
+ use std::io::Cursor;
+
+ fn hexstring_from(s: &[u8]) -> String {
+ s.iter().map(|byte| format!("{:02x}", byte)).reduce(|i, j| i + &j).unwrap_or(String::new())
+ }
+
+ #[test]
+ fn parse_idsig_file() {
+ let idsig = Cursor::new(include_bytes!("../testdata/test.apk.idsig"));
+ let parsed = V4Signature::from(idsig).unwrap();
+
+ assert_eq!(Version::V2, parsed.version);
+
+ let hi = parsed.hashing_info;
+ assert_eq!(HashAlgorithm::SHA256, hi.hash_algorithm);
+ assert_eq!(12, hi.log2_blocksize);
+ assert_eq!("", hexstring_from(hi.salt.as_ref()));
+ assert_eq!(
+ "ce1194fdb3cb2537daf0ac8cdf4926754adcbce5abeece7945fe25d204a0df6a",
+ hexstring_from(hi.raw_root_hash.as_ref())
+ );
+
+ let si = parsed.signing_info;
+ assert_eq!(
+ "b5225523a813fb84ed599dd649698c080bcfed4fb19ddb00283a662a2683bc15",
+ hexstring_from(si.apk_digest.as_ref())
+ );
+ assert_eq!("", hexstring_from(si.additional_data.as_ref()));
+ assert_eq!(
+ "303d021c77304d0f4732a90372bbfce095223e4ba82427ceb381f69bc6762d78021d008b99924\
+ a8585c38d7f654835eb219ae9e176b44e86dcb23153e3d9d6",
+ hexstring_from(si.signature.as_ref())
+ );
+ assert_eq!(SignatureAlgorithmId::DSA_SHA2_256, si.signature_algorithm_id);
+
+ assert_eq!(36864, parsed.merkle_tree_size);
+ assert_eq!(2251, parsed.merkle_tree_offset);
+ }
+}
diff --git a/apkverity/src/dm.rs b/apkverity/src/dm.rs
new file mode 100644
index 0000000..d1cd2eb
--- /dev/null
+++ b/apkverity/src/dm.rs
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// `dm` module implements part of the `device-mapper` ioctl interfaces. It currently supports
+// creation and deletion of the mapper device. It doesn't support other operations like querying
+// the status of the mapper device. And there's no plan to extend the support unless it is
+// required.
+//
+// Why in-house development? [`devicemapper`](https://crates.io/crates/devicemapper) is a public
+// Rust implementation of the device mapper APIs. However, it doesn't provide any abstraction for
+// the target-specific tables. User has to manually craft the table. Ironically, the library
+// provides a lot of APIs for the features that are not required for `apkdmverity` such as listing
+// the device mapper block devices that are currently listed in the kernel. Size is an important
+// criteria for Microdroid.
+
+use crate::util::*;
+
+use anyhow::Result;
+use std::fs::{File, OpenOptions};
+use std::io::Write;
+use std::mem::size_of;
+use std::os::unix::io::AsRawFd;
+use std::path::{Path, PathBuf};
+use uuid::Uuid;
+
+mod sys;
+mod verity;
+use sys::*;
+pub use verity::*;
+
+nix::ioctl_readwrite!(_dm_dev_create, DM_IOCTL, Cmd::DM_DEV_CREATE, DmIoctl);
+nix::ioctl_readwrite!(_dm_dev_remove, DM_IOCTL, Cmd::DM_DEV_REMOVE, DmIoctl);
+nix::ioctl_readwrite!(_dm_dev_suspend, DM_IOCTL, Cmd::DM_DEV_SUSPEND, DmIoctl);
+nix::ioctl_readwrite!(_dm_table_load, DM_IOCTL, Cmd::DM_TABLE_LOAD, DmIoctl);
+
+fn dm_dev_create(dm: &DeviceMapper, ioctl: *mut DmIoctl) -> Result<i32> {
+ // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the
+ // state of this process in any way.
+ Ok(unsafe { _dm_dev_create(dm.0.as_raw_fd(), ioctl) }?)
+}
+
+fn dm_dev_remove(dm: &DeviceMapper, ioctl: *mut DmIoctl) -> Result<i32> {
+ // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the
+ // state of this process in any way.
+ Ok(unsafe { _dm_dev_remove(dm.0.as_raw_fd(), ioctl) }?)
+}
+
+fn dm_dev_suspend(dm: &DeviceMapper, ioctl: *mut DmIoctl) -> Result<i32> {
+ // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the
+ // state of this process in any way.
+ Ok(unsafe { _dm_dev_suspend(dm.0.as_raw_fd(), ioctl) }?)
+}
+
+fn dm_table_load(dm: &DeviceMapper, ioctl: *mut DmIoctl) -> Result<i32> {
+ // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the
+ // state of this process in any way.
+ Ok(unsafe { _dm_table_load(dm.0.as_raw_fd(), ioctl) }?)
+}
+
+// `DmTargetSpec` is the header of the data structure for a device-mapper target. When doing the
+// ioctl, one of more `DmTargetSpec` (and its body) are appened to the `DmIoctl` struct.
+#[repr(C)]
+struct DmTargetSpec {
+ sector_start: u64,
+ length: u64, // number of 512 sectors
+ status: i32,
+ next: u32,
+ target_type: [u8; DM_MAX_TYPE_NAME],
+}
+
+impl DmTargetSpec {
+ fn new(target_type: &str) -> Result<Self> {
+ // SAFETY: zero initialized C struct is safe
+ let mut spec = unsafe { std::mem::MaybeUninit::<Self>::zeroed().assume_init() };
+ spec.target_type.as_mut().write_all(target_type.as_bytes())?;
+ Ok(spec)
+ }
+
+ fn as_u8_slice(&self) -> &[u8; size_of::<Self>()] {
+ // SAFETY: lifetime of the output reference isn't changed.
+ unsafe { std::mem::transmute::<&Self, &[u8; size_of::<Self>()]>(&self) }
+ }
+}
+
+impl DmIoctl {
+ fn new(name: &str) -> Result<DmIoctl> {
+ // SAFETY: zero initialized C struct is safe
+ let mut data = unsafe { std::mem::MaybeUninit::<Self>::zeroed().assume_init() };
+ data.version[0] = DM_VERSION_MAJOR;
+ data.version[1] = DM_VERSION_MINOR;
+ data.version[2] = DM_VERSION_PATCHLEVEL;
+ data.data_size = size_of::<Self>() as u32;
+ data.data_start = 0;
+ data.name.as_mut().write_all(name.as_bytes())?;
+ Ok(data)
+ }
+
+ fn set_uuid(&mut self, uuid: &str) -> Result<()> {
+ let mut dst = self.uuid.as_mut();
+ dst.fill(0);
+ dst.write_all(uuid.as_bytes())?;
+ Ok(())
+ }
+
+ fn as_u8_slice(&self) -> &[u8; size_of::<Self>()] {
+ // SAFETY: lifetime of the output reference isn't changed.
+ unsafe { std::mem::transmute::<&Self, &[u8; size_of::<Self>()]>(&self) }
+ }
+}
+
+/// `DeviceMapper` is the entry point for the device mapper framework. It essentially is a file
+/// handle to "/dev/mapper/control".
+pub struct DeviceMapper(File);
+
+impl DeviceMapper {
+ /// Constructs a new `DeviceMapper` entrypoint. This is essentially the same as opening
+ /// "/dev/mapper/control".
+ pub fn new() -> Result<DeviceMapper> {
+ let f = OpenOptions::new().read(true).write(true).open("/dev/mapper/control")?;
+ Ok(DeviceMapper(f))
+ }
+
+ /// Creates a device mapper device and configure it according to the `target` specification.
+ /// The path to the generated device is "/dev/mapper/<name>".
+ pub fn create_device(&self, name: &str, target: &DmVerityTarget) -> Result<PathBuf> {
+ // Step 1: create an empty device
+ let mut data = DmIoctl::new(&name)?;
+ data.set_uuid(&uuid())?;
+ dm_dev_create(&self, &mut data)?;
+
+ // Step 2: load table onto the device
+ let payload_size = size_of::<DmIoctl>() + target.as_u8_slice().len();
+
+ let mut data = DmIoctl::new(&name)?;
+ data.data_size = payload_size as u32;
+ data.data_start = size_of::<DmIoctl>() as u32;
+ data.target_count = 1;
+ data.flags |= Flag::DM_READONLY_FLAG;
+
+ let mut payload = Vec::with_capacity(payload_size);
+ payload.extend_from_slice(&data.as_u8_slice()[..]);
+ payload.extend_from_slice(&target.as_u8_slice()[..]);
+ dm_table_load(&self, payload.as_mut_ptr() as *mut DmIoctl)?;
+
+ // Step 3: activate the device (note: the term 'suspend' might be misleading, but it
+ // actually activates the table. See include/uapi/linux/dm-ioctl.h
+ let mut data = DmIoctl::new(&name)?;
+ dm_dev_suspend(&self, &mut data)?;
+
+ // Step 4: wait unti the device is created and return the device path
+ let path = Path::new("/dev/mapper").join(&name);
+ wait_for_path(&path)?;
+ Ok(path)
+ }
+
+ /// Removes a mapper device
+ pub fn delete_device_deferred(&self, name: &str) -> Result<()> {
+ let mut data = DmIoctl::new(&name)?;
+ data.flags |= Flag::DM_DEFERRED_REMOVE;
+ dm_dev_remove(&self, &mut data)?;
+ Ok(())
+ }
+}
+
+/// Used to derive a UUID that uniquely identifies a device mapper device when creating it.
+// TODO(jiyong): the v4 is a randomly generated UUID. We might want another version of UUID (e.g.
+// v3) where we can specify the namespace so that we can easily identify UUID's created for this
+// purpose. For now, this random UUID is fine because we are expected to have only "one" instance
+// of dm-verity device in Microdroid.
+fn uuid() -> String {
+ String::from(Uuid::new_v4().to_hyphenated().encode_lower(&mut Uuid::encode_buffer()))
+}
diff --git a/apkverity/src/dm/sys.rs b/apkverity/src/dm/sys.rs
new file mode 100644
index 0000000..f623a2b
--- /dev/null
+++ b/apkverity/src/dm/sys.rs
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use bitflags::bitflags;
+
+// UAPI for device mapper can be found at include/uapi/linux/dm-ioctl.h
+
+pub const DM_IOCTL: u8 = 0xfd;
+
+#[repr(u16)]
+#[allow(non_camel_case_types)]
+#[allow(dead_code)]
+pub enum Cmd {
+ DM_VERSION = 0,
+ DM_REMOVE_ALL,
+ DM_LIST_DEVICES,
+ DM_DEV_CREATE,
+ DM_DEV_REMOVE,
+ DM_DEV_RENAME,
+ DM_DEV_SUSPEND,
+ DM_DEV_STATUS,
+ DM_DEV_WAIT,
+ DM_TABLE_LOAD,
+ DM_TABLE_CLEAR,
+ DM_TABLE_DEPS,
+ DM_TABLE_STATUS,
+ DM_LIST_VERSIONS,
+ DM_TARGET_MSG,
+ DM_DEV_SET_GEOMETRY,
+}
+
+#[repr(C)]
+pub struct DmIoctl {
+ pub version: [u32; 3],
+ pub data_size: u32,
+ pub data_start: u32,
+ pub target_count: u32,
+ pub open_count: i32,
+ pub flags: Flag,
+ pub event_nr: u32,
+ pub padding: u32,
+ pub dev: u64,
+ pub name: [u8; DM_NAME_LEN],
+ pub uuid: [u8; DM_UUID_LEN],
+ pub data: [u8; 7],
+}
+
+pub const DM_VERSION_MAJOR: u32 = 4;
+pub const DM_VERSION_MINOR: u32 = 0;
+pub const DM_VERSION_PATCHLEVEL: u32 = 0;
+
+pub const DM_NAME_LEN: usize = 128;
+pub const DM_UUID_LEN: usize = 129;
+pub const DM_MAX_TYPE_NAME: usize = 16;
+
+bitflags! {
+ pub struct Flag: u32 {
+ const DM_READONLY_FLAG = 1 << 0;
+ const DM_SUSPEND_FLAG = 1 << 1;
+ const DM_PERSISTENT_DEV_FLAG = 1 << 3;
+ const DM_STATUS_TABLE_FLAG = 1 << 4;
+ const DM_ACTIVE_PRESENT_FLAG = 1 << 5;
+ const DM_INACTIVE_PRESENT_FLAG = 1 << 6;
+ const DM_BUFFER_FULL_FLAG = 1 << 8;
+ const DM_SKIP_BDGET_FLAG = 1 << 9;
+ const DM_SKIP_LOCKFS_FLAG = 1 << 10;
+ const DM_NOFLUSH_FLAG = 1 << 11;
+ const DM_QUERY_INACTIVE_TABLE_FLAG = 1 << 12;
+ const DM_UEVENT_GENERATED_FLAG = 1 << 13;
+ const DM_UUID_FLAG = 1 << 14;
+ const DM_SECURE_DATA_FLAG = 1 << 15;
+ const DM_DATA_OUT_FLAG = 1 << 16;
+ const DM_DEFERRED_REMOVE = 1 << 17;
+ const DM_INTERNAL_SUSPEND_FLAG = 1 << 18;
+ }
+}
diff --git a/apkverity/src/dm/verity.rs b/apkverity/src/dm/verity.rs
new file mode 100644
index 0000000..cfc9504
--- /dev/null
+++ b/apkverity/src/dm/verity.rs
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// `dm::verity` module implements the "verity" target in the device mapper framework. Specifically,
+// it provides `DmVerityTargetBuilder` struct which is used to construct a `DmVerityTarget` struct
+// which is then given to `DeviceMapper` to create a mapper device.
+
+use anyhow::{bail, Context, Result};
+use std::io::Write;
+use std::mem::size_of;
+use std::path::Path;
+
+use super::DmTargetSpec;
+use crate::util::*;
+
+// The UAPI for the verity target is here.
+// https://www.kernel.org/doc/Documentation/device-mapper/verity.txt
+
+/// Version of the verity target spec. Only `V1` is supported.
+pub enum DmVerityVersion {
+ V1,
+}
+
+/// The hash algorithm to use. SHA256 and SHA512 are supported.
+pub enum DmVerityHashAlgorithm {
+ SHA256,
+ SHA512,
+}
+
+/// A builder that constructs `DmVerityTarget` struct.
+pub struct DmVerityTargetBuilder<'a> {
+ version: DmVerityVersion,
+ data_device: Option<&'a Path>,
+ data_size: u64,
+ hash_device: Option<&'a Path>,
+ hash_algorithm: DmVerityHashAlgorithm,
+ root_digest: Option<&'a [u8]>,
+ salt: Option<&'a [u8]>,
+}
+
+pub struct DmVerityTarget(Box<[u8]>);
+
+impl DmVerityTarget {
+ pub fn as_u8_slice(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+
+impl<'a> Default for DmVerityTargetBuilder<'a> {
+ fn default() -> Self {
+ DmVerityTargetBuilder {
+ version: DmVerityVersion::V1,
+ data_device: None,
+ data_size: 0,
+ hash_device: None,
+ hash_algorithm: DmVerityHashAlgorithm::SHA256,
+ root_digest: None,
+ salt: None,
+ }
+ }
+}
+
+impl<'a> DmVerityTargetBuilder<'a> {
+ /// Sets the device that will be used as the data device (i.e. providing actual data).
+ pub fn data_device(&mut self, p: &'a Path, size: u64) -> &mut Self {
+ self.data_device = Some(p);
+ self.data_size = size;
+ self
+ }
+
+ /// Sets the device that provides the merkle tree.
+ pub fn hash_device(&mut self, p: &'a Path) -> &mut Self {
+ self.hash_device = Some(p);
+ self
+ }
+
+ /// Sets the hash algorithm that the merkel tree is using.
+ pub fn hash_algorithm(&mut self, algo: DmVerityHashAlgorithm) -> &mut Self {
+ self.hash_algorithm = algo;
+ self
+ }
+
+ /// Sets the root digest of the merkle tree. The format is hexadecimal string.
+ pub fn root_digest(&mut self, digest: &'a [u8]) -> &mut Self {
+ self.root_digest = Some(digest);
+ self
+ }
+
+ /// Sets the salt used when creating the merkle tree. Note that this is empty for merkle trees
+ /// created following the APK signature scheme V4.
+ pub fn salt(&mut self, salt: &'a [u8]) -> &mut Self {
+ self.salt = Some(salt);
+ self
+ }
+
+ /// Constructs a `DmVerityTarget`.
+ pub fn build(&self) -> Result<DmVerityTarget> {
+ // The `DmVerityTarget` struct actually is a flattened data consisting of a header and
+ // body. The format of the header is `dm_target_spec` as defined in
+ // include/uapi/linux/dm-ioctl.h. The format of the body, in case of `verity` target is
+ // https://www.kernel.org/doc/Documentation/device-mapper/verity.txt
+ //
+ // Step 1: check the validity of the inputs and extra additional data (e.g. block size)
+ // from them.
+ let version = match self.version {
+ DmVerityVersion::V1 => 1,
+ };
+
+ let data_device_path = self
+ .data_device
+ .context("data device is not set")?
+ .to_str()
+ .context("data device path is not encoded in utf8")?;
+ let stat = fstat(self.data_device.unwrap())?; // safe; checked just above
+ let data_block_size = stat.st_blksize as u64;
+ let data_size = self.data_size;
+ let num_data_blocks = data_size / data_block_size;
+
+ let hash_device_path = self
+ .hash_device
+ .context("hash device is not set")?
+ .to_str()
+ .context("hash device path is not encoded in utf8")?;
+ let stat = fstat(self.data_device.unwrap())?; // safe; checked just above
+ let hash_block_size = stat.st_blksize;
+
+ let hash_algorithm = match self.hash_algorithm {
+ DmVerityHashAlgorithm::SHA256 => "sha256",
+ DmVerityHashAlgorithm::SHA512 => "sha512",
+ };
+
+ let root_digest = if let Some(root_digest) = self.root_digest {
+ hexstring_from(root_digest)
+ } else {
+ bail!("root digest is not set")
+ };
+
+ let salt = if self.salt.is_none() || self.salt.unwrap().is_empty() {
+ "-".to_string() // Note. It's not an empty string!
+ } else {
+ hexstring_from(self.salt.unwrap())
+ };
+
+ // Step2: serialize the information according to the spec, which is ...
+ // DmTargetSpec{...}
+ // <version> <dev> <hash_dev>
+ // <data_block_size> <hash_block_size>
+ // <num_data_blocks> <hash_start_block>
+ // <algorithm> <digest> <salt>
+ // [<#opt_params> <opt_params>]
+ // null terminator
+
+ // TODO(jiyong): support the optional parameters... if needed.
+ let mut body = String::new();
+ use std::fmt::Write;
+ write!(&mut body, "{} ", version)?;
+ write!(&mut body, "{} ", data_device_path)?;
+ write!(&mut body, "{} ", hash_device_path)?;
+ write!(&mut body, "{} ", data_block_size)?;
+ write!(&mut body, "{} ", hash_block_size)?;
+ write!(&mut body, "{} ", num_data_blocks)?;
+ write!(&mut body, "{} ", 0)?; // hash_start_block
+ write!(&mut body, "{} ", hash_algorithm)?;
+ write!(&mut body, "{} ", root_digest)?;
+ write!(&mut body, "{}", salt)?;
+ write!(&mut body, "\0")?; // null terminator
+
+ let size = size_of::<DmTargetSpec>() + body.len();
+ let aligned_size = (size + 7) & !7; // align to 8 byte boundaries
+ let padding = aligned_size - size;
+ let mut header = DmTargetSpec::new("verity")?;
+ header.sector_start = 0;
+ header.length = data_size / 512; // number of 512-byte sectors
+ header.next = aligned_size as u32;
+
+ let mut buf = Vec::with_capacity(aligned_size);
+ buf.write_all(header.as_u8_slice())?;
+ buf.write_all(body.as_bytes())?;
+ buf.write_all(vec![0; padding].as_slice())?;
+ Ok(DmVerityTarget(buf.into_boxed_slice()))
+ }
+}
diff --git a/apkverity/src/loopdevice.rs b/apkverity/src/loopdevice.rs
new file mode 100644
index 0000000..bb0e767
--- /dev/null
+++ b/apkverity/src/loopdevice.rs
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// `loopdevice` module provides `attach` and `detach` functions that are for attaching and
+// detaching a regular file to and from a loop device. Note that
+// `loopdev`(https://crates.io/crates/loopdev) is a public alternative to this. In-house
+// implementation was chosen to make Android-specific changes (like the use of the new
+// LOOP_CONFIGURE instead of the legacy LOOP_SET_FD + LOOP_SET_STATUS64 combo which is considerably
+// slower than the former).
+
+mod sys;
+
+use anyhow::{Context, Result};
+use std::fs::{File, OpenOptions};
+use std::os::unix::io::AsRawFd;
+use std::path::{Path, PathBuf};
+use std::thread;
+use std::time::{Duration, Instant};
+
+use crate::loopdevice::sys::*;
+use crate::util::*;
+
+// These are old-style ioctls, thus *_bad.
+nix::ioctl_none_bad!(_loop_ctl_get_free, LOOP_CTL_GET_FREE);
+nix::ioctl_write_ptr_bad!(_loop_configure, LOOP_CONFIGURE, loop_config);
+nix::ioctl_none_bad!(_loop_clr_fd, LOOP_CLR_FD);
+
+fn loop_ctl_get_free(ctrl_file: &File) -> Result<i32> {
+ // SAFETY: this ioctl changes the state in kernel, but not the state in this process.
+ // The returned device number is a global resource; not tied to this process. So, we don't
+ // need to keep track of it.
+ Ok(unsafe { _loop_ctl_get_free(ctrl_file.as_raw_fd()) }?)
+}
+
+fn loop_configure(device_file: &File, config: &loop_config) -> Result<i32> {
+ // SAFETY: this ioctl changes the state in kernel, but not the state in this process.
+ Ok(unsafe { _loop_configure(device_file.as_raw_fd(), config) }?)
+}
+
+fn loop_clr_fd(device_file: &File) -> Result<i32> {
+ // SAFETY: this ioctl disassociates the loop device with `device_file`, where the FD will
+ // remain opened afterward. The association itself is kept for open FDs.
+ Ok(unsafe { _loop_clr_fd(device_file.as_raw_fd()) }?)
+}
+
+/// Creates a loop device and attach the given file at `path` as the backing store.
+pub fn attach<P: AsRef<Path>>(path: P, offset: u64, size_limit: u64) -> Result<PathBuf> {
+ // Attaching a file to a loop device can make a race condition; a loop device number obtained
+ // from LOOP_CTL_GET_FREE might have been used by another thread or process. In that case the
+ // subsequet LOOP_CONFIGURE ioctl returns with EBUSY. Try until it succeeds.
+ //
+ // Note that the timing parameters below are chosen rather arbitrarily. In practice (i.e.
+ // inside Microdroid) we can't experience the race condition because `apkverity` is the only
+ // user of /dev/loop-control at the moment. This loop is mostly for testing where multiple
+ // tests run concurrently.
+ const TIMEOUT: Duration = Duration::from_secs(1);
+ const INTERVAL: Duration = Duration::from_millis(10);
+
+ let begin = Instant::now();
+ loop {
+ match try_attach(&path, offset, size_limit) {
+ Ok(loop_dev) => return Ok(loop_dev),
+ Err(e) => {
+ if begin.elapsed() > TIMEOUT {
+ return Err(e);
+ }
+ }
+ };
+ thread::sleep(INTERVAL);
+ }
+}
+
+fn try_attach<P: AsRef<Path>>(path: P, offset: u64, size_limit: u64) -> Result<PathBuf> {
+ // Get a free loop device
+ wait_for_path(LOOP_CONTROL)?;
+ let ctrl_file = OpenOptions::new()
+ .read(true)
+ .write(true)
+ .open(LOOP_CONTROL)
+ .context("Failed to open loop control")?;
+ let num = loop_ctl_get_free(&ctrl_file).context("Failed to get free loop device")?;
+
+ // Construct the loop_config struct
+ let backing_file = OpenOptions::new()
+ .read(true)
+ .open(&path)
+ .context(format!("failed to open {:?}", path.as_ref()))?;
+ // SAFETY: zero initialized C structs is safe
+ let mut config = unsafe { std::mem::MaybeUninit::<loop_config>::zeroed().assume_init() };
+ config.fd = backing_file.as_raw_fd() as u32;
+ config.block_size = 4096;
+ config.info.lo_offset = offset;
+ config.info.lo_sizelimit = size_limit;
+ config.info.lo_flags |= Flag::LO_FLAGS_DIRECT_IO | Flag::LO_FLAGS_READ_ONLY;
+
+ // Special case: don't use direct IO when the backing file is already a loop device, which
+ // happens only during test. DirectIO-on-loop-over-loop makes the outer loop device
+ // unaccessible.
+ #[cfg(test)]
+ if path.as_ref().to_str().unwrap().starts_with("/dev/loop") {
+ config.info.lo_flags.remove(Flag::LO_FLAGS_DIRECT_IO);
+ }
+
+ // Configure the loop device to attach the backing file
+ let device_path = format!("/dev/loop{}", num);
+ wait_for_path(&device_path)?;
+ let device_file = OpenOptions::new()
+ .read(true)
+ .write(true)
+ .open(&device_path)
+ .context(format!("failed to open {:?}", &device_path))?;
+ loop_configure(&device_file, &mut config)
+ .context(format!("Failed to configure {:?}", &device_path))?;
+
+ Ok(PathBuf::from(device_path))
+}
+
+/// Detaches backing file from the loop device `path`.
+pub fn detach<P: AsRef<Path>>(path: P) -> Result<()> {
+ let device_file = OpenOptions::new().read(true).write(true).open(&path)?;
+ loop_clr_fd(&device_file)?;
+ Ok(())
+}
diff --git a/apkverity/src/loopdevice/sys.rs b/apkverity/src/loopdevice/sys.rs
new file mode 100644
index 0000000..2d4977b
--- /dev/null
+++ b/apkverity/src/loopdevice/sys.rs
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use bitflags::bitflags;
+
+// This UAPI is copied and converted from include/uapi/linux/loop.h Note that this module doesn't
+// implement all the features introduced in loop(4). Only the features that are required to support
+// the `apkdmverity` use cases are implemented.
+
+pub const LOOP_CONTROL: &str = "/dev/loop-control";
+
+pub const LOOP_CTL_GET_FREE: libc::c_ulong = 0x4C82;
+pub const LOOP_CONFIGURE: libc::c_ulong = 0x4C0A;
+pub const LOOP_CLR_FD: libc::c_ulong = 0x4C01;
+
+#[repr(C)]
+pub struct loop_config {
+ pub fd: u32,
+ pub block_size: u32,
+ pub info: loop_info64,
+ pub reserved: [u64; 8],
+}
+
+#[repr(C)]
+pub struct loop_info64 {
+ pub lo_device: u64,
+ pub lo_inode: u64,
+ pub lo_rdevice: u64,
+ pub lo_offset: u64,
+ pub lo_sizelimit: u64,
+ pub lo_number: u32,
+ pub lo_encrypt_type: u32,
+ pub lo_encrypt_key_size: u32,
+ pub lo_flags: Flag,
+ pub lo_file_name: [u8; LO_NAME_SIZE],
+ pub lo_crypt_name: [u8; LO_NAME_SIZE],
+ pub lo_encrypt_key: [u8; LO_KEY_SIZE],
+ pub lo_init: [u64; 2],
+}
+
+bitflags! {
+ pub struct Flag: u32 {
+ const LO_FLAGS_READ_ONLY = 1 << 0;
+ const LO_FLAGS_AUTOCLEAR = 1 << 2;
+ const LO_FLAGS_PARTSCAN = 1 << 3;
+ const LO_FLAGS_DIRECT_IO = 1 << 4;
+ }
+}
+
+pub const LO_NAME_SIZE: usize = 64;
+pub const LO_KEY_SIZE: usize = 32;
diff --git a/apkverity/src/main.rs b/apkverity/src/main.rs
new file mode 100644
index 0000000..6fe12a0
--- /dev/null
+++ b/apkverity/src/main.rs
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//! `apkdmverity` is a program that protects a signed APK file using dm-verity. The APK is assumed
+//! to be signed using APK signature scheme V4. The idsig file generated by the signing scheme is
+//! also used as an input to provide the merkle tree. This program is currently intended to be used
+//! to securely mount the APK inside Microdroid. Since the APK is physically stored in the file
+//! system managed by the host Android which is assumed to be compromisable, it is important to
+//! keep the integrity of the file "inside" Microdroid.
+
+mod apksigv4;
+mod dm;
+mod loopdevice;
+mod util;
+
+use crate::apksigv4::*;
+
+use anyhow::{bail, Context, Result};
+use clap::{App, Arg};
+use std::fmt::Debug;
+use std::fs;
+use std::fs::File;
+use std::os::unix::fs::FileTypeExt;
+use std::path::{Path, PathBuf};
+
+fn main() -> Result<()> {
+ let matches = App::new("apkverity")
+ .about("Creates a dm-verity block device out of APK signed with APK signature scheme V4.")
+ .arg(
+ Arg::with_name("apk")
+ .help("Input APK file. Must be signed using the APK signature scheme V4.")
+ .required(true),
+ )
+ .arg(
+ Arg::with_name("idsig")
+ .help("The idsig file having the merkle tree and the signing info.")
+ .required(true),
+ )
+ .arg(
+ Arg::with_name("name")
+ .help(
+ "Name of the dm-verity block device. The block device is created at \
+ \"/dev/mapper/<name>\".",
+ )
+ .required(true),
+ )
+ .get_matches();
+
+ let apk = matches.value_of("apk").unwrap();
+ let idsig = matches.value_of("idsig").unwrap();
+ let name = matches.value_of("name").unwrap();
+ enable_verity(apk, idsig, name)?;
+ Ok(())
+}
+
+struct VerityResult {
+ data_device: PathBuf,
+ hash_device: PathBuf,
+ mapper_device: PathBuf,
+}
+
+const BLOCK_SIZE: u64 = 4096;
+
+// Makes a dm-verity block device out of `apk` and its accompanying `idsig` files.
+fn enable_verity<P: AsRef<Path> + Debug>(apk: P, idsig: P, name: &str) -> Result<VerityResult> {
+ // Attach the apk file to a loop device if the apk file is a regular file. If not (i.e. block
+ // device), we only need to get the size and use the block device as it is.
+ let (data_device, apk_size) = if fs::metadata(&apk)?.file_type().is_block_device() {
+ (apk.as_ref().to_path_buf(), util::blkgetsize64(apk.as_ref())?)
+ } else {
+ let apk_size = fs::metadata(&apk)?.len();
+ if apk_size % BLOCK_SIZE != 0 {
+ bail!("The size of {:?} is not multiple of {}.", &apk, BLOCK_SIZE)
+ }
+ (loopdevice::attach(&apk, 0, apk_size)?, apk_size)
+ };
+
+ // Parse the idsig file to locate the merkle tree in it, then attach the file to a loop device
+ // with the offset so that the start of the merkle tree becomes the beginning of the loop
+ // device.
+ let sig = V4Signature::from(File::open(&idsig)?)?;
+ let offset = sig.merkle_tree_offset;
+ let size = sig.merkle_tree_size as u64;
+ let hash_device = loopdevice::attach(&idsig, offset, size)?;
+
+ // Build a dm-verity target spec from the information from the idsig file. The apk and the
+ // idsig files are used as the data device and the hash device, respectively.
+ let target = dm::DmVerityTargetBuilder::default()
+ .data_device(&data_device, apk_size)
+ .hash_device(&hash_device)
+ .root_digest(&sig.hashing_info.raw_root_hash)
+ .hash_algorithm(match sig.hashing_info.hash_algorithm {
+ apksigv4::HashAlgorithm::SHA256 => dm::DmVerityHashAlgorithm::SHA256,
+ })
+ .salt(&sig.hashing_info.salt)
+ .build()
+ .context(format!("Merkle tree in {:?} is not compatible with dm-verity", &idsig))?;
+
+ // Actually create a dm-verity block device using the spec.
+ let dm = dm::DeviceMapper::new()?;
+ let mapper_device =
+ dm.create_device(&name, &target).context("Failed to create dm-verity device")?;
+
+ Ok(VerityResult { data_device, hash_device, mapper_device })
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::*;
+ use std::fs::OpenOptions;
+ use std::io::{Cursor, Write};
+ use std::os::unix::fs::FileExt;
+
+ struct TestContext<'a> {
+ data_backing_file: &'a Path,
+ hash_backing_file: &'a Path,
+ result: &'a VerityResult,
+ }
+
+ fn create_block_aligned_file(path: &Path, data: &[u8]) {
+ let mut f = File::create(&path).unwrap();
+ f.write_all(data).unwrap();
+
+ // Add padding so that the size of the file is multiple of 4096.
+ let aligned_size = (data.len() as u64 + BLOCK_SIZE - 1) & !(BLOCK_SIZE - 1);
+ let padding = aligned_size - data.len() as u64;
+ f.write_all(vec![0; padding as usize].as_slice()).unwrap();
+ }
+
+ fn prepare_inputs(test_dir: &Path, apk: &[u8], idsig: &[u8]) -> (PathBuf, PathBuf) {
+ let apk_path = test_dir.join("test.apk");
+ let idsig_path = test_dir.join("test.apk.idsig");
+ create_block_aligned_file(&apk_path, apk);
+ create_block_aligned_file(&idsig_path, idsig);
+ (apk_path, idsig_path)
+ }
+
+ fn run_test(apk: &[u8], idsig: &[u8], name: &str, check: fn(TestContext)) {
+ let test_dir = tempfile::TempDir::new().unwrap();
+ let (apk_path, idsig_path) = prepare_inputs(&test_dir.path(), apk, idsig);
+
+ // Run the program and register clean-ups.
+ let ret = enable_verity(&apk_path, &idsig_path, name).unwrap();
+ let ret = scopeguard::guard(ret, |ret| {
+ loopdevice::detach(ret.data_device).unwrap();
+ loopdevice::detach(ret.hash_device).unwrap();
+ let dm = dm::DeviceMapper::new().unwrap();
+ dm.delete_device_deferred(name).unwrap();
+ });
+
+ check(TestContext {
+ data_backing_file: &apk_path,
+ hash_backing_file: &idsig_path,
+ result: &ret,
+ });
+ }
+
+ #[test]
+ fn correct_inputs() {
+ let apk = include_bytes!("../testdata/test.apk");
+ let idsig = include_bytes!("../testdata/test.apk.idsig");
+ run_test(apk.as_ref(), idsig.as_ref(), "correct", |ctx| {
+ let verity = fs::read(&ctx.result.mapper_device).unwrap();
+ let original = fs::read(&ctx.result.data_device).unwrap();
+ assert_eq!(verity.len(), original.len()); // fail fast
+ assert_eq!(verity.as_slice(), original.as_slice());
+ });
+ }
+
+ // A single byte change in the APK file causes an IO error
+ #[test]
+ fn incorrect_apk() {
+ let apk = include_bytes!("../testdata/test.apk");
+ let idsig = include_bytes!("../testdata/test.apk.idsig");
+
+ let mut modified_apk = Vec::new();
+ modified_apk.extend_from_slice(apk);
+ if let Some(byte) = modified_apk.get_mut(100) {
+ *byte = 1;
+ }
+
+ run_test(modified_apk.as_slice(), idsig.as_ref(), "incorrect_apk", |ctx| {
+ let ret = fs::read(&ctx.result.mapper_device).map_err(|e| e.kind());
+ assert_eq!(ret, Err(std::io::ErrorKind::Other));
+ });
+ }
+
+ // A single byte change in the merkle tree also causes an IO error
+ #[test]
+ fn incorrect_merkle_tree() {
+ let apk = include_bytes!("../testdata/test.apk");
+ let idsig = include_bytes!("../testdata/test.apk.idsig");
+
+ // Make a single-byte change to the merkle tree
+ let offset = V4Signature::from(Cursor::new(&idsig)).unwrap().merkle_tree_offset as usize;
+
+ let mut modified_idsig = Vec::new();
+ modified_idsig.extend_from_slice(idsig);
+ if let Some(byte) = modified_idsig.get_mut(offset + 10) {
+ *byte = 1;
+ }
+
+ run_test(apk.as_ref(), modified_idsig.as_slice(), "incorrect_merkle_tree", |ctx| {
+ let ret = fs::read(&ctx.result.mapper_device).map_err(|e| e.kind());
+ assert_eq!(ret, Err(std::io::ErrorKind::Other));
+ });
+ }
+
+ // APK is not altered when the verity device is created, but later modified. IO error should
+ // occur when trying to read the data around the modified location. This is the main scenario
+ // that we'd like to protect.
+ #[test]
+ fn tampered_apk() {
+ let apk = include_bytes!("../testdata/test.apk");
+ let idsig = include_bytes!("../testdata/test.apk.idsig");
+
+ run_test(apk.as_ref(), idsig.as_ref(), "tampered_apk", |ctx| {
+ // At this moment, the verity device is created. Then let's change 10 bytes in the
+ // backing data file.
+ const MODIFIED_OFFSET: u64 = 10000;
+ let f = OpenOptions::new().read(true).write(true).open(ctx.data_backing_file).unwrap();
+ f.write_at(&[0, 1], MODIFIED_OFFSET).unwrap();
+
+ // Read around the modified location causes an error
+ let f = File::open(&ctx.result.mapper_device).unwrap();
+ let mut buf = vec![0; 10]; // just read 10 bytes
+ let ret = f.read_at(&mut buf, MODIFIED_OFFSET).map_err(|e| e.kind());
+ assert!(ret.is_err());
+ assert_eq!(ret, Err(std::io::ErrorKind::Other));
+ });
+ }
+
+ // idsig file is not alread when the verity device is created, but later modified. Unlike to
+ // the APK case, this doesn't occur IO error because the merkle tree is already cached.
+ #[test]
+ fn tampered_idsig() {
+ let apk = include_bytes!("../testdata/test.apk");
+ let idsig = include_bytes!("../testdata/test.apk.idsig");
+ run_test(apk.as_ref(), idsig.as_ref(), "tampered_idsig", |ctx| {
+ // Change 10 bytes in the merkle tree.
+ let f = OpenOptions::new().read(true).write(true).open(ctx.hash_backing_file).unwrap();
+ f.write_at(&[0, 10], 100).unwrap();
+
+ let verity = fs::read(&ctx.result.mapper_device).unwrap();
+ let original = fs::read(&ctx.result.data_device).unwrap();
+ assert_eq!(verity.len(), original.len());
+ assert_eq!(verity.as_slice(), original.as_slice());
+ });
+ }
+
+ // test if both files are already block devices
+ #[test]
+ fn inputs_are_block_devices() {
+ use std::ops::Deref;
+ let apk = include_bytes!("../testdata/test.apk");
+ let idsig = include_bytes!("../testdata/test.apk.idsig");
+
+ let test_dir = tempfile::TempDir::new().unwrap();
+ let (apk_path, idsig_path) = prepare_inputs(&test_dir.path(), apk, idsig);
+
+ // attach the files to loop devices to make them block devices
+ let apk_size = fs::metadata(&apk_path).unwrap().len();
+ let idsig_size = fs::metadata(&idsig_path).unwrap().len();
+
+ // Note that apk_loop_device is not detatched. This is because, when the apk file is
+ // already a block device, `enable_verity` uses the block device as it is. The detatching
+ // of the data device is done in the scopeguard for the return value of `enable_verity`
+ // below. Only the idsig_loop_device needs detatching.
+ let apk_loop_device = loopdevice::attach(&apk_path, 0, apk_size).unwrap();
+ let idsig_loop_device =
+ scopeguard::guard(loopdevice::attach(&idsig_path, 0, idsig_size).unwrap(), |dev| {
+ loopdevice::detach(dev).unwrap()
+ });
+
+ let name = "loop_as_input";
+ // Run the program WITH the loop devices, not the regular files.
+ let ret = enable_verity(apk_loop_device.deref(), idsig_loop_device.deref(), &name).unwrap();
+ let ret = scopeguard::guard(ret, |ret| {
+ loopdevice::detach(ret.data_device).unwrap();
+ loopdevice::detach(ret.hash_device).unwrap();
+ let dm = dm::DeviceMapper::new().unwrap();
+ dm.delete_device_deferred(name).unwrap();
+ });
+
+ let verity = fs::read(&ret.mapper_device).unwrap();
+ let original = fs::read(&apk_path).unwrap();
+ assert_eq!(verity.len(), original.len()); // fail fast
+ assert_eq!(verity.as_slice(), original.as_slice());
+ }
+}
diff --git a/apkverity/src/util.rs b/apkverity/src/util.rs
new file mode 100644
index 0000000..415e99b
--- /dev/null
+++ b/apkverity/src/util.rs
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use anyhow::{bail, Result};
+use nix::sys::stat::FileStat;
+use std::fs;
+use std::fs::File;
+use std::os::unix::fs::FileTypeExt;
+use std::os::unix::io::AsRawFd;
+use std::path::Path;
+use std::thread;
+use std::time::{Duration, Instant};
+
+/// Returns when the file exists on the given `path` or timeout (1s) occurs.
+pub fn wait_for_path<P: AsRef<Path>>(path: P) -> Result<()> {
+ const TIMEOUT: Duration = Duration::from_secs(1);
+ const INTERVAL: Duration = Duration::from_millis(10);
+ let begin = Instant::now();
+ while !path.as_ref().exists() {
+ if begin.elapsed() > TIMEOUT {
+ bail!("{:?} not found. TIMEOUT.", path.as_ref());
+ }
+ thread::sleep(INTERVAL);
+ }
+ Ok(())
+}
+
+/// Returns hexadecimal reprentation of a given byte array.
+pub fn hexstring_from(s: &[u8]) -> String {
+ s.iter().map(|byte| format!("{:02x}", byte)).reduce(|i, j| i + &j).unwrap_or(String::new())
+}
+
+/// fstat that accepts a path rather than FD
+pub fn fstat(p: &Path) -> Result<FileStat> {
+ let f = File::open(p)?;
+ Ok(nix::sys::stat::fstat(f.as_raw_fd())?)
+}
+
+// From include/uapi/linux/fs.h
+const BLK: u8 = 0x12;
+const BLKGETSIZE64: u8 = 114;
+nix::ioctl_read!(_blkgetsize64, BLK, BLKGETSIZE64, libc::size_t);
+
+/// Gets the size of a block device
+pub fn blkgetsize64(p: &Path) -> Result<u64> {
+ let f = File::open(p)?;
+ if !f.metadata()?.file_type().is_block_device() {
+ bail!("{:?} is not a block device", p);
+ }
+ let mut size: usize = 0;
+ // SAFETY: kernel copies the return value out to `size`. The file is kept open until the end of
+ // this function.
+ unsafe { _blkgetsize64(f.as_raw_fd(), &mut size) }?;
+ Ok(size as u64)
+}
diff --git a/apkverity/testdata/README b/apkverity/testdata/README
new file mode 100644
index 0000000..b219b24
--- /dev/null
+++ b/apkverity/testdata/README
@@ -0,0 +1,24 @@
+The test data is generated as follows:
+
+$ keytool -keystore keystore -genkey -alias mykey
+The password for the keystore and the key is 123456.
+
+The signer information is set as follows:
+
+CN=Android, OU=Android, O=Android, L=Mountain View, ST=CA, C=US
+
+Build a random apk (Bluetooth.apk is chosen arbitrary)
+
+$ m Bluetooth
+$ cp $ANDROID_PRODUCT_OUT/system/app/Bluetooth.apk ./test.apk
+
+Sign it using the apksigner.
+
+$ m apksigner
+$ apksigner sign --ks keystore ./test.apk
+
+Check that the idsig file is created.
+
+$ ls -l test.apk*
+-rw-r----- 1 jiyong primarygroup 3888734 Jun 4 01:08 test.apk
+-rw-r----- 1 jiyong primarygroup 39115 Jun 4 01:08 test.apk.idsig
diff --git a/apkverity/testdata/keystore b/apkverity/testdata/keystore
new file mode 100644
index 0000000..3a4f4a7
--- /dev/null
+++ b/apkverity/testdata/keystore
Binary files differ
diff --git a/apkverity/testdata/test.apk b/apkverity/testdata/test.apk
new file mode 100644
index 0000000..cbee532
--- /dev/null
+++ b/apkverity/testdata/test.apk
Binary files differ
diff --git a/apkverity/testdata/test.apk.idsig b/apkverity/testdata/test.apk.idsig
new file mode 100644
index 0000000..8c112de
--- /dev/null
+++ b/apkverity/testdata/test.apk.idsig
Binary files differ