Skip to content

fix(ble): notifications, write-with-response, LE secure connections check#202

Merged
AlfioEmanueleFresta merged 6 commits into
masterfrom
fix/ble-notifications-and-pairing
May 18, 2026
Merged

fix(ble): notifications, write-with-response, LE secure connections check#202
AlfioEmanueleFresta merged 6 commits into
masterfrom
fix/ble-notifications-and-pairing

Conversation

@AlfioEmanueleFresta
Copy link
Copy Markdown
Member

@AlfioEmanueleFresta AlfioEmanueleFresta commented May 10, 2026

Three FIDO-BLE protocol-level fixes, one per commit, grounded in CTAP 2.2 §11.4:

  • Consume fidoStatus notifications instead of GATT Read. fidoStatus is notify-only; spec-conformant peripherals reject Read with Not Permitted. Connection::new now subscribes and frame_recv awaits the next notification. On timeout it emits a best-effort BleCommand::Cancel and returns TransportError::Timeout.
  • Use Write Request, not Write Command, for spec-declared Write characteristics. Write-Without-Response masked ATT errors (e.g. encryption-required) and was rejected outright by some authenticators. A new write_type_for() helper picks WithoutResponse only when that is the sole property advertised.
  • Refuse unbonded LE links. §11.4.3 requires the link to be encrypted before any FIDO message is exchanged. The library queries bluez's org.bluez.Device1.{Paired,Bonded} via DBus on spawn_blocking; non-bonded devices are refused with ConnectionFailed before 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_ble example mirroring webauthn_hid.

Test plan

  • cargo test --lib (174 tests), cargo fmt --check, cargo clippy --lib --all-features -- -D warnings.
  • Bonded, FIDO2: paired a Thetis FIDO2 BLE key with bluetoothctl pair, ran cargo run --example webauthn_ble; MakeCredential + GetAssertion succeeded end-to-end.
  • Unbonded: stripped the bond, ran the same example against the Thetis; library refused with a clear WARN log and no BLE link was opened.

@AlfioEmanueleFresta AlfioEmanueleFresta force-pushed the fix/ble-notifications-and-pairing branch 2 times, most recently from a1ef9d4 to d8142f1 Compare May 12, 2026 18:38
@AlfioEmanueleFresta AlfioEmanueleFresta marked this pull request as ready for review May 12, 2026 18:38
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.
Copy link
Copy Markdown
Collaborator

@msirringhaus msirringhaus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@AlfioEmanueleFresta AlfioEmanueleFresta force-pushed the fix/ble-notifications-and-pairing branch from 97316d7 to a6917e5 Compare May 18, 2026 18:06
@AlfioEmanueleFresta AlfioEmanueleFresta merged commit 201d527 into master May 18, 2026
4 checks passed
@AlfioEmanueleFresta AlfioEmanueleFresta deleted the fix/ble-notifications-and-pairing branch May 18, 2026 18:13
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.

2 participants