跳转到内容
getnextpdf.com

Enterprise 版本

eIDAS 保证级别

NextPDF Enterprise 将欧盟信任列表证据转化为明确的 eIDAS 保证级别(LoA)。NextPDF\Enterprise\Security\Eidas\LoaMapping 服务将单个信任服务条目分类为 LowSubstantialHigh。它返回一个 LoaAssertion,其中承载级别以及机器可读的原因代码。你的工作流可以基于保证级别进行门控——“要求 High”——并将这些原因归档为审计证据。配套的守护组件 CertPiiGuard 会在审计记录离开进程之前对签名者身份字段进行脱敏。

有两条边界诚实地界定了这项能力。第一,资格认定始终归属于成员国监管下的信任服务提供者(TSP)。NextPDF 是对已发布证据做出分类断言;它绝不授予、赋予或认证资格。第二,本页仅涵盖 LoA 断言与映射。结构性 PAdES 策略 eidasQualified()(包括其通过/失败判定标准)记录在 验证 中。

这项能力随 NextPDF Enterprisenextpdf/enterprise)一同发布,并在 Enterprise 级许可证信封激活时启用。没有该授权的部署不会加载此能力的类。比较版本并获取许可证

Terminal window
composer require nextpdf/enterprise

nextpdf/premium 元包同样会解析出 Enterprise 包。激活使用你的 Enterprise 许可证信封;参见 许可与激活。eIDAS 相关的类除了引擎基线之外不需要任何 PHP 扩展。它们在 NextPDF\Enterprise\Security\EidasNextPDF\Enterprise\Signature\Eidas 下自动加载。

Regulation (EU) No 910/2014 (eIDAS) 定义了三个保证级别:lowsubstantialhigh(Article 8(1))。每个级别都表达了对所声称身份的一种信心程度。high 级别增加了旨在阻止——而不仅仅是降低——身份被滥用或篡改的控制措施(Article 8(2)(c))。Article 8 为电子身份识别方案定义了这些级别。NextPDF 复用相同的词汇表,来对签名证书背后的信任服务证据进行分类。这种复用是一种用于策略门控和审计的工程约定,而非法律上的等同。

LoaLevel 枚举对这三个级别建模。它的后备值是 eIDAS LoA URI 而非裸标签,因此持久化的断言会携带完整标识符。rank() 给出一个全序(Low = 1,Substantial = 2,High = 3),而 meetsOrExceeds() 会与所要求的下限进行比较。

LoaMapping 从单个信任列表条目——来自 Enterprise 信任列表子系统(NextPDF\Enterprise\Security\Tsl)的 TspService——计算出一个级别。该映射是确定性的:

信任列表证据级别原因代码
服务状态不是 grantedLowservice_not_granted
服务类型不是 CA/QCLowservice_not_qualified_ca
已授予(granted)的 CA/QC,带 QCWithQSCD 且不带 QCNoQSCDHighca_qc_with_qscd 加上 esig_or_esealqc_default
其他情况下已授予的 CA/QCSubstantialca_qc_no_qscd_or_unspecified

QSCD(合格签名创建设备)限定符是关键枢纽。根据 Article 3(12),合格电子签名同时要求合格证书和合格创建设备。因此,一条声明证书在 QSCD 上受管理的信任列表陈述,才是支持 High 断言的证据。若没有该陈述,一个已授予的合格 CA 仍然只能支持 Substantial,绝不会是 High

结果是一个 LoaAssertion:级别加上一个原因代码列表。这些原因让审计消费方日后能够从同样的证据重新推导出该分类。下游策略评估器可以将该断言与签名验证结果一并记录。

本模块还随附了一部分:CertPiiGuard。当验证工件被序列化为 JSON 审计包时,签名者证书携带个人数据——Subject CN、email 属性,以及 serialNumber 属性(对于自然人,它可能编码了一个国家标识符)。GDPR Article 5(1)(c) 要求处理仅限于必要范围。因此该守护组件默认对这些字段进行脱敏,将其值替换为 [REDACTED],同时保留结构性的信封(组织、国家、证书链和状态字段)。消费方仍然可以验证一个签名是否通过,而无需得知是签署的。

起支撑作用的决策是将保证断言验证裁定分离开来。签名验证——依照 ETSI EN 319 102-1——以一个状态指示结束:TOTAL-PASSEDTOTAL-FAILEDINDETERMINATE,而该裁定归属于验证层。LoA 映射则是对信任列表证据的一种独立的、可重放的分类,用原因代码而非裸标签表达。这使得 NextPDF 绝不会将一个保证断言呈现为验证结果,也不会将一个验证结果呈现为资格授予。它还使映射在构造上是保守的:缺失或含糊的证据只会降低级别,绝不会提升级别。

设计背景:合格签名解析

下列所有符号都是 nextpdf/enterprise 3.1.0 中的公开 API。

enum LoaLevel: string
{
case Low = 'http://eidas.europa.eu/LoA/low';
case Substantial = 'http://eidas.europa.eu/LoA/substantial';
case High = 'http://eidas.europa.eu/LoA/high';
public function rank(): int
public function meetsOrExceeds(self $required): bool
}

抛出或失败于:rank()meetsOrExceeds() 不会抛出任何异常。通过 LoaLevel::from() 进行的原生枚举构造在遇到无法识别的 URI 时会抛出 \ValueErrorLoaLevel::tryFrom() 则改为返回 null

final class LoaMapping
{
public function loaForService(TspService $service): LoaAssertion
}

抛出或失败于:无。该方法是全函数——每个 TspService 输入都会产出一个 LoaAssertion

输入 DTO NextPDF\Enterprise\Security\Tsl\TspServiceNextPDF\Enterprise\Security\Tsl\TspServiceQualifier 是稳定的公开 DTO(@api)。该映射会查询 TspService::STATUS_GRANTEDTspService::TYPE_CA_QC,以及限定符常量 TspServiceQualifier::QSCD_STATEMENTQCWithQSCD)、TspServiceQualifier::NO_QSCDQCNoQSCD)、TspServiceQualifier::FOR_ESIGTspServiceQualifier::FOR_ESEAL

final readonly class LoaAssertion
{
/**
* @param list<non-empty-string> $reasons Machine-readable reason codes for the assertion.
*/
public function __construct(
public LoaLevel $level,
public array $reasons,
) {}
}

抛出或失败于:无。不可变值对象。

final readonly class CertPiiGuard
{
public function __construct(
private bool $disclosePii = false,
) {}
public function disclosesPii(): bool
public function guardSignerCommonName(string $signer): string
public function guardDistinguishedName(string $dn): string
public function guardTsaName(string $tsaName): string
public function guardRootIssuer(string $issuer): string
public function guardChainIssue(string $issue): string
}

抛出或失败于:无。这些守护方法都是纯字符串变换。对于无法可靠地进行词法切分的 DN 组件,守护组件会失败关闭(fail closed),将该组件整体折叠为 [REDACTED],而不是发出一个部分掩码的值。

解析一个 LoA URI 并将其与所要求的下限进行比较。

loa-quick-start.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Enterprise\Security\Eidas\LoaLevel;
// A LoA URI as persisted in an audit record or received from a peer system.
$uri = 'http://eidas.europa.eu/LoA/substantial';
try {
$level = LoaLevel::from($uri);
} catch (\ValueError $e) {
// Unknown URI: refuse to classify. Never guess an assurance level.
echo "Unrecognized LoA URI: {$uri}\n";
exit(1);
}
echo 'Level: ' . $level->name . ' (rank ' . $level->rank() . ")\n";
echo 'Meets substantial: ' . ($level->meetsOrExceeds(LoaLevel::Substantial) ? 'yes' : 'no') . "\n";
echo 'Meets high: ' . ($level->meetsOrExceeds(LoaLevel::High) ? 'yes' : 'no') . "\n";

预期输出:

Level: Substantial (rank 2)
Meets substantial: yes
Meets high: no

对一个信任列表条目进行分类,基于所要求的级别进行门控,并发出一条脱敏的审计记录。

