如何用PHP为PDF签名添加RFC3161时间戳?TCPDF无此功能
我明白你遇到的问题:TCPDF的applyTSA功能还不支持RFC3161标准的时间戳,但你需要符合ISO 32000-1:2008(PDF 1.6)的要求,把时间戳作为未签名属性嵌入PKCS#7对象中。下面我分两种思路给你解决方法,包括手动实现和现成开源库的示例。
核心原理回顾
首先再明确下关键要求(你已经提到,但再梳理下方便后续操作):
- 时间戳令牌必须符合RFC3161标准,嵌入PKCS#7的
SignerInfo的未签名属性中 - 对应的对象标识符(OID)是:
1.2.840.113549.1.9.16.2.14(即id-aa-timeStampToken) TimeStampToken里的messageImprint字段,必须是签名的SignerInfo中signature字段的哈希值(不是整个文档的哈希)
方案一:基于TCPDF手动实现
如果不想换库,你可以分四步手动完成时间戳的嵌入:
1. 用TCPDF生成签名框架,获取签名哈希
先让TCPDF生成带签名占位符的PDF,同时获取SignerInfo中signature字段的原始值(或者其哈希)。你可以通过修改TCPDF的签名相关代码,或者在签名过程中拦截这个值——比如在调用Sign()方法后,提取签名的PKCS#7结构,解析出SignerInfo里的signature字段。
2. 向RFC3161时间戳服务器请求令牌
用OpenSSL命令行或者PHP的openssl_*函数,把刚才获取的signature字段的哈希作为messageImprint,向TSA服务器请求时间戳:
命令行示例:
# 假设你已经把signature字段的二进制内容保存到sig.bin openssl ts -query -data sig.bin -cert -sha256 | curl -H "Content-Type: application/timestamp-query" --data-binary @- https://your-tsa-server.com > ts_response.tsr
PHP代码示例:
$signatureBinary = file_get_contents('sig.bin'); $tsQuery = openssl_ts_query_create($signatureBinary, 'sha256', true); $tsResponse = file_get_contents('https://your-tsa-server.com', false, stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => 'Content-Type: application/timestamp-query', 'content' => $tsQuery ] ])); file_put_contents('ts_response.tsr', $tsResponse);
3. 把时间戳令牌嵌入PKCS#7的未签名属性
这一步需要操作PKCS#7的ASN.1结构,把时间戳作为id-aa-timeStampToken属性添加到SignerInfo的unsignedAttributes中。你可以用PHP的ASN.1解析库(比如phpseclib/phpseclib)来完成:
use phpseclib3\File\ASN1; use phpseclib3\File\X509; // 读取原始的PKCS#7签名 $p7s = file_get_contents('original_signature.p7s'); $asn1 = ASN1::decodeBER($p7s); // 读取时间戳令牌的DER编码 $tsToken = file_get_contents('ts_response.tsr'); // 构造未签名属性的ASN.1结构 $unsignedAttrs = [ 'type' => ASN1::SET, 'children' => [ [ 'type' => ASN1::SEQUENCE, 'children' => [ ['type' => ASN1::OBJECT_IDENTIFIER, 'value' => '1.2.840.113549.1.9.16.2.14'], [ 'type' => ASN1::SET, 'children' => [ ['type' => ASN1::SEQUENCE, 'value' => $tsToken] ] ] ] ] ] ]; // 将构造好的属性插入到SignerInfo中(需根据实际ASN.1解析结果调整节点位置) $asn1[0]['children'][0]['children'][3]['children'][0]['children'][4] = $unsignedAttrs; // 重新编码为DER格式 $modifiedP7s = ASN1::encodeDER($asn1); file_put_contents('modified_signature.p7s', $modifiedP7s);
4. 把修改后的PKCS#7嵌入PDF
最后用TCPDF的setSignature()方法,传入修改后的PKCS#7文件,替换原来的签名内容,完成PDF的签名和时间戳嵌入。
方案二:使用支持RFC3161的开源库
如果不想手动处理复杂的ASN.1结构,推荐使用以下现成库,它们原生支持RFC3161时间戳:
1. mPDF(推荐,PHP库)
mPDF是一个功能完善的PHP PDF生成库,其签名模块原生支持RFC3161时间戳,配置非常简单:
require_once __DIR__ . '/vendor/autoload.php'; // 初始化mPDF $mpdf = new \Mpdf\Mpdf(); $mpdf->WriteHTML('<h1>已签名并添加时间戳的文档</h1>'); // 配置签名和时间戳参数 $signatureConfig = [ 'file' => '/path/to/your/certificate.pfx', // 你的PFX证书文件 'password' => 'your-pfx-password', // 证书密码 'location' => '北京', 'reason' => '文档合法性验证', 'contact' => 'admin@example.com', 'tsa' => [ 'url' => 'https://tsa.example.com', // RFC3161时间戳服务器地址 'hash' => 'sha256', // 哈希算法 'username' => '', // 若TSA需要认证,填写用户名 'password' => '' // 认证密码 ] ]; // 添加签名和时间戳 $mpdf->SetSignature($signatureConfig); // 输出PDF $mpdf->Output('signed_with_ts.pdf', 'I');
2. PDFLib(PHP绑定)
PDFLib是一个专业的PDF处理库,其pdc_sign_pdf函数支持直接添加RFC3161时间戳作为签名属性,适合复杂的PDF处理场景。
3. iText 7(Java/ .NET/ Python)
如果你不限于PHP,iText 7是行业标准的PDF处理库,完全支持RFC3161时间戳的嵌入,文档和示例非常丰富。
注意事项
- 确保你的时间戳服务器支持RFC3161协议,比如全球通用的
https://freetsa.org/tsr(免费测试用) - 哈希算法要和签名使用的算法一致(比如签名用SHA256,时间戳也要用SHA256)
- 手动处理PKCS#7时,一定要严格遵循ASN.1编码规则,避免结构错误导致PDF签名验证失败
内容的提问来源于stack exchange,提问作者h2odev

