Skip to content

Releases: StrategicProjects/pdf_signer

v0.3.2

Choose a tag to compare

@milkway milkway released this 25 Sep 11:10

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-keystore is now 0.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 a rust-version, which is what broke the R package's 0.2.5 install on CRAN.
  • rust-version = "1.81", verified with an MSRV-aware cargo update followed by cargo +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

Choose a tag to compare

@milkway milkway released this 25 Sep 01:15

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

Choose a tag to compare

@milkway milkway released this 25 Sep 01:08

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.

v0.2.1 — Zenodo archival & DOI

Choose a tag to compare

@milkway milkway released this 15 Jul 01:20

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

Choose a tag to compare

@milkway milkway released this 16 Jun 01:57

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 — /DocTimeStamp validado de verdade: decodifica o TSTInfo, exige eContent id-ct-TSTInfo, verifica a assinatura CMS da TSA e amarra o messageImprint ao dado. A resposta da TSA no caminho de assinatura é conferida contra o imprint.
  • #6 — validação da cadeia no tempo de assinatura: usa o genTime de um timestamp RFC 3161 verificado e com TSA confiável (nunca o signingTime forjá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 — /ByteRange amarrado ao /Contents: covers_whole_document só é 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 /ByteRange em 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 (novo SignatureReport::all_trusted()).
  • #1 — B-T/B-LT/B-LTA sem tsa_url agora 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 para v0.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

Choose a tag to compare

@milkway milkway released this 15 Jun 00:38

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 emailAddress attribute in the subject DN is now promoted to an rfc822Name, so rfc822 name constraints bind it (previously accepted a path that should have been rejected).

Also

  • New public verify_certificate_chain entry 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

Choose a tag to compare

@milkway milkway released this 15 Jun 00:09

Richer visible signature appearances — the final roadmap item.

  • Embedded TrueType font (Appearance::font): a simple WinAnsi font with FontFile2, a FontDescriptor and per-glyph /Widths (via ttf-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 as DCTDecode; PNG decoded to RGB/grayscale plus an SMask for 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

Choose a tag to compare

@milkway milkway released this 14 Jun 23:52

Completes RFC 5280 §6.1 certificate policy processing, replacing the previous per-certificate required-policy subset.

What's implemented

  • The valid_policy_tree with anyPolicy propagation, subtree deletion and pruning.
  • Policy mapping (§6.1.4 b), including deletion when inhibitPolicyMapping is in effect.
  • The explicit_policy / policy_mapping / inhibit_anyPolicy counters, with requireExplicitPolicy / inhibitPolicyMapping / inhibitAnyPolicy overrides.
  • 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

Choose a tag to compare

@milkway milkway released this 14 Jun 23:38

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

Choose a tag to compare

@milkway milkway released this 14 Jun 23:25

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 or anyPolicy.

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.