Signing Backends
The three bundled key sources — PFX file, Windows store, and PKCS#11 token — the KeyStore discovery pattern, and bring-your-own Signer
Every signature in Mudrit flows through one small trait, mudrit_keystore::Signer. The PDF engine
never knows where the key lives — it just hands the Signer bytes to sign and assembles the CMS
around whatever comes back. That single seam is what lets a .pfx, a PKCS#11 token, the Windows
store, or your own HSM all drive the same signing pipeline unchanged.
The Signer trait
pub trait Signer: Send {
fn certificate(&self) -> &[u8]; // leaf (signing) cert, DER
fn chain(&self) -> &[Vec<u8>]; // intermediate certs, DER (leaf excluded)
fn sign(&self, data: &[u8]) -> Result<Vec<u8>>; // CMS-ready signature bytes
fn algorithm(&self) -> SignatureAlgorithm { /* RSA-SHA256 by default */ }
fn subject(&self) -> (String, String) { /* (CN, O) from the cert */ }
}Only three methods are mandatory. sign returns the CMS-ready bytes for the reported
algorithm: the raw signature for RSA PKCS#1 v1.5 / PSS, or a DER
SEQUENCE { r, s } for ECDSA — exactly what goes into the SignerInfo. The engine computes the
digest, calls sign once, and (for hardware) never sees the private key.
Send, but not Sync
Signer requires Send — a signer (including a Box<dyn Signer> from a KeyStore) can be moved
across threads. It intentionally does not require Sync: a live PKCS#11 session is not safely
shareable by reference, so the parallel paths (sign_batch, the async wrappers) add + Sync only at
the use site. For a single hardware token, keep signing on one thread.
The three bundled backends
| Backend | Key source | PIN prompt | Feature | Platform |
|---|---|---|---|---|
PfxSigner | .pfx / .p12 file | none (password supplied in code) | pfx | all |
WinStoreSigner | Windows MY store (CSP/KSP) | native OS prompt at sign time | winstore | Windows |
Pkcs11Signer | PKCS#11 token (by serial) | token PIN, owned by the SDK | pkcs11 | all |
All three are on by default (non-breaking); disable with default-features = false and opt back
in. A custom Signer needs no feature at all.
Algorithm support per backend
| Backend | RSA PKCS#1 v1.5 | RSA-PSS | ECDSA P-256 / P-384 / P-521 |
|---|---|---|---|
PfxSigner | yes | yes (any RSA key) | yes (auto-detected from the key) |
Pkcs11Signer | always | if the device supports it | yes — token signs the hash, SDK DER-encodes r‖s |
WinStoreSigner | always | if the provider supports it (CNG) | yes (CNG) |
For token and Windows-store ECDSA/PSS, the SDK re-verifies the produced signature against the certificate before use, so a device that returns an unexpected format or silently downgrades PSS to PKCS#1 is rejected with a clear error instead of emitting an Adobe-invalid file. See Algorithms for the full scheme/digest matrix.
PfxSigner — a .pfx / .p12 file
Offline, no prompt: the password is supplied in code. The scheme is auto-detected from the key (RSA →
PKCS#1 SHA-256; EC → ECDSA with the curve's hash). Switch an RSA key to PSS, or pick a different
digest, with .with_algorithm(...) (which returns a Result, since not every request fits the key).
use mudrit_pdfsign::prelude::*;
let signer = PfxSigner::from_file("samples/ABC12.pfx", "ABC12")?; // from a path
let signer = PfxSigner::from_bytes(&pfx_bytes, "ABC12")?; // from memory
let pss = PfxSigner::from_file("rsa.pfx", "pw")?.with_algorithm(SignatureAlgorithm::RsaPssSha256)?;
let ec = PfxSigner::from_file("ecdsa-p256.pfx", "pw")?; // ECDSA P-256 detectedThe bundled samples/ABC12.pfx (password ABC12) is a TEST certificate used by the examples
and tests. Use your own DSC for real signing.
WinStoreSigner — the Windows certificate store
Reads the Current-User MY store via native Win32 CryptoAPI / CNG — it works with soft certificates
and USB tokens that install a CSP/KSP minidriver. The PIN prompt, wrong-PIN count, and lockout are
handled by the OS/provider at sign time; Mudrit never sees the PIN. Windows-store signing produces a
raw signature (NCryptSignHash / RSA.SignData); the CMS is assembled by mudrit-pdfsign.
let signer = WinStoreSigner::from_thumbprint("AB12…")?; // headless: pick an exact cert
let signer = WinStoreSigner::select()?; // native selection dialog
let signer = WinStoreSigner::select_with(&CertFilter::default().signing_only(true))?; // filtered dialog.with_algorithm(alg) (infallible here) selects the scheme the provider will use.
For a certificate on a USB token that also installs a Windows minidriver, the Windows-store backend gives you the native PIN prompt for free — often the simplest path on Windows. To let a user pick interactively, see Certificate Pickers.
Pkcs11Signer — a PKCS#11 token
Cross-platform token access through any PKCS#11 module (.dll / .so / .dylib). Because the SDK
owns C_Login, it surfaces the PIN state as typed errors
(WrongPin { count_low, final_try }, PinLocked) instead of an opaque failure — everything a retry
UI needs. Open a signer by the token's certificate serial:
let signer = Pkcs11Signer::open(dll, pin, serial_hex)?; // dll path, user PIN, hex serialFor selecting a token cert without knowing its serial, discover with Pkcs11KeyStore (below) or the
enterprise Pkcs11Manager multi-token façade, then open the chosen entry. Never
pass the PIN on a command line in production — read it from a prompt or the environment.
KeyStore discovery — list, then get a signer by alias
Every backend is also exposed as a KeyStore for Java-KeyStore-style discovery: enumerate the
available entries, show the user a choice, then obtain a Signer for the chosen alias. This sits one
level above Signer — PFX exposes a single entry; a PKCS#11 token or the Windows store may expose
several.
pub trait KeyStore {
fn aliases(&self) -> Result<Vec<KeyEntry>>; // enumerate the keys
fn signer(&self, alias: &str) -> Result<Box<dyn Signer>>; // build a signer for one
}Each KeyEntry carries just enough to render a choice — a stable alias (serial / thumbprint), the
cn, org, and hex serial:
use mudrit_pdfsign::prelude::*;
let store = Pkcs11KeyStore::new(dll, pin); // one KeyStore over a token
for e in store.aliases()? {
println!("{} — {} / {} ({})", e.alias, e.cn, e.org, e.serial);
}
let signer = store.signer(&alias)?; // Box<dyn Signer>
let signed = sign_pdf(&pdf, signer.as_ref(), &cfg)?;The three stores line up with the three backends:
| KeyStore | Constructor | Typical entry count |
|---|---|---|
PfxKeyStore | PfxKeyStore::from_file(path, password)? · from_bytes(bytes, password) | one |
WinStoreKeyStore | WinStoreKeyStore::new() · .with_filter(CertFilter) | many |
Pkcs11KeyStore | Pkcs11KeyStore::new(dll, pin) | one or many |
All three also accept .with_algorithm(...) so the discovered signer uses the scheme you want.
Bring your own Signer (HSM / cloud KMS)
The whole point of the trait is that you are not limited to the bundled backends. Implement Signer
for an HSM, a cloud KMS (AWS/Azure/GCP), or any remote signing service and the entire PDF pipeline —
placement, PAdES, LTV, timestamps, encryption — works unchanged. No backend feature required; the
trait and SignatureAlgorithm are always compiled in, so you can even take mudrit-pdfsign with
default-features = false and pull no backend at all.
use mudrit_pdfsign::prelude::*;
struct KmsSigner {
leaf: Vec<u8>, // leaf cert, DER
chain: Vec<Vec<u8>>, // intermediates, DER
kms: MyKmsClient,
}
impl Signer for KmsSigner {
fn certificate(&self) -> &[u8] { &self.leaf }
fn chain(&self) -> &[Vec<u8>] { &self.chain }
fn sign(&self, data: &[u8]) -> Result<Vec<u8>> {
// Hash `data` with the digest of `algorithm()`, sign remotely, and return the
// CMS-ready bytes: raw signature for RSA (PKCS#1 / PSS), DER `SEQUENCE { r, s }` for ECDSA.
self.kms.sign(data)
}
fn algorithm(&self) -> SignatureAlgorithm { SignatureAlgorithm::EcdsaP256Sha256 }
}
let signed = sign_pdf(&pdf, &KmsSigner { /* … */ }, &cfg)?;Return the right bytes for your scheme
Whatever algorithm() reports must match what sign() returns: RSA schemes return the raw
signature; ECDSA returns a DER SEQUENCE { r, s } (not raw r‖s). The engine embeds that single
signature as-is — it never re-signs — so a mismatch produces an invalid CMS.
For a remote key that cannot be reached synchronously at sign time, use deferred signing instead (hash-then-sign in two steps).