Skip to content

Read and write the document security store - #120

Merged
bobsingor merged 1 commit into
embedpdf/mainfrom
embedpdf/feature/signature-validation-info
Nov 15, 2026
Merged

bobsingor merged 1 commit into
embedpdf/mainfrom
embedpdf/feature/signature-validation-info

Conversation

@bobsingor

@bobsingor bobsingor commented Nov 15, 2026 •

Copy link
Copy Markdown
Contributor

Long-term validation of a signature (PAdES B-LT) needs the certificates, OCSP answers and CRLs that prove its chain. A PDF keeps them in the catalog's document security store, /DSS, and nothing in the fork reached it. This adds three functions for the store, and one read that the signature change analysis needs.

The store

  • EPDFDss_GetCount(doc, kind) and EPDFDss_GetData(doc, kind, index, buffer, length) read the store's items of one kind, decoded. The kinds are EPDF_DSS_CERTIFICATES, EPDF_DSS_OCSP_RESPONSES and EPDF_DSS_CRLS.
    • The order is the kind's list (/Certs, /OCSPs, /CRLs), then what only a /VRI entry names (/Cert, /OCSP, /CRL), in the order of the entries' keys.
    • A stream that is named more than once counts once. An entry that is not a reference to a stream is skipped.
    • A document without a store reads 0 items.
  • EPDFDss_Add(doc, kind, data, size) writes one item as a stream of its own and appends it to the kind's list. It returns the stream's object number, or 0 when it refuses.
    • A store the document lacks is made as an object of its own, and a list the store lacks as an array inside it.
    • A store or a list that exists is extended where it is kept: as an object of its own (Acrobat writes both that way) or inside its parent (pyHanko writes the lists that way).
    • The stream holds the bytes as given, with no filter and no /Type. Neither Acrobat nor pyHanko writes /Type there.
    • It validates before it writes. A /DSS or a list that exists and is not a dictionary or an array is refused, and the document is unchanged.
    • It writes no /VRI entry and no /Extensions.

These functions move bytes and nothing more. They parse none of them, look for no duplicates and remove nothing: what is worth storing, and what a stored item proves, is the engine's decision. They open the document's view scope, so in a layer a read sees the layer, and an add leaves the base alone.

A read of any object's loaded value

EPDFDoc_GetLoadedObjectValue(doc, obj_num, buffer, buflen, out_read_status, out_truncated) returns the value of any object as the document's loaded bytes hold it. The text is the one EPDFObjectDiff_GetValue returns, and the read status is the diff's (EPDF_DIFF_READ_OK, FAILED, ABSENT). Object 0 reads the trailer.

The revision diff carries the values of the objects an update wrote. When Acrobat adds a signature field or a document timestamp to a page whose /Annots is an object of its own, it points the page at a new list and leaves the old list in the file, unwritten. The engine's change analysis has no way to compare the two lists, so it reads that update as forbidden, and Acrobat accepts it. With this read the engine opens the earlier revision and asks for the old list.

Like the diff, it parses the loaded bytes and never reads the document's object cache. A number that is free or unknown reads ABSENT. Bytes that do not parse read FAILED with the value null, so a failure is not taken for a null object.

The engine side reads and writes the store through doc.signatures and uses the object read in the change analysis (plan: signature long-term validation). Pin this there after release.

Tests

  • New embedder tests in epdf_dss_embeddertest.cpp:
    • BadArguments: a NULL document, an unknown kind, an index out of range, NULL data and size 0.
    • ADocumentWithoutAStoreReadsEmpty.
    • ReadsEachListDecoded: the three lists, one item stored through a filter, and a buffer too small to hold an item gets none of it.
    • ReadsWhatOnlyTheIndexNamesAndEachStreamOnce: /VRI entries follow the list in key order, a stream named twice reads once, and an entry that is not a stream reference is skipped.
    • AddMakesAStoreAsAnObjectOfItsOwn: an incremental save writes the catalog, the store and the new streams, and nothing else.
    • AddExtendsAListThatIsAnObjectOfItsOwn: Acrobat's layout. The save writes the list and the new stream; the store and the catalog are not written again.
    • AddExtendsAStoreInsideTheCatalog: the catalog is the one object that changes, and a kind without a list gets one inside it.
    • AddRefusesAStoreItCannotExtend: a store or a list of the wrong type is refused, and the document is unchanged.
    • AddOnALayerLeavesItsBaseAlone.
    • AnEncryptedSaveKeepsTheStoreReadable.
  • New embedder tests in epdf_signature_embeddertest.cpp:
    • LoadedObjectValueReadsWhatNoUpdateWrote: Acrobat's replaced /Annots list, read from the earlier revision.
    • LoadedObjectValueComesFromBytesNotCache: an edit in memory does not show, and an object made in memory reads ABSENT.
    • LoadedObjectValueSeparatesFailureFromNull.
  • epdf_c_api_test.c lists the four new exports.
  • Layer mode: the 13 new tests pass, and none fails only there. No writer line is added: the store tests open a plain document and a layer themselves, so each case runs both ways in every run.
  • On macOS:
    • Unit tests: 1,359 pass.
    • Embedder suite: 1,792 pass and 12 fail: the eight progressive render tests, SetTextRenderMode, RenderBug664284WithNoNativeText, RenderHelloWorldWithFlags and CaptionAppearanceGolden. Draw a signature field's box #114 and Read and write a page's viewports without loading the page #115 record the same kinds as failing on macOS without their changes. All 12 compare rendered pixels, and none calls the new functions. The base was not built again to compare.
    • Pixel tests weren't run; pdfium_test calls none of the new functions.
  • Through the engine's bindings: a local wasm build and a local macOS build, called from JavaScript on a file Acrobat made long-term valid, both read 5 certificates, 1 OCSP answer and 1 CRL, read the catalog through the object read, and added an item.
  • On Linux (the Docker test build): the store, signature and catalog suites, 70 tests with the 13 new ones, pass. The whole Linux suite was not run; the release workflow runs it.

Long-term validation of a signature needs the certificates, OCSP answers
and CRLs that prove its chain, and PAdES keeps them in the catalog's
/DSS. Nothing in the fork reached it.

EPDFDss_GetCount and EPDFDss_GetData read the store's three kinds of
item, decoded: what /Certs, /OCSPs and /CRLs list, then what only a /VRI
entry names, each stream once. EPDFDss_Add writes one item as a stream
of its own and names it in the store's list. It makes the store and the
list when they are missing and extends them where they are kept, as an
object of their own or inside the catalog. It returns the stream's
object number, and refuses a store or a list of the wrong type without
changing anything. /VRI is read and never written.

EPDFDoc_GetLoadedObjectValue reads the value of any object as the
document's loaded bytes hold it, in the text and with the read status of
EPDFObjectDiff_GetValue. The revision diff lists only the objects a
revision wrote. Acrobat points a page at a new /Annots list and leaves
the old one unwritten, and the engine needs the old list to judge what
was added.
@bobsingor
bobsingor merged commit cd9bc96 into embedpdf/main Nov 15, 2026
1 check passed
@bobsingor
bobsingor deleted the embedpdf/feature/signature-validation-info branch November 15, 2026 10:34
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.

1 participant