Mudrit
Cookbook

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

RecipeWhat it showsFeature
sign_pfxMinimal .pfx sign + writepfx
custom_signerBring your own Signer (HSM / cloud-KMS) — no backend needed
input_modesSign from a file path, in-memory bytes, or any Read stream (PdfReader)pfx
placement_dslKeyword page selectors (F/L/ODD/A/1,3,5-7,L) + multi-boxpfx
metadataSignature /Reason,/Location,… + document /Info (Title/Author/Producer)pfx
signing_time/M timezone (.tz_offset(0) = UTC) + optional PKCS#9 signingTime CMS attrpfx
digest_algorithmSelectable digest — SHA-256 / SHA-384 / SHA-512 (.with_algorithm)pfx
ecdsa_pss_demoECDSA P-256 + RSA-PSS signing (non-PKCS#1 schemes)pfx
algorithm_showcaseECDSA P-384 / P-521 + RSA-PSS SHA-384/512; EC-on-RSA-key rejected (fail-loud)pfx
batch_signOne signer + one config, many PDFs in a loop (per-file error isolation)pfx
sig_size_compareSigSize::Fixed vs Auto — reserved /Contents bytes + file sizepfx

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.

Next

On this page