Basics
Minimal signing, input modes, placement, metadata, digest/algorithm choice, batching, and Contents sizing
The core signing loop in one shape: load a Signer, build a SignConfig, call sign_pdf. Every
recipe below is a self-contained snippet — paste it into your own project, swap the placeholder paths
for your DSC and input PDF, and handle the ?. See Cookbook for how the recipes are
written.
Recipes
| Recipe | What it shows | Feature |
|---|---|---|
sign_pfx | Minimal .pfx sign + write | pfx |
custom_signer | Bring your own Signer (HSM / cloud-KMS) — no backend needed | — |
input_modes | Sign from a file path, in-memory bytes, or any Read stream (PdfReader) | pfx |
placement_dsl | Keyword page selectors (F/L/ODD/A/1,3,5-7,L) + multi-box | pfx |
metadata | Signature /Reason,/Location,… + document /Info (Title/Author/Producer) | pfx |
signing_time | /M timezone (.tz_offset(0) = UTC) + optional PKCS#9 signingTime CMS attr | pfx |
digest_algorithm | Selectable digest — SHA-256 / SHA-384 / SHA-512 (.with_algorithm) | pfx |
ecdsa_pss_demo | ECDSA P-256 + RSA-PSS signing (non-PKCS#1 schemes) | pfx |
algorithm_showcase | ECDSA P-384 / P-521 + RSA-PSS SHA-384/512; EC-on-RSA-key rejected (fail-loud) | pfx |
batch_sign | One signer + one config, many PDFs in a loop (per-file error isolation) | pfx |
sig_size_compare | SigSize::Fixed vs Auto — reserved /Contents bytes + file size | pfx |
Minimal sign
Load a PfxSigner, place one box on the first page, sign, write. This is the shape every other
recipe builds on — swap the signer, add builder calls, nothing else changes.
use mudrit::prelude::*;
let signer = PfxSigner::from_file("signer.pfx", "password")?;
let cfg = SignConfig::builder()
.place("F", [350, 60, 560, 160])? // first page + box (points)
.timestamp(Timestamp::url(mudrit::DEFAULT_TSA_URL))
.ltv(true)
.build();
let pdf = std::fs::read("input.pdf")?;
let signed = sign_pdf(&pdf, &signer, &cfg)?;
std::fs::write("signed.pdf", &signed)?;Repo example:
sign_pfx— ships with the licensed source bundle.
Bring your own Signer
The engine only needs three methods — no bundled backend feature required. Wire sign() to your HSM
or cloud KMS and the whole pipeline (placement, PAdES, LTV, encryption, …) works unchanged.
use mudrit::mudrit_keystore::{Result as KsResult, SignatureAlgorithm, Signer};
use mudrit::{sign_pdf, SignConfig};
struct KmsSigner {
leaf_der: Vec<u8>, // your leaf cert, DER
chain_der: Vec<Vec<u8>>, // intermediates, DER (leaf excluded)
// + your KMS/HSM client handle
}
impl Signer for KmsSigner {
fn certificate(&self) -> &[u8] { &self.leaf_der }
fn chain(&self) -> &[Vec<u8>] { &self.chain_der }
fn algorithm(&self) -> SignatureAlgorithm { SignatureAlgorithm::RsaPkcs1Sha256 }
fn sign(&self, data: &[u8]) -> KsResult<Vec<u8>> {
let _digest = self.algorithm().digest(data); // hash, then call your HSM/KMS
todo!("send `_digest` to the HSM/KMS, return the raw PKCS#1 v1.5 signature")
}
}
// then sign exactly as with any bundled backend:
let signer = KmsSigner { leaf_der, chain_der /* … */ };
let signed = sign_pdf(&pdf, &signer, &SignConfig::builder().place("F", [350, 60, 560, 160])?.build())?;For a scheme match: RSA returns the raw signature; ECDSA must return a DER SEQUENCE { r, s }.
See Bring your own Signer.
Repo example:
custom_signer(source bundle).
Input modes — path, bytes, or any stream
sign_pdf accepts anything convertible into a PdfReader, so
the input side is never a constraint — a file path, an in-memory buffer, or any std::io::Read.
use mudrit::prelude::*;
// in-memory bytes (the common case — `&[u8]` / `Vec<u8>` convert automatically)
let signed = sign_pdf(std::fs::read("input.pdf")?, &signer, &cfg)?;
// an explicit file path
let signed = sign_pdf(PdfReader::open("input.pdf")?, &signer, &cfg)?;
// any std::io::Read: File, TcpStream, stdin, an HTTP body, a decompressor…
let signed = sign_pdf(PdfReader::from_reader(std::io::stdin())?, &signer, &cfg)?;
// fluent shorthand straight off the reader
let signed = PdfReader::open("input.pdf")?.sign(&signer, &cfg)?;Repo example:
input_modes(source bundle).
Keyword page selectors + multi-box
.place(selector, rect) resolves against the real page count, so "1,3,5-7,L" is safe on any
document length. Call .place(...) more than once for boxes on different page sets in the same
signature.
use mudrit::prelude::*;
let cfg = SignConfig::builder()
.place("1,3,5-7,L", [350, 60, 560, 160])? // compound union, resolved per page count
.place("A", [40, 40, 180, 90])? // + a small box on every page
.build();
let signed = sign_pdf(&pdf, &signer, &cfg)?;See Placement DSL for the full selector grammar.
Repo example:
placement_dsl(source bundle).
Metadata, signing time, and digest/algorithm
Signature properties, the signing-time zone, and the digest/scheme are all one-line builder calls.
use mudrit::prelude::*;
// RSA key switched to PSS with a SHA-384 digest; ECDSA keys are auto-detected from the .pfx
let signer = PfxSigner::from_file("signer.pfx", "password")?
.with_algorithm(SignatureAlgorithm::RsaPssSha384)?;
let cfg = SignConfig::builder()
.place("F", [350, 60, 560, 160])?
.reason("Approved").location("Delhi").contact("Technical Support")
.tz_offset(0) // stamp /M in UTC (default is IST +05:30)
.cms_signing_time(true) // also emit the optional PKCS#9 signingTime CMS attribute
.build();
let signed = sign_pdf(&pdf, &signer, &cfg)?;The digest follows the algorithm: RsaPkcs1Sha256 / …384 / …512, RsaPssSha256 / …384 /
…512, and EcdsaP256Sha256 / EcdsaP384Sha384 / EcdsaP521Sha512. A request the key can't
satisfy (e.g. ECDSA on an RSA key) is rejected by .with_algorithm(...) rather than producing an
invalid file.
Repo examples:
metadata,signing_time,digest_algorithm,ecdsa_pss_demo,algorithm_showcase— all in the source bundle.
Batch a folder, isolating failures
One signer and one config over many PDFs. Handle each Result independently so a single bad input
never aborts the run. (For a parallel version see
sign_batch.)
use mudrit::prelude::*;
let signer = PfxSigner::from_file("signer.pfx", "password")?;
let cfg = SignConfig::builder().place("F", [350, 60, 560, 160])?.build();
for path in ["a.pdf", "b.pdf", "c.pdf"] {
match std::fs::read(path).map_err(Into::into).and_then(|pdf| sign_pdf(&pdf, &signer, &cfg)) {
Ok(signed) => std::fs::write(format!("{path}.signed.pdf"), &signed)?,
Err(e) => eprintln!("{path}: skipped — {e}"),
}
}Repo example:
batch_sign(source bundle).
Tune the /Contents reservation
SigSize::Auto sizes the placeholder from the cert chain + signature length + TSA-token size (one
extra TSA fetch, no extra key/PIN use), yielding a smaller file; SigSize::Fixed(n) reserves exactly
n bytes.
use mudrit::prelude::*;
let cfg = SignConfig::builder()
.place("F", [350, 60, 560, 160])?
.sig_size(SigSize::Auto) // or SigSize::Fixed(16384) for a predictable size
.build();Repo example:
sig_size_compare(source bundle).
Test cert vs. your DSC; when the network is used
The source-bundle programs sign the bundled samples/ABC12.pfx (or ecdsa-p*.pfx) test
certificate against samples/blank.pdf locally — in your own project, supply your own DSC. Recipes
that add .timestamp(...) or .ltv(true) need outbound network access for the TSA / CRL / OCSP
fetch; drop those two lines to stay fully offline.