use crate::error::PdfError;
use crate::pubsec::cms::{OID_DATA, OID_RSA_ENCRYPTION, OID_SIGNED_DATA};
use crate::pubsec::der::{
write_context_constructed, write_integer_bytes, write_integer_u64, write_null,
write_octet_string, write_oid, write_sequence, write_set, write_tlv, Class,
};
use crate::pubsec::verify::{
build_message_digest_attribute_der, implicit_signed_attrs_tlv, pack_signed_attrs_implicit,
signed_attrs_to_be_signed, HashAlg, OID_ECDSA_WITH_SHA256, OID_SHA256,
};
use crate::writer::write_pdf_from_scene;
use oxideav_scene::Scene;
const CONTENTS_HEX_LEN: usize = 8192;
const BYTE_RANGE_PLACEHOLDER: &str = "/ByteRange [ 0 0 0 0]";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SigningAlgorithm {
RsaPkcs1v15Sha256,
EcdsaP256Sha256,
}
impl SigningAlgorithm {
pub fn hash(self) -> HashAlg {
match self {
Self::RsaPkcs1v15Sha256 | Self::EcdsaP256Sha256 => HashAlg::Sha256,
}
}
pub fn digest_algorithm_oid(self) -> &'static [u64] {
match self {
Self::RsaPkcs1v15Sha256 | Self::EcdsaP256Sha256 => &OID_SHA256,
}
}
pub fn signature_algorithm_oid(self) -> &'static [u64] {
match self {
Self::RsaPkcs1v15Sha256 => &OID_RSA_ENCRYPTION,
Self::EcdsaP256Sha256 => &OID_ECDSA_WITH_SHA256,
}
}
pub fn signature_algorithm_params(self) -> Vec<u8> {
match self {
Self::RsaPkcs1v15Sha256 => write_null(),
Self::EcdsaP256Sha256 => Vec::new(),
}
}
}
pub trait Signer {
fn algorithm(&self) -> SigningAlgorithm;
fn sign(&self, tbs_hash: &[u8]) -> Result<Vec<u8>, PdfError>;
}
pub struct RsaPkcs1v15Sha256Signer {
private_key: rsa::RsaPrivateKey,
}
impl RsaPkcs1v15Sha256Signer {
pub fn new(private_key: rsa::RsaPrivateKey) -> Self {
Self { private_key }
}
}
impl Signer for RsaPkcs1v15Sha256Signer {
fn algorithm(&self) -> SigningAlgorithm {
SigningAlgorithm::RsaPkcs1v15Sha256
}
fn sign(&self, tbs_hash: &[u8]) -> Result<Vec<u8>, PdfError> {
use rsa::pkcs1v15::Pkcs1v15Sign;
use rsa::traits::SignatureScheme;
use sha2::Sha256;
Pkcs1v15Sign::new::<Sha256>()
.sign(
None::<&mut rsa::rand_core::OsRng>,
&self.private_key,
tbs_hash,
)
.map_err(|e| PdfError::other(format!("RSA-PKCS1v15 sign: {e}")))
}
}
pub struct EcdsaP256Sha256Signer {
signing_key: p256::ecdsa::SigningKey,
}
impl EcdsaP256Sha256Signer {
pub fn new(signing_key: p256::ecdsa::SigningKey) -> Self {
Self { signing_key }
}
}
impl Signer for EcdsaP256Sha256Signer {
fn algorithm(&self) -> SigningAlgorithm {
SigningAlgorithm::EcdsaP256Sha256
}
fn sign(&self, tbs_hash: &[u8]) -> Result<Vec<u8>, PdfError> {
use p256::ecdsa::signature::hazmat::PrehashSigner;
let sig: p256::ecdsa::Signature = self
.signing_key
.sign_prehash(tbs_hash)
.map_err(|e| PdfError::other(format!("ECDSA-P256 sign: {e}")))?;
Ok(sig.to_der().as_bytes().to_vec())
}
}
#[derive(Debug, Clone)]
pub struct SignerIdentity {
pub issuer_der: Vec<u8>,
pub serial: Vec<u8>,
pub cert_chain: Vec<Vec<u8>>,
}
impl SignerIdentity {
pub fn from_signer_cert_der(cert_der: Vec<u8>) -> Result<Self, PdfError> {
let parsed = crate::pubsec::x509::Certificate::parse(&cert_der)?;
Ok(Self {
issuer_der: parsed.issuer_der,
serial: parsed.serial,
cert_chain: vec![cert_der],
})
}
}
pub struct SigWriter<'a, S: Signer> {
scene: &'a Scene,
signer: &'a S,
identity: SignerIdentity,
}
impl<'a, S: Signer> SigWriter<'a, S> {
pub fn new(scene: &'a Scene, signer: &'a S, identity: SignerIdentity) -> Self {
Self {
scene,
signer,
identity,
}
}
pub fn sign(self) -> Result<Vec<u8>, PdfError> {
let base = write_pdf_from_scene(self.scene)?;
let signer_cert_for_dict = self
.identity
.cert_chain
.first()
.map(|v| v.as_slice())
.unwrap_or(&[]);
let (mut pdf, byte_range, contents_hex_offset) =
append_signed_revision(&base, signer_cert_for_dict)?;
patch_byte_range(&mut pdf, byte_range);
let signed_bytes = concat_byte_ranges(&pdf, byte_range)?;
let content_hash = self.signer.algorithm().hash().hash(&signed_bytes);
let md_attr = build_message_digest_attribute_der(&content_hash);
let ct_attr = build_content_type_attribute_der(&OID_DATA);
let attrs_body = pack_signed_attrs_implicit(&[ct_attr, md_attr]);
let tbs = signed_attrs_to_be_signed(&attrs_body);
let tbs_hash = self.signer.algorithm().hash().hash(&tbs);
let signature_bytes = self.signer.sign(&tbs_hash)?;
let cms_blob = pkcs7_wrap_signed_data(
self.signer.algorithm(),
&self.identity.issuer_der,
&self.identity.serial,
&self.identity.cert_chain,
Some(&attrs_body),
&signature_bytes,
);
patch_contents(&mut pdf, contents_hex_offset, &cms_blob)?;
Ok(pdf)
}
}
pub fn sign_pdf_from_scene<S: Signer>(
scene: &Scene,
signer: &S,
identity: SignerIdentity,
) -> Result<Vec<u8>, PdfError> {
SigWriter::new(scene, signer, identity).sign()
}
fn append_signed_revision(
base: &[u8],
signer_cert_der: &[u8],
) -> Result<(Vec<u8>, [i64; 4], usize), PdfError> {
let prev_xref_off = crate::reader::xref::find_startxref_offset(base)?;
let prev_table = crate::reader::xref::parse_xref(base)?;
let prev_root = prev_table.root()?;
let prev_max_id = prev_table.entries.keys().copied().max().unwrap_or(0);
let prev_size_from_trailer = prev_table
.trailer
.entries()
.iter()
.find(|(k, _)| k == "Size")
.and_then(|(_, v)| match v {
crate::objects::Object::Integer(n) if *n >= 0 => Some(*n as u32),
_ => None,
});
let prev_size = prev_size_from_trailer.unwrap_or(prev_max_id + 1);
let mut reader = crate::reader::document::DocumentReader::open(base)?;
let prev_catalog_obj = reader.resolve(prev_root)?;
let mut catalog_dict = match prev_catalog_obj {
crate::objects::Object::Dict(d) => d,
_ => {
return Err(PdfError::other("SigWriter: previous /Root is not a Dict"));
}
};
let acroform_id = prev_max_id + 1;
let sigfield_id = prev_max_id + 2;
let sigdict_id = prev_max_id + 3;
catalog_dict.set(
"AcroForm",
crate::objects::Object::Reference(crate::objects::ObjectId::new(acroform_id)),
);
let mut out: Vec<u8> = base.to_vec();
if !out.ends_with(b"\n") {
out.push(b'\n');
}
fn write_indirect_dict(
out: &mut Vec<u8>,
id: u32,
dict: &crate::objects::Dict,
) -> Result<usize, PdfError> {
let offset = out.len();
out.extend_from_slice(format!("{id} 0 obj\n").as_bytes());
crate::objects::write_dict_to(out, dict)?;
out.extend_from_slice(b"\nendobj\n");
Ok(offset)
}
let catalog_offset = write_indirect_dict(&mut out, prev_root.number, &catalog_dict)?;
let acroform_dict = crate::objects::Dict::new()
.with(
"Fields",
crate::objects::Object::Array(vec![crate::objects::Object::Reference(
crate::objects::ObjectId::new(sigfield_id),
)]),
)
.with("SigFlags", crate::objects::Object::Integer(3));
let acroform_offset = write_indirect_dict(&mut out, acroform_id, &acroform_dict)?;
let sigfield_dict = crate::objects::Dict::new()
.with("FT", crate::objects::Object::Name("Sig".to_string()))
.with(
"T",
crate::objects::Object::LiteralString(b"Signature1".to_vec()),
)
.with(
"V",
crate::objects::Object::Reference(crate::objects::ObjectId::new(sigdict_id)),
);
let sigfield_offset = write_indirect_dict(&mut out, sigfield_id, &sigfield_dict)?;
let sigdict_offset = out.len();
out.extend_from_slice(format!("{sigdict_id} 0 obj\n").as_bytes());
out.extend_from_slice(b"<< /Type /Sig /Filter /Adobe.PPKLite ");
out.extend_from_slice(b"/SubFilter /adbe.pkcs7.detached ");
out.extend_from_slice(BYTE_RANGE_PLACEHOLDER.as_bytes());
out.extend_from_slice(b" /Contents <");
let contents_hex_offset = out.len();
out.resize(out.len() + CONTENTS_HEX_LEN, b'0');
out.extend_from_slice(b"> ");
out.extend_from_slice(b"/Cert <");
for b in signer_cert_der {
out.extend_from_slice(format!("{b:02X}").as_bytes());
}
out.extend_from_slice(b"> >>\nendobj\n");
let xref_off = out.len();
out.extend_from_slice(b"xref\n");
out.extend_from_slice(format!("{} 1\n", prev_root.number).as_bytes());
out.extend_from_slice(format!("{catalog_offset:010} 00000 n \n").as_bytes());
out.extend_from_slice(format!("{acroform_id} 3\n").as_bytes());
out.extend_from_slice(format!("{acroform_offset:010} 00000 n \n").as_bytes());
out.extend_from_slice(format!("{sigfield_offset:010} 00000 n \n").as_bytes());
out.extend_from_slice(format!("{sigdict_offset:010} 00000 n \n").as_bytes());
let new_size = (sigdict_id + 1).max(prev_size);
out.extend_from_slice(b"trailer\n<< ");
out.extend_from_slice(format!("/Size {new_size} ").as_bytes());
out.extend_from_slice(
format!("/Root {} {} R ", prev_root.number, prev_root.generation).as_bytes(),
);
out.extend_from_slice(format!("/Prev {prev_xref_off} ").as_bytes());
if let Some(info_id) = prev_table.info() {
out.extend_from_slice(
format!("/Info {} {} R ", info_id.number, info_id.generation).as_bytes(),
);
}
out.extend_from_slice(b">>\n");
out.extend_from_slice(b"startxref\n");
out.extend_from_slice(format!("{xref_off}\n%%EOF\n").as_bytes());
let a = 0i64;
let b = contents_hex_offset as i64;
let c = (contents_hex_offset + CONTENTS_HEX_LEN) as i64;
let total = out.len() as i64;
let d = total - c;
Ok((out, [a, b, c, d], contents_hex_offset))
}
fn patch_byte_range(pdf: &mut [u8], byte_range: [i64; 4]) {
let formatted = format!(
"/ByteRange [{:>10} {:>10} {:>10} {:>10}]",
byte_range[0], byte_range[1], byte_range[2], byte_range[3]
);
debug_assert_eq!(
formatted.len(),
BYTE_RANGE_PLACEHOLDER.len(),
"ByteRange width drift"
);
let placeholder_bytes = BYTE_RANGE_PLACEHOLDER.as_bytes();
if let Some(pos) = pdf
.windows(placeholder_bytes.len())
.position(|w| w == placeholder_bytes)
{
pdf[pos..pos + placeholder_bytes.len()].copy_from_slice(formatted.as_bytes());
}
}
fn patch_contents(
pdf: &mut [u8],
contents_hex_offset: usize,
contents_der: &[u8],
) -> Result<(), PdfError> {
let hex_len_needed = contents_der.len() * 2;
if hex_len_needed > CONTENTS_HEX_LEN {
return Err(PdfError::other(format!(
"SigWriter: CMS blob {} hex chars exceeds /Contents budget {}",
hex_len_needed, CONTENTS_HEX_LEN
)));
}
for (i, b) in contents_der.iter().enumerate() {
let hi = (b >> 4) & 0x0F;
let lo = b & 0x0F;
pdf[contents_hex_offset + 2 * i] = hex_digit(hi);
pdf[contents_hex_offset + 2 * i + 1] = hex_digit(lo);
}
for byte in pdf
.iter_mut()
.skip(contents_hex_offset + hex_len_needed)
.take(CONTENTS_HEX_LEN - hex_len_needed)
{
*byte = b'0';
}
Ok(())
}
fn hex_digit(n: u8) -> u8 {
match n {
0..=9 => b'0' + n,
10..=15 => b'A' + (n - 10),
_ => unreachable!(),
}
}
fn concat_byte_ranges(pdf: &[u8], byte_range: [i64; 4]) -> Result<Vec<u8>, PdfError> {
let [a, b, c, d] = byte_range;
if a < 0 || b < 0 || c < 0 || d < 0 {
return Err(PdfError::other("SigWriter: negative /ByteRange entry"));
}
let (a, b, c, d) = (a as usize, b as usize, c as usize, d as usize);
if a + b > pdf.len() || c + d > pdf.len() {
return Err(PdfError::other(
"SigWriter: /ByteRange extends past file length",
));
}
let mut out = Vec::with_capacity(b + d);
out.extend_from_slice(&pdf[a..a + b]);
out.extend_from_slice(&pdf[c..c + d]);
Ok(out)
}
pub fn pkcs7_wrap_signed_data(
algorithm: SigningAlgorithm,
signer_issuer_der: &[u8],
signer_serial: &[u8],
cert_chain: &[Vec<u8>],
signed_attrs_body: Option<&[u8]>,
signature_bytes: &[u8],
) -> Vec<u8> {
let digest_oid = algorithm.digest_algorithm_oid();
let sig_oid = algorithm.signature_algorithm_oid();
let sig_params = algorithm.signature_algorithm_params();
let mut si_body = write_integer_u64(1);
let ias_body = {
let mut b = signer_issuer_der.to_vec();
b.extend_from_slice(&write_integer_bytes(signer_serial));
b
};
si_body.extend_from_slice(&write_sequence(&ias_body));
let da_alg = {
let mut b = write_oid(digest_oid);
b.extend_from_slice(&write_null()); write_sequence(&b)
};
si_body.extend_from_slice(&da_alg);
if let Some(sa) = signed_attrs_body {
si_body.extend_from_slice(&implicit_signed_attrs_tlv(sa));
}
let sig_alg = {
let mut b = write_oid(sig_oid);
b.extend_from_slice(&sig_params);
write_sequence(&b)
};
si_body.extend_from_slice(&sig_alg);
si_body.extend_from_slice(&write_octet_string(signature_bytes));
let signer_info = write_sequence(&si_body);
let da_set = write_set(&da_alg);
let eci = {
let body = write_oid(&OID_DATA);
write_sequence(&body)
};
let certs_body: Vec<u8> = cert_chain.iter().flat_map(|c| c.iter().copied()).collect();
let certs_field = write_tlv(Class::ContextSpecific, true, 0, &certs_body);
let si_set = write_set(&signer_info);
let mut sd_body = write_integer_u64(1); sd_body.extend_from_slice(&da_set);
sd_body.extend_from_slice(&eci);
sd_body.extend_from_slice(&certs_field);
sd_body.extend_from_slice(&si_set);
let sd = write_sequence(&sd_body);
let outer_body = {
let mut b = write_oid(&OID_SIGNED_DATA);
b.extend_from_slice(&write_context_constructed(0, &sd));
b
};
write_sequence(&outer_body)
}
pub fn build_content_type_attribute_der(content_type_oid: &[u64]) -> Vec<u8> {
use crate::pubsec::verify::OID_ATTR_CONTENT_TYPE;
let oid = write_oid(&OID_ATTR_CONTENT_TYPE);
let value = write_oid(content_type_oid);
let value_set = write_set(&value);
let mut body = oid;
body.extend_from_slice(&value_set);
write_sequence(&body)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn byterange_placeholder_width_is_fixed() {
let formatted = format!("/ByteRange [{:>10} {:>10} {:>10} {:>10}]", 0, 0, 0, 0);
assert_eq!(formatted.len(), BYTE_RANGE_PLACEHOLDER.len());
assert_eq!(formatted, BYTE_RANGE_PLACEHOLDER);
}
#[test]
fn patch_byte_range_in_place_preserves_length() {
let mut buf = format!("prefix {BYTE_RANGE_PLACEHOLDER} suffix").into_bytes();
let original_len = buf.len();
patch_byte_range(&mut buf, [0, 12345, 6789, 9876543210]);
assert_eq!(buf.len(), original_len);
let s = std::str::from_utf8(&buf).unwrap();
assert!(s.contains("/ByteRange [ 0 12345 6789 9876543210]"));
}
}