Salta ai contenuti
getnextpdf.com

Enterprise edizione

Validazione batch delle firme

NextPDF Enterprise valida le firme digitali su molti documenti PDF con una sola chiamata. NextPDF\Enterprise\Signature\BatchSignatureValidator::validate() accetta un elenco di documenti e restituisce un BatchValidationReport. Ogni firma attraversa la stessa pipeline fail-closed: autenticazione crittografica CMS sull’intervallo di byte firmato, validazione della catena dei certificati ancorata alla fiducia e controllo della revoca OCSP/CRL. Il report riporta il dettaglio per documento e per firma — CertChainStatus, RevocationStatus, TimestampStatus — così che gli strumenti di conformità possano riderivare ogni verdetto dalle prove registrate.

Il modello del verdetto è deliberatamente rigoroso. Una firma è Valid solo quando tutte le prove sono affermativamente stabilite. Prove di revoca mancanti producono Indeterminate, mai Valid. Questa pagina copre l’orchestratore batch e i suoi tipi di risultato. Il lato di verifica AdES a documento singolo è documentato in Verifica delle firme. L’incorporamento di materiale per la validazione a lungo termine è documentato in Archivio.

Questa funzionalità è distribuita in NextPDF Enterprise (nextpdf/enterprise) e si attiva con un envelope di licenza di livello Enterprise. Un deployment privo di questo diritto d’uso non carica le classi della funzionalità. Confronta le edizioni e ottieni una licenza.

Terminal window
composer require nextpdf/enterprise

Anche il metapacchetto nextpdf/premium risolve il pacchetto Enterprise. L’attivazione usa il tuo envelope di licenza Enterprise; vedi Licenza e attivazione. I tipi batch vengono caricati automaticamente sotto NextPDF\Enterprise\Signature. Non è richiesta alcuna estensione PHP oltre la baseline del motore.

Una chiamata a validate() elabora un elenco di valori DocumentSignatureInput. Ogni input porta un identificatore del documento, i byte grezzi del PDF e trust anchor opzionali codificati in PEM. Il validatore estrae i dizionari di firma di ogni documento ed esegue tre fasi per firma.

Fase 1 — autenticazione crittografica. Il blob CMS/PKCS#7 detached tratto da /Contents viene verificato sui byte coperti da /ByteRange. Il verificatore ricalcola autonomamente il digest del contenuto e lo confronta con l’attributo firmato messageDigest. Non si fida mai di un digest fornito dal produttore (RFC 5652 §5.6). Il valore della firma deve verificarsi e il certificato firmatario deve essere legato al CMS. Un /Contents o /ByteRange assente o malformato, un CMS non analizzabile, una discrepanza del digest o un controllo della firma fallito falliscono tutti in modo chiuso. Una firma che si verifica sotto SHA-1 è trattata come debole e non è mai un pass completo.

Fase 2 — validazione della catena e ancoraggio alla fiducia. La catena del firmatario recuperata dal CMS viene validata come percorso di certificazione prospettico. I trustedCerts che fornisci sono l’input di trust anchor, nel senso della RFC 5280 §6.1.1: il capolinea della catena deve corrispondere a un anchor fornito per impronta DER SHA-256. Una catena strutturalmente coerente il cui capolinea non è un anchor configurato non viene mai riportata come attendibile. In assenza di anchor utilizzabili, viene riportato solo il verdetto strutturale e CertChainStatus::$trusted resta false.

Fase 3 — revoca. La revoca viene eseguita sulla catena recuperata dopo l’autenticazione, rispecchiando il modello ETSI EN 319 102-1 in cui il controllo della revoca segue la validazione riuscita del percorso (clausola 5.2.6.2). OCSP è primario: conta solo una risposta verificata crittograficamente, come Good o Revoked. Il percorso CRL è il fallback e attesta la freschezza della lista. Quando nessuno dei due client è configurato, lo stato è unavailable.

Il verdetto per firma è un SignatureValidationStatus. La tassonomia rispecchia il modello di stato ETSI EN 319 102-1 (TOTAL-PASSED / TOTAL-FAILED / INDETERMINATE) a granularità per firma:

