密码学基础(下):数字签名(ECDSA)、密钥交换(ECDH)、证书链(X.509)

好,咱们接着聊密码学。上一节我们把哈希、对称加密、非对称加密这些基础概念过了一遍。这一节,我打算把几个在固件热更新里真正要用的东西讲透——数字签名、密钥交换、还有证书链。

说实话,这些概念光看理论很容易晕。但你在实际项目中摸爬滚打一遍,就会发现它们其实是一套完整的信任体系。我当年第一次做OTA升级方案时,就被证书链的验证逻辑坑过一次,后面我会讲到。

ECDSA:不只是“签个名”那么简单

数字签名,说白了就是给固件打个“防伪标签”。你下载了一个固件包,怎么确认它真的是官方发布的,而不是中间人篡改过的?靠的就是签名验证。

ECDSA(椭圆曲线数字签名算法)是目前嵌入式领域最常用的签名算法。为什么?因为它密钥短、性能好。同样安全强度下,RSA需要2048位密钥,ECDSA用256位就够了。这对资源受限的MCU来说,差别太大了。

核心流程:

  • 签名方(服务器): 用私钥对固件哈希值进行签名,生成签名值(r, s)。
  • 验证方(设备): 用公钥对签名值进行验证,比对哈希是否一致。

我习惯把签名过程理解为“加密哈希”。私钥签名,公钥验签。私钥只有你自己有,所以别人伪造不了。

这里有个坑,我当年踩过——签名时一定要使用安全的随机数。ECDSA的签名过程中需要一个随机数k,如果这个k值重复了,私钥就会被直接算出来。这不是理论问题,索尼PS3当年就因为这个被破解了。嗯,血的教训。

// 伪代码示例:ECDSA签名与验证
// 签名
uint8_t hash[32];
sha256(firmware_data, firmware_len, hash);
ecdsa_sign(private_key, hash, &signature_r, &signature_s);

// 验证
uint8_t hash[32];
sha256(firmware_data, firmware_len, hash);
bool valid = ecdsa_verify(public_key, hash, signature_r, signature_s);
if (!valid) {
    // 固件被篡改,拒绝更新
    abort_update();
}

实战建议: 在固件中预置公钥时,一定要做完整性校验。我曾经见过有人把公钥放在Flash的未保护区域,结果攻击者直接替换了公钥,用自己的私钥签名了恶意固件。公钥本身也要被保护,比如放在OTP区域或使用安全存储。

ECDH:如何安全地“交换”密钥

你想想看,固件热更新时,如果固件内容需要加密传输,那加密用的对称密钥怎么安全地传给设备?直接明文传?不行。用非对称加密传?可以,但性能开销大。

ECDH(椭圆曲线 Diffie-Hellman)就是干这个的。它让通信双方在不安全的信道上,协商出一个共享密钥。整个过程不传输密钥本身,只传输各自的公钥。

原理其实不复杂:

  1. 设备生成自己的ECDH密钥对(私钥d1,公钥Q1),把Q1发给服务器。
  2. 服务器生成自己的ECDH密钥对(私钥d2,公钥Q2),把Q2发给设备。
  3. 设备计算:共享密钥 = d1 * Q2
  4. 服务器计算:共享密钥 = d2 * Q1
  5. 结果是一样的,因为 d1 * Q2 = d1 * d2 * G = d2 * d1 * G = d2 * Q1

说白了,就是双方各自用自己的私钥去乘对方的公钥,得到同一个点坐标,然后取这个点的x坐标作为共享密钥。

注意: ECDH本身不提供身份认证。也就是说,你无法确认你正在和谁交换密钥。这就是为什么实际应用中,ECDH通常和数字签名配合使用——先用ECDSA签名确认身份,再用ECDH协商密钥。或者直接用ECDHE,其中“E”代表Ephemeral(临时),每次会话生成新的临时密钥对。

我在项目中遇到过一个问题:设备端计算ECDH时,没有做点验证。攻击者可以注入一个无效的曲线点,导致设备计算出错误的共享密钥,进而泄露信息。所以,收到对方的公钥后,一定要验证它是否在合法的曲线上。

X.509证书链:信任的传递

好,现在我们把签名和密钥交换串起来。你有一个公钥,怎么确认这个公钥真的属于你声称的那一方?靠的就是证书。

X.509证书,说白了就是一个数字身份证。它把公钥和身份信息绑定在一起,并由一个可信的第三方(CA,证书颁发机构)签名。

证书链的结构是这样的:

  • 根证书: 自签名,是信任的锚点。预置在设备中,不可更改。
  • 中间证书: 由根证书签发,用于签发终端证书。
  • 终端证书: 由中间证书签发,包含服务器的公钥。

验证过程就是一条信任链:终端证书 → 中间证书 → 根证书。每一级都用上一级的公钥验证下一级的签名,直到根证书。根证书是自签名的,我们信任它,因为它是预置在设备里的。

固件热更新中的典型用法:

  • 设备预置根证书(或根证书哈希)。
  • 服务器下发固件时,附带完整的证书链(终端证书 + 中间证书)。
  • 设备用预置的根证书验证中间证书,再用中间证书验证终端证书。
  • 最后用终端证书中的公钥验证固件签名。

我曾经犯过一个低级错误:在设备端只验证了终端证书,跳过了中间证书的验证。结果攻击者用另一个CA签发的证书也能通过验证。嗯,从那以后我再也不敢偷懒了。

// 证书链验证伪代码
bool verify_cert_chain(cert_t* leaf, cert_t* intermediate, cert_t* root) {
    // 1. 验证中间证书由根证书签发
    if (!verify_signature(intermediate, root->public_key)) {
        return false;
    }
    // 2. 验证终端证书由中间证书签发
    if (!verify_signature(leaf, intermediate->public_key)) {
        return false;
    }
    // 3. 检查证书有效期
    if (!check_validity_period(leaf)) {
        return false;
    }
    // 4. 检查证书是否被吊销(可选,但建议做)
    if (is_revoked(leaf)) {
        return false;
    }
    return true;
}

避坑指南: 证书吊销检查在嵌入式设备上往往被忽略,因为需要联网查询CRL或OCSP。但如果你做的是高安全级别的设备(比如车规级),这个步骤不能省。我建议至少预置一个CRL缓存,定期更新。

知识体系总览

下面这张图,我把这一节的核心逻辑画出来了。你看一眼,应该就能明白ECDSA、ECDH、X.509是怎么配合的。

固件热更新密码学体系 ECDSA 数字签名 私钥签名 · 公钥验签 确保固件完整性与来源 ECDH 密钥交换 协商共享密钥 加密固件传输通道 X.509 证书链 根→中间→终端 建立信任传递机制 固件发布流程:固件 → 哈希 → ECDSA签名 → 打包(含证书链)→ 下发 设备验证流程:证书链验证 → 提取公钥 → ECDSA验签 → 哈希比对 → 通过 安全通道:ECDH协商密钥 → AES加密传输固件 → 设备解密后验证签名

你看,这三个技术是环环相扣的。ECDSA解决“你是谁”的问题,ECDH解决“怎么安全地聊”的问题,X.509解决“凭什么相信你”的问题。少了任何一个,整个安全体系都有漏洞。

最后说一句,密码学这东西,光看书是不够的。我建议你找个开发板,实际跑一遍ECDSA签名验证、ECDH密钥协商、证书链解析的流程。踩几个坑,比看十遍书都管用。


公众号:蓝海资料掘金营,微信deep3321