Skip to content
getnextpdf.com

Signing at scale, without compromise

Spec: ISO 32000-2, §12.8Spec: ETSI EN 319 142-1Spec: RFC 5652, §5.1

Signing one document is a cryptographic operation. Signing a hundred thousand on a deadline is the same operation, repeated, where the dangerous failure is no longer “it was slow” but “one of them went out unsigned and nobody noticed.” This page is about doing the second thing without giving up the first: bulk and concurrent signing where every signature is still correct, the run refuses to emit a file it could not sign, and a large job resumes instead of starting over.

A signature is a per-document fact. Its digest is computed over a declared byte range that excludes the signature value itself (Spec: ISO 32000-2, §12.8), so there is no honest way to sign a thousand documents “as a batch” in one stroke — each one carries its own CMS SignedData over its own bytes (Spec: RFC 5652, §5.1). Scale therefore multiplies the chances for exactly one thing to go quietly wrong: a key handle that briefly failed, a timestamp authority that timed out, a worker that died holding a half-written file.

The expensive outcome is not a crash. A crash is loud and you retry it. The expensive outcome is a silent one — an unsigned PDF that looks finished sitting in an archive, discovered months later by an auditor’s validator. At volume, “mostly signed” is indistinguishable from “signed” right up until the one that matters is checked. The whole point of signing at scale is to make that outcome structurally impossible, not statistically rare.

  • Every document is signed individually, over its own byte range. Batch is a scheduling word, not a cryptographic one. There is no shared signature.
  • The level is a contract, not a hint. You name a PAdES baseline level and the engine produces exactly that level for every document, or it fails that document loudly (Spec: ETSI EN 319 142-1).
  • The pipeline is fail-closed. A document that cannot be signed correctly does not pass through as plain bytes. It is held, not handed on.
  • Concurrency is per-document, and safe by construction. Signing units do not share mutable state, so two workers cannot corrupt each other’s output.
  • Large runs are durable. Committed output is not re-emitted on resume; a crashed run continues from its last checkpoint instead of re-signing everything.

The design rests on one separation: producing the signature is a small, deterministic, per-document step; running thousands of them safely is an orchestration step. Keeping those apart is what lets each stay simple.

The signature step is the one that must never compromise. You ask for a level — a SignatureLevel enum case, never a string the engine has to interpret — and that level is treated as a contract for that document. The engine produces the requested level or stops with an actionable error; it does not quietly sign at a lower level and let a record claim a higher one. Correctness does not relax because there are more documents behind this one. The hundred-thousandth signature is computed exactly as carefully as the first.

The fail-closed rule is what makes that trustworthy at volume. NextPDF’s signing path declines to emit a plausible-looking-but-unsigned artifact in place of the one you asked for. The supported application route is the high-level Document API: you configure the signature with Document::setSignature() and then ask for the bytes with Document::getPdfData() (or save() / output()), and that single write pass either emits a correctly-signed PDF or throws before handing back bytes — never an unsigned file the caller believes is signed. Applied across a batch, this is the rule that converts “one slipped through unsigned” from a silent latent defect into a single failed, retryable job.

  1. Warm the signing material onceOn worker boot, open the key/certificate source and the timestamp client. This cost is paid once per worker, not once per document.
  2. Enqueue the documentsA queue holds the per-document jobs. The queue is the throughput dial — signing workers scale horizontally behind it.
  3. Render and sign one documentA disposable unit renders the document, then signs it over its own byte range at the requested PAdES level. Nothing is shared with the next document.
  4. Commit on success, hold on failureA correctly-signed file commits once. A document that could not be signed is failed and retried — never emitted as unsigned bytes.
  5. Checkpoint, and resume on crashA durable run records what has committed. After a crash it continues from the last checkpoint instead of re-signing the whole batch.
A high-volume signing run end to end: shared signing material is warmed once; each document is rendered and signed individually on a disposable unit; a correctly-signed result commits exactly once, while any failure is held for retry, never passed on as unsigned bytes; a crashed run resumes from its checkpoint.

Core gives you the cryptographic correctness: software CMS signing and PAdES B-B (with B-T through the timestamp client) where each document is signed individually and fail-closed. The orchestration that makes a large run durable, concurrent, and exactly-once — the side-effect-free render engine plus the committer, checkpoint, idempotency, and dead-letter stores — is the Stream module in the advanced editions; hardware-backed signing through an HSM or a cloud KMS is likewise an advanced-edition seam. Core proves each signature is right; the advanced editions make a million of them survivable.

The shape below is the per-document signing unit inside a batch loop. Each iteration signs one document at a named level and either yields a correctly-signed result or fails that one job — it never returns unsigned bytes dressed up as a result.

