Mudrit
Guides

Batch & Async Signing

Sign many PDFs in parallel with a result per input, and offload signing to Tokio's blocking pool

Part of mudrit-pdfsign

Bulk signing — invoices, payroll, certificates — is CPU-bound work that parallelises well. sign_batch runs one signer and one config over many PDFs across a small thread pool and returns a Result per input, so one bad document never aborts the batch. The tokio feature adds async wrappers for the same work inside an async server.

Parallel batch — sign_batch

use mudrit_keystore::PfxSigner;
use mudrit_pdfsign::{sign_batch, BatchOptions, SignConfig};

let signer = PfxSigner::from_file("samples/ABC12.pfx", "ABC12")?;
let cfg    = SignConfig::builder().place("F", [350, 60, 560, 160])?.build();
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
    }
}

The results come back in the same order as inputs. A failing document yields an Err in its slot — the batch never aborts on one error (failure isolation).

Options — BatchOptions

OptionDefaultEffect
threadsavailable CPUsWorker thread count (clamped to at least 1). Set with BatchOptions::threads(n)

One hardware token → threads(1)

Parallel signing is safe for software signers (PfxSigner — RSA/ECDSA signing is reentrant). A single hardware token, though, has one PKCS#11 session that is not safe for concurrent signing. For one token use BatchOptions::threads(1) — the work is then sequential but still gets per-item results. With several tokens, sign each token's batch on its own.

let opts = BatchOptions::default().threads(1);   // serialize onto the single token

The + Sync bound

sign_batch shares signer across worker threads, so it must be Sync. The Signer trait requires only Send (a live PKCS#11 session is Send but not Sync), so the parallel paths add the + Sync bound at the use site rather than forcing it on every backend. &PfxSigner satisfies it directly. For a type-erased signer, wrap it:

let signed = sign_batch(&inputs, &signer as &(dyn Signer + Sync), &cfg, &opts);

Async — sign_pdf_async / sign_batch_async

Signing is inherently blocking — CPU-bound crypto, and for hardware backends a synchronous PKCS#11 / CNG call. The async wrappers (feature tokio) don't pretend otherwise: they offload the blocking work onto Tokio's blocking thread-pool (spawn_blocking) so it never stalls an async runtime. That is the honest, correct shape for an axum / actix server — no executor thread is blocked, and you avoid the easy mistake of calling the blocking API directly from an async task.

use std::sync::Arc;
use mudrit_keystore::PfxSigner;
use mudrit_pdfsign::{sign_pdf_async, sign_batch_async, SignConfig, BatchOptions};

let signer = Arc::new(PfxSigner::from_file("samples/ABC12.pfx", "ABC12")?);
let cfg    = SignConfig::builder().place("F", [350, 60, 560, 160])?.build();

// one document
let signed = sign_pdf_async(std::fs::read("in.pdf")?, signer.clone(), cfg.clone()).await?;

// a whole batch
let results = sign_batch_async(inputs, signer, cfg, BatchOptions::default()).await?;

The signer is shared as an Arc

Because the work runs on another thread, the signer must be Send + Sync + 'static and is passed as Arc<S>. For a type-erased signer use Arc<dyn Signer + Send + Sync> as S. sign_batch_async resolves to the same per-input Vec<Result<...>> as the blocking sign_batch.

Next

On this page