Commit Graph
125 Commits
Author SHA1 Message Date
bflatastic e70cacf36f Merge #6290: fix(render): a stream with no endstream after it draws from its /Length instead of a blank page (hayro-syntax patch 36)
# Conflicts:
#	crates/render/src/raster/tests.rs
#	vendor/README.md
2026-11-09 08:11:43 +07:00
oracola91 72311bc101 Signature validation takes the certification from the catalog (#6453) 2026-11-09 08:50:39 +08:00
BFlat 53eec5cd24 Merge pull request #6502 from mtlcl410/fix/sign-string-bytes-not-spelling
M9: a signed string respelled in hexadecimal is no change after signing (sign.changes-after-signing)
2026-11-09 08:48:07 +08:00
jonathanwxh-cell 5ee4a85876 fix(sign): merge CMS unsignedAttrs into one SET and validate SignerIdentifier (#6137) 2026-11-09 08:44:34 +08:00
Dylan M. Taylor 9642ed6f87 fix(sign): read each existing DSS stream once and within a decoded-size allowance (#6040) 2026-11-09 08:42:09 +08:00
jonathanwxh-cell 1f48670fce refactor(cos): consolidate duplicate civil_from_days onto pdfcraft_cos::civil_from_days (#6540)
Fixes #6466
2026-11-09 08:32:08 +08:00
FredApps 82c2009d09 Merge pull request #6208 from vangel76/fix/5939-sign-need-appearances
Signing a form that sets NeedAppearances no longer makes Acrobat ignore the signature
2026-11-08 13:31:20 +01:00
vangel76andClaude Opus 5.5 dcad5ff3b3 fix(sign): zero-filled placeholders are unsigned; look inside appearance XObjects (#5939)
- `has_signatures` treats a /Contents of only zero bytes as unsigned and finds
  signature dictionaries in one pass over the file (field /V values, plus
  /ByteRange dictionaries and document timestamps) instead of two.
- Before the first signature, a field's own appearance counts as drawing text
  only if it or a form XObject it paints shows text; a background-only XObject
  no longer keeps an appearance that would leave the field blank. The look
  inside opens at most 16 streams; past that it counts as text, as before.
- Tests: zero-filled placeholders (hex and literal), document timestamps, a
  widget without /AP, background-only and text-drawing XObject appearances
  with an unencodable character.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 05:31:05 +01:00
vangel76andClaude Opus 5.5 d4b8c94a00 fix(sign): draw the form's fields before the first signature drops /NeedAppearances (#5939)
Once the flag is gone, viewers show each field's stored appearance, so a
filled field whose /AP /N draws no text (a /Tx BMC EMC placeholder or a
stale look) went blank. Session::sign and sign_with_timestamp now sign the
copy with_field_appearances draws, as the document is displayed, when the
signature is the first. A field whose own appearance draws text keeps it
where PdfCraft's drawing would show a character as `?`.

The earlier-signature check is now pdfcraft_sign::has_signatures, shared by
the engine and the signer, and it also finds signed fields without a /T.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 04:30:02 +01:00
Chiranjeevi U adbf1dc01f M9: signed appearances show their real validation state (#79) (#2808)
* M9: redraw named-layer signature status layers

- /N drawing one Form that holds n1 and n3: n1, n3 and n4 redrawn for the state
- copies made once per original and state; originals never edited
- Lucide icons; captions in Helvetica, embedded Inter, or craft-fonts outlines

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

* M9: draw signature states in the display copy, exports and print

- marks follow the bytes shown; a changed verdict redraws after a metadata edit
- background opens pick up the trust and captions current at commit

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

* M9: status captions in the interface language

- the three captions in all 22 catalogues
- Signatures panel colours from sign::status_rgb

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

* M9: signature badge in the widget's corner

- a quarter of the shorter side, 48pt cap; outline only under 32pt
- dropped for a drawn valid state; bottom-right where the appearance draws one
- sign_list reports verdict_shown; parity and roadmap

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

* M9: keep Arabic signature captions in logical order

Pass raw RTL catalogue text to the signature appearance shaper so UAX #9 is applied exactly once. This keeps the rendered word order and exported /ActualText consistent.

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

* M2: read /ActualText over painted paths and pictures

- A replacement that marks no glyph reads where it paints, cut to what is visible and along the way it is drawn.
- A span the page content leaves open ends before the annotations.
- OCR and scan field detection still count such a page as having no text of its own.

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

* M8: redaction strips /ActualText around paint it clips

- A path or shading written again under a clip counts as changed, so a partly redacted word drawn as outlines leaves no name in the file.

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

* M9: recognise only layer forms that draw n1 and n3

- Settle on the one form holding both first, then read its content once per call.
- Skip an /N that is its own layer form; copies keep the layer's /OC.
- A verdict counts as drawn when any visible widget shows it.

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

* M9: keep the signature badge and redraw wherever a state changes

- The badge stays for every state; its mark sits bottom-right as the widget lies on the page.
- An edit that changes a verdict redraws every page; captions apply before startup files open.
- Print, the CLI's render and the repair log follow the display copy.

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

* M9: restore the attribution usage the rebase dropped

- Usage lines for the three icons, Inter and the craft-fonts input; ATTRIBUTION.md rebuilt from them.
- Roadmap and localization notes for this round.

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

* M9: pin signature field names on a nested form

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

* M9: bound the signature field walk

- kids share their parent's name and type instead of each copying them
- a widget stays listed under every field that claims it, as one shared copy
- a field written in place in a shared /Kids array is walked once
- caps of 20,000 fields, 100,000 entries and 4096-byte names; no states drawn past them

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

* M9: keep a document signed when the field walk stops early

- list_cached adds a signed row of unknown validity for the fields the walk didn't read
- Save As and actions then append, and Optimize, Sanitize and full rewrites are refused, as for any signed file

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

* M8: removing hidden content no longer strips the names of shadings

- With no regions to clip, a shading is left alone instead of wrapped and marked clipped
- clean_replacements skips scopes already marked, so deep nesting stays linear

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

* M2: read a picture's /ActualText over the whole picture

- draw_image's transform maps pixels, not a unit square, so the text sat on one pixel in the corner; the test now draws a 2x2 picture
- Painted grows in place instead of being copied through an Option

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

* M9: cap the bytes of names the signature field walk keeps

- A kid's name repeats its parent's, so one long name over 20,000 kids came to 80 MB; the walk now stops at 4 MiB of names and counts the rest as unread
- Cover the widget written into a shared /Kids array and the field cap among kids, which no test caught
- sig_fields is private again, and the cap docs say what happens past each cap

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

* M9: skip status-layer work that changes nothing; fix tests that could not fail

- show_status_layers and has_status_layers return before reading the form when nothing is signed, so an unsigned form no longer walks every field on each edit
- revalidate and draw_captions leave documents with no drawn state alone, and no longer copy the signature list to update it
- Scan form detection reads the page's text once; comment and badge tips share one hover rule
- Tests: drop a table row that repeated another, stop taking an expectation from the code under test, stop comparing an Arc with itself, give bare asserts messages

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

* M9: test the signature field name cap at the engine boundary

- A 4,096-byte name is read and blocks a full rewrite; at 4,097 bytes the list holds the unread-fields row and a full rewrite is still refused

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

* M9: test signature states beside fields the viewer draws

- One display copy holds both a field's drawn appearance and the signature's state.

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

* M9: draw Thai and Devanagari status captions in their own script

- script_of names Thai and Devanagari, so a craft-fonts face meant for either draws the caption as outlines; without one it stays English.
- Localization, parity and the craft-fonts usage line list the new scripts.

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

* M2: paint left out by an exclusion mixes its /ActualText replacement

- A replacement placed by outlines or pictures, some of them excluded, fails filtered extraction as one with excluded glyphs does.
- Glyphs still place a replacement that has them.

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

* M9: a long-named field that can't hold a signature leaves the walk whole

- A leaf of another type passed over for its name is no signature field and has none below it, so an unsigned form stays unsigned.
- A signature field, or any field with kids, still counts as unread.

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

* M9: status captions use document faces only

- Each script takes the face PdfCraft writes it into documents with, so Chinese stays English although the pinned craft-fonts' interface-only Chinese face is meant for Hani.
- The craft-fonts usage line names Devanagari: the pinned revision has a face for it, and Hindi captions are drawn in it.

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

* M9: refuse a new signature field the field walk can't read back

- Signing adds a field only when the walk reads the whole form with it: past a cap the signature would be listed as unread fields, and its name could repeat an unread field's.
- A field the walk already reads is signed as before.

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

* M9: refuse a new signature field with a blank name

- The walk lists no field without a name, so signing now also requires the walk to list the new field: one named only with spaces or NULs would be a signature nothing shows.

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

* M9: an unnamed signed field or an unreadable form counts as unread

- A signed field with no usable /T, or an /AcroForm or /Fields of the wrong type, cuts the field walk short: the document keeps its unread-fields row and stays signed.
- Signing refuses a blank new field name before it adds the field, so the refusal still says why.

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

* M9: no display copy when no signature state is drawn

- display_bytes_for makes a copy only when it added an appearance or drew a state, as the app does.

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

* M9: the verdict mark stays on the page

- A widget past the crop box takes its mark in the part on the page, or none when too little of it is there.

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

* M9: typed values stay drawn while a signature redraws every page

- A form edit that changes a signature's state puts its values back in unbaked after the pages are invalidated.

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

* M9: the action, Save As and shading tests check their effects

- The signed action and Save As tests reopen the output for the title and the comment; the shading test checks its clip.

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

* M9: values typed earlier stay drawn while a signature redraws every page

- A form edit that changes a signature's state keeps every value still waiting on its page, not only its own.

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

* M9: comment and content edits keep typed values drawn too

- An edit to one page's comments or content that changes a signature's state moves no page, so the values still waiting on their pages stay pending, as after a form edit.
- The pending-value test runs a field deletion and a comment after a fill, under a certification that allows the fill; its messages say what it checks.

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

* M9: the language-change test skips without catalogs

- Like the other language tests, it returns early when the build has no craft-i18n catalogs.

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

* M9: signed fields behind kids without a name count

- A kid with no /T but kids of its own is walked as a field node, so a signed field under it is listed by its full name.
- An unnamed kid claiming a signature its field doesn't have cuts the walk short, so the document stays signed and keeps its unread-fields row.

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

---------

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
2026-11-07 20:37:17 -06:00
vangel76andClaude Opus 5.5 66ee360067 docs(sign): note the /NeedAppearances removal and its rendering cost
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 02:18:55 +01:00
vangel76 fda0b9ece0 Merge remote-tracking branch 'upstream/main' into pr6208-local 2026-11-08 02:17:34 +01:00
vangel76andClaude Sonnet 5.5 7b6baec065 sign: an empty /Contents is not an earlier signature
Only a non-empty signature value counts when deciding whether the first real
signature may drop /NeedAppearances.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 16:54:58 +01:00
mtlcl410andClaude Opus 5.5 44428b36d3 M9: a signed number saved back as an integer is no change after signing (sign.changes-after-signing)
ISO 32000-2 §7.3.3: wherever a real number is expected, an integer may
be used instead. PdfCraft's own writer writes a whole real as an
integer, so a form fill that rewrites a signed widget turned
/Rect [20.0 20.0 120.0 40.0] into [20 20 120 40] and was refused as a
page content change. same_object now compares an integer and a real by
value (exactly, in i128), for the widget /Rect and every other signed
comparison, such as a page /MediaBox rewritten when a comment is added.

Also states the reordered page's verdict as None explicitly, in a test
of its own next to the one from #5962.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
2026-11-07 12:46:10 +08:00
mtlcl410andClaude Opus 5.5 715c310f5b M9: a signed string respelled in hexadecimal is no change after signing (sign.changes-after-signing)
A string object is its bytes; literal and hexadecimal are two ways to
write them (ISO 32000-2 §7.3.4.1-7.3.4.3). The modification check
compared objects with the derived equality, which also compares how a
string was written, so a later save that rewrote the catalog's /Lang
from (en-US) to <656E2D5553> made an untouched signature read as a
document structure change.

same_object now compares strings by their bytes, and every comparison
in changes.rs goes through it: the unchanged-object skip, the catalog,
its /AcroForm and /Perms, developer extensions, signed widgets, page
resource containers, the security store and the timestamps check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
2026-11-07 10:25:41 +08:00
mtlcl410andClaude Opus 5.5 ef2d762ec5 Merge upstream/main into fix/sign-string-bytes-not-spelling
Conflict in crates/sign/src/pdf/changes.rs: main moved the signed-page
check into ChangedObjectClassifier::page_change_kind. The method now
uses same_but_annots, as the free function it replaced did on this
branch; the branch's tests reach it through a test helper of the same
name, and both test sets are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
2026-11-07 10:20:53 +08:00
dc5cb122db trust certificate key type when CNG algorithm group is unreadable (#6391)
* trust certificate key type when CNG algorithm group is unreadable

When the algorithm group can't be read from the key store due to a provider bug, the certificate should not be rejected: its SubjectPublicKeyInfo already states the key type, and a real mismatch will surface at signing time instead of at enumeration.

Change matches_key to fall back to Ok(()) when key.algorithm_group() returns an error, trusting the certificate's key type; extract the group comparison into key_group_matches(group: Option<&AlgorithmGroup>, want: StoreKey) so the fallback and mismatch cases can be unit tested without a live CNG key.

Test: key_group_matches_trusts_when_group_unreadable failed before the change and passes after.

* Sign: back to LF line endings, shorter test comment

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

---------

Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:23:18 +01:00
LetoCoder26 fbe079c953 fix(sign): compare the catalog's /Perms by content under P=1
Rebased #5173 onto 9afa1266. Its later-signature predicate is superseded by
#6364's object-based check, so only the /Perms rule lands: under DocMDP P=1 a
/Perms that differs from the signed catalog's, by content, is a structure
change however it was rewritten.

Co-Authored-By: LetoCoder26 <leto@lvanwesten.eu>
2026-11-05 21:43:14 -05:00
LetoCoder26 560c68db89 fix(sign): an existing object is judged by the role it had when signed
Rebased #5187 onto 9afa1266. Landing the signed-role rule only: an existing
object that gains form or annotation keys is judged by what it was when
signed, and a signed annotation turned into another kind is no comment.

The two P1 regressions the original PR carried are not landed: the signature
decision still reads the new /FT (a signed widget relabelled as a text field
is not signing), and the dropped-/Fields check runs before the form-key
comparison (already carried by #6358's lost_entries).

Co-Authored-By: LetoCoder26 <leto@lvanwesten.eu>
2026-11-05 21:41:29 -05:00
Martin Ball ddeca878df sign: judge the signed page tree, catalog and form by their signed roles
Rebased onto 102cae3b/9afa1266 for landing (#6358). The signed page tree's
nodes, a /Kids array of its own, the catalog by object number and an indirect
AcroForm keep the roles they had when signed; a field dropped from the form is
no permitted change at any level; a signed widget's /Rect or /F is page content.

Co-Authored-By: Martin Ball <martin.ball@ucscaregroup.co.uk>
2026-11-05 21:40:30 -05:00
Dylan M. Taylor 9afa126653 Merge pull request #6364 from benandez/fix/sign-p1-only-invisible-timestamps
sign: under no-changes certification, only invisible document timestamps may follow
2026-11-05 19:51:52 -05:00
Martin BallandClaude Opus 4.8 e1a3c033aa sign: under no-changes certification, only invisible document timestamps may follow
DocMDP P=1 allowed any change classified as a signature. A signature added
after signing now breaks the certification unless it is a document timestamp
drawing nothing (no positive-area /Rect, or Hidden/NoView). The added objects
are judged directly, so a widget with no /T or one only in a page's /Annots is
covered. The document security store and a genuine timestamp still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-11-06 00:19:58 +00:00
Martin BallandClaude Opus 5.5 de344a4bb1 sign: compare annotation lists by what resolves, within a bound
An annotation list is compared by the entries that resolve in each revision,
and an unchanged list is no change. Expanding annotation lists is bounded per
classification. A damaged slot rewritten with fewer accepted generations is the
same slot when nothing signed refers to a generation it gave up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 00:10:06 +00:00
Martin BallandClaude Opus 5.5 ce3857c2e5 sign: classify changes by signed object role
Judge each object changed after signing by the role it held in the signed
revision, keyed by object number, and compare a slot's accepted generations
across revisions. Adds `Document::accepted_generations` to cos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 00:10:06 +00:00
eliets2andeliets2 0d7ae1b0fc Tests: a signed encrypted document's byte ranges cover its earlier revision (#6303)
Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
2026-11-05 20:11:10 +01:00
mtlcl410andClaude Opus 5.5 1a8922203d M1: the sign discovery test reads a stream with neither endstream nor /Length as unreadable
With hayro-syntax patch 36 and its cos counterpart, a stream with no
endstream left in the file is the bytes its /Length gives, so the fixture's
`<< /Length 0 >>` stream without endstream now reads as an empty stream.
The test needs an object that cannot be read: a stream with no /Length
either still is one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
2026-11-05 22:09:41 +08:00
vangel76andClaude Sonnet 5.5 a371b24f2a test(sign): tighten the NeedAppearances tests (#5939)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 01:25:41 +01:00
vangel76andClaude Sonnet 5.5 a62633e902 fix(sign): the first signature drops /NeedAppearances so Acrobat recognises it (#5939)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 01:16:17 +01:00
NetterSilverandClaude Opus 5.5 eb3cfd4ff7 Ignore dictionary key order when checking signed pages for changes
Fixes #5955

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:08:30 +01:00
NetterSilverandClaude Haiku 5.5 fdaa502f27 Reject signed and empty arcs in try_oid (#5581)
Found while testing crates/sign/src/der.rs:467-472 (try_oid)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-01 15:04:32 -05:00
Dylan M. Taylor 5fecdb9cee Group signing and timestamp validation inputs 2026-10-27 23:25:40 +00:00
Dylan M. Taylor 074d4f3111 Refactor sign for strict Clippy checks 2026-10-27 23:12:51 +00:00
aiongg a1b094399e test(sign): read the split pdf modules in wordings_are_complete 2026-10-26 22:18:50 +00:00
aiongg 67be9ea2aa refactor(sign): split pdf.rs into pdf/
New modules: listing, types, validate, changes, signing.
2026-10-26 22:18:50 +00:00
skylar fc578a0b02 chore(deps): bump 11 safe dependency updates 2026-10-25 16:36:22 -06:00
aiongg f94b34adc9 Integration: fix(redact,cos): cleared redaction marks leave nothing behind (#4226) 2026-10-24 22:48:11 +00:00
aiongg 75f3a267eb Merge PR #4133: fix(sign): let an LTV update declare the ESIC extension in the catalog 2026-10-24 06:22:03 +00:00
aiongg 40258a5cc2 Merge PR #4132: fix(sign): judge a page's indirect /Annots array by the annotations it gains 2026-10-24 06:22:02 +00:00
a047c6ba09 chore: resolve SonarCloud maintainability issues (#4356)
* chore: resolve SonarCloud maintainability issues

* fix(packaging): use verify_sha256 helper function

* style: cargo fmt

* fix: restore wildcard imports and fix method-reference type errors

Revert the glob-import expansions (they missed SomIndices and left unused
imports), revert char::is_ascii_digit where the iterator yields char by
value, and pass the content slice to prune_doomed_xobjects as &[Vec<u8>].

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

* Keep XObject decodability check without binding content in interp

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
2026-10-23 23:28:18 -04:00
alehelandClaude Opus 5.5 ba33257fa3 fix(sign,cos): signing leaves out what was added and removed again
The signing saves wrote with default options, so a redaction mark added
and cleared in the session (or a comment added and deleted) reached the
file through a signature, a timestamped signature, embedded LTV evidence
or a document timestamp, with its QuadPoints, overlay text and authors.
A signed revision can't be changed afterwards, so nothing could take it
out again.

`sign`, `sign_with_timestamp` and `dss::embed` now save with
`drop_unlinked`: everything they add is reachable (the signature through
the field's `/V`, the store from the catalog), and the ByteRange
placeholder is found in the written bytes.

`timestamp_document` can't: its `/DocTimeStamp` dictionary has no field
pointing to it, so the option would drop it and every document timestamp
would fail with "ByteRange placeholder not found". It frees the leftovers
first with the new `Document::free_unlinked_additions`, which frees
exactly what the option leaves out, and then adds the dictionary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 21:32:46 +02:00
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
ffishmanandClaude Opus 5.5 c829a1ad44 fix(sign): a removed annotation is not hidden behind an added signature
Review follow-up: an indirect /Annots array that drops a signed
annotation while gaining a signature widget was judged by the widget
alone, so a form-fill certification let a signed comment be deleted.
A removed comment is now a comment change, and a removed widget is a
change no permission covers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 17:03:37 -03:00