use std::collections::{HashMap, HashSet};
use std::io::{self, Read, Seek};
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use cms::cert::{CertificateChoices, IssuerAndSerialNumber};
use cms::content_info::{CmsVersion, ContentInfo};
use cms::signed_data::{
CertificateSet, EncapsulatedContentInfo, SignedData, SignerIdentifier, SignerInfo, SignerInfos,
};
use der::asn1::{Any, Null, ObjectIdentifier, OctetString, SetOfVec};
use der::{Decode, Encode, EncodingRules};
use x509_cert::spki::AlgorithmIdentifierOwned;
use zip::ZipArchive;
use super::{
Certificate, DigestAlgorithm, KeyError, KeyKind, Scheme, SignError, SignatureError, SigningIdentity, VerifyError,
};
use crate::archive::is_signed_entry;
use crate::archive::{ZipError, entry_name, read_capped};
use crate::types::ArchivePath;
const MANIFEST: &str = "META-INF/MANIFEST.MF";
const MAX_LINE: usize = 72;
const SIGNATURE_FILE_LIMIT: u64 = 64 << 20;
const ID_DATA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.7.1");
const ID_SIGNED_DATA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.7.2");
const ID_CONTENT_TYPE: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.3");
const ID_MESSAGE_DIGEST: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.4");
const RSA_ENCRYPTION: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1");
const SHA1_WITH_RSA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.5");
const SHA256_WITH_RSA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.11");
const SHA384_WITH_RSA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.12");
const SHA512_WITH_RSA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.13");
const EC_PUBLIC_KEY: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.2.1");
const ECDSA_WITH_SHA1: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.4.1");
const ECDSA_WITH_SHA256: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
const ECDSA_WITH_SHA384: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.3");
const ECDSA_WITH_SHA512: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.4");
const RSA_ALGORITHMS: [(ObjectIdentifier, Option<DigestAlgorithm>); 5] = [
(RSA_ENCRYPTION, None),
(SHA1_WITH_RSA, Some(DigestAlgorithm::Sha1)),
(SHA256_WITH_RSA, Some(DigestAlgorithm::Sha256)),
(SHA384_WITH_RSA, Some(DigestAlgorithm::Sha384)),
(SHA512_WITH_RSA, Some(DigestAlgorithm::Sha512)),
];
const EC_ALGORITHMS: [(ObjectIdentifier, Option<DigestAlgorithm>); 5] = [
(EC_PUBLIC_KEY, None),
(ECDSA_WITH_SHA1, Some(DigestAlgorithm::Sha1)),
(ECDSA_WITH_SHA256, Some(DigestAlgorithm::Sha256)),
(ECDSA_WITH_SHA384, Some(DigestAlgorithm::Sha384)),
(ECDSA_WITH_SHA512, Some(DigestAlgorithm::Sha512)),
];
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum JarError {
#[error("META-INF/MANIFEST.MF is missing")]
MissingManifest,
#[error("{file} is malformed at line {line}")]
MalformedManifest { file: String, line: usize },
#[error("{file} has more than one section for {name:?}")]
DuplicateSection { file: String, name: String },
#[error("signature block {block} has no signature file")]
MissingSignatureFile { block: String },
#[error("signature block {file} is malformed: {source}")]
MalformedSignatureBlock { file: String, source: der::Error },
#[error("signature block {file} is not a detached signature by exactly one signer")]
UnsupportedSignatureBlock { file: String },
#[error("signature block {file} does not contain its signer's certificate")]
UnknownSignerCertificate { file: String },
#[error("signature block {file} names signature algorithm {algorithm}, which does not fit its key and digest")]
SignatureAlgorithmMismatch { file: String, algorithm: ObjectIdentifier },
#[error("signature block {file} has signed attributes that do not match its signature file")]
SignedAttributesMismatch { file: String },
#[error("signature block {file} does not verify: {source}")]
BadSignature { file: String, source: SignatureError },
#[error("{file} uses no supported digest algorithm (SHA-1, SHA-256, SHA-384, or SHA-512)")]
UnsupportedDigest { file: String },
#[error("{file} has a digest that is not valid base64")]
InvalidDigest { file: String },
#[error("signature file {signature_file} does not match MANIFEST.MF")]
ManifestDigestMismatch { signature_file: String },
#[error("entry {entry:?} is not covered by MANIFEST.MF")]
UnsignedEntry { entry: String },
#[error("MANIFEST.MF lists {entry:?}, which is not in the APK")]
MissingEntry { entry: String },
#[error("entry {entry:?} does not match its digest in MANIFEST.MF")]
EntryDigestMismatch { entry: String },
}
pub(crate) fn sign(
entries: &[(String, Vec<u8>)],
digest: DigestAlgorithm,
identities: &[SigningIdentity],
declared: &[Scheme],
) -> Result<Vec<(ArchivePath, Vec<u8>)>, SignError> {
let digest_name = format!("{}-Digest", digest.jar_name());
let mut signed: Vec<&(String, Vec<u8>)> = entries.iter().collect();
signed.sort_by(|a, b| a.0.cmp(&b.0));
let mut manifest = section(&[("Manifest-Version", "1.0"), ("Created-By", "1.0 (Android)")]);
let mut sections = Vec::with_capacity(signed.len());
for (name, entry_digest) in signed {
let entry_section = section(&[("Name", name), (&digest_name, &BASE64.encode(entry_digest))]);
manifest.extend_from_slice(&entry_section);
sections.push((name, entry_section));
}
let manifest_digest = BASE64.encode(digest.digest(&manifest));
let manifest_attribute = format!("{}-Digest-Manifest", digest.jar_name());
let declared = declared.iter().map(|s| s.number().to_string()).collect::<Vec<_>>().join(", ");
let mut main = vec![("Signature-Version", "1.0"), ("Created-By", "1.0 (Android)")];
main.push((&manifest_attribute, &manifest_digest));
if !declared.is_empty() {
main.push(("X-Android-APK-Signed", &declared));
}
let mut signature_file = section(&main);
for (name, entry_section) in §ions {
signature_file.extend(section(&[("Name", name), (&digest_name, &BASE64.encode(digest.digest(entry_section)))]));
}
let mut files = vec![(ArchivePath::known(MANIFEST), manifest)];
for (index, identity) in identities.iter().enumerate() {
let base = if identities.len() == 1 { "CERT".to_owned() } else { format!("CERT{}", index + 1) };
let extension = match identity.key().kind() {
KeyKind::Rsa { .. } => "RSA",
KeyKind::EcP256 | KeyKind::EcP384 => "EC",
};
let block = signature_block(&signature_file, identity, digest).map_err(SignError::Key)?;
files.push((ArchivePath::known(&format!("META-INF/{base}.SF")), signature_file.clone()));
files.push((ArchivePath::known(&format!("META-INF/{base}.{extension}")), block));
}
Ok(files)
}
fn section(attributes: &[(&str, &str)]) -> Vec<u8> {
let mut out = Vec::new();
for (name, value) in attributes {
let line = format!("{name}: {value}");
let mut bytes = line.as_bytes();
let mut limit = MAX_LINE;
loop {
let (chunk, rest) = bytes.split_at(bytes.len().min(limit));
out.extend_from_slice(chunk);
out.extend_from_slice(b"\r\n");
if rest.is_empty() {
break;
}
out.push(b' ');
bytes = rest;
limit = MAX_LINE - 1;
}
}
out.extend_from_slice(b"\r\n");
out
}
fn signature_algorithm(key: KeyKind, digest: DigestAlgorithm) -> Result<AlgorithmIdentifierOwned, der::Error> {
Ok(match key {
KeyKind::Rsa { .. } => {
AlgorithmIdentifierOwned { oid: RSA_ENCRYPTION, parameters: Some(Any::encode_from(&Null)?) }
}
KeyKind::EcP256 | KeyKind::EcP384 => {
let (oid, _) = EC_ALGORITHMS.into_iter().find(|&(_, named)| named == Some(digest)).expect("every digest");
AlgorithmIdentifierOwned { oid, parameters: None }
}
})
}
fn algorithm_fits(algorithm: &ObjectIdentifier, key: KeyKind, digest: DigestAlgorithm) -> bool {
let family = match key {
KeyKind::Rsa { .. } => RSA_ALGORITHMS,
KeyKind::EcP256 | KeyKind::EcP384 => EC_ALGORITHMS,
};
family.into_iter().any(|(oid, named)| oid == *algorithm && named.is_none_or(|named| named == digest))
}
fn signature_block(
signature_file: &[u8],
identity: &SigningIdentity,
digest: DigestAlgorithm,
) -> Result<Vec<u8>, KeyError> {
let signature = identity.key().sign_jar(digest, signature_file)?;
let leaf = identity.certificate().x509().tbs_certificate();
let digest_algorithm = AlgorithmIdentifierOwned {
oid: digest.oid(),
parameters: Some(Any::encode_from(&Null).map_err(KeyError::Encoding)?),
};
let signer = SignerInfo {
version: CmsVersion::V1,
sid: SignerIdentifier::IssuerAndSerialNumber(IssuerAndSerialNumber {
issuer: leaf.issuer().clone(),
serial_number: leaf.serial_number().clone(),
}),
digest_alg: digest_algorithm.clone(),
signed_attrs: None,
signature_algorithm: signature_algorithm(identity.key().kind(), digest).map_err(KeyError::Encoding)?,
signature: OctetString::new(signature).map_err(KeyError::Encoding)?,
unsigned_attrs: None,
};
let certificates: Vec<CertificateChoices> =
identity.chain().iter().map(|c| CertificateChoices::Certificate(c.x509().clone())).collect();
let signed = SignedData {
version: CmsVersion::V1,
digest_algorithms: SetOfVec::try_from(vec![digest_algorithm]).map_err(KeyError::Encoding)?,
encap_content_info: EncapsulatedContentInfo { econtent_type: ID_DATA, econtent: None },
certificates: Some(CertificateSet(SetOfVec::try_from(certificates).map_err(KeyError::Encoding)?)),
crls: None,
signer_infos: SignerInfos(SetOfVec::try_from(vec![signer]).map_err(KeyError::Encoding)?),
};
let content = Any::encode_from(&signed).map_err(KeyError::Encoding)?;
ContentInfo { content_type: ID_SIGNED_DATA, content }.to_der().map_err(KeyError::Encoding)
}
pub(crate) struct JarSignature {
pub signers: Vec<Vec<Certificate>>,
pub declared_schemes: Vec<u32>,
}
fn is_signature_block(name: &str) -> bool {
let Some(file) = name.strip_prefix("META-INF/").filter(|f| !f.contains('/')) else { return false };
let file = file.to_ascii_uppercase();
[".RSA", ".DSA", ".EC"].iter().any(|extension| file.ends_with(extension))
}
pub(crate) fn verify<R: Read + Seek>(archive: &mut ZipArchive<R>) -> Result<Option<JarSignature>, VerifyError> {
let names: HashMap<String, usize> =
(0..archive.len()).map(|i| entry_name(archive, i).map(|name| (name, i))).collect::<Result<_, _>>()?;
let mut blocks: Vec<&String> = names.keys().filter(|name| is_signature_block(name)).collect();
if blocks.is_empty() {
return Ok(None);
}
blocks.sort();
let mut read = |name: &str| -> Result<Option<Vec<u8>>, VerifyError> {
let Some(&index) = names.get(name) else { return Ok(None) };
Ok(Some(read_capped(archive.by_index(index).map_err(ZipError::from)?, name, SIGNATURE_FILE_LIMIT)?))
};
let manifest_bytes = read(MANIFEST)?.ok_or(JarError::MissingManifest)?;
let manifest = Manifest::parse(&manifest_bytes, MANIFEST)?;
let mut signed_entries: Vec<(&String, usize)> =
names.iter().filter(|(name, _)| is_signed_entry(name)).map(|(name, &index)| (name, index)).collect();
signed_entries.sort();
let mut signers = Vec::with_capacity(blocks.len());
let mut declared_schemes = Vec::new();
for block_name in blocks {
let base = &block_name[..block_name.rfind('.').expect("blocks have extensions")];
let signature_file = format!("{base}.SF");
let signature_bytes =
read(&signature_file)?.ok_or_else(|| JarError::MissingSignatureFile { block: block_name.clone() })?;
let block = read(block_name)?.unwrap_or_default();
signers.push(verify_block(&block, &signature_bytes, block_name)?);
let signature = Manifest::parse(&signature_bytes, &signature_file)?;
check_signature_file(&signature, &manifest, &manifest_bytes, &signature_file)?;
if let Some((entry, _)) = signed_entries.iter().find(|(name, _)| signature.entry(name).is_none()) {
return Err(JarError::UnsignedEntry { entry: (*entry).clone() }.into());
}
if let Some(schemes) = signature.main.get("X-Android-APK-Signed") {
declared_schemes.extend(schemes.split(',').filter_map(|s| s.trim().parse::<u32>().ok()));
}
}
for (name, index) in signed_entries {
let section = manifest.entry(name).ok_or_else(|| JarError::UnsignedEntry { entry: name.clone() })?;
let (algorithm, expected) = section
.digest("-Digest", MANIFEST)?
.ok_or_else(|| JarError::UnsupportedDigest { file: MANIFEST.into() })?;
let mut hasher = algorithm.hasher();
io::copy(&mut archive.by_index(index).map_err(ZipError::from)?, &mut hasher).map_err(ZipError::from)?;
if hasher.finish() != expected {
return Err(JarError::EntryDigestMismatch { entry: name.clone() }.into());
}
}
if let Some(missing) = manifest.entries.iter().filter_map(Section::name).find(|n| !names.contains_key(*n)) {
return Err(JarError::MissingEntry { entry: missing.to_owned() }.into());
}
Ok(Some(JarSignature { signers, declared_schemes }))
}
fn verify_block(block: &[u8], signature_file: &[u8], file: &str) -> Result<Vec<Certificate>, JarError> {
let malformed = |source| JarError::MalformedSignatureBlock { file: file.to_owned(), source };
let unsupported = || JarError::UnsupportedSignatureBlock { file: file.to_owned() };
let info = ContentInfo::from_ber(block).map_err(malformed)?;
if info.content_type != ID_SIGNED_DATA {
return Err(unsupported());
}
let signed: SignedData = info.content.decode_as_encoding(EncodingRules::Ber).map_err(malformed)?;
let content = &signed.encap_content_info;
if content.econtent_type != ID_DATA || content.econtent.is_some() {
return Err(unsupported());
}
let [signer] = signed.signer_infos.0.as_slice() else { return Err(unsupported()) };
let SignerIdentifier::IssuerAndSerialNumber(id) = &signer.sid else { return Err(unsupported()) };
let certificates: Vec<&x509_cert::Certificate> = signed
.certificates
.iter()
.flat_map(|set| set.0.iter())
.filter_map(|choice| match choice {
CertificateChoices::Certificate(certificate) => Some(certificate),
CertificateChoices::Other(_) => None,
})
.collect();
let position = certificates
.iter()
.position(|c| {
c.tbs_certificate().issuer() == &id.issuer && c.tbs_certificate().serial_number() == &id.serial_number
})
.ok_or_else(|| JarError::UnknownSignerCertificate { file: file.to_owned() })?;
let digest = DigestAlgorithm::from_oid(&signer.digest_alg.oid)
.ok_or_else(|| JarError::UnsupportedDigest { file: file.to_owned() })?;
let to_certificate = |c: &x509_cert::Certificate| -> Result<Certificate, JarError> {
Certificate::from_der(&c.to_der().map_err(malformed)?).map_err(|_| unsupported())
};
let leaf = to_certificate(certificates[position])?;
let key = leaf.public_key().map_err(|_| unsupported())?;
let algorithm = &signer.signature_algorithm.oid;
if !algorithm_fits(algorithm, key.kind(), digest) {
return Err(JarError::SignatureAlgorithmMismatch { file: file.to_owned(), algorithm: *algorithm });
}
let signed_attributes;
let message = match &signer.signed_attrs {
None => signature_file,
Some(attributes) => {
let value_of = |oid| attributes.iter().find(|a| a.oid == oid).and_then(|a| a.values.get(0));
let content_type = value_of(ID_CONTENT_TYPE).and_then(|v| v.decode_as::<ObjectIdentifier>().ok());
let message_digest = value_of(ID_MESSAGE_DIGEST).and_then(|v| v.decode_as::<OctetString>().ok());
if content_type != Some(ID_DATA)
|| message_digest.as_ref().map(OctetString::as_bytes) != Some(&digest.digest(signature_file)[..])
{
return Err(JarError::SignedAttributesMismatch { file: file.to_owned() });
}
signed_attributes = attributes.to_der().map_err(malformed)?;
&signed_attributes
}
};
key.verify_jar(digest, message, signer.signature.as_bytes())
.map_err(|source| JarError::BadSignature { file: file.to_owned(), source })?;
let mut chain = vec![leaf];
for (_, certificate) in certificates.iter().enumerate().filter(|(i, _)| *i != position) {
chain.push(to_certificate(certificate)?);
}
Ok(chain)
}
fn check_signature_file(
signature: &Manifest<'_>,
manifest: &Manifest<'_>,
manifest_bytes: &[u8],
file: &str,
) -> Result<(), JarError> {
let mismatch = || JarError::ManifestDigestMismatch { signature_file: file.to_owned() };
if let Some((algorithm, expected)) = signature.main.digest("-Digest-Manifest", file)?
&& algorithm.digest(manifest_bytes) == expected
{
return Ok(());
}
if let Some((algorithm, expected)) = signature.main.digest("-Digest-Manifest-Main-Attributes", file)?
&& algorithm.digest(manifest.main.raw) != expected
{
return Err(mismatch());
}
let mut listed = HashSet::new();
for section in &signature.entries {
let name = section.name().ok_or_else(mismatch)?;
let (algorithm, expected) =
section.digest("-Digest", file)?.ok_or_else(|| JarError::UnsupportedDigest { file: file.to_owned() })?;
let manifest_section = manifest.entry(name).ok_or_else(mismatch)?;
if algorithm.digest(manifest_section.raw) != expected {
return Err(mismatch());
}
listed.insert(name);
}
match manifest.entries.iter().filter_map(Section::name).find(|name| !listed.contains(name)) {
Some(unlisted) => Err(JarError::UnsignedEntry { entry: unlisted.to_owned() }),
None => Ok(()),
}
}
type Line = (usize, Vec<u8>);
struct Manifest<'a> {
main: Section<'a>,
entries: Vec<Section<'a>>,
index: HashMap<String, usize>,
}
struct Section<'a> {
raw: &'a [u8],
attributes: Vec<(String, String)>,
}
impl Section<'_> {
fn get(&self, name: &str) -> Option<&str> {
self.attributes.iter().find(|(n, _)| n.eq_ignore_ascii_case(name)).map(|(_, v)| v.as_str())
}
fn name(&self) -> Option<&str> {
self.get("Name")
}
fn digest(&self, suffix: &str, file: &str) -> Result<Option<(DigestAlgorithm, Vec<u8>)>, JarError> {
let strongest = self
.attributes
.iter()
.filter_map(|(name, value)| {
let (algorithm, tail) = name.split_at_checked(name.len().checked_sub(suffix.len())?)?;
tail.eq_ignore_ascii_case(suffix).then_some(())?;
Some((DigestAlgorithm::from_jar_name(algorithm)?, value))
})
.max_by_key(|(algorithm, _)| *algorithm);
let Some((algorithm, value)) = strongest else { return Ok(None) };
let digest = BASE64.decode(value.trim()).map_err(|_| JarError::InvalidDigest { file: file.to_owned() })?;
Ok(Some((algorithm, digest)))
}
}
impl<'a> Manifest<'a> {
fn parse(bytes: &'a [u8], file: &str) -> Result<Self, JarError> {
let malformed = |line| JarError::MalformedManifest { file: file.to_owned(), line };
let mut raw_sections: Vec<(&[u8], Vec<Line>)> = Vec::new();
let mut lines: Vec<Line> = Vec::new();
let (mut start, mut pos, mut line_number) = (0, 0, 0);
while pos < bytes.len() {
line_number += 1;
let end = bytes[pos..].iter().position(|&b| b == b'\n' || b == b'\r').map_or(bytes.len(), |i| pos + i);
let next = match bytes.get(end..end + 2) {
Some(b"\r\n") => end + 2,
_ => (end + 1).min(bytes.len()),
};
let line = &bytes[pos..end];
if line.is_empty() {
if !lines.is_empty() {
raw_sections.push((&bytes[start..next], std::mem::take(&mut lines)));
}
start = next;
} else if let Some(continuation) = line.strip_prefix(b" ") {
lines.last_mut().ok_or(malformed(line_number))?.1.extend_from_slice(continuation);
} else {
lines.push((line_number, line.to_vec()));
}
pos = next;
}
if !lines.is_empty() {
raw_sections.push((&bytes[start..], lines));
}
let mut sections = Vec::with_capacity(raw_sections.len());
for (raw, lines) in raw_sections {
let attributes = lines
.into_iter()
.map(|(number, line)| {
let text = String::from_utf8(line).map_err(|_| malformed(number))?;
let (name, value) = text.split_once(": ").ok_or(malformed(number))?;
Ok((name.to_owned(), value.to_owned()))
})
.collect::<Result<_, JarError>>()?;
sections.push(Section { raw, attributes });
}
let mut sections = sections.into_iter();
let main = sections.next().unwrap_or(Section { raw: &[], attributes: Vec::new() });
let entries: Vec<Section<'a>> = sections.collect();
let mut index = HashMap::new();
for (i, section) in entries.iter().enumerate() {
let name = section.name().ok_or(malformed(0))?;
if index.insert(name.to_owned(), i).is_some() {
return Err(JarError::DuplicateSection { file: file.to_owned(), name: name.to_owned() });
}
}
Ok(Self { main, entries, index })
}
fn entry(&self, name: &str) -> Option<&Section<'a>> {
self.index.get(name).map(|&i| &self.entries[i])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::archive::is_signature_file;
#[test]
fn folds_lines_at_72_bytes() {
let name = format!("assets/{}雪.bin", "x".repeat(80));
let bytes = section(&[("Name", &name)]);
assert!(bytes.split(|&b| b == b'\n').all(|line| line.len() <= MAX_LINE + 1));
let manifest = Manifest::parse(&bytes, "test").unwrap();
assert_eq!(manifest.main.get("Name"), Some(name.as_str()));
}
#[test]
fn classifies_signature_files_like_apksigner() {
for name in ["META-INF/MANIFEST.MF", "META-INF/CERT.SF", "META-INF/cert.rsa", "META-INF/A.EC", "META-INF/SIG-X"]
{
assert!(is_signature_file(name), "{name}");
assert!(!is_signed_entry(name), "{name}");
}
for name in ["META-INF/services/x", "META-INF/LICENSE", "classes.dex", "meta-inf/CERT.SF"] {
assert!(!is_signature_file(name), "{name}");
assert!(is_signed_entry(name), "{name}");
}
assert!(!is_signed_entry("assets/"));
}
#[test]
fn multibyte_attribute_names_do_not_panic() {
let manifest = Manifest::parse("€€€: x\r\n€€€€€€: y\r\n\r\n".as_bytes(), "m").unwrap();
assert_eq!(manifest.main.digest("-Digest", "m").unwrap(), None);
assert_eq!(manifest.main.digest("-Digest-Manifest", "m").unwrap(), None);
}
#[test]
fn the_strongest_digest_counts() {
let text = b"Name: a\r\nSHA-256-Digest: AAAA\r\nSHA-512-Digest: AQID\r\n\r\n";
let manifest = Manifest::parse(text, "m").unwrap();
assert_eq!(manifest.main.digest("-Digest", "m").unwrap(), Some((DigestAlgorithm::Sha512, vec![1, 2, 3])));
let manifest = Manifest::parse(b"Name: a\r\nSHA-256-Digest: AAAA\r\nSHA-512-Digest: !!\r\n\r\n", "m").unwrap();
assert!(matches!(manifest.main.digest("-Digest", "m"), Err(JarError::InvalidDigest { .. })));
}
fn signed_archive(listed: &[&str]) -> Vec<u8> {
let identity = SigningIdentity::from_pem(include_str!("../../tests/fixtures/keys/debug.pem")).unwrap();
let entries = [("a", b"alpha".as_slice()), ("b", b"beta".as_slice())];
let digests: Vec<(String, Vec<u8>)> =
entries.iter().map(|(name, data)| (name.to_string(), DigestAlgorithm::Sha256.digest(data))).collect();
let manifest_bytes =
sign(&digests, DigestAlgorithm::Sha256, std::slice::from_ref(&identity), &[]).unwrap()[0].1.clone();
let manifest = Manifest::parse(&manifest_bytes, MANIFEST).unwrap();
let whole = BASE64.encode(DigestAlgorithm::Sha256.digest(&manifest_bytes));
let mut signature_file = section(&[("Signature-Version", "1.0"), ("SHA-256-Digest-Manifest", &whole)]);
for name in listed {
let digest = BASE64.encode(DigestAlgorithm::Sha256.digest(manifest.entry(name).unwrap().raw));
signature_file.extend(section(&[("Name", name), ("SHA-256-Digest", &digest)]));
}
let block = signature_block(&signature_file, &identity, DigestAlgorithm::Sha256).unwrap();
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
let files = entries.iter().map(|&(name, data)| (name, data)).chain([
(MANIFEST, manifest_bytes.as_slice()),
("META-INF/CERT.SF", signature_file.as_slice()),
("META-INF/CERT.RSA", block.as_slice()),
]);
for (name, data) in files {
zip.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
std::io::Write::write_all(&mut zip, data).unwrap();
}
zip.finish().unwrap().into_inner()
}
#[test]
fn every_signed_entry_must_be_named_by_every_signer() {
let verify_listing = |listed: &[&str]| {
let bytes = signed_archive(listed);
verify(&mut ZipArchive::new(std::io::Cursor::new(bytes)).unwrap())
};
assert_eq!(verify_listing(&["a", "b"]).unwrap().unwrap().signers.len(), 1);
assert!(matches!(
verify_listing(&["a"]),
Err(VerifyError::Jar(JarError::UnsignedEntry { entry })) if entry == "b"
));
}
#[test]
fn per_section_signature_files_must_cover_every_manifest_section() {
let manifest_bytes = b"Manifest-Version: 1.0\r\n\r\nName: a\r\nSHA-256-Digest: AA==\r\n\r\nName: b\r\nSHA-256-Digest: AA==\r\n\r\n";
let manifest = Manifest::parse(manifest_bytes, MANIFEST).unwrap();
let section_a = BASE64.encode(DigestAlgorithm::Sha256.digest(manifest.entry("a").unwrap().raw));
let signature = format!("Signature-Version: 1.0\r\n\r\nName: a\r\nSHA-256-Digest: {section_a}\r\n\r\n");
let signature = Manifest::parse(signature.as_bytes(), "CERT.SF").unwrap();
assert!(matches!(
check_signature_file(&signature, &manifest, manifest_bytes, "CERT.SF"),
Err(JarError::UnsignedEntry { entry }) if entry == "b"
));
}
#[test]
fn parses_lf_and_crlf_manifests() {
let text = b"Manifest-Version: 1.0\n\nName: a\nSHA-256-Digest: AAAA\n\nName: b\r\nSHA1-Digest: AAAA\r\n";
let manifest = Manifest::parse(text, "m").unwrap();
assert_eq!(manifest.entries.len(), 2);
let algorithm = |name| manifest.entry(name).unwrap().digest("-Digest", "m").unwrap().unwrap().0;
assert_eq!((algorithm("a"), algorithm("b")), (DigestAlgorithm::Sha256, DigestAlgorithm::Sha1));
assert!(matches!(
Manifest::parse(b"A: 1\n\nName: x\n\nName: x\n", "m"),
Err(JarError::DuplicateSection { .. })
));
}
fn block_claiming(key: &str, algorithm: ObjectIdentifier, signature_file: &[u8]) -> Vec<u8> {
let pem = match key {
"rsa" => include_str!("../../tests/fixtures/keys/debug.pem"),
_ => include_str!("../../tests/fixtures/keys/ec-p256.pem"),
};
let identity = SigningIdentity::from_pem(pem).unwrap();
let block = signature_block(signature_file, &identity, DigestAlgorithm::Sha256).unwrap();
let info = ContentInfo::from_der(&block).unwrap();
let mut signed: SignedData = info.content.decode_as().unwrap();
let mut signer = signed.signer_infos.0.as_slice()[0].clone();
signer.signature_algorithm.oid = algorithm;
signed.signer_infos = SignerInfos(SetOfVec::try_from(vec![signer]).unwrap());
let content = Any::encode_from(&signed).unwrap();
ContentInfo { content_type: ID_SIGNED_DATA, content }.to_der().unwrap()
}
#[test]
fn signature_algorithms_must_fit_the_key_and_digest() {
let file = b"Signature-Version: 1.0\r\n\r\n";
for (key, algorithm) in
[("rsa", RSA_ENCRYPTION), ("rsa", SHA256_WITH_RSA), ("ec", ECDSA_WITH_SHA256), ("ec", EC_PUBLIC_KEY)]
{
verify_block(&block_claiming(key, algorithm, file), file, "CERT")
.unwrap_or_else(|e| panic!("{algorithm}: {e}"));
}
for (key, algorithm) in
[("ec", RSA_ENCRYPTION), ("rsa", ECDSA_WITH_SHA256), ("rsa", SHA1_WITH_RSA), ("ec", ECDSA_WITH_SHA1)]
{
assert!(
matches!(
verify_block(&block_claiming(key, algorithm, file), file, "CERT"),
Err(JarError::SignatureAlgorithmMismatch { .. })
),
"{key} key accepted {algorithm}"
);
}
}
}