Skip to content

Signatures: timestamps, document timestamps and validation data (#193) - #229

Merged
timzifer merged 5 commits into
mainfrom
claude/issue-193
Oct 14, 2026
Merged

timzifer merged 5 commits into
mainfrom
claude/issue-193

Conversation

@timzifer

Copy link
Copy Markdown
Owner

Fixes #193. Stacked on #227 (on #226, #225); merge those first. Follow-up: #228.

API

type SignOptions struct {
	// …
	Timestamp TimestampFunc // B-T: a token over the signature value
}
type TimestampFunc func(ctx context.Context, digest []byte) ([]byte, error)

func (e *Editor) SignContext(ctx context.Context, w io.Writer, opt SignOptions) error
func (e *Editor) Timestamp(ctx context.Context, w io.Writer, ts TimestampFunc) error // ETSI.RFC3161

type Validation struct {
	Certificates []*x509.Certificate
	OCSPs, CRLs  [][]byte // DER
}
func (e *Editor) AddValidation(sig *Signature, v Validation) error // /DSS, B-LT

Details

  • B-T (internal/cms.AddTimestamp): the token for the SHA-256 digest of the signer info's signature value becomes the unsigned attribute signature-time-stamp. The function may return the TimeStampToken or the whole TimeStampResp (cms.Token unwraps it, refusing a status other than granted). cms.Stamps checks that the authority signed the token and that it stamps the digest. The room for /Contents grows by 16 KB (timestampRoom; public TSA tokens take 2–8 KB); a larger one gives ErrSignatureRoom. Sign = SignContext(context.Background(), …).
  • Document timestamp: Editor.Timestamp writes an update with an invisible signature field whose value is /Type /DocTimeStamp /SubFilter /ETSI.RFC3161, /Contents the token over the byte range. The signing code (signed, signTarget) is shared with Sign.
  • B-LT / B-LTA: AddValidation records data; dssPlan writes /DSS (a patch of the catalogue, or of the store where it is an object): /Certs, /OCSPs, /CRLs as Flate streams, and /VRI keyed by the upper-case hex SHA-1 of the signature's /Contents (for a document timestamp: of the token without padding, ETSI EN 319 142-1, 5.4.2.2). Existing entries stay, data already stored is not stored twice (by SHA-256), entries of one signature are merged. A document timestamp in the same or a later update makes it B-LTA.
  • Certification: per PDF 2.0, 12.8.2.2.2 a DSS and document timestamps are no change: allowed under DocMDP P=1, and Verify does not count a DocTimeStamp field as Changes.Signed.

Two faults of Verify fixed on the way (found by the corpus run)

  • objectCompare.same compared a reference to a page by the page's content: a Link to a page whose /Annots a later signature changed made the first signature report Changes.Annotated. Pages are now compared by their place. (TestSign now checks it.)
  • changes() matched fields by name: issue15096.pdf has two fields Group1.Group1, and signing twice reported FormFilled. Fields are now matched by their objects (TestSignSameNames).

Tests

  • TestSignTimestamp (token and response forms; refused, another digest, a failing authority, a cancelled context — nothing written), TestDocumentTimestamp (approval and P=1 certification: the earlier signature sees no change; the timestamp verifies, is trusted by the TSA root, covers the file), TestAddValidation (counts in /DSS and /VRI, the VRI key, a second round merged without duplicates under a document timestamp, P=1 still allowed, the key of a timestamp), TestSignSameNames.
  • Corpus API step sign now: sign with a timestamp (B-T), add validation data and a document timestamp, verify both. pdf.js corpus: no difference (the first run showed issue15096.pdf, fixed above).
  • go test ./... green.

Not done (#228): revocation from /DSS when verifying, the chain of a timestamp authority, streaming the base file while signing, EdDSA/PSS keys, an HTTP client package.

The /BBox of an annotation's appearance stream is mapped onto its /Rect,
and the border of a field lies on the edge of that box. With the box off
the pixels, the exact clip and the border's edge, both antialiased,
lightened the edge pixels twice: a field Editor.SetFields wrote was drawn
with other edges than the same value shown from a FormState, which is not
clipped. The clip of an annotation's appearance is now rounded outward to
whole device pixels when it stays on the axes, as PDFium, MuPDF, Poppler
and Ghostscript clip rectangles. The accuracy report against PDFium is
unchanged.

overlayFile puts its fields off the pixels, which the test held them to.

Fixes #222.
The first fix of #222 rounded the clip of every annotation's appearance
outward to whole pixels. That made the render of an appearance stream
differ from the page it is flattened to, whose content clips exactly: the
corpus API run reported 41 files of pdf.js whose flattened first page
differed, by up to 4.9 % of its pixels.

The other way round: the appearance cera generates for a widget is
clipped to the widget's box exactly, as the stream Editor.SetFields
writes for it and the form Editor.Flatten draws are. The render with a
FormState, the saved file and the flattened page are drawn alike at any
scale; the border of a field off the pixels is lighter on its outer edge
in all three, as the appearance streams of documents are.

The corpus API report and the accuracy report against PDFium are the same
as on main.
ImportOptions.Structure brings the elements of the source's structure
tree whose content is on imported pages, with the elements above them;
an element left without content is dropped. Where the result has a tree,
the elements follow its own in a Part under its Document element (or its
root): the source's Document becomes the Part, or one is made around its
elements, with the source's /Lang where it differs. A result without a
tree takes the first one brought as its own.

The keys of the parent tree are numbered anew after the result's: pages
(/StructParents), their annotations (/StructParent) and the form XObjects
they draw. A page imported twice is tagged once, its first copy; a form
only pages that did not come draw as tagged content loses its keys. Marked
content and object references are mapped to the copies. Role maps are
merged, a type the result maps otherwise written as what the source means
by it; a class the result defines otherwise is renamed Cls_2, an
identifier the result has id_2; namespaces are joined. /MarkInfo says
/Marked.

The corpus API run gets a step: the pages looked at, imported with their
structure into the document itself, keep their marked content in the
tree. Over the pdf.js corpus it finds no difference.

Fixes #183.
PDF 2.0 lets a page carry /OutputIntents of its own (14.11.5), which
take precedence over the document's on it. Such a page is now drawn with
a view of the document (Page.cmykDoc) whose DeviceCMYK converts through
the profile of the page's intent, read as the catalogue's is: the first
intent of kind GTS_PDFX, GTS_PDFA1 or ISO_PDFE1 with a CMYK profile.

Document is split into what all views share (docShared: the reader,
fonts, the form, the profiles and their budget, transfer functions) and
what stands on DeviceCMYK (cmykView: colour spaces, shadings and
patterns by reference, decoded images under an id of their own). Pages
with one profile share a view; forms and patterns drawn on a page take
its view. NaiveCMYK, CMYKProfile and IgnoreOutputIntent decide for the
pages as for the document. A page profile counts against
maxCMYKProfiles, and one cera cannot read is counted as icc-lut, the page
then converting as the document does.

The corpus API report and the accuracy report against PDFium are the
same as before.

Fixes #186.
SignOptions.Timestamp takes a TimestampFunc that gets a token (RFC 3161)
from a timestamp authority for the SHA-256 digest of the signature value;
the token goes into the signer info as the unsigned attribute
signature-time-stamp (PAdES B-T). cera does no network I/O: the function
makes the round trip, and may answer with the token or with the whole
TimeStampResp (cms.Token). A token that does not stamp the digest, or
whose authority did not sign it, is an error (cms.Stamps). The room for
/Contents grows by 16 KB; Editor.SignContext passes a context.

Editor.Timestamp writes a document timestamp (/Type /DocTimeStamp,
ETSI.RFC3161) over the update, in an invisible field. Editor.AddValidation
records certificates, OCSP responses and CRLs the caller fetched, written
into the document security store (/DSS with /Certs, /OCSPs, /CRLs and a
/VRI entry per signature, keyed by the SHA-1 of its /Contents) by Update,
Sign and Timestamp; what the store has stays and is not stored twice.
With a document timestamp after it the signatures are B-LTA. Both are no
change by the rules of a certification (PDF 2.0, 12.8.2.2.2), so a
document certified to allow none may get them, and Verify does not count
a document timestamp as a change.

Two faults of Verify's comparison of revisions, found on the way: a
reference to a page was compared by the page's content, so that a link
to a page whose /Annots a later signature changed made the signature
report annotations changed; and fields were matched by name, so that a
form with two fields of one name reported a value filled in. Pages are
now compared by their place, fields by their objects.

The corpus API step "sign" signs with a timestamp, adds validation data
and a document timestamp, and checks both signatures.

Fixes #193. Revocation from /DSS when verifying, and the smaller points
#143 left, are #228.
@timzifer
timzifer merged commit 11e6f88 into main Oct 14, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Signatures: timestamps and validation data (PAdES B-T, B-LT)

1 participant