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}
101
102impl Signature {
103 pub fn is_cryptographically_valid(&self) -> bool {
114 self.digest == DigestStatus::Verified && self.crypto == CryptoStatus::Valid
115 }
116}
117
118#[derive(Debug, Clone)]
120pub struct ByteRangeCoverage {
121 pub ranges: Vec<(usize, usize)>,
123 pub covers_whole_document: bool,
127 pub bytes_after_signature: usize,
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum DigestStatus {
137 Verified,
140 Mismatch,
142 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum CryptoStatus {
161 Valid,
164 Invalid,
167 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
184pub 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
202fn 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
215fn 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 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 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
316struct 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
325fn 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 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
363fn 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 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 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, },
420 _ => DigestStatus::Unsupported,
421 };
422
423 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
435fn 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 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 _ => return CryptoStatus::Unsupported,
459 };
460
461 match verified {
462 Some(true) => CryptoStatus::Valid,
463 Some(false) => CryptoStatus::Invalid,
464 None => CryptoStatus::Unsupported, }
466}
467
468fn 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
483fn 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#[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 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
541mod cms {
551 use super::DigestAlg;
552
553 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 pub(super) signed_attrs_der: Option<Vec<u8>>,
563 pub(super) signature: Option<Vec<u8>>,
565 pub(super) sig_alg: Option<SigAlg>,
567 pub(super) signer_key: Option<PublicKeyInfo>,
569 }
570
571 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
573 pub(super) enum SigAlg {
574 Rsa,
576 RsaPss,
578 Ecdsa,
580 }
581
582 pub(super) struct PublicKeyInfo {
584 pub(super) alg: KeyAlg,
585 pub(super) key: Vec<u8>,
588 }
589
590 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
592 pub(super) enum KeyAlg {
593 Rsa,
594 EcP256,
595 EcP384,
596 }
597
598 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; 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 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 const OID_RSA_PUBLIC_KEY: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01];
617 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 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 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; }
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 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 #[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 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 let (tag, signed_data, _) = tlv(content.1)?;
699 if tag != SEQUENCE {
700 return None;
701 }
702
703 let sd = children(signed_data, 16);
706 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 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 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 let signed_attrs = si.iter().find(|(t, _, _)| *t == CONTEXT_0);
741 let message_digest = signed_attrs.and_then(|(_, attrs, _)| find_message_digest(attrs));
742 let signed_attrs_der = signed_attrs.map(|(_, _, full)| {
744 let mut der = full.to_vec();
745 der[0] = SET;
746 der
747 });
748
749 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 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 Some(SigAlg::Rsa)
776 } else if oid.starts_with(OID_ECDSA_PREFIX) {
777 Some(SigAlg::Ecdsa)
778 } else {
779 None
780 }
781 }
782
783 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 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 fn first_cert_cn(certs: &[u8]) -> Option<String> {
811 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 let subject = children(tbs, 16)
823 .into_iter()
824 .filter(|(t, _)| *t == SEQUENCE)
825 .nth(3)?;
826 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 fn first_cert_public_key(certs: &[u8]) -> Option<PublicKeyInfo> {
854 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 let spki = children(tbs, 16)
866 .into_iter()
867 .filter(|(t, _)| *t == SEQUENCE)
868 .nth(4)?;
869
870 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 let key_bytes = bit_string
878 .split_first()
879 .and_then(|(unused, rest)| (*unused == 0).then(|| rest.to_vec()))?;
880
881 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 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 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
930mod 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 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 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 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
985fn 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 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 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 let digest_alg = der(SEQ, &der(OID, &sha256_oid));
1042
1043 let md_attr = der(
1045 SEQ,
1046 &[der(OID, &md_oid), der(SET, &der(OCTET, digest))].concat(),
1047 );
1048 let signed_attrs = der(CTX0, &md_attr);
1050
1051 let signer_info = der(
1054 SEQ,
1055 &[
1056 der(INT, &[1]),
1057 der(SEQ, &[]), digest_alg.clone(),
1059 signed_attrs,
1060 der(SEQ, &der(OID, &[0x2a])), der(OCTET, &[0xde, 0xad]), ]
1063 .concat(),
1064 );
1065 let signer_infos = der(SET, &signer_info);
1066
1067 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 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 for cut in 1..blob.len() {
1105 let _ = cms::parse(&blob[..cut]);
1106 }
1107 }
1108
1109 #[test]
1110 fn cms_rejects_indefinite_length() {
1111 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 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 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 use crate::test_util::build_pdf;
1162 use zpdf_parser::PdfFile;
1163
1164 #[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 assert_eq!(sigs[0].digest, DigestStatus::Unsupported);
1180 }
1181
1182 #[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 "<< /FT /Sig /T (Truncated) /V << /ByteRange [0 10 20 30] /Contents <30304142> >> >>",
1193 "<< /FT /Sig /T (Empty) /V << /ByteRange [0 10 20 30] /Contents <> >> >>",
1195 "<< /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 assert_eq!(sigs.len(), 3);
1202 for s in &sigs {
1203 assert_eq!(s.digest, DigestStatus::Unsupported);
1204 }
1205 }
1206
1207 #[test]
1209 fn deep_field_tree_terminates() {
1210 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 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 assert!(sigs.len() < 100);
1233 }
1234}