Phase 8 #6 of REVIEWS/DART_PDF_VS_MAKEPAD_PDF_GAP_ANALYSIS.md
("Signatures"). Design and merge criteria in
REVIEWS/adr/0010-pdf-signatures.md.
Two defects, and the first is not about signatures at all.
1. acroform() dereferenced /AcroForm with self.resolve(), which recurses.
That replaced every /Fields [5 0 R] entry with an inline dictionary,
so AcroForm::walk saw node.as_ref() == None, decided the field had no
identity to key an edit on, and dropped it. The form came back EMPTY
rather than wrong, which is indistinguishable from a document with no
fields, so nothing announced the loss.
This is the third appearance of one mistake: page_annotations once
destroyed every annotation's obj_ref the same way, and
extract_xobjects resolved a reference then asked the resolved object
for as_ref(), leaving every page's XObject map empty.
It was invisible because both existing fixtures declare /AcroForm as
an INDIRECT reference, where only one level is dereferenced and the
refs survive. A direct /AcroForm dictionary - equally legal - hits it.
The new fixture uses one deliberately; reverting the one-line fix
fails 9 of the 12 new acceptance tests.
2. Nothing read signatures. FieldType::Signature was classified and then
ignored; /ByteRange, /Contents, /SubFilter and /DocMDP appear nowhere
in the codebase. A signed contract was presented exactly like an
unsigned one.
New pdf-document/src/signature.rs reads the signature dictionary and
checks BYTE-RANGE INTEGRITY, which needs no cryptography and catches the
common real-world tampering: whether the signed range reaches the end of
the file. A signature that stops short leaves appended bytes uncovered,
which is exactly how an incremental-update attack hides content behind a
signature that still verifies.
Cryptographic verification is NOT implemented and cannot be faked:
VerificationStatus has no Valid variant. That is enforced by the type,
not by convention, because the failure mode for a signature feature is
not "it doesn't work" - it is a green tick beside a document nobody
checked. A test asserts the capability's absence so adding Valid without
the cryptography breaks the build rather than shipping a false tick.
Signing is out of scope entirely: no private keys in this crate.
Also verified ADR 0003's claim that an append-only save preserves a
signed byte range, byte for byte, rather than leaving it asserted.
Implementation bug worth recording: /Contents was initially hex-decoded,
but the COS lexer already decodes <...>. Running it twice on the common
128-zero-byte placeholder - which contains no hex digits - produced an
EMPTY vector, silently discarding the signature blob while every other
field looked right. Caught by asserting the blob is non-empty rather
than asserting the parse returned Ok.
6 corpus fixtures, 12 acceptance tests, 28 unit tests, and a
parse_signature fuzz target because /ByteRange is four
attacker-controlled integers used to index the file.
TEST_TARGET=pdf 497 -> 536 passing. rustfmt and clippy -D warnings clean.