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 값은 byte-range 다이제스트 위에 패딩된 16진수 문자열입니다(ISO 32000-2 §12.8.1). 다이제스트는 파일을 포괄하고 서명 값 자체는 제외합니다(ISO 32000-2 §12.8.1).
PAdES B-T는 정확히 하나의 RFC 3161 signature-time-stamp를 추가합니다. 타임스탬프의 메시지 임프린트는 SignerInfo 서명 값 옥텟의 해시이며, ASN.1 태그나 길이 접두사가 없습니다(ETSI EN 319 122-1 §5.3; RFC 3161 Appendix A). 토큰은 id-aa-timeStampToken unsigned 속성으로 전달되며, OID는 1.2.840.113549.1.9.16.2.14이고(RFC 3161 Appendix A), SignerInfo.unsignedAttrs [1] IMPLICIT에 배치됩니다(RFC 5652 §5.3). unsigned 속성은 서명으로 보호되지 않으므로(RFC 5652 §5.4), B-B에서 서명된 다이제스트, /ByteRange, 그리고 B-B 서명 바이트는 변경되지 않습니다. B-T는 타임스탬프만 추가합니다. TSA 인증서는 ESSCertIDv2로 식별됩니다(RFC 5816이 RFC 3161을 업데이트함).
U-1 주의 사항(B-T 주장 부분에서 재진술). NextPDF는 어떠한 PAdES B-T에 대한 독립적인 ETSI EN 319 142-1 인증도 주장하지 않습니다. 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 토큰을 unsigned 속성으로 추가하며, 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 파서가 읽을 수 있는 올바른 형식의 CMS SignedData 객체입니다. 이는 ETSI EN 319 142-1 베이스라인 프로파일 적합성이나 법적 유효성과 동일하지 않습니다. 그러한 판단은 독립적인 검증자가 내립니다(위의 U-1 주의 사항 참조). B-T의 경우, 고수준 호출은 개념적 개요에서 설명한 단일 RFC 3161 signature-time-stamp를 정확히 추가합니다. TsaClient를 전달하는 것이 B-B와의 유일한 차이입니다.
아래의 저수준 DigitalSigner 안내는 알고리즘, byte-range 데이터, 또는 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";코드 샘플 — 프로덕션
섹션 제목: “코드 샘플 — 프로덕션”이것은 자체 완결적이고 하니스에서 실행할 수 있는 프로그램입니다. 예제 examples/36-sign-pades-b-b-and-b-t.php를 반영합니다. 이 프로그램은 문서를 빌드하고 PAdES 서명을 위해 구성한 다음, B-B로 서명하고 다시 TSA 클라이언트를 사용하여 B-T로 서명합니다. 프로덕션에서는 TsaClient가 강화된 PSR-18 클라이언트, 즉 TSA SPKI를 핀하고 DNS를 안전하게 해석하는 보안 인식 HTTP 클라이언트를 통해 실제 RFC 3161 엔드포인트를 가리킵니다. 이 프로그램을 오프라인에서 결정적으로 실행되도록 유지하기 위해 저장소의 테스트 지원용 가짜 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는 PAdES B-T에 대한 어떠한 독립적인 ETSI EN 319 142-1 인증도 주장하지 않습니다. 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 서명을 할 수 없음을 의미합니다. 처리량에 맞는 서킷 브레이커와 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은 로깅해서는 안 됩니다.
위협 모델
섹션 제목: “위협 모델”고려한 사항: 서명 후 변조된 입력(byte-range 다이제스트로 탐지), 키 침해(라이브러리 범위 밖 — 키 보관은 통합자의 책임), TSA 사칭(TSA HTTP 클라이언트의 SPKI 핀으로 완화), 그리고 레벨 간 다운그레이드(레벨 enum은 명시적이며, 엔진은 B-T를 B-B로 조용히 다운그레이드하지 않음). 주장하지 않는 사항: 취약점의 부재, 또는 결과로 나온 어떠한 서명도 법적으로 유효하다는 것.
FIPS 모드 동작
섹션 제목: “FIPS 모드 동작”서명 프리미티브는 OpenSSL이 제공합니다. FIPS 검증을 받은 OpenSSL 빌드에서는 RSA/ECDSA 및 SHA-256 작업이 FIPS 프로바이더를 통해 실행됩니다. NextPDF 자체는 FIPS 검증을 주장하지 않습니다. CryptoCapabilities는 호스트에서 사용 가능한 프리미티브를 보고합니다. 배포 환경에서 OpenSSL 프로바이더 체인을 검증해야 합니다.
적합성
섹션 제목: “적합성”| 진술 | 사양 | 조항 | 참조 ID |
|---|---|---|---|
| byte-range 다이제스트는 파일을 포괄하고 서명 값을 제외합니다. | ISO 32000-2 | §12.8.1 | |
Contents는 DER CMS SignedData를 담습니다. 문서 타임스탬프 Contents는 TimeStampToken을 담습니다. | ISO 32000-2 | §12.8.1 | |
Contents는 byte-range 다이제스트 위에 패딩된 16진수 문자열입니다. | 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 | App. A | |
id-aa-timeStampToken OID는 1.2.840.113549.1.9.16.2.14입니다. | RFC 3161 | App. A | |
SignerInfo는 unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL을 전달합니다. | RFC 5652 | §5.3 | |
| unsigned 속성은 서명에 의해 보호되지 않습니다. 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만 다룹니다. 상위 레벨은 별개이며 별도로 검증되는 기능으로, 여기서는 범위를 벗어납니다.