跳转至

绑定与白名单

使用技术: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_cbauth_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_cbauth_complete_cbpair_remove_cb
sle_set_announce_param() Server 配置广播参数,通过 announce_level 启用白名单过滤

案例说明

做什么

演示完整的绑定-白名单-重连生命周期,覆盖五个阶段:

  1. 首次配对 + 绑定:Client 连接后发起配对,auth_complete_cb 中确认 is_bond == SLE_PAIR_NEED_BOND,LTK 写入 NV
  2. 断开后自动重连:Client 主动断开 → 重新扫描 → 重新连接 → pair_complete_cb 直接返回 SUCCESS(密钥从 NV 恢复,无需重新配对)
  3. 白名单过滤:Server 将 announce_level 设为 SLE_ANNOUNCE_LEVEL_SPECIAL → 陌生设备无法连接
  4. 断电重启后仍可重连:双方断电重启 → Client 重新连接 → NV 中的 LTK 仍然有效 → 自动恢复加密
  5. 删除绑定后需重新配对:调用 sle_remove_paired_remote_device() 清除 NV → 下次该设备连接时 pair_state == SLE_PAIR_NONE → 必须重新配对

绑定 vs 无绑定对比

维度 无绑定(仅配对) 有绑定
重连体验 每次断开都需重新配对(增加 1~2 秒延迟) 重连直接恢复加密(< 50ms)
断电恢复 断电后密钥丢失,必须重新配对 断电重启后 LTK 仍在 NV 中
安全风险 断开瞬间攻击者可伪装配对 只接受已绑定设备,无法伪造
NV 占用 0 ~256 B / 设备
设备容量 无限制(无持久化) 最多 8 个绑定设备
适用场景 开发调试、临时连接 产品化部署、安全需求

案例操作指导

第一步:编译 Server 固件

Top → Application → Samples → BT → SLE → SLE UART → [*] SLE UART Server Sample

这等于设置了 CONFIG_SAMPLE_SUPPORT_SLE_UART_SERVER_SAMPLE=y

本案例以 UART Bridge 示例为基础进行修改——因为它已包含完整的连接和配对回调链。

fbb build ws63-liteos-app -p menuconfig
fbb build ws63-liteos-app

第二步:编译 Client 固件

同上,改为启用 Client:

Top → Application → Samples → BT → SLE → SLE UART → [*] SLE UART Client Sample

第三步:烧录

将 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_levelSLE_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, &param);
    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_cbis_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() 清理旧绑定