Salta ai contenuti
getnextpdf.com

Firmare su larga scala, senza compromessi

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

Firmare un documento è un’operazione crittografica. Firmarne centomila entro una scadenza è la stessa operazione, ripetuta, dove il guasto pericoloso non è più «è stato lento» ma «uno di essi è uscito non firmato e nessuno se n’è accorto». Questa pagina parla di fare la seconda cosa senza rinunciare alla prima: firma in blocco e concorrente dove ogni firma è comunque corretta, l’esecuzione rifiuta di emettere un file che non è riuscita a firmare, e un grande lavoro riprende anziché ricominciare da capo.

Una firma è un fatto specifico per ciascun documento. Il suo digest è calcolato su un intervallo di byte dichiarato che esclude il valore della firma stessa (Spec: ISO 32000-2, §12.8), perciò non esiste un modo onesto di firmare mille documenti «come lotto» in un solo gesto — ciascuno porta con sé il proprio CMS SignedData sui propri byte (Spec: RFC 5652, §5.1). La scala quindi moltiplica le possibilità che esattamente una cosa vada silenziosamente storta: una key handle che ha fallito per un istante, un’autorità di marcatura temporale andata in timeout, un worker morto mentre teneva un file scritto a metà.

L’esito costoso non è un crash. Un crash è rumoroso e lo si riprova. L’esito costoso è quello silenzioso — un PDF non firmato che sembra completo, seduto in un archivio, scoperto mesi dopo dal validatore di un revisore. Su grandi volumi, «per lo più firmato» è indistinguibile da «firmato» fino al momento esatto in cui si verifica quello che conta. L’intero scopo della firma su larga scala è rendere quell’esito strutturalmente impossibile, non statisticamente raro.

  • Ogni documento è firmato individualmente, sul proprio intervallo di byte. Il lotto è una parola di scheduling, non una crittografica. Non esiste una firma condivisa.
  • Il livello è un contratto, non un suggerimento. Si indica un livello baseline PAdES e il motore produce esattamente quel livello per ogni documento, oppure fa fallire rumorosamente quel documento (Spec: ETSI EN 319 142-1).
  • La pipeline è fail-closed. Un documento che non può essere firmato correttamente non prosegue come byte grezzi. Viene trattenuto, non passato oltre.
  • La concorrenza è per documento, e sicura per costruzione. Le unità di firma non condividono stato mutabile, perciò due worker non possono corrompere l’output l’uno dell’altro.
  • Le grandi esecuzioni sono durevoli. L’output committato non viene riemesso alla ripresa; un’esecuzione interrotta da un crash continua dall’ultimo checkpoint anziché rifirmare tutto.

Il progetto poggia su una separazione: produrre la firma è un passo piccolo, deterministico, specifico per documento; eseguirne migliaia in sicurezza è un passo di orchestrazione. Tenerli separati è ciò che consente a ciascuno di restare semplice.

Il passo di firma è quello che non deve mai scendere a compromessi. Si chiede un livello — un caso dell’enum SignatureLevel, mai una stringa che il motore debba interpretare — e quel livello è trattato come un contratto per quel documento. Il motore produce il livello richiesto oppure si ferma con un errore azionabile; non firma silenziosamente a un livello inferiore lasciando che un record dichiari uno superiore. La correttezza non si allenta perché ci sono altri documenti dietro a questo. La centomillesima firma è calcolata con la stessa cura della prima.

