title: ECDSA加密算法
date: 2021-12-31 16:42:15
categories:
tags:
- openssl
- c/c++
1、ECDSA简述
ECDSA的全名是Elliptic Curve DSA,即椭圆曲线DSA。它是Digital Signature Algorithm (DSA)应用了椭圆曲线加密算法的变种。椭圆曲线算法的原理很复杂,但是具有很好的公开密钥算法特性,通过公钥无法逆向获得私钥。
ECDSA算法用于数字签名,是ECC与DSA的结合,整个签名过程与DSA类似,所不一样的是签名中采取的算法为ECC,最后签名出来的值也是分为r,s。
ECC是建立在基于椭圆曲线的离散对数问题上的密码体制,给定椭圆曲线上的一个点G,并选取一个整数k,求解K=kG很容易(注意根据kG求解出来的K也是椭圆曲线上的一个点);反过来,在椭圆曲线上给定两个点K和G,若使K=kG,求整数k是一个难题。ECC就是建立在此数学难题之上,这一数学难题称为椭圆曲线离散对数问题。其中椭圆曲线上的点K则为公钥(注意公钥K不是一个整数而是一个椭圆曲线点,这个点在OpenSSL里面是用结构体EC_Point来表示的,整数k则为私钥(实际上是一个大整数)。
签名过程如下:
1、选择一条椭圆曲线Ep(a,b),和基点G;
2、选择私有密钥k(k<n,n为G的阶),利用基点G计算公开密钥K=kG;
3、产生一个随机整数r(r<n),计算点R=rG;
4、将原数据和点R的坐标值x,y作为参数,计算SHA256做为hash,即Hash=SHA1(原数据,x,y);
5、计算s≡r - Hash * k (mod n)
6、r和s做为签名值,如果r和s其中一个为0,重新从第3步开始执行
验证过程如下:
1、接受方在收到消息(m)和签名值(r,s)后,进行以下运算
2、计算:sG+H(m)P=(x1,y1), r1≡ x1 mod p。
3、验证等式:r1 ≡ r mod p。
4、如果等式成立,接受签名,否则签名无效。
2、生成ECDSA公钥和私钥
其中,ECDSAPriKeyFileName、ECDSAPubKeyFileName为要保存公钥和私钥的文件路径。
int GeneratorRsaKey::CreateECDSAKey()
{
int Ret = 0;
EC_KEY* ec_key = NULL; //椭圆曲线的参数、私钥和公钥都保存在这个结构中。
EC_GROUP* ec_group; //这个结构体保存着椭圆曲线的参数
ec_key = EC_KEY_new();//通过调用EC_KEY_new()来构造没有关联曲线的新EC_KEY。
if (!ec_key)
{
printf("Error:ec_key is err!\n");
return Ret;
}
ec_group = EC_GROUP_new_by_curve_name(NID_secp256k1); //根据指定的椭圆曲线来生成密钥参数。
if (!ec_group)
{
printf("Error:ec_group is err\n");
return Ret;
}
//asn1_标志值用于确定曲线编码是使用显式参数还是使用asn1 OID的命名曲线:许多应用程序仅支持后一种形式。
EC_GROUP_set_asn1_flag(ec_group, OPENSSL_EC_NAMED_CURVE);
//如果asn1_标志为OPENSSL_EC_NAMED_CURVE,则使用命名曲线形式,并且参数必须具有相应的命名曲线NID集
EC_GROUP_set_point_conversion_form(ec_group, POINT_CONVERSION_UNCOMPRESSED);//获取曲线的点转换形式。
if (1 != EC_KEY_set_group(ec_key, ec_group))//将EC_GROUP结构体的内容填充到EC_KEY中
{
printf("Error:EC_KEY_set_group err\n");
return Ret;
}
//EC_KEY_generate_KEY()为提供的eckey对象生成新的公钥和私钥。在调用此函数之前,eckey必须有一个与之关联的EC_组对象.
//私钥是一个随机整数(0<priv_key<order,其中order是EC_组对象的顺序)。公钥是曲线上的一个EC_点,通过曲线生成器乘以私钥计算得出。
if (!EC_KEY_generate_key(ec_key))
{
printf("Error:EC_KEY_generate_key() err\n");
return Ret;
}
//保存公钥
if (!ECDSAPubKeyFileName)
{
fprintf(stderr, "Cannot open file %s\r\n", ECDSAPubKeyFileName);
return ERR_OPENFILE;
}
Ret = WritePublicKeyToFile(ECDSAPubKeyFileName, ec_key);
//保存私钥
if (!ECDSAPriKeyFileName)
{
fprintf(stderr, "Cannot open file %s\r\n", ECDSAPriKeyFileName);
return ERR_OPENFILE;
}
Ret = WritePrivateKeyToFile(ECDSAPriKeyFileName, ec_key, ec_group);
if (ec_key)
{
EC_KEY_free(ec_key);
ec_key = NULL;
}
return 0;
}
int GeneratorRsaKey::WritePublicKeyToFile(const char* Path, EC_KEY* pKey)
{
int Ret = 0;
BIO* pBioFile = BIO_new_file(Path, "wb+");
if (!pBioFile)
{
printf("Error:pBioFile err \n");
return Ret;
}
if (1 != PEM_write_bio_EC_PUBKEY(pBioFile, pKey)) //写入公钥
{
printf("Error:PEM_write_bio_EC_PUBKEY err\n");
return Ret;
}
Ret = ECDSA_SUCESS;
BIO_free(pBioFile);
return Ret;
}
int GeneratorRsaKey::WritePrivateKeyToFile(const char* Path, EC_KEY* pKey, EC_GROUP* ec_group)
{
int Ret = 0;
BIO* pBioFile = BIO_new_file(Path, "wb+");
if (!pBioFile)
{
printf("Error:pBioFile err \n");
return Ret;
}
PEM_write_bio_ECPKParameters(pBioFile, ec_group);
if (1 != PEM_write_bio_ECPrivateKey(pBioFile, pKey, NULL, NULL, 0, NULL, NULL)) //写入私钥
{
printf("Error:PEM_write_bio_ECPrivateKey err\n");
return Ret;
}
Ret = ECDSA_SUCESS;
BIO_free(pBioFile);
return Ret;
}
生成密钥
-----BEGIN EC PARAMETERS-----
BgUrgQQACg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIAEDdomPvSs/hjUobObUr5Kq4xfjcKDn+QV1ExnIsyPQoAcGBSuBBAAK
oUQDQgAEkHwZd/CTpWrcfuvXGbHDZbyfPna1hiAQlEsiSIaVTFu99IEwcTginJiM
9TEJrYJ5LODD4znP9kVDuRbcZaguaQ==
-----END EC PRIVATE KEY-----
*BgUrgQQACg==*
是椭圆曲线的关键参数,对应secp256k1
标识。
用secp256k1
生成私钥每次私钥是不同的,但EC PARAMETERS
都是相同的。
只有用不同的name指定不同曲线EC PARAMETERS
才会不同。
生成公钥
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEkHwZd/CTpWrcfuvXGbHDZbyfPna1hiAQ
lEsiSIaVTFu99IEwcTginJiM9TEJrYJ5LODD4znP9kVDuRbcZaguaQ==
-----END PUBLIC KEY-----
生成的是非压缩格式的公钥。
密钥数据结构
密钥数据结构定义在openssl-1.1.1\crypto\ec\ec_local.h文件中。
struct ec_key_st {
const EC_KEY_METHOD *meth;
ENGINE *engine;
int version;
EC_GROUP *group; //密钥参数
EC_POINT *pub_key; //公钥 是曲线上的一个点
BIGNUM *priv_key; //私钥
unsigned int enc_flag;
point_conversion_form_t conv_form;
int references;
int flags;
CRYPTO_EX_DATA ex_data;
CRYPTO_RWLOCK *lock;
};
参数解释:
1、 EC_KEY_METHOD *meth;
在结构体ec_method_st
中列举了实现过程中用到的各种椭圆曲线算法,比如椭圆曲线点群的建立和释放,设置群参数,点的比较,点的加法和倍乘等等,覆盖面很广,几乎涉及所有的椭圆曲线算法。
其主要作用在于能够将函数在素域和二元域的接口统一起来。举个例子,“判断点是否在曲线上”只需要调用EC_POINT_is_on_curve
函数,而无需考虑是二元域还是素域。
2、ENGINE *engine;
ENGINE
是OPENSSL
预留的用以加载第三方加密库引擎,主要包括了动态库加载的代码和加密函数指针管理的一系列接口。如果要使用Engine,那么首先要加载该*Engine
(比如ENGINE_load_XXXX
),然后选择要使用的算法或者使用支持的所有加密算法(有相关函数)。这样你的应用程序在调用加解密算法时,它就会指向你加载的动态库里的加解密算法,而不是原先的OPENSSL
*的库里的加解密算法。
3、EC_GROUP *group;
椭圆曲线数据结构:EC_GROUP
,该结构不仅包含各个参数,还包含了各种算法;根据密钥参数group
来生公私钥。
对于ECC
算法来说,仅仅知道公钥和私钥是不能调用OpenSSL
自带的签名和验签API,还需要知道对应的椭圆曲线。*BgUrgQQACg==*
是椭圆曲线的关键参数,对应secp256k1
标识。所生产得密钥中这参数便是用来确认椭圆曲线的。
4、EC_POINT *pub_key;
EC_POINT,其中的大数X、Y和Z为雅克比投影坐标,向量;
5、BIGNUM *priv_key;
私钥,为一个大数。
6、unsigned int enc_flag;
当前定义了两个编码标志EC_PKEY_NO_PARAMETERS
和EC_PKEY_NO_PUBKEY
。这些标志定义了调用i2d_ECPrivateKey()
时如何将密钥转换为ASN1的行为。如果设置了EC_PKEY_NO_PARAMETERS
,则曲线的公共参数不会与私钥一起编码。如果设置了EC_PKEY_NO_PUBKEY
,则公钥不会与私钥一起编码。
7、point_conversion_form_t conv_form;
/** Enum for the point conversion form as defined in X9.62 (ECDSA)
* for the encoding of a elliptic curve point (x,y) */
typedef enum {
/** the point is encoded as z||x, where the octet z specifies
* which solution of the quadratic equation y is */
POINT_CONVERSION_COMPRESSED = 2, //表示采用点压缩。
/** the point is encoded as z||x||y, where z is the octet 0x04 */
POINT_CONVERSION_UNCOMPRESSED = 4, //表示不采用压缩
/** the point is encoded as z||x||y, where the octet z specifies
* which solution of the quadratic equation y is */
POINT_CONVERSION_HYBRID = 6 //表示混合使用,即既包含点压缩又包含未压缩
} point_conversion_form_t;
8、CRYPTO_EX_DATA ex_data;
额外的附加信息
9、CRYPTO_RWLOCK *lock;
加密时候的线程锁
3、通过ECDSA私钥进行签名
其中,hash、hashsize为其他算法(sha256/SM3)所生成的32位摘要。
使用256位EC密钥进行签名使用的是DER 编码模式,所以签名长度是不定的,在71 - 73 个字节之间
相关链接:
https://www.thinbug.com/q/17269238
https://learnblockchain.cn/article/1038
https://zhuanlan.zhihu.com/p/422864492
int GeneratorRsaKey::ECDSAPrivkeySign(uint8_t* hash_value, uint32_t hash_size)
{
unsigned char* buffer;
unsigned int buf_len;
int Ret = 0;
int size = 0;
//-------------ECDSA加密------------------------------
EC_KEY* ec_key;
BIO* pBioKeyFile;
pBioKeyFile = BIO_new_file(ECDSAPriKeyFileName, "rb");
ec_key = PEM_read_bio_ECPrivateKey(pBioKeyFile, NULL, NULL, NULL); //读取私钥
if (!ec_key)
{
printf("read ec_key err!\n");
Ret = ERR_ECDSA;
goto Failed;
}
buf_len = ECDSA_size(ec_key);//获取ECC密钥大小字节数
buffer = (unsigned char*)malloc(buf_len); //分配内存,buffer用来保存签名后的数据
if (!(Ret=ECDSA_sign(0, Hash, HashSize, buffer, &buf_len, ec_key)))
{
printf("ECDSA_sign is err!\n");
Ret = ERR_ECDSA;
goto Failed;
}
if (ec_key)
{
EC_KEY_free(ec_key);
ec_key = NULL;
}
if (pBioKeyFile)
{
BIO_free(pBioKeyFile);
pBioKeyFile = NULL;
}
return 0;
Failed:
return Ret;
}
其中,ECDSA_size函数用来获取ECC密钥的大小,后面生成的签名数据大小也是由他来决定。
int ECDSA_size(const EC_KEY *ec)
{
int ret;
ECDSA_SIG sig;
const EC_GROUP *group;
const BIGNUM *bn;
if (ec == NULL)
return 0;
group = EC_KEY_get0_group(ec); //获取EC_key中的曲线参数
if (group == NULL)
return 0;
bn = EC_GROUP_get0_order(group); //获取group的order,是一个大数,具体干啥的不知道
if (bn == NULL)
return 0;
sig.r = sig.s = (BIGNUM *)bn; //初始化sig.r和sig.s
ret = i2d_ECDSA_SIG(&sig, NULL); //通过i2d_ECDSA_SIG函数对sig结构体进行编码为DER结构,返回值为其编码大小。
if (ret < 0)
ret = 0;
return ret;
}
其中,i2d_ECDSA_SIG
函数中有个关键函数:ossl_encode_der_dsa_sig
该函数输出ECDSA-Sig-Value
的DER编码至pkt,由pkt用来存储编码内容。
签名值数据结构
ECDSA 的签名结果表示为两项。 ECDSA 的签名结果数据结构定义在 crypto\ec\ec_lcl.h
中。
一般情况,最后的结果r
和s
是用asn1
格式的DER
编码封装的,至少的TLS
签名和数字证书签名中是这样的,不是简单的r+s
这样字节直接拼接.
关于der
编码方式可参见这边博客:https://www.twblogs.net/a/5b7e3af92b71776838560af9/?lang=zh-cn
struct ECDSA_SIG_st {
BIGNUM *r;
BIGNUM *s;
};
这里使用SM3或者SHA256得到数据摘要后进行签名,以下面一个签名为例讲解DER编码后的签名序列。
buffer=[3046022100adf6eafb0a32f398a88819ee984333e4241764487bbb47e68adb0b6533d6454c022100a4d4e55e928f1ba32e53405dffb781ae0bccf2acb8c19a1095e440189c20d388]
// 0x30表示DER序列的开始
// 0x46 - 序列的长度(70字节)
// 0x02 - 一个整数值
// 0x21 - 整数的长度(33字节)
// R-00adf6eafb0a32f398a88819ee984333e4241764487bbb47e68adb0b6533d6454c
// 0x02 - 接下来是一个整数
// 0x21 - 整数的长度(33字节)
// S-00a4d4e55e928f1ba32e53405dffb781ae0bccf2acb8c19a1095e440189c20d388
4、通过ECDSA公钥验签
其中,hash、hashsize为其他算法(sha256/SM3)所生成的32位摘要。
int GeneratorRsaKey::ECDSAPubkeyVerify(unsigned char* buffer, unsigned int buf_len)
{
int Ret = 0;
EC_KEY* ec_key = NULL;
BIO* pBioKeyFile = NULL;
pBioKeyFile = BIO_new_file(ECDSAPubKeyFileName, "rb");
ec_key = PEM_read_bio_EC_PUBKEY(pBioKeyFile, NULL, NULL, NULL); //从pem文件中读取公钥
if (!ec_key)
{
printf("read pubkey failed!\n");
}
Ret = ECDSA_verify(0, Hash, HashSize, buffer, buf_len, ec_key); //buffer为加密后的数据,buf_len为加密数据长度一般为71
if (Ret == 0)
{
printf("ECDSA_verify failed!\n");
}
if (pBioKeyFile)
{
BIO_free(pBioKeyFile);
pBioKeyFile = NULL;
}
if (ec_key)
{
EC_KEY_free(ec_key);
ec_key = NULL;
}
return Ret;
}
5、如何使用别人给的公私密钥对生成对应的密钥文件?
查看openssl中所支持的椭圆曲线
openssl ecparam -list_curves
选择一条曲线,生成参数文件
Linux中openssl支持很对椭圆曲线,这里只贴了一部分,下面以曲线brainpoolP256r1为例。
openssl ecparam -name brainpoolP256r1 -out brainpool_test.pem
显示文件参数
openssl ecparam -in brainpool_test.pem -text -param_enc explicit -noout
使用参数文件生成私钥
openssl ecparam -in brainpool_test.pem -genkey -out brainpool_priv.key
从私钥中导出公钥
openssl ec -in brainpool_priv.key -pubout -out brainpool_pub.key
查看公私密钥对
openssl ec -in brainpool_priv.key -text -noout
至此,我们可以用使用Linux中自带的openssl库和相关命令生成ecdsa公私密钥对,但是如果工作中只有上图32字节的私钥和64字节的公钥(除去固定的开头04,X、Y各32字节),怎么生成对应的密钥文件呢?
6、通过设置公私密钥对生成对应密钥文件
通过设置曲线名称、私钥、公钥的方式生成固定的公私密钥文件。通过相关命令验证,和上面用openssl命令生成的公私密钥文件是一样的。