use std::collections::BTreeSet;
use std::fs::File;
use std::io::{Cursor, Read, Seek};
use std::ops::RangeInclusive;
use std::path::Path;
use super::scheme::{MAX_SDK, SchemeSigner, V3_MIN_SDK};
use super::{BlockId, Certificate, Scheme, Schemes, SignatureAlgorithm, SigningBlock, VerifyError, jar, scheme};
use crate::archive::{self, ReadAt, ZipError};
use crate::digest::{ContentDigest, content_digests};
use crate::error::{Error, FileOperation};
#[derive(Clone, Debug)]
pub struct Verification {
pub schemes: Schemes,
signers: Vec<VerifiedSigner>,
}
impl Verification {
pub fn certificate(&self) -> &Certificate {
self.signers[0].certificate()
}
pub fn signers(&self) -> &[VerifiedSigner] {
&self.signers
}
}
#[derive(Clone, Debug)]
pub struct VerifiedSigner {
certificates: Vec<Certificate>,
pub algorithm: Option<SignatureAlgorithm>,
pub sdk_range: Option<RangeInclusive<u32>>,
}
impl VerifiedSigner {
pub fn certificate(&self) -> &Certificate {
&self.certificates[0]
}
pub fn certificates(&self) -> &[Certificate] {
&self.certificates
}
}
impl From<SchemeSigner> for VerifiedSigner {
fn from(signer: SchemeSigner) -> Self {
Self {
certificates: signer.certificates,
algorithm: Some(signer.algorithm),
sdk_range: signer.sdk.map(|sdk| sdk.min..=sdk.max),
}
}
}
pub fn verify(path: impl AsRef<Path>) -> Result<Verification, Error> {
let path = path.as_ref();
let file = File::open(path).map_err(Error::file(FileOperation::Open, path))?;
verify_source(&file, &file)
}
pub fn verify_bytes(bytes: &[u8]) -> Result<Verification, Error> {
verify_source(bytes, Cursor::new(bytes))
}
fn verify_source(source: &(impl ReadAt + ?Sized), reader: impl Read + Seek) -> Result<Verification, Error> {
let (mut archive, layout) = archive::open(reader)?;
let manifest = crate::apk::read_manifest(&mut archive)?;
let first_v3_sdk = manifest
.as_ref()
.map(|manifest| manifest.min_sdk().api_level())
.transpose()?
.map_or(V3_MIN_SDK, |sdk| sdk.get().max(V3_MIN_SDK));
let mut digests: Vec<(ContentDigest, Vec<u8>)> = Vec::new();
let mut content_digest = |kind: ContentDigest| -> Result<Option<Vec<u8>>, VerifyError> {
if let Some((_, digest)) = digests.iter().find(|(k, _)| *k == kind) {
return Ok(Some(digest.clone()));
}
let (_, digest) = content_digests(source, &layout, &[kind])?.remove(0);
digests.push((kind, digest.clone()));
Ok(Some(digest))
};
let block = match &layout.signing_block {
Some(range) => {
let bytes = source.read_vec_at(range.start, (range.end - range.start) as usize).map_err(ZipError::from)?;
SigningBlock::parse(&bytes)?
}
None => SigningBlock::default(),
};
if block.get(BlockId::V3_1).is_some() {
return Err(VerifyError::KeyRotationUnsupported.into());
}
let mut verified: Vec<(Scheme, Vec<VerifiedSigner>)> = Vec::new();
let mut declared: Vec<(Scheme, u32)> = Vec::new();
for scheme in [Scheme::V3, Scheme::V2] {
let Some(value) = block.get(scheme.block_id().expect("block scheme")) else { continue };
let signers = scheme::verify(scheme, value, &mut content_digest)?;
if scheme == Scheme::V3 {
check_v3_coverage(&signers, first_v3_sdk)?;
}
declared.extend(signers.iter().flat_map(|s| s.protected_schemes.iter().map(move |&p| (scheme, p))));
verified.push((scheme, signers.into_iter().map(VerifiedSigner::from).collect()));
}
if let Some(signature) = jar::verify(&mut archive)? {
declared.extend(signature.declared_schemes.iter().map(|&p| (Scheme::V1, p)));
let signers = signature
.signers
.into_iter()
.map(|certificates| VerifiedSigner { certificates, algorithm: None, sdk_range: None })
.collect();
verified.push((Scheme::V1, signers));
}
let Some((strongest, _)) = verified.first() else { return Err(VerifyError::Unsigned.into()) };
let schemes = verified.iter().fold(Schemes::empty(), |set, (scheme, _)| set | scheme.flag());
for (protected_by, number) in declared {
match Scheme::from_number(number) {
Some(scheme) if !schemes.contains(scheme.flag()) => {
return Err(VerifyError::StrippedScheme { scheme, protected_by }.into());
}
_ => {}
}
}
let leaves =
|signers: &[VerifiedSigner]| signers.iter().map(|s| s.certificates[0].der().to_vec()).collect::<BTreeSet<_>>();
let expected = leaves(&verified[0].1);
if let Some((other, _)) = verified.iter().skip(1).find(|(_, signers)| leaves(signers) != expected) {
return Err(VerifyError::SignerMismatch { first: *strongest, second: *other }.into());
}
if let Some(manifest) = &manifest {
let (min_sdk, target_sdk) = (manifest.min_sdk().api_level()?, manifest.effective_target_sdk()?);
schemes.check_coverage(min_sdk, target_sdk).map_err(VerifyError::Coverage)?;
}
let (_, signers) = verified.swap_remove(0);
Ok(Verification { schemes, signers })
}
fn check_v3_coverage(signers: &[SchemeSigner], first_sdk: u32) -> Result<(), VerifyError> {
let gap = |sdk| VerifyError::SdkCoverage { scheme: Scheme::V3, sdk };
let mut ranges: Vec<_> = signers.iter().filter_map(|s| s.sdk).filter(|r| r.max >= first_sdk).collect();
ranges.sort_by_key(|r| r.min);
let mut next = first_sdk;
for range in ranges {
let start = range.min.max(first_sdk);
if start != next {
return Err(gap(start.min(next)));
}
next = range.max + 1;
}
if next != MAX_SDK + 1 {
return Err(gap(next));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sign::scheme::SdkRange;
fn signers(ranges: &[(u32, u32)]) -> Vec<SchemeSigner> {
ranges
.iter()
.map(|&(min, max)| SchemeSigner {
certificates: Vec::new(),
algorithm: SignatureAlgorithm::EcdsaSha256,
sdk: Some(SdkRange { min, max }),
protected_schemes: Vec::new(),
})
.collect()
}
#[test]
fn v3_coverage_starts_at_the_apps_minimum_sdk() {
let ranges = signers(&[(35, MAX_SDK)]);
check_v3_coverage(&ranges, 35).unwrap();
assert!(matches!(check_v3_coverage(&ranges, 28), Err(VerifyError::SdkCoverage { sdk: 28, .. })));
check_v3_coverage(&signers(&[(28, MAX_SDK)]), 35).unwrap();
}
#[test]
fn v3_coverage_rejects_gaps_and_overlaps() {
for (ranges, sdk) in
[(vec![(28, 39), (41, MAX_SDK)], 40), (vec![(28, 40), (40, MAX_SDK)], 40), (vec![(28, 40)], 41)]
{
assert!(matches!(
check_v3_coverage(&signers(&ranges), 28),
Err(VerifyError::SdkCoverage { sdk: found, .. }) if found == sdk
));
}
}
}