Zum Inhalt springen

Ein PDF mit PAdES B-B signieren und anschließend auf PAdES B-T erweitern

Dieses Recipe erzeugt eine PAdES-Signatur des Profils B-B — ein CMS-SignedData mit den signierten Attributen (content-type, message-digest, signing-time). Anschließend erweitert es diese Signatur zu PAdES B-T, indem es einen signature-time-stamp nach RFC 3161 hinzufügt. B-T ist B-B plus ein einzelner Zeitstempel; es ist keine eigene Signaturklasse. Außerdem benennt das Recipe die Vertrauensgrenze: Eine Signatur zu erzeugen, ist nicht dasselbe wie deren Gültigerklärung durch einen Prüfer.

U-1-Hinweis. NextPDF erhebt keinerlei eigenständige ETSI EN 319 142-1 Zertifizierung für PAdES B-T. EN 319 142-1 ist nicht Teil des Prüfkorpus; die Anforderung an den B-T-signature-time-stamp wurde anhand von ETSI EN 319 122-1 §5.3 zusammen mit RFC 3161, RFC 5652, RFC 5816 und ISO 32000-2 §12.8. Die Unterstützung des B-T-Profils ist keine Konformitäts- oder Rechtsgültigkeitszertifizierung; diese Feststellung trifft ein unabhängiger Validator.

B-LT und B-LTA (DSS-Validierungsmaterial, Archiv-Zeitstempelschleife) liegen außerhalb des Geltungsbereichs dieses Recipe und sind nicht Teil der hier behandelten Core-/Pro-Signaturfläche.

Terminal-Fenster
composer require nextpdf/core:^3

ext-openssl muss aktiviert sein — CertificateInfo parst Schlüssel über OpenSSL. B-T benötigt zusätzlich einen erreichbaren TSA-Endpunkt nach RFC 3161 und einen PSR-18-HTTP-Client für den Zugriff darauf.

Eine PAdES B-B-Signatur speichert ein DER-kodiertes CMS-SignedData im Eintrag Contents des Signaturwörterbuchs; der Wert von Contents ist eine hexadezimale Zeichenkette, die über den Byte-Range-Digest aufgefüllt ist (ISO 32000-2 §12.8.1). Der Digest deckt die Datei ab und schließt den Signaturwert selbst aus (ISO 32000-2 §12.8.1).

PAdES B-T fügt genau einen signature-time-stamp nach RFC 3161 hinzu. Der Message Imprint des Zeitstempels ist der Hash der Signaturwert-Oktette von SignerInfo — ohne ASN.1-Tag oder Längenpräfix (ETSI EN 319 122-1 §5.3; RFC 3161 Anhang A). Das Token wird als unsigniertes Attribut id-aa-timeStampToken geführt, OID 1.2.840.113549.1.9.16.2.14 (RFC 3161 Anhang A), und in SignerInfo.unsignedAttrs [1] IMPLICIT platziert (RFC 5652 §5.3). Da unsignierte Attribute nicht von der Signatur geschützt sind (RFC 5652 §5.4), bleiben der signierte B-B-Digest, der /ByteRange und die B-B-Signaturbytes unverändert — B-T fügt lediglich den Zeitstempel an. Das TSA-Zertifikat wird mit ESSCertIDv2 identifiziert (RFC 5816 aktualisiert RFC 3161).

U-1-Hinweis (wiederholt bei der B-T-Aussage). NextPDF erhebt keinerlei eigenständige ETSI EN 319 142-1-Zertifizierung für PAdES B-T. EN 319 142-1 ist nicht Teil des Prüfkorpus; die Anforderung an den B-T-signature-time-stamp wurde anhand von ETSI EN 319 122-1 §5.3 zusammen mit RFC 3161, RFC 5652, RFC 5816 und ISO 32000-2 §12.8. Die Unterstützung des B-T-Profils ist keine Konformitäts- oder Rechtsgültigkeitszertifizierung; ein unabhängiger Validator trifft diese Feststellung.

