From 3a7740d4ac9265f8b00fc0182d8fdb8af05b1c5d Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Sun, 1 Nov 2026 22:39:16 +0000 Subject: [PATCH 1/4] daemon: Bump libwebauthn to 0.11.0 The cable transport is renamed to hybrid in 0.11.0, so move the daemon to the new type names. --- Cargo.lock | 119 +++++++++--------- credentialsd/Cargo.toml | 8 +- credentialsd/src/credential_service/hybrid.rs | 42 +++---- credentialsd/src/credential_service/mod.rs | 2 +- 4 files changed, 80 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18a5d7a..a3fbecd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -323,24 +323,15 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.91" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", "syn 3.0.3", ] -[[package]] -name = "atomic-polyfill" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" -dependencies = [ - "critical-section", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -766,9 +757,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cosey" -version = "0.3.2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75494895fa1a9713ca725ddf2db084ee84fb0c20938fdd7c89293febe732d30a" +checksum = "540d9a2a9f2d35a876fedb35f4c0b51c7a69f71f042252cf34fc159708f962d4" dependencies = [ "heapless-bytes", "serde", @@ -814,7 +805,7 @@ dependencies = [ "futures-lite", "gio", "libwebauthn", - "nfc1 0.7.1", + "nfc1", "rand 0.10.2", "ring", "serde", @@ -857,12 +848,6 @@ dependencies = [ "zbus", ] -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - [[package]] name = "crossbeam-utils" version = "0.8.22" @@ -900,9 +885,9 @@ dependencies = [ [[package]] name = "ctap-types" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb8b105c5e728afd373e99874f0c1911c170b3a56848456cc16feb4506321606" +checksum = "ab74c7ac39f3bde2a13960ab8257975a35cfcc3d45420ca59b21b55dcbadc6f9" dependencies = [ "bitflags 1.3.2", "cbor-smol", @@ -1831,9 +1816,9 @@ dependencies = [ [[package]] name = "hash32" -version = "0.2.1" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" dependencies = [ "byteorder", ] @@ -1852,27 +1837,23 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heapless" -version = "0.7.17" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" dependencies = [ - "atomic-polyfill", "hash32", - "rustc_version", - "serde", - "spin", + "serde_core", "stable_deref_trait", ] [[package]] name = "heapless-bytes" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7285eba272c6af3e9f15fb9e1c1b6e7d35aa70580ffe0d47af017e97dfb6f48b" +checksum = "774f9dbf3f3a66edc65579e68b9f564ad5ec52e770a755af46b87d40909b8024" dependencies = [ "heapless", - "serde", - "typenum", + "serde_core", ] [[package]] @@ -2166,9 +2147,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iso7816" -version = "0.1.4" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c7e91da489667bb054f9cd2f1c60cc2ac4478a899f403d11dbc62189215b0" +checksum = "cedf53c7a948f16076ec811512f186a90009540f3cdfdb8263737244485a9015" dependencies = [ "heapless", ] @@ -2307,8 +2288,9 @@ dependencies = [ [[package]] name = "libwebauthn" -version = "0.10.0" -source = "git+https://github.com/linux-credentials/libwebauthn.git?rev=efa374d6303eb1ebeff373878fa76736f3d2c308#efa374d6303eb1ebeff373878fa76736f3d2c308" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bcae5a7cd420e87f4f1e53eab05651135d403d94beb58905a32aac385cdc7c0" dependencies = [ "aes", "aes-gcm", @@ -2317,7 +2299,6 @@ dependencies = [ "async-trait", "base64-url", "bitflags 2.13.1", - "bluer", "btleplug", "byteorder", "cbc", @@ -2328,7 +2309,6 @@ dependencies = [ "der", "flate2", "futures", - "heapless", "hex", "hidapi", "hkdf", @@ -2338,7 +2318,7 @@ dependencies = [ "idna", "maplit", "mockall", - "nfc1 0.6.0", + "nfc1", "nfc1-sys", "num-derive", "num-traits", @@ -2346,9 +2326,9 @@ dependencies = [ "p256", "pcsc", "publicsuffix", + "pxp", "rand 0.8.7", "reqwest", - "rustls", "serde", "serde-indexed 0.2.0", "serde_bytes", @@ -2357,16 +2337,13 @@ dependencies = [ "serde_json", "serde_repr", "sha2", - "snow", "spki", "text_io", "thiserror 2.0.19", "time", "tokio", "tokio-stream", - "tokio-tungstenite", "tracing", - "tungstenite", "url", "uuid", "x509-parser", @@ -2527,15 +2504,6 @@ dependencies = [ "pxfm", ] -[[package]] -name = "nfc1" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d6dc2e4110af159c220d2d004661e380b6c40d93c5b04e839e4944f9d5291d" -dependencies = [ - "nfc1-sys", -] - [[package]] name = "nfc1" version = "0.7.1" @@ -3052,6 +3020,40 @@ version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" +[[package]] +name = "pxp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a68da311ec7a720ec6e41339fb6d593966d38d7f4b251277d1021484d9d8cdd" +dependencies = [ + "aes", + "async-trait", + "base64-url", + "bluer", + "btleplug", + "futures", + "hex", + "hkdf", + "hmac", + "p256", + "rand 0.8.7", + "rustls", + "serde", + "serde-indexed 0.2.0", + "serde_bytes", + "serde_cbor_2", + "serde_repr", + "sha2", + "snow", + "thiserror 2.0.19", + "tokio", + "tokio-tungstenite", + "tracing", + "tungstenite", + "url", + "uuid", +] + [[package]] name = "qrcode" version = "0.14.1" @@ -3741,15 +3743,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "spin" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" -dependencies = [ - "lock_api", -] - [[package]] name = "spki" version = "0.7.3" diff --git a/credentialsd/Cargo.toml b/credentialsd/Cargo.toml index fa0fae2..a0e4272 100644 --- a/credentialsd/Cargo.toml +++ b/credentialsd/Cargo.toml @@ -15,14 +15,10 @@ tracing-subscriber.workspace = true zbus = { workspace = true, default-features = false, features = ["tokio"] } async-stream = "=0.3.6" -async-trait = "=0.1.91" +async-trait = "=0.1.92" base64 = "=0.22.1" futures = "=0.3.33" -libwebauthn = { - git = "https://github.com/linux-credentials/libwebauthn.git", - rev = "efa374d6303eb1ebeff373878fa76736f3d2c308", - features = ["nfc-backend-libnfc", "nfc-backend-pcsc", "reqwest-related-origins-source"] -} +libwebauthn = { version = "=0.11.0", features = ["nfc-backend-libnfc", "nfc-backend-pcsc", "reqwest-related-origins-source"] } nfc1 = { version = "=0.7.1", default-features = false } rand = "=0.10.2" ring = "=0.17.14" diff --git a/credentialsd/src/credential_service/hybrid.rs b/credentialsd/src/credential_service/hybrid.rs index 97f0c5b..cca80d5 100644 --- a/credentialsd/src/credential_service/hybrid.rs +++ b/credentialsd/src/credential_service/hybrid.rs @@ -8,9 +8,9 @@ use libwebauthn::{ proto::CtapError, transport::{ Channel, ChannelSettings, Device, - cable::{ - channel::{CableUpdate, CableUxUpdate}, - qr_code_device::{CableQrCodeDevice, CableTransports, QrCodeOperationHint}, + hybrid::{ + channel::{HybridUxUpdate, PxpUpdate}, + qr_code_device::{PxpQrCodeDevice, PxpTransports, QrCodeOperationHint}, }, }, webauthn::{WebAuthn, error::WebAuthnError}, @@ -65,25 +65,25 @@ impl HybridHandler for InternalHybridHandler { .map(|s| s.to_lowercase() == "true") .unwrap_or_default() { - CableTransports::CloudAssistedOrLocal + PxpTransports::CloudAssistedOrLocal } else { - CableTransports::CloudAssistedOnly + PxpTransports::CloudAssistedOnly }; // Outer retry loop: re-issues QR on non-terminating failures. - // Each iteration creates a fresh CableQrCodeDevice (the previous one + // Each iteration creates a fresh PxpQrCodeDevice (the previous one // is consumed by channel()), so the old QR secret is discarded. loop { - let mut device = match CableQrCodeDevice::new_transient(hint, hybrid_transports) { + let mut device = match PxpQrCodeDevice::new_transient(hint, hybrid_transports) { Ok(device) => device, Err(err) => { - tracing::error!("Failed to create caBLE QR code device: {:?}", err); + tracing::error!("Failed to create hybrid QR code device: {:?}", err); // Device creation failure cannot be retried meaningfully — // break to avoid a tight error loop. let _ = tx .send(HybridStateInternal::Failed( CredentialServiceError::Internal(format!( - "Failed to create caBLE device: {err:?}" + "Failed to create hybrid device: {err:?}" )), )) .await; @@ -93,7 +93,7 @@ impl HybridHandler for InternalHybridHandler { let qr_code = device.qr_code.to_string(); if let Err(err) = tx.send(HybridStateInternal::Init(qr_code)).await { - tracing::error!("Failed to send caBLE update: {:?}", err); + tracing::error!("Failed to send hybrid update: {:?}", err); break; } @@ -244,14 +244,14 @@ impl From<&HybridState> for BackgroundEvent { } } -/// Runs a single hybrid ceremony attempt: opens the caBLE channel, spawns the UX +/// Runs a single hybrid ceremony attempt: opens the hybrid channel, spawns the UX /// update forwarder, and drives the `webauthn_make_credential` / `webauthn_get_assertion` /// retry loop until a terminal result or cancellation. /// /// Returns `Ok(CredentialResponse)` on success, or `Err(CredentialServiceError)` on /// failure. `NonTerminatingCancellation` is returned when the cancellation token fires. async fn run_hybrid_ceremony( - device: &mut CableQrCodeDevice, + device: &mut PxpQrCodeDevice, request: &CredentialRequest, tx: &Sender, cancellation: CancellationToken, @@ -343,26 +343,26 @@ async fn run_hybrid_ceremony( async fn handle_hybrid_updates( state_sender: &Sender, - mut ux_update_receiver: broadcast::Receiver, + mut ux_update_receiver: broadcast::Receiver, ) { while let Ok(msg) = ux_update_receiver.recv().await { debug!(?msg, "Received hybrid update"); let new_state: Option = match msg { // Our UI already indicates this and authenticators over hybrid also generally indicate - CableUxUpdate::UvUpdate(UvUpdate::PresenceRequired) => None, - CableUxUpdate::UvUpdate(uv_update) => { + HybridUxUpdate::UvUpdate(UvUpdate::PresenceRequired) => None, + HybridUxUpdate::UvUpdate(uv_update) => { error!( "Received unexpected UV update in hybrid handler: {:?}", uv_update ); None } - CableUxUpdate::CableUpdate(cable_update) => match cable_update { - CableUpdate::ProximityCheck => None, - CableUpdate::Connecting => Some(HybridStateInternal::Connecting), - CableUpdate::Authenticating => Some(HybridStateInternal::Connecting), - CableUpdate::Connected => Some(HybridStateInternal::Connected), - CableUpdate::Error(transport_error) => { + HybridUxUpdate::PxpUpdate(pxp_update) => match pxp_update { + PxpUpdate::ProximityCheck => None, + PxpUpdate::Connecting => Some(HybridStateInternal::Connecting), + PxpUpdate::Authenticating => Some(HybridStateInternal::Connecting), + PxpUpdate::Connected => Some(HybridStateInternal::Connected), + PxpUpdate::Error(transport_error) => { error!(?transport_error, "Hybrid transport error"); Some(HybridStateInternal::Failed( CredentialServiceError::AuthenticatorError, diff --git a/credentialsd/src/credential_service/mod.rs b/credentialsd/src/credential_service/mod.rs index fd8189b..38ec879 100644 --- a/credentialsd/src/credential_service/mod.rs +++ b/credentialsd/src/credential_service/mod.rs @@ -318,7 +318,7 @@ impl Manage id: String::from("0"), transport: Transport::Usb, }]; - if libwebauthn::transport::cable::is_available().await { + if libwebauthn::transport::hybrid::is_available().await { devices.push(Device { id: String::from("1"), transport: Transport::HybridQr, From 061dc9936c92f4275ea04c0547e2d1c7b18e23eb Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Sun, 1 Nov 2026 22:39:16 +0000 Subject: [PATCH 2/4] daemon: Close the hybrid channel after the ceremony --- credentialsd/src/credential_service/hybrid.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/credentialsd/src/credential_service/hybrid.rs b/credentialsd/src/credential_service/hybrid.rs index cca80d5..a61ee97 100644 --- a/credentialsd/src/credential_service/hybrid.rs +++ b/credentialsd/src/credential_service/hybrid.rs @@ -329,7 +329,7 @@ async fn run_hybrid_ceremony( }; tracing::debug!("Polling hybrid channel for updates."); - match cancellation + let result = match cancellation .run_until_cancelled(wait_for_response_fut) .await { @@ -338,7 +338,11 @@ async fn run_hybrid_ceremony( tracing::debug!("Hybrid handler cancelled, stopping processing"); Err(CredentialServiceError::NonTerminatingCancellation) } - } + }; + + // Send the tunnel Shutdown so the phone does not show an error. + channel.close().await; + result } async fn handle_hybrid_updates( From ae93bbf2c817456c605e81be352ded170fb7efbd Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Sun, 1 Nov 2026 22:39:16 +0000 Subject: [PATCH 3/4] doc: Add pxp to the RUST_LOG examples --- .vscode/launch.json | 2 +- BUILDING.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 7670238..a45a932 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ "program": "${workspaceFolder}/build/credentialsd/src/credentialsd", "args": [], "env": { - "RUST_LOG": "credentialsd=debug,libwebauthn=debug,libwebauthn::webauthn=debug,libwebauthn=warn,libwebauthn::proto::ctap2::preflight=debug,libwebauthn::transport::channel=debug,zbus::object_server::debug,zbus=debug", + "RUST_LOG": "credentialsd=debug,libwebauthn=debug,pxp=debug,libwebauthn::webauthn=debug,libwebauthn=warn,libwebauthn::proto::ctap2::preflight=debug,libwebauthn::transport::channel=debug,zbus::object_server::debug,zbus=debug", "RUST_BACKTRACE": "1", "CREDSD_TRUSTED_CALLERS": "/usr/local/libexec/xdg-desktop-portal,/usr/bin/python3.14,${workspaceFolder}/../../portal/xdg-desktop-portal/build/desktop-portal/xdg-desktop-portal", "CREDSD_TRUSTED_APP_IDS": "xyz.iinuwa.credentialsd.DemoCredentialsUi", diff --git a/BUILDING.md b/BUILDING.md index 287dd49..5b3d860 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -176,7 +176,7 @@ $XDP_BINARY & # Run the server, with debug logging enabled, and configure the server to trust your xdg-desktop-portal export CREDSD_TRUSTED_CALLERS=$XDP_BINARY -export RUST_LOG=credentialsd=debug,libwebauthn=debug +export RUST_LOG=credentialsd=debug,libwebauthn=debug,pxp=debug $CREDSD_REPO/build/credentialsd/src/credentialsd & # Run the backend UI From 02703d40fe2bbe47ab9c0e15ff4ccbbc990c059c Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Mon, 2 Nov 2026 16:05:38 -0600 Subject: [PATCH 4/4] doc: Update changelog with libwebauthn 0.11.0 changes --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66005f8..f516d32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ - daemon: Fix all subsequent requests being rejected after the UI failed to launch. - daemon: Don't busy loop polling for NFC devices when NFC is enabled. - daemon: Use systemd unit name to determine whether a caller is trusted. (Thank you, @NyCodeGHG!) +- daemon: Update to libwebauthn 0.11.0, with various PXP and attestation format improvements. +- daemon: Change to standardized PXP BLE length-prefixed framing. +- daemon: Send PXP shutdown on close to prevent Android clients from hanging. - ui: Add Bulgarian translation (Thank you, @salif!) - webext: Try to read app ID from associated desktop entry (Thank you, @michaelbeaumont!) - ui: Basic recovery from failed attempts to use devices (e.g. flaky bluetooth)