rust-apk 1.0.0

Build, align, sign, verify, and inspect Android APKs in pure Rust
Documentation
//! Verifying all of an APK's signatures.

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};

/// The outcome of a successful verification.
#[derive(Clone, Debug)]
pub struct Verification {
    /// The schemes whose signatures are present and valid.
    pub schemes: Schemes,
    signers: Vec<VerifiedSigner>,
}

impl Verification {
    /// The first signer's leaf certificate.
    pub fn certificate(&self) -> &Certificate {
        self.signers[0].certificate()
    }

    /// Signers from the strongest scheme present. Always nonempty.
    pub fn signers(&self) -> &[VerifiedSigner] {
        &self.signers
    }
}

/// A signer whose signature verified.
#[derive(Clone, Debug)]
pub struct VerifiedSigner {
    certificates: Vec<Certificate>,
    /// The algorithm of a v2 or v3 signature.
    pub algorithm: Option<SignatureAlgorithm>,
    /// The API levels a v3 signer applies to.
    pub sdk_range: Option<RangeInclusive<u32>>,
}

impl VerifiedSigner {
    /// The leaf certificate that verified this signer's signature.
    pub fn certificate(&self) -> &Certificate {
        &self.certificates[0]
    }

    /// The embedded certificate chain, leaf first. Always nonempty.
    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),
        }
    }
}

/// Verifies APK signatures, content digests, and SDK scheme coverage.
///
/// Every scheme present must verify and name the same signers, any scheme
/// another declares must be present (stripping protection), and the
/// schemes must cover every release from the manifest's `minSdkVersion`.
/// This establishes integrity and who signed the APK, not whether that
/// signer is trusted. APKs that rotate their signing key are rejected with
/// [`VerifyError::KeyRotationUnsupported`].
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)
}

/// Verifies an APK held in memory, like [`verify`].
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 })
}

/// Covers the app's supported v3 releases exactly once.
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
            ));
        }
    }
}