Repository navigation
Releases: StrategicProjects/pdf_signer
Release list
v0.3.2
v0.3.2 — minimum Rust version lowered to 1.81 (CRAN)
Same behaviour as v0.3.0/v0.3.1. CRAN requires Rust-backed packages to build with a toolchain about two years old (rustc 1.81 today), so:
p12-keystoreis now0.1.5(identical API): 0.2.x is edition 2024 (rustc 1.85+) and 0.2.1 uses let-chains (rustc 1.88) without declaring arust-version, which is what broke the R package's 0.2.5 install on CRAN.rust-version = "1.81", verified with an MSRV-awarecargo updatefollowed bycargo +1.81.0 check --all-features --all-targets.- README documents the MSRV policy and how to resolve an old-toolchain lockfile.
Published to crates.io.
v0.3.1
v0.3.1 — MSRV correction
Same code as v0.3.0 (see its release notes for the full list of security and correctness fixes) with the declared minimum toolchain lowered from 1.88 to 1.83, so downstream builds on older compilers (the R package on CRAN, rustc 1.86) can consume the crate. Two standard-library calls introduced in 1.87/1.82 were replaced with plain equivalents; no behavioural change.
The committed Cargo.lock still resolves newer transitive releases (time 0.3.49, p12-keystore 0.2.1) that require 1.88; consumers on older toolchains pin those (time = 0.3.45, p12-keystore = 0.2.0), which is what the R package does.
This is the version published to crates.io.
v0.3.0
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_intactrequires 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()isfalsewithout 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 asVerifiedSignature::trusted_time. - A TSA must carry
id-kp-timeStamping(RFC 3161 §2.3) before itsgenTimeis used — an ordinary certificate under a trusted root can no longer forge a "trusted signing time". LeafkeyUsage(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;
pathLenConstraintcounts 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, singlemessage-digest, ESSsigning-certificate(-v2)binding, exactly oneSignerInfo, consistentsignatureAlgorithm(RSASSA-PSS explicitly rejected), Ed25519 + SHA-512. - A malformed
/ByteRangeor/Contentsis reported as an invalid entry instead of aborting the whole report; checked arithmetic throughout (der_total_lencould 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=1are refused too. /DSSis merged with an existing one (dedup by DER,/VRIkept) 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,/ContactInfoare written as UTF-16BE text strings — "Aprovação" no longer renders as mojibake./Mis written by default and the CMSsigning-timeattribute is dropped (ETSI EN 319 142-1 baseline). Poppler shows the/Mtime.- 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,.ttccollections are rejected, CMYK JPEGs are handled, text is laid out beside the logo, an out-of-range page or NaN geometry is an error,/NeedAppearancesis cleared, andsign_pdf_filewrites atomically.
TSA client
genTimewith 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
httpsand plain-HTTP paths; strict status-line and chunked-encoding parsing.
CLI
--password-file <FILE>(-= stdin);KEY_PASSWORDis hidden from--help;--contact-info,--signature-capacity; appearance flags require--text;document_intactis printed and enforced.
Build / API
rust-version = "1.88"(the locked dependency set already required it).- New
testkitfeature gates the fixture builders and adds an in-process RFC 3161 mock TSA; B-T / B-LT / B-LTA are now tested offline. VerifiedSignaturegainedis_timestampandtrusted_time;SignatureReportgaineddocument_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.
v0.2.1 — Zenodo archival & DOI
Docs-only release: adds .zenodo.json, CITATION.cff, and a DOI badge so the crate gets a citable Zenodo DOI. No code changes since v0.2.0.
v0.2.0 — verification & trust hardening
Fecha uma auditoria interna de 10 achados na engine de assinatura/verificação (issues #1–#10). A criptografia segue 100% Rust puro (RustCrypto), sem OpenSSL/Java.
Destaques de segurança
- #4 —
/DocTimeStampvalidado de verdade: decodifica oTSTInfo, exige eContentid-ct-TSTInfo, verifica a assinatura CMS da TSA e amarra omessageImprintao dado. A resposta da TSA no caminho de assinatura é conferida contra o imprint. - #6 — validação da cadeia no tempo de assinatura: usa o
genTimede um timestamp RFC 3161 verificado e com TSA confiável (nunca osigningTimeforjável); senão cai em "agora". Evita que cert expirado/revogado seja retrodatado. - #7 — revogação autenticada: CRL precisa estar em escopo, assinada pela CA e vigente; OCSP precisa ser assinado pelo emissor ou responder delegado (
id-kp-OCSPSigning) e vigente. Política soft-fail documentada. - #3 —
/ByteRangeamarrado ao/Contents:covers_whole_documentsó é verdade se o gap excluído for exatamente o/Contents. - #5 — localização estrutural de assinaturas: enumera pelos dicionários do documento, não varrendo bytes (sem falsos
/ByteRangeem streams; sem suposição de ordem de chaves). - #9 — path building com backtracking: encontra cadeias válidas sob cross-signing / intermediários duplicados.
- #2 — exit code honesto da CLI: com
--roots, exige cadeia confiável (novoSignatureReport::all_trusted()). - #1 —
B-T/B-LT/B-LTAsemtsa_urlagora falham em vez de degradar silenciosamente para B-B. - #8 — HTTPS para CRL/OCSP aceito com a feature
https. - #10 — doc-comments internos atualizados.
Compatibilidade
API Rust pública compatível (apenas adições, ex. all_trusted()). Mudanças de comportamento a revisar: sign_pdf_bytes erra sem tsa_url em níveis com timestamp; exit code da CLI considera confiança da cadeia sob --roots; verificação de timestamp/revogação mais estrita.
Consumidores
- R
pdfsigner(extendr): re-sincronizar e re-vendorar a cópia do crate. - Python
pdfsignerpy(PyO3): bumpar a tag pinada parav0.2.0.
Testes: 24 de integração + unit + 2 de rede (freetsa.org, DigiCert); cargo clippy limpo (default + --features https).
v0.1.7 — NIST PKITS-validated policy & name-constraint engine
Validates the RFC 5280 §6.1 certificate-policy and name-constraint engine against the NIST Public Key Interoperability Test Suite (PKITS).
PKITS results
- 42/42 certificate-policy tests (§4.8–4.12)
- 38/38 name-constraint tests (§4.13)
Run with PKITS_DIR=/path/to/pkits cargo test --test pkits -- --ignored.
Bugs found and fixed by PKITS
- §6.1.3(b)/(c) — a self-issued intermediate certificate is now exempt from the subject name-constraint check (previously rejected valid paths routed through a self-issued CA).
- §4.2.1.10 — a legacy
emailAddressattribute in the subject DN is now promoted to anrfc822Name, sorfc822name constraints bind it (previously accepted a path that should have been rejected).
Also
- New public
verify_certificate_chainentry point for direct path validation (decoupled from PDF signing). - README caveat "not yet PKITS-validated" removed.
🤖 Generated with Claude Code
v0.1.6 — embedded fonts + logo images
Richer visible signature appearances — the final roadmap item.
- Embedded TrueType font (
Appearance::font): a simple WinAnsi font withFontFile2, aFontDescriptorand per-glyph/Widths(viattf-parser), for consistent rendering across viewers. Falls back to standard Helvetica when absent. Simple font / Latin-1 — not Type0/Unicode. - Logo image (
Appearance::image+image_rect): JPEG embedded directly asDCTDecode; PNG decoded to RGB/grayscale plus anSMaskfor alpha, drawn in the appearance.
A CoreGraphics render confirms the embedded monospace font and the logo appear, with the signature still valid under Poppler pdfsig.
🎉 Feature-complete
With this, the whole roadmap is done. pdf_signer is a pure-Rust PAdES B-B → B-LTA signer and validator with RSA/ECDSA/Ed25519 keys, RFC 3161 timestamps, DSS (CRL + OCSP), broad RFC 5280 path validation (incl. name constraints and the policy engine — not PKITS-validated), optional HTTPS, xref-table and xref-stream incremental updates, and rich visible appearances.
v0.1.5 — RFC 5280 policy engine
Completes RFC 5280 §6.1 certificate policy processing, replacing the previous per-certificate required-policy subset.
What's implemented
- The
valid_policy_treewithanyPolicypropagation, subtree deletion and pruning. - Policy mapping (§6.1.4 b), including deletion when
inhibitPolicyMappingis in effect. - The
explicit_policy/policy_mapping/inhibit_anyPolicycounters, withrequireExplicitPolicy/inhibitPolicyMapping/inhibitAnyPolicyoverrides. - The §6.1.5 wrap-up intersection with the user-initial-policy-set.
TrustStore::require_policy(oid) now maps to initial-policy-set = {oid} + initial-explicit-policy. With no required policy the set is anyPolicy, so unconstrained chains validate exactly as before.
A new test demonstrates a case the previous subset could not: a chain mapping policy A → B with the leaf asserting B is trusted when requiring A, and rejected for an unrelated policy.
⚠️ Honest caveat
This is a from-scratch implementation of one of the most intricate, security-sensitive parts of RFC 5280. It is exercised by the crate's hand-built scenario tests but has not been validated against the NIST PKITS suite. Review it before relying on policy decisions for high-stakes use.
With this, the only roadmap item left is richer visible appearances (embedded fonts/images).
v0.1.4 — Ed25519 + xref-stream
Two more items off the roadmap.
Ed25519 signing keys (RFC 8419)
The signer key may now be Ed25519 (in addition to RSA and ECDSA), detected automatically from the keystore. An adapter gives ed25519-dalek the SignatureBitStringEncoding the CMS/X.509 builders need; signing uses SHA-512 for the message digest; the verifier validates Ed25519 SignerInfo signatures and Ed25519 certificate chains.
Note: most PDF readers (e.g. Adobe) do not validate Ed25519 PDF signatures yet — this crate's own verifier does.
Cross-reference stream incremental updates
Incremental updates now match the source: if the original PDF's last cross-reference is a stream (PDF 1.5+), the update is emitted as a /Type /XRef stream (FlateDecode, W=[1 4 2], self-indexing, chained via /Prev); otherwise the traditional table is used. Auto-detected — no configuration.
Verified end to end: an xref-stream PDF (generated by qpdf) signs to an xref-stream incremental update that Poppler pdfsig validates, with the original bytes preserved byte-for-byte.
Remaining: full policy-tree processing (deliberately out of scope) and richer visible appearances.
v0.1.3 — name constraints + policy check
Extends certificate-path validation with two more RFC 5280 features:
- Name constraints (§4.2.1.10): permitted/excluded subtrees from the CAs above a certificate are enforced against its subject DN and SANs (DirectoryName, DNS, email, IP, URI). Excluded takes precedence over permitted.
- Required-policy check:
TrustStore::require_policy(oid)— the leaf (and any intermediate carrying a policies extension) must assert that policy OID oranyPolicy.
Honest scope: this is a practical subset of RFC 5280 §6.1 policy processing. The full policy engine — valid_policy_tree, policy mapping, requireExplicitPolicy/inhibitPolicyMapping — is deliberately not implemented, because a half-correct policy engine is a security hazard. Name constraints, by contrast, are implemented in full for the common GeneralName types.
New tests cover permitted/excluded names and present/absent required policy.