Перейти к содержимому
getnextpdf.com

Подписание в масштабе, без компромиссов

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

Подписать один документ — это криптографическая операция. Подписать сто тысяч к дедлайну — та же операция, повторённая, где опасный сбой уже не «было медленно», а «один из них ушёл неподписанным, и никто не заметил». Эта страница — о том, как делать второе, не отказываясь от первого: массовое и конкурентное подписание, где каждая подпись по-прежнему корректна, прогон отказывается выдать файл, который не смог подписать, а большое задание возобновляется, а не начинается заново.

Подпись — это факт уровня отдельного документа. Её дайджест вычисляется по объявленному диапазону байтов, исключающему само значение подписи (Spec: ISO 32000-2, §12.8), поэтому нет честного способа подписать тысячу документов «пакетом» одним махом — каждый несёт собственный CMS SignedData по собственным байтам (Spec: RFC 5652, §5.1). Поэтому масштаб умножает шансы, что ровно одна вещь тихо пойдёт не так: дескриптор ключа, который ненадолго отказал, центр меток времени, который превысил тайм-аут, worker, который умер, держа наполовину записанный файл.

Дорогой исход — это не аварийное завершение. Аварийное завершение громко, и вы его повторяете. Дорогой исход — это тихий исход: неподписанный PDF, который выглядит завершённым, лежащий в архиве и обнаруженный спустя месяцы валидатором аудитора. На объёме «в основном подписано» неотличимо от «подписано» вплоть до того момента, когда проверяют тот единственный, что имеет значение. Весь смысл подписания в масштабе в том, чтобы сделать этот исход структурно невозможным, а не статистически редким.

  • Каждый документ подписывается индивидуально, по собственному диапазону байтов. Пакет — это слово про планирование, а не про криптографию. Общей подписи нет.
  • Уровень — это контракт, а не подсказка. Вы называете базовый уровень PAdES, и движок производит ровно этот уровень для каждого документа — или громко проваливает этот документ (Spec: ETSI EN 319 142-1).
  • Конвейер работает по принципу fail-closed. Документ, который нельзя подписать корректно, не проходит дальше как обычные байты. Он удерживается, а не передаётся дальше.
  • Конкурентность — на уровне документа и безопасна по построению. Единицы подписания не разделяют изменяемое состояние, поэтому два worker не могут испортить вывод друг друга.
  • Большие прогоны устойчивы. Зафиксированный вывод не выдаётся повторно при возобновлении; аварийно завершённый прогон продолжается с последней контрольной точки, а не подписывает всё заново.

Конструкция держится на одном разделении: производство подписи — это малый, детерминированный шаг на уровне отдельного документа; безопасное выполнение тысяч таких — это шаг оркестрации. Их разделение и есть то, что позволяет каждому оставаться простым.

Шаг подписания — тот, что никогда не должен идти на компромисс. Вы запрашиваете уровень — вариант перечисления SignatureLevel, а не строку, которую движку нужно интерпретировать, — и этот уровень рассматривается как контракт для этого документа. Движок производит запрошенный уровень или останавливается с пригодной для действий ошибкой; он не подписывает тихо на более низком уровне, позволяя записи заявлять более высокий. Корректность не ослабевает оттого, что за этим документом стоят ещё. Стотысячная подпись вычисляется ровно так же тщательно, как первая.

Правило fail-closed — это то, что делает это заслуживающим доверия на объёме. Путь подписания NextPDF отказывается выдать правдоподобно выглядящий, но неподписанный артефакт вместо того, который вы запросили. Поддерживаемый прикладной маршрут — это высокоуровневый API Document: вы настраиваете подпись через Document::setSignature(), а затем запрашиваете байты через Document::getPdfData() (или save() / output()), и этот единственный проход записи либо выдаёт корректно подписанный PDF, либо выбрасывает исключение до возврата байтов — никогда неподписанный файл, который вызывающий считает подписанным. В применении к пакету это и есть правило, превращающее «один проскользнул неподписанным» из тихого скрытого дефекта в единственное проваленное задание, которое можно повторить.

  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 даёт вам криптографическую корректность: программное подписание CMS и PAdES B-B (с B-T через клиент меток времени), где каждый документ подписывается индивидуально и по принципу fail-closed. Оркестрация, которая делает большой прогон устойчивым, конкурентным и exactly-once — движок отрисовки без побочных эффектов плюс коммиттер, хранилища контрольных точек, идемпотентности и dead-letter — это модуль Stream в продвинутых редакциях; подписание с аппаратной поддержкой через HSM или облачный KMS — точно так же шов продвинутой редакции. Core доказывает, что каждая подпись верна; продвинутые редакции делают переживаемым миллион таких.

Форма ниже — это единица подписания на уровне документа внутри пакетного цикла. Каждая итерация подписывает один документ на названном уровне и либо выдаёт корректно подписанный результат, либо проваливает именно это задание — она никогда не возвращает неподписанные байты, выданные за результат.

<?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]);
}
}

catch — это несущая строка. Это разница между прогоном, который удерживает документы, которые не смог подписать, и прогоном, который всё равно их выпускает. continue не заминает сбой — задание записывается и оставляется для повтора, поэтому пакет завершается с известным, полным списком того, что подписано и что нет, никогда не с тихим пробелом.

