java.lang.IllegalArgumentException:无效点编码 0x30

问题描述 投票:0回答:1

我有一个公钥,以及它对应的签名,由nodejs(v20.14.0)生成的R,S值,其函数如下

const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { 
  namedCurve:  'P-256', //'secp256k1', // invalid 'secp256r1', 
  publicKeyEncoding: { type: 'spki', format: 'der' }
});
const sign = crypto.createSign('SHA256');
const message = 'my message';
sign.update(message);
sign.end();
const signature = sign.sign(privateKey);
const RLength = parseInt(signature.toString('hex', 3, 4), 16);
const R = signature.subarray(4, 4+RLength);
const S = signature.subarray(4+RLength+2, signature.length);

结果示例:

  • 公钥:
    MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEL+RfybPlDY/+KMwY3ROpD4aSgtJFxnPOraWQpJkMJ0Ovj4rkrOSMPj+5rhE3jCJNYOV35TIGomJSxI65shfKug==
  • 签名:
    MEUCIQD+SjM+JD2u91p2Fy8UKtkMqXCkSPaCCIDFBaqzVbDA6QIgU5pGg8qnt7iWHpL9Anw5uxLXTH64gj9V9o0HjEmsBWo=
  • R:
    AP5KMz4kPa73WnYXLxQq2QypcKRI9oIIgMUFqrNVsMDp
  • S:
    U5pGg8qnt7iWHpL9Anw5uxLXTH64gj9V9o0HjEmsBWo=

这些值将传递给我的java程序(java 11)进行验证。 java程序是


String pkey = "MFkwEw...Kug=="; // the entire base64 string above
byte[] publicKey = Base64.getDecoder().decode(pkey);
X9ECParameters curve = NISTNamedCurves.getByName("P-256"); //"secp256k1"
ECDomainParameters domain = new ECDomainParameters(curve.getCurve(), curve.getG(), curve.getN(), curve.getH());
ECDSASigner signer = new ECDSASigner(); // I use bouncy castle 1.78
signer.init (
    false, 
    new ECPublicKeyParameters(
        curve.getCurve().decodePoint(publicKey),  // the place where exception is thrown
        domain
    )
);
signer.verifySignature(
    message, 
    new BigInteger(Base64.getDecoder().decode(R)), 
    new BigInteger(Base64.getDecoder().decode(S))
);

但是,java代码抛出错误

java.lang.IllegalArgumentException: Invalid point encoding 0x30

为什么 NISTNamedCurves 无法解码 Base64 字符串?我对密码学完全陌生,所以我进行了例外搜索

Invalid point encoding 0x30
。像[1]这样的一些线程出现了,但我不明白为什么。

我很感激任何建议。非常感谢。

[1]。 读取 EC 公钥,在 python 中工作,在 java 中出错

node.js bouncycastle java-security node-crypto
1个回答
0
投票

Java代码验证失败,原因如下:

  • 公钥导入错误。

    ECCurve#decodePoint()
    返回公钥为
    org.bouncycastle.math.ec.ECPoint.ECPoint
    ,并需要未压缩/压缩的密钥。可能的修复方法是进行以下更改以导入 DER 编码的公共 SPKI 密钥并获取所需的
    ECPoint
    :

    import java.security.KeyFactory;
    import java.security.spec.X509EncodedKeySpec;
    import org.bouncycastle.jce.interfaces.ECPublicKey;
    import org.bouncycastle.crypto.signers.ECDSASigner;
    import org.bouncycastle.jce.provider.BouncyCastleProvider;
    ...
    Security.addProvider(new BouncyCastleProvider());
    ...
    byte[] publicKey = Base64.getDecoder().decode(pkey);      
    KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
    X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKey);
    ECPublicKey ecPublicKey = (ECPublicKey)keyFactory.generatePublic(x509EncodedKeySpec); // note: org.bouncycastle.jce.interfaces.ECPublicKey
    ...
    signer.init (false, new ECPublicKeyParameters(ecPublicKey.getQ(), domain)); // note: public key is imported as ECPoint
    ...
    
  • 验证时,散列丢失。这必须明确完成,因为

    ECDSASigner#verifySignature()
    不会自动散列:

    import java.security.MessageDigest;
    ...
    MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
    messageDigest.update(message.getBytes(StandardCharsets.UTF_8));
    byte[] hash = messageDigest.digest();
    ...
    String R = "AMt4LZPxkiWu47DcqbRMcrzLFww9Lm584eq6zdj+/Jgy";
    String S = "AKgoAIV50yXYGIARdNE4bEt+eaujKFwX2cX6pf/y681p";
    boolean verified = signer.verifySignature(hash, new BigInteger(Base64.getDecoder().decode(R)), new BigInteger(Base64.getDecoder().decode(S)));
    

