【问题标题】:Calculating ECDSA signature in Java as per an RFC test vector根据 RFC 测试向量在 Java 中计算 ECDSA 签名
【发布时间】:2023-03-06 12:08:01
【问题描述】:

我正在用 java 为与 ikev2 协议相关的程序编写测试工具。作为其中的一部分,我需要能够计算 ECDSA 签名(特别是使用 NIST P-256 曲线)。

RFC 4754 描述了 IKEv2 中 ECDSA 的使用,并提供了一组测试向量(包括我需要的 p256 曲线)。

我正在尝试使用以下代码通过 java 的 ECDSA 签名实现运行 ECDSA-256 测试向量值(RFC 中的Section 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 917DD72E 97566EA1 C066957C 86FA3BB4 E26CAD5B F90B7F81 899256CE 7594BB1E A0C89212 748BFF3B 3D5B0315

相反,我得到:

30460221 00DD9131 EDEB5FD C5E718DF C8A7AB2D 5532B85B 7D4C012A E5A4E90C 3B824AB5 D7022100 9A8A2B12 9E10AFF 7066FF79 89AA73D5 BA37C868 5.36517 216S43 FFA876D7 P>

我知道长度差异是由于 Java ASN.1 对签名进行编码。但是,其余部分完全错误,我不知道为什么。

任何帮助或建议将不胜感激!

P.S 我不是 ECDSA 或 Java 加密专家,所以这可能是我犯的一个愚蠢的错误

【问题讨论】:

    标签: java cryptography elliptic-curve ecdsa


    【解决方案1】:

    我猜测每次运行程序时,相同的明文(待签名)输入都会得到不同的签名值。

    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 之后。我再说一遍(请原谅大喊大叫)- 不要在生产中使用它!

    接下来,您需要使用允许您指定自己的 SecureRandom 的 ECDSA 实现。为此,您可以使用 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[](应该是长度==2)是(r,s)值。您需要对其进行 ASN.1 DER 编码,这应该会为您提供您正在寻找的 droids 字节。

    【讨论】:

    • 成功!感谢您的快速和非常详细的回答。当我读到它时,它是有道理的,它现在对我有用。非常感谢您的帮助:-)
    • P.S 对于其他正在查看此内容的人。我最初犯了新手错误 fsr_k.setBytes(new BigInteger("9E56F509196784D963D1C0A401510EE7ADA3DCC5DEE04B154BF61AF1D5A6DECE", 16).toByteArray()) 并且几乎哭了。在这样做时,字节数组会附加一个额外的 0 字节,因此您仍然会得到错误的 K 值:-(
    • @PinkyNoBrain 您需要一个名为 I2OS 的函数(整数到八位字节字符串)。此方法接受 两个 输入,一个整数和(键)长度。它返回整数的无符号大端编码,前面加上00 字节,直到密钥大小。你现在有未签名的部分,确保你也有填充部分......
    • 感谢一百万:ECDSASigner 只实现 EC 签名部分,而不是散列。
    【解决方案2】:

    这是我在使用 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));
    

    【讨论】:

    • 帮助,我。我执行ECPrivateKey spriv = (ECPrivateKey) kf.generatePrivate(privateKeySpec); 使应用程序忽略其余代码
    猜你喜欢
    • 2017-01-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-01
    • 1970-01-01
    • 2017-08-29
    • 2022-12-15
    • 1970-01-01
    相关资源
    最近更新 更多