コンテンツにスキップ
getnextpdf.com

Enterprise エディション

トラストリスト(TSL)

EU の署名検証は、公表された事実、すなわちどのプロバイダーが適格ステータスを保持しているかから始まります。その事実はトラストリスト(TSL)に存在します。TSL は各加盟国が公表する署名済み XML 文書であり、EU のトラストリストのリスト(LOTL)によってインデックス化されています。NextPDF\Enterprise\Security\Tsl\TslPolicyEnforcer は、TSL の URL または生の XML を、信頼できる TslDocument に変換します。ガード付き HTTPS で取得し、ピン留めしたアンカーに対して XMLDSig 署名を検証し、堅牢化された XML を解析し、期限切れのリストを拒否します。さらに TslTrustAnchorProvider::buildBundle() を呼び出せば、アクティブな CA/QC サービスをバージョン付きのトラストアンカーバンドルに変換できます。すべてのゲートは fail-closed であり、すべての拒否は型付き例外です。

このページはリストの取り込みとアンカーの導出を担当します。証明書パス検証は 署名検証 にあります。eIDAS 保証レベルのマッピングは eIDAS 保証レベル にあります。コンテナのトラストバインディングは ASiC トラストバインディング にあります。

この機能は NextPDF Enterprisenextpdf/enterprise)に同梱され、Enterprise ティアのライセンスエンベロープで有効化されます。その権限のないデプロイメントでは、この機能のクラスはロードされません。エディションを比較してライセンスを取得する

Terminal window
composer require nextpdf/enterprise

有効化には Enterprise ライセンスエンベロープが必要です。インストールと認証 を参照してください。このページのクラスは NextPDF\Enterprise\Security\Tsl の下に存在し、ネットワークポリシー型は NextPDF\Enterprise\Security の下に存在します。オンライン取得にはさらに任意の PSR-18 クライアントと PSR-17 ファクトリ(例: guzzlehttp/guzzle)が必要です。

eIDAS 第 22 条により、各加盟国は自国の適格トラストサービスプロバイダーのトラストリストを公表し、自動処理向けに署名またはシールを施します。ETSI TS 119 612 が XML フォーマットを定義しています。リストは、3 つのチェック、すなわち署名、構造、鮮度と同じ程度にしか信頼できません。NextPDF はそれらを 1 つのパイプラインとして、この順序で実行します。

  1. 取得(Fetch)TslFetcher が XML を HTTPS のみで取得します。SSRF ガードが送信前にホストを検証します。レスポンスにはサイズ上限があり、PSR-16 キャッシュにより ETag 再検証とエアギャップ読み取りが可能になります。
  2. 検証(Verify)TslSignatureVerifier が enveloped な XMLDSig 署名をチェックします。署名証明書は、帯域外でピン留めしたトラストアンカーにチェーンしなければなりません。文書内の情報が単独で信頼されることはありません。
  3. 解析(Parse)TslXmlParser がスキーム情報とすべての TSP サービスを不変の TslDocument に抽出します。DOCTYPE を持つ文書は、エンティティテーブルが構築される前に拒否されます。
  4. 適用(Enforce) — リストの NextUpdate の時点が経過していてはなりません。期限切れのリストは破棄され、消費されることはありません。

TslPolicyEnforcer はこれら 4 つすべてを構成します。そこから得られる TslDocument はすべてのゲートを通過済みです。さらに TslTrustAnchorProvider::buildBundle() が、granted ステータスかつ CA/QC 型の両方に該当するサービスをフィルタリングし、EnterpriseCaTrustAnchorBundle を出力します。これはピン留めされた PEM アンカー、tsl-<territory>-seq<N> のバージョン、そして SHA-256 の完全性ダイジェストを含みます。そのバンドルこそ、パス検証と ASiC トラストバインディングが消費するものです。

同じ仕組みが LOTL のワークフローもカバーします。手動でピン留めしたアンカーに対して LOTL を検証し、次に LOTL が各加盟国に対して宣言している署名証明書に対して各加盟国の TSL を検証します。

