Checking What a Signature Covers
A signature protects the bytes listed in its /ByteRange and nothing else. PDF
lets anyone append an incremental update to a signed file, a new revision that
redefines pages, annotations or metadata, and the signed bytes stay exactly as they
were. The signature still verifies. If the signer is in your trust roots, it is
still trusted. The document a reader opens, though, is the latest revision, which
the signer never saw.
So a valid, trusted signature does not mean the whole file is covered. Integrity and
trust are two of the questions; the third is how much of what you are looking at the
signature actually signed. SignatureValidationResult answers that with four
fields, filled by ValidateSignatures and ValidateSignaturesWithOpts alike,
because they come from the file bytes and not from the signature handler.
| Field | Meaning |
|---|---|
IsCoveringEntireFile | The signed range, plus the /Contents gap it leaves out, runs to the end of the file. Nothing was appended after this signature. |
IsCoveringEntireRevision | The signature covers everything up to the end of the revision it was applied in, even if later revisions were appended. IsCoveringEntireFile implies it. |
RevisionsAfterSignature | How many revisions were appended after the one this signature covers. Only meaningful when IsCoveringEntireRevision is true; it stays zero when no revision is covered at all. |
ContentsGapOnly | The only bytes the signed range leaves out are this signature’s own /Contents hex string, delimiters included. |
Checking coverage
res, err := reader.ValidateSignatures(handlers)
if err != nil {
return err
}
for i, item := range res {
if !item.IsVerified {
fmt.Printf("signature %d: not verified: %v\n", i+1, item.Errors)
continue
}
switch {
case item.IsCoveringEntireFile:
fmt.Printf("signature %d: covers the entire file\n", i+1)
case item.IsCoveringEntireRevision:
fmt.Printf("signature %d: covers its revision, %d revision(s) appended after it\n",
i+1, item.RevisionsAfterSignature)
default:
fmt.Printf("signature %d: signed range stops before the end of its revision\n", i+1)
}
if !item.ContentsGapOnly {
fmt.Printf("signature %d: ByteRange leaves out more than /Contents\n", i+1)
}
}The handlers are the validation-only set from
Validating Digital Signature. Coverage is computed for every
signature the reader finds, but it only means something for one that verified, so
check IsVerified first and skip the coverage checks when it is false. Errors
says why: a digest mismatch, handler not set when none of your handlers supports
the signature, or a missing or invalid /ByteRange.
String() prints the same information after the certificate lines:
Coverage: Entire revision is covered by the signature (1 revision(s) appended after signing)
ByteRange gap: matches /ContentsReading the result
The four fields combine into a small number of cases.
| Coverage | What it means | What to do |
|---|---|---|
IsCoveringEntireFile | The file is what was signed. | Nothing more to check for this signature. |
IsCoveringEntireRevision, RevisionsAfterSignature > 0 | The signature is intact for its revision, and later revisions changed the file on top of it. | Find out what the later revisions contain before you rely on the visible document. |
IsCoveringEntireRevision, RevisionsAfterSignature = 0, not IsCoveringEntireFile | The file has bytes after the end of the last revision that do not form a revision. | Inspect them before you rely on the file. |
| Neither | The signed range ends partway through its revision. Some bytes of the revision that holds the signature are unsigned. | Treat it as a failure. PdfAppender always signs up to the end of the revision it writes. |
ContentsGapOnly false | The range leaves a hole somewhere other than /Contents, so unsigned bytes can sit inside the span the signature appears to cover. | Treat it as a failure unless you know the producer. |
ContentsGapOnly is also false for two layouts that are legal but unusual: a
/ByteRange that does not start at offset 0, and a /Contents written as a literal
string instead of hex. Signers do not produce either in practice, which is why the
example rejects them too.
Appended revisions are normal
Most files with an appended revision are fine. In a real workflow the first signature on a document almost never covers the whole file, because each of these adds a revision after it:
- a second signer, or a counter-signature;
- LTV enabling, which appends the Document Security Store with certificates, OCSP responses and CRLs;
- a document timestamp, as in PAdES B-LTA;
- filling in form fields left for the next person, on a document certified to allow it.
The coverage fields cannot tell these apart from an edit you should reject. Here are
two files built from the same input, run through the coverage example. The first was
signed twice with pdf_sign_with_appearance.go. The second was signed once, then a
revision was appended by hand that changes the document title and nothing else.
$ ./pdf_sign_validate_coverage signed-twice.pdf
--- Signature 1: field "Approval 1"
Result: signature covers its revision; 1 revision(s) appended after it
--- Signature 2: field "Approval 2"
Result: signature covers the entire file
$ ./pdf_sign_validate_coverage edited-after-signing.pdf
--- Signature 1: field "Approval 1"
Result: signature covers its revision; 1 revision(s) appended after itFor the first signature both files report the same thing, and both pass integrity. Coverage tells you that something was added and how many revisions there are. What was added is a separate check, and you decide it by what your workflow expects:
- When every later revision should be another signature, an LTV update or a
timestamp, count them. A signature or document timestamp applied with
PdfAppenderadds one revision, and so does each LTV update written separately.RevisionsAfterSignaturehigher than you can account for is an unexplained edit. - When the document is certified, validate it with a
DocMDP handler. It compares the revisions and
reports changes the certification does not permit in
DiffResults. - When you need to see the change, extract the revision the signature covers with
PdfReader.GetRevisionand compare it with the full document. See Get Revision.
A policy that accepts only IsCoveringEntireFile is simple and safe, and it rejects
every countersigned or LTV-enabled file. A policy that accepts any
IsCoveringEntireRevision accepts the edited file above. Pick between them
knowingly, or do the work in the list.
Run the example
The example validates every signature with the four validation-only handlers and
prints one coverage line per signature. It exits with status 1, naming the
signatures, if one does not verify, stops short of the end of its revision, or has a
ByteRange that leaves out more than its /Contents. Appended revisions alone do
not fail it; they are reported with their count so you can apply your own policy.
A signature that does not verify gets its errors listed and no coverage checks. Here one byte of the signed revision was changed:
--- Signature 1: field "Approval 1"
Result: signature not verified
- pkcs7: Message digest mismatch
Expected: ED6764F2D7FAF315C278EB626651AA5CE7020228
Actual : EA93AE1F650BD6E2E40930350629B6B67DDA4442
Fail: coverage check failed for signature(s) 1git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/signatures/validation
go run pdf_sign_validate_coverage.go <INPUT_PDF_PATH>If this is your first time using UniPDF, follow the getting started guide to create an API key and set up your development environment.