Mudrit
Cookbook

Integration & Server

Detached CMS, deferred hash-then-sign, existing fields, seed values, offline LTV, parallel batch, and async signing

Patterns for embedding Mudrit in a server or pipeline: signing bytes that aren't a PDF, splitting the private-key operation from PDF assembly, filling designer-placed fields, and running many signatures in parallel or off the async runtime's blocking pool. Every recipe below is a self-contained snippet for your own project — see Cookbook for how they're written.

Recipes

RecipeWhat it showsFeature
detached_cmsDetached CMS / PKCS#7 (.p7s) over arbitrary bytes — no PDF (sign_detached_cms)pfx
deferred_signingDeferred two-step "hash-then-sign": prepare_signature → external/remote key → finalizepfx
deferred_advancedDeferred with an ECDSA identity + B-T via finalize_with_timestamp after to_bytes/from_bytespfx
sign_existing_fieldSign into a pre-existing named empty field + list_signature_fields discoverypfx
seed_valueWrite an /SV seed-value dict (mandated digest / SubFilter / reason / TSA / LTV) onto the fieldpfx
ltv_validationInject pre-fetched CRL/OCSP, offline LTV, LtvPolicy::Require (fail-loud) + LtvReportpfx
batch_parallelParallel batch (sign_batch) — thread pool, per-item Result, failure isolationpfx
async_signingAsync sign_pdf_async / sign_batch_async over Tokio's blocking poolpfx + tokio

Detached CMS over arbitrary bytes

sign_detached_cms runs the same key backends over any bytes — a JSON payload, a firmware image, an invoice — producing a standalone .p7s. No PDF is involved.

use mudrit::prelude::*;

let signer = PfxSigner::from_file("signer.pfx", "password")?;
let data   = std::fs::read("invoice.xml")?;

// CAdES-BES (includes the ESS signing-certificate attribute by default)
let p7s = sign_detached_cms(&signer, &data, &CmsOptions::default())?;
std::fs::write("invoice.xml.p7s", &p7s)?;

// CAdES-B-T — add an RFC-3161 signature timestamp
let opts  = CmsOptions::default().timestamp(Timestamp::url("http://timestamp.comodoca.com"));
let p7s_t = sign_detached_cms(&signer, &data, &opts)?;

The .p7s carries the signature, not your data — distribute the original file and its .p7s side by side. Verify with OpenSSL:

openssl smime -verify -binary -inform DER -in invoice.xml.p7s \
    -content invoice.xml -noverify -out /dev/null

Repo example: detached_cms — ships with the licensed source bundle. See the Detached CMS guide.

Deferred "hash-then-sign"

The pattern web apps and cloud-key deployments need: the server prepares the PDF and exposes the bytes-to-sign, an external service (cloud HSM, eIDAS remote QSCD, a browser/mobile token) performs the private-key operation, and the server embeds the result. The private key never touches the server process.

use mudrit::prelude::*;

// STEP 1 (server) — prepare, no private key used. The cert + chain are known up front.
let identity = SigningCertificate::new(leaf_cert_der).chain(chain_ders);
let cfg      = SignConfig::builder().place("1", [350, 60, 560, 160])?.build();
let prepared = prepare_signature(&pdf, &identity, &cfg)?;
let blob     = prepared.to_bytes()?; // crosses a process/request boundary (DB row, session, queue)

// STEP 2 (remote service) — sign the exposed bytes with the private key
let to_sign   = PreparedSignature::from_bytes(&blob)?;
let signature = remote_sign(to_sign.signed_attributes())?; // your HSM/API call — RSA raw / ECDSA DER {r,s}

// STEP 3 (server) — finalize: embed the external signature
let restored = PreparedSignature::from_bytes(&blob)?;
let signed   = restored.finalize(&signature)?;

Deferred signing v1 scope

prepare_signature / finalize supports a fresh (unsigned), unencrypted input, Single / MultiShared, certification, LTV, and PAdES B-B / B-T. Encrypted output, MultiChained, appending to an already-signed PDF, and PAdES B-LT / B-LTA are rejected with a clear error — use the in-process sign_pdf for those. A timestamp can't be serialized with to_bytes, so for a timestamped (B-T) deferred flow, prepare with Timestamp::None and pass the TSA to finalize_with_timestamp after from_bytes.