SignatureLevel::PAdES_B_T ist eine Core-Fähigkeit: SignatureLevel::PAdES_B_T->requiresTimestamp() ist true, ->isAvailableInEnvironment() ist true und ->requiresDss() ist false — B-T zieht keinen Document Security Store nach sich. B-T ≠ B-LT ≠ B-LTA: ein Signatur-Zeitstempel fügt weder Validierungsmaterial noch einen Archiv-Zeitstempel hinzu; diese gehören zu eigenen, höheren Stufen, die hier nicht erzeugt werden.

Das folgende Diagramm zeigt den Ablauf von B-B zu B-T in der Reihenfolge, die die Engine tatsächlich verwendet. Der ByteRange wird erst berechnet, nachdem die gesamte Datei geschrieben wurde, sodass die echten Offsets die gehashten Bytes nicht verschieben können. B-T fügt anschließend ein Token nach RFC 3161 als unsigniertes Attribut an und lässt den signierten B-B-Digest unberührt.

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

Der Einstiegspunkt für die Konfiguration ist Document::setSignature(CertificateInfo $certInfo, SignatureLevel $level = SignatureLevel::PAdES_B_B, ?TsaClient $tsaClient = null). Diese Methode hält die Signaturabsicht auf dem Dokument fest. Die kryptografische Signatur erzeugt die Core-Signatur-Engine für PAdES (NextPDF\Security\Signature\DigitalSigner). Die Integrationssuite deckt diese Engine ab, und das ausführbare Beispiel steuert sie direkt an, sodass die Ausgabe ein echtes, parsbares CMS-Objekt ist. SignatureLevel::PAdES_B_T erfordert einen vorhandenen TsaClient; einen B-T-Signierer ohne einen solchen zu konstruieren, löst eine SignatureException aus.

Der schnellste Weg führt über die High-Level-Schnittstelle: Konfigurieren Sie die Signatur auf dem Dokument und serialisieren Sie es anschließend. Die Schnittstelle führt im Hintergrund dieselbe Core-PAdES-Engine aus (DigitalSigner) — sie ist ein schlanker Komfortaufsatz über der ausführlicheren Anleitung weiter unten, kein eigener Codepfad.

<?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() (und ebenso output() / getPdfData()) schreibt den Eintrag /Contents als DER-kodiertes CMS-SignedData unter SubFilter ETSI.CAdES.detached (ISO 32000-2 §12.8, §12.7.5.5; RFC 5652). Die Ausgabe ist CMS-verifizierbar — ein wohlgeformtes CMS-SignedData-Objekt, das ein CMS-Parser lesen kann — was nicht dasselbe ist wie Konformität mit dem Baseline-Profil nach ETSI EN 319 142-1 oder Rechtsgültigkeit; diese Feststellungen trifft ein unabhängiger Validator (siehe den U-1-Hinweis oben). Für B-T fügt der High-Level-Aufruf genau den einen RFC 3161 signature-time-stamp hinzu, der im konzeptionellen Überblick beschrieben ist; die Übergabe des TsaClient ist der einzige Unterschied zu B-B.

Nutzen Sie die ausführlichere DigitalSigner-Anleitung weiter unten, wenn Sie explizite Kontrolle über den Algorithmus, die Byte-Range-Daten oder das SignatureResult benötigen.

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

Dies ist das eigenständige, im Harness ausführbare Programm. Es spiegelt examples/36-sign-pades-b-b-and-b-t.php wider. Das Programm baut ein Dokument auf, konfiguriert es für eine PAdES-Signatur und signiert es anschließend als B-B sowie erneut als B-T mit einem TSA-Client. In der Produktion verweist der TsaClient auf einen echten Endpunkt nach RFC 3161 über einen gehärteten PSR-18-Client — einen sicherheitsbewussten HTTP-Client, der die SPKI der TSA pinnt und DNS sicher auflöst. Damit dieses Programm offline und deterministisch bleibt, injiziert es den Test-Support-Fake-TSA-Client des Repositorys. Der Fake-TSA-Client gibt eine strukturell gültige TimeStampResp nach RFC 3161 zurück.

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

