# ECDSA **Repository Path**: qomoliao/ecdsa ## Basic Information - **Project Name**: ECDSA - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 1 - **Created**: 2021-04-11 - **Last Updated**: 2022-08-13 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 背景 最近有个嵌入式的项目需要做IAP升级,而且要对升级固件进行签名校验,实现安全升级。 之前没有做过固件的签名校验,网上也很少能找到IAP签名校验的教程,于是在摸索干活的过程也顺带一记,做一分享。希望对看到的人和未来的自己有所帮助。 ## 数字签名简介 所谓签名,就是给某段信息做认证,说明段信息是签名者认可的(防抵赖——合同签名),没有被人修改过的(防篡改——邮件帖封条并签名)。 数字世界的签名,也是为了防篡改和防抵赖。在我们的固件升级场景,主要是防止不被信任的攻击者制作恶意固件给我们的系统升级,造成某种不良后果。同时能够校验固件传输过程是否产生数据丢失或变化,防止升级了异常固件导致系统失效。 ![数字签名原理](/media/202104/数字签名原理_1618064938.jpg) 数字签名的大体过程如上图,虚线左边是签名过程: 1. 首先,需要对编译好的固件先做哈希运算,得到一段比较短的摘要信息。 2. 然后使用我们自己的私钥对摘要进行加密,加密后的这段信息就叫做数字签名。 之后,可以将固件+加密后的摘要一起发给接收方,接收方: 1. 用公钥对加密后的摘要解密,获得原始摘要。 2. 用相同的哈希算法对接收到的固件做哈希运算,得到另一份摘要。 3. 将两种计算方法获得的摘要做对比,若相同,说明固件是可信的,否则不可信。 数字签名有两个关键: 1. 一是必须采用非对称算法。只有信息发送方掌握私钥,即使攻击者获取到存在MCU里的公钥,也没法伪造加密的摘要。 2. 二是非对称算法对算力要求比较高,不可能对整个固件做加密。所以需要先用哈希算法计算一个摘要,再对摘要进行加密。 ## 非对称加密算法 非对称加密算法有两类:RSA和ECC。 1. RSA算法的原理是:根据数论,寻求两个大素数比较简单,而将它们的乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥。 2. ECC是建立在基于椭圆曲线的离散对数问题上的密码体制,给定椭圆曲线上的一个点G,并选取一个整数k,求解K=kG很容易(注意根据kG求解出来的K也是椭圆曲线上的一个点);反过来,在椭圆曲线上给定两个点K和G,若使K=kG,求整数k是一个难题。ECC就是建立在此数学难题之上,这一数学难题称为椭圆曲线离散对数问题。 具体原理不再展开。 ## 哈希算法 哈希算法有很多,如MD2、MD5、SHA等,目前被认为安全的哈希算法是256位或以上的SHA算法。 ## 公司要求与算法选型 为了密钥安全,我司使用的签名密钥必须通过公司平台申请。申请通过后平台返回一个签名工具和公钥,私钥是存在公司服务器里的,任何人都无法获取。 同时,为了保证加密信息的破解难度,平台限制了加密算法的种类和参数,仅提供有限的几种算法。 若采用RSA算法,必须使用3072位的RSA3072bits;若采用ECC算法,椭圆曲线只能选prime256v1或secp384r1。 考虑到ECC算法的密钥长度比较短,计算资源消耗小,考虑采用ECC算法。SHA+ECC用于数字签名也叫做ECDSA,本项目选用了secp384r1参数的ECDSA签名算法。关于ECDSA算法的原理,可以参考知乎文章[《一文读懂ECDSA算法如何保护数据》](https://zhuanlan.zhihu.com/p/97953640)。 ## 用openssl进行签名与校验 先使用openssl工具对固件进行签名,获取签名过程中生成的摘要、最终签名信息。以与开发的算法计算结果做比对,验证算法准确性。 openssl生成的公钥等信息都经过了某种规范的编码,为了能够对比,必须了解这种格式的编码规则,从而提取出算法使用的具体参数。具体的编码规则和解析工具可看考附录。 ### 摘要计算 显示支持的摘要算法列表: `openssl dgst -list` 用sha384计算摘要: `openssl dgst -sha384 test.txt` ### ECC签名 显示支持的ECC曲线: `openssl ecparam -list_curves` 选择secp384r1曲线,生成密钥: `openssl ecparam -name secp384r1 -genkey -out ec.key` 生成的密钥采用ECC格式,用以下命令获取16进制的私钥和公钥: ```actionscript root@localhost:~# openssl ec -in ec.key -noout -text read EC key Private-Key: (384 bit) priv: fd:6d:22:76:6e:89:34:a1:08:5e:c9:d8:9b:a9:69: 51:03:ac:0e:bd:14:f0:09:72:fb:5b:1e:ff:33:92: fd:d9:f3:b8:a7:26:27:1e:2d:bb:b0:1c:31:f7:00: 18:7c:28 pub: 04:5f:50:a4:36:4a:e9:a2:78:f6:0f:56:78:5b:11: b8:92:0b:98:5e:9b:a0:69:6a:88:11:3f:5f:21:b3: 80:08:bc:05:53:c8:12:dc:e0:02:af:c4:d2:79:4c: 35:a9:6f:be:6d:0f:78:b1:3d:3e:f9:5c:45:24:53: 76:31:3b:22:1c:64:1b:8c:42:f6:84:75:68:14:14: 5b:96:34:f3:de:2d:b9:0f:46:2d:9f:2c:fd:d4:28: b2:0f:ea:ee:ab:7d:c1 ASN1 OID: secp384r1 NIST CURVE: P-384 ``` > 参考[一文读懂ECDSA算法如何保护数据 ](https://zhuanlan.zhihu.com/p/97953640),ECC加密的依据是公式**Qa = dA × G**。其中,G是椭圆曲线的起始点,dA是一个随机数(也即私钥),Qa是G与dA进行乘法运算后得到的点(也即公钥)。 对于secp384r1曲线 - 私钥就是一个48bytes的16进制数,如上priv字段所示。 - 公钥的表达格式有两种,[压缩格式或非压缩格式](https://www.jianshu.com/p/6f08bebc5aa6)。如上pub字段采用了**非压缩格式**:前缀04+x坐标+y坐标。可以转换为**压缩格式的公钥**:前缀03+x(如果y是奇数),前缀02+x(如果y是偶数)。 > 后面可以看到,Easy-ECC采用了压缩格式公钥作为参数。 生成公钥: `openssl ec -in ec.key -pubout -out ec.pub` 使用哈希sha384算法,和ECC密钥对test.txt文件签名,生成签名文件ec.sig: `openssl dgst -sha384 -sign ec.key -out ec.sig test.txt` > ec.sig是一个二进制文件,用ASN.1语言与DER编码规则描述。用[ASN.1在线解析工具](https://lapo.it/asn1js/)可以看到,其实就是两个384bits长度的整数,也即[ECDSA签名原理](https://zhuanlan.zhihu.com/p/97953640)中的值对(R, S)。 > 注意:ASN.1 规定整型 INTEGER 需要支持正整数、负整数和零。BER/DER 使用大端模式存储 INTEGER,并通过最高位来编码正负数(最高位0为正数,1为负数)。 如果密钥参数值最高位为 1,则 BER/DER 编码会在参数前额外加 0x00 以表示正数,这也就是为什么有时候密钥参数前面会多出1字节 0x00 的原因。 ![ec.sig签名示例](/media/202104/ECDSA签名解析_1618136074.jpeg) 为了方便的将签名的数据拷贝到我们的工程里,也可以使用如下指令输出十六进制格式的签名: ```bash $ xxd -i ec.sig unsigned char ec_sig[] = { 0x30, 0x66, 0x02, 0x31, 0x00, 0xaa, 0xc6, 0x8c, 0x0d, 0x2f, 0x18, 0x87, 0xbc, 0x96, 0x1c, 0xa4, 0x40, 0x01, 0xec, 0x7a, 0x71, 0xbd, 0x13, 0x3c, 0xd4, 0x74, 0xfc, 0xce, 0x22, 0xbf, 0x64, 0x57, 0x8a, 0x86, 0xde, 0x1e, 0xc4, 0x50, 0x75, 0x7b, 0x47, 0xc2, 0x0f, 0x2f, 0x31, 0x47, 0xa2, 0xa9, 0x0b, 0xba, 0xe0, 0x40, 0xdb, 0x02, 0x31, 0x00, 0xd9, 0x5a, 0xff, 0xf3, 0xe0, 0x08, 0xf8, 0x2c, 0x1b, 0x45, 0x0c, 0x2d, 0xcf, 0x4a, 0x57, 0xa7, 0x97, 0x49, 0x32, 0x17, 0x27, 0x63, 0x19, 0x8c, 0x9c, 0x2f, 0x04, 0x88, 0x61, 0x52, 0x55, 0x55, 0x8c, 0xc2, 0xda, 0x92, 0xca, 0x56, 0xc0, 0x1b, 0xfd, 0x43, 0x1e, 0xf9, 0x59, 0x24, 0x13, 0x20 }; unsigned int ec_sig_len = 104; ``` ### 校验 `openssl dgst -sha384 -verify ec.pub -signature ec.sig test.txt` ## 签名校验的C语言实现 至此,我们知道: * 签名的基本原理与流程 * 如何使用openssl计算文件的Hash * 如何使用openssl生成密钥对 * openssl生成的密钥对的格式,如何提取私钥和公钥的十六进制表示 * 公钥的非压缩格式和压缩格式转换关系 * 用openssl对文件签名 * ECDSA签名文件的格式 * 用openssl进行验签 下面将寻找合适嵌入式环境的哈希算法和ECC算法库,并用openssl对两个算法库计算效果进行验证。相关代码链接请看[gitee工程](https://gitee.com/qomoliao/ecdsa)。主要验证了: 1. libhash库的Hash算法的准确性 2. Easy-ECC库的密钥对生成,签名,校验流程 3. Easy-ECC使用openssl生成的密钥对用于签名与校验 4. Easy-ECC使用openssl生成的公钥对openssl签名的文件进行校验 ### libhash算法库 [libhash](https://github.com/HectorBst/libhash)是C语言实现的小型hash算法,它支持sha1、sha224、sha256、sha384和sha512。我们将用到他的sha384算法。 用以下代码片段即可计算hello字符串的哈希,与`openssl dgst -sha384 test.txt`命令生成的哈希比对。 > test.txt文件里也保存了hello字符串,注意文件的最后不要有换行。(如果用`echo "hello" > test.txt`命令生成test.txt文件会在字符串后面自动加一个换行符) ```c printf("\nCalculate the hash of \"hello\" string\n"); sha384(test, 5, hash); PrintHexBuf("Hash", hash, ECC_BYTES) ``` ### Easy-ECC算法库 [easy-ecc](https://github.com/jestan/easy-ecc)是C语言实现的ECC算法,只有一个头文件和一个源码文件,很容易迁移到嵌入式环境中。它支持secp128r1、secp192r1、secp256r1、secp384r1四种椭圆曲线。我们将用到secp384r1曲线。 #### 先测试一下这个库能否工作 ```c printf("\nGenerate keys\n"); ecc_make_key(pubkey, prikey); printf("\nCalculate ECDSA sign\n"); ecdsa_sign(prikey, hash, signature); printf("\nVerify the sign\n"); if (ecdsa_verify(pubkey, hash, signature)) printf("Verify OK\n"); else printf("Verify FAIL\n"); ``` #### 再将openssl生成的密钥对转换成库需要的格式 ```c void ReadOpensslPriKey(uint8_t prikey[ECC_BYTES], char *prikey_str) { for (int i = 0; i < ECC_BYTES; i++) { sscanf(prikey_str, "%2x:", &prikey[i]); prikey_str += 3; } } void ReadOpensslPubKey(uint8_t pubkey[ECC_BYTES + 1], char *pubkey_str) { uint8_t last_data; pubkey_str += 3; for (size_t i = 0; i < ECC_BYTES; i++) { sscanf(pubkey_str, "%2x:", &pubkey[i + 1]); pubkey_str += 3; } for (size_t i = 0; i < ECC_BYTES; i++) { sscanf(pubkey_str, "%2x:", &last_data); pubkey_str += 3; } if (last_data % 2 == 1) pubkey[0] = 0x03; else pubkey[0] = 0x02; } void main(void) { ... printf("\nRead prikey and pubkey from openssl generation format\n"); ReadOpensslPriKey(prikey, openssl_prikey); ReadOpensslPubKey(pubkey, openssl_pubkey); PrintHexBuf("PrivKey", prikey, ECC_BYTES); PrintHexBuf("PubKey", pubkey, ECC_BYTES + 1); ... } ``` #### 用openssl生成的密钥对进行签名与校验 ```c printf("\nCalculate ECDSA sign\n"); ecdsa_sign(prikey, hash, signature); PrintHexBuf("Signature", signature, ECC_BYTES * 2); printf("\nVerify the sign\n"); if (ecdsa_verify(pubkey, hash, signature)) printf("Verify OK\n"); else printf("Verify FAIL\n"); ``` #### 用openssl生成的公钥对openssl的签名进行验证 ```c printf("\nVerify the sign from openssl\n"); for (int i = 0; i < ECC_BYTES; i++) { hash_reverse[i] = hash[ECC_BYTES - 1 - i]; ssl_sig_reverse[i] = ssl_sig[ECC_BYTES - 1 - i]; ssl_sig_reverse[i + ECC_BYTES] = ssl_sig[2 * ECC_BYTES - 1 - i]; } if (ecdsa_verify(pubkey, hash, ssl_sig)) printf("Verify OK\n"); else printf("Verify FAIL\n"); ``` 如此,验证了这两个算法库的可用性与使用方法。 ## 附录 - [《一文读懂ECDSA算法如何保护数据》](https://zhuanlan.zhihu.com/p/97953640) - [ASN.1格式在线解析](https://lapo.it/asn1js/) - [密钥文件格式完全解析](https://www.jianshu.com/p/ce7ab5f3f33a) - [libhash](https://github.com/HectorBst/libhash) - [easy-ecc](https://github.com/jestan/easy-ecc)