Extract library for VBMeta image handling
The APEX handling library included VBMeta image verification and parsing
but split this out to its own library so that it can be used by other
components too. The new library hides the libavb FFI to provide a safe
interface.
The library is not complete, it only knows about hashtree descriptors
and its root digest, but other aspects can be exposed as there is need
for them.
Test: atest libvbmeta_rust.test
Test: atest libapexutil_rust.test
Bug: 234564414
Change-Id: Ie176b816f63d2ff7f75deab6c07e1f9bb2e54594
diff --git a/TEST_MAPPING b/TEST_MAPPING
index f40da7e..8a57515 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -35,6 +35,9 @@
"path": "packages/modules/Virtualization/libs/apkverify"
},
{
+ "path": "packages/modules/Virtualization/libs/vbmeta"
+ },
+ {
"path": "packages/modules/Virtualization/authfs"
},
{
diff --git a/libs/apexutil/Android.bp b/libs/apexutil/Android.bp
index a1a1ca6..5b55e1c 100644
--- a/libs/apexutil/Android.bp
+++ b/libs/apexutil/Android.bp
@@ -7,12 +7,11 @@
crate_name: "apexutil",
host_supported: true,
srcs: ["src/lib.rs"],
- prefer_rlib: true,
edition: "2018",
rustlibs: [
- "libavb_bindgen",
"liblog_rust",
"libthiserror",
+ "libvbmeta_rust",
"libzip",
],
}
@@ -25,6 +24,7 @@
rust_test {
name: "libapexutil_rust.test",
defaults: ["libapexutil_rust.defaults"],
+ prefer_rlib: true,
test_suites: ["general-tests"],
data: ["tests/data/*"],
target: {
diff --git a/libs/apexutil/src/lib.rs b/libs/apexutil/src/lib.rs
index d53e907..63b09de 100644
--- a/libs/apexutil/src/lib.rs
+++ b/libs/apexutil/src/lib.rs
@@ -14,15 +14,10 @@
//! 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 std::io::{self, Read};
use thiserror::Error;
+use vbmeta::VbMetaImage;
use zip::result::ZipError;
use zip::ZipArchive;
@@ -44,21 +39,12 @@
/// 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")]
+ /// There was no hashtree descriptor in the APEX payload's VBMeta image.
+ #[error("Non-hashtree descriptor found in payload's VBMeta image")]
DescriptorNotHashtree,
- /// There was an invalid hashtree descriptor in the APEX payload's AVB footer.
- #[error("Invalid hashtree descriptor found in payload AVB footer")]
- InvalidHashtreeDescriptor,
+ /// There was an error parsing the APEX payload's VBMeta image.
+ #[error("Could not parse payload's VBMeta image")]
+ PayloadVbmetaError(#[from] vbmeta::VbMetaImageParseError),
}
/// Errors from verifying an APEX.
@@ -67,12 +53,12 @@
/// 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,
+ /// There was an error validating the APEX payload's VBMeta image.
+ #[error("Could not parse payload's VBMeta image")]
+ PayloadVbmetaError(#[from] vbmeta::VbMetaImageVerificationError),
+ /// The APEX payload was not verified with the apex_pubkey.
+ #[error("APEX pubkey mismatch")]
+ ApexPubkeyMistmatch,
}
/// Verification result holds public key and root digest of apex_payload.img
@@ -87,8 +73,24 @@
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 })
+ let vbmeta = VbMetaImage::verify_reader_region(apex_file, image_offset, image_size)?;
+ let root_digest = find_root_digest(&vbmeta)?;
+ match vbmeta.public_key() {
+ Some(payload_public_key) if public_key == payload_public_key => {
+ Ok(ApexVerificationResult { public_key, root_digest })
+ }
+ _ => Err(ApexVerificationError::ApexPubkeyMistmatch),
+ }
+}
+
+fn find_root_digest(vbmeta: &VbMetaImage) -> Result<Vec<u8>, ApexParseError> {
+ // APEXs use the root digest from the first hashtree descriptor to describe the payload.
+ for descriptor in vbmeta.descriptors()?.iter() {
+ if let vbmeta::Descriptor::Hashtree(_) = descriptor {
+ return Ok(descriptor.to_hashtree()?.root_digest().to_vec());
+ }
+ }
+ Err(ApexParseError::DescriptorNotHashtree)
}
fn get_public_key_and_image_info(apex_file: &File) -> Result<(Vec<u8>, u64, u64), ApexParseError> {
@@ -125,176 +127,6 @@
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::*;
diff --git a/libs/vbmeta/Android.bp b/libs/vbmeta/Android.bp
new file mode 100644
index 0000000..84dde11
--- /dev/null
+++ b/libs/vbmeta/Android.bp
@@ -0,0 +1,46 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+ name: "libvbmeta_rust.defaults",
+ crate_name: "vbmeta",
+ host_supported: true,
+ srcs: ["src/lib.rs"],
+ edition: "2018",
+ rustlibs: [
+ "libavb_bindgen",
+ "libthiserror",
+ ],
+}
+
+rust_library {
+ name: "libvbmeta_rust",
+ defaults: ["libvbmeta_rust.defaults"],
+}
+
+rust_test_host {
+ name: "libvbmeta_rust.test",
+ defaults: ["libvbmeta_rust.defaults"],
+ prefer_rlib: true,
+ rustlibs: [
+ "libanyhow",
+ "libtempfile",
+ ],
+ data: ["tests/data/*"],
+ required: ["avbtool"],
+ test_suites: ["general-tests"],
+ test_options: {
+ unit_test: false,
+ },
+ target: {
+ host: {
+ // TODO(b/204562227): remove once the build does this automatically
+ data: [":avbtool"],
+ data_libs: [
+ "libc++",
+ "libcrypto",
+ ],
+ },
+ },
+}
diff --git a/libs/vbmeta/TEST_MAPPING b/libs/vbmeta/TEST_MAPPING
new file mode 100644
index 0000000..adfcf89
--- /dev/null
+++ b/libs/vbmeta/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit" : [
+ {
+ "name" : "libvbmeta_rust.test"
+ }
+ ]
+}
diff --git a/libs/vbmeta/src/descriptor.rs b/libs/vbmeta/src/descriptor.rs
new file mode 100644
index 0000000..10484ff
--- /dev/null
+++ b/libs/vbmeta/src/descriptor.rs
@@ -0,0 +1,149 @@
+// Copyright 2022, 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 avb_bindgen::{
+ avb_descriptor_foreach, avb_descriptor_validate_and_byteswap,
+ avb_hashtree_descriptor_validate_and_byteswap, AvbDescriptor, AvbHashtreeDescriptor,
+};
+use std::ffi::c_void;
+use std::mem::{size_of, MaybeUninit};
+use std::slice;
+
+use super::VbMetaImageParseError;
+
+// TODO: import these with bindgen
+const AVB_DESCRIPTOR_TAG_PROPERTY: u64 = 0;
+const AVB_DESCRIPTOR_TAG_HASHTREE: u64 = 1;
+const AVB_DESCRIPTOR_TAG_HASH: u64 = 2;
+const AVB_DESCRIPTOR_TAG_KERNEL_CMDLINE: u64 = 3;
+const AVB_DESCRIPTOR_TAG_CHAIN_PARTITION: u64 = 4;
+
+/// The descriptors from a VBMeta image.
+pub struct Descriptors<'a> {
+ descriptors: Vec<Descriptor<'a>>,
+}
+
+/// Enumeration of the possible descriptors.
+#[allow(missing_docs)]
+pub enum Descriptor<'a> {
+ Property(&'a [u8]),
+ Hashtree(&'a [u8]),
+ Hash(&'a [u8]),
+ KernelCmdline(&'a [u8]),
+ ChainPartition(&'a [u8]),
+ Unknown,
+}
+
+/// A hashtree descriptor.
+pub struct HashtreeDescriptor<'a> {
+ descriptor: AvbHashtreeDescriptor,
+ data: &'a [u8],
+}
+
+impl Descriptors<'_> {
+ /// Find the descriptors in a well-formed VBMeta image.
+ pub(super) fn from_image(data: &[u8]) -> Result<Descriptors<'_>, VbMetaImageParseError> {
+ extern "C" fn desc_cb(descriptor: *const AvbDescriptor, user_data: *mut c_void) -> bool {
+ // SAFETY: libavb gives a good pointer for us to work with.
+ let desc = unsafe {
+ let mut desc = MaybeUninit::uninit();
+ if !avb_descriptor_validate_and_byteswap(descriptor, desc.as_mut_ptr()) {
+ return false;
+ }
+ desc.assume_init()
+ };
+ // SAFETY: the descriptor has been validated so it is contained within the image.
+ let data = unsafe {
+ slice::from_raw_parts(
+ descriptor as *const _ as *const u8,
+ size_of::<AvbDescriptor>() + desc.num_bytes_following as usize,
+ )
+ };
+ // SAFETY: this cast gets a reference to the Vec passed as the user_data below.
+ let descriptors = unsafe { &mut *(user_data as *mut Vec<Descriptor>) };
+ descriptors.push(match desc.tag {
+ AVB_DESCRIPTOR_TAG_PROPERTY => Descriptor::Property(data),
+ AVB_DESCRIPTOR_TAG_HASHTREE => Descriptor::Hashtree(data),
+ AVB_DESCRIPTOR_TAG_HASH => Descriptor::Hash(data),
+ AVB_DESCRIPTOR_TAG_KERNEL_CMDLINE => Descriptor::KernelCmdline(data),
+ AVB_DESCRIPTOR_TAG_CHAIN_PARTITION => Descriptor::ChainPartition(data),
+ _ => Descriptor::Unknown,
+ });
+ true
+ }
+
+ let mut descriptors = Vec::new();
+ // SAFETY: the function only reads from the provided data and passes the Vec pointer to the
+ // callback function, treating it as an opaque handle. The descriptors added to the Vec are
+ // contained within the provided data so the lifetime is bound accordingly.
+ if unsafe {
+ let desc = &mut descriptors as *mut _ as *mut c_void;
+ avb_descriptor_foreach(data.as_ptr(), data.len(), Some(desc_cb), desc)
+ } {
+ Ok(Descriptors { descriptors })
+ } else {
+ Err(VbMetaImageParseError::InvalidDescriptor)
+ }
+ }
+
+ /// Get an iterator over the descriptors.
+ pub fn iter(&self) -> slice::Iter<Descriptor> {
+ self.descriptors.iter()
+ }
+}
+
+impl<'a> IntoIterator for Descriptors<'a> {
+ type Item = Descriptor<'a>;
+ type IntoIter = std::vec::IntoIter<Self::Item>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.descriptors.into_iter()
+ }
+}
+
+impl Descriptor<'_> {
+ /// Parse the descriptor as a hashtree descriptor.
+ pub fn to_hashtree(&self) -> Result<HashtreeDescriptor, VbMetaImageParseError> {
+ match self {
+ Self::Hashtree(data) => {
+ // SAFETY: data contains the entire descriptor.
+ let descriptor = unsafe {
+ let mut desc = MaybeUninit::uninit();
+ let src = data.as_ptr() as *const _ as *const AvbHashtreeDescriptor;
+ if !avb_hashtree_descriptor_validate_and_byteswap(src, desc.as_mut_ptr()) {
+ return Err(VbMetaImageParseError::InvalidDescriptor);
+ }
+ desc.assume_init()
+ };
+ Ok(HashtreeDescriptor { descriptor, data })
+ }
+ _ => Err(VbMetaImageParseError::InvalidDescriptor),
+ }
+ }
+
+ // TODO: handle other descriptor type as required
+}
+
+impl HashtreeDescriptor<'_> {
+ /// Get the root digest of the hashtree.
+ pub fn root_digest(&self) -> &[u8] {
+ let begin = size_of::<AvbHashtreeDescriptor>()
+ + self.descriptor.partition_name_len as usize
+ + self.descriptor.salt_len as usize;
+ let end = begin + self.descriptor.root_digest_len as usize;
+ &self.data[begin..end]
+ }
+
+ // TODO: expose other fields as required
+}
diff --git a/libs/vbmeta/src/lib.rs b/libs/vbmeta/src/lib.rs
new file mode 100644
index 0000000..2d3463c
--- /dev/null
+++ b/libs/vbmeta/src/lib.rs
@@ -0,0 +1,292 @@
+// Copyright 2022, 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.
+
+//! A library to verify and parse VBMeta images.
+
+mod descriptor;
+
+use avb_bindgen::{
+ avb_footer_validate_and_byteswap, avb_vbmeta_image_header_to_host_byte_order,
+ avb_vbmeta_image_verify, AvbAlgorithmType_AVB_ALGORITHM_TYPE_NONE, AvbFooter,
+ AvbVBMetaImageHeader, AvbVBMetaVerifyResult_AVB_VBMETA_VERIFY_RESULT_HASH_MISMATCH,
+ AvbVBMetaVerifyResult_AVB_VBMETA_VERIFY_RESULT_INVALID_VBMETA_HEADER,
+ AvbVBMetaVerifyResult_AVB_VBMETA_VERIFY_RESULT_OK,
+ AvbVBMetaVerifyResult_AVB_VBMETA_VERIFY_RESULT_OK_NOT_SIGNED,
+ AvbVBMetaVerifyResult_AVB_VBMETA_VERIFY_RESULT_SIGNATURE_MISMATCH,
+ AvbVBMetaVerifyResult_AVB_VBMETA_VERIFY_RESULT_UNSUPPORTED_VERSION,
+};
+use std::fs::File;
+use std::io::{self, Read, Seek, SeekFrom};
+use std::mem::{size_of, MaybeUninit};
+use std::os::raw::c_uint;
+use std::path::Path;
+use std::ptr::null_mut;
+use std::slice;
+use thiserror::Error;
+
+pub use crate::descriptor::{Descriptor, Descriptors};
+
+/// Errors from parsing a VBMeta image.
+#[derive(Debug, Error)]
+pub enum VbMetaImageParseError {
+ /// There was an IO error.
+ #[error("IO error")]
+ Io(#[from] io::Error),
+ /// The image footer was invalid.
+ #[error("Invalid footer")]
+ InvalidFooter,
+ /// The image header was invalid.
+ #[error("Invalid header")]
+ InvalidHeader,
+ /// The image version is not supported.
+ #[error("Unsupported version")]
+ UnsupportedVersion,
+ /// There was an invalid descriptor in the image.
+ #[error("Invalid descriptor ")]
+ InvalidDescriptor,
+}
+
+/// Errors from verifying a VBMeta image.
+#[derive(Debug, Error)]
+pub enum VbMetaImageVerificationError {
+ /// There was an error parsing the VBMeta image.
+ #[error("Cannot parse VBMeta image")]
+ ParseError(#[from] VbMetaImageParseError),
+ /// The VBMeta image hash did not validate.
+ #[error("Hash mismatch")]
+ HashMismatch,
+ /// The VBMeta image signature did not validate.
+ #[error("Signature mismatch")]
+ SignatureMismatch,
+ /// An unexpected libavb error code was returned.
+ #[error("Unknown libavb error: {0}")]
+ UnknownLibavbError(c_uint),
+}
+
+/// A VBMeta Image.
+pub struct VbMetaImage {
+ header: AvbVBMetaImageHeader,
+ data: Box<[u8]>,
+}
+
+impl VbMetaImage {
+ /// Load and verify a VBMeta image from the given path.
+ pub fn verify_path<P: AsRef<Path>>(path: P) -> Result<Self, VbMetaImageVerificationError> {
+ let file = File::open(path).map_err(VbMetaImageParseError::Io)?;
+ let size = file.metadata().map_err(VbMetaImageParseError::Io)?.len();
+ Self::verify_reader_region(file, 0, size)
+ }
+
+ /// Load and verify a VBMeta image from a region within a reader.
+ pub fn verify_reader_region<R: Read + Seek>(
+ mut image: R,
+ offset: u64,
+ size: u64,
+ ) -> Result<Self, VbMetaImageVerificationError> {
+ // Check for a footer in the image or assume it's an entire VBMeta image.
+ image.seek(SeekFrom::Start(offset + size)).map_err(VbMetaImageParseError::Io)?;
+ let footer = read_avb_footer(&mut image).map_err(VbMetaImageParseError::Io)?;
+ let (vbmeta_offset, vbmeta_size) = if let Some(footer) = footer {
+ if footer.vbmeta_offset > size || footer.vbmeta_size > size - footer.vbmeta_offset {
+ return Err(VbMetaImageParseError::InvalidFooter.into());
+ }
+ (footer.vbmeta_offset, footer.vbmeta_size)
+ } else {
+ (0, size)
+ };
+ image.seek(SeekFrom::Start(offset + vbmeta_offset)).map_err(VbMetaImageParseError::Io)?;
+ // Verify the image before examining it to check the size.
+ let mut data = vec![0u8; vbmeta_size as usize];
+ image.read_exact(&mut data).map_err(VbMetaImageParseError::Io)?;
+ verify_vbmeta_image(&data)?;
+ // SAFETY: the image has been verified so we know there is a valid header at the start.
+ let header = unsafe {
+ let mut header = MaybeUninit::uninit();
+ let src = data.as_ptr() as *const _ as *const AvbVBMetaImageHeader;
+ avb_vbmeta_image_header_to_host_byte_order(src, header.as_mut_ptr());
+ header.assume_init()
+ };
+ // Calculate the true size of the verified image data.
+ let vbmeta_size = (size_of::<AvbVBMetaImageHeader>() as u64)
+ + header.authentication_data_block_size
+ + header.auxiliary_data_block_size;
+ data.truncate(vbmeta_size as usize);
+ Ok(Self { header, data: data.into_boxed_slice() })
+ }
+
+ /// Get the public key that verified the VBMeta image. If the image was not signed, there
+ /// is no such public key.
+ pub fn public_key(&self) -> Option<&[u8]> {
+ if self.header.algorithm_type == AvbAlgorithmType_AVB_ALGORITHM_TYPE_NONE {
+ return None;
+ }
+ let begin = size_of::<AvbVBMetaImageHeader>()
+ + self.header.authentication_data_block_size as usize
+ + self.header.public_key_offset as usize;
+ let end = begin + self.header.public_key_size as usize;
+ Some(&self.data[begin..end])
+ }
+
+ /// Get the descriptors of the VBMeta image.
+ pub fn descriptors(&self) -> Result<Descriptors<'_>, VbMetaImageParseError> {
+ Descriptors::from_image(&self.data)
+ }
+
+ /// Get the raw VBMeta image.
+ pub fn data(&self) -> &[u8] {
+ &self.data
+ }
+}
+
+/// Verify the data as a VBMeta image, translating errors that arise.
+fn verify_vbmeta_image(data: &[u8]) -> Result<(), VbMetaImageVerificationError> {
+ // SAFETY: the function only reads from the provided data and the NULL pointers disable the
+ // output arguments.
+ let res = unsafe { avb_vbmeta_image_verify(data.as_ptr(), data.len(), null_mut(), null_mut()) };
+ #[allow(non_upper_case_globals)]
+ match res {
+ AvbVBMetaVerifyResult_AVB_VBMETA_VERIFY_RESULT_OK
+ | AvbVBMetaVerifyResult_AVB_VBMETA_VERIFY_RESULT_OK_NOT_SIGNED => Ok(()),
+ AvbVBMetaVerifyResult_AVB_VBMETA_VERIFY_RESULT_INVALID_VBMETA_HEADER => {
+ Err(VbMetaImageParseError::InvalidHeader.into())
+ }
+ AvbVBMetaVerifyResult_AVB_VBMETA_VERIFY_RESULT_UNSUPPORTED_VERSION => {
+ Err(VbMetaImageParseError::UnsupportedVersion.into())
+ }
+ AvbVBMetaVerifyResult_AVB_VBMETA_VERIFY_RESULT_HASH_MISMATCH => {
+ Err(VbMetaImageVerificationError::HashMismatch)
+ }
+ AvbVBMetaVerifyResult_AVB_VBMETA_VERIFY_RESULT_SIGNATURE_MISMATCH => {
+ Err(VbMetaImageVerificationError::SignatureMismatch)
+ }
+ err => Err(VbMetaImageVerificationError::UnknownLibavbError(err)),
+ }
+}
+
+/// Read the AVB footer, if present, given a reader that's positioned at the end of the image.
+fn read_avb_footer<R: Read + Seek>(image: &mut R) -> io::Result<Option<AvbFooter>> {
+ image.seek(SeekFrom::Current(-(size_of::<AvbFooter>() as i64)))?;
+ // SAFETY: the slice is the same size as the struct which only contains simple data types.
+ let mut footer = unsafe {
+ let mut footer = MaybeUninit::<AvbFooter>::uninit();
+ let footer_slice =
+ slice::from_raw_parts_mut(&mut footer as *mut _ as *mut u8, size_of::<AvbFooter>());
+ image.read_exact(footer_slice)?;
+ footer.assume_init()
+ };
+ // Check the magic matches "AVBf" to suppress misleading logs from libavb.
+ const AVB_FOOTER_MAGIC: [u8; 4] = [0x41, 0x56, 0x42, 0x66];
+ if footer.magic != AVB_FOOTER_MAGIC {
+ return Ok(None);
+ }
+ // SAFETY: the function updates the struct in-place.
+ if unsafe { avb_footer_validate_and_byteswap(&footer, &mut footer) } {
+ Ok(Some(footer))
+ } else {
+ Ok(None)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use anyhow::{Context, Result};
+ use std::fs::{self, OpenOptions};
+ use std::os::unix::fs::FileExt;
+ use std::process::Command;
+ use tempfile::TempDir;
+
+ #[test]
+ fn test_unsigned_image() -> Result<()> {
+ let test_dir = TempDir::new().unwrap();
+ let test_file = test_dir.path().join("test.img");
+ let mut cmd = Command::new("./avbtool");
+ cmd.args([
+ "make_vbmeta_image",
+ "--output",
+ test_file.to_str().unwrap(),
+ "--algorithm",
+ "NONE",
+ ]);
+ let status = cmd.status().context("make_vbmeta_image")?;
+ assert!(status.success());
+ let vbmeta = VbMetaImage::verify_path(test_file).context("verify_path")?;
+ assert!(vbmeta.public_key().is_none());
+ Ok(())
+ }
+
+ fn test_signed_image(algorithm: &str, key: &str) -> Result<()> {
+ let test_dir = TempDir::new().unwrap();
+ let test_file = test_dir.path().join("test.img");
+ let mut cmd = Command::new("./avbtool");
+ cmd.args([
+ "make_vbmeta_image",
+ "--output",
+ test_file.to_str().unwrap(),
+ "--algorithm",
+ algorithm,
+ "--key",
+ key,
+ ]);
+ let status = cmd.status().context("make_vbmeta_image")?;
+ assert!(status.success());
+ let vbmeta = VbMetaImage::verify_path(&test_file).context("verify_path")?;
+
+ // The image should contain the public part of the key pair.
+ let pubkey = vbmeta.public_key().unwrap();
+ let test_pubkey_file = test_dir.path().join("test.pubkey");
+ let mut cmd = Command::new("./avbtool");
+ cmd.args([
+ "extract_public_key",
+ "--key",
+ key,
+ "--output",
+ test_pubkey_file.to_str().unwrap(),
+ ]);
+ let status = cmd.status().context("extract_public_key")?;
+ assert!(status.success());
+ assert_eq!(pubkey, fs::read(test_pubkey_file).context("read public key")?);
+
+ // Flip a byte to make verification fail.
+ let file = OpenOptions::new()
+ .read(true)
+ .write(true)
+ .open(&test_file)
+ .context("open image to flip byte")?;
+ let mut data = [0; 1];
+ file.read_exact_at(&mut data, 81).context("read byte from image to flip")?;
+ data[0] = !data[0];
+ file.write_all_at(&data, 81).context("write flipped byte to image")?;
+ assert!(matches!(
+ VbMetaImage::verify_path(test_file),
+ Err(VbMetaImageVerificationError::HashMismatch)
+ ));
+ Ok(())
+ }
+
+ #[test]
+ fn test_rsa2048_signed_image() -> Result<()> {
+ test_signed_image("SHA256_RSA2048", "tests/data/testkey_rsa2048.pem")
+ }
+
+ #[test]
+ fn test_rsa4096_signed_image() -> Result<()> {
+ test_signed_image("SHA256_RSA4096", "tests/data/testkey_rsa4096.pem")
+ }
+
+ #[test]
+ fn test_rsa8192_signed_image() -> Result<()> {
+ test_signed_image("SHA256_RSA8192", "tests/data/testkey_rsa8192.pem")
+ }
+}
diff --git a/libs/vbmeta/tests/data/testkey_rsa2048.pem b/libs/vbmeta/tests/data/testkey_rsa2048.pem
new file mode 100644
index 0000000..867dcff
--- /dev/null
+++ b/libs/vbmeta/tests/data/testkey_rsa2048.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAxlVR3TIkouAOvH79vaJTgFhpfvVKQIeVkFRZPVXK/zY0Gvrh
+4JAqGjJoW/PfrQv5sdD36qtHH3a+G5hLZ6Ni+t/mtfjucxZfuLGC3kmJ1T3XqEKZ
+gXXI2IR7vVSoImREvDQGEDyJwtHzLANlkbGg0cghVhWZSCAndO8BenalC2v94/rt
+DfkPekH6dgU3Sf40T0sBSeSY94mOzTaqOR2pfV1rWlLRdWmo33zeHBv52Rlbt0dM
+uXAureXWiHztkm5GCBC1dgM+CaxNtizNEgC91KcD0xuRCCM2WxH+r1lpszyIJDct
+YbrFmVEYl/kjQpafhy7Nsk1fqSTyRdriZSYmTQIDAQABAoIBAQC+kJgaCuX8wYAn
+SXWQ0fmdZlXnMNRpcF0a0pD0SAzGb1RdYBXMaXiqtyhiwc53PPxsCDdNecjayIMd
+jJVXPTwLhTruOgMS/bp3gcgWwV34UHV4LJXGOGAE+jbS0hbDBMiudOYmj6RmVshp
+z9G1zZCSQNMXHaWsEYkX59XpzzoB384nRul2QgEtwzUNR9XlpzgtJBLk3SACkvsN
+mQ/DW8IWHXLg8vLn1LzVJ2e3B16H4MoE2TCHxqfMgr03IDRRJogkenQuQsFhevYT
+o/mJyHSWavVgzMHG9I5m+eepF4Wyhj1Y4WyKAuMI+9dHAX/h7Lt8XFCQCh5DbkVG
+zGr34sWBAoGBAOs7n7YZqNaaguovfIdRRsxxZr1yJAyDsr6w3yGImDZYju4c4WY9
+5esO2kP3FA4p0c7FhQF5oOb1rBuHEPp36cpL4aGeK87caqTfq63WZAujoTZpr9Lp
+BRbkL7w/xG7jpQ/clpA8sHzHGQs/nelxoOtC7E118FiRgvD/jdhlMyL9AoGBANfX
+vyoN1pplfT2xR8QOjSZ+Q35S/+SAtMuBnHx3l0qH2bbBjcvM1MNDWjnRDyaYhiRu
+i+KA7tqfib09+XpB3g5D6Ov7ls/Ldx0S/VcmVWtia2HK8y8iLGtokoBZKQ5AaFX2
+iQU8+tC4h69GnJYQKqNwgCUzh8+gHX5Y46oDiTmRAoGAYpOx8lX+czB8/Da6MNrW
+mIZNT8atZLEsDs2ANEVRxDSIcTCZJId7+m1W+nRoaycLTWNowZ1+2ErLvR10+AGY
+b7Ys79Wg9idYaY9yGn9lnZsMzAiuLeyIvXcSqgjvAKlVWrhOQFOughvNWvFl85Yy
+oWSCMlPiTLtt7CCsCKsgKuECgYBgdIp6GZsIfkgclKe0hqgvRoeU4TR3gcjJlM9A
+lBTo+pKhaBectplx9RxR8AnsPobbqwcaHnIfAuKDzjk5mEvKZjClnFXF4HAHbyAF
+nRzZEy9XkWFhc80T5rRpZO7C7qdxmu2aiKixM3V3L3/0U58qULEDbubHMw9bEhAT
+PudI8QKBgHEEiMm/hr9T41hbQi/LYanWnlFw1ue+osKuF8bXQuxnnHNuFT/c+9/A
+vWhgqG6bOEHu+p/IPrYm4tBMYlwsyh4nXCyGgDJLbLIfzKwKAWCtH9LwnyDVhOow
+GH9shdR+sW3Ew97xef02KAH4VlNANEmBV4sQNqWWvsYrcFm2rOdL
+-----END RSA PRIVATE KEY-----
diff --git a/libs/vbmeta/tests/data/testkey_rsa4096.pem b/libs/vbmeta/tests/data/testkey_rsa4096.pem
new file mode 100644
index 0000000..26db5c3
--- /dev/null
+++ b/libs/vbmeta/tests/data/testkey_rsa4096.pem
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIJKQIBAAKCAgEA2ASv49OEbH4NiT3CjNMSVeliyfEPXswWcqtEfCxlSpS1FisA
+uwbvEwdTTPlkuSh6G4SYiNhnpCP5p0vcSg/3OhiuVKgV/rCtrDXaO60nvK/o0y83
+NNZRK2xaJ9eWBq9ruIDK+jC0sYWzTaqqwxY0Grjnx/r5CXerl5PrRK7PILzwgBHb
+IwxHcblt1ntgR4cWVpO3wiqasEwBDDDYk4fw7W6LvjBb9qav3YB8RV6PkZNeRP64
+ggfuecq/MXNiWOPNxLzCER2hSr/+J32h9jWjXsrcVy8+8Mldhmr4r2an7c247aFf
+upuFGtUJrpROO8/LXMl5gPfMpkqoatjTMRH59gJjKhot0RpmGxZBvb33TcBK5SdJ
+X39Y4yct5clmDlI4Fjj7FutTP+b96aJeJVnYeUX/A0wmogBajsJRoRX5e/RcgZsY
+RzXYLQXprQ81dBWjjovMJ9p8XeT6BNMFC7o6sklFL0fHDUE/l4BNP8G1u3Bfpzev
+SCISRS71D4eS4oQB+RIPFBUkzomZ7rnEF3BwFeq+xmwfYrP0LRaH+1YeRauuMuRe
+ke1TZl697a3mEjkNg8noa2wtpe7EWmaujJfXDWxJx/XEkjGLCe4z2qk3tkkY+A5g
+Rcgzke8gVxC+eC2DJtbKYfkv4L8FMFJaEhwAp13MfC7FlYujO/BDLl7dANsCAwEA
+AQKCAgAWoL8P/WsktjuSwb5sY/vKtgzcHH1Ar942GsysuTXPDy686LpF3R8T/jNy
+n7k2UBAia8xSoWCR6BbRuHeV5oA+PLGeOpE7QaSfonB+yc+cy0x3Or3ssfqEsu/q
+toGHp75/8DXS6WE0K04x94u1rdC9b9sPrrGBlWCLGzqM0kbuJfyHXdd3n2SofAUO
+b5QRSgxD+2tHUpEroHqHnWJCaf4J0QegX45yktlfOYNK/PHLDQXV8ly/ejc32M4Y
+Tv7hUtOOJTuq8VCg9OWZm2Zo1QuM9XEJTPCp5l3+o5vzO6yhk2gotDvD32CdA+3k
+tLJRP54M1Sn+IXb1gGKN9rKAtGJbenWIPlNObhQgkbwG89Qd+5rfMXsiPv1Hl1tK
++tqwjD82/H3/ElaaMnwHCpeoGSp95OblAoBjzjMP2KsbvKSdL8O/rf1c3uOw9+DF
+cth0SA8y3ZzI11gJtb2QMGUrCny5n4sPGGbc3x38NdLhwbkPKZy60OiT4g2kNpdY
+dIitmAML2otttiF4AJM6AraPk8YVzkPLTksoL3azPBya5lIoDI2H3QvTtSvpXkXP
+yKchsDSWYbdqfplqC/X0Djp2/Zd8jpN5I6+1aSmpTmbwx/JTllY1N89FRZLIdxoh
+2k81LPiXhE6uRbjioJUlbnEWIpY2y2N2Clmxpjh0/IcXd1XImQKCAQEA7Zai+yjj
+8xit24aO9Tf3mZBXBjSaDodjC2KS1yCcAIXp6S7aH0wZipyZpQjys3zaBQyMRYFG
+bQqIfVAa6inWyDoofbAJHMu5BVcHFBPZvSS5YhDjc8XZ5dqSCxzIz9opIqAbm+b4
+aEV/3A3Jki5Dy8y/5j21GAK4Y4mqQOYzne7bDGi3Hyu041MGM4qfIcIkS5N1eHW4
+sDZJh6+K5tuxN5TX3nDZSpm9luNH8mLGgKAZ15b1LqXAtM5ycoBY9Hv082suPPom
+O+r0ybdRX6nDSH8+11y2KiP2kdVIUHCGkwlqgrux5YZyjCZPwOvEPhzSoOS+vBiF
+UVXA8idnxNLk1QKCAQEA6MIihDSXx+350fWqhQ/3Qc6gA/t2C15JwJ9+uFWA+gjd
+c/hn5HcmnmBJN4R04nLG/aU9SQur87a4mnC/Mp9JIARjHlZ/WNT4U0sJyPEVRg5U
+Z9VajAucWwi0JyJYCO1EMMy68Jp8qlTriK/L7nbD86JJ5ASxjojiN/0psK/Pk60F
+Rr+shKPi3jRQ1BDjDtAxOfo4ctf/nFbUM4bY0FNPQMP7WesoSKU0NBCRR6d0d2tq
+YflMjIQHx+N74P5jEdSCHTVGQm+dj47pUt3lLPLWc0bX1G/GekwXP4NUsR/70Hsi
+bwxkNnK2TSGzkt2rcOnutP125rJu6WpV7SNrq9rm7wKCAQAfMROcnbWviKHqnDPQ
+hdR/2K9UJTvEhInASOS2UZWpi+s1rez9BuSjigOx4wbaAZ4t44PW7C3uyt84dHfU
+HkIQb3I5bg8ENMrJpK9NN33ykwuzkDwMSwFcZ+Gci97hSubzoMl/IkeiiN1MapL4
+GhLUgsD+3UMVL+Y9SymK8637IgyoCGdiND6/SXsa8SwLJo3VTjqx4eKpX7cvlSBL
+RrRxc50TmwUsAhsd4CDl9YnSATLjVvJBeYlfM2tbFPaYwl1aR8v+PWkfnK0efm60
+fHki33HEnGteBPKuGq4vwVYpn6bYGwQz+f6335/A2DMfZHFSpjVURHPcRcHbCMla
+0cUxAoIBAQC25eYNkO478mo+bBbEXJlkoqLmvjAyGrNFo48F9lpVH6Y0vNuWkXJN
+PUgLUhAu6RYotjGENqG17rz8zt/PPY9Ok2P3sOx8t00y1mIn/hlDZXs55FM0fOMu
+PZaiscAPs7HDzvyOmDah+fzi+ZD8H2M3DS2W+YE0iaeJa2vZJS2t02W0BGXiDI33
+IZDqMyLYvwwPjOnShJydEzXID4xLl0tNjzLxo3GSNA7jYqlmbtV8CXIc7rMSL6WV
+ktIDKKJcnmpn3TcKeX6MEjaSIT82pNOS3fY3PmXuL+CMzfw8+u77Eecq78fHaTiL
+P5JGM93F6mzi19EY0tmInUBMCWtQLcENAoIBAQCg0KaOkb8T36qzPrtgbfou0E2D
+ufdpL1ugmD4edOFKQB5fDFQhLnSEVSJq3KUg4kWsXapQdsBd6kLdxS+K6MQrLBzr
+4tf0c7UCF1AzWk6wXMExZ8mRb2RkGZYQB2DdyhFB3TPmnq9CW8JCq+6kxg/wkU4s
+vM4JXzgcqVoSf42QJl+B9waeWhg0BTWx01lal4ds88HvEKmE0ik5GwiDbr7EvDDw
+E6UbZtQcIoSTIIZDgYqVFfR2DAho3wXJRsOXh433lEJ8X7cCDzrngFbQnlKrpwML
+Xgm0SIUc+Nf5poMM3rfLFK77t/ob4w+5PwRKcoSniyAxrHd6bwykYA8Vuydv
+-----END RSA PRIVATE KEY-----
diff --git a/libs/vbmeta/tests/data/testkey_rsa8192.pem b/libs/vbmeta/tests/data/testkey_rsa8192.pem
new file mode 100644
index 0000000..a383428
--- /dev/null
+++ b/libs/vbmeta/tests/data/testkey_rsa8192.pem
@@ -0,0 +1,99 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----