Skip to content

BLE_GAP_EVENT_REPEAT_PAIRING forces bond deletion before authentication completes #2206

Description

@syzzer

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:

  1. Attacker connects to a peripheral that has a legitimate bond with a trusted peer
  2. Attacker sends a Pairing Request
  3. Peripheral fires BLE_GAP_EVENT_REPEAT_PAIRING and application deletes the legitimate bond
  4. Attacker disconnects or allows Phase 2 to fail
  5. 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

  1. Attacker or peer initiates re-pairing by sending a Pairing Request (Phase 1)
  2. Stack fires BLE_GAP_EVENT_REPEAT_PAIRING before any Phase 2 authentication
  3. Application must delete the existing bond and return BLE_GAP_REPEAT_PAIRING_RETRY to proceed
  4. Authentication occurs in Phase 2; if it fails or is abandoned, the old bond is already gone
  5. 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()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions