You need to enable JavaScript to run this app. iText7对接自定义TSA实现PDF签名时出现BouncyCastle VerifyError异常
优惠活动
大模型
产品
解决方案
定价
更多

iText7对接自定义TSA实现PDF签名时出现BouncyCastle VerifyError异常

问题场景
  • 基于iText7开发PDF电子签名能力,对接自建TSA(时间戳权威)服务器为签名追加可信时间戳时抛出运行错误
  • 不接入TSA、直接使用本地系统时间完成签名的流程可正常运行
  • 先后测试iText7 7.1.7、7.1.14版本均复现相同报错

核心触发代码

签名调用段代码:

Timestamper ts = new Timestamper(new URL(ts_url), ts_user, ts_pass, new BouncyCastleDigest().getMessageDigest("SHA-256"));  
signer.signDetached(new BouncyCastleDigest(), pks, chain, null, null, new Timestamper(new URL(ts_url), ts_user, ts_pass, new BouncyCastleDigest().getMessageDigest("SHA-256")), 0, PdfSigner.CryptoStandard.CADES);

自定义TSA客户端实现

自行实现ITSAClient接口适配自建TSA的认证、请求逻辑,核心代码如下:

class Timestamper implements ITSAClient{
    private final URL url;
    private final String username;
    private final String password;
    private final MessageDigest digest;
 
    Timestamper(URL url, String username, String password, MessageDigest digest) {
        this.url = url;
        this.username = username;
        this.password = password;
        this.digest = digest;
    }

    @Override
    public MessageDigest getMessageDigest() throws GeneralSecurityException {
        return this.digest;
    }

    @Override
    public byte[] getTimeStampToken(byte[] arg0) throws Exception {
        this.digest.reset();
        byte[] hash = this.digest.digest(arg0);
         
        // 生成密码学随机数nonce
        SecureRandom random = new SecureRandom();
        int nonce = random.nextInt();
         
        // 构造TSA请求
        TimeStampRequestGenerator tsaGenerator = new TimeStampRequestGenerator();
        tsaGenerator.setCertReq(true);
        ASN1ObjectIdentifier oid = new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha256.getId());
        TimeStampRequest request = tsaGenerator.generate(oid, hash, BigInteger.valueOf(nonce));
         
        // 发送请求获取TSA响应
        byte[] tsaResponse = getTSAResponse(request.getEncoded());
        
        System.out.println(Base64.getEncoder().encodeToString(tsaResponse));

        TimeStampResponse response = new TimeStampResponse(tsaResponse);
        response.validate(request);
         
        TimeStampToken token = response.getTimeStampToken();
        if (token == null) {
            throw new IOException("Response does not have a time stamp token");
        }
         
        return token.getEncoded();
    }
    
    private byte[] getTSAResponse(byte[] request) throws IOException {
        URLConnection connection = this.url.openConnection();
        connection.setDoOutput(true);
        connection.setDoInput(true);
        connection.setRequestProperty("Content-Type", "application/timestamp-query");
        connection.setRequestProperty(this.username, this.password);
        
        // 写入请求体
        OutputStream output = null;
        try {
            output = connection.getOutputStream();
            output.write(request);
        } finally {
            IOUtils.closeQuietly(output);
        }

        // 读取响应
        InputStream input = null;
        byte[] response;
        try {
            input = connection.getInputStream();
            response = IOUtils.toByteArray(input);
        } finally {
            IOUtils.closeQuietly(input);
        }

        return response;
    }

    @Override
    public int getTokenSizeEstimate() {
        // 自建TSA返回的时间戳令牌实际大小在2576-2577字节区间,预留缓冲返回2580
        return 2580;
    }
}

运行异常栈

触发java.lang.VerifyError,栈轨迹如下:

Exception in thread "main" java.lang.VerifyError: (class: org/bouncycastle/cms/CMSSignedHelper, method: <clinit> signature: ()V) Incompatible argument to function
    at org.bouncycastle.cms.CMSSignedData.<clinit>(Unknown Source)
    at org.bouncycastle.tsp.TimeStampToken.getSignedData(Unknown Source)
    at org.bouncycastle.tsp.TimeStampToken.<init>(Unknown Source)
    at org.bouncycastle.tsp.TimeStampResponse.<init>(Unknown Source)
    at org.bouncycastle.tsp.TimeStampResponse.<init>(Unknown Source)
    at org.bouncycastle.tsp.TimeStampResponse.<init>(Unknown Source)
    at com.xxx.main.Timestamper.getTimeStampToken(SignatureHandler.java:301)
    at com.itextpdf.signatures.PdfPKCS7.getEncodedPKCS7(PdfPKCS7.java:900)
    at com.itextpdf.signatures.PdfSigner.signDetached(PdfSigner.java:648)
    at com.itextpdf.signatures.PdfSigner.signDetached(PdfSigner.java:538)
    at com.xxx.main.SignatureHandler.sign(SignatureHandler.java:239)
    at com.xxx.main.Main.main(Main.java:81)

根因定位与修复方案

  • 核心报错原因为类路径下存在多版本BouncyCastle依赖冲突,导致CMSSignedHelper类在静态初始化阶段,调用的方法参数与当前加载的类版本不匹配,触发JVM字节码校验失败。
  • 为什么不接入TSA时流程正常:未接入TSA的签名流程不会触发org.bouncycastle.cms、org.bouncycastle.tsp包的类加载动作,因此静态初始化的版本冲突校验不会被触发。一旦走到时间戳令牌解析逻辑,对应类被加载时就会立即报错。

修复步骤

  • 排查全类路径下的BouncyCastle相关依赖,包括bcprov-jdk系列、bcpkix-jdk系列、bcmail-jdk系列,删除所有冗余、版本不匹配的包,统一对齐到与iText7 7.1.x兼容的BouncyCastle版本(兼容区间为1.66~1.70)。
  • 修正TSA请求的认证头设置逻辑:原代码connection.setRequestProperty(this.username, this.password)将用户名直接作为请求头键名、密码作为头值,完全不符合HTTP请求头规范,需根据自建TSA的认证要求,正确配置对应请求头(如基础认证需设置Authorization头,值为Basic 拼接Base64编码的用户名:密码串)。
  • 清理代码中重复构造Timestamper对象的冗余逻辑:原签名调用段先实例化了一个ts对象,又在signDetached参数中重新new了一个新的Timestamper实例,属于无意义的资源浪费,直接复用已实例化的对象即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.11.02 05:45:05