Skip to content

v0.3.0

Choose a tag to compare

@milkway milkway released this 25 Sep 01:08
· 4 commits to main since this release

v0.3.0 — whole-document integrity, TSA purpose validation, hardened signing

This release closes a multi-agent security/quality review (3× Claude Opus 5.5 + Codex gpt-5.6-sol, every finding reproduced or confirmed in code before fixing). It is a behaviour-changing release for verification: several documents that previously "passed" were not what was signed.

Verification — false accepts fixed

  • Whole-document integrity. SignatureReport::document_intact requires the last valid signature or document timestamp to cover the entire file, or the tail to be a DSS-only incremental update. all_valid(), all_trusted() and the CLI exit code now require it, so the classic "append an incremental update after signing" attack is rejected. all_trusted() is false without a trust store.
  • Document timestamps are chain-validated against the roots, as TSAs, and report their own chain_trusted. A later trusted archival timestamp anchors earlier TSAs (the B-LTA chain); the authenticated time actually used is exposed as VerifiedSignature::trusted_time.
  • A TSA must carry id-kp-timeStamping (RFC 3161 §2.3) before its genTime is used — an ordinary certificate under a trusted root can no longer forge a "trusted signing time". Leaf keyUsage (digitalSignature/nonRepudiation) is enforced for signers.
  • Revocation by date: an authenticated CRL/OCSP entry whose revocation date is at or before the validation time counts even if the evidence was issued later (RFC 3161 App. B) — previously the OCSP a B-LT signer embeds could never flag a revoked signer. OCSP CertIDs are matched under the responder's hash algorithm; delegated responders must be valid.
  • RFC 5280 §6.1.3 extension processing: unknown critical or malformed/duplicated extensions fail the path; pathLenConstraint counts non-self-issued intermediates only; inner/outer signature algorithms must agree; ECDSA is verified under the declared digest.
  • Bounded path building (256 candidate evaluations, deduplicated pool): a hostile certificate pool in the CMS can no longer make verification run for hours.
  • CMS profile checks: content-type, single message-digest, ESS signing-certificate(-v2) binding, exactly one SignerInfo, consistent signatureAlgorithm (RSASSA-PSS explicitly rejected), Ed25519 + SHA-512.
  • A malformed /ByteRange or /Contents is reported as an invalid entry instead of aborting the whole report; checked arithmetic throughout (der_total_len could overflow on a crafted /Contents).

Signing — invalid outputs fixed

  • Encrypted PDFs are refused (even with an empty user password: the update was written in clear and the document became unreadable). Documents certified with DocMDP P=1 are refused too.
  • /DSS is merged with an existing one (dedup by DER, /VRI kept) instead of replaced — a second B-LT signature no longer downgrades the first to B-T.
  • /Info (and other trailer entries) are carried into the new trailer; Title/Author no longer vanish after signing.
  • /Reason, /Name, /Location, /ContactInfo are written as UTF-16BE text strings — "Aprovação" no longer renders as mojibake.
  • /M is written by default and the CMS signing-time attribute is dropped (ETSI EN 319 142-1 baseline). Poppler shows the /M time.
  • Files with bytes before %PDF- get header-relative xref offsets; xref-stream offsets widen past 4 GiB; the signature placeholder is located inside the signature object rather than by byte search.
  • CFF OpenType fonts embed as /FontFile3, .ttc collections are rejected, CMYK JPEGs are handled, text is laid out beside the logo, an out-of-range page or NaN geometry is an error, /NeedAppearances is cleared, and sign_pdf_file writes atomically.

TSA client

  • genTime with fractional seconds is accepted (many production TSAs emit it; signing at B-T+ against them failed before).
  • A nonce is sent and checked; the token's signature is verified before it is embedded; connect/overall timeouts and response-size limits on both the https and plain-HTTP paths; strict status-line and chunked-encoding parsing.

CLI

  • --password-file <FILE> (- = stdin); KEY_PASSWORD is hidden from --help; --contact-info, --signature-capacity; appearance flags require --text; document_intact is printed and enforced.

Build / API

  • rust-version = "1.88" (the locked dependency set already required it).
  • New testkit feature gates the fixture builders and adds an in-process RFC 3161 mock TSA; B-T / B-LT / B-LTA are now tested offline.
  • VerifiedSignature gained is_timestamp and trusted_time; SignatureReport gained document_intact; both structs are #[non_exhaustive].
  • README commands fixed (--features cli, -p), PKITS harness fails when its data is missing.

Full details in the commit message of cd993bb.