Repo examples: deferred_signing, deferred_advanced (source bundle). See the Deferred Signing guide.

Sign into a designer-placed field

Enterprise templates ship with a named empty /Sig field placed by a designer (Acrobat / iText). Signing must fill that field — same name, page, rectangle — not create a new one.

use mudrit::prelude::*;

// discover the empty field(s)
for f in list_signature_fields(&pdf)? {
    println!("{} page {} rect {:?} {}", f.name, f.page, f.rect, if f.signed { "SIGNED" } else { "empty" });
}

// sign into the named field — its page + rectangle are reused, no new field is created
let cfg = SignConfig::builder()
    .sign_existing_field("DirectorSignature")
    .reason("Approved")
    .build();
let signed = sign_pdf(&pdf, &signer, &cfg)?;

Repo example: sign_existing_field (source bundle).

Seed values — constrain a field

Write an /SV seed-value dictionary onto the created field so a downstream signer is constrained to a mandated digest / SubFilter / reason / TSA / revocation-info.

use mudrit::prelude::*;

let cfg = SignConfig::builder()
    .place("F", [350, 60, 560, 160])?
    .seed_value(SeedValue::default())   // fill in the constraints you want to mandate
    .build();
let signed = sign_pdf(&pdf, &signer, &cfg)?;

Repo example: seed_value (source bundle).

Offline LTV with a fail-loud policy

By default the engine fetches CRL/OCSP live while signing. A restricted or air-gapped deployment can inject pre-fetched material and go offline, and demand LtvPolicy::Require so an incomplete DSS is a loud error rather than a silently half-LTV document.

use mudrit::prelude::*;

let vm = ValidationMaterial::new()
    .add_cert(issuer_der)   // an extra chain cert for /DSS /Certs
    .add_crl(crl_der)       // a pre-fetched CRL (DER)
    .add_ocsp(ocsp_der)     // a pre-fetched OCSP response (DER)
    .offline(true);         // embed ONLY the injected material — no network fetches

let cfg = SignConfig::builder()
    .place("F", [350, 60, 560, 160])?
    .ltv(true)
    .validation_material(vm)
    .ltv_policy(LtvPolicy::Require)      // fail loud if revocation can't be embedded
    .build();

// BestEffort never lies — ask what actually landed in the /DSS:
let (signed, report) = sign_pdf_reported(&pdf, &signer, &cfg)?;
if !report.complete {
    eprintln!("signed without revocation info: {} certs, no CRL/OCSP", report.certs);
}

Repo example: ltv_validation (source bundle). See the LTV guide.

Parallel batch and async

sign_batch runs one signer and one config over many PDFs across a thread pool, returning a Result per input in order. The tokio feature offloads the same work onto Tokio's blocking pool.

use mudrit::prelude::*;

let inputs: Vec<Vec<u8>> = vec![std::fs::read("a.pdf")?, std::fs::read("b.pdf")?];
let results = sign_batch(&inputs, &signer, &cfg, &BatchOptions::default());
for (i, r) in results.iter().enumerate() {
    match r {
        Ok(pdf) => println!("#{i}: {} bytes", pdf.len()),
        Err(e)  => eprintln!("#{i}: {e}"),   // this input failed; the rest still succeeded
    }
}
// async (feature = "tokio") — the signer is shared as an Arc across the blocking pool
use std::sync::Arc;

let signer  = Arc::new(PfxSigner::from_file("signer.pfx", "password")?);
let signed  = sign_pdf_async(std::fs::read("in.pdf")?, signer.clone(), cfg.clone()).await?;
let results = sign_batch_async(inputs, signer, cfg, BatchOptions::default()).await?;

One hardware token → threads(1)

Parallel signing is safe for software signers (PfxSigner). A single hardware token has one PKCS#11 session that is not safe for concurrent signing — use BatchOptions::default().threads(1) to serialize onto it while still getting per-item results. See the Batch & Async guide.

Repo examples: batch_parallel, async_signing (source bundle).

Network for LTV / timestamp variants

Any config that sets .timestamp(...) or .ltv(true) — and detached_cms's timestamped run — needs outbound network access for the TSA / CRL / OCSP fetch. The offline-LTV recipe above shows the fully offline path: inject pre-fetched material via ValidationMaterial, .offline(true), and set LtvPolicy::Require to fail loud instead of writing a silently half-LTV document.

Next

On this page