Skip to content
Checking What a Signature Covers

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.

FieldMeaning
IsCoveringEntireFileThe signed range, plus the /Contents gap it leaves out, runs to the end of the file. Nothing was appended after this signature.
IsCoveringEntireRevisionThe signature covers everything up to the end of the revision it was applied in, even if later revisions were appended. IsCoveringEntireFile implies it.
RevisionsAfterSignatureHow 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.
ContentsGapOnlyThe 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 /Contents

Reading the result

The four fields combine into a small number of cases.

CoverageWhat it meansWhat to do
IsCoveringEntireFileThe file is what was signed.Nothing more to check for this signature.
IsCoveringEntireRevision, RevisionsAfterSignature > 0The 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 IsCoveringEntireFileThe file has bytes after the end of the last revision that do not form a revision.Inspect them before you rely on the file.
NeitherThe 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 falseThe 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 it

For 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 PdfAppender adds one revision, and so does each LTV update written separately. RevisionsAfterSignature higher 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.GetRevision and 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) 1
git 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.

View the full source
Last updated on