ProvaVerdetto
Certificato confermato revocatoInvalid (decisivo, indipendentemente dagli altri controlli)
Autenticazione CMS fallita, nessun materiale del firmatario recuperatoError
Autenticazione CMS fallita, materiale del firmatario presenteInvalid
Autenticato, ma la catena non si validaInvalid (o Error senza catena)
Autenticato e catena valida, ma nessun trust anchor confermatoIndeterminate
Autenticato, catena valida, attendibile, ma nessuna non-revoca conclusivaIndeterminate
Tutto quanto sopra affermativamente stabilitoValid

La regola della non-revoca conclusiva. “Non dimostrato revocato” non è lo stesso di “dimostrato non revocato”. Un verdetto Valid richiede almeno un risultato di revoca Good. Una risposta OCSP verificata come buona è la forma conclusiva: asserisce lo stato del certificato firmatario stesso. Anche una CRL accettata crittograficamente e fresca soddisfa il gate in questa implementazione, ma solo come attestazione di freschezza e integrità — il percorso non analizza le voci per numero di serie, quindi non fornisce alcuna garanzia di revoca per numero di serie e mai un verdetto revoked positivo. Configura OCSP ovunque il rilevamento positivo della revoca conti: un deployment solo CRL non farà emergere un certificato revocato come Invalid. Quando sia il risultato OCSP che quello CRL sono Unknown o Unavailable, lo stato di revoca è indeterminato e il verdetto è Indeterminate. Questo segue ETSI EN 319 102-1: informazioni sullo stato di revoca non disponibili producono INDETERMINATE, mai un pass (clausola 5.1.3, TRY_LATER). Questo è un irrigidimento del comportamento in 3.1.0 con impatto sulla retrocompatibilità: le release precedenti potevano riportare Valid senza prove di revoca conclusive. I deployment che non configurano alcun client OCSP o CRL ora vedono comunemente Indeterminate dove prima vedevano Valid.

Due limiti inquadrano onestamente questa funzionalità. Primo, il validatore batch non valuta i token di marca temporale incorporati: TimestampStatus nei risultati batch è sempre lo stato assente. La valutazione delle marche temporali RFC 3161 appartiene al lato di verifica a documento singolo; vedi Verifica delle firme. Secondo, questa pagina riguarda la validazione in sola lettura. L’incorporamento di materiale DSS/VRI per la validità a lungo termine è la funzionalità Archivio.

La decisione portante è un produttore di verdetti fail-closed. Valid viene coniato solo da prove affermative su tutti e tre gli assi: autenticazione crittografica, catena ancorata alla fiducia e non-revoca conclusiva. Qualsiasi cosa non stabilita degrada a Indeterminate anziché ripiegare su un pass, che è la postura EN 319 102-1 per il materiale di revoca mancante. Il throughput batch non riacquista mai il rigore: lo strato batch è orchestrazione sopra lo stesso verificatore CMS auditato usato per un singolo documento, quindi un’esecuzione di 1.000 documenti applica una crittografia identica. Il report separa anche le prove dal verdetto — CertChainStatus e RevocationStatus registrano gli input su cui poggia ogni verdetto, così che un auditor possa riderivarlo in seguito.

Contesto di progettazione: Firmare su larga scala, senza compromessi.

Tutti i simboli sottostanti sono API pubblica in nextpdf/enterprise 3.1.0.

final class BatchSignatureValidator
{
public function __construct(
?SignatureExtractor $extractor = null,
?CertificateChainValidator $chainValidator = null,
private readonly ?OcspClient $ocspClient = null,
private readonly ?CrlFetcher $crlFetcher = null,
?CmsSignatureDataExtractor $cmsExtractor = null,
private readonly ClockInterface $clock = new SystemClock(),
)
public function validate(array $inputs): BatchValidationReport
}

Solleva o fallisce con: validate() solleva \InvalidArgumentException se l’elenco di input è vuoto, e \OverflowException quando il batch supera i 1.000 documenti. Un documento che non è un PDF analizzabile non solleva un’eccezione; diventa un risultato per documento Error. Il $clock è un Psr\Clock\ClockInterface PSR-20 usato per la decisione sulla freschezza della CRL, quindi i verdetti sono deterministici sotto un clock di test congelato.

