apkverify: verify content digest

Bug: 190343842
Test: MicrodroidHostTestCases
Change-Id: I370aaf6bf88e7267f5a03b7a2da32c87d49552db
diff --git a/apkverify/src/sigutil.rs b/apkverify/src/sigutil.rs
index 9de794a..06645fe 100644
--- a/apkverify/src/sigutil.rs
+++ b/apkverify/src/sigutil.rs
@@ -18,10 +18,12 @@
 
 use anyhow::{anyhow, bail, Result};
 use byteorder::{LittleEndian, ReadBytesExt};
-use bytes::{Buf, Bytes};
-use std::io::{Read, Seek, SeekFrom};
+use bytes::{Buf, BufMut, Bytes};
+use ring::digest;
+use std::cmp::min;
+use std::io::{Cursor, Read, Seek, SeekFrom, Take};
 
-use crate::ziputil::zip_sections;
+use crate::ziputil::{set_central_directory_offset, zip_sections};
 
 const APK_SIG_BLOCK_MIN_SIZE: u32 = 32;
 const APK_SIG_BLOCK_MAGIC: u128 = 0x3234206b636f6c4220676953204b5041;
@@ -45,27 +47,146 @@
 #[allow(unused)]
 const CONTENT_DIGEST_SHA256: u32 = 4;
 
