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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 12:15:53