Skip to content

Sign a PDF with PAdES B-B, then extend to PAdES B-T

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-stamp requirement 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.

Terminal window
composer require nextpdf/core:^3

ext-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.

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-stamp requirement 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.

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

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.

<?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";

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=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).

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-stamp requirement 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-T without a TSA client. Constructing a B-T DigitalSigner with no TsaClient throws SignatureException (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 TsaClient accepts a circuit breaker.
  • Hardening the TSA HTTP client. Point the TsaClient at 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 throws InvalidConfigException when the other is already set.
  • HSM keys. For a hardware security module (HSM)-held key, build CertificateInfo with CertificateInfo::fromHsm(); the private key never enters process memory. The PKCS#11 signer contract is Core; a working provider is Premium.

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.

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.

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.

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.

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.

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.

StatementSpecClausereference_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 3161App. A
id-aa-timeStampToken OID is 1.2.840.113549.1.9.16.2.14.RFC 3161App. 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.

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.