-pub struct SignatureInfo {
-    pub signature_block: Bytes,
+const CHUNK_SIZE_BYTES: u64 = 1024 * 1024;
+
+pub struct ApkSections<R> {
+    inner: R,
+    signing_block_offset: u32,
+    signing_block_size: u32,
+    central_directory_offset: u32,
+    central_directory_size: u32,
+    eocd_offset: u32,
+    eocd_size: u32,
 }
 
-/// Returns the APK Signature Scheme block contained in the provided file for the given ID
-/// and the additional information relevant for verifying the block against the file.
-pub fn find_signature<F: Read + Seek>(f: F, block_id: u32) -> Result<SignatureInfo> {
-    let (mut f, sections) = zip_sections(f)?;
+impl<R: Read + Seek> ApkSections<R> {
+    pub fn new(reader: R) -> Result<ApkSections<R>> {
+        let (mut f, zip_sections) = zip_sections(reader)?;
+        let (signing_block_offset, signing_block_size) =
+            find_signing_block(&mut f, zip_sections.central_directory_offset)?;
+        Ok(ApkSections {
+            inner: f,
+            signing_block_offset,
+            signing_block_size,
+            central_directory_offset: zip_sections.central_directory_offset,
+            central_directory_size: zip_sections.central_directory_size,
+            eocd_offset: zip_sections.eocd_offset,
+            eocd_size: zip_sections.eocd_size,
+        })
+    }
 
-    let (signing_block, _signing_block_offset) =
-        find_signing_block(&mut f, sections.central_directory_offset)?;
+    /// Returns the APK Signature Scheme block contained in the provided file for the given ID
+    /// and the additional information relevant for verifying the block against the file.
+    pub fn find_signature(&mut self, block_id: u32) -> Result<Bytes> {
+        let signing_block = self.bytes(self.signing_block_offset, self.signing_block_size)?;
+        // TODO(jooyung): propagate NotFound error so that verification can fallback to V2
+        find_signature_scheme_block(Bytes::from(signing_block), block_id)
+    }
 
-    // TODO(jooyung): propagate NotFound error so that verification can fallback to V2
-    let signature_scheme_block = find_signature_scheme_block(signing_block, block_id)?;
-    Ok(SignatureInfo { signature_block: signature_scheme_block })
+    /// Computes digest with "signature algorithm" over APK contents, central directory, and EOCD.
+    /// 1. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk’s
+    ///    length in bytes (little-endian uint32), and the chunk’s contents.
+    /// 2. The top-level digest is computed over the concatenation of byte 0x5a, the number of
+    ///    chunks (little-endian uint32), and the concatenation of digests of the chunks in the
+    ///    order the chunks appear in the APK.
+    /// (see https://source.android.com/security/apksigning/v2#integrity-protected-contents)
+    pub fn compute_digest(&mut self, signature_algorithm_id: u32) -> Result<Vec<u8>> {
+        let digester = Digester::new(signature_algorithm_id)?;
+
+        let mut digests_of_chunks = bytes::BytesMut::new();
+        let mut chunk_count = 0u32;
+        let mut chunk = vec![0u8; CHUNK_SIZE_BYTES as usize];
+        for data in &[
+            ApkSections::zip_entries,
+            ApkSections::central_directory,
+            ApkSections::eocd_for_verification,
+        ] {
+            let mut data = data(self)?;
+            while data.limit() > 0 {
+                let chunk_size = min(CHUNK_SIZE_BYTES, data.limit());
+                let mut slice = &mut chunk[..(chunk_size as usize)];
+                data.read_exact(&mut slice)?;
+                digests_of_chunks.put_slice(
+                    digester.digest(slice, CHUNK_HEADER_MID, chunk_size as u32).as_ref(),
+                );
+                chunk_count += 1;
+            }
+        }
+        Ok(digester.digest(&digests_of_chunks, CHUNK_HEADER_TOP, chunk_count).as_ref().into())
+    }
+
+    fn zip_entries(&mut self) -> Result<Take<Box<dyn Read + '_>>> {
+        scoped_read(&mut self.inner, 0, self.signing_block_offset as u64)
+    }
+    fn central_directory(&mut self) -> Result<Take<Box<dyn Read + '_>>> {
+        scoped_read(
+            &mut self.inner,
+            self.central_directory_offset as u64,
+            self.central_directory_size as u64,
+        )
+    }
+    fn eocd_for_verification(&mut self) -> Result<Take<Box<dyn Read + '_>>> {
+        let mut eocd = self.bytes(self.eocd_offset, self.eocd_size)?;
+        // Protection of section 4 (ZIP End of Central Directory) is complicated by the section
+        // containing the offset of ZIP Central Directory. The offset changes when the size of the
+        // APK Signing Block changes, for instance, when a new signature is added. Thus, when
+        // computing digest over the ZIP End of Central Directory, the field containing the offset
+        // of ZIP Central Directory must be treated as containing the offset of the APK Signing
+        // Block.
+        set_central_directory_offset(&mut eocd, self.signing_block_offset)?;
+        Ok(Read::take(Box::new(Cursor::new(eocd)), self.eocd_size as u64))
+    }
+    fn bytes(&mut self, offset: u32, size: u32) -> Result<Vec<u8>> {
+        self.inner.seek(SeekFrom::Start(offset as u64))?;
+        let mut buf = vec![0u8; size as usize];
+        self.inner.read_exact(&mut buf)?;
+        Ok(buf)
+    }
+}
+
+fn scoped_read<'a, R: Read + Seek>(
+    src: &'a mut R,
+    offset: u64,
+    size: u64,
+) -> Result<Take<Box<dyn Read + 'a>>> {
+    src.seek(SeekFrom::Start(offset))?;
+    Ok(Read::take(Box::new(src), size))
+}
+
+struct Digester {
+    algorithm: &'static digest::Algorithm,
+}
+
+const CHUNK_HEADER_TOP: &[u8] = &[0x5a];
+const CHUNK_HEADER_MID: &[u8] = &[0xa5];
+impl Digester {
+    fn new(signature_algorithm_id: u32) -> Result<Digester> {
+        let digest_algorithm_id = to_content_digest_algorithm(signature_algorithm_id)?;
+        let algorithm = match digest_algorithm_id {
+            CONTENT_DIGEST_CHUNKED_SHA256 => &digest::SHA256,
+            CONTENT_DIGEST_CHUNKED_SHA512 => &digest::SHA512,
+            // TODO(jooyung): implement
+            CONTENT_DIGEST_VERITY_CHUNKED_SHA256 => {
+                bail!("TODO(b/190343842): CONTENT_DIGEST_VERITY_CHUNKED_SHA256: not implemented")
+            }
+            _ => bail!("Unknown digest algorithm: {}", digest_algorithm_id),
+        };
+        Ok(Digester { algorithm })
+    }
+    // v2/v3 digests are computed after prepending "header" byte and "size" info.
+    fn digest(&self, data: &[u8], header: &[u8], size: u32) -> digest::Digest {
+        let mut ctx = digest::Context::new(self.algorithm);
+        ctx.update(header);
+        ctx.update(&size.to_le_bytes());
+        ctx.update(data);
+        ctx.finish()
+    }
 }
 
 fn find_signing_block<T: Read + Seek>(
     reader: &mut T,
     central_directory_offset: u32,
-) -> Result<(Bytes, u32)> {
+) -> Result<(u32, u32)> {
     // FORMAT:
     // OFFSET       DATA TYPE  DESCRIPTION
     // * @+0  bytes uint64:    size in bytes (excluding this field)
@@ -96,10 +217,7 @@
             size_in_footer
         );
     }
