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代码
核心问题分析
- 签名格式不兼容:Java使用的
SHA256withPlain-ECDSA生成的是原始R/S拼接格式的签名,而C#的ECDsa.SignHash默认生成DER编码格式的签名,XAdES标准要求使用DER编码的签名,这是验证失败的主要原因。 - 哈希处理错误: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
相关产品推荐
相关产品推荐

