使用PDFBox实现PDF签名时,如何确保各环节签名时间一致?
问题
我正在使用PDFBox实现带外部TSA(时间戳授权中心)的PDF数字签名,操作步骤如下:
- 创建PDDocument和PDSignature实例;
- 根据需求为签名域添加外观;
- 配置PDSignature的sub-filter、signDate等参数;
- 将PDSignature添加到PDDocument;
- 生成PDF哈希并完成签名;
- 使用签名摘要获取时间戳令牌,将其作为未签名属性添加;
- 生成输出PDF。
我需要在步骤2设置签名外观的日期,步骤3设置PDSignature.signDate,但时间戳令牌要到步骤6才能获取。据我了解,步骤6后无法修改PDSignature.signDate和签名外观的日期。我的需求是让PDF的未签名属性、PDSignature.signDate以及签名外观中的签名时间保持一致,请问是否有办法实现这一点?
解决方案
核心思路是先获取TSA时间戳,再基于该时间戳完成签名外观和PDSignature的配置,调整原有步骤顺序即可实现三者一致:
提前获取TSA时间戳
- 先生成一个用于请求TSA的临时哈希(可以用固定测试字符串生成,或提前计算文档的预签名哈希),向TSA服务器请求时间戳令牌;
- 解析令牌提取出标准的
SigningTime时间值,作为后续统一使用的时间戳。
基于TSA时间戳配置签名参数与外观
- 直接用提取到的时间戳设置
PDSignature.setSignDate(); - 绘制签名外观时,将该时间戳作为签名文本中的日期显示(通过
PDAppearanceContentStream绘制文本时直接引用这个时间值)。
- 直接用提取到的时间戳设置
执行常规签名流程
- 把配置好的PDSignature添加到文档;
- 生成正式PDF哈希并完成签名;
- 将提前获取的TSA令牌作为未签名属性添加到签名数据中。
关键代码示例
// 1. 提前获取并解析TSA时间戳 TSAClient tsaClient = new TSAClient(new URL("你的TSA地址"), "用户名", "密码", 60); // 生成临时哈希(或提前计算文档预签名哈希) byte[] tempHash = MessageDigest.getInstance("SHA-256").digest("temp-data".getBytes()); byte[] tsaToken = tsaClient.getTimeStampToken(tempHash); // 解析令牌中的时间戳 CMSSignedData signedData = new CMSSignedData(tsaToken); SignerInformation signer = (SignerInformation) signedData.getSignerInfos().getSigners().iterator().next(); Attribute signingTimeAttr = signer.getSignedAttributes().get(new ASN1ObjectIdentifier("1.2.840.113549.1.9.5")); Date tsaDate = SigningTime.getInstance(signingTimeAttr.getAttrValues().getObjectAt(0)).getDate(); // 2. 配置PDSignature和签名外观 PDSignature signature = new PDSignature(); signature.setSignDate(tsaDate); signature.setSubFilter(COSName.getPDFName("adbe.pkcs7.detached")); // 创建签名外观并绘制统一时间戳 PDPage targetPage = document.getPage(0); PDSignatureField signatureField = new PDSignatureField(document); signatureField.setValue(signature); signatureField.setPage(targetPage); PDAppearanceStream appearanceStream = new PDAppearanceStream(document); appearanceStream.setBBox(new PDRectangle(100, 100, 300, 50)); try (PDAppearanceContentStream cs = new PDAppearanceContentStream(document, appearanceStream)) { cs.beginText(); cs.setFont(PDType1Font.HELVETICA, 12); cs.newLineAtOffset(110, 120); cs.showText("签名时间: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(tsaDate)); cs.endText(); } PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary(); appearanceDict.setNormalAppearance(appearanceStream); signatureField.setAppearance(appearanceDict); document.getDocumentCatalog().getAcroForm().getFields().add(signatureField); // 3. 完成签名流程 document.addSignature(signature); ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(new FileOutputStream("输出文件.pdf")); byte[] docHash = MessageDigest.getInstance("SHA-256").digest(externalSigning.getContent()); // 私钥签名生成签名值(替换为你的签名逻辑) PrivateKey privateKey = ...; byte[] signatureValue = Signature.getInstance("SHA256withRSA").sign(privateKey, docHash); // 将TSA令牌作为未签名属性整合到签名数据 ASN1EncodableVector unsignedAttrs = new ASN1EncodableVector(); unsignedAttrs.add(new Attribute(new ASN1ObjectIdentifier("1.2.840.113549.1.9.16.2.14"), new DERSet(new DEROctetString(tsaToken)))); // 组装包含未签名属性的最终签名数据(省略具体组装逻辑,可参考PDFBox官方外部签名示例) byte[] finalSignedData = assembleSignedData(docHash, signatureValue, unsignedAttrs); externalSigning.setSignature(finalSignedData);
注意事项
- 若担心临时哈希不符合TSA服务器要求,可以先执行原步骤5的文档哈希计算(不签名),用这个真实的文档哈希请求TSA,更合规;
- 确保签名外观的日期格式与PDF阅读器默认显示格式一致,避免视觉差异;
- 提前获取的TSA令牌可以直接复用为未签名属性,不会影响签名的合法性。
内容的提问来源于stack exchange,提问作者Qazazazaz
相关产品推荐
相关产品推荐

