Move APEX handling to a library
Make a library for APEX handling using the microdroid_manager logic as
the basis. Other components are going to need to be able to extract the
keys from APEXs so make the logic reusable and available to host tools.
Test: atest microdroid_manager_test
Test: atest libapexutil_rust.test
Change-Id: I36abf21b59b67046b1464bf47f7be40186926410
diff --git a/libs/apexutil/Android.bp b/libs/apexutil/Android.bp
new file mode 100644
index 0000000..a1a1ca6
--- /dev/null
+++ b/libs/apexutil/Android.bp
@@ -0,0 +1,40 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+ name: "libapexutil_rust.defaults",
+ crate_name: "apexutil",
+ host_supported: true,
+ srcs: ["src/lib.rs"],
+ prefer_rlib: true,
+ edition: "2018",
+ rustlibs: [
+ "libavb_bindgen",
+ "liblog_rust",
+ "libthiserror",
+ "libzip",
+ ],
+}
+
+rust_library {
+ name: "libapexutil_rust",
+ defaults: ["libapexutil_rust.defaults"],
+}
+
+rust_test {
+ name: "libapexutil_rust.test",
+ defaults: ["libapexutil_rust.defaults"],
+ test_suites: ["general-tests"],
+ data: ["tests/data/*"],
+ target: {
+ host: {
+ // TODO(b/204562227): remove once the build does this automatically
+ data_libs: [
+ "libc++",
+ "libcrypto",
+ "libz",
+ ],
+ },
+ },
+}
diff --git a/libs/apexutil/src/lib.rs b/libs/apexutil/src/lib.rs
new file mode 100644
index 0000000..d53e907
--- /dev/null
+++ b/libs/apexutil/src/lib.rs
@@ -0,0 +1,312 @@
+// Copyright 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.
+
+//! Routines for handling APEX payload
+
+use avb_bindgen::*;
+use std::ffi::{c_void, CStr};
+use std::fs::File;
+use std::io::{self, Read, Seek, SeekFrom};
+use std::mem::{size_of, zeroed};
+use std::ops::Deref;
+use std::ptr::null_mut;
+use std::slice::{from_raw_parts, from_raw_parts_mut};
+use thiserror::Error;
+use zip::result::ZipError;
+use zip::ZipArchive;
+
+const APEX_PUBKEY_ENTRY: &str = "apex_pubkey";
+const APEX_PAYLOAD_ENTRY: &str = "apex_payload.img";
+
+/// Errors from parsing an APEX.
+#[derive(Debug, Error)]
+pub enum ApexParseError {
+ /// There was an IO error.
+ #[error("IO error")]
+ Io(#[from] io::Error),
+ /// The Zip archive was invalid.
+ #[error("Cannot read zip archive")]
+ InvalidZip(&'static str),
+ /// The apex_pubkey file was missing from the APEX.
+ #[error("APEX doesn't contain apex_pubkey")]
+ PubkeyMissing,
+ /// The apex_payload.img file was missing from the APEX.
+ #[error("APEX doesn't contain apex_payload.img")]
+ PayloadMissing,
+ /// The AVB footer in the APEX payload was invalid.
+ #[error("Cannot validate APEX payload AVB footer")]
+ InvalidPayloadAvbFooter,
+ /// There were no descriptors in the APEX payload's AVB footer.
+ #[error("No descriptors found in payload AVB footer")]
+ NoDescriptors,
+ /// There was an invalid descriptor in the APEX payload's AVB footer.
+ #[error("Invalid descriptor found in payload AVB footer")]
+ InvalidDescriptor,
+ /// There was no hashtree descriptor in the APEX payload's AVB footer.
+ #[error("Non-hashtree descriptor found in payload AVB footer")]
+ DescriptorNotHashtree,
+ /// There was an invalid hashtree descriptor in the APEX payload's AVB footer.
+ #[error("Invalid hashtree descriptor found in payload AVB footer")]
+ InvalidHashtreeDescriptor,
+}
+
+/// Errors from verifying an APEX.
+#[derive(Debug, Error)]
+pub enum ApexVerificationError {
+ /// There was an error parsing the APEX.
+ #[error("Cannot parse APEX file")]
+ ParseError(#[from] ApexParseError),
+ /// The APEX payload signature did not validate.
+ #[error("Cannot verify payload signature")]
+ BadPayloadSignature(String),
+ /// The APEX payload was signed with a different key.
+ #[error("Payload is signed with the wrong key")]
+ BadPayloadKey,
+}
+
+/// Verification result holds public key and root digest of apex_payload.img
+pub struct ApexVerificationResult {
+ /// The public key that verifies the payload signature.
+ pub public_key: Vec<u8>,
+ /// The root digest of the payload hashtree.
+ pub root_digest: Vec<u8>,
+}
+
+/// Verify APEX payload by AVB verification and return public key and root digest
+pub fn verify(path: &str) -> Result<ApexVerificationResult, ApexVerificationError> {
+ let apex_file = File::open(path).map_err(ApexParseError::Io)?;
+ let (public_key, image_offset, image_size) = get_public_key_and_image_info(&apex_file)?;
+ let root_digest = verify_vbmeta(apex_file, image_offset, image_size, &public_key)?;
+ Ok(ApexVerificationResult { public_key, root_digest })
+}
+
+fn get_public_key_and_image_info(apex_file: &File) -> Result<(Vec<u8>, u64, u64), ApexParseError> {
+ let mut z = ZipArchive::new(apex_file).map_err(|err| match err {
+ ZipError::Io(err) => ApexParseError::Io(err),
+ ZipError::InvalidArchive(s) | ZipError::UnsupportedArchive(s) => {
+ ApexParseError::InvalidZip(s)
+ }
+ ZipError::FileNotFound => unreachable!(),
+ })?;
+
+ let mut public_key = Vec::new();
+ z.by_name(APEX_PUBKEY_ENTRY)
+ .map_err(|err| match err {
+ ZipError::Io(err) => ApexParseError::Io(err),
+ ZipError::FileNotFound => ApexParseError::PubkeyMissing,
+ ZipError::InvalidArchive(s) | ZipError::UnsupportedArchive(s) => {
+ ApexParseError::InvalidZip(s)
+ }
+ })?
+ .read_to_end(&mut public_key)?;
+
+ let (image_offset, image_size) = z
+ .by_name(APEX_PAYLOAD_ENTRY)
+ .map(|f| (f.data_start(), f.size()))
+ .map_err(|err| match err {
+ ZipError::Io(err) => ApexParseError::Io(err),
+ ZipError::FileNotFound => ApexParseError::PayloadMissing,
+ ZipError::InvalidArchive(s) | ZipError::UnsupportedArchive(s) => {
+ ApexParseError::InvalidZip(s)
+ }
+ })?;
+
+ Ok((public_key, image_offset, image_size))
+}
+
+// Manual addition of a missing enum
+#[allow(non_camel_case_types, dead_code)]
+#[repr(u8)]
+enum AvbDescriptorTag {
+ AVB_DESCRIPTOR_TAG_PROPERTY = 0,
+ AVB_DESCRIPTOR_TAG_HASHTREE,
+ AVB_DESCRIPTOR_TAG_HASH,
+ AVB_DESCRIPTOR_TAG_KERNEL_CMDLINE,
+ AVB_DESCRIPTOR_TAG_CHAIN_PARTITION,
+}
+
+const FOOTER_SIZE: usize = size_of::<AvbFooter>();
+const HASHTREE_DESCRIPTOR_SIZE: usize = size_of::<AvbHashtreeDescriptor>();
+
+/// Verify VBmeta image and return root digest
+fn verify_vbmeta<R: Read + Seek>(
+ image: R,
+ offset: u64,
+ size: u64,
+ public_key: &[u8],
+) -> Result<Vec<u8>, ApexVerificationError> {
+ let vbmeta = VbMeta::from(image, offset, size)?;
+ vbmeta.verify(public_key)?;
+ for &descriptor in vbmeta.descriptors()?.iter() {
+ if let Ok(hashtree_descriptor) = HashtreeDescriptor::from(descriptor) {
+ return Ok(hashtree_descriptor.root_digest());
+ }
+ }
+ Err(ApexParseError::DescriptorNotHashtree.into())
+}
+
+struct VbMeta {
+ data: Vec<u8>,
+}
+
+impl VbMeta {
+ // Read a VbMeta data from a given image
+ fn from<R: Read + Seek>(
+ mut image: R,
+ offset: u64,
+ size: u64,
+ ) -> Result<VbMeta, ApexParseError> {
+ // Get AvbFooter first
+ image.seek(SeekFrom::Start(offset + size - FOOTER_SIZE as u64))?;
+ // SAFETY: AvbDescriptor is a "repr(C,packed)" struct from bindgen
+ let mut footer: AvbFooter = unsafe { zeroed() };
+ // SAFETY: safe to read because of seek(-FOOTER_SIZE) above
+ let avb_footer_valid = unsafe {
+ let footer_slice = from_raw_parts_mut(&mut footer as *mut _ as *mut u8, FOOTER_SIZE);
+ image.read_exact(footer_slice)?;
+ avb_footer_validate_and_byteswap(&footer, &mut footer)
+ };
+ if !avb_footer_valid {
+ return Err(ApexParseError::InvalidPayloadAvbFooter);
+ }
+ // Get VbMeta block
+ image.seek(SeekFrom::Start(offset + footer.vbmeta_offset))?;
+ let vbmeta_size = footer.vbmeta_size as usize;
+ let mut data = vec![0u8; vbmeta_size];
+ image.read_exact(&mut data)?;
+ Ok(VbMeta { data })
+ }
+ // Verify VbMeta image. Its enclosed public key should match with a given public key.
+ fn verify(&self, outer_public_key: &[u8]) -> Result<(), ApexVerificationError> {
+ // SAFETY: self.data points to a valid VBMeta data and avb_vbmeta_image_verify should work fine
+ // with it
+ let public_key = unsafe {
+ let mut pk_ptr: *const u8 = null_mut();
+ let mut pk_len: usize = 0;
+ let res = avb_vbmeta_image_verify(
+ self.data.as_ptr(),
+ self.data.len(),
+ &mut pk_ptr,
+ &mut pk_len,
+ );
+ if res != AvbVBMetaVerifyResult_AVB_VBMETA_VERIFY_RESULT_OK {
+ return Err(ApexVerificationError::BadPayloadSignature(
+ CStr::from_ptr(avb_vbmeta_verify_result_to_string(res))
+ .to_string_lossy()
+ .into_owned(),
+ ));
+ }
+ from_raw_parts(pk_ptr, pk_len)
+ };
+
+ if public_key != outer_public_key {
+ return Err(ApexVerificationError::BadPayloadKey);
+ }
+ Ok(())
+ }
+ // Return a slice of AvbDescriptor pointers
+ fn descriptors(&self) -> Result<Descriptors, ApexParseError> {
+ let mut num: usize = 0;
+ // SAFETY: ptr will be freed by Descriptor.
+ Ok(unsafe {
+ let ptr = avb_descriptor_get_all(self.data.as_ptr(), self.data.len(), &mut num);
+ if ptr.is_null() {
+ return Err(ApexParseError::NoDescriptors);
+ }
+ let all = from_raw_parts(ptr, num);
+ Descriptors { ptr, all }
+ })
+ }
+}
+
+struct HashtreeDescriptor {
+ ptr: *const u8,
+ inner: AvbHashtreeDescriptor,
+}
+
+impl HashtreeDescriptor {
+ fn from(descriptor: *const AvbDescriptor) -> Result<HashtreeDescriptor, ApexParseError> {
+ // SAFETY: AvbDescriptor is a "repr(C,packed)" struct from bindgen
+ let mut desc: AvbDescriptor = unsafe { zeroed() };
+ // SAFETY: both points to valid AvbDescriptor pointers
+ if !unsafe { avb_descriptor_validate_and_byteswap(descriptor, &mut desc) } {
+ return Err(ApexParseError::InvalidDescriptor);
+ }
+ if desc.tag != AvbDescriptorTag::AVB_DESCRIPTOR_TAG_HASHTREE as u64 {
+ return Err(ApexParseError::DescriptorNotHashtree);
+ }
+ // SAFETY: AvbHashtreeDescriptor is a "repr(C, packed)" struct from bindgen
+ let mut hashtree_descriptor: AvbHashtreeDescriptor = unsafe { zeroed() };
+ // SAFETY: With tag == AVB_DESCRIPTOR_TAG_HASHTREE, descriptor should point to
+ // a AvbHashtreeDescriptor.
+ if !unsafe {
+ avb_hashtree_descriptor_validate_and_byteswap(
+ descriptor as *const AvbHashtreeDescriptor,
+ &mut hashtree_descriptor,
+ )
+ } {
+ return Err(ApexParseError::InvalidHashtreeDescriptor);
+ }
+ Ok(Self { ptr: descriptor as *const u8, inner: hashtree_descriptor })
+ }
+ fn root_digest(&self) -> Vec<u8> {
+ // SAFETY: digest_ptr should point to a valid buffer of root_digest_len
+ let root_digest = unsafe {
+ let digest_ptr = self.ptr.offset(
+ HASHTREE_DESCRIPTOR_SIZE as isize
+ + self.inner.partition_name_len as isize
+ + self.inner.salt_len as isize,
+ );
+ from_raw_parts(digest_ptr, self.inner.root_digest_len as usize)
+ };
+ root_digest.to_owned()
+ }
+}
+
+// Wraps pointer to a heap-allocated array of AvbDescriptor pointers
+struct Descriptors<'a> {
+ ptr: *mut *const AvbDescriptor,
+ all: &'a [*const AvbDescriptor],
+}
+
+// Wrapped pointer should be freed with avb_free.
+impl Drop for Descriptors<'_> {
+ fn drop(&mut self) {
+ // SAFETY: ptr is allocated by avb_descriptor_get_all
+ unsafe { avb_free(self.ptr as *mut c_void) }
+ }
+}
+
+impl<'a> Deref for Descriptors<'a> {
+ type Target = &'a [*const AvbDescriptor];
+ fn deref(&self) -> &Self::Target {
+ &self.all
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ fn to_hex_string(buf: &[u8]) -> String {
+ buf.iter().map(|b| format!("{:02x}", b)).collect()
+ }
+ #[test]
+ fn test_open_apex() {
+ let res = verify("tests/data/test.apex").unwrap();
+ assert_eq!(
+ to_hex_string(&res.root_digest),
+ "fe11ab17da0a3a738b54bdc3a13f6139cbdf91ec32f001f8d4bbbf8938e04e39"
+ );
+ }
+}
diff --git a/libs/apexutil/tests/data/README.md b/libs/apexutil/tests/data/README.md
new file mode 100644
index 0000000..82ebec6
--- /dev/null
+++ b/libs/apexutil/tests/data/README.md
@@ -0,0 +1,3 @@
+# Test data
+
+- test.apex: copied from system/apexshim/prebuilts/x86/com.android.apex.cts.shim.v1.apex
\ No newline at end of file
diff --git a/libs/apexutil/tests/data/test.apex b/libs/apexutil/tests/data/test.apex
new file mode 100644
index 0000000..fd79365
--- /dev/null
+++ b/libs/apexutil/tests/data/test.apex
Binary files differ