mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 12:32:52 +00:00
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:
1 parent
d22641a875
commit
a62633e902
2 files changed
+67
No files matched your search
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user