Skip to content

v2.0.0-rc.1 - Encryption on the platform, a bounded regional layer, and validation that says what changed

Pre-release
Pre-release

Choose a tag to compare

@lsnepomuceno lsnepomuceno released this 14 Aug 02:00
· 120 commits to main since this release
14df106

A release candidate for 2.0.0. Please exercise the certificate vault
migration before this goes stable
, for the reason below.

composer require lsnepomuceno/signet-pdf:2.0.0-rc.1

Composer will only resolve it with minimum-stability set accordingly, which is
what makes it safe to try.

The break nothing can check for you

CertificateVault::create() keeps its signature and returns a 32-byte key
where it returned 16. A storage column sized for the old width truncates it
silently, and no type checker can see that: the backward-compatibility check
reports nothing about it and is right to.

That checker also skipped seventeen files it could not compile, alongside the
twelve breaks it did report, so twelve is a floor rather than a total. Read
UPGRADE.md,
not the green check.

Nothing has to be re-encrypted. A key issued by 1.x keeps opening what it
sealed: the payload carries its version and withKey() picks the reader from
the key's length.

Breaking

  • ext-sodium is now required, and new certificate material is sealed with
    XChaCha20-Poly1305 instead of an AES-128-CBC and HMAC construction this package
    assembled itself. Encrypt-then-MAC written in application code is the shape
    that fails quietly, and encryption at rest is a convenience beside a PDF
    signing package rather than the product. Material sealed here no longer opens
    in lsnepomuceno/laravel-a1-pdf-sign until that package learns the same
    envelope; the other direction is unaffected. (0103)

  • The ICP-Brasil layer moved to IcpBrasil\, eight class names, behaviour
    identical. If you do not sign Brazilian documents, none of it reaches you.
    (0104)

  • SealPlacement::LAST_PAGE is gone, replaced by Enums\SealPage::Last.
    A numbered page is unchanged and SealPage::Last is still the default, so a
    placement that never named a page needs no edit. (0105)

Validation now says what changed, not only that something did

Six additions, none of which costs a consumer anything.

  • onlyAddedSignatures(). coversWholeDocument said bytes were appended
    after a signature and never what they did, which is the live attack surface
    for PAdES: append an annotation over the payment terms and the signature still
    verifies, because the new bytes are outside its /ByteRange. Each revision is
    now reported with the objects it defines and what they touched. True is not
    a verdict of safe
    : a counter-signer produces the same shape. (0110)

  • The /ByteRange is checked before it is believed. It is the one input to
    validation an attacker writes, and everything downstream derived from it
    unchecked: which bytes are hashed, and where the CMS is read from. Six
    conditions now, the decisive one being that the gap is the value of a
    /Contents key rather than any window holding hexadecimal. Nothing changes
    for a well-formed document. (0107)

  • Enums\ValidationFinding. Ten cases naming facts the validator already
    established, where the only ways to reach them were a dozen properties or
    matching English in $error. Exactly one decides validity, and the enum
    carries no severity: how much NotTrusted matters is not this package's call.
    signet verify --json prints them. (0106)

  • missingValidationMaterial(). hasLongTermMaterial() answers presence;
    B-LT promises a verifier could decide offline. A store with one certificate, a
    /VRI entry and no OCSP satisfies the first completely. (0109)

  • verifiableUntil(), so a document can be re-stamped before its chain can
    no longer be built. An archive timestamp renews the horizon. Null means
    unanswerable, never "never". (0108)

  • $messageDigest, short and stable enough for an audit trail to record and
    compare later. Not proof on its own. (0108)

Also

Every docblock in src/ now explains its design without naming the framework
the package was extracted from, and the arch rule that enforced that for imports
covers prose as well.

lsnepomuceno/laravel-a1-pdf-sign is not a consumer of this package. The
documentation said it was; it is a separate implementation sharing a lineage,
and that is now stated wherever it was claimed otherwise.

Verification

674 tests on PHP 8.4 and 8.5 with --fail-on-skipped, PHPStan at level max with
no baseline, type coverage at 100%, and zero warnings. Checked independently
against veraPDF, qpdf, pyHanko, poppler's pdfsig and the Arlington PDF Model.

Full detail in
CHANGELOG.md.