Erwartete STDOUT-Ausgabe (Größen variieren mit dem Zertifikat und dem 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-Hinweis (an der B-T-Produktionsaussage verortet). NextPDF erhebt keine eigenständige ETSI EN 319 142-1-Zertifizierung für PAdES B-T. EN 319 142-1 ist nicht Teil des Prüfkorpus; der B-T- signature-time-stamp wurde anhand von ETSI EN 319 122-1 §5.3 zusammen mit RFC 3161, RFC 5652, RFC 5816 und ISO 32000-2 §12.8. Die Unterstützung des B-T-Profils ist keine Konformitäts- oder Rechtsgültigkeitszertifizierung; diese Feststellung trifft ein unabhängiger Validator.

  • B-T ohne TSA-Client. Einen B-T-DigitalSigner ohne TsaClient zu konstruieren wirft SignatureException (die TSA ist für B-T erforderlich). Sichere die TSA-Konfiguration vor dem Signieren ab.
  • TSA-Erreichbarkeit. B-T führt pro Signatur einen Live-Roundtrip nach RFC 3161 aus. Ein TSA-Ausfall bedeutet keine B-T-Signatur. Verwende einen Circuit Breaker und ein TSA-SLA, das zu deinem Durchsatz passt; der TsaClient akzeptiert einen Circuit Breaker.
  • Den TSA-HTTP-Client härten. Richten Sie den TsaClient auf einen PSR-18-Client aus, der die SPKI der TSA pinnt (RFC 7469-Format) und DNS sicher auflöst; TsaClient::extractPublicKeyPin() leitet den Pin aus dem TSA-Zertifikat ab.
  • B-T ist nicht B-LT/B-LTA. Ein Signatur-Zeitstempel bettet weder Validierungsmaterial (Zertifikate, OCSP, CRL) noch einen Archiv-Zeitstempel ein. Das sind die Stufen B-LT/B-LTA, die dieses Recipe nicht erzeugt.
  • Linearisierungskonflikt. enableLinearization() und eine konfigurierte Signatur schließen sich gegenseitig aus — jeder Aufruf wirft InvalidConfigException, wenn das jeweils andere bereits gesetzt ist.
  • HSM-Schlüssel. Erstellen Sie CertificateInfo mit CertificateInfo::fromHsm() für einen hardwaregebundenen Schlüssel; der private Schlüssel gelangt nie in den Prozessspeicher. Der PKCS#11-Signierer-Vertrag ist Core; ein funktionierender Provider ist Premium.

Eine B-B-Signatur ist eine lokale CMS-Operation. B-T fügt pro Signatur einen synchronen HTTP-Roundtrip nach RFC 3161 zur TSA hinzu. Planen Sie TSA-Latenz und Ratenlimits in Batch-Workloads ein. Bevorzugen Sie einen TsaClient, der durch einen Circuit Breaker abgesichert ist.

Eine erzeugte Signatur ist nicht automatisch vertrauenswürdig. Ob eine Signatur erfolgreich verifiziert wird, hängt vom Zertifikat, seinem Vertrauensanker und der Richtlinie des Prüfers ab — sie alle liegen außerhalb dieser Bibliothek. Verschlüsselung ist Vertraulichkeit, nicht Integrität; Signieren ist integrity/authenticity, nicht Vertraulichkeit. Behandeln Sie die Schlüsselverwahrung als primäres Risiko: Ein Software-Schlüssel im Prozessspeicher ist nur so sicher wie der Host.

Die Signaturoperation läuft im Prozess; die Dokumentbytes und der private Schlüssel verlassen den Host nicht, außer beim B-T-TSA-Roundtrip, der nur den Message Imprint (einen Hash des Signaturwerts) sendet, niemals Dokumentinhalte (RFC 3161 §2.4.1 MessageImprint). Kein Dokumenttext und keine PII werden an die TSA übertragen. Wählen Sie eine TSA, deren Rechtsraum zu Ihrer Datenresidenz-Richtlinie passt.

DigitalSigner akzeptiert einen optionalen PSR-3-Logger; er protokolliert Algorithmus und Stufe, nicht jedoch Schlüsselmaterial oder Signaturbytes. Die password-Parameter an CertificateInfo und TsaClient sind als #[SensitiveParameter] markiert, sodass Passphrasen aus Stack-Traces entfernt werden. Protokollieren Sie nicht SignatureResult::$cmsSignedData oder $timestampToken.

Berücksichtigt: manipulierte Eingaben nach dem Signieren (erkannt durch den Byte-Range-Digest), Schlüsselkompromittierung (außerhalb des Bibliotheksumfangs — die Schlüsselverwahrung liegt in der Verantwortung des Integrators), TSA-Impersonation (gemindert durch SPKI-Pinning am TSA-HTTP-Client) und Downgrade zwischen Stufen (das Stufen-Enum ist explizit; die Engine stuft B-T nicht stillschweigend auf B-B herab). Nicht zugesichert: dass keine Schwachstellen vorhanden sind oder dass eine resultierende Signatur rechtsgültig ist.

Die Signaturprimitiven werden von OpenSSL bereitgestellt. Auf einem FIPS-validierten OpenSSL-Build laufen die RSA-/ECDSA- und SHA-256-Operationen über den FIPS-Provider; NextPDF selbst sichert keine FIPS-Validierung zu. CryptoCapabilities meldet die auf dem Host verfügbaren Primitiven; prüfen Sie die OpenSSL-Provider-Kette in Ihrem Deployment.

AussageSpezifikationKlauselreference_id
Der Byte-Range-Digest deckt die Datei ab und schließt den Signaturwert aus.ISO 32000-2§12.8.1
Contents enthält DER-CMS-SignedData; ein Contents eines Dokument-Zeitstempels enthält ein TimeStampToken.ISO 32000-2§12.8.1
Contents ist eine hexadezimale Zeichenkette, die über den Byte-Range-Digest aufgefüllt ist.ISO 32000-2§12.8.1
Der Imprint des signature-time-stamp ist der Hash der Signaturwert-Oktette von SignerInfo (kein ASN.1-tag/length).ETSI EN 319 122-1§5.3
Der Wert des signature-time-stamp ist ein SignatureTimeStampToken.ETSI EN 319 122-1§6
MessageImprint ::= SEQUENCE { hashAlgorithm, hashedMessage }.RFC 3161§2.4.1
Der Imprint des Signatur-Zeitstempels ist der Hash des Signaturfelds von SignerInfo; SignatureTimeStampToken ::= TimeStampToken.RFC 3161Anh. A
id-aa-timeStampToken-OID ist 1.2.840.113549.1.9.16.2.14.RFC 3161Anh. A
SignerInfo trägt unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL.RFC 5652§5.3
Unsignierte Attribute sind nicht von der Signatur geschützt; der signierte B-B-Digest ist unverändert.RFC 5652§5.4
RFC 5816 aktualisiert RFC 3161; ESSCertIDv2 identifiziert das TSA-Zertifikat ohne SHA-1.RFC 5816§1

Dieses Recipe beschreibt, wie NextPDF eine B-B- und eine B-T-Signatur erzeugt. Es sichert nicht zu, dass eine resultierende Signatur rechtsgültig ist oder dass die PAdES-Konformität erfüllt ist; diese Feststellungen trifft ein unabhängiger Validator.

PAdES B-LT und B-LTA (DSS-Validierungsmaterial und die Archiv-Zeitstempelschleife) sowie die PKCS#11-HSM-Schlüsselverwahrung sind in den Editionen Pro und Enterprise enthalten. Dieses Recipe behandelt bewusst nur B-B und B-T; die höheren Stufen sind eigenständige, separat verifizierte Fähigkeiten und liegen hier außerhalb des Geltungsbereichs.