使用 OpenSSL 将 P1363 编码签名转换为 ECDSA_SIG
Posted
技术标签:
【中文标题】使用 OpenSSL 将 P1363 编码签名转换为 ECDSA_SIG【英文标题】:Transform P1363 encoded signature into ECDSA_SIG using OpenSSL 【发布时间】:2019-11-12 16:55:35 【问题描述】:虽然 OpenSSL 始终假定 ECDSA 签名是 ASN.1/DER 编码的,但我还需要能够验证 P1363 编码的签名。 (有关这两种形式的全面介绍,请参见例如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://***.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 '\0', 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 == '\0')
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] = '\0';
/*
* 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 == '\0')
ps++;
hex_len = (sigbuf + sig_len) - ps;
hex = (char *)malloc(hex_len + 1);
strncpy(hex, (const char *)ps, hex_len);
hex[hex_len] = '\0';
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\006\002\001\007\002\001”。这显然不是我期望的 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 序列化时做错了。但对于我的生活,我看不到它是什么。任何帮助表示赞赏!
【问题讨论】:
if (strlen((char *)sigbuf) != sig_len)
- 糟糕的主意,因为sigbuf
肯定不是终止的字符串。不,在末尾添加一个零八位字节并不能解决任何问题。嵌入的零八位字节可以出现在该序列某处中当然是可行的。 TL;DR:您不应该使用任何与字符串相关的操作将其视为字符串,因为它不是字符串;它只是八位字节。
我想知道我是否会明白为什么我对 WhozCraig 评论的回答被删除了,部分是对坏主意部分的肯定,部分指出这与我的问题无关,因为对于给定的 sigbuf测试明显通过了?
唯一可以在 SO 上删除 anything 的人是版主和原作者。如果版主删除了您的评论,您当然有权询问原因。在我参与 SO 的 6 年多的时间里,我从未见过评论、答案或问题被删除,除非它是广告垃圾邮件或粗俗/冒犯性内容。我在您的评论中没有看到任何内容,所以我很好奇。关于您对“测试显然通过”的评估,而不是在定义的行为范围内。 strlen
需要一个终止的缓冲区;你没有提供它,它正在进入未知的记忆。
也就是说,我真的不认为首先需要使用 ASN.1 库来执行此操作。只需自己制造实际的 ASN.1 编码就足够了(字面意思是逐字节;这不像您不知道前导序列标记的样子,或者 ASN.1 整数 0x02
的作用)。我不得不对来自那边的类似编码执行此操作,包括 PGP 签名,最后,自己(好吧,我自己)将八位字节构建到 ASN.1 中比其他任何事情都容易。
我正在查看您想要的序列,它看起来正确,除了序列的大小。序列0x30
由两个34字节的整数组成(每个整数由2个字节表示类型和长度,后面的32个字节组成每个整数的256位值。)因此,序列应该是0x30 0x44 0x02 0x20。 ... 0x02 0x20 ......正如我所说,除非您真的深入研究奇怪的 ASN.1 工件(可选是一个很好的例子),否则自己编写字节通常更容易。当然,这取决于你。
【参考方案1】:
哇!这太傻了……
BN_hex2bn() 中的“十六进制”并不表示十六进制值的数组。相反,它表示十六进制值的字符串表示!
所以在我上面的例子中,如果我改为像这样初始化
const unsigned char sigbuf[] = "37258a3cf0056e239783aef5840a5e0a"
"1fc88a548405341d8286477c145114f8"
"0af4bccf58efcd69bdc023f1e2966aa8"
"28cf3560e6756d894a609b2b2a6d0651";
const int sig_len = 128;
我最终得到了一个不错的 ECDSA_SIG,因此也得到了不错的 der 和 derlen。
希望这可能对将来的某人有所帮助。 有时我真的很讨厌 OpenSSL 文档...
【讨论】:
【参考方案2】:所以真正的答案是使用BN_bin2bn(),就像上面的例子一样
s->r = BN_bin2bn(pr, hex_len, NULL);
请注意这是如何使用长度参数的。这强调了@WhozCraig 指出的事实,它实际上只是在这里处理的八位字节序列,而不是字符串。
另请注意,文档指出
BN_bin2bn() 将 s 处长度为 len 的 big-endian 形式的正整数转换为 BIGNUM 并将其放在 ret 中。如果 ret 为 NULL,则创建一个新的 BIGNUM。
因此,由于整数必须是正数,因此最终需要应用填充。
【讨论】:
以上是关于使用 OpenSSL 将 P1363 编码签名转换为 ECDSA_SIG的主要内容,如果未能解决你的问题,请参考以下文章
将 cer 转换为 pem 时出现 OpenSSL 编码错误
了解将 .pem 文件转换为 .pfx 的 OpenSSL 步骤 [关闭]
将 AWS KMS ECDSA_SHA_256 签名从 DER 编码的 ANS.1 格式转换为 JWT base64url 编码的 R || NodeJS/Javascript 中的 S 格式