ECDSA 使用 BouncyCastle 签名并使用 Crypto++ 进行验证

Posted

技术标签:

【中文标题】ECDSA 使用 BouncyCastle 签名并使用 Crypto++ 进行验证【英文标题】:ECDSA sign with BouncyCastle and verify with Crypto++ 【发布时间】:2018-02-14 09:36:16 【问题描述】:

这是Java代码:

public static String sign(String data) throws Exception 
    KeyPair keyPair = loadKeyPair(System.getProperty("user.dir"), "ECDSA");
    Signature signature = Signature.getInstance("SHA256withECDSA", "BC");
    signature.initSign(keyPair.getPrivate(), new SecureRandom());

    byte[] message = data.getBytes();
    signature.update(message);

    byte[] sigBytes = signature.sign();        
    String signatureStr = new BigInteger(1, sigBytes).toString(16);
    return signatureStr;

然后是验证签名的 C++ 代码

bool VerifyMessage( const ECDSA<ECP, SHA256>::PublicKey& key, const string& message, const string& signature )

    bool result = false;

    // Hexa encoding version, more readable
    std::string decodedSignature;
    StringSource(signature, true,
                    new HexDecoder(
                       new StringSink(decodedSignature)));

    StringSource(decodedSignature+message, true,
                    new SignatureVerificationFilter(ECDSA<ECP,SHA256>::Verifier(key),
                       new ArraySink((byte*)&result, sizeof(result))));

    return result;

我在想我需要将我的签名编码为十六进制,但这并没有解决我的问题。我已经使用 crypto++ 编写了签名方法的 c++ 版本,并且已经过验证。那么为什么当我使用java代码时,签名没有被验证。谢谢

【问题讨论】:

另见 Crypto++ wiki 上的 DSAConvertSignatureFormat。这是一个相对较新的页面。它是在这个问题之后写的。 【参考方案1】:

...为什么我使用java代码时,签名没有被验证?

OpenSSL 和 Java 对签名使用 ASN.1/DER 编码,而 Crypto++ 对签名使用 IEEE P1363 格式。

ASN.1:SEQUENCE ::= r INTEGER, s INTEGER P1363:[byte array r][byte array s]

您需要在格式之间进行转换。 Crypto++ 提供DSAConvertSignatureFormat 在格式之间进行转换。 Crypto++ wiki 上有一个示例 Elliptic Curve Digital Signature Algorithm | OpenSSL and Java Interop。

这是来自 wiki 的 Crypto++ 代码。它使用 OpenSSL 及其命令行工具而不是 Java。没有实质性区别,因为 OpenSSL 和 Java 以 ASN.1/DER 格式输出签名。

#include "cryptlib.h"
#include "eccrypto.h"
#include "files.h"
#include "dsa.h"
#include "sha.h"
#include "hex.h"

#include <iostream>

using namespace CryptoPP;

int main(int argc, char* argv[])

    // Load DER encoded public key
    FileSource pubKey("secp256k1-pub.der", true /*binary*/);
    ECDSA<ECP, SHA1>::Verifier verifier(pubKey);

    // Java or OpenSSL created signature. It is ANS.1
    //   SEQUENCE ::=  r INTEGER, s INTEGER .
    const byte derSignature[] = 
        0x30, 0x44, 0x02, 0x20, 0x08, 0x66, 0xc8, 0xf1,
        0x6f, 0x15, 0x00, 0x40, 0x8a, 0xe2, 0x1b, 0x40,
        0x56, 0x28, 0x9c, 0x17, 0x8b, 0xca, 0x64, 0x99,
        0x37, 0xdc, 0x35, 0xad, 0xad, 0x60, 0x18, 0x4d,
        0x63, 0xcf, 0x4a, 0x06, 0x02, 0x20, 0x78, 0x4c,
        0xb7, 0x0b, 0xa3, 0xff, 0x4f, 0xce, 0xd3, 0x01,
        0x27, 0x5c, 0x6c, 0xed, 0x06, 0xf0, 0xd7, 0x63,
        0x6d, 0xc6, 0xbe, 0x06, 0x59, 0xe8, 0xc3, 0xa5,
        0xce, 0x8a, 0xf1, 0xde, 0x01, 0xd5
    ;

    // P1363 'r || s' concatenation. The size is 32+32 due to field
    // size for r and s in secp-256. It is not 20+20 due to SHA-1.
    SecByteBlock signature(verifier.SignatureLength());
    DSAConvertSignatureFormat(signature, signature.size(), DSA_P1363,
                              derSignature, sizeof(derSignature), DSA_DER);

    // Message "Attack at dawn!"
    const byte message[] = 
        0x41, 0x74, 0x74, 0x61, 0x63, 0x6b, 0x20, 0x61,
        0x74, 0x20, 0x64, 0x61, 0x77, 0x6e, 0x21, 0x0a
    ;

    // https://www.cryptopp.com/wiki/Elliptic_Curve_Digital_Signature_Algorithm
    bool result = verifier.VerifyMessage(message, sizeof(message), signature, signature.size());
    if (result)
        std::cout << "Verified message" << std::endl;
    else
        std::cout << "Failed to verify message" << std::endl;

    return 0;

