コンテンツにスキップ

PAdES B-B で PDF に署名し、PAdES B-T へ拡張する

このレシピでは PAdES B-B 署名を生成します。これは署名属性(content-type、message-digest、signing-time)を持つ CMS SignedData です。続いて、その署名に RFC 3161 の signature-time-stamp を 1 つ追加して 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 の署名サーフェスにも含まれません。

Terminal window
composer require nextpdf/core:^3

ext-openssl が有効である必要があります。CertificateInfo は OpenSSL 経由で鍵を解析します。B-T ではさらに、到達可能な RFC 3161 TSA エンドポイントと、接続に使用する PSR-18 HTTP クライアントが必要です。

PAdES B-B 署名は、DER エンコードされた CMS SignedData を署名ディクショナリの Contents エントリに格納します。Contents の値は、バイトレンジダイジェストにわたってパディングされた 16 進文字列です(ISO 32000-2 §12.8.1)。このダイジェストはファイルを対象とし、署名値そのものは除外されます(ISO 32000-2 §12.8.1)。

PAdES B-T は、RFC 3161 の signature-time-stamp をちょうど 1 つ追加します。タイムスタンプのメッセージインプリントは、SignerInfo の署名値オクテットのハッシュであり、ASN.1 のタグや長さプレフィックスは含みません(ETSI EN 319 122-1 §5.3、RFC 3161 Appendix A)。このトークンは id-aa-timeStampToken 未署名属性(OID 1.2.840.113549.1.9.16.2.14、RFC 3161 Appendix 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 トークンを 1 つ未署名属性として追加し、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

構成のエントリポイントは 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 をちょうど 1 つだけ追加します。B-B との唯一の違いは TsaClient を渡すことです。

以下の低レベル DigitalSigner ウォークスルーは、アルゴリズム、バイトレンジデータ、または 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 を安全に resolve(解決)するセキュリティ考慮済みの 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=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 に関する注意(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 の DigitalSignerTsaClient なしで構築すると SignatureException がスローされます(TSA は B-T に必須です)。署名前に 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 鍵。 ハードウェア保持の鍵では、CertificateInfoCertificateInfo::fromHsm() で構築してください。秘密鍵がプロセスメモリに入ることはありません。PKCS#11 署名器のインターフェイス契約は Core ですが、動作するプロバイダーは Premium です。

B-B 署名はローカルの CMS 操作です。B-T は署名ごとに TSA への同期的な RFC 3161 HTTP ラウンドトリップを 1 回追加します。バッチワークロードでは TSA のレイテンシーとレート制限を見込んでください。サーキットブレーカーでガードされた TsaClient を推奨します。

生成された署名は、自動的に信頼済み署名になるわけではありません。署名が検証されるかどうかは、証明書、そのトラストアンカー、および検証者のポリシーに依存します。これらはこのライブラリの外部に存在します。暗号化が提供するのは機密性であり、完全性ではありません。署名が提供するのは integrity/authenticity であり、機密性ではありません。鍵の管理を主要なリスクとして扱ってください。プロセスメモリ内のソフトウェア鍵は、ホストと同程度にしか安全ではありません。

署名操作はプロセス内で実行されます。ドキュメントのバイトと秘密鍵は、B-T の TSA ラウンドトリップを除き、ホストから外に出ません。このラウンドトリップはメッセージインプリント(署名値のハッシュ)のみを送信し、ドキュメントの内容を送ることは決してありません(RFC 3161 §2.4.1 MessageImprint)。ドキュメントのテキストや PII が TSA に送信されることはありません。管轄区域がデータレジデンシーポリシーに合致する TSA を選択してください。

DigitalSigner はオプションの PSR-3 ロガーを受け付けます。アルゴリズムとレベルをログに記録しますが、鍵マテリアルや署名バイトは記録しません。password パラメーター(CertificateInfoTsaClient のもの)には #[SensitiveParameter] が付与されているため、パスフレーズはスタックトレースから秘匿されます。SignatureResult::$cmsSignedData$timestampToken はログに記録しないでください。

考慮事項:署名後の入力改ざん(バイトレンジダイジェストで検出)、鍵の漏洩(ライブラリの範囲外。鍵の管理はインテグレーターの責任)、TSA のなりすまし(TSA HTTP クライアントでの SPKI ピン留めにより緩和)、およびレベル間のダウングレード(レベル列挙型は明示的であり、エンジンは B-T を B-B へ暗黙的にダウングレードしません)。主張しない事項:脆弱性が存在しないこと、または生成されたいかなる署名も法的に有効であること。

署名のプリミティブは 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
バイトレンジダイジェストにわたってパディングされた 16 進文字列としての ContentsISO 32000-2§12.8.1
SignerInfo の署名値オクテットのハッシュとしての signature-time-stamp インプリント(ASN.1 の tag/length なし)ETSI EN 319 122-1§5.3
SignatureTimeStampToken としての signature-time-stamp 値ETSI EN 319 122-1§6
MessageImprint ::= SEQUENCE { hashAlgorithm, hashedMessage }RFC 3161§2.4.1
SignerInfo の署名フィールドのハッシュとしての署名タイムスタンプのインプリント。SignatureTimeStampToken ::= TimeStampTokenRFC 3161付録 A
id-aa-timeStampToken の OID 1.2.840.113549.1.9.16.2.14RFC 3161付録 A
unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL を持つ SignerInfoRFC 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 のみを扱います。上位レベルは別個に検証される独立した機能であり、ここでは対象外です。