From b167422b2e3429c32d0a2e5ff5f2660bce517f1e Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 06:22:54 -0500 Subject: [PATCH] fix(sign): every signature's VRI entry shares the store's evidence arrays instead of copying them (#7038) --- crates/sign/src/dss.rs | 25 +++++++++++++++---------- crates/sign/tests/it/pdf.rs | 7 +++++++ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/crates/sign/src/dss.rs b/crates/sign/src/dss.rs index 0c76ca1967..7e5fe5f7ad 100644 --- a/crates/sign/src/dss.rs +++ b/crates/sign/src/dss.rs @@ -69,12 +69,17 @@ pub fn embed(doc: &Document, evidence: &Evidence) -> Result, SignError> let mut dss = Dict::new(); dss.set(b"Type".to_vec(), Object::name("DSS")); dss.set(b"Filter".to_vec(), Object::name("Adobe.PPKLite")); - dss.set(b"Certs".to_vec(), Object::Ref(doc.add(Object::Array(cert_refs.clone())))); - if !ocsp_refs.is_empty() { - dss.set(b"OCSPs".to_vec(), Object::Ref(doc.add(Object::Array(ocsp_refs.clone())))); + // Each list is one indirect array the store and every signature's VRI entry point to, so the + // output does not grow with signatures times evidence. + let cert_list = Object::Ref(doc.add(Object::Array(cert_refs))); + let ocsp_list = (!ocsp_refs.is_empty()).then(|| Object::Ref(doc.add(Object::Array(ocsp_refs)))); + let crl_list = (!crl_refs.is_empty()).then(|| Object::Ref(doc.add(Object::Array(crl_refs)))); + dss.set(b"Certs".to_vec(), cert_list.clone()); + if let Some(l) = &ocsp_list { + dss.set(b"OCSPs".to_vec(), l.clone()); } - if !crl_refs.is_empty() { - dss.set(b"CRLs".to_vec(), Object::Ref(doc.add(Object::Array(crl_refs.clone())))); + if let Some(l) = &crl_list { + dss.set(b"CRLs".to_vec(), l.clone()); } // Keep any existing /VRI entries, then cover every signature in the file. let mut vri = @@ -82,12 +87,12 @@ pub fn embed(doc: &Document, evidence: &Evidence) -> Result, SignError> for contents in crate::pdf::signature_contents(&doc) { let mut e = Dict::new(); e.set(b"Type".to_vec(), Object::name("VRI")); - e.set(b"Cert".to_vec(), Object::Array(cert_refs.clone())); - if !ocsp_refs.is_empty() { - e.set(b"OCSP".to_vec(), Object::Array(ocsp_refs.clone())); + e.set(b"Cert".to_vec(), cert_list.clone()); + if let Some(l) = &ocsp_list { + e.set(b"OCSP".to_vec(), l.clone()); } - if !crl_refs.is_empty() { - e.set(b"CRL".to_vec(), Object::Array(crl_refs.clone())); + if let Some(l) = &crl_list { + e.set(b"CRL".to_vec(), l.clone()); } vri.set(vri_key(&contents).into_bytes(), Object::Dict(e)); } diff --git a/crates/sign/tests/it/pdf.rs b/crates/sign/tests/it/pdf.rs index 5d24d05dce..6e88830a7a 100644 --- a/crates/sign/tests/it/pdf.rs +++ b/crates/sign/tests/it/pdf.rs @@ -1050,6 +1050,13 @@ fn embedding_ltv_evidence_adds_a_dss_and_keeps_signatures_valid() { doc.get(root).as_dict().unwrap().get(b"DSS").map(|d| doc.resolve(d)).and_then(|d| d.as_dict().cloned()).unwrap(); assert_eq!(dss_dict.name(b"Type"), Some(b"DSS".as_slice())); assert!(dss_dict.contains(b"Certs") && dss_dict.contains(b"OCSPs") && dss_dict.contains(b"CRLs") && dss_dict.contains(b"VRI")); + // A signature's /VRI entry points at the store's arrays instead of holding its own copy of each. + let vri = dss_dict.get(b"VRI").map(|v| doc.resolve(v)).and_then(|v| v.as_dict().cloned()).unwrap(); + let entry = vri.iter().next().and_then(|(_, e)| e.as_dict().cloned()).unwrap(); + for (vri_key, dss_key) in [(&b"Cert"[..], &b"Certs"[..]), (b"OCSP", b"OCSPs"), (b"CRL", b"CRLs")] { + assert!(matches!(entry.get(vri_key), Some(pdfcraft_cos::Object::Ref(_)))); + assert_eq!(entry.get(vri_key), dss_dict.get(dss_key)); + } // The earlier signature still validates; the DSS counts as a permitted change. let s = signatures(&doc, <v, &TrustStore::default()).into_iter().find(|s| s.signed && !s.doc_timestamp).unwrap(); assert_eq!(s.status, Status::Unknown);