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
相关产品推荐
相关产品推荐

