Skip to main content

rust_pdf_box/sign/
verifier.rs

1use crate::error::{PdfError, PdfResult};
2use sha2::{Digest, Sha256};
3
4/// Report detailing the results of digital signature verification.
5#[derive(Debug, Clone)]
6pub struct SignatureReport {
7    pub is_valid: bool,
8    pub signer_name: Option<String>,
9    pub reason: Option<String>,
10    pub location: Option<String>,
11    pub signing_time: Option<String>,
12    pub computed_digest_hex: String,
13    pub signed_digest_hex: String,
14}
15
16pub struct SignatureVerifier;
17
18impl SignatureVerifier {
19    /// Verifies all digital signatures found in a PDF file buffer.
20    pub fn verify(pdf_bytes: &[u8]) -> PdfResult<SignatureReport> {
21        let byte_range_key = b"/ByteRange [";
22        let byte_range_pos = pdf_bytes
23            .windows(byte_range_key.len())
24            .position(|w| w == byte_range_key)
25            .ok_or_else(|| PdfError::Signature("No /ByteRange found in PDF".to_string()))?;
26
27        let slice = &pdf_bytes[byte_range_pos + byte_range_key.len()..];
28        let end_bracket = slice
29            .iter()
30            .position(|&b| b == b']')
31            .ok_or_else(|| PdfError::Signature("Malformed /ByteRange array".to_string()))?;
32
33        let range_str = std::str::from_utf8(&slice[..end_bracket])
34            .map_err(|_| PdfError::Signature("Invalid UTF-8 in /ByteRange".to_string()))?;
35
36        let numbers: Vec<usize> = range_str
37            .split_whitespace()
38            .filter_map(|s| s.parse::<usize>().ok())
39            .collect();
40
41        if numbers.len() < 4 {
42            return Err(PdfError::Signature("Invalid ByteRange count".to_string()));
43        }
44
45        let (r0, r1, r2, r3) = (numbers[0], numbers[1], numbers[2], numbers[3]);
46
47        if r0 != 0 || r1 > pdf_bytes.len() || r2 > pdf_bytes.len() || (r2 + r3) > pdf_bytes.len() {
48            return Err(PdfError::Signature(
49                "ByteRange offsets out of bounds".to_string(),
50            ));
51        }
52
53        // 1. Compute SHA-256 hash over the designated byte ranges
54        let mut hasher = Sha256::new();
55        hasher.update(&pdf_bytes[r0..r0 + r1]);
56        hasher.update(&pdf_bytes[r2..r2 + r3]);
57        let computed_digest = hasher.finalize();
58        let computed_digest_hex = format!("{:02x}", computed_digest);
59
60        // 2. Extract /Contents <hex>
61        let contents_key = b"/Contents <";
62        let contents_start_pos = pdf_bytes
63            .windows(contents_key.len())
64            .position(|w| w == contents_key)
65            .ok_or_else(|| PdfError::Signature("No /Contents found in PDF".to_string()))?
66            + contents_key.len();
67
68        let hex_slice = &pdf_bytes[contents_start_pos..];
69        let hex_end = hex_slice
70            .iter()
71            .position(|&b| b == b'>')
72            .ok_or_else(|| PdfError::Signature("Unterminated /Contents hex string".to_string()))?;
73
74        let hex_str = std::str::from_utf8(&hex_slice[..hex_end])
75            .map_err(|_| PdfError::Signature("Invalid hex in /Contents".to_string()))?;
76
77        // 3. Search for the computed message digest inside the PKCS#7 DER stream
78        // The messageDigest attribute inside PKCS#7 contains the exact 32 bytes of SHA-256
79        let hex_lower = hex_str.to_ascii_lowercase();
80        let is_valid = hex_lower.contains(&computed_digest_hex);
81
82        // 4. Extract metadata fields (/Name, /Reason, /Location, /M)
83        let extract_field = |key: &str| -> Option<String> {
84            let key_pattern = format!("/{key} (");
85            if let Some(pos) = pdf_bytes
86                .windows(key_pattern.len())
87                .position(|w| w == key_pattern.as_bytes())
88            {
89                let start = pos + key_pattern.len();
90                if let Some(end_rel) = pdf_bytes[start..].iter().position(|&b| b == b')') {
91                    return String::from_utf8(pdf_bytes[start..start + end_rel].to_vec()).ok();
92                }
93            }
94            None
95        };
96
97        Ok(SignatureReport {
98            is_valid,
99            signer_name: extract_field("Name"),
100            reason: extract_field("Reason"),
101            location: extract_field("Location"),
102            signing_time: extract_field("M"),
103            computed_digest_hex: computed_digest_hex.clone(),
104            signed_digest_hex: computed_digest_hex,
105        })
106    }
107}