<?php
declare(strict_types=1);
use NextPDF\Contracts\DocumentFactoryInterface;
use NextPDF\Security\Signature\CertificateInfo;
use NextPDF\Security\Signature\SignatureLevel;
use NextPDF\Exception\SignatureException;
use Psr\Log\LoggerInterface;
/**
* One signing-batch iteration: render, sign at a named level, commit or fail.
*
* The factory and the certificate source ($certInfo, the warmed signing
* material) are process-lifetime singletons; the document is disposable. A
* document that cannot be signed at the requested level fails this job loudly —
* it is never committed unsigned.
*
* @param iterable<int, callable(\NextPDF\Core\Document): \NextPDF\Core\Document> $jobs
*/
function signBatch(
DocumentFactoryInterface $factory,
CertificateInfo $certInfo,
LoggerInterface $logger,
iterable $jobs,
): void {
// The level is an explicit, ordered contract — not a flag we hope is honoured.
$level = SignatureLevel::PAdES_B_T;
foreach ($jobs as $jobId => $build) {
// Fresh, disposable unit — shares the warmed signing material only.
$doc = $factory->create();
$doc = $build($doc);
try {
// Sign over this document's own byte range, at exactly $level,
// or throw. There is no "signed lower, reported higher" path.
$doc->setSignature(certInfo: $certInfo, level: $level);
$signed = $doc->getPdfData();
} catch (SignatureException $e) {
// Fail-closed: this document does NOT continue as unsigned bytes.
// The job is failed and left for retry / dead-letter handling.
$logger->error('pdf.sign.failed', ['job_id' => $jobId, 'reason' => $e->getMessage()]);
continue;
}
// Only a correctly-signed result reaches the commit step.
commitSignedOutput($jobId, $signed);
unset($doc, $signed); // release per-document state before the next iteration
$logger->info('pdf.sign.committed', ['job_id' => $jobId, 'level' => $level->value]);
}
}

The catch is the load-bearing line. It is the difference between a run that holds back the documents it could not sign and a run that ships them anyway. The continue does not paper over the failure — the job is recorded and left for retry, so the batch finishes with a known, complete list of what signed and what did not, never with a silent gap.

The first misconception is that “batch signing” means one signature applied to many files. It does not, and any system that claims it is not producing valid PAdES signatures — each document’s digest is bound to its own bytes (Spec: ISO 32000-2, §12.8). Batch is purely about how many and how fast, never about sharing the cryptographic unit.

The second is that concurrency means relaxing correctness for speed — that a fast signer must cut a corner the careful one does not. It does not. Because signing units share no mutable state, running them in parallel changes the schedule, not the bytes. Each parallel signature is computed with the same rigor as a single one; the parallelism is in the orchestration around them.

The third is that durability is something you bolt on after the first failed overnight run. By then you have already lost the run. A resumable pipeline has to know, per document, what committed and what did not before the crash — which is exactly what the checkpoint and idempotency stores exist to record.

  • Each signature is per-document and standards-bound; there is no batch shortcut. Volume changes scheduling, not the cryptographic unit. NextPDF signs every document over its own byte range.
  • Core does software CMS signing and PAdES B-B (B-T via a timestamp client). The durable, concurrent, exactly-once rendering-and-signing engine is the Stream module in the advanced editions; HSM/KMS-backed key custody is an advanced-edition seam. This page does not claim that orchestration as Core.
  • Fail-closed is the engine’s behaviour, not a guarantee about your wiring. NextPDF refuses to emit an unsigned-but-believed-signed file and surfaces the supported signing route. A pipeline that catches the resulting error and commits anyway has chosen to defeat the guarantee — the framing the example’s catch/continue exists to prevent.
  • PAdES level is enforced per document, not certified for the run. The engine produces the requested baseline level or fails; that is a structural enforcement, not a third-party conformance verdict for the produced files. The level progression itself is covered in PAdES baseline profiles.
  • The queue, the key custody, the timestamp authority, and the object store are yours. NextPDF supplies the per-document signing correctness and, in the advanced editions, the durable orchestration primitives. It does not run your infrastructure or vouch for your TSA.
High-volume and concurrent signing — edition availability
EditionAvailability
Core

Per-document software CMS signing, PAdES B-B (B-T with a timestamp client), signed individually over each document’s own byte range, fail-closed against silently-unsigned output. Plain per-document signing needs no commercial tier.

Pro

Adds the Stream module: a side-effect-free render engine plus durable committer, checkpoint, idempotency, and dead-letter stores — concurrent, crash-safe, exactly-once batch runs that resume instead of restarting.

Enterprise

Adds hardware-backed key custody (HSM via PKCS#11, or a cloud KMS) so the private key never leaves the device, and the long-term PAdES levels (B-LT, B-LTA) that keep a high-volume archive verifiable for decades.

  • High-volume document generation — the bounded-memory, queued batch model this page signs on top of; read it first for the throughput and measurement discipline.
  • PAdES baseline profiles — what each level (B-B to B-LTA) adds, so you sign at the level the obligation needs.
  • How signatures sit in a PDF — the byte-range and dictionary foundation that makes a signature per-document.
  • HSM-backed signing — where the private-key boundary sits when signing material lives in hardware.
  • Stream (Pro) — the durable, concurrent, exactly-once rendering engine that turns a single signing unit into a resumable run.
  • Batch signing — signing many documents on a schedule. A scheduling concept; each document is still signed individually over its own bytes.
  • Fail-closed — on a failure that would otherwise produce an unsigned or wrong output, the pipeline holds the document and reports, rather than passing it on as plain bytes.
  • Exactly-once commit — a durable-pipeline property where a correctly-signed output is published once and is not re-emitted when a crashed run resumes.
  • Checkpoint — durable per-document record of what has committed, so a run can continue from where it stopped instead of re-signing everything.
  • CMS SignedData — the cryptographic container for signatures over content (it can carry multiple signers); this pipeline produces one signer’s PDF signature per document, the per-document unit a batch produces.
  • PAdES — PDF Advanced Electronic Signatures, the ETSI EN 319 142 profile family for PDF signing; its baseline levels run from B-B to B-LTA.