Mudrit
Guides

Deferred Signing

Two-step hash-then-sign — prepare the PDF, sign the digest with a remote or HSM key, then finalize

Part of mudrit-pdfsign

sign_pdf does everything in one call and needs the private key locally. Deferred signing splits that into two halves so the key can live somewhere else — a cloud HSM, an eIDAS remote QSCD, a browser or mobile token, or a REST signing gateway. The server builds the PDF and exposes the bytes to be signed; the key holder signs them off-box; the server embeds the result.

The three steps

Prepare (server)

prepare_signature builds the PDF — placeholders, the AcroForm field, and any DocMDP / DSS — and returns a PreparedSignature. No private key is touched.

Sign (key holder)

Your external service signs the exposed bytes with the private key — off-box, async, behind auth.

Finalize (server)

PreparedSignature::finalize embeds the returned signature and yields the final signed PDF.

use mudrit_pdfsign::{prepare_signature, SigningCertificate, SignConfig};

// STEP 1 (server): the cert + chain are known up front; the key is remote.
let identity = SigningCertificate::new(leaf_cert_der).chain(chain_ders);
let prepared = prepare_signature(&pdf, &identity, &SignConfig::builder().build())?;
let to_sign  = prepared.signed_attributes().to_vec();   // or prepared.digest()

// STEP 2 (cloud HSM / browser / mobile): produce the signature out-of-process.
let signature = my_remote_signer(&to_sign)?;            // RSA PKCS#1 / PSS, or ECDSA DER {r,s}

// STEP 3 (server): embed it.
let signed_pdf = prepared.finalize(&signature)?;

The signing identity — SigningCertificate

Everything prepare_signature needs except the private key: the leaf certificate, its chain, and the scheme the remote key signs with.

MethodEffect
SigningCertificate::new(leaf_der)Start from the leaf certificate DER (no chain, RSA PKCS#1 SHA-256 by default)
.chain(chain_ders)Set the intermediate / chain certificates (DER, leaf excluded)
.algorithm(alg)Set the scheme the remote key uses — drives the digest and CMS labelling
SigningCertificate::from_signer(&signer)Borrow a live Signer's published certificate + chain + algorithm

The algorithm must match the remote key

algorithm selects the digest and how the SignedData is labelled. If your remote key produces ECDSA P-256 but the identity says RSA, the finalized signature will be mislabelled and rejected by validators. Set it explicitly with .algorithm(...) whenever the key is not RSA PKCS#1 SHA-256.

The prepared PDF — PreparedSignature

Returned by prepare_signature. Hand the bytes to your remote signer, then finalize.

MemberPurpose
signed_attributes()The CMS signed attributes (DER) — feed these to a signer that hashes then signs (a drop-in Signer::sign target)
digest()The pre-computed hash — algorithm.digest(signed_attributes()) — for an HSM/API that signs a raw digest
algorithm()The scheme the external signer must produce
certificate()The leaf certificate (DER) this signature is bound to
finalize(signature)Embed the signature and return the final PDF (applies the timestamp configured at prepare time, if any)
finalize_with_timestamp(signature, &ts)Finalize but apply ts instead of the prepared one — use after from_bytes to still get B-T
to_bytes()Serialize the prepared state to persist it across a process / request boundary
from_bytes(&data)Restore a PreparedSignature from to_bytes

What the external signer must do

Apply the same operation a local Signer would to signed_attributes(): hash with the algorithm()'s digest and produce the signature — a raw RSA PKCS#1 v1.5 / PSS signature, or an ECDSA SEQUENCE { r, s } DER. If your API signs a pre-computed hash instead, feed it digest() — that is exactly algorithm.digest(signed_attributes()).

Serializing across a network boundary

The two halves rarely run in the same process. to_bytes / from_bytes persist the prepared state in a DB row, a session, or a queue between the prepare request and the finalize request:

// request A — prepare, then stash the blob (DB / cache / queue)
let blob = prepared.to_bytes()?;

// request B — restore, embed the externally-produced signature
let prepared = PreparedSignature::from_bytes(&blob)?;
let signed   = prepared.finalize(&signature)?;

Timestamps are not serializable

The signature timestamp's TSA client cannot be serialized, so to_bytes errors if a timestamp was configured. For a timestamped (B-T) deferred flow, either finalize in the same process, or prepare_signature with Timestamp::None and pass the TSA to finalize_with_timestamp after from_bytes:

use mudrit_pdfsign::Timestamp;

let prepared = PreparedSignature::from_bytes(&blob)?;
let tsa = Timestamp::url("http://timestamp.comodoca.com");
let signed = prepared.finalize_with_timestamp(&signature, &tsa)?;   // CAdES-B-T

v1 scope

cfg is the same SignConfig as in-process signing, minus the key. Out-of-scope configurations fail loud with Error::InvalidInput rather than silently producing something unexpected.

ConfigurationDeferred v1
Fresh (unsigned), unencrypted inputSupported
Method::Single / Method::MultiSharedSupported
Certification (.certify(...))Supported
LTV (.ltv(true))Supported
PAdES B-B / B-TSupported
Appending to an already-signed PDFRejected
Encrypted output (.encrypt(...))Rejected
Encrypted input PDFRejected
Method::MultiChainedRejected
PAdES B-LT / B-LTARejected

For any of the rejected cases, use the in-process sign_pdf — it handles appending, encryption, MultiChained, and the higher PAdES levels directly.

Next

On this page