这是运行测试程序的结果。

$ ./test.exe
Signature (64):
0866C8F16F1500408AE21B4056289C178BCA649937DC35ADAD60184D63CF4A06784CB70BA3FF4FCE
D301275C6CED06F0D7636DC6BE0659E8C3A5CE8AF1DE01D5
Verified message

这是我用来重现cat test.txt | openssl dgst -ecdsa-with-SHA1 -sign sample.key -keyform DER &gt; test.sig 的设置。来自@DivB 在ECDSA sign with OpenSSL, verify with Crypto++ 的问题。

$ cat test.txt
Attack at dawn!

$ hexdump -C test.txt
00000000  41 74 74 61 63 6b 20 61  74 20 64 61 77 6e 21 0a  |Attack at dawn!.|
00000010

# Create private key in PEM format
$ openssl ecparam -name secp256k1 -genkey -noout -out secp256k1-key.pem

$ cat secp256k1-key.pem
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIO0D5Rjmes/91Nb3dHY9dxmbM7gVfxmB2+OVuLmWMbGXoAcGBSuBBAAK
oUQDQgAEgVNEuirUNCEVdf7nLSBUgU1GXLrtIBeglIbK54s91HlWKOKjk4CkJ3/B
wGAfcYKa+DgJ2IUQSD15K1T/ghM9eQ==
-----END EC PRIVATE KEY-----

# Convert private key to ASN.1/DER format
$ openssl ec -in secp256k1-key.pem -inform PEM -out secp256k1-key.der -outform DER

$ dumpasn1 secp256k1-key.der
  0 116: SEQUENCE 
  2   1:   INTEGER 1
  5  32:   OCTET STRING
       :     ED 03 E5 18 E6 7A CF FD D4 D6 F7 74 76 3D 77 19
       :     9B 33 B8 15 7F 19 81 DB E3 95 B8 B9 96 31 B1 97
 39   7:   [0] 
 41   5:     OBJECT IDENTIFIER secp256k1 (1 3 132 0 10)
       :     
 48  68:   [1] 
 50  66:     BIT STRING
       :       04 81 53 44 BA 2A D4 34 21 15 75 FE E7 2D 20 54
       :       81 4D 46 5C BA ED 20 17 A0 94 86 CA E7 8B 3D D4
       :       79 56 28 E2 A3 93 80 A4 27 7F C1 C0 60 1F 71 82
       :       9A F8 38 09 D8 85 10 48 3D 79 2B 54 FF 82 13 3D
       :       79
       :     
       :   

# Create public key from private key
$ openssl ec -in secp256k1-key.der -inform DER -pubout -out secp256k1-pub.der -outform DER

$ dumpasn1 secp256k1-pub.der
  0  86: SEQUENCE 
  2  16:   SEQUENCE 
  4   7:     OBJECT IDENTIFIER ecPublicKey (1 2 840 10045 2 1)
 13   5:     OBJECT IDENTIFIER secp256k1 (1 3 132 0 10)
       :     
 20  66:   BIT STRING
       :     04 81 53 44 BA 2A D4 34 21 15 75 FE E7 2D 20 54
       :     81 4D 46 5C BA ED 20 17 A0 94 86 CA E7 8B 3D D4
       :     79 56 28 E2 A3 93 80 A4 27 7F C1 C0 60 1F 71 82
       :     9A F8 38 09 D8 85 10 48 3D 79 2B 54 FF 82 13 3D
       :     79
       :   

# Sign the message using the private key
$ cat test.txt | openssl dgst -ecdsa-with-SHA1 -sign secp256k1-key.der -keyform DER > test.sig

