Firmar un PDF con PAdES B-B y ampliarlo después a PAdES B-T
De un vistazo
Sección titulada «De un vistazo»Esta receta produce una firma PAdES B-B: un SignedData CMS con los atributos firmados (content-type, message-digest, signing-time). A continuación, amplía esa firma a PAdES B-T añadiendo un signature-time-stamp de RFC 3161. B-T es B-B más una única marca de tiempo; no es una clase de firma aparte. La receta también indica el límite de confianza: producir una firma no equivale a que un verificador determine su validez.
Salvedad U-1. NextPDF no declara contar con ninguna certificación independiente según ETSI EN 319 142-1 para PAdES B-T. EN 319 142-1 no está en el corpus de verificación; el requisito de
signature-time-stampde B-T se verificó frente a ETSI EN 319 122-1 §5.3 junto con RFC 3161, RFC 5652, RFC 5816 y ISO 32000-2 §12.8. La compatibilidad con el perfil B-T no es una certificación de conformidad ni de validez legal; esa determinación corresponde a un validador independiente.
B-LT y B-LTA (material de validación DSS, bucle de marca de tiempo de archivo) están fuera del alcance de esta receta y no forman parte de la superficie de firma de Core/Pro cubierta aquí.
Instalación
Sección titulada «Instalación»composer require nextpdf/core:^3ext-openssl debe estar habilitada: CertificateInfo analiza las claves mediante OpenSSL. B-T necesita además un endpoint de TSA de RFC 3161 accesible y un cliente HTTP PSR-18 para acceder a él.
Resumen conceptual
Sección titulada «Resumen conceptual»Una firma PAdES B-B almacena un SignedData CMS codificado en DER en la entrada Contents del diccionario de firma; el valor de Contents es una cadena hexadecimal rellenada sobre el resumen del rango de bytes (ISO 32000-2 §12.8.1). El resumen cubre el archivo y excluye el valor de la propia firma (ISO 32000-2 §12.8.1).
PAdES B-T añade exactamente un signature-time-stamp de RFC 3161. La huella del mensaje de la marca de tiempo es el hash de los octetos del valor de la firma de SignerInfo, sin etiqueta ASN.1 ni prefijo de longitud (ETSI EN 319 122-1 §5.3; RFC 3161 Apéndice A). El token se transporta como atributo no firmado id-aa-timeStampToken, OID 1.2.840.113549.1.9.16.2.14 (RFC 3161 Apéndice A), y se coloca en SignerInfo.unsignedAttrs [1] IMPLICIT (RFC 5652 §5.3). Como los atributos no firmados no están protegidos por la firma (RFC 5652 §5.4), el resumen firmado de B-B, el /ByteRange y los bytes de la firma de B-B permanecen sin cambios: B-T solo añade la marca de tiempo. El certificado del TSA se identifica con ESSCertIDv2 (RFC 5816 actualiza RFC 3161).
Salvedad U-1 (reiterada en la afirmación de B-T). NextPDF no declara contar con ninguna certificación independiente según ETSI EN 319 142-1 para PAdES B-T. EN 319 142-1 no está en el corpus de verificación; el requisito del
signature-time-stampde B-T se verificó frente a ETSI EN 319 122-1 §5.3 junto con RFC 3161, RFC 5652, RFC 5816 e ISO 32000-2 §12.8. La compatibilidad con el perfil B-T no es una certificación de conformidad ni de validez legal; esa determinación corresponde a un validador independiente.
SignatureLevel::PAdES_B_T es una capacidad de Core: SignatureLevel::PAdES_B_T->requiresTimestamp() es true, ->isAvailableInEnvironment() es true y ->requiresDss() es false; B-T no incorpora un Document Security Store. B-T ≠ B-LT ≠ B-LTA: una marca de tiempo de firma no añade material de validación ni una marca de tiempo de archivo; esos son niveles distintos y superiores que no se producen aquí.
El diagrama siguiente muestra el flujo B-B y luego B-T, en el orden real que utiliza el motor. El ByteRange se calcula solo después de escribir todo el archivo, de modo que los desplazamientos reales no puedan alterar los bytes sobre los que se calcula el hash. Después, B-T añade un token de RFC 3161 como atributo no firmado, dejando intacto el resumen firmado de B-B.
Superficie de la API
Sección titulada «Superficie de la API»El punto de entrada de configuración es Document::setSignature(CertificateInfo $certInfo, SignatureLevel $level = SignatureLevel::PAdES_B_B, ?TsaClient $tsaClient = null). Registra en el documento la intención de firmar. El motor de firma PAdES de Core (NextPDF\Security\Signature\DigitalSigner) produce la firma criptográfica. La suite de integración prueba este motor y el ejemplo ejecutable lo invoca directamente, de modo que la salida es un objeto CMS real y analizable. SignatureLevel::PAdES_B_T requiere un TsaClient no nulo; crear un firmante B-T sin uno lanza una SignatureException.
API de alto nivel — una sola llamada, salida firmada
Sección titulada «API de alto nivel — una sola llamada, salida firmada»El camino más rápido es la ruta de alto nivel: configurar la firma en el documento y luego serializarlo. Por debajo, esta ruta ejecuta el mismo motor PAdES de Core (DigitalSigner): es una capa de conveniencia ligera sobre el recorrido de nivel inferior que se muestra más abajo, no una ruta de código aparte.
<?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 igualmente output() / getPdfData()) escribe la entrada /Contents como un CMS SignedData codificado en DER bajo SubFilter ETSI.CAdES.detached (ISO 32000-2 §12.8, §12.7.5.5; RFC 5652). La salida se puede verificar como CMS: un objeto CMS SignedData bien formado que un analizador CMS puede leer. Esto no equivale a la conformidad con el perfil base de ETSI EN 319 142-1 ni a la validez legal; esas determinaciones corresponden a un validador independiente (consulta la salvedad U-1 más arriba). Para B-T, la llamada de alto nivel añade exactamente un único signature-time-stamp de RFC 3161, descrito en el resumen conceptual; pasar el TsaClient es la única diferencia con B-B.
Usar el recorrido de nivel inferior con DigitalSigner que aparece más abajo cuando se necesite control explícito sobre el algoritmo, los datos del rango de bytes o el SignatureResult.
Ejemplo de código — Inicio rápido
Sección titulada «Ejemplo de código — Inicio rápido»<?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";Ejemplo de código — Producción
Sección titulada «Ejemplo de código — Producción»Este es el programa autónomo que ejecuta el harness. Corresponde a examples/36-sign-pades-b-b-and-b-t.php. El programa construye un documento, lo configura para una firma PAdES y luego lo firma en B-B y otra vez en B-T con un cliente TSA. En producción, el TsaClient apunta a un endpoint RFC 3161 real sobre un cliente PSR-18 reforzado: un cliente HTTP orientado a la seguridad que fija la SPKI del TSA y resuelve el DNS de forma segura. Para mantener este programa sin conexión y determinista, se inyecta el cliente TSA falso incluido en el soporte de pruebas del repositorio. El cliente TSA falso devuelve un TimeStampResp de RFC 3161 estructuralmente válido.
<?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 esperado (los tamaños varían según el certificado y el token del TSA):
PAdES B-B CMS: <n> bytes, timestamp=noPAdES B-T CMS: <n> bytes, timestamp=yes (<m>-byte RFC 3161 token)B-T = B-B + one RFC 3161 signature-time-stamp (unsigned attribute).Salvedad U-1 (situada junto a la afirmación de B-T en producción). NextPDF no declara contar con ninguna certificación independiente según ETSI EN 319 142-1 para PAdES B-T. EN 319 142-1 no está en el corpus de verificación; el requisito de B-T para
signature-time-stampse verificó frente a ETSI EN 319 122-1 §5.3 junto con RFC 3161, RFC 5652, RFC 5816 e ISO 32000-2 §12.8. La compatibilidad con el perfil B-T no es una certificación de conformidad ni de validez legal; esa determinación corresponde a un validador independiente.
Casos límite y trampas
Sección titulada «Casos límite y trampas»- B-T sin un cliente TSA. Construir un
DigitalSignerB-T sinTsaClientlanzaSignatureException(el TSA es obligatorio para B-T). Validar la configuración del TSA antes de firmar. - Accesibilidad del TSA. B-T realiza un intercambio RFC 3161 de ida y vuelta en directo por firma. Una interrupción del TSA impide obtener una firma B-T. Usar un disyuntor y un SLA de TSA acorde con el rendimiento esperado; el
TsaClientacepta un disyuntor. - Refuerzo del cliente HTTP del TSA. Configurar el
TsaClientcon un cliente PSR-18 que fije la SPKI del TSA (formato RFC 7469) y resuelva el DNS de forma segura;TsaClient::extractPublicKeyPin()deriva el pin del certificado del TSA. - B-T no es B-LT/B-LTA. Una marca de tiempo de firma no incorpora material de validación (certificados, OCSP, CRL) ni una marca de tiempo de archivo. Esos son los niveles B-LT/B-LTA y esta receta no los produce.
- Conflicto de linealización.
enableLinearization()y una firma configurada son mutuamente excluyentes — cualquiera de las dos llamadas lanzaInvalidConfigExceptioncuando la otra ya está establecida. - Claves de HSM. Construir
CertificateInfoconCertificateInfo::fromHsm()para una clave alojada en hardware; la clave privada nunca entra en la memoria del proceso. El contrato del firmante PKCS#11 pertenece a Core; un proveedor funcional forma parte de Premium.
Rendimiento
Sección titulada «Rendimiento»Una firma B-B es una operación CMS local. B-T añade un intercambio HTTP RFC 3161 síncrono de ida y vuelta con el TSA por firma. Prever la latencia del TSA y los límites de tasa en cargas de trabajo por lotes. Preferir un TsaClient protegido por un disyuntor.
Notas de seguridad
Sección titulada «Notas de seguridad»Una firma producida no es automáticamente una firma de confianza. La verificación de una firma depende del certificado, de su ancla de confianza y de la política del verificador, que residen fuera de esta biblioteca. El cifrado aporta confidencialidad, no integridad; la firma aporta integridad/autenticidad, no confidencialidad. Tratar la custodia de claves como el riesgo principal: una clave de software en la memoria del proceso es tan segura como lo sea el host.
Residencia de datos y mitigaciones de PII
Sección titulada «Residencia de datos y mitigaciones de PII»La operación de firma se ejecuta en el proceso; los bytes del documento y la clave privada no salen del host salvo por el intercambio de ida y vuelta con el TSA en B-T, que envía solo la huella del mensaje (un hash del valor de la firma), nunca el contenido del documento (RFC 3161 §2.4.1 MessageImprint). No se transmite ningún texto del documento ni PII al TSA. Elegir un TSA cuya jurisdicción coincida con la política de residencia de datos.
Telemetría segura y depuración de registros
Sección titulada «Telemetría segura y depuración de registros»DigitalSigner acepta un logger PSR-3 opcional; registra el algoritmo y el nivel, no el material de clave ni los bytes de la firma. Los parámetros password de CertificateInfo y TsaClient están marcados como #[SensitiveParameter] para que las frases de contraseña se oculten en las trazas de pila. No registrar el SignatureResult::$cmsSignedData ni el $timestampToken.
Modelo de amenazas
Sección titulada «Modelo de amenazas»Se considera: entrada manipulada después de firmar (detectada por el resumen del rango de bytes), compromiso de la clave (fuera del alcance de la biblioteca: la custodia de claves es responsabilidad del integrador), suplantación del TSA (mitigada por la fijación de SPKI en el cliente HTTP del TSA) y degradación entre niveles (el enum de nivel es explícito; el motor no degrada silenciosamente B-T a B-B). No se afirma ni la ausencia de vulnerabilidades ni que cualquier firma resultante sea legalmente válida.
Comportamiento en modo FIPS
Sección titulada «Comportamiento en modo FIPS»OpenSSL proporciona las primitivas de firma. En una compilación de OpenSSL validada para FIPS, las operaciones RSA/ECDSA y SHA-256 se ejecutan a través del proveedor FIPS; NextPDF no declara por sí mismo una validación FIPS. CryptoCapabilities informa de las primitivas disponibles del host; verificar la cadena de proveedores de OpenSSL en el despliegue.
Conformidad
Sección titulada «Conformidad»| Declaración | Especificación | Cláusula | reference_id |
|---|---|---|---|
| El resumen del rango de bytes cubre el archivo y excluye el valor de la firma. | ISO 32000-2 | §12.8.1 | |
Contents contiene SignedData CMS en DER; un Contents de marca de tiempo de documento contiene un TimeStampToken. | ISO 32000-2 | §12.8.1 | |
Contents es una cadena hexadecimal rellenada sobre el resumen del rango de bytes. | ISO 32000-2 | §12.8.1 | |
| La huella del signature-time-stamp es el hash de los octetos del valor de la firma de SignerInfo (sin tag/length ASN.1). | ETSI EN 319 122-1 | §5.3 | |
| El valor del signature-time-stamp es un SignatureTimeStampToken. | ETSI EN 319 122-1 | §6 | |
MessageImprint ::= SEQUENCE { hashAlgorithm, hashedMessage }. | RFC 3161 | §2.4.1 | |
La huella de la marca de tiempo de firma es el hash del campo de firma de SignerInfo; SignatureTimeStampToken ::= TimeStampToken. | RFC 3161 | App. A | |
id-aa-timeStampToken es el OID 1.2.840.113549.1.9.16.2.14. | RFC 3161 | App. A | |
SignerInfo transporta unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL. | RFC 5652 | §5.3 | |
| Los atributos no firmados no están protegidos por la firma; el resumen firmado de B-B permanece sin cambios. | RFC 5652 | §5.4 | |
| RFC 5816 actualiza RFC 3161; ESSCertIDv2 identifica el certificado del TSA sin SHA-1. | RFC 5816 | §1 |
Esta receta describe cómo NextPDF produce una firma B-B y una B-T. No afirma que cualquier firma resultante sea legalmente válida ni que se alcance la conformidad PAdES; esas determinaciones corresponden a un validador independiente.
Contexto comercial
Sección titulada «Contexto comercial»PAdES B-LT y B-LTA (material de validación DSS y el bucle de marca de tiempo de archivo) y la custodia de claves de HSM PKCS#11 se incluyen en las ediciones Pro y Enterprise. Esta receta cubre deliberadamente solo B-B y B-T; los niveles superiores son capacidades distintas, verificadas por separado, y están fuera del alcance aquí.