Skip to main content

rust_pdf_box/sign/
signer.rs

1use crate::cos::object::{COSArray, COSDictionary, COSName, COSString};
2use crate::crypto::pkcs7::generate_pkcs7_detached;
3use crate::crypto::x509::SigningKey;
4use crate::error::{PdfError, PdfResult};
5use crate::pd::document::PDDocument;
6use crate::pd::info::format_pdf_date;
7use crate::pd::rectangle::PDRectangle;
8use crate::sign::visual::VisualSignature;
9use chrono::Utc;
10use sha2::{Digest, Sha256};
11
12const SIG_CONTENTS_BYTE_CAPACITY: usize = 4096; // 8192 hex chars
13
14/// High-level, context-oriented PDF digital signer.
15#[derive(Clone)]
16pub struct Signer {
17    pub key: SigningKey,
18    pub name: String,
19    pub reason: Option<String>,
20    pub location: Option<String>,
21    pub contact_info: Option<String>,
22    pub visual_signature: Option<VisualSignature>,
23}
24
25impl Signer {
26    pub fn new(key: SigningKey) -> Self {
27        Self {
28            key,
29            name: "Digital Signer".to_string(),
30            reason: None,
31            location: None,
32            contact_info: None,
33            visual_signature: None,
34        }
35    }
36
37    pub fn with_name(mut self, name: impl Into<String>) -> Self {
38        self.name = name.into();
39        self
40    }
41
42    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
43        self.reason = Some(reason.into());
44        self
45    }
46
47    pub fn with_location(mut self, location: impl Into<String>) -> Self {
48        self.location = Some(location.into());
49        self
50    }
51
52    pub fn with_contact_info(mut self, contact: impl Into<String>) -> Self {
53        self.contact_info = Some(contact.into());
54        self
55    }
56
57    pub fn with_visual_stamp(mut self, vis: VisualSignature) -> Self {
58        self.visual_signature = Some(vis);
59        self
60    }
61
62    /// Digitally signs the document and returns the final signed PDF bytes.
63    pub fn sign_document(&self, doc: &mut PDDocument) -> PdfResult<Vec<u8>> {
64        // 1. Prepare visual appearance stream if configured
65        let ap_stream_id = if let Some(ref vis) = self.visual_signature {
66            Some(vis.build_appearance_stream(&mut doc.cos_doc)?)
67        } else {
68            None
69        };
70
71        // 2. Prepare Signature dictionary (/Type /Sig)
72        let sig_dict_id = doc.cos_doc.allocate_id();
73        let mut sig_dict = COSDictionary::new();
74        sig_dict.set("Type", COSName::new("Sig"));
75        sig_dict.set("Filter", COSName::new("Adobe.PPKLite"));
76        sig_dict.set("SubFilter", COSName::new("adbe.pkcs7.detached"));
77        sig_dict.set("Name", COSString::literal(self.name.clone().into_bytes()));
78
79        if let Some(ref reason) = self.reason {
80            sig_dict.set("Reason", COSString::literal(reason.clone().into_bytes()));
81        }
82        if let Some(ref location) = self.location {
83            sig_dict.set(
84                "Location",
85                COSString::literal(location.clone().into_bytes()),
86            );
87        }
88        if let Some(ref contact) = self.contact_info {
89            sig_dict.set(
90                "ContactInfo",
91                COSString::literal(contact.clone().into_bytes()),
92            );
93        }
94
95        let now = Utc::now();
96        sig_dict.set("M", COSString::literal(format_pdf_date(now).into_bytes()));
97
98        // Placeholder for ByteRange: [ 0000000000 0000000000 0000000000 0000000000 ]
99        let mut byte_range_placeholder = COSArray::new();
100        byte_range_placeholder.push(1000000000i64);
101        byte_range_placeholder.push(1000000000i64);
102        byte_range_placeholder.push(1000000000i64);
103        byte_range_placeholder.push(1000000000i64);
104        sig_dict.set("ByteRange", byte_range_placeholder);
105
106        // Placeholder for /Contents: 4096 zero bytes in hex (8192 characters)
107        let placeholder_bytes = vec![0u8; SIG_CONTENTS_BYTE_CAPACITY];
108        sig_dict.set("Contents", COSString::hex(placeholder_bytes));
109
110        doc.cos_doc.set_object(sig_dict_id, sig_dict);
111
112        // 3. Signature Field / Widget Annotation (/FT /Sig)
113        let sig_field_id = doc.cos_doc.allocate_id();
114        let mut sig_field = COSDictionary::new();
115        sig_field.set("Type", COSName::new("Annot"));
116        sig_field.set("Subtype", COSName::new("Widget"));
117        sig_field.set("FT", COSName::new("Sig"));
118        sig_field.set("T", COSString::literal("Signature1".as_bytes().to_vec()));
119        sig_field.set("V", sig_dict_id);
120        sig_field.set("F", 4); // Print flag
121
122        if let Some(ref vis) = self.visual_signature {
123            sig_field.set("Rect", vis.rect);
124            if let Some(ap_id) = ap_stream_id {
125                let mut ap_dict = COSDictionary::new();
126                ap_dict.set("N", ap_id);
127                sig_field.set("AP", ap_dict);
128            }
129
130            // Attach annotation to target page
131            let page_idx = (vis.page_number.max(1) - 1).min(doc.page_count().saturating_sub(1));
132            let page = doc.get_page_mut(page_idx)?;
133            let mut annots_arr = page.dict.get_array("Annots").cloned().unwrap_or_default();
134            annots_arr.push(sig_field_id);
135            page.dict.set("Annots", annots_arr);
136        } else {
137            sig_field.set("Rect", PDRectangle::new(0.0, 0.0, 0.0, 0.0));
138        }
139
140        doc.cos_doc.set_object(sig_field_id, sig_field);
141
142        // 4. Update Document Catalog AcroForm
143        let mut catalog_dict = doc.cos_doc.get_catalog().cloned().unwrap_or_default();
144        let mut acro_form = catalog_dict
145            .get_dict("AcroForm")
146            .cloned()
147            .unwrap_or_default();
148        acro_form.set("SigFlags", 3); // SignaturesExist | AppendOnly
149
150        let mut fields = acro_form.get_array("Fields").cloned().unwrap_or_default();
151        fields.push(sig_field_id);
152        acro_form.set("Fields", fields);
153
154        let catalog_id = doc.catalog.catalog_id;
155        catalog_dict.set("AcroForm", acro_form);
156        doc.cos_doc.set_object(catalog_id, catalog_dict);
157
158        // 5. Serialize document to in-memory bytes
159        let mut pdf_buffer = Vec::new();
160        doc.save_to_writer(&mut pdf_buffer)?;
161
162        // 6. Locate /Contents <... hex ...> in the serialized buffer
163        let contents_key = b"/Contents <";
164        let contents_start_pos = pdf_buffer
165            .windows(contents_key.len())
166            .position(|w| w == contents_key)
167            .ok_or_else(|| {
168                PdfError::Signature("Could not locate /Contents in serialized PDF".to_string())
169            })?
170            + b"/Contents ".len();
171
172        let hex_start = contents_start_pos + 1; // skip '<'
173        let hex_end = hex_start + (SIG_CONTENTS_BYTE_CAPACITY * 2);
174
175        if hex_end >= pdf_buffer.len() || pdf_buffer[hex_end] != b'>' {
176            return Err(PdfError::Signature(
177                "Corrupted /Contents hex placeholder".to_string(),
178            ));
179        }
180
181        let offset1 = contents_start_pos;
182        let offset2 = hex_end + 1;
183        let len2 = pdf_buffer.len() - offset2;
184
185        // 7. Locate /ByteRange [...] and overwrite with exact offsets
186        let byte_range_key = b"/ByteRange [";
187        let byte_range_pos = pdf_buffer
188            .windows(byte_range_key.len())
189            .position(|w| w == byte_range_key)
190            .ok_or_else(|| {
191                PdfError::Signature("Could not locate /ByteRange in serialized PDF".to_string())
192            })?;
193
194        // Format ByteRange with fixed width padding: "/ByteRange [ 0          {:10} {:10} {:10} ]"
195        let byte_range_str = format!(
196            "/ByteRange [ 0          {:10} {:10} {:10} ]",
197            offset1, offset2, len2
198        );
199        let byte_range_bytes = byte_range_str.as_bytes();
200
201        pdf_buffer[byte_range_pos..byte_range_pos + byte_range_bytes.len()]
202            .copy_from_slice(byte_range_bytes);
203
204        // 8. Hash byte ranges with SHA-256
205        let mut hasher = Sha256::new();
206        hasher.update(&pdf_buffer[0..offset1]);
207        hasher.update(&pdf_buffer[offset2..]);
208        let digest = hasher.finalize();
209
210        // 9. Generate PKCS#7 detached signature
211        let p7_der = generate_pkcs7_detached(&self.key, &digest)?;
212        if p7_der.len() > SIG_CONTENTS_BYTE_CAPACITY {
213            return Err(PdfError::Signature(format!(
214                "Signature size {} exceeds reserved space {}",
215                p7_der.len(),
216                SIG_CONTENTS_BYTE_CAPACITY
217            )));
218        }
219
220        // 10. Write uppercase hex signature into placeholder
221        let hex_sig: String = p7_der.iter().map(|b| format!("{:02X}", b)).collect();
222        let hex_sig_bytes = hex_sig.as_bytes();
223
224        pdf_buffer[hex_start..hex_start + hex_sig_bytes.len()].copy_from_slice(hex_sig_bytes);
225
226        // Pad remaining space with '0'
227        for b in &mut pdf_buffer[hex_start + hex_sig_bytes.len()..hex_end] {
228            *b = b'0';
229        }
230
231        Ok(pdf_buffer)
232    }
233}