我正在用java为与ikev2协议相关的程序编写一个测试工具。 作为其中的一部分,我需要能够计算ECDSA签名(特别是使用NIST P-256曲线)。
RFC 4754 描述了 ECDSA 在 IKEv2 中的使用,并提供了一组测试向量(包括我需要的 p256 曲线)。
我正在尝试使用以下代码通过 java 的 ECDSA 签名实现运行 ECDSA-256 测试向量值(RFC 中的第 8.1 节):
//"abc" for the input
byte[] input = { 0x61, 0x62, 0x63 };
//Ugly way of getting the ECParameterSpec for the P-256 curve by name as opposed to specifying all the parameters manually.
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
ECGenParameterSpec kpgparams = new ECGenParameterSpec("secp256r1");
kpg.initialize(kpgparams);
ECParameterSpec params = ((ECPublicKey) kpg.generateKeyPair().getPublic()).getParams();
//Create the static private key W from the Test Vector
ECPrivateKeySpec static_privates = new ECPrivateKeySpec(new BigInteger("DC51D3866A15BACDE33D96F992FCA99DA7E6EF0934E7097559C27F1614C88A7F", 16), params);
KeyFactory kf = KeyFactory.getInstance("EC");
ECPrivateKey spriv = (ECPrivateKey) kf.generatePrivate(static_privates);
//Perfrom ECDSA signature of the data with SHA-256 as the hash algorithm
Signature dsa = Signature.getInstance("SHA256withECDSA");
dsa.initSign(spriv);
dsa.update(input);
byte[] output = dsa.sign();
System.out.println("Result: " + new BigInteger(1, output).toString(16));
结果应该是:
CB28E099 9B9C7715 FD0A80D8 E47A7707 9716CBBF 917DD72E97566EA1 C066957C 86FA3BB4 E26CAD5B F90B7F81 899256CE 7594BB1E A0C89212748BFF3B 3D5B0315
相反,我得到:
30460221 00DD9131 EdeB5EFD C5E718DF C8A7AB2d 5532B85B 7D4C012A E5A4E90C 3B824AB5 D7022100 9A8A2B12 9E10A2FF 7066FF79 89AA73D5 BA37C868 5EC36517 216E2E43 FFA876D7
我知道长度差异是由于Java ASN.1编码签名造成的。然而,其余部分是完全错误的,我对为什么感到困惑。
任何帮助或建议将不胜感激!
附言我不是ECDSA或Java加密专家,所以这可能是我犯的一个愚蠢的错误
我猜每次运行程序时,对于相同的纯文本(待签名)输入,您都会获得不同的签名值。
ECDSA 指定为每个签名生成一个随机的临时 ECDSA 私钥。为此,Signature.getInstance("SHA256withECDSA")
不允许你指定一个临时键(这是一件好事,以防止许多人在脚上自枪!相反,它会获得自己的 SecureRandom 实例,这将使您的输出不确定。
这可能意味着你不能使用JCE(Signature.getInstance()
)进行测试向量验证。
您可以做的是以返回确定性数据的方式扩展SecureRandom
。显然,您不应该在实际部署中使用它:
public class FixedSecureRandom extends SecureRandom {
private static boolean debug = false;
private static final long serialVersionUID = 1L;
public FixedSecureRandom() { }
private int nextBytesIndex = 0;
private byte[] nextBytesValues = null;
public void setBytes(byte[] values) {
this.nextBytesValues = values;
}
public void nextBytes(byte[] b) {
if (nextBytesValues==null) {
super.nextBytes(b);
} else if (nextBytesValues.length==0) {
super.nextBytes(b);
} else {
for (int i=0; i<b.length; i++) {
b[i] = nextBytesValues[nextBytesIndex];
nextBytesIndex = (nextBytesIndex + 1) % nextBytesValues.length;
}
}
}
}
唷。好的,现在你有一个 SecureRandom 类,它返回一些已知字节,然后回退到真正的 SecureRandom。我会再说一遍(请原谅大喊大叫)- 不要在生产中使用它!
接下来,您需要使用 ECDSA 实现,该实现允许您指定自己的 SecureRandom。您可以使用BouncyCastle的ECDSASigner
来实现此目的。除了这里,你将给它你自己的盗版FixedSecureRandom,这样当它调用secureRandom.getBytes()
时,它就会得到你想要的字节。 这使您可以控制临时键以匹配测试向量中指定的键。您可能需要调整实际字节(例如,添加零预填充)以匹配ECDSASigner
将要请求的内容。
ECPrivateKeyParameters ecPriv = ...; // this is the user's EC private key (not ephemeral)
FixedSecureRandom fsr_k = new FixedSecureRandom();
fsr_k.setBytes(tempKeyK);
ECDSASigner signer = new ECDSASigner();
ParametersWithRandom ecdsaprivrand = new ParametersWithRandom(ecPriv, fsr_k);
signer.init(true, ecdsaprivrand);
请注意,BC 的 ECDSASigner
仅实现 EC 签名部分,而不是哈希。您仍然需要进行自己的哈希处理(假设您的输入数据是data
):
Digest md = new SHA256Digest()
md.reset();
md.update(data, 0, data.length);
byte[] hash = new byte[md.getDigestSize()];
md.doFinal(hash, 0);
在创建 ECDSA 签名之前:
BigInteger[] sig = signer.generateSignature(hash);
最后,这个BigInteger[]
(应该是 length==2)是 (r,s) 值。您需要对它进行 ASN.1 DER 编码,这应该为您提供所需的机器人字节。
BouncyCastle的tsechin解决方案的完整测试,但坚持使用旧的JCA API:
byte[] input = { 0x61, 0x62, 0x63 };
//Create the static private key W from the Test Vector
ECNamedCurveParameterSpec parameterSpec = ECNamedCurveTable.getParameterSpec("secp256r1");
org.bouncycastle.jce.spec.ECPrivateKeySpec privateKeySpec = new org.bouncycastle.jce.spec.ECPrivateKeySpec(new BigInteger("DC51D3866A15BACDE33D96F992FCA99DA7E6EF0934E7097559C27F1614C88A7F", 16), parameterSpec);
KeyFactory kf = KeyFactory.getInstance("EC");
ECPrivateKey spriv = (ECPrivateKey) kf.generatePrivate(privateKeySpec);
//Perfrom ECDSA signature of the data with SHA-256 as the hash algorithm
Signature dsa = Signature.getInstance("SHA256withECDSA", "BC");
FixedSecureRandom random = new FixedSecureRandom();
random.setBytes(Hex.decode("9E56F509196784D963D1C0A401510EE7ADA3DCC5DEE04B154BF61AF1D5A6DECE"));
dsa.initSign(spriv, random);
dsa.update(input);
byte[] output = dsa.sign();
// compare the signature with the expected reference values
ASN1Sequence sequence = ASN1Sequence.getInstance(output);
DERInteger r = (DERInteger) sequence.getObjectAt(0);
DERInteger s = (DERInteger) sequence.getObjectAt(1);
Assert.assertEquals(r.getValue(), new BigInteger("CB28E0999B9C7715FD0A80D8E47A77079716CBBF917DD72E97566EA1C066957C", 16));
Assert.assertEquals(s.getValue(), new BigInteger("86FA3BB4E26CAD5BF90B7F81899256CE7594BB1EA0C89212748BFF3B3D5B0315", 16));