fix(sign): the first signature drops /NeedAppearances so Acrobat recognises it (#5939)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76andClaude Sonnet 5.5 committed 2026-11-05 01:16:17 +01:00
1 parent d22641a875
commit a62633e902
2 files changed
+67

No files matched your search

+17
View File
@@ -153,6 +153,7 @@ fn sign_inner(
}
check_signing_permissions(doc, opts)?;
let mut doc = doc.clone();
let earlier_signatures = !super::validate::signature_contents(&doc).is_empty();
let root = doc.root().ok_or_else(|| SignError::Pdf("the document has no catalog".into()))?;
let alg = id.key.preferred_digest();
let reserve =
@@ -293,6 +294,22 @@ fn sign_inner(
doc.update_dict(r, |af| af.set(b"SigFlags".to_vec(), Object::Int(3)))?;
}
}
// Acrobat does not recognise a signature in a form that asks viewers to redraw its fields
// (`/NeedAppearances true`): redrawing would change what the signature covers. The first
// signature drops the flag; a later one leaves it, as removing it would count as a change
// to the earlier signatures.
if !earlier_signatures {
if let Some(Object::Ref(r)) = doc.get(root).as_dict().and_then(|c| c.get(b"AcroForm").cloned()) {
doc.update_dict(r, |af| {
af.remove(b"NeedAppearances");
})?;
}
doc.update_dict(root, |c| {
if let Some(Object::Dict(af)) = c.get_mut(b"AcroForm") {
af.remove(b"NeedAppearances");
}
})?;
}
// The appearance.
let ap = appearance(&mut doc, rect, turn, &name, &id.certificate, opts)?;
let ap_ref = doc.add(Object::Stream(ap));
+50
View File
@@ -122,6 +122,56 @@ fn signing_then_validating_with_and_without_trust() {
}
}
/// The form dictionary of `bytes`, with the catalog's reference resolved.
fn form_of(bytes: &[u8]) -> Dict {
let doc = open(bytes);
let form = doc.get(doc.root().unwrap()).as_dict().and_then(|c| c.get(b"AcroForm").map(|a| doc.resolve(a))).unwrap();
form.as_dict().unwrap().clone()
}
/// A form with `/NeedAppearances true` makes Acrobat ignore the signature (#5939): the first
/// signature drops the flag, whether the AcroForm is inline or indirect, and leaves the rest of
/// the form alone.
#[test]
fn signing_drops_need_appearances() {
let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap();
let inline = fixture_with(b"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [6 0 R] /NeedAppearances true >> >>", &[]);
let indirect =
fixture_with(b"<< /Type /Catalog /Pages 2 0 R /AcroForm 7 0 R >>", &[b"<< /Fields [6 0 R] /NeedAppearances true /DA (/Helv 0 Tf 0 g) >>"]);
for (what, bytes) in [("inline", inline), ("indirect", indirect)] {
assert_eq!(form_of(&bytes).get(b"NeedAppearances"), Some(&Object::Bool(true)), "{what}");
let signed = pdfcraft_sign::sign(&open(&bytes), &id, &SignOptions { field: Some("Approval".into()), ..opts() }).unwrap();
let form = form_of(&signed);
assert!(form.get(b"NeedAppearances").is_none(), "{what}");
assert_eq!(form.get(b"SigFlags"), Some(&Object::Int(3)), "{what}");
assert!(form.get(b"Fields").is_some(), "{what}");
let doc = open(&signed);
assert!(signatures(&doc, &signed, &TrustStore::default()).iter().any(|s| s.field == "Approval" && s.signed), "{what}");
}
}
/// A second signer leaves the form's flags alone: changing them would count as a change to
/// what the first signature covered.
#[test]
fn a_later_signature_does_not_change_need_appearances() {
let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap();
let first = pdfcraft_sign::sign(&open(&fixture()), &id, &SignOptions { field: Some("Approval".into()), ..opts() }).unwrap();
let mut doc = open(&first);
let root = doc.root().unwrap();
doc.update_dict(root, |c| {
if let Some(Object::Dict(af)) = c.get_mut(b"AcroForm") {
af.set(b"NeedAppearances".to_vec(), Object::Bool(true));
}
})
.unwrap();
if let Some(Object::Ref(r)) = doc.get(root).as_dict().and_then(|c| c.get(b"AcroForm").cloned()) {
doc.update_dict(r, |af| af.set(b"NeedAppearances".to_vec(), Object::Bool(true))).unwrap();
}
let flagged = write_incremental(&doc, &SaveOptions { object_streams: false, ..SaveOptions::default() }).unwrap();
let second = pdfcraft_sign::sign(&open(&flagged), &id, &opts()).unwrap();
assert_eq!(form_of(&second).get(b"NeedAppearances"), Some(&Object::Bool(true)));
}
#[test]
fn signing_an_existing_field_and_counter_signing() {
let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap();