fix(sign): judge a shared signature value once per listing; dedupe DSS evidence by object number

This commit is contained in:
Dylan M. Taylor
2026-11-03 21:01:11 +00:00
parent ecdd1e658c
commit 229769d562
3 changed files with 49 additions and 2 deletions
+13
View File
@@ -268,6 +268,9 @@ pub fn list_cached(doc: &Document, bytes: &[u8], trust: &TrustStore, cache: &Dig
let pages = annot_pages(doc);
let mut out = Vec::new();
let mut field_values: HashSet<ObjRef> = HashSet::new();
// Fields that name one signature value are judged once: validation does not depend on the
// field, and repeating it per field would repeat the digest, chain and evidence work.
let mut judged: HashMap<ObjRef, SignatureInfo> = HashMap::new();
for f in sig_fields(doc) {
if let Some(v_ref) = f.dict.get(b"V").and_then(Object::as_ref) {
field_values.insert(v_ref);
@@ -310,9 +313,19 @@ pub fn list_cached(doc: &Document, bytes: &[u8], trust: &TrustStore, cache: &Dig
modification: Modification::None,
details: Vec::new(),
};
let shared = f.dict.get(b"V").and_then(Object::as_ref).filter(|_| v.is_some());
if let Some(first) = shared.and_then(|r| judged.get(&r)) {
let own =
SignatureInfo { field: info.field, signed: info.signed, page: info.page, rect: info.rect, visible: info.visible, ..first.clone() };
out.push(own);
continue;
}
if let Some(v) = v {
validate_into(doc, bytes, trust, &v, &mut info, cache);
}
if let Some(r) = shared {
judged.insert(r, info.clone());
}
out.push(info);
}
// Standalone document timestamps (ISO 32000-2 §12.8.2.2) live outside AcroForm fields.
+4 -2
View File
@@ -340,7 +340,9 @@ pub(crate) const MAX_DSS_BYTES: usize = 64 << 20;
pub(crate) struct DssEvidence<'a> {
doc: &'a Document,
items: std::vec::IntoIter<Object>,
seen: HashSet<ObjRef>,
/// Object numbers read: `Document::get` resolves a reference by number, so `5 0 R` and `5 1 R`
/// are one stream.
seen: HashSet<u32>,
taken: usize,
left: usize,
/// A cap stopped the array being read in full.
@@ -364,7 +366,7 @@ impl Iterator for DssEvidence<'_> {
// Repeats are skipped before the caps count anything, so repeating one stream cannot use
// them up; the scan of the array itself is bounded by the parser's cap on array length.
for item in self.items.by_ref() {
if item.as_ref().is_some_and(|r| !self.seen.insert(r)) {
if item.as_ref().is_some_and(|r| !self.seen.insert(r.num)) {
continue;
}
let o = self.doc.resolve(&item);
+32
View File
@@ -2954,3 +2954,35 @@ fn signing_a_field_with_a_null_value_after_another_signature_keeps_the_first_val
all.iter().map(|s| (&s.status, &s.modification)).collect::<Vec<_>>()
);
}
/// Fields that name one signature value are listed each, under their own name, with the verdict
/// the value earned once.
#[test]
fn fields_sharing_a_signature_value_are_listed_with_one_verdict() {
let id = pkcs12::open(&data("rsa-aes.p12"), "test").unwrap();
let trust = TrustStore { certs: vec![id.certificate.clone()] };
let signed = pdfcraft_sign::sign(&open(&fixture()), &id, &SignOptions { field: Some("Approval".into()), ..opts() }).unwrap();
let v = open(&signed).get(pdfcraft_cos::ObjRef { num: 6, generation: 0 }).as_dict().and_then(|d| d.get(b"V").cloned()).unwrap();
let shared = edit_after(&signed, |doc| {
let mut w = pdfcraft_cos::Dict::new();
w.set(b"Type".to_vec(), Object::name("Annot"));
w.set(b"Subtype".to_vec(), Object::name("Widget"));
w.set(b"FT".to_vec(), Object::name("Sig"));
w.set(b"T".to_vec(), PdfString::text("Twin"));
w.set(b"V".to_vec(), v.clone());
w.set(b"Rect".to_vec(), Object::Array(vec![Object::Int(0); 4]));
let w = doc.add(Object::Dict(w));
let root = doc.root().unwrap();
let af = doc.get(root).as_dict().and_then(|c| c.get(b"AcroForm").cloned()).unwrap();
let af = doc.resolve(&af).as_dict().cloned().unwrap();
let mut fields = af.get(b"Fields").map(|f| doc.resolve(f)).and_then(|f| f.as_array().cloned()).unwrap();
fields.push(Object::Ref(w));
let mut af = af;
af.set(b"Fields".to_vec(), Object::Array(fields));
doc.update_dict(root, |c| c.set(b"AcroForm".to_vec(), Object::Dict(af))).unwrap();
});
let list = signatures(&open(&shared), &shared, &trust);
let (a, b) = (list.iter().find(|s| s.field == "Approval").unwrap(), list.iter().find(|s| s.field == "Twin").unwrap());
assert!(a.signed && b.signed);
assert_eq!((a.status, &a.modification, &a.signer, a.signed_len), (b.status, &b.modification, &b.signer, b.signed_len));
}