使用 OpenSSL 将 P1363 编码签名转换为 ECDSA_SIG
Transform P1363 encoded signature into ECDSA_SIG using OpenSSL
虽然 OpenSSL 始终假定 ECDSA 签名是 ASN.1/DER 编码的,但我还需要能够验证 P1363 编码的签名。 (有关这 2 种形式的全面介绍,请参阅 the answer to this SO question。)
我们的想法是修补 ECDSA_verify(),这样如果 ASN.1 解析失败,P1363 就会被假定并转换为合成的 ECDSA_SIG,然后可以将其输入 ECDSA_do_verify ().
我就是这样做的:
#include <openssl/ecdsa.h>
#include <string.h>
using namespace std;
const unsigned char sigbuf[] =
{
0x37, 0x25, 0x8a, 0x3c, 0xf0, 0x05, 0x6e, 0x23, 0x97, 0x83, 0xae, 0xf5, 0x84, 0x0a, 0x5e, 0x0a,
0x1f, 0xc8, 0x8a, 0x54, 0x84, 0x05, 0x34, 0x1d, 0x82, 0x86, 0x47, 0x7c, 0x14, 0x51, 0x14, 0xf8,
0x0a, 0xf4, 0xbc, 0xcf, 0x58, 0xef, 0xcd, 0x69, 0xbd, 0xc0, 0x23, 0xf1, 0xe2, 0x96, 0x6a, 0xa8,
0x28, 0xcf, 0x35, 0x60, 0xe6, 0x75, 0x6d, 0x89, 0x4a, 0x60, 0x9b, 0x2b, 0x2a, 0x6d, 0x06, 0x51
};
const int sig_len = 64;
//int ECDSA_verify(int type, const unsigned char *dgst, int dgst_len,
// const unsigned char *sigbuf, int sig_len, EC_KEY *eckey)
int main(int argc, char *argv[])
{
ECDSA_SIG *s;
const unsigned char *p = sigbuf;
unsigned char *der = NULL;
int derlen = -1;
int ret = -1;
s = ECDSA_SIG_new();
if (s == NULL)
return (ret);
if (d2i_ECDSA_SIG(&s, &p, sig_len) == NULL) {
/*
* ASN.1 decoding failed, see crypto/asn1/tasn_dec.c line 515ff.
* Assume s is encoded as IEEE P1363. for a comprehensive description see
* ttps://whosebug.com/questions/36542645/does-openssl-sign-for-ecdsa-apply-asn1-encoding-to-the-hash-before-signing
* Fill the ECDSA_SIG from the P1363.
*/
if ((sig_len % 2) != 0)
return (ret);
if (strlen((char *)sigbuf) != sig_len)
return (ret);
if (s == NULL)
s = ECDSA_SIG_new();
if (s == NULL)
return (ret);
/*
* BN_hex2bn() stops immediately if the hex string starts with '[=11=]', so we skip zeroes.
* I /think/ only the s part of the P1363 may be padded, but it does no harm to skip them
* for the r part, too.
*/
const unsigned char *pr = sigbuf;
while (*pr == '[=11=]')
pr++;
/*
* BN_hex2bn() is greedy, so we create null-terminated copies of both the r and s parts.
* Also note that it looks like BN_hex2bn() takes care of the required leading zero padding
* in case of negative bignums.
*/
int hex_len = (sigbuf + sig_len / 2) - pr;
char *hex = (char *)malloc(hex_len + 1);
strncpy(hex, (const char *)pr, hex_len);
hex[hex_len] = '[=11=]';
/*
* Finally create the BIGNUM and put it in the r part of the ECDSA_SIG.
*/
BN_hex2bn(&(s->r), hex);
free(hex);
/*
* Now do the same for the s part...
*/
unsigned char *ps = const_cast<unsigned char *>(sigbuf) + sig_len / 2;
while (*ps == '[=11=]')
ps++;
hex_len = (sigbuf + sig_len) - ps;
hex = (char *)malloc(hex_len + 1);
strncpy(hex, (const char *)ps, hex_len);
hex[hex_len] = '[=11=]';
BN_hex2bn(&(s->s), hex);
free(hex);
}
/* Ensure signature uses DER and doesn't have trailing garbage */
derlen = i2d_ECDSA_SIG(s, &der);
// if (derlen != sig_len || memcmp(sigbuf, der, derlen))
// goto err;
// ret = ECDSA_do_verify(dgst, dgst_len, s, eckey);
err:
if (derlen > 0) {
OPENSSL_cleanse(der, derlen);
OPENSSL_free(der);
}
ECDSA_SIG_free(s);
return (ret);
}
sigbuf 是一个 prime256v1 真实示例,从 asn1parse 中获取。
现在当我运行上面的程序时,derlen是8,der是“0[=28=]6[=28=]2[=28=]1[=28=]7[=28=]2[=28=]1”。这显然不是我期望的 ASN.1,它应该是:
#30 46 (SEQUENCE, 70 bytes) <-- edit: wrong
30 44 (SEQUENCE, 68 bytes)
02 20 (INTEGER, 32 bytes)
(no padding)
37 25 8A 3C F0 05 6E 23 97 83 AE F5 84 0A 5E 0A
1F C8 8A 54 84 05 34 1D 82 86 47 7C 14 51 14 F8
02 20 (INTEGER, 32 bytes)
(no padding)
0A F4 BC CF 58 EF CD 69 BD C0 23 F1 E2 96 6A A8
28 CF 35 60 E6 75 6D 89 4A 60 9B 2B 2A 6D 06 51
不应该吗?
显然我在从十六进制缓冲区创建 BIGNUM 或从它们创建 ECDSA_SIG 或 ASN.1 序列化时做错了。但对于我的生活,我不明白它是什么。任何帮助表示赞赏!
呸!这太傻了...
BN_hex2bn() 中的"hex" 并不表示十六进制值数组。相反,它意味着十六进制值的 字符串表示形式 !
所以在我上面的例子中,如果我改为初始化
const unsigned char sigbuf[] = "37258a3cf0056e239783aef5840a5e0a"
"1fc88a548405341d8286477c145114f8"
"0af4bccf58efcd69bdc023f1e2966aa8"
"28cf3560e6756d894a609b2b2a6d0651";
const int sig_len = 128;
我最终得到了一个体面的 ECDSA_SIG,因此也得到了体面的 der 和 derlen。
希望这对以后的人有所帮助。
有时我真的很讨厌 OpenSSL 文档...
所以真正的答案是使用BN_bin2bn(),就像上面的例子
s->r = BN_bin2bn(pr, hex_len, NULL);
请注意这是如何采用长度参数的。这强调了@WhozCraig 指出的事实,即这里处理的实际上只是八位字节序列,而不是字符串。
另请注意,文档指出
BN_bin2bn() converts the positive integer in big-endian form of length len at s into a BIGNUM and places it in ret. If ret is NULL, a new BIGNUM is created.
因此,由于整数必须为正数,因此最终需要应用填充。
虽然 OpenSSL 始终假定 ECDSA 签名是 ASN.1/DER 编码的,但我还需要能够验证 P1363 编码的签名。 (有关这 2 种形式的全面介绍,请参阅 the answer to this SO question。)
我们的想法是修补 ECDSA_verify(),这样如果 ASN.1 解析失败,P1363 就会被假定并转换为合成的 ECDSA_SIG,然后可以将其输入 ECDSA_do_verify ().
我就是这样做的:
#include <openssl/ecdsa.h>
#include <string.h>
using namespace std;
const unsigned char sigbuf[] =
{
0x37, 0x25, 0x8a, 0x3c, 0xf0, 0x05, 0x6e, 0x23, 0x97, 0x83, 0xae, 0xf5, 0x84, 0x0a, 0x5e, 0x0a,
0x1f, 0xc8, 0x8a, 0x54, 0x84, 0x05, 0x34, 0x1d, 0x82, 0x86, 0x47, 0x7c, 0x14, 0x51, 0x14, 0xf8,
0x0a, 0xf4, 0xbc, 0xcf, 0x58, 0xef, 0xcd, 0x69, 0xbd, 0xc0, 0x23, 0xf1, 0xe2, 0x96, 0x6a, 0xa8,
0x28, 0xcf, 0x35, 0x60, 0xe6, 0x75, 0x6d, 0x89, 0x4a, 0x60, 0x9b, 0x2b, 0x2a, 0x6d, 0x06, 0x51
};
const int sig_len = 64;
//int ECDSA_verify(int type, const unsigned char *dgst, int dgst_len,
// const unsigned char *sigbuf, int sig_len, EC_KEY *eckey)
int main(int argc, char *argv[])
{
ECDSA_SIG *s;
const unsigned char *p = sigbuf;
unsigned char *der = NULL;
int derlen = -1;
int ret = -1;
s = ECDSA_SIG_new();
if (s == NULL)
return (ret);
if (d2i_ECDSA_SIG(&s, &p, sig_len) == NULL) {
/*
* ASN.1 decoding failed, see crypto/asn1/tasn_dec.c line 515ff.
* Assume s is encoded as IEEE P1363. for a comprehensive description see
* ttps://whosebug.com/questions/36542645/does-openssl-sign-for-ecdsa-apply-asn1-encoding-to-the-hash-before-signing
* Fill the ECDSA_SIG from the P1363.
*/
if ((sig_len % 2) != 0)
return (ret);
if (strlen((char *)sigbuf) != sig_len)
return (ret);
if (s == NULL)
s = ECDSA_SIG_new();
if (s == NULL)
return (ret);
/*
* BN_hex2bn() stops immediately if the hex string starts with '[=11=]', so we skip zeroes.
* I /think/ only the s part of the P1363 may be padded, but it does no harm to skip them
* for the r part, too.
*/
const unsigned char *pr = sigbuf;
while (*pr == '[=11=]')
pr++;
/*
* BN_hex2bn() is greedy, so we create null-terminated copies of both the r and s parts.
* Also note that it looks like BN_hex2bn() takes care of the required leading zero padding
* in case of negative bignums.
*/
int hex_len = (sigbuf + sig_len / 2) - pr;
char *hex = (char *)malloc(hex_len + 1);
strncpy(hex, (const char *)pr, hex_len);
hex[hex_len] = '[=11=]';
/*
* Finally create the BIGNUM and put it in the r part of the ECDSA_SIG.
*/
BN_hex2bn(&(s->r), hex);
free(hex);
/*
* Now do the same for the s part...
*/
unsigned char *ps = const_cast<unsigned char *>(sigbuf) + sig_len / 2;
while (*ps == '[=11=]')
ps++;
hex_len = (sigbuf + sig_len) - ps;
hex = (char *)malloc(hex_len + 1);
strncpy(hex, (const char *)ps, hex_len);
hex[hex_len] = '[=11=]';
BN_hex2bn(&(s->s), hex);
free(hex);
}
/* Ensure signature uses DER and doesn't have trailing garbage */
derlen = i2d_ECDSA_SIG(s, &der);
// if (derlen != sig_len || memcmp(sigbuf, der, derlen))
// goto err;
// ret = ECDSA_do_verify(dgst, dgst_len, s, eckey);
err:
if (derlen > 0) {
OPENSSL_cleanse(der, derlen);
OPENSSL_free(der);
}
ECDSA_SIG_free(s);
return (ret);
}
sigbuf 是一个 prime256v1 真实示例,从 asn1parse 中获取。
现在当我运行上面的程序时,derlen是8,der是“0[=28=]6[=28=]2[=28=]1[=28=]7[=28=]2[=28=]1”。这显然不是我期望的 ASN.1,它应该是:
#30 46 (SEQUENCE, 70 bytes) <-- edit: wrong
30 44 (SEQUENCE, 68 bytes)
02 20 (INTEGER, 32 bytes)
(no padding)
37 25 8A 3C F0 05 6E 23 97 83 AE F5 84 0A 5E 0A
1F C8 8A 54 84 05 34 1D 82 86 47 7C 14 51 14 F8
02 20 (INTEGER, 32 bytes)
(no padding)
0A F4 BC CF 58 EF CD 69 BD C0 23 F1 E2 96 6A A8
28 CF 35 60 E6 75 6D 89 4A 60 9B 2B 2A 6D 06 51
不应该吗?
显然我在从十六进制缓冲区创建 BIGNUM 或从它们创建 ECDSA_SIG 或 ASN.1 序列化时做错了。但对于我的生活,我不明白它是什么。任何帮助表示赞赏!
呸!这太傻了...
BN_hex2bn() 中的"hex" 并不表示十六进制值数组。相反,它意味着十六进制值的 字符串表示形式 !
所以在我上面的例子中,如果我改为初始化
const unsigned char sigbuf[] = "37258a3cf0056e239783aef5840a5e0a"
"1fc88a548405341d8286477c145114f8"
"0af4bccf58efcd69bdc023f1e2966aa8"
"28cf3560e6756d894a609b2b2a6d0651";
const int sig_len = 128;
我最终得到了一个体面的 ECDSA_SIG,因此也得到了体面的 der 和 derlen。
希望这对以后的人有所帮助。 有时我真的很讨厌 OpenSSL 文档...
所以真正的答案是使用BN_bin2bn(),就像上面的例子
s->r = BN_bin2bn(pr, hex_len, NULL);
请注意这是如何采用长度参数的。这强调了@WhozCraig 指出的事实,即这里处理的实际上只是八位字节序列,而不是字符串。
另请注意,文档指出
BN_bin2bn() converts the positive integer in big-endian form of length len at s into a BIGNUM and places it in ret. If ret is NULL, a new BIGNUM is created.
因此,由于整数必须为正数,因此最终需要应用填充。