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
- Have a PDF with a string value in a page dictionary (e.g.
/LastModified)
- Sign the PDF with 2+ signature fields on pages that contain the string value
- The first signature rewrites the page dict with corrupted strings
- 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.
Bug Description
When signing PDFs with multiple signature fields,
createIncPageUpdatecorrupts 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.NewReaderto panic on subsequent signatures when parsing the corrupted page objects: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.NewReadertraverses the page tree and encounters the malformed value.Root Cause
In
sign/pdfvisualsignature.go, thedefaultcase ofcreateIncPageUpdateusespage.Key(key).String()to serialize page dictionary values.Value.String()callsobjfmt(), which usesstrconv.Quote()for string-typed values. This produces Go string literals ("text") rather than valid PDF literal strings ((text)).For example, a page dictionary entry like:
Gets rewritten as:
Double-quoted strings are not valid PDF syntax. The PDF lexer treats them as an unexpected keyword and panics.
Steps to Reproduce
/LastModified)pdf.NewReaderpanics when parsing the corrupted pageExpected Behavior
Page dictionary string values should be preserved with valid PDF parenthesized syntax through incremental signature updates.
Fix
Check
val.Kind()forpdf.Stringand usepdfString(val.RawString())— which properly escapes and wraps in parentheses — instead ofval.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 insign/helpers.goand handles escaping of\,(,),\rand wrapping in(...).RawString()returns the unescaped string content, andpdfString()re-escapes it for valid PDF output — a correct round-trip.