我有一个公钥,以及它对应的签名,由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=
AP5KMz4kPa73WnYXLxQq2QypcKRI9oIIgMUFqrNVsMDp
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]这样的一些线程出现了,但我不明白为什么。
我很感激任何建议。非常感谢。
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
转换是正确的。