4、SMP协议解析:配对与密钥分发、安全密钥存储、加密算法实现

好,咱们今天来聊聊SMP协议。Security Manager Protocol,蓝牙安全的核心。说实话,我刚接触蓝牙协议栈那会儿,最头疼的就是这一块。配对流程、密钥分发、加密算法……一堆概念搅在一起。但后来我发现,只要把SMP的几条主线理清楚,它其实没那么复杂。

SMP说白了就干三件事:配对密钥分发加密通信。咱们一个一个拆开讲。

4.1 配对流程:从陌生到信任

配对不是一蹴而就的。它分三个阶段,我习惯叫它「三步走」。

第一阶段:配对特性交换

两个设备第一次见面,先互相打个招呼。它们会交换各自的能力信息:

  • IO Capabilities:有没有屏幕?有没有键盘?能不能显示六位数?
  • OOB Data Flag:有没有带外数据?比如NFC碰一碰。
  • AuthReq Bonding Flags:要不要绑定?要不要MITM保护?
  • Maximum Encryption Key Size:你最大支持多少位的加密密钥?
  • Initiator Key Distribution / Responder Key Distribution:咱俩交换哪些密钥?

嗯,这里有个坑。我在项目中遇到过,两个设备IO能力不匹配,导致配对失败。比如一个设备只有「Yes/No」按钮,另一个设备只有键盘输入。这时候配对算法会退化成Just Works,没有MITM保护。所以设计产品时,IO能力一定要提前规划好。

第二阶段:认证与加密

这个阶段才是真正的「考验」。根据第一阶段交换的信息,系统会选择一种配对方式:

配对方式 MITM保护 适用场景
Just Works 耳机、音箱等无输入设备
Numeric Comparison 手机配对、有屏幕的设备
Passkey Entry 键盘输入PIN码
OOB NFC碰一碰、二维码扫描

我个人最推荐Numeric Comparison。它既安全又方便。用户只需要确认屏幕上显示的两个数字是否一致。我曾经在一个IoT项目中,因为用了Just Works,被安全审计打回来重做。后来改成Numeric Comparison,问题就解决了。

这个阶段还会生成一个关键的临时密钥——Short Term Key (STK)。STK只用于当前会话,配对结束后就失效了。它的生成过程涉及ECDH密钥交换,说白了就是双方各自生成一对公私钥,然后交换公钥,计算出共享密钥。

核心要点:STK的生成依赖于ECDH。如果攻击者能拦截公钥交换,就能计算出STK。所以LE Secure Connections引入了FIPS认证的ECDH实现,并且强制要求公钥验证。

第三阶段:密钥分发

STK建立之后,双方就可以安全地交换长期密钥了。这个阶段分发的密钥包括:

  • LTK (Long Term Key):长期密钥,用于后续加密连接。
  • EDIV (Encrypted Diversifier):加密标识符,配合LTK使用。
  • RAND (Random Number):随机数,用于派生LTK。
  • IRK (Identity Resolving Key):身份解析密钥,用于隐私保护。
  • CSRK (Connection Signature Resolving Key):连接签名解析密钥,用于数据签名。

注意,这些密钥不是一次性全部发过去的。双方在第一阶段就协商好了「你要给我哪些密钥,我要给你哪些密钥」。这样可以减少不必要的通信开销。

避坑指南:我曾经遇到一个bug,设备A要求设备B分发LTK,但设备B的LTK长度是128位,设备A只支持64位。结果加密连接建立失败。所以密钥长度一定要在配对特性交换阶段就确认好。

4.2 安全密钥存储:别把钥匙放在门口

密钥分发完了,怎么存?这是个严肃的问题。你想想看,如果密钥明文存储在Flash里,攻击者拿到固件就能直接读取。那配对还有什么意义?

我建议的存储方案是这样的:

  1. 使用硬件安全模块(HSM)或安全元件(SE):密钥永远不出芯片。应用层只能通过API使用密钥,不能直接读取。
  2. 如果没有HSM,至少要做软件加密存储:用设备唯一密钥(比如芯片UID)对LTK进行AES加密,再存入Flash。
  3. 绑定设备标识:每个密钥对应该存储对应的蓝牙地址和IRK,方便后续快速重连。

我记得有一次帮客户调试,发现他们的设备每次断电后都要重新配对。一查代码,原来密钥存储用的是全局变量,断电就丢了。嗯,这种低级错误其实挺常见的。

存储结构可以参考这样:

struct bt_keys {
    bdaddr_t bdaddr;        // 对端设备地址
    uint8_t  addr_type;     // 地址类型(Public/Random)
    uint8_t  ltk[16];       // 长期密钥
    uint8_t  ediv[2];       // 加密标识符
    uint8_t  rand[8];       // 随机数
    uint8_t  irk[16];       // 身份解析密钥
    uint8_t  csrk[16];      // 连接签名密钥
    uint8_t  keys_present;  // 位掩码,标记哪些密钥有效
};

警告:千万不要把密钥存储在可读写的公共区域。比如某些MCU的Flash最后几页,如果被Bootloader或OTA升级覆盖,密钥就丢了。更糟的是,如果攻击者能通过调试接口读取Flash,密钥就泄露了。

4.3 加密算法实现:数学是安全的基石

SMP用到的加密算法不多,但都很关键。咱们一个一个看。

AES-128

这是蓝牙加密的基石。所有密钥的派生、加密通信都依赖AES-128。具体用的是AES-CCM模式,它同时提供加密和完整性校验。

实现时要注意:

  • AES-128的密钥长度固定为128位,不能多也不能少。
  • CCM模式的nonce构造要符合蓝牙规范,否则两端算出来的MIC对不上。
  • 硬件AES加速器能大幅提升性能。我在一个低功耗项目里,用软件AES加密一个包要2ms,换成硬件加速后降到0.1ms。

ECDH(椭圆曲线Diffie-Hellman)

用于生成共享密钥。蓝牙LE Secure Connections使用P-256曲线。实现时要注意:

  • 公钥必须验证是否在曲线上。否则可能被注入无效公钥,导致密钥空间缩小。
  • 随机数生成器必须足够随机。我曾经见过一个产品,因为随机数种子固定,导致每次生成的密钥都一样。这跟没加密没区别。
  • ECDH计算比较耗时,建议在空闲时预计算。

H6 / H7 函数

这两个是蓝牙特有的密钥派生函数。H6用于将LTK派生为其他密钥,H7用于将两个密钥合并为一个。说白了就是哈希加截断。

实现起来很简单,但容易出错的地方是输入参数的字节序。蓝牙协议里很多字段是小端序,但H6/H7要求大端序。我当年就因为这个bug,调试了整整两天。

// H6函数示例:将LTK派生为其他密钥
// 输入:key = LTK (16字节), salt = 固定值 (16字节)
// 输出:派生密钥 (16字节)
void bt_smp_h6(const uint8_t *key, const uint8_t *salt, uint8_t *out) {
    // 使用AES-CMAC计算
    aes_cmac(key, salt, 16, out);
}

总结一下:SMP协议的核心就是「先交换能力,再建立信任,最后分发密钥」。密钥存储要安全,加密实现要规范。我在实际项目中踩过的坑,十有八九都是因为忽略了这些细节。

好了,SMP协议就讲到这里。下一章咱们聊聊GATT协议——蓝牙属性协议,也就是应用层怎么读写数据。到时候你会发现,有了SMP打下的安全基础,GATT才能放心地传输数据。