feat(ble/bluedroid): reject LE re-pairing that weakens an existing bond

Add smp_repairing_is_allowed() behind BT_BLE_SMP_HARDENED_REPAIRING so a
peer cannot replace an existing bond with one that has less MITM
protection, no Secure Connections, or a shorter key. Compare a preceding
Security Request against the pairing command AuthReq, not the
association-model result, and always allow first pairing.

A refusal keeps the stored bond. Pairing-failure erase is split by link
role: default is erase as Central and keep as Peripheral.

Closes BLERP (NDSS 2026) V3, V4 and V6.
This commit is contained in:
zhiweijian
2026-08-21 19:17:48 +08:00
committed by BOT
parent f864615d7d
commit 88ea45be73
11 changed files with 368 additions and 16 deletions
@@ -1817,6 +1817,9 @@ typedef struct {
BOOLEAN is_pair_cancel;
BOOLEAN smp_over_br;
tSMP_AUTH_REQ auth_mode;
/* Mirrors tSMP_CMPL.keep_bond; layouts must stay identical because
btm_proc_smp_cback casts tSMP_EVT_DATA to tBTM_LE_EVT_DATA. */
BOOLEAN keep_bond;
} tBTM_LE_COMPLT;
#endif
@@ -226,6 +226,9 @@ typedef struct {
BOOLEAN is_pair_cancel;
BOOLEAN smp_over_br;
tSMP_AUTH_REQ auth_mode;
/* TRUE when the local stack refused the procedure (e.g. hardened re-pairing)
and the stored bond must be kept regardless of REMOVE_BOND_ON_PAIR_FAIL_AS_*. */
BOOLEAN keep_bond;
} tSMP_CMPL;
typedef struct {
@@ -338,6 +338,12 @@ typedef struct {
UINT32 static_passkey;
BOOLEAN accept_specified_sec_auth;
tSMP_AUTH_REQ origin_loc_auth_req;
#if (BLE_INCLUDED == TRUE && BLE_SMP_HARDENED_REPAIRING == TRUE)
BOOLEAN sec_req_rcvd; /* peer asked for security through a Security Request */
tSMP_AUTH_REQ sec_req_auth_req; /* AuthReq of that Security Request, peer_auth_req gets
overwritten by the Pairing Response that follows */
BOOLEAN keep_bond_on_fail; /* set when smp_repairing_is_allowed() refuses the procedure */
#endif
} tSMP_CB;
/* Server Action functions are of this type */
@@ -487,6 +493,9 @@ extern BOOLEAN smp_encrypt_data (UINT8 *key, UINT8 key_len,
UINT8 *plain_text, UINT8 pt_len,
tSMP_ENC *p_out);
extern BOOLEAN smp_command_has_invalid_parameters(tSMP_CB *p_cb);
#if (BLE_INCLUDED == TRUE && BLE_SMP_HARDENED_REPAIRING == TRUE)
extern BOOLEAN smp_repairing_is_allowed(tSMP_CB *p_cb, UINT8 *p_reason);
#endif
extern void smp_reject_unexpected_pairing_command(BD_ADDR bd_addr);
extern tSMP_ASSO_MODEL smp_select_association_model(tSMP_CB *p_cb);
extern void smp_reverse_array(UINT8 *arr, UINT8 len);
@@ -481,6 +481,13 @@ void smp_proc_sec_req(tSMP_CB *p_cb, tSMP_INT_DATA *p_data)
reason = SMP_PAIR_AUTH_FAIL;
smp_sm_event(p_cb, SMP_AUTH_CMPL_EVT, &reason);
} else {
#if (BLE_INCLUDED == TRUE && BLE_SMP_HARDENED_REPAIRING == TRUE)
/* Remember what the peer asked for here: peer_auth_req is overwritten by the
Pairing Response that follows, and smp_repairing_is_allowed() has to compare
the two to catch a peer that announces a high level and then downgrades. */
p_cb->sec_req_rcvd = TRUE;
p_cb->sec_req_auth_req = auth_req;
#endif
/* initialize local i/r key to be default keys */
p_cb->peer_auth_req = auth_req;
p_cb->local_r_key = p_cb->local_i_key = SMP_SEC_DEFAULT_KEY ;
@@ -620,6 +627,15 @@ void smp_proc_pair_cmd(tSMP_CB *p_cb, tSMP_INT_DATA *p_data)
auth |= SMP_AUTH_GEN_BOND;
}
p_cb->auth_mode = auth;
#if (BLE_INCLUDED == TRUE && BLE_SMP_HARDENED_REPAIRING == TRUE)
if (!smp_repairing_is_allowed(p_cb, &reason)) {
if (BTM_IsAclConnectionUp(p_cb->pairing_bda, BT_TRANSPORT_LE)) {
btm_remove_acl (p_cb->pairing_bda, BT_TRANSPORT_LE);
}
smp_sm_event(p_cb, SMP_AUTH_CMPL_EVT, &reason);
return;
}
#endif
if (p_cb->accept_specified_sec_auth) {
if ((auth & p_cb->origin_loc_auth_req) != p_cb->origin_loc_auth_req ) {
SMP_TRACE_ERROR("%s pairing failed - slave requires auth is 0x%x but peer auth is 0x%x local auth is 0x%x",
@@ -659,6 +675,15 @@ void smp_proc_pair_cmd(tSMP_CB *p_cb, tSMP_INT_DATA *p_data)
auth |= SMP_AUTH_GEN_BOND;
}
p_cb->auth_mode = auth;
#if (BLE_INCLUDED == TRUE && BLE_SMP_HARDENED_REPAIRING == TRUE)
if (!smp_repairing_is_allowed(p_cb, &reason)) {
if (BTM_IsAclConnectionUp(p_cb->pairing_bda, BT_TRANSPORT_LE)) {
btm_remove_acl (p_cb->pairing_bda, BT_TRANSPORT_LE);
}
smp_sm_event(p_cb, SMP_AUTH_CMPL_EVT, &reason);
return;
}
#endif
if (p_cb->accept_specified_sec_auth) {
if ((auth & p_cb->origin_loc_auth_req) != p_cb->origin_loc_auth_req ) {
SMP_TRACE_ERROR("%s pairing failed - master requires auth is 0x%x but peer auth is 0x%x local auth is 0x%x",
@@ -1598,6 +1623,15 @@ void smp_process_io_response(tSMP_CB *p_cb, tSMP_INT_DATA *p_data)
auth |= SMP_AUTH_GEN_BOND;
}
p_cb->auth_mode = auth;
#if (BLE_INCLUDED == TRUE && BLE_SMP_HARDENED_REPAIRING == TRUE)
if (!smp_repairing_is_allowed(p_cb, &reason)) {
if (BTM_IsAclConnectionUp(p_cb->pairing_bda, BT_TRANSPORT_LE)) {
btm_remove_acl (p_cb->pairing_bda, BT_TRANSPORT_LE);
}
smp_sm_event(p_cb, SMP_AUTH_CMPL_EVT, &reason);
return;
}
#endif
if (p_cb->accept_specified_sec_auth) {
if ((auth & p_cb->origin_loc_auth_req) != p_cb->origin_loc_auth_req ) {
SMP_TRACE_ERROR("pairing failed - slave requires auth is 0x%x but peer auth is 0x%x local auth is 0x%x",
@@ -1044,6 +1044,10 @@ void smp_proc_pairing_cmpl(tSMP_CB *p_cb)
evt_data.cmplt.reason = p_cb->status;
evt_data.cmplt.smp_over_br = p_cb->smp_over_br;
evt_data.cmplt.auth_mode = 0;
#if (BLE_INCLUDED == TRUE && BLE_SMP_HARDENED_REPAIRING == TRUE)
/* Copied before smp_reset_control_value() zeros the control block. */
evt_data.cmplt.keep_bond = p_cb->keep_bond_on_fail;
#endif
#if (BLE_INCLUDED == TRUE)
tBTM_SEC_DEV_REC *p_rec = btm_find_dev (p_cb->pairing_bda);
if (p_cb->status == SMP_SUCCESS) {
@@ -1285,6 +1289,104 @@ BOOLEAN smp_parameter_unconditionally_invalid(tSMP_CB *p_cb)
return FALSE;
}
#if (BLE_INCLUDED == TRUE && BLE_SMP_HARDENED_REPAIRING == TRUE)
/*******************************************************************************
**
** Function smp_repairing_is_allowed
**
** Description Called once the association model of a pairing procedure is known.
** First pairing (no stored bond) is always allowed. On re-pairing,
** refuses when the peer drops AuthReq bits it announced in a Security
** Request, when the association model would weaken MITM/SC relative to
** the stored bond, or when the encryption key would get shorter.
**
** Returns TRUE when the pairing may continue. Otherwise FALSE, and *p_reason
** holds the SMP failure code to report to the peer. Also sets
** p_cb->keep_bond_on_fail so BTA/BTC keep the stored bond even when
** REMOVE_BOND_ON_PAIR_FAIL_AS_CENTRAL / _AS_PERIPHERAL is enabled. The
** caller should drop the link so the peer cannot retry with other
** parameters until one gets through.
**
*******************************************************************************/
BOOLEAN smp_repairing_is_allowed(tSMP_CB *p_cb, UINT8 *p_reason)
{
const UINT16 level_bits = SMP_AUTH_YN_BIT | SMP_SC_SUPPORT_BIT;
tBTM_SEC_DEV_REC *p_dev_rec;
UINT16 new_auth = p_cb->auth_mode;
UINT16 sec_req_level;
UINT16 old_auth;
UINT8 new_key_size;
p_dev_rec = btm_find_dev(p_cb->pairing_bda);
if (p_dev_rec == NULL ||
!(p_dev_rec->ble.key_type & (BTM_LE_KEY_PENC | BTM_LE_KEY_LENC))) {
/* First pairing with this peer, there is nothing to downgrade. A Security
Request that outruns what IO capabilities can deliver must not block it. */
SMP_TRACE_DEBUG("LE first pairing, skip downgrade check, BDA:0x%02X%02X%02X%02X%02X%02X",
p_cb->pairing_bda[0], p_cb->pairing_bda[1], p_cb->pairing_bda[2],
p_cb->pairing_bda[3], p_cb->pairing_bda[4], p_cb->pairing_bda[5]);
return TRUE;
}
/* A Security Request carries no authentication, but the pairing command that
follows must still claim the level it announced, otherwise the peer can
advertise a high level to force a re-pairing and then drop those bits in the
Pairing Response. Compare against peer_auth_req (the pairing command), not
against the association-model result: IO capabilities that force Just Works
are a local limitation, not a peer AuthReq downgrade. Only bits this side
also asked for are enforced. */
sec_req_level = p_cb->sec_req_auth_req & p_cb->loc_auth_req & level_bits;
if (p_cb->sec_req_rcvd &&
((p_cb->peer_auth_req & sec_req_level) != sec_req_level)) {
SMP_TRACE_ERROR("LE re-pair refuse: SR 0x%02x pair 0x%02x loc 0x%02x, BDA:0x%02X%02X%02X%02X%02X%02X",
p_cb->sec_req_auth_req, p_cb->peer_auth_req, p_cb->loc_auth_req,
p_cb->pairing_bda[0], p_cb->pairing_bda[1], p_cb->pairing_bda[2],
p_cb->pairing_bda[3], p_cb->pairing_bda[4], p_cb->pairing_bda[5]);
*p_reason = SMP_PAIR_AUTH_FAIL;
/* Keep the existing bond: this is a local policy refusal, not a peer that
proved the stored keys are gone. BTC must not erase NVS on this path. */
p_cb->keep_bond_on_fail = TRUE;
return FALSE;
}
old_auth = p_dev_rec->ble.auth_mode;
/* Bonds created before auth_mode was recorded, or restored from an older NVS layout,
only carry the security level. */
if (p_dev_rec->ble.keys.sec_level >= SMP_SEC_AUTHENTICATED) {
old_auth |= SMP_AUTH_YN_BIT;
}
if ((new_auth & old_auth & level_bits) != (old_auth & level_bits)) {
SMP_TRACE_ERROR("LE re-pair refuse: auth 0x%02x < bonded 0x%02x, BDA:0x%02X%02X%02X%02X%02X%02X",
new_auth, old_auth,
p_cb->pairing_bda[0], p_cb->pairing_bda[1], p_cb->pairing_bda[2],
p_cb->pairing_bda[3], p_cb->pairing_bda[4], p_cb->pairing_bda[5]);
*p_reason = SMP_PAIR_AUTH_FAIL;
p_cb->keep_bond_on_fail = TRUE;
return FALSE;
}
new_key_size = (p_cb->loc_enc_size < p_cb->peer_enc_size) ? p_cb->loc_enc_size
: p_cb->peer_enc_size;
if (new_key_size < p_dev_rec->ble.keys.key_size) {
SMP_TRACE_ERROR("LE re-pair refuse: key size %d < bonded %d, BDA:0x%02X%02X%02X%02X%02X%02X",
new_key_size, p_dev_rec->ble.keys.key_size,
p_cb->pairing_bda[0], p_cb->pairing_bda[1], p_cb->pairing_bda[2],
p_cb->pairing_bda[3], p_cb->pairing_bda[4], p_cb->pairing_bda[5]);
*p_reason = SMP_ENC_KEY_SIZE;
p_cb->keep_bond_on_fail = TRUE;
return FALSE;
}
SMP_TRACE_DEBUG("LE re-pair allowed: auth 0x%02x->0x%02x key %d->%d, BDA:0x%02X%02X%02X%02X%02X%02X",
old_auth, new_auth, p_dev_rec->ble.keys.key_size, new_key_size,
p_cb->pairing_bda[0], p_cb->pairing_bda[1], p_cb->pairing_bda[2],
p_cb->pairing_bda[3], p_cb->pairing_bda[4], p_cb->pairing_bda[5]);
return TRUE;
}
#endif ///BLE_INCLUDED == TRUE && BLE_SMP_HARDENED_REPAIRING == TRUE
/*******************************************************************************
**
** Function smp_reject_unexpected_pairing_command