Deferred Signing
Two-step hash-then-sign — prepare the PDF, sign the digest with a remote or HSM key, then finalize
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.
| Method | Effect |
|---|---|
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.
| Member | Purpose |
|---|---|
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-Tv1 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.
| Configuration | Deferred v1 |
|---|---|
| Fresh (unsigned), unencrypted input | Supported |
Method::Single / Method::MultiShared | Supported |
Certification (.certify(...)) | Supported |
LTV (.ltv(true)) | Supported |
| PAdES B-B / B-T | Supported |
| Appending to an already-signed PDF | Rejected |
Encrypted output (.encrypt(...)) | Rejected |
| Encrypted input PDF | Rejected |
Method::MultiChained | Rejected |
| PAdES B-LT / B-LTA | Rejected |
For any of the rejected cases, use the in-process sign_pdf — it handles
appending, encryption, MultiChained, and the higher PAdES levels directly.