final readonly class DocumentSignatureInput
{
public string $documentId;
public function __construct(
string $documentId,
public string $pdfData,
public array $trustedCerts = [],
)
}

Solleva o fallisce con: \InvalidArgumentException se $documentId è una stringa vuota. $trustedCerts è un elenco di certificati trust-anchor codificati in PEM.

final readonly class BatchValidationReport
{
public function __construct(
public array $documents,
public int $totalDocuments,
public int $totalSignatures,
public int $totalValid,
public int $totalInvalid,
public float $durationMs,
)
public function allValid(): bool
public function hasDocumentsWithoutSignatures(): bool
public function toJson(?CertPiiGuard $piiGuard = null): string
}

Solleva o fallisce con: toJson() solleva \JsonException se la codifica fallisce. allValid() è true solo quando ci sono firme e nessuna è non valida. Per impostazione predefinita toJson() applica un NextPDF\Enterprise\Signature\Eidas\CertPiiGuard privacy-by-default, che maschera il nome del firmatario, l’emittente radice, il nome della TSA e le diagnostiche dei problemi di catena; vedi Livelli di garanzia eIDAS per l’API della guardia.

DocumentValidationResult e DocumentValidationStatus

Sezione intitolata “DocumentValidationResult e DocumentValidationStatus”
final readonly class DocumentValidationResult
{
public function __construct(
public string $documentId,
public DocumentValidationStatus $status,
public array $signatures,
public int $validCount,
public int $invalidCount,
)
public function hasSignatures(): bool
public function totalSignatures(): int
}
enum DocumentValidationStatus: string
{
case AllValid = 'all_valid';
case SomeInvalid = 'some_invalid';
case AllInvalid = 'all_invalid';
case NoSignatures = 'no_signatures';
case Error = 'error';
}

Solleva o fallisce con: nulla. Value object immutabile ed enum tipizzato.

SignatureValidationResult e SignatureValidationStatus

Sezione intitolata “SignatureValidationResult e SignatureValidationStatus”
final readonly class SignatureValidationResult
{
public function __construct(
public SignatureValidationStatus $status,
public CertChainStatus $certChain,
public TimestampStatus $timestamp,
public RevocationStatus $revocation,
public string $signer,
public string $level = '',
public string $subFilter = '',
public string $reason = '',
)
public function isValid(): bool
}
enum SignatureValidationStatus: string
{
case Valid = 'valid';
case Invalid = 'invalid';
case Indeterminate = 'indeterminate';
case Error = 'error';
}

Solleva o fallisce con: nulla. $signer è il soggetto del certificato verificato dal CMS quando l’autenticazione è riuscita, altrimenti la stringa vuota. $level è un’etichetta derivata da SubFilter (per esempio B-B per ETSI.CAdES.detached), non una determinazione di conformità AdES.

final readonly class CertChainStatus
{
public function __construct(
public bool $valid,
public bool $trusted,
public int $chainLength,
public string $rootIssuer,
public array $issues = [],
)
public function hasIssues(): bool
}

Solleva o fallisce con: nulla. $trusted viene impostato solo su un riscontro confermato di appartenenza a un trust anchor, mai dalla non-vuotezza della lista degli anchor.

final readonly class RevocationStatus
{
public function __construct(
public RevocationCheckResult $ocspStatus,
public RevocationCheckResult $crlStatus,
public bool $isRevoked,
public ?DateTimeImmutable $revocationDate = null,
)
public static function unavailable(): self
public function hasConclusiveGood(): bool
}
enum RevocationCheckResult: string
{
case Good = 'good';
case Revoked = 'revoked';
case Unknown = 'unknown';
case Unavailable = 'unavailable';
}

Solleva o fallisce con: nulla dai membri mostrati. La classe espone anche factory statiche verificate sulle prove (good(), revoked(), fromResults()), che sollevano \InvalidArgumentException quando lo stato dichiarato contraddice le prove OCSP/CRL — un risultato revocato non può mai essere coniato come non-revocato, o viceversa. hasConclusiveGood() è true solo per uno stato non-revocato in cui almeno un controllo è Good.

final readonly class TimestampStatus
{
public function __construct(
public bool $present,
public bool $valid,
public ?DateTimeImmutable $timestampTime = null,
public string $tsaName = '',
public array $issues = [],
)
public static function absent(): self
}

