使用iText实现PDF延迟签名时遇CMSSignerDigestMismatchException问题求助
解决iText延迟签名的CMSSignerDigestMismatchException问题
问题背景
使用Java 11、BouncyCastle 1.70、iText 7.0.4实现PDF延迟签名时,出现CMSSignerDigestMismatchException: message-digest attribute value does not match calculated value错误,且PDF查看器提示“文档在签名后已被修改”。
核心问题分析
出现摘要不匹配的根本原因是文档哈希计算错误或签名数据生成/合并逻辑不完整,结合你的代码,主要有以下几个问题点:
- 文档哈希计算未使用iText提供的、自动排除签名占位符的专用流,导致计算范围错误
- 远程签名环节仅初始化
PdfPKCS7对象,未将远程返回的签名值转化为合法的CMS签名数据 - 签名合并环节变量引用不明确,字节填充逻辑存在对齐隐患
修复后的完整实现步骤
步骤1:获取完整证书链
CertificateFactory cf = CertificateFactory.getInstance("X509"); // 注意:证书链需包含自身证书+所有上级CA证书,仅用自身证书会导致验证失败 List<Certificate> certList = new ArrayList<>(); certList.add(cf.generateCertificate(new ByteArrayInputStream(Hex.decode(certHexStr)))); // 若有上级CA证书,在此处依次添加 Certificate[] chain = certList.toArray(new Certificate[0]);
步骤2:添加空签名域并获取文档哈希
// reader需为只读模式的PdfReader,os为临时文件输出流(用于生成待签名的PDF) PdfStamper stamper = PdfStamper.createSignature(reader, os, '\0', null, true); PdfSignatureAppearance appearance = stamper.getSignatureAppearance(); String fieldName = appearance.getFieldName(); document.setSigField(fieldName); logger.info("Signature fieldName: " + fieldName); appearance.setCertificate(chain[0]); // 配置签名外观信息 if (document.getSignReq() != null) { SignRequest signReq = document.getSignReq(); if (StringUtils.isNotBlank(signReq.getReason())) { appearance.setReason(signReq.getReason()); } if (StringUtils.isNotBlank(signReq.getLocation())) { appearance.setLocation(signReq.getLocation()); } int pageNumber = signReq.getPageNumber() == 0 ? 1 : signReq.getPageNumber(); Rectangle rect = new Rectangle(signReq.getLeftX(), signReq.getLeftY(), signReq.getRightX(), signReq.getRightY()); logger.info(rect.toString()); appearance.setVisibleSignature(rect, pageNumber, fieldName); } // 关键:获取自动排除签名占位符的摘要计算流 ExternalDigest externalDigest = new BouncyCastleDigest(); PdfSignatureExternalContainer externalContainer = new PdfSignatureExternalContainer() { @Override public byte[] sign(InputStream data) throws IOException { // 计算正确的文档哈希,保存用于远程签名 byte[] hash = DigestAlgorithms.digest(data, externalDigest.getMessageDigest("SHA256")); document.setDigest(Base64.getEncoder().encodeToString(hash)); // 延迟签名阶段返回空数组,后续填充真实签名 return new byte[0]; } @Override public int getEstimatedSize() { // 预估签名大小,建议设置为8192(需足够容纳CMS签名数据) return 8192; } }; // 初始化签名占位符 stamper.getSignatureAppearance().setExternalDigest(externalContainer, externalDigest, null); stamper.close(); reader.close();
步骤3:远程签名并生成CMS签名数据
// 读取之前保存的文档哈希 byte[] hash = Base64.getDecoder().decode(document.getDigest()); // 调用远程签名接口,获取签名值(假设返回Base64编码的签名字节) byte[] remoteSignature = Base64.getDecoder().decode(document.getSignedEncoded()); // 生成合法的CMS签名数据(解决摘要不匹配问题) BouncyCastleDigest bcDigest = new BouncyCastleDigest(); PdfPKCS7 sgn = new PdfPKCS7(null, chain, "SHA256", null, bcDigest, false); // 设置远程返回的签名值 sgn.setExternalDigest(remoteSignature, null, "RSA"); // 生成包含消息摘要属性的CMS签名数据 byte[] cmsSignature = sgn.getEncodedPKCS7(hash, Calendar.getInstance(), null, null, null, false);
步骤4:合并签名到PDF
// 重新打开步骤2生成的临时待签名PDF PdfReader signedReader = new PdfReader(new FileInputStream("temp_unsigned.pdf")); FileOutputStream outs = new FileOutputStream("final_signed.pdf"); AcroFields af = signedReader.getAcroFields(); String fieldName = document.getSigField(); PdfDictionary v = af.getSignatureDictionary(fieldName); if (v == null) { throw new DocumentException("No signature field found"); } if (!af.signatureCoversWholeDocument(fieldName)) { throw new DocumentException("Not the last signature"); } PdfArray b = v.getAsArray(PdfName.BYTERANGE); long[] gaps = b.asLongArray(); if (b.size() != 4 || gaps[0] != 0) { throw new DocumentException("Only single exclusion space supported"); } RandomAccessSource readerSource = signedReader.getSafeFile().createSourceView(); // 计算预留的签名空间大小 int spaceAvailable = (int) (gaps[2] - gaps[1]) - 2; if ((spaceAvailable & 1) != 0) { throw new DocumentException("Gap length must be even"); } spaceAvailable /= 2; if (spaceAvailable < cmsSignature.length) { throw new DocumentException("Not enough space for signature"); } // 写入签名前的PDF内容 StreamUtil.CopyBytes(readerSource, 0, gaps[1] + 1, outs); // 写入CMS签名的十六进制表示 ByteBuffer bb = new ByteBuffer(spaceAvailable * 2); for (byte bi : cmsSignature) { bb.appendHex(bi); } // 填充剩余空间为十六进制'0' int remain = (spaceAvailable - cmsSignature.length) * 2; for (int k = 0; k < remain; ++k) { bb.append((byte) '0'); } bb.writeTo(outs); // 写入签名后的PDF内容 StreamUtil.CopyBytes(readerSource, gaps[2] - 1, gaps[3] + 1, outs); // 关闭资源 signedReader.close(); bb.close(); outs.close();
关键注意事项
- 证书链完整性:必须包含所有上级CA证书,否则PDF查看器无法验证签名合法性
- 签名大小预估:
getEstimatedSize()需设置足够大的值,避免预留空间不足导致签名截断 - 算法一致性:文档哈希计算、远程签名、
PdfPKCS7初始化需使用同一哈希算法(如SHA256) - 临时文件处理:步骤2生成的临时PDF必须完整保存,合并签名时需读取此文件
内容的提问来源于stack exchange,提问作者desertSniper87
相关产品推荐
相关产品推荐

