mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 03:10:21 +00:00
dbd0748177
* M14: bound large-PDF memory use (#307) Inspect large documents lazily, share parser/resource metadata, bound full-save and raster retention, and cache signature candidate discovery safely. Preserve upstream rendering deduplication, pixel alignment, signature previews, and organizer insertion behavior, with regression coverage for their interaction. Validated with 1,336 workspace tests, 193 parser tests, strict workspace Clippy, 30 WASM crate checks, 1x/2x GUI protocols, and same-PDF workflow comparisons. * cos: drop the writer cache-policy probe and its test-only hooks in production paths * cos: the temporary reader keeps parsed objects until a count or owned-bytes budget Since #552 a stream read from the file is a view into the input, so clearing the whole parse cache after every read saved little memory and cost most of the full-save slowdown (every object parsed twice, indirect /Length objects re-parsed per stream). Clear only past OBJECTS entries or STREAM_BYTES of data of the cache's own (decrypted streams, strings). * cos: the writers refuse a document opened with a stream limit (read-only inspection) * cos: a full save fails rather than writing null when an object reads differently the second time * render: parsing is not timed by the watchdog; timing tests with wide margins A slow first parse (a huge or damaged file) made the whole pool fail every page for its lifetime. Parsing, shared or private, is now its own untimed stage, as on main before the shared parser; rendering is timed from when parsing ends. The init-timeout tests become one deterministic test (a 2 s parse against a 10 ms limit, shared and private). Watchdog tests use limits and injected delays ~10x apart instead of 25-50 ms against real renders. Also: saturating retired-worker count, main's stop-flag test runs on private parsers (the first shared timeout is a private retry), set_queue documents that unlisted results are dropped, and retire() documents the permanent per-worker-parser fallback. * render: page numbers in the lazy page tree use checked conversion * render: the lazy inspector is compared with lopdf on named destinations and object streams too * ui: on-screen pages always render; pages turned back to stay cached; stable thumbnail tags - Visible pages and tiles are always admitted and uploaded (the viewport bounds them); the byte and count limits decide only how much prefetch fits. A HiDPI two-up view or a 5K viewport left pages on 'Rendering...' for good. - Pages no longer demanded stay cached, nearest the current page first, while the 128 MiB allowance and the 24-page count have room: paging back no longer re-renders. - Thumbnail density follows the demand count rounded up to a power of two, so rows scrolling in and out of an organize grid don't change every thumbnail's tag and re-render it. - A suspended (background) tab drains its pool's finished results instead of holding them. - Organize drag-and-drop ends on the primary button only. - Tests: the stale-result test uses valid pixels and checks nothing was uploaded; the organizer wheel test runs 40 steps; regressions for the first three fixes. * vendor: mark the hayro-syntax lock and SegmentList patches and list them as (8) and (9); clippy 1.99 as_chunks in a raster test * render: clippy 1.99 — contains over iter().any() in a raster test --------- Co-authored-by: Brandon Thomas <echelon@gmail.com>
pdfcraft-sign
Layer L4: digital signatures (execution plan M9; ISO 32000-2 §12.8; PAdES, ETSI EN 319 142).
let id = pkcs12::open(&std::fs::read("me.p12")?, "password")?; // a digital ID
let signed = sign(&doc, &id, &SignOptions { page: 0, rect: Some(r), date, ..Default::default() })?;
for s in signatures(&doc, &bytes, &trust) { // list + validate
println!("{}: {} — {:?}", s.field, s.summary(), s.details);
}
- Formats, on RustCrypto primitives: a small DER reader and writer (
der), X.509 certificates (x509), CMS SignedData (cms), and PKCS #12 digital ID files (pkcs12): PBES2 (PBKDF2 + AES/3DES), the legacy SHA-1 3DES/RC2 schemes, and MAC checks. Writing uses OpenSSL 3's defaults (AES-256-CBC, HMAC-SHA-256). - Keys: RSA, ECDSA P-256 and P-384. Verification uses RustCrypto everywhere. RSA
private-key operations (signing, key generation) use
aws-lc-rson native targets and are refused in the browser (ADR-0009: thersacrate's Marvin advisory, RUSTSEC-2023-0071, has no fix, sorsais only used to verify). ECDSA signs with deterministic nonces (RFC 6979). - Signing: PAdES B-B (
ETSI.CAdES.detached, signing-certificate-v2, SHA-256/384, no SHA-1). An existing unsigned field or a new one (visible with Acrobat's name-and-details appearance, or invisible); certification with DocMDP P=1/2/3. The document is written incrementally with a zero-filled/Contentsand fixed-width/ByteRange, which are then patched in place. Encrypted documents are refused for now. - Timestamps (PAdES B-T): the
timestampmodule builds and parses RFC 3161 requests, responses and TSTInfo tokens (size-capped, imprint- and signature-checked).sign_with_timestampattaches the TSA's token as an unsigned attribute over the signature value; the transport is the caller's (TimestampAuthority— this crate never opens a socket). Validation verifies embedded tokens; when the TSA chains to the trust store it reports the trusted time inSignatureInfo::timestamp_time, which then anchors certificate-validity and revocation checks. An untrusted TSA's time is reported as an unverified timestamp and never used as the validation time (the signer's claimed time is).timestamp::respondis the TSA-side signer behind the deterministic test authority. - Document timestamps and LTV:
timestamp_documentappends a standalone RFC 3161 document timestamp (/ETSI.RFC3161) covering the whole file; validation discovers and verifies these dictionaries (imprint over the signed bytes, token signature, TSA cert validity).dss::embedmerges revocation evidence into the catalog's/DSSwith/VRIentries keyed per signature (uppercase-hex SHA-1 of/Contents), deduplicating byte-identical blobs — sign → DSS → timestamp makes a B-LTA file, and the change classifier treats the store as a permitted change.revocationparses and verifies RFC 5280 CRLs and RFC 6960 OCSP responses (responder identity, OCSP-signing EKU for delegated responders, CertID hash matching, validity windows); validation checks embedded evidence against the signer's chain and a verified revocation invalidates the signature.
Not yet: revocation fetching (AIA/CRLDP extraction and a fetcher), timestamp-server
configuration, FieldMDP locks, certificate security, smart cards and PKCS #11 tokens.
macOS Keychain and Windows Current User Personal (My) store identities sign through
ExternalKey without exporting private keys. Windows CNG supports RSA PKCS #1 v1.5 and
ECDSA P-256/P-384; the store integration is tested with software-backed keys.
- Validation:
/ByteRangeand the CMS are read from the file's own bytes; the digest, the signature value and the signer's chain (against aTrustStore) are checked. Later revisions are diffed against the signed one, and the changes are classified (signing, form fill, comments, metadata, page content, document structure) under the DocMDP permissions. The verdict follows Acrobat: valid, unknown (intact but the identity isn't trusted) or invalid.
Oracles: poppler's pdfsig reports our signatures valid; OpenSSL reads our .p12 files and
verifies our CMS; tests/data/openssl-signed.pdf is a signature OpenSSL made, which we validate.