From 4cabdba2e86bab3dc2022be290f056699099e9b8 Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Mon, 18 May 2026 20:35:12 +0100 Subject: [PATCH 1/3] chore(deps): bump libwebauthn to 0.5.0 - Rename feature flags: libnfc/pcsc -> nfc-backend-libnfc/nfc-backend-pcsc - Migrate to the new from_json(RequestOrigin, PublicSuffixList, json) API - Convert NavigationContext to libwebauthn RequestOrigin (synthesise https:// for AppId origins) - Use SystemPublicSuffixList::auto() to validate rpId - MakeCredentialRequest.cross_origin renamed to top_origin - CableQrCodeDevice::new_transient now takes a CableTransports arg (CloudAssistedOnly preserves legacy caBLE behaviour) - Assertion no longer exposes large_blob_key --- Cargo.lock | 117 ++++++++++++++- credentialsd/Cargo.toml | 5 +- credentialsd/src/credential_service/hybrid.rs | 20 +-- credentialsd/src/credential_service/mod.rs | 4 +- credentialsd/src/gateway/util.rs | 135 +++++++----------- 5 files changed, 180 insertions(+), 101 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a1c226f1..2724c15f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -467,6 +467,27 @@ dependencies = [ "piper", ] +[[package]] +name = "bluer" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af68112f5c60196495c8b0eea68349817855f565df5b04b2477916d09fb1a901" +dependencies = [ + "futures", + "hex", + "libc", + "log", + "macaddr", + "nix", + "num-derive", + "num-traits", + "serde", + "serde_json", + "strum", + "tokio", + "uuid", +] + [[package]] name = "bluez-async" version = "0.8.2" @@ -639,6 +660,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chacha20" version = "0.9.1" @@ -1188,6 +1215,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fragile" version = "2.0.1" @@ -2072,9 +2108,9 @@ dependencies = [ [[package]] name = "libwebauthn" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f74ddf06fefa81809987cb138dc265bcd50131bdce8f9d4ddc7e409a5c7167" +checksum = "a559a67cfb294f94c6e20669493c246cbe4232d2facdb02bae6130721d322f3e" dependencies = [ "aes", "apdu", @@ -2082,6 +2118,7 @@ dependencies = [ "async-trait", "base64-url", "bitflags 2.11.0", + "bluer", "btleplug", "byteorder", "cbc", @@ -2105,6 +2142,7 @@ dependencies = [ "num_enum", "p256", "pcsc", + "publicsuffix", "rand 0.8.5", "rustls", "serde", @@ -2124,6 +2162,7 @@ dependencies = [ "tokio-tungstenite", "tracing", "tungstenite", + "url", "uuid", "x509-parser", ] @@ -2171,6 +2210,12 @@ dependencies = [ "value-bag", ] +[[package]] +name = "macaddr" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baee0bbc17ce759db233beb01648088061bf678383130602a298e6998eedb2d8" + [[package]] name = "malloc_buf" version = "0.0.6" @@ -2276,6 +2321,18 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -2567,6 +2624,12 @@ dependencies = [ "base64ct", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2731,6 +2794,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + [[package]] name = "pxfm" version = "0.1.27" @@ -3297,6 +3376,28 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.116", +] + [[package]] name = "subtle" version = "2.6.1" @@ -3728,6 +3829,18 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + [[package]] name = "utf-8" version = "0.7.6" diff --git a/credentialsd/Cargo.toml b/credentialsd/Cargo.toml index 10e0115c..39bcdf76 100644 --- a/credentialsd/Cargo.toml +++ b/credentialsd/Cargo.toml @@ -12,9 +12,8 @@ base64 = "0.22.1" credentialsd-common = { path = "../credentialsd-common" } futures-lite.workspace = true libc.workspace = true -libwebauthn = { version = "0.3.0", features = ["libnfc","pcsc"] } -# TODO: split nfc and pcsc into separate features -# Also, 0.6.1 fails to build with non-vendored library. +libwebauthn = { version = "0.5.0", features = ["nfc-backend-libnfc", "nfc-backend-pcsc"] } +# 0.6.1 fails to build with non-vendored library. # https://github.com/alexrsagen/rs-nfc1/issues/15 nfc1 = { version = "=0.6.0", default-features = false } rand = "0.9.2" diff --git a/credentialsd/src/credential_service/hybrid.rs b/credentialsd/src/credential_service/hybrid.rs index dde57562..3171647e 100644 --- a/credentialsd/src/credential_service/hybrid.rs +++ b/credentialsd/src/credential_service/hybrid.rs @@ -9,7 +9,9 @@ use tokio::sync::mpsc::{self, Sender}; use tracing::{debug, error}; use libwebauthn::transport::cable::channel::{CableUpdate, CableUxUpdate}; -use libwebauthn::transport::cable::qr_code_device::{CableQrCodeDevice, QrCodeOperationHint}; +use libwebauthn::transport::cable::qr_code_device::{ + CableQrCodeDevice, CableTransports, QrCodeOperationHint, +}; use libwebauthn::transport::{Channel, Device}; use libwebauthn::webauthn::{Error as WebAuthnError, WebAuthn}; @@ -51,13 +53,14 @@ impl HybridHandler for InternalHybridHandler { QrCodeOperationHint::GetAssertionRequest } }; - let mut device = match CableQrCodeDevice::new_transient(hint) { - Ok(device) => device, - Err(err) => { - tracing::error!("Failed to create caBLE QR code device: {:?}", err); - return; - } - }; + let mut device = + match CableQrCodeDevice::new_transient(hint, CableTransports::CloudAssistedOnly) { + Ok(device) => device, + Err(err) => { + tracing::error!("Failed to create caBLE QR code device: {:?}", err); + return; + } + }; 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); @@ -351,7 +354,6 @@ pub(super) mod test { user: None, credentials_count: Some(1), user_selected: None, - large_blob_key: None, unsigned_extensions_output: None, enterprise_attestation: None, attestation_statement: None, diff --git a/credentialsd/src/credential_service/mod.rs b/credentialsd/src/credential_service/mod.rs index 7bc8fc0a..02d548f0 100644 --- a/credentialsd/src/credential_service/mod.rs +++ b/credentialsd/src/credential_service/mod.rs @@ -366,14 +366,13 @@ mod test { fn create_credential_request() -> CredentialRequest { let challenge = "Ox0AXQz7WUER7BGQFzvVrQbReTkS3sepVGj26qfUhhrWSarkDbGF4T4NuCY1aAwHYzOzKMJJ2YRSatetl0D9bQ"; let origin = "webauthn.io".to_string(); - let is_cross_origin = false; let challenge_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD .decode(challenge) .expect("valid base64url challenge"); let make_request = MakeCredentialRequest { challenge: challenge_bytes, origin: origin.clone(), - cross_origin: Some(is_cross_origin), + top_origin: None, relying_party: Ctap2PublicKeyCredentialRpEntity { id: "webauthn.io".to_string(), name: Some("webauthn.io".to_string()), @@ -435,7 +434,6 @@ mod test { user: None, credentials_count: Some(1), user_selected: None, - large_blob_key: None, unsigned_extensions_output: None, enterprise_attestation: None, attestation_statement: None, diff --git a/credentialsd/src/gateway/util.rs b/credentialsd/src/gateway/util.rs index f1fac4dc..864b256a 100644 --- a/credentialsd/src/gateway/util.rs +++ b/credentialsd/src/gateway/util.rs @@ -9,57 +9,46 @@ use credentialsd_common::{ GetPublicKeyCredentialResponse, }, }; +use libwebauthn::ops::webauthn::idl::origin::{ + Origin as LibwebauthnOrigin, RequestOrigin as LibwebauthnRequestOrigin, +}; +use libwebauthn::ops::webauthn::psl::SystemPublicSuffixList; use crate::model::{GetAssertionResponseInternal, MakeCredentialResponseInternal}; use crate::webauthn::{ - GetAssertionRequest, MakeCredentialRequest, NavigationContext, Origin, RelyingPartyId, - WebAuthnIDL, WebAuthnIDLResponse, + GetAssertionRequest, MakeCredentialRequest, NavigationContext, Origin, WebAuthnIDL, + WebAuthnIDLResponse, }; -/// Reads the rpId from a create-credential request JSON (`rp.id`). -/// -/// Used as a fallback when the origin is an AppId and the effective domain -/// cannot be derived from the origin alone. -// TODO(libwebauthn#185) -fn peek_make_credential_rp_id(request_json: &str) -> Result { - let value = serde_json::from_str::(request_json).map_err(|err| { - tracing::info!("Invalid request JSON: {err}"); - WebAuthnError::TypeError - })?; - let rp_id_str = value - .get("rp") - .and_then(|rp| rp.get("id")) - .and_then(|id| id.as_str()) - .ok_or_else(|| { - tracing::info!("RP ID required if using app ID as origin"); +fn to_libwebauthn_origin(o: &Origin) -> Result { + match o { + Origin::Https { .. } => o.to_string().parse().map_err(|err| { + tracing::info!("Cannot convert origin to libwebauthn Origin: {err}"); WebAuthnError::SecurityError - })?; - RelyingPartyId::try_from(rp_id_str).map_err(|_| { - tracing::info!("Invalid relying party ID"); - WebAuthnError::TypeError - }) + }), + // TODO: AppId support is being removed. + Origin::AppId(_) => unimplemented!("AppId origins are not supported"), + } } -/// Reads the rpId from a get-credential request JSON (`rpId`). -/// -/// Used as a fallback when the origin is an AppId and the effective domain -/// cannot be derived from the origin alone. -// TODO(libwebauthn#185) -fn peek_get_assertion_rp_id(request_json: &str) -> Result { - let value = serde_json::from_str::(request_json).map_err(|err| { - tracing::info!("Invalid request JSON: {err}"); - WebAuthnError::TypeError - })?; - let rp_id_str = value - .get("rpId") - .and_then(|id| id.as_str()) - .ok_or_else(|| { - tracing::info!("RP ID required if using app ID as origin"); - WebAuthnError::SecurityError - })?; - RelyingPartyId::try_from(rp_id_str).map_err(|_| { - tracing::info!("Invalid relying party ID"); - WebAuthnError::TypeError +fn to_libwebauthn_request_origin( + context: &NavigationContext, +) -> Result { + match context { + NavigationContext::SameOrigin(o) => { + Ok(LibwebauthnRequestOrigin::new(to_libwebauthn_origin(o)?)) + } + NavigationContext::CrossOrigin((o, top)) => Ok(LibwebauthnRequestOrigin::new_cross_origin( + to_libwebauthn_origin(o)?, + to_libwebauthn_origin(top)?, + )), + } +} + +fn load_system_psl() -> Result { + SystemPublicSuffixList::auto().map_err(|err| { + tracing::error!("Failed to load system Public Suffix List: {err}"); + WebAuthnError::NotAllowedError }) } @@ -76,27 +65,16 @@ pub(super) fn create_credential_request_try_into_ctap2( WebAuthnError::NotSupportedError })?; - let origin = request_environment.origin(); - let rp_id = match origin { - Origin::Https { .. } => RelyingPartyId::try_from(origin).map_err(|err| { - tracing::info!("Cannot derive relying party ID from origin: {err}"); - WebAuthnError::SecurityError - })?, - Origin::AppId(_) => peek_make_credential_rp_id(&options.request_json)?, - }; - - let mut make_cred_request = MakeCredentialRequest::from_json(&rp_id, &options.request_json) - .map_err(|err| { - tracing::info!("Failed to parse MakeCredential request JSON: {err}"); - WebAuthnError::TypeError - })?; - - // TODO(libwebauthn#185) - make_cred_request.origin = origin.to_string(); - make_cred_request.cross_origin = Some(matches!( - request_environment, - NavigationContext::CrossOrigin(_) - )); + let request_origin = to_libwebauthn_request_origin(request_environment)?; + let psl = load_system_psl()?; + + let make_cred_request = + MakeCredentialRequest::from_json(&request_origin, &psl, &options.request_json).map_err( + |err| { + tracing::info!("Failed to parse MakeCredential request JSON: {err}"); + WebAuthnError::TypeError + }, + )?; Ok(make_cred_request) } @@ -141,27 +119,16 @@ pub(super) fn get_credential_request_try_into_ctap2( WebAuthnError::NotSupportedError })?; - let origin = request_environment.origin(); - let rp_id = match origin { - Origin::Https { .. } => RelyingPartyId::try_from(origin).map_err(|err| { - tracing::info!("Cannot derive relying party ID from origin: {err}"); - WebAuthnError::SecurityError - })?, - Origin::AppId(_) => peek_get_assertion_rp_id(&options.request_json)?, - }; - - let mut get_assertion_request = GetAssertionRequest::from_json(&rp_id, &options.request_json) - .map_err(|err| { - tracing::info!("Failed to parse GetAssertion request JSON: {err}"); - WebAuthnError::TypeError - })?; + let request_origin = to_libwebauthn_request_origin(request_environment)?; + let psl = load_system_psl()?; - // TODO(libwebauthn#185) - get_assertion_request.origin = origin.to_string(); - get_assertion_request.cross_origin = Some(matches!( - request_environment, - NavigationContext::CrossOrigin(_) - )); + let get_assertion_request = + GetAssertionRequest::from_json(&request_origin, &psl, &options.request_json).map_err( + |err| { + tracing::info!("Failed to parse GetAssertion request JSON: {err}"); + WebAuthnError::TypeError + }, + )?; Ok(get_assertion_request) } From a02b0330c1ff3f98848cf4ea1aa5a7ad012feae4 Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Mon, 18 May 2026 20:46:28 +0100 Subject: [PATCH 2/3] feat(hybrid): advertise BLE L2CAP transport in QR (CloudAssistedOrLocal) --- credentialsd/src/credential_service/hybrid.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/credentialsd/src/credential_service/hybrid.rs b/credentialsd/src/credential_service/hybrid.rs index 3171647e..ef2c68fe 100644 --- a/credentialsd/src/credential_service/hybrid.rs +++ b/credentialsd/src/credential_service/hybrid.rs @@ -54,7 +54,8 @@ impl HybridHandler for InternalHybridHandler { } }; let mut device = - match CableQrCodeDevice::new_transient(hint, CableTransports::CloudAssistedOnly) { + match CableQrCodeDevice::new_transient(hint, CableTransports::CloudAssistedOrLocal) + { Ok(device) => device, Err(err) => { tracing::error!("Failed to create caBLE QR code device: {:?}", err); From 6e917556b11ea7fe6d2a27037f0c9cc0eedb6017 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Fri, 22 May 2026 06:54:30 -0500 Subject: [PATCH 3/3] daemon: Use TryFrom pattern for origin conversions --- credentialsd/src/gateway/util.rs | 43 +++++++++++++++++--------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/credentialsd/src/gateway/util.rs b/credentialsd/src/gateway/util.rs index 864b256a..42d9c6e1 100644 --- a/credentialsd/src/gateway/util.rs +++ b/credentialsd/src/gateway/util.rs @@ -20,28 +20,31 @@ use crate::webauthn::{ WebAuthnIDLResponse, }; -fn to_libwebauthn_origin(o: &Origin) -> Result { - match o { - Origin::Https { .. } => o.to_string().parse().map_err(|err| { - tracing::info!("Cannot convert origin to libwebauthn Origin: {err}"); - WebAuthnError::SecurityError - }), - // TODO: AppId support is being removed. - Origin::AppId(_) => unimplemented!("AppId origins are not supported"), +impl TryFrom<&Origin> for LibwebauthnOrigin { + type Error = WebAuthnError; + + fn try_from(value: &Origin) -> Result { + match value { + Origin::Https { .. } => value.to_string().parse().map_err(|err| { + tracing::info!("Cannot convert origin to libwebauthn Origin: {err}"); + WebAuthnError::SecurityError + }), + // TODO: AppId support is being removed. + Origin::AppId(_) => unimplemented!("AppId origins are not supported"), + } } } -fn to_libwebauthn_request_origin( - context: &NavigationContext, -) -> Result { - match context { - NavigationContext::SameOrigin(o) => { - Ok(LibwebauthnRequestOrigin::new(to_libwebauthn_origin(o)?)) +impl TryFrom<&NavigationContext> for LibwebauthnRequestOrigin { + type Error = WebAuthnError; + + fn try_from(value: &NavigationContext) -> Result { + match value { + NavigationContext::SameOrigin(o) => Ok(LibwebauthnRequestOrigin::new(o.try_into()?)), + NavigationContext::CrossOrigin((o, top)) => Ok( + LibwebauthnRequestOrigin::new_cross_origin(o.try_into()?, top.try_into()?), + ), } - NavigationContext::CrossOrigin((o, top)) => Ok(LibwebauthnRequestOrigin::new_cross_origin( - to_libwebauthn_origin(o)?, - to_libwebauthn_origin(top)?, - )), } } @@ -65,7 +68,7 @@ pub(super) fn create_credential_request_try_into_ctap2( WebAuthnError::NotSupportedError })?; - let request_origin = to_libwebauthn_request_origin(request_environment)?; + let request_origin: LibwebauthnRequestOrigin = request_environment.try_into()?; let psl = load_system_psl()?; let make_cred_request = @@ -119,7 +122,7 @@ pub(super) fn get_credential_request_try_into_ctap2( WebAuthnError::NotSupportedError })?; - let request_origin = to_libwebauthn_request_origin(request_environment)?; + let request_origin: LibwebauthnRequestOrigin = request_environment.try_into()?; let psl = load_system_psl()?; let get_assertion_request =