4、Join过程安全机制:OTAA与ABP安全对比、Join-Request/Join-Accept消息结构、MIC计算与验证、Join-Nonce与DevNonce防重放机制
聊到LoRaWAN的入网,很多新手第一反应就是“发个请求,等个回复”。嗯,表面看确实是这样。但如果你真在项目里这么干,大概率会被重放攻击教做人。
我最早接触LoRaWAN时,就踩过这个坑。当时为了赶进度,直接用了ABP模式,结果设备上线后老是收到乱数据。后来一查,原来是有人抓包重放了旧帧。从那以后,我对Join过程的安全机制再也不敢马虎了。
这一章,咱们就把OTAA和ABP的底裤扒干净。看看Join-Request和Join-Accept到底长什么样,MIC是怎么算的,Nonce又是怎么防重放的。
4.1 OTAA vs ABP:安全性的分水岭
先说结论:能用OTAA就别用ABP。这不是我个人的偏执,而是协议设计上的根本差异。
| 对比项 | OTAA(空中激活) | ABP(个人化激活) |
|---|---|---|
| 密钥生成方式 | 动态派生,每次入网不同 | 预置在设备中,固定不变 |
| 会话密钥 | 每次Join重新生成 | 出厂即固定,永不更新 |
| 防重放能力 | 强(DevNonce + JoinNonce双重校验) | 弱(仅靠FCnt,容易被绕过) |
| 设备更换网络 | 支持(重新Join即可) | 不支持(需重新烧录密钥) |
| 适用场景 | 绝大多数生产环境 | 实验室测试、封闭内网 |
4.2 Join-Request消息结构:设备的第一声招呼
设备要入网,第一步就是发Join-Request。这个包不长,但每个字段都有讲究。
Join-Request 消息格式(MAC层,不含PHY头):
+----------------+----------------+----------------+----------------+
| AppEUI (8B) | DevEUI (8B) | DevNonce (2B) | MIC (4B) |
+----------------+----------------+----------------+----------------+
总长度:22字节
- AppEUI:应用标识,告诉网络服务器“我想加入哪个应用”。
- DevEUI:设备唯一标识,全球唯一,类似设备的身份证。
- DevNonce:设备生成的随机数,2字节,用于防重放。
- MIC:消息完整性校验码,4字节,用AppKey计算。
这里有个细节:Join-Request是明文传输的。你可能会问:“明文?那安全吗?” 别急,MIC就是干这个的。虽然内容是明文的,但MIC保证了消息的完整性和真实性。攻击者可以看见你的AppEUI和DevEUI,但他伪造不了MIC,因为他没有AppKey。
4.3 Join-Accept消息结构:服务器的回应
服务器收到Join-Request后,如果验证通过,就回复Join-Accept。这个包是加密的,用AppKey加密。
Join-Accept 消息格式(加密前):
+----------------+----------------+----------------+----------------+----------------+
| JoinNonce (3B) | NetID (3B) | DevAddr (4B) | DLSettings (1B)| RxDelay (1B) |
+----------------+----------------+----------------+----------------+----------------+
| CFList (0或16B) | MIC (4B) |
+---------------------+----------------+
总长度:17或33字节(含MIC)
- JoinNonce:服务器生成的随机数,3字节,用于派生会话密钥。
- NetID:网络标识,3字节。
- DevAddr:设备在网络中的短地址,后续通信就用这个。
- DLSettings:下行链路参数,比如数据速率、RX窗口配置。
- RxDelay:接收窗口延迟。
- CFList:可选,信道频率列表,用于配置设备的工作频点。
4.4 MIC计算与验证:谁在说谎?
MIC(Message Integrity Code)是LoRaWAN安全的第一道防线。它的计算方式不复杂,但容易出错。
对于Join-Request,MIC的计算公式如下:
MIC = aes128_cmac(AppKey, msg | 0x00)[0..3]
其中:
- aes128_cmac:基于AES-128的CMAC算法
- AppKey:应用密钥,16字节
- msg:Join-Request消息体(AppEUI + DevEUI + DevNonce)
- 0x00:一个字节的填充,表示这是Join-Request
- [0..3]:取CMAC输出的前4个字节
对于Join-Accept,MIC的计算稍有不同:
MIC = aes128_cmac(AppKey, msg | 0x01)[0..3]
注意:填充字节变成了0x01,表示这是Join-Accept
我在项目中遇到过一个问题:设备发Join-Request后,服务器一直不回复。抓包一看,MIC算错了。原因是设备端的AppKey和服务器端的不一致。这种问题最坑,因为两边都觉得自己是对的,但就是握不上手。
4.5 Join-Nonce与DevNonce:防重放的黄金搭档
重放攻击是IoT安全的老大难问题。攻击者不需要破解你的密钥,只需要把之前抓到的合法包再发一遍,就能让设备误以为自己是合法入网。
LoRaWAN用两个Nonce来防重放:
- DevNonce:设备生成的随机数,2字节,每次Join-Request必须不同。
- JoinNonce:服务器生成的随机数,3字节,每次Join-Accept必须不同。
服务器会维护一个DevNonce列表,记录每个设备最近使用过的DevNonce。如果收到重复的DevNonce,直接丢弃。同样,设备也会记录JoinNonce,防止服务器被伪造。
你想想看,如果只有DevNonce,攻击者可以伪造Join-Accept。如果只有JoinNonce,攻击者可以重放Join-Request。两个一起用,才能做到双向防重放。
4.6 完整Join流程:一张图说清楚
说了这么多,咱们用一张SVG图把整个流程串起来。从设备上电到会话密钥生成,每一步都清清楚楚。
这张图把整个流程拆成了8步。你注意看,从设备生成DevNonce到服务器验证MIC,再到服务器生成JoinNonce并加密回复,最后设备解密并验证——每一步都在做安全校验。少了任何一步,整个系统就不安全了。
4.7 实战建议:如何选择Join模式?
说了这么多理论,最后给点实在的建议:
- 生产环境必须用OTAA。别为了省那几次空中的交互流量,把整个系统的安全搭进去。
- DevNonce的随机性很重要。有些低成本的MCU,随机数生成器质量很差,生成的DevNonce容易重复。我建议用硬件随机数发生器,或者至少混入ADC噪声。
- Join-Accept的超时处理。设备发完Join-Request后,如果没收到回复,应该重试。但重试间隔要随机化,避免所有设备同时重试造成网络拥塞。
- 记录Join历史。服务器端要记录每个设备的Join时间、DevNonce、JoinNonce。这不仅是安全需要,也是后期排查问题的依据。