跳转到内容

契约 / 签章

签章领域包含六项契约。它们定义如何生成 CMS 签章、应用 RFC 3161 时间戳、使用硬件密钥签章,并启用长期验证。Core 发布这些契约;Pro 和 Enterprise 版本提供生产环境实现。

Terminal window
composer require nextpdf/core:^3

PDF 数字签章是存储在签章字典中的 CMS SignedData 结构。Contents 条目存放 DER 编码结构。ByteRange 条目指明摘要覆盖的字节范围。摘要覆盖整个文件,但排除签章值本身 —— 见 ISO 32000-2 §12.8.1。CMS 结构遵循 RFC 5652 §5.1:依次包含版本、摘要算法、封装内容和签章者信息。

SignerInterface 是核心契约。它为一段字节范围生成 CMS SignedData、为签章值应用时间戳,并报告是否支持长期验证。它承载 ETSI EN 319 142 定义的内容,覆盖从 PAdES 基线等级 B-B 到 B-LTA 的各个级别。每提升一个级别,都会增加对应材料。B-B 承载基本签章。B-T 添加签章时间戳。B-LT 添加撤销数据。B-LTA 添加归档时间戳。

HsmSignerInterface 使用存放在硬件安全模块(HSM)内的密钥进行签章。私钥不会离开硬件边界。此契约以 DER 形式返回签章者证书和证书链,供 CMS 层构建结构。DeferredSignerInterface 扩展 SignerInterface,用于支持异步签章。调用方提交数据、取得一个工作标识符、轮询完成状态,然后取回结果。当远程 HSM 或云密钥服务无法立即返回结果时,可以使用它。

TimestampProviderInterface 会请求一个 RFC 3161 时间戳令牌。该协议通过与时间戳机构(TSA)交换请求和响应来工作 —— 见 RFC 3161 §2.4。令牌中的 messageImprint 是签章值的哈希 —— RFC 3161 §2.4.2。LtvManagerInterface 用于启用长期验证。它会收集证书链、获取 OCSP 和 CRL 响应、构建文档安全存储区,并为 B-LTA 添加文档时间戳。CryptoPolicyInterface 会在任何密码学运算运行之前,控制允许使用的哈希、签章和加密算法,以及允许的密钥强度。

