Merge "Extract library for VBMeta image handling"
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-----
+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
+-----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-----