Enterprise 版本
ASiC 信任绑定
一个 ASiC 容器把已签名的文件与保护它们的签名捆绑在一起。真正的难题不是”签名能否计算通过?“,而是”谁为签名者背书?“。NextPDF\Enterprise\Security\Asic\AsicTrustBinder 正是回答这个问题。你把容器签名中的签名证书、一份可信列表以及一个验证时间交给它,它以一个 AsicTrustBindingResult 作答:一个可信/不可信的裁决、它据以判定的锚 bundle 版本,以及机器可读的原因。每一次拒绝都会指明其成因,因此审计证据可以自动成文。
有一条边界是刻意为之,值得先讲清楚。这个 API 不解析 ASiC 容器。由你的工具打开容器并提取签名证书;信任决策则由 NextPDF 负责。
可用性与授权
标题为“可用性与授权”的章节此能力随 NextPDF Enterprise(nextpdf/enterprise)发布,并通过 Enterprise 层级的 license envelope 激活。缺少该授权的部署不会加载此能力的类。比较版本并获取授权。
composer require nextpdf/enterprise激活需要你的 Enterprise license envelope。参见 安装与认证。本页涉及的类位于 NextPDF\Enterprise\Security\Asic 与 NextPDF\Enterprise\Security\Tsl 命名空间下。
概念概述
标题为“概念概述”的章节ASiC(Associated Signature Containers,ETSI EN 319 162-1)把数据文件与签名打包进一个归档中。一个 baseline ASiC 容器只嵌入 CAdES 或 XAdES baseline 签名。一个 CAdES baseline 签名会把它的签名证书带在 SignedData.certificates 里,因此当签名格式良好且受容器工具支持时,验证方应当从容器签名中提取它。被提取出的那份证书就是本 API 的输入。
信任来源是一份 ETSI TS 119 612 可信列表(TSL):一个已签名的 XML 文档,枚举了各信任服务提供者及其服务证书。NextPDF\Enterprise\Security\Tsl\TslTrustAnchorProvider 把一个已解析的 TslDocument 转换为一个锚 bundle。只有同时处于 granted 状态且属于 CA/QC 服务类型的服务才会为锚集提供种子。该 bundle 携带一个由 TSL 序列号与地区派生出的版本字符串,外加一个 SHA-256 完整性摘要。
在任何锚比较之前,会运行两道失败即拒的门控:
- TSL 新鲜度。 一份
NextUpdate时刻已过的可信列表必须作为已过期而被丢弃。AsicTrustBinder::verify()会在派生任何单个锚之前,以给定的验证时间断言其新鲜度。一份陈旧的列表,或一个不带显式 UTC 指示符的NextUpdate值,会抛出TslParseException。 - 签名者有效期。 RFC 5280 路径验证要求证书有效期包含验证时间。一个密码学上完好、但其证书在该时刻已过期或尚未生效的签名,会以一个精确的原因码被拒绝。
只有到此之后,绑定器才会把签名证书与每个锚逐一比对。匹配则得出 trusted: true,原因为 anchor_signature_match。不匹配则得出 trusted: false,原因为 no_anchor_chain。
为何如此设计
标题为“为何如此设计”的章节承重的设计决策,是在容器机制与信任决策之间实行严格分离,并强制信任决策对时间保持显式。容器格式各不相同(ASiC-S、ASiC-E,CAdES 或 XAdES 载荷),但信任问题是一个不变的内核:这份证书在某个既定时刻,是否链接到一份新鲜可信列表中的一个锚?让这个内核不掺入 ZIP 与 XML 解析,就能保持它足够小,从而可以详尽测试,并在每道门控上失败即拒。同样的推理禁止一个静默的 now 默认值:验证时间会改变裁决,因此调用方必须持有它。新鲜度是在锚派生路径本身之内断言的,而非在一个可选的协作者中,因此没有任何生产路径能跳过它。
设计背景:数字签名如何证明谁签了名。
API 表面
标题为“API 表面”的章节AsicTrustBinder
标题为“AsicTrustBinder”的章节构造函数接受一个锚提供者,用它把可信列表转换为锚 bundle。
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 且带原因码的结果。
对于批量负载,针对一个预先构建的 bundle 进行验证:
public function verifyAgainstBundle( string $signerCertPem, EnterpriseCaTrustAnchorBundle $bundle, DateTimeInterface $validationTime,): AsicTrustBindingResult抛出或失败于: 它自身不抛任何异常;每一个结果都是一个 AsicTrustBindingResult。请从 TslTrustAnchorProvider::buildBundle() 获取 bundle — 不要手工构造它。
TslTrustAnchorProvider
标题为“TslTrustAnchorProvider”的章节public function buildBundle(TslDocument $tsl, DateTimeImmutable $now): EnterpriseCaTrustAnchorBundle抛出或失败于: 若 TSL 陈旧、其 NextUpdate 不是规范的 UTC 值,或它不含活跃的 CA/QC 服务,则抛出 TslParseException。
AsicTrustBindingResult
标题为“AsicTrustBindingResult”的章节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 | bundle 中没有锚能验证签名者证书。不可信。 |
signer_cert_expired | 验证时间落在证书的 notAfter 之后。不可信。 |
signer_cert_not_yet_valid | 验证时间落在证书的 notBefore 之前。不可信。 |
cannot_parse_signer_cert | 所提供的 PEM 无法解析为 X.509 证书。不可信。 |
代码示例 — 快速上手
标题为“代码示例 — 快速上手”的章节你的容器工具已经提取出了签名证书。将它绑定到一份你已获取并认证的成员国可信列表(参见 Trusted lists)。
<?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 服务签发的签名者,预期输出:
TRUSTEDAnchors: tsl-eu-seq42Reasons: anchor_signature_match代码示例 — 生产环境
标题为“代码示例 — 生产环境”的章节每份可信列表只派生一次锚 bundle,然后针对它验证许多容器签名者。任何一份陈旧或不可用的 TSL 都会让整个批次失败即拒;个别签名者的问题则逐容器浮现。
<?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意味着信任来源不可用。请把它当作一次运维故障处理:刷新列表,不要把它记为一次签名者拒绝。 - 锚被当作直接签发者来测试。 每个锚都被尝试作为签署了签名者证书的那份证书。EU 成员国 TSL 会列出签发方的 CA/QC 服务证书,因此终端实体的合格证书通常会直接匹配。一个由某个本身并非列入的活跃 CA/QC 服务的中间 CA 所签发的签名者,会得出
no_anchor_chain。 - 锚派生的筛选很严格。 已撤回的服务,或任何非 CA/QC 类型的服务,都绝不会成为锚。一个活跃 CA/QC 集为空的列表会抛异常,而非产出一个空 bundle。
NextUpdate必须是规范 UTC。 一个不带显式Z或数值偏移指示符的值会被失败即拒地拒绝,绝不会按服务器本地时区重新解释。- 畸形输入会精确降级。 一个无法解析的 PEM 返回
cannot_parse_signer_cert;一个尚未生效的证书会与一个已过期的证书区分开来。 - 记录
anchorBundleVersion。 它指明了每个裁决背后的确切锚集(tsl-<territory>-seq<N>),而这正是审计员会要求的。
安全说明
标题为“安全说明”的章节- 在构造上失败即拒。 新鲜度在派生任何锚之前就已断言。签名者有效期门控在任何锚比较之前运行。不可用的信任材料会抛异常;可疑的签名者会带原因被拒绝。没有任何路径会降级为静默通过。
- 信任绑定只是一层,而非全部验证。 这个 API 不验证覆盖容器内容的 CAdES 签名值,不检查吊销状态(没有 CRL 或 OCSP 查询),也不认证 TSL 文档本身。请先通过可信列表流水线认证该列表(参见 Trusted lists),用你的签名工具从密码学上验证签名,并按你的策略加上吊销检查。
- 审慎选择验证时间。 裁决是你所传入时间的函数。请从可信证据(一个合格时间戳、一份归档记录)派生它,而不是从一个可被攻击者影响的时钟。
- 证据输出是确定性的。
trusted、anchorBundleVersion与reasons是稳定、机器可读的值,适用于已签名的审计日志。
合规性
标题为“合规性”的章节AsicTrustBinder 支持与 ETSI EN 319 162-1(ASiC baseline 容器)、ETSI EN 319 122-1(CAdES baseline 签名)以及 ETSI TS 119 612(可信列表)对齐的工作流,并在给定验证时间应用 RFC 5280 有效期门控。
支持不等于合规,合规也不等于认证。NextPDF 实现了本页所述的检查;它未曾由任何机构就这些标准进行认证,而且使用这个 API 本身并不会使你的输出在 eIDAS 或任何其他制度下”合格”或具备法律效力。NextPDF 不持有任何认证,也不授予任何认证。一个完整的验证流程是否满足某项特定的法律或采购要求,是由你的评估方来判定的事项。
FIPS 模式行为
标题为“FIPS 模式行为”的章节信任绑定在进程内执行 X.509 证书签名检查;它不经由 Enterprise FIPS 模式运行时守护,且启用 FIPS 模式不会改变其行为。它不是一个经 FIPS 验证的密码学服务,也不声称任何 FIPS 140 认证。有 FIPS 义务的部署应据此界定这个 API 的范围,并参见 FIPS 140-2/3 密码学策略。
行为契约
标题为“行为契约”的章节verify()只从一份在给定验证时间新鲜的 TSL 派生锚;一份陈旧或畸形的列表会在任何锚存在之前抛出TslParseException。- 锚只从处于 granted 状态且属于 CA/QC 服务类型的 TSL 服务派生;一个空的活跃集会抛异常。
- 签名者证书的有效期必须包含验证时间;违反此条会返回
signer_cert_expired或signer_cert_not_yet_valid。 - 每一个结果都是一个携带
trusted、anchorBundleVersion以及至少一个原因码的AsicTrustBindingResult;不存在无原因的裁决。 - 不可信的签名者被返回,绝不被抛出;不可用的信任材料被抛出,绝不作为裁决被返回。
- 容器解析绝不发生在这个 API 内部;输入是被提取出的 PEM、可信列表与验证时间。
Core 回退
标题为“Core 回退”的章节NextPDF Core 针对你通过其 CaTrustAnchorBundle 契约显式钉入的信任锚,验证 PDF(CMS/PAdES)签名 — 参见 Core security。Core 没有可信列表(TSL)摄取,也没有 ASiC 专属的信任绑定。仅凭 Core,你可以维护自己的锚集用于 PDF 签名验证;而从一份 ETSI TS 119 612 可信列表派生锚并把 ASiC 容器签名者绑定到它们,则需要 NextPDF Enterprise。
发布边界
标题为“发布边界”的章节本页仅记录外部可观测的行为与受支持的公共 API 表面。内部命名空间路径、辅助类、机制表、运行手册文件名以及工单前缀均不在范围内。
- Trusted lists — 获取、认证并解析为锚提供者提供数据的 TSL。
- Signature verification — 用于 PDF 签名的 Enterprise 验证表面。
- FIPS 140-2/3 密码学策略 — Enterprise FIPS 模式姿态。
- 数字签名如何证明谁签了名 — 第一性原理背景。
- 长期验证 — 为何验证时间与保存的证据很重要。