Skip to main content

zpdf_document/
signature.rs

1//! Digital signature fields (ISO 32000-1 §12.8, ISO 32000-2 + PAdES/ETSI).
2//!
3//! An interactive-form field of type `/Sig` carries a **signature dictionary**
4//! (`/V`) describing a digital signature over the file: the handler that
5//! produced it (`/Filter`), the encoding of the signed data (`/SubFilter`), the
6//! human-declared signer / reason / location, and — the two entries that make
7//! the signature verifiable — a `/ByteRange` naming which spans of the file are
8//! signed and a `/Contents` string holding the CMS (PKCS #7) signature blob.
9//!
10//! This module reads that dictionary into a data model and performs two
11//! independent checks:
12//!
13//! 1. **Byte-range integrity** ([`DigestStatus`]): it recomputes the digest of
14//!    the signed byte range and compares it against the `messageDigest`
15//!    attribute embedded in the CMS structure. A match proves the covered bytes
16//!    are exactly what the signature committed to — the document was **not
17//!    altered inside the signed range** after signing.
18//!
19//! 2. **Cryptographic signature** ([`CryptoStatus`]): it verifies the signer's
20//!    RSA (PKCS #1 v1.5) or ECDSA (NIST P-256 / P-384) signature over the
21//!    signed attributes, using the public key of the first certificate carried
22//!    in the CMS blob. A [`CryptoStatus::Valid`] verdict proves the signed
23//!    attributes (which bind the `messageDigest`) were produced by the holder of
24//!    that certificate's private key.
25//!
26//! What this module deliberately does **not** do: validate the certificate
27//! chain to a trust anchor, check revocation (CRL/OCSP), or honour signing-time
28//! validity. Those require a trust store and network access, neither of which
29//! lives in this pure-Rust, dependency-light crate. So even a fully
30//! [`DigestStatus::Verified`] + [`CryptoStatus::Valid`] signature means "the
31//! signed bytes are intact and were signed by the private key matching the
32//! embedded certificate" — **not** "the signer is a trusted, non-revoked
33//! identity." Callers presenting this to users must not overstate it. See
34//! [`Signature::is_cryptographically_valid`].
35//!
36//! Everything here is bounded and best-effort: a malformed field tree, an
37//! out-of-range `/ByteRange`, an unsupported algorithm, or a corrupt CMS blob
38//! yields `None` / an [`DigestStatus::Unsupported`] / [`CryptoStatus::Unsupported`]
39//! verdict, never a panic.
40
41use std::collections::HashSet;
42
43use sha1::Sha1;
44use sha2::{Digest, Sha256, Sha384, Sha512};
45use zpdf_core::{ObjectId, PdfDict, PdfObject};
46use zpdf_parser::PdfFile;
47
48use crate::forms::pdf_string_to_unicode;
49
50/// Bounds on the field-tree walk (mirrors [`crate::forms`]).
51const MAX_FIELD_DEPTH: usize = 50;
52const MAX_SIG_FIELDS: usize = 4_096;
53/// Cap on the CMS blob we attempt to DER-parse. Real signatures — even with a
54/// full certificate chain and timestamp token — are comfortably under this;
55/// the cap bounds work against an adversarial `/Contents`.
56const MAX_CMS_BYTES: usize = 4 * 1024 * 1024;
57
58/// A digital signature attached to a `/Sig` form field.
59#[derive(Debug, Clone)]
60pub struct Signature {
61    /// The fully-qualified name of the signature field (`/T` chain).
62    pub field_name: String,
63    /// `/Filter` — the security handler that produced the signature
64    /// (conventionally `Adobe.PPKLite`).
65    pub filter: Option<String>,
66    /// `/SubFilter` — the encoding of the signed data, e.g.
67    /// `adbe.pkcs7.detached`, `adbe.pkcs7.sha1`, or `ETSI.CAdES.detached`
68    /// (PAdES).
69    pub sub_filter: Option<String>,
70    /// `/Name` — the signer's name as declared in the dictionary (not
71    /// cryptographically bound; see [`Signature::signer_common_name`]).
72    pub name: Option<String>,
73    /// `/M` — the signing time, as the raw PDF date string.
74    pub signing_time: Option<String>,
75    /// `/Location`.
76    pub location: Option<String>,
77    /// `/Reason`.
78    pub reason: Option<String>,
79    /// `/ContactInfo`.
80    pub contact_info: Option<String>,
81    /// The signed spans of the file (`/ByteRange`) and what they cover.
82    pub coverage: ByteRangeCoverage,
83    /// Result of comparing the recomputed digest of the covered bytes to the
84    /// digest embedded in the CMS blob.
85    pub digest: DigestStatus,
86    /// Result of verifying the signer's public-key signature over the CMS signed
87    /// attributes, using the first embedded certificate's public key.
88    pub crypto: CryptoStatus,
89    /// Human name of the digest algorithm named by the CMS `SignerInfo`
90    /// (`SHA-1`, `SHA-256`, …), when it could be identified.
91    pub digest_algorithm: Option<String>,
92    /// Human name of the signature (public-key) algorithm identified from the
93    /// CMS `SignerInfo` and the signer certificate (`RSA`, `ECDSA (P-256)`, …),
94    /// when it could be identified.
95    pub signature_algorithm: Option<String>,
96    /// The Common Name (`CN`) of the first certificate in the CMS blob —
97    /// typically, but not guaranteed to be, the signer's leaf certificate.
98    /// Best-effort; `None` when no certificate / CN could be extracted.
99    pub signer_common_name: Option<String>,
100}
101
102impl Signature {
103    /// True only when **both** checks pass: the signed bytes are intact
104    /// ([`DigestStatus::Verified`]) **and** the signer's signature over the
105    /// signed attributes verifies against the embedded certificate's public key
106    /// ([`CryptoStatus::Valid`]).
107    ///
108    /// This still does **not** establish trust: the certificate is not validated
109    /// against any anchor, nor checked for revocation. A `true` here means
110    /// "cryptographically sound, from the private key matching the embedded
111    /// certificate" — the certificate's *trustworthiness* is a separate,
112    /// out-of-scope question.
113    pub fn is_cryptographically_valid(&self) -> bool {
114        self.digest == DigestStatus::Verified && self.crypto == CryptoStatus::Valid
115    }
116}
117
118/// How a signature's `/ByteRange` covers the file.
119#[derive(Debug, Clone)]
120pub struct ByteRangeCoverage {
121    /// The `(offset, length)` spans of the file that are signed, in order.
122    pub ranges: Vec<(usize, usize)>,
123    /// True when the ranges start at byte 0 and the last range ends exactly at
124    /// end-of-file (the single gap being the `/Contents` placeholder) — i.e. the
125    /// signature covers the whole document.
126    pub covers_whole_document: bool,
127    /// Bytes present after the last signed span. Non-zero means the file was
128    /// extended after this signature was applied — a later incremental update
129    /// (possibly another signature, possibly a modification the signature does
130    /// not cover).
131    pub bytes_after_signature: usize,
132}
133
134/// Verdict of the byte-range digest check.
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum DigestStatus {
137    /// The recomputed digest of the signed byte range matches the
138    /// `messageDigest` embedded in the CMS: the covered bytes are intact.
139    Verified,
140    /// The digests differ: the covered bytes were altered after signing.
141    Mismatch,
142    /// No comparable digest could be obtained — an unsupported `/SubFilter`,
143    /// an unknown digest algorithm, an out-of-range `/ByteRange`, or a CMS blob
144    /// without an extractable `messageDigest`. The other fields are still valid.
145    Unsupported,
146}
147
148impl DigestStatus {
149    pub fn as_str(self) -> &'static str {
150        match self {
151            DigestStatus::Verified => "verified",
152            DigestStatus::Mismatch => "mismatch",
153            DigestStatus::Unsupported => "unsupported",
154        }
155    }
156}
157
158/// Verdict of the public-key signature check over the CMS signed attributes.
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum CryptoStatus {
161    /// The signer's signature over the signed attributes verifies against the
162    /// public key of the embedded (first) certificate.
163    Valid,
164    /// A signature and key were present and of a supported algorithm, but the
165    /// signature does **not** verify — a forged, corrupt, or wrong-key blob.
166    Invalid,
167    /// The signature could not be checked: an unsupported `/SubFilter`, no
168    /// signed attributes, an unsupported signature/key algorithm (e.g. RSA-PSS,
169    /// DSA, or a curve other than P-256/P-384), or an unparseable certificate /
170    /// public key. The [`DigestStatus`] check may still be meaningful.
171    Unsupported,
172}
173
174impl CryptoStatus {
175    pub fn as_str(self) -> &'static str {
176        match self {
177            CryptoStatus::Valid => "valid",
178            CryptoStatus::Invalid => "invalid",
179            CryptoStatus::Unsupported => "unsupported",
180        }
181    }
182}
183
184/// Parse all digital signatures in the document's AcroForm. Returns an empty
185/// vector when the document has no signature fields (the common case). Read-only
186/// and bounded; safe to call on adversarial input.
187pub fn parse_signatures(file: &PdfFile) -> Vec<Signature> {
188    let mut out = Vec::new();
189    let Some(fields) = acroform_fields(file) else {
190        return out;
191    };
192
193    let mut visited = HashSet::new();
194    for obj in &fields {
195        if let PdfObject::Ref(r) = obj {
196            walk(file, *r, "", None, 0, &mut visited, &mut out);
197        }
198    }
199    out
200}
201
202/// The `/Root /AcroForm /Fields` array, or `None`.
203fn acroform_fields(file: &PdfFile) -> Option<Vec<PdfObject>> {
204    let root_ref = file.trailer.get_ref("Root").ok()?;
205    let root = file.resolve(root_ref).ok()?;
206    let root = root.as_dict().ok()?;
207    let af = deref(file, root.get("AcroForm")?);
208    let af = af.as_dict().ok()?;
209    match deref(file, af.get("Fields")?) {
210        PdfObject::Array(a) => Some(a),
211        _ => None,
212    }
213}
214
215/// Walk the field tree, emitting a [`Signature`] for every terminal `/Sig` field
216/// whose `/V` resolves to a signature dictionary. `/FT` is inheritable, so it is
217/// threaded down from ancestors.
218fn walk(
219    file: &PdfFile,
220    id: ObjectId,
221    parent_name: &str,
222    inherited_ft: Option<&str>,
223    depth: usize,
224    visited: &mut HashSet<ObjectId>,
225    out: &mut Vec<Signature>,
226) {
227    if depth > MAX_FIELD_DEPTH || out.len() >= MAX_SIG_FIELDS || !visited.insert(id) {
228        return;
229    }
230    let Ok(obj) = file.resolve(id) else { return };
231    let Ok(dict) = obj.as_dict() else { return };
232
233    let partial = dict
234        .get("T")
235        .and_then(|o| text_string(file, o))
236        .unwrap_or_default();
237    let name = if partial.is_empty() {
238        parent_name.to_string()
239    } else if parent_name.is_empty() {
240        partial
241    } else {
242        format!("{parent_name}.{partial}")
243    };
244
245    let ft = dict
246        .get_name("FT")
247        .ok()
248        .map(String::from)
249        .or_else(|| inherited_ft.map(String::from));
250
251    // Interior node: recurse into child fields (those carrying their own /T).
252    let kids = match deref(file, dict.get("Kids").unwrap_or(&PdfObject::Null)) {
253        PdfObject::Array(a) => a,
254        _ => Vec::new(),
255    };
256    let mut has_child_field = false;
257    for kid in &kids {
258        if let PdfObject::Ref(r) = kid {
259            let has_t = file
260                .resolve(*r)
261                .ok()
262                .and_then(|o| o.as_dict().ok().map(|d| d.get("T").is_some()))
263                .unwrap_or(false);
264            if has_t {
265                has_child_field = true;
266                walk(file, *r, &name, ft.as_deref(), depth + 1, visited, out);
267            }
268        }
269    }
270    if has_child_field {
271        return;
272    }
273
274    // Terminal field: emit a signature when it is a /Sig field with a /V dict.
275    if ft.as_deref() != Some("Sig") {
276        return;
277    }
278    let Some(sig_dict) = deref(file, dict.get("V").unwrap_or(&PdfObject::Null))
279        .as_dict()
280        .ok()
281        .cloned()
282    else {
283        return;
284    };
285    out.push(build_signature(file, name, &sig_dict));
286}
287
288fn build_signature(file: &PdfFile, field_name: String, sig: &PdfDict) -> Signature {
289    let sub_filter = sig.get_name("SubFilter").ok().map(String::from);
290    let contents = match deref(file, sig.get("Contents").unwrap_or(&PdfObject::Null)) {
291        PdfObject::String(s) => Some(s.as_bytes().to_vec()),
292        _ => None,
293    };
294
295    let coverage = parse_byte_range(file, sig, file.data().len());
296    let outcome = verify(file, &coverage, contents.as_deref(), sub_filter.as_deref());
297
298    Signature {
299        field_name,
300        filter: sig.get_name("Filter").ok().map(String::from),
301        sub_filter,
302        name: sig.get("Name").and_then(|o| text_string(file, o)),
303        signing_time: sig.get("M").and_then(|o| text_string(file, o)),
304        location: sig.get("Location").and_then(|o| text_string(file, o)),
305        reason: sig.get("Reason").and_then(|o| text_string(file, o)),
306        contact_info: sig.get("ContactInfo").and_then(|o| text_string(file, o)),
307        coverage,
308        digest: outcome.digest,
309        crypto: outcome.crypto,
310        digest_algorithm: outcome.digest_algorithm,
311        signature_algorithm: outcome.signature_algorithm,
312        signer_common_name: outcome.signer_common_name,
313    }
314}
315
316/// The full result of verifying one signature's CMS blob.
317struct VerifyOutcome {
318    digest: DigestStatus,
319    crypto: CryptoStatus,
320    digest_algorithm: Option<String>,
321    signature_algorithm: Option<String>,
322    signer_common_name: Option<String>,
323}
324
325/// Parse `/ByteRange` into `(offset, length)` spans and classify coverage.
326fn parse_byte_range(file: &PdfFile, sig: &PdfDict, file_len: usize) -> ByteRangeCoverage {
327    let mut ranges = Vec::new();
328    if let PdfObject::Array(arr) = deref(file, sig.get("ByteRange").unwrap_or(&PdfObject::Null)) {
329        let nums: Vec<i64> = arr
330            .iter()
331            .filter_map(|o| match deref(file, o) {
332                PdfObject::Integer(n) => Some(n),
333                PdfObject::Real(r) if r.is_finite() => Some(r as i64),
334                _ => None,
335            })
336            .collect();
337        for pair in nums.chunks_exact(2) {
338            if let (Ok(off), Ok(len)) = (usize::try_from(pair[0]), usize::try_from(pair[1])) {
339                ranges.push((off, len));
340            }
341        }
342    }
343
344    // Whole-document coverage: first span at 0, last span ends at EOF.
345    let covers_whole_document = ranges.first().zip(ranges.last()).is_some_and(
346        |(&(first_off, _), &(last_off, last_len))| {
347            first_off == 0 && last_off.saturating_add(last_len) == file_len
348        },
349    );
350    let end = ranges
351        .last()
352        .map(|&(off, len)| off.saturating_add(len))
353        .unwrap_or(0);
354    let bytes_after_signature = file_len.saturating_sub(end);
355
356    ByteRangeCoverage {
357        ranges,
358        covers_whole_document,
359        bytes_after_signature,
360    }
361}
362
363/// Recompute the covered-bytes digest, compare it to the CMS `messageDigest`,
364/// and verify the signer's public-key signature over the signed attributes.
365fn verify(
366    file: &PdfFile,
367    coverage: &ByteRangeCoverage,
368    contents: Option<&[u8]>,
369    sub_filter: Option<&str>,
370) -> VerifyOutcome {
371    let unsupported = VerifyOutcome {
372        digest: DigestStatus::Unsupported,
373        crypto: CryptoStatus::Unsupported,
374        digest_algorithm: None,
375        signature_algorithm: None,
376        signer_common_name: None,
377    };
378
379    let Some(cms) = contents.filter(|c| !c.is_empty() && c.len() <= MAX_CMS_BYTES) else {
380        return unsupported;
381    };
382
383    let Some(parsed) = cms::parse(cms) else {
384        return unsupported;
385    };
386    let digest_algorithm = parsed.digest_alg.map(|a| a.name().to_string());
387    let signature_algorithm = signature_alg_name(&parsed);
388    let signer_common_name = parsed.signer_cn.clone();
389
390    // The checks apply to the detached CMS SubFilters (PKCS#7 / CAdES), where the
391    // digest is taken over the byte range and stored as the messageDigest signed
392    // attribute. Other encodings (e.g. adbe.x509.rsa_sha1) are reported without a
393    // verdict.
394    let is_detached = matches!(
395        sub_filter,
396        Some("adbe.pkcs7.detached") | Some("ETSI.CAdES.detached")
397    );
398    if !is_detached {
399        return VerifyOutcome {
400            digest: DigestStatus::Unsupported,
401            crypto: CryptoStatus::Unsupported,
402            digest_algorithm,
403            signature_algorithm,
404            signer_common_name,
405        };
406    }
407
408    // (1) Byte-range digest vs the messageDigest signed attribute.
409    let digest = match (parsed.digest_alg, parsed.message_digest.as_deref()) {
410        (Some(alg), Some(embedded)) => match gather_ranges(file.data(), &coverage.ranges) {
411            Some(spans) => {
412                if alg.hash(&spans) == embedded {
413                    DigestStatus::Verified
414                } else {
415                    DigestStatus::Mismatch
416                }
417            }
418            None => DigestStatus::Unsupported, // /ByteRange out of file bounds
419        },
420        _ => DigestStatus::Unsupported,
421    };
422
423    // (2) Public-key signature over the signed attributes.
424    let crypto = verify_crypto(&parsed);
425
426    VerifyOutcome {
427        digest,
428        crypto,
429        digest_algorithm,
430        signature_algorithm,
431        signer_common_name,
432    }
433}
434
435/// Verify the signer's RSA/ECDSA signature over the CMS signed attributes using
436/// the embedded certificate's public key. Returns [`CryptoStatus::Unsupported`]
437/// whenever a required piece is missing or the algorithm is not one we handle.
438fn verify_crypto(p: &cms::Cms) -> CryptoStatus {
439    let (Some(attrs), Some(sig), Some(key), Some(dalg), Some(salg)) = (
440        p.signed_attrs_der.as_deref(),
441        p.signature.as_deref(),
442        p.signer_key.as_ref(),
443        p.digest_alg,
444        p.sig_alg,
445    ) else {
446        return CryptoStatus::Unsupported;
447    };
448
449    // The signature is computed over the DER encoding of the signed attributes,
450    // hashed with the SignerInfo digest algorithm.
451    let hashed = dalg.hash(attrs);
452
453    let verified = match (salg, key.alg) {
454        (cms::SigAlg::Rsa, cms::KeyAlg::Rsa) => pk::rsa_verify(dalg, &key.key, &hashed, sig),
455        (cms::SigAlg::Ecdsa, cms::KeyAlg::EcP256) => pk::ecdsa_p256_verify(&key.key, &hashed, sig),
456        (cms::SigAlg::Ecdsa, cms::KeyAlg::EcP384) => pk::ecdsa_p384_verify(&key.key, &hashed, sig),
457        // RSA-PSS, DSA, mismatched sig/key algorithms, or unsupported curves.
458        _ => return CryptoStatus::Unsupported,
459    };
460
461    match verified {
462        Some(true) => CryptoStatus::Valid,
463        Some(false) => CryptoStatus::Invalid,
464        None => CryptoStatus::Unsupported, // key/signature failed to parse
465    }
466}
467
468/// A display name combining the signer's public-key algorithm with the curve,
469/// e.g. `RSA`, `ECDSA (P-256)`, `RSA-PSS`.
470fn signature_alg_name(p: &cms::Cms) -> Option<String> {
471    let salg = p.sig_alg?;
472    Some(match salg {
473        cms::SigAlg::Rsa => "RSA".to_string(),
474        cms::SigAlg::RsaPss => "RSA-PSS".to_string(),
475        cms::SigAlg::Ecdsa => match p.signer_key.as_ref().map(|k| k.alg) {
476            Some(cms::KeyAlg::EcP256) => "ECDSA (P-256)".to_string(),
477            Some(cms::KeyAlg::EcP384) => "ECDSA (P-384)".to_string(),
478            _ => "ECDSA".to_string(),
479        },
480    })
481}
482
483/// Collect the covered byte spans into a single buffer, or `None` if any span
484/// falls outside the file (a malformed or tampered `/ByteRange`).
485fn gather_ranges(data: &[u8], ranges: &[(usize, usize)]) -> Option<Vec<u8>> {
486    if ranges.is_empty() {
487        return None;
488    }
489    let mut buf = Vec::new();
490    for &(off, len) in ranges {
491        let end = off.checked_add(len)?;
492        let slice = data.get(off..end)?;
493        buf.extend_from_slice(slice);
494    }
495    Some(buf)
496}
497
498// ---------------------------------------------------------------------------
499// Digest algorithms
500// ---------------------------------------------------------------------------
501
502#[derive(Debug, Clone, Copy, PartialEq, Eq)]
503enum DigestAlg {
504    Sha1,
505    Sha256,
506    Sha384,
507    Sha512,
508}
509
510impl DigestAlg {
511    fn name(self) -> &'static str {
512        match self {
513            DigestAlg::Sha1 => "SHA-1",
514            DigestAlg::Sha256 => "SHA-256",
515            DigestAlg::Sha384 => "SHA-384",
516            DigestAlg::Sha512 => "SHA-512",
517        }
518    }
519
520    fn hash(self, data: &[u8]) -> Vec<u8> {
521        match self {
522            DigestAlg::Sha1 => Sha1::digest(data).to_vec(),
523            DigestAlg::Sha256 => Sha256::digest(data).to_vec(),
524            DigestAlg::Sha384 => Sha384::digest(data).to_vec(),
525            DigestAlg::Sha512 => Sha512::digest(data).to_vec(),
526        }
527    }
528
529    /// Map a digest-algorithm OID (the raw content bytes of the `06` TLV).
530    fn from_oid(oid: &[u8]) -> Option<DigestAlg> {
531        match oid {
532            [0x2b, 0x0e, 0x03, 0x02, 0x1a] => Some(DigestAlg::Sha1),
533            [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01] => Some(DigestAlg::Sha256),
534            [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02] => Some(DigestAlg::Sha384),
535            [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03] => Some(DigestAlg::Sha512),
536            _ => None,
537        }
538    }
539}
540
541// ---------------------------------------------------------------------------
542// Minimal, bounded DER / CMS reader
543// ---------------------------------------------------------------------------
544//
545// A hand-written TLV walker: enough of RFC 5652 (CMS SignedData) and X.509 to
546// pull the digest algorithm, the messageDigest signed attribute, and the first
547// certificate's subject CN. It never recurses without a depth bound, never
548// indexes past the buffer, and returns `None` on any structural surprise.
549
550mod cms {
551    use super::DigestAlg;
552
553    /// The pieces we extract from a CMS `SignedData` blob.
554    pub(super) struct Cms {
555        pub(super) digest_alg: Option<DigestAlg>,
556        pub(super) message_digest: Option<Vec<u8>>,
557        pub(super) signer_cn: Option<String>,
558        /// The signed attributes, DER-encoded with the outer `[0] IMPLICIT` tag
559        /// rewritten to `SET OF` (0x31) — exactly the bytes the signature is
560        /// computed over (RFC 5652 §5.4). `None` when the SignerInfo carries no
561        /// signed attributes.
562        pub(super) signed_attrs_der: Option<Vec<u8>>,
563        /// The `SignerInfo` signature value (the `signature` OCTET STRING).
564        pub(super) signature: Option<Vec<u8>>,
565        /// The signature (public-key) algorithm from the `SignerInfo`.
566        pub(super) sig_alg: Option<SigAlg>,
567        /// The public key of the first embedded certificate.
568        pub(super) signer_key: Option<PublicKeyInfo>,
569    }
570
571    /// The public-key algorithm named by the `SignerInfo` `signatureAlgorithm`.
572    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
573    pub(super) enum SigAlg {
574        /// RSA PKCS #1 v1.5 (`rsaEncryption` or `sha*WithRSAEncryption`).
575        Rsa,
576        /// RSA-PSS (`id-RSASSA-PSS`) — recognised but not verified.
577        RsaPss,
578        /// ECDSA (`ecdsa-with-SHA*`).
579        Ecdsa,
580    }
581
582    /// A signer certificate's public key: its algorithm and raw key material.
583    pub(super) struct PublicKeyInfo {
584        pub(super) alg: KeyAlg,
585        /// For RSA: the `RSAPublicKey` DER (`SEQUENCE { modulus, exponent }`).
586        /// For ECDSA: the SEC1-encoded public point.
587        pub(super) key: Vec<u8>,
588    }
589
590    /// The public-key algorithm of a certificate's `SubjectPublicKeyInfo`.
591    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
592    pub(super) enum KeyAlg {
593        Rsa,
594        EcP256,
595        EcP384,
596    }
597
598    // DER tags we care about.
599    const SEQUENCE: u8 = 0x30;
600    const SET: u8 = 0x31;
601    const OID: u8 = 0x06;
602    const OCTET_STRING: u8 = 0x04;
603    const BIT_STRING: u8 = 0x03;
604    const CONTEXT_0: u8 = 0xA0; // [0] constructed / EXPLICIT
605
606    // OIDs (raw content bytes).
607    const OID_SIGNED_DATA: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02];
608    const OID_MESSAGE_DIGEST: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x04];
609    const OID_CN: &[u8] = &[0x55, 0x04, 0x03];
610
611    // Public-key / signature algorithm OIDs.
612    // RSA family: 1.2.840.113549.1.1.{1=rsaEncryption, 10=PSS, 4/5/11/12/13=sha*WithRSA}.
613    const OID_RSA_PREFIX: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01];
614    const OID_RSA_PSS: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0a];
615    // rsaEncryption 1.2.840.113549.1.1.1 (SPKI key algorithm).
616    const OID_RSA_PUBLIC_KEY: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01];
617    // EC: id-ecPublicKey 1.2.840.10045.2.1; ecdsa-with-* 1.2.840.10045.4.*.
618    const OID_EC_PUBLIC_KEY: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01];
619    const OID_ECDSA_PREFIX: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04];
620    // Named curves.
621    const OID_CURVE_P256: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07];
622    const OID_CURVE_P384: &[u8] = &[0x2b, 0x81, 0x04, 0x00, 0x22];
623
624    /// Read one DER TLV from the front of `buf`: returns `(tag, content, rest)`.
625    /// Rejects the indefinite-length form and lengths that run past `buf`.
626    fn tlv(buf: &[u8]) -> Option<(u8, &[u8], &[u8])> {
627        if buf.len() < 2 {
628            return None;
629        }
630        let tag = buf[0];
631        let first = buf[1];
632        let (len, header) = if first < 0x80 {
633            (first as usize, 2)
634        } else {
635            let n = (first & 0x7f) as usize;
636            if n == 0 || n > 4 || buf.len() < 2 + n {
637                return None; // indefinite length, or absurdly large length field
638            }
639            let mut len = 0usize;
640            for &b in &buf[2..2 + n] {
641                len = (len << 8) | b as usize;
642            }
643            (len, 2 + n)
644        };
645        let end = header.checked_add(len)?;
646        if end > buf.len() {
647            return None;
648        }
649        Some((tag, &buf[header..end], &buf[end..]))
650    }
651
652    /// Collect the TLVs directly contained in `content`, up to `max` items.
653    fn children(content: &[u8], max: usize) -> Vec<(u8, &[u8])> {
654        let mut out = Vec::new();
655        let mut rest = content;
656        while !rest.is_empty() && out.len() < max {
657            let Some((tag, body, next)) = tlv(rest) else {
658                break;
659            };
660            out.push((tag, body));
661            rest = next;
662        }
663        out
664    }
665
666    /// Like [`children`], but each entry also carries the element's **full** raw
667    /// bytes (tag + length + content) — needed to re-encode the signed
668    /// attributes for hashing. Returns `(tag, content, full_tlv)`.
669    #[allow(clippy::type_complexity)]
670    fn children_raw(content: &[u8], max: usize) -> Vec<(u8, &[u8], &[u8])> {
671        let mut out = Vec::new();
672        let mut rest = content;
673        while !rest.is_empty() && out.len() < max {
674            let before = rest;
675            let Some((tag, body, next)) = tlv(rest) else {
676                break;
677            };
678            let consumed = before.len() - next.len();
679            out.push((tag, body, &before[..consumed]));
680            rest = next;
681        }
682        out
683    }
684
685    pub(super) fn parse(blob: &[u8]) -> Option<Cms> {
686        // ContentInfo ::= SEQUENCE { contentType OID, content [0] SignedData }
687        let (tag, ci, _) = tlv(blob)?;
688        if tag != SEQUENCE {
689            return None;
690        }
691        let ci = children(ci, 4);
692        let ctype = ci.iter().find(|(t, _)| *t == OID)?;
693        if ctype.1 != OID_SIGNED_DATA {
694            return None;
695        }
696        let content = ci.iter().find(|(t, _)| *t == CONTEXT_0)?;
697        // content [0] EXPLICIT wraps the SignedData SEQUENCE.
698        let (tag, signed_data, _) = tlv(content.1)?;
699        if tag != SEQUENCE {
700            return None;
701        }
702
703        // SignedData ::= SEQUENCE { version, digestAlgorithms SET,
704        //   encapContentInfo, certificates [0]?, crls [1]?, signerInfos SET }
705        let sd = children(signed_data, 16);
706        // signerInfos is the last SET; digestAlgorithms is the first SET.
707        let signer_infos = sd.iter().rev().find(|(t, _)| *t == SET)?;
708        let certs = sd.iter().find(|(t, _)| *t == CONTEXT_0).map(|(_, c)| *c);
709
710        // signerInfos SET OF SignerInfo — take the first SignerInfo.
711        let (tag, signer_info, _) = tlv(signer_infos.1)?;
712        if tag != SEQUENCE {
713            return None;
714        }
715        let si = children_raw(signer_info, 16);
716
717        // SignerInfo: version INT, sid, digestAlgorithm SEQ, signedAttrs [0]?,
718        // signatureAlgorithm SEQ, signature OCTET, unsignedAttrs [1]?.
719        // `sid` (issuerAndSerialNumber) is *also* a SEQUENCE, so we can't pick the
720        // algorithm SEQUENCEs positionally. Instead classify each SEQUENCE's OID:
721        // sid's OIDs are X.509 attribute types (2.5.4.x) — never digest or
722        // signature OIDs — so the first SEQUENCE yielding each is unambiguous.
723        let seq_oid = |seq: &[u8]| -> Option<Vec<u8>> {
724            children(seq, 2)
725                .iter()
726                .find(|(t, _)| *t == OID)
727                .map(|(_, oid)| oid.to_vec())
728        };
729        let digest_alg = si
730            .iter()
731            .filter(|(t, _, _)| *t == SEQUENCE)
732            .find_map(|(_, seq, _)| seq_oid(seq).and_then(|oid| DigestAlg::from_oid(&oid)));
733        let sig_alg = si
734            .iter()
735            .filter(|(t, _, _)| *t == SEQUENCE)
736            .find_map(|(_, seq, _)| seq_oid(seq).and_then(|oid| sig_alg_from_oid(&oid)));
737
738        // signedAttrs is the [0] IMPLICIT tag; its content is the concatenated
739        // Attribute SEQUENCEs. Find the messageDigest attribute.
740        let signed_attrs = si.iter().find(|(t, _, _)| *t == CONTEXT_0);
741        let message_digest = signed_attrs.and_then(|(_, attrs, _)| find_message_digest(attrs));
742        // For hashing, the [0] IMPLICIT tag is replaced by SET OF (RFC 5652 §5.4).
743        let signed_attrs_der = signed_attrs.map(|(_, _, full)| {
744            let mut der = full.to_vec();
745            der[0] = SET;
746            der
747        });
748
749        // The signature value is the OCTET STRING after the two algorithm SEQs.
750        let signature = si
751            .iter()
752            .find(|(t, _, _)| *t == OCTET_STRING)
753            .map(|(_, body, _)| body.to_vec());
754
755        let signer_cn = certs.and_then(first_cert_cn);
756        let signer_key = certs.and_then(first_cert_public_key);
757
758        Some(Cms {
759            digest_alg,
760            message_digest,
761            signer_cn,
762            signed_attrs_der,
763            signature,
764            sig_alg,
765            signer_key,
766        })
767    }
768
769    /// Classify a `SignerInfo` `signatureAlgorithm` OID into an [`SigAlg`].
770    fn sig_alg_from_oid(oid: &[u8]) -> Option<SigAlg> {
771        if oid == OID_RSA_PSS {
772            Some(SigAlg::RsaPss)
773        } else if oid.starts_with(OID_RSA_PREFIX) {
774            // rsaEncryption or any sha*WithRSAEncryption → PKCS#1 v1.5.
775            Some(SigAlg::Rsa)
776        } else if oid.starts_with(OID_ECDSA_PREFIX) {
777            Some(SigAlg::Ecdsa)
778        } else {
779            None
780        }
781    }
782
783    /// Within a signed-attributes body (concatenated `Attribute` SEQUENCEs),
784    /// find the `messageDigest` attribute's OCTET STRING value.
785    fn find_message_digest(attrs: &[u8]) -> Option<Vec<u8>> {
786        for (tag, attr) in children(attrs, 64) {
787            if tag != SEQUENCE {
788                continue;
789            }
790            // Attribute ::= SEQUENCE { attrType OID, attrValues SET }
791            let parts = children(attr, 4);
792            let is_md = parts
793                .iter()
794                .find(|(t, _)| *t == OID)
795                .is_some_and(|(_, oid)| *oid == OID_MESSAGE_DIGEST);
796            if !is_md {
797                continue;
798            }
799            let values = parts.iter().find(|(t, _)| *t == SET)?;
800            let (vtag, digest, _) = tlv(values.1)?;
801            if vtag == OCTET_STRING {
802                return Some(digest.to_vec());
803            }
804        }
805        None
806    }
807
808    /// Extract the subject Common Name of the first X.509 certificate in the
809    /// `certificates [0]` body. Best-effort.
810    fn first_cert_cn(certs: &[u8]) -> Option<String> {
811        // The first Certificate ::= SEQUENCE { tbsCertificate, sigAlg, sig }.
812        let (tag, cert, _) = tlv(certs)?;
813        if tag != SEQUENCE {
814            return None;
815        }
816        let (tag, tbs, _) = tlv(cert)?;
817        if tag != SEQUENCE {
818            return None;
819        }
820        // TBSCertificate SEQUENCEs, in order: signatureAlg, issuer, validity,
821        // subject, spki. The subject Name is the 4th SEQUENCE.
822        let subject = children(tbs, 16)
823            .into_iter()
824            .filter(|(t, _)| *t == SEQUENCE)
825            .nth(3)?;
826        // subject Name ::= SEQUENCE OF RDN(SET) OF ATV(SEQUENCE{OID, value}).
827        for (tag, rdn) in children(subject.1, 32) {
828            if tag != SET {
829                continue;
830            }
831            for (tag, atv) in children(rdn, 8) {
832                if tag != SEQUENCE {
833                    continue;
834                }
835                let parts = children(atv, 2);
836                let is_cn = parts
837                    .iter()
838                    .find(|(t, _)| *t == OID)
839                    .is_some_and(|(_, oid)| *oid == OID_CN);
840                if is_cn {
841                    if let Some((vtag, value)) = parts.iter().rev().find(|(t, _)| *t != OID) {
842                        return Some(decode_directory_string(*vtag, value));
843                    }
844                }
845            }
846        }
847        None
848    }
849
850    /// Extract the [`PublicKeyInfo`] from the first X.509 certificate's
851    /// `SubjectPublicKeyInfo`. Best-effort; `None` on any structural surprise or
852    /// an unsupported key algorithm / curve.
853    fn first_cert_public_key(certs: &[u8]) -> Option<PublicKeyInfo> {
854        // Certificate ::= SEQUENCE { tbsCertificate, sigAlg, sig }.
855        let (tag, cert, _) = tlv(certs)?;
856        if tag != SEQUENCE {
857            return None;
858        }
859        let (tag, tbs, _) = tlv(cert)?;
860        if tag != SEQUENCE {
861            return None;
862        }
863        // TBSCertificate SEQUENCEs, in order: signatureAlg, issuer, validity,
864        // subject, subjectPublicKeyInfo. The SPKI is the 5th SEQUENCE.
865        let spki = children(tbs, 16)
866            .into_iter()
867            .filter(|(t, _)| *t == SEQUENCE)
868            .nth(4)?;
869
870        // SubjectPublicKeyInfo ::= SEQUENCE { algorithm AlgorithmIdentifier,
871        //   subjectPublicKey BIT STRING }.
872        let spki_parts = children(spki.1, 2);
873        let alg_id = spki_parts.iter().find(|(t, _)| *t == SEQUENCE)?.1;
874        let bit_string = spki_parts.iter().find(|(t, _)| *t == BIT_STRING)?.1;
875        // A BIT STRING's first content byte is the count of unused trailing bits
876        // (0 for keys); the key itself follows.
877        let key_bytes = bit_string
878            .split_first()
879            .and_then(|(unused, rest)| (*unused == 0).then(|| rest.to_vec()))?;
880
881        // AlgorithmIdentifier ::= SEQUENCE { algorithm OID, parameters ANY? }.
882        let alg_parts = children(alg_id, 2);
883        let alg_oid = alg_parts.iter().find(|(t, _)| *t == OID)?.1;
884
885        if alg_oid == OID_RSA_PUBLIC_KEY {
886            Some(PublicKeyInfo {
887                alg: KeyAlg::Rsa,
888                key: key_bytes,
889            })
890        } else if alg_oid == OID_EC_PUBLIC_KEY {
891            // The named curve is the *second* OID (the AlgorithmIdentifier
892            // parameter) after id-ecPublicKey.
893            let curve = alg_parts
894                .iter()
895                .filter(|(t, _)| *t == OID)
896                .nth(1)
897                .map(|(_, oid)| *oid)?;
898            let alg = if curve == OID_CURVE_P256 {
899                KeyAlg::EcP256
900            } else if curve == OID_CURVE_P384 {
901                KeyAlg::EcP384
902            } else {
903                return None;
904            };
905            Some(PublicKeyInfo {
906                alg,
907                key: key_bytes,
908            })
909        } else {
910            None
911        }
912    }
913
914    /// Decode an X.520 DirectoryString value by tag: BMPString is UTF-16BE, the
915    /// rest (UTF8String / PrintableString / IA5String / …) are treated as UTF-8.
916    fn decode_directory_string(tag: u8, value: &[u8]) -> String {
917        const BMP_STRING: u8 = 0x1e;
918        if tag == BMP_STRING {
919            let units: Vec<u16> = value
920                .chunks_exact(2)
921                .map(|c| u16::from_be_bytes([c[0], c[1]]))
922                .collect();
923            String::from_utf16_lossy(&units)
924        } else {
925            String::from_utf8_lossy(value).into_owned()
926        }
927    }
928}
929
930// ---------------------------------------------------------------------------
931// Public-key signature verification (RustCrypto)
932// ---------------------------------------------------------------------------
933//
934// Each verifier takes the already-computed digest of the signed attributes and
935// the raw signature/key bytes, and returns `Some(true)` on a valid signature,
936// `Some(false)` on a well-formed-but-failing one, or `None` when the key or
937// signature could not be parsed at all.
938
939mod pk {
940    use super::DigestAlg;
941    use rsa::pkcs1::DecodeRsaPublicKey;
942    use rsa::{Pkcs1v15Sign, RsaPublicKey};
943    use sha1::Sha1;
944    use sha2::{Sha256, Sha384, Sha512};
945
946    /// Verify an RSA PKCS #1 v1.5 signature. `key_der` is the `RSAPublicKey`
947    /// DER (`SEQUENCE { modulus, publicExponent }`); `hashed` is the digest of
948    /// the signed attributes under `alg`.
949    pub(super) fn rsa_verify(
950        alg: DigestAlg,
951        key_der: &[u8],
952        hashed: &[u8],
953        sig: &[u8],
954    ) -> Option<bool> {
955        let key = RsaPublicKey::from_pkcs1_der(key_der).ok()?;
956        let scheme = match alg {
957            DigestAlg::Sha1 => Pkcs1v15Sign::new::<Sha1>(),
958            DigestAlg::Sha256 => Pkcs1v15Sign::new::<Sha256>(),
959            DigestAlg::Sha384 => Pkcs1v15Sign::new::<Sha384>(),
960            DigestAlg::Sha512 => Pkcs1v15Sign::new::<Sha512>(),
961        };
962        Some(key.verify(scheme, hashed, sig).is_ok())
963    }
964
965    /// Verify an ECDSA signature over the NIST P-256 curve. `point` is the
966    /// SEC1-encoded public point; `sig` is the DER-encoded `(r, s)`.
967    pub(super) fn ecdsa_p256_verify(point: &[u8], hashed: &[u8], sig: &[u8]) -> Option<bool> {
968        use p256::ecdsa::signature::hazmat::PrehashVerifier;
969        use p256::ecdsa::{Signature, VerifyingKey};
970        let key = VerifyingKey::from_sec1_bytes(point).ok()?;
971        let sig = Signature::from_der(sig).ok()?;
972        Some(key.verify_prehash(hashed, &sig).is_ok())
973    }
974
975    /// Verify an ECDSA signature over the NIST P-384 curve.
976    pub(super) fn ecdsa_p384_verify(point: &[u8], hashed: &[u8], sig: &[u8]) -> Option<bool> {
977        use p384::ecdsa::signature::hazmat::PrehashVerifier;
978        use p384::ecdsa::{Signature, VerifyingKey};
979        let key = VerifyingKey::from_sec1_bytes(point).ok()?;
980        let sig = Signature::from_der(sig).ok()?;
981        Some(key.verify_prehash(hashed, &sig).is_ok())
982    }
983}
984
985// ---------------------------------------------------------------------------
986// Small object-graph helpers (local copies, mirroring crate::forms)
987// ---------------------------------------------------------------------------
988
989fn deref(file: &PdfFile, obj: &PdfObject) -> PdfObject {
990    match obj {
991        PdfObject::Ref(r) => file.resolve(*r).unwrap_or(PdfObject::Null),
992        other => other.clone(),
993    }
994}
995
996fn text_string(file: &PdfFile, obj: &PdfObject) -> Option<String> {
997    match deref(file, obj) {
998        PdfObject::String(s) => Some(pdf_string_to_unicode(s.as_bytes())),
999        _ => None,
1000    }
1001}
1002
1003#[cfg(test)]
1004mod tests {
1005    use super::*;
1006
1007    // ---- DER / CMS unit tests -------------------------------------------
1008
1009    /// Build a DER TLV with short/long length as appropriate.
1010    fn der(tag: u8, content: &[u8]) -> Vec<u8> {
1011        let mut out = vec![tag];
1012        let len = content.len();
1013        if len < 0x80 {
1014            out.push(len as u8);
1015        } else if len < 0x100 {
1016            out.push(0x81);
1017            out.push(len as u8);
1018        } else {
1019            out.push(0x82);
1020            out.push((len >> 8) as u8);
1021            out.push((len & 0xff) as u8);
1022        }
1023        out.extend_from_slice(content);
1024        out
1025    }
1026
1027    const SEQ: u8 = 0x30;
1028    const SET: u8 = 0x31;
1029    const OID: u8 = 0x06;
1030    const OCTET: u8 = 0x04;
1031    const INT: u8 = 0x02;
1032    const CTX0: u8 = 0xA0;
1033
1034    /// Hand-assemble a minimal detached-CMS blob carrying `digest` as the
1035    /// messageDigest attribute, signed with SHA-256.
1036    fn synth_cms(digest: &[u8]) -> Vec<u8> {
1037        let sha256_oid = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01];
1038        let md_oid = [0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x04];
1039
1040        // digestAlgorithm SEQUENCE { OID sha256 }
1041        let digest_alg = der(SEQ, &der(OID, &sha256_oid));
1042
1043        // messageDigest Attribute SEQUENCE { OID, SET { OCTET digest } }
1044        let md_attr = der(
1045            SEQ,
1046            &[der(OID, &md_oid), der(SET, &der(OCTET, digest))].concat(),
1047        );
1048        // signedAttrs [0] IMPLICIT holding the one attribute.
1049        let signed_attrs = der(CTX0, &md_attr);
1050
1051        // SignerInfo SEQUENCE { version, sid(SEQ), digestAlg(SEQ), signedAttrs[0],
1052        //   sigAlg(SEQ), signature(OCTET) }
1053        let signer_info = der(
1054            SEQ,
1055            &[
1056                der(INT, &[1]),
1057                der(SEQ, &[]), // sid placeholder
1058                digest_alg.clone(),
1059                signed_attrs,
1060                der(SEQ, &der(OID, &[0x2a])), // sigAlg placeholder
1061                der(OCTET, &[0xde, 0xad]),    // signature placeholder
1062            ]
1063            .concat(),
1064        );
1065        let signer_infos = der(SET, &signer_info);
1066
1067        // SignedData SEQUENCE { version, digestAlgorithms SET, encap SEQ, signerInfos SET }
1068        let signed_data = der(
1069            SEQ,
1070            &[
1071                der(INT, &[1]),
1072                der(SET, &digest_alg),
1073                der(
1074                    SEQ,
1075                    &der(OID, &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x01]),
1076                ),
1077                signer_infos,
1078            ]
1079            .concat(),
1080        );
1081
1082        // ContentInfo SEQUENCE { OID signedData, [0] SignedData }
1083        let signed_data_oid = [0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02];
1084        der(
1085            SEQ,
1086            &[der(OID, &signed_data_oid), der(CTX0, &signed_data)].concat(),
1087        )
1088    }
1089
1090    #[test]
1091    fn cms_extracts_digest_and_algorithm() {
1092        let digest: Vec<u8> = (0u8..32).collect();
1093        let blob = synth_cms(&digest);
1094        let parsed = cms::parse(&blob).expect("cms");
1095        assert_eq!(parsed.digest_alg, Some(DigestAlg::Sha256));
1096        assert_eq!(parsed.message_digest.as_deref(), Some(digest.as_slice()));
1097    }
1098
1099    #[test]
1100    fn cms_rejects_truncated_blob() {
1101        let blob = synth_cms(&[0u8; 32]);
1102        // Any prefix shorter than the whole must not panic; parse returns None
1103        // or a partial-but-safe result.
1104        for cut in 1..blob.len() {
1105            let _ = cms::parse(&blob[..cut]);
1106        }
1107    }
1108
1109    #[test]
1110    fn cms_rejects_indefinite_length() {
1111        // Tag SEQUENCE, indefinite length byte 0x80 — DER forbids it.
1112        assert!(cms::parse(&[0x30, 0x80, 0x00, 0x00]).is_none());
1113    }
1114
1115    #[test]
1116    fn digest_alg_oid_mapping() {
1117        assert_eq!(
1118            DigestAlg::from_oid(&[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01]),
1119            Some(DigestAlg::Sha256)
1120        );
1121        assert_eq!(
1122            DigestAlg::from_oid(&[0x2b, 0x0e, 0x03, 0x02, 0x1a]),
1123            Some(DigestAlg::Sha1)
1124        );
1125        assert_eq!(DigestAlg::from_oid(&[0x00]), None);
1126    }
1127
1128    #[test]
1129    fn gather_ranges_bounds_checked() {
1130        let data = b"0123456789";
1131        assert_eq!(
1132            gather_ranges(data, &[(0, 3), (7, 3)]).as_deref(),
1133            Some(&b"012789"[..])
1134        );
1135        // Out-of-range span is rejected.
1136        assert!(gather_ranges(data, &[(0, 3), (7, 99)]).is_none());
1137        assert!(gather_ranges(data, &[]).is_none());
1138    }
1139
1140    #[test]
1141    fn sha256_matches_reference() {
1142        // "abc" SHA-256, a well-known test vector.
1143        let d = DigestAlg::Sha256.hash(b"abc");
1144        assert_eq!(
1145            d,
1146            hex(b"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")
1147        );
1148    }
1149
1150    fn hex(h: &[u8]) -> Vec<u8> {
1151        h.chunks_exact(2)
1152            .map(|c| {
1153                let s = std::str::from_utf8(c).unwrap();
1154                u8::from_str_radix(s, 16).unwrap()
1155            })
1156            .collect()
1157    }
1158
1159    // ---- Robustness / adversarial inputs ---------------------------------
1160
1161    use crate::test_util::build_pdf;
1162    use zpdf_parser::PdfFile;
1163
1164    /// A signature field with an out-of-range /ByteRange (spans past EOF) must
1165    /// not panic, and must report Unsupported (cannot verify what doesn't exist).
1166    #[test]
1167    fn out_of_range_byte_range_reports_unsupported() {
1168        let pdf = build_pdf(&[
1169            "<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>",
1170            "<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
1171            "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>",
1172            "<< /Fields [5 0 R] >>",
1173            "<< /FT /Sig /T (S1) /V << /ByteRange [0 100 200 999999] /Contents <aabbcc> >> >>",
1174        ]);
1175        let file = PdfFile::parse(pdf.as_slice()).expect("parse");
1176        let sigs = parse_signatures(&file);
1177        assert_eq!(sigs.len(), 1);
1178        // An out-of-range span cannot be hashed; verdict is Unsupported.
1179        assert_eq!(sigs[0].digest, DigestStatus::Unsupported);
1180    }
1181
1182    /// A corrupt or oversized /Contents blob must not hang or panic. The parser
1183    /// caps CMS size at 4 MiB and the DER walker rejects truncated/indefinite TLVs.
1184    #[test]
1185    fn malformed_cms_contents_do_not_hang() {
1186        let pdf = build_pdf(&[
1187            "<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>",
1188            "<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
1189            "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>",
1190            "<< /Fields [5 0 R 6 0 R 7 0 R] >>",
1191            // Corrupt: truncated SEQUENCE (length claims 0x30 bytes, content is 4).
1192            "<< /FT /Sig /T (Truncated) /V << /ByteRange [0 10 20 30] /Contents <30304142> >> >>",
1193            // Empty /Contents.
1194            "<< /FT /Sig /T (Empty) /V << /ByteRange [0 10 20 30] /Contents <> >> >>",
1195            // Indefinite-length form (DER forbids): tag 0x30, length 0x80.
1196            "<< /FT /Sig /T (Indefinite) /V << /ByteRange [0 10 20 30] /Contents <308000> >> >>",
1197        ]);
1198        let file = PdfFile::parse(pdf.as_slice()).expect("parse");
1199        let sigs = parse_signatures(&file);
1200        // All three parse, but none can extract a digest (Unsupported).
1201        assert_eq!(sigs.len(), 3);
1202        for s in &sigs {
1203            assert_eq!(s.digest, DigestStatus::Unsupported);
1204        }
1205    }
1206
1207    /// A pathological field tree (deep nesting, many fields) must terminate cleanly.
1208    #[test]
1209    fn deep_field_tree_terminates() {
1210        // 60 fields in a flat tree (exceeds MAX_SIG_FIELDS = 4096 is impractical
1211        // in a hand-rolled PDF; test depth instead). A chain of 100 nested /Kids
1212        // exceeds MAX_FIELD_DEPTH = 50 and is pruned.
1213        let mut objs = vec![
1214            "<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".to_string(),
1215            "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_string(),
1216            "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>".to_string(),
1217            "<< /Fields [5 0 R] >>".to_string(),
1218        ];
1219        // Build a chain: obj 5 → obj 6 → obj 7 … → obj 104 (100 links).
1220        for i in 0..100 {
1221            let next = if i < 99 {
1222                format!("{} 0 R", 5 + i + 1)
1223            } else {
1224                "null".to_string()
1225            };
1226            objs.push(format!("<< /T (Field{i}) /FT /Sig /Kids [{}] >>", next));
1227        }
1228        let pdf = build_pdf(&objs.iter().map(|s| s.as_str()).collect::<Vec<_>>());
1229        let file = PdfFile::parse(pdf.as_slice()).expect("parse");
1230        let sigs = parse_signatures(&file);
1231        // The walk terminates at depth 50; no signatures are extracted (none had /V).
1232        assert!(sigs.len() < 100);
1233    }
1234}