要となる決定は、汎用的な XMLDSig ではなく、固定された最小限の検証プロファイルです。柔軟な XML 署名処理、すなわち任意の変換チェーン、攻撃者が宣言する ID 参照、アルゴリズムのアジリティは、検証器が歴史的に破綻してきた箇所です。そこで検証器は正確に 1 つの処理モデルのみを受け入れます。すなわち排他的 C14N、ルートを覆う参照、そして 2 段階の変換パイプライン [enveloped-signature, exclusive-C14N] であり、それ以外はすべて fail-closed で拒否されます。信頼が文書そのものからブートストラップされることはありません。KeyInfo の証明書は、あなたが設定したアンカーにのみチェーンします。鮮度は TslDocument 自体に存在するため、1 つの任意の協調オブジェクトではなく、すべての消費者パスがそれを適用します。結果として、テスト可能で決定論的、かつ拒否する対象について正直な、小さなカーネルが得られます。

設計の背景: 適格署名の解説

オーケストレーションされたエントリーポイント。取得、検証、解析、鮮度チェックをワンコールで行います。

public function __construct(
private readonly TslFetcher $fetcher,
private readonly TslSignatureVerifier $verifier,
private readonly TslXmlParser $parser,
) {}
public function fetchAndVerify(string $url): TslDocument
public function verifyXml(string $xml): TslDocument

スロー/失敗の条件: 取得ステージからの TslFetchException および NextPDF\Enterprise\Security\NetworkPolicyViolation、署名検証からの TslSignatureException、解析・非正規の NextUpdate 値・期限切れリストからの TslParseException。両メソッドは、すべてのゲートを通過した場合にのみ TslDocument を返します。ここでの鮮度ゲートは NextUpdate を現在のシステムクロックと比較します。

ETag ベースのキャッシュとネットワークポリシーゲートを備えた HTTP フェッチャー。

public function __construct(
private readonly ClientInterface $httpClient,
private readonly RequestFactoryInterface $requestFactory,
private readonly ?CacheInterface $cache = null,
private readonly int $defaultTtlSeconds = 3600,
private readonly int $maxBytes = 16_777_216,
private readonly NetworkPolicy $networkPolicy = NetworkPolicy::ONLINE,
) {}
public function fetch(string $url): string

スロー/失敗の条件: 非 HTTPS の URL、拒否された(SSRF)ホスト、HTTP エラーステータス、過大なレスポンス、空のボディの場合は TslFetchExceptionNetworkPolicy::STRICT_OFFLINE が有効でキャッシュされたボディが存在しない場合は NetworkPolicyViolation。キャッシュされたボディは 304 Not Modified の再検証を満たし、STRICT_OFFLINE の下で提供される唯一のボディです。キャッシュエントリは $defaultTtlSeconds の間有効です。

署名済みトラストリスト向けの XMLDSig 検証器。

public function __construct(
private readonly array $trustAnchorsPem,
private readonly int $clockTolerance = 0,
)
public function verify(string $xml): string

verify() は署名証明書の PEM を返し、それが $trustAnchorsPem のいずれかにチェーンすることが証明されます。アンカーリストが空の場合、コンストラクタは InvalidArgumentException をスローします。$clockTolerance は証明書の有効期間ウィンドウを対称的に、秒単位で広げます。

受け入れられるプロファイルは固定です。署名アルゴリズム: ALLOWED_SIG_ALG 許可リスト(rsa-sha256/384/512ecdsa-sha256/384/512)。ダイジェスト: ALLOWED_DIGEST_ALG 許可リスト(SHA-256、SHA-384、SHA-512)。正規化: 排他的 C14N 1.0 のみ。SHA-1 と MD5 は unsupported_algorithm として拒否されます。

スロー/失敗の条件: TslSignatureException。機械可読な reason を伴います。

Reason code意味
missing_signature文書に ds:Signature 要素がない。
untrusted_signerKeyInfo 証明書が設定されたアンカーにチェーンしない。
invalid_signature構造的欠陥、または RSA/ECDSA チェックの失敗。
digest_mismatch参照ダイジェストが正規化された文書と一致しない。
unsupported_algorithm署名またはダイジェストアルゴリズムが許可リスト外。
unsupported_transform正規化または変換パイプラインが固定プロファイル外。
expired_anchorチェーン証明書が有効期間外、またはその有効性が解析不能。

