1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//! Digital signature verification for PDFs.
//!
//! This module provides signature verification for PKCS#7-signed PDFs.
//! Currently, signatures are extracted but not cryptographically verified (stub implementation).
use lopdf::{Dictionary, Object};
use crate::{Document, Result};
/// Information about a PDF signature field.
///
/// Returned by [`Document::verify_signatures`].
#[derive(Clone, Debug)]
pub struct SignatureInfo {
/// The signature field name (from `/T` in the signature dictionary).
pub field_name: String,
/// Signer name extracted from the certificate's CN attribute, if available.
pub signer_name: Option<String>,
/// Signing time from the signature or CMS metadata, if available.
pub signing_time: Option<String>,
/// Whether the signature is valid (hash matches + certificate chain validates).
pub is_valid: bool,
/// Reason for signing, from the `/Reason` field if present.
pub reason: Option<String>,
}
impl Document {
/// Verifies all signatures in the PDF document.
///
/// # Arguments
///
/// * `pdf_bytes` — The raw PDF file bytes. Required for byte-range validation.
///
/// # Returns
///
/// A vector of signature information objects. Returns an empty vector if
/// the document has no signature fields.
///
/// # Errors
///
/// Returns an error if the PDF structure is malformed or unreadable.
///
/// # Example
///
/// ```no_run
/// # use harumi::Document;
/// # fn main() -> harumi::Result<()> {
/// let pdf_bytes = std::fs::read("signed.pdf")?;
/// let doc = Document::from_bytes(&pdf_bytes)?;
/// let signatures = doc.verify_signatures(&pdf_bytes)?;
///
/// for sig in signatures {
/// if sig.is_valid {
/// println!("✓ Valid signature: {}", sig.field_name);
/// } else {
/// println!("✗ Invalid signature: {}", sig.field_name);
/// }
/// }
/// # Ok(())
/// # }
/// ```
pub fn verify_signatures(&self, pdf_bytes: &[u8]) -> Result<Vec<SignatureInfo>> {
let mut signatures = Vec::new();
// Locate the AcroForm in the document catalog
let root_ref = self.inner.trailer.get(b"Root")?.as_reference()?;
let catalog = self.inner.get_object(root_ref)?.as_dict()?;
// Get the AcroForm dictionary
let acroform_ref = match catalog.get(b"AcroForm") {
Ok(Object::Reference(id)) => id,
_ => return Ok(signatures), // No AcroForm or not a reference = no forms/signatures
};
let acroform = match self.inner.get_object(*acroform_ref) {
Ok(obj) => match obj.as_dict() {
Ok(dict) => dict,
Err(_) => return Ok(signatures),
},
Err(_) => return Ok(signatures),
};
// Get the Fields array
let fields_array = match acroform.get(b"Fields") {
Ok(Object::Array(arr)) => arr,
_ => return Ok(signatures),
};
// Process each field, looking for signature fields
for field_obj in fields_array {
if let Object::Reference(field_id) = field_obj
&& let Ok(field_obj) = self.inner.get_object(*field_id)
&& let Ok(field) = field_obj.as_dict()
&& let Ok(Object::Name(name)) = field.get(b"FT")
&& name == b"Sig"
{
// This is a signature field
if let Some(sig_info) = self.extract_signature_info(field, pdf_bytes) {
signatures.push(sig_info);
}
}
}
Ok(signatures)
}
/// Extracts signature information from a signature field dictionary.
fn extract_signature_info(&self, field: &Dictionary, _pdf_bytes: &[u8]) -> Option<SignatureInfo> {
// Get field name
let field_name = match field.get(b"T") {
Ok(Object::String(bytes, _)) => String::from_utf8_lossy(bytes).to_string(),
_ => "unknown".to_string(),
};
// Get reason for signing
let reason = match field.get(b"Reason") {
Ok(Object::String(bytes, _)) => Some(String::from_utf8_lossy(bytes).to_string()),
_ => None,
};
// Get signing time
let signing_time = match field.get(b"M") {
Ok(Object::String(bytes, _)) => Some(String::from_utf8_lossy(bytes).to_string()),
_ => None,
};
// TODO: Implement actual signature validation
// For now, always return is_valid = false to indicate validation is not yet implemented
let is_valid = false;
Some(SignatureInfo {
field_name,
signer_name: None, // TODO: Extract from certificate
signing_time,
is_valid,
reason,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verify_signatures_returns_empty_for_unsigned_doc() {
let mut doc = Document::new((100.0, 100.0)).unwrap();
let pdf_bytes = doc.save_to_bytes().unwrap();
let signatures = doc.verify_signatures(&pdf_bytes).unwrap();
assert_eq!(signatures.len(), 0, "unsigned document should have no signatures");
}
}