Skip to content

createIncPageUpdate corrupts PDF string values with Go-style double quotes #132

@fredgig

Description

@fredgig

Bug Description

When signing PDFs with multiple signature fields, createIncPageUpdate corrupts string values in page dictionaries (e.g. /LastModified, /CreationDate) by writing them with Go-style double quotes ("...") instead of PDF parenthesized strings ((...)).

This causes pdf.NewReader to panic on subsequent signatures when parsing the corrupted page objects:

unexpected keyword "\"D:20220711015152-06'00'\"" parsing object

The bug is cumulative — each incremental signature update rewrites the page dictionary with the corrupted string. The panic occurs when a later signature's pdf.NewReader traverses the page tree and encounters the malformed value.

Root Cause

In sign/pdfvisualsignature.go, the default case of createIncPageUpdate uses page.Key(key).String() to serialize page dictionary values. Value.String() calls objfmt(), which uses strconv.Quote() for string-typed values. This produces Go string literals ("text") rather than valid PDF literal strings ((text)).

// Before (broken):
default:
    page_buffer.WriteString(fmt.Sprintf("  /%s %s\n", key, page.Key(key).String()))

For example, a page dictionary entry like:

/LastModified (D:20220711015152-06'00')

Gets rewritten as:

/LastModified "D:20220711015152-06'00'"

Double-quoted strings are not valid PDF syntax. The PDF lexer treats them as an unexpected keyword and panics.

Steps to Reproduce

  1. Have a PDF with a string value in a page dictionary (e.g. /LastModified)
  2. Sign the PDF with 2+ signature fields on pages that contain the string value
  3. The first signature rewrites the page dict with corrupted strings
  4. The second signature's pdf.NewReader panics when parsing the corrupted page

Expected Behavior

Page dictionary string values should be preserved with valid PDF parenthesized syntax through incremental signature updates.

Fix

Check val.Kind() for pdf.String and use pdfString(val.RawString()) — which properly escapes and wraps in parentheses — instead of val.String():

default:
    val := page.Key(key)
    if val.Kind() == pdf.String {
        page_buffer.WriteString(fmt.Sprintf("  /%s %s\n", key, pdfString(val.RawString())))
    } else {
        page_buffer.WriteString(fmt.Sprintf("  /%s %s\n", key, val.String()))
    }

pdfString() already exists in sign/helpers.go and handles escaping of \, (, ), \r and wrapping in (...). RawString() returns the unescaped string content, and pdfString() re-escapes it for valid PDF output — a correct round-trip.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions