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..ef2c68fe 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,15 @@ 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::CloudAssistedOrLocal) + { + 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 +355,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..42d9c6e1 100644 --- a/credentialsd/src/gateway/util.rs +++ b/credentialsd/src/gateway/util.rs @@ -9,57 +9,49 @@ 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"); - WebAuthnError::SecurityError - })?; - RelyingPartyId::try_from(rp_id_str).map_err(|_| { - tracing::info!("Invalid relying party ID"); - WebAuthnError::TypeError - }) +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"), + } + } } -/// 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 +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()?), + ), + } + } +} + +fn load_system_psl() -> Result { + SystemPublicSuffixList::auto().map_err(|err| { + tracing::error!("Failed to load system Public Suffix List: {err}"); + WebAuthnError::NotAllowedError }) } @@ -76,27 +68,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: LibwebauthnRequestOrigin = request_environment.try_into()?; + 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 +122,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: LibwebauthnRequestOrigin = request_environment.try_into()?; + 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) }