M9: check the bundle's SignedData shape and refuse trailing bytes

A .p7b must be SignedData with version, digestAlgorithms,
encapContentInfo, optional certificates and CRLs, then signerInfos
(RFC 5652 §5.1), and nothing may follow it (CodeRabbit on #3561).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ffishman
2026-10-20 16:46:26 +00:00
co-authored by Claude Opus 5.5
parent ddff6b64fe
commit f96e440a47
2 changed files with 27 additions and 6 deletions
+14 -6
View File
@@ -74,19 +74,27 @@ fn carried_certificates(set: &Tlv) -> Result<Vec<Certificate>, SignError> {
/// authorities hand out a chain or a set of CA certificates. Signers and CRLs, if present, are
/// ignored; only the certificates are read. DER or BER.
pub fn certificate_bundle(bytes: &[u8]) -> Result<Vec<Certificate>, SignError> {
let (ci, _) = Tlv::parse_ber(bytes)?;
let (ci, rest) = Tlv::parse_ber(bytes)?;
// A trust file is the bundle alone: bytes after it mean a damaged or spliced file.
if !rest.is_empty() {
return Err(bad("data after the bundle"));
}
let ci = ci.expect(tag::SEQUENCE, "ContentInfo")?.children()?;
let [ct, content] = ci.as_slice() else { return Err(bad("ContentInfo")) };
if ct.oid()? != SIGNED_DATA {
return Err(bad("not SignedData"));
}
let sd = content.expect(tag::ctx(0), "content")?.inner()?.children()?;
// version, digestAlgorithms, encapContentInfo, then the optional [0] certificates.
// version, digestAlgorithms, encapContentInfo, the optional [0] certificates and [1] CRLs,
// then signerInfos (RFC 5652 §5.1).
let [version, digests, encap, middle @ .., signers] = sd.as_slice() else { return Err(bad("SignedData")) };
let shape = [(version, tag::INTEGER), (digests, tag::SET), (encap, tag::SEQUENCE), (signers, tag::SET)];
if shape.iter().any(|(t, want)| t.tag != *want) || middle.iter().any(|t| t.tag != tag::ctx(0) && t.tag != tag::ctx(1)) {
return Err(bad("SignedData"));
}
let mut certificates = Vec::new();
for t in sd.iter().skip(3) {
if t.tag == tag::ctx(0) {
certificates.extend(carried_certificates(t)?);
}
for t in middle.iter().filter(|t| t.tag == tag::ctx(0)) {
certificates.extend(carried_certificates(t)?);
}
if certificates.is_empty() {
return Err(bad("the bundle holds no certificate"));
+13
View File
@@ -295,6 +295,19 @@ fn certificates_load_from_pkcs7_bundles() {
assert!(load_certificates(&p7b(&[])).is_err());
let not_signed_data = pdfcraft_sign::der::seq(&[&pdfcraft_sign::der::oid(cms::DATA), &pdfcraft_sign::der::explicit(0, &[])]);
assert!(load_certificates(&not_signed_data).is_err());
// A SignedData out of shape: no signerInfos SET, or a version that isn't an INTEGER.
{
use pdfcraft_sign::der::{explicit, int, oid, seq, set_of, tlv};
let wrap = |sd: &[&[u8]]| seq(&[&oid(cms::SIGNED_DATA), &explicit(0, &seq(sd))]);
let (certs, encap) = (tlv(0xA0, &[leaf.raw.as_slice(), &root.raw].concat()), seq(&[&oid(cms::DATA)]));
assert_eq!(load_certificates(&wrap(&[&int(1), &set_of(&[]), &encap, &certs, &set_of(&[])])).unwrap().len(), 2);
assert!(load_certificates(&wrap(&[&int(1), &set_of(&[]), &encap, &certs])).is_err());
assert!(load_certificates(&wrap(&[&set_of(&[]), &set_of(&[]), &encap, &certs, &set_of(&[])])).is_err());
}
let mut trailing = bundle.clone();
trailing.extend_from_slice(&[0x05, 0x00]);
assert!(load_certificates(&trailing).is_err());
assert!(cms::certificate_bundle(&bundle).is_ok());
// Truncated anywhere: an error, never a panic.
for n in 0..bundle.len() {
let _ = load_certificates(&bundle[..n]);