-    reader.seek(SeekFrom::Start(signing_block_offset as u64))?;
-    let mut buf = vec![0u8; total_size as usize];
-    reader.read_exact(&mut buf)?;
-    Ok((Bytes::from(buf), signing_block_offset))
+    Ok((signing_block_offset, total_size))
 }
 
 fn find_signature_scheme_block(buf: Bytes, block_id: u32) -> Result<Bytes> {
diff --git a/apkverify/src/v3.rs b/apkverify/src/v3.rs
index 91043ab..0ef035c 100644
--- a/apkverify/src/v3.rs
+++ b/apkverify/src/v3.rs
@@ -22,6 +22,7 @@
 use anyhow::{anyhow, bail, Result};
 use bytes::Bytes;
 use std::fs::File;
+use std::io::{Read, Seek};
 use std::ops::Range;
 use std::path::Path;
 
@@ -85,16 +86,18 @@
 /// associated with each signer.
 pub fn verify<P: AsRef<Path>>(path: P) -> Result<()> {
     let f = File::open(path.as_ref())?;
-    let signature = find_signature(f, APK_SIGNATURE_SCHEME_V3_BLOCK_ID)?;
-    verify_signature(&signature.signature_block)?;
+    let mut sections = ApkSections::new(f)?;
+    verify_signature(&mut sections)?;
     Ok(())
 }
 
 /// Verifies the contents of the provided APK file against the provided APK Signature Scheme v3
 /// Block.
-fn verify_signature(block: &Bytes) -> Result<()> {
+fn verify_signature<R: Read + Seek>(sections: &mut ApkSections<R>) -> Result<()> {
+    let mut block = sections.find_signature(APK_SIGNATURE_SCHEME_V3_BLOCK_ID)?;
+
     // parse v3 scheme block
-    let signers = block.slice(..).read::<Signers>()?;
+    let signers = block.read::<Signers>()?;
 
     // find supported by platform
     let mut supported =
@@ -106,13 +109,13 @@
     }
 
     // and it should be verified
-    supported.pop().unwrap().verify()?;
+    supported.pop().unwrap().verify(sections)?;
 
     Ok(())
 }
 
 impl Signer {
-    fn verify(&self) -> Result<()> {
+    fn verify<R: Read + Seek>(&self, sections: &mut ApkSections<R>) -> Result<()> {
         // 1. Choose the strongest supported signature algorithm ID from signatures. The strength
         //    ordering is up to each implementation/platform version.
         let strongest: &Signature = self
@@ -134,9 +137,36 @@
         if self.sdk_range() != signed_data.sdk_range() {
             bail!("SDK versions mismatch between signed and unsigned in v3 signer block.");
         }
-        // TODO(jooyung) 4. Verify that the ordered list of signature algorithm IDs in digests and signatures is identical. (This is to prevent signature stripping/addition.)
-        // TODO(jooyung) 5. Compute the digest of APK contents using the same digest algorithm as the digest algorithm used by the signature algorithm.
-        // TODO(jooyung) 6. Verify that the computed digest is identical to the corresponding digest from digests.
+
+        // 4. Verify that the ordered list of signature algorithm IDs in digests and signatures is
+        //    identical. (This is to prevent signature stripping/addition.)
+        if !self
+            .signatures
+            .iter()
+            .map(|sig| sig.signature_algorithm_id)
+            .eq(signed_data.digests.iter().map(|dig| dig.signature_algorithm_id))
+        {
+            bail!("Signature algorithms don't match between digests and signatures records");
+        }
+
+        // 5. Compute the digest of APK contents using the same digest algorithm as the digest
+        //    algorithm used by the signature algorithm.
+        let digest = signed_data
+            .digests
+            .iter()
+            .find(|&dig| dig.signature_algorithm_id == strongest.signature_algorithm_id)
+            .unwrap(); // ok to unwrap since we check if two lists are the same above
+        let computed = sections.compute_digest(digest.signature_algorithm_id)?;
+
+        // 6. Verify that the computed digest is identical to the corresponding digest from digests.
+        if computed != digest.digest.as_ref() {
+            bail!(
+                "digest mismatch: computed={:?} vs expected={:?}",
+                to_hex_string(&computed),
+                to_hex_string(&digest.digest),
+            );
+        }
+
         // TODO(jooyung) 7. Verify that SubjectPublicKeyInfo of the first certificate of certificates is identical to public key.
         // TODO(jooyung) 8. If the proof-of-rotation attribute exists for the signer verify that the struct is valid and this signer is the last certificate in the list.
         Ok(())
@@ -215,3 +245,8 @@
         Ok(Self { signature_algorithm_id: buf.read()?, digest: buf.read()? })
     }
 }
+
+#[inline]
+fn to_hex_string(buf: &[u8]) -> String {
+    buf.iter().map(|b| format!("{:02X}", b)).collect()
+}
diff --git a/apkverify/src/ziputil.rs b/apkverify/src/ziputil.rs
index 28ecf87..dbf5131 100644
--- a/apkverify/src/ziputil.rs
+++ b/apkverify/src/ziputil.rs
@@ -17,7 +17,7 @@
 //! Utilities for zip handling
 
 use anyhow::{bail, Result};
-use bytes::Buf;
+use bytes::{Buf, BufMut};
 use std::io::{Read, Seek, SeekFrom};
 use zip::ZipArchive;
 
@@ -69,3 +69,12 @@
     }
     Ok((&buf[EOCD_CENTRAL_DIRECTORY_OFFSET_FIELD_OFFSET..]).get_u32_le())
 }