La regola fail-closed è ciò che rende tutto ciò affidabile su grandi volumi. Il percorso di firma di NextPDF si rifiuta di emettere un artefatto dall’aspetto plausibile ma non firmato al posto di quello richiesto. La via applicativa supportata è l’API Document di alto livello: si configura la firma con Document::setSignature() e poi si chiedono i byte con Document::getPdfData() (oppure save() / output()), e quel singolo passaggio di scrittura emette un PDF correttamente firmato oppure solleva un’eccezione prima di restituire i byte — mai un file non firmato che il chiamante crede firmato. Applicata a un intero lotto, questa è la regola che converte «uno è sgusciato via non firmato» da difetto latente e silenzioso a un singolo job fallito e riprovabile.

  1. Scaldare il materiale di firma una volta solaAll'avvio del worker, aprire la sorgente di chiave/certificato e il client di marcatura temporale. Questo costo si paga una volta per worker, non una volta per documento.
  2. Accodare i documentiUna coda contiene i job per documento. La coda è la manopola del throughput — i worker di firma scalano orizzontalmente dietro di essa.
  3. Renderizzare e firmare un documentoUn'unità usa e getta renderizza il documento, poi lo firma sul proprio intervallo di byte al livello PAdES richiesto. Nulla è condiviso con il documento successivo.
  4. Committare in caso di successo, trattenere in caso di guastoUn file correttamente firmato viene committato una volta. Un documento che non è stato possibile firmare viene fatto fallire e riprovato — mai emesso come byte non firmati.
  5. Fare checkpoint, e riprendere dopo un crashUn'esecuzione durevole registra cosa è stato committato. Dopo un crash continua dall'ultimo checkpoint anziché rifirmare l'intero lotto.
Un'esecuzione di firma ad alto volume dall'inizio alla fine: il materiale di firma condiviso viene scaldato una volta sola; ogni documento è renderizzato e firmato individualmente su un'unità usa e getta; un risultato correttamente firmato viene committato esattamente una volta, mentre qualsiasi guasto è trattenuto per il retry, mai passato oltre come byte non firmati; un'esecuzione interrotta da un crash riprende dal suo checkpoint.

Il core offre la correttezza crittografica: firma software CMS e PAdES B-B (con B-T tramite il client di marcatura temporale) dove ogni documento è firmato individualmente e fail-closed. L’orchestrazione che rende una grande esecuzione durevole, concorrente ed esattamente-una-volta — il motore di rendering privo di effetti collaterali più il committer e gli store di checkpoint, idempotenza e dead-letter — è il modulo Stream nelle edizioni avanzate; la firma con custodia hardware tramite un HSM o un KMS in cloud è del pari una giuntura delle edizioni avanzate. Il core dimostra che ogni firma è corretta; le edizioni avanzate rendono un milione di firme sostenibili.

La forma qui sotto è l’unità di firma per documento all’interno di un ciclo di lotto. Ogni iterazione firma un documento a un livello indicato e o produce un risultato correttamente firmato o fa fallire quel singolo job — non restituisce mai byte non firmati travestiti da risultato.

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

Il catch è la riga portante. È la differenza tra un’esecuzione che trattiene i documenti che non è riuscita a firmare e un’esecuzione che li spedisce comunque. Il continue non maschera il guasto — il job viene registrato e lasciato per il retry, così il lotto termina con un elenco noto e completo di ciò che è stato firmato e ciò che non lo è stato, mai con una lacuna silenziosa.

Il primo equivoco è che «firma in blocco» significhi una sola firma applicata a molti file. Non è così, e qualsiasi sistema che lo sostenga non sta producendo firme PAdES valide — il digest di ciascun documento è legato ai propri byte (Spec: ISO 32000-2, §12.8). Il lotto riguarda puramente quanti e quanto in fretta, mai la condivisione dell’unità crittografica.

Il secondo è che la concorrenza significhi allentare la correttezza per la velocità — che un firmatario veloce debba tagliare un angolo che quello attento non taglia. Non è così. Poiché le unità di firma non condividono stato mutabile, eseguirle in parallelo cambia lo scheduling, non i byte. Ogni firma parallela è calcolata con lo stesso rigore di una singola; il parallelismo sta nell’orchestrazione attorno a esse.

