Salta ai contenuti

Firmare un PDF con PAdES B-B e poi estenderlo a PAdES B-T

Questa ricetta produce una firma PAdES B-B — un CMS SignedData con gli attributi firmati (content-type, message-digest, signing-time). Estende poi questa firma a PAdES B-T aggiungendo un solo signature-time-stamp RFC 3161. B-T è B-B più una singola marca temporale; non è una classe di firma autonoma. La ricetta esplicita inoltre il limite di attendibilità: produrre una firma non equivale a una decisione di validità da parte di un verificatore.

Avvertenza U-1. NextPDF non rivendica alcuna certificazione ETSI EN 319 142-1 indipendente per PAdES B-T. EN 319 142-1 non è presente nel corpus di verifica; il requisito del B-T signature-time-stamp è stato verificato rispetto a ETSI EN 319 122-1 §5.3 insieme a RFC 3161, RFC 5652, RFC 5816 e ISO 32000-2 §12.8. Il supporto del profilo B-T non è una certificazione di conformità o di validità legale; tale determinazione spetta a un validatore indipendente.

B-LT e B-LTA (materiale di convalida DSS, ciclo di marca temporale di archiviazione) non rientrano nell’ambito di questa ricetta e non fanno parte della superficie di firma Core/Pro trattata qui.

Terminal window
composer require nextpdf/core:^3

ext-openssl deve essere abilitata — CertificateInfo analizza le chiavi tramite OpenSSL. B-T richiede inoltre un endpoint RFC 3161 TSA raggiungibile e un client HTTP PSR-18 per contattarlo.

Una firma PAdES B-B memorizza un CMS SignedData codificato in DER nella voce Contents del dizionario della firma; il valore di Contents è una stringa esadecimale con padding sul digest dell’intervallo di byte (ISO 32000-2 §12.8.1). Il digest copre il file ed esclude il valore della firma stesso (ISO 32000-2 §12.8.1).

PAdES B-T aggiunge esattamente un signature-time-stamp RFC 3161. Il message imprint della marca temporale è l’hash degli ottetti del valore di firma del SignerInfo — senza alcun tag o prefisso di lunghezza ASN.1 (ETSI EN 319 122-1 §5.3; RFC 3161 Appendice A). Il token è veicolato come attributo non firmato id-aa-timeStampToken, OID 1.2.840.113549.1.9.16.2.14 (RFC 3161 Appendice A), collocato in SignerInfo.unsignedAttrs [1] IMPLICIT (RFC 5652 §5.3). Poiché gli attributi non firmati non sono protetti dalla firma (RFC 5652 §5.4), il digest firmato B-B, il /ByteRange e i byte della firma B-B rimangono invariati — B-T si limita ad accodare la marca temporale. Il certificato della TSA è identificato tramite ESSCertIDv2 (RFC 5816 aggiorna RFC 3161).

Avvertenza U-1 (ribadita in corrispondenza dell’asserzione B-T). NextPDF non rivendica alcuna certificazione ETSI EN 319 142-1 indipendente per PAdES B-T. EN 319 142-1 non è presente nel corpus di verifica; il requisito del B-T signature-time-stamp è stato verificato rispetto a ETSI EN 319 122-1 §5.3 insieme a RFC 3161, RFC 5652, RFC 5816 e ISO 32000-2 §12.8. Il supporto del profilo B-T non è una certificazione di conformità o di validità legale; tale determinazione spetta a un validatore indipendente.

SignatureLevel::PAdES_B_T è una funzionalità Core: SignatureLevel::PAdES_B_T->requiresTimestamp() è true, ->isAvailableInEnvironment() è true e ->requiresDss() è false — B-T non comporta un Document Security Store. B-T ≠ B-LT ≠ B-LTA: una marca temporale di firma non aggiunge materiale di convalida né una marca temporale di archiviazione; si tratta di livelli distinti e superiori, che qui non vengono prodotti.

Il diagramma seguente mostra il flusso B-B e poi B-T, nell’ordine effettivamente usato dal motore. Il ByteRange viene calcolato solo dopo la scrittura dell’intero file, in modo che gli offset reali non alterino i byte sottoposti ad hash. B-T accoda poi un solo token RFC 3161 come attributo non firmato, lasciando intatto il digest firmato B-B.

RFC 3161 TSANextPDF DigitalSignerRFC 3161 TSANextPDF DigitalSignerReserve fixed-width /Contents slotand /ByteRange placeholderByteRange covers the whole fileexcluding the /Contents valuePAdES B-B completeB-T = B-B + 1 timestampB-B signed digest unchangedalt[level == PAdES B-T]Callersign — level B-B or B-T1Write the complete PDF incl. xref + EOF2Compute the two real ByteRange offsets3Hash the two concatenated segments4Build CMS SignedData with signed attrs5Hash the SignerInfo signature value — message imprint6TimeStampReq — message imprint + fresh nonce7TimeStampToken — signed, echoes imprint + nonce8Verify token — status, nonce, imprint, signature, time9Embed token in SignerInfo.unsignedAttrs10Signed PDF — /Contents = DER CMS SignedData11Caller
Diagram

Il punto di ingresso per la configurazione è Document::setSignature(CertificateInfo $certInfo, SignatureLevel $level = SignatureLevel::PAdES_B_B, ?TsaClient $tsaClient = null). Registra sul documento l’intento di firma. Il motore di firma PAdES Core (NextPDF\Security\Signature\DigitalSigner) produce la firma crittografica. La suite di integrazione esegue questo motore e l’esempio eseguibile lo invoca direttamente, perciò l’output è un oggetto CMS reale e analizzabile. SignatureLevel::PAdES_B_T richiede un TsaClient non nullo; costruire un firmatario B-T senza di esso solleva una SignatureException.

API di alto livello — una chiamata, output firmato

Sezione intitolata “API di alto livello — una chiamata, output firmato”

Il percorso più rapido passa dal punto di ingresso di alto livello: configurare la firma sul documento, quindi serializzare. Questo punto di ingresso esegue internamente lo stesso motore PAdES Core (DigitalSigner) — è un comodo strato sottile sul percorso dettagliato di livello inferiore descritto più avanti, non un percorso di codice separato.

<?php
declare(strict_types=1);
use NextPDF\Core\Document;
use NextPDF\Security\Signature\CertificateInfo;
use NextPDF\Security\Signature\SignatureLevel;
use NextPDF\Security\Timestamp\TsaClient;
$certInfo = CertificateInfo::fromPkcs12(
p12Path: __DIR__ . '/signer.p12',
password: 'p12-passphrase',
);
// PAdES B-B end to end: configure, then serialise.
$doc = Document::createStandalone();
$doc->addPage();
$doc->setFont('helvetica', '', 12);
$doc->cell(0, 10, 'Signed end to end.', newLine: true);
$doc->setSignature(certInfo: $certInfo, level: SignatureLevel::PAdES_B_B);
$doc->save(__DIR__ . '/signed.pdf'); // or output() to stream, getPdfData() for bytes
// PAdES B-T: pass a TsaClient on the same call — one RFC 3161
// signature-time-stamp is added (see the TsaClient hardening notes below).
$doc->setSignature(
certInfo: $certInfo,
level: SignatureLevel::PAdES_B_T,
tsaClient: $tsa,
);
$doc->save(__DIR__ . '/signed-bt.pdf');

save() (e allo stesso modo output() / getPdfData()) scrive la voce /Contents come un CMS SignedData codificato in DER sotto SubFilter ETSI.CAdES.detached (ISO 32000-2 §12.8, §12.7.5.5; RFC 5652). L’output è verificabile come CMS — un oggetto CMS SignedData ben formato che un parser CMS può leggere — ma questo non equivale alla conformità al profilo baseline ETSI EN 319 142-1 né alla validità legale; tali determinazioni spettano a un validatore indipendente (vedere l’avvertenza U-1 sopra). Per B-T, la chiamata di alto livello aggiunge esattamente l’unico signature-time-stamp RFC 3161 descritto nella panoramica concettuale; passare il TsaClient è l’unica differenza rispetto a B-B.