Solleva o fallisce con: nulla. Nei risultati batch questo è sempre lo stato absent(); vedi Casi limite e insidie.

Valida un documento e leggi il report. Questo esempio usa un PDF non firmato, quindi l’output è deterministico.

batch-quick-start.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Enterprise\Signature\BatchSignatureValidator;
use NextPDF\Enterprise\Signature\DocumentSignatureInput;
// A minimal, unsigned PDF: the validator reports it as no_signatures.
$unsigned = "%PDF-1.7\n1 0 obj\n<< /Type /Catalog >>\nendobj\ntrailer\n<< /Root 1 0 R >>\n%%EOF\n";
$validator = new BatchSignatureValidator();
try {
$report = $validator->validate([
new DocumentSignatureInput(documentId: 'doc-001', pdfData: $unsigned),
]);
} catch (\InvalidArgumentException $e) {
// Empty input list, or an empty documentId.
echo 'Rejected: ' . $e->getMessage() . "\n";
exit(1);
}
echo 'Documents: ' . $report->totalDocuments . "\n";
echo 'Signatures: ' . $report->totalSignatures . "\n";
foreach ($report->documents as $doc) {
echo $doc->documentId . ': ' . $doc->status->value . "\n";
}
echo 'All valid: ' . ($report->allValid() ? 'yes' : 'no') . "\n";
echo 'Unsigned documents: ' . ($report->hasDocumentsWithoutSignatures() ? 'yes' : 'no') . "\n";

Output atteso:

Documents: 1
Signatures: 0
doc-001: no_signatures
All valid: no
Unsigned documents: yes

Nota che qui allValid() riporta no: richiede almeno una firma e nessun risultato non valido, quindi un insieme di firme vuoto non passa mai silenziosamente.

Valida una directory di contratti firmati con client di revoca, trust anchor, suddivisione in batch e un report JSON protetto dalle informazioni personali.

batch-validate-contracts.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Enterprise\Security\Ltv\CrlFetcher;
use NextPDF\Enterprise\Security\Ltv\OcspClient;
use NextPDF\Enterprise\Security\Ltv\OcspResponseCache;
use NextPDF\Enterprise\Signature\BatchSignatureValidator;
use NextPDF\Enterprise\Signature\DocumentSignatureInput;
use NextPDF\Enterprise\Signature\SignatureValidationStatus;
// Any PSR-18 client works; Guzzle shown here.
$httpClient = new \GuzzleHttp\Client(['timeout' => 10]);
// Revocation clients make a conclusive non-revoked (Good) result reachable.
// Without them, every verdict tops out at Indeterminate. The response cache
// lets repeat signers across the batch resolve without extra network calls.
$validator = new BatchSignatureValidator(
ocspClient: new OcspClient($httpClient, cache: new OcspResponseCache()),
crlFetcher: new CrlFetcher($httpClient),
);
// Trust anchors are an input: the chain terminus must match one of these.
$anchors = [(string) file_get_contents('/etc/nextpdf/trust/enterprise-root.pem')];
$inputs = [];
foreach (glob('/var/contracts/signed/*.pdf') ?: [] as $path) {
$inputs[] = new DocumentSignatureInput(
documentId: basename($path),
pdfData: (string) file_get_contents($path),
trustedCerts: $anchors,
);
}
$exit = 0;
// One call is capped at 1,000 documents; chunk larger runs.
foreach (array_chunk($inputs, 1000) as $batch) {
try {
$report = $validator->validate($batch);
// Signer PII is redacted by default in the serialized report.
file_put_contents('/var/log/nextpdf/batch-report.jsonl', $report->toJson() . PHP_EOL, FILE_APPEND); // one JSON document per line
} catch (\InvalidArgumentException | \OverflowException $e) {
fwrite(STDERR, 'Batch rejected: ' . $e->getMessage() . "\n");
exit(2);
} catch (\JsonException $e) {
fwrite(STDERR, 'Report encoding failed: ' . $e->getMessage() . "\n");
exit(3);
}
foreach ($report->documents as $doc) {
foreach ($doc->signatures as $sig) {
if ($sig->status !== SignatureValidationStatus::Valid) {
$exit = 1;
fwrite(STDERR, sprintf(
"%s: %s (chain trusted: %s, revoked: %s)\n",
$doc->documentId,
$sig->status->value,
$sig->certChain->trusted ? 'yes' : 'no',
$sig->revocation->isRevoked ? 'yes' : 'no',
));
}
}
}
}
exit($exit);

