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里,攻击者拿到固件就能直接读取。那配对还有什么意义?
我建议的存储方案是这样的:
- 使用硬件安全模块(HSM)或安全元件(SE):密钥永远不出芯片。应用层只能通过API使用密钥,不能直接读取。
- 如果没有HSM,至少要做软件加密存储:用设备唯一密钥(比如芯片UID)对LTK进行AES加密,再存入Flash。
- 绑定设备标识:每个密钥对应该存储对应的蓝牙地址和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才能放心地传输数据。