Il terzo è che la durabilità sia qualcosa da aggiungere dopo la prima esecuzione notturna fallita. A quel punto l’esecuzione è già persa. Una pipeline riprendibile deve sapere, per ciascun documento, cosa è stato committato e cosa no prima del crash — che è esattamente ciò che gli store di checkpoint e idempotenza esistono per registrare.

  • Ogni firma è specifica per documento e vincolata agli standard; non esiste una scorciatoia per lotti. Il volume cambia lo scheduling, non l’unità crittografica. NextPDF firma ogni documento sul proprio intervallo di byte.
  • Il core fa firma software CMS e PAdES B-B (B-T tramite un client di marcatura temporale). Il motore di rendering-e-firma durevole, concorrente ed esattamente-una-volta è il modulo Stream nelle edizioni avanzate; la custodia delle chiavi con supporto HSM/KMS è una giuntura delle edizioni avanzate. Questa pagina non rivendica quell’orchestrazione come parte del core.
  • Il fail-closed è il comportamento del motore, non una garanzia sul tuo cablaggio. NextPDF rifiuta di emettere un file non firmato ma creduto firmato e fa emergere la via di firma supportata. Una pipeline che cattura l’errore risultante e committa comunque ha scelto di vanificare la garanzia — l’inquadramento che il catch/continue dell’esempio esiste per prevenire.
  • Il livello PAdES è imposto per documento, non certificato per l’esecuzione. Il motore produce il livello baseline richiesto oppure fallisce; è un’imposizione strutturale, non un verdetto di conformità di terze parti per i file prodotti. La progressione dei livelli è trattata in PAdES baseline profiles.
  • La coda, la custodia delle chiavi, l’autorità di marcatura temporale e l’object store sono tuoi. NextPDF fornisce la correttezza della firma per documento e, nelle edizioni avanzate, le primitive di orchestrazione durevole. Non gestisce la tua infrastruttura né fa da garante per la tua TSA.
High-volume and concurrent signing — edition availability
EditionAvailability
Core

Firma software CMS per documento, PAdES B-B (B-T con un client di marcatura temporale), firmata individualmente sul proprio intervallo di byte di ciascun documento, fail-closed contro l’output silenziosamente non firmato. La semplice firma per documento non richiede alcun tier commerciale.

Pro

Aggiunge il modulo Stream: un motore di rendering privo di effetti collaterali più committer durevole e store di checkpoint, idempotenza e dead-letter — esecuzioni di lotto concorrenti, crash-safe ed esattamente-una-volta che riprendono anziché ricominciare.

Enterprise

Aggiunge la custodia delle chiavi con supporto hardware (HSM tramite PKCS#11, o un KMS in cloud) così che la chiave privata non lasci mai il dispositivo, e i livelli PAdES di lungo termine (B-LT, B-LTA) che mantengono verificabile per decenni un archivio ad alto volume.

  • High-volume document generation — il modello di lotto in coda e a memoria limitata su cui questa pagina firma; leggilo prima per la disciplina del throughput e della misurazione.
  • PAdES baseline profiles — cosa aggiunge ciascun livello (da B-B a B-LTA), così da firmare al livello che l’obbligo richiede.
  • How signatures sit in a PDF — il fondamento di intervallo di byte e dizionario che rende una firma specifica per documento.
  • HSM-backed signing — dove si colloca il confine della chiave privata quando il materiale di firma vive nell’hardware.
  • Stream (Pro) — il motore di rendering durevole, concorrente ed esattamente-una-volta che trasforma una singola unità di firma in un’esecuzione riprendibile.
  • Firma in blocco — firmare molti documenti secondo uno scheduling. Un concetto di scheduling; ogni documento è comunque firmato individualmente sui propri byte.
  • Fail-closed — di fronte a un guasto che altrimenti produrrebbe un output non firmato o errato, la pipeline trattiene il documento e segnala, anziché passarlo oltre come byte grezzi.
  • Commit esattamente-una-volta — una proprietà della pipeline durevole per cui un output correttamente firmato viene pubblicato una volta sola e non viene riemesso quando un’esecuzione interrotta da un crash riprende.
  • Checkpoint — record durevole per documento di ciò che è stato committato, così che un’esecuzione possa continuare da dove si era fermata anziché rifirmare tutto.
  • CMS SignedData — il contenitore crittografico per le firme sul contenuto (può portare più firmatari); questa pipeline produce la firma PDF di un firmatario per documento, l’unità per documento che un lotto produce.
  • PAdES — PDF Advanced Electronic Signatures, la famiglia di profili ETSI EN 319 142 per la firma PDF; i suoi livelli baseline vanno da B-B a B-LTA.