Output atteso (stderr, per un documento le cui prove di revoca erano non disponibili; le altre righe variano con i tuoi input):

contract-0042.pdf: indeterminate (chain trusted: yes, revoked: no)

Il report JSON serializza i campi di identità del firmatario attraverso il CertPiiGuard predefinito, quindi una voce per firma appare così (estratto, illustrativo):

{
"status": "indeterminate",
"signer": "[REDACTED]",
"level": "B-B",
"subFilter": "ETSI.CAdES.detached"
}
  • Un elenco di input vuoto solleva \InvalidArgumentException; più di 1.000 documenti in una sola chiamata sollevano \OverflowException. Suddividi le esecuzioni più grandi, come nell’esempio di produzione.
  • Aggiornamento da release precedenti: senza alcun client OCSP o CRL configurato, la revoca è unavailable, quindi nessuna firma può raggiungere Valid. Le release precedenti riportavano Valid qui; la 3.1.0 riporta Indeterminate (vedi Panoramica concettuale).
  • I contatori a livello di documento sono rigorosi: solo Valid incrementa validCount. Invalid, Indeterminate ed Error incrementano tutti invalidCount. Un documento la cui unica firma è Indeterminate riporta pertanto all_invalid. Filtra sullo status per firma quando la distinzione conta.
  • Il controllo OCSP viene eseguito solo quando la catena recuperata ha almeno due certificati, perché la query necessita dell’emittente. Una catena a certificato singolo ricade sul percorso CRL o su unavailable.
  • crlStatus non riporta mai revoked nei risultati batch. Il fallback CRL attesta solo la freschezza della lista; un risultato revocato autorevole proviene da OCSP.
  • timestamp è sempre absent() nei risultati batch. Il validatore batch non valuta i token RFC 3161 incorporati; usa Verifica delle firme per la valutazione delle marche temporali.
  • signer è vuoto quando l’autenticazione è fallita. Quando impostato, è il CN (o O) del soggetto del certificato verificato dal CMS — mai la stringa /Name non autenticata del dizionario di firma.
  • Le voci di trustedCerts devono essere certificati PEM. Un elenco di anchor vuoto o malformato produce un verdetto di catena solo strutturale con trusted: false, limitando il verdetto a Indeterminate.
  • I byte che non iniziano con un header PDF producono uno stato per documento error con zero firme — nessuna eccezione.
  • toJson() oscura le informazioni personali per impostazione predefinita. Passa new CertPiiGuard(disclosePii: true) solo dove disponi di una base giuridica documentata per trattare l’identità del firmatario.
  • Produttore di verdetti fail-closed. Valid richiede tutto quanto segue: autenticazione CMS verificata sul digest di /ByteRange, una catena valida, appartenenza a un trust anchor confermata e uno stato non-revocato conclusivo. Ogni controllo non stabilito degrada il verdetto; nulla ripiega su un pass.
  • Nessun riciclaggio di identità. Il firmatario riportato è il soggetto del certificato legato crittograficamente. La voce /Name è metadato controllabile dall’attaccante e non viene mai fatta emergere come firmatario.
  • Gli algoritmi deboli non passano mai. Una firma SHA-1 che si verifica è comunque riportata come non valida; la validità crittografica sotto un digest debole non viene riciclata in un pass completo.
  • La fiducia è un input, non un’inferenza. Gli anchor che fornisci vengono confrontati con il capolinea della catena per impronta DER SHA-256 (RFC 5280 §6.1.1). L’auto-coerenza di una catena, o una lista di anchor non vuota da sola, non stabilisce mai la fiducia.
  • La revoca è decisiva. Una dichiarazione di revoca verificata forza Invalid indipendentemente da ogni altro controllo; prove non disponibili forzano Indeterminate.
  • Privacy per impostazione predefinita nell’output serializzato. toJson() maschera il CN del firmatario, il DN dell’emittente radice, il nome della TSA e le diagnostiche dei problemi di catena a meno che non si scelga di rinunciare, implementando la minimizzazione dei dati dell’Articolo 5(1)(c) del GDPR al confine di serializzazione.
  • Tempo deterministico. La decisione sulla freschezza della CRL legge il clock PSR-20 iniettato, non l’orologio di sistema dell’host, quindi i verdetti di revoca sono riproducibili sotto test.