Usare il percorso dettagliato di livello inferiore DigitalSigner descritto più avanti quando serve un controllo esplicito sull’algoritmo, sui dati dell’intervallo di byte o sul SignatureResult.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Security\Signature\CertificateInfo;
use NextPDF\Security\Signature\DigitalSigner;
use NextPDF\Security\Signature\SignatureAlgorithm;
use NextPDF\Security\Signature\SignatureLevel;
$certInfo = CertificateInfo::fromPkcs12(
p12Path: __DIR__ . '/signer.p12',
password: 'p12-passphrase',
);
// PAdES B-B — a CMS SignedData, no timestamp.
$signer = new DigitalSigner(
certInfo: $certInfo,
level: SignatureLevel::PAdES_B_B,
algorithm: SignatureAlgorithm::Pkcs1v15,
);
$result = $signer->sign($byteRangeData);
echo $result->hasTimestamp() ? "B-T\n" : "B-B (no timestamp)\n";

Questo è il programma autonomo, eseguibile dall’harness. Rispecchia examples/36-sign-pades-b-b-and-b-t.php. Il programma costruisce un documento, lo configura per una firma PAdES, quindi firma in B-B e poi in B-T con un client TSA. In produzione il TsaClient punta a un endpoint RFC 3161 reale tramite un client PSR-18 rafforzato — un client HTTP attento alla sicurezza che applica il pinning dell’SPKI della TSA e risolve il DNS in modo sicuro. Per mantenere questo programma offline e deterministico, usa il client TSA fittizio fornito dal supporto ai test del repository. Il client TSA fittizio restituisce una TimeStampResp RFC 3161 strutturalmente valida.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
use NextPDF\Security\Signature\CertificateInfo;
use NextPDF\Security\Signature\DigitalSigner;
use NextPDF\Security\Signature\SignatureAlgorithm;
use NextPDF\Security\Signature\SignatureLevel;
use NextPDF\Security\Timestamp\TsaClient;
use NextPDF\Tests\Support\FakeTsaHttpClient;
// In your application, build CertificateInfo from your own signing material:
// CertificateInfo::fromPkcs12($p12Path, $passphrase) — a .p12/.pfx bundle
// CertificateInfo::fromFiles($certPem, $keyPem, $pass) — separate PEM files
// This program uses the repository RSA-2048 test fixtures so it is offline.
$certDir = __DIR__ . '/tests/Fixtures/Certificates';
$certPath = $certDir . '/test-rsa-2048-cert.pem';
$keyPath = $certDir . '/test-rsa-2048-key.pem';
if (!is_file($certPath) || !is_file($keyPath)) {
fwrite(STDERR, "Certificate fixtures absent. Run tests/Fixtures/Certificates/generate.sh\n");
exit(1);
}
$certInfo = new CertificateInfo(
certificate: (string) file_get_contents($certPath),
privateKey: (string) file_get_contents($keyPath),
);
// Build the document and record the signing intent on it. The ByteRange
// digest input is the document bytes with the /Contents placeholder
// excluded (ISO 32000-2 §12.8); getPdfData() yields the bytes to hash.
$doc = Document::createStandalone();
$doc->setTitle('Signed Invoice 2026-0042');
$doc->setAuthor('NextPDF Cookbook');
$doc->addPage();
$doc->setFont('helvetica', '', 12);
$doc->cell(0, 10, 'This document is configured for a PAdES signature.', newLine: true);
$doc->setSignature(certInfo: $certInfo, level: SignatureLevel::PAdES_B_B);
$byteRangeData = $doc->getPdfData();
// --- PAdES B-B: a CMS SignedData, no timestamp ---
$bb = (new DigitalSigner(
certInfo: $certInfo,
level: SignatureLevel::PAdES_B_B,
algorithm: SignatureAlgorithm::Pkcs1v15,
))->sign($byteRangeData);
// --- PAdES B-T: B-B + one RFC 3161 signature-time-stamp ---
// In production, build the TsaClient with your TSA endpoint and a hardened
// PSR-18 client (use the security-aware HTTP client for SSRF/DNS pinning):
// $tsa = new TsaClient(
// tsaUrl: 'https://tsa.example.com/timestamp',
// httpClient: $hardenedPsr18Client,
// );
// Here the offline fake TSA client keeps the program network-free.
$tsa = new TsaClient(
tsaUrl: 'https://tsa.example.com/timestamp',
httpClient: new FakeTsaHttpClient(),
);
$bt = (new DigitalSigner(
certInfo: $certInfo,
tsaClient: $tsa,
level: SignatureLevel::PAdES_B_T,
algorithm: SignatureAlgorithm::Pkcs1v15,
))->sign($byteRangeData);
// B-T = B-B + a single timestamp token. The B-B signed digest is unchanged;
// $bt->timestampToken holds the DER-encoded RFC 3161 token.
printf("PAdES B-B CMS: %d bytes, timestamp=%s\n", $bb->getSize(), $bb->hasTimestamp() ? 'yes' : 'no');
printf(
"PAdES B-T CMS: %d bytes, timestamp=%s (%d-byte RFC 3161 token)\n",
$bt->getSize(),
$bt->hasTimestamp() ? 'yes' : 'no',
strlen($bt->timestampToken),
);
echo "B-T = B-B + one RFC 3161 signature-time-stamp (unsigned attribute).\n";
// The harness sets NEXTPDF_COOKBOOK_OUTPUT and runs this script under the
// semantic profile (the signed CMS/timestamp bytes are inherently
// non-reproducible and are asserted by the PHPUnit harness, not a byte hash).
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');
file_put_contents($out !== false && $out !== '' ? $out : __DIR__ . '/signed-invoice.pdf', $byteRangeData);