署名に依存しない構造パーサー。呼び出し側はその出力を信頼する前に検証しなければなりません。TslPolicyEnforcer はその順序を代わりに適用します。

public function parse(string $xml): TslDocument

スロー/失敗の条件: XML が DOCTYPE を宣言している(XXE およびエンティティ展開の堅牢化)、解析できない、TrustServiceStatusList ルートを欠く、または無効な TSLSequenceNumber を持つ場合の TslParseException。このクラスは名前空間定数 NS_TSLNS_DSIGNS_TSL_X を公開します。

TslDocument は不変の値オブジェクトです: schemeTerritoryschemeOperatorNametslTypesequenceNumberissueDateTimenextUpdatetspServices、そして rawXmlSha256(生バイト列に対する証跡ハッシュ)。

public function isStale(DateTimeImmutable $now): bool
public function assertFresh(DateTimeImmutable $now): void
public function servicesOfType(string $serviceTypeIdentifier): array
public function activeServices(): array

スロー/失敗の条件: isStale()assertFresh() は、nextUpdate が明示的な Z または数値オフセットを伴う正規の UTC dateTime でない場合に TslParseException をスローします。期限切れのリストは assertFresh() をスローさせます。activeServices() は granted ステータスのサービスのみを返します。servicesOfType() は ETSI サービス型 URI でフィルタリングします。

TspService エントリは tspNameserviceNameserviceTypeIdentifierserviceStatusstatusStartingTimeserviceCertificatePemqualifiersadditionalServiceInformation を公開し、さらに次を備えます。

public function isGranted(): bool
public function isQualifiedCa(): bool

有用な定数: TspService::STATUS_GRANTEDTspService::STATUS_WITHDRAWNTspService::TYPE_CA_QCTspService::TYPE_OCSP_QCTspService::TYPE_TSA_QTST。修飾子 URI(例: TspServiceQualifier::FOR_ESIGFOR_ESEALQSCD_STATEMENTNO_QSCD)は eIDAS マッピングレイヤー向けに TspServiceQualifier に現れます。

public function buildBundle(TslDocument $tsl, DateTimeImmutable $now): EnterpriseCaTrustAnchorBundle

スロー/失敗の条件: TSL が $now で期限切れの場合、nextUpdate が正規の UTC 値でない場合、またはリストにアクティブな CA/QC サービスが含まれない場合の TslParseException

BC ノート — buildBundle($now) の鮮度ルール。 buildBundle() は検証時点を要求し、単一のアンカーを抽出する前に TslDocument::assertFresh($now) を呼び出します。以前のリビジョンでは、鮮度チェックなしにパーサーが生成した TslDocument からアンカーを導出できました。キャッシュまたはアーカイブされたリストを与えていた呼び出し側は、今や検証が実行される時点を渡さなければなりません。その時点で期限切れのリストは、黙ってトラストアンカーをシードするのではなくスローします。

返される EnterpriseCaTrustAnchorBundle は読み取り専用の値オブジェクトです: anchorsPem(PEM アンカー)、bundleVersiontsl-<territory>-seq<N>)、そして bundleSha256(正規化された PEM 連結に対する完全性ダイジェスト)。これは buildBundle() から取得してください。手で構築しないでください。コンストラクタはダイジェスト不一致または不正な PEM に対して InvalidArgumentException をスローします。

public function containsFingerprint(string $anchorDerSha256Hex): bool
public static function computeBundleSha256(array $anchorsPem): string

ローカルにミラーリングしたトラストリストを認証して消費します。このパスに HTTP 依存は不要です。検証し、解析し、次に検証時点で鮮度をゲートします。

tsl-verify-quickstart.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Enterprise\Security\Tsl\TslParseException;
use NextPDF\Enterprise\Security\Tsl\TslSignatureException;
use NextPDF\Enterprise\Security\Tsl\TslSignatureVerifier;
use NextPDF\Enterprise\Security\Tsl\TslXmlParser;
// The list-signing certificate, pinned OUT-OF-BAND. Never take it from the list itself.
$pinnedAnchorPem = (string) file_get_contents(__DIR__ . '/tsl-signer-anchor.pem');
// A trusted-list XML document you mirrored locally.
$tslXml = (string) file_get_contents(__DIR__ . '/member-state-tsl.xml');
try {
// 1. Authenticate: XMLDSig must verify AND the signer must chain to the pinned anchor.
(new TslSignatureVerifier(trustAnchorsPem: [$pinnedAnchorPem]))->verify($tslXml);
// 2. Parse the now-authenticated bytes.
$tsl = (new TslXmlParser())->parse($tslXml);
// 3. Freshness: refuse a list whose NextUpdate has passed.
$tsl->assertFresh(new DateTimeImmutable('now', new DateTimeZone('UTC')));
} catch (TslSignatureException $e) {
fwrite(STDERR, "TSL rejected ({$e->reason}): {$e->getMessage()}" . PHP_EOL);
exit(1);
} catch (TslParseException $e) {
fwrite(STDERR, 'TSL unusable: ' . $e->getMessage() . PHP_EOL);
exit(1);
}
echo "Territory: {$tsl->schemeTerritory}\n";
echo "Sequence: {$tsl->sequenceNumber}\n";
echo 'Active services: ' . count($tsl->activeServices()) . "\n";

想定される出力(値はリストによって異なります):

Territory: DE
Sequence: 127
Active services: 143

オンラインパイプライン全体を配線します。キャッシュ付きのガード付き取得、署名検証、解析、鮮度、そしてアンカーバンドルの導出です。各失敗クラスは個別に捕捉して報告されます。

tsl-anchor-bundle-production.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;
use NextPDF\Enterprise\Security\NetworkPolicy;
use NextPDF\Enterprise\Security\NetworkPolicyViolation;
use NextPDF\Enterprise\Security\Tsl\TslFetchException;
use NextPDF\Enterprise\Security\Tsl\TslFetcher;
use NextPDF\Enterprise\Security\Tsl\TslParseException;
use NextPDF\Enterprise\Security\Tsl\TslPolicyEnforcer;
use NextPDF\Enterprise\Security\Tsl\TslSignatureException;
use NextPDF\Enterprise\Security\Tsl\TslSignatureVerifier;
use NextPDF\Enterprise\Security\Tsl\TslTrustAnchorProvider;
use NextPDF\Enterprise\Security\Tsl\TslXmlParser;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;
// Any PSR-18 client, PSR-17 factory, and PSR-16 cache work; these are examples.
$enforcer = new TslPolicyEnforcer(
fetcher: new TslFetcher(
httpClient: new Client(),
requestFactory: new HttpFactory(),
cache: new Psr16Cache(new FilesystemAdapter('tsl')),
defaultTtlSeconds: 3600,
maxBytes: 16_777_216,
networkPolicy: NetworkPolicy::ONLINE,
),
verifier: new TslSignatureVerifier(
trustAnchorsPem: [(string) file_get_contents(__DIR__ . '/tsl-signer-anchor.pem')],
clockTolerance: 300,
),
parser: new TslXmlParser(),
);
// Use the official publication URL for your scheme territory (HTTPS required).
$tslUrl = 'https://trusted-lists.example.eu/member-state-tsl.xml';
$now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
try {
$tsl = $enforcer->fetchAndVerify($tslUrl);
$bundle = (new TslTrustAnchorProvider())->buildBundle($tsl, $now);
} catch (NetworkPolicyViolation $e) {
// Air-gapped posture: egress forbidden and no cached body available.
fwrite(STDERR, 'Network policy: ' . $e->getMessage() . PHP_EOL);
exit(75);
} catch (TslFetchException $e) {
// Transport layer: SSRF-rejected URL, HTTP error, oversized or empty body.
fwrite(STDERR, 'Fetch failed: ' . $e->getMessage() . PHP_EOL);
exit(1);
} catch (TslSignatureException $e) {
// Authentication layer: treat as a potential attack, not a retry case.
fwrite(STDERR, "Signature rejected ({$e->reason}): {$e->getMessage()}" . PHP_EOL);
exit(1);
} catch (TslParseException $e) {
// Structure or freshness: stale list, malformed NextUpdate, no active CA/QC services.
fwrite(STDERR, 'List unusable: ' . $e->getMessage() . PHP_EOL);
exit(1);
}
printf(
"Anchor bundle %s: %d anchors (sha256 %s...)\n",
$bundle->bundleVersion,
count($bundle->anchorsPem),
substr($bundle->bundleSha256, 0, 12),
);

想定される出力(値はリストによって異なります):

Anchor bundle tsl-de-seq127: 96 anchors (sha256 4b0e2a9f31c8...)

バンドルに対して実行するすべての検証とともに bundleVersionbundleSha256 を記録してください。これらは各判定の背後にある正確なアンカーセットを名指しします。

  • エンフォーサーの鮮度ゲートは現在のクロックを使用します。 fetchAndVerify()verifyXml() は、NextUpdate が既に経過したリストを拒否します。アーカイブされたリストに対する過去の検証では、TslSignatureVerifierTslXmlParser を直接駆動し、次に証跡が裏付ける過去の時点で assertFresh() を呼び出してください。
  • buildBundle() はあなたの $now で鮮度を再度アサートします。 エンフォーサーを通過したリストでも、検証時点がより後であればここで拒否され得ます。上記の BC ノートを参照してください。
  • 検証中のリストから trustAnchorsPem をシードしてはなりません。 アンカーは帯域外でピン留めされたソース(LOTL の場合)、または既に検証済みの親リスト(加盟国 TSL の場合)から来なければなりません。それ以外は検証を循環的にします。
  • どこかにある DOCTYPE は致命的です。 適合する TSL は DTD を持たないため、パーサーは libxml がエンティティテーブルを構築する前にあらゆる DOCTYPE を拒否します。これは意図的な堅牢化であり、パーサーの制限ではありません。
  • 欠落した構造フィールドは安全に劣化します。 読み取り可能なステータスのないサービスは withdrawn として扱われ、決してアンカーになりません。スキーム地域が欠落した場合は unknown として解析されます。fail-closed なデフォルトが、不正なエントリをトラストマテリアルから締め出します。
  • 中間証明書は本物の CA でなければなりません。 チェーン構築中、basicConstraints cA=TRUE のない候補発行者(または keyCertSign なしで keyUsage をアサートするもの)はスキップされます。KeyInfo に紛れ込んだエンドエンティティ証明書は、パスの中間証明書として機能できません。チェーンは深さ 8 で上限が設けられます。
  • NextUpdate は正規の UTC でなければなりません。 明示的な Z または数値オフセットのない値は TslParseException をスローします。サーバーのローカルタイムゾーンで再解釈されることは決してありません。
  • 大きなリストとバイト上限。 レスポンスは $maxBytes(デフォルト 16 MiB)まで読み取られます。スキームのリストがそれより大きい場合はコンストラクタで上限を引き上げてください。切り詰めは署名失敗として表面化し、決して黙って受け入れられることはありません。
  • clockTolerance は広げるだけです。 これは証明書の有効性チェックに対称的な余裕を加えます。リストレベルの鮮度ゲートを緩めることはありません。
  • 常に、解析の前に検証してください。 TslXmlParser は設計上、署名に依存しません。TslPolicyEnforcer は検証を先に順序付けます。部品を自分で組み合わせる場合は、その順序を保ってください。
  • 多層防御の SSRF 対策。 fetch()https:// を要求し、プライベート、ループバック、リンクローカル、CGN、クラウドメタデータの各範囲に対してホストを検証し、リバインディングを緩和するために A および AAAA の DNS 解決を行います。拒否された URL は送信前にスローします。
  • XXE とエンティティ展開の堅牢化。 DOCTYPE を持つ文書は、エンティティテーブルが存在する前と、ロード後の再度、拒否されます。ネットワークエンティティのロードは無効化され、外部エンティティが置換されることは決してありません。
  • 厳格な XMLDSig プロファイル。 排他的 C14N のみ、正確に [enveloped-signature, exclusive-C14N] の変換ペア、検証済み参照は文書ルートを覆わなければならず、enveloped 変換は検証済み署名のみを除去し、兄弟署名を保持します。非推奨のアルゴリズム(SHA-1、MD5)は拒否されます。
  • チェーンの規律。 すべてのチェーンリンク、すなわち署名者、中間証明書、直接アンカーのケースは、時間的有効性についてチェックされ、解析不能な有効性境界に対して fail-closed です。ループは検出され、深さは上限が設けられます。
  • エアギャップの姿勢。 NetworkPolicy::STRICT_OFFLINE の下では、取得パスは一切の送信を行いません。以前にキャッシュされたボディのみが提供され得て、それ以外は NetworkPolicyViolation を fail-fast でスローします。
  • バンドルダイジェストは改ざんではなく破損を検出します。 bundleSha256 は構築時に検証され、転記のずれを検出します。ダイジェストが、それが保護するのと同じアンカーから導出される場合、独立した改ざん証拠にはなりません。システム間でバンドルを転送する際は、ダイジェストを帯域外でピン留めしてください。

このパイプラインは、ETSI TS 119 612 が定義するとおりにトラストリストを消費します。すなわち、スキームオペレーターの署名を認証し(§5.7)、スキーム情報とプロバイダーリスト構造を解析し(§5.3、§5.4、§5.5)、UTC dateTime ルールを適用し(§5.1.3)、NextUpdate が経過したリストを破棄します(§5.3.15)。これは、署名済みで機械処理可能なトラストリストという eIDAS 第 22 条のモデルを支えます。チェーン構築は、候補発行者に RFC 5280 の basic-constraints および key-usage ゲートを適用します。

サポートは適合ではなく、適合は認証ではありません。NextPDF はこのページが説明するチェックを実装していますが、ETSI TS 119 612、eIDAS、その他いかなる標準についても、いかなる機関からも認証を受けておらず、NextPDF はいかなる認証も保持せず、いかなる認証も付与しません。この API を通じてトラストリストを消費すること自体が、署名を「適格」または法的に有効にするわけではありません。あなたの完全な検証プロセスが法的または調達の要件を満たすかどうかは、あなたの評価者が判断する事柄です。

TSL 署名検証は、同梱の暗号ライブラリを通じて RSA および ECDSA チェックをインプロセスで実行します。それは Enterprise の FIPS モードランタイムガードを経由せず、FIPS モードを有効にしてもその挙動は変わりません。それは FIPS 検証済みの暗号サービスではなく、FIPS 140 認証は主張されていません。FIPS の義務を負うデプロイメントは、この API のスコープをそれに応じて設定し、FIPS 140-2/3 暗号ポリシー を参照してください。

  • fetch() は、SSRF 検証を通過した HTTPS URL に対してのみ送信を行い、最大 $maxBytes を読み取り、設定された NetworkPolicy を尊重します。STRICT_OFFLINE の下では、キャッシュされたボディのみが常に返されます。
  • verify() が成功する前に、いかなるパーサー出力もトラストマテリアルにはなりません。TslPolicyEnforcer がその順序を保証します。
  • verify() は、固定プロファイルの下でダイジェストと署名のチェックが通り、かつ署名者が深さ 8 以内で各リンクが時間的に有効なまま設定されたアンカーにチェーンする場合にのみ、署名者の PEM を返します。
  • エンフォーサーは、現在のクロックで NextUpdate が経過したリストをすべて拒否します。buildBundle() は、アンカーを導出する前に、呼び出し側が指定した時点で鮮度を再度アサートします。
  • アンカーは、CA/QC サービス型を持つ granted ステータスのサービスからのみ導出されます。アクティブなセットが空の場合は、空のバンドルを生成するのではなくスローします。
  • すべての失敗は型付き例外です(TslFetchExceptionNetworkPolicyViolation、reason code を伴う TslSignatureExceptionTslParseException)。部分的または未検証の文書を返すメソッドはありません。

NextPDF Core は、その CaTrustAnchorBundle 契約を通じて明示的にピン留めしたトラストアンカーに対して PDF 署名を検証します。Core セキュリティ を参照してください。Core にはトラストリスト機能がありません。TSL 取得も、XMLDSig リスト認証も、ETSI TS 119 612 解析も、適格サービスエントリからのアンカー導出もありません。Core 単独では、アンカーセットを手作業で維持することになります。認証済みの EU トラストリストからそれを導出するには NextPDF Enterprise が必要です。

このページは、外部から観測可能な挙動とサポートされる公開 API サーフェスのみを記述します。内部の名前空間パス、ヘルパークラス、メカニズムテーブル、ランブックのファイル名、チケットプレフィックスは対象外です。