diff --git a/Cargo.lock b/Cargo.lock index 946dbc58..6413ad3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1126,6 +1126,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.1.0" @@ -1761,7 +1770,7 @@ dependencies = [ [[package]] name = "libwebauthn" -version = "0.3.1" +version = "0.4.0" dependencies = [ "aes", "apdu", @@ -1814,6 +1823,7 @@ dependencies = [ "tracing", "tracing-subscriber", "tungstenite", + "url", "uuid", "x509-parser", ] @@ -2284,6 +2294,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.17" @@ -3598,6 +3614,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/libwebauthn-tests/tests/basic_ctap2.rs b/libwebauthn-tests/tests/basic_ctap2.rs index 7ea49230..96d7cc31 100644 --- a/libwebauthn-tests/tests/basic_ctap2.rs +++ b/libwebauthn-tests/tests/basic_ctap2.rs @@ -40,7 +40,7 @@ async fn test_webauthn_basic_ctap2() { let make_credentials_request = MakeCredentialRequest { challenge: Vec::from(challenge), origin: "example.org".to_owned(), - cross_origin: None, + top_origin: None, relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"), user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"), resident_key: Some(ResidentKeyRequirement::Discouraged), @@ -66,7 +66,7 @@ async fn test_webauthn_basic_ctap2() { relying_party_id: "example.org".to_owned(), challenge: Vec::from(challenge), origin: "example.org".to_string(), - cross_origin: None, + top_origin: None, allow: vec![credential], user_verification: UserVerificationRequirement::Discouraged, extensions: Some(GetAssertionRequestExtensions::default()), diff --git a/libwebauthn-tests/tests/preflight.rs b/libwebauthn-tests/tests/preflight.rs index cc71960d..3280f4d5 100644 --- a/libwebauthn-tests/tests/preflight.rs +++ b/libwebauthn-tests/tests/preflight.rs @@ -49,7 +49,7 @@ async fn make_credential_call( exclude: exclude_list, extensions: None, timeout: TIMEOUT, - cross_origin: None, + top_origin: None, }; let response = channel @@ -71,7 +71,7 @@ async fn get_assertion_call( user_verification: UserVerificationRequirement::Discouraged, extensions: None, timeout: TIMEOUT, - cross_origin: None, + top_origin: None, }; channel.webauthn_get_assertion(&get_assertion).await diff --git a/libwebauthn-tests/tests/prf.rs b/libwebauthn-tests/tests/prf.rs index 6c6da6ef..2223283e 100644 --- a/libwebauthn-tests/tests/prf.rs +++ b/libwebauthn-tests/tests/prf.rs @@ -116,7 +116,7 @@ async fn run_test_battery(channel: &mut HidChannel<'_>, using_pin: bool) { exclude: None, extensions: Some(extensions), timeout: TIMEOUT, - cross_origin: None, + top_origin: None, }; let state_recv = channel.get_ux_update_receiver(); @@ -175,7 +175,7 @@ async fn run_test_battery(channel: &mut HidChannel<'_>, using_pin: bool) { user_verification: UserVerificationRequirement::Preferred, extensions: None, timeout: TIMEOUT, - cross_origin: None, + top_origin: None, }; let _response = channel @@ -494,7 +494,7 @@ async fn run_success_test( ..Default::default() }), timeout: TIMEOUT, - cross_origin: None, + top_origin: None, }; let response = channel @@ -561,7 +561,7 @@ async fn run_failed_test( ..Default::default() }), timeout: TIMEOUT, - cross_origin: None, + top_origin: None, }; let response: Result<(), WebAuthnError> = loop { diff --git a/libwebauthn/Cargo.toml b/libwebauthn/Cargo.toml index 9bb3a06f..4536c05a 100644 --- a/libwebauthn/Cargo.toml +++ b/libwebauthn/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "libwebauthn" description = "FIDO2 (WebAuthn) and FIDO U2F platform library for Linux written in Rust " -version = "0.3.1" +version = "0.4.0" authors = ["Alfie Fresta "] edition = "2021" license-file = "../COPYING" @@ -32,6 +32,7 @@ base64-url = "3.0.0" dbus = "0.9.5" tracing = "0.1.29" idna = "1.0.3" +url = "2.5" maplit = "1.0.2" sha2 = "0.10.2" uuid = { version = "1.5.0", features = ["serde", "v4"] } diff --git a/libwebauthn/examples/prf_test.rs b/libwebauthn/examples/prf_test.rs index 577938b7..7b4a3733 100644 --- a/libwebauthn/examples/prf_test.rs +++ b/libwebauthn/examples/prf_test.rs @@ -149,7 +149,7 @@ async fn run_success_test( relying_party_id: "demo.yubico.com".to_owned(), challenge: Vec::from(challenge), origin: "demo.yubico.com".to_string(), - cross_origin: None, + top_origin: None, allow: vec![credential.clone()], user_verification: UserVerificationRequirement::Preferred, extensions: Some(GetAssertionRequestExtensions { diff --git a/libwebauthn/examples/webauthn_cable.rs b/libwebauthn/examples/webauthn_cable.rs index b6f4c7ba..4b8ac6f3 100644 --- a/libwebauthn/examples/webauthn_cable.rs +++ b/libwebauthn/examples/webauthn_cable.rs @@ -130,7 +130,7 @@ pub async fn main() -> Result<(), Box> { let make_credentials_request = MakeCredentialRequest { challenge: Vec::from(challenge), origin: "example.org".to_owned(), - cross_origin: None, + top_origin: None, relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"), user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"), resident_key: Some(ResidentKeyRequirement::Discouraged), @@ -170,7 +170,7 @@ pub async fn main() -> Result<(), Box> { relying_party_id: "example.org".to_owned(), challenge: Vec::from(challenge), origin: "example.org".to_string(), - cross_origin: None, + top_origin: None, allow: vec![credential], user_verification: UserVerificationRequirement::Discouraged, extensions: Some(GetAssertionRequestExtensions::default()), diff --git a/libwebauthn/examples/webauthn_extensions_hid.rs b/libwebauthn/examples/webauthn_extensions_hid.rs index 321ed311..d9d19da0 100644 --- a/libwebauthn/examples/webauthn_extensions_hid.rs +++ b/libwebauthn/examples/webauthn_extensions_hid.rs @@ -109,7 +109,7 @@ pub async fn main() -> Result<(), Box> { let make_credentials_request = MakeCredentialRequest { challenge: Vec::from(challenge), origin: "example.org".to_owned(), - cross_origin: None, + top_origin: None, relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"), user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"), resident_key: Some(ResidentKeyRequirement::Required), @@ -149,7 +149,7 @@ pub async fn main() -> Result<(), Box> { relying_party_id: "example.org".to_owned(), challenge: Vec::from(challenge), origin: "example.org".to_string(), - cross_origin: None, + top_origin: None, allow: vec![credential], user_verification: UserVerificationRequirement::Discouraged, extensions: Some(GetAssertionRequestExtensions { diff --git a/libwebauthn/examples/webauthn_hid.rs b/libwebauthn/examples/webauthn_hid.rs index 57ed0428..369577d5 100644 --- a/libwebauthn/examples/webauthn_hid.rs +++ b/libwebauthn/examples/webauthn_hid.rs @@ -121,7 +121,7 @@ pub async fn main() -> Result<(), Box> { let make_credentials_request = MakeCredentialRequest { challenge: Vec::from(challenge), origin: "example.org".to_owned(), - cross_origin: None, + top_origin: None, relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"), user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"), resident_key: Some(ResidentKeyRequirement::Discouraged), @@ -160,7 +160,7 @@ pub async fn main() -> Result<(), Box> { relying_party_id: "example.org".to_owned(), challenge: Vec::from(challenge), origin: "example.org".to_string(), - cross_origin: None, + top_origin: None, allow: vec![credential], user_verification: UserVerificationRequirement::Discouraged, extensions: Some(GetAssertionRequestExtensions::default()), diff --git a/libwebauthn/examples/webauthn_json_hid.rs b/libwebauthn/examples/webauthn_json_hid.rs index 3101a3b2..f7a652d4 100644 --- a/libwebauthn/examples/webauthn_json_hid.rs +++ b/libwebauthn/examples/webauthn_json_hid.rs @@ -8,7 +8,7 @@ use tokio::sync::broadcast::Receiver; use tracing_subscriber::{self, EnvFilter}; use libwebauthn::ops::webauthn::{ - GetAssertionRequest, JsonFormat, MakeCredentialRequest, RelyingPartyId, WebAuthnIDL as _, + GetAssertionRequest, JsonFormat, MakeCredentialRequest, RequestOrigin, WebAuthnIDL as _, WebAuthnIDLResponse as _, }; use libwebauthn::pin::PinRequestReason; @@ -79,7 +79,8 @@ pub async fn main() -> Result<(), Box> { let mut channel = device.channel().await?; channel.wink(TIMEOUT).await?; - let rpid = RelyingPartyId("example.org".to_owned()); + let request_origin: RequestOrigin = + "https://example.org".try_into().expect("Invalid origin"); let request_json = r#" { "rp": { @@ -105,7 +106,7 @@ pub async fn main() -> Result<(), Box> { } "#; let make_credentials_request: MakeCredentialRequest = - MakeCredentialRequest::from_json(&rpid, request_json) + MakeCredentialRequest::from_json(&request_origin, request_json) .expect("Failed to parse request JSON"); println!( "WebAuthn MakeCredential request: {:?}", @@ -157,7 +158,7 @@ pub async fn main() -> Result<(), Box> { } "#; let get_assertion: GetAssertionRequest = - GetAssertionRequest::from_json(&rpid, request_json) + GetAssertionRequest::from_json(&request_origin, request_json) .expect("Failed to parse request JSON"); println!("WebAuthn GetAssertion request: {:?}", get_assertion); diff --git a/libwebauthn/examples/webauthn_nfc.rs b/libwebauthn/examples/webauthn_nfc.rs index 62eec065..7958502d 100644 --- a/libwebauthn/examples/webauthn_nfc.rs +++ b/libwebauthn/examples/webauthn_nfc.rs @@ -122,7 +122,7 @@ pub async fn main() -> Result<(), Box> { let make_credentials_request = MakeCredentialRequest { challenge: Vec::from(challenge), origin: "example.org".to_owned(), - cross_origin: None, + top_origin: None, relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"), user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"), resident_key: Some(ResidentKeyRequirement::Discouraged), @@ -161,7 +161,7 @@ pub async fn main() -> Result<(), Box> { relying_party_id: "example.org".to_owned(), challenge: Vec::from(challenge), origin: "example.org".to_owned(), - cross_origin: None, + top_origin: None, allow: vec![credential], user_verification: UserVerificationRequirement::Discouraged, extensions: None, diff --git a/libwebauthn/examples/webauthn_preflight_hid.rs b/libwebauthn/examples/webauthn_preflight_hid.rs index 8481291a..1dc57a69 100644 --- a/libwebauthn/examples/webauthn_preflight_hid.rs +++ b/libwebauthn/examples/webauthn_preflight_hid.rs @@ -164,7 +164,7 @@ async fn make_credential_call( let make_credentials_request = MakeCredentialRequest { challenge: Vec::from(challenge), origin: "example.org".to_owned(), - cross_origin: None, + top_origin: None, relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"), user: Ctap2PublicKeyCredentialUserEntity::new(user_id, "mario.rossi", "Mario Rossi"), resident_key: Some(ResidentKeyRequirement::Discouraged), @@ -203,7 +203,7 @@ async fn get_assertion_call( relying_party_id: "example.org".to_owned(), challenge: Vec::from(challenge), origin: "example.org".to_string(), - cross_origin: None, + top_origin: None, allow: allow_list, user_verification: UserVerificationRequirement::Discouraged, extensions: Some(GetAssertionRequestExtensions::default()), diff --git a/libwebauthn/examples/webauthn_prf_hid.rs b/libwebauthn/examples/webauthn_prf_hid.rs index 167b557d..1aca6de5 100644 --- a/libwebauthn/examples/webauthn_prf_hid.rs +++ b/libwebauthn/examples/webauthn_prf_hid.rs @@ -103,7 +103,7 @@ pub async fn main() -> Result<(), Box> { let make_credentials_request = MakeCredentialRequest { challenge: Vec::from(challenge), origin: "example.org".to_owned(), - cross_origin: None, + top_origin: None, relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"), user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"), resident_key: Some(ResidentKeyRequirement::Required), @@ -421,7 +421,7 @@ async fn run_success_test( relying_party_id: "example.org".to_owned(), challenge: Vec::from(challenge), origin: "example.org".to_string(), - cross_origin: None, + top_origin: None, allow: vec![credential.clone()], user_verification: UserVerificationRequirement::Discouraged, extensions: Some(GetAssertionRequestExtensions { @@ -465,7 +465,7 @@ async fn run_failed_test( relying_party_id: "example.org".to_owned(), challenge: Vec::from(challenge), origin: "example.org".to_string(), - cross_origin: None, + top_origin: None, allow: credential.map(|x| vec![x.clone()]).unwrap_or_default(), user_verification: UserVerificationRequirement::Discouraged, extensions: Some(GetAssertionRequestExtensions { diff --git a/libwebauthn/src/ops/u2f.rs b/libwebauthn/src/ops/u2f.rs index 64a6fb15..ba40c6f4 100644 --- a/libwebauthn/src/ops/u2f.rs +++ b/libwebauthn/src/ops/u2f.rs @@ -215,7 +215,7 @@ impl UpgradableResponse for SignResponse { relying_party_id: String::new(), // We don't have access to that info here, but we don't need it either challenge: Vec::new(), // U2F path doesn't use client_data for response serialization origin: String::new(), - cross_origin: None, + top_origin: None, allow: vec![Ctap2PublicKeyCredentialDescriptor { r#type: Ctap2PublicKeyCredentialType::PublicKey, id: request.key_handle.clone().into(), diff --git a/libwebauthn/src/ops/webauthn/client_data.rs b/libwebauthn/src/ops/webauthn/client_data.rs index a9f95d06..f138e652 100644 --- a/libwebauthn/src/ops/webauthn/client_data.rs +++ b/libwebauthn/src/ops/webauthn/client_data.rs @@ -9,8 +9,8 @@ pub struct ClientData { pub operation: Operation, pub challenge: Vec, pub origin: String, - pub cross_origin: Option, - /// The origin of the top-level document, if in an iframe. + /// The origin of the top-level document, if the request was made in a + /// cross-origin nested browsing context (e.g. an iframe). /// https://www.w3.org/TR/webauthn-3/#dom-collectedclientdata-toporigin pub top_origin: Option, } @@ -24,12 +24,19 @@ impl ClientData { }; let challenge_str = base64_url::encode(&self.challenge); let origin_str = &self.origin; - let cross_origin_str = if self.cross_origin.unwrap_or(false) { + let cross_origin_str = if self.top_origin.is_some() { "true" } else { "false" }; - format!("{{\"type\":\"{op_str}\",\"challenge\":\"{challenge_str}\",\"origin\":\"{origin_str}\",\"crossOrigin\":{cross_origin_str}}}") + match &self.top_origin { + Some(top) => format!( + "{{\"type\":\"{op_str}\",\"challenge\":\"{challenge_str}\",\"origin\":\"{origin_str}\",\"crossOrigin\":{cross_origin_str},\"topOrigin\":\"{top}\"}}" + ), + None => format!( + "{{\"type\":\"{op_str}\",\"challenge\":\"{challenge_str}\",\"origin\":\"{origin_str}\",\"crossOrigin\":{cross_origin_str}}}" + ), + } } pub fn hash(&self) -> Vec { @@ -44,65 +51,56 @@ impl ClientData { mod tests { use super::*; - fn make_client_data(cross_origin: Option) -> ClientData { + fn make_client_data(top_origin: Option) -> ClientData { ClientData { operation: Operation::GetAssertion, challenge: b"test-challenge".to_vec(), origin: "https://example.org".to_string(), - cross_origin, - top_origin: None, + top_origin, } } #[test] - fn test_cross_origin_none_produces_false() { + fn same_origin_emits_cross_origin_false() { let client_data = make_client_data(None); let json = client_data.to_json(); assert!( json.contains("\"crossOrigin\":false"), - "Expected crossOrigin:false, got: {}", - json + "Expected crossOrigin:false, got: {json}" ); - } - - #[test] - fn test_cross_origin_false_produces_false() { - let client_data = make_client_data(Some(false)); - let json = client_data.to_json(); assert!( - json.contains("\"crossOrigin\":false"), - "Expected crossOrigin:false, got: {}", - json + !json.contains("topOrigin"), + "Did not expect topOrigin, got: {json}" ); } #[test] - fn test_cross_origin_true_produces_true() { - let client_data = make_client_data(Some(true)); + fn cross_origin_emits_cross_origin_true_and_top_origin() { + let client_data = make_client_data(Some("https://top.example.org".to_string())); let json = client_data.to_json(); assert!( json.contains("\"crossOrigin\":true"), - "Expected crossOrigin:true, got: {}", - json + "Expected crossOrigin:true, got: {json}" + ); + assert!( + json.contains("\"topOrigin\":\"https://top.example.org\""), + "Expected topOrigin, got: {json}" ); } #[test] - fn test_to_json_format() { + fn to_json_format() { let client_data = ClientData { operation: Operation::MakeCredential, challenge: b"DEADCODE".to_vec(), origin: "https://example.org".to_string(), - cross_origin: Some(true), top_origin: None, }; let json = client_data.to_json(); - // Verify the JSON contains expected structure assert!(json.contains("\"type\":\"webauthn.create\"")); assert!(json.contains("\"origin\":\"https://example.org\"")); - assert!(json.contains("\"crossOrigin\":true")); - // Challenge should be base64url encoded - assert!(json.contains("\"challenge\":\"REVBRENPREU\"")); // base64url of "DEADCODE" + assert!(json.contains("\"crossOrigin\":false")); + assert!(json.contains("\"challenge\":\"REVBRENPREU\"")); } } diff --git a/libwebauthn/src/ops/webauthn/get_assertion.rs b/libwebauthn/src/ops/webauthn/get_assertion.rs index b7ca054b..3b3acf43 100644 --- a/libwebauthn/src/ops/webauthn/get_assertion.rs +++ b/libwebauthn/src/ops/webauthn/get_assertion.rs @@ -31,7 +31,9 @@ use crate::{ }; use super::timeout::DEFAULT_TIMEOUT; -use super::{DowngradableRequest, RelyingPartyId, SignRequest, UserVerificationRequirement}; +use super::{ + DowngradableRequest, RelyingPartyId, RequestOrigin, SignRequest, UserVerificationRequirement, +}; #[derive(Debug, Default, Clone, Serialize, PartialEq)] pub struct PRFValue { @@ -46,7 +48,9 @@ pub struct GetAssertionRequest { pub relying_party_id: String, pub challenge: Vec, pub origin: String, - pub cross_origin: Option, + /// The top-level origin if the request was made from a cross-origin + /// nested browsing context. None for same-origin requests. + pub top_origin: Option, pub allow: Vec, pub extensions: Option, pub user_verification: UserVerificationRequirement, @@ -59,8 +63,7 @@ impl GetAssertionRequest { operation: Operation::GetAssertion, challenge: self.challenge.clone(), origin: self.origin.clone(), - cross_origin: self.cross_origin, - top_origin: None, + top_origin: self.top_origin.clone(), } } @@ -111,18 +114,19 @@ impl FromIdlModel Result { + let effective_rp_id = request_origin.origin.host.as_str(); if let Some(relying_party_id) = inner.relying_party_id.as_deref() { let parsed = RelyingPartyId::try_from(relying_party_id).map_err(|err| { GetAssertionRequestParsingError::InvalidRelyingPartyId(err.to_string()) })?; // TODO(#160): Add support for related origin per WebAuthn Level 3. - if parsed.0 != rpid.0 { + if parsed.0 != effective_rp_id { return Err(GetAssertionRequestParsingError::MismatchingRelyingPartyId( parsed.0, - rpid.0.to_string(), + effective_rp_id.to_string(), )); } } @@ -154,10 +158,10 @@ impl FromIdlModel Result { + fn from_json(request_origin: &RequestOrigin, json: &str) -> Result { let idl_model: Self::IdlModel = serde_json::from_str(json)?; - Self::from_idl_model(rpid, idl_model).map_err(From::from) + Self::from_idl_model(request_origin, idl_model).map_err(From::from) } } @@ -43,5 +44,5 @@ where T: DeserializeOwned, E: std::error::Error, { - fn from_idl_model(rpid: &RelyingPartyId, model: T) -> Result; + fn from_idl_model(request_origin: &RequestOrigin, model: T) -> Result; } diff --git a/libwebauthn/src/ops/webauthn/idl/origin.rs b/libwebauthn/src/ops/webauthn/idl/origin.rs new file mode 100644 index 00000000..94d7aba4 --- /dev/null +++ b/libwebauthn/src/ops/webauthn/idl/origin.rs @@ -0,0 +1,510 @@ +use std::convert::TryFrom; +use std::fmt::{self, Display}; +use std::str::FromStr; + +use url::{Host, ParseError, Url}; + +#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] +pub enum HostParseError { + #[error("empty host")] + Empty, + #[error("invalid IP address: {0}")] + InvalidIp(String), + #[error("invalid domain: {0}")] + InvalidDomain(String), +} + +#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] +pub enum OriginParseError { + #[error("invalid scheme (only https, or http with localhost, is supported)")] + InvalidScheme, + #[error("http scheme is only allowed for localhost, got {0}")] + InsecureHttpHost(String), + #[error("missing host")] + MissingHost, + #[error("invalid host: {0}")] + InvalidHost(#[from] HostParseError), + #[error("invalid port: {0}")] + InvalidPort(String), + #[error("unexpected path or fragment: {0}")] + UnexpectedPath(String), + #[error("origin must not contain userinfo")] + UnexpectedUserinfo, +} + +/// Validated host component of an HTTPS origin. +/// +/// Parsing follows the WHATWG URL Standard host parser via [`url::Host`], which +/// accepts ASCII / IDNA domains, IPv4 literals, and bracketed IPv6 literals, +/// and rejects everything else. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OriginHost(String); + +impl OriginHost { + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl FromStr for OriginHost { + type Err = HostParseError; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + return Err(HostParseError::Empty); + } + Host::parse(s) + .map(|h| OriginHost(h.to_string())) + .map_err(|err| match err { + ParseError::InvalidIpv4Address | ParseError::InvalidIpv6Address => { + HostParseError::InvalidIp(err.to_string()) + } + _ => HostParseError::InvalidDomain(err.to_string()), + }) + } +} + +impl TryFrom<&str> for OriginHost { + type Error = HostParseError; + + fn try_from(s: &str) -> Result { + Self::from_str(s) + } +} + +impl Display for OriginHost { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +/// Scheme of a WebAuthn origin. +/// +/// `Https` is the standard case. `Http` is permitted only with the literal +/// `localhost` host, because Web specs (Secure Contexts) treat +/// `http://localhost` as a secure context for development purposes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Scheme { + Https, + Http, +} + +impl Scheme { + pub fn as_str(self) -> &'static str { + match self { + Scheme::Https => "https", + Scheme::Http => "http", + } + } +} + +impl Display for Scheme { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// A WebAuthn origin. The scheme is `https`, or `http` only when the host is +/// the literal `localhost`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Origin { + pub scheme: Scheme, + pub host: OriginHost, + pub port: Option, +} + +impl Origin { + /// Constructs an HTTPS origin. Use [`Origin::from_str`] to parse an + /// arbitrary origin string (which will also accept `http://localhost`). + pub fn new(host: OriginHost, port: Option) -> Self { + Self { + scheme: Scheme::Https, + host, + port, + } + } +} + +impl Display for Origin { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}://{}", self.scheme, self.host)?; + if let Some(port) = self.port { + write!(f, ":{port}")?; + } + Ok(()) + } +} + +/// Returns true iff `host` qualifies for the `http://` scheme. The W3C Secure +/// Contexts spec considers a broader set of hosts trustworthy (`localhost`, +/// `*.localhost`, `127.0.0.0/8`, `[::1]`). We intentionally restrict to the +/// literal `localhost` here as the minimum dev affordance; this can be +/// widened later without breaking existing callers. +/// +/// Case comparison is safe: [`url::Host::parse`] ASCII-lowercases the domain +/// during parsing, so `LOCALHOST` and `localhost` both compare equal here. +fn host_allows_http(host: &OriginHost) -> bool { + host.as_str() == "localhost" +} + +impl FromStr for Origin { + type Err = OriginParseError; + + /// Parses a WebAuthn origin from a string. Delegates to [`url::Url`] for + /// scheme, host (including IDNA / IPv4 / IPv6), and port parsing, then + /// applies WebAuthn-specific rules: + /// + /// * scheme must be `https`, or `http` when the host is the literal + /// `localhost` + /// * no userinfo (`user:pw@host`) + /// * no path beyond `/`, no query, no fragment + /// + /// Per the WHATWG URL Standard, default ports (e.g. `:443` for https) + /// are dropped during parsing, matching the canonical origin form used + /// in `clientDataJSON.origin`. + fn from_str(s: &str) -> Result { + let url = Url::parse(s).map_err(map_url_parse_error)?; + + let scheme = match url.scheme() { + "https" => Scheme::Https, + "http" => Scheme::Http, + _ => return Err(OriginParseError::InvalidScheme), + }; + + if !url.username().is_empty() || url.password().is_some() { + return Err(OriginParseError::UnexpectedUserinfo); + } + if !matches!(url.path(), "" | "/") { + return Err(OriginParseError::UnexpectedPath(url.path().to_string())); + } + if let Some(q) = url.query() { + return Err(OriginParseError::UnexpectedPath(format!("?{q}"))); + } + if let Some(f) = url.fragment() { + return Err(OriginParseError::UnexpectedPath(format!("#{f}"))); + } + + let host = match url.host() { + Some(Host::Domain(d)) => OriginHost(d.to_string()), + Some(Host::Ipv4(ip)) => OriginHost(ip.to_string()), + // Restore the brackets that `url::Url` strips off internally. + Some(Host::Ipv6(ip)) => OriginHost(format!("[{ip}]")), + None => return Err(OriginParseError::MissingHost), + }; + + if matches!(scheme, Scheme::Http) && !host_allows_http(&host) { + return Err(OriginParseError::InsecureHttpHost( + host.as_str().to_string(), + )); + } + + Ok(Origin { + scheme, + host, + port: url.port(), + }) + } +} + +impl TryFrom<&str> for Origin { + type Error = OriginParseError; + + fn try_from(s: &str) -> Result { + Self::from_str(s) + } +} + +fn map_url_parse_error(err: ParseError) -> OriginParseError { + match err { + ParseError::EmptyHost => OriginParseError::MissingHost, + ParseError::InvalidIpv4Address | ParseError::InvalidIpv6Address => { + OriginParseError::InvalidHost(HostParseError::InvalidIp(err.to_string())) + } + ParseError::InvalidPort => OriginParseError::InvalidPort(err.to_string()), + ParseError::RelativeUrlWithoutBase => OriginParseError::InvalidScheme, + ParseError::IdnaError => { + OriginParseError::InvalidHost(HostParseError::InvalidDomain(err.to_string())) + } + _ => OriginParseError::InvalidHost(HostParseError::InvalidDomain(err.to_string())), + } +} + +/// The origin context of an incoming WebAuthn request: the request's own +/// origin, plus the top-level origin when the request was made from a nested +/// (cross-origin) browsing context. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RequestOrigin { + pub origin: Origin, + pub top_origin: Option, +} + +impl RequestOrigin { + /// Same-origin request: no top-level origin. + pub fn new(origin: Origin) -> Self { + Self { + origin, + top_origin: None, + } + } + + /// Cross-origin request: the request was made from a nested browsing + /// context whose top-level origin is `top_origin`. + pub fn new_cross_origin(origin: Origin, top_origin: Origin) -> Self { + Self { + origin, + top_origin: Some(top_origin), + } + } + + /// True iff the request was made from a nested browsing context with a + /// different top-level origin. + pub fn is_cross_origin(&self) -> bool { + self.top_origin.is_some() + } +} + +impl FromStr for RequestOrigin { + type Err = OriginParseError; + + fn from_str(s: &str) -> Result { + Ok(Self::new(Origin::from_str(s)?)) + } +} + +impl TryFrom<&str> for RequestOrigin { + type Error = OriginParseError; + + fn try_from(s: &str) -> Result { + Self::from_str(s) + } +} + +impl TryFrom for RequestOrigin { + type Error = OriginParseError; + + fn try_from(s: String) -> Result { + Self::from_str(&s) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn host_parses_domain() { + let h: OriginHost = "example.org".parse().unwrap(); + assert_eq!(h.as_str(), "example.org"); + } + + #[test] + fn host_idna_normalises() { + let h: OriginHost = "例え.テスト".parse().unwrap(); + assert_eq!(h.as_str(), "xn--r8jz45g.xn--zckzah"); + } + + #[test] + fn host_accepts_ipv4() { + let h: OriginHost = "127.0.0.1".parse().unwrap(); + assert_eq!(h.as_str(), "127.0.0.1"); + } + + #[test] + fn host_accepts_bracketed_ipv6() { + let h: OriginHost = "[::1]".parse().unwrap(); + assert_eq!(h.as_str(), "[::1]"); + } + + #[test] + fn host_rejects_empty() { + assert!(matches!( + "".parse::(), + Err(HostParseError::Empty) + )); + } + + #[test] + fn origin_parses_bare_host() { + let o: Origin = "https://example.org".parse().unwrap(); + assert_eq!(o.host.as_str(), "example.org"); + assert_eq!(o.port, None); + assert_eq!(o.to_string(), "https://example.org"); + } + + #[test] + fn origin_parses_host_with_port() { + let o: Origin = "https://example.org:8443".parse().unwrap(); + assert_eq!(o.host.as_str(), "example.org"); + assert_eq!(o.port, Some(8443)); + assert_eq!(o.to_string(), "https://example.org:8443"); + } + + #[test] + fn origin_parses_ipv6_with_port() { + let o: Origin = "https://[::1]:8443".parse().unwrap(); + assert_eq!(o.host.as_str(), "[::1]"); + assert_eq!(o.port, Some(8443)); + assert_eq!(o.to_string(), "https://[::1]:8443"); + } + + #[test] + fn origin_allows_trailing_slash() { + let o: Origin = "https://example.org/".parse().unwrap(); + assert_eq!(o.to_string(), "https://example.org"); + } + + #[test] + fn origin_rejects_unknown_scheme() { + assert!(matches!( + "ftp://example.org".parse::(), + Err(OriginParseError::InvalidScheme) + )); + } + + #[test] + fn origin_rejects_http_for_non_localhost() { + assert!(matches!( + "http://example.org".parse::(), + Err(OriginParseError::InsecureHttpHost(_)) + )); + } + + #[test] + fn origin_accepts_http_localhost() { + let o: Origin = "http://localhost".parse().unwrap(); + assert_eq!(o.scheme, Scheme::Http); + assert_eq!(o.host.as_str(), "localhost"); + assert_eq!(o.port, None); + assert_eq!(o.to_string(), "http://localhost"); + } + + #[test] + fn origin_accepts_http_localhost_with_port() { + let o: Origin = "http://localhost:3000".parse().unwrap(); + assert_eq!(o.scheme, Scheme::Http); + assert_eq!(o.host.as_str(), "localhost"); + assert_eq!(o.port, Some(3000)); + assert_eq!(o.to_string(), "http://localhost:3000"); + } + + #[test] + fn origin_accepts_https_localhost() { + let o: Origin = "https://localhost:8443".parse().unwrap(); + assert_eq!(o.scheme, Scheme::Https); + assert_eq!(o.host.as_str(), "localhost"); + assert_eq!(o.port, Some(8443)); + } + + #[test] + fn origin_rejects_http_loopback_ip() { + // Loopback IPs are not covered by this narrow allowance; only the + // literal "localhost" host qualifies for http://. + assert!(matches!( + "http://127.0.0.1".parse::(), + Err(OriginParseError::InsecureHttpHost(_)) + )); + assert!(matches!( + "http://[::1]".parse::(), + Err(OriginParseError::InsecureHttpHost(_)) + )); + } + + #[test] + fn origin_rejects_path() { + assert!(matches!( + "https://example.org/foo".parse::(), + Err(OriginParseError::UnexpectedPath(_)) + )); + } + + #[test] + fn origin_rejects_query() { + assert!(matches!( + "https://example.org?x=1".parse::(), + Err(OriginParseError::UnexpectedPath(_)) + )); + } + + #[test] + fn origin_rejects_invalid_port() { + assert!(matches!( + "https://example.org:notaport".parse::(), + Err(OriginParseError::InvalidPort(_)) + )); + } + + #[test] + fn request_origin_same_origin() { + let r: RequestOrigin = "https://example.org".parse().unwrap(); + assert!(!r.is_cross_origin()); + assert_eq!(r.top_origin, None); + } + + #[test] + fn request_origin_cross_origin() { + let inner: Origin = "https://embed.example.org".parse().unwrap(); + let top: Origin = "https://example.org".parse().unwrap(); + let r = RequestOrigin::new_cross_origin(inner.clone(), top.clone()); + assert!(r.is_cross_origin()); + assert_eq!(r.origin, inner); + assert_eq!(r.top_origin, Some(top)); + } + + #[test] + fn request_origin_try_from_string() { + // Default ports are stripped during parsing (WHATWG URL Standard), so + // `:443` on an https origin normalises to `port = None`. + let r = RequestOrigin::try_from("https://example.org:443".to_string()).unwrap(); + assert_eq!(r.origin.host.as_str(), "example.org"); + assert_eq!(r.origin.port, None); + assert_eq!(r.origin.to_string(), "https://example.org"); + } + + #[test] + fn origin_strips_default_http_port() { + let o: Origin = "http://localhost:80".parse().unwrap(); + assert_eq!(o.port, None); + assert_eq!(o.to_string(), "http://localhost"); + } + + #[test] + fn origin_rejects_userinfo() { + assert!(matches!( + "https://user:pw@example.org".parse::(), + Err(OriginParseError::UnexpectedUserinfo) + )); + } + + #[test] + fn origin_normalises_uppercase_scheme_and_host() { + let o: Origin = "HTTPS://Example.ORG".parse().unwrap(); + assert_eq!(o.scheme, Scheme::Https); + assert_eq!(o.host.as_str(), "example.org"); + assert_eq!(o.to_string(), "https://example.org"); + } + + #[test] + fn origin_accepts_port_boundaries() { + let o: Origin = "https://example.org:1".parse().unwrap(); + assert_eq!(o.port, Some(1)); + let o: Origin = "https://example.org:65535".parse().unwrap(); + assert_eq!(o.port, Some(65535)); + } + + #[test] + fn origin_accepts_port_zero() { + // Port 0 is syntactically valid per the WHATWG URL Standard, even + // though it is not a usable network port. Pin current behavior so a + // future change is visible. + let o: Origin = "https://example.org:0".parse().unwrap(); + assert_eq!(o.port, Some(0)); + } + + #[test] + fn origin_new_defaults_to_https() { + let host: OriginHost = "example.org".parse().unwrap(); + let origin = Origin::new(host, Some(8443)); + assert_eq!(origin.scheme, Scheme::Https); + assert_eq!(origin.to_string(), "https://example.org:8443"); + } +} diff --git a/libwebauthn/src/ops/webauthn/make_credential.rs b/libwebauthn/src/ops/webauthn/make_credential.rs index b7d65cd2..6bf597da 100644 --- a/libwebauthn/src/ops/webauthn/make_credential.rs +++ b/libwebauthn/src/ops/webauthn/make_credential.rs @@ -19,7 +19,7 @@ use crate::{ }, Base64UrlString, FromIdlModel, JsonError, WebAuthnIDL, }, - Operation, RelyingPartyId, + Operation, RelyingPartyId, RequestOrigin, }, proto::{ ctap1::{Ctap1RegisteredKey, Ctap1Version}, @@ -321,8 +321,9 @@ pub struct MakeCredentialRequest { pub challenge: Vec, /// The origin of the request. pub origin: String, - /// Whether the request is cross-origin (optional per WebAuthn spec). - pub cross_origin: Option, + /// The top-level origin if the request was made from a cross-origin + /// nested browsing context. None for same-origin requests. + pub top_origin: Option, /// rpEntity pub relying_party: Ctap2PublicKeyCredentialRpEntity, /// userEntity @@ -345,8 +346,7 @@ impl MakeCredentialRequest { operation: Operation::MakeCredential, challenge: self.challenge.clone(), origin: self.origin.clone(), - cross_origin: self.cross_origin, - top_origin: None, + top_origin: self.top_origin.clone(), } } @@ -365,18 +365,19 @@ impl FromIdlModel Result { + let effective_rp_id = request_origin.origin.host.as_str(); let rp_id = RelyingPartyId::try_from(inner.rp.id.as_str()).map_err(|err| { MakeCredentialRequestParsingError::InvalidRelyingPartyId(err.to_string()) })?; // TODO(#160): Add support for related origin per WebAuthn Level 3. - if rp_id.0 != rpid.0 { + if rp_id.0 != effective_rp_id { return Err( MakeCredentialRequestParsingError::MismatchingRelyingPartyId( rp_id.0, - rpid.0.to_string(), + effective_rp_id.to_string(), ), ); } @@ -410,8 +411,8 @@ impl FromIdlModel for MakeCredentialRequest { mod tests { use std::time::Duration; - use crate::ops::webauthn::MakeCredentialRequest; - use crate::ops::webauthn::RelyingPartyId; + use crate::ops::webauthn::{MakeCredentialRequest, RequestOrigin}; use crate::proto::ctap2::Ctap2PublicKeyCredentialType; use super::*; @@ -679,8 +679,8 @@ mod tests { fn request_base() -> MakeCredentialRequest { MakeCredentialRequest { challenge: base64_url::decode("Y3JlZGVudGlhbHMtZm9yLWxpbnV4L2xpYndlYmF1dGhu").unwrap(), - origin: "example.org".to_string(), - cross_origin: None, + origin: "https://example.org".to_string(), + top_origin: None, relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"), user: Ctap2PublicKeyCredentialUserEntity::new(b"userid", "mario.rossi", "Mario Rossi"), resident_key: Some(ResidentKeyRequirement::Discouraged), @@ -707,10 +707,10 @@ mod tests { } fn test_request_from_json_required_field(field: &str) { - let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let request_origin: RequestOrigin = "https://example.org".parse().unwrap(); let req_json = json_field_rm(REQUEST_BASE_JSON, field); - let result = MakeCredentialRequest::from_json(&rpid, &req_json); + let result = MakeCredentialRequest::from_json(&request_origin, &req_json); assert!(matches!( result, Err(MakeCredentialRequestParsingError::EncodingError(_)) @@ -719,9 +719,9 @@ mod tests { #[test] fn test_request_from_json_base() { - let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let request_origin: RequestOrigin = "https://example.org".parse().unwrap(); let req: MakeCredentialRequest = - MakeCredentialRequest::from_json(&rpid, REQUEST_BASE_JSON).unwrap(); + MakeCredentialRequest::from_json(&request_origin, REQUEST_BASE_JSON).unwrap(); assert_eq!(req, request_base()); } @@ -748,11 +748,11 @@ mod tests { #[test] #[ignore] // FIXME(#134): Add validation for challenges fn test_request_from_json_challenge_empty() { - let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let request_origin: RequestOrigin = "https://example.org".parse().unwrap(); let req_json: String = json_field_rm(REQUEST_BASE_JSON, "challenge"); let req_json = json_field_add(&req_json, "challenge", r#""""#); - let result = MakeCredentialRequest::from_json(&rpid, &req_json); + let result = MakeCredentialRequest::from_json(&request_origin, &req_json); assert!(matches!( result, Err(MakeCredentialRequestParsingError::EncodingError(_)) @@ -761,7 +761,7 @@ mod tests { #[test] fn test_request_from_json_prf_extension() { - let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let request_origin: RequestOrigin = "https://example.org".parse().unwrap(); let req_json = json_field_add( REQUEST_BASE_JSON, "extensions", @@ -769,7 +769,7 @@ mod tests { ); let req: MakeCredentialRequest = - MakeCredentialRequest::from_json(&rpid, &req_json).unwrap(); + MakeCredentialRequest::from_json(&request_origin, &req_json).unwrap(); assert!(matches!( req.extensions, Some(MakeCredentialsRequestExtensions { prf: Some(_), .. }) @@ -778,14 +778,14 @@ mod tests { #[test] fn test_request_from_json_unknown_pub_key_cred_params() { - let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let request_origin: RequestOrigin = "https://example.org".parse().unwrap(); let req_json = json_field_add( REQUEST_BASE_JSON, "pubKeyCredParams", r#"[{"type": "something", "alg": -12345}]"#, ); let req: MakeCredentialRequest = - MakeCredentialRequest::from_json(&rpid, &req_json).unwrap(); + MakeCredentialRequest::from_json(&request_origin, &req_json).unwrap(); assert_eq!( req.algorithms, vec![Ctap2CredentialType { @@ -797,11 +797,11 @@ mod tests { #[test] fn test_request_from_json_default_timeout() { - let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let request_origin: RequestOrigin = "https://example.org".parse().unwrap(); let req_json = json_field_rm(REQUEST_BASE_JSON, "timeout"); let req: MakeCredentialRequest = - MakeCredentialRequest::from_json(&rpid, &req_json).unwrap(); + MakeCredentialRequest::from_json(&request_origin, &req_json).unwrap(); assert_eq!(req.timeout, DEFAULT_TIMEOUT); } @@ -809,11 +809,11 @@ mod tests { /// https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-userverification #[test] fn test_request_from_json_default_user_verification_preferred() { - let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let request_origin: RequestOrigin = "https://example.org".parse().unwrap(); let req_json = json_field_rm(REQUEST_BASE_JSON, "authenticatorSelection"); let req: MakeCredentialRequest = - MakeCredentialRequest::from_json(&rpid, &req_json).unwrap(); + MakeCredentialRequest::from_json(&request_origin, &req_json).unwrap(); assert_eq!( req.user_verification, UserVerificationRequirement::Preferred @@ -824,7 +824,7 @@ mod tests { /// it should default to "preferred". #[test] fn test_request_from_json_missing_user_verification_in_authenticator_selection() { - let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let request_origin: RequestOrigin = "https://example.org".parse().unwrap(); // Replace authenticatorSelection with one that has no userVerification field let mut req_json = json_field_rm(REQUEST_BASE_JSON, "authenticatorSelection"); req_json = json_field_add( @@ -834,7 +834,7 @@ mod tests { ); let req: MakeCredentialRequest = - MakeCredentialRequest::from_json(&rpid, &req_json).unwrap(); + MakeCredentialRequest::from_json(&request_origin, &req_json).unwrap(); assert_eq!( req.user_verification, UserVerificationRequirement::Preferred @@ -843,14 +843,14 @@ mod tests { #[test] fn test_request_from_json_invalid_rp_id() { - let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let request_origin: RequestOrigin = "https://example.org".parse().unwrap(); let req_json = json_field_add( REQUEST_BASE_JSON, "rp", r#"{"id": "example.org.", "name": "example.org"}"#, ); - let result = MakeCredentialRequest::from_json(&rpid, &req_json); + let result = MakeCredentialRequest::from_json(&request_origin, &req_json); assert!(matches!( result, Err(MakeCredentialRequestParsingError::InvalidRelyingPartyId(_)) @@ -859,14 +859,14 @@ mod tests { #[test] fn test_request_from_json_mismatching_rp_id() { - let rpid = RelyingPartyId::try_from("example.org").unwrap(); + let request_origin: RequestOrigin = "https://example.org".parse().unwrap(); let req_json = json_field_add( REQUEST_BASE_JSON, "rp", r#"{"id": "other.example.org", "name": "example.org"}"#, ); - let result = MakeCredentialRequest::from_json(&rpid, &req_json); + let result = MakeCredentialRequest::from_json(&request_origin, &req_json); assert!(matches!( result, Err(MakeCredentialRequestParsingError::MismatchingRelyingPartyId(_, _)) @@ -918,7 +918,7 @@ mod tests { MakeCredentialRequest { challenge: b"DEADCODE_challenge".to_vec(), origin: "example.org".to_string(), - cross_origin: None, + top_origin: None, relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"), user: Ctap2PublicKeyCredentialUserEntity::new(b"userid", "mario.rossi", "Mario Rossi"), resident_key: Some(ResidentKeyRequirement::Discouraged), diff --git a/libwebauthn/src/ops/webauthn/mod.rs b/libwebauthn/src/ops/webauthn/mod.rs index 0c1864f3..2104bc14 100644 --- a/libwebauthn/src/ops/webauthn/mod.rs +++ b/libwebauthn/src/ops/webauthn/mod.rs @@ -15,7 +15,9 @@ pub use get_assertion::{ HMACGetSecretOutput, PRFValue, PrfInput, }; pub use idl::{ - rpid::RelyingPartyId, AuthenticationExtensionsClientOutputsJSON, AuthenticationResponseJSON, + origin::{HostParseError, Origin, OriginHost, OriginParseError, RequestOrigin, Scheme}, + rpid::RelyingPartyId, + AuthenticationExtensionsClientOutputsJSON, AuthenticationResponseJSON, AuthenticatorAssertionResponseJSON, AuthenticatorAttestationResponseJSON, Base64UrlString, JsonFormat, RegistrationResponseJSON, ResponseSerializationError, WebAuthnIDL, WebAuthnIDLResponse, diff --git a/libwebauthn/src/proto/ctap2/model/get_assertion.rs b/libwebauthn/src/proto/ctap2/model/get_assertion.rs index 26bd7594..2d911eb5 100644 --- a/libwebauthn/src/proto/ctap2/model/get_assertion.rs +++ b/libwebauthn/src/proto/ctap2/model/get_assertion.rs @@ -612,7 +612,7 @@ mod tests { relying_party_id: "example.com".to_string(), challenge: vec![0u8; 32], origin: "https://example.com".to_string(), - cross_origin: None, + top_origin: None, allow, extensions: None, user_verification: Default::default(), diff --git a/libwebauthn/src/webauthn/pin_uv_auth_token.rs b/libwebauthn/src/webauthn/pin_uv_auth_token.rs index 3e72f844..5748dd1e 100644 --- a/libwebauthn/src/webauthn/pin_uv_auth_token.rs +++ b/libwebauthn/src/webauthn/pin_uv_auth_token.rs @@ -583,7 +583,7 @@ mod test { extensions, user_verification: UserVerificationRequirement::Preferred, timeout: TIMEOUT, - cross_origin: None, + top_origin: None, }, info, )