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

iText7对接外部签名服务签署PDF后Adobe验证提示文档被篡改

iText7对接外部签名服务PDF签署验证失败解决方案

核心问题定位

你的实现存在4个致命逻辑错误,直接导致签名验证失败:

  • 哈希计算逻辑完全不符合PDF签名规范:你提前对磁盘上的完整临时PDF文件计算全文件哈希,但PDF签名实际的签名范围是iText在签名流程中动态划定的、排除签名字段预留写入空间的字节区间,提前计算的全文件哈希和实际待签名内容完全不匹配,必然触发“文档被篡改或损坏”的校验错误。
  • 签名接口实现逻辑错误:自定义IExternalSignature时,完全忽略sign(byte[] message)方法的入参message——这个参数才是iText按照规范计算出的、真正需要提交给外部签名服务的待签名数据,你直接返回提前获取的签名值,签名内容和待签内容不匹配。
  • CMS容器构造参数错误:自行封装CMS时,错误传入提前计算的全文件哈希作为已认证属性参数,没有使用iText在签名流程中生成的标准已认证属性结构,导致CMS格式不符合规范,触发签名格式错误。
  • 多签逻辑缺失:签名时没有开启追加模式,写入签名时会重写原有PDF结构,多签场景下会直接破坏前序签名的有效性。

正确实现方案

首先需要和外部签名服务确认返回值类型,分两类场景适配:

场景1:外部服务返回原始裸签名值(最常见)

这类场景下服务端仅返回对传入哈希的原始签名结果(RSA/SM2/ECDSA签名值),CMS容器由iText本地构造,流程如下:

  1. 提前生成带所有签名字段的临时PDF,该部分你的现有逻辑可正常使用,注意生成时不要加密、不要携带多余增量更新内容。
  2. 禁止提前计算文件哈希,所有待签名数据必须在PdfSigner签名流程中由iText生成。
  3. 自定义IExternalSignature实现,在sign(byte[] message)方法内部,将iText传入的message(即符合PDF规范的待签名数据)做Base64编码提交给签名服务,拿到返回的签名值解码后直接返回,不要做额外哈希或包装。
  4. 签名时必须开启追加模式,避免修改原有文件结构破坏签名有效性。
  5. 多签场景下,每次签署都以上一次签署完成的PDF作为源文件,始终保持追加模式开启。

场景2:外部服务返回完整CMS签名容器

这类场景下服务端直接封装好符合规范的PKCS#7/CMS结构(包含证书链、签名值、已认证属性),不需要iText本地构造CMS,需要使用signExternalContainer方法对接,自定义IExternalSignatureContainer实现即可。

修正后核心代码示例

裸签名值场景实现

// 自定义外部签名实现
public class ExternalServiceSignature implements IExternalSignature {
    private final Certificate[] chain;
    private final SignServiceClient signServiceClient;

    public ExternalServiceSignature(Certificate[] chain, SignServiceClient client) {
        this.chain = chain;
        this.signServiceClient = client;
    }

    @Override
    public String getHashAlgorithm() {
        // 按实际签名算法调整,国密场景对应填SM3
        return DigestAlgorithms.SHA256;
    }

    @Override
    public String getEncryptionAlgorithm() {
        // 按证书类型调整,RSA填RSA,SM2填SM2
        return "RSA";
    }

    @Override
    public byte[] sign(byte[] message) throws GeneralSecurityException {
        // message为iText生成的标准待签名数据,直接提交给服务端
        String base64ToSign = Base64.getEncoder().encodeToString(message);
        String base64SignResult = signServiceClient.getRawSign(base64ToSign);
        return Base64.getDecoder().decode(base64SignResult);
    }
}

// 签名调用逻辑
public void sign(String src, String dest, Certificate[] chain, String fieldName) throws GeneralSecurityException, IOException {
    try (FileOutputStream os = new FileOutputStream(dest); InputStream is = new FileInputStream(src)) {
        PdfReader reader = new PdfReader(is);
        // 关键:开启追加模式,多签不破坏前序签名
        StampingProperties properties = new StampingProperties().useAppendMode();
        PdfSigner signer = new PdfSigner(reader, os, properties);
        signer.setFieldName(fieldName);

        IExternalDigest digest = new BouncyCastleDigest();
        IExternalSignature signature = new ExternalServiceSignature(chain, new SignServiceClient());
        // 预留签名空间修正为8192(原代码8096为笔误),证书链较长/带时间戳可适当调大
        signer.signDetached(digest, signature, chain, null, null, null,
                8192, PdfSigner.CryptoStandard.CMS);
    }
}

完整CMS容器场景实现

public class ExternalCmsContainer implements IExternalSignatureContainer {
    private final SignServiceClient signServiceClient;

    public ExternalCmsContainer(SignServiceClient client) {
        this.signServiceClient = client;
    }

    @Override
    public byte[] sign(InputStream data) throws GeneralSecurityException {
        try {
            byte[] docRangeBytes = StreamUtil.inputStreamToArray(data);
            // 按服务端要求计算哈希,部分服务会直接处理原始字节无需本地算哈希
            byte[] hash = DigestAlgorithms.digest(new ByteArrayInputStream(docRangeBytes), DigestAlgorithms.SHA256, null);
            String base64Hash = Base64.getEncoder().encodeToString(hash);
            String base64Cms = signServiceClient.getCmsSign(base64Hash);
            return Base64.getDecoder().decode(base64Cms);
        } catch (IOException e) {
            throw new GeneralSecurityException("签名流程异常", e);
        }
    }

    @Override
    public void modifySigningDictionary(PdfDictionary signDic) {
        signDic.put(PdfName.Filter, PdfName.Adobe_PPKLite);
        signDic.put(PdfName.SubFilter, PdfName.Adbe_pkcs7_detached);
    }
}

// 调用逻辑
public void signWithCms(String src, String dest, String fieldName) throws GeneralSecurityException, IOException {
    try (FileOutputStream os = new FileOutputStream(dest); InputStream is = new FileInputStream(src)) {
        PdfReader reader = new PdfReader(is);
        StampingProperties properties = new StampingProperties().useAppendMode();
        PdfSigner signer = new PdfSigner(reader, os, properties);
        signer.setFieldName(fieldName);
        // CMS容器预留空间建议设为16384以上,避免空间不足截断
        signer.signExternalContainer(new ExternalCmsContainer(new SignServiceClient()), 16384);
    }
}

避坑说明

  • 对接前必须和签名服务确认待签数据要求:部分服务会要求传入原始文档字节,服务端自行做哈希计算,这类场景不要本地提前计算哈希,严格按服务端接口要求传参,保证服务端签名的内容和iText传入的待签内容完全一致。
  • 预留签名空间大小要匹配实际签名内容长度,空间不足会导致签名值被截断,触发格式错误。
  • 如果需要添加签名外观、时间戳、OCSP/CRL吊销信息,直接在signDetached对应参数位置传入即可,不影响外部签名核心逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 15:09:22