Summary
When a peer initiates re-pairing, the application is forced to delete the existing bond via BLE_GAP_EVENT_REPEAT_PAIRING before Phase 2 authentication has completed. This allows an unauthenticated attacker to permanently evict a legitimate bond from the store simply by initiating a pairing request. No knowledge of the passkey, OOB data, or existing keys is required.
Security Impact
Any device within radio range can send a Pairing Request to a peripheral that has bonded peers. Because BLE_GAP_EVENT_REPEAT_PAIRING fires during Phase 1 (feature exchange), before any authentication has taken place, the recommended handler pattern of deleting the old bond and returning BLE_GAP_REPEAT_PAIRING_RETRY exposes the following attack:
- Attacker connects to a peripheral that has a legitimate bond with a trusted peer
- Attacker sends a Pairing Request
- Peripheral fires
BLE_GAP_EVENT_REPEAT_PAIRING and application deletes the legitimate bond
- Attacker disconnects or allows Phase 2 to fail
- The legitimate bond is permanently gone; the trusted peer can no longer reconnect using its stored keys
This requires no credentials and is trivially repeatable.
The attack applies to all Phase 2 authentication methods that provide MITM protection:
- Passkey Entry: passkey is verified in Phase 2, after the bond is already evicted
- Numeric Comparison: user confirmation occurs in Phase 2, after the bond is already evicted
- OOB: OOB data is verified in Phase 2, after the bond is already evicted
(The issue obviously does not apply to Just Works, which provides no MITM protection and has no authentication step in Phase 2.)
Background
BLE_GAP_EVENT_REPEAT_PAIRING is fired from ble_sm_chk_repeat_pairing() in ble_sm.c, which is called from ble_sm_pair_req_rx() during Phase 1. The function blocks pairing progression inside a do...while loop until the application either returns BLE_GAP_REPEAT_PAIRING_RETRY (after deleting the bond) or BLE_GAP_REPEAT_PAIRING_IGNORE. There is no option to defer the decision until after authentication succeeds.
Current behavior
- Attacker or peer initiates re-pairing by sending a Pairing Request (Phase 1)
- Stack fires
BLE_GAP_EVENT_REPEAT_PAIRING before any Phase 2 authentication
- Application must delete the existing bond and return
BLE_GAP_REPEAT_PAIRING_RETRY to proceed
- Authentication occurs in Phase 2; if it fails or is abandoned, the old bond is already gone
- The previously bonded peer can no longer reconnect
Expected behavior
The existing bond should only be evicted once the new pairing has been confirmed as authenticated (i.e. after Phase 2 completes successfully, before Phase 3 key distribution). Specifically, ble_sm_chk_repeat_pairing() should be called from ble_sm_key_exchange_complete() rather than from ble_sm_pair_req_rx().
Suggested fix
Move the ble_gap_repeat_pairing_event() call to Phase 3, immediately before the new keys are written to the store. At that point authentication has already succeeded, so bond eviction is safe.
Alternatively, add a BLE_GAP_REPEAT_PAIRING_DEFER return value that causes the stack to shadow the old bond internally and only commit the eviction on pairing success, leaving the original bond intact if Phase 2 fails or the connection drops.
Workaround
Applications can manually save the existing bond before deleting it and restore it in BLE_GAP_EVENT_ENC_CHANGE if status != 0, and in BLE_GAP_EVENT_DISCONNECT if the connection drops before encryption completes. However this requires using internal store APIs (ble_store_write_peer_sec / ble_store_write_our_sec), leaves a window where the bond is absent, and does not fully close the attack surface since the attacker only needs to trigger the event — not complete the pairing.
Environment
- NimBLE version: verified for 1.9.0 and current master. Probably applies to older versions too.
- Relevant source:
nimble/host/src/ble_sm.c, ble_sm_chk_repeat_pairing(), ble_sm_pair_req_rx()
Summary
When a peer initiates re-pairing, the application is forced to delete the existing bond via
BLE_GAP_EVENT_REPEAT_PAIRINGbefore Phase 2 authentication has completed. This allows an unauthenticated attacker to permanently evict a legitimate bond from the store simply by initiating a pairing request. No knowledge of the passkey, OOB data, or existing keys is required.Security Impact
Any device within radio range can send a Pairing Request to a peripheral that has bonded peers. Because
BLE_GAP_EVENT_REPEAT_PAIRINGfires during Phase 1 (feature exchange), before any authentication has taken place, the recommended handler pattern of deleting the old bond and returningBLE_GAP_REPEAT_PAIRING_RETRYexposes the following attack:BLE_GAP_EVENT_REPEAT_PAIRINGand application deletes the legitimate bondThis requires no credentials and is trivially repeatable.
The attack applies to all Phase 2 authentication methods that provide MITM protection:
(The issue obviously does not apply to Just Works, which provides no MITM protection and has no authentication step in Phase 2.)
Background
BLE_GAP_EVENT_REPEAT_PAIRINGis fired fromble_sm_chk_repeat_pairing()inble_sm.c, which is called fromble_sm_pair_req_rx()during Phase 1. The function blocks pairing progression inside ado...whileloop until the application either returnsBLE_GAP_REPEAT_PAIRING_RETRY(after deleting the bond) orBLE_GAP_REPEAT_PAIRING_IGNORE. There is no option to defer the decision until after authentication succeeds.Current behavior
BLE_GAP_EVENT_REPEAT_PAIRINGbefore any Phase 2 authenticationBLE_GAP_REPEAT_PAIRING_RETRYto proceedExpected behavior
The existing bond should only be evicted once the new pairing has been confirmed as authenticated (i.e. after Phase 2 completes successfully, before Phase 3 key distribution). Specifically,
ble_sm_chk_repeat_pairing()should be called fromble_sm_key_exchange_complete()rather than fromble_sm_pair_req_rx().Suggested fix
Move the
ble_gap_repeat_pairing_event()call to Phase 3, immediately before the new keys are written to the store. At that point authentication has already succeeded, so bond eviction is safe.Alternatively, add a
BLE_GAP_REPEAT_PAIRING_DEFERreturn value that causes the stack to shadow the old bond internally and only commit the eviction on pairing success, leaving the original bond intact if Phase 2 fails or the connection drops.Workaround
Applications can manually save the existing bond before deleting it and restore it in
BLE_GAP_EVENT_ENC_CHANGEifstatus != 0, and inBLE_GAP_EVENT_DISCONNECTif the connection drops before encryption completes. However this requires using internal store APIs (ble_store_write_peer_sec/ble_store_write_our_sec), leaves a window where the bond is absent, and does not fully close the attack surface since the attacker only needs to trigger the event — not complete the pairing.Environment
nimble/host/src/ble_sm.c,ble_sm_chk_repeat_pairing(),ble_sm_pair_req_rx()