Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b93c2c6
refactor(cable): introduce CableDataChannel transport abstraction
AlfioEmanueleFresta May 14, 2026
fc9a1d3
refactor(cable): move Noise protocol onto the CableDataChannel abstra…
AlfioEmanueleFresta May 14, 2026
80256a0
feat(cable): parse the CTAP 2.3 hybrid advertisement suffix
AlfioEmanueleFresta May 14, 2026
cf241b6
feat(cable): use QR key 6 for the CTAP 2.3 hybrid transport-channels …
AlfioEmanueleFresta May 14, 2026
a628068
feat(cable): add bluer-backed L2CAP data channel
AlfioEmanueleFresta May 14, 2026
b28770b
feat(cable): connect over BLE L2CAP when the CMHD offers it
AlfioEmanueleFresta May 14, 2026
72ec321
docs(cable): note CTAP 2.3 hybrid BLE data channel support
AlfioEmanueleFresta May 14, 2026
a6f6141
fix(cable): use bluer Stream::connect for L2CAP, drop set_security
AlfioEmanueleFresta May 15, 2026
389446e
fix(cable): wait for negotiated L2CAP send MTU before first write
AlfioEmanueleFresta May 15, 2026
4be7fe7
fix(cable): omit QR key 6 by default to keep legacy caBLE clients happy
AlfioEmanueleFresta May 15, 2026
cd2369b
fix(cable): omit QR key 4 when state_assisted is false
AlfioEmanueleFresta May 15, 2026
df8842e
feat(cable): require an explicit CableTransports in QR constructors
AlfioEmanueleFresta May 15, 2026
9c4b96f
feat(cable): fall back to a fresh QR when the peer offers no linking …
AlfioEmanueleFresta May 15, 2026
ba7b618
refactor(cable): rename webauthn_cable example to webauthn_cable_wss
AlfioEmanueleFresta May 15, 2026
fd614e2
feat(cable): add webauthn_cable example exercising both transports
AlfioEmanueleFresta May 15, 2026
0c42f4e
fix(cable): treat ECONNRESET / EPIPE on L2CAP read as a clean close
AlfioEmanueleFresta May 15, 2026
78214b6
chore(cable): drop dead unsafe Send/Sync, fix top-of-README table
AlfioEmanueleFresta May 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ _Looking for the D-Bus API proposal?_ Check out [credentialsd][credentialsd].
- 🟢 Discoverable credentials (resident keys)
- 🟢 Hybrid transport (caBLE v2): QR-initiated transactions
- 🟢 Hybrid transport (caBLE v2): State-assisted transactions (remember this phone)
- 🟢 Hybrid transport (CTAP 2.3): direct BLE L2CAP data channel, QR-initiated, no tunnel server

## Runtime requirements

