2. 密钥的诞生:从OTAA入网流程看AppKey与NwkKey的第一次握手

好,我们直接进入正题。上一章聊了AppKey和NwkKey「是什么」,这一章我们来看它们「怎么用」。

我个人习惯把OTAA入网流程比作一场面试——终端设备是求职者,网络服务器是面试官。求职者得先证明自己是谁(身份认证),再证明自己能干活(会话密钥协商)。而AppKey和NwkKey,就是这场面试里两把不同的钥匙。

2.1 OTAA入网的三次握手(其实是四次)

标准的LoRaWAN 1.0.x协议里,OTAA流程是经典的「Join Request → Join Accept」两步。但到了1.1版本,多了个ReKey机制,实际上变成了四步。不过别急,我们先从最基础的1.0.x版本讲起。

整个流程可以概括为:

  1. 终端发送Join Request:包含AppEUI、DevEUI、DevNonce(随机数)。
  2. 网络服务器回复Join Accept:包含AppNonce、NetID、DevAddr、DLSettings、RxDelay、CFList。
  3. 双方派生会话密钥:AppSKey和NwkSKey(1.0.x版本)。

嗯,这里要注意——1.0.x版本里只有一把网络密钥NwkSKey,而1.1版本拆成了FNwkSIntKey、SNwkSIntKey、NwkSEncKey三把。但不管怎么拆,根密钥始终是NwkKey和AppKey。

核心结论:Join Request用NwkKey保护完整性,Join Accept用AppKey加密负载。这两把钥匙在入网流程里各司其职,互不串岗。

2.2 Join Request:NwkKey的第一次亮相

终端设备上电后,第一件事就是组装Join Request消息。这条消息的结构很简单:

MHDR (1 byte) | AppEUI (8 bytes) | DevEUI (8 bytes) | DevNonce (2 bytes) | MIC (4 bytes)

关键在最后4个字节的MIC(消息完整性校验码)。这个MIC是怎么算的?

MIC = aes128_cmac(NwkKey, MHDR | AppEUI | DevEUI | DevNonce)[0..3]

看到了吗?用的是NwkKey,不是AppKey。

我在项目中遇到过好几次这样的坑:有人把AppKey当成万能钥匙,所有加密和完整性校验都用AppKey算。结果终端发出去的Join Request,网络服务器那边死活通不过MIC校验。查了半天才发现,1.1版本里Join Request的MIC必须用NwkKey算。

实战技巧:如果你在调试时发现终端能发出Join Request,但服务器没有任何响应,先检查MIC计算用的是不是NwkKey。我建议在代码里把NwkKey和AppKey的用途写清楚,别混用。

2.3 Join Accept:AppKey的加密舞台

网络服务器收到Join Request并验证MIC通过后,会生成一个Join Accept消息。这条消息的结构稍微复杂些:

MHDR (1 byte) | AppNonce (3 bytes) | NetID (3 bytes) | DevAddr (4 bytes) | DLSettings (1 byte) | RxDelay (1 byte) | CFList (0 or 16 bytes) | MIC (4 bytes)

但这里有个关键区别——Join Accept的负载是加密的。加密用的是AppKey:

JoinAccept_payload = aes128_encrypt(AppKey, MHDR | AppNonce | NetID | DevAddr | DLSettings | RxDelay | CFList | MIC)

而Join Accept的MIC计算,用的也是AppKey:

MIC = aes128_cmac(AppKey, JoinAccept_payload)[0..3]

你想想看,为什么这么设计?

我个人理解是:Join Accept里包含了会话密钥的种子(AppNonce),必须保证只有合法的终端才能解密。而AppKey是终端和服务器之间共享的私密密钥,用它加密最合适。NwkKey虽然也共享,但它的职责是保护网络层的完整性,不是加密应用层数据。

避坑指南:我曾经在一个项目里看到,有人把Join Accept的加密和MIC计算都用了NwkKey。结果终端能解密,但服务器端验证MIC时发现不匹配。原因很简单——1.1版本里Join Accept的加密和MIC必须用AppKey。这个错误在1.0.x版本里不会暴露,因为那时候只有一把NwkSKey,很多人习惯性地把NwkKey当成了唯一根密钥。

2.4 密钥派生:从根密钥到会话密钥

终端收到Join Accept并解密成功后,双方就可以派生会话密钥了。在1.1版本里,派生公式是这样的:

AppSKey = aes128_encrypt(AppKey, 0x02 | AppNonce | NetID | DevNonce | pad16)
FNwkSIntKey = aes128_encrypt(NwkKey, 0x01 | AppNonce | NetID | DevNonce | pad16)
SNwkSIntKey = aes128_encrypt(NwkKey, 0x03 | AppNonce | NetID | DevNonce | pad16)
NwkSEncKey = aes128_encrypt(NwkKey, 0x04 | AppNonce | NetID | DevNonce | pad16)

注意看,AppSKey只依赖AppKey,而三把网络会话密钥都依赖NwkKey。这就是我反复强调的「职责分离」——AppKey只管应用层,NwkKey只管网络层。

会话密钥 派生根密钥 用途
AppSKey AppKey 加密应用层负载(FRMPayload)
FNwkSIntKey NwkKey 前向网络完整性校验(FCnt上行)
SNwkSIntKey NwkKey 后向网络完整性校验(FCnt下行)
NwkSEncKey NwkKey 加密MAC命令(FOpts或FRMPayload中的MAC命令)

这个表格我建议你保存下来。每次写代码前看一眼,能避免80%的密钥混用问题。

2.5 完整流程图:密钥的诞生

下面这张图把整个流程串起来了。我画的时候特意把NwkKey和AppKey的路径用不同颜色标出来,方便你理解。

OTAA入网流程:AppKey与NwkKey的职责分工 终端设备 网络服务器 ① 组装Join Request MIC = CMAC(NwkKey, ...) Join Request (含MIC) ② 验证MIC CMAC(NwkKey, ...) 校验 ③ 加密Join Accept Encrypt(AppKey, ...) Join Accept (加密负载) ④ 解密Join Accept Decrypt(AppKey, ...) ⑤ 派生会话密钥 AppSKey / NwkSKeys ⑤ 派生会话密钥 AppSKey / NwkSKeys 关键说明: • Join Request的MIC → 使用 NwkKey(完整性保护) • Join Accept的加密 → 使用 AppKey(机密性保护) • 会话密钥派生 → AppSKey依赖AppKey,NwkSKeys依赖NwkKey

2.6 常见误区与避坑

说了这么多,我总结几个实际项目中容易踩的坑:

  • 误区一:Join Request的MIC用AppKey算。这是1.0.x版本的习惯遗留问题。1.1版本必须用NwkKey。
  • 误区二:Join Accept的加密用NwkKey。记住,Join Accept的加密和MIC都用AppKey。
  • 误区三:派生会话密钥时混用根密钥。AppSKey只能用AppKey派生,NwkSKeys只能用NwkKey派生。

调试小技巧:如果你在抓包工具里看到Join Request发出去了,但服务器没回Join Accept,大概率是MIC校验失败。先检查NwkKey是否正确。如果服务器回了Join Accept但终端不解密,那问题出在AppKey上。这个排查思路我用了好几年,基本没失手过。