Mudrit
Guides

Signing Backends

The three bundled key sources — PFX file, Windows store, and PKCS#11 token — the KeyStore discovery pattern, and bring-your-own Signer

Part of mudrit-keystore

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

BackendKey sourcePIN promptFeaturePlatform
PfxSigner.pfx / .p12 filenone (password supplied in code)pfxall
WinStoreSignerWindows MY store (CSP/KSP)native OS prompt at sign timewinstoreWindows
Pkcs11SignerPKCS#11 token (by serial)token PIN, owned by the SDKpkcs11all

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

BackendRSA PKCS#1 v1.5RSA-PSSECDSA P-256 / P-384 / P-521
PfxSigneryesyes (any RSA key)yes (auto-detected from the key)
Pkcs11Signeralwaysif the device supports ityes — token signs the hash, SDK DER-encodes r‖s
WinStoreSigneralwaysif 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 detected

The 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 serial

For 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:

KeyStoreConstructorTypical entry count
PfxKeyStorePfxKeyStore::from_file(path, password)? · from_bytes(bytes, password)one
WinStoreKeyStoreWinStoreKeyStore::new() · .with_filter(CertFilter)many
Pkcs11KeyStorePkcs11KeyStore::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).

Next

On this page