loa-audit-gate.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Enterprise\Security\Eidas\LoaLevel;
use NextPDF\Enterprise\Security\Eidas\LoaMapping;
use NextPDF\Enterprise\Security\Tsl\TspService;
use NextPDF\Enterprise\Security\Tsl\TspServiceQualifier;
use NextPDF\Enterprise\Signature\Eidas\CertPiiGuard;
// Normally produced by the Enterprise trusted-list subsystem from a
// member-state TSL; constructed inline here for a self-contained example.
$caPem = (string) file_get_contents(__DIR__ . '/example-qc-ca.pem');
$service = new TspService(
tspName: 'Example Qualified TSP',
serviceName: 'Example Qualified CA G2',
serviceTypeIdentifier: TspService::TYPE_CA_QC,
serviceStatus: TspService::STATUS_GRANTED,
statusStartingTime: '2024-01-01T00:00:00Z',
serviceCertificatePem: $caPem,
qualifiers: [
new TspServiceQualifier(qualifierUri: TspServiceQualifier::QSCD_STATEMENT),
new TspServiceQualifier(qualifierUri: TspServiceQualifier::FOR_ESIG),
],
additionalServiceInformation: [],
);
try {
// Required floor from deployment configuration; defaults to High.
$required = LoaLevel::from(getenv('LOA_REQUIRED') ?: LoaLevel::High->value);
} catch (\ValueError $e) {
echo "Invalid LOA_REQUIRED URI; refusing to continue.\n";
exit(1);
}
$mapping = new LoaMapping();
$assertion = $mapping->loaForService($service);
// Privacy by default: signer identity fields are redacted in audit output.
$guard = new CertPiiGuard();
$audit = [
'loa' => $assertion->level->value,
'reasons' => $assertion->reasons,
'meets_required' => $assertion->level->meetsOrExceeds($required),
'signer' => $guard->guardSignerCommonName('CN=Jane Example, O=Example Corp, C=DE'),
];
echo json_encode($audit, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";

预期输出:

{
"loa": "http://eidas.europa.eu/LoA/high",
"reasons": [
"ca_qc_with_qscd",
"esig_or_eseal"
],
"meets_required": true,
"signer": "CN=[REDACTED], O=Example Corp, C=DE"
}
  • LoaLevel::from() 在遇到未知 URI 时会抛出 \ValueError。在更适合处理 null 的场景下,请使用 LoaLevel::tryFrom()
  • 相互冲突的设备证据会保持保守。同时携带 QCWithQSCDQCNoQSCD 的服务会映射到 Substantial,而不是 High
  • 一个不带任何限定符的已授予 CA/QC 服务会映射到 Substantial,原因为 ca_qc_no_qscd_or_unspecified——默认合格,但设备未经证明。
  • 追踪集合之外的限定符 URI 不会影响分类。未知的或未来的限定符绝不会提升级别。
  • 该映射仅读取当前服务状态。它不评估 statusStartingTime 历史;时点窗口归属于验证层。
  • 请持久化枚举的后备 URI,而不是 rank() 整数。rank 仅为比较而存在。
  • CertPiiGuard 会将一个不含 = 的裸名称整体折叠为 [REDACTED];空字符串则原样通过所有守护方法。
  • 传统 OpenSSL 斜杠分隔的 DN 会被检测出来并进行结构性掩码。RFC 4514 值内部的 / 会被当作内容而非分隔符处理。
  • 非 PII 的 DN 属性(OOUCSTL)会被保留,因此管辖权推理在脱敏后仍然存续。
  • 默认启用隐私保护。 守护组件构造函数默认为 disclosePii: false。仅在你持有处理签名者身份的、已记录在案的合法依据时,才构造 new CertPiiGuard(disclosePii: true)。这在序列化边界处实现了 GDPR Article 5(1)(c) 的数据最小化。
  • 失败关闭式脱敏。 当一个 DN 组件无法被可靠地进行词法切分时,整个组件会折叠为 [REDACTED]。隐私控制绝不会失败开放(fail open)。
  • 确定性输出。 守护方法使用纯字符串处理——没有时钟,没有随机性——因此对于相同输入,掩码输出是字节稳定的。稳定的输出使审计差异保持有意义。
  • 脱敏不是加密。 [REDACTED] 会从记录中移除该值。如果你需要身份可恢复,请将其单独存储在其自身的合法依据和访问控制之下。
  • 垃圾进,垃圾出。 一个 LoaAssertion 的可信度只等同于其背后的信任列表证据。在将条目喂给映射之前,请通过 Enterprise 信任列表子系统获取并对信任列表进行签名校验。

NextPDF Enterprise 实现了受 Regulation (EU) No 910/2014 Article 8(保证级别)和 Article 3(12)(合格电子签名的构成要素)以及 ETSI 信任列表限定符词汇表启发的行为。支持不等于一致性,一致性不等于认证。NextPDF 不持有任何认证,也不授予任何认证。NextPDF 不是合格信任服务提供者,不是合规评估机构,也不是监管机构。一个 LoaAssertion 是对已发布证据的软件分类。它不是对资格或保证的法律裁定,也无法使一个签名成为合格签名。

Regulation (EU) 2024/1183 (eIDAS 2) 继续引用 Article 8 的级别,并要求 European Digital Identity Wallets 在 high 保证级别下提供。本页将其作为监管背景加以引用;NextPDF 不做任何与钱包相关的能力声明。

某个特定签名是否满足结构性的 eIDAS 导向策略,是一个单独的问题,由验证模块回答;参见 验证

eIDAS LoA 相关的类不执行任何密码学操作——没有哈希,没有签名验证,没有随机性。Enterprise FIPS 模式策略对密码学选择进行门控,因此在本模块中它没有任何可门控的对象。启用 FIPS 模式不会改变 LoA 映射或 PII 守护行为。签名和信任列表的密码学验证由验证模块和安全模块管辖,FIPS 模式策略在那里适用。

  • LoaMapping::loaForService() 是全函数且确定性的。每个 TspService 都会产出一个 LoaAssertion;该方法从不抛出异常,也不查询任何时钟、网络或全局状态。
  • 分类是保守的。缺失、未知或冲突的证据会降低级别;除了明确的已授予-CA/QC-带-QSCD 证据外,没有任何东西会提升级别。
  • 原因代码是机器可读且稳定的:service_not_grantedservice_not_qualified_caca_qc_with_qscdesig_or_esealqc_defaultca_qc_no_qscd_or_unspecified
  • 级别顺序是固定的:Low < Substantial < High,通过 rank()meetsOrExceeds() 暴露。
  • CertPiiGuard 默认进行脱敏,并在词法切分存疑时失败关闭。在 disclosePii: true 时,每个守护方法都会原样返回其输入。
  • 对于相同输入,守护输出是字节稳定的。

NextPDF Core 对 PDF 签名进行密码学验证,并在证据损坏时失败关闭。Core 没有欧盟信任列表模型,没有 LoaLevel 词汇表,没有 LoA 映射,也没有用于审计序列化的 eIDAS 层 PII 守护。仅使用 Core 时,你必须自行从你所维护的信任数据推导保证级别分类,并在审计记录离开进程之前应用你自己的脱敏。

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