Skip to content

StrategicProjects/pdf_signer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pdf_signer

License: GPL v3 Rust PAdES pure Rust

A self-contained Rust library + CLI to digitally sign PDF documents with a PKCS#12 keystore and verify their signatures — implementing the PAdES baseline profiles (ETSI EN 319 142) from B-B all the way to B-LTA.

The cryptography is 100 % pure Rust (RustCrypto) — no OpenSSL, no Java, no system C libraries — so the crate vendors cleanly (it powers the signer R package on CRAN). TLS is the only optional, opt-in exception (for HTTPS timestamp/CRL endpoints).

Every signature this produces is cross-validated by Poppler's pdfsig and opens as valid in Adobe Reader.

$ cargo run --example gen_assets        # writes sample.pdf + keystore.p12
$ pdf_signer sign sample.pdf signed.pdf keystore.p12 \
      --password password --reason "Approved" --level blta \
      --tsa-url http://timestamp.digicert.com \
      --text "Digitally signed" --image logo.png --font Arial.ttf
$ pdf_signer verify signed.pdf --roots icp-brasil-roots.pem
signature #1:
  valid:                 true
  signer:                CN=...
  chain_trusted:         true
  detail:                valid CMS signature; signer: ...
$ pdfsig signed.pdf
  - Signature Type: ETSI.CAdES.detached
  - Signature Validation: Signature is Valid.
  - Total document signed

Run pdf_signer sign --help / verify --help for all options.

