Dev.to Security ๐Ÿ” Cybersecurity ๐Ÿ‘ 0 ๐Ÿ“– 5 min read

Why Modifying a Signed PDF Breaks Digital Signatures (and How Incremental Updates Work)

If you build document workflows, electronic signature tools, or legal intake pipelines, you have probably run into this high-stakes customer complaint: a user signs a PDF agreement, a downstream process or web viewer fil

If you build document workflows, electronic signature tools, or legal intake pipelines, you have probably run into this high-stakes customer complaint: a user signs a PDF agreement, a downstream process or web viewer fills in a remaining approval timestamp or adds an annotation, and saves the file. When the client opens the result in Adobe Acrobat, a bright warning banner appears:

"At least one signature has problems: The document has been altered or corrupted since it was signed."

The user assumes the software corrupted their file. In reality, the document's cryptographic integrity was intentionally tripped. Unlike simple web forms or office files, PDF digital signatures do not sign an abstract data payload alone. They sign exact byte ranges of the physical file on disk.

Understanding this behavior requires looking at how the PDF standard structures byte ranges, why common save operations break them, and how incremental updates preserve cryptographic integrity.

1. How /ByteRange works in ISO 32000-1

In ISO 32000-1 (ยง12.8), a digital signature is represented by a signature dictionary (/Sig) embedded inside a signature field widget annotation.

Unlike formats that sign a detached hash of raw text, a PDF digital signature computes a cryptographic digest (typically SHA-256) over the binary file itself. But there is a circular dependency: how can a PDF contain its own cryptographic signature without altering the hash of the file that contains it?

PDF resolves this via the /ByteRange array:

/ByteRange [ 0 84000 96000 24000 ]
/Contents <3082046b06092a864886f70d010702a082045c...>

The /ByteRange array consists of four numbers representing two continuous byte intervals:

  1. 0: The starting byte of the first range (the very beginning of the PDF).
  2. 84000: The length in bytes of the first range.
  3. 96000: The starting byte of the second range.
  4. 24000: The length in bytes of the second range (extending to the end of the file).

The gap between byte 84000 and byte 96000 is the exact space allocated for the /Contents hexadecimal string, which stores the PKCS#7 / CMS cryptographic signature container.

When a viewer validates the signature:

  1. It reads the /ByteRange offsets.
  2. It hashes bytes [0 to 84000] concatenated with [96000 to 120000].
  3. It extracts the public key from the embedded certificate in /Contents and verifies the signed digest against the recomputed hash.

If a single byte within those two ranges changes, even by 1 bit, the hash fails immediately.

2. Why standard file saves invalidate signatures

When a developer opens a signed PDF using a typical PDF library or in-browser editor and calls save() or saveToBytes(), the library usually performs a full serialization rewrite:

  • Unused objects are garbage collected.
  • PDF object IDs may be renumbered sequentially.
  • Cross-reference tables (xref) or cross-reference streams are reconstructed.
  • Document streams may be recompressed with FlateDecode.

Even if the visual appearance of the document looks identical, every physical byte offset in the file has moved. Because the original bytes covered by the initial /ByteRange no longer exist at their designated offsets, signature validators report that the document has been tampered with or corrupted.

3. The compliant solution: incremental updates

The PDF specification anticipated this challenge through Incremental Updates (ISO 32000-1 ยง7.5.6).

Rather than rewriting the entire file from byte 0, an incremental update appends new changes directly to the very end of the existing file:

  1. Preserve Original Bytes: The bytes from 0 to the original %%EOF marker are left 100% untouched.
  2. Append Modified Objects: Any changed objects (such as an updated field value /V, a new annotation, or a second signature widget) are assigned the same object number and appended after the old %%EOF.
  3. Append New Cross-Reference Table: An incremental xref section is appended, pointing to the new byte offsets of the modified objects.
  4. Append New Trailer: A new trailer dictionary is appended, containing a /Prev pointer holding the exact byte offset of the previous cross-reference table.

When a conforming viewer loads an incrementally updated PDF, it reads backwards from the final %%EOF, follows the /Prev chain to assemble the latest document state, but still evaluates the original signature against the untouched byte ranges of the first revision.

+------------------------------------------+
|  Original Document Revision (Bytes 0..N) |  <-- First /ByteRange covers this
|  Includes Signature 1 (/Sig)             |
|  %%EOF (Revision 1)                      |
+------------------------------------------+
|  Incremental Update Section              |
|  - Updated Form Field or Signature 2     |
|  - New xref table                        |  <-- /Prev points to Rev 1 xref
|  - New trailer & %%EOF (Revision 2)      |
+------------------------------------------+

4. Modification permissions: DocMDP and FieldMDP

Preserving byte integrity through incremental saving is necessary, but it is not always sufficient. A document author can enforce semantic restrictions via DocMDP (Modification Detection and Permissions) or FieldMDP:

  • P = 1 (No Changes Allowed): Any subsequent incremental change, including adding a form field value or an annotation, invalidates the certification signature. Only DSS (Document Security Store) timestamping for LTV (Long-Term Validation) is permitted.
  • P = 2 (Form Filling and Signing Allowed): Incremental updates that fill existing form fields or add subsequent approval signatures remain valid. Adding page graphics or arbitrary annotations invalidates the signature.
  • P = 3 (Form Filling, Annotations, and Signing Allowed): Incremental updates may add review comments, fill fields, and add signatures.

5. Detecting signatures before mutation

In automated document processing pipelines, you can prevent accidental signature invalidation by checking whether an incoming PDF contains digital signatures before choosing between a standard save and an append-only workflow:

import { PDFDocument, PDFName, PDFDict, PDFArray } from 'pdf-lib';
import fs from 'fs/promises';

async function checkDigitalSignatureStatus(pdfPath) {
  const fileBytes = await fs.readFile(pdfPath);
  const pdfDoc = await PDFDocument.load(fileBytes, { ignoreEncryption: true });

  const catalog = pdfDoc.catalog;
  const acroFormRef = catalog.get(PDFName.of('AcroForm'));
  if (!acroFormRef) {
    return { signed: false, signatures: [] };
  }

  const acroForm = pdfDoc.context.lookup(acroFormRef, PDFDict);
  const fieldsRef = acroForm.get(PDFName.of('Fields'));
  if (!fieldsRef) {
    return { signed: false, signatures: [] };
  }

  const fields = pdfDoc.context.lookup(fieldsRef, PDFArray);
  const signatures = [];

  for (let i = 0; i < fields.size(); i++) {
    const field = pdfDoc.context.lookup(fields.get(i), PDFDict);
    const fieldType = field.get(PDFName.of('FT'));

    // /Sig indicates a digital signature field
    if (fieldType?.toString() === '/Sig') {
      const sigValue = field.get(PDFName.of('V'));
      if (sigValue) {
        signatures.push({
          fieldName: field.get(PDFName.of('T'))?.toString() || 'Unnamed',
          signed: true
        });
      }
    }
  }

  return {
    signed: signatures.length > 0,
    signatureCount: signatures.length,
    signatures
  };
}

6. Architecture takeaways for PDF pipelines

  1. Order Matters: Fill all automated template variables, generate barcode stamps, and flatten static annotations before applying cryptographic signatures.
  2. Refuse Naive Saves on Signed Inputs: If an ingestion worker must modify an already-signed file, verify that your PDF library supports true incremental appending. If only full-rewrite saves are available, reject the operation or explicitly notify the user that existing signatures will be invalidated.
  3. Audit Revision History: When reviewing signed documents, use readers or APIs capable of traversing the /Prev trailer chain to inspect each historical revision exactly as it looked at the time of each signature.
๐Ÿ“ฐ Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes โ€” full credit and traffic to the original publisher.