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

Itext 9调用外部签名服务签名PDF后提示‘文档在签名后被修改或损坏’的问题排查

Itext 9调用外部签名服务签名PDF后提示‘文档在签名后被修改或损坏’的问题排查

看起来你已经在调整代码适配外部签名服务了,但还是踩了iText签名流程的核心坑,咱们一步步拆解问题,把验证不通过的根源找出来:

先理清楚核心矛盾:外部签名的「待签内容」和iText期望的「待签内容」完全不匹配

iText的PDF数字签名,本质是对PDF签名覆盖范围的内容哈希进行签名(签名覆盖范围是PDF的前半部分,不包括后续添加的签名本身),而且这个哈希会被iText包装成标准的DigestInfo结构(PKCS#1规范)后,才交给签名逻辑。而你对接的外部API,签名的是「原始文档哈希」,这俩完全不是一回事——哪怕你用了useAppendMode,iText也会在签名前添加签名域、签名字典等元数据,导致原始文档和签名覆盖范围的内容天差地别,验证肯定失败。

你修改后的代码的3个致命错误

1. 多做了一次SHA256哈希,完全改变了待签内容

看你generateHash方法里的这段代码:

MessageDigest messageDigest = MessageDigest.getInstance("SHA256");
byte[] hash = messageDigest.digest(extracted.hash);
return Base64.getEncoder().encodeToString(hash);

这里的extracted.hash已经是iText为你准备好的待签消息(也就是签名覆盖范围哈希包装后的DigestInfo结构),你再对它做一次SHA256,等于把API要签名的内容彻底改了,返回的签名自然和iText的验证逻辑不匹配。

2. 预生成哈希时的签名配置,和实际签名时的配置不一致

你在generateHash里创建PdfSigner时,没有设置reason和location,但如果实际签名时(addCallbackSignatureHash)需要这些参数,两次PdfSigner的配置差异会导致签名覆盖范围的内容变化,哈希值直接不一样,验证必败。所有签名相关的配置(页面位置、页码、reason、location等)必须完全一致。

3. 对外部API的签名逻辑理解偏差

根据你贴的API文档,API是对「文档哈希」做RSA签名,但这里的「文档哈希」绝对不是原始PDF的哈希,而是iText计算出的签名覆盖范围的哈希(或者是iText包装后的DigestInfo结构,取决于API的签名实现)。

正确的实现步骤

根据你的API逻辑(对哈希做RSA签名),我们需要让iText和API的待签内容对齐,分两步走:

步骤1:预计算iText签名覆盖范围的哈希,发给API获取签名

// 预计算签名覆盖范围的SHA256哈希(和实际签名配置完全一致)
private byte[] getSignRangeHash(byte[] content, String reason, String location) throws Exception {
    PdfReader reader = new PdfReader(new ByteArrayInputStream(content));
    // 这里输出流随便用一个,因为我们只算哈希不生成签名
    PdfSigner signer = new PdfSigner(reader, new ByteArrayOutputStream(), new StampingProperties().useAppendMode());
    
    // 所有配置必须和后续实际签名时完全一致!
    signer.getSignerProperties()
          .setPageRect(new Rectangle(36, 648, 200, 100))
          .setPageNumber(1);
    if (reason != null) signer.getSignerProperties().setReason(reason);
    if (location != null) signer.getSignerProperties().setLocation(location);

    MessageDigest sha256 = MessageDigest.getInstance("SHA256");
    IExternalSignatureContainer hashCalculator = new IExternalSignatureContainer() {
        @Override
        public byte[] sign(InputStream data) throws GeneralSecurityException {
            // 读取签名覆盖范围的内容,计算SHA256哈希
            byte[] buffer = new byte[8192];
            int len;
            while ((len = data.read(buffer)) != -1) {
                sha256.update(buffer, 0, len);
            }
            return new byte[0]; // 不需要返回签名,只是计算哈希
        }

        @Override
        public void modifySigningDictionary(PdfDictionary signDic) {
            // 配置签名字典,和实际签名时一致
            signDic.put(PdfName.Filter, PdfName.Adobe_PPKLite);
            signDic.put(PdfName.SubFilter, PdfName.Adbe_pkcs7_detached);
            signDic.put(PdfName.DigestAlgorithm, new PdfName("SHA256"));
            signDic.put(PdfName.SignatureAlgorithm, new PdfName("RSA"));
        }
    };

    signer.signExternalContainer(hashCalculator, 8192);
    return sha256.digest();
}

// 调用API的方法
public String callSignApi(byte[] signRangeHash) {
    // 把signRangeHash转成Base64作为hashByte参数,发给/api/v1/signature
    String hashByte = Base64.getEncoder().encodeToString(signRangeHash);
    // 这里实现你的API请求逻辑,返回signByte的Base64字符串
    return apiClient.sendSignRequest(hashByte, "SHA256", "your-doc-name");
}

步骤2:用API返回的签名,完成PDF签名

public byte[] finalizePdfSignature(byte[] content, String signByteBase64, String reason, String location) throws Exception {
    byte[] signByte = Base64.getDecoder().decode(signByteBase64);
    PdfReader reader = new PdfReader(new ByteArrayInputStream(content));
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    PdfSigner signer = new PdfSigner(reader, outputStream, new StampingProperties().useAppendMode());
    
    // 注意:这里的配置必须和getSignRangeHash方法里的完全一致!
    signer.getSignerProperties()
          .setPageRect(new Rectangle(36, 648, 200, 100))
          .setPageNumber(1);
    if (reason != null) signer.getSignerProperties().setReason(reason);
    if (location != null) signer.getSignerProperties().setLocation(location);

    IExternalSignatureContainer signatureContainer = new IExternalSignatureContainer() {
        @Override
        public byte[] sign(InputStream data) throws GeneralSecurityException {
            // 直接返回API生成的签名,因为我们已经预计算过哈希
            return signByte;
        }

        @Override
        public void modifySigningDictionary(PdfDictionary signDic) {
            // 和预计算哈希时的签名字典配置完全一致
            signDic.put(PdfName.Filter, PdfName.Adobe_PPKLite);
            signDic.put(PdfName.SubFilter, PdfName.Adbe_pkcs7_detached);
            signDic.put(PdfName.DigestAlgorithm, new PdfName("SHA256"));
            signDic.put(PdfName.SignatureAlgorithm, new PdfName("RSA"));
        }
    };

    signer.signExternalContainer(signatureContainer, 8192);
    return outputStream.toByteArray();
}

调用流程

// 1. 预计算签名覆盖范围哈希
byte[] signRangeHash = getSignRangeHash(originalPdfBytes, "签名原因", "签名位置");
// 2. 调用API获取签名
String signByteBase64 = callSignApi(signRangeHash);
// 3. 完成PDF签名
byte[] signedPdfBytes = finalizePdfSignature(originalPdfBytes, signByteBase64, "签名原因", "签名位置");

关键注意事项

  • 所有签名配置必须完全一致:包括页码、签名域位置、reason、location、是否用AppendMode等,任何一点差异都会导致签名覆盖范围变化,哈希不匹配。
  • 不要随意修改待签内容:iText的sign方法传入的内容是经过规范包装的,不要额外做哈希或编码,除非你完全理解API的签名逻辑。
  • 验证签名时,用Adobe Acrobat打开PDF看详细错误:它会告诉你是哈希不匹配,还是证书问题,还是签名格式问题,比泛泛的“文档被修改”更有用。

备注:内容来源于stack exchange,提问作者Alex Dolhescu

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.13 19:58:11