What a signature actually covers

Three signal families on digital signatures: how much of the file the signature covers, whether what was signed is still what is there, and what the signer allowed to change afterwards.

A signed PDF is not the same as an unmodified one. A signature covers a defined range of bytes, and anything appended after that range is outside it, by design. These three families report the distance between “signed” and “what was signed”.

Part of the field guide. Three of the nineteen families the engine reports.

Signals, not verdicts. Every finding here is a structural fact about a file. "This document was modified after generation" is true or false about the bytes; "this document is fraudulent" is a judgement about a person, and Tamperlens does not make it. The API returns no boolean verdict field, by design.

A /ByteRange is two offset/length pairs, and the bytes after the second pair ends are simply not in the signature: appending them breaks nothing and proves nothing.
THE FILE, AS BYTES covered the signature value itself covered appended after not protected pair 1 pair 2 coveredEnd file length WHAT THIS ARITHMETIC ANSWERS two integers, no certificate, no trust store it holds whether or not the signature validates WHAT IT DOES NOT whether the appended bytes were hostile, or permitted, or who put them there THE ORDINARY CAUSE, AND IT IS ORDINARY A second signer's bytes land here. So do a permitted annotation and a filled form field. Multi-signature workflows fire this legitimately, every time.

signature-coverage

info high
What it detects
Whether each signature's /ByteRange actually covers the whole file. /ByteRange is an array of offset/length pairs naming exactly which bytes a signature protects: normally everything except the hole where the signature value itself sits. Tamperlens computes the highest covered offset and compares it with the file length. Anything beyond it was appended after signing and is not protected by that signature.
This is the strongest structural finding in the engine, and it is arithmetic rather than cryptography: two integers, no certificate needed, no trust store consulted. It holds regardless of whether the signature's mathematics validate, which is exactly why it is worth reporting separately from validation.
Evidence returned
Per signature: object number, /SubFilter, the raw /ByteRange array, coveredEnd, bytesAfterCoverage, coversWholeFile, and the total file size. Plus uncoveredCount and the document's revision count.
Benign causes
The mechanism itself is a normal and intended part of PDF: a second signer, or a legitimate incremental save such as adding a permitted annotation to an already-signed document, produces bytes outside the first signature's range. PDF has a whole concept, DocMDP permissions, for expressing which subsequent changes a signer allowed. Tamperlens reports the coverage gap; it does not evaluate whether the change was permitted. Multi-signature workflows will therefore fire this legitimately.
Two things this family explicitly does not do: verify signature cryptography, and validate the signing certificate or its trust chain. Those are separate jobs and a coverage report is not a substitute for either.
On a contract signed by two parties this family fires by design: the second signer's bytes are, correctly and unavoidably, outside the first signature's range.
THE FILE AFTER TWO PARTIES HAVE SIGNED the contract, as party A signed it party B's signature revision signature A's /ByteRange stops here high: “signatures no longer cover the file” signature B's /ByteRange does reach the last byte info, for that signature EVERY STATEMENT IS TRUE OF THE BYTES, AND NONE IS EVIDENCE OF AN EDIT A countersigned contract is the most ordinary legitimate document in this market, and the engine currently calls it high risk. Read a high band on a multi-signature PDF as uninformative.

Measured on 1,317 published Brazilian federal procurement contracts: 77% of the signed ones score high, against 0 of 1,150 unsigned. Full numbers and the recalibration plan on the measured-accuracy page.

Severity logic
No signature fields: silent. high when at least one signature has a parsed /ByteRange that does not reach the end of the file. info when the document is signed and every signature spans the entire file: reported deliberately, as a positive fact worth having in the report rather than silence.

signature-integrity

