fix(ble/bluedroid): harden LE bond handling across encryption

Keep the existing bond until the new pairing is encrypted, and on encryption
failure drop the link instead of clearing keys. Recovering from a peer that
really deleted the bond is opt-in through BT_BLE_SMP_UNBOND_ON_KEY_MISSING.

Closes BLERP (NDSS 2026) V5, and stops an unauthenticated Pairing Request
from dropping the stored keys (V2 exploitation).


(cherry picked from commit f864615d7d)

Co-authored-by: zhiweijian <zhiweijian@espressif.com>
This commit is contained in:
Zhi Wei Jian
2026-09-04 11:03:46 +08:00
committed by BOT
parent f63cfa51bf
commit 734e55fcbf
5 changed files with 89 additions and 16 deletions

View File

@@ -441,6 +441,29 @@ config BT_BLE_SMP_BOND_NVS_FLASH
help
This select can save SMP bonding keys to nvs flash
config BT_BLE_SMP_UNBOND_ON_KEY_MISSING
bool "Drop the local LE keys when the peer reports it has no key"
depends on BT_BLE_SMP_ENABLE
default n
help
As Central, an encryption attempt using the stored LTK fails with "PIN or Key
Missing" when the peer has deleted the bond on its side. This option controls how
the stack reacts to that specific error.
Enabled: the local LE keys are discarded and the link is kept up, so the
application can re-pair straight away and recovers without user interaction. The
problem is that an impersonating device can report the same error, so a peer can
strip the stored security level without any authentication and then re-pair at a
weaker level. That is exactly the re-pairing downgrade the BLERP paper describes,
which is why this is not the default.
Disabled: the link is dropped and the keys are kept, so no peer can drop the bond
by refusing to encrypt. The problem is that the same stale LTK is then used again
on every following connection and encryption keeps failing, which never recovers
on its own. The application has to call esp_ble_remove_bond_device() from the
ESP_GAP_BLE_AUTH_CMPL_EVT failure callback to get out of it.
config BT_BLE_PERIPH_PSEUDO_ADDR_BOND
bool "Peripheral dual local-identity bond isolation (pseudo address)"
depends on BT_BLE_SMP_ENABLE && BT_BLE_50_EXTEND_ADV_EN

View File

@@ -194,6 +194,13 @@
#define UC_BT_BLE_PERIPH_PSEUDO_ADDR_BOND FALSE
#endif
#ifdef CONFIG_BT_BLE_SMP_UNBOND_ON_KEY_MISSING
#define UC_BT_BLE_SMP_UNBOND_ON_KEY_MISSING CONFIG_BT_BLE_SMP_UNBOND_ON_KEY_MISSING
#else
#define UC_BT_BLE_SMP_UNBOND_ON_KEY_MISSING FALSE
#endif
#ifdef CONFIG_BT_BLE_42_FEATURES_SUPPORTED
#define UC_BT_BLE_42_FEATURES_SUPPORTED CONFIG_BT_BLE_42_FEATURES_SUPPORTED
#else

View File

@@ -211,6 +211,14 @@
** BLE features
**
******************************************************************************/
/* Discard the local LE keys when the peer rejects encryption with "PIN or Key Missing". */
#if (UC_BT_BLE_SMP_UNBOND_ON_KEY_MISSING == TRUE)
#define BLE_SMP_UNBOND_ON_KEY_MISSING TRUE
#else
#define BLE_SMP_UNBOND_ON_KEY_MISSING FALSE
#endif
#if (UC_BT_BLE_ENABLED ==TRUE)
#define BLE_INCLUDED TRUE
#else

View File

