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

PDFBox使用Certification签名时如何添加验证信息解决DSS警告

Certification类型PDF签名添加合规验证信息的实现方案

问题本质

设置MDP权限等级1时,规则为禁止所有文档修改。虽然PDF规范明确说明,添加DSS(文档安全存储)字典存放LTV验证信息属于合规的增量更新操作,但多数旧版PDF阅读器(包括部分版本的Adobe Acrobat)未严格遵循该规范,会将签名完成后追加的DSS修改判定为违规篡改,直接提示签名无效,这就是警告信息触发的核心原因。

提前写入DSS的思路方向正确,但现有实现存在三个会导致签名异常的问题:

  • DSS写入时机错误:在doc.addSignature()前直接挂载DSS到目录字典,未将DSS内容纳入签名的哈希计算范围,且缺少VRI(签名验证关联)字典对应当前签名的条目,无法通过标准LTV校验。
  • 存在代码逻辑缺陷:证书链遍历的循环边界未做限制,取i+1位置的证书时会触发数组越界。
  • MDP权限设置时机错误:若在DSS写入前调用setMDPPermission,会导致权限标记的文档初始状态与实际签名内容不匹配,触发校验失败。

正确实现步骤

  • 调整操作顺序:所有文档内容修改(包括DSS构造、验证数据填充)必须在MDP权限设置、最终签名写入前完成,保证DSS内容完全被签名哈希保护,签名完成后不对文档做任何修改,从根源上避免增量更新触发的篡改误判。
  • 补全DSS结构:除了证书链、CRL、OCSP数据外,必须在DSS下新增VRI字典,以当前签名字段的签名值SHA1哈希(十六进制大写格式)为键,关联该签名对应的验证数据引用,让阅读器可以直接匹配到签名对应的验证材料,无需联网拉取。
  • 正确标记更新属性:新建的DSS字典、VRI字典、Certs/CRLs/OCSPs数组、所有嵌入的验证数据COS对象,都要调用setNeedToBeUpdate(true),避免被签名库忽略未写入最终文档。
  • 固定内容后设置权限:等所有DSS内容构造完成、文档内容不再变更后,再调用SigUtils.setMDPPermission(doc, signature, 1),保证MDP标记的初始文档状态包含完整LTV数据。

核心参考代码

流程顺序调整:

// 1. 初始化文档、签名字段、签名接口,配置基础签名参数
doc.addSignature(signature, signatureInterface, signatureOptions);

// 2. 构造完整LTV验证数据,传入签名字段对象用于VRI关联
makeLTV(signature);

// 3. 所有文档内容固定后,设置MDP认证权限
SigUtils.setMDPPermission(doc, signature, 1);

// 4. 执行签名写入,完成后不再修改文档
signing.signPDF();

修正后的makeLTV方法:

private void makeLTV(PDSignature signature) {
    try {
        COSDictionary catalogDict = doc.getDocumentCatalog().getCOSObject();
        catalogDict.setNeedToBeUpdated(true);
        byte[][] certs = new byte[certificateChain.length][];
        for (int i = 0; i < certificateChain.length; i++) {
            certs[i] = certificateChain[i].getEncoded();
        }
        List<CRL> crlList = new ArrayList<CRL>();
        List<OCSPResp> ocspList = new ArrayList<OCSPResp>();
        // 修复原代码数组越界问题,循环上限调整为证书链长度-1
        for (int i = 0; i < certificateChain.length - 1; i++) {
            X509Certificate cert = (X509Certificate) certificateChain[i];
            if (!cert.getIssuerDN().equals(cert.getSubjectDN())) {
                X509Certificate issuerCert = (X509Certificate) certificateChain[i + 1];
                OCSPResp ocspResp = new GetOcspResp().getOcspResp(cert, issuerCert);
                if (ocspResp != null) {
                    ocspList.add(ocspResp);
                }
                crlList.addAll(new DssHelper().readCRLsFromCert(cert));
            }
        }
        byte[][] crls = new byte[crlList.size()][];
        for (int i = 0; i < crlList.size(); i++) {
            crls[i] = ((X509CRL) crlList.get(i)).getEncoded();
        }
        byte[][] ocsps = new byte[ocspList.size()][];
        for (int i = 0; i < ocspList.size(); i++) {
            ocsps[i] = ocspList.get(i).getEncoded();
        }
        COSDictionary dss = new DssHelper().createDssDictionary(Arrays.asList(certs), Arrays.asList(crls),
                Arrays.asList(ocsps));
        dss.setNeedToBeUpdated(true);
        // 标记DSS下所有子对象需要写入文档
        COSDictionary vriDict = dss.getCOSDictionary(COSName.getPDFName("VRI"));
        vriDict.setNeedToBeUpdated(true);
        dss.getCOSArray(COSName.getPDFName("Certs")).setNeedToBeUpdated(true);
        dss.getCOSArray(COSName.getPDFName("CRLs")).setNeedToBeUpdated(true);
        dss.getCOSArray(COSName.getPDFName("OCSPs")).setNeedToBeUpdated(true);
        catalogDict.setItem(COSName.getPDFName("DSS"), dss);
        // 可通过签名接口回调,在计算出签名值后,补全VRI字典中当前签名对应的键值关联
    } catch (Exception e) {
        LogSystem.error(e.toString());
        e.printStackTrace();
    }
}

补充说明

  • PDFBox 2.0.x及以上版本官方提供的addValidationInformation方法默认采用签名后增量写入DSS的逻辑,天生不兼容MDP等级1的认证签名场景,不要直接使用。
  • 禁止对MDP等级1的签名采用签名后追加DSS的实现方式,哪怕符合PDF规范,也会存在大量阅读器兼容性问题,将DSS纳入签名保护范围是目前兼容性最优的方案。

内容的提问来源于stack exchange,提问作者blinkbink

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 19:15:39