rust-apk 1.0.0

Build, align, sign, verify, and inspect Android APKs in pure Rust
Documentation
//! Signing identities: a private key and its certificate chain.

use std::fmt;
use std::path::Path;

use zeroize::Zeroizing;

use super::{Certificate, KeyError, PrivateKey};
use crate::error::{Error, FileOperation};

/// A private key together with the certificate chain that identifies it.
///
/// Constructors check that the key matches the leaf certificate, so a
/// `SigningIdentity` can always produce verifiable signatures.
///
/// ```no_run
/// use apk::sign::SigningIdentity;
///
/// let identity = SigningIdentity::from_pem_file("release.pem")?;
/// println!("signing as {}", identity.certificate().subject());
/// # Ok::<(), apk::Error>(())
/// ```
#[derive(Clone)]
pub struct SigningIdentity {
    key: PrivateKey,
    chain: Vec<Certificate>,
}

impl SigningIdentity {
    /// Pairs `key` with `chain` (leaf first).
    pub fn new(key: PrivateKey, chain: Vec<Certificate>) -> Result<Self, KeyError> {
        let leaf = chain.first().ok_or(KeyError::MissingCertificate)?;
        if leaf.public_key()? != key.public_key() {
            return Err(KeyError::CertificateMismatch);
        }
        Ok(Self { key, chain })
    }

    /// Parses a PEM bundle holding a private key (`PRIVATE KEY`,
    /// `RSA PRIVATE KEY`, or `EC PRIVATE KEY`) and one or more `CERTIFICATE`s.
    pub fn from_pem(pem: &str) -> Result<Self, KeyError> {
        Self::from_pem_blocks(pem, None)
    }

    /// Like [`from_pem`](Self::from_pem), also accepting an
    /// `ENCRYPTED PRIVATE KEY` (PKCS#8, PBES2) unlocked with `password`.
    #[cfg(feature = "keystore")]
    pub fn from_encrypted_pem(pem: &str, password: &str) -> Result<Self, KeyError> {
        Self::from_pem_blocks(pem, Some(password))
    }

    /// Reads a PEM bundle from a file.
    pub fn from_pem_file(path: impl AsRef<Path>) -> Result<Self, Error> {
        let path = path.as_ref();
        let pem = Zeroizing::new(std::fs::read_to_string(path).map_err(Error::file(FileOperation::Read, path))?);
        Ok(Self::from_pem(&pem)?)
    }

    /// Parses a PKCS#8 private key and a certificate, both DER-encoded.
    pub fn from_der(private_key_pkcs8: &[u8], certificate: &[u8]) -> Result<Self, KeyError> {
        Self::new(PrivateKey::from_pkcs8_der(private_key_pkcs8)?, vec![Certificate::from_der(certificate)?])
    }

    fn from_pem_blocks(pem: &str, password: Option<&str>) -> Result<Self, KeyError> {
        let mut key = None;
        let mut chain = Vec::new();
        for block in pem_blocks(pem) {
            let (label, der) = pem_rfc7468::decode_vec(block.as_bytes())?;
            let der = Zeroizing::new(der);
            match label {
                "CERTIFICATE" => chain.push(Certificate::from_der(&der)?),
                "PRIVATE KEY" if key.is_none() => key = Some(PrivateKey::from_pkcs8_der(&der)?),
                "RSA PRIVATE KEY" if key.is_none() => key = Some(PrivateKey::from_pkcs1_der(&der)?),
                "EC PRIVATE KEY" if key.is_none() => key = Some(PrivateKey::from_sec1_der(&der)?),
                "ENCRYPTED PRIVATE KEY" if key.is_none() => key = Some(decrypt(&der, password)?),
                _ => {}
            }
        }
        Self::new(key.ok_or(KeyError::MissingPrivateKey)?, chain)
    }

    pub fn key(&self) -> &PrivateKey {
        &self.key
    }

    /// The leaf certificate.
    pub fn certificate(&self) -> &Certificate {
        &self.chain[0]
    }

    /// The certificate chain, leaf first.
    pub fn chain(&self) -> &[Certificate] {
        &self.chain
    }
}

impl fmt::Debug for SigningIdentity {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("SigningIdentity").field("key", &self.key).field("certificate", self.certificate()).finish()
    }
}