Expand All @@ -50,7 +51,7 @@ Validating the relying party ID against the calling origin requires the [Public
| **Bluetooth Low Energy** | 🟢 Supported (bluez) | 🟢 Supported (bluez) |
| **NFC** | 🟢 Supported (pcsc or libnfc) | 🟢 Supported (pcsc or libnfc) |
| **TPM 2.0 (Platform)** | 🟠 Planned ([#4][#4]) | 🟠 Planned ([#4][#4]) |
| **Hybrid (QR code scan, aka caBLE v2)** | N/A | 🟢 Supported |
| **Hybrid (QR code scan, caBLE v2 + CTAP 2.3)** | N/A | 🟢 Supported |

## Example programs

Expand All @@ -72,7 +73,8 @@ WebAuthn examples consume and emit JSON per the [WebAuthn IDL][webauthn].
| **USB (HID)** | `cargo run --example u2f_hid` | `cargo run --example webauthn_hid` |
| **Bluetooth (BLE)** | `cargo run --example u2f_ble` | — |
| **NFC** [^nfc] | `cargo run --features nfc-backend-pcsc --example u2f_nfc`<br>`cargo run --features nfc-backend-libnfc --example u2f_nfc` | `cargo run --features nfc-backend-pcsc --example webauthn_nfc`<br>`cargo run --features nfc-backend-libnfc --example webauthn_nfc` |
| **Hybrid (caBLE v2)** | — | `cargo run --example webauthn_cable` |
| **Hybrid (caBLE v2 + CTAP 2.3)** | — | `cargo run --example webauthn_cable` |
| **Hybrid (caBLE v2)** | — | `cargo run --example webauthn_cable_wss` |

[^nfc]: `nfc-backend-pcsc` is pure userspace and recommended on most systems. `nfc-backend-libnfc` requires the `libnfc` system library. Both can be enabled together; the first FIDO device found by either backend is used.

Expand Down
5 changes: 5 additions & 0 deletions libwebauthn/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ 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"] }
thiserror = "2.0.12"
serde_json = "1.0.141"
apdu-core = { version = "0.4.0", optional = true }
Expand Down Expand Up @@ -125,6 +126,10 @@ required-features = ["nfc"]
name = "webauthn_cable"
path = "examples/ceremony/webauthn_cable.rs"

[[example]]
name = "webauthn_cable_wss"
path = "examples/ceremony/webauthn_cable_wss.rs"

[[example]]
name = "webauthn_extensions_hid"
path = "examples/features/webauthn_extensions_hid.rs"
Expand Down
101 changes: 28 additions & 73 deletions libwebauthn/examples/ceremony/webauthn_cable.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
//! caBLE / CTAP 2.3 hybrid: the QR advertises both the WebSocket tunnel and
//! the BLE L2CAP channel so the authenticator can pick either one. Transient
//! MakeCredential only.
use std::error::Error;
use std::sync::Arc;
use std::time::Duration;

use libwebauthn::transport::cable::is_available;
use libwebauthn::transport::cable::known_devices::{
CableKnownDevice, ClientPayloadHint, EphemeralDeviceInfoStore,
use libwebauthn::transport::cable::qr_code_device::{
CableQrCodeDevice, CableTransports, QrCodeOperationHint,
};
use libwebauthn::transport::cable::qr_code_device::{CableQrCodeDevice, QrCodeOperationHint};
use qrcode::render::unicode;
use qrcode::QrCode;
use tokio::time::sleep;

use libwebauthn::ops::webauthn::{
DatFilePublicSuffixList, GetAssertionRequest, JsonFormat, MakeCredentialRequest, RequestOrigin,
WebAuthnIDL as _, WebAuthnIDLResponse as _,
DatFilePublicSuffixList, JsonFormat, MakeCredentialRequest, RequestOrigin, WebAuthnIDL as _,
WebAuthnIDLResponse as _,
};
use libwebauthn::transport::{Channel as _, Device};
use libwebauthn::webauthn::WebAuthn;
Expand Down Expand Up @@ -46,15 +45,6 @@ const MAKE_CREDENTIAL_REQUEST: &str = r#"
}
"#;

const GET_ASSERTION_REQUEST: &str = r#"
{
"challenge": "Y3JlZGVudGlhbHMtZm9yLWxpbnV4L2xpYndlYmF1dGhu",
"timeout": 120000,
"rpId": "example.org",
"userVerification": "discouraged"
}
"#;

#[tokio::main]
pub async fn main() -> Result<(), Box<dyn Error>> {
common::setup_logging();
Expand All @@ -64,74 +54,39 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
return Err("Cable transport not available".into());
}

let device_info_store = Arc::new(EphemeralDeviceInfoStore::default());
let request_origin: RequestOrigin = "https://example.org".try_into().expect("Invalid origin");
let psl = DatFilePublicSuffixList::from_system_file().expect(
"PSL not available; install the publicsuffix-list package or pass an explicit path",
);

{
let mut device: CableQrCodeDevice = CableQrCodeDevice::new_persistent(
QrCodeOperationHint::MakeCredential,
device_info_store.clone(),
)?;

println!("Created QR code, awaiting for advertisement.");
let qr_code = QrCode::new(device.qr_code.to_string()).unwrap();
let image = qr_code
.render::<unicode::Dense1x2>()
.dark_color(unicode::Dense1x2::Light)
.light_color(unicode::Dense1x2::Dark)
.build();
println!("{}", image);
let mut device: CableQrCodeDevice = CableQrCodeDevice::new_transient(
QrCodeOperationHint::MakeCredential,
CableTransports::CloudAssistedOrLocal,
)?;

let mut channel = device.channel().await.unwrap();
println!("Tunnel established {:?}", channel);
println!("Created QR code, awaiting for advertisement.");
let qr_code = QrCode::new(device.qr_code.to_string()).unwrap();
let image = qr_code
.render::<unicode::Dense1x2>()
.dark_color(unicode::Dense1x2::Light)
.light_color(unicode::Dense1x2::Dark)
.build();
println!("{}", image);

let state_recv = channel.get_ux_update_receiver();
tokio::spawn(common::handle_cable_updates(state_recv));

let request =
MakeCredentialRequest::from_json(&request_origin, &psl, MAKE_CREDENTIAL_REQUEST)
.expect("Failed to parse request JSON");

let response = retry_user_errors!(channel.webauthn_make_credential(&request)).unwrap();
let response_json = response
.to_json_string(&request, JsonFormat::Prettified)
.expect("Failed to serialize MakeCredential response");
println!("WebAuthn MakeCredential response (JSON):\n{response_json}");
}

println!("Waiting for 5 seconds before contacting the device...");
sleep(Duration::from_secs(5)).await;

let all_devices = device_info_store.list_all().await;
let (_known_device_id, known_device_info) =
all_devices.first().expect("No known devices found");

let mut known_device: CableKnownDevice = CableKnownDevice::new(
ClientPayloadHint::GetAssertion,
known_device_info,
device_info_store.clone(),
)
.await
.unwrap();

let mut channel = known_device.channel().await.unwrap();
println!("Tunnel established {:?}", channel);
let mut channel = device.channel().await.unwrap();
println!("Channel established {:?}", channel);

let state_recv = channel.get_ux_update_receiver();
tokio::spawn(common::handle_cable_updates(state_recv));

let request = GetAssertionRequest::from_json(&request_origin, &psl, GET_ASSERTION_REQUEST)
let request = MakeCredentialRequest::from_json(&request_origin, &psl, MAKE_CREDENTIAL_REQUEST)
.expect("Failed to parse request JSON");
let response = retry_user_errors!(channel.webauthn_get_assertion(&request)).unwrap();
for assertion in &response.assertions {
let assertion_json = assertion
.to_json_string(&request, JsonFormat::Prettified)
.expect("Failed to serialize GetAssertion response");
println!("WebAuthn GetAssertion response (JSON):\n{assertion_json}");
}

let response = retry_user_errors!(channel.webauthn_make_credential(&request)).unwrap();
let response_json = response
.to_json_string(&request, JsonFormat::Prettified)
.expect("Failed to serialize MakeCredential response");
println!("WebAuthn MakeCredential response (JSON):\n{response_json}");

Ok(())
}
Loading
Loading