smp: wait for key distribution to be flushed before completing pairing - #967
smp: wait for key distribution to be flushed before completing pairing#967TheArkadiuszGrzelka wants to merge 1 commit into
Conversation
`Session.pair()` resolved as soon as the pairing procedure itself was done, which is before the key distribution PDUs the initiator has just queued have been acknowledged by the controller. An application that disconnects right after `await connection.pair()` therefore races its own last key PDU. Observed against an LE Audio sink (Zephyr 4.4, SC + MITM, the peer asking for the initiator's IRK and distributing nothing itself): the sink receives Identity Information, never Identity Address Information, so it reports pairing status 0x08 and clears the half distributed key. The initiator keeps its LTK and prints success, and every later connection fails to encrypt with PIN_OR_KEY_MISSING. `run_pair` in apps/auracast.py does exactly that disconnect, and apps/pair.py avoids the same race today only with a one second sleep before disconnecting. Wait for the connection's data packet queue to drain before resolving the pairing result, and add `Connection.drain()` for that, mirroring the existing `_IsoLink.drain()`.
|
Verified on hardware since opening this, so the last line of the description is now out of date. Bench: an external LE Audio controller over HCI UART, and a Zephyr 4.4 broadcast sink on nRF5340 (BASS + PACS, SC and MITM required). With this change, One note for anyone reproducing it: the sink has to start out unbonded. Re-running |
|
Thanks for this. I will take a look shortly and try it out on a few controllers. |
Problem
Session.pair()resolves as soon as the pairing procedure is done, which happens before the key distribution PDUs the initiator has just queued have been acknowledged by the controller.distribute_keys()only enqueues them into the connection'sDataPacketQueue, andSession.on_pairing()setspairing_resultright after. An application that disconnects as soon asawait connection.pair()returns therefore races its own last key PDU, andConnection.__aexit__does exactly that.Observed on a bench with an external LE Audio controller and a Zephyr 4.4 broadcast sink (SC + MITM, the sink asking for the initiator's IRK only and distributing nothing itself). Peer-side SMP trace:
Identity Address Information (0x09) never arrives, so the peer reports pairing status 0x08 and clears the half distributed key, while the initiator keeps its LTK and prints success. Every later connection then fails to encrypt with
PIN_OR_KEY_MISSING.run_pairinapps/auracast.pyhits this on every run.apps/pair.pyavoids the same race today only by sleepingPOST_PAIRING_DELAYbefore disconnecting.Change
Connection.drain(), mirroring the existing_IsoLink.drain(), so a caller can wait for the controller to complete what was queued for a connection.Session.on_pairing()awaits it before resolvingpairing_result. When the queue is already drained the event is set, so the await does not yield and adds no latency to the common path.Testing
tests/self_test.py::test_self_smp_waits_for_key_distributionwithholds the controller's packet completions, asserts the packets really are still in flight, and asserts that pairing is not reported as complete until they are acknowledged. It fails on main and passes with this change.pytest tests: 934 passed, 1 skipped, 6 failed. The 6 failures are intests/transport_test.py::test_open_transport_with_metadata(android-netsim spec parsing) and fail identically on unmodified main.black -S --check,ruff checkandmypyclean on the touched files.