use super::byterange::ByteRangeCalculator;
use super::types::{DigestAlgorithm, SignOptions, SigningCredentials};
use crate::error::{Error, Result};
#[cfg(feature = "signatures")]
use sha2::{Digest, Sha256, Sha384, Sha512};
#[cfg(feature = "signatures")]
use sha1::Sha1;
pub struct PdfSigner {
credentials: SigningCredentials,
options: SignOptions,
byte_range_calc: ByteRangeCalculator,
}
impl PdfSigner {
pub fn new(credentials: SigningCredentials, options: SignOptions) -> Self {
let byte_range_calc = ByteRangeCalculator::new(options.estimated_size);
Self {
credentials,
options,
byte_range_calc,
}
}
pub fn placeholder_size(&self) -> usize {
self.byte_range_calc.placeholder_size()
}
pub fn generate_contents_placeholder(&self) -> String {
self.byte_range_calc.generate_placeholder()
}
pub fn build_signature_dictionary(&self) -> String {
let mut dict = String::new();
dict.push_str("/Type /Sig\n");
dict.push_str("/Filter /Adobe.PPKLite\n");
dict.push_str(&format!("/SubFilter /{}\n", self.options.sub_filter.as_pdf_name()));
dict.push_str("/ByteRange [0 0 0 0]\n");
if let Some(ref name) = self.options.name {
dict.push_str(&format!("/Name ({})\n", escape_pdf_string(name)));
}
if let Some(ref reason) = self.options.reason {
dict.push_str(&format!("/Reason ({})\n", escape_pdf_string(reason)));
}
if let Some(ref location) = self.options.location {
dict.push_str(&format!("/Location ({})\n", escape_pdf_string(location)));
}
if let Some(ref contact) = self.options.contact_info {
dict.push_str(&format!("/ContactInfo ({})\n", escape_pdf_string(contact)));
}
let signing_time = format_pdf_date();
dict.push_str(&format!("/M ({})\n", signing_time));
dict
}
#[cfg(feature = "signatures")]
pub fn compute_digest(&self, signed_bytes: &[u8]) -> Vec<u8> {
match self.options.digest_algorithm {
DigestAlgorithm::Sha1 => {
let mut hasher = Sha1::new();
hasher.update(signed_bytes);
hasher.finalize().to_vec()
},
DigestAlgorithm::Sha256 => {
let mut hasher = Sha256::new();
hasher.update(signed_bytes);
hasher.finalize().to_vec()
},
DigestAlgorithm::Sha384 => {
let mut hasher = Sha384::new();
hasher.update(signed_bytes);
hasher.finalize().to_vec()
},
DigestAlgorithm::Sha512 => {
let mut hasher = Sha512::new();
hasher.update(signed_bytes);
hasher.finalize().to_vec()
},
}
}
#[cfg(feature = "signatures")]
pub fn sign(&self, signed_bytes: &[u8]) -> Result<Vec<u8>> {
self.create_pkcs7_signature_inner(signed_bytes, None, None)
}
#[cfg(feature = "signatures")]
pub fn sign_pades(&self, signed_bytes: &[u8]) -> Result<Vec<u8>> {
let ess = crate::signatures::pades::build_signing_certificate_v2(
&self.credentials.certificate,
self.options.digest_algorithm,
)?;
self.create_pkcs7_signature_inner(signed_bytes, Some(&ess), None)
}
#[cfg(feature = "signatures")]
pub fn sign_pades_t(
&self,
signed_bytes: &[u8],
timestamper: &dyn Fn(&[u8]) -> Result<Vec<u8>>,
) -> Result<Vec<u8>> {
let ess = crate::signatures::pades::build_signing_certificate_v2(
&self.credentials.certificate,
self.options.digest_algorithm,
)?;
self.create_pkcs7_signature_inner(signed_bytes, Some(&ess), Some(timestamper))
}
#[cfg(feature = "signatures")]
fn create_pkcs7_signature_inner(
&self,
signed_bytes: &[u8],
ess_attr: Option<&[u8]>,
timestamper: Option<&dyn Fn(&[u8]) -> Result<Vec<u8>>>,
) -> Result<Vec<u8>> {
use super::crypto::digest_info_prefix;
use cms::cert::x509::Certificate as X509Certificate;
use der::oid::db::rfc5912::{ID_SHA_1, ID_SHA_256, ID_SHA_384, ID_SHA_512};
use der::{Decode, Encode};
use rsa::pkcs8::DecodePrivateKey;
use rsa::{Pkcs1v15Sign, RsaPrivateKey};
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha384, Sha512};
const OID_SIGNED_DATA: &[u8] = &[0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x07, 0x02];
const OID_DATA: &[u8] = &[0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x07, 0x01];
const OID_SHA1: &[u8] = &[0x2B, 0x0E, 0x03, 0x02, 0x1A];
const OID_SHA256: &[u8] = &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01];
const OID_SHA384: &[u8] = &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02];
const OID_SHA512: &[u8] = &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03];
const OID_RSA_ENC: &[u8] = &[0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01];
const OID_CONTENT_TYPE: &[u8] = &[0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x09, 0x03];
const OID_MSG_DIGEST: &[u8] = &[0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x09, 0x04];
let (digest_oid_bytes, digest_oid, message_digest): (&[u8], _, Vec<u8>) =
match self.options.digest_algorithm {
DigestAlgorithm::Sha1 => (OID_SHA1, ID_SHA_1, Sha1::digest(signed_bytes).to_vec()),
DigestAlgorithm::Sha256 => {
(OID_SHA256, ID_SHA_256, Sha256::digest(signed_bytes).to_vec())
},
DigestAlgorithm::Sha384 => {
(OID_SHA384, ID_SHA_384, Sha384::digest(signed_bytes).to_vec())
},
DigestAlgorithm::Sha512 => {
(OID_SHA512, ID_SHA_512, Sha512::digest(signed_bytes).to_vec())
},
};
let cert = X509Certificate::from_der(&self.credentials.certificate)
.map_err(|e| Error::InvalidPdf(format!("cannot parse signer certificate: {e}")))?;
let issuer_der = cert
.tbs_certificate
.issuer
.to_der()
.map_err(|e| Error::InvalidPdf(format!("cannot DER-encode issuer: {e}")))?;
let serial_der = cert
.tbs_certificate
.serial_number
.to_der()
.map_err(|e| Error::InvalidPdf(format!("cannot DER-encode serial: {e}")))?;
let rsa_key = RsaPrivateKey::from_pkcs8_der(&self.credentials.private_key)
.or_else(|_| {
use pkcs1::DecodeRsaPrivateKey;
RsaPrivateKey::from_pkcs1_der(&self.credentials.private_key)
})
.map_err(|_| {
Error::InvalidPdf("private key is not valid PKCS#8 or PKCS#1 RSA DER".into())
})?;
let modulus_bits = {
use rsa::traits::PublicKeyParts;
rsa_key.n().bits()
};
if crate::crypto::active_policy().rsa_modulus_allowed(modulus_bits as u32)
== crate::crypto::Decision::Deny
{
return Err(Error::Unsupported(format!(
"RSA signing key modulus is {modulus_bits} bits; the active crypto \
SecurityPolicy requires at least {} (#230 Phase D)",
crate::crypto::active_policy().min_rsa_modulus_bits()
)));
}
let attr_ct = {
let mut c = Vec::new();
c.extend(der_oid(OID_CONTENT_TYPE));
c.extend(der_set(&der_oid(OID_DATA)));
der_sequence(&c)
};
let attr_md = {
let mut c = Vec::new();
c.extend(der_oid(OID_MSG_DIGEST));
c.extend(der_set(&der_octet_string(&message_digest)));
der_sequence(&c)
};
let mut attrs_content = Vec::new();
attrs_content.extend(&attr_ct);
attrs_content.extend(&attr_md);
if let Some(ess) = ess_attr {
attrs_content.extend_from_slice(ess);
}
let attrs_for_hashing = der_set(&attrs_content);
let attrs_for_storage = der_tag(0xA0, &attrs_content);
let attrs_hash: Vec<u8> = match self.options.digest_algorithm {
DigestAlgorithm::Sha1 => Sha1::digest(&attrs_for_hashing).to_vec(),
DigestAlgorithm::Sha256 => Sha256::digest(&attrs_for_hashing).to_vec(),
DigestAlgorithm::Sha384 => Sha384::digest(&attrs_for_hashing).to_vec(),
DigestAlgorithm::Sha512 => Sha512::digest(&attrs_for_hashing).to_vec(),
};
let di_prefix = digest_info_prefix(digest_oid)
.ok_or_else(|| Error::InvalidPdf("no DigestInfo prefix for digest OID".into()))?;
let mut digest_info_bytes = Vec::with_capacity(di_prefix.len() + attrs_hash.len());
digest_info_bytes.extend_from_slice(di_prefix);
digest_info_bytes.extend_from_slice(&attrs_hash);
let sig_bytes = rsa_key
.sign(Pkcs1v15Sign::new_unprefixed(), &digest_info_bytes)
.map_err(|e| Error::InvalidPdf(format!("RSA signing failed: {e}")))?;
let unsigned_attrs: Option<Vec<u8>> = match timestamper {
Some(ts) => {
let token = ts(&sig_bytes)?;
let attr = crate::signatures::pades::build_signature_timestamp_attr(&token)?;
Some(der_tag(0xA1, &attr))
},
None => None,
};
let signer_info = {
let mut isn = Vec::new();
isn.extend(&issuer_der);
isn.extend(&serial_der);
let isn = der_sequence(&isn);
let digest_alg = der_sequence(&der_oid(digest_oid_bytes));
let sig_alg = {
let mut c = Vec::new();
c.extend(der_oid(OID_RSA_ENC));
c.extend_from_slice(&[0x05, 0x00]); der_sequence(&c)
};
let mut si = Vec::new();
si.extend(der_integer(1));
si.extend(isn);
si.extend(digest_alg);
si.extend(attrs_for_storage);
si.extend(sig_alg);
si.extend(der_octet_string(&sig_bytes));
if let Some(ref ua) = unsigned_attrs {
si.extend_from_slice(ua);
}
der_sequence(&si)
};
let signed_data = {
let digest_algs = der_set(&der_sequence(&der_oid(digest_oid_bytes)));
let encap_ci = der_sequence(&der_oid(OID_DATA));
let certs = der_tag(0xA0, &self.credentials.certificate);
let signer_infos = der_set(&signer_info);
let mut sd = Vec::new();
sd.extend(der_integer(1)); sd.extend(digest_algs);
sd.extend(encap_ci);
sd.extend(certs);
sd.extend(signer_infos);
der_sequence(&sd)
};
let mut ci = Vec::new();
ci.extend(der_oid(OID_SIGNED_DATA));
ci.extend(der_tag(0xA0, &signed_data)); Ok(der_sequence(&ci))
}
pub fn calculate_byte_range(&self, file_size: usize, contents_offset: usize) -> [i64; 4] {
self.byte_range_calc
.calculate_byte_range(file_size, contents_offset)
}
pub fn extract_signed_bytes(pdf_data: &[u8], byte_range: &[i64; 4]) -> Result<Vec<u8>> {
ByteRangeCalculator::extract_signed_bytes(pdf_data, byte_range)
}
pub fn insert_signature(
&self,
pdf_data: &mut [u8],
contents_offset: usize,
signature: &[u8],
) -> Result<()> {
let signature_hex = bytes_to_hex(signature);
self.byte_range_calc
.insert_signature(pdf_data, contents_offset, &signature_hex)
}
pub fn options(&self) -> &SignOptions {
&self.options
}
pub fn credentials(&self) -> &SigningCredentials {
&self.credentials
}
}
#[cfg(feature = "signatures")]
use super::der_util::{der_integer, der_octet_string, der_oid, der_sequence, der_set, der_tag};
fn bytes_to_hex(bytes: &[u8]) -> String {
const HEX_CHARS: &[u8] = b"0123456789ABCDEF";
let mut hex = String::with_capacity(bytes.len() * 2);
for &byte in bytes {
hex.push(HEX_CHARS[(byte >> 4) as usize] as char);
hex.push(HEX_CHARS[(byte & 0x0F) as usize] as char);
}
hex
}
fn escape_pdf_string(s: &str) -> String {
let mut result = String::with_capacity(s.len() + 10);
for c in s.chars() {
match c {
'\\' => result.push_str("\\\\"),
'(' => result.push_str("\\("),
')' => result.push_str("\\)"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
_ => result.push(c),
}
}
result
}
fn format_pdf_date() -> String {
super::pdf_date::format_pdf_date_utc()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_escape_pdf_string() {
assert_eq!(escape_pdf_string("Hello"), "Hello");
assert_eq!(escape_pdf_string("Hello (World)"), "Hello \\(World\\)");
assert_eq!(escape_pdf_string("Line1\nLine2"), "Line1\\nLine2");
assert_eq!(escape_pdf_string("Path\\to\\file"), "Path\\\\to\\\\file");
}
#[test]
fn test_format_pdf_date() {
let date = format_pdf_date();
assert!(date.starts_with("D:"));
assert!(date.ends_with("Z"));
}
#[test]
fn test_signer_placeholder() {
let creds = SigningCredentials::new(vec![], vec![]);
let opts = SignOptions {
estimated_size: 1024,
..Default::default()
};
let signer = PdfSigner::new(creds, opts);
let placeholder = signer.generate_contents_placeholder();
assert_eq!(placeholder.len(), 2050);
assert!(placeholder.starts_with('<'));
assert!(placeholder.ends_with('>'));
}
#[test]
fn test_build_signature_dictionary() {
let creds = SigningCredentials::new(vec![], vec![]);
let opts = SignOptions {
reason: Some("Test signing".to_string()),
location: Some("Test City".to_string()),
..Default::default()
};
let signer = PdfSigner::new(creds, opts);
let dict = signer.build_signature_dictionary();
assert!(dict.contains("/Type /Sig"));
assert!(dict.contains("/Filter /Adobe.PPKLite"));
assert!(dict.contains("/SubFilter /adbe.pkcs7.detached"));
assert!(dict.contains("/Reason (Test signing)"));
assert!(dict.contains("/Location (Test City)"));
assert!(dict.contains("/ByteRange"));
assert!(dict.contains("/M (D:"));
}
#[test]
fn test_calculate_byte_range() {
let creds = SigningCredentials::new(vec![], vec![]);
let opts = SignOptions {
estimated_size: 50, ..Default::default()
};
let signer = PdfSigner::new(creds, opts);
let byte_range = signer.calculate_byte_range(1000, 400);
assert_eq!(byte_range[0], 0);
assert_eq!(byte_range[1], 400);
assert_eq!(byte_range[2], 502); assert_eq!(byte_range[3], 498); }
#[test]
#[cfg(feature = "signatures")]
fn test_sign_produces_valid_cms_blob() {
use super::super::cms_verify::SignerVerify;
use super::super::types::SignOptions;
use super::super::{verify_signer_detached, SigningCredentials};
let cert_pem = std::fs::read_to_string("tests/fixtures/test_signing_cert.pem")
.expect("test fixture must exist");
let key_pem = std::fs::read_to_string("tests/fixtures/test_signing_key.pem")
.expect("test fixture must exist");
let creds =
SigningCredentials::from_pem(&cert_pem, &key_pem).expect("credentials must load");
let content = b"hello world this is the signed PDF content";
let signer = PdfSigner::new(creds, SignOptions::default());
let cms_blob = signer.sign(content).expect("sign must succeed");
let result = verify_signer_detached(&cms_blob, content)
.expect("verify_signer_detached must not error");
assert_eq!(
result,
SignerVerify::Valid,
"signature must verify as Valid with the same content"
);
}
#[test]
#[cfg(feature = "signatures")]
fn test_sign_pades_adds_ess_and_still_verifies() {
use super::super::cms_verify::SignerVerify;
use super::super::types::SignOptions;
use super::super::{verify_signer_detached, SigningCredentials};
let cert_pem = std::fs::read_to_string("tests/fixtures/test_signing_cert.pem").unwrap();
let key_pem = std::fs::read_to_string("tests/fixtures/test_signing_key.pem").unwrap();
let creds = SigningCredentials::from_pem(&cert_pem, &key_pem).unwrap();
let content = b"PAdES-B-B content under signature";
let signer = PdfSigner::new(creds, SignOptions::default());
const ESS_OID: &[u8] = &[
0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x09, 0x10, 0x02, 0x2F,
];
let legacy = signer.sign(content).unwrap();
assert_eq!(verify_signer_detached(&legacy, content).unwrap(), SignerVerify::Valid);
assert!(
!legacy.windows(ESS_OID.len()).any(|w| w == ESS_OID),
"legacy sign() must not add the ESS attribute"
);
let pades = signer.sign_pades(content).unwrap();
assert!(
pades.windows(ESS_OID.len()).any(|w| w == ESS_OID),
"sign_pades() must embed the ESS signing-certificate-v2 attr"
);
assert_eq!(
verify_signer_detached(&pades, content).unwrap(),
SignerVerify::Valid,
"PAdES signature with ESS must still verify as Valid"
);
assert_ne!(
verify_signer_detached(&pades, b"different content").unwrap(),
SignerVerify::Valid
);
}
#[test]
#[cfg(feature = "signatures")]
fn test_sign_pades_t_embeds_timestamp_and_classifies_bt() {
use super::super::cms_verify::SignerVerify;
use super::super::types::{SignOptions, SignatureInfo};
use super::super::{classify_pades_level, verify_signer_detached, SigningCredentials};
use crate::signatures::PadesLevel;
let cert_pem = std::fs::read_to_string("tests/fixtures/test_signing_cert.pem").unwrap();
let key_pem = std::fs::read_to_string("tests/fixtures/test_signing_key.pem").unwrap();
let creds = SigningCredentials::from_pem(&cert_pem, &key_pem).unwrap();
let content = b"PAdES-B-T content under signature";
let signer = PdfSigner::new(creds, SignOptions::default());
let seen = std::cell::RefCell::new(Vec::new());
let token: &dyn Fn(&[u8]) -> Result<Vec<u8>> = &|sig: &[u8]| {
*seen.borrow_mut() = sig.to_vec();
Ok(vec![0x30, 0x07, 0x02, 0x01, 0x01, 0x04, 0x02, b't', b's'])
};
let b_b = signer.sign_pades(content).unwrap();
let b_t = signer.sign_pades_t(content, token).unwrap();
assert!(!seen.borrow().is_empty(), "timestamper must see the sig value");
let sig = &seen.borrow().clone();
assert!(b_b.windows(sig.len()).any(|w| w == sig.as_slice()));
assert!(b_t.windows(sig.len()).any(|w| w == sig.as_slice()));
assert_eq!(verify_signer_detached(&b_t, content).unwrap(), SignerVerify::Valid);
const TS_OID: &[u8] = &[
0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x09, 0x10, 0x02, 0x0E,
];
assert!(b_t.windows(TS_OID.len()).any(|w| w == TS_OID));
let info = SignatureInfo {
signer_name: None,
signing_time: None,
reason: None,
location: None,
contact_info: None,
sub_filter: None,
covers_whole_document: false,
byte_range: vec![],
certificate_cn: None,
certificate_issuer: None,
valid_from: None,
valid_to: None,
contents: Some(b_t.clone()),
};
assert_eq!(classify_pades_level(&info, None), PadesLevel::BT);
let info_bb = SignatureInfo {
contents: Some(b_b),
..info
};
assert_eq!(classify_pades_level(&info_bb, None), PadesLevel::BB);
}
#[test]
#[cfg(feature = "signatures")]
fn test_sign_detects_tampered_content() {
use super::super::cms_verify::SignerVerify;
use super::super::types::SignOptions;
use super::super::{verify_signer_detached, SigningCredentials};
let cert_pem = std::fs::read_to_string("tests/fixtures/test_signing_cert.pem")
.expect("test fixture must exist");
let key_pem = std::fs::read_to_string("tests/fixtures/test_signing_key.pem")
.expect("test fixture must exist");
let creds =
SigningCredentials::from_pem(&cert_pem, &key_pem).expect("credentials must load");
let content = b"original content";
let tampered = b"tampered content!";
let signer = PdfSigner::new(creds, SignOptions::default());
let cms_blob = signer.sign(content).expect("sign must succeed");
let result = verify_signer_detached(&cms_blob, tampered)
.expect("verify must not error on tampered content");
assert_eq!(result, SignerVerify::Invalid, "tampered content must verify as Invalid");
}
#[test]
#[cfg(feature = "signatures")]
fn test_sign_via_pkcs12() {
use super::super::cms_verify::SignerVerify;
use super::super::types::SignOptions;
use super::super::{verify_signer_detached, SigningCredentials};
let p12_data =
std::fs::read("tests/fixtures/test_signing.p12").expect("test fixture must exist");
let creds =
SigningCredentials::from_pkcs12(&p12_data, "testpass").expect("PKCS#12 must load");
let content = b"PDF content for pkcs12 signing test";
let signer = PdfSigner::new(creds, SignOptions::default());
let cms_blob = signer.sign(content).expect("sign must succeed");
let result = verify_signer_detached(&cms_blob, content).expect("verify must not error");
assert_eq!(result, SignerVerify::Valid, "PKCS#12-signed blob must verify as Valid");
}
}