4、数字签名与证书:ECDSA签名原理、X.509证书链、代码签名流程
说到安全启动和OTA升级,绕不开两个东西:数字签名和证书。
我刚开始做嵌入式安全时,觉得签名就是算个哈希、拿私钥加密一下。后来踩了坑才发现——签名算法选不对、证书链没验全,整个安全体系就是纸糊的。
这一章,咱们把ECDSA签名原理、X.509证书链、代码签名流程彻底讲透。
4.1 ECDSA签名原理
ECDSA,全称Elliptic Curve Digital Signature Algorithm,椭圆曲线数字签名算法。
为什么选它?说白了:同样的安全强度,密钥比RSA短得多。256位的ECDSA,安全性和3072位的RSA差不多。对嵌入式MCU那点可怜的Flash和RAM来说,这太重要了。
4.1.1 核心数学概念
ECDSA基于椭圆曲线上的离散对数问题。你不用深究数学细节,但得理解这几个关键点:
- 私钥:一个随机生成的256位整数(d)
- 公钥:私钥乘以椭圆曲线基点G得到的点(Q = d * G)
- 签名:对消息哈希后,用私钥生成一对大整数(r, s)
- 验签:用公钥和签名(r, s)验证消息是否由私钥持有者签署
关键认知:ECDSA签名不是加密。私钥签名、公钥验签——公钥是公开的,任何人都能验签,但只有私钥持有者才能签名。
4.1.2 签名与验签流程
我习惯把ECDSA签名流程画成三步:
- 计算消息摘要:对固件镜像做SHA-256哈希,得到固定长度的摘要
- 生成签名:用私钥对摘要签名,输出(r, s)
- 附加签名:将签名数据附加到固件包中,一起分发
验签流程反过来:
- 提取摘要:对收到的固件做SHA-256哈希
- 提取签名:从固件包中取出(r, s)
- 验签:用公钥验证签名是否匹配
我的经验:ECDSA签名中,随机数k的生成质量至关重要。如果k值重复或可预测,私钥就能被算出来。我在一个项目中遇到过某厂商的签名库用了弱随机数生成器,差点酿成大祸。一定要用硬件真随机数发生器(TRNG)来生成k值。
4.1.3 常用曲线对比
| 曲线名称 | 密钥长度 | 签名长度 | 安全等级 | 典型应用 |
|---|---|---|---|---|
| secp256r1 (P-256) | 256位 | 64字节 | 128位 | 车规、TLS、代码签名 |
| secp384r1 (P-384) | 384位 | 96字节 | 192位 | 高安全场景 |
| secp521r1 (P-521) | 521位 | 132字节 | 256位 | 军工、政府 |
| Curve25519 (Ed25519) | 256位 | 64字节 | 128位 | 新兴标准、性能优先 |
我个人建议:车规级CAN升级用secp256r1,OTA远程升级用Ed25519。前者是行业标准,后者实现更简单、性能更好。
4.2 X.509证书链
光有签名还不够——你怎么知道公钥是可信的?
这就是证书链要解决的问题。
4.2.1 证书结构
X.509证书本质上是一个绑定了公钥和身份信息的数据结构,由CA(证书颁发机构)签名。
一个典型的X.509 v3证书包含:
- 版本号:通常为v3
- 序列号:CA分配的唯一编号
- 签名算法:如ecdsa-with-SHA256
- 颁发者:签发该证书的CA名称
- 有效期:起始时间和结束时间
- 主体:证书持有者名称
- 公钥:持有者的公钥信息
- 扩展:密钥用途、基本约束等
- 签名:CA对上述内容的签名
避坑指南:我曾经遇到一个项目,证书有效期检查没做,结果设备在证书过期后还能正常启动。攻击者拿一个过期证书签名的固件照样能刷进去。记住:验签前必须先检查证书有效期。
4.2.2 证书链结构
嵌入式系统中,我们通常用三级证书链:
- 根证书(Root CA):自签名,预置在设备ROM中,不可更改
- 中间证书(Intermediate CA):由根证书签发,用于签发最终实体证书
- 实体证书(End Entity):由中间证书签发,包含代码签名公钥
为什么要搞这么复杂?
你想想看:如果根证书泄露,整个安全体系就完了。用中间证书隔离一层,即使中间证书被攻破,根证书还在,可以吊销并重新签发。
4.2.3 证书链验证流程
验证证书链时,Bootloader会做以下检查:
- 逐级验证签名:从实体证书开始,用上一级证书的公钥验证当前证书的签名
- 检查证书有效期:每个证书的当前时间必须在有效期内
- 检查证书吊销状态:通过CRL或OCSP检查证书是否被吊销
- 检查密钥用途:确认证书的密钥用途包含digitalSignature
- 信任锚验证:最终根证书必须是设备预置的可信根
我的习惯:在Bootloader中,我只预置根证书的公钥哈希,而不是完整证书。这样即使Flash被读取,攻击者也拿不到根证书的完整内容。当然,验签时需要用完整证书链。
4.3 代码签名流程
好了,理论讲完了。咱们看看实际代码签名怎么做。
4.3.1 签名工具链
我常用的签名工具组合:
- OpenSSL:生成密钥对、证书、签名
- Python + cryptography库:自动化签名流程
- HSM(硬件安全模块):生产环境中保护私钥
下面是一个典型的签名脚本片段:
# 生成ECDSA私钥(secp256r1)
openssl ecparam -genkey -name prime256v1 -out code_signing_private.pem
# 提取公钥
openssl ec -in code_signing_private.pem -pubout -out code_signing_public.pem
# 生成自签名根证书
openssl req -x509 -new -key code_signing_private.pem \
-days 3650 -out root_ca.crt \
-subj "/CN=MyCompany Root CA"
# 对固件签名
openssl dgst -sha256 -sign code_signing_private.pem \
-out firmware.bin.sig firmware.bin
# 将签名附加到固件
cat firmware.bin firmware.bin.sig > firmware_signed.bin
4.3.2 生产环境签名流程
在量产环境中,签名流程更严格:
- 构建固件:CI/CD流水线编译出原始固件
- 计算哈希:对固件做SHA-256摘要
- 请求签名:将摘要发送到签名服务器(私钥存储在HSM中)
- 返回签名:签名服务器返回(r, s)签名值
- 打包:将固件、签名、证书链打包成升级包
- 分发:通过CAN或OTA通道下发
安全原则:私钥永远不应该离开HSM。签名请求只发送哈希摘要,不发送完整固件。这样即使签名服务器被攻破,攻击者也拿不到私钥。
4.3.3 Bootloader验签流程
设备端Bootloader的验签流程:
// 伪代码:Bootloader验签流程
bool verify_firmware(uint8_t* firmware, uint32_t size) {
// 1. 从固件包中提取签名和证书链
Signature sig = extract_signature(firmware, size);
CertificateChain chain = extract_cert_chain(firmware, size);
// 2. 验证证书链
if (!verify_cert_chain(chain, root_ca_hash)) {
return false; // 证书链验证失败
}
// 3. 获取代码签名公钥
PublicKey pub_key = chain.get_end_entity_pubkey();
// 4. 计算固件哈希
uint8_t hash[32];
sha256(firmware, firmware_size, hash);
// 5. 验签
return ecdsa_verify(hash, sig, pub_key);
}
4.4 知识结构图
下面这张图总结了本章的核心知识体系:
4.5 常见问题与避坑
最后,分享几个我实际项目中踩过的坑:
- 证书链不完整:Bootloader只验了实体证书,没验中间证书。攻击者可以用自己的中间证书签发固件。
- 哈希算法不匹配:签名时用SHA-256,验签时用了SHA-1。验签失败,设备变砖。
- 签名格式错误:ECDSA签名有DER编码和原始(r,s)两种格式。Bootloader和签名工具必须统一。
- 证书过期未处理:设备出厂后证书过期,OTA升级全部失败。建议证书有效期设10年以上。
我曾经踩过的坑:有一次,生产线上签名脚本的随机数生成器被误配置为伪随机模式。结果签了1000份固件,有3份的k值重复了。攻击者只要拿到这3份固件和签名,就能算出私钥。从那以后,我强制要求所有签名操作必须使用HSM或硬件TRNG。
ECDSA签名和X.509证书链,是嵌入式安全启动的基石。理解原理、掌握流程、避开常见的坑,你的设备才能真正安全。