跳转到内容
getnextpdf.com

Enterprise 版本

ASiC 信任绑定

一个 ASiC 容器把已签名的文件与保护它们的签名捆绑在一起。真正的难题不是”签名能否计算通过?“,而是”谁为签名者背书?“。NextPDF\Enterprise\Security\Asic\AsicTrustBinder 正是回答这个问题。你把容器签名中的签名证书、一份可信列表以及一个验证时间交给它,它以一个 AsicTrustBindingResult 作答:一个可信/不可信的裁决、它据以判定的锚 bundle 版本,以及机器可读的原因。每一次拒绝都会指明其成因,因此审计证据可以自动成文。

有一条边界是刻意为之,值得先讲清楚。这个 API 解析 ASiC 容器。由你的工具打开容器并提取签名证书;信任决策则由 NextPDF 负责。

此能力随 NextPDF Enterprisenextpdf/enterprise)发布,并通过 Enterprise 层级的 license envelope 激活。缺少该授权的部署不会加载此能力的类。比较版本并获取授权

Terminal window
composer require nextpdf/enterprise

激活需要你的 Enterprise license envelope。参见 安装与认证。本页涉及的类位于 NextPDF\Enterprise\Security\AsicNextPDF\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 完整性摘要。

在任何锚比较之前,会运行两道失败即拒的门控:

  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 解析,就能保持它足够小,从而可以详尽测试,并在每道门控上失败即拒。同样的推理禁止一个静默的 now 默认值:验证时间会改变裁决,因此调用方必须持有它。新鲜度是在锚派生路径本身之内断言的,而非在一个可选的协作者中,因此没有任何生产路径能跳过它。

设计背景:数字签名如何证明谁签了名

构造函数接受一个锚提供者,用它把可信列表转换为锚 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 — 不要手工构造它。

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_chainbundle 中没有锚能验证签名者证书。不可信。
signer_cert_expired验证时间落在证书的 notAfter 之后。不可信。
signer_cert_not_yet_valid验证时间落在证书的 notBefore 之前。不可信。
cannot_parse_signer_cert所提供的 PEM 无法解析为 X.509 证书。不可信。

你的容器工具已经提取出了签名证书。将它绑定到一份你已获取并认证的成员国可信列表(参见 Trusted lists)。

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

每份可信列表只派生一次锚 bundle,然后针对它验证许多容器签名者。任何一份陈旧或不可用的 TSL 都会让整个批次失败即拒;个别签名者的问题则逐容器浮现。

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 意味着信任来源不可用。请把它当作一次运维故障处理:刷新列表,不要把它记为一次签名者拒绝。
  • 锚被当作直接签发者来测试。 每个锚都被尝试作为签署了签名者证书的那份证书。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),用你的签名工具从密码学上验证签名,并按你的策略加上吊销检查。
  • 审慎选择验证时间。 裁决是你所传入时间的函数。请从可信证据(一个合格时间戳、一份归档记录)派生它,而不是从一个可被攻击者影响的时钟。
  • 证据输出是确定性的。 trustedanchorBundleVersionreasons 是稳定、机器可读的值,适用于已签名的审计日志。

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 不持有任何认证,也不授予任何认证。一个完整的验证流程是否满足某项特定的法律或采购要求,是由你的评估方来判定的事项。

信任绑定在进程内执行 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 security。Core 没有可信列表(TSL)摄取,也没有 ASiC 专属的信任绑定。仅凭 Core,你可以维护自己的锚集用于 PDF 签名验证;而从一份 ETSI TS 119 612 可信列表派生锚并把 ASiC 容器签名者绑定到它们,则需要 NextPDF Enterprise。

本页仅记录外部可观测的行为与受支持的公共 API 表面。内部命名空间路径、辅助类、机制表、运行手册文件名以及工单前缀均不在范围内。