high low
What it detects
Whether the bytes a signature covers still produce the digest that signature committed to. This is the companion to signature-coverage, and the pair only makes sense together: coverage asks whether anything was added outside the signed range, and this asks whether anything changed inside it. A document can pass either and fail the other.
Bytes added outside the signed range are completely normal; a digest that no longer matches inside it is not: a correctly-produced signed PDF cannot have one.
OUTSIDE THE SIGNED RANGE signature-coverage was anything added after it? a second signer · an added annotation a filled form field completely normal INSIDE THE SIGNED RANGE signature-integrity did anything change under it? the covered spans are hashed and compared with the messageDigest in the PKCS#7 blob essentially no innocent cause A document can pass either and fail the other, which is why they are two findings and not one. No network request is made and no clock is read, so the same file gives the same answer forever and an unreadable signature is reported at low, because silence would read the same as “checked and fine”.
The difference matters because their innocent explanations are not comparable. Bytes appended after a signature are completely normal: a second signer, an added annotation, a filled form field all do it. A digest mismatch is not: a correctly-produced signed PDF cannot have one. Something inside the protected region was changed after it was protected.
It is arithmetic over the file's own bytes. The signature's /ByteRange names the spans it covers; those spans are hashed and compared with the messageDigest attribute inside the PKCS#7 blob. No network request is made and no clock is read, so the same file gives the same answer forever.
Evidence returned
signatures, verified, mismatched and unreadable as counts, then findings with each signature's status, its digest algorithm, and, where they differ: the leading bytes of both expectedDigest and actualDigest, so the claim can be checked rather than taken on trust.
Benign causes
For a mismatch, essentially none, which is why it is the rare finding reported at high on its own. For an unreadable result there are many, and it is reported at low precisely because it is not an accusation: the signature simply is not in a form this engine reads.
What it cannot see
This is not certificate validation, and a verified digest is not a statement that the document is trustworthy. Tamperlens does not examine who signed, does not walk the certificate chain to a trusted root, and does not check whether that certificate was revoked. A mathematically intact signature by a completely unknown signer reads exactly the same here as one from a bank.
A verified digest answers one question, did these bytes change, and the three questions a reader usually means by “is this signature valid” are all outside it.
CHECKED the spans named by /ByteRange hashed with the stated algorithm compared with messageDigest both digests are returned when they differ NOT CHECKED, AND NOT APPROXIMATED who signed whether the certificate chains to a trusted root whether the certificate was revoked these are certificate validation, and this is not it A mathematically intact signature by an unknown signer reads exactly the same here as one from a bank. Only detached PKCS#7 is checked. adbe.pkcs7.sha1 stores its digest differently and is declined rather than approximated. Checking it the same way would report a mismatch on a perfectly good signature.
Only detached PKCS#7 signatures (adbe.pkcs7.detached, ETSI.CAdES.detached) are checked. adbe.pkcs7.sha1 is deliberately declined rather than approximated: it stores its digest differently, and checking it the same way would report a mismatch on a perfectly good signature.
Severity logic
high when covered bytes do not produce the committed digest. low when a signature is present but could not be checked. Reported rather than omitted, because silence would read the same as "checked and fine". A verified digest is reported as info and contributes nothing to the risk score.

signature-permissions