# Dump the signature as hex
$ hexdump -C test.sig
00000000  30 44 02 20 08 66 c8 f1  6f 15 00 40 8a e2 1b 40  |0D. .f..o..@...@|
00000010  56 28 9c 17 8b ca 64 99  37 dc 35 ad ad 60 18 4d  |V(....d.7.5..`.M|
00000020  63 cf 4a 06 02 20 78 4c  b7 0b a3 ff 4f ce d3 01  |c.J.. xL....O...|
00000030  27 5c 6c ed 06 f0 d7 63  6d c6 be 06 59 e8 c3 a5  |'\l....cm...Y...|
00000040  ce 8a f1 de 01 d5                                 |......|
00000046

# Dump the signature as ASN.1/DER
$ dumpasn1 test.sig
  0  68: SEQUENCE 
  2  32:   INTEGER
       :     08 66 C8 F1 6F 15 00 40 8A E2 1B 40 56 28 9C 17
       :     8B CA 64 99 37 DC 35 AD AD 60 18 4D 63 CF 4A 06
 36  32:   INTEGER
       :     78 4C B7 0B A3 FF 4F CE D3 01 27 5C 6C ED 06 F0
       :     D7 63 6D C6 BE 06 59 E8 C3 A5 CE 8A F1 DE 01 D5
       :   

【讨论】:

【参考方案2】:

顺便说一句,解决您的问题的另一种方法(特别是允许您避免使用命令行)是修改 Java 代码以便有一种方法来生成 R 和 S 值,以及重现 DER编码值。

例如,您可以使用以下方法从 Java 签名中提取 R 和 S 值:

public static BigInteger extractR(byte[] signature) throws Exception 
    int startR = (signature[1] & 0x80) != 0 ? 3 : 2;
    int lengthR = signature[startR + 1];
    return new BigInteger(Arrays.copyOfRange(signature, startR + 2, startR + 2 + lengthR));


public static BigInteger extractS(byte[] signature) throws Exception 
    int startR = (signature[1] & 0x80) != 0 ? 3 : 2;
    int lengthR = signature[startR + 1];
    int startS = startR + 2 + lengthR;
    int lengthS = signature[startS + 1];
    return new BigInteger(Arrays.copyOfRange(signature, startS + 2, startS + 2 + lengthS));

这些方法尤其是 used in Wycheproof 可以直接使用 BigIntegers。

这些可以让您重建 CryptoPP 在 Java 中使用的 P1363 编码,但请注意不要忘记字节数组的左侧填充 0。 (否则,当 R 或 S 字节数组小于预期长度时,您可能会遇到问题。

您还可以使用以下方法从大整数重构 DER 编码签名:

public static byte[] derSign(BigInteger r, BigInteger s) throws Exception 
    byte[] rb = r.toByteArray();
    byte[] sb = s.toByteArray();
    int off = (2 + 2) + rb.length;
    int tot = off + (2 - 2) + sb.length;
    byte[] der = new byte[tot + 2];
    der[0] = 0x30;
    der[1] = (byte) (tot & 0xff);
    der[2 + 0] = 0x02;
    der[2 + 1] = (byte) (rb.length & 0xff);
    System.arraycopy(rb, 0, der, 2 + 2, rb.length);
    der[off + 0] = 0x02;
    der[off + 1] = (byte) (sb.length & 0xff);
    System.arraycopy(sb, 0, der, off + 2, sb.length);
    return der;

如您所见,这些方法可能会被翻译成 C++ 代码,因为它们实际上是基本的字节操作,但这是另一回事;)

【讨论】:

【参考方案3】:

在 Lery 出色的答案之上,我发现自己想要一个 64 字节的固定 P1363 样式签名。发布的 Java 解决方案很棒,但 rs 值可能包含符号位,因此会产生 64-66 字节的签名。

在这个 Kotlin 函数中,我计算了 rs 值,我只取了每个低 32 字节,这给了我想要的 64 字节签名。

fun generateSignatureFromKeystore(message: ByteArray, privateKey: PrivateKey): ByteArray 
    // BouncyCastle's signing doesn't work with android Keystore's ECPrivateKey
    val signatureConfig = Signature.getInstance("SHA256withECDSA").apply 
        initSign(privateKey)
        update(message)
    
    val signature = signatureConfig.sign()

    // Convert ASN.1 DER signature to IEEE P1363
    val startR = if (signature[1].toUnsignedInt().and(0) != 0) 3 else 2
    val lengthR = signature[startR + 1].toUnsignedInt()
    val r = signature.copyOfRange(startR + 2, startR + 2 + lengthR).takeLast(32).toByteArray()
    val startS = startR + 2 + lengthR
    val lengthS = signature[startS + 1].toInt()
    val s = signature.copyOfRange(startS + 2, startS + 2 + lengthS).takeLast(32).toByteArray()

    return r + s


private fun Byte.toUnsignedInt(): Int = toInt().and(0xFF)

【讨论】:

以上是关于ECDSA 使用 BouncyCastle 签名并使用 Crypto++ 进行验证的主要内容,如果未能解决你的问题,请参考以下文章

验证充气城堡上的 javacard 签名 ALG_ECDSA_SHA

使用带有曲线 secp224k1 的私钥签署 ECDSA

从两个大整数创建 ASN.1

每个椭圆曲线签名生成的不同签名

ECDSA 使用 HSM 签名并在 Java/Kotlin 中进行验证

使用 ecdsa 包验证 ECDSA 签名失败