重载已签名PDF后无法添加DSS的技术问题求助
问题分析与解决方案
问题背景
给PDF添加签名时间戳时,无法同时为时间戳证书添加DSS,因此先完成签名并保存,再重载文档尝试为时间戳证书补充DSS。调试时可见时间戳证书已成功加入DSS,但增量保存后输出的PDF仅保留签名证书的验证信息,时间戳相关内容丢失。
核心代码
主流程代码
// 保存签名后的文件到responseStream ByteArrayInputStream responseStream = new ByteArrayInputStream(outputStream.toByteArray()); // 重新加载PDF,准备为TSA证书嵌入DSS PDDocument documentForLTV = PDDocument.load(responseStream); // 用于存储添加DSS后的输出流 ByteArrayOutputStream outputStreamLTV = new ByteArrayOutputStream(); // 为文档DSS添加TSA证书的吊销信息与证书本身 addValidationInformation(documentForLTV, TSACertificates); // 增量保存文档 documentForLTV.saveIncremental(outputStreamLTV); // 调试:此时DSS中已包含预期的TSA证书 PDDocumentCatalog docCatalog = documentForLTV.getDocumentCatalog(); COSDictionary catalog = docCatalog.getCOSObject(); COSDictionary dss = validationService.getOrCreateDictionaryEntry(COSDictionary.class, catalog, "DSS"); COSArray certs = validationService.getOrCreateDictionaryEntry(COSArray.class, dss, "Certs"); // 最终输出流:不包含TSA证书及吊销信息 ByteArrayInputStream responseStreamLTV = new ByteArrayInputStream(outputStreamLTV.toByteArray()); documentForLTV.close(); return responseStreamLTV;
addValidationInformation方法代码
public void addValidationInformation(PDDocument document, Certificate[] certificateChain) { List<String> ocspResponse = getOcspData(certificateChain); List<String> crlResponse = getClrData(certificateChain); List<String> certificates = getCertificates(certificateChain); PDDocumentCatalog docCatalog = document.getDocumentCatalog(); COSDictionary catalog = docCatalog.getCOSObject(); catalog.setNeedsTobeUpdated(true); // 参考PDFBox示例创建/获取DSS字典 COSDictionary dss = getOrCreateDictionaryEntry(COSDictionary.class, catalog, "DSS"); dss.setNeedsTobeUpdated(true); COSDictionary vri = getOrCreateDictionaryEntry(COSDictionary.class, dss, "VRI"); vri.setNeedsTobeUpdated(true); COSArray ocsps = getOrCreateDictionaryEntry(COSArray.class, dss, "OCSPs"); ocsps.setNeedsTobeUpdated(true); COSArray crls = getOrCreateDictionaryEntry(COSArray.class, dss, "CRLs"); crls.setNeedsTobeUpdated(true); COSArray certs = getOrCreateDictionaryEntry(COSArray.class, dss, "Certs"); certs.setNeedsTobeUpdated(true); if (ocspResponse != null) { ocspResponse.forEach(ocsp -> { byte[] ocspData = b64Decoder.decode(ocsp); try { ocsps.add(writeDataToStream(ocspData, document)); } catch (IOException e) { e.printStackTrace(); } }); } if (!ocspResponse.isEmpty()) { vri.setItem("OCSP", ocsps); } // CRL与证书的处理逻辑类似... }
排查发现
- 使用PDFBox官方示例
AddValidationInformation可实现需求,但会产生重复代码且不符合现有架构; - 若使用
PDDocument.save()而非增量保存,输出PDF包含预期DSS,但原有签名会失效。
问题原因
- VRI字典处理不符合规范:DSS中的VRI(Validation Reference Information)是按证书哈希值作为键的字典,每个键对应该证书的验证信息(OCSP/CRL),代码中直接给VRI设置
"OCSP"键,会覆盖原有VRI内容,且不符合PDF规范,导致增量保存时该部分修改不被正确识别; - 增量保存的变更追踪问题:虽然标记了
setNeedsTobeUpdated(true),但新增的流对象(OCSP/CRL/证书流)未被正确关联到文档的变更集合中,且增量保存时PDFBox未将修改后的DSS字典纳入增量更新范围; - DSS对象的复用问题:若文档已有DSS,直接重新创建或修改时未正确继承原有内容的变更标记,导致增量保存时只保留旧的DSS内容。
修复方案
1. 修正VRI字典的处理逻辑
VRI的每个条目需以证书的SHA-1哈希(Hex编码)作为键,值为包含该证书验证信息的字典,而非直接设置全局"OCSP"键。示例代码如下:
// 处理单证书的OCSP信息(以证书链中第一个证书为例) if (ocspResponse != null && !ocspResponse.isEmpty() && certificateChain.length > 0) { X509Certificate cert = (X509Certificate) certificateChain[0]; String certHash = getCertificateSHA1Hash(cert); // 实现证书SHA-1哈希的Hex编码方法 COSDictionary certVri = getOrCreateDictionaryEntry(COSDictionary.class, vri, certHash); certVri.setNeedsTobeUpdated(true); COSArray certOcsps = new COSArray(); for (String ocspStr : ocspResponse) { byte[] ocspData = b64Decoder.decode(ocspStr); certOcsps.add(writeDataToStream(ocspData, document)); } certVri.setItem("OCSP", certOcsps); }
2. 确保增量保存的变更被正确追踪
在增量保存前,需确保所有修改的COS对象都被标记为需要更新,并且新增的流对象被添加到文档的对象池中:
// 在addValidationInformation方法末尾添加 document.getDocumentCatalog().getCOSObject().setNeedToBeUpdated(true); // 确保DSS及子对象都被标记 dss.setNeedToBeUpdated(true); vri.setNeedToBeUpdated(true); ocsps.setNeedToBeUpdated(true); crls.setNeedToBeUpdated(true); certs.setNeedToBeUpdated(true); // 主流程中,增量保存前强制刷新文档的变更集合 documentForLTV.getDocumentCatalog().getCOSObject().setNeedToBeUpdated(true);
3. 调整DSS的获取与修改逻辑
优先复用文档中已有的DSS字典,而非重新创建,确保原有内容被保留并追加新内容:
// 修改getOrCreateDictionaryEntry方法的逻辑,若存在则直接返回,而非创建新的 public <T extends COSBase> T getOrCreateDictionaryEntry(Class<T> clazz, COSDictionary parent, String key) { COSBase entry = parent.getDictionaryObject(key); if (entry != null && clazz.isInstance(entry)) { return clazz.cast(entry); } try { T newEntry = clazz.getDeclaredConstructor().newInstance(); parent.setItem(key, newEntry); return newEntry; } catch (Exception e) { throw new RuntimeException("Failed to create dictionary entry", e); } }
4. 验证增量保存的正确性
增量保存时,PDFBox会基于原文档的字节流生成增量更新,需确保原文档流未被篡改,且加载文档时使用正确的解析模式:
// 加载已签名文档时,使用内存加载模式,避免文件锁或流问题 PDDocument documentForLTV = PDDocument.load(responseStream, MemoryUsageSetting.setupTempFileOnly());
结论
这不是PDFBox的Bug,而是代码不符合PDF DSS规范以及增量保存的变更追踪逻辑导致的问题。按照上述方案修正后,可在保留原有签名有效性的前提下,通过增量保存为时间戳证书添加DSS信息。
内容的提问来源于stack exchange,提问作者Qazazazaz
相关产品推荐
相关产品推荐