high medium low
What it detects
Whether the document changed in a way its own signature said it must not. This is the third of the signature families and the one that makes the first usable: signature-coverage reports that bytes were appended after signing and then has to list the innocent explanations, because it cannot tell them apart. A certification signature already answered that question at signing time.
When a document is certified, the signer writes a permission level into it. /DocMDP /P. 1 permits no changes at all; 2 permits filling in form fields and adding further signatures; 3 additionally permits annotations. This family compares that number against what the revisions written afterwards actually touched.
No permission level lets a page be repainted: so a page content stream written after certification is high at every level, and the interesting differences are all in the rows above it.
/P 1 /P 2 /P 3 no changes forms · signatures + annotations a page content stream, or an image a page names high high high a non-form annotation high medium permitted a form field filled in high permitted permitted signature and bookkeeping objects alone under /P 1 are low, not high: a “look at this”, not an accusation THE GAP THIS GRID CANNOT CLOSE An annotation carries arbitrary visible content in its appearance stream. A /P 3 document can therefore be changed visibly with no permission violated at all, and no reading of /DocMDP alone fixes that.
Most signed PDFs are not certified. An ordinary approval signature attests to the document as it stood and declares nothing about what may follow, and that is itself reported, because "the signature covers the whole file" reads to most people as "verified" when it only means the signature's extent, not anyone's permission.
Evidence returned
docMDPPermission (and whether it was docMDPPermissionDefaulted), permsTarget, appendedRevisions, and a touched breakdown counting how many page content streams, images, page-reachable images, pages, form widgets, other annotations and signature objects the later revisions wrote. images and pageImages are reported separately, so 3 and 0 tells you pictures arrived and none of them landed on a page, plus changedObjectSample, the object numbers behind the finding, so it can be checked in the file.
Benign causes
Filling in a form regenerates appearance streams, which look like content streams and are not. Only objects reached from a page's /Contents count here, which is what keeps a permitted form fill-in from reading as a violation.
A visible signature carries a picture. The rendered signature block, an institutional mark, a scanned rubric: these live in the signature widget's appearance stream, not on the page, and adding a signature is exactly what /P 2 permits. Only images that a page's resources name are counted. Measured across the corpus, 109 of 320 revisions that added a signature carry such an image and no page content at all; before engine 1.24.0 every one of them would have been reported as a violation.
A signature's own picture is not on the page, and counting it as if it were is what made this family wrong on 109 revisions before engine 1.24.0.
THE PAGE /Page → /Contents an image the page's resources name counted: the page was repainted THE SIGNATURE WIDGET /Annot /Widget → /AP the rendered signature block, a mark, a rubric not counted: /P 2 permits exactly this WHY THE DISTINCTION IS WORTH THE CODE 109 of 320 signature-adding revisions carry such an image and no page content Before engine 1.24.0 every one of those 109 was reported as a permission violation. The evidence reports images and pageImages separately, so 3 and 0 tells you pictures arrived and none landed on a page.
Long-term validation data. Adding the certificates and revocation responses that let a signature be checked years later appends a revision even to a document certified /P 1. It is routine, it is what ICP-Brasil's Referência de Tempo policy produces, and a document whose catalog carries a /DSS store gets it reported as info rather than as a change.
What it cannot see
/P is a number in a dictionary, and this family makes no trust decision whatsoever. It does not check who certified the document, whether their certificate chains to a trusted root, or whether the signature validates at all, that last question belongs to signature-integrity. A permission declared by an unknown signer reads exactly the same here as one from a government issuer.
It cannot say what a change was for. It reports that a page content stream was rewritten after a certification that did not permit it, not what the new page says.
It compares against the certification and against nothing else. In a document with several signatures, a change made after an earlier signer signed but before the document was certified was part of what got certified, and is invisible here. That is a per-signature question, answered separately by signature-coverage and signature-integrity.
A /P 3 document can be changed visibly without any permission being violated. /P 3 permits annotations, and an annotation carries arbitrary visible content in its appearance stream. The spec-correct answer and the answer a reader wants are not the same at /P 3, and no reading of /DocMDP alone closes that gap.
Severity logic
high when a page content stream, or an image one of the document's pages names, was written after certification (no permission level allows the page to be repainted), or when anything at all was altered under /P 1. high also when the catalog names a certification signature that carries no certification, which is what stripping one leaves behind.
medium when a non-form annotation appears under /P 2. low when bytes were appended to a /P 1 document but touched only signature and bookkeeping objects with no /DSS to explain them: a "look at this", not an accusation. Everything else is info and contributes nothing to the risk score: an uncertified document, and a signature declaring a certification the catalog never adopted. That second one was medium for about an hour: measured against 915 real documents it fired 5 times and was wrong 5 times, every one a government contract whose producer writes the transform and omits the catalog entry. See docs/CORPUS-PHASE0-PERMISSIONS-2026-08-05.md.

See a signature that no longer covers its file

Open the “Signed, then changed” sample: a precomputed report on a PDF whose /ByteRange stops short of the file's last byte, which is the arithmetic at the top of this page with the evidence attached. Your own file goes into the same checker: it is sent over HTTPS, parsed in memory and never written to disk. No account, no quota. To run it in your own pipeline, see the API quickstart.

Tamperlens reports risk signals, not authenticity verdicts. Signals can have benign causes; combine them with your own decision logic.

The rest of the guide