类型类别主要成员稳定性起始版本
SignerInterface接口sign(string): SignatureResulttimestamp(string): stringsupportsLtv(): bool稳定1.0.0
HsmSignerInterface接口sign(string, string): stringgetCertificateDer()getCertificateChainDer()getPublicKeyAlgorithm()稳定1.0.0
DeferredSignerInterface接口submitForSigning(string): stringretrieveSignature(string)isComplete(string)(扩充 SignerInterface实验性3.0.0
TimestampProviderInterface接口getTimestamp(string): string实验性3.0.0
LtvManagerInterface接口enableLtv(...)addDocumentTimestamp(...)稳定1.10.0
CryptoPolicyInterface接口isHashAlgorithmAllowed()isSignatureAlgorithmAllowed()isEncryptionAlgorithmAllowed()isKeyStrengthAllowed()getPreferredHashAlgorithm()getName()稳定1.9.0

SignerInterface::sign() 会返回一个 NextPDF\Security\Signature\SignatureResult。公开的 $cmsSignedData 属性存放 DER 编码的 CMS。公开的 $digestHex 属性存放 SHA-256 摘要。公开的 $timestampToken 属性存放可选的 TSA 令牌。访问方法包括 toHex() / toHexPadded(int) / getSize() / hasTimestamp()HsmSignerInterface::sign() 对 RSA 返回 DER 编码字节,对 ECDSA 返回原始 r‖s 字节。算法参数使用 OpenSSL 标识符。

examples/contracts/signing-quickstart.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\SignerInterface;
/**
* Sign a byte range with any SignerInterface implementation.
*
* @param SignerInterface $signer A core or Premium signer.
* @param string $byteRange The PDF byte range to sign.
*
* @return string Hex-encoded CMS SignedData for the PDF /Contents field.
*/
function signByteRange(SignerInterface $signer, string $byteRange): string
{
$result = $signer->sign($byteRange);
return $result->toHex();
}

SignerInterface::sign() 会返回一个 SignatureResulttoHex() 会生成写入器放入 /Contents 字段的十六进制字符串;原始 DER 字节则保存在公开的 $cmsSignedData 属性上。此函数依赖的是契约,而不是具体类。无论是 Core 的测试签章器,还是 Premium 的 HSM 签章器,都能满足这个契约。

examples/contracts/signing-production.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\CryptoPolicyInterface;
use NextPDF\Contracts\SignerInterface;
use NextPDF\Contracts\TimestampProviderInterface;
use NextPDF\Exception\NextPdfException;
use Psr\Log\LoggerInterface;
final readonly class TimestampedSigningService
{
public function __construct(
private SignerInterface $signer,
private TimestampProviderInterface $timestamps,
private CryptoPolicyInterface $policy,
private LoggerInterface $logger,
) {}
/**
* Sign a byte range, then timestamp the CMS structure.
*
* @param string $byteRange The PDF byte range to sign.
*
* @return array{cms: string, digest: string, tst: string}
*/
public function sign(string $byteRange): array
{
if (!$this->policy->isHashAlgorithmAllowed($this->policy->getPreferredHashAlgorithm())) {
throw new \LogicException('Preferred hash rejected by crypto policy.');
}
try {
$signature = $this->signer->sign($byteRange);
$token = $this->timestamps->getTimestamp($signature->cmsSignedData);
return [
'cms' => $signature->toHex(),
'digest' => $signature->digestHex,
'tst' => $token,
];
} catch (NextPdfException $e) {
$this->logger->error('Signing failed', [
'policy' => $this->policy->getName(),
'error' => $e->getMessage(),
]);
throw $e;
}
}
}

此服务注入三项契约。运行签章运算之前,会先检查密码学原则。SignatureResult 通过公开的 $cmsSignedData 属性暴露 CMS 字节,通过 $digestHex 暴露 SHA-256 摘要,并使用 toHex() 生成 /Contents 十六进制字符串。时间戳提供者接收 CMS 字节,并返回一个 DER 编码令牌。catch 块会记录原则名称并重新抛出异常。该服务绝不会吞掉失败。

  • 字节范围摘要必须排除签章值。覆盖 Contents 条目的摘要会生成永远无法验证的签章 —— ISO 32000-2 §12.8.1。
  • SignerInterface::supportsLtv() 反映的是能力,而不是状态。签章器可以支持长期验证,但未配置时间戳服务时,仍会生成 B-B 签章。
  • DeferredSignerInterface::retrieveSignature() 在工作完成之前始终返回 null。请先轮询 isComplete(),避免每次检查都传输负载数据。结果一旦存在,取回操作就是幂等的。
  • LtvManagerInterface::addDocumentTimestamp() 必须在文档安全存储区写入之后才运行。若先调用它,会生成无效的 B-LTA 结构。
  • CryptoPolicyInterface 在未配置任何原则时,会对每个算法返回 true。在受监管环境中,请配置明确的原则;不要依赖这个开放的默认值。
  • HsmSignerInterface::getCertificateChainDer() 不包含签章者证书。请使用 getCertificateDer() 获取签章者叶证书,并使用链方法获取中间证书。

签章成本主要取决于密码学运算和网络往返,而不是契约本身。本机软件签章通常为个位数毫秒。HSM 签章会增加设备往返时间。时间戳会增加一次到时间戳机构的网络往返。长期验证会为链中的每张证书增加一次 OCSP 或 CRL 获取。1500 毫秒墙钟时间的 performance_budget 覆盖连接已预热时、使用远程 TSA 的单个带时间戳签章。针对缓慢撤销端点的长期验证会超出此预算,应在请求路径之外运行。可重现性配置文件是 structural,而非 bitwise。时间戳会嵌入签章发生的瞬间时刻,因此两次运行的时间戳字节会有差异,而文档结构会保持相同。

签章契约是引擎主要的密码学边界,因此威胁模型很明确。密钥保管是第一项考量:HsmSignerInterface 将私钥保留在硬件边界内,且此契约绝不会暴露密钥材料。算法降级是第二项考量:CryptoPolicyInterface 会在运算之前阻止弱哈希和过短密钥,使部署无需分支引擎即可强制应用 FIPS 140-3 或 eIDAS 配置文件。时间戳信任是第三项考量:RFC 3161 令牌的可信度仅限于其时间戳机构,因此提供者契约可注入,使部署能够固定选用自己的时间戳机构。长期验证是第四项考量:撤销材料会在签章时获取并存储在文档安全存储区,使验证在证书到期后仍能维持。请将每一笔签章器输入都视为不可信。字节范围由引擎计算,绝不接受调用方提供的字节范围。由于这些契约掌管密码学签章,本页标示为 export_control_class: legal-review-required。按照引用卫生原则,本文释义所有规范性来源,并未引述其原文。

主张标准条款证据
签章值以 DER 编码存储在签章字典的 Contents 条目中,形式为 CMS SignedData 或 TimeStampToken。ISO 32000-2§12.8.1
摘要针对 ByteRange 数组定义的字节范围计算,并排除签章值。ISO 32000-2§12.8.1
长期验证材料承载于文档安全存储区,其中包含 VRI、OCSP、CRL 和证书条目。ISO 32000-2§12.8.4.3
PAdES 长期验证会将验证数据放入 DSS 和 VRI 字典中。ETSI EN 319 142-2§6.3
RFC 3161 时间戳令牌通过 messageImprint 绑定签章值的哈希,并通过 TSA 请求和响应进行交换。RFC 3161§2.4.2、§2.4
CMS SignedData 序列承载版本、摘要算法、封装内容和签章者信息。RFC 5652§5.1

所有条款均为释义。NextPDF 不会重制规范性文本。如需权威措辞,请查阅已发布的标准。

Core 发布并冻结签章契约。HsmSignerInterfaceLtvManagerInterface 以及延迟签章器背后的生产环境实现,会随 Pro 和 Enterprise 版本一同提供,包含 PKCS#11 硬件集成以及 PAdES B-LT 与 B-LTA。Core 会在运行阶段通过 class_exists() resolve(解析)这些实现并转换为契约,因此开源引擎不带任何商业依赖,且 API 在升级时不会改变。