STDOUT atteso (le dimensioni variano in base al certificato e al token TSA):

PAdES B-B CMS: <n> bytes, timestamp=no
PAdES B-T CMS: <n> bytes, timestamp=yes (<m>-byte RFC 3161 token)
B-T = B-B + one RFC 3161 signature-time-stamp (unsigned attribute).

Avvertenza U-1 (collocata in corrispondenza dell’asserzione di produzione B-T). NextPDF non rivendica alcuna certificazione ETSI EN 319 142-1 indipendente per PAdES B-T. EN 319 142-1 non è presente nel corpus di verifica; il requisito del B-T signature-time-stamp è stato verificato rispetto a ETSI EN 319 122-1 §5.3 insieme a RFC 3161, RFC 5652, RFC 5816 e ISO 32000-2 §12.8. Il supporto del profilo B-T non è una certificazione di conformità o di validità legale; tale determinazione spetta a un validatore indipendente.

  • B-T senza un client TSA. Costruire un B-T DigitalSigner senza alcun TsaClient solleva SignatureException (la TSA è obbligatoria per B-T). Verificare la configurazione della TSA prima della firma.
  • Raggiungibilità della TSA. B-T esegue un round-trip RFC 3161 attivo per ogni firma. Un’indisponibilità della TSA significa nessuna firma B-T. Usare un circuit breaker e un SLA della TSA adeguato al proprio throughput; il TsaClient accetta un circuit breaker.
  • Rafforzamento del client HTTP della TSA. Puntare il TsaClient a un client PSR-18 che applica il pinning dell’SPKI della TSA (formato RFC 7469) e risolve il DNS in modo sicuro; TsaClient::extractPublicKeyPin() ricava il pin dal certificato della TSA.
  • B-T non è B-LT/B-LTA. Una marca temporale di firma non incorpora materiale di convalida (certificati, OCSP, CRL) né una marca temporale di archiviazione. Questi sono i livelli B-LT/B-LTA e non vengono prodotti da questa ricetta.
  • Conflitto di linearizzazione. enableLinearization() e una firma configurata si escludono a vicenda — l’una o l’altra chiamata solleva InvalidConfigException quando l’altra è già impostata.
  • Chiavi HSM. Costruire CertificateInfo con CertificateInfo::fromHsm() per una chiave custodita in hardware; la chiave privata non entra mai nella memoria di processo. Il contratto del firmatario PKCS#11 è Core; un provider funzionante è Premium.

Una firma B-B è un’operazione CMS locale. B-T aggiunge un round-trip HTTP RFC 3161 sincrono verso la TSA per ogni firma. Tenere conto della latenza della TSA e dei limiti di frequenza nei carichi di lavoro batch. Preferire un TsaClient protetto da circuit breaker.