NextPDF Enterprise implementa un comportamento informato da ETSI EN 319 102-1 (il modello di stato di validazione a tre valori e la regola per cui informazioni di revoca non disponibili producono INDETERMINATE), RFC 5652 §5.6 (ricalcolo del digest lato verificatore) e RFC 5280 §6.1 (trust anchor come input della relying party alla validazione del percorso). Il supporto non è conformità, e la conformità non è certificazione. NextPDF non detiene alcuna certificazione e non ne concede alcuna. Il validatore batch non è un servizio di validazione qualificato, e i suoi stati sono verdetti ingegneristici allineati alla tassonomia EN 319 102-1 — non indicazioni TOTAL-PASSED/TOTAL-FAILED/INDETERMINATE da un processo di validazione completo della clausola 5. In particolare, la modalità batch non esegue alcuna prova di esistenza o elaborazione di marca temporale; il lato di verifica a documento singolo copre quel terreno.

Il validatore batch non consulta alcuna policy della modalità FIPS, e abilitare la modalità FIPS non cambia i verdetti batch. La sua gestione degli algoritmi lato verifica è fissa e fail-closed: le firme deboli (SHA-1) non sono mai riportate Valid, con o senza la modalità FIPS. La policy della modalità FIPS Enterprise regola il lato firma/generazione, documentato in FIPS 140 — Deep Reference. Il supporto FIPS 140 è una dichiarazione di capacità, non un’affermazione di validazione o certificazione.

  • validate() solleva \InvalidArgumentException per un elenco vuoto e \OverflowException oltre i 1.000 documenti. I documenti malformati non sollevano mai un’eccezione; producono risultati per documento error.
  • Valid richiede la congiunzione: CMS verificato crittograficamente, catena valida, appartenenza a un trust anchor confermata e RevocationStatus::hasConclusiveGood() vero.
  • Un certificato confermato revocato è decisivo: il verdetto è Invalid indipendentemente da tutte le altre prove.
  • Entrambi i controlli di revoca Unknown/Unavailable significa Indeterminate, mai Valid (irrigidimento 3.1.0, impatto sulla retrocompatibilità).
  • Una firma autenticata e con catena valida senza un trust anchor confermato è Indeterminate — autentica, fiducia non stabilita.
  • signer è il soggetto verificato dal CMS o la stringa vuota; la voce /Name non viene mai usata.
  • timestamp è sempre lo stato assente nei risultati batch.
  • validCount conta solo Valid; tutti gli altri stati confluiscono in invalidCount, e lo stato del documento si aggrega da quei contatori.
  • toJson() applica il CertPiiGuard privacy-by-default a meno che non venga passata esplicitamente una guardia.
  • I totali del report sono somme esatte sui risultati per documento; durationMs è il tempo di parete misurato per il batch.

Il modulo Sicurezza / Firma di NextPDF Core è il lato produttore: crea firme CMS, applica marche temporali RFC 3161 e valida catene e revoca per il materiale che incorpora al momento della firma. Core non distribuisce alcun orchestratore batch lato verifica: nessun report multi-documento, nessuna tassonomia di stato aggregata, nessun verdetto di revoca OCSP/CRL per documenti di terze parti e nessuna serializzazione di report protetta dalle informazioni personali. Con Core da solo, dovresti estrarre e verificare ogni firma manualmente e costruire il tuo reporting. Il lato di verifica a documento singolo Enterprise (Verifica delle firme) e questo orchestratore batch forniscono quello strato.

Questa pagina documenta solo il comportamento osservabile esternamente e la superficie API pubblica supportata. I percorsi di namespace interni, le classi helper, le tabelle dei meccanismi, i nomi dei file dei runbook e i prefissi dei ticket sono fuori ambito.