use cms::cert::CertificateChoices;
use cms::signed_data::{SignedData, SignerIdentifier, SignerInfo};
use const_oid::{AssociatedOid, ObjectIdentifier};
use der::asn1::OctetStringRef;
use der::referenced::OwnedToRef;
use der::{AnyRef, Decode, Encode, Reader, SliceReader, Tag};
use spki::{AlgorithmIdentifierOwned, AlgorithmIdentifierRef};
use x509_cert::ext::pkix::SubjectKeyIdentifier;
use x509_cert::Certificate;
use x509_verify::{Signature, VerifyInfo, VerifyingKey};
use super::digest::DigestAlgorithm;
use super::signed_data_from_der;
const OID_CONTENT_TYPE: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.3");
const OID_SIGNING_CERTIFICATE: ObjectIdentifier =
ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.16.2.12");
const OID_SIGNING_CERTIFICATE_V2: ObjectIdentifier =
ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.16.2.47");
const OID_RSA_ENCRYPTION: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1");
const OID_SHA1_RSA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.5");
const OID_SHA256_RSA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.11");
const OID_SHA384_RSA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.12");
const OID_SHA512_RSA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.13");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignatureStatus {
Valid,
Invalid,
Unverifiable(&'static str),
}
impl SignatureStatus {
#[must_use]
pub fn is_valid(self) -> bool {
matches!(self, Self::Valid)
}
#[must_use]
pub fn describe(self) -> String {
match self {
Self::Valid => "Signature OK -- signer signature verifies".to_owned(),
Self::Invalid => {
"Signature INVALID -- does not verify against the signer certificate".to_owned()
}
Self::Unverifiable(why) => format!("Signature not verified ({why})"),
}
}
}
#[must_use]
pub fn has_signed_attributes(cms_der: &[u8]) -> Option<bool> {
let signed_data = signed_data_from_der(cms_der).ok()?;
let signer_info = signed_data.signer_infos.0.iter().next()?;
Some(signer_info.signed_attrs.is_some())
}
#[must_use]
pub fn verify_signer_signature(cms_der: &[u8], content: Option<&[u8]>) -> SignatureStatus {
match signed_data_from_der(cms_der) {
Ok(signed_data) => verify_from_signed_data(&signed_data, content),
Err(_) => SignatureStatus::Unverifiable("CMS did not parse"),
}
}
fn verify_from_signed_data(signed_data: &SignedData, content: Option<&[u8]>) -> SignatureStatus {
let Some(signer_info) = signed_data.signer_infos.0.iter().next() else {
return SignatureStatus::Unverifiable("no SignerInfo present");
};
let Some(digest_alg) = DigestAlgorithm::from_oid(&signer_info.digest_alg.oid) else {
return SignatureStatus::Unverifiable("unrecognized digest algorithm");
};
if !is_rsa(&signer_info.signature_algorithm.oid) {
return SignatureStatus::Unverifiable("non-RSA signer is unsupported");
}
let certs = embedded_certificates(signed_data);
let Some(signer_cert) = find_signer_cert(&certs, &signer_info.sid) else {
return SignatureStatus::Unverifiable("signer certificate not embedded");
};
let message = if let Some(signed_attrs) = signer_info.signed_attrs.as_ref() {
let Ok(encoded) = signed_attrs.to_der() else {
return SignatureStatus::Unverifiable("cannot re-encode signed attributes");
};
encoded
} else {
let embedded = signed_data
.encap_content_info
.econtent
.as_ref()
.map(der::asn1::Any::value);
let contradicts_caller =
matches!((content, embedded), (Some(caller), Some(own)) if caller != own);
if contradicts_caller {
return SignatureStatus::Unverifiable(
"embedded CMS content differs from the data being verified",
);
}
let Some(body) = content.or(embedded) else {
return SignatureStatus::Unverifiable("no signed attributes and no content to verify");
};
body.to_vec()
};
let algorithm = AlgorithmIdentifierOwned {
oid: rsa_with_hash_oid(digest_alg),
parameters: None,
};
let signature = Signature::new(&algorithm, signer_info.signature.as_bytes());
let spki = signer_cert
.tbs_certificate
.subject_public_key_info
.owned_to_ref();
let Ok(key) = VerifyingKey::try_from(spki) else {
return SignatureStatus::Unverifiable("signer public key is unusable");
};
let verify_info = VerifyInfo::new(message.into(), signature);
match key.verify(&verify_info) {
Ok(()) => finalize_valid(signed_data, signer_info, signer_cert),
Err(x509_verify::Error::Verification | x509_verify::Error::InvalidSignature) => {
SignatureStatus::Invalid
}
Err(_) => SignatureStatus::Unverifiable("signature could not be checked"),
}
}
fn finalize_valid(
signed_data: &SignedData,
signer_info: &SignerInfo,
signer_cert: &Certificate,
) -> SignatureStatus {
if signer_info.signed_attrs.is_none() {
return SignatureStatus::Valid;
}
if !content_type_ok(signed_data, signer_info) {
return SignatureStatus::Unverifiable("contentType attribute missing or inconsistent");
}
match signing_cert_binding(signer_info, signer_cert) {
SigningCertBinding::Absent | SigningCertBinding::Match => SignatureStatus::Valid,
SigningCertBinding::Mismatch => SignatureStatus::Unverifiable(
"signingCertificate attribute names a different certificate",
),
SigningCertBinding::Unparsable => {
SignatureStatus::Unverifiable("signingCertificate attribute could not be parsed")
}
}
}
fn content_type_ok(signed_data: &SignedData, signer_info: &SignerInfo) -> bool {
let Some(signed_attrs) = signer_info.signed_attrs.as_ref() else {
return false;
};
signed_attrs
.iter()
.find(|attr| attr.oid == OID_CONTENT_TYPE)
.and_then(|attr| attr.values.iter().next())
.and_then(|value| value.decode_as::<ObjectIdentifier>().ok())
.is_some_and(|oid| oid == signed_data.encap_content_info.econtent_type)
}
enum SigningCertBinding {
Absent,
Match,
Mismatch,
Unparsable,
}
fn signing_cert_binding(signer_info: &SignerInfo, signer_cert: &Certificate) -> SigningCertBinding {
let Some(signed_attrs) = signer_info.signed_attrs.as_ref() else {
return SigningCertBinding::Absent;
};
let (attr, v2) = if let Some(attr) = signed_attrs
.iter()
.find(|a| a.oid == OID_SIGNING_CERTIFICATE)
{
(attr, false)
} else if let Some(attr) = signed_attrs
.iter()
.find(|a| a.oid == OID_SIGNING_CERTIFICATE_V2)
{
(attr, true)
} else {
return SigningCertBinding::Absent;
};
let Some((algo, want_hash)) = attr
.values
.iter()
.next()
.and_then(|value| value.to_der().ok())
.and_then(|der| ess_cert_hash(&der, v2))
else {
return SigningCertBinding::Unparsable;
};
let Ok(cert_der) = signer_cert.to_der() else {
return SigningCertBinding::Unparsable;
};
if algo.hash(&cert_der) == want_hash {
SigningCertBinding::Match
} else {
SigningCertBinding::Mismatch
}
}
fn ess_cert_hash(signing_cert_der: &[u8], v2: bool) -> Option<(DigestAlgorithm, Vec<u8>)> {
let signing_cert_body = first_seq_content(signing_cert_der)?;
let certs_body = first_seq_content(signing_cert_body)?;
let ess_cert_id_body = first_seq_content(certs_body)?;
let mut reader = SliceReader::new(ess_cert_id_body).ok()?;
let algo = if v2 {
if reader.peek_header().ok()?.tag == Tag::Sequence {
let alg = reader.decode::<AlgorithmIdentifierRef<'_>>().ok()?;
DigestAlgorithm::from_oid(&alg.oid)?
} else {
DigestAlgorithm::Sha256
}
} else {
DigestAlgorithm::Sha1
};
let cert_hash = reader.decode::<OctetStringRef<'_>>().ok()?;
Some((algo, cert_hash.as_bytes().to_vec()))
}
fn first_seq_content(body: &[u8]) -> Option<&[u8]> {
let mut reader = SliceReader::new(body).ok()?;
if reader.peek_header().ok()?.tag != Tag::Sequence {
return None;
}
let tlv = reader.tlv_bytes().ok()?;
AnyRef::from_der(tlv).ok().map(AnyRef::value)
}
fn embedded_certificates(signed_data: &SignedData) -> Vec<Certificate> {
match &signed_data.certificates {
Some(set) => set
.0
.iter()
.filter_map(|choice| match choice {
CertificateChoices::Certificate(cert) => Some(cert.clone()),
CertificateChoices::Other(_) => None,
})
.collect(),
None => Vec::new(),
}
}
fn find_signer_cert<'a>(
certs: &'a [Certificate],
signer_id: &SignerIdentifier,
) -> Option<&'a Certificate> {
let mut matches = certs
.iter()
.filter(|cert| matches_signer_id(cert, signer_id));
let only = matches.next()?;
matches.next().is_none().then_some(only)
}
fn matches_signer_id(cert: &Certificate, signer_id: &SignerIdentifier) -> bool {
match signer_id {
SignerIdentifier::IssuerAndSerialNumber(ias) => {
cert.tbs_certificate.issuer == ias.issuer
&& cert.tbs_certificate.serial_number == ias.serial_number
}
SignerIdentifier::SubjectKeyIdentifier(skid) => {
cert_ski(cert).as_deref() == Some(skid.0.as_bytes())
}
}
}
#[must_use]
pub fn signer_certificate(cms_der: &[u8]) -> Option<Certificate> {
let signed_data = signed_data_from_der(cms_der).ok()?;
let signer_info = signed_data.signer_infos.0.iter().next()?;
let certs = embedded_certificates(&signed_data);
find_signer_cert(&certs, &signer_info.sid).cloned()
}
fn cert_ski(cert: &Certificate) -> Option<Vec<u8>> {
let extensions = cert.tbs_certificate.extensions.as_ref()?;
let ext = extensions
.iter()
.find(|e| e.extn_id == SubjectKeyIdentifier::OID)?;
SubjectKeyIdentifier::from_der(ext.extn_value.as_bytes())
.ok()
.map(|ski| ski.0.as_bytes().to_vec())
}
fn is_rsa(oid: &ObjectIdentifier) -> bool {
matches!(
*oid,
OID_RSA_ENCRYPTION | OID_SHA1_RSA | OID_SHA256_RSA | OID_SHA384_RSA | OID_SHA512_RSA
)
}
fn rsa_with_hash_oid(digest: DigestAlgorithm) -> ObjectIdentifier {
match digest {
DigestAlgorithm::Sha1 => OID_SHA1_RSA,
DigestAlgorithm::Sha256 => OID_SHA256_RSA,
DigestAlgorithm::Sha384 => OID_SHA384_RSA,
DigestAlgorithm::Sha512 => OID_SHA512_RSA,
}
}
#[cfg(test)]
mod tests {
use super::*;
const CMS_LEAF_DIRECT: &[u8] = include_bytes!("../pki/testdata/cms_leaf_direct.der");
const CMS_CHAIN3: &[u8] = include_bytes!("../pki/testdata/cms_chain3.der");
const CMS_NONCONFORMING_DN: &[u8] = include_bytes!("../pki/testdata/cms_nonconforming_dn.der");
const CMS_DER_SIGNED_ATTRS: &[u8] = include_bytes!("../pki/testdata/cms_der_signed_attrs.der");
#[test]
fn valid_signature_verifies() {
assert_eq!(
verify_signer_signature(CMS_LEAF_DIRECT, None),
SignatureStatus::Valid
);
assert_eq!(
verify_signer_signature(CMS_CHAIN3, None),
SignatureStatus::Valid
);
}
#[test]
fn signature_over_canonically_reencoded_attributes_verifies() {
assert_eq!(
verify_signer_signature(CMS_DER_SIGNED_ATTRS, None),
SignatureStatus::Valid
);
}
#[test]
fn cms_without_signed_attributes_verifies_over_the_content() {
const CMS_NO_ATTRS: &[u8] = include_bytes!("../pki/testdata/cms_no_signed_attrs.der");
assert_eq!(
verify_signer_signature(CMS_NO_ATTRS, Some(b"test data")),
SignatureStatus::Valid
);
assert_eq!(
verify_signer_signature(CMS_NO_ATTRS, Some(b"tampered data")),
SignatureStatus::Invalid,
"the signature must bind the exact content"
);
assert!(matches!(
verify_signer_signature(CMS_NO_ATTRS, None),
SignatureStatus::Unverifiable(_)
));
assert_eq!(has_signed_attributes(CMS_NO_ATTRS), Some(false));
assert_eq!(has_signed_attributes(CMS_LEAF_DIRECT), Some(true));
assert_eq!(has_signed_attributes(&[0x30, 0x03, 0xAB, 0xAB, 0xAB]), None);
}
#[test]
fn attached_content_never_stands_in_for_the_callers_bytes() {
const CMS_ATTACHED: &[u8] = include_bytes!("../pki/testdata/cms_no_attrs_attached.der");
const PAYLOAD: &[u8] = b"payload the signer actually signed";
assert!(
matches!(
verify_signer_signature(CMS_ATTACHED, Some(b"an unrelated document")),
SignatureStatus::Unverifiable(_)
),
"embedded content must not vouch for bytes the caller supplied"
);
assert_eq!(
verify_signer_signature(CMS_ATTACHED, Some(PAYLOAD)),
SignatureStatus::Valid
);
assert_eq!(
verify_signer_signature(CMS_ATTACHED, None),
SignatureStatus::Valid,
"verifying the attached CMS on its own terms still works"
);
assert_eq!(has_signed_attributes(CMS_ATTACHED), Some(false));
}
#[test]
fn nonconforming_subject_dn_still_verifies() {
assert_eq!(
verify_signer_signature(CMS_NONCONFORMING_DN, None),
SignatureStatus::Valid
);
}
#[test]
fn tampered_signature_is_invalid() {
let mut tampered = CMS_LEAF_DIRECT.to_vec();
let last = tampered.len() - 5;
tampered[last] ^= 0xFF;
assert_eq!(
verify_signer_signature(&tampered, None),
SignatureStatus::Invalid,
"a tampered signature must be Invalid"
);
}
#[test]
fn garbage_is_unverifiable_not_valid() {
assert!(matches!(
verify_signer_signature(b"not a cms blob at all", None),
SignatureStatus::Unverifiable(_)
));
}
#[test]
fn status_helpers() {
assert!(SignatureStatus::Valid.is_valid());
assert!(!SignatureStatus::Invalid.is_valid());
assert!(!SignatureStatus::Unverifiable("x").is_valid());
assert!(SignatureStatus::Invalid.describe().contains("INVALID"));
}
fn tlv(tag: u8, content: &[u8]) -> Vec<u8> {
let mut out = vec![tag];
let len = content.len();
if len < 0x80 {
out.push(u8::try_from(len).unwrap());
} else if len < 0x100 {
out.push(0x81);
out.push(u8::try_from(len).unwrap());
} else {
out.push(0x82);
out.push(u8::try_from(len >> 8).unwrap());
out.push(u8::try_from(len & 0xff).unwrap());
}
out.extend_from_slice(content);
out
}
fn v1_signing_cert(cert_hash: &[u8]) -> Vec<u8> {
let ess_cert_id = tlv(0x30, &tlv(0x04, cert_hash));
tlv(0x30, &tlv(0x30, &ess_cert_id)) }
fn v2_signing_cert(cert_hash: &[u8], alg_id: Option<&[u8]>) -> Vec<u8> {
let mut ess_cert_id_body = Vec::new();
if let Some(alg) = alg_id {
ess_cert_id_body.extend_from_slice(alg);
}
ess_cert_id_body.extend_from_slice(&tlv(0x04, cert_hash));
tlv(0x30, &tlv(0x30, &tlv(0x30, &ess_cert_id_body)))
}
#[test]
fn ess_v1_parses_real_ekeng_structure() {
let real = [
0x30, 0x1A, 0x30, 0x18, 0x30, 0x16, 0x04, 0x14, 0x36, 0x36, 0x5A, 0x81, 0x69, 0x93,
0x10, 0x6B, 0xCE, 0x0F, 0xCD, 0x46, 0xB9, 0x6B, 0xE1, 0x75, 0xB6, 0xC2, 0x3E, 0x64,
];
let (algo, hash) = ess_cert_hash(&real, false).expect("v1 ESSCertID parses");
assert_eq!(algo, DigestAlgorithm::Sha1);
assert_eq!(hash, real[8..]);
}
#[test]
fn ess_v1_recovers_certificate_hash() {
let cert = Certificate::from_der(include_bytes!("../pki/testdata/leaf.der")).unwrap();
let want = DigestAlgorithm::Sha1.hash(&cert.to_der().unwrap());
let (algo, got) = ess_cert_hash(&v1_signing_cert(&want), false).unwrap();
assert_eq!(algo, DigestAlgorithm::Sha1);
assert_eq!(got, want);
let mut wrong = want.clone();
wrong[0] ^= 0x01;
assert_ne!(
ess_cert_hash(&v1_signing_cert(&wrong), false).unwrap().1,
want
);
}
#[test]
fn ess_v2_default_algorithm_is_sha256() {
let hash = [0xABu8; 32];
let (algo, got) = ess_cert_hash(&v2_signing_cert(&hash, None), true).unwrap();
assert_eq!(algo, DigestAlgorithm::Sha256);
assert_eq!(got, hash);
}
#[test]
fn ess_v2_explicit_algorithm_is_honored() {
let sha384_alg = tlv(
0x30,
&[
0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02,
],
);
let hash = [0xCDu8; 48];
let (algo, got) = ess_cert_hash(&v2_signing_cert(&hash, Some(&sha384_alg)), true).unwrap();
assert_eq!(algo, DigestAlgorithm::Sha384);
assert_eq!(got, hash);
}
#[test]
fn ess_rejects_malformed() {
assert!(ess_cert_hash(b"", false).is_none());
assert!(ess_cert_hash(&[0x30, 0x00], false).is_none()); assert!(ess_cert_hash(&[0x04, 0x01, 0xAA], false).is_none()); }
}