+
+/// Update EOCD's central_directory_offset field.
+pub fn set_central_directory_offset(buf: &mut [u8], value: u32) -> Result<()> {
+    if buf.len() < EOCD_MIN_SIZE {
+        bail!("Invalid EOCD size: {}", buf.len());
+    }
+    (&mut buf[EOCD_CENTRAL_DIRECTORY_OFFSET_FIELD_OFFSET..]).put_u32_le(value);
+    Ok(())
+}
diff --git a/apkverify/tests/apkverify_test.rs b/apkverify/tests/apkverify_test.rs
index 03db61a..b40c435 100644
--- a/apkverify/tests/apkverify_test.rs
+++ b/apkverify/tests/apkverify_test.rs
@@ -16,7 +16,29 @@
 
 use apkverify::verify;
 
+macro_rules! assert_contains {
+    ($haystack:expr,$needle:expr $(,)?) => {
+        match (&$haystack, &$needle) {
+            (haystack_value, needle_value) => {
+                assert!(
+                    haystack_value.contains(needle_value),
+                    "{} is not found in {}",
+                    needle_value,
+                    haystack_value
+                );
+            }
+        }
+    };
+}
+
 #[test]
 fn test_verify_v3() {
     assert!(verify("tests/data/test.apex").is_ok());
 }
+
+#[test]
+fn test_verify_v3_digest_mismatch() {
+    let res = verify("tests/data/v3-only-with-rsa-pkcs1-sha512-8192-digest-mismatch.apk");
+    assert!(res.is_err());
+    assert_contains!(res.err().unwrap().to_string(), "digest mismatch");
+}
diff --git a/apkverify/tests/data/README.md b/apkverify/tests/data/README.md
index df40af6..581a522 100644
--- a/apkverify/tests/data/README.md
+++ b/apkverify/tests/data/README.md
@@ -9,4 +9,7 @@
 Verified using v4 scheme (APK Signature Scheme v4): false
 Verified for SourceStamp: false
 Number of signers: 1
-```
\ No newline at end of file
+```
+
+Some test APKs are copied from tools/apksig/src/test/resources/com/android/apksig/.
+- v3-only-with-rsa-pkcs1-sha512-8192-digest-mismatch.apk
diff --git a/apkverify/tests/data/v3-only-with-rsa-pkcs1-sha512-8192-digest-mismatch.apk b/apkverify/tests/data/v3-only-with-rsa-pkcs1-sha512-8192-digest-mismatch.apk
new file mode 100644
index 0000000..2800929
--- /dev/null
+++ b/apkverify/tests/data/v3-only-with-rsa-pkcs1-sha512-8192-digest-mismatch.apk
Binary files differ