Java 17下Bouncy Castle生成的签名无法通过第三方API验证
解决Java 17中Bouncy Castle生成secp256k1签名无法通过第三方验证的问题
问题背景
Java 8下对接第三方API的签名代码可正常生成secp256k1签名并通过验证,但迁移到Java 17后出现Curve not supported: secp256k1 (1.3.132.0.10)错误。改用Bouncy Castle生成签名后,签名无法通过第三方API验证,且无法回退到Java 8版本。
核心原因
Java 8默认SunEC提供商生成的secp256k1签名是64字节的紧凑格式(r+s拼接,各32字节),而Bouncy Castle默认生成的是带ASN.1结构的DER格式签名(长度不固定,通常70-72字节)。第三方API大概率期望紧凑格式签名,这是验证失败的主要原因。此外还需注意字符编码、密钥加载一致性等细节。
解决方案
1. 引入Bouncy Castle依赖
确保项目引入正确的Bouncy Castle安全提供商依赖(以Maven为例):
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> <!-- 推荐使用最新稳定版本 --> </dependency>
2. 注册Bouncy Castle提供商
在应用启动时注册Bouncy Castle作为安全提供商:
import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.Security; // 应用初始化时执行 static { Security.addProvider(new BouncyCastleProvider()); }
3. 将DER格式签名转换为紧凑格式
添加工具方法,把Bouncy Castle生成的DER签名解析为r和s值,再拼接成64字节的紧凑格式:
import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.ASN1Primitive; import org.bouncycastle.asn1.DLSequence; import java.io.IOException; import java.nio.ByteBuffer; private byte[] convertDerToCompact(byte[] derSignature) throws IOException { try (ASN1InputStream asn1InputStream = new ASN1InputStream(derSignature)) { ASN1Primitive primitive = asn1InputStream.readObject(); if (!(primitive instanceof DLSequence)) { throw new IllegalArgumentException("无效的DER签名格式"); } DLSequence sequence = (DLSequence) primitive; ASN1Integer r = (ASN1Integer) sequence.getObjectAt(0); ASN1Integer s = (ASN1Integer) sequence.getObjectAt(1); byte[] rBytes = r.getPositiveValue().toByteArray(); byte[] sBytes = s.getPositiveValue().toByteArray(); ByteBuffer buffer = ByteBuffer.allocate(64); // 填充r到32字节(不足时前面补0) if (rBytes.length < 32) { buffer.put(new byte[32 - rBytes.length]); } buffer.put(rBytes); // 填充s到32字节(不足时前面补0) if (sBytes.length < 32) { buffer.put(new byte[32 - sBytes.length]); } buffer.put(sBytes); return buffer.array(); } }
4. 修改签名生成代码
更新原sign方法,替换签名处理逻辑,将DER签名转换为紧凑格式后再Base64编码:
import java.io.File; import java.nio.file.Files; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.Signature; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Base64; public String sign(String payload) throws Exception { // 加载私钥文件(确保路径正确,和Java8使用同一密钥) File keyFile = new File(getClass().getClassLoader().getResource("keys/private-key.der").getFile()); byte[] keyData = Files.readAllBytes(keyFile.toPath()); // 使用Bouncy Castle生成EC私钥 KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC"); PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyData); PrivateKey privateKey = keyFactory.generatePrivate(spec); // 初始化签名实例 Signature ecdsa = Signature.getInstance("SHA256withECDSA", "BC"); ecdsa.initSign(privateKey); // 统一使用UTF-8编码处理payload(和Java8保持一致) ecdsa.update(payload.getBytes("UTF-8")); byte[] derSignature = ecdsa.sign(); // 转换为第三方期望的紧凑格式签名 byte[] compactSignature = convertDerToCompact(derSignature); // Base64编码(和Java8的Base64.encode行为一致) String signed = Base64.getEncoder().encodeToString(compactSignature); String headerValue = "ecdsa=" + signed; return headerValue; }
关键注意事项
- 编码一致性:确保payload的字符编码统一为UTF-8,Java8中需将
payload.getBytes()改为payload.getBytes("UTF-8"),避免因编码不同导致原始数据差异。 - 密钥一致性:确认Java17中加载的私钥文件和Java8使用的完全相同,密钥不同会直接导致签名不一致。
- 签名格式验证:对比Java8和修改后Java17生成的签名字节长度,两者都应为64字节,确保格式匹配。
内容的提问来源于stack exchange,提问作者Talha Asif
相关产品推荐
相关产品推荐

