You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

重载已签名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,但原有签名会失效。

问题原因

  1. VRI字典处理不符合规范:DSS中的VRI(Validation Reference Information)是按证书哈希值作为键的字典,每个键对应该证书的验证信息(OCSP/CRL),代码中直接给VRI设置"OCSP"键,会覆盖原有VRI内容,且不符合PDF规范,导致增量保存时该部分修改不被正确识别;
  2. 增量保存的变更追踪问题:虽然标记了setNeedsTobeUpdated(true),但新增的流对象(OCSP/CRL/证书流)未被正确关联到文档的变更集合中,且增量保存时PDFBox未将修改后的DSS字典纳入增量更新范围;
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 22:47:18