Una firma prodotta non è automaticamente una firma attendibile. Che una firma venga verificata o meno dipende dal certificato, dal suo trust anchor e dalla policy del verificatore — elementi che risiedono al di fuori di questa libreria. La cifratura offre riservatezza, non integrità; la firma offre integrity/authenticity, non riservatezza. Trattare la custodia delle chiavi come il rischio principale: una chiave software nella memoria di processo è sicura solo quanto l’host.

L’operazione di firma viene eseguita in-process; i byte del documento e la chiave privata non lasciano l’host tranne che per il round-trip B-T verso la TSA, che invia solo il message imprint (un hash del valore della firma), mai il contenuto del documento (RFC 3161 §2.4.1 MessageImprint). Nessun testo del documento né PII viene trasmesso alla TSA. Scegliere una TSA la cui giurisdizione corrisponda alla propria policy di residenza dei dati.

DigitalSigner accetta un logger PSR-3 facoltativo; registra l’algoritmo e il livello, non il materiale delle chiavi né i byte della firma. I parametri password su CertificateInfo e TsaClient sono contrassegnati #[SensitiveParameter], così le passphrase vengono oscurate dalle tracce dello stack. Non registrare nei log SignatureResult::$cmsSignedData$timestampToken.

Sono considerati: input manomesso dopo la firma (rilevato dal digest dell’intervallo di byte), compromissione della chiave (al di fuori dell’ambito della libreria — la custodia delle chiavi è responsabilità dell’integratore), impersonificazione della TSA (mitigata dal pinning SPKI sul client HTTP della TSA) e downgrade tra livelli (l’enum del livello è esplicito; il motore non effettua silenziosamente il downgrade da B-T a B-B). Non viene asserita l’assenza di vulnerabilità, né che una firma risultante sia legalmente valida.

Le primitive di firma sono fornite da OpenSSL. Su una build OpenSSL validata FIPS le operazioni RSA/ECDSA e SHA-256 vengono eseguite tramite il provider FIPS; NextPDF non asserisce di per sé la validazione FIPS. CryptoCapabilities riporta le primitive disponibili sull’host; verificare la catena di provider OpenSSL nel proprio deployment.

DichiarazioneSpecificaClausolareference_id
Il digest dell’intervallo di byte copre il file ed esclude il valore della firma.ISO 32000-2§12.8.1
Contents contiene un CMS SignedData DER; un Contents di document-timestamp contiene un TimeStampToken.ISO 32000-2§12.8.1
Contents è una stringa esadecimale riempita sul digest dell’intervallo di byte.ISO 32000-2§12.8.1
L’imprint del signature-time-stamp è l’hash degli ottetti del valore di firma del SignerInfo (senza tag/length ASN.1).ETSI EN 319 122-1§5.3
Il valore del signature-time-stamp è un SignatureTimeStampToken.ETSI EN 319 122-1§6
MessageImprint ::= SEQUENCE { hashAlgorithm, hashedMessage }.RFC 3161§2.4.1
L’imprint della marca temporale di firma è l’hash del campo signature del SignerInfo; SignatureTimeStampToken ::= TimeStampToken.RFC 3161App. A
id-aa-timeStampToken è l’OID 1.2.840.113549.1.9.16.2.14.RFC 3161App. A
SignerInfo contiene unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL.RFC 5652§5.3
Gli attributi non firmati non sono protetti dalla firma; il digest firmato B-B rimane invariato.RFC 5652§5.4
RFC 5816 aggiorna RFC 3161; ESSCertIDv2 identifica il certificato della TSA senza SHA-1.RFC 5816§1

Questa ricetta descrive come NextPDF produce una firma B-B e una B-T. Non asserisce che una firma risultante sia legalmente valida né che la conformità PAdES sia soddisfatta; tali determinazioni spettano a un validatore indipendente.

PAdES B-LT e B-LTA (materiale di convalida DSS e ciclo di marca temporale di archiviazione) e la custodia di chiavi HSM PKCS#11 sono incluse nelle edizioni Pro ed Enterprise. Questa ricetta copre deliberatamente solo B-B e B-T; i livelli superiori sono funzionalità distinte, verificate separatamente, e qui non rientrano nell’ambito.