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