CNG中的数字签名算法主要包括RSA、DSA和ECDSA,其中RSA算法所得的签名值是可以直接与OpenSSL相互验证的,而DSA和ECDSA则不行,这是因为输出的格式不同
OpenSSL采用DER,而ECDSA在OpenSSL3之前可以使用ECDSA_do_sign获得ECDSA_SIG格式,但OpenSSL3以后的版本因为隐藏了底部实现,所以其数字签名函数只能使用EVP_SignXXXXX、EVP_Digest和EVP_PKEY_sign这些函数了,其输出的签名格式为DER
而CNG的格式就比较直接了,以ECDSA为例,其签名格式就是r值和s值的拼接
r[size / 2]
s[size / 2]
其中size代表签名的长度,以BCRYPT_ECDSA_P256_ALGORITHM(即ECDSA_P256)为例,其计算出的签名大小为64个字节,其中前32个字节为r的值(大端数值),后32个字节为s的值(大端数值)
转换方式就是解析DER格式提取r值和s值,而后拼接。
OpenSSL3->CNG
基本思路和方法有两种:
一、直接解码转换
1、解析DER格式,读取r值和s值
2、拼接转换成CNG格式
二、间接转换
1、将签名的DER格式,转换成ECDSA_SIG格式
2、读取ECDSA_SIG格式中的r值和s值
3、拼接转换成CNG格式
方法一、效率高,但是需要对DER格式有足够的认识,如果只是简单读取两个大数值的话,实现还是比较简单的,但考虑到容错、查错和后续格式变化等问题,无异于要间接实现一个DER解析库,处理起来其它挺繁琐的
简单介绍一下,DER格式,其数据以TLV表示数据
EVP_Sign:
0x30, 0x45, 0x02, 0x21, 0x00, 0xa0, 0xd3, 0x0b, 0x0b, 0xda,
0xb1, 0x74, 0x97, 0x15, 0x76, 0xa2, 0x0b, 0x34, 0x42, 0xb4,
0xa3, 0xbe, 0xac, 0x20, 0xc2, 0xbb, 0x4c, 0x01, 0xcc, 0x86,
0x88, 0x2d, 0xdb, 0x66, 0x24, 0x3e, 0x17, 0x02, 0x20, 0x74,
0xd7, 0xa1, 0xad, 0x88, 0xb7, 0x0b, 0xb7, 0x3e, 0x96, 0xcc,
0x60, 0x02, 0x54, 0x16, 0xd4, 0x20, 0xec, 0x0b, 0xbe, 0xa5,
0x82, 0xbc, 0x78, 0xf9, 0x60, 0x85, 0xd7, 0x50, 0xb3, 0x75,
0x4b,
0x30[total-length]0x02[R-length][R]0x02[S-length][S][sighash]
total-length: 共一字节,表示其后的字节序列长度,不包括[sighash]部分。注意这里的total-length表示的不是整个DER数字签名的长度,而是该[total-length]字段之后的字节长度,但是为了表述方便,下文中就用该字段表示整个签名长度来叙述。
R-length: 共一字节,表示r部分长度。
R: 任意长度的大端序编码R值。它必须对正整数使用尽可能短的编码,这意味着在开始时没有空字节,除非r的第一个字节大于等于0x80,在r前置0x00。
S-length: 共一字节,表示s部分长度。
S: 任意长度的大端序编码S值,和R用同样的规则。
sighash: 一字节长度,该标志指定签名签署交易的哪个部分。
● 0x30表示DER序列的开始
● 0x45 - 序列的长度(69字节)
● 0x02 - 一个整数值
● 0x21 - 整数的长度(33字节)
● 0x00如果R值的第一个字节大于等于0x80,则需要前缀补0x00,长度加1
● R-00a0d30b0bdab174971576a20b3442b4a3beac20c2bb4c01cc86882ddb66243e17
● 0x02 - 接下来是一个整数
● 0x20 - 整数的长度(32字节)
● 如果S值的第一个字节大于等于0x80,则需要前缀补0x00(此处没有),长度加1
● S-74d7a1ad88b70bb73e96cc60025416d420ec0bbea582bc78f96085d750b3754b
● 后缀(此处没有)(如0x01)指示使用的哈希的类型(SIGHASH_ALL)
其中长度当小于127时,用一个字节表示,而大于127时,会另外前置一个字节用于表示长度所占用的字节大小,并在此字节最高位设为1
如468 对应二进制【0000 0001 1101 0100】
在当前二进制高位前加8位,最高位为1,低位表示数据长度字节数,实际长度为2字节,二进制编码为0010(十进制为2),所以前置新增的8位表示为【1000 0010】,其第7bit为1,而0~6bit的000 0010=2,代码此后用2个字节代表数据长度,对应DER编码格式长度为【1000 0010 0000 0001 1101 0100】,十六进制为0x8201D4
方法二、效率略低,但OpenSSL内部已经提供了相关函数,不用我们再担心后续的维护的问题
此处就以方法二的代码做演示,代码如下:
int ECDSA_openssl_to_CNG(const unsigned char* DER, long DER_len, unsigned char** CNG_SIG)
{
//数据转换的算法,将ECDSA_SIG二进制编码转换成ECDSA_SIG结构,再转换成DER二进制格式
ECDSA_SIG* sig = d2i_ECDSA_SIG(NULL, &DER, DER_len);
if (sig == NULL) return 0;
const BIGNUM *r = NULL, *s = NULL;
ECDSA_SIG_get0(sig, &r, &s);
if (r == NULL || s == NULL) return 0;
int rlen = BN_num_bytes(r);
int slen = BN_num_bytes(s);
int vlen = 0, sig_len = 0;
//CNG对签名大小有严格的要求,而rlen或slen实际可能略小于CNG签名所需大小(size / 2),所以需要补足
//int vlen = rlen > slen ? rlen : slen;//rlen和slen同时小于CNG签名大小(size / 2)
if (rlen <= 32) {//DER_len <= 72,ECDSA_P256签名的大小
vlen = 32;
sig_len = 64;
}
else if (rlen <= 48) {//DER_len <= 104,ECDSA_P384签名的大小
vlen = 48;
sig_len = 96;
}
else if (rlen <= 66) {//DER_len <= 139,ECDSA_P521签名的大小
vlen = 66;
sig_len = 132;
}
//CNG_SIG为NULL时,返回所需空间大小
if(NULL == CNG_SIG) {
return sig_len;
}
if(NULL == *CNG_SIG) {
*CNG_SIG = (PBYTE)HeapAlloc(GetProcessHeap (), 0, sig_len);
}
unsigned char* buf = *CNG_SIG;
BN_bn2binpad(r, buf, vlen);
BN_bn2binpad(s, &buf[vlen], vlen);
ECDSA_SIG_free(sig);
return sig_len;
}
CNG->OpenSSL
int ECDSA_CNG_to_openssl(const unsigned char* CNG_SIG, DWORD CNG_SIG_SIZE, unsigned char** DER)
{
//数据转换的算法,将ECDSA_SIG二进制编码转换成ECDSA_SIG结构,再转换成DER二进制格式
ECDSA_SIG* sig = ECDSA_SIG_new();
DWORD vlen = CNG_SIG_SIZE / 2;
BIGNUM* r = BN_bin2bn(CNG_SIG, vlen, NULL);
BIGNUM* s = BN_bin2bn(&CNG_SIG[vlen], vlen, NULL);
//此时,r和s的所有权会被转移到sig中去,所以不能BN_free
ECDSA_SIG_set0(sig, r, s);
int ret = i2d_ECDSA_SIG(sig, DER);
ECDSA_SIG_free(sig);
return ret;
}
备注:此处i2d_ECDSA_SIG(ECDSA_SIG *a, unsigned char **ppout)函数有个大坑
i2d_TYPE() encodes the structure pointed to by a into DER format. If ppout is not NULL, it writes the DER encoded data to the buffer at *ppout, and increments it to point after the data just written. If the return value is negative an error occurred, otherwise it returns the length of the encoded data.
If *ppout is NULL memory will be allocated for a buffer and the encoded data written to it. In this case *ppout is not incremented and it points to the start of the data just written.
当*ppout为NULL时,i2d_ECDSA_SIG会内部构造空间,并且运行后*ppout的值指向该空间的头部,此时可以用OPENSSL_free正常释放,如果*ppout不为NULL,则运行后*ppout的值指向该空间写入数据的尾部。如果*ppout是被动态分配的,那此时不能直接调用OPENSSL_free或其它释放空间的函数释放,也不可以直接把它当成DER格式的数据直接使用,因为*ppout已经被改写了
unsigned char* DER = NULL;
int ret = i2d_ECDSA_SIG(sig, &DER);
...
OPENSSL_free(DER);//可以正常使用和释放
//但是
unsigned char* DER = NULL;
int ret = i2d_ECDSA_SIG(sig, NULL);
DER = OPENSSL_malloc(ret);
ret = i2d_ECDSA_SIG(sig, &DER);
...//不可以直接使用DER,因为i2d_ECDSA_SIG改变了DER最后的指向
OPENSSL_free(DER);//发送错误
//需要
unsigned char* DER = NULL;
int ret = i2d_ECDSA_SIG(sig, NULL);
DER = OPENSSL_malloc(ret);
unsigned char* bu

本文详细介绍了如何在CryptographyAPI:NextGeneration(CNG)和OpenSSL3之间进行ECDSA数字签名的互验证。主要涉及CNG的签名格式转换为OpenSSL3兼容的DER格式,以及OpenSSL3的签名转换回CNG的格式。同时,文章还涵盖了密钥对的转换,包括CNG公钥和私钥转换为OpenSSL格式,反之亦然。
最低0.47元/天 解锁文章
1450





