Commit Graph
82 Commits
Author SHA1 Message Date
f46268fbe4 M9: visible signatures read upright on rotated pages (#4067)
* M9: visible signatures read upright on rotated pages

A visible digital signature placed on a page with /Rotate was drawn in
user space, so its graphic and details turned with the page: on scans
stored landscape with /Rotate 270 the signature came out sideways.
Stamps (#1398) and Fill & Sign marks (#2392) were already corrected;
signatures were not.

A new signature widget on a turned page gets /MK /R equal to the page
rotation (inherited rotation included, via pdfcraft_model::pages), and
its appearance is laid out in the box as displayed (sides swapped for a
quarter turn) with a /Matrix from pdfcraft_model::view_matrix_for that
maps it exactly onto /Rect. Signing an existing field follows that
field's own /MK /R, so a field without one is drawn as before. Pages
that are not turned produce the same bytes as before.

Tests: sign crate (layout equals the upright box's, /MK /R, /Matrix,
inherited /Rotate, existing fields with and without /MK /R, malformed
/MK /R) and an engine render test on 0/90/180/270 pages with the
quadrant image. Both fail with the fix reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep one evidence/notes pair in the sign.digital parity entry

---------

Co-authored-by: vjeko2404 <vjekoslav@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-23 10:43:54 -04:00
Dylan M. Taylor 60643020b6 Merge pull request #4244 from dylanmtaylor/fix/high-severity-redact-sign
fix(redact,sign,cos): close the still-open high-severity scan findings
2026-10-23 10:25:51 -04:00
Dylan M. Taylor 5162da1b9f fix(sign): a widget counts as signed only when its /V resolves to a dictionary 2026-10-23 10:22:33 -04:00
Eric Kaden 1b247a4812 M9: sign_list lists a hidden or NoView signature field as not visible (#4141) 2026-10-23 10:19:04 -04:00
30331b811d test(sign): serialize the Keychain search-list tests (#4124)
* test(sign): serialize the Keychain search-list tests

Both ignored Keychain tests save the user's keychain search list, change it
and restore what they saved. Run together (`--ignored` runs them in
parallel), one could restore its list while the other needed its temporary
keychain, or put back a keychain the other had already deleted. A shared
lock now runs them one at a time.

With the lock, the issuer test still failed about one run in ten when it
came second: the Security framework caches the search list in-process and
learns of `security list-keychains` asynchronously, so `keychain::find` did
not yet look in the new keychain. The test now waits up to five seconds for
the identity to appear.

Follows up a CodeRabbit review comment on #3716.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(sign): restore the keychain search list before deleting the keychain

If deleting the temporary keychain panicked, the test left it on the user's
search list, and the next Keychain test (which accepts the poisoned lock)
saved that list and later restored it. Restore the list first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(sign): fail when the keychain search list isn't restored

Both Keychain tests ignored the result of the `security list-keychains` that
restores the user's search list, so a failed restore went unnoticed and left
the deleted temporary keychain on the list. They now check it after deleting
the keychain.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(sign): keep the issuer test's keychain at one path

trustd remembers which keychain file it found an issuer in. Each run of
`keychain_identities_carry_their_issuers` created its keychain at a path with
the process id and deleted it afterwards, so the next run's trust evaluation
kept looking in the deleted file and built an empty chain, until trustd
forgot it. The test failed every run after the first for a while.

The keychain now lives at the same path every run, and a copy left by a
killed run is deleted first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: ffishman <20211677+ffishman@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 10:19:00 -04:00
Dylan M. Taylor 39ee93fe70 fix(redact,sign): address review — pop-up replies, signed-role deletions, widget role from signed revision 2026-10-23 10:11:10 -04:00
Dylan M. Taylor dad86bf9f5 fix(redact,sign,cos): close high-severity findings from the agent scan
Redact: refuse Redact marks written in place; remove replies and pop-ups listed on
other pages; delete form fields by object, not name; resolve Do by its last operand
like the renderer; sanitize drops PCAdded and flattens widgets before hidden-content
cleanup. Sign: freed signed objects and edits to an already-signed widget are
modifications. Cos: only reading trusts /Type /XRef to skip encryption.
2026-10-23 10:11:10 -04:00
Dylan M. Taylor 93b25f67a6 Merge main into fix/coderabbit-c5-forms-misc
Keep the Ops iterator from this branch and carry main's long-number skip
(MAX_RETRIED_TOKEN) into its operand error path.
2026-10-22 22:07:19 -04:00
aiongg a887405ea1 fix(sign): don't report valid when a DSS cap hides revocation evidence
The evidence caps counted repeated references to one stream, so a /DSS
array could list one benign stream 1024 times ahead of a revoking CRL,
and the check never reached it. Repeats are skipped before the entry
cap counts anything, and when a cap still stops an array being read in
full without a revocation found, the signature's status is unknown
rather than valid, with the reason in its details.
2026-10-22 20:07:41 +00:00
aiongg ae791d667b fix(sign): a time-stamp response must carry the requested nonce and policy
parse_response compared only the digest algorithm and imprint with the
query, and the token parser dropped the TSTInfo nonce. A genuine token for
the same imprint from another request or under another policy was
accepted. The token keeps its nonce, and a requested nonce or policy must
match.
2026-10-22 17:57:53 +00:00
aiongg 77ee1c8a26 fix(sign): read DSS revocation evidence one stream at a time within caps
Revocation checking decoded every /CRLs and /OCSPs entry under the
general 1 GiB stream limit and kept them all before parsing any, so one
compressed stream referenced many times multiplied decoding and memory.
Evidence now decodes lazily, each stream once and within the 2 MiB
evidence cap, with at most 1024 entries and 64 MiB per array.
2026-10-22 17:08:54 +00:00
aiongg 7c4731234f style(sign): rustfmt the new tests 2026-10-22 12:13:16 -04:00
aiongg 4e9d8f9b3b fix(sign): require the time-stamping purpose for trusted timestamps
A timestamp counted as trusted time whenever its signer's certificate
chained to a trusted certificate, whatever the certificate was issued
for. Any key from a trusted CA could then vouch for an arbitrary time.
Trusted time, for a document timestamp and for a signature's embedded
one, now also needs a certificate whose extended key usage includes
timeStamping, whose key usage allows signing, and which has no critical
extension PdfCraft doesn't process (RFC 3161 §2.3). The test timestamp
authority gets such a certificate.
2026-10-22 12:13:16 -04:00
aiongg d9cb7c2fe7 fix(sign): don't build chains through a CA with unprocessed critical extensions
Chain building accepted an intermediate CA without looking at its critical
extensions, which were checked only on the signer's certificate. A
subordinate CA limited by critical name constraints could vouch for a
signer outside its namespace, because PdfCraft does not enforce those
constraints. A CA certificate (other than a self-signed root, whose
limits are the trust store's business) with a critical extension
PdfCraft doesn't process is refused as an issuer, and the reason is
reported.
2026-10-22 12:13:16 -04:00
aiongg fbada004d3 fix(sign): treat a changed trailer /Root after signing as not permitted
Revision checks compared objects only. A later revision that pointed the
trailer at another catalog already present in the signed file changed no
object, so the signature stayed valid while the reader showed a different
document. A /Root that differs from the signed revision's is now a
disallowed change to the document structure.
2026-10-22 12:13:16 -04:00
aiongg faab42d405 fix(sign): authorize an embedded OCSP responder by signature, not by name
An OCSP response's signer counted as the issuer itself whenever its
certificate carried the issuer's subject name. A response naming its
responder by key hash could embed a certificate for any key with the
issuer's name copied in, skip the delegation checks, and mark a valid
certificate revoked. Whether the responder is the issuer now follows from
how it was selected (the issuer's own name or key hash); every embedded
certificate must be signed by the issuer and carry the OCSP-signing
purpose.
2026-10-22 12:13:16 -04:00
aiongg 9526363152 Build each crate's integration tests as one binary (#4087)
* Build each crate's integration tests as one binary

Every tests/*.rs file is its own executable that links the whole crate
graph. Move each crate's test files into tests/it/ as modules of one
tests/it/main.rs, for apps/pdfcraft, apps/pdfcraft-cli, automation, cos,
crypt, engine, render, sign and ui-egui. ui-egui's ui_group binary and
the support modules move into tests/it/ too. Crates with a single test
file keep it. The test executables go from 150 to 52, and every crate
runs the same number of tests as before.

ui-egui perf keeps its own binary (own process): its frame-time budgets
would charge each measurement for the other tests running beside it.

What the move needs:
- include_str!/include_bytes! and #[path] paths gain one "../".
- ui-egui: one GPU lock in tests/it/main.rs replaces the five per-file
  ones (content, document_links, editing, fill_sign, view), so one
  process creates one wgpu device at a time. The drop-line test in ui.rs
  and the screenshot blocks behind PDFCRAFT_*_SHOT(S) and
  CRAFT_UI_DOCKING_DIR, which render without holding it, take it too.
- pdfcraft winit_wayland_scroll: the vendored winit source it compiles
  names crate::event, so that shim lives in main.rs.
- parity evidence names the new paths.

* Name the one-binary test paths in docs and comments

- READMEs, docs, ATTRIBUTION and source comments name tests/it/<file>.rs.
- The oracle README's example filters with --test it encryption::qpdf_agrees.
- pdfcraft's GPU lock comment points at the UI tests' shared lock in
  crates/ui-egui/tests/it/main.rs.
2026-10-22 11:28:58 -04:00
aiongg e8e070f392 Merge PR #2897: M9: rewriting a certified form's XFA data is a form fill (#2759) 2026-10-22 01:46:29 +00:00
aiongg b3d61dae5b Merge PR #1962: M9: read DocMDP permissions from the signed revision (#1788) 2026-10-22 01:46:29 +00:00
aiongg ce9763dc6b Merge PR #2559: Signatures: signed attributes must carry the message digest and the data content type
Conflicts:
- ATTRIBUTION.toml: sha256 lines for es/lt/vi taken from main; recomputed after all merges.
2026-10-22 01:46:21 +00:00
aiongg bb606504bb Merge PR #3561: M9: read .p7b/.p7c certificate bundles for trust import 2026-10-22 01:46:12 +00:00
aiongg 8e08d266b7 Integration: #1679 test that a Japanese label beside a Latin Extended name is refused, not drawn in the wrong glyphs 2026-10-22 01:46:12 +00:00
aiongg e42ccbb440 Merge PR #1679: fix(sign): render Unicode characters in visible signatures using bundled Inter font
Conflicts:
- crates/sign/src/pdf.rs: details switch to Inter as the PR does; drawn labels keep main's
  Helvetica check and refusal, so the PR's label-driven switch to Inter is dropped. Doc comment
  merged.
- crates/sign/tests/pdf.rs: both tests kept.
2026-10-22 01:46:01 +00:00
aiongg 7f1d4b6f80 Merge PR #2513: fix(sign): fill typed signature outlines with non-zero winding (#1786) 2026-10-22 01:44:55 +00:00
aiongg 05eea2da3b Merge PR #2989: fix(sign): explain 0x8009001D and 0xC0000225 when a store key's provider fails 2026-10-22 01:44:55 +00:00
aiongg e95fb49afa Merge PR #3716: fix(sign): embed the issuers of macOS Keychain identities 2026-10-22 01:44:51 +00:00
Dylan M. Taylor 606c560d24 Use chrono instead of hand-rolled UTC conversion (#3824)
* Use chrono for UTC/calendar conversion instead of hand-rolled copies

Replaces the hand-rolled civil_from_days/days_from_civil copies and the
leap-year/month math around them (log timestamp, PDF /Info dates, ASN.1
UTCTime/GeneralizedTime, FormCalc Date/Num2Date, the Combine modified-time
stamp, the form date picker, and the demo-PDF timestamps) with chrono as a
workspace dependency. The engine's existing chrono (clock) is lifted to the
workspace. Every output format is unchanged and pinned by tests. CLAUDE.md now
says time goes through chrono.

Follow-up to storytold/cadcraft#1785

* Guard chrono date paths: no panic on overflow, no '+' on 5-digit years

Systemic chrono follow-ups to the UTC/calendar PR:

- logging::timestamp and combine_ui::utc_stamp dropped chrono's
  From<SystemTime> (which unwraps internally and panics on a clock past
  year 262143) for the fallible DateTime::from_timestamp, reading an
  out-of-range clock as the epoch, as the old clamping code did.
- timestamp, utc_stamp, pdf_date and the xtask Stamp now write the year
  with {:04} instead of %Y, which prefixes a '+' before years past 9999;
  the old hand-rolled code wrote the plain zero-padded year.
- forms_ui calendar navigation and length, and formcalc days_in, use
  checked_add for the December year successor so y + 1 can't overflow at
  i32::MAX / i64::MAX (reachable from a parsed or persisted date).

Docstrings added to the chrono-touched civil_from_days and Time::from_unix
for the docstring-coverage gate. Tests cover the out-of-range-epoch and
10000-01-01 cases for each.

* Don't build an unrepresentable SystemTime in the far-future test

UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.

* Keep FormCalc dates exact past chrono's range

MAX_DAYS accepts day numbers up to 1e8 (years past 270000), but NaiveDate
stops near ±262143, so Num2Date(99000000) and Format fell back to
1900-01-01. days_from_civil, civil_from_days and days_in now have chrono
convert within the date's 400-year Gregorian cycle and add whole cycles
back (via i128), which gives the pre-chrono results for every year. Tests
cover accepted inputs past chrono's range on both sides of the epoch,
through Num2Date and Format, with values from the old formula.

* Bump rustls-webpki and webpki-root-certs for RUSTSEC-2026-0382/0383

rustls-webpki 0.103.15 -> 0.103.16 and webpki-root-certs 1.0.9 -> 1.0.10
(both semver-compatible, lockfile only) drop the distrusted emSign
intermediates and root. Only these two entries change; cargo deny check
advisories passes. Upstream main fails the same check.

* logging/cos/sign/combine/demo: keep extended-year dates, no epoch fallback

For consistency with the stricter no-epoch-fallback rule now agreed across the sibling
chrono PRs (wordcraft#3427, filmcraft#2662): `DateTime::from_timestamp(..).unwrap_or_default()`
changes a timestamp the old code converted past chrono's year 262143 range — it collapses
to 1970-01-01 instead of the real proleptic-Gregorian date. The FormCalc Num2Date path was
already fixed (FredApps approved); this applies the same fix to every remaining conversion.
Each site uses the 400-year Gregorian cycle (146_097 days) with chrono as the only calendar
logic: chrono converts the day within one cycle, whole cycles carry in the year (i128,
saturated into i64). PDF/ASN.1 format-specific year handling is kept exactly as the old code
had it. Verified the cycle result equals the old formula across a wide range and at the extremes.

- apps/pdfcraft/src/logging.rs `timestamp`: cycle `civil_from_days(u64)`; pre-1970 epoch clamp
  and ms/time-of-day format kept. Pins: +10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z
  (was 1970); checked_add u64::MAX/2 s -> 292277026596-12-04T15:30:07.000Z (was epoch).
- crates/cos/src/writer.rs `pdf_date` (i64): cycle converter, no year clamp (old code wrote `y`
  as-is, so a negative year keeps its sign). Pins: pdf_date(10_000_000_000_000)="D:3188570520174640Z",
  pdf_date(-10_000_000_000_000)="D:-3149180813061320Z".
- crates/sign/src/der.rs `Time::from_unix` (i64): cycle converter; the `year` field is a u32, so
  the year is cast `as u32` (wrapping) exactly as the old code did — ASN.1 year width preserved
  byte-for-byte. Pins: from_unix(10e12).year=318857, from_unix(-10e12).year=4_294_652_378,
  from_unix(i64::MAX).year=219_250_468, from_unix(i64::MIN).year=4_075_720_767.
- crates/ui-egui/src/combine_ui.rs `utc_stamp` (u64): cycle converter; pre-1970 epoch clamp kept.
  Pins: utc_stamp(10e12)="318857-05-20 17:46 UTC", u64::MAX/2 s="292277026596-12-04 15:30 UTC".
- xtask/src/demo_pdf/pdf.rs `Stamp::from_unix` (i64): cycle converter, no year clamp. Pins:
  from_unix(10e12).iso="318857-05-20T17:46:40Z"/.pdf="D:3188570520174640Z",
  from_unix(-10e12).iso="-314918-08-13T06:13:20Z".

cos/sign/xtask built, tested (pdf_dates_are_utc_and_byte_identical, der::tests::times,
stamps_round_trip), fmt and clippy clean. pdfcraft-ui-egui and the pdfcraft app can't link
here (unrelated LLVM intrinsic bug in the rten-gemm dependency, 1.98.1), so their logging and
combine_ui functions + tests were copied verbatim into a scratch crate with chrono 0.4.45 and
pass there; fmt --check clean on all five packages. CI builds these on all platforms.

* Don't build a year-318857 SystemTime unchecked in the logging test

Windows' SystemTime only reaches about year 30828, so
UNIX_EPOCH + Duration::from_secs(10_000_000_000_000) panicked before
timestamp() ran and failed the Windows test job. Build it with
checked_add and assert where the platform can hold it. The year-10000
case fits everywhere and stays unconditional.

* Name the 400-year range extension as the one exception to the time rule

The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.

* Say the library computes month and day; time-of-day is plain arithmetic

Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
2026-10-21 19:36:46 -04:00
ffishmanandClaude Opus 5.5 63b4af8ba5 fix(sign): embed the issuers of macOS Keychain identities
A Keychain identity came back with an empty `chain`, so a signature made with
it carried only the signer's certificate. A validator that doesn't already
hold the intermediates (Acrobat, or PdfCraft on another machine) then can't
build the path to the root. The Windows certificate store identities already
carry the chain Windows builds.

`keychain::find` now asks the Security framework for the chain (SecTrust with
the basic X.509 policy, no network fetch) and keeps every certificate but the
signer's. It is best effort: a certificate macOS can't chain still signs.

`chain-legacy.p12` is `chain.p12` re-exported in the legacy format that
`security import` accepts, for a new ignored Keychain test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 14:27:52 -03:00
ffishmanandClaude Opus 5.5 b941402558 M9: require eContentType and at most one certificates and CRLs field
The bundle's encapContentInfo must start with its eContentType, and the
optional [0] certificates and [1] CRLs appear at most once each, in that
order (RFC 5652 §5.1; CodeRabbit on #3561).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 16:54:43 +00:00
ffishmanandClaude Opus 5.5 f96e440a47 M9: check the bundle's SignedData shape and refuse trailing bytes
A .p7b must be SignedData with version, digestAlgorithms,
encapContentInfo, optional certificates and CRLs, then signerInfos
(RFC 5652 §5.1), and nothing may follow it (CodeRabbit on #3561).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 16:46:26 +00:00
ffishmanandClaude Opus 5.5 ddff6b64fe M9: refuse an unclosed PEM block; review follow-ups
A PEM file whose last block never ends is now an error instead of a
shorter certificate list (CodeRabbit on #3561). The test's doc comment
for the bundle builder is back above its function.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 15:42:04 +00:00
jonathanwxh-cell a10c0d8937 Merge branch 'upstream/main' into fix/typed-signature-winding 2026-10-20 08:15:30 +08:00
ffishmanandClaude Opus 5.5 0a56784df8 M9: read .p7b/.p7c certificate bundles for trust import
load_certificates now reads PKCS #7 certificate bundles (degenerate
SignedData, RFC 5652 §5.2) in DER and as PEM PKCS7/CMS blocks, the way
Windows, Acrobat and certificate authorities export chains. PEM END
lines must match their BEGIN label.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 20:32:37 -03:00
oracola91 ffe99c192d fix(sign): eContentType must be data, and both content types must be OIDs
A content-type attribute value or an eContentType that was not an OID
was read as missing, which skipped the check that the two match. Such a
CMS is now not read, and neither is one without an eContentType.

A PDF signature's eContentType must also be data. With a content-type
attribute this already follows from the checks on the attribute.
Without one, a signature whose eContentType named another type could
still validate as a signature of the document. It is now invalid.

The PDF test now covers a wrong eContentType and a repeated
content-type attribute, and the README says that a missing
content-type attribute is read anyway, with a note.
2026-10-19 23:45:45 +03:00
oracola91 4f56e18103 fix(sign): one content type and one message digest in the signed attributes
RFC 5652 allows one content-type and one message-digest attribute in
the signed attributes, each with one value. The parser kept the last
attribute and the first value, so a CMS with two of either could mean
one thing here and another in a different reader. Such a CMS is now
not read.

The content-type attribute must also name the type of the encapsulated
content. The signature covers the attribute but not eContentType, so a
CMS where the two differ is not read either.
2026-10-19 23:38:58 +03:00
oracola91 737f2f725f test(sign): a small RSA key's signature verifies with the short-key note
verify_noting adds the note only when the signature verifies. The test
uses a synthetic 1024-bit key and its PKCS #1 v1.5 signature: the valid
signature carries the note, and one that doesn't verify has none.
2026-10-19 18:57:26 +03:00
oracola91 cb226fb08b fix(sign): the key description counts the modulus bits like the warning
describe() named an RSA key by its modulus length in bytes times
eight, so a 2047-bit key read "RSA 2048-bit" next to the new warning
that it is shorter than 2048 bits, and a modulus with a leading zero
byte read 8 bits too long. It now uses rsa_bits, as weakness() does.
2026-10-19 18:27:34 +03:00
TomCatTandClaude Opus 5.5 4d42b48d20 fix(sign): offer only certificates that may sign documents from the Windows store (#3365)
A CAC puts its encryption, logon and signing certificates in the store
under the same name, so the digital ID list showed several identical
entries and only one of them makes a signature that validates. Grey out
the ones whose key usage or extended key usage doesn't allow document
signing, with the reason, the same way an ID without a usable key is
already treated.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:49:57 -04:00
oracola91 343f1b05cc fix(sign): signed attributes must carry the message digest and the data content type
With signed attributes, the signature value is made over the attributes,
and their messageDigest is what ties it to the document. The digest was
compared only when the attribute was present, and the content-type
attribute was never checked. RFC 5652 section 5.3 requires both.

A signature whose signed attributes have no messageDigest, or name a
content type other than id-data, is now invalid. A missing content type
is reported and the signature is read anyway. A signature that still
validates with an RSA key under 2048 bits now gets a note, as SHA-1 and
RIPEMD-160 do. The four new sentences are in WORDINGS and in Spanish.
2026-10-18 21:19:46 +03:00
Ridzzz0Alam 0fe2c07091 M9: read DocMDP permissions from the signed revision (#1788)
A certification signature's /P was read from the current document, so
a later revision could rewrite P 1 to P 3 and have prohibited changes
judged as permitted. Rewriting the existing SigRef/TransformParams was
itself classified as a signature change, allowed at every level.

/P is now taken from the signature in the signed revision (the one
whose ByteRange ends that revision), and a changed Sig, DocTimeStamp,
SigRef or TransformParams that existed when signed is no longer a
signature change.

Test: a P 1 certification relaxed to P 3 in a later revision, plus a
comment, was Valid with the changes permitted; it is now Invalid.
2026-10-17 15:44:30 +02:00
mtlcl410andClaude Opus 5.5 5b813ce985 fix(sign): explain 0xC0000225 when a token's key provider can't find the key (#1535)
STATUS_NOT_FOUND passed through by a smart card or token provider (an A3
certificate whose token isn't connected) was shown as a bare code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-17 16:39:54 +08:00
mtlcl410andClaude Opus 5.5 07524f5e8c fix(sign): explain 0x8009001D when a store key's provider fails to load (#2207)
Windows reports NTE_PROVIDER_DLL_FAIL when the key storage provider
holding a certificate's private key can't load its DLL: smart card or
token middleware that is missing or broken, or a provider that exists
only as 32-bit code a 64-bit process can't load. The certificate list
showed it only as "Windows error 0x8009001D"; it now says what that
means, as it already did for a missing card or provider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-17 15:29:59 +08:00
mtlcl410andClaude Opus 5.5 4a35a1cb23 M9: rewriting a certified form's XFA data is a form fill (#2759)
The IRCC IMM forms are certified (DocMDP P=2) and then saved once more
with Reader Extensions: a later revision adds the UR3 usage-rights
signature and rewrites the XFA `datasets` packet. The diff classified any
rewritten stream that wasn't page content or the catalog's XMP as "other
changes", so these untouched forms validated as invalid.

A stream the signed revision's /AcroForm /XFA array names `datasets` or
`form` (the form's data and state) is now a form fill when rewritten,
permitted at P=2 and P=3 and refused at P=1 as before. The roles come from
the signed revision, so a later revision that names the template
"datasets" doesn't make rewriting it a form fill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-17 01:21:28 +08:00
Chiranjeevi Uandskylar taylor-barrick 5fdd820bdb fix(ui): validated signature display, snapshot tab reuse (#1895)
* fix(ui): validated signature display, snapshot tab reuse

Signature widgets with layered appearances baked the unverified state
into the page, so a valid signature kept reading 'Signature Not Verified'
while the banner and panel said valid. Validated verdicts are now drawn
into the icon and caption layers in the display copy only: the signed
bytes are untouched, saving keeps the original appearance, and files
without those layers or without a valid verdict render exactly as before.

Other valid signatures still get the verdict mark over their widget,
sharing one style with the Signatures panel, and hide it again once the
appearance itself shows the verdict.

View signed version and View revision opened a new tab on every click,
and every tab showed the document title, so repeats piled up identical
tabs. Repeats now reuse the snapshot tab, snapshot tabs read like their
source plus the suffix in the tab strip, window title and Combine picker,
and carry a notice bar naming the revision with a Close that asks about
saving first.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* Address review comments on validated appearances and trust refresh

Count every field when checking shared appearance streams, so a
caption shared with an unvalidated field is never rewritten for the
valid one alone.

Pick the icon slot by drawing instead of length, and only when a
single layer qualifies. Bare rectangles never count as the icon, and
ambiguous or missing slots fall back to the overlay badge.

Revalidate and invalidate together through one helper, so trusting a
certificate always drops rendered pages. Trust alone still marks
nothing dirty.

Tests: mixed-verdict sharing, longer decorative borders, competing
and doubled layers, capture-before preservation, trust invalidation
without zooming, and signed Session round trips with rendering,
saving and passwords.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* Prove the icon slot geometrically and cover real layered signatures

A lone line drawing is still not the icon: frames and borders span the
box while the status mark lives in a corner of it. The icon now has to
sit strictly inside the appearance frame and cover at most half of it;
anything else falls back to the overlay badge. Widgets without a
measurable frame veto the rewrite the same way.

Tests: parsed extents and inhabitant checks as units, both reported
border repros plus a frameless widget, and real layered signatures
built with cms::sign_detached through Session open, trust, rendering,
save and reopen, plain and password-protected.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* Identify the icon by shape instead of geometry

A lone line drawing is still not the icon, but size and position were
the wrong signals: the real status mark is large, and geometry blocked
it. Frames, borders and panels are rectangles by nature while the mark
is not, so a drawing made only of axis-aligned rectangles is decoration
however it is stroked. Singularity, shared-stream and template checks
are unchanged; anything ambiguous still falls back to the overlay badge.

Tests: rectangularity as a unit, nested line-drawn frames, and a
triangular fixture mark in place of the rectangular one.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* Budget the appearance walk and keep the marks with the bytes

The walk that finds a signature's icon and caption capped depth,
layers and fields, but not the work: a dictionary whose entries all
point at the next one is visited once per entry per level, so a small
hostile file never finishes opening. The walk now counts its visits
and gives up, and a field whose tree cannot be read to the end stops
the whole pass, because a layer we never read may be the one two
fields share and a rewritten shared layer puts one field's verdict on
another field's page. The sharing count is exact again with that: two
widgets of one field pointing at the same appearance claim it once,
or the field vetoes its own rewrite.

A mark used to outlive the bytes it described. A failed display write
left the page showing the file's own caption while the verdict still
claimed its state was drawn; a metadata edit dropped the mark while
the page kept drawing the state, so the badge landed on top of it. The
write fallback now drops the marks and logs the loss, and metadata
carries them over.

Tests: a self-referential appearance tree, which never finished before
the budget, two widgets sharing one appearance, a metadata edit that
keeps its mark, a save that goes through the writer instead of
short-circuiting on the opened file, and the snapshot bar's Close
button pressed for real.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* Cite the new regressions in parity, restore main's installer note

The appearance entry now cites the bounded walk and the save through
the writer, and says what a tree that cannot be read to the end does.
The installer entry carried main's older text, which still claims
signed installers are planned; main's newer note is taken verbatim.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: preserve rounded decoration beside raster status glyphs

* M9: place appearance progress beside signature history

* M9: retain appearances with ambiguous curved layers

* M9: keep signature verdicts in viewer overlays

Defer generic appearance rewriting: caption text and a unique drawing do not establish an icon role. Preserve original artwork for saves, image and text exports, and both print modes while retaining snapshot reuse and trust refresh. Cover chamfered borders and mixed signer/caption streams through signed Session round trips.

---------

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
2026-10-15 19:16:48 -06:00
jonathanwxh-cell f536ac7a05 fix(sign): fill typed signature outlines with non-zero winding (#1786) 2026-10-16 07:24:33 +08:00
RidwanandBrandon Thomas 586f2b2be3 M9: harden .p12 digital IDs (#1847) (#2201)
* M9: harden .p12 digital IDs (#1847)

New digital IDs are written with 600,000 PBKDF2-HMAC-SHA-256 rounds (OWASP)
for both encrypted parts and the MAC, instead of 2048. The MAC gets the same
count because checking it alone tells whether a guessed password is right.

Opening a .p12 now spends at most 10 million rounds of key derivation across
the whole file, charged before any hashing. The file chose its own iteration
counts unchecked (`u64 as u32`), so a crafted file could hang the password
check for about an hour, or wrap 2^32+1 to one round.

Creating an ID needs at least 8 characters (NIST SP 800-63B), from one
constant, pkcs12::MIN_PASSWORD_CHARS, shared by the app, sign_id_create and
its MCP schema. Opening an existing ID takes any password.

* Refresh catalog hashes after merging main

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:48:32 -04:00
716a957186 fix(sign): a signed page's resources are page content whatever keys a later revision writes (#1787) (#2115)
A rewritten resources dictionary, font, XObject or graphics state of a
signed page changes what the page shows. It was judged by the keys its
new version carried, so adding /FT or /Subtype /Widget made it a
permitted form fill. The roles now come from the signed revision's own
object graph, walked from each page's resources with bounds; a category
dictionary that only gained a name is judged as before.


Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:45:35 -04:00
Zeyu LiandBrandon Thomas f52c92882d M9: signature labels follow the interface language, and one the font can't draw is refused, never written as ? (#1951)
* M9: signature labels follow the interface language, and one the font can't draw is refused, never written as ?

The labels of a visible signature ("Digitally signed by", "Reason: ", ...) were written in WinAnsi unchecked, and every caller passed the English defaults. Now:

- sign: a drawn label is checked as the details are, and one Helvetica can't draw is refused (SignError::Unsupported) naming the label and the character. AppearanceLabels::fields and ::first_undrawable say which labels a caller can use.
- ui-egui: signing stamps the labels in the interface language when Helvetica can draw all of them, otherwise all in English (never a mix) with a note under the preview. The preview uses the same labels, the stamp's line layout and the signing time.
- automation: sign_document takes optional labels and appearance; a label the font can't draw, an unknown key or one without a visible signature is InvalidArgs.
- Catalogs: the five labels and the note in every language; refresh hashes.

Follows up the note on #1112.

* Refresh catalog hashes after merging main

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:32:38 -04:00
e07759d657 fix(sign): a signed revision of a password-protected file opens with the document's key (#1808) (#1945)
To classify what later revisions changed, the signed bytes were reopened
with Document::open, without a password, so for a file opened with a
nonempty user password the earlier revision was unreadable and the earlier
signature became Invalid as soon as it was counter-signed. Document::
open_with_same_credentials opens such bytes with the current document's own
security handler when their /Encrypt and /ID are the same (no password is
needed again or kept), and the signature validator uses it.


Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:31:30 -04:00
mtlcl410andClaude Opus 5.5 c14602993f fix(sign): an unreadable extendedKeyUsage allows no purpose (#1807) (#1866)
A malformed EKU left extended_key_usage at None, which reads as "no
restriction", so an issuer-signed certificate with a broken EKU could reach a
Valid verdict; EKU being on the processed-critical list let a critical one
through as well. Like keyUsage, a present EKU now restricts the key even when
it doesn't parse: it must be a SEQUENCE whose members are all OIDs, otherwise
it allows no purpose.


Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:27:33 -04:00