DEV Community

AsyncMonk
AsyncMonk

Posted on

A client wants a signed contract smaller: five checks I run before touching it

Clients send me signed contracts and e-invoices and then ask for "small" things: shrink it so it fits under the email limit, add a note, merge in a cover page. Each one sounds like ten minutes of billable work. Before I tested it I would have said yes to all three. Then I spent an afternoon on a signed file I built myself, and the answer to the first request turned out to be no in every tool I tried. Now I run five checks before I touch a signed PDF, and none of them takes more than a few minutes.

That file is a one-page fictional agreement with a synthetic photo, signed with a throwaway test root I generated with openssl. It is 931,980 bytes. The validators were pdfsig 26.08.0 and pyHanko 0.33.0, each trusting only that test root. I did not test real CA certificates, timestamps or LTV, and I never opened the files in Acrobat, so nothing here says what Acrobat would show.

Decide which validator answers for you

A signature check answers a narrow question: do the signed bytes still hash to the signed value? pdfsig answers only that, plus a note when the signature no longer covers the whole file. pyHanko goes further and diffs the revisions to judge whether the later changes were allowed. On my file, a countersignature was judged acceptable. A text annotation appended with pypdf was flagged as illegal, which I read as pyHanko's default policy not recognising how that annotation was written, not as proof the annotation is forbidden. If your client's counterpart uses a different tool, they may see a different verdict, so I now write the tool and version into the handover note.

Check whether the ByteRange still reaches the end

The signature covers two byte ranges with a hole for the signature value. On the original, the range was [0 923749 931275 705]: 931,275 plus 705 equals the file size, so nothing came after signing. On the countersigned copy, the first signature stopped short of the end. That one comparison tells you a later revision exists before you open anything.

Count the revisions and see what was appended

Each incremental update adds another %%EOF. The original had 2, and all three of my appended versions had 3. In all three, the leading bytes matched the original exactly and the hash over the signed ranges was unchanged. That is the only safe way to edit after signing. When I saved the same annotation as a full rewrite instead, the file came out at 931,610 bytes and both validators rejected the signature.

Find signed fields in the structure, not with grep

Searching raw bytes for /ByteRange, /Type /Sig and /SigFlags is a fine first pass, but over six files it missed one. That file had been repacked into object streams by qpdf, which hid /SigFlags inside a compressed stream. Reading the AcroForm and looking for a /Sig field with a /V value caught it. The case nothing catches is a signature that an earlier tool already deleted, because no trace is left. So I keep every original exactly as received.

Never run a compressor on a signed file

This is the "shrink it for email" request. I tried five compressors: qpdf 12.4.0, Ghostscript 10.07.1 with /ebook, iLovePDF and PDF24 online, and for in-browser compression I used ImgIng (https://imging.ai/) on its ebook preset in Chromium 149. Every one rewrote the whole file, leaving a single %%EOF. qpdf, ImgIng and PDF24 kept a signature dictionary whose ByteRange now pointed at bytes that no longer exist; the ImgIng output was 64,903 bytes with a range claiming 931,980. Ghostscript and iLovePDF removed the signature and SigFlags entirely, so a reader shows no warning at all. If a file needs to be smaller, it has to be compressed before anyone signs it.

Next time a signed PDF lands in your inbox with a request attached, run pdfsig on it first and note what it says before you change anything.

Top comments (0)