use crate::error::PdfError;
use crate::objects::Object;
use crate::pubsec::cms::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_RSA_ENCRYPTION, OID_SHA256,
};
use crate::sig::writer::{build_content_type_attribute_der, SignerIdentity};
pub const OID_CT_TST_INFO: [u64; 9] = [1, 2, 840, 113549, 1, 9, 16, 1, 4];
const TST_CONTENTS_HEX_LEN: usize = 16384;
const BYTE_RANGE_PLACEHOLDER: &str = "/ByteRange [ 0 0 0 0]";
#[derive(Debug, Clone)]
pub struct MessageImprint {
pub hash_alg: HashAlg,
pub hashed_message: Vec<u8>,
}
impl MessageImprint {
pub fn hash_alg_oid(&self) -> &'static [u64] {
match self.hash_alg {
HashAlg::Sha256 => &OID_SHA256,
other => {
panic!("Round-34 doc-timestamp: hash algorithm {other:?} not wired (SHA-256 only)")
}
}
}
}
pub trait TsaSigner {
fn timestamp(&self, imprint: &MessageImprint) -> Result<Vec<u8>, PdfError>;
}
pub struct MockTsaSigner {
private_key: rsa::RsaPrivateKey,
identity: SignerIdentity,
policy_oid: Vec<u64>,
gen_time: Vec<u8>,
serial: Vec<u8>,
}
impl MockTsaSigner {
pub fn new(
private_key: rsa::RsaPrivateKey,
identity: SignerIdentity,
gen_time: impl Into<Vec<u8>>,
) -> Result<Self, PdfError> {
let gen_time = gen_time.into();
if gen_time.len() != 15 || !gen_time.ends_with(b"Z") {
return Err(PdfError::other(
"MockTsaSigner: gen_time must be 15 ASCII bytes YYYYMMDDHHMMSSZ",
));
}
Ok(Self {
private_key,
identity,
policy_oid: vec![2, 25, 42],
gen_time,
serial: vec![0x01],
})
}
pub fn with_serial(mut self, serial: impl Into<Vec<u8>>) -> Self {
self.serial = serial.into();
self
}
pub fn with_policy_oid(mut self, oid: impl Into<Vec<u64>>) -> Self {
self.policy_oid = oid.into();
self
}
}
impl TsaSigner for MockTsaSigner {
fn timestamp(&self, imprint: &MessageImprint) -> Result<Vec<u8>, PdfError> {
let tst_info = build_tst_info(imprint, &self.policy_oid, &self.serial, &self.gen_time);
let tst_hash = HashAlg::Sha256.hash(&tst_info);
let md_attr = build_message_digest_attribute_der(&tst_hash);
let ct_attr = build_content_type_attribute_der(&OID_CT_TST_INFO);
let attrs_body = pack_signed_attrs_implicit(&[ct_attr, md_attr]);
let tbs = signed_attrs_to_be_signed(&attrs_body);
let tbs_hash = HashAlg::Sha256.hash(&tbs);
use rsa::pkcs1v15::Pkcs1v15Sign;
use rsa::traits::SignatureScheme;
use sha2::Sha256;
let sig_bytes = Pkcs1v15Sign::new::<Sha256>()
.sign(
None::<&mut rsa::rand_core::OsRng>,
&self.private_key,
&tbs_hash,
)
.map_err(|e| PdfError::other(format!("MockTsaSigner: RSA sign failed: {e}")))?;
Ok(wrap_tst_in_signed_data(
&tst_info,
&self.identity.issuer_der,
&self.identity.serial,
&self.identity.cert_chain,
Some(&attrs_body),
&sig_bytes,
))
}
}
pub fn build_tst_info(
imprint: &MessageImprint,
policy_oid: &[u64],
serial: &[u8],
gen_time_ascii: &[u8],
) -> Vec<u8> {
let version = write_integer_u64(1);
let policy = write_oid(policy_oid);
let hash_alg_oid = imprint.hash_alg_oid();
let hash_alg = write_sequence(&{
let mut b = write_oid(hash_alg_oid);
b.extend_from_slice(&write_null()); b
});
let mi = write_sequence(&{
let mut b = hash_alg;
b.extend_from_slice(&write_octet_string(&imprint.hashed_message));
b
});
let serial_field = write_integer_bytes(serial);
let gen_time = write_tlv(Class::Universal, false, 24, gen_time_ascii);
write_sequence(&{
let mut b = version;
b.extend_from_slice(&policy);
b.extend_from_slice(&mi);
b.extend_from_slice(&serial_field);
b.extend_from_slice(&gen_time);
b
})
}
pub fn wrap_tst_in_signed_data(
tst_info_der: &[u8],
signer_issuer_der: &[u8],
signer_serial: &[u8],
cert_chain: &[Vec<u8>],
signed_attrs_body: Option<&[u8]>,
signature_bytes: &[u8],
) -> Vec<u8> {
let digest_oid = &OID_SHA256;
let sig_oid = &OID_RSA_ENCRYPTION;
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(&write_null());
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 mut body = write_oid(&OID_CT_TST_INFO);
let oct = write_octet_string(tst_info_der);
body.extend_from_slice(&write_context_constructed(0, &oct));
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(3); 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 add_document_timestamp<T: TsaSigner>(pdf: &[u8], tsa: &T) -> Result<Vec<u8>, PdfError> {
let (mut out, byte_range, contents_hex_offset) = append_doctimestamp_revision(pdf)?;
patch_byte_range(&mut out, byte_range);
let signed = concat_byte_ranges(&out, byte_range)?;
let imprint = MessageImprint {
hash_alg: HashAlg::Sha256,
hashed_message: HashAlg::Sha256.hash(&signed),
};
let tst = tsa.timestamp(&imprint)?;
patch_contents(&mut out, contents_hex_offset, &tst)?;
Ok(out)
}
fn append_doctimestamp_revision(base: &[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 {
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 {
Object::Dict(d) => d,
_ => {
return Err(PdfError::other(
"add_document_timestamp: previous /Root is not a Dict",
));
}
};
let existing_acroform = catalog_dict
.entries()
.iter()
.find(|(k, _)| k == "AcroForm")
.map(|(_, v)| v.clone());
let acroform_id = prev_max_id + 1;
let ts_field_id = prev_max_id + 2;
let ts_sigdict_id = prev_max_id + 3;
let mut acroform_dict = crate::objects::Dict::new();
let mut sig_flags: i64 = 3;
if let Some(acro_obj) = existing_acroform.clone() {
let resolved = reader.deref(acro_obj)?;
if let Object::Dict(existing) = resolved {
let mut fields = Vec::new();
for (k, v) in existing.entries() {
if k == "Fields" {
if let Object::Array(items) = v {
for item in items {
fields.push(item.clone());
}
}
} else if k == "SigFlags" {
if let Object::Integer(n) = v {
sig_flags |= *n;
}
} else {
acroform_dict.set(k, v.clone());
}
}
fields.push(Object::Reference(crate::objects::ObjectId::new(
ts_field_id,
)));
acroform_dict.set("Fields", Object::Array(fields));
} else {
acroform_dict.set(
"Fields",
Object::Array(vec![Object::Reference(crate::objects::ObjectId::new(
ts_field_id,
))]),
);
}
} else {
acroform_dict.set(
"Fields",
Object::Array(vec![Object::Reference(crate::objects::ObjectId::new(
ts_field_id,
))]),
);
}
acroform_dict.set("SigFlags", Object::Integer(sig_flags));
catalog_dict.set(
"AcroForm",
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_offset = write_indirect_dict(&mut out, acroform_id, &acroform_dict)?;
let ts_field_dict = crate::objects::Dict::new()
.with("FT", Object::Name("Sig".to_string()))
.with("T", Object::LiteralString(b"DocTimeStamp1".to_vec()))
.with(
"V",
Object::Reference(crate::objects::ObjectId::new(ts_sigdict_id)),
);
let ts_field_offset = write_indirect_dict(&mut out, ts_field_id, &ts_field_dict)?;
let sigdict_offset = out.len();
out.extend_from_slice(format!("{ts_sigdict_id} 0 obj\n").as_bytes());
out.extend_from_slice(b"<< /Type /DocTimeStamp /Filter /Adobe.PPKLite ");
out.extend_from_slice(b"/SubFilter /ETSI.RFC3161 ");
out.extend_from_slice(b"/V 0 ");
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() + TST_CONTENTS_HEX_LEN, b'0');
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!("{ts_field_offset:010} 00000 n \n").as_bytes());
out.extend_from_slice(format!("{sigdict_offset:010} 00000 n \n").as_bytes());
let new_size = (ts_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 + TST_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());
let placeholder = BYTE_RANGE_PLACEHOLDER.as_bytes();
if let Some(pos) = pdf
.windows(placeholder.len())
.rposition(|w| w == placeholder)
{
pdf[pos..pos + placeholder.len()].copy_from_slice(formatted.as_bytes());
}
}
fn patch_contents(
pdf: &mut [u8],
contents_hex_offset: usize,
tst_der: &[u8],
) -> Result<(), PdfError> {
let hex_len_needed = tst_der.len() * 2;
if hex_len_needed > TST_CONTENTS_HEX_LEN {
return Err(PdfError::other(format!(
"add_document_timestamp: TST {} hex chars exceeds /Contents budget {}",
hex_len_needed, TST_CONTENTS_HEX_LEN
)));
}
for (i, b) in tst_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(TST_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(
"add_document_timestamp: 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(
"add_document_timestamp: /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)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tst_info_is_well_formed_der() {
let imprint = MessageImprint {
hash_alg: HashAlg::Sha256,
hashed_message: vec![0u8; 32],
};
let tst = build_tst_info(&imprint, &[2, 25, 42], &[0x01], b"20260517000000Z");
assert_eq!(tst[0], 0x30, "TSTInfo must start with SEQUENCE tag");
let (tlv, rest) = crate::pubsec::der::read_tlv(&tst).expect("read TSTInfo");
assert!(rest.is_empty(), "no trailing bytes after TSTInfo");
assert_eq!(tlv.tag_number, 16); }
#[test]
fn mock_tsa_rejects_short_gen_time() {
let issuer_der = crate::pubsec::der::write_sequence(b"O=R34 Unit");
let identity = SignerIdentity {
issuer_der,
serial: vec![0x01],
cert_chain: Vec::new(),
};
let mut rng = rsa::rand_core::OsRng;
let pk = rsa::RsaPrivateKey::new(&mut rng, 2048).expect("RSA gen");
match MockTsaSigner::new(pk, identity, b"D:20260517".to_vec()) {
Ok(_) => panic!("MockTsaSigner accepted invalid gen_time"),
Err(e) => {
let msg = format!("{e}");
assert!(
msg.contains("YYYYMMDDHHMMSSZ"),
"expected gen_time validation error, got {msg}"
);
}
}
}
#[test]
fn message_imprint_oid_dispatches_sha256() {
let mi = MessageImprint {
hash_alg: HashAlg::Sha256,
hashed_message: vec![],
};
assert_eq!(mi.hash_alg_oid(), &OID_SHA256);
}
}