最近有个嵌入式的项目需要做IAP升级,而且要对升级固件进行签名校验,实现安全升级。
之前没有做过固件的签名校验,网上也很少能找到IAP签名校验的教程,于是在摸索干活的过程也顺带一记,做一分享。希望对看到的人和未来的自己有所帮助。
所谓签名,就是给某段信息做认证,说明段信息是签名者认可的(防抵赖——合同签名),没有被人修改过的(防篡改——邮件帖封条并签名)。
数字世界的签名,也是为了防篡改和防抵赖。在我们的固件升级场景,主要是防止不被信任的攻击者制作恶意固件给我们的系统升级,造成某种不良后果。同时能够校验固件传输过程是否产生数据丢失或变化,防止升级了异常固件导致系统失效。
数字签名的大体过程如上图,虚线左边是签名过程:
之后,可以将固件+加密后的摘要一起发给接收方,接收方:
数字签名有两个关键:
非对称加密算法有两类:RSA和ECC。
具体原理不再展开。
哈希算法有很多,如MD2、MD5、SHA等,目前被认为安全的哈希算法是256位或以上的SHA算法。
为了密钥安全,我司使用的签名密钥必须通过公司平台申请。申请通过后平台返回一个签名工具和公钥,私钥是存在公司服务器里的,任何人都无法获取。
同时,为了保证加密信息的破解难度,平台限制了加密算法的种类和参数,仅提供有限的几种算法。
若采用RSA算法,必须使用3072位的RSA3072bits;若采用ECC算法,椭圆曲线只能选prime256v1或secp384r1。
考虑到ECC算法的密钥长度比较短,计算资源消耗小,考虑采用ECC算法。SHA+ECC用于数字签名也叫做ECDSA,本项目选用了secp384r1参数的ECDSA签名算法。关于ECDSA算法的原理,可以参考知乎文章《一文读懂ECDSA算法如何保护数据》。
先使用openssl工具对固件进行签名,获取签名过程中生成的摘要、最终签名信息。以与开发的算法计算结果做比对,验证算法准确性。
openssl生成的公钥等信息都经过了某种规范的编码,为了能够对比,必须了解这种格式的编码规则,从而提取出算法使用的具体参数。具体的编码规则和解析工具可看考附录。
显示支持的摘要算法列表:
openssl dgst -list
用sha384计算摘要:
openssl dgst -sha384 test.txt
显示支持的ECC曲线:
openssl ecparam -list_curves
选择secp384r1曲线,生成密钥:
openssl ecparam -name secp384r1 -genkey -out ec.key
生成的密钥采用ECC格式,用以下命令获取16进制的私钥和公钥:
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算法如何保护数据 ,ECC加密的依据是公式Qa = dA × G。其中,G是椭圆曲线的起始点,dA是一个随机数(也即私钥),Qa是G与dA进行乘法运算后得到的点(也即公钥)。
对于secp384r1曲线
后面可以看到,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在线解析工具可以看到,其实就是两个384bits长度的整数,也即ECDSA签名原理中的值对(R, S)。
注意:ASN.1 规定整型 INTEGER 需要支持正整数、负整数和零。BER/DER 使用大端模式存储 INTEGER,并通过最高位来编码正负数(最高位0为正数,1为负数)。 如果密钥参数值最高位为 1,则 BER/DER 编码会在参数前额外加 0x00 以表示正数,这也就是为什么有时候密钥参数前面会多出1字节 0x00 的原因。
为了方便的将签名的数据拷贝到我们的工程里,也可以使用如下指令输出十六进制格式的签名:
$ 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
至此,我们知道:
下面将寻找合适嵌入式环境的哈希算法和ECC算法库,并用openssl对两个算法库计算效果进行验证。相关代码链接请看gitee工程。主要验证了:
libhash是C语言实现的小型hash算法,它支持sha1、sha224、sha256、sha384和sha512。我们将用到他的sha384算法。
用以下代码片段即可计算hello字符串的哈希,与openssl dgst -sha384 test.txt
命令生成的哈希比对。
test.txt文件里也保存了hello字符串,注意文件的最后不要有换行。(如果用
echo "hello" > test.txt
命令生成test.txt文件会在字符串后面自动加一个换行符)
printf("\nCalculate the hash of \"hello\" string\n");
sha384(test, 5, hash);
PrintHexBuf("Hash", hash, ECC_BYTES)
easy-ecc是C语言实现的ECC算法,只有一个头文件和一个源码文件,很容易迁移到嵌入式环境中。它支持secp128r1、secp192r1、secp256r1、secp384r1四种椭圆曲线。我们将用到secp384r1曲线。
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");
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);
...
}
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");
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");
如此,验证了这两个算法库的可用性与使用方法。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。