以 PAdES B-B 簽署 PDF,再擴充成 PAdES B-T
本範例會產生 PAdES B-B 簽章,也就是帶有簽署屬性(content-type、message-digest、signing-time)的 CMS SignedData。接著加入 RFC 3161 signature-time-stamp,將該簽章擴充成 PAdES B-T。B-T 是 B-B 再加上一個時間戳記;它並非獨立的簽章類別。本範例也會說明信任邊界:產生簽章,與驗證端判定它有效,並不是同一回事。
U-1 注意事項。 NextPDF 並未主張自身具備任何獨立的 ETSI EN 319 142-1 PAdES B-T 認證。EN 319 142-1 並不在驗證語料庫之內; B-T 的
signature-time-stamp需求依下列規範驗證: ETSI EN 319 122-1 §5.3,連同 RFC 3161、RFC 5652、RFC 5816,以及 ISO 32000-2 §12.8。支援 B-T 設定檔,並不等於取得符合性或法律效力認證;該判定由獨立驗證器做出。
B-LT 與 B-LTA(DSS 驗證資料、封存時間戳記迴圈)不在本範例範圍內,也不是此處涵蓋的 Core/Pro 簽署介面的一部分。
composer require nextpdf/core:^3ext-openssl 必須啟用;CertificateInfo 會透過 OpenSSL 剖析金鑰。B-T 還需要一個可連線的 RFC 3161 TSA 端點,以及一個用來連到它的 PSR-18 HTTP 用戶端。
概念總覽
標題為「概念總覽」的區段PAdES B-B 簽章會把 DER 編碼的 CMS SignedData 存放在簽章字典的 Contents 項目中;Contents 的值是一個以位元組範圍摘要為基礎、加上填補的十六進位字串(ISO 32000-2 §12.8.1)。該摘要涵蓋整個檔案,但排除簽章值本身(ISO 32000-2 §12.8.1)。
PAdES B-T 只會加入一個 RFC 3161 signature-time-stamp。該時間戳記的訊息印記(message imprint)是 SignerInfo 簽章值八位元組的雜湊——不含任何 ASN.1 標籤或長度前綴(ETSI EN 319 122-1 §5.3;RFC 3161 附錄 A)。該權杖由 id-aa-timeStampToken 未簽署屬性承載,OID 為 1.2.840.113549.1.9.16.2.14(RFC 3161 附錄 A),放在 SignerInfo.unsignedAttrs [1] IMPLICIT 內(RFC 5652 §5.3)。由於未簽署屬性不受簽章保護(RFC 5652 §5.4),B-B 的已簽署摘要、/ByteRange,以及 B-B 簽章位元組都維持不變——B-T 只是附加時間戳記。TSA 憑證以 ESSCertIDv2 識別(RFC 5816 更新了 RFC 3161)。
U-1 注意事項(於 B-T 主張處再次重申)。 NextPDF 並未主張具備任何獨立的 ETSI EN 319 142-1 認證以涵蓋 PAdES B-T。EN 319 142-1 並不在驗證語料庫之內;B-T 的
signature-time-stamp需求是依 ETSI EN 319 122-1 §5.3,連同 RFC 3161、 RFC 5652、RFC 5816 與 ISO 32000-2 §12.8 驗證的。支援 B-T 設定檔並不是一種符合性或法律效力認證;該判定由獨立驗證器做出。
SignatureLevel::PAdES_B_T 是一項 Core 能力:SignatureLevel::PAdES_B_T->requiresTimestamp() 為 true,->isAvailableInEnvironment() 為 true,而 ->requiresDss() 為 false——B-T 不會加入 Document Security Store。B-T ≠ B-LT ≠ B-LTA:簽章時間戳記並不會加入驗證資料或封存時間戳記;那些屬於不同且更高的層級,並不在此處產生。
下方圖表按照引擎實際採用的順序,呈現先 B-B 後 B-T 的流程。ByteRange 是在整個檔案寫出之後才計算,因此填入實際偏移量時,不會讓被雜湊的位元組再發生位移。B-T 接著以未簽署屬性的形式附加一個 RFC 3161 權杖,使 B-B 的已簽署摘要維持不動。
API 介面
標題為「API 介面」的區段設定的進入點是 Document::setSignature(CertificateInfo $certInfo, SignatureLevel $level = SignatureLevel::PAdES_B_B, ?TsaClient $tsaClient = null)。它會把簽署意圖記錄在文件上。Core PAdES 簽署引擎(NextPDF\Security\Signature\DigitalSigner)會產生密碼學簽章。整合測試套件會直接測試這個引擎,而可執行的範例也會直接驅動它,因此輸出是真實、可剖析的 CMS 物件。SignatureLevel::PAdES_B_T 需要非 null 的 TsaClient;若建構 B-T 簽署器時未提供,會擲出 SignatureException。
高階 API——一次呼叫,輸出已簽署的結果
標題為「高階 API——一次呼叫,輸出已簽署的結果」的區段最快的做法是使用高階介面:在文件上設定好簽章,然後序列化。這個介面底層執行的是同一個 Core PAdES 引擎(DigitalSigner)——它只是下方較低階流程上的一層薄便利封裝,並非另一條程式碼路徑。
<?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()(以及同樣的 output() / getPdfData())會把 /Contents 項目寫成 SubFilter ETSI.CAdES.detached 下的 DER 編碼 CMS SignedData(ISO 32000-2 §12.8、§12.7.5.5;RFC 5652)。輸出可用於 CMS 驗證——它是一個格式正確的 CMS SignedData 物件,可由 CMS 剖析器讀取——這並不等同於 ETSI EN 319 142-1 基準設定檔符合性或法律效力;那些判定由獨立驗證器做出(見上方的 U-1 注意事項)。就 B-T 而言,高階呼叫只會加入概念總覽中所述的那一個 RFC 3161 signature-time-stamp;與 B-B 的唯一差異是傳入 TsaClient。
若需要明確掌控演算法、位元組範圍資料,或 SignatureResult,請參考下方較低階的 DigitalSigner 逐步說明。
程式碼範例——快速上手
標題為「程式碼範例——快速上手」的區段<?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";程式碼範例——正式環境
標題為「程式碼範例——正式環境」的區段這是一個自足、可由測試載具執行的程式。它對應 範例 examples/36-sign-pades-b-b-and-b-t.php。這個程式會建構一份文件,把它設定為要做 PAdES 簽章,接著以 B-B 簽署一次,再透過 TSA 用戶端以 B-T 簽署一次。在正式環境中,TsaClient 會透過經過強化的 PSR-18 用戶端指向真實的 RFC 3161 端點——這是一個重視安全的 HTTP 用戶端,會固定(pin)TSA SPKI,並以安全方式 resolve(解析)DNS。為了讓這個程式維持離線且具決定性,它會注入儲存庫測試支援用的假 TSA 用戶端。這個假 TSA 用戶端會回傳結構上有效的 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);預期的 STDOUT(大小會隨憑證與 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).U-1 注意事項(與 B-T 正式環境主張並列)。 NextPDF 並未主張具備任何獨立的 ETSI EN 319 142-1 認證以涵蓋 PAdES B-T。 EN 319 142-1 並不在驗證語料庫之內;B-T 的
signature-time-stamp需求是依 ETSI EN 319 122-1 §5.3,連同 RFC 3161、RFC 5652、RFC 5816 與 ISO 32000-2 §12.8 驗證的。 支援 B-T 設定檔並不是一種符合性或法律效力認證;該判定由獨立驗證器做出。
邊界情況與陷阱
標題為「邊界情況與陷阱」的區段- 沒有 TSA 用戶端的 B-T。 建構 B-T
DigitalSigner但未提供TsaClient時,會擲出SignatureException(B-T 必須要有 TSA)。請在簽署前確認 TSA 組態。 - TSA 可連線性。 B-T 每次簽署都會執行一次即時的 RFC 3161 往返。TSA 中斷時,就無法產生 B-T 簽章。請使用斷路器(circuit breaker),並採用符合你吞吐量的 TSA SLA;
TsaClient可接受斷路器。 - 強化 TSA HTTP 用戶端。 請將
TsaClient指向會對 TSA 的 SPKI 做固定(RFC 7469 格式)並安全解析 DNS 的 PSR-18 用戶端;TsaClient::extractPublicKeyPin()會從 TSA 憑證推導出該固定值。 - B-T 不是 B-LT/B-LTA。 簽章時間戳記並不會嵌入驗證資料(憑證、OCSP、CRL)或封存時間戳記。那些屬於 B-LT/B-LTA 層級,並不由本範例產生。
- 線性化衝突。
enableLinearization()與已設定的簽章互斥——當其中一方已設定時,呼叫另一方會擲出InvalidConfigException。 - HSM 金鑰。 對於由硬體保管的金鑰,請以
CertificateInfo搭配CertificateInfo::fromHsm()來建構;私鑰永遠不會進入行程記憶體。PKCS#11 簽署器合約屬於 Core;可用的提供者屬於 Premium。
B-B 簽章是一項本機的 CMS 運算。B-T 每次簽署都會對 TSA 額外執行一次同步的 RFC 3161 HTTP 往返。在批次工作負載中,請把 TSA 延遲與速率限制納入預算考量。建議採用以斷路器保護的 TsaClient。
安全性備註
標題為「安全性備註」的區段已產生的簽章,並不等於受信任的簽章。簽章是否能通過驗證,取決於憑證、其信任錨點,以及驗證端的政策——這些都存在於本函式庫之外。加密提供機密性,而非完整性;簽署提供 integrity/authenticity,而非機密性。請把金鑰保管視為首要風險:行程記憶體中的軟體金鑰,其安全程度最多等同於主機本身。
資料落地與 PII 緩解
標題為「資料落地與 PII 緩解」的區段簽署運算在行程內進行;文件位元組與私鑰都不會離開主機。B-T 的 TSA 往返只會送出訊息印記(簽章值的雜湊),絕不會送出文件內容(RFC 3161 §2.4.1 MessageImprint)。不會有任何文件文字或 PII 傳輸到 TSA。請選擇管轄區符合你資料落地政策的 TSA。
安全的遙測與日誌清理
標題為「安全的遙測與日誌清理」的區段DigitalSigner 接受一個選用的 PSR-3 記錄器;它只記錄演算法與層級,不記錄金鑰素材或簽章位元組。password 參數——位於 CertificateInfo 與 TsaClient 上——標註了 #[SensitiveParameter],因此通關密語會在堆疊追蹤中被遮蔽。請勿記錄 SignatureResult::$cmsSignedData 或 $timestampToken。
威脅模型
標題為「威脅模型」的區段已納入考量:簽署後遭竄改的輸入(由位元組範圍摘要偵測)、金鑰外洩(不在函式庫範圍內——金鑰保管是整合者的責任)、TSA 假冒(透過 TSA HTTP 用戶端上的 SPKI 固定緩解),以及層級間的降級(層級列舉是明確的;引擎不會悄悄將 B-T 降級為 B-B)。未予主張:不存在漏洞,或任何產生的簽章具有法律效力。
FIPS 模式行為
標題為「FIPS 模式行為」的區段簽署的基礎元件由 OpenSSL 提供。在經過 FIPS 驗證的 OpenSSL 組建上,RSA/ECDSA 與 SHA-256 運算會透過 FIPS 提供者執行;NextPDF 本身並未主張具備 FIPS 驗證。CryptoCapabilities 會回報主機可用的基礎元件;請在你的部署環境中驗證 OpenSSL 提供者鏈。
符合性
標題為「符合性」的區段| 陳述 | 規範 | 條款 | 參考 ID |
|---|---|---|---|
| 位元組範圍摘要涵蓋整個檔案,並排除簽章值。 | ISO 32000-2 | §12.8.1 | |
Contents 存放 DER CMS SignedData;文件時間戳記的 Contents 則存放一個 TimeStampToken。 | ISO 32000-2 | §12.8.1 | |
Contents 是一個以位元組範圍摘要為基礎、加上填補的十六進位字串。 | ISO 32000-2 | §12.8.1 | |
| signature-time-stamp 印記是 SignerInfo 簽章值八位元組的雜湊(不含 ASN.1 tag/length)。 | ETSI EN 319 122-1 | §5.3 | |
| signature-time-stamp 的值是一個 SignatureTimeStampToken。 | ETSI EN 319 122-1 | §6 | |
MessageImprint ::= SEQUENCE { hashAlgorithm, hashedMessage }。 | RFC 3161 | §2.4.1 | |
簽章時間戳記印記是 SignerInfo 簽章欄位的雜湊;SignatureTimeStampToken ::= TimeStampToken。 | RFC 3161 | 附錄 A | |
id-aa-timeStampToken OID 是 1.2.840.113549.1.9.16.2.14。 | RFC 3161 | 附錄 A | |
SignerInfo 承載 unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL。 | RFC 5652 | §5.3 | |
| 未簽署屬性不受簽章保護;B-B 的已簽署摘要維持不變。 | RFC 5652 | §5.4 | |
| RFC 5816 更新了 RFC 3161;ESSCertIDv2 在不使用 SHA-1 的情況下識別 TSA 憑證。 | RFC 5816 | §1 |
本範例描述 NextPDF 如何產生 B-B 與 B-T 簽章。它並未主張任何產生的簽章具有法律效力,或已符合 PAdES 符合性;那些判定由獨立驗證器做出。
商業情境
標題為「商業情境」的區段PAdES B-LT 與 B-LTA(DSS 驗證資料與封存時間戳記迴圈)以及 PKCS#11 HSM 金鑰保管,皆隨 Pro 與 Enterprise 版本出貨。本範例刻意只涵蓋 B-B 與 B-T;更高的層級屬於各自獨立且分開驗證的能力,不在此處範圍內。