/// Splits a PEM bundle into its `-----BEGIN ...-----` blocks.
fn pem_blocks(pem: &str) -> impl Iterator<Item = &str> {
    let mut rest = pem;
    std::iter::from_fn(move || {
        let start = rest.find("-----BEGIN ")?;
        let label_end = start + rest[start..].find("-----\n").or_else(|| rest[start..].find("-----\r\n"))?;
        let label = &rest[start + "-----BEGIN ".len()..label_end];
        let end_marker = format!("-----END {label}-----");
        let end = label_end + rest[label_end..].find(&end_marker)? + end_marker.len();
        let block = &rest[start..end];
        rest = &rest[end..];
        Some(block)
    })
}

#[cfg(feature = "keystore")]
fn decrypt(der: &[u8], password: Option<&str>) -> Result<PrivateKey, KeyError> {
    use der::Decode;
    let password = password.ok_or(KeyError::PasswordRequired)?;
    let encrypted = pkcs8::EncryptedPrivateKeyInfoRef::from_der(der).map_err(pkcs8::Error::from)?;
    let document = encrypted.decrypt(password).map_err(|error| match error {
        pkcs8::Error::EncryptedPrivateKey(pkcs8::pkcs5::Error::DecryptFailed) => KeyError::WrongPassword,
        error => KeyError::Pkcs8(error),
    })?;
    PrivateKey::from_pkcs8_der(document.as_bytes())
}

#[cfg(not(feature = "keystore"))]
fn decrypt(_: &[u8], _: Option<&str>) -> Result<PrivateKey, KeyError> {
    Err(KeyError::EncryptionDisabled)
}

#[cfg(test)]
mod tests {
    use super::*;

    const DEBUG_PEM: &str = include_str!("../../tests/fixtures/keys/debug.pem");
    const EC_PEM: &str = include_str!("../../tests/fixtures/keys/ec-p256.pem");
    const MISMATCHED_CERT: &str = include_str!("../../tests/fixtures/keys/mismatched-cert.pem");

    #[test]
    fn loads_rsa_and_ec_bundles() {
        let rsa = SigningIdentity::from_pem(DEBUG_PEM).unwrap();
        assert!(matches!(rsa.key().kind(), super::super::KeyKind::Rsa { bits: 2048 }));
        let ec = SigningIdentity::from_pem(EC_PEM).unwrap();
        assert_eq!(ec.key().kind(), super::super::KeyKind::EcP256);
    }

    #[test]
    fn rejects_a_certificate_for_another_key() {
        let key_block = pem_blocks(DEBUG_PEM).find(|b| b.contains("PRIVATE KEY")).unwrap();
        let bundle = format!("{key_block}\n{MISMATCHED_CERT}");
        assert!(matches!(SigningIdentity::from_pem(&bundle), Err(KeyError::CertificateMismatch)));
    }

    #[test]
    fn requires_both_parts() {
        let cert_only = pem_blocks(DEBUG_PEM).find(|b| b.contains("CERTIFICATE")).unwrap();
        assert!(matches!(SigningIdentity::from_pem(cert_only), Err(KeyError::MissingPrivateKey)));
        let key_only = pem_blocks(DEBUG_PEM).find(|b| b.contains("PRIVATE KEY")).unwrap();
        assert!(matches!(SigningIdentity::from_pem(key_only), Err(KeyError::MissingCertificate)));
    }

    #[cfg(feature = "keystore")]
    #[test]
    fn encrypted_pem_requires_the_right_password_and_preserves_identity() {
        let encrypted = include_str!("../../tests/fixtures/keys/ec-p256-encrypted.pem");
        let plain = SigningIdentity::from_pem(EC_PEM).unwrap();
        let loaded = SigningIdentity::from_encrypted_pem(encrypted, "test-only").unwrap();
        assert_eq!(loaded.certificate(), plain.certificate());
        assert_eq!(loaded.key().public_key(), plain.key().public_key());
        assert!(matches!(SigningIdentity::from_pem(encrypted), Err(KeyError::PasswordRequired)));
        assert!(matches!(SigningIdentity::from_encrypted_pem(encrypted, "wrong"), Err(KeyError::WrongPassword)));
    }

    #[cfg(not(feature = "keystore"))]
    #[test]
    fn encrypted_pem_reports_the_missing_feature() {
        let encrypted = include_str!("../../tests/fixtures/keys/ec-p256-encrypted.pem");
        assert!(matches!(SigningIdentity::from_pem(encrypted), Err(KeyError::EncryptionDisabled)));
    }
}