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(硬件安全模块)或者离线服务器上。私钥一旦泄露,整个安全体系就崩了。

签名流程其实很简单:

  1. 用私钥对固件的哈希值进行加密操作(实际是签名算法,不是加密)
  2. 生成一个签名值,通常和哈希值一样长(比如 ECDSA 签名是 64 字节)
  3. 把签名值和固件打包在一起,或者单独存放

我常用的签名算法是 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 设备端公钥验证签名:守住最后一道防线

设备收到固件后,要做三件事:

  1. 计算收到的固件的哈希值
  2. 用公钥验证签名是否匹配这个哈希值
  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 完整签名验证流程

把上面三步串起来,就是完整的固件签名验证链路。我画了一张流程图,方便你理解整体架构:

固件签名与验证完整流程 签名端(服务器) 原始固件 SHA-256 哈希计算 私钥签名 私钥(离线) 固件 + 签名打包 OTA 传输 验证端(设备) 接收固件 + 签名 SHA-256 哈希计算 公钥验证签名 公钥(OTP) 验证结果? 拒绝更新 失败 允许刷写 成功 核心原则:私钥永不离开签名服务器,公钥在设备出厂前烧录且不可篡改

3.5 常见问题与避坑指南

问题 风险 我的建议
公钥存储在可读写 Flash 中 攻击者可替换公钥,伪造任意固件 必须存储在 OTP 或安全元件中
签名验证失败后仍允许更新 恶意固件可被刷入设备 验证失败必须中止更新流程
使用弱哈希算法(如 MD5、SHA-1) 存在碰撞攻击风险 至少使用 SHA-256
私钥存储在构建服务器上 服务器被攻破则私钥泄露 使用 HSM 或离线签名机
签名和固件分开传输 签名可能被替换或丢失 打包在一起,统一校验

血的教训:我曾经审计过一个智能门锁项目,他们的公钥存储在外部 SPI Flash 中,而且没有做写保护。攻击者只需要拆开设备,用编程器就能改写公钥,然后刷入自己签名的固件。门锁直接变成「开门锁」。后来我帮他们把公钥移到了 MCU 的 OTP 区域,才彻底解决这个问题。

3.6 小结

固件签名与验证,说白了就是三件事:算哈希、签哈希、验哈希。但每一步都有坑,稍不注意就会出大问题。

我个人觉得,签名验证是整个 OTA 安全体系中最关键的一环。你想想看,如果连固件的来源都保证不了,那加密传输、安全存储做得再好也没用。攻击者直接给你一个带后门的固件,设备就成别人的了。

嗯,这一章的内容就到这里。记住:私钥藏好,公钥锁死,验证失败就拒绝。这三条做到了,你的 OTA 安全就及格了。