Features

  • PAdES B-B → B-LTA detached CMS signatures (ETSI.CAdES.detached).
  • Visible or invisible signatures — a bordered text box (the signing statement + validation link) at any position on any page, with word wrap, an optional embedded TrueType font and a PNG/JPEG logo.
  • True incremental updates — the original bytes are never rewritten, so multiple signatures compose and earlier ones stay valid.
  • RFC 3161 timestamps — signature timestamps (B-T) and document timestamps (B-LTA), from any TSA.
  • Long-term validation material — a /DSS with the full certificate chain, CRLs and OCSP responses fetched from the certificates' distribution points / responders (B-LT).
  • Verification — re-derives the signed byte range, checks the message digest and the signer's signature, and reports each signature and document timestamp.
  • Certificate-chain validation against a trust store (e.g. the ICP-Brasil roots): per-link signature (RSA, ECDSA P-256/P-384, Ed25519), validity, basicConstraints / pathLenConstraint / keyCertSign, CRL + OCSP revocation, name constraints (§4.2.1.10), and the full policy engine (valid_policy_tree, policy mapping) with an optional required-policy set.
  • RSA, ECDSA and Ed25519 signing keys (RSA PKCS#1 v1.5 + SHA-256; ECDSA P-256/SHA-256 and P-384/SHA-384; Ed25519 per RFC 8419), detected automatically from the keystore.
  • Pure Rust, with an optional https feature (rustls) for TLS endpoints.

PAdES levels

Level What it adds pades_level Needs a TSA
B-B signing-certificate-v2 (CAdES baseline) Bb no
B-T + RFC 3161 signature timestamp Bt yes
B-LT + /DSS (certificate chain + CRLs) Blt yes
B-LTA + /DocTimeStamp over the whole file Blta yes

Command-line interface

The crate ships a pdf_signer binary with two subcommands. Install it (or run it straight from a checkout):

$ cargo install --path .            # puts `pdf_signer` on your PATH
# …or, without installing:
$ cargo run --release -- sign  …    # everything after `--` is forwarded
$ cargo run --release -- verify …

sign

$ pdf_signer sign <INPUT> <OUTPUT> <KEYSTORE> --password <PWD> [options]
Argument / flag Meaning
<INPUT> <OUTPUT> <KEYSTORE> input PDF, signed output PDF, PKCS#12 .p12/.pfx
-p, --password keystore password (or set KEY_PASSWORD in the environment)
--level <bb|bt|blt|blta> PAdES level (default bb); bt+ need --tsa-url
--tsa-url <URL> RFC 3161 timestamp authority (http://, or https:// with the https feature)
--reason / --name / --location signature dictionary metadata
--text <STR> draw a visible signature box with this text
--page --x --y --width --height --font-size box placement/size, in points
--no-border omit the box border
--font <FILE.ttf> embed a TrueType/OpenType font in the box
--image <FILE.png|jpg> draw a PNG/JPEG logo in the box
# Invisible PAdES-B-B signature, password from the environment:
$ KEY_PASSWORD=secret pdf_signer sign in.pdf out.pdf keystore.p12

# Visible box, long-term (B-LTA) with a timestamp and an embedded logo:
$ pdf_signer sign in.pdf out.pdf keystore.p12 \
      --password secret --level blta \
      --tsa-url http://timestamp.digicert.com \
      --reason "Approved" --name "André Leite" \
      --text "Digitally signed" --image logo.png --font Arial.ttf

verify

$ pdf_signer verify <INPUT> [--roots <ROOTS.pem>]

Without --roots it reports cryptographic validity only; pass a PEM bundle of trusted roots (e.g. ICP-Brasil) to additionally validate each signer's chain.

$ pdf_signer verify out.pdf --roots icp-brasil-roots.pem
signature #1:
  valid:                 true
  signer:                CN=…
  chain_trusted:         true
  covers_whole_document: true
  detail:                valid CMS signature; signer: …

The process exits 0 only when at least one signature is present and all found signatures are valid. Run pdf_signer sign --help / verify --help for the full, authoritative list of options.

Library usage

use pdf_signer::{sign_pdf_file, verify_pdf_file_with_roots, Appearance, PadesLevel, SignOptions, TrustStore};

// Sign at PAdES-B-LTA with a visible appearance and a timestamp.
sign_pdf_file("in.pdf", "out.pdf", "keystore.p12", "password", &SignOptions {
    reason: Some("Approved".into()),
    pades_level: PadesLevel::Blta,
    tsa_url: Some("http://timestamp.digicert.com".into()),
    appearance: Some(Appearance {
        page: 1, x: 36.0, y: 36.0, width: 320.0, height: 64.0,
        font_size: 8.0, border: true,
        text: "Digitally signed.\nValidate at: example.org/validate".into(),
    }),
    ..Default::default()
})?;

// Verify and validate the signer chain against trusted roots.
let roots = TrustStore::from_pem(&std::fs::read("icp-brasil-roots.pem")?)?;
let report = verify_pdf_file_with_roots("out.pdf", &roots)?;
for s in &report.signatures {
    println!("valid={} trusted={:?} — {}", s.valid, s.chain_trusted, s.detail);
}

The https feature enables TLS TSA/CRL endpoints:

pdf_signer = { version = "0.1", features = ["https"] }

How it works

  1. PDF structure (lopdf) — add an AcroForm signature field and a /Sig dictionary with /SubFilter /ETSI.CAdES.detached, a /ByteRange placeholder and a zero-filled /Contents placeholder.
  2. Incremental update (incremental.rs) — keep the original bytes verbatim; append the new objects, a fresh xref table and a /Prev-chained trailer. Byte surgery (within the appended region) computes the real /ByteRange and patches it length-preservingly.
  3. CMS (cms + rsa + sha2) — build a detached SignedData with the contentType, messageDigest, signingTime and signing-certificate-v2 signed attributes; optionally fetch and embed an RFC 3161 timestamp.
  4. DSS / DocTimeStamp (dss.rs) — collect the chain + CRLs into a /DSS, then append a document timestamp over the whole file.
  5. Verify (verify.rs + trust.rs) — validate the CMS and, optionally, the certificate path against a trust store.

Scope & limitations

  • Path validation implements RFC 5280 §6.1 broadly: signatures, validity, basic constraints, path length, key usage, CRL + OCSP revocation, name constraints, and the policy engine (valid_policy_tree, policy mapping, requireExplicit­Policy/inhibitPolicyMapping/inhibitAnyPolicy). The policy engine and name-constraint processing are validated against the NIST PKITS suite — 42/42 certificate-policy tests (§4.8–4.12) and 38/38 name-constraint tests (§4.13) pass. Run them with PKITS_DIR=/path/to/pkits cargo test --test pkits -- --ignored (the revocation/CRL-shape PKITS sections rely on features this crate does not claim, so they are not asserted).
  • Signing keys: RSA (SHA-256), ECDSA (P-256/P-384) and Ed25519. Most PDF readers (e.g. Adobe) do not validate Ed25519 PDF signatures yet — this crate's own verifier does.
  • Incremental updates match the source: a traditional xref table or a cross-reference stream (auto-detected), chained via /Prev.
  • Visible appearances can embed a TrueType font (a simple WinAnsi font — Latin-1, not Type0/Unicode, so non-Latin-1 glyphs become ?) and a PNG or JPEG logo; the default font is standard Helvetica. Line wrapping is approximate (character-count).

Roadmap

  • Pure-Rust CMS signing & verification (no OpenSSL/Java)
  • Visible appearance, incremental updates, multi-signature
  • PAdES B-B / B-T / B-LT / B-LTA (DSS + document timestamp)
  • Certificate-chain validation (RSA + ECDSA, CRL + OCSP, RFC 5280 subset)
  • Optional HTTPS (rustls) for TSA / CRL / OCSP
  • extendr bindings + vendoring for R / CRAN
  • ECDSA signing keys (P-256 / P-384)
  • RFC 5280 name constraints + required-policy check
  • Ed25519 signing keys; xref-stream incremental updates
  • RFC 5280 policy engine (valid_policy_tree, policy mapping) — NIST PKITS validated (42/42 policy + 38/38 name-constraint tests)
  • Richer visible appearances (embedded TrueType fonts + PNG/JPEG images)

License

GPL-3.0-or-later.

About

Native Rust PDF signing & verification (PKCS#12 / CMS) — PoC backend to replace the BatchPDFSign JAR in the signer R package

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages