Bỏ qua để đến nội dung

Ký một PDF bằng PAdES B-B, rồi mở rộng sang PAdES B-T

Dùng công thức này để tạo chữ ký Portable Document Format (PDF) Advanced Electronic Signatures (PAdES) B-B: một Cryptographic Message Syntax (CMS) SignedData với các thuộc tính đã ký (content-type, message-digest, signing-time). Sau đó, mở rộng chữ ký đó sang PAdES B-T bằng cách thêm một RFC 3161 signature-time-stamp. B-T là B-B cộng thêm một dấu thời gian duy nhất; nó không phải là một lớp chữ ký riêng. Ranh giới tin cậy được nêu rõ: tạo ra chữ ký khác với việc trình kiểm tra quyết định chữ ký đó có hợp lệ hay không.

Lưu ý U-1. NextPDF không khẳng định có bất kỳ chứng nhận độc lập nào theo ETSI EN 319 142-1 cho PAdES B-T. EN 319 142-1 không nằm trong kho tham chiếu dùng cho kiểm tra; yêu cầu B-T signature-time-stamp đã được xác minh dựa trên ETSI EN 319 122-1 §5.3 cùng với RFC 3161, RFC 5652, RFC 5816 và ISO 32000-2 §12.8. Việc hỗ trợ hồ sơ B-T không phải là chứng nhận tuân thủ hay chứng nhận về giá trị pháp lý; quyết định đó thuộc về một trình xác thực độc lập.

B-LT và B-LTA (vật liệu xác thực trong Document Security Store (DSS), vòng lặp archival-timestamp) nằm ngoài phạm vi công thức này và không thuộc bề mặt ký Core/Pro được đề cập ở đây.

Terminal window
composer require nextpdf/core:^3

ext-openssl phải được bật vì CertificateInfo phân tích khóa bằng OpenSSL. B-T cũng cần một điểm cuối RFC 3161 Time Stamping Authority (TSA) có thể truy cập được và một HTTP client PHP Standards Recommendation (PSR)-18.

Chữ ký PAdES B-B lưu một CMS SignedData được mã hóa theo Distinguished Encoding Rules (DER) trong mục Contents của từ điển chữ ký; giá trị Contents là chuỗi thập lục phân được đệm trên byte-range digest (ISO 32000-2 §12.8.1). Digest bao phủ toàn bộ tệp và loại trừ chính giá trị chữ ký (ISO 32000-2 §12.8.1).

PAdES B-T thêm đúng một RFC 3161 signature-time-stamp. Message imprint của dấu thời gian là hàm băm của các octet thuộc giá trị chữ ký trong SignerInfo, không kèm tiền tố tag hay length của Abstract Syntax Notation One (ASN.1) (ETSI EN 319 122-1 §5.3; RFC 3161 Appendix A). Token được mang trong thuộc tính chưa ký id-aa-timeStampToken, object identifier (OID) 1.2.840.113549.1.9.16.2.14 (RFC 3161 Appendix A), và được đặt trong SignerInfo.unsignedAttrs [1] IMPLICIT (RFC 5652 §5.3). Vì các thuộc tính chưa ký không được chữ ký bảo vệ (RFC 5652 §5.4), digest đã ký B-B, /ByteRange, và các byte chữ ký B-B đều không thay đổi — B-T chỉ nối thêm dấu thời gian. Chứng chỉ TSA được nhận diện bằng ESSCertIDv2 (RFC 5816 cập nhật RFC 3161).

Lưu ý U-1 (nhắc lại tại tuyên bố B-T). NextPDF không khẳng định có bất kỳ chứng nhận độc lập nào theo ETSI EN 319 142-1 cho PAdES B-T. EN 319 142-1 không nằm trong kho tham chiếu dùng cho kiểm tra; yêu cầu B-T signature-time-stamp đã được xác minh dựa trên ETSI EN 319 122-1 §5.3 cùng với RFC 3161, RFC 5652, RFC 5816, và ISO 32000-2 §12.8. Việc hỗ trợ hồ sơ B-T không phải là chứng nhận tuân thủ hay chứng nhận về giá trị pháp lý; quyết định đó thuộc về một trình xác thực độc lập.

SignatureLevel::PAdES_B_T có sẵn trong Core: SignatureLevel::PAdES_B_T->requiresTimestamp()true, ->isAvailableInEnvironment()true, và ->requiresDss()false — B-T không kéo theo Document Security Store. B-T ≠ B-LT ≠ B-LTA: một dấu thời gian chữ ký không bổ sung vật liệu xác thực hay dấu thời gian lưu trữ; đó là những cấp cao hơn, riêng biệt và không được tạo ra ở đây.

Sơ đồ bên dưới cho thấy luồng B-B rồi B-T theo đúng thứ tự mà engine sử dụng. ByteRange chỉ được tính sau khi toàn bộ tệp đã được ghi, nên các offset cuối cùng không thể làm thay đổi những byte được băm. Sau đó, B-T nối thêm một token RFC 3161 dưới dạng thuộc tính chưa ký, đồng thời giữ nguyên digest đã ký B-B.

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

Điểm vào để cấu hình là Document::setSignature(CertificateInfo $certInfo, SignatureLevel $level = SignatureLevel::PAdES_B_B, ?TsaClient $tsaClient = null). Lệnh gọi này ghi nhận ý định ký tài liệu. Engine ký Core PAdES (NextPDF\Security\Signature\DigitalSigner) tạo ra chữ ký mật mã. Vì bộ kiểm thử tích hợp chạy engine này và ví dụ chạy được cũng gọi engine trực tiếp, đầu ra là một đối tượng CMS thực, có thể phân tích được. SignatureLevel::PAdES_B_T yêu cầu một TsaClient không phải null; việc dựng signer B-T khi thiếu nó sẽ ném ra SignatureException.

API cấp cao — một lệnh gọi, đầu ra đã ký

Phần tiêu đề “API cấp cao — một lệnh gọi, đầu ra đã ký”

Cách nhanh nhất là dùng API cấp cao: cấu hình chữ ký trên tài liệu rồi serialize. Bên dưới, nó chạy chính engine Core PAdES (DigitalSigner). Đây là lớp tiện ích mỏng bao quanh phần hướng dẫn cấp thấp hơn bên dưới, không phải một đường mã riêng.

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

Giống như output()getPdfData(), save() ghi mục /Contents dưới dạng một CMS SignedData được mã hóa DER với SubFilter ETSI.CAdES.detached (ISO 32000-2 §12.8, §12.7.5.5; RFC 5652). Đầu ra có thể được kiểm tra bằng CMS — một đối tượng CMS SignedData đúng định dạng mà bộ phân tích CMS có thể đọc — nhưng điều này không đồng nghĩa với sự tuân thủ hồ sơ cơ sở ETSI EN 319 142-1 hay giá trị pháp lý; một trình xác thực độc lập đưa ra những quyết định đó (xem lưu ý U-1 ở trên). Đối với B-T, lệnh gọi cấp cao thêm đúng một RFC 3161 signature-time-stamp được mô tả trong phần tổng quan khái niệm; việc truyền vào TsaClient là điểm khác biệt duy nhất so với B-B.

Dùng phần hướng dẫn cấp thấp hơn về DigitalSigner bên dưới khi bạn cần kiểm soát trực tiếp thuật toán, dữ liệu byte-range, hay 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";

Chương trình khép kín này chạy trong bộ khung cookbook. Nó được mô phỏng theo examples/36-sign-pades-b-b-and-b-t.php. Chương trình dựng một tài liệu, cấu hình tài liệu đó cho chữ ký PAdES, rồi ký ở B-B và ký lại ở B-T bằng một TSA client. Trong môi trường sản xuất, hãy trỏ TsaClient tới một điểm cuối RFC 3161 thực thông qua một PSR-18 client đã được tăng cường bảo mật: một HTTP client chú trọng bảo mật, ghim TSA SubjectPublicKeyInfo (SPKI) và phân giải Domain Name System (DNS) an toàn. Để giữ chương trình này ngoại tuyến và tất định, nó đưa vào TSA client giả thuộc phần test-support của kho lưu trữ. TSA client giả trả về một RFC 3161 TimeStampResp hợp lệ về mặt cấu trúc.

<?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 dự kiến (kích thước thay đổi tùy chứng chỉ và token TSA):

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

Lưu ý U-1 (đặt cùng vị trí tại tuyên bố sản xuất B-T). NextPDF không khẳng định có bất kỳ chứng nhận độc lập nào theo ETSI EN 319 142-1 cho PAdES B-T. EN 319 142-1 không nằm trong kho tham chiếu dùng cho kiểm tra; yêu cầu B-T signature-time-stamp đã được xác minh dựa trên ETSI EN 319 122-1 §5.3 cùng với RFC 3161, RFC 5652, RFC 5816, và ISO 32000-2 §12.8. Việc hỗ trợ hồ sơ B-T không phải là chứng nhận tuân thủ hay chứng nhận về giá trị pháp lý; quyết định đó thuộc về một trình xác thực độc lập.

  • B-T mà không có TSA client. Việc dựng một B-T DigitalSigner mà không có TsaClient sẽ ném ra SignatureException (TSA là bắt buộc đối với B-T). Hãy bảo vệ cấu hình TSA trước khi ký.
  • Khả năng truy cập TSA. B-T thực hiện một lượt trao đổi RFC 3161 trực tiếp cho mỗi chữ ký. Khi TSA ngừng hoạt động, chữ ký B-T sẽ không được tạo. Hãy dùng một bộ ngắt mạch và một thỏa thuận mức dịch vụ (SLA) TSA phù hợp với lưu lượng của bạn; TsaClient chấp nhận một bộ ngắt mạch.
  • Tăng cường HTTP client TSA. Hãy trỏ TsaClient tới một PSR-18 client ghim SubjectPublicKeyInfo (SPKI, định dạng RFC 7469) của TSA và phân giải Domain Name System (DNS) an toàn; TsaClient::extractPublicKeyPin() suy ra pin từ chứng chỉ TSA.
  • B-T không phải là B-LT/B-LTA. Một dấu thời gian chữ ký không nhúng vật liệu xác thực (chứng chỉ, Online Certificate Status Protocol (OCSP), danh sách thu hồi chứng chỉ (CRL)) hay dấu thời gian lưu trữ. Đó là các cấp B-LT/B-LTA và không được tạo ra bởi công thức này.
  • Xung đột tuyến tính hóa. enableLinearization() và một chữ ký đã cấu hình loại trừ lẫn nhau — bất kỳ lệnh gọi nào cũng ném ra InvalidConfigException khi cái kia đã được đặt.
  • Khóa HSM. Với khóa được giữ trong hardware security module (HSM), hãy dựng CertificateInfo bằng CertificateInfo::fromHsm(); khóa riêng tư không bao giờ đi vào bộ nhớ tiến trình. Hợp đồng signer PKCS#11 thuộc Core; provider hoạt động được là Premium.

Chữ ký B-B là một thao tác CMS cục bộ. B-T thêm một lượt trao đổi HTTP RFC 3161 đồng bộ tới TSA cho mỗi chữ ký. Hãy tính đến độ trễ TSA và giới hạn tốc độ trong các tải hàng loạt. Hãy dùng một TsaClient được bảo vệ bằng bộ ngắt mạch.

Chữ ký được tạo ra không mặc nhiên là chữ ký đáng tin cậy. Việc chữ ký có xác minh được hay không phụ thuộc vào chứng chỉ, neo tin cậy của nó, và chính sách của trình kiểm tra, những yếu tố nằm ngoài thư viện này. Mã hóa bảo vệ tính bí mật, không phải tính toàn vẹn; việc ký bảo vệ tính toàn vẹn và tính xác thực, không phải tính bí mật. Hãy coi việc lưu giữ khóa là rủi ro chính: khóa phần mềm trong bộ nhớ tiến trình chỉ an toàn ngang với máy chủ.

Thao tác ký chạy trong tiến trình; các byte tài liệu và khóa riêng tư không rời khỏi máy chủ, ngoại trừ lượt trao đổi TSA của B-T vốn chỉ gửi đi message imprint (một hàm băm của giá trị chữ ký), không bao giờ gửi nội dung tài liệu (RFC 3161 §2.4.1 MessageImprint). Không có văn bản tài liệu hay thông tin nhận dạng cá nhân (PII) nào được truyền tới TSA. Hãy chọn một TSA có thẩm quyền pháp lý phù hợp với chính sách lưu trú dữ liệu của bạn.

DigitalSigner chấp nhận một logger PSR-3 tùy chọn. Logger ghi lại thuật toán và cấp độ, không ghi vật liệu khóa hay các byte chữ ký. Các tham số password trên CertificateInfoTsaClient được đánh dấu #[SensitiveParameter], nên các cụm mật khẩu được lược bỏ khỏi stack trace. Đừng ghi nhật ký SignatureResult::$cmsSignedData hay $timestampToken.

Đã xem xét: đầu vào bị giả mạo sau khi ký (được phát hiện nhờ byte-range digest), khóa bị xâm phạm (ngoài phạm vi thư viện vì việc lưu giữ khóa là trách nhiệm của người tích hợp), giả mạo TSA (được giảm thiểu bằng việc ghim SPKI trên HTTP client TSA), và hạ cấp giữa các cấp độ (enum cấp độ là rõ ràng; engine không âm thầm hạ cấp B-T xuống B-B). Không khẳng định: không có lỗ hổng, hay bất kỳ chữ ký nào được tạo ra đều hợp lệ về mặt pháp lý.

Các nguyên thủy ký do OpenSSL cung cấp. Trên một bản dựng OpenSSL được kiểm định Federal Information Processing Standards (FIPS), các thao tác RSA/ECDSA và SHA-256 chạy qua FIPS provider; NextPDF không tự khẳng định việc kiểm định FIPS. CryptoCapabilities báo cáo các nguyên thủy có sẵn của máy chủ; hãy xác minh chuỗi provider OpenSSL trong triển khai của bạn.

Phát biểuĐặc tảĐiều khoảnreference_id
Byte-range digest bao phủ tệp và loại trừ giá trị chữ ký.ISO 32000-2§12.8.1
Contents chứa DER CMS SignedData; một Contents của document-timestamp chứa một TimeStampToken.ISO 32000-2§12.8.1
Contents là một chuỗi thập lục phân được đệm trên byte-range digest.ISO 32000-2§12.8.1
Imprint của signature-time-stamp là hàm băm của các octet giá trị chữ ký SignerInfo (không có tag/length của ASN.1).ETSI EN 319 122-1§5.3
Giá trị signature-time-stamp là một SignatureTimeStampToken.ETSI EN 319 122-1§6
MessageImprint ::= SEQUENCE { hashAlgorithm, hashedMessage }.RFC 3161§2.4.1
Imprint của dấu thời gian chữ ký là hàm băm của trường chữ ký SignerInfo; SignatureTimeStampToken ::= TimeStampToken.RFC 3161Phụ lục A
id-aa-timeStampToken là OID 1.2.840.113549.1.9.16.2.14.RFC 3161Phụ lục A
SignerInfo mang unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL.RFC 5652§5.3
Các thuộc tính chưa ký không được chữ ký bảo vệ; digest đã ký B-B không thay đổi.RFC 5652§5.4
RFC 5816 cập nhật RFC 3161; ESSCertIDv2 nhận diện chứng chỉ TSA mà không dùng SHA-1.RFC 5816§1

Công thức này mô tả cách NextPDF tạo chữ ký B-B và chữ ký B-T. Nó không khẳng định rằng bất kỳ chữ ký nào được tạo ra đều hợp lệ về mặt pháp lý hay đáp ứng tuân thủ PAdES; một trình xác thực độc lập đưa ra những quyết định đó.

PAdES B-LT và B-LTA (vật liệu xác thực DSS và vòng lặp archival-timestamp) cùng việc lưu giữ khóa PKCS#11 HSM được cung cấp trong các phiên bản Pro và Enterprise. Công thức này chỉ đề cập B-B và B-T; các cấp cao hơn là những khả năng riêng biệt, được xác minh riêng và nằm ngoài phạm vi ở đây.