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
相关产品推荐
相关产品推荐

