For my capstone I'm building a small module that stores contract PDFs, and the plan was to compress every file before saving it. I don't have real contracts, so I made a fake one: a one-page fictional agreement with a synthetic image, signed with pyHanko using a certificate from a test root CA I generated myself. It came out at 931,980 bytes. I ran it through qpdf first, then asked poppler's pdfsig about the result, and got "Illegal values in ByteRange array". I didn't know what that meant. So I pushed the same file through four more tools (Ghostscript /ebook, iLovePDF, PDF24 and ImgIng) and checked every output with both pdfsig and pyHanko. None of the five kept a valid signature, and the messages I got fell into three kinds.
Illegal values means the range points past the file
A signature doesn't sign "the document". It signs bytes. The signature dictionary has a ByteRange, and in my file it was [0 923749 931275 705]: the first 923,749 bytes, then 705 bytes starting at offset 931,275. The gap between them holds the signature value, and 931,275 + 705 is exactly the file size. The qpdf output was 930,748 bytes, so it had barely shrunk, but it still kept those old numbers. My guess is that a range ending past the last byte is what pdfsig calls illegal. The same thing happened with PDF24 (50,877 bytes) and with ImgIng. For the browser run I used ImgIng on its ebook preset, on an M4 Mac, and its output was 64,903 bytes while the range still claimed 931,980. The size wasn't the problem, though. Every output had one %%EOF where the original had two, and none of them started with the original bytes. All five tools had rewritten the file from scratch, and any rewrite changes the signed bytes.
No signatures can mean one was deleted
Ghostscript and iLovePDF gave a different answer: pdfsig said the file does not contain any signatures. They had dropped the signature field completely, /SigFlags included. That output looks exactly like a PDF nobody ever signed. Honestly this one scared me more than the error, because with the error I at least knew something was wrong. With a deleted signature there is nothing left in the file to find, so no check after compression can catch it.
pyHanko said valid True on a broken file
This confused me for a while. On the rewritten files pyHanko still printed valid: True. That only means the CMS signature structure itself is mathematically fine. The field that tells you the signed bytes changed is intact: False, and the bottom line was INVALID. I should have read to the end of the output before panicking or celebrating.
What my module does now
The check runs before compression, never after. My first version searched the raw bytes for /ByteRange, /Type /Sig and /SigFlags, and it missed the qpdf file that packs objects into object streams, because /SigFlags was hidden inside a compressed stream. Reading the AcroForm and looking for a /FT /Sig field with a /V value found it. Signed files are stored untouched. Files we generate get compressed first and signed after. Appending an incremental update does keep an existing signature (I appended a second signature and the first stayed valid), but appending only makes the file bigger, so it doesn't help with size at all.
What I didn't cover: only pdfsig and pyHanko, no Acrobat or Foxit, a self-made certificate, and no timestamps.
Top comments (0)