1use 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
50const MAX_FIELD_DEPTH: usize = 50;
52const MAX_SIG_FIELDS: usize = 4_096;
53const MAX_CMS_BYTES: usize = 4 * 1024 * 1024;
57
58#[derive(Debug, Clone)]
60pub struct Signature {
61 pub field_name: String,
63 pub filter: Option<String>,
66 pub sub_filter: Option<String>,
70 pub name: Option<String>,
73 pub signing_time: Option<String>,
75 pub location: Option<String>,
77 pub reason: Option<String>,
79 pub contact_info: Option<String>,
81 pub coverage: ByteRangeCoverage,
83 pub digest: DigestStatus,
86 pub crypto: CryptoStatus,
89 pub digest_algorithm: Option<String>,
92 pub signature_algorithm: Option<String>,
96 pub signer_common_name: Option<String>,
100 pub cms_blob: Option<Vec<u8>>,
104}
105
106impl Signature {
107 pub fn is_cryptographically_valid(&self) -> bool {
118 self.digest == DigestStatus::Verified && self.crypto == CryptoStatus::Valid
119 }
120}
121
122#[derive(Debug, Clone)]
124pub struct ByteRangeCoverage {
125 pub ranges: Vec<(usize, usize)>,
127 pub covers_whole_document: bool,
131 pub bytes_after_signature: usize,
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum DigestStatus {
141 Verified,
144 Mismatch,
146 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub enum CryptoStatus {
165 Valid,
168 Invalid,
171 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
188pub 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
206fn 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
219fn 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 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 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
321struct 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
330fn 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 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
368fn 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 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 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, },
425 _ => DigestStatus::Unsupported,
426 };
427
428 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
440fn 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 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 _ => return CryptoStatus::Unsupported,
464 };
465
466 match verified {
467 Some(true) => CryptoStatus::Valid,
468 Some(false) => CryptoStatus::Invalid,
469 None => CryptoStatus::Unsupported, }
471}
472
473fn 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
488fn 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#[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 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
546mod cms {
556 use super::DigestAlg;
557
558 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 pub(super) signed_attrs_der: Option<Vec<u8>>,
568 pub(super) signature: Option<Vec<u8>>,
570 pub(super) sig_alg: Option<SigAlg>,
572 pub(super) signer_key: Option<PublicKeyInfo>,
574 }
575
576 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
578 pub(super) enum SigAlg {
579 Rsa,
581 RsaPss,
583 Ecdsa,
585 }
586
587 pub(super) struct PublicKeyInfo {
589 pub(super) alg: KeyAlg,
590 pub(super) key: Vec<u8>,
593 }
594
595 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
597 pub(super) enum KeyAlg {
598 Rsa,
599 EcP256,
600 EcP384,
601 }
602
603 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; 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 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 const OID_RSA_PUBLIC_KEY: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01];
622 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 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 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; }
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 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 #[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 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 let (tag, signed_data, _) = tlv(content.1)?;
704 if tag != SEQUENCE {
705 return None;
706 }
707
708 let sd = children(signed_data, 16);
711 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 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 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 let signed_attrs = si.iter().find(|(t, _, _)| *t == CONTEXT_0);
746 let message_digest = signed_attrs.and_then(|(_, attrs, _)| find_message_digest(attrs));
747 let signed_attrs_der = signed_attrs.map(|(_, _, full)| {
749 let mut der = full.to_vec();
750 der[0] = SET;
751 der
752 });
753
754 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 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 Some(SigAlg::Rsa)
781 } else if oid.starts_with(OID_ECDSA_PREFIX) {
782 Some(SigAlg::Ecdsa)
783 } else {
784 None
785 }
786 }
787
788 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 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 fn first_cert_cn(certs: &[u8]) -> Option<String> {
816 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 let subject = children(tbs, 16)
828 .into_iter()
829 .filter(|(t, _)| *t == SEQUENCE)
830 .nth(3)?;
831 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 fn first_cert_public_key(certs: &[u8]) -> Option<PublicKeyInfo> {
859 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 let spki = children(tbs, 16)
871 .into_iter()
872 .filter(|(t, _)| *t == SEQUENCE)
873 .nth(4)?;
874
875 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 let key_bytes = bit_string
883 .split_first()
884 .and_then(|(unused, rest)| (*unused == 0).then(|| rest.to_vec()))?;
885
886 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 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 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
935mod 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 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 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 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
990fn 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 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 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 let digest_alg = der(SEQ, &der(OID, &sha256_oid));
1047
1048 let md_attr = der(
1050 SEQ,
1051 &[der(OID, &md_oid), der(SET, &der(OCTET, digest))].concat(),
1052 );
1053 let signed_attrs = der(CTX0, &md_attr);
1055
1056 let signer_info = der(
1059 SEQ,
1060 &[
1061 der(INT, &[1]),
1062 der(SEQ, &[]), digest_alg.clone(),
1064 signed_attrs,
1065 der(SEQ, &der(OID, &[0x2a])), der(OCTET, &[0xde, 0xad]), ]
1068 .concat(),
1069 );
1070 let signer_infos = der(SET, &signer_info);
1071
1072 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 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 for cut in 1..blob.len() {
1110 let _ = cms::parse(&blob[..cut]);
1111 }
1112 }
1113
1114 #[test]
1115 fn cms_rejects_indefinite_length() {
1116 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 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 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 use crate::test_util::build_pdf;
1167 use zpdf_parser::PdfFile;
1168
1169 #[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 assert_eq!(sigs[0].digest, DigestStatus::Unsupported);
1185 }
1186
1187 #[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 "<< /FT /Sig /T (Truncated) /V << /ByteRange [0 10 20 30] /Contents <30304142> >> >>",
1198 "<< /FT /Sig /T (Empty) /V << /ByteRange [0 10 20 30] /Contents <> >> >>",
1200 "<< /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 assert_eq!(sigs.len(), 3);
1207 for s in &sigs {
1208 assert_eq!(s.digest, DigestStatus::Unsupported);
1209 }
1210 }
1211
1212 #[test]
1214 fn deep_field_tree_terminates() {
1215 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 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 assert!(sigs.len() < 100);
1238 }
1239}