use std::collections::HashSet;
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha384, Sha512};
use zpdf_core::{ObjectId, PdfDict, PdfObject};
use zpdf_parser::PdfFile;
use crate::forms::pdf_string_to_unicode;
const MAX_FIELD_DEPTH: usize = 50;
const MAX_SIG_FIELDS: usize = 4_096;
const MAX_CMS_BYTES: usize = 4 * 1024 * 1024;
#[derive(Debug, Clone)]
pub struct Signature {
pub field_name: String,
pub filter: Option<String>,
pub sub_filter: Option<String>,
pub name: Option<String>,
pub signing_time: Option<String>,
pub location: Option<String>,
pub reason: Option<String>,
pub contact_info: Option<String>,
pub coverage: ByteRangeCoverage,
pub digest: DigestStatus,
pub crypto: CryptoStatus,
pub digest_algorithm: Option<String>,
pub signature_algorithm: Option<String>,
pub signer_common_name: Option<String>,
pub cms_blob: Option<Vec<u8>>,
}
impl Signature {
pub fn is_cryptographically_valid(&self) -> bool {
self.digest == DigestStatus::Verified && self.crypto == CryptoStatus::Valid
}
}
#[derive(Debug, Clone)]
pub struct ByteRangeCoverage {
pub ranges: Vec<(usize, usize)>,
pub covers_whole_document: bool,
pub bytes_after_signature: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DigestStatus {
Verified,
Mismatch,
Unsupported,
}
impl DigestStatus {
pub fn as_str(self) -> &'static str {
match self {
DigestStatus::Verified => "verified",
DigestStatus::Mismatch => "mismatch",
DigestStatus::Unsupported => "unsupported",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CryptoStatus {
Valid,
Invalid,
Unsupported,
}
impl CryptoStatus {
pub fn as_str(self) -> &'static str {
match self {
CryptoStatus::Valid => "valid",
CryptoStatus::Invalid => "invalid",
CryptoStatus::Unsupported => "unsupported",
}
}
}
pub fn parse_signatures(file: &PdfFile) -> Vec<Signature> {
let mut out = Vec::new();
let Some(fields) = acroform_fields(file) else {
return out;
};
let mut visited = HashSet::new();
for obj in &fields {
if let PdfObject::Ref(r) = obj {
walk(file, *r, "", None, 0, &mut visited, &mut out);
}
}
out
}
fn acroform_fields(file: &PdfFile) -> Option<Vec<PdfObject>> {
let root_ref = file.trailer.get_ref("Root").ok()?;
let root = file.resolve(root_ref).ok()?;
let root = root.as_dict().ok()?;
let af = deref(file, root.get("AcroForm")?);
let af = af.as_dict().ok()?;
match deref(file, af.get("Fields")?) {
PdfObject::Array(a) => Some(a),
_ => None,
}
}
fn walk(
file: &PdfFile,
id: ObjectId,
parent_name: &str,
inherited_ft: Option<&str>,
depth: usize,
visited: &mut HashSet<ObjectId>,
out: &mut Vec<Signature>,
) {
if depth > MAX_FIELD_DEPTH || out.len() >= MAX_SIG_FIELDS || !visited.insert(id) {
return;
}
let Ok(obj) = file.resolve(id) else { return };
let Ok(dict) = obj.as_dict() else { return };
let partial = dict
.get("T")
.and_then(|o| text_string(file, o))
.unwrap_or_default();
let name = if partial.is_empty() {
parent_name.to_string()
} else if parent_name.is_empty() {
partial
} else {
format!("{parent_name}.{partial}")
};
let ft = dict
.get_name("FT")
.ok()
.map(String::from)
.or_else(|| inherited_ft.map(String::from));
let kids = match deref(file, dict.get("Kids").unwrap_or(&PdfObject::Null)) {
PdfObject::Array(a) => a,
_ => Vec::new(),
};
let mut has_child_field = false;
for kid in &kids {
if let PdfObject::Ref(r) = kid {
let has_t = file
.resolve(*r)
.ok()
.and_then(|o| o.as_dict().ok().map(|d| d.get("T").is_some()))
.unwrap_or(false);
if has_t {
has_child_field = true;
walk(file, *r, &name, ft.as_deref(), depth + 1, visited, out);
}
}
}
if has_child_field {
return;
}
if ft.as_deref() != Some("Sig") {
return;
}
let Some(sig_dict) = deref(file, dict.get("V").unwrap_or(&PdfObject::Null))
.as_dict()
.ok()
.cloned()
else {
return;
};
out.push(build_signature(file, name, &sig_dict));
}
fn build_signature(file: &PdfFile, field_name: String, sig: &PdfDict) -> Signature {
let sub_filter = sig.get_name("SubFilter").ok().map(String::from);
let contents = match deref(file, sig.get("Contents").unwrap_or(&PdfObject::Null)) {
PdfObject::String(s) => Some(s.as_bytes().to_vec()),
_ => None,
};
let coverage = parse_byte_range(file, sig, file.data().len());
let outcome = verify(file, &coverage, contents.as_deref(), sub_filter.as_deref());
Signature {
field_name,
filter: sig.get_name("Filter").ok().map(String::from),
sub_filter,
name: sig.get("Name").and_then(|o| text_string(file, o)),
signing_time: sig.get("M").and_then(|o| text_string(file, o)),
location: sig.get("Location").and_then(|o| text_string(file, o)),
reason: sig.get("Reason").and_then(|o| text_string(file, o)),
contact_info: sig.get("ContactInfo").and_then(|o| text_string(file, o)),
coverage,
digest: outcome.digest,
crypto: outcome.crypto,
digest_algorithm: outcome.digest_algorithm,
signature_algorithm: outcome.signature_algorithm,
signer_common_name: outcome.signer_common_name,
cms_blob: contents,
}
}
struct VerifyOutcome {
digest: DigestStatus,
crypto: CryptoStatus,
digest_algorithm: Option<String>,
signature_algorithm: Option<String>,
signer_common_name: Option<String>,
}
fn parse_byte_range(file: &PdfFile, sig: &PdfDict, file_len: usize) -> ByteRangeCoverage {
let mut ranges = Vec::new();
if let PdfObject::Array(arr) = deref(file, sig.get("ByteRange").unwrap_or(&PdfObject::Null)) {
let nums: Vec<i64> = arr
.iter()
.filter_map(|o| match deref(file, o) {
PdfObject::Integer(n) => Some(n),
PdfObject::Real(r) if r.is_finite() => Some(r as i64),
_ => None,
})
.collect();
for pair in nums.chunks_exact(2) {
if let (Ok(off), Ok(len)) = (usize::try_from(pair[0]), usize::try_from(pair[1])) {
ranges.push((off, len));
}
}
}
let covers_whole_document = ranges.first().zip(ranges.last()).is_some_and(
|(&(first_off, _), &(last_off, last_len))| {
first_off == 0 && last_off.saturating_add(last_len) == file_len
},
);
let end = ranges
.last()
.map(|&(off, len)| off.saturating_add(len))
.unwrap_or(0);
let bytes_after_signature = file_len.saturating_sub(end);
ByteRangeCoverage {
ranges,
covers_whole_document,
bytes_after_signature,
}
}
fn verify(
file: &PdfFile,
coverage: &ByteRangeCoverage,
contents: Option<&[u8]>,
sub_filter: Option<&str>,
) -> VerifyOutcome {
let unsupported = VerifyOutcome {
digest: DigestStatus::Unsupported,
crypto: CryptoStatus::Unsupported,
digest_algorithm: None,
signature_algorithm: None,
signer_common_name: None,
};
let Some(cms) = contents.filter(|c| !c.is_empty() && c.len() <= MAX_CMS_BYTES) else {
return unsupported;
};
let Some(parsed) = cms::parse(cms) else {
return unsupported;
};
let digest_algorithm = parsed.digest_alg.map(|a| a.name().to_string());
let signature_algorithm = signature_alg_name(&parsed);
let signer_common_name = parsed.signer_cn.clone();
let is_detached = matches!(
sub_filter,
Some("adbe.pkcs7.detached") | Some("ETSI.CAdES.detached")
);
if !is_detached {
return VerifyOutcome {
digest: DigestStatus::Unsupported,
crypto: CryptoStatus::Unsupported,
digest_algorithm,
signature_algorithm,
signer_common_name,
};
}
let digest = match (parsed.digest_alg, parsed.message_digest.as_deref()) {
(Some(alg), Some(embedded)) => match gather_ranges(file.data(), &coverage.ranges) {
Some(spans) => {
if alg.hash(&spans) == embedded {
DigestStatus::Verified
} else {
DigestStatus::Mismatch
}
}
None => DigestStatus::Unsupported, },
_ => DigestStatus::Unsupported,
};
let crypto = verify_crypto(&parsed);
VerifyOutcome {
digest,
crypto,
digest_algorithm,
signature_algorithm,
signer_common_name,
}
}
fn verify_crypto(p: &cms::Cms) -> CryptoStatus {
let (Some(attrs), Some(sig), Some(key), Some(dalg), Some(salg)) = (
p.signed_attrs_der.as_deref(),
p.signature.as_deref(),
p.signer_key.as_ref(),
p.digest_alg,
p.sig_alg,
) else {
return CryptoStatus::Unsupported;
};
let hashed = dalg.hash(attrs);
let verified = match (salg, key.alg) {
(cms::SigAlg::Rsa, cms::KeyAlg::Rsa) => pk::rsa_verify(dalg, &key.key, &hashed, sig),
(cms::SigAlg::Ecdsa, cms::KeyAlg::EcP256) => pk::ecdsa_p256_verify(&key.key, &hashed, sig),
(cms::SigAlg::Ecdsa, cms::KeyAlg::EcP384) => pk::ecdsa_p384_verify(&key.key, &hashed, sig),
_ => return CryptoStatus::Unsupported,
};
match verified {
Some(true) => CryptoStatus::Valid,
Some(false) => CryptoStatus::Invalid,
None => CryptoStatus::Unsupported, }
}
fn signature_alg_name(p: &cms::Cms) -> Option<String> {
let salg = p.sig_alg?;
Some(match salg {
cms::SigAlg::Rsa => "RSA".to_string(),
cms::SigAlg::RsaPss => "RSA-PSS".to_string(),
cms::SigAlg::Ecdsa => match p.signer_key.as_ref().map(|k| k.alg) {
Some(cms::KeyAlg::EcP256) => "ECDSA (P-256)".to_string(),
Some(cms::KeyAlg::EcP384) => "ECDSA (P-384)".to_string(),
_ => "ECDSA".to_string(),
},
})
}
fn gather_ranges(data: &[u8], ranges: &[(usize, usize)]) -> Option<Vec<u8>> {
if ranges.is_empty() {
return None;
}
let mut buf = Vec::new();
for &(off, len) in ranges {
let end = off.checked_add(len)?;
let slice = data.get(off..end)?;
buf.extend_from_slice(slice);
}
Some(buf)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DigestAlg {
Sha1,
Sha256,
Sha384,
Sha512,
}
impl DigestAlg {
fn name(self) -> &'static str {
match self {
DigestAlg::Sha1 => "SHA-1",
DigestAlg::Sha256 => "SHA-256",
DigestAlg::Sha384 => "SHA-384",
DigestAlg::Sha512 => "SHA-512",
}
}
fn hash(self, data: &[u8]) -> Vec<u8> {
match self {
DigestAlg::Sha1 => Sha1::digest(data).to_vec(),
DigestAlg::Sha256 => Sha256::digest(data).to_vec(),
DigestAlg::Sha384 => Sha384::digest(data).to_vec(),
DigestAlg::Sha512 => Sha512::digest(data).to_vec(),
}
}
fn from_oid(oid: &[u8]) -> Option<DigestAlg> {
match oid {
[0x2b, 0x0e, 0x03, 0x02, 0x1a] => Some(DigestAlg::Sha1),
[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01] => Some(DigestAlg::Sha256),
[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02] => Some(DigestAlg::Sha384),
[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03] => Some(DigestAlg::Sha512),
_ => None,
}
}
}
mod cms {
use super::DigestAlg;
pub(super) struct Cms {
pub(super) digest_alg: Option<DigestAlg>,
pub(super) message_digest: Option<Vec<u8>>,
pub(super) signer_cn: Option<String>,
pub(super) signed_attrs_der: Option<Vec<u8>>,
pub(super) signature: Option<Vec<u8>>,
pub(super) sig_alg: Option<SigAlg>,
pub(super) signer_key: Option<PublicKeyInfo>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum SigAlg {
Rsa,
RsaPss,
Ecdsa,
}
pub(super) struct PublicKeyInfo {
pub(super) alg: KeyAlg,
pub(super) key: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum KeyAlg {
Rsa,
EcP256,
EcP384,
}
const SEQUENCE: u8 = 0x30;
const SET: u8 = 0x31;
const OID: u8 = 0x06;
const OCTET_STRING: u8 = 0x04;
const BIT_STRING: u8 = 0x03;
const CONTEXT_0: u8 = 0xA0;
const OID_SIGNED_DATA: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02];
const OID_MESSAGE_DIGEST: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x04];
const OID_CN: &[u8] = &[0x55, 0x04, 0x03];
const OID_RSA_PREFIX: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01];
const OID_RSA_PSS: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0a];
const OID_RSA_PUBLIC_KEY: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01];
const OID_EC_PUBLIC_KEY: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01];
const OID_ECDSA_PREFIX: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04];
const OID_CURVE_P256: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07];
const OID_CURVE_P384: &[u8] = &[0x2b, 0x81, 0x04, 0x00, 0x22];
fn tlv(buf: &[u8]) -> Option<(u8, &[u8], &[u8])> {
if buf.len() < 2 {
return None;
}
let tag = buf[0];
let first = buf[1];
let (len, header) = if first < 0x80 {
(first as usize, 2)
} else {
let n = (first & 0x7f) as usize;
if n == 0 || n > 4 || buf.len() < 2 + n {
return None; }
let mut len = 0usize;
for &b in &buf[2..2 + n] {
len = (len << 8) | b as usize;
}
(len, 2 + n)
};
let end = header.checked_add(len)?;
if end > buf.len() {
return None;
}
Some((tag, &buf[header..end], &buf[end..]))
}
fn children(content: &[u8], max: usize) -> Vec<(u8, &[u8])> {
let mut out = Vec::new();
let mut rest = content;
while !rest.is_empty() && out.len() < max {
let Some((tag, body, next)) = tlv(rest) else {
break;
};
out.push((tag, body));
rest = next;
}
out
}
#[allow(clippy::type_complexity)]
fn children_raw(content: &[u8], max: usize) -> Vec<(u8, &[u8], &[u8])> {
let mut out = Vec::new();
let mut rest = content;
while !rest.is_empty() && out.len() < max {
let before = rest;
let Some((tag, body, next)) = tlv(rest) else {
break;
};
let consumed = before.len() - next.len();
out.push((tag, body, &before[..consumed]));
rest = next;
}
out
}
pub(super) fn parse(blob: &[u8]) -> Option<Cms> {
let (tag, ci, _) = tlv(blob)?;
if tag != SEQUENCE {
return None;
}
let ci = children(ci, 4);
let ctype = ci.iter().find(|(t, _)| *t == OID)?;
if ctype.1 != OID_SIGNED_DATA {
return None;
}
let content = ci.iter().find(|(t, _)| *t == CONTEXT_0)?;
let (tag, signed_data, _) = tlv(content.1)?;
if tag != SEQUENCE {
return None;
}
let sd = children(signed_data, 16);
let signer_infos = sd.iter().rev().find(|(t, _)| *t == SET)?;
let certs = sd.iter().find(|(t, _)| *t == CONTEXT_0).map(|(_, c)| *c);
let (tag, signer_info, _) = tlv(signer_infos.1)?;
if tag != SEQUENCE {
return None;
}
let si = children_raw(signer_info, 16);
let seq_oid = |seq: &[u8]| -> Option<Vec<u8>> {
children(seq, 2)
.iter()
.find(|(t, _)| *t == OID)
.map(|(_, oid)| oid.to_vec())
};
let digest_alg = si
.iter()
.filter(|(t, _, _)| *t == SEQUENCE)
.find_map(|(_, seq, _)| seq_oid(seq).and_then(|oid| DigestAlg::from_oid(&oid)));
let sig_alg = si
.iter()
.filter(|(t, _, _)| *t == SEQUENCE)
.find_map(|(_, seq, _)| seq_oid(seq).and_then(|oid| sig_alg_from_oid(&oid)));
let signed_attrs = si.iter().find(|(t, _, _)| *t == CONTEXT_0);
let message_digest = signed_attrs.and_then(|(_, attrs, _)| find_message_digest(attrs));
let signed_attrs_der = signed_attrs.map(|(_, _, full)| {
let mut der = full.to_vec();
der[0] = SET;
der
});
let signature = si
.iter()
.find(|(t, _, _)| *t == OCTET_STRING)
.map(|(_, body, _)| body.to_vec());
let signer_cn = certs.and_then(first_cert_cn);
let signer_key = certs.and_then(first_cert_public_key);
Some(Cms {
digest_alg,
message_digest,
signer_cn,
signed_attrs_der,
signature,
sig_alg,
signer_key,
})
}
fn sig_alg_from_oid(oid: &[u8]) -> Option<SigAlg> {
if oid == OID_RSA_PSS {
Some(SigAlg::RsaPss)
} else if oid.starts_with(OID_RSA_PREFIX) {
Some(SigAlg::Rsa)
} else if oid.starts_with(OID_ECDSA_PREFIX) {
Some(SigAlg::Ecdsa)
} else {
None
}
}
fn find_message_digest(attrs: &[u8]) -> Option<Vec<u8>> {
for (tag, attr) in children(attrs, 64) {
if tag != SEQUENCE {
continue;
}
let parts = children(attr, 4);
let is_md = parts
.iter()
.find(|(t, _)| *t == OID)
.is_some_and(|(_, oid)| *oid == OID_MESSAGE_DIGEST);
if !is_md {
continue;
}
let values = parts.iter().find(|(t, _)| *t == SET)?;
let (vtag, digest, _) = tlv(values.1)?;
if vtag == OCTET_STRING {
return Some(digest.to_vec());
}
}
None
}
fn first_cert_cn(certs: &[u8]) -> Option<String> {
let (tag, cert, _) = tlv(certs)?;
if tag != SEQUENCE {
return None;
}
let (tag, tbs, _) = tlv(cert)?;
if tag != SEQUENCE {
return None;
}
let subject = children(tbs, 16)
.into_iter()
.filter(|(t, _)| *t == SEQUENCE)
.nth(3)?;
for (tag, rdn) in children(subject.1, 32) {
if tag != SET {
continue;
}
for (tag, atv) in children(rdn, 8) {
if tag != SEQUENCE {
continue;
}
let parts = children(atv, 2);
let is_cn = parts
.iter()
.find(|(t, _)| *t == OID)
.is_some_and(|(_, oid)| *oid == OID_CN);
if is_cn {
if let Some((vtag, value)) = parts.iter().rev().find(|(t, _)| *t != OID) {
return Some(decode_directory_string(*vtag, value));
}
}
}
}
None
}
fn first_cert_public_key(certs: &[u8]) -> Option<PublicKeyInfo> {
let (tag, cert, _) = tlv(certs)?;
if tag != SEQUENCE {
return None;
}
let (tag, tbs, _) = tlv(cert)?;
if tag != SEQUENCE {
return None;
}
let spki = children(tbs, 16)
.into_iter()
.filter(|(t, _)| *t == SEQUENCE)
.nth(4)?;
let spki_parts = children(spki.1, 2);
let alg_id = spki_parts.iter().find(|(t, _)| *t == SEQUENCE)?.1;
let bit_string = spki_parts.iter().find(|(t, _)| *t == BIT_STRING)?.1;
let key_bytes = bit_string
.split_first()
.and_then(|(unused, rest)| (*unused == 0).then(|| rest.to_vec()))?;
let alg_parts = children(alg_id, 2);
let alg_oid = alg_parts.iter().find(|(t, _)| *t == OID)?.1;
if alg_oid == OID_RSA_PUBLIC_KEY {
Some(PublicKeyInfo {
alg: KeyAlg::Rsa,
key: key_bytes,
})
} else if alg_oid == OID_EC_PUBLIC_KEY {
let curve = alg_parts
.iter()
.filter(|(t, _)| *t == OID)
.nth(1)
.map(|(_, oid)| *oid)?;
let alg = if curve == OID_CURVE_P256 {
KeyAlg::EcP256
} else if curve == OID_CURVE_P384 {
KeyAlg::EcP384
} else {
return None;
};
Some(PublicKeyInfo {
alg,
key: key_bytes,
})
} else {
None
}
}
fn decode_directory_string(tag: u8, value: &[u8]) -> String {
const BMP_STRING: u8 = 0x1e;
if tag == BMP_STRING {
let units: Vec<u16> = value
.chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&units)
} else {
String::from_utf8_lossy(value).into_owned()
}
}
}
mod pk {
use super::DigestAlg;
use rsa::pkcs1::DecodeRsaPublicKey;
use rsa::{Pkcs1v15Sign, RsaPublicKey};
use sha1::Sha1;
use sha2::{Sha256, Sha384, Sha512};
pub(super) fn rsa_verify(
alg: DigestAlg,
key_der: &[u8],
hashed: &[u8],
sig: &[u8],
) -> Option<bool> {
let key = RsaPublicKey::from_pkcs1_der(key_der).ok()?;
let scheme = match alg {
DigestAlg::Sha1 => Pkcs1v15Sign::new::<Sha1>(),
DigestAlg::Sha256 => Pkcs1v15Sign::new::<Sha256>(),
DigestAlg::Sha384 => Pkcs1v15Sign::new::<Sha384>(),
DigestAlg::Sha512 => Pkcs1v15Sign::new::<Sha512>(),
};
Some(key.verify(scheme, hashed, sig).is_ok())
}
pub(super) fn ecdsa_p256_verify(point: &[u8], hashed: &[u8], sig: &[u8]) -> Option<bool> {
use p256::ecdsa::signature::hazmat::PrehashVerifier;
use p256::ecdsa::{Signature, VerifyingKey};
let key = VerifyingKey::from_sec1_bytes(point).ok()?;
let sig = Signature::from_der(sig).ok()?;
Some(key.verify_prehash(hashed, &sig).is_ok())
}
pub(super) fn ecdsa_p384_verify(point: &[u8], hashed: &[u8], sig: &[u8]) -> Option<bool> {
use p384::ecdsa::signature::hazmat::PrehashVerifier;
use p384::ecdsa::{Signature, VerifyingKey};
let key = VerifyingKey::from_sec1_bytes(point).ok()?;
let sig = Signature::from_der(sig).ok()?;
Some(key.verify_prehash(hashed, &sig).is_ok())
}
}
fn deref(file: &PdfFile, obj: &PdfObject) -> PdfObject {
match obj {
PdfObject::Ref(r) => file.resolve(*r).unwrap_or(PdfObject::Null),
other => other.clone(),
}
}
fn text_string(file: &PdfFile, obj: &PdfObject) -> Option<String> {
match deref(file, obj) {
PdfObject::String(s) => Some(pdf_string_to_unicode(s.as_bytes())),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn der(tag: u8, content: &[u8]) -> Vec<u8> {
let mut out = vec![tag];
let len = content.len();
if len < 0x80 {
out.push(len as u8);
} else if len < 0x100 {
out.push(0x81);
out.push(len as u8);
} else {
out.push(0x82);
out.push((len >> 8) as u8);
out.push((len & 0xff) as u8);
}
out.extend_from_slice(content);
out
}
const SEQ: u8 = 0x30;
const SET: u8 = 0x31;
const OID: u8 = 0x06;
const OCTET: u8 = 0x04;
const INT: u8 = 0x02;
const CTX0: u8 = 0xA0;
fn synth_cms(digest: &[u8]) -> Vec<u8> {
let sha256_oid = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01];
let md_oid = [0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x04];
let digest_alg = der(SEQ, &der(OID, &sha256_oid));
let md_attr = der(
SEQ,
&[der(OID, &md_oid), der(SET, &der(OCTET, digest))].concat(),
);
let signed_attrs = der(CTX0, &md_attr);
let signer_info = der(
SEQ,
&[
der(INT, &[1]),
der(SEQ, &[]), digest_alg.clone(),
signed_attrs,
der(SEQ, &der(OID, &[0x2a])), der(OCTET, &[0xde, 0xad]), ]
.concat(),
);
let signer_infos = der(SET, &signer_info);
let signed_data = der(
SEQ,
&[
der(INT, &[1]),
der(SET, &digest_alg),
der(
SEQ,
&der(OID, &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x01]),
),
signer_infos,
]
.concat(),
);
let signed_data_oid = [0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02];
der(
SEQ,
&[der(OID, &signed_data_oid), der(CTX0, &signed_data)].concat(),
)
}
#[test]
fn cms_extracts_digest_and_algorithm() {
let digest: Vec<u8> = (0u8..32).collect();
let blob = synth_cms(&digest);
let parsed = cms::parse(&blob).expect("cms");
assert_eq!(parsed.digest_alg, Some(DigestAlg::Sha256));
assert_eq!(parsed.message_digest.as_deref(), Some(digest.as_slice()));
}
#[test]
fn cms_rejects_truncated_blob() {
let blob = synth_cms(&[0u8; 32]);
for cut in 1..blob.len() {
let _ = cms::parse(&blob[..cut]);
}
}
#[test]
fn cms_rejects_indefinite_length() {
assert!(cms::parse(&[0x30, 0x80, 0x00, 0x00]).is_none());
}
#[test]
fn digest_alg_oid_mapping() {
assert_eq!(
DigestAlg::from_oid(&[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01]),
Some(DigestAlg::Sha256)
);
assert_eq!(
DigestAlg::from_oid(&[0x2b, 0x0e, 0x03, 0x02, 0x1a]),
Some(DigestAlg::Sha1)
);
assert_eq!(DigestAlg::from_oid(&[0x00]), None);
}
#[test]
fn gather_ranges_bounds_checked() {
let data = b"0123456789";
assert_eq!(
gather_ranges(data, &[(0, 3), (7, 3)]).as_deref(),
Some(&b"012789"[..])
);
assert!(gather_ranges(data, &[(0, 3), (7, 99)]).is_none());
assert!(gather_ranges(data, &[]).is_none());
}
#[test]
fn sha256_matches_reference() {
let d = DigestAlg::Sha256.hash(b"abc");
assert_eq!(
d,
hex(b"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")
);
}
fn hex(h: &[u8]) -> Vec<u8> {
h.chunks_exact(2)
.map(|c| {
let s = std::str::from_utf8(c).unwrap();
u8::from_str_radix(s, 16).unwrap()
})
.collect()
}
use crate::test_util::build_pdf;
use zpdf_parser::PdfFile;
#[test]
fn out_of_range_byte_range_reports_unsupported() {
let pdf = build_pdf(&[
"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>",
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>",
"<< /Fields [5 0 R] >>",
"<< /FT /Sig /T (S1) /V << /ByteRange [0 100 200 999999] /Contents <aabbcc> >> >>",
]);
let file = PdfFile::parse(pdf.as_slice()).expect("parse");
let sigs = parse_signatures(&file);
assert_eq!(sigs.len(), 1);
assert_eq!(sigs[0].digest, DigestStatus::Unsupported);
}
#[test]
fn malformed_cms_contents_do_not_hang() {
let pdf = build_pdf(&[
"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>",
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>",
"<< /Fields [5 0 R 6 0 R 7 0 R] >>",
"<< /FT /Sig /T (Truncated) /V << /ByteRange [0 10 20 30] /Contents <30304142> >> >>",
"<< /FT /Sig /T (Empty) /V << /ByteRange [0 10 20 30] /Contents <> >> >>",
"<< /FT /Sig /T (Indefinite) /V << /ByteRange [0 10 20 30] /Contents <308000> >> >>",
]);
let file = PdfFile::parse(pdf.as_slice()).expect("parse");
let sigs = parse_signatures(&file);
assert_eq!(sigs.len(), 3);
for s in &sigs {
assert_eq!(s.digest, DigestStatus::Unsupported);
}
}
#[test]
fn deep_field_tree_terminates() {
let mut objs = vec![
"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".to_string(),
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_string(),
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>".to_string(),
"<< /Fields [5 0 R] >>".to_string(),
];
for i in 0..100 {
let next = if i < 99 {
format!("{} 0 R", 5 + i + 1)
} else {
"null".to_string()
};
objs.push(format!("<< /T (Field{i}) /FT /Sig /Kids [{}] >>", next));
}
let pdf = build_pdf(&objs.iter().map(|s| s.as_str()).collect::<Vec<_>>());
let file = PdfFile::parse(pdf.as_slice()).expect("parse");
let sigs = parse_signatures(&file);
assert!(sigs.len() < 100);
}
}