通过这些更改,测试数据的验证(发布在问题末尾)成功。


正如评论中已经指出的,Java 11 中支持两种格式(ASN.1/DER 和 IEEE P1363)的 ECDSA,因此不需要 BouncyCastle。由于 NodeJS 代码中的

sign.sign()
默认生成 ASN.1/DER 格式的签名(从 v13.2.0 开始也支持 IEEE P1363,请参见此处),因此可以在 上以这种格式直接验证签名Java 端:

import java.security.KeyFactory;
import java.security.spec.X509EncodedKeySpec;
import java.security.interfaces.ECPublicKey;
import java.security.Signature;
...
KeyFactory keyFactory = KeyFactory.getInstance("EC");
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(pkey));
ECPublicKey ecPublicKey = (ECPublicKey) keyFactory.generatePublic(x509EncodedKeySpec); // note: java.security.interfaces.ECPublicKey

Signature verifier = Signature.getInstance("SHA256withEcdsa");
verifier.initVerify(ecPublicKey);
verifier.update(message.getBytes(StandardCharsets.UTF_8));
String signature = "MEYCIQDLeC2T8ZIlruOw3Km0THK8yxcMPS5ufOHqus3Y/vyYMgIhAKgoAIV50yXYGIARdNE4bEt+eaujKFwX2cX6pf/y681p";
boolean verified = verifier.verify(Base64.getDecoder().decode(signature));
...

这也成功验证了签名。


为了完整起见:IEEE P1363 的说明符是

SHA256withECDSAinP1363Format
, s。 SunEC 提供商。 在这种情况下,签名将作为字节数组 r 和 s 的串联传递,其中 r 和 s 是固定大小的,unsigned 大端字节序。 r 和 s 各自具有生成点的阶数长度(如有必要,带有前导 0x00 值),例如对于您的示例(十六进制编码):

cb782d93f19225aee3b0dca9b44c72bccb170c3d2e6e7ce1eabacdd8fefc9832 a828008579d325d818801174d1386c4b7e79aba3285c17d9c5faa5fff2ebcd69

空间仅供展示!请注意与 ASN.1/DER 格式的区别:r 和 s 的大小最小,signed 大端字节序(这就是为什么在前导字节 > 0x7f 的情况下必须添加 0x00 前缀,以便值是正数) :

30460221 00cb782d93f19225aee3b0dca9b44c72bccb170c3d2e6e7ce1eabacdd8fefc9832 0221 00a828008579d325d818801174d1386c4b7e79aba3285c17d9c5faa5fff2ebcd69

基于这些定义,IEEE P1363 提供了固定大小的签名(P-256 为 64 字节),而 ASN.1/DER 没有固定大小,而只有最大大小(P-256 为 72 字节)。另请参阅这篇文章,了解有关这两种格式的更多详细信息。

从无符号 r 和 s 创建

BigInteger
值时,请使用构造函数
BigInteger(int, byte[])
而不是
BigInteger(byte[])
。在您的测试数据中,r 和 s 带符号,因此
BigInteger
转换是正确的。

© www.soinside.com 2019 - 2024. All rights reserved.