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 3、产生一个随机整数r(r 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、如果等式成立,接受签名,否则签名无效。 其中,ECDSAPriKeyFileName、ECDSAPubKeyFileName为要保存公钥和私钥的文件路径。 生成密钥 用 只有用不同的name指定不同曲线 生成公钥 生成的是非压缩格式的公钥。 密钥数据结构 密钥数据结构定义在openssl-1.1.1\crypto\ec\ec_local.h文件中。 参数解释: 1、 EC_KEY_METHOD *meth; 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; 8、CRYPTO_EX_DATA ex_data; 额外的附加信息 9、CRYPTO_RWLOCK *lock; 加密时候的线程锁 3、通过ECDSA私钥进行签名 使用256位EC密钥进行签名使用的是DER 编码模式,所以签名长度是不定的,在71 - 73 个字节之间 相关链接: 应用错误收集 伪代码简述 ECDSA 签名过程 | 联盟链开发 | 登链社区 | 区块链技术社区 C++版本ECDSA-with-SHA256签名验证 - 知乎 其中,i2d_ECDSA_SIG函数中有个关键函数:ossl_encode_der_dsa_sig该函数输出ECDSA-Sig-Value的DER编码至pkt,由pkt用来存储编码内容。 签名值数据结构 一般情况,最后的结果r和s是用asn1格式的DER编码封装的,至少的TLS签名和数字证书签名中是这样的,不是简单的r+s这样字节直接拼接. 这里使用SM3或者SHA256得到数据摘要后进行签名,以下面一个签名为例讲解DER编码后的签名序列。 其中,hash、hashsize为其他算法(sha256/SM3)所生成的32位摘要。2、生成ECDSA公钥和私钥
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
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
都是相同的。EC PARAMETERS
才会不同。-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEkHwZd/CTpWrcfuvXGbHDZbyfPna1hiAQ
lEsiSIaVTFu99IEwcTginJiM9TEJrYJ5LODD4znP9kVDuRbcZaguaQ==
-----END PUBLIC KEY-----
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;
};
在结构体ec_method_st中列举了实现过程中用到的各种椭圆曲线算法,比如椭圆曲线点群的建立和释放,设置群参数,点的比较,点的加法和倍乘等等,覆盖面很广,几乎涉及所有的椭圆曲线算法。
其主要作用在于能够将函数在素域和二元域的接口统一起来。举个例子,“判断点是否在曲线上”只需要调用EC_POINT_is_on_curve函数,而无需考虑是二元域还是素域。
/** 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;
其中,hash、hashsize为其他算法(sha256/SM3)所生成的32位摘要。
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;
}
ECDSA 的签名结果表示为两项。 ECDSA 的签名结果数据结构定义在 crypto\ec\ec_lcl.h 中。
关于der
编码方式可参见这边博客:https://www.twblogs.net/a/5b7e3af92b71776838560af9/?lang=zh-cnstruct ECDSA_SIG_st {
BIGNUM *r;
BIGNUM *s;
};
buffer=[3046022100adf6eafb0a32f398a88819ee984333e4241764487bbb47e68adb0b6533d6454c022100a4d4e55e928f1ba32e53405dffb781ae0bccf2acb8c19a1095e440189c20d388]
// 0x30表示DER序列的开始
// 0x46 - 序列的长度(70字节)
// 0x02 - 一个整数值
// 0x21 - 整数的长度(33字节)
// R-00adf6eafb0a32f398a88819ee984333e4241764487bbb47e68adb0b6533d6454c
// 0x02 - 接下来是一个整数
// 0x21 - 整数的长度(33字节)
// S-00a4d4e55e928f1ba32e53405dffb781ae0bccf2acb8c19a1095e440189c20d388
4、通过ECDSA公钥验签
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;
}