Skip to content

fix: freeze the PDF certification level with the request snapshot - #9375

Open
paulo-raoni wants to merge 2 commits into
LibreSign:mainfrom
paulo-raoni:fix/8715-docmdp-freeze
Open

paulo-raoni wants to merge 2 commits into
LibreSign:mainfrom
paulo-raoni:fix/8715-docmdp-freeze

Conversation

@paulo-raoni

Copy link
Copy Markdown
Contributor

Refs: #8715

📝 Summary

Next slice of #8715: docmdp follows the request-snapshot freeze. As decided in the issue (2026-10-08, item 1), the signing operation uses the frozen request value: once a request is frozen, both signing engines certify with the level the request stores, not with the current system level.

One commit per step:

  • fix: certify a frozen request with the level it stores – until now JSignPdf and the PHP-native engine read only the current system level (DocMdp\ConfigService::getLevel()), whatever the request stored. SignFileService::configureEngine() now hands the request's level to the engine when the request (or its envelope) is frozen and has a stored docmdp, through SignEngineHandler::setDocMdpLevel(), next to the existing setters. A given level is used as is, in both directions: a request stored as not certified is not certified while the system level certifies, and the reverse. Without a stored level, or on a draft that was never sent, the current level applies as before. The level is set on every configureEngine(), null included, so it never carries over to the next file.
  • fix: freeze the PDF certification once the request is sent – DocMdpFilePolicyApplier::sync() returns before any resolution when the request is frozen. Nothing or null submitted keeps the stored level; the same level (also as a numeric string) is a no-op; any other value gets 422, "The PDF certification cannot be changed after the signing flow has started." This also covers a PATCH by uuid: docmdp was already in the core-flow sync.
Request Submitted docmdp Save Signing
Frozen (marker, or status ABLE_TO_SIGN or later without marker, or document of a frozen envelope), back in DRAFT included nothing or null kept, no resolution, no write stored level
Frozen the stored level no-op stored level
Frozen another level 422 stored level
Frozen, sent before docmdp was stored in the snapshot any level compared with the docmdp_level column current level, as before
Draft never sent anything re-resolved on save, as before current level, as before

Behaviour change: besides a later admin change no longer reaching a sent request, frozen requests now certify with their own resolved level instead of the system level. When a group or user rule differs from the system value (for example system 2, group 3), a request from that group was certified at 2 and is now certified at 3, which is the level stored for it. No payload changes; nothing new is sent to anonymous or #[PublicPage] endpoints. The request sidebar has no docmdp control, so there is no frontend change.

Not in this PR: the other snapshot keys and envelope new-file inheritance.

🧪 How to test

composer test:unit -- --filter 'DocMdpFilePolicyApplierTest|SignFileServiceTest|PhpNativeHandlerTest|Pkcs12HandlerTest'
vendor/bin/phpunit -c tests/php/phpunit.xml tests/php/Integration/Handler/SignEngine/JSignPdf/JSignPdfHandlerTest.php
vendor/bin/phpunit -c tests/php/phpunit.xml tests/php/Api/Controller/RequestSignatureControllerTest.php
  • PhpNativeHandlerTest / JSignPdfHandlerTest: a given level wins over the current one (another certifying level, not certified while the system certifies, certified while it does not); without one, the current level applies.
  • Pkcs12HandlerTest: the level reaches the concrete engine.
  • SignFileServiceTest: a frozen request (sent, back in DRAFT, sent before the marker existed, document of a sent envelope) hands its stored level to the engine; a request without a stored level and a draft never sent use the current one.
  • DocMdpFilePolicyApplierTest: the strict sync rules above; a draft still resolves the submitted level.
  • RequestSignatureControllerTest: a sent request keeps level 2 after the admin sets 3 and an unrelated PATCH by uuid; another level by uuid gets 422 and the snapshot is unchanged; a draft sent by uuid picks up the admin change.

Manually: enable DocMDP, send a request, change the certification level as admin, then sign: the signed PDF carries the level the request was sent with.

Validation evidence

  • Focused regression/acceptance test – red before the change, green after
  • Relevant lint/type/static-analysis checks – composer psalm, php-cs-fixer dry-run on the touched files
  • Broader suite required by the changed surface – partly, see below
  • CI reviewed

Ran locally, on top of current main: tests/php/Unit/Service/Policy (1030 tests); --filter 'JSignPdfHandlerTest|PhpNativeHandlerTest|Pkcs12HandlerTest' (181 tests); SignFileServiceTest (157 tests); --filter DocMdp (120 tests); RequestSignatureControllerTest (11 tests); composer psalm; php-cs-fixer on the changed files.

Not run locally: the whole PHPUnit, Behat and Playwright suites, and no end-to-end signature checking the certification level of a signed PDF; the engines are covered by unit and integration tests. composer openapi / npm run typescript:generate were not needed: no contract change.

⚙️ API / Back-end changes

  • A frozen request is certified with its stored docmdp level by both engines.
  • docmdp is frozen with the request policy snapshot; a different value after the freeze returns 422.
  • Unit and/or integration tests added or updated
  • Authorization and negative paths checked when applicable – 422 at the applier and through the API
  • Capabilities updated when applicable – not applicable
  • API documentation regenerated with composer openapi when necessary – not needed, no contract change

❓ Open questions

  1. A request frozen before docmdp was stored in the snapshot still signs with the current level, like the footer. Its docmdp_level column does hold the level from creation time, but rows older than the column hold 0, so using it would sign those requests without certification. Should signing use the column for them?
  2. DocMdp\Validator::getDocMdpLevel() treats a stored level 0 as unset and reads the current config. It runs when signers are added, not at signing time, so it is not changed here. As a result, a second signer can be refused on a request sent as not certified if the admin later enables certification.

✅ Checklist

  • I have read and followed the contribution guide.
  • The diff does not contain unrelated refactors or generated changes.
  • Commits follow Conventional Commits where applicable.
  • All commits include the required DCO sign-off.

🤖 AI (if applicable)

  • The content of this PR was partially or fully generated using AI

Later changes to the PDF certification policy no longer apply to it.

Signed-off-by: Paulo Raoni <raoni_jkd@hotmail.com>
Signed-off-by: Paulo Raoni <raoni_jkd@hotmail.com>
@paulo-raoni
paulo-raoni requested a review from a team as a code owner November 6, 2026 11:50

This branch has not been deployed

No deployments
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