Первое заблуждение — что «пакетное подписание» означает одну подпись, применённую к многим файлам. Это не так, и любая система, которая это утверждает, не производит действительных подписей PAdES — дайджест каждого документа привязан к его собственным байтам (Spec: ISO 32000-2, §12.8). Пакет — это исключительно про сколько и как быстро, никогда про разделение криптографической единицы.

Второе — что конкурентность означает ослабление корректности ради скорости — будто быстрый подписант обязан срезать угол, которого аккуратный не срезает. Это не так. Поскольку единицы подписания не разделяют изменяемого состояния, запуск их параллельно меняет расписание, а не байты. Каждая параллельная подпись вычисляется с той же строгостью, что и одиночная; параллелизм — в оркестрации вокруг них.

Третье — что устойчивость — это то, что прикручивают после первого проваленного ночного прогона. К тому моменту вы уже потеряли прогон. Возобновляемый конвейер должен знать, по каждому документу, что зафиксировано, а что нет, до аварийного завершения — что ровно и есть то, для записи чего существуют хранилища контрольных точек и идемпотентности.

  • Каждая подпись — на уровне отдельного документа и привязана к стандартам; пакетного сокращённого пути нет. Объём меняет планирование, а не криптографическую единицу. NextPDF подписывает каждый документ по его собственному диапазону байтов.
  • Core делает программное подписание CMS и PAdES B-B (B-T через клиент меток времени). Устойчивый, конкурентный, exactly-once движок отрисовки-и-подписания — это модуль Stream в продвинутых редакциях; хранение ключей с поддержкой HSM/KMS — это шов продвинутой редакции. Эта страница не заявляет эту оркестрацию как Core.
  • Fail-closed — это поведение движка, а не гарантия про вашу обвязку. NextPDF отказывается выдать файл, который неподписан, но считается подписанным, и выводит поддерживаемый маршрут подписания. Конвейер, который ловит возникшую ошибку и всё равно фиксирует, выбрал победить гарантию — та формулировка, для предотвращения которой существуют catch/continue в примере.
  • Уровень PAdES обеспечивается на уровне документа, а не сертифицируется для прогона. Движок производит запрошенный базовый уровень или проваливается; это структурное обеспечение, а не вердикт стороннего соответствия для произведённых файлов. Сама прогрессия уровней рассмотрена в Базовые профили PAdES.
  • Очередь, хранение ключей, центр меток времени и хранилище объектов — ваши. NextPDF поставляет корректность подписания на уровне документа и, в продвинутых редакциях, устойчивые примитивы оркестрации. Он не управляет вашей инфраструктурой и не ручается за ваш TSA.
High-volume and concurrent signing — edition availability
EditionAvailability
Core

Программное подписание CMS на уровне документа, PAdES B-B (B-T с клиентом меток времени), подписанное индивидуально по собственному диапазону байтов каждого документа, fail-closed против тихо неподписанного вывода. Простое подписание на уровне документа не требует коммерческого уровня.

Pro

Добавляет модуль Stream: движок отрисовки без побочных эффектов плюс устойчивый коммиттер, хранилища контрольных точек, идемпотентности и dead-letter — конкурентные, устойчивые к сбоям, exactly-once пакетные прогоны, которые возобновляются, а не начинаются заново.

Enterprise

Добавляет хранение ключей с аппаратной поддержкой (HSM через PKCS#11 или облачный KMS), так что закрытый ключ никогда не покидает устройство, и долгосрочные уровни PAdES (B-LT, B-LTA), которые держат высоконагруженный архив проверяемым десятилетиями.

  • Генерация документов в больших объёмах — модель с ограниченной памятью и очередью, поверх которой подписывает эта страница; прочтите её первой ради дисциплины пропускной способности и измерения.
  • Базовые профили PAdES — что добавляет каждый уровень (от B-B до B-LTA), чтобы вы подписывали на том уровне, которого требует обязательство.
  • Как подписи располагаются в PDF — основа диапазона байтов и словаря, которая делает подпись принадлежащей отдельному документу.
  • Подписание с поддержкой HSM — где проходит граница закрытого ключа, когда материал подписания живёт в аппаратуре.
  • Stream (Pro) — устойчивый, конкурентный, exactly-once движок отрисовки, который превращает единственную единицу подписания в возобновляемый прогон.
  • Пакетное подписание — подписание многих документов по расписанию. Концепция планирования; каждый документ всё равно подписывается индивидуально по собственным байтам.
  • Fail-closed — при сбое, который иначе произвёл бы неподписанный или неправильный вывод, конвейер удерживает документ и сообщает, а не передаёт его дальше как обычные байты.
  • Exactly-once commit — свойство устойчивого конвейера, при котором корректно подписанный вывод публикуется один раз и не выдаётся повторно при возобновлении аварийно завершённого прогона.
  • Контрольная точка (checkpoint) — устойчивая запись на уровне документа о том, что зафиксировано, чтобы прогон мог продолжиться с того места, где остановился, а не подписывать всё заново.
  • CMS SignedData — криптографический контейнер для подписей над содержимым (он может нести нескольких подписантов); этот конвейер производит подпись PDF одного подписанта на документ, единицу на уровне документа, которую производит пакет.
  • PAdES — PDF Advanced Electronic Signatures, семейство профилей ETSI EN 319 142 для подписания PDF; его базовые уровни идут от B-B до B-LTA.