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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 16:05:45