use lopdf::{Dictionary, Object};
use crate::{Document, Result};
fn dict_string(dict: &Dictionary, key: &[u8]) -> Option<String> {
match dict.get(key) {
Ok(Object::String(bytes, _)) => Some(String::from_utf8_lossy(bytes).to_string()),
_ => None,
}
}
#[derive(Clone, Debug)]
pub struct SignatureInfo {
pub field_name: String,
pub signer_name: Option<String>,
pub signing_time: Option<String>,
pub is_valid: bool,
pub reason: Option<String>,
}
impl Document {
pub fn verify_signatures(&self, pdf_bytes: &[u8]) -> Result<Vec<SignatureInfo>> {
let mut signatures = Vec::new();
let root_ref = self.inner.trailer.get(b"Root")?.as_reference()?;
let catalog = self.inner.get_object(root_ref)?.as_dict()?;
let acroform_ref = match catalog.get(b"AcroForm") {
Ok(Object::Reference(id)) => id,
_ => return Ok(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),
};
let fields_array = match acroform.get(b"Fields") {
Ok(Object::Array(arr)) => arr,
_ => return Ok(signatures),
};
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"
{
if let Some(sig_info) = self.extract_signature_info(field, pdf_bytes) {
signatures.push(sig_info);
}
}
}
Ok(signatures)
}
fn extract_signature_info(
&self,
field: &Dictionary,
pdf_bytes: &[u8],
) -> Option<SignatureInfo> {
let field_name = dict_string(field, b"T").unwrap_or_else(|| "unknown".to_string());
let reason = dict_string(field, b"Reason");
let signing_time = dict_string(field, b"M");
let sig_dict_ref = field.get(b"V").ok()?.as_reference().ok()?;
let sig_dict = self.inner.get_object(sig_dict_ref).ok()?.as_dict().ok()?;
let contents_hex = dict_string(sig_dict, b"Contents")?;
let byte_range = match sig_dict.get(b"ByteRange") {
Ok(Object::Array(arr)) => arr
.iter()
.filter_map(|obj| obj.as_i64().ok().map(|n| n as u32))
.collect::<Vec<u32>>(),
_ => return None,
};
if byte_range.len() != 4 || byte_range[0] != 0 {
return None;
}
let signer_name = dict_string(sig_dict, b"Name")
.or_else(|| Some(format!("Signed by {}", field_name)));
let is_valid = self.verify_signature_crypto(&contents_hex, &byte_range, pdf_bytes);
Some(SignatureInfo {
field_name,
signer_name,
signing_time,
is_valid,
reason,
})
}
#[cfg(feature = "digital-signature")]
fn verify_signature_crypto(&self, contents_hex: &str, byte_range: &[u32], pdf_bytes: &[u8]) -> bool {
use num_bigint::BigUint;
let cms_bytes = match hex_to_bytes(contents_hex) {
Some(b) => b,
None => return false,
};
let hash = hash_pdf_byte_range(pdf_bytes, byte_range);
let (signature_bytes, cert_der) = match extract_signature_and_cert_from_pkcs7(&cms_bytes) {
Some((sig, cert)) => (sig, cert),
None => return false,
};
let (n_bytes, e_bytes) = match extract_rsa_public_key_from_cert(&cert_der) {
Some((n, e)) => (n, e),
None => return false,
};
let n = BigUint::from_bytes_be(&n_bytes);
let e = BigUint::from_bytes_be(&e_bytes);
let digest_info = build_digest_info_for_verification(&hash);
let modulus_size = ((n.bits() + 7) / 8) as usize;
let padded_expected = match build_pkcs1v15_signature_padding(&digest_info, modulus_size) {
Some(p) => p,
None => return false,
};
let sig_int = BigUint::from_bytes_be(&signature_bytes);
let decrypted_int = sig_int.modpow(&e, &n);
let mut decrypted = decrypted_int.to_bytes_be();
if decrypted.len() < modulus_size {
let mut padded_dec = vec![0u8; modulus_size - decrypted.len()];
padded_dec.extend_from_slice(&decrypted);
decrypted = padded_dec;
}
decrypted == padded_expected
}
#[cfg(not(feature = "digital-signature"))]
fn verify_signature_crypto(&self, _contents_hex: &str, _byte_range: &[u32], _pdf_bytes: &[u8]) -> bool {
false
}
}
#[cfg(feature = "digital-signature")]
fn hex_to_bytes(hex: &str) -> Option<Vec<u8>> {
let hex = hex.trim();
if hex.len() % 2 != 0 {
return None;
}
let mut bytes = Vec::with_capacity(hex.len() / 2);
for chunk in hex.as_bytes().chunks(2) {
let hex_str = std::str::from_utf8(chunk).ok()?;
let byte = u8::from_str_radix(hex_str, 16).ok()?;
bytes.push(byte);
}
Some(bytes)
}
#[cfg(feature = "digital-signature")]
fn hash_pdf_byte_range(pdf_bytes: &[u8], byte_range: &[u32]) -> Vec<u8> {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
let start1 = byte_range[0] as usize;
let length1 = byte_range[1] as usize;
let start2 = byte_range[2] as usize;
let length2 = byte_range[3] as usize;
if start1 + length1 <= pdf_bytes.len() {
hasher.update(&pdf_bytes[start1..start1 + length1]);
}
if start2 + length2 <= pdf_bytes.len() {
hasher.update(&pdf_bytes[start2..start2 + length2]);
}
hasher.finalize().to_vec()
}
#[cfg(feature = "digital-signature")]
fn extract_signature_and_cert_from_pkcs7(cms_bytes: &[u8]) -> Option<(Vec<u8>, Vec<u8>)> {
let mut sig_bytes = Vec::new();
let mut cert_bytes = Vec::new();
let mut i = 0;
while i < cms_bytes.len() {
if cms_bytes[i] == 0xa0 && i + 1 < cms_bytes.len() {
let cert_len = cms_bytes[i + 1] as usize;
if i + 2 + cert_len <= cms_bytes.len() {
cert_bytes = cms_bytes[i + 2..i + 2 + cert_len].to_vec();
i += 2 + cert_len;
continue;
}
}
if cms_bytes[i] == 0x04 && i + 1 < cms_bytes.len() {
let len = cms_bytes[i + 1] as usize;
if i + 2 + len <= cms_bytes.len() && len > 4 {
sig_bytes = cms_bytes[i + 2..i + 2 + len].to_vec();
}
}
i += 1;
}
if !sig_bytes.is_empty() && !cert_bytes.is_empty() {
Some((sig_bytes, cert_bytes))
} else {
None
}
}
#[cfg(feature = "digital-signature")]
fn extract_rsa_public_key_from_cert(cert_der: &[u8]) -> Option<(Vec<u8>, Vec<u8>)> {
let mut integers = Vec::new();
let mut i = 0;
while i < cert_der.len() {
if cert_der[i] == 0x02 && i + 1 < cert_der.len() {
let len = cert_der[i + 1] as usize;
if i + 2 + len <= cert_der.len() && len > 32 {
integers.push(cert_der[i + 2..i + 2 + len].to_vec());
i += 2 + len;
continue;
}
}
i += 1;
}
if integers.len() >= 2 {
Some((integers[0].clone(), integers[1].clone()))
} else {
None
}
}
#[cfg(feature = "digital-signature")]
fn build_digest_info_for_verification(hash: &[u8]) -> Vec<u8> {
let sha256_oid = vec![0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01];
let mut alg_id = vec![0x30];
let alg_content_len = 2 + sha256_oid.len() + 2;
encode_der_length_for_verify(&mut alg_id, alg_content_len);
alg_id.push(0x06);
encode_der_length_for_verify(&mut alg_id, sha256_oid.len());
alg_id.extend_from_slice(&sha256_oid);
alg_id.push(0x05);
alg_id.push(0x00);
let mut digest_info = vec![0x30];
let digest_info_content_len = alg_id.len() + 2 + hash.len();
encode_der_length_for_verify(&mut digest_info, digest_info_content_len);
digest_info.extend_from_slice(&alg_id);
digest_info.push(0x04);
encode_der_length_for_verify(&mut digest_info, hash.len());
digest_info.extend_from_slice(hash);
digest_info
}
#[cfg(feature = "digital-signature")]
fn encode_der_length_for_verify(result: &mut Vec<u8>, len: usize) {
if len < 128 {
result.push(len as u8);
} else {
let be = len.to_be_bytes();
let significant = &be[be.iter().take_while(|&&b| b == 0).count()..];
result.push(0x80 | significant.len() as u8);
result.extend_from_slice(significant);
}
}
#[cfg(feature = "digital-signature")]
fn build_pkcs1v15_signature_padding(message: &[u8], modulus_size: usize) -> Option<Vec<u8>> {
if message.len() + 11 > modulus_size {
return None;
}
let mut padded = Vec::with_capacity(modulus_size);
padded.push(0x00);
padded.push(0x01);
let ps_len = modulus_size - message.len() - 3;
for _ in 0..ps_len {
padded.push(0xFF);
}
padded.push(0x00);
padded.extend_from_slice(message);
Some(padded)
}
#[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"
);
}
}