use std::path::{Path, PathBuf};
use anyhow::{Result, anyhow, bail};
use clap::Args;
use cms::builder::{SignedDataBuilder, SignerInfoBuilder};
use cms::cert::{CertificateChoices, IssuerAndSerialNumber};
use cms::content_info::ContentInfo;
use cms::signed_data::{EncapsulatedContentInfo, SignedData, SignerIdentifier, SignerInfo};
use der::asn1::ObjectIdentifier;
use der::{Decode, Encode};
use lopdf::{Dictionary, Document, Object, StringFormat};
use rsa::pkcs1::DecodeRsaPrivateKey;
use rsa::pkcs8::{DecodePrivateKey, DecodePublicKey};
use rsa::signature::Verifier;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::{Value, json};
use sha2::{Digest, Sha256, Sha384, Sha512};
use x509_cert::Certificate;
use x509_cert::spki::AlgorithmIdentifierOwned;
use crate::doc;
#[derive(Args, Deserialize, JsonSchema, Debug)]
#[serde(deny_unknown_fields)]
pub struct SignArgs {
pub input: PathBuf,
#[arg(short, long)]
pub output: PathBuf,
#[arg(long)]
pub cert: Option<PathBuf>,
#[arg(long)]
pub key: Option<PathBuf>,
#[arg(long)]
pub p12: Option<PathBuf>,
#[arg(long)]
pub p12_password: Option<String>,
#[arg(long)]
pub reason: Option<String>,
#[arg(long)]
pub location: Option<String>,
#[arg(long)]
pub name: Option<String>,
}
#[derive(Args, Deserialize, JsonSchema, Debug)]
#[serde(deny_unknown_fields)]
pub struct SignaturesArgs {
pub input: PathBuf,
#[arg(long)]
pub password: Option<String>,
}
const SHA1: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.14.3.2.26");
const SHA256: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.2.1");
const SHA384: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.2.2");
const SHA512: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.2.3");
const DATA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.7.1");
const SIGNED_DATA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.7.2");
const MESSAGE_DIGEST: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.4");
const SIGNING_TIME: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.5");
const RSA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1");
const EC: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.2.1");
const COMMON_NAME: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.4.3");
const HOLE: usize = 16384;
const RANGE_PLACEHOLDER: i64 = 9_999_999_999;
enum Key {
Rsa(Box<rsa::RsaPrivateKey>),
P256(p256::ecdsa::SigningKey),
}
struct Identity {
key: Key,
certs: Vec<Certificate>,
}
fn parse_key(pem_or_der: &[u8]) -> Result<Key> {
let unsupported =
|| anyhow!("the private key is not an RSA or P-256 key in PKCS #8 or PKCS #1 form");
if let Ok(text) = std::str::from_utf8(pem_or_der) {
if text.contains("ENCRYPTED PRIVATE KEY") {
bail!(
"the private key is encrypted; decrypt it first or use a PKCS #12 file with its password"
);
}
if text.contains("BEGIN RSA PRIVATE KEY") {
return Ok(Key::Rsa(Box::new(
rsa::RsaPrivateKey::from_pkcs1_pem(text).map_err(|_| unsupported())?,
)));
}
if text.contains("BEGIN PRIVATE KEY") {
if let Ok(key) = rsa::RsaPrivateKey::from_pkcs8_pem(text) {
return Ok(Key::Rsa(Box::new(key)));
}
return p256::ecdsa::SigningKey::from_pkcs8_pem(text)
.map(Key::P256)
.map_err(|_| unsupported());
}
}
if let Ok(key) = rsa::RsaPrivateKey::from_pkcs8_der(pem_or_der) {
return Ok(Key::Rsa(Box::new(key)));
}
p256::ecdsa::SigningKey::from_pkcs8_der(pem_or_der)
.map(Key::P256)
.map_err(|_| unsupported())
}
impl Key {
fn public_der(&self) -> Result<Vec<u8>> {
use rsa::pkcs8::EncodePublicKey;
Ok(match self {
Key::Rsa(key) => key.to_public_key().to_public_key_der()?.into_vec(),
Key::P256(key) => key.verifying_key().to_public_key_der()?.into_vec(),
})
}
}
fn load_identity(a: &SignArgs) -> Result<Identity> {
let read = |path: &Path| {
std::fs::read(path).map_err(|e| anyhow!("cannot read {}: {e}", path.display()))
};
let (key, mut certs) = match (&a.p12, &a.cert, &a.key) {
(Some(p12), None, None) => {
let store = p12_keystore::KeyStore::from_pkcs12(
&read(p12)?,
a.p12_password.as_deref().unwrap_or(""),
p12_keystore::Pkcs12ImportPolicy::Relaxed,
)
.map_err(|e| anyhow!("cannot open {}: {e} (wrong password?)", p12.display()))?;
let (_, chain) = store
.private_key_chain()
.ok_or_else(|| anyhow!("{} holds no private key", p12.display()))?;
let certs = chain
.certs()
.iter()
.map(|c| {
Certificate::from_der(c.as_der())
.map_err(|e| anyhow!("unreadable certificate: {e}"))
})
.collect::<Result<Vec<_>>>()?;
(parse_key(chain.key().as_der())?, certs)
}
(None, Some(cert), Some(key)) => {
let certs = Certificate::load_pem_chain(&read(cert)?)
.map_err(|e| anyhow!("{} is not a PEM certificate: {e}", cert.display()))?;
(parse_key(&read(key)?)?, certs)
}
_ => bail!("give either a PKCS #12 file, or a certificate together with its key"),
};
let public = key.public_der()?;
let own = certs
.iter()
.position(|c| {
c.tbs_certificate
.subject_public_key_info
.to_der()
.is_ok_and(|der| der == public)
})
.ok_or_else(|| anyhow!("none of the certificates belongs to the private key"))?;
certs.swap(0, own);
Ok(Identity { key, certs })
}
fn common_name(name: &x509_cert::name::Name) -> Option<String> {
name.0
.iter()
.flat_map(|rdn| rdn.0.iter())
.find(|atv| atv.oid == COMMON_NAME)
.and_then(|atv| {
let bytes = atv.value.value();
std::str::from_utf8(bytes).ok().map(str::to_string)
})
}
fn cms_signature(identity: &Identity, digest: &[u8]) -> Result<Vec<u8>> {
let content = EncapsulatedContentInfo {
econtent_type: DATA,
econtent: None,
};
let algorithm = AlgorithmIdentifierOwned {
oid: SHA256,
parameters: None,
};
let own = &identity.certs[0];
let signer_id = SignerIdentifier::IssuerAndSerialNumber(IssuerAndSerialNumber {
issuer: own.tbs_certificate.issuer.clone(),
serial_number: own.tbs_certificate.serial_number.clone(),
});
let mut builder = SignedDataBuilder::new(&content);
builder
.add_digest_algorithm(algorithm.clone())
.map_err(|e| anyhow!("cannot build the signature: {e}"))?;
for cert in &identity.certs {
builder
.add_certificate(CertificateChoices::Certificate(cert.clone()))
.map_err(|e| anyhow!("cannot build the signature: {e}"))?;
}
let failed = |e: cms::builder::Error| anyhow!("cannot build the signature: {e}");
let signed = match &identity.key {
Key::Rsa(key) => {
let signer = rsa::pkcs1v15::SigningKey::<Sha256>::new((**key).clone());
let info =
SignerInfoBuilder::new(&signer, signer_id, algorithm, &content, Some(digest))
.map_err(failed)?;
builder
.add_signer_info::<_, rsa::pkcs1v15::Signature>(info)
.map_err(failed)?
.build()
.map_err(failed)?
}
Key::P256(key) => {
let info = SignerInfoBuilder::new(key, signer_id, algorithm, &content, Some(digest))
.map_err(failed)?;
builder
.add_signer_info::<_, p256::ecdsa::DerSignature>(info)
.map_err(failed)?
.build()
.map_err(failed)?
}
};
Ok(signed.to_der()?)
}
fn civil(days: i64) -> (i64, u32, u32) {
let z = days + 719_468;
let era = z.div_euclid(146_097);
let day_of_era = z.rem_euclid(146_097);
let year_of_era =
(day_of_era - day_of_era / 1460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
let month_index = (5 * day_of_year + 2) / 153;
let day = (day_of_year - (153 * month_index + 2) / 5 + 1) as u32;
let month = if month_index < 10 {
month_index + 3
} else {
month_index - 9
} as u32;
(year_of_era + era * 400 + i64::from(month <= 2), month, day)
}
fn pdf_date(seconds: u64) -> String {
let (year, month, day) = civil((seconds / 86_400) as i64);
let rest = seconds % 86_400;
format!(
"D:{year:04}{month:02}{day:02}{:02}{:02}{:02}Z",
rest / 3600,
rest % 3600 / 60,
rest % 60
)
}
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack.windows(needle.len()).position(|w| w == needle)
}
fn add_signature(
source: &Document,
target: &mut Document,
signer: Option<&str>,
a: &SignArgs,
now: u64,
) -> Result<()> {
let wide = Object::Integer(RANGE_PLACEHOLDER);
let mut sig = Dictionary::new();
sig.set("Type", Object::Name(b"Sig".to_vec()));
sig.set("Filter", Object::Name(b"Adobe.PPKLite".to_vec()));
sig.set("SubFilter", Object::Name(b"adbe.pkcs7.detached".to_vec()));
sig.set(
"ByteRange",
vec![Object::Integer(0), wide.clone(), wide.clone(), wide],
);
sig.set(
"Contents",
Object::String(vec![0; HOLE], StringFormat::Hexadecimal),
);
sig.set("M", Object::string_literal(pdf_date(now)));
for (key, value) in [
("Name", signer),
("Reason", a.reason.as_deref()),
("Location", a.location.as_deref()),
] {
if let Some(value) = value.filter(|v| !v.is_empty()) {
sig.set(key, lopdf::text_string(value));
}
}
let sig = target.add_object(sig);
let page = *doc::page_ids(source)
.first()
.ok_or_else(|| anyhow!("the document has no pages"))?;
let catalog_id = doc::catalog_id(source)?;
let catalog = source.get_dictionary(catalog_id)?;
let form_entry = catalog.get(b"AcroForm").ok().cloned();
let mut form = form_entry
.as_ref()
.and_then(|f| doc::resolve(source, f).as_dict().ok())
.cloned()
.unwrap_or_default();
let mut fields: Vec<Object> = form
.get(b"Fields")
.ok()
.and_then(|f| doc::resolve(source, f).as_array().ok())
.cloned()
.unwrap_or_default();
let mut field = Dictionary::new();
field.set("Type", Object::Name(b"Annot".to_vec()));
field.set("Subtype", Object::Name(b"Widget".to_vec()));
field.set("FT", Object::Name(b"Sig".to_vec()));
field.set(
"T",
Object::string_literal(format!("Signature{}", fields.len() + 1)),
);
field.set("V", sig);
field.set("P", page);
field.set("Rect", vec![Object::Integer(0); 4]);
field.set("F", 132);
let field = target.add_object(field);
fields.push(Object::Reference(field));
form.set("Fields", fields);
form.set("SigFlags", 3);
match form_entry {
Some(Object::Reference(id)) => target.set_object(id, form),
_ => {
let mut catalog = catalog.clone();
catalog.set("AcroForm", form);
target.set_object(catalog_id, catalog);
}
}
let mut page_dict = source.get_dictionary(page)?.clone();
let mut annots = page_dict
.get(b"Annots")
.ok()
.and_then(|o| doc::resolve(source, o).as_array().ok())
.cloned()
.unwrap_or_default();
annots.push(Object::Reference(field));
page_dict.set("Annots", annots);
target.set_object(page, page_dict);
Ok(())
}
pub fn sign(a: SignArgs) -> Result<Value> {
let identity = load_identity(&a)?;
let (mut d, rebuilt) = doc::load_noting(&a.input, None)?;
if d.was_encrypted() {
bail!(
"{} is encrypted; remove the protection first with the decrypt command",
a.input.display()
);
}
let earlier = signature_dicts(&d).len();
let signer = a
.name
.clone()
.or_else(|| common_name(&identity.certs[0].tbs_certificate.subject));
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |t| t.as_secs());
let original =
std::fs::read(&a.input).map_err(|e| anyhow!("cannot read {}: {e}", a.input.display()))?;
let appended = (|| -> Result<Vec<u8>> {
if rebuilt {
bail!("the file was damaged and had to be rebuilt");
}
let mut update = lopdf::IncrementalDocument::create_from(original.clone(), d.clone());
let mut addition = std::mem::take(&mut update.new_document);
add_signature(&d, &mut addition, signer.as_deref(), &a, now)?;
update.new_document = addition;
let mut bytes = Vec::new();
update.save_to(&mut bytes)?;
if find(&bytes[original.len()..], b"/Prev").is_none() {
bail!("the original cross-reference section cannot be chained to");
}
Ok(bytes)
})();
let (mut bytes, searched_from, kept) = match appended {
Ok(bytes) => (bytes, original.len(), true),
Err(_) => {
let source = d.clone();
add_signature(&source, &mut d, signer.as_deref(), &a, now)?;
doc::seal(&mut d)?;
let mut bytes = Vec::new();
d.save_to(&mut bytes)?;
(bytes, 0, false)
}
};
let hole = format!("/Contents<{}>", "0".repeat(HOLE * 2));
let at = searched_from
+ find(&bytes[searched_from..], hole.as_bytes())
.ok_or_else(|| anyhow!("cannot locate the signature in the written file"))?;
let (start, end) = (at + "/Contents".len(), at + hole.len());
let placeholder =
format!("/ByteRange[0 {RANGE_PLACEHOLDER} {RANGE_PLACEHOLDER} {RANGE_PLACEHOLDER}]");
let range_at = searched_from
+ find(&bytes[searched_from..], placeholder.as_bytes())
.ok_or_else(|| anyhow!("cannot locate the byte range in the written file"))?;
let range = format!("/ByteRange[0 {start} {end} {}]", bytes.len() - end);
let padded = format!("{range:<width$}", width = placeholder.len());
bytes[range_at..range_at + placeholder.len()].copy_from_slice(padded.as_bytes());
let mut hasher = Sha256::new();
hasher.update(&bytes[..start]);
hasher.update(&bytes[end..]);
let cms = cms_signature(&identity, &hasher.finalize())?;
if cms.len() > HOLE {
bail!(
"the certificate chain is too large to embed ({} bytes)",
cms.len()
);
}
let hex: String = cms.iter().map(|b| format!("{b:02X}")).collect();
bytes[start + 1..start + 1 + hex.len()].copy_from_slice(hex.as_bytes());
doc::write_atomic(&a.output, &bytes)?;
Ok(json!({
"output": a.output,
"signer": signer,
"algorithm": match identity.key { Key::Rsa(_) => "RSA with SHA-256", Key::P256(_) => "ECDSA P-256 with SHA-256" },
"certificates_embedded": identity.certs.len(),
"earlier_signatures": earlier,
"earlier_signatures_kept": kept || earlier == 0,
"size_bytes": bytes.len(),
}))
}
fn signature_dicts(d: &Document) -> Vec<(String, &Dictionary)> {
let mut found = Vec::new();
for object in d.objects.values() {
let Ok(field) = object.as_dict() else {
continue;
};
if field.get(b"FT").ok().and_then(|t| t.as_name().ok()) != Some(b"Sig") {
continue;
}
let value = field
.get(b"V")
.ok()
.map(|v| doc::resolve(d, v))
.and_then(|v| v.as_dict().ok());
if let Some(sig) = value.filter(|s| s.has(b"ByteRange") && s.has(b"Contents")) {
let name = field
.get(b"T")
.ok()
.and_then(|t| doc::text(doc::resolve(d, t)))
.unwrap_or_default();
found.push((name, sig));
}
}
found.sort_by(|a, b| a.0.cmp(&b.0));
found
}
fn der_length(bytes: &[u8]) -> Option<usize> {
let first = *bytes.get(1)? as usize;
if first < 0x80 {
return Some(2 + first);
}
let count = first & 0x7f;
let length = bytes
.get(2..2 + count)?
.iter()
.fold(0usize, |n, b| (n << 8) | *b as usize);
Some(2 + count + length)
}
fn raw_signatures(file: &[u8]) -> Vec<([usize; 4], Vec<u8>)> {
let pattern = regex::bytes::Regex::new(r"/ByteRange\s*\[\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s*\]")
.expect("a valid pattern");
let mut found = Vec::new();
for captures in pattern.captures_iter(file) {
let number = |i: usize| {
std::str::from_utf8(&captures[i])
.ok()
.and_then(|n| n.parse::<usize>().ok())
};
let (Some(a), Some(b), Some(c), Some(len)) = (number(1), number(2), number(3), number(4))
else {
continue;
};
let gap = a
.checked_add(b)
.and_then(|start| file.get(start..c))
.unwrap_or_default();
let digits: Vec<u8> = gap.iter().copied().filter(u8::is_ascii_hexdigit).collect();
let contents = digits
.as_chunks::<2>()
.0
.iter()
.filter_map(|pair| u8::from_str_radix(std::str::from_utf8(pair).ok()?, 16).ok())
.collect();
found.push(([a, b, c, len], contents));
}
found
}
fn verify([a, b, c, len]: [usize; 4], contents: &[u8], file: &[u8]) -> Value {
let mut out = json!({
"trust": "not checked: pdfops has no list of trusted certificate authorities",
});
let (Some(first), Some(second)) = (
file.get(a..a.saturating_add(b)),
c.checked_add(len).and_then(|end| file.get(c..end)),
) else {
out["problem"] = json!("the byte range lies outside the file");
out["valid"] = json!(false);
return out;
};
out["covers_whole_document"] = json!(a == 0 && c + len == file.len());
let parsed = der_length(contents)
.and_then(|n| contents.get(..n))
.and_then(|der| ContentInfo::from_der(der).ok())
.filter(|info| info.content_type == SIGNED_DATA)
.and_then(|info| info.content.decode_as::<SignedData>().ok());
let Some(data) = parsed else {
out["problem"] = json!("the signature is not a readable CMS structure");
out["valid"] = json!(false);
return out;
};
let Some(info) = data.signer_infos.0.iter().next() else {
out["problem"] = json!("the signature names no signer");
out["valid"] = json!(false);
return out;
};
let digest: Vec<u8> = match info.digest_alg.oid {
oid if oid == SHA256 => Sha256::new()
.chain_update(first)
.chain_update(second)
.finalize()
.to_vec(),
oid if oid == SHA384 => Sha384::new()
.chain_update(first)
.chain_update(second)
.finalize()
.to_vec(),
oid if oid == SHA512 => Sha512::new()
.chain_update(first)
.chain_update(second)
.finalize()
.to_vec(),
oid if oid == SHA1 => {
out["problem"] = json!("the signature uses SHA-1, which is no longer checked");
out["valid"] = json!(false);
return out;
}
other => {
out["problem"] = json!(format!("unsupported digest algorithm {other}"));
out["valid"] = json!(false);
return out;
}
};
let attribute = |oid: ObjectIdentifier| {
info.signed_attrs
.as_ref()
.and_then(|attrs| attrs.iter().find(|a| a.oid == oid))
.and_then(|a| a.values.iter().next())
};
let stated = attribute(MESSAGE_DIGEST).map(|v| v.value().to_vec());
let unchanged = stated.as_deref() == Some(&digest[..]);
out["document_unchanged"] = json!(unchanged);
if let Some(time) =
attribute(SIGNING_TIME).and_then(|v| v.decode_as::<der::asn1::UtcTime>().ok())
{
out["signed_at"] = json!(pdf_date(time.to_unix_duration().as_secs()));
}
let certificate = signer_certificate(&data, info);
if let Some(cert) = certificate {
let tbs = &cert.tbs_certificate;
out["certificate"] = json!({
"subject": tbs.subject.to_string(),
"issuer": tbs.issuer.to_string(),
"valid_from": pdf_date(tbs.validity.not_before.to_unix_duration().as_secs()),
"valid_to": pdf_date(tbs.validity.not_after.to_unix_duration().as_secs()),
"self_signed": tbs.subject == tbs.issuer,
});
out["signer"] = json!(common_name(&tbs.subject));
}
let genuine = certificate
.zip(info.signed_attrs.as_ref())
.and_then(|(cert, attrs)| {
let message = attrs.to_der().ok()?;
let spki = cert.tbs_certificate.subject_public_key_info.to_der().ok()?;
let signature = info.signature.as_bytes();
let algorithm = cert.tbs_certificate.subject_public_key_info.algorithm.oid;
if algorithm == RSA {
let key = rsa::RsaPublicKey::from_public_key_der(&spki).ok()?;
let signature = rsa::pkcs1v15::Signature::try_from(signature).ok()?;
Some(match info.digest_alg.oid {
oid if oid == SHA384 => rsa::pkcs1v15::VerifyingKey::<Sha384>::new(key)
.verify(&message, &signature)
.is_ok(),
oid if oid == SHA512 => rsa::pkcs1v15::VerifyingKey::<Sha512>::new(key)
.verify(&message, &signature)
.is_ok(),
_ => rsa::pkcs1v15::VerifyingKey::<Sha256>::new(key)
.verify(&message, &signature)
.is_ok(),
})
} else if algorithm == EC && info.digest_alg.oid == SHA256 {
let key = p256::ecdsa::VerifyingKey::from_public_key_der(&spki).ok()?;
let signature = p256::ecdsa::DerSignature::from_bytes(signature).ok()?;
Some(key.verify(&message, &signature).is_ok())
} else {
None
}
});
out["signature_genuine"] = json!(genuine);
out["valid"] = json!(unchanged && genuine == Some(true));
if genuine.is_none() {
out["problem"] =
json!("the signature algorithm is not one pdfops can check (RSA or ECDSA P-256)");
}
out
}
fn signer_certificate<'a>(data: &'a SignedData, info: &SignerInfo) -> Option<&'a Certificate> {
let certs: Vec<&Certificate> = data
.certificates
.as_ref()?
.0
.iter()
.filter_map(|c| match c {
CertificateChoices::Certificate(cert) => Some(cert),
_ => None,
})
.collect();
match &info.sid {
SignerIdentifier::IssuerAndSerialNumber(id) => certs
.iter()
.find(|c| {
c.tbs_certificate.issuer == id.issuer
&& c.tbs_certificate.serial_number == id.serial_number
})
.copied(),
_ => certs.first().copied(),
}
}
pub fn signatures(a: SignaturesArgs) -> Result<Value> {
let file =
std::fs::read(&a.input).map_err(|e| anyhow!("cannot read {}: {e}", a.input.display()))?;
let parsed = doc::read(&a.input, a.password.as_deref()).ok();
let stated: Vec<(String, &Dictionary)> = parsed
.as_ref()
.map(|(d, _)| signature_dicts(d))
.unwrap_or_default();
let found: Vec<Value> = raw_signatures(&file)
.into_iter()
.map(|(range, contents)| {
let mut entry = verify(range, &contents, &file);
let own = parsed.as_ref().zip(stated.iter().find(|(_, sig)| {
sig.get(b"ByteRange")
.ok()
.and_then(|r| r.as_array().ok())
.is_some_and(|r| {
r.iter()
.filter_map(|o| o.as_i64().ok())
.map(|n| n as usize)
.eq(range)
})
}));
if let Some(((d, _), (field, sig))) = own {
entry["field"] = json!(field);
for (name, key) in [
("reason", &b"Reason"[..]),
("location", b"Location"),
("sub_filter", b"SubFilter"),
] {
if let Some(value) = sig
.get(key)
.ok()
.and_then(|o| doc::text(doc::resolve(d, o)))
{
entry[name] = json!(value);
}
}
if entry.get("signed_at").is_none() {
entry["signed_at"] = json!(
sig.get(b"M")
.ok()
.and_then(|o| doc::text(doc::resolve(d, o)))
);
}
}
entry
})
.collect();
let valid = found.iter().filter(|s| s["valid"] == true).count();
Ok(json!({"file": a.input, "signatures": found.len(), "valid": valid, "details": found}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dates_convert_from_unix_time() {
assert_eq!(pdf_date(0), "D:19700101000000Z");
assert_eq!(pdf_date(951_782_400), "D:20000229000000Z");
assert_eq!(pdf_date(1_791_203_696), "D:20261005123456Z");
assert_eq!(civil(-1), (1969, 12, 31));
}
#[test]
fn der_length_reads_short_and_long_forms() {
assert_eq!(der_length(&[0x30, 0x03, 1, 2, 3, 0, 0]), Some(5));
assert_eq!(der_length(&[0x30, 0x82, 0x01, 0x00]), Some(260));
assert_eq!(der_length(&[0x30]), None);
}
}