@@ -4251,12 +4251,36 @@ void btm_sec_encrypt_change (UINT16 handle, UINT8 status, UINT8 encr_enable)
}
if (p_acl && p_acl->transport == BT_TRANSPORT_LE) {
if (status == HCI_ERR_KEY_MISSING || status == HCI_ERR_AUTH_FAILURE ||
status == HCI_ERR_ENCRY_MODE_NOT_ACCEPTABLE) {
p_dev_rec->sec_flags &= ~ (BTM_SEC_LE_LINK_KEY_KNOWN);
BOOLEAN disconnect = (status != HCI_SUCCESS);
BD_ADDR pseudo_addr;
/* btm_ble_link_encrypted() runs the SMP state machine and the upper layer
callbacks, which may retire p_dev_rec, so keep our own copy of the address. */
memcpy(pseudo_addr, p_dev_rec->ble.pseudo_addr, BD_ADDR_LEN);
#if (BLE_SMP_UNBOND_ON_KEY_MISSING == TRUE)
/* The peer answered our LL_ENC_REQ saying it no longer holds the LTK. Forget our
copy and keep the link up so the application can pair again right away. This is
unauthenticated, an impersonator can report the same error to strip the stored
security level, which is why the option defaults to disabled. */
if (disconnect && status == HCI_ERR_KEY_MISSING && p_acl->link_role == BTM_ROLE_MASTER) {
BTM_TRACE_WARNING("%s peer reports no LTK, dropping the local LE keys\n", __func__);
p_dev_rec->sec_flags &= ~(BTM_SEC_LE_LINK_KEY_KNOWN);
p_dev_rec->ble.key_type = BTM_LE_KEY_NONE;
disconnect = FALSE;
}
#endif ///BLE_SMP_UNBOND_ON_KEY_MISSING == TRUE
btm_ble_link_encrypted(pseudo_addr, encr_enable);
/* A peer that refuses to encrypt must not be able to drop our bond, otherwise it
can strip the stored security level and then re-pair at a weaker one. Tear the
link down instead and keep the keys until the application unbonds. */
if (disconnect && BTM_IsAclConnectionUp(pseudo_addr, BT_TRANSPORT_LE)) {
BTM_TRACE_WARNING("%s LE encryption failed (status 0x%02x), disconnecting\n",
__func__, status);
btm_remove_acl(pseudo_addr, BT_TRANSPORT_LE);
}
btm_ble_link_encrypted(p_dev_rec->ble.pseudo_addr, encr_enable);
return;
} else {
/* BR/EDR connection, update the encryption key size to be 16 as always */

View File

@@ -260,13 +260,9 @@ void smp_send_pair_req(tSMP_CB *p_cb, tSMP_INT_DATA *p_data)
{
SMP_TRACE_DEBUG("%s", __func__);
#if (BLE_INCLUDED == TRUE)
tBTM_SEC_DEV_REC *p_dev_rec = btm_find_dev (p_cb->pairing_bda);
/* erase all keys when master sends pairing req*/
if (p_dev_rec) {
btm_sec_clear_ble_keys(p_dev_rec);
}
#endif ///BLE_INCLUDED == TRUE
/* Any existing bond is replaced only once the new pairing is authenticated,
see smp_check_auth_req(). */
/* do not manipulate the key, let app decide,
leave out to BTM to mandate key distribution for bonding case */
smp_send_cmd(SMP_OPCODE_PAIRING_REQ, p_cb);
@@ -574,13 +570,11 @@ void smp_proc_pair_cmd(tSMP_CB *p_cb, tSMP_INT_DATA *p_data)
{
UINT8 *p = (UINT8 *)p_data;
UINT8 reason = SMP_ENC_KEY_SIZE;
tBTM_SEC_DEV_REC *p_dev_rec = btm_find_dev (p_cb->pairing_bda);
SMP_TRACE_DEBUG("%s", __func__);
/* erase all keys if it is slave proc pairing req*/
if (p_dev_rec && (p_cb->role == HCI_ROLE_SLAVE)) {
btm_sec_clear_ble_keys(p_dev_rec);
}
/* Any existing bond is replaced only once the new pairing is authenticated, see
smp_check_auth_req(). An unauthenticated Pairing Request must not drop the bond. */
p_cb->flags |= SMP_PAIR_FLAG_ENC_AFTER_PAIR;
@@ -596,6 +590,7 @@ void smp_proc_pair_cmd(tSMP_CB *p_cb, tSMP_INT_DATA *p_data)
smp_sm_event(p_cb, SMP_AUTH_CMPL_EVT, &reason);
return;
}
p_cb->accept_specified_sec_auth = bta_dm_co_ble_get_accept_auth_enable();
p_cb->origin_loc_auth_req = bta_dm_co_ble_get_auth_req();
if (p_cb->role == HCI_ROLE_SLAVE) {
@@ -1384,6 +1379,22 @@ void smp_check_auth_req(tSMP_CB *p_cb, tSMP_INT_DATA *p_data)
"(i-initiator r-responder)\n",
__func__, enc_enable, p_cb->local_i_key, p_cb->local_r_key);
if (enc_enable == 1) {
#if (BLE_INCLUDED == TRUE)
/* The new pairing has been authenticated and the link is now encrypted with the
freshly generated key, so the previous bond can be dropped. Doing it here rather
than when the Pairing Request is exchanged keeps the old key in place for a
pairing that never completes. */
tBTM_SEC_DEV_REC *p_dev_rec = btm_find_dev(p_cb->pairing_bda);
if (p_dev_rec) {
if (p_dev_rec->ble.key_type & (BTM_LE_KEY_PENC | BTM_LE_KEY_LENC)) {
SMP_TRACE_DEBUG("LE replace bond after enc, 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]);
}
btm_sec_clear_ble_keys(p_dev_rec);
}
#endif ///BLE_INCLUDED == TRUE
if (p_cb->le_secure_connections_mode_is_used) {
/* In LE SC mode LTK is used instead of STK and has to be always saved */
p_cb->local_i_key |= SMP_SEC_KEY_TYPE_ENC;