From 70fe449c6c1b05d7e5fd19535de6e2458178c696 Mon Sep 17 00:00:00 2001 From: Mike Beaumont Date: Mon, 28 Sep 2026 15:42:40 +0200 Subject: [PATCH 1/3] refactor: create libwebauthn-pxp --- .github/workflows/rust-latest.yml | 4 +- .github/workflows/rust.yml | 4 +- Cargo.lock | 39 +++- Cargo.toml | 2 +- README.md | 5 + libwebauthn-pxp/Cargo.toml | 53 ++++++ .../src}/advertisement.rs | 23 +-- libwebauthn-pxp/src/ble.rs | 86 +++++++++ libwebauthn-pxp/src/cbor.rs | 3 + libwebauthn-pxp/src/connection.rs | 168 ++++++++++++++++++ .../src}/connection_stages.rs | 59 +++--- .../cable => libwebauthn-pxp/src}/crypto.rs | 13 +- .../src}/data_channel.rs | 2 +- .../src}/digit_encode.rs | 0 .../cable => libwebauthn-pxp/src}/error.rs | 10 +- libwebauthn-pxp/src/get_info.rs | 73 ++++++++ .../src}/known_devices.rs | 84 +-------- .../cable => libwebauthn-pxp/src}/l2cap.rs | 2 +- libwebauthn-pxp/src/lib.rs | 43 +++++ .../cable => libwebauthn-pxp/src}/protocol.rs | 97 ++++++---- .../src}/qr_code_device.rs | 80 +-------- .../cable => libwebauthn-pxp/src}/tunnel.rs | 6 +- libwebauthn/Cargo.toml | 8 +- libwebauthn/src/transport/cable/channel.rs | 90 ++-------- libwebauthn/src/transport/cable/device.rs | 56 ++++++ libwebauthn/src/transport/cable/mod.rs | 19 +- libwebauthn/src/transport/cable/stages.rs | 0 libwebauthn/src/transport/error.rs | 2 +- 28 files changed, 675 insertions(+), 356 deletions(-) create mode 100644 libwebauthn-pxp/Cargo.toml rename {libwebauthn/src/transport/cable => libwebauthn-pxp/src}/advertisement.rs (89%) create mode 100644 libwebauthn-pxp/src/ble.rs create mode 100644 libwebauthn-pxp/src/cbor.rs create mode 100644 libwebauthn-pxp/src/connection.rs rename {libwebauthn/src/transport/cable => libwebauthn-pxp/src}/connection_stages.rs (88%) rename {libwebauthn/src/transport/cable => libwebauthn-pxp/src}/crypto.rs (94%) rename {libwebauthn/src/transport/cable => libwebauthn-pxp/src}/data_channel.rs (98%) rename {libwebauthn/src/transport/cable => libwebauthn-pxp/src}/digit_encode.rs (100%) rename {libwebauthn/src/transport/cable => libwebauthn-pxp/src}/error.rs (94%) create mode 100644 libwebauthn-pxp/src/get_info.rs rename {libwebauthn/src/transport/cable => libwebauthn-pxp/src}/known_devices.rs (69%) rename {libwebauthn/src/transport/cable => libwebauthn-pxp/src}/l2cap.rs (99%) create mode 100644 libwebauthn-pxp/src/lib.rs rename {libwebauthn/src/transport/cable => libwebauthn-pxp/src}/protocol.rs (91%) rename {libwebauthn/src/transport/cable => libwebauthn-pxp/src}/qr_code_device.rs (78%) rename {libwebauthn/src/transport/cable => libwebauthn-pxp/src}/tunnel.rs (98%) create mode 100644 libwebauthn/src/transport/cable/device.rs delete mode 100644 libwebauthn/src/transport/cable/stages.rs diff --git a/.github/workflows/rust-latest.yml b/.github/workflows/rust-latest.yml index 401451bc..1f2342ea 100644 --- a/.github/workflows/rust-latest.yml +++ b/.github/workflows/rust-latest.yml @@ -43,5 +43,5 @@ jobs: run: cargo test -p libwebauthn --lib --features nfc-backend-pcsc --verbose env: LIBWEBAUTHN_PSL_SYSTEM_TEST: "1" - - name: Verify libwebauthn publishes cleanly - run: cargo publish --dry-run -p libwebauthn + - name: Verify libwebauthn and libwebauthn-pxp publish cleanly + run: cargo publish --dry-run -p libwebauthn-pxp -p libwebauthn diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9c7c05b4..3a5df6a7 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -40,8 +40,8 @@ jobs: run: cargo test -p libwebauthn --lib --features nfc-backend-pcsc --verbose env: LIBWEBAUTHN_PSL_SYSTEM_TEST: "1" - - name: Verify libwebauthn publishes cleanly - run: cargo publish --dry-run -p libwebauthn + - name: Verify libwebauthn and libwebauthn-pxp publish cleanly + run: cargo publish --dry-run -p libwebauthn-pxp -p libwebauthn msrv: name: Verify MSRV diff --git a/Cargo.lock b/Cargo.lock index c6e91e53..e52ae0cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1978,7 +1978,6 @@ dependencies = [ "async-trait", "base64-url", "bitflags 2.11.1", - "bluer", "btleplug", "byteorder", "cbc", @@ -1997,6 +1996,7 @@ dependencies = [ "http", "icu_normalizer", "idna", + "libwebauthn-pxp", "maplit", "mockall", "nfc1", @@ -2010,7 +2010,6 @@ dependencies = [ "qrcode", "rand 0.8.6", "reqwest", - "rustls", "serde", "serde-indexed 0.2.0", "serde_bytes", @@ -2019,7 +2018,6 @@ dependencies = [ "serde_json", "serde_repr", "sha2 0.10.9", - "snow", "spki", "test-log", "text_io", @@ -2027,16 +2025,47 @@ dependencies = [ "time", "tokio", "tokio-stream", - "tokio-tungstenite", "tracing", "tracing-subscriber", - "tungstenite", "url", "uuid", "x509-parser", "zeroize", ] +[[package]] +name = "libwebauthn-pxp" +version = "0.10.0" +dependencies = [ + "aes", + "async-trait", + "base64-url", + "bluer", + "btleplug", + "futures", + "hex", + "hkdf", + "hmac 0.12.1", + "p256 0.13.2", + "rand 0.8.6", + "rustls", + "serde", + "serde-indexed 0.2.0", + "serde_bytes", + "serde_cbor_2", + "serde_repr", + "sha2 0.10.9", + "snow", + "test-log", + "thiserror 2.0.18", + "tokio", + "tokio-tungstenite", + "tracing", + "tungstenite", + "url", + "uuid", +] + [[package]] name = "libwebauthn-tests" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index af703773..633cee30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["libwebauthn", "libwebauthn-tests"] +members = ["libwebauthn", "libwebauthn-pxp", "libwebauthn-tests"] # The trussed ecosystem is currently a bit messy, so we have to do some patching of the versions [patch.crates-io] diff --git a/README.md b/README.md index 7fbb3863..7fe1953b 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,10 @@ opt-in: the crate ships with `default = []` for the NFC stack, so enable the `nfc-backend-pcsc` feature (pure userspace, recommended) or `nfc-backend-libnfc` (requires the `libnfc` system library) to compile it in. +The hybrid transports are built on the standalone +[`libwebauthn-pxp`](libwebauthn-pxp) crate, which implements the FIDO +[Proximity Exchange Protocol][pxp] (formerly CTAP hybrid). + [^nfc-optin]: Off by default. Enable `nfc-backend-pcsc` and/or `nfc-backend-libnfc`. ## Example programs @@ -137,3 +141,4 @@ If you don't know where to start, check out the _Issues_ tab. [#18]: https://github.com/linux-credentials/libwebauthn/issues/18 [#31]: https://github.com/linux-credentials/libwebauthn/issues/31 [psl]: https://publicsuffix.org/ +[pxp]: https://fidoalliance.org/specs/hybrid/proximity-exchange-protocol-v1.0-wd-20260717.html diff --git a/libwebauthn-pxp/Cargo.toml b/libwebauthn-pxp/Cargo.toml new file mode 100644 index 00000000..25d39acf --- /dev/null +++ b/libwebauthn-pxp/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "libwebauthn-pxp" +description = "FIDO Proximity Exchange Protocol (PXP, formerly CTAP hybrid / caBLE) client for Linux, written in Rust" +version = "0.10.0" +authors = [ + "Alfie Fresta ", + "Martin Sirringhaus ", + "Isaiah Inuwa ", +] +edition = "2021" +rust-version = "1.88" +license = "LGPL-2.1-or-later" +license-file = "../COPYING" +readme = "../README.md" +homepage = "https://github.com/linux-credentials" +repository = "https://github.com/linux-credentials/libwebauthn" + +[lib] +name = "libwebauthn_pxp" +path = "src/lib.rs" + +[dependencies] +async-trait = "0.1.36" +base64-url = "3.0.0" +bluer = { version = "0.17", default-features = false, features = ["l2cap"] } +btleplug = "0.11.7" +aes = "0.8.2" +futures = "0.3.5" +hex = "0.4.3" +hkdf = "0.12" +hmac = "0.12.1" +p256 = { version = "0.13.2", features = ["ecdh", "arithmetic", "serde"] } +rand = "0.8.5" +rustls = { version = "0.23.27", default-features = false, features = ["ring"] } +serde = "1.0.110" +serde_bytes = "0.11.5" +serde_cbor_2 = "0.13" +serde-indexed = "0.2.0" +serde_repr = "0.1.6" +sha2 = "0.10.2" +snow = { version = "0.10", features = ["use-p256"] } +thiserror = "2.0.12" +tokio = { version = "1.45", features = ["full"] } +tokio-tungstenite = { version = "0.26", features = [ + "rustls-tls-native-roots", +] } +tracing = "0.1.29" +tungstenite = { version = "0.26.2" } +url = "2.5" +uuid = { version = "1.5.0", features = ["serde", "v4"] } + +[dev-dependencies] +test-log = { version = "0.2" } diff --git a/libwebauthn/src/transport/cable/advertisement.rs b/libwebauthn-pxp/src/advertisement.rs similarity index 89% rename from libwebauthn/src/transport/cable/advertisement.rs rename to libwebauthn-pxp/src/advertisement.rs index 25e0a30b..ea12519e 100644 --- a/libwebauthn/src/transport/cable/advertisement.rs +++ b/libwebauthn-pxp/src/advertisement.rs @@ -1,16 +1,16 @@ use std::collections::BTreeMap; -use ::btleplug::api::Central; +use ::btleplug::api::{Central, PeripheralProperties}; use futures::StreamExt; use serde_cbor_2 as serde_cbor; use std::pin::pin; use tracing::{debug, instrument, trace, warn}; use uuid::Uuid; -use crate::proto::ctap2::cbor::Value; -use crate::transport::ble::btleplug::{self, FidoDevice}; -use crate::transport::cable::crypto::trial_decrypt_advert; -use crate::transport::cable::error::CableError; +use crate::ble; +use crate::cbor::Value; +use crate::crypto::trial_decrypt_advert; +use crate::error::CableError; const CABLE_UUID_FIDO: &str = "0000fff9-0000-1000-8000-00805f9b34fb"; const CABLE_UUID_GOOGLE: &str = "0000fde2-0000-1000-8000-00805f9b34fb"; @@ -72,23 +72,18 @@ impl From<[u8; 16]> for DecryptedAdvert { #[instrument(skip_all, err)] pub(crate) async fn await_advertisement( eid_key: &[u8], -) -> Result<(FidoDevice, DecryptedAdvert), CableError> { +) -> Result<(PeripheralProperties, DecryptedAdvert), CableError> { let uuids = &[ Uuid::parse_str(CABLE_UUID_FIDO)?, Uuid::parse_str(CABLE_UUID_GOOGLE)?, // Deprecated, but may still be in use. ]; - let stream = btleplug::manager::start_discovery_for_service_data(uuids) - .await - .or(Err(CableError::TransportUnavailable))?; + let (adapter, stream) = ble::start_discovery_for_service_data(uuids).await?; let mut stream = pin!(stream); - while let Some((adapter, peripheral, data)) = stream.as_mut().next().await { + while let Some((peripheral, data)) = stream.as_mut().next().await { debug!({ ?peripheral, ?data }, "Found device with service data"); - let Some(device) = btleplug::manager::get_device(peripheral.clone()) - .await - .or(Err(CableError::TransportUnavailable))? - else { + let Some(device) = ble::get_properties(&peripheral).await? else { warn!( ?peripheral, "Unable to fetch peripheral properties, ignoring" diff --git a/libwebauthn-pxp/src/ble.rs b/libwebauthn-pxp/src/ble.rs new file mode 100644 index 00000000..13cb25fc --- /dev/null +++ b/libwebauthn-pxp/src/ble.rs @@ -0,0 +1,86 @@ +//! Minimal btleplug scan for the CMHD's BLE advertisement. Only service data +//! and the peripheral's properties are needed; no GATT connection is made. +use std::collections::HashMap; + +use btleplug::api::{Central as _, CentralEvent, Manager as _, Peripheral as _}; +use btleplug::api::{PeripheralProperties, ScanFilter}; +use btleplug::platform::{Adapter, Manager, Peripheral, PeripheralId}; +use futures::{Stream, StreamExt}; +use tracing::{debug, instrument, trace, warn, Level}; +use uuid::Uuid; + +use crate::error::CableError; + +/// TODO(#86): Support multiple adapters. +async fn get_adapter() -> Result { + let manager = Manager::new() + .await + .or(Err(CableError::TransportUnavailable))?; + manager + .adapters() + .await + .or(Err(CableError::TransportUnavailable))? + .into_iter() + .next() + .ok_or(CableError::TransportUnavailable) +} + +async fn on_peripheral_service_data( + adapter: &Adapter, + id: &PeripheralId, + uuids: &[Uuid], + service_data: HashMap>, +) -> Option<(Peripheral, Vec)> { + let data = uuids.iter().find_map(|uuid| service_data.get(uuid))?; + trace!(?id, ?data, "Found service data"); + + let Ok(peripheral) = adapter.peripheral(id).await else { + warn!(?id, "Could not get peripheral"); + return None; + }; + + debug!({ ?id, ?data }, "Found service data for peripheral"); + Some((peripheral, data.to_owned())) +} + +/// Scans for peripherals advertising service data on any of `uuids`. Returns +/// the adapter, so the caller can stop the scan, and the matching stream. +#[instrument(level = Level::DEBUG, skip_all)] +pub(crate) async fn start_discovery_for_service_data( + uuids: &[Uuid], +) -> Result<(Adapter, impl Stream)> + use<'_>), CableError> { + let adapter = get_adapter().await?; + let events = adapter + .events() + .await + .or(Err(CableError::TransportUnavailable))?; + + adapter + .start_scan(ScanFilter::default()) + .await + .or(Err(CableError::TransportUnavailable))?; + + let scan_adapter = adapter.clone(); + let stream = events.filter_map(move |event| { + let adapter = scan_adapter.clone(); + async move { + match event { + CentralEvent::ServiceDataAdvertisement { id, service_data } => { + on_peripheral_service_data(&adapter, &id, uuids, service_data).await + } + _ => None, + } + } + }); + + Ok((adapter, stream)) +} + +pub(crate) async fn get_properties( + peripheral: &Peripheral, +) -> Result, CableError> { + peripheral + .properties() + .await + .or(Err(CableError::TransportUnavailable)) +} diff --git a/libwebauthn-pxp/src/cbor.rs b/libwebauthn-pxp/src/cbor.rs new file mode 100644 index 00000000..9367dd35 --- /dev/null +++ b/libwebauthn-pxp/src/cbor.rs @@ -0,0 +1,3 @@ +//! PXP's own CBOR messages (QR data, advert suffix, post-handshake and update +//! messages). CTAP payloads are never decoded here. +pub(crate) use serde_cbor_2::{from_slice, to_vec, Value}; diff --git a/libwebauthn-pxp/src/connection.rs b/libwebauthn-pxp/src/connection.rs new file mode 100644 index 00000000..eb0f3b0c --- /dev/null +++ b/libwebauthn-pxp/src/connection.rs @@ -0,0 +1,168 @@ +//! Public entry point: open a PXP connection to a CMHD and exchange opaque +//! CTAP frames over it. +use tokio::sync::{mpsc, watch}; +use tokio::task; + +use crate::connection_stages::{MpscUxUpdateSender, TunnelConnectionInput, UxUpdateSender}; +use crate::error::CableError; +use crate::known_devices::CableKnownDevice; +use crate::protocol; +use crate::qr_code_device::CableQrCodeDevice; + +#[derive(Debug, Clone, PartialEq)] +pub enum ConnectionState { + /// Connection is being established (proximity check, connecting, authenticating) + Connecting, + /// Connection is fully established and ready for operations + Connected, + /// Connection has terminated + Terminated, +} + +#[derive(Debug, Clone)] +pub enum CableUpdate { + /// Waiting for proximity check user interaction (eg. scan a QR code, or confirm on the device). + ProximityCheck, + /// Connecting to the tunnel server. + Connecting, + /// Connected to the tunnel server, authenticating the channel. + Authenticating, + /// Connected to the authenticator device via the tunnel server. + Connected, + /// The connection to the authenticator device has failed. + Error(CableError), +} + +/// Receives connection progress updates from the connection task. +pub trait UpdateSink: Send + Sync + 'static { + fn send_update(&self, update: CableUpdate); +} + +/// The CMHD to connect to. +#[derive(Debug, Clone)] +pub enum ConnectTarget { + /// A new device, invoked by scanning a QR code. + QrCode(CableQrCodeDevice), + /// A previously linked device, invoked state-assisted via the tunnel service. + Known(CableKnownDevice), +} + +impl From for ConnectTarget { + fn from(device: CableQrCodeDevice) -> Self { + Self::QrCode(device) + } +} + +impl From for ConnectTarget { + fn from(device: CableKnownDevice) -> Self { + Self::Known(device) + } +} + +/// A running PXP connection. Dropping it aborts the connection task. +#[derive(Debug)] +pub struct TunnelHandle { + /// The connection task: proximity check, handshake, then message exchange. + pub task: task::JoinHandle<()>, + /// Outbound CTAP frames: command byte followed by the CBOR payload. + pub ctap_sender: mpsc::Sender>, + /// Inbound CTAP frames: status byte followed by the CBOR payload. + pub ctap_receiver: mpsc::Receiver>, + pub connection_state: watch::Receiver, +} + +impl TunnelHandle { + /// Waits until the handshake completes, or fails if the connection terminates first. + pub async fn wait_for_connection(&self) -> Result<(), CableError> { + let mut rx = self.connection_state.clone(); + + // If already connected, return immediately + if *rx.borrow() == ConnectionState::Connected { + return Ok(()); + } + + // If already terminated, return error immediately. Mirror the + // post-`changed()` branch below so that an early-terminated channel + // surfaces the same variant as one that terminates while we wait; + // the caller can't observe the timing difference and the asymmetry + // was accidental. + if *rx.borrow() == ConnectionState::Terminated { + return Err(CableError::ConnectionFailed); + } + + // Wait for state change + while rx.changed().await.is_ok() { + match *rx.borrow() { + ConnectionState::Connected => return Ok(()), + ConnectionState::Terminated => return Err(CableError::ConnectionFailed), + ConnectionState::Connecting => continue, + } + } + + // If the sender was dropped, consider it a failure + Err(CableError::ConnectionLost) + } +} + +impl Drop for TunnelHandle { + fn drop(&mut self) { + self.task.abort(); + } +} + +/// Spawns the connection task for `target`. Progress is reported to `updates`; +/// CTAP frames are exchanged through the returned handle once connected. +pub fn connect(target: impl Into, updates: impl UpdateSink) -> TunnelHandle { + let target = target.into(); + let (ctap_tx_send, ctap_tx_recv) = mpsc::channel(16); + let (ctap_rx_send, ctap_rx_recv) = mpsc::channel(16); + let (connection_state_sender, connection_state_receiver) = + watch::channel(ConnectionState::Connecting); + + let task = task::spawn(async move { + let ux_sender = MpscUxUpdateSender::new(Box::new(updates), connection_state_sender); + + let (handshake_output, store) = match &target { + ConnectTarget::QrCode(device) => ( + CableQrCodeDevice::connection(device, &ux_sender).await, + device.store.clone(), + ), + ConnectTarget::Known(device) => ( + CableKnownDevice::connection(device, &ux_sender).await, + Some(device.store.clone()), + ), + }; + let handshake_output = match handshake_output { + Ok(handshake_output) => handshake_output, + Err(e) => { + ux_sender.send_error(e).await; + return; + } + }; + + let tunnel_input = TunnelConnectionInput::from_handshake_output( + handshake_output, + store, + ctap_tx_recv, + ctap_rx_send, + ); + match protocol::connection(tunnel_input).await { + Ok(()) => { + ux_sender + .set_connection_state(ConnectionState::Terminated) + .await; + } + Err(e) => { + // send_error already transitions to Terminated. + ux_sender.send_error(e).await; + } + } + }); + + TunnelHandle { + task, + ctap_sender: ctap_tx_send, + ctap_receiver: ctap_rx_recv, + connection_state: connection_state_receiver, + } +} diff --git a/libwebauthn/src/transport/cable/connection_stages.rs b/libwebauthn-pxp/src/connection_stages.rs similarity index 88% rename from libwebauthn/src/transport/cable/connection_stages.rs rename to libwebauthn-pxp/src/connection_stages.rs index 740c64bc..c1253319 100644 --- a/libwebauthn/src/transport/cable/connection_stages.rs +++ b/libwebauthn-pxp/src/connection_stages.rs @@ -1,10 +1,10 @@ -use ::btleplug::api::{AddressType, BDAddr}; +use ::btleplug::api::{AddressType, BDAddr, PeripheralProperties}; use async_trait::async_trait; -use tokio::sync::{broadcast, mpsc, watch}; +use tokio::sync::{mpsc, watch}; use tracing::{debug, error, info, instrument, trace, warn}; use super::advertisement::{await_advertisement, DecryptedAdvert}; -use super::channel::{CableUpdate, CableUxUpdate, ConnectionState}; +use super::connection::{CableUpdate, ConnectionState, UpdateSink}; use super::crypto::{derive, KeyPurpose}; use super::data_channel::{CableDataChannel, WebSocketDataChannel}; use super::known_devices::{CableKnownDevice, CableKnownDeviceInfoStore, ClientNonce}; @@ -12,9 +12,7 @@ use super::l2cap::L2capDataChannel; use super::protocol::{self, CableTunnelConnectionType, TunnelNoiseState}; use super::qr_code_device::CableQrCodeDevice; use super::tunnel; -use crate::proto::ctap2::cbor::{CborRequest, CborResponse}; -use crate::transport::ble::btleplug::FidoDevice; -use crate::transport::cable::error::CableError; +use crate::error::CableError; use std::sync::Arc; #[derive(Debug)] @@ -47,7 +45,7 @@ impl ProximityCheckInput { #[derive(Debug)] pub(crate) struct ProximityCheckOutput { - pub device: FidoDevice, + pub device: PeripheralProperties, pub advert: DecryptedAdvert, } @@ -99,8 +97,8 @@ impl ConnectionInput { .as_ref() .and_then(|suffix| suffix.ble_psm()) .map(|psm| BleConnectionParams { - address: proximity_output.device.properties.address, - address_type: proximity_output.device.properties.address_type, + address: proximity_output.device.address, + address_type: proximity_output.device.address_type, psm, }); @@ -199,16 +197,16 @@ pub(crate) struct TunnelConnectionInput { pub known_device_store: Option>, pub data_channel: Box, pub noise_state: TunnelNoiseState, - pub cbor_tx_recv: mpsc::Receiver, - pub cbor_rx_send: mpsc::Sender, + pub ctap_tx_recv: mpsc::Receiver>, + pub ctap_rx_send: mpsc::Sender>, } impl TunnelConnectionInput { pub fn from_handshake_output( handshake_output: HandshakeOutput, known_device_store: Option>, - cbor_tx_recv: mpsc::Receiver, - cbor_rx_send: mpsc::Sender, + ctap_tx_recv: mpsc::Receiver>, + ctap_rx_send: mpsc::Sender>, ) -> Self { Self { connection_type: handshake_output.connection_type, @@ -216,27 +214,27 @@ impl TunnelConnectionInput { known_device_store, data_channel: handshake_output.data_channel, noise_state: handshake_output.noise_state, - cbor_tx_recv, - cbor_rx_send, + ctap_tx_recv, + ctap_rx_send, } } } #[async_trait] pub(crate) trait UxUpdateSender: Send + Sync { - async fn send_update(&self, update: CableUxUpdate); + async fn send_update(&self, update: CableUpdate); async fn send_error(&self, error: CableError); async fn set_connection_state(&self, state: ConnectionState); } pub(crate) struct MpscUxUpdateSender { - sender: broadcast::Sender, + sender: Box, connection_state_tx: watch::Sender, } impl MpscUxUpdateSender { pub fn new( - sender: broadcast::Sender, + sender: Box, connection_state_tx: watch::Sender, ) -> Self { Self { @@ -249,16 +247,13 @@ impl MpscUxUpdateSender { #[async_trait] impl UxUpdateSender for MpscUxUpdateSender { #[instrument(skip(self))] - async fn send_update(&self, update: CableUxUpdate) { + async fn send_update(&self, update: CableUpdate) { trace!("Sending UX update"); - if let Err(err) = self.sender.send(update) { - warn!(?err, "No receivers found for UX update."); - } + self.sender.send_update(update); } async fn send_error(&self, error: CableError) { - self.send_update(CableUxUpdate::CableUpdate(CableUpdate::Error(error))) - .await; + self.send_update(CableUpdate::Error(error)).await; let _ = self.connection_state_tx.send(ConnectionState::Terminated); } @@ -274,9 +269,7 @@ pub(crate) async fn proximity_check_stage( ) -> Result { debug!("Starting proximity check stage"); - ux_sender - .send_update(CableUxUpdate::CableUpdate(CableUpdate::ProximityCheck)) - .await; + ux_sender.send_update(CableUpdate::ProximityCheck).await; let (device, advert) = await_advertisement(&input.eid_key).await?; @@ -291,9 +284,7 @@ pub(crate) async fn connection_stage( ) -> Result { debug!(?input.tunnel_domain, "Starting connection stage"); - ux_sender - .send_update(CableUxUpdate::CableUpdate(CableUpdate::Connecting)) - .await; + ux_sender.send_update(CableUpdate::Connecting).await; let data_channel = connect_data_channel(&input).await?; @@ -354,18 +345,14 @@ pub(crate) async fn handshake_stage( ) -> Result { debug!("Starting handshake stage"); - ux_sender - .send_update(CableUxUpdate::CableUpdate(CableUpdate::Authenticating)) - .await; + ux_sender.send_update(CableUpdate::Authenticating).await; let mut data_channel = input.data_channel; let noise_state = protocol::do_handshake(&mut *data_channel, input.psk, &input.connection_type).await?; debug!("Handshake stage completed successfully"); - ux_sender - .send_update(CableUxUpdate::CableUpdate(CableUpdate::Connected)) - .await; + ux_sender.send_update(CableUpdate::Connected).await; ux_sender .set_connection_state(ConnectionState::Connected) diff --git a/libwebauthn/src/transport/cable/crypto.rs b/libwebauthn-pxp/src/crypto.rs similarity index 94% rename from libwebauthn/src/transport/cable/crypto.rs rename to libwebauthn-pxp/src/crypto.rs index b0b721f8..6797ebbc 100644 --- a/libwebauthn/src/transport/cable/crypto.rs +++ b/libwebauthn-pxp/src/crypto.rs @@ -1,11 +1,11 @@ use aes::cipher::{generic_array::GenericArray, BlockDecrypt, KeyInit}; use aes::{Aes256, Block}; use hkdf::Hkdf; +use hmac::{Hmac, Mac}; use sha2::Sha256; use tracing::{instrument, warn}; -use crate::pin::hmac_sha256; -use crate::transport::cable::error::CableError; +use crate::error::CableError; pub enum KeyPurpose { EIDKey = 1, @@ -27,6 +27,13 @@ pub fn derive( Ok(output) } +pub(crate) fn hmac_sha256(key: &[u8], message: &[u8]) -> Result, CableError> { + let mut hmac = + as Mac>::new_from_slice(key).map_err(|_| CableError::InvalidKey)?; + hmac.update(message); + Ok(hmac.finalize().into_bytes().to_vec()) +} + fn reserved_bits_are_zero(plaintext: &[u8]) -> bool { plaintext.first().copied() == Some(0) } @@ -74,9 +81,9 @@ pub fn trial_decrypt_advert(eid_key: &[u8], candidate_advert: &[u8]) -> Option<[ #[cfg(test)] mod tests { use super::derive; + use super::hmac_sha256; use super::trial_decrypt_advert; use super::KeyPurpose; - use crate::pin::hmac_sha256; use aes::cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit}; use aes::{Aes256, Block}; diff --git a/libwebauthn/src/transport/cable/data_channel.rs b/libwebauthn-pxp/src/data_channel.rs similarity index 98% rename from libwebauthn/src/transport/cable/data_channel.rs rename to libwebauthn-pxp/src/data_channel.rs index a2ac9c96..1a74adc5 100644 --- a/libwebauthn/src/transport/cable/data_channel.rs +++ b/libwebauthn-pxp/src/data_channel.rs @@ -6,7 +6,7 @@ use tokio_tungstenite::tungstenite::{Error, Message}; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; use tracing::error; -use crate::transport::cable::error::CableError; +use crate::error::CableError; /// A bidirectional channel carrying discrete protocol messages: the Noise /// handshake messages, then the encrypted CTAP frames. caBLE rides this over a diff --git a/libwebauthn/src/transport/cable/digit_encode.rs b/libwebauthn-pxp/src/digit_encode.rs similarity index 100% rename from libwebauthn/src/transport/cable/digit_encode.rs rename to libwebauthn-pxp/src/digit_encode.rs diff --git a/libwebauthn/src/transport/cable/error.rs b/libwebauthn-pxp/src/error.rs similarity index 94% rename from libwebauthn/src/transport/cable/error.rs rename to libwebauthn-pxp/src/error.rs index 0914ec01..55333731 100644 --- a/libwebauthn/src/transport/cable/error.rs +++ b/libwebauthn-pxp/src/error.rs @@ -5,9 +5,7 @@ use std::sync::Arc; use tokio_tungstenite::tungstenite::http::header::InvalidHeaderValue; use tokio_tungstenite::tungstenite::Error as TungsteniteError; -use crate::proto::ctap2::cbor::CborError; - -/// caBLE transport error. `Clone` because it rides the [`CableUpdate`] UX +/// caBLE transport error. `Clone` because it rides the [`CableUpdate`](crate::CableUpdate) UX /// broadcast stream, which requires `Clone`; non-`Clone` native causes /// (`snow`, `io`, `tungstenite`, `serde_cbor`, `http`) are kept behind an /// `Arc` rather than flattened. @@ -21,7 +19,7 @@ pub enum CableError { #[error("websocket error: {0}")] WebSocket(Arc), #[error("cbor error: {0}")] - Cbor(Arc), + Cbor(Arc), #[error("url parse error: {0}")] Url(#[from] url::ParseError), #[error("uuid parse error: {0}")] @@ -74,8 +72,8 @@ impl From for CableError { } } -impl From for CableError { - fn from(error: CborError) -> Self { +impl From for CableError { + fn from(error: serde_cbor_2::Error) -> Self { CableError::Cbor(Arc::new(error)) } } diff --git a/libwebauthn-pxp/src/get_info.rs b/libwebauthn-pxp/src/get_info.rs new file mode 100644 index 00000000..8adf58fc --- /dev/null +++ b/libwebauthn-pxp/src/get_info.rs @@ -0,0 +1,73 @@ +//! The getInfo response carried in the post-handshake message. Only the +//! members CTAP requires are decoded; the CTAP layer decodes the rest. +use serde_bytes::ByteBuf; +use serde_indexed::DeserializeIndexed; + +#[derive(Debug, Clone, DeserializeIndexed)] +#[allow(dead_code)] +pub(crate) struct GetInfoResponse { + /// versions (0x01) + #[serde(index = 0x01)] + pub versions: Vec, + + /// aaguid (0x03) + #[serde(index = 0x03)] + pub aaguid: ByteBuf, +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use super::*; + use crate::cbor::{self, Value}; + + fn get_info(entries: Vec<(i128, Value)>) -> Vec { + let map: BTreeMap = entries + .into_iter() + .map(|(k, v)| (Value::Integer(k), v)) + .collect(); + cbor::to_vec(&map).unwrap() + } + + fn versions() -> Value { + Value::Array(vec![Value::Text("FIDO_2_0".into())]) + } + + #[test] + fn accepts_minimal_get_info() { + let bytes = get_info(vec![(0x01, versions()), (0x03, Value::Bytes(vec![0; 16]))]); + let info: GetInfoResponse = cbor::from_slice(&bytes).unwrap(); + assert_eq!(info.versions, vec!["FIDO_2_0".to_string()]); + } + + #[test] + fn ignores_other_members() { + let bytes = get_info(vec![ + (0x01, versions()), + (0x03, Value::Bytes(vec![0; 16])), + (0x05, Value::Integer(1200)), + ]); + assert!(cbor::from_slice::(&bytes).is_ok()); + } + + #[test] + fn rejects_missing_aaguid() { + let bytes = get_info(vec![(0x01, versions())]); + assert!(cbor::from_slice::(&bytes).is_err()); + } + + #[test] + fn rejects_mistyped_versions() { + let bytes = get_info(vec![ + (0x01, Value::Integer(2)), + (0x03, Value::Bytes(vec![0; 16])), + ]); + assert!(cbor::from_slice::(&bytes).is_err()); + } + + #[test] + fn rejects_non_map() { + assert!(cbor::from_slice::(&[0x01]).is_err()); + } +} diff --git a/libwebauthn/src/transport/cable/known_devices.rs b/libwebauthn-pxp/src/known_devices.rs similarity index 69% rename from libwebauthn/src/transport/cable/known_devices.rs rename to libwebauthn-pxp/src/known_devices.rs index 01d42cdc..b3913eb1 100644 --- a/libwebauthn/src/transport/cable/known_devices.rs +++ b/libwebauthn-pxp/src/known_devices.rs @@ -2,30 +2,20 @@ use std::collections::HashMap; use std::fmt::{Debug, Display}; use std::sync::Arc; -use crate::transport::cable::channel::ConnectionState; -use crate::transport::cable::connection_stages::{ +use crate::connection_stages::{ connection_stage, handshake_stage, proximity_check_stage, ConnectionInput, HandshakeInput, - HandshakeOutput, MpscUxUpdateSender, ProximityCheckInput, TunnelConnectionInput, - UxUpdateSender, + HandshakeOutput, MpscUxUpdateSender, ProximityCheckInput, }; - -use crate::transport::cable::error::CableError; -use crate::transport::ChannelSettings; -use crate::transport::Device; -use crate::webauthn::error::WebAuthnError; +use crate::error::CableError; use async_trait::async_trait; use futures::lock::Mutex; use serde::Serialize; use serde_bytes::ByteBuf; use serde_indexed::SerializeIndexed; -use tokio::sync::{broadcast, mpsc, watch}; -use tokio::task; use tracing::{debug, instrument, trace}; -use super::channel::CableChannel; -use super::protocol::{self, CableLinkingInfo}; -use super::Cable; +use super::protocol::CableLinkingInfo; #[async_trait] pub trait CableKnownDeviceInfoStore: Debug + Send + Sync { @@ -163,9 +153,9 @@ impl CableKnownDevice { } #[instrument(skip_all, err)] - async fn connection( + pub(crate) async fn connection( known_device: &CableKnownDevice, - ux_sender: &super::connection_stages::MpscUxUpdateSender, + ux_sender: &MpscUxUpdateSender, ) -> Result { let client_nonce = rand::random::(); @@ -190,66 +180,6 @@ impl CableKnownDevice { } } -#[async_trait] -impl<'d> Device<'d, Cable, CableChannel> for CableKnownDevice { - async fn channel( - &'d mut self, - settings: ChannelSettings, - ) -> Result> { - debug!(?self.device_info.tunnel_domain, "Creating channel to tunnel server"); - - let (ux_update_sender, _) = broadcast::channel(16); - let (cbor_tx_send, cbor_tx_recv) = mpsc::channel(16); - let (cbor_rx_send, cbor_rx_recv) = mpsc::channel(16); - let (connection_state_sender, connection_state_receiver) = - watch::channel(ConnectionState::Connecting); - - let ux_update_sender_clone = ux_update_sender.clone(); - let known_device: CableKnownDevice = self.clone(); - - let handle_connection = task::spawn(async move { - let ux_sender = - MpscUxUpdateSender::new(ux_update_sender_clone, connection_state_sender); - - let handshake_output = match Self::connection(&known_device, &ux_sender).await { - Ok(handshake_output) => handshake_output, - Err(e) => { - ux_sender.send_error(e).await; - return; - } - }; - - let tunnel_input = TunnelConnectionInput::from_handshake_output( - handshake_output, - Some(known_device.store), - cbor_tx_recv, - cbor_rx_send, - ); - - match protocol::connection(tunnel_input).await { - Ok(()) => { - ux_sender - .set_connection_state(ConnectionState::Terminated) - .await; - } - Err(e) => { - // send_error already transitions to Terminated. - ux_sender.send_error(e).await; - } - } - }); - - Ok(CableChannel { - handle_connection, - cbor_sender: cbor_tx_send, - cbor_receiver: cbor_rx_recv, - ux_update_sender, - connection_state_receiver, - persistent_token_store: settings.persistent_token_store, - }) - } -} - pub(crate) type ClientNonce = [u8; 16]; // Key 3: either the string “ga” to hint that a getAssertion will follow, or “mc” to hint that a makeCredential will follow. @@ -275,7 +205,7 @@ pub enum ClientPayloadHint { #[cfg(test)] mod tests { - use crate::transport::cable::tunnel::KNOWN_TUNNEL_DOMAINS; + use crate::tunnel::KNOWN_TUNNEL_DOMAINS; #[test] fn known_tunnels_domains_count() { diff --git a/libwebauthn/src/transport/cable/l2cap.rs b/libwebauthn-pxp/src/l2cap.rs similarity index 99% rename from libwebauthn/src/transport/cable/l2cap.rs rename to libwebauthn-pxp/src/l2cap.rs index f9459e93..49a93a42 100644 --- a/libwebauthn/src/transport/cable/l2cap.rs +++ b/libwebauthn-pxp/src/l2cap.rs @@ -9,7 +9,7 @@ use tokio::time::Instant; use tracing::{debug, error, warn}; use super::data_channel::CableDataChannel; -use crate::transport::cable::error::CableError; +use crate::error::CableError; /// End-of-Message sequence terminating every L2CAP message (CRLF). const EOM: [u8; 2] = [0x0D, 0x0A]; diff --git a/libwebauthn-pxp/src/lib.rs b/libwebauthn-pxp/src/lib.rs new file mode 100644 index 00000000..c03367b8 --- /dev/null +++ b/libwebauthn-pxp/src/lib.rs @@ -0,0 +1,43 @@ +//! Client platform side of the FIDO Proximity Exchange Protocol (PXP), +//! formerly the CTAP hybrid transport (caBLE). +//! +//! PXP covers invocation (QR code or state-assisted), proximity proof over a +//! BLE advertisement, channel negotiation (WebSocket tunnel or BLE L2CAP), the +//! Noise handshake, and the encrypted message exchange. CTAP messages are +//! carried as opaque frames. The only CTAP this crate knows is what PXP itself +//! requires: the getInfo command byte and the getInfo response structure. +//! `libwebauthn` builds its hybrid transport on top. +//! +//! See . + +// Production code must not panic. Tests keep unwrap/expect/panic latitude +// through `not(test)`. +#![cfg_attr(not(test), deny(clippy::unwrap_used))] +#![cfg_attr(not(test), deny(clippy::expect_used))] +#![cfg_attr(not(test), deny(clippy::panic))] +#![cfg_attr(not(test), deny(clippy::todo))] +#![cfg_attr(not(test), deny(clippy::unreachable))] +#![cfg_attr(not(test), deny(clippy::indexing_slicing))] +#![cfg_attr(not(test), deny(clippy::unwrap_in_result))] + +mod ble; +mod cbor; +mod connection_stages; +mod crypto; +mod data_channel; +mod digit_encode; +mod get_info; +mod l2cap; +mod protocol; + +pub mod advertisement; +pub mod connection; +pub mod error; +pub mod known_devices; +pub mod qr_code_device; +pub mod tunnel; + +pub use connection::{ + connect, CableUpdate, ConnectTarget, ConnectionState, TunnelHandle, UpdateSink, +}; +pub use digit_encode::digit_encode; diff --git a/libwebauthn/src/transport/cable/protocol.rs b/libwebauthn-pxp/src/protocol.rs similarity index 91% rename from libwebauthn/src/transport/cable/protocol.rs rename to libwebauthn-pxp/src/protocol.rs index 73f68bb8..6f9fbe8f 100644 --- a/libwebauthn/src/transport/cable/protocol.rs +++ b/libwebauthn-pxp/src/protocol.rs @@ -18,11 +18,11 @@ use tracing::{debug, error, trace, warn}; use super::data_channel::CableDataChannel; use super::known_devices::ClientPayload; use super::known_devices::{CableKnownDeviceInfo, CableKnownDeviceInfoStore}; -use crate::proto::ctap2::cbor::{self, CborRequest, CborResponse, Value}; -use crate::proto::ctap2::{Ctap2CommandCode, Ctap2GetInfoResponse}; -use crate::transport::cable::connection_stages::TunnelConnectionInput; -use crate::transport::cable::error::CableError; -use crate::transport::cable::known_devices::CableKnownDeviceId; +use crate::cbor::{self, Value}; +use crate::connection_stages::TunnelConnectionInput; +use crate::error::CableError; +use crate::get_info::GetInfoResponse; +use crate::known_devices::CableKnownDeviceId; const P256_X962_LENGTH: usize = 65; const MAX_CBOR_SIZE: usize = 1024 * 1024; @@ -31,6 +31,12 @@ const PADDING_GRANULARITY: usize = 32; const CABLE_PROLOGUE_STATE_ASSISTED: &[u8] = &[0u8]; const CABLE_PROLOGUE_QR_INITIATED: &[u8] = &[1u8]; +/// CTAP2 authenticatorGetInfo command byte. The only CTAP fact PXP relies on: +/// the post-handshake message already carries the getInfo response. +const CTAP2_GET_INFO: u8 = 0x04; +/// CTAP2 success status byte, prefixed to the cached getInfo response. +const CTAP2_OK: u8 = 0x00; + #[derive(Debug, Clone)] struct CableTunnelMessage { message_type: CableTunnelMessageType, @@ -83,9 +89,27 @@ struct CableInitialMessage { #[serde(index = 0x01)] pub info: ByteBuf, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(skip_serializing_if = "SupportedFeatures::is_default")] #[serde(index = 0x03)] - pub _supported_features: Option>, + pub supported_features: SupportedFeatures, +} + +/// Post-handshake message key 3. Its absence MUST be treated as `["ctap"]`. +#[derive(Clone, Debug, PartialEq, Deserialize)] +#[serde(transparent)] +struct SupportedFeatures(Vec); + +impl Default for SupportedFeatures { + fn default() -> Self { + Self(vec!["ctap".to_string()]) + } +} + +impl SupportedFeatures { + #[allow(dead_code)] + fn is_default(&self) -> bool { + *self == Self::default() + } } #[derive(Clone, Debug)] @@ -307,7 +331,7 @@ pub(crate) async fn connection(mut input: TunnelConnectionInput) -> Result<(), C &input.tunnel_domain, &input.known_device_store, message, - &input.cbor_rx_send, + &input.ctap_rx_send, &mut input.noise_state, ) .await @@ -330,19 +354,20 @@ pub(crate) async fn connection(mut input: TunnelConnectionInput) -> Result<(), C } } } - Some(request) = input.cbor_tx_recv.recv() => { - match request.command { + Some(request) = input.ctap_tx_recv.recv() => { + match request.first() { // Optimisation: respond to GetInfo requests immediately with the cached response - Ctap2CommandCode::AuthenticatorGetInfo => { + Some(&CTAP2_GET_INFO) => { debug!("Responding to GetInfo request with cached response"); - let response = CborResponse::new_success_from_slice(&get_info_response_serialized); - if let Err(e) = input.cbor_rx_send.send(response).await { - error!(?e, "CBOR response receiver dropped"); + let mut response = vec![CTAP2_OK]; + response.extend_from_slice(&get_info_response_serialized); + if let Err(e) = input.ctap_rx_send.send(response).await { + error!(?e, "CTAP response receiver dropped"); return Err(CableError::ConnectionFailed); } } - _ => { - debug!(?request.command, "Sending CBOR request"); + command => { + debug!(?command, "Sending CTAP request"); if let Err(e) = connection_send( request, &mut *input.data_channel, @@ -361,14 +386,11 @@ pub(crate) async fn connection(mut input: TunnelConnectionInput) -> Result<(), C } async fn connection_send( - request: CborRequest, + cbor_request: Vec, data_channel: &mut dyn CableDataChannel, noise_state: &mut TunnelNoiseState, ) -> Result<(), CableError> { debug!("Sending CBOR request"); - trace!(?request); - - let cbor_request = request.raw_long().map_err(CableError::from)?; if cbor_request.len() > MAX_CBOR_SIZE { error!( cbor_request_len = cbor_request.len(), @@ -481,13 +503,12 @@ async fn connection_recv_initial( } }; - let _: Ctap2GetInfoResponse = match cbor::from_slice(&initial_message.info) { - Ok(get_info_response) => get_info_response, - Err(e) => { - error!(?e, "Failed to decode GetInfo response"); - return Err(CableError::InvalidFraming); - } - }; + if let Err(e) = cbor::from_slice::(&initial_message.info) { + error!(?e, "Failed to decode GetInfo response"); + return Err(CableError::InvalidFraming); + } + + debug!(?initial_message.supported_features, "Received post-handshake message"); Ok(initial_message.info.to_vec()) } @@ -560,7 +581,7 @@ async fn connection_recv( tunnel_domain: &str, known_device_store: &Option>, encrypted_frame: Vec, - cbor_rx_send: &Sender, + ctap_rx_send: &Sender>, noise_state: &mut TunnelNoiseState, ) -> Result { let decrypted_frame = decrypt_frame(encrypted_frame, noise_state).await?; @@ -575,14 +596,10 @@ async fn connection_recv( Ok(RecvOutcome::PeerShutdown) } CableTunnelMessageType::Ctap => { - let cbor_response: CborResponse = (&cable_message.payload.to_vec()) - .try_into() - .or(Err(CableError::InvalidFraming))?; - - debug!("Received CBOR response"); - trace!(?cbor_response); - cbor_rx_send - .send(cbor_response) + debug!("Received CTAP response"); + trace!(?cable_message.payload); + ctap_rx_send + .send(cable_message.payload.into_vec()) .await .or(Err(CableError::ConnectionFailed))?; Ok(RecvOutcome::Continue) @@ -773,6 +790,14 @@ mod tests { ); } + #[test] + fn absent_supported_features_defaults_to_ctap() { + let map = BTreeMap::from([(Value::Integer(1), Value::Bytes(vec![]))]); + let bytes = cbor::to_vec(&map).unwrap(); + let message: CableInitialMessage = cbor::from_slice(&bytes).unwrap(); + assert_eq!(message.supported_features.0, vec!["ctap"]); + } + #[test] fn strip_frame_padding_rejects_empty() { let result = strip_frame_padding(Vec::new()); diff --git a/libwebauthn/src/transport/cable/qr_code_device.rs b/libwebauthn-pxp/src/qr_code_device.rs similarity index 78% rename from libwebauthn/src/transport/cable/qr_code_device.rs rename to libwebauthn-pxp/src/qr_code_device.rs index 5f85e8a3..34b3314c 100644 --- a/libwebauthn/src/transport/cable/qr_code_device.rs +++ b/libwebauthn-pxp/src/qr_code_device.rs @@ -2,7 +2,6 @@ use std::fmt::{Debug, Display}; use std::sync::Arc; use std::time::SystemTime; -use async_trait::async_trait; use p256::elliptic_curve::sec1::ToEncodedPoint; use p256::{NonZeroScalar, SecretKey}; use rand::rngs::OsRng; @@ -11,24 +10,17 @@ use serde::Serialize; use serde_bytes::ByteArray; use serde_indexed::SerializeIndexed; use serde_repr::Serialize_repr; -use tokio::sync::{broadcast, mpsc, watch}; -use tokio::task; use tracing::instrument; use super::connection_stages::{ connection_stage, handshake_stage, proximity_check_stage, ConnectionInput, HandshakeInput, - MpscUxUpdateSender, ProximityCheckInput, TunnelConnectionInput, UxUpdateSender, + MpscUxUpdateSender, ProximityCheckInput, }; use super::known_devices::CableKnownDeviceInfoStore; -use super::protocol; use super::tunnel::KNOWN_TUNNEL_DOMAINS; -use super::{channel::CableChannel, channel::ConnectionState, Cable}; -use crate::proto::ctap2::cbor; -use crate::transport::cable::digit_encode; -use crate::transport::cable::error::CableError; -use crate::transport::ChannelSettings; -use crate::transport::Device; -use crate::webauthn::error::WebAuthnError; +use crate::cbor; +use crate::digit_encode; +use crate::error::CableError; #[derive(Debug, Clone, Copy, Serialize, PartialEq)] pub enum QrCodeOperationHint { @@ -211,7 +203,7 @@ impl CableQrCodeDevice { } #[instrument(skip_all, err)] - async fn connection( + pub(crate) async fn connection( qr_device: &CableQrCodeDevice, ux_sender: &MpscUxUpdateSender, ) -> Result { @@ -238,68 +230,6 @@ impl Display for CableQrCodeDevice { } } -#[async_trait] -impl<'d> Device<'d, Cable, CableChannel> for CableQrCodeDevice { - async fn channel( - &'d mut self, - settings: ChannelSettings, - ) -> Result> { - let (ux_update_sender, _) = broadcast::channel(16); - let (cbor_tx_send, cbor_tx_recv) = mpsc::channel(16); - let (cbor_rx_send, cbor_rx_recv) = mpsc::channel(16); - let (connection_state_sender, connection_state_receiver) = - watch::channel(ConnectionState::Connecting); - - let ux_update_sender_clone = ux_update_sender.clone(); - let qr_device = self.clone(); - - let handle_connection = task::spawn(async move { - let ux_sender = - MpscUxUpdateSender::new(ux_update_sender_clone.clone(), connection_state_sender); - - let handshake_output = match Self::connection(&qr_device, &ux_sender).await { - Ok(handshake_output) => handshake_output, - Err(e) => { - ux_sender.send_error(e).await; - return; - } - }; - - let tunnel_input = TunnelConnectionInput::from_handshake_output( - handshake_output, - qr_device.store, - cbor_tx_recv, - cbor_rx_send, - ); - match protocol::connection(tunnel_input).await { - Ok(()) => { - ux_sender - .set_connection_state(ConnectionState::Terminated) - .await; - } - Err(e) => { - // send_error already transitions to Terminated. - ux_sender.send_error(e).await; - } - } - }); - - Ok(CableChannel { - handle_connection, - cbor_sender: cbor_tx_send, - cbor_receiver: cbor_rx_recv, - ux_update_sender, - connection_state_receiver, - persistent_token_store: settings.persistent_token_store, - }) - } - - // #[instrument(skip_all)] - // async fn supported_protocols(&mut self) -> Result { - // Ok(SupportedProtocols::fido2_only()) - // } -} - #[cfg(test)] mod tests { use super::*; diff --git a/libwebauthn/src/transport/cable/tunnel.rs b/libwebauthn-pxp/src/tunnel.rs similarity index 98% rename from libwebauthn/src/transport/cable/tunnel.rs rename to libwebauthn-pxp/src/tunnel.rs index 528bcc66..edba250f 100644 --- a/libwebauthn/src/transport/cable/tunnel.rs +++ b/libwebauthn-pxp/src/tunnel.rs @@ -12,8 +12,8 @@ use url::Url; use super::error::CableTunnelError; use super::known_devices::CableKnownDeviceId; use super::protocol::CableTunnelConnectionType; -use crate::proto::ctap2::cbor; -use crate::transport::cable::error::CableError; +use crate::cbor; +use crate::error::CableError; const MAX_TUNNEL_REDIRECTS: usize = 5; @@ -186,7 +186,7 @@ pub(crate) async fn connect( #[cfg(test)] mod tests { use super::*; - use crate::transport::cable::known_devices::{ClientPayload, ClientPayloadHint}; + use crate::known_devices::{ClientPayload, ClientPayloadHint}; use p256::NonZeroScalar; use rand::rngs::OsRng; use serde_bytes::ByteBuf; diff --git a/libwebauthn/Cargo.toml b/libwebauthn/Cargo.toml index a59328b2..f3da133a 100644 --- a/libwebauthn/Cargo.toml +++ b/libwebauthn/Cargo.toml @@ -93,16 +93,10 @@ cbc = { version = "0.1", features = ["alloc"] } hkdf = "0.12" zeroize = { version = "1.8", features = ["derive"] } text_io = "0.1" -tungstenite = { version = "0.26.2" } -tokio-tungstenite = { version = "0.26", features = [ - "rustls-tls-native-roots", -] } -rustls = { version = "0.23.27", default-features = false, features = ["ring"] } tokio-stream = "0.1" -snow = { version = "0.10", features = ["use-p256"] } ctap-types = { version = "0.4.0" } btleplug = "0.11.7" -bluer = { version = "0.17", default-features = false, features = ["l2cap"] } +libwebauthn-pxp = { version = "0.10.0", path = "../libwebauthn-pxp" } thiserror = "2.0.12" serde_json = "1.0.141" apdu-core = { version = "0.4.0", optional = true } diff --git a/libwebauthn/src/transport/cable/channel.rs b/libwebauthn/src/transport/cable/channel.rs index 5bc1cacb..204f10fe 100644 --- a/libwebauthn/src/transport/cable/channel.rs +++ b/libwebauthn/src/transport/cable/channel.rs @@ -3,8 +3,9 @@ use std::sync::Arc; use std::time::Duration; use async_trait::async_trait; -use tokio::sync::{broadcast, mpsc, watch}; -use tokio::{task, time}; +use libwebauthn_pxp::TunnelHandle; +use tokio::sync::broadcast; +use tokio::time; use tracing::error; use crate::pin::persistent_token::PersistentTokenStore; @@ -12,7 +13,6 @@ use crate::proto::{ ctap1::apdu::{ApduRequest, ApduResponse}, ctap2::cbor::{CborRequest, CborResponse}, }; -use crate::transport::cable::error::CableError; use crate::transport::AuthTokenData; use crate::transport::{ channel::ChannelStatus, device::SupportedProtocols, Channel, Ctap2AuthTokenStore, @@ -21,18 +21,11 @@ use crate::webauthn::error::WebAuthnError; use crate::Transport; use crate::UvUpdate; +use super::error::CableError; use super::known_devices::CableKnownDevice; use super::qr_code_device::CableQrCodeDevice; -#[derive(Debug, Clone, PartialEq)] -pub enum ConnectionState { - /// Connection is being established (proximity check, connecting, authenticating) - Connecting, - /// Connection is fully established and ready for operations - Connected, - /// Connection has terminated - Terminated, -} +pub use libwebauthn_pxp::{CableUpdate, ConnectionState}; #[derive(Debug)] pub enum CableChannelDevice<'d> { @@ -42,78 +35,23 @@ pub enum CableChannelDevice<'d> { #[derive(Debug)] pub struct CableChannel { - pub(crate) handle_connection: task::JoinHandle<()>, - pub(crate) cbor_sender: mpsc::Sender, - pub(crate) cbor_receiver: mpsc::Receiver, + pub(crate) tunnel: TunnelHandle, pub(crate) ux_update_sender: broadcast::Sender, - pub(crate) connection_state_receiver: watch::Receiver, pub(crate) persistent_token_store: Option>, } -impl CableChannel { - async fn wait_for_connection(&self) -> Result<(), CableError> { - let mut rx = self.connection_state_receiver.clone(); - - // If already connected, return immediately - if *rx.borrow() == ConnectionState::Connected { - return Ok(()); - } - - // If already terminated, return error immediately. Mirror the - // post-`changed()` branch below so that an early-terminated channel - // surfaces the same variant as one that terminates while we wait; - // the caller can't observe the timing difference and the asymmetry - // was accidental. - if *rx.borrow() == ConnectionState::Terminated { - return Err(CableError::ConnectionFailed); - } - - // Wait for state change - while rx.changed().await.is_ok() { - match *rx.borrow() { - ConnectionState::Connected => return Ok(()), - ConnectionState::Terminated => return Err(CableError::ConnectionFailed), - ConnectionState::Connecting => continue, - } - } - - // If the sender was dropped, consider it a failure - Err(CableError::ConnectionLost) - } -} - impl Display for CableChannel { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "CableChannel") } } -impl Drop for CableChannel { - fn drop(&mut self) { - self.handle_connection.abort(); - } -} - #[derive(Debug, Clone)] pub enum CableUxUpdate { UvUpdate(UvUpdate), CableUpdate(CableUpdate), } -#[derive(Debug, Clone)] -pub enum CableUpdate { - /// Waiting for proximity check user interaction (eg. scan a QR code, or confirm on the device). - ProximityCheck, - /// Connecting to the tunnel server. - Connecting, - /// Connected to the tunnel server, authenticating the channel. - Authenticating, - /// Connected to the authenticator device via the tunnel server. - Connected, - /// The connection to the authenticator device has failed. - Error(CableError), -} - impl From for CableUxUpdate { fn from(update: UvUpdate) -> Self { CableUxUpdate::UvUpdate(update) @@ -134,7 +72,7 @@ impl Channel for CableChannel { } async fn status(&self) -> ChannelStatus { - match self.handle_connection.is_finished() { + match self.tunnel.task.is_finished() { true => ChannelStatus::Closed, false => ChannelStatus::Ready, } @@ -164,10 +102,11 @@ impl Channel for CableChannel { timeout: Duration, ) -> Result<(), CableError> { // First, wait for connection to be established (no timeout for handshake) - self.wait_for_connection().await?; + self.tunnel.wait_for_connection().await?; // Now apply timeout only to the actual CBOR operation - match time::timeout(timeout, self.cbor_sender.send(request.clone())).await { + let frame = request.ctap_hid_data(); + match time::timeout(timeout, self.tunnel.ctap_sender.send(frame)).await { Ok(Ok(_)) => Ok(()), Ok(Err(error)) => { error!(%error, "CBOR request send failure"); @@ -182,11 +121,14 @@ impl Channel for CableChannel { async fn cbor_recv(&mut self, timeout: Duration) -> Result { // First, wait for connection to be established (no timeout for handshake) - self.wait_for_connection().await?; + self.tunnel.wait_for_connection().await?; // Now apply timeout only to the actual CBOR operation - match time::timeout(timeout, self.cbor_receiver.recv()).await { - Ok(Some(response)) => Ok(response), + match time::timeout(timeout, self.tunnel.ctap_receiver.recv()).await { + Ok(Some(frame)) => CborResponse::try_from(&frame).map_err(|e| { + error!(%e, "Malformed CTAP response frame"); + CableError::InvalidFraming + }), Ok(None) => Err(CableError::TransportUnavailable), Err(elapsed) => { error!({ %elapsed, ?timeout }, "CBOR response recv timeout"); diff --git a/libwebauthn/src/transport/cable/device.rs b/libwebauthn/src/transport/cable/device.rs new file mode 100644 index 00000000..97c5b062 --- /dev/null +++ b/libwebauthn/src/transport/cable/device.rs @@ -0,0 +1,56 @@ +//! [`Device`] impls opening a [`CableChannel`] to a PXP device. +use async_trait::async_trait; +use libwebauthn_pxp::{CableUpdate, ConnectTarget, UpdateSink}; +use tokio::sync::broadcast; +use tracing::{debug, trace, warn}; + +use super::channel::{CableChannel, CableUxUpdate}; +use super::error::CableError; +use super::known_devices::CableKnownDevice; +use super::qr_code_device::CableQrCodeDevice; +use super::Cable; +use crate::transport::{ChannelSettings, Device}; +use crate::webauthn::error::WebAuthnError; + +/// Forwards PXP connection updates onto the channel's UX update stream. +struct UxUpdateForwarder(broadcast::Sender); + +impl UpdateSink for UxUpdateForwarder { + fn send_update(&self, update: CableUpdate) { + trace!("Sending UX update"); + if let Err(err) = self.0.send(CableUxUpdate::CableUpdate(update)) { + warn!(?err, "No receivers found for UX update."); + } + } +} + +fn open_channel(target: ConnectTarget, settings: ChannelSettings) -> CableChannel { + let (ux_update_sender, _) = broadcast::channel(16); + let tunnel = libwebauthn_pxp::connect(target, UxUpdateForwarder(ux_update_sender.clone())); + CableChannel { + tunnel, + ux_update_sender, + persistent_token_store: settings.persistent_token_store, + } +} + +#[async_trait] +impl<'d> Device<'d, Cable, CableChannel> for CableQrCodeDevice { + async fn channel( + &'d mut self, + settings: ChannelSettings, + ) -> Result> { + Ok(open_channel(self.clone().into(), settings)) + } +} + +#[async_trait] +impl<'d> Device<'d, Cable, CableChannel> for CableKnownDevice { + async fn channel( + &'d mut self, + settings: ChannelSettings, + ) -> Result> { + debug!(?self.device_info.tunnel_domain, "Creating channel to tunnel server"); + Ok(open_channel(self.clone().into(), settings)) + } +} diff --git a/libwebauthn/src/transport/cable/mod.rs b/libwebauthn/src/transport/cable/mod.rs index 5258344e..8a0da155 100644 --- a/libwebauthn/src/transport/cable/mod.rs +++ b/libwebauthn/src/transport/cable/mod.rs @@ -1,21 +1,16 @@ +//! Hybrid transport: `libwebauthn`'s `Channel` / `Device` adapter over the +//! Proximity Exchange Protocol implemented in [`libwebauthn_pxp`]. use std::fmt::Display; -mod crypto; -mod data_channel; -mod digit_encode; -mod l2cap; -mod protocol; +mod device; -pub mod advertisement; pub mod channel; -pub mod connection_stages; -pub mod error; -pub mod known_devices; -pub mod qr_code_device; -pub mod tunnel; + +pub use libwebauthn_pxp::{ + advertisement, digit_encode, error, known_devices, qr_code_device, tunnel, +}; use super::Transport; -pub use digit_encode::digit_encode; /// Checks if the Cable/Hybrid transport is available on the system. /// Cable depends on a Bluetooth adapter for BLE advertisement discovery. diff --git a/libwebauthn/src/transport/cable/stages.rs b/libwebauthn/src/transport/cable/stages.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/libwebauthn/src/transport/error.rs b/libwebauthn/src/transport/error.rs index 2200ec0a..eb66703b 100644 --- a/libwebauthn/src/transport/error.rs +++ b/libwebauthn/src/transport/error.rs @@ -1,4 +1,4 @@ //! Per-transport errors live with each transport (e.g. `hid::HidError`, -//! `ble::BleError`, `cable::CableError`). The ceremony error is +//! `ble::BleError`, `cable::CableError` from `libwebauthn-pxp`). The ceremony error is //! [`WebAuthnError`](crate::webauthn::error::WebAuthnError), generic over the //! channel's concrete transport error. From 9f54bfcb107ad0cebb03b091c943063a26645590 Mon Sep 17 00:00:00 2001 From: Mike Beaumont Date: Mon, 28 Sep 2026 16:01:42 +0200 Subject: [PATCH 2/3] refactor: rename libwebauthn::transport::cable to hybrid --- libwebauthn/examples/ceremony/webauthn_cable.rs | 4 ++-- libwebauthn/examples/ceremony/webauthn_cable_wss.rs | 8 ++++---- libwebauthn/examples/common/mod.rs | 2 +- libwebauthn/examples/features/webauthn_prf_cable.rs | 6 +++--- libwebauthn/src/transport/error.rs | 2 +- libwebauthn/src/transport/{cable => hybrid}/channel.rs | 0 libwebauthn/src/transport/{cable => hybrid}/device.rs | 0 libwebauthn/src/transport/{cable => hybrid}/mod.rs | 0 libwebauthn/src/transport/mod.rs | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) rename libwebauthn/src/transport/{cable => hybrid}/channel.rs (100%) rename libwebauthn/src/transport/{cable => hybrid}/device.rs (100%) rename libwebauthn/src/transport/{cable => hybrid}/mod.rs (100%) diff --git a/libwebauthn/examples/ceremony/webauthn_cable.rs b/libwebauthn/examples/ceremony/webauthn_cable.rs index 975c3a67..a409928a 100644 --- a/libwebauthn/examples/ceremony/webauthn_cable.rs +++ b/libwebauthn/examples/ceremony/webauthn_cable.rs @@ -3,8 +3,8 @@ //! MakeCredential only. use std::error::Error; -use libwebauthn::transport::cable::is_available; -use libwebauthn::transport::cable::qr_code_device::{ +use libwebauthn::transport::hybrid::is_available; +use libwebauthn::transport::hybrid::qr_code_device::{ CableQrCodeDevice, CableTransports, QrCodeOperationHint, }; use qrcode::render::unicode; diff --git a/libwebauthn/examples/ceremony/webauthn_cable_wss.rs b/libwebauthn/examples/ceremony/webauthn_cable_wss.rs index e9555583..390c3216 100644 --- a/libwebauthn/examples/ceremony/webauthn_cable_wss.rs +++ b/libwebauthn/examples/ceremony/webauthn_cable_wss.rs @@ -2,11 +2,11 @@ use std::error::Error; use std::sync::Arc; use std::time::Duration; -use libwebauthn::transport::cable::is_available; -use libwebauthn::transport::cable::known_devices::{ +use libwebauthn::transport::hybrid::is_available; +use libwebauthn::transport::hybrid::known_devices::{ CableKnownDevice, ClientPayloadHint, EphemeralDeviceInfoStore, }; -use libwebauthn::transport::cable::qr_code_device::{ +use libwebauthn::transport::hybrid::qr_code_device::{ CableQrCodeDevice, CableTransports, QrCodeOperationHint, }; use qrcode::render::unicode; @@ -17,7 +17,7 @@ use libwebauthn::ops::webauthn::{ GetAssertionRequest, JsonFormat, MakeCredentialRequest, OriginValidation, RelatedOrigins, RequestOrigin, RequestSettings, SystemPublicSuffixList, WebAuthnIDLResponse as _, }; -use libwebauthn::transport::cable::channel::CableChannel; +use libwebauthn::transport::hybrid::channel::CableChannel; use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; use libwebauthn::webauthn::WebAuthn; diff --git a/libwebauthn/examples/common/mod.rs b/libwebauthn/examples/common/mod.rs index 6cfa33af..091d2f5a 100644 --- a/libwebauthn/examples/common/mod.rs +++ b/libwebauthn/examples/common/mod.rs @@ -9,7 +9,7 @@ use std::io::{self, Write}; use libwebauthn::pin::{PinNotSetReason, PinRequestReason}; -use libwebauthn::transport::cable::channel::{CableUpdate, CableUxUpdate}; +use libwebauthn::transport::hybrid::channel::{CableUpdate, CableUxUpdate}; use libwebauthn::UvUpdate; use text_io::read; use tokio::sync::broadcast::Receiver; diff --git a/libwebauthn/examples/features/webauthn_prf_cable.rs b/libwebauthn/examples/features/webauthn_prf_cable.rs index 6d5f6f24..26238e3b 100644 --- a/libwebauthn/examples/features/webauthn_prf_cable.rs +++ b/libwebauthn/examples/features/webauthn_prf_cable.rs @@ -22,9 +22,9 @@ use libwebauthn::proto::ctap2::{ Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, Ctap2PublicKeyCredentialType, Ctap2PublicKeyCredentialUserEntity, }; -use libwebauthn::transport::cable::channel::CableChannel; -use libwebauthn::transport::cable::is_available; -use libwebauthn::transport::cable::qr_code_device::{ +use libwebauthn::transport::hybrid::channel::CableChannel; +use libwebauthn::transport::hybrid::is_available; +use libwebauthn::transport::hybrid::qr_code_device::{ CableQrCodeDevice, CableTransports, QrCodeOperationHint, }; use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; diff --git a/libwebauthn/src/transport/error.rs b/libwebauthn/src/transport/error.rs index eb66703b..36523836 100644 --- a/libwebauthn/src/transport/error.rs +++ b/libwebauthn/src/transport/error.rs @@ -1,4 +1,4 @@ //! Per-transport errors live with each transport (e.g. `hid::HidError`, -//! `ble::BleError`, `cable::CableError` from `libwebauthn-pxp`). The ceremony error is +//! `ble::BleError`, `hybrid::CableError` from `libwebauthn-pxp`). The ceremony error is //! [`WebAuthnError`](crate::webauthn::error::WebAuthnError), generic over the //! channel's concrete transport error. diff --git a/libwebauthn/src/transport/cable/channel.rs b/libwebauthn/src/transport/hybrid/channel.rs similarity index 100% rename from libwebauthn/src/transport/cable/channel.rs rename to libwebauthn/src/transport/hybrid/channel.rs diff --git a/libwebauthn/src/transport/cable/device.rs b/libwebauthn/src/transport/hybrid/device.rs similarity index 100% rename from libwebauthn/src/transport/cable/device.rs rename to libwebauthn/src/transport/hybrid/device.rs diff --git a/libwebauthn/src/transport/cable/mod.rs b/libwebauthn/src/transport/hybrid/mod.rs similarity index 100% rename from libwebauthn/src/transport/cable/mod.rs rename to libwebauthn/src/transport/hybrid/mod.rs diff --git a/libwebauthn/src/transport/mod.rs b/libwebauthn/src/transport/mod.rs index b4cc7014..8e156f22 100644 --- a/libwebauthn/src/transport/mod.rs +++ b/libwebauthn/src/transport/mod.rs @@ -14,9 +14,9 @@ pub(crate) mod error; pub mod ble; -pub mod cable; pub mod device; pub mod hid; +pub mod hybrid; #[cfg(test)] /// A mock channel that can be used in tests to /// queue expected requests and responses in unittests From 7b0a7f834140b07275fd39815a4e4268b9a15619 Mon Sep 17 00:00:00 2001 From: Mike Beaumont Date: Mon, 28 Sep 2026 22:14:18 +0200 Subject: [PATCH 3/3] refactor: renames --- libwebauthn-pxp/src/advertisement.rs | 16 +- libwebauthn-pxp/src/ble.rs | 20 +- libwebauthn-pxp/src/connection.rs | 36 ++-- libwebauthn-pxp/src/connection_stages.rs | 98 +++++----- libwebauthn-pxp/src/crypto.rs | 11 +- libwebauthn-pxp/src/data_channel.rs | 26 +-- libwebauthn-pxp/src/error.rs | 28 +-- libwebauthn-pxp/src/known_devices.rs | 70 ++++--- libwebauthn-pxp/src/l2cap.rs | 30 +-- libwebauthn-pxp/src/lib.rs | 2 +- libwebauthn-pxp/src/protocol.rs | 184 +++++++++--------- libwebauthn-pxp/src/qr_code_device.rs | 78 ++++---- libwebauthn-pxp/src/tunnel.rs | 74 ++++--- .../examples/ceremony/webauthn_cable.rs | 8 +- .../examples/ceremony/webauthn_cable_wss.rs | 22 +-- libwebauthn/examples/common/mod.rs | 18 +- .../examples/features/webauthn_prf_cable.rs | 12 +- libwebauthn/src/transport/error.rs | 2 +- libwebauthn/src/transport/hybrid/channel.rs | 66 +++---- libwebauthn/src/transport/hybrid/device.rs | 32 +-- libwebauthn/src/transport/hybrid/mod.rs | 12 +- libwebauthn/src/webauthn.rs | 2 +- 22 files changed, 418 insertions(+), 429 deletions(-) diff --git a/libwebauthn-pxp/src/advertisement.rs b/libwebauthn-pxp/src/advertisement.rs index ea12519e..e60a42d8 100644 --- a/libwebauthn-pxp/src/advertisement.rs +++ b/libwebauthn-pxp/src/advertisement.rs @@ -10,10 +10,10 @@ use uuid::Uuid; use crate::ble; use crate::cbor::Value; use crate::crypto::trial_decrypt_advert; -use crate::error::CableError; +use crate::error::PxpError; -const CABLE_UUID_FIDO: &str = "0000fff9-0000-1000-8000-00805f9b34fb"; -const CABLE_UUID_GOOGLE: &str = "0000fde2-0000-1000-8000-00805f9b34fb"; +const PXP_UUID_FIDO: &str = "0000fff9-0000-1000-8000-00805f9b34fb"; +const PXP_UUID_GOOGLE: &str = "0000fde2-0000-1000-8000-00805f9b34fb"; /// `transport_channel_identifier` for the BLE data channel. const TRANSPORT_CHANNEL_BLE: i128 = 1; @@ -72,10 +72,10 @@ impl From<[u8; 16]> for DecryptedAdvert { #[instrument(skip_all, err)] pub(crate) async fn await_advertisement( eid_key: &[u8], -) -> Result<(PeripheralProperties, DecryptedAdvert), CableError> { +) -> Result<(PeripheralProperties, DecryptedAdvert), PxpError> { let uuids = &[ - Uuid::parse_str(CABLE_UUID_FIDO)?, - Uuid::parse_str(CABLE_UUID_GOOGLE)?, // Deprecated, but may still be in use. + Uuid::parse_str(PXP_UUID_FIDO)?, + Uuid::parse_str(PXP_UUID_GOOGLE)?, // Deprecated, but may still be in use. ]; let (adapter, stream) = ble::start_discovery_for_service_data(uuids).await?; @@ -121,13 +121,13 @@ pub(crate) async fn await_advertisement( adapter .stop_scan() .await - .or(Err(CableError::TransportUnavailable))?; + .or(Err(PxpError::TransportUnavailable))?; return Ok((device, advert)); } warn!("BLE advertisement discovery stream terminated"); - Err(CableError::TransportUnavailable) + Err(PxpError::TransportUnavailable) } #[cfg(test)] diff --git a/libwebauthn-pxp/src/ble.rs b/libwebauthn-pxp/src/ble.rs index 13cb25fc..4d07704f 100644 --- a/libwebauthn-pxp/src/ble.rs +++ b/libwebauthn-pxp/src/ble.rs @@ -9,20 +9,20 @@ use futures::{Stream, StreamExt}; use tracing::{debug, instrument, trace, warn, Level}; use uuid::Uuid; -use crate::error::CableError; +use crate::error::PxpError; /// TODO(#86): Support multiple adapters. -async fn get_adapter() -> Result { +async fn get_adapter() -> Result { let manager = Manager::new() .await - .or(Err(CableError::TransportUnavailable))?; + .or(Err(PxpError::TransportUnavailable))?; manager .adapters() .await - .or(Err(CableError::TransportUnavailable))? + .or(Err(PxpError::TransportUnavailable))? .into_iter() .next() - .ok_or(CableError::TransportUnavailable) + .ok_or(PxpError::TransportUnavailable) } async fn on_peripheral_service_data( @@ -48,17 +48,17 @@ async fn on_peripheral_service_data( #[instrument(level = Level::DEBUG, skip_all)] pub(crate) async fn start_discovery_for_service_data( uuids: &[Uuid], -) -> Result<(Adapter, impl Stream)> + use<'_>), CableError> { +) -> Result<(Adapter, impl Stream)> + use<'_>), PxpError> { let adapter = get_adapter().await?; let events = adapter .events() .await - .or(Err(CableError::TransportUnavailable))?; + .or(Err(PxpError::TransportUnavailable))?; adapter .start_scan(ScanFilter::default()) .await - .or(Err(CableError::TransportUnavailable))?; + .or(Err(PxpError::TransportUnavailable))?; let scan_adapter = adapter.clone(); let stream = events.filter_map(move |event| { @@ -78,9 +78,9 @@ pub(crate) async fn start_discovery_for_service_data( pub(crate) async fn get_properties( peripheral: &Peripheral, -) -> Result, CableError> { +) -> Result, PxpError> { peripheral .properties() .await - .or(Err(CableError::TransportUnavailable)) + .or(Err(PxpError::TransportUnavailable)) } diff --git a/libwebauthn-pxp/src/connection.rs b/libwebauthn-pxp/src/connection.rs index eb0f3b0c..3b888b72 100644 --- a/libwebauthn-pxp/src/connection.rs +++ b/libwebauthn-pxp/src/connection.rs @@ -4,10 +4,10 @@ use tokio::sync::{mpsc, watch}; use tokio::task; use crate::connection_stages::{MpscUxUpdateSender, TunnelConnectionInput, UxUpdateSender}; -use crate::error::CableError; -use crate::known_devices::CableKnownDevice; +use crate::error::PxpError; +use crate::known_devices::PxpKnownDevice; use crate::protocol; -use crate::qr_code_device::CableQrCodeDevice; +use crate::qr_code_device::PxpQrCodeDevice; #[derive(Debug, Clone, PartialEq)] pub enum ConnectionState { @@ -20,7 +20,7 @@ pub enum ConnectionState { } #[derive(Debug, Clone)] -pub enum CableUpdate { +pub enum PxpUpdate { /// Waiting for proximity check user interaction (eg. scan a QR code, or confirm on the device). ProximityCheck, /// Connecting to the tunnel server. @@ -30,31 +30,31 @@ pub enum CableUpdate { /// Connected to the authenticator device via the tunnel server. Connected, /// The connection to the authenticator device has failed. - Error(CableError), + Error(PxpError), } /// Receives connection progress updates from the connection task. pub trait UpdateSink: Send + Sync + 'static { - fn send_update(&self, update: CableUpdate); + fn send_update(&self, update: PxpUpdate); } /// The CMHD to connect to. #[derive(Debug, Clone)] pub enum ConnectTarget { /// A new device, invoked by scanning a QR code. - QrCode(CableQrCodeDevice), + QrCode(PxpQrCodeDevice), /// A previously linked device, invoked state-assisted via the tunnel service. - Known(CableKnownDevice), + Known(PxpKnownDevice), } -impl From for ConnectTarget { - fn from(device: CableQrCodeDevice) -> Self { +impl From for ConnectTarget { + fn from(device: PxpQrCodeDevice) -> Self { Self::QrCode(device) } } -impl From for ConnectTarget { - fn from(device: CableKnownDevice) -> Self { +impl From for ConnectTarget { + fn from(device: PxpKnownDevice) -> Self { Self::Known(device) } } @@ -73,7 +73,7 @@ pub struct TunnelHandle { impl TunnelHandle { /// Waits until the handshake completes, or fails if the connection terminates first. - pub async fn wait_for_connection(&self) -> Result<(), CableError> { + pub async fn wait_for_connection(&self) -> Result<(), PxpError> { let mut rx = self.connection_state.clone(); // If already connected, return immediately @@ -87,20 +87,20 @@ impl TunnelHandle { // the caller can't observe the timing difference and the asymmetry // was accidental. if *rx.borrow() == ConnectionState::Terminated { - return Err(CableError::ConnectionFailed); + return Err(PxpError::ConnectionFailed); } // Wait for state change while rx.changed().await.is_ok() { match *rx.borrow() { ConnectionState::Connected => return Ok(()), - ConnectionState::Terminated => return Err(CableError::ConnectionFailed), + ConnectionState::Terminated => return Err(PxpError::ConnectionFailed), ConnectionState::Connecting => continue, } } // If the sender was dropped, consider it a failure - Err(CableError::ConnectionLost) + Err(PxpError::ConnectionLost) } } @@ -124,11 +124,11 @@ pub fn connect(target: impl Into, updates: impl UpdateSink) -> Tu let (handshake_output, store) = match &target { ConnectTarget::QrCode(device) => ( - CableQrCodeDevice::connection(device, &ux_sender).await, + PxpQrCodeDevice::connection(device, &ux_sender).await, device.store.clone(), ), ConnectTarget::Known(device) => ( - CableKnownDevice::connection(device, &ux_sender).await, + PxpKnownDevice::connection(device, &ux_sender).await, Some(device.store.clone()), ), }; diff --git a/libwebauthn-pxp/src/connection_stages.rs b/libwebauthn-pxp/src/connection_stages.rs index c1253319..fb0b9e52 100644 --- a/libwebauthn-pxp/src/connection_stages.rs +++ b/libwebauthn-pxp/src/connection_stages.rs @@ -4,15 +4,15 @@ use tokio::sync::{mpsc, watch}; use tracing::{debug, error, info, instrument, trace, warn}; use super::advertisement::{await_advertisement, DecryptedAdvert}; -use super::connection::{CableUpdate, ConnectionState, UpdateSink}; +use super::connection::{ConnectionState, PxpUpdate, UpdateSink}; use super::crypto::{derive, KeyPurpose}; -use super::data_channel::{CableDataChannel, WebSocketDataChannel}; -use super::known_devices::{CableKnownDevice, CableKnownDeviceInfoStore, ClientNonce}; +use super::data_channel::{PxpDataChannel, WebSocketDataChannel}; +use super::known_devices::{ClientNonce, PxpKnownDevice, PxpKnownDeviceInfoStore}; use super::l2cap::L2capDataChannel; -use super::protocol::{self, CableTunnelConnectionType, TunnelNoiseState}; -use super::qr_code_device::CableQrCodeDevice; +use super::protocol::{self, PxpTunnelConnectionType, TunnelNoiseState}; +use super::qr_code_device::PxpQrCodeDevice; use super::tunnel; -use crate::error::CableError; +use crate::error::PxpError; use std::sync::Arc; #[derive(Debug)] @@ -21,7 +21,7 @@ pub(crate) struct ProximityCheckInput { } impl ProximityCheckInput { - pub fn new_for_qr_code(qr_device: &CableQrCodeDevice) -> Result { + pub fn new_for_qr_code(qr_device: &PxpQrCodeDevice) -> Result { let eid_key: [u8; 64] = derive( qr_device.qr_code.qr_secret.as_ref(), None, @@ -31,9 +31,9 @@ impl ProximityCheckInput { } pub fn new_for_known_device( - known_device: &CableKnownDevice, + known_device: &PxpKnownDevice, client_nonce: &ClientNonce, - ) -> Result { + ) -> Result { let eid_key: [u8; 64] = derive( &known_device.device_info.link_secret, Some(client_nonce), @@ -60,19 +60,19 @@ pub(crate) struct BleConnectionParams { #[derive(Debug, Clone)] pub(crate) struct ConnectionInput { pub tunnel_domain: String, - pub connection_type: CableTunnelConnectionType, + pub connection_type: PxpTunnelConnectionType, /// Some if the CMHD offered a BLE L2CAP channel; None selects WebSocket. pub ble: Option, /// Present for known-device connections, so a 410 Gone can forget the record. - pub known_device_store: Option>, + pub known_device_store: Option>, } impl ConnectionInput { #[instrument(skip_all, err)] pub fn new_for_qr_code( - qr_device: &CableQrCodeDevice, + qr_device: &PxpQrCodeDevice, proximity_output: &ProximityCheckOutput, - ) -> Result { + ) -> Result { let tunnel_domain = decode_tunnel_domain_from_advert(&proximity_output.advert)?; let routing_id_str = hex::encode(proximity_output.advert.routing_id); @@ -81,11 +81,11 @@ impl ConnectionInput { None, KeyPurpose::TunnelID, ) - .map_err(|_| CableError::InvalidKey)?; - let tunnel_id = tunnel_id_full.get(..16).ok_or(CableError::InvalidKey)?; + .map_err(|_| PxpError::InvalidKey)?; + let tunnel_id = tunnel_id_full.get(..16).ok_or(PxpError::InvalidKey)?; let tunnel_id_str = hex::encode(tunnel_id); - let connection_type = CableTunnelConnectionType::QrCode { + let connection_type = PxpTunnelConnectionType::QrCode { routing_id: routing_id_str, tunnel_id: tunnel_id_str, private_key: qr_device.private_key, @@ -111,7 +111,7 @@ impl ConnectionInput { } pub fn new_for_known_device( - known_device: &super::known_devices::CableKnownDevice, + known_device: &super::known_devices::PxpKnownDevice, client_nonce: &ClientNonce, ) -> Self { use super::known_devices::ClientPayload; @@ -123,7 +123,7 @@ impl ConnectionInput { hint: known_device.hint, }; let contact_id = base64_url::encode(&known_device.device_info.contact_id); - let connection_type = CableTunnelConnectionType::KnownDevice { + let connection_type = PxpTunnelConnectionType::KnownDevice { contact_id, authenticator_public_key: known_device.device_info.public_key.to_vec(), client_payload, @@ -139,24 +139,24 @@ impl ConnectionInput { } pub(crate) struct ConnectionOutput { - pub data_channel: Box, - pub connection_type: CableTunnelConnectionType, + pub data_channel: Box, + pub connection_type: PxpTunnelConnectionType, pub tunnel_domain: String, } pub(crate) struct HandshakeInput { - pub data_channel: Box, + pub data_channel: Box, pub psk: [u8; 32], - pub connection_type: CableTunnelConnectionType, + pub connection_type: PxpTunnelConnectionType, pub tunnel_domain: String, } impl HandshakeInput { pub fn new_for_qr_code( - qr_device: &CableQrCodeDevice, + qr_device: &PxpQrCodeDevice, connection_output: ConnectionOutput, proximity_output: ProximityCheckOutput, - ) -> Result { + ) -> Result { let advert_plaintext = &proximity_output.advert.plaintext; let psk = derive_psk(qr_device.qr_code.qr_secret.as_ref(), advert_plaintext)?; Ok(Self { @@ -168,10 +168,10 @@ impl HandshakeInput { } pub fn new_for_known_device( - known_device: &CableKnownDevice, + known_device: &PxpKnownDevice, connection_output: ConnectionOutput, proximity_output: ProximityCheckOutput, - ) -> Result { + ) -> Result { let link_secret = known_device.device_info.link_secret; let advert_plaintext = proximity_output.advert.plaintext; let psk = derive_psk(&link_secret, &advert_plaintext)?; @@ -185,17 +185,17 @@ impl HandshakeInput { } pub(crate) struct HandshakeOutput { - pub data_channel: Box, + pub data_channel: Box, pub noise_state: TunnelNoiseState, - pub connection_type: CableTunnelConnectionType, + pub connection_type: PxpTunnelConnectionType, pub tunnel_domain: String, } pub(crate) struct TunnelConnectionInput { - pub connection_type: CableTunnelConnectionType, + pub connection_type: PxpTunnelConnectionType, pub tunnel_domain: String, - pub known_device_store: Option>, - pub data_channel: Box, + pub known_device_store: Option>, + pub data_channel: Box, pub noise_state: TunnelNoiseState, pub ctap_tx_recv: mpsc::Receiver>, pub ctap_rx_send: mpsc::Sender>, @@ -204,7 +204,7 @@ pub(crate) struct TunnelConnectionInput { impl TunnelConnectionInput { pub fn from_handshake_output( handshake_output: HandshakeOutput, - known_device_store: Option>, + known_device_store: Option>, ctap_tx_recv: mpsc::Receiver>, ctap_rx_send: mpsc::Sender>, ) -> Self { @@ -222,8 +222,8 @@ impl TunnelConnectionInput { #[async_trait] pub(crate) trait UxUpdateSender: Send + Sync { - async fn send_update(&self, update: CableUpdate); - async fn send_error(&self, error: CableError); + async fn send_update(&self, update: PxpUpdate); + async fn send_error(&self, error: PxpError); async fn set_connection_state(&self, state: ConnectionState); } @@ -247,13 +247,13 @@ impl MpscUxUpdateSender { #[async_trait] impl UxUpdateSender for MpscUxUpdateSender { #[instrument(skip(self))] - async fn send_update(&self, update: CableUpdate) { + async fn send_update(&self, update: PxpUpdate) { trace!("Sending UX update"); self.sender.send_update(update); } - async fn send_error(&self, error: CableError) { - self.send_update(CableUpdate::Error(error)).await; + async fn send_error(&self, error: PxpError) { + self.send_update(PxpUpdate::Error(error)).await; let _ = self.connection_state_tx.send(ConnectionState::Terminated); } @@ -266,10 +266,10 @@ impl UxUpdateSender for MpscUxUpdateSender { pub(crate) async fn proximity_check_stage( input: ProximityCheckInput, ux_sender: &dyn UxUpdateSender, -) -> Result { +) -> Result { debug!("Starting proximity check stage"); - ux_sender.send_update(CableUpdate::ProximityCheck).await; + ux_sender.send_update(PxpUpdate::ProximityCheck).await; let (device, advert) = await_advertisement(&input.eid_key).await?; @@ -281,10 +281,10 @@ pub(crate) async fn proximity_check_stage( pub(crate) async fn connection_stage( input: ConnectionInput, ux_sender: &dyn UxUpdateSender, -) -> Result { +) -> Result { debug!(?input.tunnel_domain, "Starting connection stage"); - ux_sender.send_update(CableUpdate::Connecting).await; + ux_sender.send_update(PxpUpdate::Connecting).await; let data_channel = connect_data_channel(&input).await?; @@ -301,7 +301,7 @@ pub(crate) async fn connection_stage( /// back to the tunnel, whose routing details are always present in the advert. async fn connect_data_channel( input: &ConnectionInput, -) -> Result, CableError> { +) -> Result, PxpError> { if let Some(ble) = input.ble { match L2capDataChannel::connect(ble.address, ble.address_type, ble.psm).await { Ok(channel) => { @@ -342,17 +342,17 @@ async fn connect_data_channel( pub(crate) async fn handshake_stage( input: HandshakeInput, ux_sender: &dyn UxUpdateSender, -) -> Result { +) -> Result { debug!("Starting handshake stage"); - ux_sender.send_update(CableUpdate::Authenticating).await; + ux_sender.send_update(PxpUpdate::Authenticating).await; let mut data_channel = input.data_channel; let noise_state = protocol::do_handshake(&mut *data_channel, input.psk, &input.connection_type).await?; debug!("Handshake stage completed successfully"); - ux_sender.send_update(CableUpdate::Connected).await; + ux_sender.send_update(PxpUpdate::Connected).await; ux_sender .set_connection_state(ConnectionState::Connected) @@ -366,19 +366,19 @@ pub(crate) async fn handshake_stage( }) } -fn derive_psk(secret: &[u8], advert_plaintext: &[u8]) -> Result<[u8; 32], CableError> { +fn derive_psk(secret: &[u8], advert_plaintext: &[u8]) -> Result<[u8; 32], PxpError> { let derived = derive(secret, Some(advert_plaintext), KeyPurpose::Psk)?; let mut psk: [u8; 32] = [0u8; 32]; - psk.copy_from_slice(derived.get(..32).ok_or(CableError::InvalidKey)?); + psk.copy_from_slice(derived.get(..32).ok_or(PxpError::InvalidKey)?); Ok(psk) } pub(crate) fn decode_tunnel_domain_from_advert( advert: &DecryptedAdvert, -) -> Result { +) -> Result { tunnel::decode_tunnel_server_domain(advert.encoded_tunnel_server_domain) .ok_or_else(|| { error!({ encoded = %advert.encoded_tunnel_server_domain }, "Failed to decode tunnel server domain"); - CableError::InvalidFraming + PxpError::InvalidFraming }) } diff --git a/libwebauthn-pxp/src/crypto.rs b/libwebauthn-pxp/src/crypto.rs index 6797ebbc..93dc3c2d 100644 --- a/libwebauthn-pxp/src/crypto.rs +++ b/libwebauthn-pxp/src/crypto.rs @@ -5,7 +5,7 @@ use hmac::{Hmac, Mac}; use sha2::Sha256; use tracing::{instrument, warn}; -use crate::error::CableError; +use crate::error::PxpError; pub enum KeyPurpose { EIDKey = 1, @@ -17,19 +17,18 @@ pub fn derive( secret: &[u8], salt: Option<&[u8]>, purpose: KeyPurpose, -) -> Result<[u8; 64], CableError> { +) -> Result<[u8; 64], PxpError> { let purpose32 = [purpose as u8, 0, 0, 0]; let hkdf = Hkdf::::new(salt, secret); let mut output = [0u8; 64]; hkdf.expand(&purpose32, &mut output) - .map_err(|_| CableError::InvalidKey)?; + .map_err(|_| PxpError::InvalidKey)?; Ok(output) } -pub(crate) fn hmac_sha256(key: &[u8], message: &[u8]) -> Result, CableError> { - let mut hmac = - as Mac>::new_from_slice(key).map_err(|_| CableError::InvalidKey)?; +pub(crate) fn hmac_sha256(key: &[u8], message: &[u8]) -> Result, PxpError> { + let mut hmac = as Mac>::new_from_slice(key).map_err(|_| PxpError::InvalidKey)?; hmac.update(message); Ok(hmac.finalize().into_bytes().to_vec()) } diff --git a/libwebauthn-pxp/src/data_channel.rs b/libwebauthn-pxp/src/data_channel.rs index 1a74adc5..b6e810d2 100644 --- a/libwebauthn-pxp/src/data_channel.rs +++ b/libwebauthn-pxp/src/data_channel.rs @@ -6,22 +6,22 @@ use tokio_tungstenite::tungstenite::{Error, Message}; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; use tracing::error; -use crate::error::CableError; +use crate::error::PxpError; /// A bidirectional channel carrying discrete protocol messages: the Noise /// handshake messages, then the encrypted CTAP frames. caBLE rides this over a /// WebSocket tunnel; CTAP 2.3 hybrid can also ride it over a BLE L2CAP connection. #[async_trait] -pub(crate) trait CableDataChannel: Send { +pub(crate) trait PxpDataChannel: Send { /// Sends one message as a discrete unit. - async fn send(&mut self, message: &[u8]) -> Result<(), CableError>; + async fn send(&mut self, message: &[u8]) -> Result<(), PxpError>; /// Receives the next message. `Ok(None)` signals a clean close by the peer. /// Must be cancel-safe so it can be used as a `tokio::select!` branch. - async fn recv(&mut self) -> Result>, CableError>; + async fn recv(&mut self) -> Result>, PxpError>; } -/// [`CableDataChannel`] over the caBLE WebSocket tunnel. Each protocol message is +/// [`PxpDataChannel`] over the caBLE WebSocket tunnel. Each protocol message is /// a single binary WebSocket frame. pub(crate) struct WebSocketDataChannel { stream: WebSocketStream>, @@ -34,21 +34,21 @@ impl WebSocketDataChannel { } #[async_trait] -impl CableDataChannel for WebSocketDataChannel { - async fn send(&mut self, message: &[u8]) -> Result<(), CableError> { +impl PxpDataChannel for WebSocketDataChannel { + async fn send(&mut self, message: &[u8]) -> Result<(), PxpError> { self.stream .send(Message::Binary(message.to_vec().into())) .await .map_err(|e| { error!(?e, "Failed to send WebSocket message"); match e { - Error::Io(io) => CableError::from(io), - _ => CableError::ConnectionFailed, + Error::Io(io) => PxpError::from(io), + _ => PxpError::ConnectionFailed, } }) } - async fn recv(&mut self) -> Result>, CableError> { + async fn recv(&mut self) -> Result>, PxpError> { loop { match self.stream.next().await { Some(Ok(Message::Binary(data))) => return Ok(Some(data.into())), @@ -58,15 +58,15 @@ impl CableDataChannel for WebSocketDataChannel { } Some(Ok(other)) => { error!(?other, "Unexpected WebSocket message type"); - return Err(CableError::ConnectionFailed); + return Err(PxpError::ConnectionFailed); } Some(Err(Error::Io(e))) => { error!(?e, "Failed to read WebSocket message"); - return Err(CableError::from(e)); + return Err(PxpError::from(e)); } Some(Err(e)) => { error!(?e, "Failed to read WebSocket message"); - return Err(CableError::ConnectionFailed); + return Err(PxpError::ConnectionFailed); } } } diff --git a/libwebauthn-pxp/src/error.rs b/libwebauthn-pxp/src/error.rs index 55333731..1b1b703a 100644 --- a/libwebauthn-pxp/src/error.rs +++ b/libwebauthn-pxp/src/error.rs @@ -5,13 +5,13 @@ use std::sync::Arc; use tokio_tungstenite::tungstenite::http::header::InvalidHeaderValue; use tokio_tungstenite::tungstenite::Error as TungsteniteError; -/// caBLE transport error. `Clone` because it rides the [`CableUpdate`](crate::CableUpdate) UX +/// caBLE transport error. `Clone` because it rides the [`PxpUpdate`](crate::PxpUpdate) UX /// broadcast stream, which requires `Clone`; non-`Clone` native causes /// (`snow`, `io`, `tungstenite`, `serde_cbor`, `http`) are kept behind an /// `Arc` rather than flattened. #[derive(thiserror::Error, Debug, Clone)] #[non_exhaustive] -pub enum CableError { +pub enum PxpError { #[error("noise protocol error: {0}")] Noise(Arc), #[error("input/output error: {0}")] @@ -27,7 +27,7 @@ pub enum CableError { #[error("invalid http header: {0}")] HttpHeader(Arc), #[error(transparent)] - CableTunnel(#[from] CableTunnelError), + PxpTunnel(#[from] PxpTunnelError), #[error("connection failed")] ConnectionFailed, #[error("connection lost")] @@ -48,38 +48,38 @@ pub enum CableError { EncryptionFailed, } -impl From for CableError { +impl From for PxpError { fn from(error: snow::Error) -> Self { - CableError::Noise(Arc::new(error)) + PxpError::Noise(Arc::new(error)) } } -impl From for CableError { +impl From for PxpError { fn from(error: std::io::Error) -> Self { - CableError::Io(Arc::new(error)) + PxpError::Io(Arc::new(error)) } } -impl From for CableError { +impl From for PxpError { fn from(error: TungsteniteError) -> Self { - CableError::WebSocket(Arc::new(error)) + PxpError::WebSocket(Arc::new(error)) } } -impl From for CableError { +impl From for PxpError { fn from(error: InvalidHeaderValue) -> Self { - CableError::HttpHeader(Arc::new(error)) + PxpError::HttpHeader(Arc::new(error)) } } -impl From for CableError { +impl From for PxpError { fn from(error: serde_cbor_2::Error) -> Self { - CableError::Cbor(Arc::new(error)) + PxpError::Cbor(Arc::new(error)) } } #[derive(thiserror::Error, Debug, PartialEq, Clone)] -pub enum CableTunnelError { +pub enum PxpTunnelError { /// The tunnel server returned HTTP 410 Gone for the contacted resource. #[error("tunnel server reported the resource is gone (HTTP 410)")] Gone, diff --git a/libwebauthn-pxp/src/known_devices.rs b/libwebauthn-pxp/src/known_devices.rs index b3913eb1..f9c47620 100644 --- a/libwebauthn-pxp/src/known_devices.rs +++ b/libwebauthn-pxp/src/known_devices.rs @@ -6,7 +6,7 @@ use crate::connection_stages::{ connection_stage, handshake_stage, proximity_check_stage, ConnectionInput, HandshakeInput, HandshakeOutput, MpscUxUpdateSender, ProximityCheckInput, }; -use crate::error::CableError; +use crate::error::PxpError; use async_trait::async_trait; use futures::lock::Mutex; @@ -15,20 +15,20 @@ use serde_bytes::ByteBuf; use serde_indexed::SerializeIndexed; use tracing::{debug, instrument, trace}; -use super::protocol::CableLinkingInfo; +use super::protocol::PxpLinkingInfo; #[async_trait] -pub trait CableKnownDeviceInfoStore: Debug + Send + Sync { +pub trait PxpKnownDeviceInfoStore: Debug + Send + Sync { /// Called whenever a known device should be added or updated. - async fn put_known_device(&self, device_id: &CableKnownDeviceId, device: &CableKnownDeviceInfo); + async fn put_known_device(&self, device_id: &PxpKnownDeviceId, device: &PxpKnownDeviceInfo); /// Called whenever a known device becomes permanently unavailable. - async fn delete_known_device(&self, device_id: &CableKnownDeviceId); + async fn delete_known_device(&self, device_id: &PxpKnownDeviceId); } /// An in-memory store for testing purposes. #[derive(Debug, Default, Clone)] pub struct EphemeralDeviceInfoStore { - pub known_devices: Arc>>, + pub known_devices: Arc>>, } impl EphemeralDeviceInfoStore { @@ -38,7 +38,7 @@ impl EphemeralDeviceInfoStore { } } - pub async fn list_all(&self) -> Vec<(CableKnownDeviceId, CableKnownDeviceInfo)> { + pub async fn list_all(&self) -> Vec<(PxpKnownDeviceId, PxpKnownDeviceInfo)> { debug!("Listing all known devices"); let known_devices = self.known_devices.lock().await; known_devices @@ -51,29 +51,25 @@ impl EphemeralDeviceInfoStore { unsafe impl Send for EphemeralDeviceInfoStore {} #[async_trait] -impl CableKnownDeviceInfoStore for EphemeralDeviceInfoStore { - async fn put_known_device( - &self, - device_id: &CableKnownDeviceId, - device: &CableKnownDeviceInfo, - ) { +impl PxpKnownDeviceInfoStore for EphemeralDeviceInfoStore { + async fn put_known_device(&self, device_id: &PxpKnownDeviceId, device: &PxpKnownDeviceInfo) { debug!(?device_id, "Inserting or updating known device"); trace!(?device); let mut known_devices = self.known_devices.lock().await; known_devices.insert(device_id.clone(), device.clone()); } - async fn delete_known_device(&self, device_id: &CableKnownDeviceId) { + async fn delete_known_device(&self, device_id: &PxpKnownDeviceId) { debug!(?device_id, "Deleting known device"); let mut known_devices = self.known_devices.lock().await; known_devices.remove(device_id); } } -pub type CableKnownDeviceId = String; +pub type PxpKnownDeviceId = String; #[derive(Debug, Clone)] -pub struct CableKnownDeviceInfo { +pub struct PxpKnownDeviceInfo { pub contact_id: Vec, pub link_id: [u8; 8], pub link_secret: [u8; 32], @@ -82,34 +78,34 @@ pub struct CableKnownDeviceInfo { pub tunnel_domain: String, } -impl From<&CableLinkingInfo> for CableKnownDeviceId { - fn from(linking_info: &CableLinkingInfo) -> Self { +impl From<&PxpLinkingInfo> for PxpKnownDeviceId { + fn from(linking_info: &PxpLinkingInfo) -> Self { hex::encode(linking_info.authenticator_public_key.as_slice()) } } -impl CableKnownDeviceInfo { +impl PxpKnownDeviceInfo { pub(crate) fn new( tunnel_domain: &str, - linking_info: &CableLinkingInfo, - ) -> Result { + linking_info: &PxpLinkingInfo, + ) -> Result { let info = Self { contact_id: linking_info.contact_id.to_vec(), link_id: linking_info .link_id .clone() .try_into() - .map_err(|_| CableError::InvalidFraming)?, + .map_err(|_| PxpError::InvalidFraming)?, link_secret: linking_info .link_secret .clone() .try_into() - .map_err(|_| CableError::InvalidFraming)?, + .map_err(|_| PxpError::InvalidFraming)?, public_key: linking_info .authenticator_public_key .clone() .try_into() - .map_err(|_| CableError::InvalidFraming)?, + .map_err(|_| PxpError::InvalidFraming)?, name: linking_info.authenticator_name.clone(), tunnel_domain: tunnel_domain.to_string(), }; @@ -118,13 +114,13 @@ impl CableKnownDeviceInfo { } #[derive(Debug, Clone)] -pub struct CableKnownDevice { +pub struct PxpKnownDevice { pub hint: ClientPayloadHint, - pub device_info: CableKnownDeviceInfo, - pub(crate) store: Arc, + pub device_info: PxpKnownDeviceInfo, + pub(crate) store: Arc, } -impl Display for CableKnownDevice { +impl Display for PxpKnownDevice { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, @@ -135,16 +131,16 @@ impl Display for CableKnownDevice { } } -unsafe impl Send for CableKnownDevice {} -unsafe impl Sync for CableKnownDevice {} +unsafe impl Send for PxpKnownDevice {} +unsafe impl Sync for PxpKnownDevice {} -impl CableKnownDevice { +impl PxpKnownDevice { pub async fn new( hint: ClientPayloadHint, - device_info: &CableKnownDeviceInfo, - store: Arc, - ) -> Result { - let device = CableKnownDevice { + device_info: &PxpKnownDeviceInfo, + store: Arc, + ) -> Result { + let device = PxpKnownDevice { hint, device_info: device_info.clone(), store, @@ -154,9 +150,9 @@ impl CableKnownDevice { #[instrument(skip_all, err)] pub(crate) async fn connection( - known_device: &CableKnownDevice, + known_device: &PxpKnownDevice, ux_sender: &MpscUxUpdateSender, - ) -> Result { + ) -> Result { let client_nonce = rand::random::(); // Stage 1: Connection (no proximity check needed for known devices) diff --git a/libwebauthn-pxp/src/l2cap.rs b/libwebauthn-pxp/src/l2cap.rs index 49a93a42..e3b41aed 100644 --- a/libwebauthn-pxp/src/l2cap.rs +++ b/libwebauthn-pxp/src/l2cap.rs @@ -1,4 +1,4 @@ -//! [`CableDataChannel`] over a direct BLE L2CAP connection-oriented channel. +//! [`PxpDataChannel`] over a direct BLE L2CAP connection-oriented channel. use std::str::FromStr; use std::time::Duration; @@ -8,8 +8,8 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::time::Instant; use tracing::{debug, error, warn}; -use super::data_channel::CableDataChannel; -use crate::error::CableError; +use super::data_channel::PxpDataChannel; +use crate::error::PxpError; /// End-of-Message sequence terminating every L2CAP message (CRLF). const EOM: [u8; 2] = [0x0D, 0x0A]; @@ -20,7 +20,7 @@ const EOM: [u8; 2] = [0x0D, 0x0A]; const MTU_READY_TIMEOUT: Duration = Duration::from_secs(2); const MTU_POLL_INTERVAL: Duration = Duration::from_millis(50); -/// [`CableDataChannel`] over the insecure L2CAP CoC socket the CMHD opens for CTAP 2.3 hybrid. +/// [`PxpDataChannel`] over the insecure L2CAP CoC socket the CMHD opens for CTAP 2.3 hybrid. /// Messages are CRLF-terminated per the CTAP 2.3 hybrid draft. pub(crate) struct L2capDataChannel { stream: bluer::l2cap::Stream, @@ -40,7 +40,7 @@ impl L2capDataChannel { addr: BDAddr, addr_type: Option, psm: u16, - ) -> Result { + ) -> Result { let (addr, addr_type) = bdaddr_to_bluer(addr, addr_type)?; let stream = @@ -48,7 +48,7 @@ impl L2capDataChannel { .await .map_err(|e| { error!(?e, %addr, psm, "Failed to connect L2CAP CoC"); - CableError::from(e) + PxpError::from(e) })?; await_send_mtu(&stream).await; @@ -85,20 +85,20 @@ async fn await_send_mtu(stream: &bluer::l2cap::Stream) { } #[async_trait] -impl CableDataChannel for L2capDataChannel { - async fn send(&mut self, message: &[u8]) -> Result<(), CableError> { +impl PxpDataChannel for L2capDataChannel { + async fn send(&mut self, message: &[u8]) -> Result<(), PxpError> { self.stream.write_all(message).await.map_err(|e| { error!(?e, "Failed to write L2CAP message"); - CableError::from(e) + PxpError::from(e) })?; self.stream.write_all(&EOM).await.map_err(|e| { error!(?e, "Failed to write L2CAP EOM"); - CableError::from(e) + PxpError::from(e) })?; Ok(()) } - async fn recv(&mut self) -> Result>, CableError> { + async fn recv(&mut self) -> Result>, PxpError> { loop { if let Some(message) = split_next_message(&mut self.read_buf) { return Ok(Some(message)); @@ -121,7 +121,7 @@ impl CableDataChannel for L2capDataChannel { } Err(e) => { error!(?e, "Failed to read L2CAP message"); - return Err(CableError::from(e)); + return Err(PxpError::from(e)); } }; if n == 0 { @@ -130,7 +130,7 @@ impl CableDataChannel for L2capDataChannel { return Ok(None); } error!(buffered = self.read_buf.len(), "L2CAP closed mid-message"); - return Err(CableError::ConnectionLost); + return Err(PxpError::ConnectionLost); } self.read_buf .extend_from_slice(chunk.get(..n).unwrap_or(&[])); @@ -152,10 +152,10 @@ fn split_next_message(buf: &mut Vec) -> Option> { fn bdaddr_to_bluer( addr: BDAddr, addr_type: Option, -) -> Result<(bluer::Address, bluer::AddressType), CableError> { +) -> Result<(bluer::Address, bluer::AddressType), PxpError> { let addr = bluer::Address::from_str(&addr.to_string()).map_err(|e| { error!(?e, "Failed to parse Bluetooth address"); - CableError::InvalidEndpoint + PxpError::InvalidEndpoint })?; let addr_type = match addr_type { Some(AddressType::Public) => bluer::AddressType::LePublic, diff --git a/libwebauthn-pxp/src/lib.rs b/libwebauthn-pxp/src/lib.rs index c03367b8..03e447b0 100644 --- a/libwebauthn-pxp/src/lib.rs +++ b/libwebauthn-pxp/src/lib.rs @@ -38,6 +38,6 @@ pub mod qr_code_device; pub mod tunnel; pub use connection::{ - connect, CableUpdate, ConnectTarget, ConnectionState, TunnelHandle, UpdateSink, + connect, ConnectTarget, ConnectionState, PxpUpdate, TunnelHandle, UpdateSink, }; pub use digit_encode::digit_encode; diff --git a/libwebauthn-pxp/src/protocol.rs b/libwebauthn-pxp/src/protocol.rs index 6f9fbe8f..91a3b48b 100644 --- a/libwebauthn-pxp/src/protocol.rs +++ b/libwebauthn-pxp/src/protocol.rs @@ -1,5 +1,5 @@ //! Transport-agnostic Noise handshake and encrypted CTAP framing for the -//! hybrid transport. Runs over any [`CableDataChannel`]. +//! hybrid transport. Runs over any [`PxpDataChannel`]. use std::collections::BTreeMap; use std::sync::Arc; @@ -15,21 +15,21 @@ use snow::{Builder, TransportState}; use tokio::sync::mpsc::Sender; use tracing::{debug, error, trace, warn}; -use super::data_channel::CableDataChannel; +use super::data_channel::PxpDataChannel; use super::known_devices::ClientPayload; -use super::known_devices::{CableKnownDeviceInfo, CableKnownDeviceInfoStore}; +use super::known_devices::{PxpKnownDeviceInfo, PxpKnownDeviceInfoStore}; use crate::cbor::{self, Value}; use crate::connection_stages::TunnelConnectionInput; -use crate::error::CableError; +use crate::error::PxpError; use crate::get_info::GetInfoResponse; -use crate::known_devices::CableKnownDeviceId; +use crate::known_devices::PxpKnownDeviceId; const P256_X962_LENGTH: usize = 65; const MAX_CBOR_SIZE: usize = 1024 * 1024; const PADDING_GRANULARITY: usize = 32; -const CABLE_PROLOGUE_STATE_ASSISTED: &[u8] = &[0u8]; -const CABLE_PROLOGUE_QR_INITIATED: &[u8] = &[1u8]; +const PXP_PROLOGUE_STATE_ASSISTED: &[u8] = &[0u8]; +const PXP_PROLOGUE_QR_INITIATED: &[u8] = &[1u8]; /// CTAP2 authenticatorGetInfo command byte. The only CTAP fact PXP relies on: /// the post-handshake message already carries the getInfo response. @@ -38,30 +38,30 @@ const CTAP2_GET_INFO: u8 = 0x04; const CTAP2_OK: u8 = 0x00; #[derive(Debug, Clone)] -struct CableTunnelMessage { - message_type: CableTunnelMessageType, +struct PxpTunnelMessage { + message_type: PxpTunnelMessageType, payload: ByteBuf, } -impl CableTunnelMessage { - pub fn new(message_type: CableTunnelMessageType, payload: &[u8]) -> Self { +impl PxpTunnelMessage { + pub fn new(message_type: PxpTunnelMessageType, payload: &[u8]) -> Self { Self { message_type, payload: ByteBuf::from(payload.to_vec()), } } - pub fn from_slice(slice: &[u8]) -> Result { - let (type_byte, payload) = slice.split_first().ok_or(CableError::InvalidFraming)?; + pub fn from_slice(slice: &[u8]) -> Result { + let (type_byte, payload) = slice.split_first().ok_or(PxpError::InvalidFraming)?; if payload.is_empty() { - return Err(CableError::InvalidFraming); + return Err(PxpError::InvalidFraming); } let message_type = match *type_byte { - 0 => CableTunnelMessageType::Shutdown, - 1 => CableTunnelMessageType::Ctap, - 2 => CableTunnelMessageType::Update, + 0 => PxpTunnelMessageType::Shutdown, + 1 => PxpTunnelMessageType::Ctap, + 2 => PxpTunnelMessageType::Update, _ => { - return Err(CableError::InvalidFraming); + return Err(PxpError::InvalidFraming); } }; @@ -81,7 +81,7 @@ impl CableTunnelMessage { } #[derive(Clone, Debug, DeserializeIndexed)] -struct CableInitialMessage { +struct PxpInitialMessage { #[serde(skip_serializing_if = "Option::is_none")] #[serde(index = 0x00)] pub _padding: Option, @@ -113,7 +113,7 @@ impl SupportedFeatures { } #[derive(Clone, Debug)] -pub(crate) struct CableLinkingInfo { +pub(crate) struct PxpLinkingInfo { /// Used by the tunnel to identify the authenticator (eg. Android FCM token) pub contact_id: Vec, /// Used by the authenticator to identify the client platform @@ -132,7 +132,7 @@ pub(crate) struct CableLinkingInfo { #[repr(u8)] #[derive(Debug, Clone, Copy, Deserialize)] -enum CableTunnelMessageType { +enum PxpTunnelMessageType { Shutdown = 0, Ctap = 1, Update = 2, @@ -147,7 +147,7 @@ enum RecvOutcome { } #[derive(Clone)] -pub(crate) enum CableTunnelConnectionType { +pub(crate) enum PxpTunnelConnectionType { QrCode { routing_id: String, tunnel_id: String, @@ -160,7 +160,7 @@ pub(crate) enum CableTunnelConnectionType { }, } -impl std::fmt::Debug for CableTunnelConnectionType { +impl std::fmt::Debug for PxpTunnelConnectionType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::QrCode { @@ -194,24 +194,24 @@ pub(crate) struct TunnelNoiseState { } pub(crate) async fn do_handshake( - data_channel: &mut dyn CableDataChannel, + data_channel: &mut dyn PxpDataChannel, psk: [u8; 32], - connection_type: &CableTunnelConnectionType, -) -> Result { + connection_type: &PxpTunnelConnectionType, +) -> Result { let noise_handshake = match connection_type { - CableTunnelConnectionType::QrCode { private_key, .. } => { + PxpTunnelConnectionType::QrCode { private_key, .. } => { let local_private_key = private_key.to_owned().to_bytes(); Builder::new("Noise_KNpsk0_P256_AESGCM_SHA256".parse()?) - .prologue(CABLE_PROLOGUE_QR_INITIATED)? + .prologue(PXP_PROLOGUE_QR_INITIATED)? .local_private_key(local_private_key.as_slice())? .psk(0, &psk)? .build_initiator() } - CableTunnelConnectionType::KnownDevice { + PxpTunnelConnectionType::KnownDevice { authenticator_public_key, .. } => Builder::new("Noise_NKpsk0_P256_AESGCM_SHA256".parse()?) - .prologue(CABLE_PROLOGUE_STATE_ASSISTED)? + .prologue(PXP_PROLOGUE_STATE_ASSISTED)? .remote_public_key(authenticator_public_key)? .psk(0, &psk)? .build_initiator(), @@ -222,7 +222,7 @@ pub(crate) async fn do_handshake( Ok(handshake) => handshake, Err(e) => { error!(?e, "Failed to build Noise handshake"); - return Err(CableError::ConnectionFailed); + return Err(PxpError::ConnectionFailed); } }; @@ -231,14 +231,14 @@ pub(crate) async fn do_handshake( Ok(msg_len) => msg_len, Err(e) => { error!(?e, "Failed to write initial handshake message"); - return Err(CableError::ConnectionFailed); + return Err(PxpError::ConnectionFailed); } }; let initial_msg: Vec = initial_msg_buffer .get(..initial_msg_len) .map(<[u8]>::to_vec) - .ok_or(CableError::ConnectionFailed)?; + .ok_or(PxpError::ConnectionFailed)?; trace!( { handshake = ?initial_msg }, "Sending initial handshake message" @@ -256,7 +256,7 @@ pub(crate) async fn do_handshake( } Ok(None) => { error!("Connection was closed before handshake was complete"); - return Err(CableError::ConnectionFailed); + return Err(PxpError::ConnectionFailed); } Err(e) => { error!(?e, "Failed to read handshake response"); @@ -269,7 +269,7 @@ pub(crate) async fn do_handshake( { len = response.len() }, "Peer handshake message is too short" ); - return Err(CableError::ConnectionFailed); + return Err(PxpError::ConnectionFailed); } let mut payload = [0u8; 1024]; @@ -277,7 +277,7 @@ pub(crate) async fn do_handshake( Ok(len) => len, Err(e) => { error!(?e, "Failed to read handshake response message"); - return Err(CableError::ConnectionFailed); + return Err(PxpError::ConnectionFailed); } }; @@ -288,7 +288,7 @@ pub(crate) async fn do_handshake( if !noise_handshake.is_handshake_finished() { error!("Handshake did not complete"); - return Err(CableError::ConnectionFailed); + return Err(PxpError::ConnectionFailed); } Ok(TunnelNoiseState { @@ -299,7 +299,7 @@ pub(crate) async fn do_handshake( /// Returns `Ok(())` on a clean close and `Err(_)` on any fault that leaves /// the encrypted channel unusable; callers surface `Err(_)` via `send_error`. -pub(crate) async fn connection(mut input: TunnelConnectionInput) -> Result<(), CableError> { +pub(crate) async fn connection(mut input: TunnelConnectionInput) -> Result<(), PxpError> { let get_info_response_serialized: Vec = match input.data_channel.recv().await { Ok(Some(message)) => match connection_recv_initial(message, &mut input.noise_state).await { Ok(initial) => initial, @@ -310,7 +310,7 @@ pub(crate) async fn connection(mut input: TunnelConnectionInput) -> Result<(), C }, Ok(None) => { error!("Connection closed before initial message was received"); - return Err(CableError::ConnectionLost); + return Err(PxpError::ConnectionLost); } Err(e) => { error!(?e, "Failed to read initial message"); @@ -363,7 +363,7 @@ pub(crate) async fn connection(mut input: TunnelConnectionInput) -> Result<(), C response.extend_from_slice(&get_info_response_serialized); if let Err(e) = input.ctap_rx_send.send(response).await { error!(?e, "CTAP response receiver dropped"); - return Err(CableError::ConnectionFailed); + return Err(PxpError::ConnectionFailed); } } command => { @@ -387,16 +387,16 @@ pub(crate) async fn connection(mut input: TunnelConnectionInput) -> Result<(), C async fn connection_send( cbor_request: Vec, - data_channel: &mut dyn CableDataChannel, + data_channel: &mut dyn PxpDataChannel, noise_state: &mut TunnelNoiseState, -) -> Result<(), CableError> { +) -> Result<(), PxpError> { debug!("Sending CBOR request"); if cbor_request.len() > MAX_CBOR_SIZE { error!( cbor_request_len = cbor_request.len(), "CBOR request too large" ); - return Err(CableError::InvalidFraming); + return Err(PxpError::InvalidFraming); } trace!(?cbor_request, cbor_request_len = cbor_request.len()); @@ -409,7 +409,7 @@ async fn connection_send( *last = (extra_bytes - 1) as u8; } - let frame = CableTunnelMessage::new(CableTunnelMessageType::Ctap, &padded_cbor_request); + let frame = PxpTunnelMessage::new(PxpTunnelMessageType::Ctap, &padded_cbor_request); let frame_serialized = frame.to_vec(); trace!(?frame_serialized); @@ -423,7 +423,7 @@ async fn connection_send( } Err(e) => { error!(?e, "Failed to encrypt frame"); - return Err(CableError::EncryptionFailed); + return Err(PxpError::EncryptionFailed); } } @@ -437,12 +437,12 @@ async fn connection_send( /// Strip the trailing padding-length byte and `padding_len` bytes of padding /// from a decrypted Noise transport frame, returning `InvalidFraming` on an /// empty plaintext or a declared padding length that exceeds the frame. -fn strip_frame_padding(mut decrypted_frame: Vec) -> Result, CableError> { +fn strip_frame_padding(mut decrypted_frame: Vec) -> Result, PxpError> { let padding_len = match decrypted_frame.last() { Some(&b) => b as usize, None => { error!("Decrypted frame is empty; cannot read padding length"); - return Err(CableError::InvalidFraming); + return Err(PxpError::InvalidFraming); } }; let new_len = decrypted_frame @@ -453,7 +453,7 @@ fn strip_frame_padding(mut decrypted_frame: Vec) -> Result, CableErr frame_len = decrypted_frame.len(), padding_len, "Padding length exceeds frame length" ); - CableError::InvalidFraming + PxpError::InvalidFraming })?; decrypted_frame.truncate(new_len); Ok(decrypted_frame) @@ -462,7 +462,7 @@ fn strip_frame_padding(mut decrypted_frame: Vec) -> Result, CableErr async fn decrypt_frame( encrypted_frame: Vec, noise_state: &mut TunnelNoiseState, -) -> Result, CableError> { +) -> Result, PxpError> { let mut decrypted_frame = vec![0u8; MAX_CBOR_SIZE]; match noise_state .transport_state @@ -475,7 +475,7 @@ async fn decrypt_frame( } Err(e) => { error!(?e, "Failed to decrypt CBOR response"); - return Err(CableError::EncryptionFailed); + return Err(PxpError::EncryptionFailed); } } @@ -492,20 +492,20 @@ async fn decrypt_frame( async fn connection_recv_initial( encrypted_frame: Vec, noise_state: &mut TunnelNoiseState, -) -> Result, CableError> { +) -> Result, PxpError> { let decrypted_frame = decrypt_frame(encrypted_frame, noise_state).await?; - let initial_message: CableInitialMessage = match cbor::from_slice(&decrypted_frame) { + let initial_message: PxpInitialMessage = match cbor::from_slice(&decrypted_frame) { Ok(initial_message) => initial_message, Err(e) => { error!(?e, "Failed to decode initial message"); - return Err(CableError::InvalidFraming); + return Err(PxpError::InvalidFraming); } }; if let Err(e) = cbor::from_slice::(&initial_message.info) { error!(?e, "Failed to decode GetInfo response"); - return Err(CableError::InvalidFraming); + return Err(PxpError::InvalidFraming); } debug!(?initial_message.supported_features, "Received post-handshake message"); @@ -513,7 +513,7 @@ async fn connection_recv_initial( Ok(initial_message.info.to_vec()) } -async fn connection_recv_update(message: &[u8]) -> Result, CableError> { +async fn connection_recv_update(message: &[u8]) -> Result, PxpError> { // TODO(#66): Android adds a 999-key to the end the message, which is not part of the standard. // For now, we parse the message to a map and manuually import fields. @@ -521,7 +521,7 @@ async fn connection_recv_update(message: &[u8]) -> Result update_message, Err(e) => { error!(?e, "Failed to decode update message"); - return Err(CableError::InvalidFraming); + return Err(PxpError::InvalidFraming); } }; @@ -564,7 +564,7 @@ async fn connection_recv_update(message: &[u8]) -> Result Result>, + known_device_store: &Option>, encrypted_frame: Vec, ctap_rx_send: &Sender>, noise_state: &mut TunnelNoiseState, -) -> Result { +) -> Result { let decrypted_frame = decrypt_frame(encrypted_frame, noise_state).await?; - let cable_message: CableTunnelMessage = CableTunnelMessage::from_slice(&decrypted_frame) + let pxp_message: PxpTunnelMessage = PxpTunnelMessage::from_slice(&decrypted_frame) .inspect_err(|e| error!(?e, "Failed to decode CABLE tunnel message"))?; - trace!(?cable_message); - match cable_message.message_type { - CableTunnelMessageType::Shutdown => { + trace!(?pxp_message); + match pxp_message.message_type { + PxpTunnelMessageType::Shutdown => { debug!("Peer sent Shutdown control message; closing connection cleanly"); Ok(RecvOutcome::PeerShutdown) } - CableTunnelMessageType::Ctap => { + PxpTunnelMessageType::Ctap => { debug!("Received CTAP response"); - trace!(?cable_message.payload); + trace!(?pxp_message.payload); ctap_rx_send - .send(cable_message.payload.into_vec()) + .send(pxp_message.payload.into_vec()) .await - .or(Err(CableError::ConnectionFailed))?; + .or(Err(PxpError::ConnectionFailed))?; Ok(RecvOutcome::Continue) } - CableTunnelMessageType::Update => { + PxpTunnelMessageType::Update => { // Malformed or unsigned update: log, drop the update, keep the channel. - let maybe_update_message = match connection_recv_update(&cable_message.payload).await { + let maybe_update_message = match connection_recv_update(&pxp_message.payload).await { Ok(m) => m, Err(e) => { warn!(?e, "Malformed update message; ignoring"); @@ -619,7 +619,7 @@ async fn connection_recv( return Ok(RecvOutcome::Continue); }; - let CableTunnelConnectionType::QrCode { private_key, .. } = connection_type else { + let PxpTunnelConnectionType::QrCode { private_key, .. } = connection_type else { warn!("Ignoring update message for non-QR code connection"); return Ok(RecvOutcome::Continue); }; @@ -649,13 +649,13 @@ async fn connection_recv( /// Stores the update only on a valid signature; invalid updates are dropped without evicting. async fn apply_linking_update( - store: &Arc, + store: &Arc, private_key: &NonZeroScalar, tunnel_domain: &str, - linking_info: &CableLinkingInfo, + linking_info: &PxpLinkingInfo, handshake_hash: &[u8], ) { - let device_id: CableKnownDeviceId = linking_info.into(); + let device_id: PxpKnownDeviceId = linking_info.into(); match parse_known_device(private_key, tunnel_domain, linking_info, handshake_hash) { Ok(known_device) => { debug!(?device_id, "Updating known device"); @@ -675,17 +675,17 @@ async fn apply_linking_update( fn parse_known_device( private_key: &NonZeroScalar, tunnel_domain: &str, - linking_info: &CableLinkingInfo, + linking_info: &PxpLinkingInfo, handshake_hash: &[u8], -) -> Result { - let known_device = CableKnownDeviceInfo::new(tunnel_domain, linking_info)?; +) -> Result { + let known_device = PxpKnownDeviceInfo::new(tunnel_domain, linking_info)?; let secret_key = SecretKey::from(private_key); let Ok(authenticator_public_key) = PublicKey::from_sec1_bytes(&linking_info.authenticator_public_key) else { error!("Failed to parse public key."); - return Err(CableError::InvalidKey); + return Err(PxpError::InvalidKey); }; let shared_secret: Vec = ecdh::diffie_hellman( @@ -696,14 +696,14 @@ fn parse_known_device( .to_vec(); let mut hmac = - Hmac::::new_from_slice(&shared_secret).map_err(|_| CableError::InvalidKey)?; + Hmac::::new_from_slice(&shared_secret).map_err(|_| PxpError::InvalidKey)?; hmac.update(handshake_hash); let expected_mac = hmac.finalize().into_bytes().to_vec(); if expected_mac != linking_info.handshake_signature { error!("Invalid handshake signature, rejecting update message"); trace!(?expected_mac, ?linking_info.handshake_signature); - return Err(CableError::InvalidSignature); + return Err(PxpError::InvalidSignature); } debug!("Parsed known device with valid signature"); @@ -722,30 +722,30 @@ mod tests { #[derive(Debug, Default)] struct RecordingStore { - puts: Mutex>, - deletes: Mutex>, + puts: Mutex>, + deletes: Mutex>, } #[async_trait] - impl CableKnownDeviceInfoStore for RecordingStore { + impl PxpKnownDeviceInfoStore for RecordingStore { async fn put_known_device( &self, - device_id: &CableKnownDeviceId, - _device: &CableKnownDeviceInfo, + device_id: &PxpKnownDeviceId, + _device: &PxpKnownDeviceInfo, ) { if let Ok(mut puts) = self.puts.lock() { puts.push(device_id.clone()); } } - async fn delete_known_device(&self, device_id: &CableKnownDeviceId) { + async fn delete_known_device(&self, device_id: &PxpKnownDeviceId) { if let Ok(mut deletes) = self.deletes.lock() { deletes.push(device_id.clone()); } } } - fn linking_info_with_authenticator_key(authenticator_public_key: Vec) -> CableLinkingInfo { - CableLinkingInfo { + fn linking_info_with_authenticator_key(authenticator_public_key: Vec) -> PxpLinkingInfo { + PxpLinkingInfo { contact_id: vec![0u8; 4], link_id: vec![0u8; 8], link_secret: vec![0u8; 32], @@ -768,7 +768,7 @@ mod tests { let linking_info = linking_info_with_authenticator_key(authenticator_public_key); let recording = Arc::new(RecordingStore::default()); - let store: Arc = recording.clone(); + let store: Arc = recording.clone(); // Signature is intentionally bogus, so the update must be rejected. apply_linking_update( @@ -794,14 +794,14 @@ mod tests { fn absent_supported_features_defaults_to_ctap() { let map = BTreeMap::from([(Value::Integer(1), Value::Bytes(vec![]))]); let bytes = cbor::to_vec(&map).unwrap(); - let message: CableInitialMessage = cbor::from_slice(&bytes).unwrap(); + let message: PxpInitialMessage = cbor::from_slice(&bytes).unwrap(); assert_eq!(message.supported_features.0, vec!["ctap"]); } #[test] fn strip_frame_padding_rejects_empty() { let result = strip_frame_padding(Vec::new()); - assert!(matches!(result, Err(CableError::InvalidFraming))); + assert!(matches!(result, Err(PxpError::InvalidFraming))); } #[test] @@ -809,7 +809,7 @@ mod tests { // Length 1 + declared padding of 5 -> would require subtracting 6 from 1. let frame = vec![0x05u8]; let result = strip_frame_padding(frame); - assert!(matches!(result, Err(CableError::InvalidFraming))); + assert!(matches!(result, Err(PxpError::InvalidFraming))); } #[test] diff --git a/libwebauthn-pxp/src/qr_code_device.rs b/libwebauthn-pxp/src/qr_code_device.rs index 34b3314c..2ef8760e 100644 --- a/libwebauthn-pxp/src/qr_code_device.rs +++ b/libwebauthn-pxp/src/qr_code_device.rs @@ -16,11 +16,11 @@ use super::connection_stages::{ connection_stage, handshake_stage, proximity_check_stage, ConnectionInput, HandshakeInput, MpscUxUpdateSender, ProximityCheckInput, }; -use super::known_devices::CableKnownDeviceInfoStore; +use super::known_devices::PxpKnownDeviceInfoStore; use super::tunnel::KNOWN_TUNNEL_DOMAINS; use crate::cbor; use crate::digit_encode; -use crate::error::CableError; +use crate::error::PxpError; #[derive(Debug, Clone, Copy, Serialize, PartialEq)] pub enum QrCodeOperationHint { @@ -33,14 +33,14 @@ pub enum QrCodeOperationHint { /// One of the data transfer channels listed in QR code key 6. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr)] #[repr(u8)] -pub(crate) enum CableTransportChannel { +pub(crate) enum PxpTransportChannel { WebSocket = 0, Ble = 1, } /// Which hybrid transport(s) the QR code advertises support for. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CableTransports { +pub enum PxpTransports { /// caBLE v2 only: advertise the cloud-assisted WebSocket tunnel. Omits QR /// key 6, so the QR stays valid for legacy peers that read key 6 as a /// `supports_non_discoverable_mc` boolean and would hard-reject a CBOR @@ -54,22 +54,22 @@ pub enum CableTransports { CloudAssistedOrLocal, } -impl CableTransports { +impl PxpTransports { /// CBOR form of QR key 6. `None` for `CloudAssistedOnly` so a legacy peer /// doesn't see an unexpected CBOR array where it wants a boolean. - pub(crate) fn to_qr_field(self) -> Option> { + pub(crate) fn to_qr_field(self) -> Option> { match self { Self::CloudAssistedOnly => None, Self::CloudAssistedOrLocal => Some(vec![ - CableTransportChannel::WebSocket, - CableTransportChannel::Ble, + PxpTransportChannel::WebSocket, + PxpTransportChannel::Ble, ]), } } } #[derive(Debug, Clone, SerializeIndexed)] -pub struct CableQrCode { +pub struct PxpQrCode { // Key 0: a 33-byte, P-256, X9.62, compressed public key. #[serde(index = 0x00)] pub public_key: ByteArray<33>, @@ -102,15 +102,15 @@ pub struct CableQrCode { pub operation_hint: QrCodeOperationHint, /// Key 6: data transfer channels the client supports (CTAP 2.3 hybrid). - /// Set via [`CableTransports`] at construction time; stored here as a + /// Set via [`PxpTransports`] at construction time; stored here as a /// `Vec` for CBOR serialization. `None` omits key 6 entirely so the QR /// stays valid for caBLE v2 peers that interpret key 6 incompatibly. #[serde(skip_serializing_if = "Option::is_none")] #[serde(index = 0x06)] - pub(crate) transports: Option>, + pub(crate) transports: Option>, } -impl std::fmt::Display for CableQrCode { +impl std::fmt::Display for PxpQrCode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let serialized = cbor::to_vec(&self).map_err(|_| std::fmt::Error)?; write!(f, "FIDO:/{}", digit_encode(&serialized)) @@ -120,41 +120,41 @@ impl std::fmt::Display for CableQrCode { /// Represents a new device which will connect by scanning a QR code. /// This could be a new device, or an ephmemeral device whose details were not stored. #[derive(Clone)] -pub struct CableQrCodeDevice { +pub struct PxpQrCodeDevice { /// The QR code to be scanned by the new authenticator. - pub qr_code: CableQrCode, + pub qr_code: PxpQrCode, /// An ephemeral private key, corresponding to the public key within the QR code. pub private_key: NonZeroScalar, /// An optional reference to the store. This may be None, if no persistence is desired. - pub(crate) store: Option>, + pub(crate) store: Option>, } -impl Debug for CableQrCodeDevice { +impl Debug for PxpQrCodeDevice { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("CableQrCodeDevice") + f.debug_struct("PxpQrCodeDevice") .field("qr_code", &self.qr_code) .field("store", &self.store) .finish() } } -impl CableQrCodeDevice { +impl PxpQrCodeDevice { /// Generates a QR code, linking the provided known-device store. A device scanning /// this QR code may be persisted to the store after a successful connection. pub fn new_persistent( hint: QrCodeOperationHint, - store: Arc, - transports: CableTransports, - ) -> Result { + store: Arc, + transports: PxpTransports, + ) -> Result { Self::new(hint, true, Some(store), transports) } fn new( hint: QrCodeOperationHint, state_assisted: bool, - store: Option>, - transports: CableTransports, - ) -> Result { + store: Option>, + transports: PxpTransports, + ) -> Result { let private_key_scalar = NonZeroScalar::random(&mut OsRng); let private_key = SecretKey::from(private_key_scalar); let public_key: [u8; 33] = private_key @@ -163,7 +163,7 @@ impl CableQrCodeDevice { .to_encoded_point(true) .as_bytes() .try_into() - .map_err(|_| CableError::InvalidKey)?; + .map_err(|_| PxpError::InvalidKey)?; let mut qr_secret = [0u8; 16]; OsRng.fill_bytes(&mut qr_secret); @@ -175,7 +175,7 @@ impl CableQrCodeDevice { let transports = transports.to_qr_field(); Ok(Self { - qr_code: CableQrCode { + qr_code: PxpQrCode { public_key: ByteArray::from(public_key), qr_secret: ByteArray::from(qr_secret), known_tunnel_domains_count: KNOWN_TUNNEL_DOMAINS.len() as u8, @@ -192,21 +192,21 @@ impl CableQrCodeDevice { } } -impl CableQrCodeDevice { +impl PxpQrCodeDevice { /// Generates a QR code, without any known-device store. A device scanning this QR code /// will not be persisted. pub fn new_transient( hint: QrCodeOperationHint, - transports: CableTransports, - ) -> Result { + transports: PxpTransports, + ) -> Result { Self::new(hint, false, None, transports) } #[instrument(skip_all, err)] pub(crate) async fn connection( - qr_device: &CableQrCodeDevice, + qr_device: &PxpQrCodeDevice, ux_sender: &MpscUxUpdateSender, - ) -> Result { + ) -> Result { // Stage 1: Proximity check let proximity_input = ProximityCheckInput::new_for_qr_code(qr_device)?; let proximity_output = proximity_check_stage(proximity_input, ux_sender).await?; @@ -224,9 +224,9 @@ impl CableQrCodeDevice { } } -impl Display for CableQrCodeDevice { +impl Display for PxpQrCodeDevice { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "CableQrCodeDevice") + write!(f, "PxpQrCodeDevice") } } @@ -235,18 +235,18 @@ mod tests { use super::*; use std::collections::BTreeMap; - // Downstream callers (e.g. credentialsd) move a CableQrCodeDevice across + // Downstream callers (e.g. credentialsd) move a PxpQrCodeDevice across // tokio::spawn boundaries, so both Send and Sync need to auto-derive. const _: fn() = || { fn assert_send_sync() {} - assert_send_sync::(); + assert_send_sync::(); }; #[test] fn qr_code_omits_key_6_for_cloud_assisted_only() { - let device = CableQrCodeDevice::new_transient( + let device = PxpQrCodeDevice::new_transient( QrCodeOperationHint::MakeCredential, - CableTransports::CloudAssistedOnly, + PxpTransports::CloudAssistedOnly, ) .unwrap(); let bytes = cbor::to_vec(&device.qr_code).unwrap(); @@ -256,9 +256,9 @@ mod tests { #[test] fn qr_code_encodes_key_6_for_cloud_assisted_or_local() { - let device = CableQrCodeDevice::new_transient( + let device = PxpQrCodeDevice::new_transient( QrCodeOperationHint::MakeCredential, - CableTransports::CloudAssistedOrLocal, + PxpTransports::CloudAssistedOrLocal, ) .unwrap(); let bytes = cbor::to_vec(&device.qr_code).unwrap(); diff --git a/libwebauthn-pxp/src/tunnel.rs b/libwebauthn-pxp/src/tunnel.rs index edba250f..7418659b 100644 --- a/libwebauthn-pxp/src/tunnel.rs +++ b/libwebauthn-pxp/src/tunnel.rs @@ -9,11 +9,11 @@ use tracing::{debug, error, trace}; use tungstenite::client::IntoClientRequest; use url::Url; -use super::error::CableTunnelError; -use super::known_devices::CableKnownDeviceId; -use super::protocol::CableTunnelConnectionType; +use super::error::PxpTunnelError; +use super::known_devices::PxpKnownDeviceId; +use super::protocol::PxpTunnelConnectionType; use crate::cbor; -use crate::error::CableError; +use crate::error::PxpError; const MAX_TUNNEL_REDIRECTS: usize = 5; @@ -65,13 +65,13 @@ pub fn decode_tunnel_server_domain(encoded: u16) -> Option { /// Builds the tunnel request, re-attaching the fido.cable and client-payload headers. pub(crate) fn build_tunnel_request( url: &str, - connection_type: &CableTunnelConnectionType, -) -> Result { - let mut request = url.into_client_request().map_err(CableError::from)?; + connection_type: &PxpTunnelConnectionType, +) -> Result { + let mut request = url.into_client_request().map_err(PxpError::from)?; let headers = request.headers_mut(); headers.insert("Sec-WebSocket-Protocol", "fido.cable".parse()?); - if let CableTunnelConnectionType::KnownDevice { client_payload, .. } = connection_type { + if let PxpTunnelConnectionType::KnownDevice { client_payload, .. } = connection_type { let client_payload = cbor::to_vec(client_payload)?; headers.insert( "X-caBLE-Client-Payload", @@ -82,30 +82,30 @@ pub(crate) fn build_tunnel_request( } /// Resolves a redirect Location, which may be relative, against the current URL. -fn resolve_redirect_target(base: &str, location: &str) -> Result { +fn resolve_redirect_target(base: &str, location: &str) -> Result { let base = Url::parse(base)?; let target = base.join(location)?; Ok(target.to_string()) } /// Maps a non-101 tunnel handshake status to a transport error, distinguishing 410 Gone. -fn tunnel_status_error(status: StatusCode) -> CableError { +fn tunnel_status_error(status: StatusCode) -> PxpError { if status == StatusCode::GONE { - CableTunnelError::Gone.into() + PxpTunnelError::Gone.into() } else { - CableTunnelError::UnexpectedStatus(status.as_u16()).into() + PxpTunnelError::UnexpectedStatus(status.as_u16()).into() } } /// The known-device id to forget on a 410 Gone, for a known-device connection. pub(crate) fn known_device_id_to_forget( - error: &CableError, - connection_type: &CableTunnelConnectionType, -) -> Option { + error: &PxpError, + connection_type: &PxpTunnelConnectionType, +) -> Option { match (error, connection_type) { ( - CableError::CableTunnel(CableTunnelError::Gone), - CableTunnelConnectionType::KnownDevice { + PxpError::PxpTunnel(PxpTunnelError::Gone), + PxpTunnelConnectionType::KnownDevice { authenticator_public_key, .. }, @@ -116,12 +116,12 @@ pub(crate) fn known_device_id_to_forget( pub(crate) async fn connect( tunnel_domain: &str, - connection_type: &CableTunnelConnectionType, -) -> Result>, CableError> { + connection_type: &PxpTunnelConnectionType, +) -> Result>, PxpError> { ensure_rustls_crypto_provider(); let mut connect_url = match connection_type { - CableTunnelConnectionType::QrCode { + PxpTunnelConnectionType::QrCode { routing_id, tunnel_id, .. @@ -129,7 +129,7 @@ pub(crate) async fn connect( "wss://{}/cable/connect/{}/{}", tunnel_domain, routing_id, tunnel_id ), - CableTunnelConnectionType::KnownDevice { contact_id, .. } => { + PxpTunnelConnectionType::KnownDevice { contact_id, .. } => { format!("wss://{}/cable/contact/{}", tunnel_domain, contact_id) } }; @@ -144,7 +144,7 @@ pub(crate) async fn connect( debug!(?response, "Connected to tunnel server"); if response.status() != StatusCode::SWITCHING_PROTOCOLS { error!(?response, "Failed to switch to websocket protocol"); - return Err(CableError::ConnectionFailed); + return Err(PxpError::ConnectionFailed); } debug!("Tunnel server returned success"); return Ok(ws_stream); @@ -156,7 +156,7 @@ pub(crate) async fn connect( TungsteniteError::Http(response) => response, error => { error!(?error, "Failed to connect to tunnel server"); - return Err(CableError::from(error)); + return Err(PxpError::from(error)); } }; @@ -168,7 +168,7 @@ pub(crate) async fn connect( .and_then(|value| value.to_str().ok()) else { error!(?status, "Tunnel redirect missing a usable Location header"); - return Err(CableError::ConnectionFailed); + return Err(PxpError::ConnectionFailed); }; connect_url = resolve_redirect_target(&connect_url, location)?; debug!(?connect_url, "Following tunnel redirect"); @@ -180,7 +180,7 @@ pub(crate) async fn connect( } error!("Exceeded the maximum number of tunnel redirects"); - Err(CableTunnelError::TooManyRedirects.into()) + Err(PxpTunnelError::TooManyRedirects.into()) } #[cfg(test)] @@ -191,8 +191,8 @@ mod tests { use rand::rngs::OsRng; use serde_bytes::ByteBuf; - fn known_device_connection_type(public_key: Vec) -> CableTunnelConnectionType { - CableTunnelConnectionType::KnownDevice { + fn known_device_connection_type(public_key: Vec) -> PxpTunnelConnectionType { + PxpTunnelConnectionType::KnownDevice { contact_id: "contact-id".to_string(), authenticator_public_key: public_key, client_payload: ClientPayload { @@ -203,8 +203,8 @@ mod tests { } } - fn qr_connection_type() -> CableTunnelConnectionType { - CableTunnelConnectionType::QrCode { + fn qr_connection_type() -> PxpTunnelConnectionType { + PxpTunnelConnectionType::QrCode { routing_id: "aabbcc".to_string(), tunnel_id: "00112233445566778899aabbccddeeff".to_string(), private_key: NonZeroScalar::random(&mut OsRng), @@ -281,10 +281,7 @@ mod tests { let public_key = vec![7u8; 65]; let connection_type = known_device_connection_type(public_key.clone()); assert_eq!( - known_device_id_to_forget( - &CableError::CableTunnel(CableTunnelError::Gone), - &connection_type - ), + known_device_id_to_forget(&PxpError::PxpTunnel(PxpTunnelError::Gone), &connection_type), Some(hex::encode(&public_key)) ); } @@ -293,10 +290,7 @@ mod tests { fn gone_does_not_forget_qr_code() { let connection_type = qr_connection_type(); assert_eq!( - known_device_id_to_forget( - &CableError::CableTunnel(CableTunnelError::Gone), - &connection_type - ), + known_device_id_to_forget(&PxpError::PxpTunnel(PxpTunnelError::Gone), &connection_type), None ); } @@ -305,7 +299,7 @@ mod tests { fn non_gone_error_does_not_forget_known_device() { let connection_type = known_device_connection_type(vec![7u8; 65]); assert_eq!( - known_device_id_to_forget(&CableError::ConnectionFailed, &connection_type), + known_device_id_to_forget(&PxpError::ConnectionFailed, &connection_type), None ); } @@ -314,11 +308,11 @@ mod tests { fn gone_status_maps_to_distinct_error() { assert!(matches!( tunnel_status_error(StatusCode::GONE), - CableError::CableTunnel(CableTunnelError::Gone) + PxpError::PxpTunnel(PxpTunnelError::Gone) )); assert!(matches!( tunnel_status_error(StatusCode::BAD_GATEWAY), - CableError::CableTunnel(CableTunnelError::UnexpectedStatus(502)) + PxpError::PxpTunnel(PxpTunnelError::UnexpectedStatus(502)) )); } } diff --git a/libwebauthn/examples/ceremony/webauthn_cable.rs b/libwebauthn/examples/ceremony/webauthn_cable.rs index a409928a..c3509187 100644 --- a/libwebauthn/examples/ceremony/webauthn_cable.rs +++ b/libwebauthn/examples/ceremony/webauthn_cable.rs @@ -5,7 +5,7 @@ use std::error::Error; use libwebauthn::transport::hybrid::is_available; use libwebauthn::transport::hybrid::qr_code_device::{ - CableQrCodeDevice, CableTransports, QrCodeOperationHint, + PxpQrCodeDevice, PxpTransports, QrCodeOperationHint, }; use qrcode::render::unicode; use qrcode::QrCode; @@ -65,9 +65,9 @@ pub async fn main() -> Result<(), Box> { }, }; - let mut device: CableQrCodeDevice = CableQrCodeDevice::new_transient( + let mut device: PxpQrCodeDevice = PxpQrCodeDevice::new_transient( QrCodeOperationHint::MakeCredential, - CableTransports::CloudAssistedOrLocal, + PxpTransports::CloudAssistedOrLocal, )?; println!("Created QR code, awaiting for advertisement."); @@ -83,7 +83,7 @@ pub async fn main() -> Result<(), Box> { println!("Channel established {:?}", channel); let state_recv = channel.get_ux_update_receiver(); - tokio::spawn(common::handle_cable_updates(state_recv)); + tokio::spawn(common::handle_hybrid_updates(state_recv)); let request = MakeCredentialRequest::prepare(&request_origin, MAKE_CREDENTIAL_REQUEST, &settings) diff --git a/libwebauthn/examples/ceremony/webauthn_cable_wss.rs b/libwebauthn/examples/ceremony/webauthn_cable_wss.rs index 390c3216..b201f70a 100644 --- a/libwebauthn/examples/ceremony/webauthn_cable_wss.rs +++ b/libwebauthn/examples/ceremony/webauthn_cable_wss.rs @@ -4,10 +4,10 @@ use std::time::Duration; use libwebauthn::transport::hybrid::is_available; use libwebauthn::transport::hybrid::known_devices::{ - CableKnownDevice, ClientPayloadHint, EphemeralDeviceInfoStore, + ClientPayloadHint, EphemeralDeviceInfoStore, PxpKnownDevice, }; use libwebauthn::transport::hybrid::qr_code_device::{ - CableQrCodeDevice, CableTransports, QrCodeOperationHint, + PxpQrCodeDevice, PxpTransports, QrCodeOperationHint, }; use qrcode::render::unicode; use qrcode::QrCode; @@ -17,7 +17,7 @@ use libwebauthn::ops::webauthn::{ GetAssertionRequest, JsonFormat, MakeCredentialRequest, OriginValidation, RelatedOrigins, RequestOrigin, RequestSettings, SystemPublicSuffixList, WebAuthnIDLResponse as _, }; -use libwebauthn::transport::hybrid::channel::CableChannel; +use libwebauthn::transport::hybrid::channel::HybridChannel; use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; use libwebauthn::webauthn::WebAuthn; @@ -74,10 +74,10 @@ pub async fn main() -> Result<(), Box> { ); { - let mut device: CableQrCodeDevice = CableQrCodeDevice::new_persistent( + let mut device: PxpQrCodeDevice = PxpQrCodeDevice::new_persistent( QrCodeOperationHint::MakeCredential, device_info_store.clone(), - CableTransports::CloudAssistedOnly, + PxpTransports::CloudAssistedOnly, )?; println!("Created QR code, awaiting for advertisement."); @@ -93,7 +93,7 @@ pub async fn main() -> Result<(), Box> { println!("Channel established {:?}", channel); let state_recv = channel.get_ux_update_receiver(); - tokio::spawn(common::handle_cable_updates(state_recv)); + tokio::spawn(common::handle_hybrid_updates(state_recv)); let request = MakeCredentialRequest::prepare( &request_origin, @@ -124,7 +124,7 @@ pub async fn main() -> Result<(), Box> { let all_devices = device_info_store.list_all().await; if let Some((_, known_device_info)) = all_devices.first() { println!("Reconnecting state-assisted to known device..."); - let mut known_device: CableKnownDevice = CableKnownDevice::new( + let mut known_device: PxpKnownDevice = PxpKnownDevice::new( ClientPayloadHint::GetAssertion, known_device_info, device_info_store.clone(), @@ -139,10 +139,10 @@ pub async fn main() -> Result<(), Box> { run_get_assertion(&mut channel, &request_origin, &psl).await?; } else { println!("No known devices (peer did not offer linking). Falling back to QR."); - let mut device: CableQrCodeDevice = CableQrCodeDevice::new_persistent( + let mut device: PxpQrCodeDevice = PxpQrCodeDevice::new_persistent( QrCodeOperationHint::GetAssertionRequest, device_info_store.clone(), - CableTransports::CloudAssistedOnly, + PxpTransports::CloudAssistedOnly, )?; let qr_code = QrCode::new(device.qr_code.to_string()).unwrap(); let image = qr_code @@ -160,12 +160,12 @@ pub async fn main() -> Result<(), Box> { } async fn run_get_assertion( - channel: &mut CableChannel, + channel: &mut HybridChannel, request_origin: &RequestOrigin, psl: &SystemPublicSuffixList, ) -> Result<(), Box> { let state_recv = channel.get_ux_update_receiver(); - tokio::spawn(common::handle_cable_updates(state_recv)); + tokio::spawn(common::handle_hybrid_updates(state_recv)); let request = GetAssertionRequest::prepare( request_origin, diff --git a/libwebauthn/examples/common/mod.rs b/libwebauthn/examples/common/mod.rs index 091d2f5a..ad517f78 100644 --- a/libwebauthn/examples/common/mod.rs +++ b/libwebauthn/examples/common/mod.rs @@ -9,7 +9,7 @@ use std::io::{self, Write}; use libwebauthn::pin::{PinNotSetReason, PinRequestReason}; -use libwebauthn::transport::hybrid::channel::{CableUpdate, CableUxUpdate}; +use libwebauthn::transport::hybrid::channel::{HybridUxUpdate, PxpUpdate}; use libwebauthn::UvUpdate; use text_io::read; use tokio::sync::broadcast::Receiver; @@ -32,16 +32,16 @@ pub async fn handle_uv_updates(mut rx: Receiver) { /// Like [`handle_uv_updates`], but for caBLE channels which surface both /// transport-level updates and UV updates. -pub async fn handle_cable_updates(mut rx: Receiver) { +pub async fn handle_hybrid_updates(mut rx: Receiver) { while let Ok(update) = rx.recv().await { match update { - CableUxUpdate::UvUpdate(uv) => handle_uv_update(uv), - CableUxUpdate::CableUpdate(c) => match c { - CableUpdate::ProximityCheck => println!("Proximity check in progress..."), - CableUpdate::Connecting => println!("Connecting to the device..."), - CableUpdate::Authenticating => println!("Authenticating with the device..."), - CableUpdate::Connected => println!("Tunnel established successfully!"), - CableUpdate::Error(err) => println!("Error during connection: {}", err), + HybridUxUpdate::UvUpdate(uv) => handle_uv_update(uv), + HybridUxUpdate::PxpUpdate(c) => match c { + PxpUpdate::ProximityCheck => println!("Proximity check in progress..."), + PxpUpdate::Connecting => println!("Connecting to the device..."), + PxpUpdate::Authenticating => println!("Authenticating with the device..."), + PxpUpdate::Connected => println!("Tunnel established successfully!"), + PxpUpdate::Error(err) => println!("Error during connection: {}", err), }, } } diff --git a/libwebauthn/examples/features/webauthn_prf_cable.rs b/libwebauthn/examples/features/webauthn_prf_cable.rs index 26238e3b..55227292 100644 --- a/libwebauthn/examples/features/webauthn_prf_cable.rs +++ b/libwebauthn/examples/features/webauthn_prf_cable.rs @@ -22,10 +22,10 @@ use libwebauthn::proto::ctap2::{ Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity, Ctap2PublicKeyCredentialType, Ctap2PublicKeyCredentialUserEntity, }; -use libwebauthn::transport::hybrid::channel::CableChannel; +use libwebauthn::transport::hybrid::channel::HybridChannel; use libwebauthn::transport::hybrid::is_available; use libwebauthn::transport::hybrid::qr_code_device::{ - CableQrCodeDevice, CableTransports, QrCodeOperationHint, + PxpQrCodeDevice, PxpTransports, QrCodeOperationHint, }; use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; use libwebauthn::webauthn::WebAuthn; @@ -73,9 +73,9 @@ pub async fn main() -> Result<(), Box> { async fn connect( hint: QrCodeOperationHint, -) -> Result<(CableQrCodeDevice, CableChannel), Box> { - let mut device: CableQrCodeDevice = - CableQrCodeDevice::new_transient(hint, CableTransports::CloudAssistedOrLocal)?; +) -> Result<(PxpQrCodeDevice, HybridChannel), Box> { + let mut device: PxpQrCodeDevice = + PxpQrCodeDevice::new_transient(hint, PxpTransports::CloudAssistedOrLocal)?; println!("Created QR code, awaiting advertisement."); let qr_code = QrCode::new(device.qr_code.to_string()).unwrap(); @@ -90,7 +90,7 @@ async fn connect( println!("Channel established {:?}", channel); let state_recv = channel.get_ux_update_receiver(); - tokio::spawn(common::handle_cable_updates(state_recv)); + tokio::spawn(common::handle_hybrid_updates(state_recv)); Ok((device, channel)) } diff --git a/libwebauthn/src/transport/error.rs b/libwebauthn/src/transport/error.rs index 36523836..3d443299 100644 --- a/libwebauthn/src/transport/error.rs +++ b/libwebauthn/src/transport/error.rs @@ -1,4 +1,4 @@ //! Per-transport errors live with each transport (e.g. `hid::HidError`, -//! `ble::BleError`, `hybrid::CableError` from `libwebauthn-pxp`). The ceremony error is +//! `ble::BleError`, `hybrid::PxpError` from `libwebauthn-pxp`). The ceremony error is //! [`WebAuthnError`](crate::webauthn::error::WebAuthnError), generic over the //! channel's concrete transport error. diff --git a/libwebauthn/src/transport/hybrid/channel.rs b/libwebauthn/src/transport/hybrid/channel.rs index 204f10fe..2d5c3863 100644 --- a/libwebauthn/src/transport/hybrid/channel.rs +++ b/libwebauthn/src/transport/hybrid/channel.rs @@ -21,53 +21,53 @@ use crate::webauthn::error::WebAuthnError; use crate::Transport; use crate::UvUpdate; -use super::error::CableError; -use super::known_devices::CableKnownDevice; -use super::qr_code_device::CableQrCodeDevice; +use super::error::PxpError; +use super::known_devices::PxpKnownDevice; +use super::qr_code_device::PxpQrCodeDevice; -pub use libwebauthn_pxp::{CableUpdate, ConnectionState}; +pub use libwebauthn_pxp::{ConnectionState, PxpUpdate}; #[derive(Debug)] -pub enum CableChannelDevice<'d> { - QrCode(&'d CableQrCodeDevice), - Known(&'d CableKnownDevice), +pub enum HybridChannelDevice<'d> { + QrCode(&'d PxpQrCodeDevice), + Known(&'d PxpKnownDevice), } #[derive(Debug)] -pub struct CableChannel { +pub struct HybridChannel { pub(crate) tunnel: TunnelHandle, - pub(crate) ux_update_sender: broadcast::Sender, + pub(crate) ux_update_sender: broadcast::Sender, pub(crate) persistent_token_store: Option>, } -impl Display for CableChannel { +impl Display for HybridChannel { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "CableChannel") + write!(f, "HybridChannel") } } #[derive(Debug, Clone)] -pub enum CableUxUpdate { +pub enum HybridUxUpdate { UvUpdate(UvUpdate), - CableUpdate(CableUpdate), + PxpUpdate(PxpUpdate), } -impl From for CableUxUpdate { +impl From for HybridUxUpdate { fn from(update: UvUpdate) -> Self { - CableUxUpdate::UvUpdate(update) + HybridUxUpdate::UvUpdate(update) } } #[async_trait] -impl Channel for CableChannel { - type UxUpdate = CableUxUpdate; - type TransportError = CableError; +impl Channel for HybridChannel { + type UxUpdate = HybridUxUpdate; + type TransportError = PxpError; fn transport(&self) -> Transport { Transport::Hybrid } - async fn supported_protocols(&self) -> Result> { + async fn supported_protocols(&self) -> Result> { Ok(SupportedProtocols::fido2_only()) } @@ -79,28 +79,28 @@ impl Channel for CableChannel { } async fn close(&mut self) { - // TODO Send CableTunnelMessageType#Shutdown and drop the connection + // TODO Send PxpTunnelMessageType#Shutdown and drop the connection } async fn apdu_send( &mut self, _request: &ApduRequest, _timeout: Duration, - ) -> Result<(), CableError> { + ) -> Result<(), PxpError> { error!("APDU send not supported in caBLE transport"); - Err(CableError::TransportUnavailable) + Err(PxpError::TransportUnavailable) } - async fn apdu_recv(&mut self, _timeout: Duration) -> Result { + async fn apdu_recv(&mut self, _timeout: Duration) -> Result { error!("APDU recv not supported in caBLE transport"); - Err(CableError::TransportUnavailable) + Err(PxpError::TransportUnavailable) } async fn cbor_send( &mut self, request: &CborRequest, timeout: Duration, - ) -> Result<(), CableError> { + ) -> Result<(), PxpError> { // First, wait for connection to be established (no timeout for handshake) self.tunnel.wait_for_connection().await?; @@ -110,16 +110,16 @@ impl Channel for CableChannel { Ok(Ok(_)) => Ok(()), Ok(Err(error)) => { error!(%error, "CBOR request send failure"); - Err(CableError::TransportUnavailable) + Err(PxpError::TransportUnavailable) } Err(elapsed) => { error!({ %elapsed, ?timeout }, "CBOR request send timeout"); - Err(CableError::Timeout) + Err(PxpError::Timeout) } } } - async fn cbor_recv(&mut self, timeout: Duration) -> Result { + async fn cbor_recv(&mut self, timeout: Duration) -> Result { // First, wait for connection to be established (no timeout for handshake) self.tunnel.wait_for_connection().await?; @@ -127,17 +127,17 @@ impl Channel for CableChannel { match time::timeout(timeout, self.tunnel.ctap_receiver.recv()).await { Ok(Some(frame)) => CborResponse::try_from(&frame).map_err(|e| { error!(%e, "Malformed CTAP response frame"); - CableError::InvalidFraming + PxpError::InvalidFraming }), - Ok(None) => Err(CableError::TransportUnavailable), + Ok(None) => Err(PxpError::TransportUnavailable), Err(elapsed) => { error!({ %elapsed, ?timeout }, "CBOR response recv timeout"); - Err(CableError::Timeout) + Err(PxpError::Timeout) } } } - fn get_ux_update_sender(&self) -> &broadcast::Sender { + fn get_ux_update_sender(&self) -> &broadcast::Sender { &self.ux_update_sender } @@ -147,7 +147,7 @@ impl Channel for CableChannel { } } -impl Ctap2AuthTokenStore for CableChannel { +impl Ctap2AuthTokenStore for HybridChannel { fn store_auth_data(&mut self, _auth_token_data: AuthTokenData) {} fn get_auth_data(&self) -> Option<&AuthTokenData> { diff --git a/libwebauthn/src/transport/hybrid/device.rs b/libwebauthn/src/transport/hybrid/device.rs index 97c5b062..639ea70f 100644 --- a/libwebauthn/src/transport/hybrid/device.rs +++ b/libwebauthn/src/transport/hybrid/device.rs @@ -1,33 +1,33 @@ -//! [`Device`] impls opening a [`CableChannel`] to a PXP device. +//! [`Device`] impls opening a [`HybridChannel`] to a PXP device. use async_trait::async_trait; -use libwebauthn_pxp::{CableUpdate, ConnectTarget, UpdateSink}; +use libwebauthn_pxp::{ConnectTarget, PxpUpdate, UpdateSink}; use tokio::sync::broadcast; use tracing::{debug, trace, warn}; -use super::channel::{CableChannel, CableUxUpdate}; -use super::error::CableError; -use super::known_devices::CableKnownDevice; -use super::qr_code_device::CableQrCodeDevice; -use super::Cable; +use super::channel::{HybridChannel, HybridUxUpdate}; +use super::error::PxpError; +use super::known_devices::PxpKnownDevice; +use super::qr_code_device::PxpQrCodeDevice; +use super::Hybrid; use crate::transport::{ChannelSettings, Device}; use crate::webauthn::error::WebAuthnError; /// Forwards PXP connection updates onto the channel's UX update stream. -struct UxUpdateForwarder(broadcast::Sender); +struct UxUpdateForwarder(broadcast::Sender); impl UpdateSink for UxUpdateForwarder { - fn send_update(&self, update: CableUpdate) { + fn send_update(&self, update: PxpUpdate) { trace!("Sending UX update"); - if let Err(err) = self.0.send(CableUxUpdate::CableUpdate(update)) { + if let Err(err) = self.0.send(HybridUxUpdate::PxpUpdate(update)) { warn!(?err, "No receivers found for UX update."); } } } -fn open_channel(target: ConnectTarget, settings: ChannelSettings) -> CableChannel { +fn open_channel(target: ConnectTarget, settings: ChannelSettings) -> HybridChannel { let (ux_update_sender, _) = broadcast::channel(16); let tunnel = libwebauthn_pxp::connect(target, UxUpdateForwarder(ux_update_sender.clone())); - CableChannel { + HybridChannel { tunnel, ux_update_sender, persistent_token_store: settings.persistent_token_store, @@ -35,21 +35,21 @@ fn open_channel(target: ConnectTarget, settings: ChannelSettings) -> CableChanne } #[async_trait] -impl<'d> Device<'d, Cable, CableChannel> for CableQrCodeDevice { +impl<'d> Device<'d, Hybrid, HybridChannel> for PxpQrCodeDevice { async fn channel( &'d mut self, settings: ChannelSettings, - ) -> Result> { + ) -> Result> { Ok(open_channel(self.clone().into(), settings)) } } #[async_trait] -impl<'d> Device<'d, Cable, CableChannel> for CableKnownDevice { +impl<'d> Device<'d, Hybrid, HybridChannel> for PxpKnownDevice { async fn channel( &'d mut self, settings: ChannelSettings, - ) -> Result> { + ) -> Result> { debug!(?self.device_info.tunnel_domain, "Creating channel to tunnel server"); Ok(open_channel(self.clone().into(), settings)) } diff --git a/libwebauthn/src/transport/hybrid/mod.rs b/libwebauthn/src/transport/hybrid/mod.rs index 8a0da155..e1edc837 100644 --- a/libwebauthn/src/transport/hybrid/mod.rs +++ b/libwebauthn/src/transport/hybrid/mod.rs @@ -18,13 +18,13 @@ pub async fn is_available() -> bool { super::ble::is_available().await } -pub struct Cable {} -impl Transport for Cable {} -unsafe impl Send for Cable {} -unsafe impl Sync for Cable {} +pub struct Hybrid {} +impl Transport for Hybrid {} +unsafe impl Send for Hybrid {} +unsafe impl Sync for Hybrid {} -impl Display for Cable { +impl Display for Hybrid { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Cable") + write!(f, "Hybrid") } } diff --git a/libwebauthn/src/webauthn.rs b/libwebauthn/src/webauthn.rs index 9d715daa..fb0f2e8f 100644 --- a/libwebauthn/src/webauthn.rs +++ b/libwebauthn/src/webauthn.rs @@ -938,7 +938,7 @@ mod tests { // An id longer than maxCredentialIdLength cannot belong to this device and must be // dropped before sending (CTAP 2.1/2.2 6.4). #[tokio::test] - async fn oversized_allow_entries_are_filtered_before_send_on_cable() { + async fn oversized_allow_entries_are_filtered_before_send_on_hybrid() { let valid = descriptor(&[1u8; 16]); let oversized = descriptor(&[2u8; 64]); let info = Ctap2GetInfoResponse {