fix(ble): notifications, write-with-response, LE secure connections check#202
Merged
Merged
Conversation
a1ef9d4 to
d8142f1
Compare
AlfioEmanueleFresta
added a commit
that referenced
this pull request
May 17, 2026
…re any BLE link list_fido_devices filtered on Peripheral::services(), which per btleplug docs is empty until discover_services() is called, so unconnected peripherals were dropped even when their advertised UUIDs included the FIDO service. Filter on PeripheralProperties::services (advertised UUIDs) instead, and run a brief scan first so adapter.peripherals() returns currently-advertising authenticators. supported_fido_revisions performed a GATT read without first calling Peripheral::connect() / discover_services(), so the characteristic lookup found nothing on a peripheral whose services had not been discovered in the current process. Enforce bonding here too (it's only a bluez D-Bus lookup) so unbonded peripherals are refused before any BLE link is opened. Both bugs predate PR #202 and only surface when the peripheral is not already an actively-connected, services-discovered device in the current btleplug session.
msirringhaus
approved these changes
May 18, 2026
Collaborator
msirringhaus
left a comment
There was a problem hiding this comment.
With my limited knowledge around BLE: LGTM
…eristics Per CTAP 2.2 §11.4, both fidoControlPoint and fidoServiceRevisionBitfield expose the standard Write property (GATT Write Request, with response). The previous code always issued WriteType::WithoutResponse, which is spec-incorrect and may silently drop bytes on conforming authenticators. Introduces a write_type_for() helper that inspects the characteristic's declared GATT properties and picks WithResponse when WRITE is set, falling back to WithoutResponse only when the authenticator explicitly advertises just that property. Unit tests cover the property-detection logic.
Per CTAP 2.2 §11.4 the fidoStatus characteristic is Notify-only. The previous receive path called peripheral.read() against it, which bluez rejects with NotPermitted and makes FIDO2-over-BLE non-functional on Linux. Other backends may tolerate the Read by returning stale cached data, which is not real notification-driven framing either way. Connection::new now subscribes to fidoStatus, obtains the peripheral's notification stream, filters it to the fidoStatus UUID, and stores it on the connection. frame_recv awaits the next notification with the caller-supplied operation timeout; on expiry it sends a BleCommand::Cancel on fidoControlPoint (best-effort, WithoutResponse) and returns Timeout. The channel's apdu_recv and cbor_recv now forward their timeout to frame_recv and map Error::Timeout to TransportError::Timeout.
Per CTAP 2.2 §11.4, BLE FIDO authenticator traffic MUST run on a bonded LE Secure Connections link. The previous connect path issued FIDO operations without verifying bonding state, so a session that fell back to an unauthenticated link would proceed in violation of the spec. Adds a pairing module that, on Linux, queries org.bluez.Device1.Paired and org.bluez.Device1.Bonded over DBus and refuses to proceed if either is false. The DBus call is dispatched on spawn_blocking so it doesn't stall the tokio runtime. On non-bluez backends or when DBus is unreachable the check falls back gracefully: macOS and Windows enforce bonding at the OS level for authenticated GATT characteristics, so deferring there is acceptable. The library itself cannot trigger pairing; the user is expected to pair the device beforehand (e.g. via bluetoothctl). manager::connect calls enforce_bonded after establishing the link and before discovering services.
…re any BLE link list_fido_devices filtered on Peripheral::services(), which per btleplug docs is empty until discover_services() is called, so unconnected peripherals were dropped even when their advertised UUIDs included the FIDO service. Filter on PeripheralProperties::services (advertised UUIDs) instead, and run a brief scan first so adapter.peripherals() returns currently-advertising authenticators. supported_fido_revisions performed a GATT read without first calling Peripheral::connect() / discover_services(), so the characteristic lookup found nothing on a peripheral whose services had not been discovered in the current process. Enforce bonding here too (it's only a bluez D-Bus lookup) so unbonded peripherals are refused before any BLE link is opened. Both bugs predate PR #202 and only surface when the peripheral is not already an actively-connected, services-discovered device in the current btleplug session.
Mirrors webauthn_hid using transport::ble. Requires the BLE FIDO authenticator to be bonded via the OS (e.g. `bluetoothctl pair <ADDR>`) before running.
97316d7 to
a6917e5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three FIDO-BLE protocol-level fixes, one per commit, grounded in CTAP 2.2 §11.4:
fidoStatusnotifications instead of GATT Read.fidoStatusis notify-only; spec-conformant peripherals reject Read withNot Permitted.Connection::newnow subscribes andframe_recvawaits the next notification. On timeout it emits a best-effortBleCommand::Canceland returnsTransportError::Timeout.Write-Without-Responsemasked ATT errors (e.g. encryption-required) and was rejected outright by some authenticators. A newwrite_type_for()helper picksWithoutResponseonly when that is the sole property advertised.org.bluez.Device1.{Paired,Bonded}via DBus onspawn_blocking; non-bonded devices are refused withConnectionFailedbefore any BLE connect. On non-bluez backends or when DBus is unreachable, the check falls through.A follow-up commit fixes two pre-existing BLE enumeration bugs that only show up for peripherals not already actively connected in the current btleplug session, and adds a
webauthn_bleexample mirroringwebauthn_hid.Test plan
cargo test --lib(174 tests),cargo fmt --check,cargo clippy --lib --all-features -- -D warnings.bluetoothctl pair, rancargo run --example webauthn_ble; MakeCredential + GetAssertion succeeded end-to-end.