Release note · August 6, 2026
A PDF signature ByteRange must exclude its own Contents
A historical PDF signature is not necessarily stale evidence. After a later incremental update, its /ByteRange should still describe the revision that introduced it. But reaching that old revision footer is only half the stored-layout question: the range should omit exactly that signature's own /Contents value.
PDFFence 1.22.0 adds PFP014, a fail-closed static review gate for that combined boundary. It deliberately does not validate a digest, certificate, permission, transform, or trust decision.
Two ranges, one excluded value, one revision
RFC 3778's PDF-signature overview describes the conventional /ByteRange as two offset-and-length pairs surrounding the excluded signature value, while /Contents is normally hexadecimal. The PDF 2.0 signature errata add the critical multiple-signature detail: the range terminates at the %%EOF that closes the incremental update adding that signature dictionary, with a possible optional line ending.
Those two facts belong together. For an earlier signature after a later update, checking only current EOF is too strict. Checking only its historical footer is too loose if the omitted gap is not actually its direct hexadecimal /Contents token.
PFP014: exact historical Contents coverage
The opt-in require_contents_bound_own_revision_signature_coverage rule requires every semantic signature dictionary with /ByteRange to satisfy all of the following:
- the range is a well-formed, direct two-pair layout beginning at byte zero;
- its only excluded span matches the opening and closing delimiters of that signature dictionary's direct hexadecimal
/Contentstoken; and - its final endpoint is the private, structurally identified footer of the revision containing that signature.
version: 1
rules:
require_contents_bound_own_revision_signature_coverage: true
PDFFence uses only catalog-reachable AcroForm and catalog /Perms signature roots. It follows a confirmed /Prev-linked xref chain, maps an xref-addressable indirect signature object to its first later revision footer, and scans its raw object with fixed limits for one direct hexadecimal /Contents. Public output is aggregate-only: no offsets, object references, revision boundaries, delimiter positions, signature bytes, digests, certificates, transforms, or trust data are emitted.
The rule fails closed. Direct or compressed signature objects, indirect or malformed /Contents, ambiguous revision footers, malformed ranges, unavailable xref links, and over-limit scans receive no positive evidence.
Why PFP014 is separate from PFP011 and PFP013
PFP011 asks whether a current-file range excludes direct /Contents. PFP013 asks whether a range reaches its own revision footer. PFP014 combines the exact gap and the historical endpoint, so a correctly bounded older signature can pass after an ordinary later update.
Its dedicated diff event appears only when the existing generic coverage and own-revision aggregates are unchanged. That keeps a current-file gap change owned by PFP011 and an own-footer change owned by PFP013, rather than emitting a cascade of derivative signals.
A benchmark pair with the same historical endpoint
PDF Change Assurance Benchmark 1.22.0 adds its 159th deterministic pair for this boundary. Both PDFs contain a semantic signature followed by a later incremental update. Both ranges reach their own signing-revision footers, so neither is current-file coverage and PFP013 stays positive on both sides.
The baseline's excluded gap is exactly direct /Contents. The candidate widens that gap by one byte before the token. The expected result is only signature_own_revision_contents_coverage_inventory_changed and PFP014. PFP009, PFP012, and PFP013 stay quiet; PFP010 and PFP011 remain deliberately outside this historical-signature contract. Held PDFFence 1.21 reports changed bytes alone for this pair.
Evidence, not authenticity
PDFFence 1.22 passed 256 tests and Ruff; PDFCAB passed 89 tests and Ruff. The public process-bound score was exact at 159 of 159 pairs. Both packages built reproducibly with fixed timestamps, passed Twine metadata checks, and passed clean paired wheel installs on Python 3.12 and 3.13 plus a Python 3.12 source-archive install. A local pyHanko exercise also counted an older signature after a later update and both signatures in a two-signature file.
None of that establishes cryptographic validity or whether a later update is permitted, authentic, safe, or malicious. Use a conforming signature validator and the surrounding review process for those decisions.
The tagged policy reference, threat model, and validation record define the supported boundary.
python -m pip install https://github.com/SybilGambleyyu/pdffence/releases/download/v1.22.0/pdffence-1.22.0-py3-none-any.whl
pdffence check before.pdf after.pdf --policy pdffence.yml --format sarif
PDFFence is MIT-licensed and available on GitHub. The release assets include SHA-256 checksums for the wheel and source archive.