1.“配网”中的’网‘具体是什么
配网是将未配网设备加入网络,使其成为网络中的节点的过程。
1.1.未配网设备在程序中如何表示?
换句话说,就是未配网设备的数据结构是什么样的?可能我们更感兴趣的是为什么要选择这样的数据结构?
在信标阶段,配网器上扫描到未配网设备,会记录设备的蓝牙对象信息,主要包括蓝牙名,蓝牙RSSI,和蓝牙广播数据。蓝牙名和信号强度RSSI主要是做App内显示用,蓝牙Mesh协议主要是根据蓝牙广播信息
创建未配网设备(UnprovisionedDevice)
类的对象。
在iOS App中,对未配网设备的进行的封装如下
//swift语言
typealias DiscoveredPeripheral = (
device: UnprovisionedDevice,
peripheral: CBPeripheral,
rssi: Int
)
在App中,必须根据广播数据创建 Unprovisioned Device
对象。 Mesh UUID 和 OOB 信息必须存在于广播数据中,否则对象创建失败,配网无法往下进行。
未配网设备Beacon格式
这里必须得介绍一下未配网设备Beacon格式,mesh规范中定义如下:
字段 | 长度(字节) | 注释 |
---|---|---|
Beacon Type | 1 | 未配网设备 beacon type (0x00) |
Device UUID | 16 | 用来识别设备的唯一码Device UUID |
OOB Information | 2 | 下表中定义 |
URI Hash | 4 | 使用 URI AD 类型广播的关联 URI 的哈希(可选字段) |
未配网设备Beacon中OOB字段的含义:
目前我司
OOB Information
的值均是0,未做其他研究。后面部分会写《OOB在配网中到底影响的是什么》,知道了原理,任凭形式如何变化,都很容易解决,这里不再做更多解释。
未配网设备(UnprovisionedDevice)
类在iOS App中的属性定义:
public var name: String?
public let uuid: UUID
public let oobInformation: OobInformation
未配网设备(UnprovisionedDevice)
类中使用广播数据的方式:
- 取广播数据中服务为1827的ServiceData部分,得到的就是未配网设备Beacon。
- 在iOS端收到的数据长度是18字节,系统层已经将数据类型过滤。
- 前16个字节就是未配网设备的Device UUID。
- 后两个字节就是OOB Information。
注意:在手机系统中,标识一个蓝牙设备的唯一性,安卓系统使用的MAC地址,在iOS上使用的是UUID。未配网设备的Device UUID来源于广播数据,是在mesh网络中使用,并不是手机系统给分配的标识蓝牙的UUID。
1.2.节点在程序中如何表示?
上面未配网设备的数据结构是配网前的数据。这小节是在配网后,节点应该包含哪些信息。
根据角色划分,网络中有两种节点:配网器节点和设备节点。配网器节点是配网器在启动时自动创建。
我们可以通过App端的导出mesh网络数据进行分析。在这里不对节点中每个字段含义做过多解释。
设备节点在持久化储存时的数据结构:
appkeys`:节点的应用秘钥信息。
cid
:company id,节点所用的蓝牙芯片公司,在Sig注册的标识符。比如:奉加微的cid是0x0504,对应的公司名是PHYPLUS Inc。
configComplete
:节点的配置是否完成。
crpl
:设备中重放保护列表项的最小数量。
defaultTTL
:消息在网络中的转发次数。
deviceKey
:设备密钥。
elements
:节点的元素。
excluded
:密钥刷新时,是否已将该设备排除。
name
:节点名称,来自于扫描时的未配网设备,也可以修改。
netKeys
:网络密钥。
pid
:product id,产品ID。
security
:节点的安全等级。
unicastAddress
:节点单播地址。
UUID
:节点唯一标识。
vid
:version id,节点的版本号。
1.3.mesh网络在程序中如何表示?
在配网器端第一次打开APP时,会创建一个网络,并持久化储存这个网络到本地数据。这个本地网络数据在之后的APP启动时,都会重新加载到内存。每一个设备的加入或者其它密钥的添加与修改,都会在一个操作流程处理完后,立刻保存到本地网络数据。
appKeys
:应用程序密钥。应用与上层传输层的安全通信。
groups
:分组信息。将多个节点统一管理,可根据场景自行设置。比如将客厅的几个灯放在一组,实现同时开关灯。
meshName
:网络名称。
meshUUID
:每个网络的唯一标识符,程序自动生成的加密随机数。在网络导入时,可能不是本网络的信息,所以需要通过这个唯一标识符进行过滤。
netKeys
:网络密钥。应用于网络层的安全通信。
nodes
:节点。
Provisioners
:配网器。
scenes
:场景。
2.配网流程通信数据分析
2.1信标阶段
信标阶段再细分为两个部分。
- 在App中扫描到设备后,只做显示,并不会直接去连接设备,需要用户选择想配网的设备后,进行连接。
- 但在我司gateway方式中,扫描到设备便会进行连接,实现自动配网。
2.1.1广播阶段
未配网设备端向周围发送广播,配网器在接收到扫描信息后将未配网设备的name、meshUUID、oobInformation以及蓝牙对象保存内存。可以参考1.1。
2.1.2连接阶段
这个阶段就是BLE常规操作:连接,发现服务、发现特性,打开特性通知。
在配网器上,需要对服务和特性做确认,判断连接的设备是否具备mesh中需要用到数据写入写出特性。
配网协议使用的服务是1827。
数据写入特性:2ADB。
数据写出特性:2ADC。
用变量记录写这些特性,方便在数据写入写出时做比较。
这个部分就是配网承载器中的GATT承载器,手机端仅支持GATT承载器这一种。
2.2邀请阶段
信标阶段的设备连接,和邀请阶段,是一个步骤完成。当连接设备完成,打开特性通知成功时,被视为承载器准备就绪,可以发送邀请数据。上文《配网协议》->《配网PDU》->《配网邀请》对这个指令有详细解释。这个阶段的数据是明文。
我司
Attention Duration
的值默认是5秒。配网器发送
配网邀请
指令:0x030005设备端返回
配网能力
给APP端指令:0x03010100010000000000000000
我司目前设备端返回的设备能力,代表的意思如下:
Device Capabilities:
Number of elements: 1
Algorithms: FIPS P-256 Elliptic Curve
Public Key Type: None
Static OOB Type: None
Output OOB Size: 0
Output OOB Actions: None
Input OOB Size: 0
Input OOB Actions: None received
2.3交换公钥阶段
在这个阶段开始时,需要判断配网器是否能正常分配单播地址。如果无法再生成地址,则无法进行配网。这个阶段需要生成密钥对和随机数。
我司有两种配网模式,快速配网和慢速配网。
- 快速模式下,配网器在收到设备端的配网能力后,会自动发送
配网开始
命令。 - 在慢速配网模式下,需要手动点击App界面中配网按钮进行指令发送。
- 注意:我司会已淘汰慢速配网模式,后续客户必须是一键自动配网。
配网器发送
配网开始
到设备端:0x03020000000000配网器发送
配置公钥
到设备端:0x0303F494396E480FC7F73BC5ACE37CD06406CE1DAE3F4980F818C383968D1766CBD3514CA87C914ACF4ED5B809D3499E6A2D97A5359E1B47DEBF028571CCE1693FEF设备端发送
配置公钥
到配网端:0x0303F48D07966C9AD7005A81F8DB91C06447FF6CC40A94B58006372AF4805B8C79F710D114C80AAF131A2EA7DDD5281DB2E8F30300F600A41C181C3F52998BE32C2E
在配网端发送配网开始
指令与配置公钥
指令间,需要有一定的时间间隔。
密钥对
包含X和Y两部分,分别占32字节。当配网开始(Provision Start)
消息中的公钥为OOB公钥时,配置公钥
消息不存在,双方通过诸如二维码、NFC等OOB方式交互公钥。
2.4身份认证阶段
蓝牙mesh规范使用的ECDH算法可以较为显著的对抗被动监听及暴力计算攻击,但无法对抗中间人攻击,所以需要在ECDH计算密钥结束后对配网器和未配网设备进行身份认证,认证的方式是通过两者共享的密钥对某个随机值进行加密计算并生成确认值,然后将这两个值都交给对方设备进行身份认证。
在蓝牙mesh规范中,身份认证过程包含了对设备端和配网器端的认证,两者均会和对方交互一个Confirmation Value,以及生成此Confirmation Value的Random Value,Confirmation value的计算使用了ECDH秘钥、配网交互数据包及OOB认证信息。当一方接收到完整的Confirmation Value和Random Value后会根据自己的ECDH密钥、配网交互数据包及OOB认证信息,对收到的Random Value重新计算,生成一个Confirmation Value,然后与收到的Confirmation Value对比,如果相同则认证成功,如果失败则退出配网。当两者均完成认证后,整个认证流程结束。
配网器发送
配网确认
指令到设备端:0x03055E8E0814BAD9C4E5387D183D3A76A36B设备端发送
配网确认
指令到配网器:0x03050F172A8372F24686D510DFE5223AC3DD配网器发送
配网随机数
指令到设备端:0x0306FE1B83782226C5B3E6BEFA3476FF28AC设备端发送
配网随机数
指令到配网器:0x030696EF2DDFB9464EEA24F57A6D95BC2128
2.5分发配网数据
配网器发送
配网数据
指令到设备端:0x0307F94C0BCC18558B67E750187CE3EF5CD7E3A74DC8631F9018A053CCE72A599690F4设备端发送
配网完成
指令到配网器:0x0308
3.4身份验证的代码逻辑
在mesh规范中有如下定义
- ConfirmationInputs = ProvisioningInvitePDUValue || ProvisioningCapabilitiesPDUValue || ProvisioningStartPDUValue || PublicKeyProvisioner || PublicKeyDevice
- ConfirmationSalt = s1(ConfirmationInputs)
- ConfirmationKey = k1(ECDHSecret, ConfirmationSalt, “prck”)
下面是具体的计算方式
3.4.1 ConfirmationInputs的计算方式
ConfirmationInputs是由整个配网过程中的交互数据组成,包括Provisioning Invite
PDU, Provisioning Capabilities
PDU,Provisioning Start
PDU, 配网器的 Public Key 和设备的 Public Key.一共1 + 11 + 5 + 64 + 64 = 145字节。
配网器发送
配网邀请
指令:0x030005设备端返回
配网能力
给APP端指令:0x03010100010000000000000000配网器发送
配网开始
到设备端:0x03020000000000配网器发送
配置公钥
到设备端:0x0303F494396E480FC7F73BC5ACE37CD06406CE1DAE3F4980F818C383968D1766CBD3514CA87C914ACF4ED5B809D3499E6A2D97A5359E1B47DEBF028571CCE1693FEF设备端发送
配置公钥
到配网端:0x0303F48D07966C9AD7005A81F8DB91C06447FF6CC40A94B58006372AF4805B8C79F710D114C80AAF131A2EA7DDD5281DB2E8F30300F600A41C181C3F52998BE32C2E
最终的结果是如下字符串
ConfirmationInputs = "05" + "0100010000000000000000" + "0000000000" + "F494396E480FC7F73BC5ACE37CD06406CE1DAE3F4980F818C383968D1766CBD3514CA87C914ACF4ED5B809D3499E6A2D97A5359E1B47DEBF028571CCE1693FEF" + "F48D07966C9AD7005A81F8DB91C06447FF6CC40A94B58006372AF4805B8C79F710D114C80AAF131A2EA7DDD5281DB2E8F30300F600A41C181C3F52998BE32C2E"
3.4.2 ConfirmationSalt的计算方式
所谓加Salt,就是加点“佐料”。当用户首次提供密码时(通常是注册时),由系统自动往这个密码里加一些“Salt值”,这个值是由系统随机生成的,并且只有系统知道。然后再散列。而当用户登录时,系统为用户提供的代码撒上同样的“Salt值”,然后散列,再比较散列值,以确定密码是否正确。
这样,即便两个用户使用了同一个密码,由于系统为它们生成的salt值不同,他们的散列值也是不同的。即便黑客可以通过自己的密码和自己生成的散列值来找具有特定密码的用户,但这个几率太小了(密码和salt值都得和黑客使用的一样才行)。
ConfirmationSalt = s1(ConfirmationInputs)
具体S1计算Salt的方式如下:
- (NSData*) calculateSalt: (NSData*) someData {
//For S1, the key is constant
unsigned char key[16] = {0x00};
NSData* keyData = [[NSData alloc] initWithBytes: key length: 16];
return [self calculateCMAC: someData andKey: keyData];
}
具体计算流程:ConfirmationInputs作为函数唯一输入参数,然后函数内生成16个字节的数组keyData,keyData约定全部赋值为0。计算一次AES-CMAC算法,ConfirmationInputs作为数据段,keyData作为秘钥。
3.4.3 ConfirmationKey的计算方法
利用生成的共享密钥(ECDHSecret)
和 佐料(ConfirmationSalt)
,生成通信用的离散密钥key。
ConfirmationKey = k1(ECDHSecret, ConfirmationSalt, “prck”)
3.4.4 计算需要发送的Confirmation值
Calculate the Confirmation Provisioner using CMAC(random + authValue)。计算以AES-128作为块密码功能的基于密文的消息认证码(CMAC),也称为AES-CMAC。Calculates Cipher-based Message Authentication Code (CMAC) that uses AES-128 as the block cipher function, also known as AES-CMAC.
(1)随机数 0xC1A844F3D34886D79BBD3F9711C018ED
(2)no OOB的authValue : 16字节的0
(3)confirmationData = random + authValue
(4)calculateCMAC (confirmationData, confirmationKey)
得到Sending Provisioner Confirmation (0x5E8E0814BAD9C4E5387D183D3A76A36B)
两端都会这样操作。发送完Confirmation值后,两端相互给对方发送自己的随机数。
3.4.5 通过Random和authValue验证
配网器收到来自设备端的随机数后,根据deviceRandom和authValue计算出confirmation:
salt和Confirmation Key这两步跟上面配网器生成confirmation一样。不同点在于随机数是设备端发来的随机数。confirmationData = deviceRandom + authValue。
与收到的设备端confirmation比较,如果相等就说明验证成功。发送Encrypted Provisioning Data。
3.4.6 特别说明:OOB方式影响的对象是AuthValue
AuthValue默认是一个16字节的字符串。我们项目中默认是No OOB模式,那么的它的值就是16个字节⻓度的0。其他三种:静态OOB,输出式OOB,输入式OOB;也是通过影响AuthValue来达到加强安全性的目的。
一些做了很久蓝牙mesh的同事,也不太清楚这块的区别。
3.5 分发配网数据的详细过程
配网器负责生成并分发配网数据到未配网设备,网络数据包括如:“Network Key(网络密钥)”以及“Unicast Address(设备地址)”等重要数据。
字段 | 长度 | 备注 |
---|---|---|
Encrypted Provision Data | 25 | 设备地址、网络密钥等参数 |
MIC | 8 | AES-CCM算法生成的完整性检查值 |
分发配网数据的加密方式
为了更安全地分发配网数据,配网器需要使用AES-CCM算法来加密配网数据,比如算法涉及两个加密密钥参数:Session Key和Session Nonce,他们均由ECDH密钥派生。
配网器将加密后的配网数据包发送给未配网设备,未配网设备从数据中获知Unicast Address和Network Key之后,即可加入蓝牙mesh网络,成为网络中的一个节点。
配网数据生成的细节:
1.根据已知的三个数据The Confirmation Inputs、16位Provisioner Random 和 16位device Random,生成三个新数据Session Key, Session Nonce 和 the Device Key
固件端的DeviceKey是固件端自己生成的。DeviceKey在配网客户端和固件端各自生成。
固件端的NetworkKey、NetworkKey Index和UnicastAddress是在第五步“分发配网数据”时由APP指定的。
AppKey是在Config阶段通过mesh消息流程由配网客户端给固件端设置
(1)根据confirmationInputs计算confirmationSalt
let confirmationSalt = helper.calculateSalt(confirmationInputs)!
(2)将刚生成的confirmationSalt和之前的两个随机数相加,作为秘钥生成salt值provisioningSalt。
let provisioningSalt = helper.calculateSalt(confirmationSalt + provisionerRandom! + deviceRandom!)!
(3)根据共享密钥sharedSecret、刚生成的provisioningSalt,按AES-CMAC算法传参Salt K1 计算衍生值sessionKey,sessionNonce,deviceKey.
sessionKey = helper.calculateK1(withN: sharedSecret!,salt: provisioningSalt,andP: "prsk".data(using: .ascii)!)!
sessionNonce= helper.calculateK1(withN: sharedSecret!,salt: provisioningSalt,andP: "prsn".data(using: .ascii)!)!.dropFirst(3)
deviceKey = helper.calculateK1(withN: sharedSecret!,salt: provisioningSalt,andP: "prdk".data(using: .ascii)!)!
为什么deviceKey的生成是由双方自己生成?
因为deviceKey的生成与4个值有关,confirmationInputs、provisionerRandom、deviceRandom、sharedSecret,这4个参数共同确定deviceKey的结果。confirmationInputs是通信交互中的数据,provisionerRandom,deviceRandom是第四步中的随机数,sharedSecret是计算出来的共享密钥。这个4个参数在两个端都知道,而且是相同的,所以deviceKey的计算各自生成就可以了。
2.用ivIndex和networkKey计算Flags,然后得到计算出25位的Provision Data
let flags = Flags(ivIndex: ivIndex, networkKey: networkKey)
let data = networkKey.key + networkKey.index.bigEndian + flags.rawValue + ivIndex.index.bigEndian + unicastAddress.bigEndian
networkKey.key:3918C5649E9A1CAB5746B2CF2B2E2397
networkKey.index:0000
flags.rawValue:00
ivIndex.index:00000000
unicastAddress:0004
data:3918C5649E9A1CAB5746B2CF2B2E2397000000000000000004
3.用CBC-MAC(CCM)算法计算完整的数据。参数分别是25位Provision Data,sessionKey,sessionNonce,指定MIC长度为8
RFC3610 defines teh AES Counted with CBC-MAC (CCM).
helper.calculateCCM(data, withKey: keys.sessionKey, nonce: keys.sessionNonce, andMICSize: 8, withAdditionalData: nil)
0xF94C0BCC18558B67E750187CE3EF5CD7E3A74DC8631F9018A053CCE72A599690F4