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/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()))
+}