Sign a PDF with PAdES B-B, then extend to PAdES B-T
At a glance
Section titled “At a glance”Use this recipe to produce a Portable Document Format (PDF) Advanced Electronic Signatures (PAdES) B-B signature: a Cryptographic Message Syntax (CMS) SignedData with signed attributes (content-type, message-digest, signing-time). Then extend that signature to PAdES B-T by adding one RFC 3161 signature-time-stamp. B-T is B-B plus a single timestamp; it is not a separate signature class. The trust boundary is explicit: producing a signature is not the same as a verifier deciding it is valid.
U-1 caveat. NextPDF does not assert any independent ETSI EN 319 142-1 certification for PAdES B-T. EN 319 142-1 is not in the verification corpus; the B-T
signature-time-stamprequirement was verified against ETSI EN 319 122-1 §5.3 together with RFC 3161, RFC 5652, RFC 5816 and ISO 32000-2 §12.8. Support for the B-T profile is not a conformance or legal-validity certification; an independent validator makes that determination.
B-LT and B-LTA (Document Security Store (DSS) validation material, archival-timestamp loop) are out of scope for this recipe and are not part of the Core/Pro signing surface covered here.
Install
Section titled “Install”composer require nextpdf/core:^3ext-openssl must be enabled because CertificateInfo parses keys through
OpenSSL. B-T also needs a reachable RFC 3161 Time Stamping Authority (TSA)
endpoint and a PHP Standards Recommendation (PSR)-18 HTTP client.
Conceptual overview
Section titled “Conceptual overview”A PAdES B-B signature stores a Distinguished Encoding Rules (DER)-encoded CMS
SignedData in the Contents entry of the signature dictionary; the Contents
value is a hexadecimal string padded over the byte-range digest (ISO 32000-2
§12.8.1).
The digest covers the file and excludes the signature value itself (ISO 32000-2
§12.8.1).
PAdES B-T adds exactly one RFC 3161 signature-time-stamp. The timestamp’s
message imprint is the hash of the SignerInfo signature value octets, with
no Abstract Syntax Notation One (ASN.1) tag or length prefix
(ETSI EN 319 122-1 §5.3;
RFC 3161 Appendix A).
The token is carried as the id-aa-timeStampToken unsigned attribute, object
identifier (OID) 1.2.840.113549.1.9.16.2.14 (RFC 3161 Appendix A),
placed in SignerInfo.unsignedAttrs [1] IMPLICIT (RFC 5652 §5.3).
Because unsigned attributes are not protected by the signature (RFC 5652 §5.4),
the B-B signed digest, the /ByteRange, and the B-B signature bytes are
unchanged — B-T only appends the timestamp. The TSA certificate is
identified with ESSCertIDv2 (RFC 5816 updates RFC 3161).
U-1 caveat (restated at the B-T claim). NextPDF does not assert any independent ETSI EN 319 142-1 certification for PAdES B-T. EN 319 142-1 is not in the verification corpus; the B-T
signature-time-stamprequirement was verified against ETSI EN 319 122-1 §5.3 together with RFC 3161, RFC 5652, RFC 5816, and ISO 32000-2 §12.8. Support for the B-T profile is not a conformance or legal-validity certification; an independent validator makes that determination.
SignatureLevel::PAdES_B_T is available in Core:
SignatureLevel::PAdES_B_T->requiresTimestamp() is true,
->isAvailableInEnvironment() is true, and ->requiresDss() is false —
B-T does not pull in a Document Security Store. B-T ≠ B-LT ≠ B-LTA: a
signature timestamp does not add validation material or an archival timestamp;
those are separate, higher levels not produced here.
The diagram below shows the B-B then B-T flow in the order the engine uses. The
ByteRange is computed only after the whole file is written, so the final
offsets cannot change the bytes being hashed. B-T then appends one RFC 3161
token as an unsigned attribute, leaving the B-B signed digest untouched.
API surface
Section titled “API surface”The configuration entry point is
Document::setSignature(CertificateInfo $certInfo, SignatureLevel $level = SignatureLevel::PAdES_B_B, ?TsaClient $tsaClient = null).
This call records the signing intent on the document. The Core PAdES signing engine
(NextPDF\Security\Signature\DigitalSigner) produces the cryptographic
signature. Because the integration suite exercises this engine and the runnable
example drives it directly, the output is a real, parseable CMS object.
SignatureLevel::PAdES_B_T requires a non-null TsaClient; constructing a B-T
signer without one throws a SignatureException.
High-level API — one call, signed output
Section titled “High-level API — one call, signed output”The fastest path is the high-level API: configure the signature on the
document, then serialize. It runs the same Core PAdES engine (DigitalSigner)
under the hood. This is a thin convenience over the lower-level walkthrough
below, not a separate code path.
<?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');Like output() and getPdfData(), save() writes the
/Contents entry as a DER-encoded CMS SignedData under SubFilter
ETSI.CAdES.detached (ISO 32000-2 §12.8, §12.7.5.5; RFC 5652). The output is
CMS-verifiable — a well-formed CMS SignedData object that a CMS parser can read
— which is not the same as ETSI EN 319 142-1 baseline-profile conformance or
legal validity; an independent validator makes those determinations (see the
U-1 caveat above). For B-T, the high-level call adds exactly the single RFC 3161
signature-time-stamp described in the conceptual overview; passing the
TsaClient is the only difference from B-B.
Use the lower-level DigitalSigner walkthrough below when you need direct
control over the algorithm, the byte-range data, or the SignatureResult.
Code sample — Quick start
Section titled “Code sample — Quick start”<?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";Code sample — Production
Section titled “Code sample — Production”This self-contained program runs under the cookbook harness. It mirrors
examples/36-sign-pades-b-b-and-b-t.php.
It builds a document, configures it for a PAdES signature, then signs at B-B
and again at B-T with a TSA client. In production, point the TsaClient at a
real RFC 3161 endpoint over a hardened PSR-18 client: a security-aware HTTP
client that pins the TSA SubjectPublicKeyInfo (SPKI) and resolves Domain Name
System (DNS) safely. To keep this program offline and deterministic, it injects
the repository test-support fake TSA client. The fake TSA client returns a
structurally valid RFC 3161
TimeStampResp.
<?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);Expected STDOUT (sizes vary with the certificate and TSA token):
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).U-1 caveat (co-located at the B-T production claim). NextPDF does not assert any independent ETSI EN 319 142-1 certification for PAdES B-T. EN 319 142-1 is not in the verification corpus; the B-T
signature-time-stamprequirement was verified against ETSI EN 319 122-1 §5.3 together with RFC 3161, RFC 5652, RFC 5816, and ISO 32000-2 §12.8. Support for the B-T profile is not a conformance or legal-validity certification; an independent validator makes that determination.
Edge cases & gotchas
Section titled “Edge cases & gotchas”- B-T without a TSA client. Constructing a B-T
DigitalSignerwith noTsaClientthrowsSignatureException(the TSA is required for B-T). Guard the TSA configuration before you sign. - TSA reachability. B-T performs a live RFC 3161 round-trip per signature.
A TSA outage means no B-T signature. Use a circuit breaker and a TSA
service-level agreement (SLA) appropriate to your throughput; the
TsaClientaccepts a circuit breaker. - Hardening the TSA HTTP client. Point the
TsaClientat a PSR-18 client that pins the TSA’s SubjectPublicKeyInfo (SPKI, RFC 7469 format) and resolves Domain Name System (DNS) safely;TsaClient::extractPublicKeyPin()derives the pin from the TSA certificate. - B-T is not B-LT/B-LTA. A signature timestamp does not embed validation material (certificates, Online Certificate Status Protocol (OCSP), certificate revocation list (CRL)) or an archival timestamp. Those are the B-LT/B-LTA levels and are not produced by this recipe.
- Linearization conflict.
enableLinearization()and a configured signature are mutually exclusive — either call throwsInvalidConfigExceptionwhen the other is already set. - HSM keys. For a hardware security module (HSM)-held key, build
CertificateInfowithCertificateInfo::fromHsm(); the private key never enters process memory. The PKCS#11 signer contract is Core; a working provider is Premium.
Performance
Section titled “Performance”A B-B signature is a local CMS operation. B-T adds one synchronous RFC 3161
HTTP round-trip to the TSA per signature. Budget for TSA latency and rate
limits in batch workloads. Use a circuit-breaker-guarded TsaClient.
Security notes
Section titled “Security notes”A produced signature is not a trusted signature. Whether a signature verifies depends on the certificate, its trust anchor, and the verifier’s policy, which live outside this library. Encryption protects confidentiality, not integrity; signing protects integrity and authenticity, not confidentiality. Treat key custody as the primary risk: a software key in process memory is only as safe as the host.
Data Residency & PII Mitigations
Section titled “Data Residency & PII Mitigations”The signing operation runs in-process; the document bytes and private key do
not leave the host except for the B-T TSA round-trip, which sends only the
message imprint (a hash of the signature value), never document content
(RFC 3161 §2.4.1 MessageImprint).
No document text or personally identifiable information (PII) is transmitted to
the TSA. Choose a TSA whose jurisdiction matches your data-residency policy.
Safe Telemetry & Log Scrubbing
Section titled “Safe Telemetry & Log Scrubbing”DigitalSigner accepts an optional PSR-3 logger. It logs the algorithm and
level, not key material or signature bytes. The password parameters on
CertificateInfo and TsaClient are marked #[SensitiveParameter], so
passphrases are redacted from stack traces. Do not log the
SignatureResult::$cmsSignedData or $timestampToken.
Threat model
Section titled “Threat model”Considered: tampered input after signing (detected by the byte-range digest), key compromise (outside library scope because key custody is the integrator’s responsibility), TSA impersonation (mitigated by SPKI pinning on the TSA HTTP client), and downgrade between levels (the level enum is explicit; the engine does not silently downgrade B-T to B-B). Not asserted: absence of vulnerabilities, or that any resulting signature is legally valid.
FIPS-mode behavior
Section titled “FIPS-mode behavior”The signing primitives are provided by OpenSSL. On a Federal Information
Processing Standards (FIPS)-validated OpenSSL build, the RSA/ECDSA and SHA-256
operations run through the FIPS provider; NextPDF does not itself assert FIPS
validation. CryptoCapabilities reports the host’s available primitives;
verify the OpenSSL provider chain in your deployment.
Conformance
Section titled “Conformance”| Statement | Spec | Clause | reference_id |
|---|---|---|---|
| The byte-range digest covers the file and excludes the signature value. | ISO 32000-2 | §12.8.1 | |
Contents holds DER CMS SignedData; a document-timestamp Contents holds a TimeStampToken. | ISO 32000-2 | §12.8.1 | |
Contents is a hexadecimal string padded over the byte-range digest. | ISO 32000-2 | §12.8.1 | |
| The signature-time-stamp imprint is the hash of the SignerInfo signature value octets (no ASN.1 tag/length). | ETSI EN 319 122-1 | §5.3 | |
| The signature-time-stamp value is a SignatureTimeStampToken. | ETSI EN 319 122-1 | §6 | |
MessageImprint ::= SEQUENCE { hashAlgorithm, hashedMessage }. | RFC 3161 | §2.4.1 | |
The signature timestamp imprint is the hash of the SignerInfo signature field; SignatureTimeStampToken ::= TimeStampToken. | RFC 3161 | App. A | |
id-aa-timeStampToken OID is 1.2.840.113549.1.9.16.2.14. | RFC 3161 | App. A | |
SignerInfo carries unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL. | RFC 5652 | §5.3 | |
| Unsigned attributes are not protected by the signature; the B-B signed digest is unchanged. | RFC 5652 | §5.4 | |
| RFC 5816 updates RFC 3161; ESSCertIDv2 identifies the TSA certificate without SHA-1. | RFC 5816 | §1 |
This recipe describes how NextPDF produces a B-B and a B-T signature. It does not assert that any resulting signature is legally valid or that PAdES conformance is met; an independent validator makes those determinations.
Commercial context
Section titled “Commercial context”PAdES B-LT and B-LTA (DSS validation material and the archival-timestamp loop) and PKCS#11 HSM key custody ship in the Pro and Enterprise editions. This recipe covers B-B and B-T only; the higher levels are distinct, separately-verified capabilities and are out of scope here.