use sha1::{Digest, Sha1};
use super::incremental::{
assemble_incremental_update, detect_xref_stream, find_root_obj_num, find_startxref_offset,
patch_byterange, PreparedPdf, UpdatePlan,
};
use super::objects::{build_catalog_override, build_page_override, RawObject, SigObjectNums};
use super::position::{
compute_sig_rect, resolve_page_index, PageSpec, Position, SigRect, SIG_HEIGHT, SIG_MARGIN_H,
SIG_MARGIN_V, SIG_WIDTH,
};
use super::reader::{ObjRef, PageInfo, PdfReader};
use super::render::{
build_annot_widget, build_embedded_font_objects, build_form_xobjects,
build_invisible_annot_widget, build_sig_dict,
};
use crate::appearance::{build_appearance_stream, get_font, load_signature_image, make_date_str};
use crate::{Result, RevenantError};
const SHA1_LEN: usize = 20;
#[derive(Debug, Clone)]
pub struct PrepareOptions<'a> {
pub page: PageSpec,
pub position: Position,
pub manual_xy: Option<(f64, f64)>,
pub size: (f64, f64),
pub reason: &'a str,
pub name: Option<&'a str>,
pub image_path: Option<&'a str>,
pub fields: Option<Vec<String>>,
pub visible: bool,
pub font: Option<&'a str>,
}
impl Default for PrepareOptions<'_> {
fn default() -> Self {
Self {
page: PageSpec::Index(0),
position: Position::BottomRight,
manual_xy: None,
size: (SIG_WIDTH, SIG_HEIGHT),
reason: "",
name: None,
image_path: None,
fields: None,
visible: true,
font: None,
}
}
}
pub fn prepare_pdf_with_sig_field(
pdf_bytes: &[u8],
opts: &PrepareOptions<'_>,
) -> Result<PreparedPdf> {
let root = find_root_obj_num(pdf_bytes)?;
if root.gen != 0 {
return Err(RevenantError::Pdf(format!(
"Catalog /Root has generation {}; only generation 0 is supported.",
root.gen
)));
}
let reader = PdfReader::open(pdf_bytes)?;
if reader.is_encrypted() {
return Err(RevenantError::Pdf(
"PDF is encrypted; encrypted documents cannot be signed with an \
embedded signature. Remove the password/encryption and try again."
.to_owned(),
));
}
let page_index = resolve_page_index(opts.page, reader.page_count())?;
let page_info = reader.page_info(page_index)?;
let prev_xref = find_startxref_offset(pdf_bytes)?;
let prev_size = u32::try_from(reader.size()?)
.map_err(|_| RevenantError::Pdf("PDF /Size is negative or too large.".to_owned()))?;
let trailer_extra = reader.trailer_carry_forward();
let use_xref_stream = detect_xref_stream(pdf_bytes, prev_xref);
let (raw_objects, new_size) = if opts.visible {
prepare_visible(&reader, prev_size, root.num, &page_info, opts)?
} else {
prepare_invisible(&reader, prev_size, root.num, &page_info, opts)?
};
let plan = UpdatePlan {
raw_objects: &raw_objects,
new_size,
prev_xref,
root,
trailer_extra: &trailer_extra,
use_xref_stream,
};
let full_pdf = assemble_incremental_update(pdf_bytes, &plan)?;
patch_byterange(full_pdf, pdf_bytes.len())
}
fn prepare_visible(
reader: &PdfReader,
prev_size: u32,
root_obj_num: u32,
page_info: &PageInfo,
opts: &PrepareOptions<'_>,
) -> Result<(Vec<RawObject>, u32)> {
let font = get_font(opts.font)?;
let (w, h) = opts.size;
let rect = match opts.manual_xy {
Some((x, y)) => {
if !(w > 0.0 && h > 0.0 && [x, y, w, h].iter().all(|v| v.is_finite())) {
return Err(RevenantError::Pdf(format!(
"Invalid manual signature geometry: x={x}, y={y}, w={w}, h={h}"
)));
}
SigRect { x, y, w, h }
}
None => compute_sig_rect(
page_info.width,
page_info.height,
opts.position,
w,
h,
SIG_MARGIN_H,
SIG_MARGIN_V,
)?,
};
let img_data = match opts.image_path {
Some(path) => Some(load_signature_image(path)?),
None => None,
};
let image_aspect = img_data
.as_ref()
.and_then(|d| (d.height > 0).then(|| f64::from(d.width) / f64::from(d.height)));
let default_fields;
let display_fields: &[String] = if let Some(fields) = &opts.fields {
fields
} else {
let name = opts
.name
.filter(|n| !n.is_empty())
.unwrap_or("Digital Signature");
default_fields = vec![name.to_owned(), format!("Date: {}", make_date_str())];
&default_fields
};
let ap_info = build_appearance_stream(
rect.w,
rect.h,
display_fields,
opts.image_path.is_some(),
font,
image_aspect,
);
let has_image = img_data.is_some();
let has_smask = img_data.as_ref().is_some_and(|d| d.smask.is_some());
let obj_nums = SigObjectNums::allocate(prev_size, has_image, has_smask, true);
let Some(v) = obj_nums.visible else {
return Err(RevenantError::Pdf(
"Internal error: visible allocation produced no appearance objects.".to_owned(),
));
};
let sig = build_sig_dict(obj_nums.sig, opts.reason, opts.name);
let font_objects = build_embedded_font_objects(v.fonts, font)?;
let forms = build_form_xobjects(&v, rect.w, rect.h, &ap_info, img_data.as_ref());
let annot = build_annot_widget(
obj_nums.sig,
obj_nums.annot,
v.forms.ap,
page_info.obj_num,
rect,
);
let annots_list = build_annots_list(&page_info.annots, obj_nums.annot);
let page_override = build_page_override(reader, page_info.obj_num, &annots_list)?;
let catalog_override = build_catalog_override(reader, root_obj_num, obj_nums.annot)?;
let mut objects: Vec<RawObject> = Vec::new();
objects.push((sig, obj_nums.sig));
objects.push((annot, obj_nums.annot));
objects.extend(font_objects);
objects.push((forms.n0, v.forms.n0));
objects.push((forms.n2, v.forms.n2));
objects.push((forms.frm, v.forms.frm));
objects.push((forms.ap, v.forms.ap));
if let (Some(bytes), Some(num)) = (forms.img, v.img) {
objects.push((bytes, num));
}
if let (Some(bytes), Some(num)) = (forms.smask, v.smask) {
objects.push((bytes, num));
}
objects.push((page_override, page_info.obj_num));
objects.push((catalog_override, root_obj_num));
Ok((objects, obj_nums.new_size))
}
fn prepare_invisible(
reader: &PdfReader,
prev_size: u32,
root_obj_num: u32,
page_info: &PageInfo,
opts: &PrepareOptions<'_>,
) -> Result<(Vec<RawObject>, u32)> {
let obj_nums = SigObjectNums::allocate(prev_size, false, false, false);
let sig = build_sig_dict(obj_nums.sig, opts.reason, opts.name);
let annot = build_invisible_annot_widget(obj_nums.sig, obj_nums.annot, page_info.obj_num);
let annots_list = build_annots_list(&page_info.annots, obj_nums.annot);
let page_override = build_page_override(reader, page_info.obj_num, &annots_list)?;
let catalog_override = build_catalog_override(reader, root_obj_num, obj_nums.annot)?;
let objects = vec![
(sig, obj_nums.sig),
(annot, obj_nums.annot),
(page_override, page_info.obj_num),
(catalog_override, root_obj_num),
];
Ok((objects, obj_nums.new_size))
}
fn build_annots_list(existing: &[ObjRef], annot_num: u32) -> String {
use std::fmt::Write as _;
let mut list = String::new();
for obj_ref in existing {
let _ = write!(list, "{obj_ref} ");
}
let _ = write!(list, "{annot_num} 0 R");
list
}
pub fn compute_byterange_hash(
pdf_bytes: &[u8],
hex_start: usize,
hex_len: usize,
) -> Result<[u8; SHA1_LEN]> {
let Some(end) = hex_start.checked_add(hex_len) else {
return Err(RevenantError::Pdf(format!(
"Invalid hex range: start={hex_start}, len={hex_len} overflows pdf_size={}",
pdf_bytes.len()
)));
};
if hex_start == 0 || end >= pdf_bytes.len() {
return Err(RevenantError::Pdf(format!(
"Invalid hex range: start={hex_start}, len={hex_len}, pdf_size={}",
pdf_bytes.len()
)));
}
if pdf_bytes.get(hex_start - 1) != Some(&b'<') {
return Err(RevenantError::Pdf(
"Malformed Contents field: expected '<' before hex data".to_owned(),
));
}
if pdf_bytes.get(end) != Some(&b'>') {
return Err(RevenantError::Pdf(
"Malformed Contents field: expected '>' after hex data".to_owned(),
));
}
let mut hasher = Sha1::new();
hasher.update(&pdf_bytes[..hex_start]);
hasher.update(&pdf_bytes[end + 1..]);
Ok(hasher.finalize().into())
}
pub fn insert_cms(
pdf_bytes: &[u8],
hex_start: usize,
hex_len: usize,
cms_der: &[u8],
) -> Result<Vec<u8>> {
let cms_hex = hex::encode(cms_der);
if cms_hex.len() > hex_len {
return Err(RevenantError::Pdf(format!(
"CMS too large: {} hex chars > {hex_len} reserved",
cms_hex.len()
)));
}
let end = hex_start
.checked_add(hex_len)
.ok_or_else(|| RevenantError::Pdf("Contents region offset overflows.".to_owned()))?;
let mut out = pdf_bytes.to_vec();
let region = out
.get_mut(hex_start..end)
.ok_or_else(|| RevenantError::Pdf("Contents region is out of range.".to_owned()))?;
let (filled, padding) = region.split_at_mut(cms_hex.len());
filled.copy_from_slice(cms_hex.as_bytes());
padding.fill(b'0');
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cms::{extract_signature_data, find_byteranges};
const BLANK_LETTER: &[u8] = include_bytes!("testdata/blank_letter.pdf");
const TWO_PAGE_A4: &[u8] = include_bytes!("testdata/two_page_a4.pdf");
const XREF_STREAM: &[u8] = include_bytes!("testdata/blank_letter_xref_stream.pdf");
const ENCRYPTED: &[u8] = include_bytes!("testdata/encrypted.pdf");
#[test]
fn rejects_encrypted_pdf() {
let err = prepare_pdf_with_sig_field(ENCRYPTED, &PrepareOptions::default()).unwrap_err();
assert!(matches!(err, RevenantError::Pdf(_)));
assert!(err.to_string().contains("encrypted"));
}
fn fake_cms() -> Vec<u8> {
let mut der = vec![0x30, 0x81, 0xC8]; der.extend(std::iter::repeat_n(0xAB, 200));
der
}
fn sha1(data: &[u8]) -> [u8; 20] {
let mut hasher = Sha1::new();
hasher.update(data);
hasher.finalize().into()
}
fn assert_round_trip(pdf: &[u8], opts: &PrepareOptions<'_>, expect_pages: usize) {
let PreparedPdf {
bytes: prepared,
contents_hex_offset: hex_start,
contents_hex_len: hex_len,
} = prepare_pdf_with_sig_field(pdf, opts).unwrap();
let reader = PdfReader::open(&prepared).unwrap();
assert_eq!(reader.page_count(), expect_pages);
assert_eq!(find_byteranges(&prepared).unwrap().len(), 1);
let hash_before = compute_byterange_hash(&prepared, hex_start, hex_len).unwrap();
let cms = fake_cms();
let signed = insert_cms(&prepared, hex_start, hex_len, &cms).unwrap();
let (signed_data, extracted_cms) = extract_signature_data(&signed).unwrap();
assert_eq!(sha1(&signed_data), hash_before);
assert_eq!(extracted_cms, cms);
}
#[test]
fn round_trip_visible_letter() {
let opts = PrepareOptions {
reason: "Approved",
name: Some("John Doe"),
..Default::default()
};
assert_round_trip(BLANK_LETTER, &opts, 1);
}
#[test]
fn round_trip_invisible() {
let opts = PrepareOptions {
visible: false,
reason: "Invisible",
name: Some("Jane"),
..Default::default()
};
assert_round_trip(BLANK_LETTER, &opts, 1);
}
#[test]
fn round_trip_xref_stream_source() {
let opts = PrepareOptions {
name: Some("XRef Stream"),
..Default::default()
};
assert_round_trip(XREF_STREAM, &opts, 1);
}
#[test]
fn round_trip_last_page_of_two() {
let opts = PrepareOptions {
page: PageSpec::Last,
position: Position::TopLeft,
name: Some("Page 2"),
..Default::default()
};
assert_round_trip(TWO_PAGE_A4, &opts, 2);
}
#[test]
fn round_trip_with_explicit_fields_and_font() {
let opts = PrepareOptions {
fields: Some(vec!["Ստորագրող".to_owned(), "SSN: 1234567890".to_owned()]),
font: Some("ghea-mariam"),
..Default::default()
};
assert_round_trip(BLANK_LETTER, &opts, 1);
}
#[test]
fn build_annots_list_appends() {
assert_eq!(build_annots_list(&[], 7), "7 0 R");
assert_eq!(
build_annots_list(&[ObjRef::new(3, 0), ObjRef::new(4, 0)], 7),
"3 0 R 4 0 R 7 0 R"
);
}
#[test]
fn insert_cms_pads_and_rejects_oversize() {
let pdf = b"<<<<<<<<<<<<<<<<<<<<".to_vec();
let out = insert_cms(&pdf, 0, 20, &[0xAB, 0xCD]).unwrap();
assert_eq!(&out[..4], b"abcd");
assert!(out[4..20].iter().all(|&b| b == b'0'));
assert!(insert_cms(&pdf, 0, 2, &[0xAB, 0xCD]).is_err());
}
}