Merge branch 'feat/ble_smp_repairing_hardening_v5.4' into 'release/v5.4'

feat(ble/bluedroid): add Kconfig options for BLE re-pairing hardening (5.4)

See merge request espressif/esp-idf!52419
This commit is contained in:
Island
2026-09-15 14:12:10 +08:00
12 changed files with 448 additions and 23 deletions
@@ -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 */
@@ -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);
@@ -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);
@@ -485,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 ;
@@ -574,13 +577,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 +597,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) {
@@ -625,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",
@@ -664,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",
@@ -1384,6 +1404,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;
@@ -1607,6 +1643,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