绑定与白名单
使用技术:SLE 绑定(Bonding)、NV 持久化、白名单过滤
前置阅读:Just Works 配对 — 需要先理解配对流程和
sle_pair_remote_device()
学习目标
- 理解绑定与配对的区别——配对建立临时加密,绑定将密钥永久保存到 NV
- 掌握绑定使能的完整流程:
sle_pair_remote_device()→auth_complete_cb中确认is_bond == SLE_PAIR_NEED_BOND→ 协议栈自动写入 NV - 掌握白名单过滤模式——只允许已绑定设备连接,拒绝陌生设备
- 理解已绑定设备自动重连的流程——无需重新配对,从 NV 恢复密钥即可通信
- 能够实现绑定设备管理——查询绑定列表、删除过期绑定、处理超出容量
基本概念
典型使用场景
产品化设备需要"一次配对,永久信任"的安全模型:
| 场景 | 安全需求 | 绑定 + 白名单的作用 |
|---|---|---|
| 智能门锁 | 只允许下载过 App 的手机开门 | 首次配对绑定手机,之后拒绝所有陌生连接 |
| 医疗传感器 | 专属接收器不可被替换 | 绑定出厂配置的接收器,防止恶意设备窃取健康数据 |
| 车载诊断 | 仅授权诊断仪可读取车辆数据 | 绑定 4S 店诊断仪,防止 OBD 端口被他人滥用 |
| 工业传感器网 | 网关只对接本厂传感器 | 白名单过滤避免跨厂设备误连产生垃圾数据 |
绑定 vs 配对
配对和绑定是两个密切相关但目的不同的操作:
| 配对 | 绑定 | |
|---|---|---|
| 操作 | 交换密钥,建立加密链路 | 将配对生成的密钥存储到 NV Flash |
| 触发 | sle_pair_remote_device() |
SDK 自动——配对时检测到 is_bond = SLE_PAIR_NEED_BOND |
| 有效期 | 本次连接有效,断开后密钥丢失 | 永久有效,断电重启保持 |
| 重连行为 | 每次断开都需重新配对 | 重连时从 NV 恢复密钥,跳过配对步骤 |
| NV 占用 | 0 | 约 256 字节 / 设备 |
| 标志位 | 无需特殊配置 | SLE_PAIR_NEED_BOND (值为 0x01) |
sequenceDiagram
participant C as Client
participant S as Server
participant NV as NV Flash
Note over C,S: 首次连接 —— 配对 + 绑定
C->>S: sle_pair_remote_device
Note over C,S: 密钥交换
Note over C: pair_complete_cb
Note over S: pair_complete_cb
C->>NV: is_bond=1,写 LTK
S->>NV: is_bond=1,写 LTK
Note over C: auth_complete_cb
C--xS: 断开连接
Note over NV: LTK 仍在 NV 中
Note over C,S: 第二次连接 —— 自动恢复加密
C->>S: sle_connect_remote_device
C->>NV: 读取 LTK
S->>NV: 读取 LTK
Note over C,S: 跳过配对,直接恢复加密
Note over C: pair_complete_cb status=0
关键理解:绑定是配对的持久化。没有绑定,每次重连都要重新配对,既浪费功耗又增加了被中间人攻击的窗口期——攻击者可在断开瞬间伪装成合法设备发起新配对。
白名单机制
SLE 的白名单工作在两个层面:
| 层面 | 配置位置 | 过滤对象 | 效果 |
|---|---|---|---|
| 广播白名单 | announce_level = SLE_ANNOUNCE_LEVEL_SPECIAL |
扫描请求 / 连接请求 | 仅白名单设备能发现和连接,陌生设备扫描到但无法交互 |
| 扫描白名单 | seek_filter_policy = SLE_SEEK_FILTER_ALLOW_WLST |
扫描结果 | 只上报白名单内设备的广播,忽略陌生广播 |
广播白名单的核心价值:门锁在上电后虽然广播,但不在白名单内的手机即使扫到了门锁的信号,连接请求也会被协议栈在底层拒绝——应用层代码根本不会收到连接回调。这比在应用层判断"是谁连进来了"更安全,因为恶意设备连连接都建立不了。
扫描白名单的用途:Client 端只关心曾经绑定过的设备。开启扫描白名单后,seek_result_cb 只上报已绑定设备的广播包,省去了在应用层过滤连接的成本。
绑定白名单通信流程
sequenceDiagram
participant C as Client 手机
participant S as Server 门锁
participant NV as NV Flash
Note over S: announce_level=SPECIAL
Note over S: 白名单初始为空
Note over C,S: ═══ 首次配对绑定 ═══
C->>S: 扫描 → 连接 → sle_pair_remote_device
Note over C,S: Just Works 密钥交换
Note over C: pair_complete_cb status=0
Note over S: pair_complete_cb status=0
S->>NV: 写 LTK + 设备地址到白名单
C->>NV: 写 LTK
C--xS: 断开连接
Note over S: 白名单已有手机地址
Note over C,S: ═══ 陌生设备尝试连接 ═══
Note over S: 陌生设备扫描到门锁广播
S-->>S: 检查白名单:地址不匹配
Note over S: 协议栈拒绝连接请求
Note over S: 应用层无感知
Note over C,S: ═══ 已绑定手机重连 ═══
C->>S: sle_connect_remote_device
S-->>S: 检查白名单:地址匹配
S->>C: 连接接受
S->>NV: 读 LTK
C->>NV: 读 LTK
Note over C,S: 恢复加密,跳过配对
Note over C: pair_complete_cb status=0
涉及 API
| API | 谁调用 | 用途 |
|---|---|---|
sle_pair_remote_device(&addr) |
Client | 发起配对(绑定标志在 auth_complete_cb 的 auth_info.is_bond 中由协议栈回传) |
sle_get_bonded_devices(&addr, &num) |
双方 | 获取已绑定设备地址列表(仅返回绑定设备,不包含仅配对的设备) |
sle_get_paired_devices_num(&num) |
双方 | 查询已配对/绑定设备总数 |
sle_remove_paired_remote_device(&addr) |
双方 | 删除指定设备的绑定信息(清除 NV 中的密钥,异步操作,结果在 pair_remove_cb 返回) |
sle_remove_all_pairs() |
双方 | 删除所有绑定(同步操作,清空 NV 中全部密钥) |
sle_connection_register_callbacks() |
双方 | 注册回调,含 pair_complete_cb、auth_complete_cb、pair_remove_cb |
sle_set_announce_param() |
Server | 配置广播参数,通过 announce_level 启用白名单过滤 |
案例说明
做什么
演示完整的绑定-白名单-重连生命周期,覆盖五个阶段:
- 首次配对 + 绑定:Client 连接后发起配对,
auth_complete_cb中确认is_bond == SLE_PAIR_NEED_BOND,LTK 写入 NV - 断开后自动重连:Client 主动断开 → 重新扫描 → 重新连接 →
pair_complete_cb直接返回 SUCCESS(密钥从 NV 恢复,无需重新配对) - 白名单过滤:Server 将
announce_level设为SLE_ANNOUNCE_LEVEL_SPECIAL→ 陌生设备无法连接 - 断电重启后仍可重连:双方断电重启 → Client 重新连接 → NV 中的 LTK 仍然有效 → 自动恢复加密
- 删除绑定后需重新配对:调用
sle_remove_paired_remote_device()清除 NV → 下次该设备连接时pair_state == SLE_PAIR_NONE→ 必须重新配对
绑定 vs 无绑定对比
| 维度 | 无绑定(仅配对) | 有绑定 |
|---|---|---|
| 重连体验 | 每次断开都需重新配对(增加 1~2 秒延迟) | 重连直接恢复加密(< 50ms) |
| 断电恢复 | 断电后密钥丢失,必须重新配对 | 断电重启后 LTK 仍在 NV 中 |
| 安全风险 | 断开瞬间攻击者可伪装配对 | 只接受已绑定设备,无法伪造 |
| NV 占用 | 0 | ~256 B / 设备 |
| 设备容量 | 无限制(无持久化) | 最多 8 个绑定设备 |
| 适用场景 | 开发调试、临时连接 | 产品化部署、安全需求 |
案例操作指导
第一步:编译 Server 固件
这等于设置了 CONFIG_SAMPLE_SUPPORT_SLE_UART_SERVER_SAMPLE=y。
本案例以 UART Bridge 示例为基础进行修改——因为它已包含完整的连接和配对回调链。
第二步:编译 Client 固件
同上,改为启用 Client:
第三步:烧录
将 Server 固件烧录到板子 A,Client 固件烧录到板子 B。
第四步:上电验证首次绑定
先给 Server 上电,预期输出:
[uart server] init ok
[uart server] start announce success, announce_level=SPECIAL
[uart server] waiting for bonded device...
再给 Client 上电,预期输出:
[uart client] init...
[uart client] start seek...
[uart client] found uart_server, connecting...
[uart client] connected, conn_id=0x01, pair_state=NONE
[uart client] start pairing...
[uart client] pair complete, status=0
[uart client] auth complete: crypto_algo=1, is_bond=1
[uart client] keys saved to NV
第五步:验证重连(无需配对)
断开 Client 电源再上电,或按复位键:
[uart client] connected, conn_id=0x01, pair_state=PAIRED
[uart client] pair complete, status=0 ← 直接从 NV 恢复
注意:第二次 pair_state 不再是 NONE,而是 SLE_PAIR_PAIRED——协议栈已从 NV 读取到密钥。
pair_complete_cb仍然会触发(status=0),但实际的密钥交换过程被跳过了——协议栈发现 NV 中已有有效密钥,直接恢复加密。
第六步:验证白名单过滤
准备第三块板子烧录一个没有绑定记录的 Client。它也能扫描到 Server,但连接会被拒绝:
[unknown client] found uart_server, connecting...
[unknown client] connect failed / disconnected, reason=... ← 协议栈拒绝
Server 端不会产生任何输出——连接在协议栈底层被拦截,应用层无感知。
第七步:验证删除绑定
在 Client 端调用 sle_remove_paired_remote_device(&g_remote_addr) 后再重连:
[uart client] pair_remove_cb: status=0
[uart client] reconnecting...
[uart client] connected, conn_id=0x01, pair_state=NONE ← 密钥已被清除
[uart client] start pairing... ← 需要重新配对
关键配置
绑定标志:auth_info_evt_t 结构体
绑定信息在配对完成后的认证事件中携带:
typedef struct {
uint8_t link_key[SLE_LINK_KEY_LEN]; // 16 字节链路密钥
uint8_t crypto_algo; // 加密算法: SLE_CRYTO_ALGO_AC1/AC2/EA1/EA2
uint8_t key_deriv_algo; // 密钥派生: SLE_KEY_DERIV_ALGO_HA1/HA2
uint8_t integr_chk_ind; // 完整性校验指示
uint8_t is_bond; // 绑定标志: SLE_PAIR_NO_BOND(0) / SLE_PAIR_NEED_BOND(1)
} sle_auth_info_evt_t;
is_bond 字段由协议栈在配对过程中自动设置——应用层无需也不能手动设置它。应用层只需在 auth_complete_cb 中读取它来确认绑定是否生效。
白名单配置:广播参数中的过滤
Server 通过 announce_level 控制白名单行为:
sle_announce_param_t param = {0};
/* ... 其他参数 ... */
/* 发现等级:SPECIAL = 仅白名单设备可发现和连接。
为什么选 SPECIAL?因为门锁类产品只允许曾经绑定过的手机连接。
如果设为 NORMAL,任何设备都能连接——这对安全产品是不允许的。 */
param.announce_level = SLE_ANNOUNCE_LEVEL_SPECIAL;
announce_level 取值 |
可发现性 | 可连接性 | 适用场景 |
|---|---|---|---|
SLE_ANNOUNCE_LEVEL_NORMAL |
任何设备 | 任何设备 | 开发调试、公共服务 |
SLE_ANNOUNCE_LEVEL_SPECIAL |
仅白名单 | 仅白名单 | 门锁、医疗、车联网 |
Client 端通过 seek_filter_policy 控制扫描白名单:
sle_seek_param_t seek_param = {0};
/* ... 其他参数 ... */
/* 扫描过滤策略:ALLOW_WLST = 只上报白名单设备的广播。
为什么用这个?网关只需要找到曾经绑定过的传感器,
忽略周边其他无关设备,减少 seek_result_cb 的触发次数。 */
seek_param.seek_filter_policy = SLE_SEEK_FILTER_ALLOW_WLST;
NV 存储与容量限制
| 参数 | 值 | 说明 |
|---|---|---|
| 每设备 NV 占用 | ~256 字节 | 包含 LTK、设备地址、密钥派生参数 |
| 最大绑定数 | 8 | 超过后新绑定将失败 |
| 密钥有效期 | 永久 | 除非手动删除或 NV 被擦除 |
| NV 区域 | SLE_NV_TAG_PAIRED_ADDR |
SDK 内部管理,应用层无需关心 |
代码详解
1. 配对时启用绑定
绑定不是应用层主动开启的——它在 auth_complete_cb 中"被动确认"。协议栈在配对协商时自动决定是否绑定,应用层只需在回调中记录结果:
static void auth_complete_cb(uint16_t conn_id,
const sle_addr_t *addr,
errcode_t status,
const sle_auth_info_evt_t *info)
{
if (status != ERRCODE_SUCC) {
printf("[app] auth failed: %d\n", status);
return;
}
printf("[app] auth complete: algo=%d, key_deriv=%d, bond=%d\n",
info->crypto_algo, info->key_deriv_algo, info->is_bond);
if (info->is_bond == SLE_PAIR_NEED_BOND) {
printf("[app] keys saved to NV, reconnect without pairing\n");
g_is_bonded = true;
} else {
printf("[app] no bonding, will need to pair again after disconnect\n");
}
}
绑定能力的开关在协议栈初始化阶段由 SDK 配置决定,不在应用层回调中。不绑定不一定是你的代码写错了——SDK 可能默认关闭了绑定功能。检查
sle_common.h中的绑定相关宏。
2. 绑定设备重连
已绑定设备重连时,connect_state_changed_cb 中的 pair_state 是关键判断依据:
static void connect_state_changed_cb(uint16_t conn_id,
const sle_addr_t *addr,
sle_acb_state_t conn_state,
sle_pair_state_t pair_state,
sle_disc_reason_t disc_reason)
{
if (conn_state == SLE_ACB_STATE_CONNECTED) {
g_conn_id = conn_id;
memcpy(&g_remote_addr, addr, sizeof(sle_addr_t));
if (pair_state == SLE_PAIR_NONE) {
// 未配对——需要发起配对(可能是首次连接或绑定被清除)
printf("[app] pair_state=NONE, starting pairing...\n");
sle_pair_remote_device(addr);
} else {
// pair_state != NONE ——密钥已从 NV 恢复
// pair_complete_cb 仍会触发,status=0
printf("[app] pair_state=%d, keys restored from NV\n", pair_state);
}
} else if (conn_state == SLE_ACB_STATE_DISCONNECTED) {
printf("[app] disconnected, reason=%d\n", disc_reason);
if (disc_reason == SLE_DISC_REASON_AUTH_FAILED) {
// NV 中密钥可能已过期或被覆盖——清理后重新配对
sle_remove_paired_remote_device(&g_remote_addr);
}
g_conn_id = 0;
sle_start_seek(); // 恢复扫描,等待重新连接
}
}
3. 白名单过滤配置
Server 端在广播前设置 announce_level 为 SLE_ANNOUNCE_LEVEL_SPECIAL:
static errcode_t sle_server_start_with_whitelist(void)
{
sle_announce_param_t param = {0};
param.announce_mode = SLE_ANNOUNCE_MODE_CONNECTABLE_SCANABLE;
param.announce_handle = 1;
param.announce_gt_role = SLE_ANNOUNCE_ROLE_T_CAN_NEGO;
/* 白名单过滤的关键配置 */
param.announce_level = SLE_ANNOUNCE_LEVEL_SPECIAL;
param.announce_channel_map = SLE_ADV_CHANNEL_MAP_DEFAULT;
param.announce_interval_min = 0xC8;
param.announce_interval_max = 0xC8;
param.conn_interval_min = 0x64;
param.conn_interval_max = 0x64;
param.conn_max_latency = 0;
param.conn_supervision_timeout = 0x1F4;
param.announce_tx_power = 18;
errcode_t ret = sle_set_announce_param(1, ¶m);
if (ret != ERRCODE_SUCC) {
return ret;
}
return sle_start_announce(1);
}
SLE_ANNOUNCE_LEVEL_SPECIAL生效后,协议栈在收到连接请求时首先检查对端地址是否在白名单中。不在白名单中的连接请求不会传递到应用层的connect_state_changed_cb——这就是为什么"陌生设备连不上"。
4. 绑定设备管理
当绑定设备数量达到上限(8 个)时,需要清理旧绑定:
static errcode_t manage_bonded_devices(void)
{
uint16_t device_num = 0;
sle_addr_t addr_list[8] = {0};
// 获取已绑定设备列表
errcode_t ret = sle_get_bonded_devices(addr_list, &device_num);
if (ret != ERRCODE_SUCC) {
return ret;
}
printf("[app] bonded devices: %d\n", device_num);
if (device_num >= 8) {
// 已满——删除最早绑定的设备(索引 0)
printf("[app] bonded list full, removing oldest...\n");
ret = sle_remove_paired_remote_device(&addr_list[0]);
// 注意:删除是异步操作,结果在 pair_remove_cb 中返回
}
return ret;
}
删除绑定的结果通过 pair_remove_cb 异步返回:
static void pair_remove_cb(const sle_addr_t *addr, errcode_t status)
{
if (status == ERRCODE_SUCC) {
printf("[app] pair removed successfully\n");
// 此时可以允许新设备配对绑定了
} else {
printf("[app] pair remove failed: %d\n", status);
}
}
// 注册回调
static sle_connection_callbacks_t g_conn_callbacks = {
.connect_state_changed_cb = connect_state_changed_cb,
.pair_complete_cb = pair_complete_cb,
.auth_complete_cb = auth_complete_cb,
.pair_remove_cb = pair_remove_cb, // 删除绑定结果回调
};
5. 删除绑定后的行为
删除绑定后,NV 中的密钥被清除。下次该设备连接时必须重新配对:
// 删除指定绑定
sle_remove_paired_remote_device(&target_addr);
// 等待 pair_remove_cb 确认删除成功后...
// 删除所有绑定(如恢复出厂设置)
sle_remove_all_pairs(); // 同步操作,立即返回
// 下次该设备重连时:
// connect_state_changed_cb 中 pair_state == SLE_PAIR_NONE
// → 必须调用 sle_pair_remote_device() 重新配对
// → 重新协商密钥 → 重新写入 NV
常见问题排查
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
auth_complete_cb 中 is_bond=0 |
SDK 配置关闭了绑定功能 | 检查 sle_common.h 中的绑定相关 Kconfig |
| 重连后仍需配对 | NV 中密钥被擦除或 NV 分区损坏 | 检查 sle_get_paired_devices_num() 返回值是否为 0 |
| 白名单不生效 | announce_level 未设为 SPECIAL |
确认 sle_set_announce_param() 传入的 announce_level 值 |
| 陌生设备仍能连接 | 白名单初始为空时行为同 NORMAL |
至少完成一次绑定后白名单才生效 |
sle_get_bonded_devices() 返回 0 |
get_paired 返回包括配对在内,get_bonded 只返回有绑定的 |
确认配对时确实启用了绑定 |
| 绑定超过 8 个后新绑定失败 | NV 容量上限 | 调用 sle_remove_paired_remote_device() 清理旧绑定 |