3. 固件签名与验证:从哈希到公钥校验的完整链路
各位好,我是老周。这一章咱们来啃固件签名与验证这块硬骨头。说白了,就是给固件加个「防伪标签」,让设备能认出哪些固件是官方发的,哪些是被人动过手脚的。
我在项目里见过太多因为签名验证没做好而出事的案例。有的直接把公钥硬编码在容易被读出来的地方,有的干脆跳过了验证步骤。嗯,这些坑咱们今天都得绕过去。
3.1 固件哈希计算:给固件打上「数字指纹」
签名之前,第一步是算哈希。哈希是什么?你可以把它理解成固件的「数字指纹」——不管固件多大,算出来的哈希值长度是固定的。而且只要固件里改了一个比特,哈希值就会变得面目全非。
核心要点:签名不是直接签固件本身,而是签固件的哈希值。这样做有两个好处:一是效率高(哈希很短),二是安全性好(哈希能保证完整性)。
我个人习惯用 SHA-256,虽然 SHA-1 也能用,但我不建议。为什么?SHA-1 已经被证明有碰撞攻击的可能了。你想想看,如果有人能构造出两个哈希值一样的固件,那签名就形同虚设了。
来看一段实际计算哈希的代码:
# 计算固件哈希(Python 示例)
import hashlib
def calculate_firmware_hash(firmware_path):
sha256 = hashlib.sha256()
with open(firmware_path, 'rb') as f:
# 分块读取,避免大固件撑爆内存
for chunk in iter(lambda: f.read(4096), b''):
sha256.update(chunk)
return sha256.hexdigest()
# 使用示例
hash_value = calculate_firmware_hash('firmware_v2.1.bin')
print(f'SHA-256: {hash_value}')
我的经验:分块读取这个细节很重要。我曾经遇到一个同事,直接把整个 64MB 的固件读到内存里算哈希,结果在低端设备上直接 OOM 了。分块读取,每块 4KB 或 16KB,既安全又高效。
3.2 使用私钥签名:给哈希值盖上「官方印章」
哈希算完了,接下来就是签名。签名用的是私钥,私钥必须保存在绝对安全的地方——比如 HSM(硬件安全模块)或者离线服务器上。私钥一旦泄露,整个安全体系就崩了。
签名流程其实很简单:
- 用私钥对固件的哈希值进行加密操作(实际是签名算法,不是加密)
- 生成一个签名值,通常和哈希值一样长(比如 ECDSA 签名是 64 字节)
- 把签名值和固件打包在一起,或者单独存放
我常用的签名算法是 ECDSA 搭配 P-256 曲线。RSA 也能用,但密钥太长(2048 位起步),在资源受限的 IoT 设备上验证起来比较慢。
来看实际签名操作:
# 使用 ECDSA 签名(Python 示例)
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import (
Prehashed,
decode_dss_signature,
encode_dss_signature
)
def sign_firmware(private_key, firmware_hash_bytes):
# 注意:这里签的是哈希值,不是原始固件
signature = private_key.sign(
firmware_hash_bytes,
ec.ECDSA(Prehashed(hashes.SHA256()))
)
return signature
# 假设 private_key 已经加载好
# signature = sign_firmware(private_key, hash_bytes)
避坑指南:我曾经见过有人直接把私钥放在代码仓库里,还上传到了 GitHub。结果不到 24 小时,就有安全研究员发邮件提醒他。私钥必须离线保存,最好用 HSM 或者至少用加密 U 盘。别图方便,安全没有捷径。
3.3 设备端公钥验证签名:守住最后一道防线
设备收到固件后,要做三件事:
- 计算收到的固件的哈希值
- 用公钥验证签名是否匹配这个哈希值
- 如果验证通过,才允许刷写固件
公钥是公开的,但必须保证它的完整性。公钥一般会烧录在设备的 OTP(一次性可编程)存储器里,或者存储在安全元件中。绝对不能让攻击者替换掉公钥,否则他就能用自己的私钥签固件了。
验证代码示例:
# 设备端验证签名(Python 示例)
def verify_firmware_signature(public_key, firmware_path, signature):
# 1. 计算固件哈希
sha256 = hashlib.sha256()
with open(firmware_path, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
sha256.update(chunk)
firmware_hash = sha256.digest()
# 2. 验证签名
try:
public_key.verify(
signature,
firmware_hash,
ec.ECDSA(Prehashed(hashes.SHA256()))
)
return True # 验证通过
except Exception as e:
print(f'签名验证失败: {e}')
return False # 验证失败
关键点:验证失败后,设备绝对不能执行固件更新。我见过有些实现为了「用户体验」,验证失败后只是打个日志,然后继续更新。这是致命的——攻击者只需要伪造一个签名就能完全控制设备。
3.4 完整签名验证流程
把上面三步串起来,就是完整的固件签名验证链路。我画了一张流程图,方便你理解整体架构:
3.5 常见问题与避坑指南
| 问题 | 风险 | 我的建议 |
|---|---|---|
| 公钥存储在可读写 Flash 中 | 攻击者可替换公钥,伪造任意固件 | 必须存储在 OTP 或安全元件中 |
| 签名验证失败后仍允许更新 | 恶意固件可被刷入设备 | 验证失败必须中止更新流程 |
| 使用弱哈希算法(如 MD5、SHA-1) | 存在碰撞攻击风险 | 至少使用 SHA-256 |
| 私钥存储在构建服务器上 | 服务器被攻破则私钥泄露 | 使用 HSM 或离线签名机 |
| 签名和固件分开传输 | 签名可能被替换或丢失 | 打包在一起,统一校验 |
血的教训:我曾经审计过一个智能门锁项目,他们的公钥存储在外部 SPI Flash 中,而且没有做写保护。攻击者只需要拆开设备,用编程器就能改写公钥,然后刷入自己签名的固件。门锁直接变成「开门锁」。后来我帮他们把公钥移到了 MCU 的 OTP 区域,才彻底解决这个问题。
3.6 小结
固件签名与验证,说白了就是三件事:算哈希、签哈希、验哈希。但每一步都有坑,稍不注意就会出大问题。
我个人觉得,签名验证是整个 OTA 安全体系中最关键的一环。你想想看,如果连固件的来源都保证不了,那加密传输、安全存储做得再好也没用。攻击者直接给你一个带后门的固件,设备就成别人的了。
嗯,这一章的内容就到这里。记住:私钥藏好,公钥锁死,验证失败就拒绝。这三条做到了,你的 OTA 安全就及格了。