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

Java ECDSA哈希签名验证失败问题及修复方案咨询

问题:ECDSA签名验证失败(SignatureValue与SignedInfo不匹配)

我有一个Java函数用于对指定哈希签名,配合Python脚本生成带XAdES签名的adoc文件,但生成的文件在验证时失败,错误提示:

使用公钥验证SignatureValue字段与SignedInfo字段不匹配(说明三者之一已被修改)

已确认Python脚本功能正常,因为其他开发者用C#生成的签名测试该脚本可通过验证。

原Java签名代码

public static String signHash(String hash, String base64PrivateKey) throws Exception {
    try {
        setupBouncyCastle();

        PrivateKey privateKey = JavaSignUtil.importECPrivateKey(base64PrivateKey);

        Signature signature = Signature.getInstance("SHA256withPlain-ECDSA");

        signature.initSign(privateKey);
        signature.update(hash.getBytes());

        // Sign the hash
        byte[] signedHash = signature.sign();

        // Convert the signed hash to Base64 for easy handling (optional)
        String signedHashBase64 = new String(Base64.encodeBase64(signedHash), StandardCharsets.UTF_8);

        return signedHashBase64;
    } catch (Exception e) {
        e.printStackTrace();
        throw e; // TODO: handle the exception
    }
}

private static PrivateKey importECPrivateKey(String base64Key) throws Exception {
    // Decode the Base64 string to get the raw key data
    byte[] keyBytes = Base64.decodeBase64(base64Key.getBytes());

    // Assuming P-256 curve, extract private scalar 'K'
    // First byte is 0x04 and then 64 bytes for X and Y, so K starts at 65th byte
    byte[] kBytes = new byte[32]; // Size of K for P-256
    System.arraycopy(keyBytes, 65, kBytes, 0, 32);
    BigInteger k = new BigInteger(1, kBytes);

    // Specify the curve parameters (example for P-256)
    // Get the parameters for 'secp256r1' curve from BouncyCastle's curve table
    ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256r1");
    // Create the private key spec for BouncyCastle
    ECPrivateKeySpec privateKeySpec = new ECPrivateKeySpec(k, ecSpec);
    KeyFactory kf = KeyFactory.getInstance("ECDSA", "BC");

    // Create the key spec and generate the private key
    return kf.generatePrivate(privateKeySpec);
}

private static void setupBouncyCastle() {
    if (JavaSignUtil.provider != null) { return; } // The provider is already set

    Provider provider = new BouncyCastleProvider();
    Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
    Security.insertProviderAt(provider, 1);

    JavaSignUtil.provider = provider;
}

可正常工作的C#签名代码

void SignHashWithEcdsa()
{
    ECDsa ecdsa = EcdsaPrivateKeyImporter.ImportEcPrivateKey(privateKeyBase64);

    Console.WriteLine("Please insert the hash in base64 format:");
    var hashBase64 = Console.ReadLine();
    var rgbHash = Convert.FromBase64String(hashBase64);
    byte[] signature = ecdsa.SignHash(rgbHash);
    var signatureBase64 = Convert.ToBase64String(signature);
    Console.WriteLine(signatureBase64);
}

public static class EcdsaPrivateKeyImporter
{
    public static ECDsa ImportEcPrivateKey(string base64Key)
    {
        byte[] keyBytes = Convert.FromBase64String(base64Key);

        if (keyBytes.Length != 97 || keyBytes[0] != 0x04)
        {
            throw new ArgumentException("Invalid key format.");
        }

        byte[] kBytes = new byte[32];
        Array.Copy(keyBytes, 65, kBytes, 0, 32);

        byte[] xBytes = new byte[32];
        byte[] yBytes = new byte[32];
        Array.Copy(keyBytes, 1, xBytes, 0, 32);
        Array.Copy(keyBytes, 33, yBytes, 0, 32);

        ECParameters ecParams = new ECParameters
        {
            Curve = ECCurve.NamedCurves.nistP256,
            D = kBytes,
            Q = new ECPoint
            {
                X = xBytes,
                Y = yBytes
            }
        };

        ECDsa ecdsa = ECDsa.Create(ecParams);
        Console.WriteLine(ecdsa.SignatureAlgorithm);
        
        // Extract public key parameters
        ECParameters publicParams = ecdsa.ExportParameters(false);

        // Concatenate X and Y coordinates
        byte[] publicKeyBytes = new byte[64];
        Array.Copy(publicParams.Q.X, 0, publicKeyBytes, 0, 32);
        Array.Copy(publicParams.Q.Y, 0, publicKeyBytes, 32, 32);

        // Convert to base64 string
        string base64PublicKey = Convert.ToBase64String(publicKeyBytes);
        Console.WriteLine($"Public Key (Base64): {base64PublicKey}");
        
        return ecdsa;
    }
}

解决方案与修改后的Java代码

核心问题分析

  1. 签名格式不兼容:Java使用的SHA256withPlain-ECDSA生成的是原始R/S拼接格式的签名,而C#的ECDsa.SignHash默认生成DER编码格式的签名,XAdES标准要求使用DER编码的签名,这是验证失败的主要原因。
  2. 哈希处理错误:Java直接将哈希字符串转为字节数组,但传入的hash参数是Base64编码的哈希值,需要先解码为原始字节再签名,C#中已做此处理。

修改后的Java代码

public static String signHash(String hash, String base64PrivateKey) throws Exception {
    try {
        setupBouncyCastle();

        PrivateKey privateKey = importECPrivateKey(base64PrivateKey);

        // 使用标准DER编码的ECDSA签名算法,替换Plain版本
        Signature signature = Signature.getInstance("SHA256withECDSA", "BC");

        signature.initSign(privateKey);
        // 先将Base64编码的哈希解码为原始字节,再更新签名对象
        byte[] hashBytes = Base64.decodeBase64(hash.getBytes(StandardCharsets.UTF_8));
        signature.update(hashBytes);

        byte[] signedHash = signature.sign();

        String signedHashBase64 = new String(Base64.encodeBase64(signedHash), StandardCharsets.UTF_8);

        return signedHashBase64;
    } catch (Exception e) {
        e.printStackTrace();
        throw e;
    }
}

// 密钥导入方法保持不变,因为私钥签名只需要D值,C#中传入Q点是为了完整密钥对象,但不影响签名逻辑
private static PrivateKey importECPrivateKey(String base64Key) throws Exception {
    byte[] keyBytes = Base64.decodeBase64(base64Key.getBytes(StandardCharsets.UTF_8));

    byte[] kBytes = new byte[32];
    System.arraycopy(keyBytes, 65, kBytes, 0, 32);
    BigInteger k = new BigInteger(1, kBytes);

    ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256r1");
    ECPrivateKeySpec privateKeySpec = new ECPrivateKeySpec(k, ecSpec);
    KeyFactory kf = KeyFactory.getInstance("ECDSA", "BC");

    return kf.generatePrivate(privateKeySpec);
}

private static void setupBouncyCastle() {
    if (JavaSignUtil.provider != null) { return; }

    Provider provider = new BouncyCastleProvider();
    Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
    Security.insertProviderAt(provider, 1);

    JavaSignUtil.provider = provider;
}

额外说明

  • SHA256withECDSA是BouncyCastle支持的标准算法,生成的签名符合X.509 DER编码格式,与C#的输出格式一致。
  • 确保传入的hash参数是Base64编码的SHA-256哈希值,解码后的字节长度必须为32字节,否则会导致签名无效。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 02:57:36