Skip to content

firmware: Bluetooth allow list and key-save gate (#144) - #153

Open
Skeyelab wants to merge 6 commits into
mainfrom
ble-allow-list
Open

Skeyelab wants to merge 6 commits into
mainfrom
ble-allow-list

Conversation

@Skeyelab

@Skeyelab Skeyelab commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Closes #144.

Changes

  • Allow list. With a Computer stored and no pairing window open, advertising only accepts connections from the Computer (ble_gap_wl_set with its identity address, plus setScanFilter(false, true)). That stops a nearby device from connecting and starting the repeat pairing that makes the library delete the Computer's bond.

    • Opening a window removes the filter.
    • After a window lapses or Cancel is pressed, the once-a-second tick puts the filter back.
    • A stranger's Just Works pairing that finishes while Eric's passkey is showing can save keys over the old Computer's, and rejecting it then deletes them. The window was replacing that Computer anyway; it only matters if Eric's pairing then fails or he cancels.
  • A Computer that turns its privacy off after being seen on private addresses is refused in network mode until it pairs again in a window.

  • A host that pairs from a private address but reconnects from its public one would be refused until it pairs again (network mode is set at pairing). Macs always use private addresses.

  • If the allow list can't be set, the panel advertises to anyone, so a failure can never lock the Computer out.

  • Key-save gate. ble_hs_cfg.store_write_cb is wrapped, and OUR_SEC/PEER_SEC writes are refused unless a passkey is showing, which only happens in a window. So a Just Works pairing claiming the Computer's identity address can't overwrite its keys, and strangers' bonds don't fill the store. That also fixes the "full store refuses the real Computer's keys" note from the firmware: Bluetooth keyboard hardening after #140 (#141) #143 re-review. Deletes are not vetoed, because the library's RETRY would spin the host task.

  • Private-address resolution: correction. An earlier version of this PR said main never turns the controller's address resolution on, and that a Mac couldn't reconnect. That was wrong. NimBLE turns it on at every sync (ble_hs_startup_go → ble_hs_pvcy_set_our_irk) and restores bonded IRKs to the resolving list, so the allow list matches a Mac's resolved identity. The redundant calls have been removed.

  • Network privacy mode, for a Computer seen on private addresses. NimBLE puts every resolving-list peer in device mode, which also lets a peer's identity address through when it's sent in the clear. Once the Computer has connected from a resolvable private address (recorded in the panel NVS as ble/private), and it sent a real IRK, it's switched to network mode, so only addresses made from its secret IRK pass. A Computer that sends an IRK but connects from its public address (BlueZ by default) stays in device mode; network mode would lock it out (AppSec). A host resync puts peers back in device mode and ends advertising, so the tick re-applies the mode and advertises again.

  • The disconnect delete re-checks (AppSec): a rejected stranger that claimed the Computer's address no longer deletes the Computer's bond, because isComputer() is checked when the disconnect lands.

Known limits

  • A Computer with no IRK (a public address and no privacy, like black2) can still be spoofed by its address outside a window, and that spoofer can trigger the library's REPEAT_PAIRING delete. A Mac sends an IRK, so it gets network mode.
  • During a window, any device can connect, as designed. The REPEAT_PAIRING or forged-identity wipes only matter if the window then ends without a new pairing.
  • The gate checks that a passkey is showing, not this peer's. A stranger's Just Works keys saved during Eric's passkey entry are still rejected, never trusted, and deleted at disconnect, unless by then they're the Computer's.
  • A stranger's Just Works pairing that finishes while Eric's passkey is showing can save keys over the old Computer's, and rejecting it then deletes them. The window was replacing that Computer anyway; it only matters if Eric's pairing then fails or he cancels.
  • If the allow list can't be set, the panel advertises to anyone and logs a warning; the tick retries every second.

Tests: pio test -e native passes 73/73, adding test_only_the_computer_may_connect_outside_a_window, test_only_a_window_pairing_saves_keys, test_a_resolvable_private_address (fake RPAs) and test_a_computer_seen_on_private_addresses_is_matched_by_them_only. pio run -e crowpanel builds. RPA resolution happens in the controller, so no native test can exercise it; that's for hardware.

Hardware, black2 only, through the panel-owner rule:

  1. A legitimate pairing saves its keys and reconnects, including after a restart.
  2. Another device can't connect while the filter is on.
  3. A window still lets a new Computer in.
  4. With black2's privacy on (btmgmt privacy, if main OKs it), black2 reconnects from a new private address with the filter on, which stands in for the Mac.
  5. A Just Works pairing claiming the Computer's address during a window that's then canceled: the Computer still reconnects.

Before merging: merge main with #157 in. #157's persistBond() rewrites the Computer's keys after g_pairing has been cleared, so this PR's key-save gate would refuse them, after the deletes have run, leaving the new Computer keyless. The gate needs a host-task flag around persistBond(). Then rerun the private-address reconnect test on top.

No auto-merge until the firmware and AppSec reviews pass and these hardware checks are done.

- With a Computer stored and no pairing window open, the panel advertises
  to the Computer only, so a nearby device can't connect and start the
  repeat pairing that makes the library delete the Computer's bond. A
  window opens it to anyone; the tick puts the filter back within a
  second of the window lapsing or being canceled.
- Pairing keys are saved only while a passkey is showing, i.e. for a
  pairing from a window. A Just Works pairing that claims the Computer's
  identity address can no longer overwrite its keys, and strangers'
  bonds no longer fill the store.

Assisted-by: Claude Code:claude-opus-5-5
- Turn on the controller's address resolution. NimBLE loads bonded
  peers' IRKs into the resolving list but enables resolution only when
  the panel itself uses a private address, so a Mac reconnecting from a
  new private address had no key and wouldn't pass the allow list.
- With a real IRK, put the Computer in network privacy mode: only
  private addresses made from its IRK get through the allow list, not
  its copyable identity address.
- A rejected stranger that claimed the Computer's address no longer
  deletes the Computer's bond at disconnect (AppSec finding).
- Log when the allow list can't be set.

Assisted-by: Claude Code:claude-opus-5-5
…es (#144)

- AppSec: a Computer that sends an IRK but connects from its public
  address (BlueZ by default) would be locked out in network mode. The
  panel now records, in its NVS, that the Computer connected from a
  resolvable private address, and only then switches it to network mode.
- After a host resync, resolution and the privacy mode are applied again.

Assisted-by: Claude Code:claude-opus-5-5
…#144)

- NimBLE already turns address resolution on at every sync
  (ble_hs_pvcy_set_our_irk from ble_hs_startup_go); the extra calls
  could only fail Bluetooth's start over nothing.
- After a resync, advertise when no Computer is here or a window is
  open, not only if it was advertising before: the reset ends it.
- advertise() runs under a lock: openPairing() calls it on the net task.

Assisted-by: Claude Code:claude-opus-5-5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

firmware: Bluetooth allow list so a spoofer can't wipe the paired Computer's bond (#141 item 3)

1 participant