跳到內容
getnextpdf.com

Enterprise 版本

ASiC 信任綁定

ASiC 容器將已簽署的檔案,與保護這些檔案的簽章打包在一起。真正棘手的問題不是「簽章能不能算得出來?」,而是「誰為這位簽署者背書?」。NextPDF\Enterprise\Security\Asic\AsicTrustBinder 回答的正是這個問題。你把容器簽章中的簽署憑證、一份信任清單,以及一個驗證時間交給它。它會以一份 AsicTrustBindingResult 回覆你:一個信任/不信任的裁決、它據以判定的錨集合版本,以及機器可讀的原因。每一次拒絕都會指名其成因,因此稽核佐證會自動成形。

有一條界線是刻意設計的,值得先講清楚。這個 API 不會解析 ASiC 容器。由你的工具開啟容器並擷取簽署憑證;NextPDF 負責信任決策。

這項能力隨 NextPDF Enterprisenextpdf/enterprise)出貨,並透過 Enterprise 級授權封套啟用。缺少該授權的部署不會載入這項能力的類別。比較版本並取得授權

Terminal window
composer require nextpdf/enterprise

啟用需要你的 Enterprise 授權封套。參見 安裝與認證。本頁的類別位於 NextPDF\Enterprise\Security\AsicNextPDF\Enterprise\Security\Tsl 之下。

ASiC(Associated Signature Containers,ETSI EN 319 162-1)將資料檔案與簽章封裝在單一封存檔中。基準 ASiC 容器僅內嵌 CAdES 或 XAdES 基準簽章。CAdES 基準簽章會將其簽署憑證攜帶在 SignedData.certificates 之中,因此當簽章格式正確且容器工具支援時,驗證者應從容器的簽章中擷取它。擷取出的那份憑證即為這個 API 的輸入。

信任來源是一份 ETSI TS 119 612 信任清單(TSL):一份列舉信任服務提供者及其服務憑證的已簽署 XML 文件。NextPDF\Enterprise\Security\Tsl\TslTrustAnchorProvider 會把解析後的 TslDocument 轉換成錨集合。只有同時處於 granted 狀態且屬於 CA/QC 服務類型的服務,才會成為錨集合的種子。這個集合帶有一個由 TSL 序號與領域衍生而來的版本字串,外加一個 SHA-256 完整性摘要。

在任何錨比對之前,會執行兩道 fail-closed 閘門:

  1. TSL 新鮮度。 若信任清單的 NextUpdate 時刻已經過去,就必須以逾期為由捨棄。AsicTrustBinder::verify() 會在衍生任何一個錨之前,於所提供的驗證時間點主張其新鮮度。逾期的清單,或不帶明確 UTC 指示符的 NextUpdate 值,都會拋出 TslParseException
  2. 簽署者有效期。 RFC 5280 路徑驗證要求憑證的有效期必須涵蓋驗證時間。若簽章在密碼學上完整無缺,但其憑證在該時間點已逾期或尚未生效,則會以精確的原因碼予以拒絕。

唯有到此,綁定器才會以每個錨測試簽署憑證。相符會產出 trusted: true,原因為 anchor_signature_match。不相符則產出 trusted: false,原因為 no_anchor_chain

承載全局的設計決策,是在容器機制與信任決策之間劃下嚴格分界,並強制信任決策必須對時間有明確交代。容器格式各不相同(ASiC-S、ASiC-E、CAdES 或 XAdES 酬載),但信任問題只有一個不變的核心:這份憑證是否在指定時刻,鏈結到某份新鮮信任清單所提供的錨?讓這個核心不沾染 ZIP 與 XML 解析,就能讓它小到足以窮盡測試,並在每一道閘門上都能 fail closed。同樣的推理禁止採用隱含的 now 預設值:驗證時間會改變裁決,因此必須由呼叫端擁有它。新鮮度是在錨衍生路徑本身之中主張的,而非在某個選用的協作者中,因此沒有任何產生路徑能夠略過它。

設計背景:數位簽章如何證明簽署者身分

建構時接受一個錨提供者,負責把信任清單轉換成錨集合。

public function __construct(
private readonly TslTrustAnchorProvider $anchorProvider,
) {}

主要進入點會以信任清單驗證簽署者憑證:

public function verify(
string $signerCertPem,
TslDocument $tsl,
DateTimeInterface $validationTime,
): AsicTrustBindingResult
  • $signerCertPem — 非空 PEM 字串:來自 ASiC 簽章的簽署憑證。
  • $tsl — 已解析且經過認證的信任清單。
  • $validationTime — 簽署者憑證有效期必須涵蓋的時刻。沒有預設值。

拋出或失敗於: 當 TSL 逾期(NextUpdate 已過)、NextUpdate 不是標準 UTC 值,或清單不含任何有效的 CA/QC 服務時,會拋出 NextPDF\Enterprise\Security\Tsl\TslParseException。不受信任的簽署者不會拋出;它們會回傳一份帶有 trusted: false 與原因碼的結果。

對於批次工作負載,請以預先建立的集合進行驗證:

public function verifyAgainstBundle(
string $signerCertPem,
EnterpriseCaTrustAnchorBundle $bundle,
DateTimeInterface $validationTime,
): AsicTrustBindingResult

拋出或失敗於: 它本身不拋出任何例外;每個結果都是一份 AsicTrustBindingResult。請從 TslTrustAnchorProvider::buildBundle() 取得集合 — 不要手動建構它。

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

拋出或失敗於: 若 TSL 逾期、其 NextUpdate 不是標準 UTC 值,或不含有效的 CA/QC 服務,則拋出 TslParseException

public function __construct(
public bool $trusted,
public string $anchorBundleVersion,
public array $reasons,
) {}

$reasons 是一個機器可讀原因碼的 list<non-empty-string>$anchorBundleVersion 記錄所使用的錨集合,格式為 tsl-<territory>-seq<N>(例如 tsl-eu-seq42)。

原因碼意義
anchor_signature_match簽署者憑證通過某個源自 TSL 的錨驗證。受信任。
no_anchor_chain集合中沒有任何錨能驗證該簽署者憑證。不受信任。
signer_cert_expired驗證時間落在憑證 notAfter 之後。不受信任。
signer_cert_not_yet_valid驗證時間落在憑證 notBefore 之前。不受信任。
cannot_parse_signer_cert所提供的 PEM 無法解析為 X.509 憑證。不受信任。

你的容器工具已經擷取出簽署憑證。將它綁定到一份你已擷取並認證的會員國信任清單(參見 信任清單)。

asic-trust-binding-quickstart.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Enterprise\Security\Asic\AsicTrustBinder;
use NextPDF\Enterprise\Security\Tsl\TslParseException;
use NextPDF\Enterprise\Security\Tsl\TslTrustAnchorProvider;
use NextPDF\Enterprise\Security\Tsl\TslXmlParser;
// Extracted by YOUR tooling from META-INF/signature.p7s or signatures.xml.
$signerCertPem = (string) file_get_contents(__DIR__ . '/asic-signer.pem');
// A trusted list you have already fetched and authenticated.
$tslXml = (string) file_get_contents(__DIR__ . '/member-state-tsl.xml');
$binder = new AsicTrustBinder(new TslTrustAnchorProvider());
try {
$tsl = (new TslXmlParser())->parse($tslXml);
$result = $binder->verify(
signerCertPem: $signerCertPem,
tsl: $tsl,
validationTime: new DateTimeImmutable('2026-07-03T12:00:00Z'),
);
} catch (TslParseException $e) {
// Fail closed: stale TSL, malformed NextUpdate, or no active CA/QC services.
fwrite(STDERR, 'Trusted list rejected: ' . $e->getMessage() . PHP_EOL);
exit(1);
}
echo $result->trusted ? "TRUSTED\n" : "NOT TRUSTED\n";
echo 'Anchors: ' . $result->anchorBundleVersion . "\n";
echo 'Reasons: ' . implode(', ', $result->reasons) . "\n";

由列於清單中的 CA/QC 服務所簽發之簽署者,其預期輸出:

TRUSTED
Anchors: tsl-eu-seq42
Reasons: anchor_signature_match

每份信任清單只衍生一次錨集合,然後以它驗證眾多容器簽署者。一份逾期或不可用的 TSL 會讓整個批次 fail closed;個別簽署者的問題則會逐一在各容器上浮現。

asic-trust-binding-batch.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Enterprise\Security\Asic\AsicTrustBinder;
use NextPDF\Enterprise\Security\Asic\AsicTrustBindingResult;
use NextPDF\Enterprise\Security\Tsl\TslDocument;
use NextPDF\Enterprise\Security\Tsl\TslParseException;
use NextPDF\Enterprise\Security\Tsl\TslTrustAnchorProvider;
use NextPDF\Enterprise\Security\Tsl\TslXmlParser;
/**
* @param array<string, non-empty-string> $signerPemsByContainer PEM per container path.
* @return array<string, AsicTrustBindingResult>
* @throws TslParseException When no anchor set can be derived from the TSL.
*/
function bindBatch(
TslDocument $tsl,
array $signerPemsByContainer,
DateTimeImmutable $validationTime,
): array {
$provider = new TslTrustAnchorProvider();
// Derive the anchor set ONCE; a throw here means the trusted list itself
// is unusable at this validation time.
$bundle = $provider->buildBundle($tsl, $validationTime);
$binder = new AsicTrustBinder($provider);
$results = [];
foreach ($signerPemsByContainer as $container => $signerPem) {
$results[$container] = $binder->verifyAgainstBundle(
signerCertPem: $signerPem,
bundle: $bundle,
validationTime: $validationTime,
);
}
return $results;
}
$tsl = (new TslXmlParser())->parse(
(string) file_get_contents(__DIR__ . '/member-state-tsl.xml'),
);
$signerPems = [
'invoice-2026-06.asice' => (string) file_get_contents(__DIR__ . '/signer-a.pem'),
'tender-2019.asice' => (string) file_get_contents(__DIR__ . '/signer-b.pem'),
];
try {
$results = bindBatch(
tsl: $tsl,
signerPemsByContainer: $signerPems,
validationTime: new DateTimeImmutable('now', new DateTimeZone('UTC')),
);
} catch (TslParseException $e) {
// Fail closed for the WHOLE batch: no trustworthy anchor set exists.
fwrite(STDERR, 'Anchor derivation failed: ' . $e->getMessage() . PHP_EOL);
exit(1);
}
foreach ($results as $container => $result) {
printf(
"%s => %s (%s; anchors %s)\n",
$container,
$result->trusted ? 'trusted' : 'rejected',
implode(',', $result->reasons),
$result->anchorBundleVersion,
);
}

當其中一份簽署者憑證已逾期時的預期輸出:

invoice-2026-06.asice => trusted (anchor_signature_match; anchors tsl-eu-seq42)
tender-2019.asice => rejected (signer_cert_expired; anchors tsl-eu-seq42)
  • 驗證時間是必要且具決定性的。 沒有隱含的 now 預設值。一份在 2019 年通過驗證的簽章,當你在超過 notAfter 的 2026 年時刻進行驗證時,會回報 signer_cert_expired。對於歷史性材料,請傳入你的佐證所支持的時間(例如某個存在性證明時間),而非牆上時鐘的時間。
  • 逾期的 TSL 會拋出;它不是「不受信任」的裁決。 來自 verify()buildBundle()TslParseException,代表信任來源不可用。請把它當作一次維運失敗來處理:更新清單,不要把它記錄為簽署者被拒。
  • 錨是以直接簽發者的身分受測。 每個錨都會被當作簽署了簽署者憑證的那份憑證來嘗試。歐盟會員國的 TSL 會列出簽發用的 CA/QC 服務憑證,因此末端實體的合格憑證通常會直接相符。若簽署者是由某個本身並未列於清單中之有效 CA/QC 服務的中繼 CA 所簽發,則會產出 no_anchor_chain
  • 錨衍生會嚴格過濾。 已撤回的服務,或任何非 CA/QC 類型的服務,都絕不會成為錨。若清單的有效 CA/QC 集合為空,則會拋出,而不會產出一個空集合。
  • NextUpdate 必須是標準 UTC。 不帶明確 Z 或數值偏移指示符的值會被 fail-closed 拒絕,絕不會依伺服器的本地時區重新解讀。
  • 格式不正確的輸入會精確地降級。 無法解析的 PEM 會回傳 cannot_parse_signer_cert;尚未生效的憑證會與已逾期的憑證有所區別。
  • 記錄 anchorBundleVersion 它指名每個裁決背後所依據的確切錨集合(tsl-<territory>-seq<N>),這正是稽核人員會要求的資訊。
  • 從設計上即 fail-closed。 新鮮度會在衍生任何錨之前就先行主張。簽署者有效性閘門會在任何錨比對之前執行。不可用的信任材料會拋出;有疑慮的簽署者會連同原因一併被拒。沒有任何路徑會降級為隱默的放行。
  • 信任綁定只是一層,並非整套驗證。 這個 API 不會驗證涵蓋容器內容的 CAdES 簽章值,不會檢查撤銷(不做 CRL 或 OCSP 查詢),也不會認證 TSL 文件本身。請先透過信任清單管線認證該清單(參見 信任清單),以你的簽章工具在密碼學上驗證簽章,並依你的政策加上撤銷檢查。
  • 審慎地選擇驗證時間。 裁決是你所傳入時間的函數。請從可信的佐證(一個合格時戳、一筆封存紀錄)衍生它,而非從可被攻擊者影響的時鐘取得。
  • 佐證輸出具決定性。 trustedanchorBundleVersionreasons 都是穩定、機器可讀的值,適合用於已簽署的稽核日誌。

AsicTrustBinder 支援與 ETSI EN 319 162-1(ASiC 基準容器)、ETSI EN 319 122-1(CAdES 基準簽章)及 ETSI TS 119 612(信任清單)相符的工作流程,並在所提供的驗證時間點套用 RFC 5280 有效期閘門。

支援不等於符合,符合不等於認證。NextPDF 實作本頁所描述的檢查;它並未經任何機構針對這些標準進行認證,而且單憑使用這個 API,並不會讓你的輸出在 eIDAS 或任何其他制度下成為「合格」或具法律效力。NextPDF 未持有任何認證,也不授予任何認證。一套完整的驗證流程是否滿足特定法律或採購要求,是由你的評估者判定的事項。

信任綁定會在行程內執行 X.509 憑證簽章檢查;它不會經由 Enterprise FIPS 模式的執行期防護,而啟用 FIPS 模式也不會改變其行為。它不是一項通過 FIPS 驗證的密碼服務,也未主張任何 FIPS 140 認證。負有 FIPS 義務的部署應據此界定這個 API 的範圍,並參見 FIPS 140-2/3 密碼政策

  • verify() 只會從在所提供驗證時間點仍屬新鮮的 TSL 衍生錨;逾期或格式不正確的清單,會在任何錨存在之前就拋出 TslParseException
  • 錨只會從處於 granted 狀態且屬 CA/QC 服務類型的 TSL 服務衍生;有效集合為空時會拋出。
  • 簽署者憑證的有效期必須涵蓋驗證時間;違反者會回傳 signer_cert_expiredsigner_cert_not_yet_valid
  • 每個結果都是一份帶有 trustedanchorBundleVersion 與至少一個原因碼的 AsicTrustBindingResult;不存在無原因的裁決。
  • 不受信任的簽署者一律回傳,絕不拋出;不可用的信任材料一律拋出,絕不作為裁決回傳。
  • 容器解析絕不會在這個 API 內部發生;輸入是擷取出的 PEM、信任清單,以及驗證時間。

NextPDF Core 會以你透過其 CaTrustAnchorBundle 契約明確釘選的信任錨,驗證 PDF(CMS/PAdES)簽章 — 參見 Core 安全性。Core 沒有信任清單(TSL)匯入,也沒有 ASiC 專屬的信任綁定。僅憑 Core,你可以維護自己的錨集合用於 PDF 簽章驗證;而要從 ETSI TS 119 612 信任清單衍生錨,並將 ASiC 容器簽署者綁定到它們,則需要 NextPDF Enterprise。

本頁僅記載外部可觀察的行為,以及受支援的公開 API 介面。內部命名空間路徑、輔助類別、機制表格、維運手冊檔名,以及工單前綴皆不在範圍內。