最近调一个BLE外设的绑定逻辑,被一个看起来不起眼的报错折腾了大半天:在连接调试工具里发起配对,结果界面上直接弹出一行“bonding failed,reason:auth rejected”。表面上是调试工具被设备拒绝了,但往深了挖会发现,问题绝大多数不在调试工具这一侧,而在固件侧的安全参数配置上。这篇文章就把这个报错的完整排查思路捋一遍,从配对流程的底层逻辑到抓包定位,再到最终修复验证,希望能让正在被同样问题卡住的开发者少走弯路。
先说结论方向:auth rejected 并不是某个随机故障,它是BLE安全协议栈在配对协商阶段主动返回的认证失败结果。只要你理解配对参数协商的规则,这个报错是可以稳定复现、也能稳定解决的。下面我会按“现象确认 → 机制拆解 → 逐层排查 → 修复验证 → 避坑心得”的顺序来写,中间会穿插我在实际项目里遇到过的情况和实测数据,尽量做到每一步都可以直接照着操作。
1. 现象复盘:这个报错到底出现在哪一步
1.1 报错出现的前后现场
我用的调试工具是某芯片厂商旗下常见的BLE接入调试工具,流程一般是:扫描到设备→点击连接→连接成功后手动发起配对。正常情况下配对请求发出去后,调试工具会弹出一个配对确认窗口,或者直接进入just works流程,随后状态栏显示“bonded”,绑定记录里多出一行。问题场景下,配对请求发出不到一秒,工具就弹出失败提示,整个绑定流程被掐断。
有一个容易被忽略的细节:报错出现的时间点并不一定在刚连接那一刻。有的朋友是在连接建立之后,去读取某个特征值时才发现数据读不出来,日志里翻出来一大堆配对失败记录,再往前看才发现绑定流程早早就被拒绝了。也就是说,这个错误本质上是安全关联建立阶段的拒绝动作,不是数据读写阶段的偶发错误。
另外我还注意到一个现象:如果把设备断电重启,或者把调试工具里的绑定缓存删掉重试,第一次大概率还是失败,第二次碰运气能成。这种“时好时坏”的假象最容易误导人,实际上只是某些参数在重连时被跳过了,给你一种“修好了”的错觉。真正的原因是安全参数协商没有达成一致,和运气没有任何关系。
1.2 是外设拒绝,还是调试工具主动放弃
需要先明确一个方向性的问题:auth rejected到底是外设拒绝,还是调试工具这边主动放弃。
从BLE协议栈的视角来看,central(主设备)发起配对时,会把它的安全参数通过Pairing Request发出去,peripheral(从设备)收到后,会拿自己配置的安全参数去对比:对端是否要求MITM、两边加密模式是否兼容、密钥大小是否在允许范围、希望绑定还是临时配对等等。其中任何一项对不上,协议栈就会在底层直接返回一个安全状态错误,最终展示出来就是“auth rejected”。
我一开始也以为是调试工具太挑剔,后来去查协议栈日志才发现,外设固件里压根没有配置对应的认证回调。换句话说,调试工具只是把我们的意图原样发出去,最终接不接受是由外设固件里的安全策略决定的。搞清楚了这个责任边界,排查方向才能定在固件侧,而不是拿着手机一遍一遍删除重连。
2. 绑定流程的底层拆解:auth rejected是谁拒绝了谁
2.1 配对和绑定到底是不是一回事
很多同学会把配对(pairing)和绑定(bonding)混在一起,但这两个概念在BLE里完全是两件事,而且这个报错恰好就发生在它们的边界上。
配对是指两台设备临时建立加密链路的过程,目的是在当前连接里保证通信安全;绑定则是在配对成功之后,把交换出来或生成出来的长期密钥(LTK)等数据存储下来,下次重连时直接使用这个密钥完成加密,不需要再走一遍完整的配对协商。调试工具提示“bonding failed”,其实是说配对协商已经走到了某个阶段,但在密钥分发、确认这个环节被中断了。
一个非常常见的错误配置就是:central在Pairing Request里明确要求bond,而peripheral的固件里把bond字段写成了0,只允许临时配对。两边对“要不要长期保存密钥”的态度不一致,协议栈在协商阶段就会认为安全策略冲突,直接返回失败。所以当你看到bonding failed时,第一个要检查的不是加密方式,而是bond标志位是否对齐。
2.2 安全模式和安全等级如何影响认证结果
BLE规范里定义了多种安全模式,实际开发中最常用的就是Security Mode 1,它下面又分了四个等级:
- Level 1:无加密,明文传输;
- Level 2:未认证的加密,适合没有输入输出能力的设备;
- Level 3:带MITM认证的加密,需要能力匹配的认证方式;
- Level 4:在LE Secure Connections下带MITM认证的加密,安全性最高。
很多量产固件为了省事,只配置了Level 2,也就是“只要加密就行,不校验身份”。如果调试工具或手机端用Level 3或Level 4的要求去发起配对,外设协议栈一看“你要的等级超出了我允许的范围”,就会回一个安全错误。这个错误在界面上表现出来的,就是auth rejected。
反过来也有一种情况:外设固件要求Level 3,但central侧使用的加密方式和认证能力不支持,同样会协商失败。所以等级本身没有对错,关键是两边要对齐,或者说peripheral要明确知道自己能接受的上限是多少。
2.3 IO capabilities、MITM、OOB是怎么配合的
这一块是最容易踩坑的,而且踩坑之后定位也最难,因为表面上看参数似乎都“没什么问题”。
BLE的认证方式由三个要素共同决定:两端的IO能力(显示屏、按键、键盘、无输入输出)、是否要求MITM保护、以及是否使用OOB数据。最常见的两种结果是Just Works和Passkey Entry。如果双方都希望有MITM保护,但IO能力组合无法支撑Passkey或OOB,协议栈就会在认证阶段无法达成一致,最终只能拒绝。
举个我实际遇到过的例子:某外设硬件上有一颗按钮,固件里却把IO capability配置成了NoInputNoOutput,同时又在安全参数里要求MITM。没有输入输出能力,还要中间人保护,这本身就是矛盾配置。如果对端也比较严格,请求被拒几乎是必然的。后来把IO能力改成KeyboardOnly,让按键触发配对确认,问题立刻消失。
还需要注意LESC和legacy pairing的差异。LESC(LE Secure Connections)使用椭圆曲线加密,legacy使用AES-128加密,两套机制并不能无缝兼容。如果外设固件只开启了LESC,而调试工具或手机端用legacy方式发起配对,协商阶段很可能会因为加密方式不匹配而拒绝认证。
3. 排查链路:从抓包到参数逐层定位(实战部分)
3.1 先用工具日志确认失败发生在配对流程的哪个子状态
BLE配对流程大体可以分成三个阶段:第一阶段是Pairing Feature Exchange,双方交换能力与安全参数;第二阶段是认证阶段,根据协商结果进行Just Works或者Passkey Entry;第三阶段是密钥分发,把LTK等密钥数据落地存储。auth rejected可能出现在第一和第二阶段之间,也可能出现在第二阶段内部。
我排查这个报错时,首选方案是开启协议栈的详细日志,或者用带日志功能的BLE抓包器在空中抓Pairing Request和Pairing Response两个包。重点看两个字段:
- AuthReq字段,低bit位表示是否要求MITM,对应安全等级是Level 3还是Level 2;
- 加密方式相关标志位,判断是否启用了LESC。
有一次我对比两个包里的AuthReq,发现central发出的请求里MITM位是1,外设返回的响应里MITM位却是0,这就非常清晰地说明外设并不接受MITM要求,核心矛盾一目了然。如果抓包条件不具备,也可以用两端协议栈日志里的错误码辅助判断,只是效率略低一些。
3.2 逐个核对外设侧的关键安全参数
当抓包确认失败点在外设侧时,我会把外设固件里的安全参数结构逐个过一遍。不同芯片SDK的字段名有差异,但核心含义差不多,主要包含以下几项。拿某BLE芯片厂商SDK里常见的参数结构举例:
static const ble_gap_sec_params_t sec_params = { .bond = 1, .mitm = 0, .lesc = 1, .io_caps = BLE_GAP_IO_CAPS_NONE, .min_key_size = 7, .max_key_size = 16, .kdist_own = ..., .kdist_peer = ..., };逐个说说我踩过的坑:
- bond字段:如果central请求绑定而这里写0,必然失败;
- mitm字段:要看真实硬件能力,没有输入输出设备却要求MITM,基本必挂;
- io_caps字段:很多人默认用NONE,实际上设备有按键就应该填KeyboardOnly;
- min_key_size和max_key_size:合法范围是7到16,如果写得太小,或两端范围没有交集,也会被拒;
- lesc字段:需要确认对端是否支持,老设备不支持LESC时,协商会被卡住。
很多看似无解的问题,最后定位都是这些字段的组合不成立,而不是某个单独字段写错。
3.3 调试工具侧的安全策略别只看表面
调试工具这边的排查要小心一个陷阱:界面显示的安全等级不等于它实际发送的安全参数。某次我把工具设置成“High Security”后发起配对,日志里却发现工具发出的AuthReq既没带MITM,也没带LESC,相当于还是按照最低安全级别去请求的。界面和实际行为不一致的情况确实存在。
建议以工具日志里打印的Pairing Request原始数据为准,不要只看界面上那个安全等级图标。另外,手机系统作为central时也有自己的一套策略,例如某些系统版本不允许和不带MITM的外设做持久化绑定。如果你确认外设一切配置正常,却只在手机上失败,在调试工具上能通过,那就要考虑是不是系统侧的安全策略太严格。
3.4 Passkey和Just Works的匹配判断
这里有一个可以快速自测的判断逻辑。如果外设固件希望走Passkey Entry认证,那它必须有能力输入或者输出六位PIN码。具体来说:
- 有显示屏的,可以显示PIN,能力是DisplayOnly;
- 有键盘的,可以输入PIN,能力是KeyboardOnly;
- 两者都有,能力是DisplayYesNo或KeyboardDisplay;
- 什么都没有,只能走Just Works。
如果你给一个没有任何交互界面的传感器配置了Passkey要求,那协议栈根本无法完成PIN的展示或输入,只能尝试往Just Works降级。如果对端策略不允许Just Works,结果就是认证被拒。我的建议是,硬件没有交互能力时,老老实实使用Level 2或者Just Works,不要在固件里强行要求MITM,否则不仅产品体验奇怪,问题还极难排查。
3.5 错误码和协议栈回调的对应关系
大部分BLE协议栈在认证状态变化时都会触发一个回调,比如有的SDK叫on_sec_status_changed,有的叫on_authentication_status。回调参数里会带一个状态值,这个值比界面提示更有参考意义。我在不同工程里遇到过的几种典型值整理如下:
| 状态码 | 常见含义 | 排查方向 |
|---|---|---|
| 0x04 | Authentication failed | 检查MITM与IO能力配置 |
| 0x05 | Passkey entry failed | 检查PIN输入是否超时或错误 |
| 0x06 | OOB not available | 检查OOB数据是否正确传递 |
| 0x09 | Repeated attempts | 检查是否短时间内重复触发配对 |
| 0x0B | Invalid parameters | 检查安全参数结构内容 |
auth rejected一般对应0x04这个级别,但不要止步于查表,一定要回到3.1小节的抓包对比去确认,因为协议栈在某些情况下会把略有差异的底层错误统一映射成“认证失败”。只看错误码、不看原始包,很容易把方向带偏。
4. 修复方案与验证:让绑定真正落盘
4.1 方案A:降低安全等级,先保证能绑定
如果业务对中间人攻击的防护没有硬性要求,最简单的做法是把外设安全策略降到Level 2:加密但不要求MITM。这样无论是调试工具还是手机,只要发起普通的legacy pairing,基本都能配对成功并完成绑定。
我遇到过一款数据采集外设,业务场景是近距离低频传输,数据本身不敏感,也没有在线支付级别的安全需求。这种case上Level 2完全够用,开发成本最低,兼容性也最好。但我通常会用一条硬性标准来判断能不能这么干:传输的数据里有没有用户的隐私内容、有没有可被远程利用的指令通道。只要有,Level 2就得慎重。
4.2 方案B:保留MITM保护,但补上IO能力配置
对于必须走Level 3或Level 4的产品,问题往往出在“想安全但硬件能力没跟上”。以我之前做的一个带实体按键的门锁配件为例,硬件上有按钮,但固件初始化时io_caps填的是无输入输出,导致MITM协商失败。改成KeyboardOnly之后,配对时按键触发确认,整个bonding流程就很顺畅。
改完之后不要忘了处理交互逻辑:至少在配对进行期间,让按键事件及时上报给协议栈,完成用户确认动作,否则会出现看得到配对请求、但按了没反应的假故障。另外,显示屏的产品记得确认PIN码显示的超时时间,太短会导致用户还没输完就被判失败。
4.3 验证步骤:删干净再连,别只测一次
修复之后,验证环节我有一套固定流程,照做基本能排除偶然因素:
- 恢复设备出厂绑定状态,删掉外设侧所有已存的配对记录;
- 在调试工具或手机端删除这台设备的绑定缓存;
- 重新发起一次完整配对,确认提示已绑定(bonded);
- 断开连接,再重新连接,确认此时不需要重新配对,直接就进入加密状态;
- 再次删除绑定,重复2到4步至少三轮。
这个流程里最关键的是第4步。如果断开重连后,对端又重新弹出了配对请求,说明绑定的密钥并没有真正落盘。这种情况多半是bond字段或密钥存储模块没有正确初始化,而不是配对流程本身的问题。我见过有人“修好”一次配对就以为大功告成,结果客户重新上电后问题复现,差的正是这一步验证。
4.4 不同加密方式下的密钥存储注意事项
当使用LESC时,绑定成功后存储的是LTK、EDIV和RAND等参数;使用legacy pairing时,除了这些还需要关注CSRK等签名密钥。设备重新上电后,如果协议栈的flash存储模块没有正确读写这些参数,绑定记录就会在重启后消失,表现为“配对时好,重启就没”。
我通常会在验证清单里加一条:配对成功并绑定后,彻底断电,再上电重连,确认绑定关系仍然有效。这一条能同时验证存储模块、初始化顺序和绑定记录恢复逻辑,比连续多次连接更能说明问题。
5. 避坑心得:这类问题里最常见的三种“假修复”
5.1 复制示例工程参数,等于把坑也复制过来了
我发现很多开发者遇到这个问题,第一反应不是查参数,而是翻官方的示例工程,把示例里的安全参数原样抄过来。示例工程为了演示便利,通常会把MITM关掉、IO能力设为无输入输出、兼容性拉满。这在开发板上跑通很容易,但一旦产品加了敏感服务特征值,或者客户设备要求更高的安全等级,原样抄来的参数就会立刻变成一根绊线。
正确做法是把示例工程当参考,而不是当答案。每个产品都要结合自己硬件的交互能力和业务安全需求,独立地推导出一套合理的安全参数组合。
5.2 “改一个字段”通常解决不了协商类问题
auth rejected这类错误往往不是单点故障,而是两端参数组合之后的不匹配。我摸索出来的排查顺序是:先抓包确认是哪个方向拒绝,再对比AuthReq字段,然后逐个核对本端参数,最后才考虑改配置。如果跳过抓包直接改代码,很容易陷入“改了A字段,发现B字段又报错”的循环。
5.3 风险用例越接近真实场景越容易发现问题
我习惯保留一个最小可复现工程:只包含一个基础服务、一个需要加密读取的特征值和一套最精简的安全回调,其它业务代码全部砍掉。这样在排查auth rejected时可以快速排除业务代码干扰。基于我的经验,这套组合思路配合抓包数据,通常在半小时内就能定位问题根因,而不是靠盲猜。
最后分享一个我在实际项目中确认过的经验:遇到这类认证类报错,不要急着把问题归咎于调试工具或手机系统。先问自己三个问题:我的外设能支撑哪种认证方式?我的安全等级配置是不是和硬件能力自相矛盾?我有没有足够的数据证明是对方拒绝,还是自己在拒绝?把这三个问题想清楚,这个报错的答案其实已经在自己手里了。