BLE CTKD 完整学习笔记
基于 Bluetooth Core Spec 5.4、固件代码(BES BEST1603)整理
覆盖:基础概念 / SMP 配对原理 / LTK 机制 / LL 加密过程 / CTKD 原理与流程 / iOS vs Android 差异 / iOS SMP 失败恢复 / 排错指南
一、基础概念速查
1.1 密钥体系总览
| 缩写 | 全称 | 作用 | 所属层 |
|---|---|---|---|
| LTK | Long Term Key | BLE 重连加密的主密钥,16 字节 | BLE SMP |
| EDIV | Encrypted Diversifier | LTK 的查找索引(Legacy Pairing 用),2 字节 | BLE SMP |
| Rand | Random Number | 与 EDIV 配合定位 LTK,8 字节 | BLE SMP |
| IRK | Identity Resolving Key | 解析对端的随机地址(RPA),16 字节 | BLE SMP |
| CSRK | Connection Signature Resolving Key | 数据签名验证,16 字节 | BLE SMP |
| Link Key | BR/EDR Link Key | 经典蓝牙连接加密密钥,16 字节 | BR/EDR |
| STK | Short Term Key | Legacy Pairing 临时会话密钥(不存储) | BLE SMP |
| DHKey | Diffie-Hellman Key | SC Pairing 中 ECDH 协商的共享密钥 | BLE SMP |
| LK | Link Key(CTKD 语境) | 经 CTKD 从 BLE LTK 派生的 BR/EDR Link Key | CTKD |
1.2 SMP 是什么
SMP(Security Manager Protocol) 是 BLE 协议栈中负责配对和密钥分发的协议,跑在 L2CAP 固定信道(CID 0x0006)上。
SMP 的核心任务:
- 配对(Pairing):协商安全参数,生成会话密钥
- 绑定(Bonding):将长期密钥(LTK/IRK/CSRK)持久化存储,用于后续重连
- 加密(Encryption):用 LTK 启动链路层加密
1.3 LTK 是什么,和 SMP 的关系
SMP 是协议(过程)
LTK 是结果(密钥)
- SMP 配对成功后,双方各自生成并交换 LTK
- LTK 持久化存储在 NV(非易失存储)中
- 重连时不再走 SMP 配对,直接用存储的 LTK 启动链路层加密(LL Encryption)
- Central(手机)用 EDIV+Rand 向 Peripheral(耳机)请求 LTK,耳机从 NV 查找并回复
关键区分:
| 场景 | 是否走 SMP | 是否使用 LTK |
|---|---|---|
| 首次连接(未配对) | 走 SMP Pairing | 配对完成后生成并存储 LTK |
| 已配对设备重连 | 不走 SMP | 直接用已存的 LTK 加密 |
| 配对信息丢失一方 | 走 SMP 重新配对 | 生成新 LTK |
这就是为什么安卓正常连接 log 里看不到 [SMP_REQUIRE] 和 [SMP_PAIR_CMP]------安卓已和耳机配对过,重连时直接走 LTK 加密,SMP 事件根本不发生。
1.4 LTK 的生成原理(Spec 精确定义)
LTK 的生成方式因配对模式不同而有本质区别。
SC Pairing 中的 LTK(推荐,CTKD 必须使用)
规范参考 :Core Spec 5.4 Vol 3 Part H § 2.2.8(f5 函数)
SC Pairing 用 ECDH 共享密钥 DHKey 通过密码学函数 f5 同时派生 MacKey 和 LTK:
f5(W, N1, N2, A1, A2) → { MacKey, LTK }
输入:
W = DHKey(ECDH P-256 协商的 256 位共享密钥)
N1 = Initiator 的随机数 Na(128 位)
N2 = Responder 的随机数 Nb(128 位)
A1 = Initiator 蓝牙地址(含类型,7 字节)
A2 = Responder 蓝牙地址(含类型,7 字节)
内部实现(双 AES-CMAC):
T = AES-CMAC_SALT("btle") 其中 SALT = 6C888391AAF5A538
MacKey = AES-CMAC_T( counter=0 || keyID="ltk " || Swapped(N1) || Swapped(N2) || A1 || A2 || length=256 )
LTK = AES-CMAC_T( counter=1 || keyID="ltk " || Swapped(N1) || Swapped(N2) || A1 || A2 || length=256 )
- 双方独立推算 :Initiator(手机)和 Responder(耳机)各自拥有相同的 DHKey、随机数和地址,可以独立计算出完全相同的 LTK,无需传输 LTK 本体
- MacKey 用于 DHKey Check (Phase 2 最后的
f6认证验证,防中间人攻击) - SC 模式下 EDIV=0, Rand=0:LTK 不依赖 EDIV/Rand 索引,LL 加密请求时 EDIV 和 Rand 字段均填 0
Legacy Pairing 中的 LTK(已过时,不支持 CTKD)
规范参考:Core Spec 5.4 Vol 3 Part H § 2.3.5.1
Legacy Pairing 的 LTK 由 Responder(耳机)随机生成,再通过 STK 加密后分发给 Initiator:
配对流程:
1. 双方计算 STK(Short Term Key):
STK = s1(TK, Srand, Mrand)
其中 TK = 临时密钥(Just Works 下为全0,Passkey Entry 下为6位数字)
2. 用 STK 加密 L2CAP 通道
3. Responder 生成随机 LTK + 随机 EDIV + 随机 Rand
4. Phase 3 密钥分发:
Responder → Initiator:
Encryption Information PDU 包含 LTK(用加密通道传输)
Central Identification PDU 包含 EDIV + Rand
- LTK 通过链路传输:明文 LTK 在加密信道内传输(STK 保护),但 STK 本身易被暴力破解
- EDIV+Rand 是 LTK 的索引键:重连时 Central 发送 EDIV+Rand,Peripheral 从 NV 查找对应的 LTK
- 不支持 CTKD:Legacy Pairing 缺乏 DHKey,h6/h7 派生 BR/EDR Link Key 的安全证明不成立,规范明确禁止
两种模式对比
| 特性 | SC Pairing | Legacy Pairing |
|---|---|---|
| 规范基础 | BT 4.2+,§ 2.3.5.6 | BT 4.0,§ 2.3.5.1 |
| LTK 生成方式 | 双方从 DHKey 独立计算(f5) |
Responder 随机生成,加密分发 |
| LTK 在链路上传输? | 否(双方各自推算) | 是(STK 加密保护) |
| EDIV/Rand 用途 | SC 下填 0,仅兼容用 | 实际索引,查 NV 找 LTK |
| 是否支持 CTKD | 是 | 否 |
| 安全强度 | 256-bit ECDH,抗被动窃听 | STK 可被暴力破解,不推荐 |
二、SMP 配对详解
2.1 两种配对方式
BLE 配对方式
├── Legacy Pairing(BT 4.0/4.1)
│ └── 生成 STK → 用 STK 加密 → 分发 LTK
└── Secure Connections (SC) Pairing(BT 4.2+)
└── ECDH P-256 协商 DHKey → 直接生成 LTK
Secure Connections 更安全(防中间人),CTKD 必须基于 SC。
2.2 配对三阶段
Phase 1:配对能力协商(Pairing Feature Exchange)
Central (手机) Peripheral (耳机)
| |
|--- Pairing Request (Code 0x01) --->|
| io_cap / auth_req / key_dist |
| |
|<-- Pairing Response (Code 0x02) ---|
| io_cap / auth_req / key_dist |
关键字段(对应代码 app_ble.c:3836):
auth_req:认证需求位域SMP_AUTH_SC_SUPPORT (0x08):要求 Secure ConnectionsSMP_AUTH_MITM_PROTECT (0x04):要求 MITM 防护(CTKD 必须置位)SMP_AUTH_BONDING (0x01):要求绑定(保存密钥)
init_key_dist/resp_key_dist:密钥分发位域GAP_KDIST_ENCKEY (0x01):分发 LTK+EDIV+RandGAP_KDIST_IDKEY (0x02):分发 IRK+Identity AddressGAP_KDIST_SIGNKEY(0x04):分发 CSRKGAP_KDIST_LINKKEY(0x08):分发 BR/EDR Link Key(CTKD 专用)
Phase 2:密钥生成(Authentication)
取决于双方 IO Capability 决定的 Association Model:
| IO Cap 组合 | Association Model | MITM 防护 |
|---|---|---|
| NoInputNoOutput + NoInputNoOutput | Just Works | 无 |
| DisplayOnly + KeyboardOnly | Passkey Entry | 有 |
| DisplayYesNo + DisplayYesNo | Numeric Comparison | 有 |
| 任意一方有 OOB 数据 | OOB | 有 |
对于 CTKD 场景(app_ble.c:3839-3840):
c
p_requirements->auth_req |= SMP_AUTH_SC_SUPPORT; // 强制 SC
p_requirements->auth_req |= SMP_AUTH_MITM_PROTECT; // 强制 MITM
耳机强制要求 SC + MITM,手机侧无 IO Capability 时降级为 Just Works(无 MITM),但 SC 仍保持。
SC Pairing Phase 2 完整流程(Spec Vol 3 Part H § 2.3.5.6):
Central (手机/Initiator) Peripheral (耳机/Responder)
| |
|====== 步骤 1:公钥交换 ===============|
|--- Pairing Public Key (0x0C) Pkx,Pky->| Initiator 的 ECDH P-256 公钥
|<-- Pairing Public Key (0x0C) Pkx,Pky--| Responder 的 ECDH P-256 公钥
| |
| 双方用对方公钥计算: |
| DHKey = ECDH(私钥, 对方公钥) | 共享 256-bit 秘密
| |
|====== 步骤 2:Commitment(f4 函数)===|
| (Just Works / Numeric Comparison) |
|--- Pairing Confirm (0x03) Ca ---------->| Ca = f4(PKax, PKbx, Na, 0)
|<-- Pairing Confirm (0x03) Cb -----------| Cb = f4(PKbx, PKax, Nb, 0)
| |
|====== 步骤 3:随机数交换 ==============|
|--- Pairing Random (0x04) Na ---------->| Initiator 揭示随机数 Na
|<-- Pairing Random (0x04) Nb -----------| Responder 揭示随机数 Nb
| |
| 双方验证 Confirm 值: |
| Responder 验证:Ca == f4(PKax, PKbx, Na, 0) ?
| Initiator 验证:Cb == f4(PKbx, PKax, Nb, 0) ?
| |
|====== 步骤 4:LTK 派生(f5 函数)====|
| MacKey, LTK = f5(DHKey, Na, Nb, A_init, A_resp)
| 双方独立计算,结果相同 |
| |
|====== 步骤 5:DHKey Check(f6 函数)=|
|--- Pairing DHKey Check (0x0D) Ea ----->| Ea = f6(MacKey, Na, Nb, rb, IOcap_a, A_init, A_resp)
|<-- Pairing DHKey Check (0x0D) Eb ------| Eb = f6(MacKey, Nb, Na, ra, IOcap_b, A_resp, A_init)
| |
| Responder 验证 Ea:防止中间人攻击 |
| Initiator 验证 Eb:防止中间人攻击 |
| DHKey Check 通过 → SMP Phase 2 完成 |
三个核心密码学函数(Spec Vol 3 Part H § 2.2.6--2.2.8):
f4(U, V, X, Z) = AES-CMAC_X(U || V || Z)
用途:生成 Commitment 值(Confirm PDU 的内容)
U, V = 各自 ECDH 公钥的 X 坐标(256-bit)
X = 随机数(128-bit Nonce)
Z = Association Model 标识(Just Works=0x00,Passkey=当前bit位)
f5(W, N1, N2, A1, A2) → { MacKey, LTK }
用途:从 DHKey 派生 MacKey 和 LTK(128-bit × 2)
W = DHKey(256-bit ECDH 共享密钥)
实现:双 AES-CMAC,counter=0 出 MacKey,counter=1 出 LTK
f6(W, N1, N2, R, IOcap, A1, A2) = AES-CMAC_W(N1 || N2 || R || IOcap || A1 || A2)
用途:生成 DHKey Check 值(防中间人)
W = MacKey(f5 的输出)
R = OOB 随机数 / Passkey Confirm 数据
安全性关键:f6 的设计保证了只有真正持有 DHKey 的一方才能生成正确的 Check 值,若有中间人劫持了 ECDH 交换,其 DHKey 不同,f6 验证必然失败。
Phase 3:密钥分发(Key Distribution)
Central (手机) Peripheral (耳机)
| |
|<-- Encryption Information (LTK) ---| ← 耳机发给手机的 LTK
|<-- Central Identification (EDIV+Rand)
|<-- Identity Information (IRK) -----|
|<-- Identity Address Information ---|
| |
|--- Encryption Information (LTK) -->| ← 手机发给耳机的 LTK
|--- Central Identification -------->|
|--- Identity Information (IRK) ---->|
|--- Identity Address Information -->|
|--- (如果 CTKD) Link Key ---------->| ← CTKD 扩展:BR/EDR Link Key
2.3 LL 加密过程(Spec Vol 6 Part B § 5.1.3)
SMP 配对完成(或重连)后,链路层(Link Layer)发起 LL Encryption Procedure 启动 AES-CCM 加密。这是 LTK 实际"被使用"的时刻。
PDU 交互流程
Central (手机) Peripheral (耳机)
| |
|--- LL_ENC_REQ ----------------------->|
| Rand[64] | SC 模式下为全0
| EDIV[16] | SC 模式下为全0
| SKDm[64] (Session Key Diversifier, 手机半部分)
| IVm[32] (Initialization Vector, 手机半部分)
| |
| 耳机执行: |
| LTK 查找: |
| nv_record_ble_record_find_ltk(addr, ltk, ediv)
| → RPA 先解析为身份地址 |
| → 匹配 EDIV+Rand 找 LTK |
| |
|<-- LL_ENC_RSP ------------------------|
| SKDs[64] (Session Key Diversifier, 耳机半部分)
| IVs[32] (Initialization Vector, 耳机半部分)
| |
| 双方计算会话密钥 SK: |
| SKD = SKDm || SKDs (128-bit 拼接)|
| SK = AES-128(LTK, SKD) |
| IV = IVm || IVs (64-bit 拼接) |
| |
|<-- LL_START_ENC_REQ ------------------| Peripheral 先启用加密
| |
|--- LL_START_ENC_RSP ----------------->| Central 确认加密
| |
| 链路加密完成(AES-CCM with SK) |
| 触发 GAP_CONN_EVENT_ENCRYPTED |
| ENCT log:error_code=0 |
关键字段说明:
| 字段 | 长度 | SC 模式 | Legacy 模式 |
|---|---|---|---|
Rand |
8 字节 | 全 0 | Responder 生成的随机数(LTK 索引) |
EDIV |
2 字节 | 全 0 | Responder 生成的随机标识(LTK 索引) |
SKDm |
8 字节 | Central 随机生成 | 同左 |
SKDs |
8 字节 | Peripheral 随机生成 | 同左 |
IVm |
4 字节 | Central 随机生成 | 同左 |
IVs |
4 字节 | Peripheral 随机生成 | 同左 |
会话密钥 SK 推导:
SK = AES-128(LTK, SKDm || SKDs)
- SKD 和 IV 每次连接随机生成(保证每次会话密钥不同)
- LTK 是长期密钥 ,SK 是临时会话密钥------LTK 不直接用于加解密数据
- AES-CCM 用 SK 和 IV 对所有 LL Data PDU 做认证加密(提供完整性保护)
LTK 查找失败的后果
若耳机 NV 找不到 EDIV+Rand 对应的 LTK(nv_record_ble_record_find_ltk 返回 false):
c
// app_ble.c:1194 app_ble_reply_peer_ltk_request
if (!ltk_found) {
gap_reply_peer_ltk_request(connhdl,
true, // negative_reply = true
NULL); // 无 LTK
}
Peripheral 发送 LL_REJECT_IND(错误码 0x06 = Pin or Key Missing),链路加密失败,触发 ENCT 事件且 error_code != 0(SMP_ERROR_ENABLE_ENC_FAILED = 0x16 或协议错误)。
LTK 存在但 MAC 验证失败
若 NV 中存有 LTK,但 LTK 内容不正确(iOS/Android"脏 LTK"场景):
- 耳机提交 LTK,会话密钥 SK 被双方各自计算
- 双方 SK 不同(因 LTK 不同)
- AES-CCM 消息认证失败:
BT_HCI_ERR_CONN_TERM_MIC_FAILURE (0x3E) - 对应 CO_LOG:
CNNE ... 16(0x16 = MIC failure)
三、CTKD 原理详解
3.1 CTKD 是什么
CTKD(Cross-Transport Key Derivation,跨传输密钥派生) 是 Bluetooth Core Spec 5.1 引入的机制(在 4.2 时已有雏形),允许在一种传输(BLE 或 BR/EDR)完成配对后,派生出另一种传输所需的密钥,从而实现"配对一次,两个传输都加密"。
两个方向:
① CTKD over LE(LE 配对 → 派生 BR/EDR Link Key) ← 本项目主要场景
② CTKD over BR/EDR(BR/EDR 配对 → 派生 BLE LTK)
代码中对应两个 NV 写入路径:
BT_NV_REC_ADD_CTKD_OVER_LE(app_ble.c:1704):BLE 配对派生出 BR/EDR 记录BLE_NV_REC_ADD_CTKD_OVER_BREDR(app_ble.c:3980):BR/EDR 配对派生出 BLE 记录
3.2 为什么需要 CTKD
传统双模设备(BLE + BR/EDR)用户需要:
- 手机通过 BLE 扫描并配对耳机(SMP)
- 手机再通过 BR/EDR 配对耳机(HFP/A2DP 连接时的 PIN/SSP)
两次独立配对,用户体验割裂,且安全性参差不齐。
CTKD 后:
- 只需 BLE 配对一次(SMP SC + MITM)
- 自动派生 BR/EDR Link Key,BR/EDR 连接时直接使用
- 用户无感知
3.3 CTKD over LE 的密钥派生算法
基于 AES-CMAC 函数,使用 BLE SC Pairing 产生的 BR/EDR Link Key 从 LTK 派生:
h6(W, keyID) = AES-CMAC_W(keyID)
h7(SALT, W) = AES-CMAC_SALT(W)
// 派生步骤(Spec Section 2.4.2.4)
ILK = h7("LEBR", LTK) // Intermediate Link Key(如果 SC)
LinkKey = h6(ILK, "lesc") // 最终 BR/EDR Link Key
关键前提:BLE 配对必须使用 Secure Connections(SC),Legacy Pairing 不支持 CTKD。
3.4 Key Distribution 中的 LinkKey 标志位
SMP Pairing Request/Response 的 key_dist 字段中 bit3(0x08)= LinkKey,双方都置位时表示"我要分发派生的 BR/EDR Link Key"。
代码(app_ble.c:3850-3853):
c
if (key_dist & GAP_KDIST_LINKKEY) {
p_requirements->init_key_dist |= GAP_KDIST_LINKKEY; // Initiator 分发
p_requirements->resp_key_dist |= GAP_KDIST_LINKKEY; // Responder 分发
}
这就是 [SMP_REQUIRE] 日志里的 ikey 和 rkey 字段(app_ble.c:1821)。
四、完整连接交互流程
4.1 首次配对(CTKD over LE)时序图
手机 (Central/Initiator) 耳机 (Peripheral/Responder)
| |
|====== BLE 广播扫描 ===================>|
|<----- ADV_IND / ADV_EXT_IND -----------| FADV/SADV
| |
|====== BLE 连接建立 ===================>|
|------ LL_CONNECT_REQ ---------------> |
|<----- [GAP_EVENT] event=8240 --------- | connhdl 分配
| |
|====== MTU 协商 ========================|
|------ ATT_EXCHANGE_MTU_REQ ----------->|
|<----- ATT_EXCHANGE_MTU_RSP ------------| [GAP_EVENT] event=8244
| (MTU: 23→512) |
| |
|====== SMP Phase 1:配对能力协商 =======|
|------ Pairing Request (0x01) --------->| 触发 [SMP_REQUIRE] 日志
| auth=SC+MITM+Bond | ikey=0x0F rkey=0x0F (含0x08 LinkKey)
|<----- Pairing Response (0x02) ---------|
| auth=SC+MITM+Bond |
| |
|====== SMP Phase 2:SC Authentication ==|
|------ Pairing Public Key (0x0C) ------>|
|<----- Pairing Public Key (0x0C) -------|
| (ECDH P-256 公钥交换) |
|------ Pairing Confirm (0x03) --------->| (Just Works 场景)
|<----- Pairing Confirm (0x03) -----------|
|------ Pairing Random (0x04) ---------->|
|<----- Pairing Random (0x04) -----------|
| 双方各自计算 DHKey→LTK |
|------ Pairing DHKey Check (0x0D) ----->|
|<----- Pairing DHKey Check (0x0D) ------|
| |
|====== LL 加密启动 =====================|
|------ LL_ENC_REQ (EDIV+Rand) --------->| 触发 GAP_USER_LTK_REQUEST_CONFIRM
|<----- LL_ENC_RSP ----------------------| 耳机查 NV → app_ble_reply_peer_ltk_request
|<----- LL_START_ENC_REQ ----------------|
|------ LL_START_ENC_RSP ---------------- |
| [GAP_EVENT] event=8245 (ENCT) | 链路加密完成
| |
|====== SMP Phase 3:密钥分发 ===========|
|<----- Encryption Information (LTK) ----|
|<----- Identity Information (IRK) ------|
|<----- Identity Address Information ----|
|<----- (CTKD) LinkKey PDU (0x0B) -------| ← CTKD 关键步骤:BR/EDR Key 分发
|------ Encryption Information (LTK) --->| 触发 GAP_CONN_EVENT_RECV_KEY_DIST
|------ Identity Information (IRK) ----->|
|------ Identity Address Information ---->|
|------ (CTKD) LinkKey PDU (0x0B) ------->|
| GAP_RECV_DERIVED_BT_LINK_KEY | 触发 RLTK 日志
| BT_NV_REC_ADD_CTKD_OVER_LE 写入 | BR/EDR 配对记录存 NV
| |
| [SMP_PAIR_CMP] err=0x00 | 配对完成
| |
|====== GATT 数据通道建立 ===============|
| app_datapath_server_connected |
| |
|====== 连接参数更新 ====================|
|<----- LL_CONNECTION_PARAM_REQ ---------| [GAP_EVENT] event=8248
|------ LL_CONNECTION_PARAM_RSP -------->| [GAP_EVENT] event=8249
| (interval=180ms, timeout=5s) |
| |
|====== BR/EDR 连接(之后) =============|
| 手机发起 BR/EDR 连接 |
| 耳机用 CTKD 派生的 Link Key |
| 直接完成 BR/EDR 认证,无需再次配对|
4.2 已配对设备重连(无 SMP)时序图
手机 (Central) 耳机 (Peripheral)
| |
|====== BLE 连接建立 ===================>|
| [GAP_EVENT] event=8240 | connhdl=3
| |
|====== MTU 协商 ========================|
| [GAP_EVENT] event=8244 | MTU→512
| |
|====== LL 加密(无 SMP!)=============|
|------ LL_ENC_REQ (EDIV+Rand) --------->| 用已存的 EDIV+Rand 查 LTK
| GAP_USER_LTK_REQUEST_CONFIRM |
| nv_record_ble_record_find_ltk() | 从 NV 查 LTK
|<----- LL_ENC_RSP ----------------------|
|<----- LL_START_ENC_REQ ----------------|
|------ LL_START_ENC_RSP ---------------- |
| ENCT 事件(无 PAIRING 标志) | ← 这里不触发 BOND 事件
| |
|====== GATT 数据通道 ===================|
| app_datapath_server_connected |
| box cmd: 510c / 5108 / b01 |
这就是本次安卓正常连接日志的完整路径:无 SMP,直接 LTK 加密。
五、iOS vs Android 差异
5.1 SMP 配对行为差异
| 行为 | Android | iOS |
|---|---|---|
| 首次配对发起方 | Central(手机主动) | Central(手机主动) |
| SC 支持 | 支持(Android 6.0+) | 支持(iOS 9+) |
| MITM 要求 | 默认 Just Works(无 MITM) | 倾向于 Numeric Comparison / Just Works |
| CTKD LinkKey 分发 | 支持(Android 12+ 标准行为) | 不支持(iOS 不分发 LinkKey PDU) |
| 重连加密方式 | LL_ENC_REQ 用 EDIV+Rand(Legacy 或 SC) | 同左,但 SC 下用 Rand=0 + EDIV=0 |
| 配对信息丢失处理 | 收到 LL_ENC_REJ 后重新配对 | 弹出系统配对对话框重新配对 |
| RPA(随机地址) | 支持,但部分厂商用公开地址 | 强制使用 RPA,需要 IRK 解析 |
5.2 CTKD 支持差异(核心)
Android (12+)
BLE 配对 ──→ SMP SC Pairing
↓
LinkKey PDU 分发(0x08 in key_dist)
↓
耳机存入 BR/EDR NV(BT_NV_REC_ADD_CTKD_OVER_LE)
↓
BR/EDR 连接时直接用 Link Key 认证 ✓
iOS (所有版本)
BLE 配对 ──→ SMP SC Pairing
↓
LinkKey PDU 不分发(key_dist 无 0x08)
↓
BR/EDR 连接时需要独立 SSP 配对(弹配对确认框)
实际影响:
- Android 用户:BLE 配对后,BR/EDR 连接无感知自动完成
- iOS 用户:BLE 配对后,BR/EDR 仍需独立配对(或通过其他机制如 MFi)
5.3 RPA 处理差异
iOS 强制使用 RPA(Resolvable Private Address),每次重连地址可能不同。耳机需要:
- SMP Phase 3 获取 iOS 的 IRK
- 将 IRK 加入 Resolving List(
app_ble_add_devices_info_to_resolving()) - 重连时用 IRK 解析 RPA,找到对应的 LTK(
app_ble_continue_recv_peer_ltk_req())
代码路径(app_ble.c:1233-1249):
c
// RPA 需要先解析
if (conn->peer_type == BT_ADDR_TYPE_RANDOM) {
gap_resolve_rpa(&conn->peer_addr,
app_ble_continue_recv_peer_ltk_req, ...)
}
Android 部分机型使用公开地址,不需要 RPA 解析,流程更简单。
5.4 连接参数协商差异
| 参数 | Android 典型值 | iOS 典型值 |
|---|---|---|
| 初始 Connection Interval | 7.5ms ~ 45ms | 30ms ~ 90ms |
| Peripheral Latency | 0 | 0 |
| 协商后 Interval | 按设备请求(本例 180ms) | 保守,通常 ≥ 20ms |
| MTU 协商 | 快速升到 512 | 升到 185 或 512(iOS 版本相关) |
六、本项目代码关键路径
6.1 事件流 → 日志对应表
| GAP 事件 | event 值 | CO_LOG tag | TRACE tag | 触发时机 |
|---|---|---|---|---|
GAP_CONN_EVENT_OPENED |
0x2030 (8240) | CNNS | --- | 连接建立 |
GAP_CONN_EVENT_MTU_CHANGED |
0x2034 (8244) | MTUE | --- | MTU 协商 |
GAP_CONN_EVENT_USER_CONFIRM |
--- | USRC | --- | 数字比对确认 |
GAP_CONN_EVENT_ENCRYPTED |
0x2035 (8245) | ENCT | --- | 链路加密完成 |
GAP_CONN_EVENT_RECV_SMP_REQUIRE |
--- | PARQ | [SMP_REQUIRE] |
收到配对请求 |
GAP_CONN_EVENT_PAIRING_COMPLETE |
--- | PACM | [SMP_PAIR_CMP] |
配对完成 |
GAP_CONN_EVENT_RECV_KEY_DIST |
--- | KEYD | --- | 收到密钥分发 |
GAP_CONN_EVENT_UPDATE_REQ |
0x2038 (8248) | UPRQ | --- | 连接参数更新请求 |
GAP_CONN_EVENT_PARAMS_UPDATE |
0x2039 (8249) | UPED | --- | 连接参数更新完成 |
GAP_EVENT_RECV_DERIVED_BLE_LTK |
--- | RLTK | --- | BR/EDR 派生 BLE LTK |
GAP_USER_LTK_REQUEST_CONFIRM |
--- | --- | --- | 对端请求 LTK |
6.2 CTKD 核心函数调用链
SMP Phase 3 收到 LinkKey PDU
└── GAP_CONN_EVENT_RECV_KEY_DIST
└── key_dist->key_type == GAP_RECV_DERIVED_BT_LINK_KEY
└── bluetooth_nv_mgr_bt_record_add(BT_NV_REC_ADD_CTKD_OVER_LE, &record)
└── BR/EDR Link Key 存入 NV,供经典蓝牙连接使用
重连时收到 LTK 请求
└── GAP_USER_LTK_REQUEST_CONFIRM
└── app_ble_ltk_request_recv_handler(conn, ediv)
├── [if RPA] gap_resolve_rpa() → app_ble_continue_recv_peer_ltk_req()
└── app_ble_reply_peer_ltk_request(conn, ediv)
└── nv_record_ble_record_find_ltk(addr, ltk, ediv)
└── gap_reply_peer_ltk_request(connhdl, false, ltk)
七、排错指南
7.1 问题:BLE 连接成功,但看不到 [SMP_REQUIRE]
可能原因:
| 原因 | 判断方法 | 处置 |
|---|---|---|
| ① 已配对重连,正常行为 | 检查是否有 ENCT 事件 + smp_pairing_ongoing=0 |
正常,无需处理 |
② __CTKD_ENABLE__ 未定义 |
检查有无 [GAP_EVENT] 打印 |
重新确认编译宏 |
| ③ SMP 被对端跳过 | 检查 ENCT 之后有无 PACM | 检查手机侧行为 |
7.2 问题:[SMP_PAIR_CMP] err != 0
常见错误码(SMP Error Code,Spec Table 3.7):
| err_code | 名称 | 含义 | 常见原因 |
|---|---|---|---|
| 0x01 | Passkey Entry Failed | 密码输入失败 | 用户取消或超时 |
| 0x02 | OOB Not Available | OOB 数据不可用 | OOB 配置错误 |
| 0x03 | Authentication Requirements | 认证需求不满足 | 双方 auth_req 不兼容 |
| 0x05 | Pairing Not Supported | 不支持配对 | 被拒绝 |
| 0x06 | Encryption Key Size | 加密密钥长度不足 | 最小密钥长度配置问题 |
| 0x07 | Command Not Supported | 命令不支持 | SC 协商失败降级 |
| 0x08 | Unspecified Reason | 未指定原因 | 各种软件问题 |
| 0x09 | Repeated Attempts | 重复尝试 | 连续配对失败 |
| 0x0A | Invalid Parameters | 参数无效 | 协议格式错误 |
| 0x0B | DHKey Check Failed | DHKey 校验失败 | MITM 攻击或密钥不匹配 |
| 0x0C | Numeric Comparison Failed | 数字比对失败 | 用户拒绝确认 |
7.3 问题:CTKD 派生失败(BR/EDR 仍需配对)
排查步骤:
- 检查
[SMP_REQUIRE]中的ikey/rkey是否包含0x08(LinkKey 位) - 检查
KEYDCO_LOG 是否有key_type=GAP_RECV_DERIVED_BT_LINK_KEY的记录 - 检查
RLTK日志是否出现 - 确认手机是否支持 CTKD(Android 12+ 支持,iOS 不支持)
- 检查 BLE 配对是否使用了 SC(
bond_info_bf中BONDED_SECURE_CONNECTION位)
7.4 问题:重连时 LTK 查找失败(加密失败)
log 特征:ENCT 事件出现且 error_code != 0,具体是 SMP_ERROR_ENABLE_ENC_FAILED
代码路径(app_ble.c:1598-1603):
c
if (SMP_ERROR_ENABLE_ENC_FAILED == encrpt->error_code && conn->conn_flag.is_central == true) {
// 删除失败的 NV 记录
bluetooth_nv_mgr_ble_record_del(BLE_NV_REC_DEL_LE_ENC_FAILURE, ...);
// 重新触发 SMP 配对
gap_start_authentication(conn->connhdl, GAP_AUTH_STARTED_BY_UPPER_APP);
}
意味着:LTK 不匹配时固件会自动清除旧记录并重新配对,用户侧可能看到配对弹框。
7.5 问题:iOS 连接正常但 Android 连接异常
对比要点:
| 检查项 | Android log 预期 | iOS log 预期 |
|---|---|---|
| 广播地址类型 | 公开地址或 RPA | 必定 RPA |
首次配对 [SMP_REQUIRE] |
ikey/rkey 含 0x08 | ikey/rkey 无 0x08 |
KEYD key_type |
含 GAP_RECV_DERIVED_BT_LINK_KEY |
无此类型 |
重连有无 [SMP_REQUIRE] |
无(直接 LTK) | 无(直接 LTK) |
ENCT error_code |
0(成功) | 0(成功) |
7.6 关键 log 速查
# 连接建立
[GAP_EVENT] connhdl=X event=8240
# 首次配对开始(有 SMP_REQUIRE → 首次配对)
[SMP_REQUIRE] ... ikey=0x0F rkey=0x0F ← ikey/rkey 含 0x08 = CTKD
# 链路加密完成(重连或首次均有)
ENCT ... error_code=0
# CTKD 密钥分发完成
RLTK ltk_generated_but_still_wait_peer_kdist=0
# 配对完成(首次配对有)
[SMP_PAIR_CMP] err=0x00
# GATT 数据通道建立
app_datapath_server_connected.[GATT/BLE] conidx:XX connhdl:X
7.7 iOS MIC Failure(CNNE 0x16):脏 LTK 死循环实例分析
背景:此案例来自真实 iOS 设备连接耳机的 log(约 73000 行)。
症状 :iOS 反复与耳机配对,每次配对成功(PACM err=0)后 iOS 主动断开(CNNE 13),重连时触发 MIC 故障(CNNE 16),最终 SMP 配对失败(PARF 08)。
日志模式解析
正常配对成功后的意外断开(反复循环):
# iOS 完成 SMP 配对,配对成功
PARQ 0401 ← Pairing Request queued, conidx=4
PARS ... ← Pairing Response stored
...
ENCR 0101 ← 加密成功(error=0)
ENCT 31010004 00 ← ENCRYPTED 事件,conhdl=0x31010004,err=0
...
KEYD ← Key Distribution 开始
...
PACM 04 ← Pairing Complete, conidx=4, err=0 ← 配对成功!
# iOS 随即主动断开(0x13 = Remote User Terminated Connection)
CNNE 31010004 13 ← 断开,conidx=4,reason=0x13
0x13 表示 iOS 主动发起断开(非故障),可能原因:iOS 内部系统行为(配对完成后触发 re-bond 流程)。
脏 LTK 触发 MIC Failure:
# iOS 重新连接,用刚配对存储的(已失效的)LTK 尝试加密
CNNS 31010003 ← 新连接 conidx=3 建立
...
LL_ENC_REQ ← iOS 发送 EDIV+Rand,耳机查 NV 找到 LTK
LL_ENC_RSP ← 耳机回复,双方各自计算 SK = AES-128(LTK, SKD)
← 由于 LTK 不一致,SK 不同 → AES-CCM MAC 验证失败
CNNE 31010004 16 ← 断开,reason=0x16 = BT_HCI_ERR_CONN_TERM_MIC_FAILURE
第二次也出现同样错误:
CNNE 30010003 16 ← 连接断开,reason=0x16(MIC failure 再次)
最终 SMP 配对失败(11:02:24):
PARQ 0401 ← iOS 再次发起配对
...
PARF 04 08 ← Pairing Failed, conidx=4, reason=0x08 (Unspecified Reason)
PARE 04 00 09 ← Pairing Error, err=0x09 (Repeated Attempts)
ENCT 31010004 08 ← 加密事件,err=0x08
PACM 04 ← Pairing Complete, 但已失败
# 代码 app_custom.c:198-203 触发:
# app_ble_disconnect() ← 主动断开
# app_ble_refresh_adv_state_generic() ← 刷新广播
0x09 = Repeated Attempts:SMP 规范(Spec Table 3.7)规定,短时间内多次配对失败后必须拒绝新的配对请求(防止暴力破解),此错误表示耳机触发了速率限制。
根因分析
iOS 内部 re-bond 机制(推测)
↓
配对成功 → iOS 立即断开(reason=0x13)
↓
iOS 内部旧 LTK 未正确更新或版本不一致
↓
重连时用旧 EDIV+Rand 发 LL_ENC_REQ
↓
耳机 NV 查到 LTK(可能是前一轮的)
↓
双方 SK 不同 → AES-CCM MAC 失败
↓
CNNE 0x16(MIC failure)
↓
iOS 再次触发 SMP Pairing
↓
多次重复后触发 Repeated Attempts (0x09)
↓
PARF 08(Unspecified)→ 最终连接失败
排查 checklist
| 检查项 | 预期(正常) | 异常信号 |
|---|---|---|
| SMP 后 iOS 是否断开 | CNNE 13 后无再连接 |
CNNE 13 后立即重连 → iOS re-bond |
| 重连加密 | ENCT ... 00(成功) |
CNNE ... 16 → MIC failure |
| 多次 SMP 后错误码 | 0x08(偶发) |
0x09(Repeated Attempts)→ 速率限制触发 |
| 广播恢复 | 失败后重新可见 | 广播未刷新 → iOS 扫描不到耳机 |
解决方案
- iOS 端 :
设置 → 蓝牙 → 忘记此设备,清除所有配对缓存,重新配对 - 耳机端:长按复位清除 NV 中的所有 BLE 配对记录
- 代码层面 :参见 8.3 方法 3 --- Peripheral 侧检测 MIC failure 后主动清除本地 LTK 并请求重配对
八、iOS SMP 配对失败后的恢复分析
场景:iOS 首次连接耳机,BLE SMP 配对失败,后续如何恢复并建立正常连接(含 CTKD)
8.1 配对失败时耳机端发生了什么
代码路径 app_custom.c:198-203:
c
if (err_code != 0) {
// 主动断开 BLE 连接
app_ble_disconnect(gap_zero_based_ble_conidx_as_hdl(conidx));
// 刷新广播,重新可被发现
app_ble_refresh_adv_state_generic();
}
SMP 配对失败后耳机自动执行:
- 主动断开 BLE 连接
- 重新刷新广播(重新进入可被扫描状态)
NV 状态 :SMP Phase 3(密钥分发)只在 Phase 2 认证成功后才执行,配对失败意味着 Phase 2 未完成,因此 NV 里不会写入任何 LTK / IRK 记录,耳机侧是干净的。
8.2 iOS 下次重连会发生什么
iOS 同样没有配对记录(配对失败),理论上下次连接时 iOS 应重新发起 SMP Pairing Request。
但 iOS 可能残留脏记录,表现为:
iOS 耳机 (Peripheral)
| |
|====== BLE 重新连接 ===================>|
| |
|====== iOS 认为已配对,发加密请求 =====|
|------ LL_ENC_REQ (脏 EDIV+Rand) ----->|
| nv_record_ble_record_find_ltk() | ← NV 查不到 LTK
| positive_reply = false |
|<----- LL_ENC_REJ (SMP_ERROR_ENABLE_ENC_FAILED)
| |
| ENCT 事件,error_code != 0 |
| is_central == false (耳机是 Peripheral)
| → 恢复逻辑不触发! | ← 代码 app_ble.c:1598 只处理 Central
| BLE_CONNECT_BOND_FAIL_EVENT |
| 连接断开 |
根源在 app_ble.c:1598:
c
// 仅对 Central(手机主动)做自动恢复,Peripheral(耳机)不处理
if (SMP_ERROR_ENABLE_ENC_FAILED == encrpt->error_code && conn->conn_flag.is_central == true)
{
bluetooth_nv_mgr_ble_record_del(BLE_NV_REC_DEL_LE_ENC_FAILURE, ...);
gap_start_authentication(conn->connhdl, GAP_AUTH_STARTED_BY_UPPER_APP);
}
// is_central == false 时:无任何恢复动作,直接上报 BOND_FAIL 并断开
8.3 恢复方法
方法 1:iOS 手动忘记设备(推荐)
iPhone → 设置 → 蓝牙 → 找到耳机 → 点"忘记此设备"
- iOS 清除自己的脏配对记录
- 重新扫描发现耳机
- 走完整 SMP 首次配对流程
- 适用场景:耳机 NV 干净,仅 iOS 侧有脏记录
方法 2:耳机出厂复位
- 触发耳机长按复位,清除所有配对 NV 记录
- 重新广播,iOS 重新发现并配对
- 适用场景:双端均可能有脏记录,彻底清干净
方法 3(代码层面):Peripheral 侧加自动恢复逻辑
当前代码只对 Central 做自动恢复,可在 Peripheral 分支补充:
c
// app_ble.c GAP_CONN_EVENT_ENCRYPTED 处理中
if (SMP_ERROR_ENABLE_ENC_FAILED == encrpt->error_code && conn->conn_flag.is_central == true)
{
// 已有逻辑:Central 自动恢复
bluetooth_nv_mgr_ble_record_del(BLE_NV_REC_DEL_LE_ENC_FAILURE, ...);
gap_start_authentication(conn->connhdl, GAP_AUTH_STARTED_BY_UPPER_APP);
}
else if (SMP_ERROR_ENABLE_ENC_FAILED == encrpt->error_code && conn->conn_flag.is_central == false)
{
// 新增:Peripheral 侧也清除失效记录,触发对端重新配对
bluetooth_nv_mgr_ble_record_del(BLE_NV_REC_DEL_LE_ENC_FAILURE, (uint8_t *)&conn->sec.peer_addr);
gap_start_authentication(conn->connhdl, GAP_AUTH_STARTED_BY_UPPER_APP);
}
注意:iOS 收到来自 Peripheral 的重配对请求行为因系统版本而异,可能弹对话框或直接接受,需实测验证。
8.4 CTKD 与 iOS 配对失败的关系
核心结论:
CTKD 的前提 = BLE SMP 配对成功
↓
iOS SMP 配对失败 = CTKD 无法进行
↓
且 iOS 本身不支持 CTKD(不分发 LinkKey PDU)
↓
iOS 的 BR/EDR 连接走独立 SSP 配对,与 BLE CTKD 无关
CTKD 完整成功的标志(必须全部出现):
| 步骤 | log 标志 | 说明 |
|---|---|---|
| SMP Phase 2 完成 | [SMP_PAIR_CMP] err=0x00 |
配对无错误 |
| 密钥分发收到 LinkKey | KEYD 含 GAP_RECV_DERIVED_BT_LINK_KEY |
仅 Android 会分发 |
| BLE LTK 派生完成 | RLTK ... wait=0 |
等待对端 IRK 完成 |
| BR/EDR 记录写入 NV | BT_NV_REC_ADD_CTKD_OVER_LE |
供经典蓝牙连接使用 |
iOS 的正常连接路径(不含 CTKD):
iOS BLE 配对成功(SMP SC, 无 LinkKey 分发)
↓
耳机存入 BLE LTK / IRK(BLE 侧)
↓
iOS 发起 BR/EDR 连接(HFP/A2DP)
↓
独立 SSP Numeric Comparison 或 Just Works 配对
↓
BR/EDR Link Key 独立存入 NV(与 BLE CTKD 无关)
九、蓝牙协议参考
9.1 Spec 章节索引
| 规范 | 相关章节 | 内容 |
|---|---|---|
| Core Spec 5.4 Vol 3 Part H | § 2 | SMP 完整规范 |
| Core Spec 5.4 Vol 3 Part H | § 2.2.6 | f4 函数:Commitment 生成(AES-CMAC) |
| Core Spec 5.4 Vol 3 Part H | § 2.2.7 | f6 函数:DHKey Check 验证(AES-CMAC) |
| Core Spec 5.4 Vol 3 Part H | § 2.2.8 | f5 函数:MacKey 和 LTK 派生(双 AES-CMAC) |
| Core Spec 5.4 Vol 3 Part H | § 2.3 | Pairing Methods(Just Works / Passkey / OOB / Numeric) |
| Core Spec 5.4 Vol 3 Part H | § 2.3.5.1 | Legacy Pairing:LTK 随机生成 + STK 保护分发 |
| Core Spec 5.4 Vol 3 Part H | § 2.3.5.6 | SC Pairing Phase 2:公钥交换 → DHKey → f5 派生 |
| Core Spec 5.4 Vol 3 Part H | § 2.4 | Cryptographic Toolbox(h6/h7/f4/f5/f6) |
| Core Spec 5.4 Vol 3 Part H | § 2.4.2.4 | CTKD 密钥派生算法(h6/h7 + LTK → Link Key) |
| Core Spec 5.4 Vol 3 Part H | § 3.6.11 | Pairing Request/Response PDU 格式 |
| Core Spec 5.4 Vol 2 Part E | § 7.7.65.29 | HCI LE Long Term Key Request Event |
| Core Spec 5.4 Vol 6 Part B | § 5.1.3 | LL Encryption Procedure(LL_ENC_REQ/RSP/START_ENC) |
| Core Spec 5.4 Vol 6 Part B | § 5.1.3.1 | SK = AES-128(LTK, SKD) 会话密钥推导 |
| Core Spec 5.4 Vol 3 Part H | Table 3.7 | SMP Error Code 表(包含 0x09 Repeated Attempts) |
9.2 关键错误码速查
| HCI/SMP 错误码 | 十六进制 | 含义 | 典型场景 |
|---|---|---|---|
BT_HCI_ERR_CONN_TERM_MIC_FAILURE |
0x3E(HCI),CO_LOG 16 |
AES-CCM MAC 验证失败,LTK 不匹配 | iOS/Android 脏 LTK 重连 |
BT_HCI_ERR_REMOTE_USER_TERM_CONN |
0x13,CO_LOG 13 |
对端主动断开 | iOS re-bond 后正常断开 |
SMP_ERROR_ENABLE_ENC_FAILED |
0x16(SMP 层,不同于 HCI 0x3E) |
LL 加密启动失败 | NV 无对应 LTK |
SMP Unspecified Reason |
0x08 |
未指定 SMP 错误 | 多种软件问题 |
SMP Repeated Attempts |
0x09 |
短时间内重复配对触发速率限制 | iOS 反复配对循环后 |
SMP DHKey Check Failed |
0x0B |
DHKey 验证(f6)失败 | MITM 攻击或密钥不匹配 |