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
| Recipe | What it shows | Feature |
|---|---|---|
detached_cms | Detached CMS / PKCS#7 (.p7s) over arbitrary bytes — no PDF (sign_detached_cms) | pfx |
deferred_signing | Deferred two-step "hash-then-sign": prepare_signature → external/remote key → finalize | pfx |
deferred_advanced | Deferred with an ECDSA identity + B-T via finalize_with_timestamp after to_bytes/from_bytes | pfx |
sign_existing_field | Sign into a pre-existing named empty field + list_signature_fields discovery | pfx |
seed_value | Write an /SV seed-value dict (mandated digest / SubFilter / reason / TSA / LTV) onto the field | pfx |
ltv_validation | Inject pre-fetched CRL/OCSP, offline LTV, LtvPolicy::Require (fail-loud) + LtvReport | pfx |
batch_parallel | Parallel batch (sign_batch) — thread pool, per-item Result, failure isolation | pfx |
async_signing | Async sign_pdf_async / sign_batch_async over Tokio's blocking pool | pfx + 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/nullRepo 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.