Cryptography API: Next Generation(CNG)使用梳理——非对称加密算法应用(五)与OpenSSL数字签名认证的互通(ECDSA)

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

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
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值