#![cfg(feature = "signatures")]
use super::crypto::{
digest_info_prefix, hash_with_oid, is_rsa_pkcs1v15_sig_oid, OID_RSA_ENCRYPTION,
};
use crate::error::{Error, Result};
use cms::cert::x509::Certificate;
use cms::cert::CertificateChoices;
use cms::content_info::ContentInfo;
use cms::signed_data::{SignedData, SignerIdentifier, SignerInfo};
use der::asn1::OctetString;
use der::oid::ObjectIdentifier;
use der::{Decode, Encode};
use rsa::pkcs8::DecodePublicKey;
use rsa::{Pkcs1v15Sign, RsaPublicKey};
#[must_use]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignerVerify {
Valid,
Invalid,
Unknown,
}
const OID_MESSAGE_DIGEST: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.4");
fn find_signer_certificate<'a>(sd: &'a SignedData, signer: &SignerInfo) -> Option<&'a Certificate> {
let certs = sd.certificates.as_ref()?;
for choice in certs.0.iter() {
let CertificateChoices::Certificate(cert) = choice else {
continue;
};
match &signer.sid {
SignerIdentifier::IssuerAndSerialNumber(isn) => {
if cert.tbs_certificate.issuer == isn.issuer
&& cert.tbs_certificate.serial_number == isn.serial_number
{
return Some(cert);
}
},
SignerIdentifier::SubjectKeyIdentifier(_) => {
return Some(cert);
},
}
}
for choice in certs.0.iter() {
if let CertificateChoices::Certificate(cert) = choice {
return Some(cert);
}
}
None
}
fn parse_signed_data(contents: &[u8]) -> Result<SignedData> {
let ci = ContentInfo::from_der(contents).map_err(|e| {
Error::InvalidPdf(format!("signature /Contents is not valid CMS ContentInfo: {e}"))
})?;
let sd_bytes = ci
.content
.to_der()
.map_err(|e| Error::InvalidPdf(format!("failed to re-encode ContentInfo content: {e}")))?;
SignedData::from_der(&sd_bytes)
.map_err(|e| Error::InvalidPdf(format!("CMS content is not valid SignedData: {e}")))
}
fn run_signer_crypto(sd: &SignedData) -> Result<(SignerVerify, Option<ObjectIdentifier>)> {
let signer = sd
.signer_infos
.0
.iter()
.next()
.ok_or_else(|| Error::InvalidPdf("SignedData has no SignerInfo".into()))?;
let Some(signed_attrs) = signer.signed_attrs.as_ref() else {
return Ok((SignerVerify::Unknown, None));
};
let digest_oid = signer.digest_alg.oid;
let Some(hash) = hash_with_oid(
digest_oid,
&signed_attrs
.to_der()
.map_err(|e| Error::InvalidPdf(format!("failed to re-encode signed_attrs: {e}")))?,
) else {
return Ok((SignerVerify::Unknown, Some(digest_oid)));
};
let sig_alg_oid = signer.signature_algorithm.oid;
if !is_rsa_pkcs1v15_sig_oid(sig_alg_oid) {
return Ok((SignerVerify::Unknown, Some(digest_oid)));
}
let Some(prefix) = digest_info_prefix(digest_oid) else {
return Ok((SignerVerify::Unknown, Some(digest_oid)));
};
let mut digest_info = Vec::with_capacity(prefix.len() + hash.len());
digest_info.extend_from_slice(prefix);
digest_info.extend_from_slice(&hash);
let Some(cert) = find_signer_certificate(sd, signer) else {
return Ok((SignerVerify::Unknown, Some(digest_oid)));
};
if cert.tbs_certificate.subject_public_key_info.algorithm.oid != OID_RSA_ENCRYPTION {
return Ok((SignerVerify::Unknown, Some(digest_oid)));
}
let spki_der = cert
.tbs_certificate
.subject_public_key_info
.to_der()
.map_err(|e| Error::InvalidPdf(format!("failed to re-encode signer SPKI: {e}")))?;
let pub_key = match RsaPublicKey::from_public_key_der(&spki_der) {
Ok(k) => k,
Err(_) => return Ok((SignerVerify::Unknown, Some(digest_oid))),
};
let sig_bytes = signer.signature.as_bytes();
let outcome = match pub_key.verify(Pkcs1v15Sign::new_unprefixed(), &digest_info, sig_bytes) {
Ok(()) => SignerVerify::Valid,
Err(_) => SignerVerify::Invalid,
};
Ok((outcome, Some(digest_oid)))
}
fn extract_message_digest(sd: &SignedData) -> Option<Vec<u8>> {
let signer = sd.signer_infos.0.iter().next()?;
let signed_attrs = signer.signed_attrs.as_ref()?;
for attr in signed_attrs.iter() {
if attr.oid != OID_MESSAGE_DIGEST {
continue;
}
let value = attr.values.iter().next()?;
let value_der = value.to_der().ok()?;
let octet = OctetString::from_der(&value_der).ok()?;
return Some(octet.as_bytes().to_vec());
}
None
}
pub fn verify_signer(contents: &[u8]) -> Result<SignerVerify> {
let sd = parse_signed_data(contents)?;
Ok(run_signer_crypto(&sd)?.0)
}
pub fn verify_signer_detached(contents: &[u8], content: &[u8]) -> Result<SignerVerify> {
let sd = parse_signed_data(contents)?;
let (crypto_outcome, digest_oid) = run_signer_crypto(&sd)?;
match crypto_outcome {
SignerVerify::Valid => {},
other => return Ok(other),
}
let digest_oid = digest_oid.expect("Valid outcome implies known digest OID");
let Some(expected) = extract_message_digest(&sd) else {
return Ok(SignerVerify::Unknown);
};
let Some(actual) = hash_with_oid(digest_oid, content) else {
return Ok(SignerVerify::Unknown);
};
if actual.len() == expected.len()
&& actual
.iter()
.zip(expected.iter())
.fold(0u8, |acc, (a, b)| acc | (a ^ b))
== 0
{
Ok(SignerVerify::Valid)
} else {
Ok(SignerVerify::Invalid)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_non_cms_bytes() {
let err = verify_signer(b"not a CMS blob").unwrap_err();
assert!(matches!(err, Error::InvalidPdf(_)));
}
#[test]
fn rejects_empty_bytes() {
let err = verify_signer(&[]).unwrap_err();
assert!(matches!(err, Error::InvalidPdf(_)));
}
#[test]
fn detached_rejects_non_cms_bytes() {
let err = verify_signer_detached(b"not a CMS blob", b"content").unwrap_err();
assert!(matches!(err, Error::InvalidPdf(_)));
}
}