Skip to content

Commit 17e91f6

Browse files
refactor(webauthn): use RequestOrigin instead of RelyingPartyId in WebAuthnIDL (fix #185) (#188)
## Motivation Closes #185. `WebAuthnIDL::from_json` currently takes a `&RelyingPartyId`, which forces callers (e.g. credentialsd) to override `request.origin` and `request.cross_origin` after parsing because the bare host is not a valid origin string and we have no place to record the top-level origin. This replaces that parameter with a `RequestOrigin` that carries the actual origin context, so the parsed request comes out correct without a post-parse fixup. ## What changes - New `Origin` and `RequestOrigin` types in `libwebauthn::ops::webauthn`. `Origin` is a struct with `host: OriginHost` and `port: Option<u16>`; `RequestOrigin` wraps it with an optional `top_origin`. Convenience constructors: `RequestOrigin::new(origin)`, `RequestOrigin::new_cross_origin(origin, top_origin)`, `RequestOrigin::try_from(&str | String)` for one-shot parsing of `"https://host[:port]"`. - Host validation goes through `url::Host` so we follow the WHATWG URL Standard host parser (domain / IPv4 / bracketed IPv6). Errors are wrapped into a local `HostParseError` / `OriginParseError` so the `url` crate's error type does not leak into the public API. - `WebAuthnIDL::from_json` and `FromIdlModel::from_idl_model` now take `&RequestOrigin`. The parsed `request.origin` is the full URL string (`"https://example.org"`, no longer the bare host), and `request.top_origin: Option<String>` replaces the old `cross_origin: Option<bool>`. - `ClientData` drops `cross_origin: Option<bool>` and derives `crossOrigin` in the JSON from `top_origin.is_some()`. `topOrigin` is now emitted when present. - Bumps libwebauthn to `0.4.0` since the public `from_json` signature and the request struct fields are breaking changes. ## Intentional non-changes - `Origin` is a plain struct, not an enum. We only support `https`. If we need to support a second scheme later it can become an enum without breaking call-site field access. - `rp.id` validation against the origin is still strict equality. The spec actually wants a registrable-suffix check (WebAuthn L3 §5.1.3 / §5.1.7); that is tracked separately in #187 and can build on the PSL helpers from #173. ## Test plan - [x] `cargo build -p libwebauthn` and `cargo build -p libwebauthn --features virt` - [x] `cargo build --workspace --all-targets --all-features` - [x] `cargo fmt --all -- --check` - [x] `cargo clippy --workspace --all-targets --all-features -- -D warnings` - [x] `cargo test --workspace` (149 tests, 14 new origin parser tests) - [x] `cargo publish --dry-run -p libwebauthn` (no working-tree hacks)
1 parent a98b730 commit 17e91f6

22 files changed

Lines changed: 681 additions & 132 deletions

Cargo.lock

Lines changed: 29 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libwebauthn-tests/tests/basic_ctap2.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ async fn test_webauthn_basic_ctap2() {
4040
let make_credentials_request = MakeCredentialRequest {
4141
challenge: Vec::from(challenge),
4242
origin: "example.org".to_owned(),
43-
cross_origin: None,
43+
top_origin: None,
4444
relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"),
4545
user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"),
4646
resident_key: Some(ResidentKeyRequirement::Discouraged),
@@ -66,7 +66,7 @@ async fn test_webauthn_basic_ctap2() {
6666
relying_party_id: "example.org".to_owned(),
6767
challenge: Vec::from(challenge),
6868
origin: "example.org".to_string(),
69-
cross_origin: None,
69+
top_origin: None,
7070
allow: vec![credential],
7171
user_verification: UserVerificationRequirement::Discouraged,
7272
extensions: Some(GetAssertionRequestExtensions::default()),

libwebauthn-tests/tests/preflight.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ async fn make_credential_call(
4949
exclude: exclude_list,
5050
extensions: None,
5151
timeout: TIMEOUT,
52-
cross_origin: None,
52+
top_origin: None,
5353
};
5454

5555
let response = channel
@@ -71,7 +71,7 @@ async fn get_assertion_call(
7171
user_verification: UserVerificationRequirement::Discouraged,
7272
extensions: None,
7373
timeout: TIMEOUT,
74-
cross_origin: None,
74+
top_origin: None,
7575
};
7676

7777
channel.webauthn_get_assertion(&get_assertion).await

libwebauthn-tests/tests/prf.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ async fn run_test_battery(channel: &mut HidChannel<'_>, using_pin: bool) {
116116
exclude: None,
117117
extensions: Some(extensions),
118118
timeout: TIMEOUT,
119-
cross_origin: None,
119+
top_origin: None,
120120
};
121121

122122
let state_recv = channel.get_ux_update_receiver();
@@ -175,7 +175,7 @@ async fn run_test_battery(channel: &mut HidChannel<'_>, using_pin: bool) {
175175
user_verification: UserVerificationRequirement::Preferred,
176176
extensions: None,
177177
timeout: TIMEOUT,
178-
cross_origin: None,
178+
top_origin: None,
179179
};
180180

181181
let _response = channel
@@ -494,7 +494,7 @@ async fn run_success_test(
494494
..Default::default()
495495
}),
496496
timeout: TIMEOUT,
497-
cross_origin: None,
497+
top_origin: None,
498498
};
499499

500500
let response = channel
@@ -561,7 +561,7 @@ async fn run_failed_test(
561561
..Default::default()
562562
}),
563563
timeout: TIMEOUT,
564-
cross_origin: None,
564+
top_origin: None,
565565
};
566566

567567
let response: Result<(), WebAuthnError> = loop {

libwebauthn/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "libwebauthn"
33
description = "FIDO2 (WebAuthn) and FIDO U2F platform library for Linux written in Rust "
4-
version = "0.3.1"
4+
version = "0.4.0"
55
authors = ["Alfie Fresta <alfie.fresta@gmail.com>"]
66
edition = "2021"
77
license-file = "../COPYING"
@@ -32,6 +32,7 @@ base64-url = "3.0.0"
3232
dbus = "0.9.5"
3333
tracing = "0.1.29"
3434
idna = "1.0.3"
35+
url = "2.5"
3536
maplit = "1.0.2"
3637
sha2 = "0.10.2"
3738
uuid = { version = "1.5.0", features = ["serde", "v4"] }

libwebauthn/examples/prf_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ async fn run_success_test(
149149
relying_party_id: "demo.yubico.com".to_owned(),
150150
challenge: Vec::from(challenge),
151151
origin: "demo.yubico.com".to_string(),
152-
cross_origin: None,
152+
top_origin: None,
153153
allow: vec![credential.clone()],
154154
user_verification: UserVerificationRequirement::Preferred,
155155
extensions: Some(GetAssertionRequestExtensions {

libwebauthn/examples/webauthn_cable.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
130130
let make_credentials_request = MakeCredentialRequest {
131131
challenge: Vec::from(challenge),
132132
origin: "example.org".to_owned(),
133-
cross_origin: None,
133+
top_origin: None,
134134
relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"),
135135
user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"),
136136
resident_key: Some(ResidentKeyRequirement::Discouraged),
@@ -170,7 +170,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
170170
relying_party_id: "example.org".to_owned(),
171171
challenge: Vec::from(challenge),
172172
origin: "example.org".to_string(),
173-
cross_origin: None,
173+
top_origin: None,
174174
allow: vec![credential],
175175
user_verification: UserVerificationRequirement::Discouraged,
176176
extensions: Some(GetAssertionRequestExtensions::default()),

libwebauthn/examples/webauthn_extensions_hid.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
109109
let make_credentials_request = MakeCredentialRequest {
110110
challenge: Vec::from(challenge),
111111
origin: "example.org".to_owned(),
112-
cross_origin: None,
112+
top_origin: None,
113113
relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"),
114114
user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"),
115115
resident_key: Some(ResidentKeyRequirement::Required),
@@ -149,7 +149,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
149149
relying_party_id: "example.org".to_owned(),
150150
challenge: Vec::from(challenge),
151151
origin: "example.org".to_string(),
152-
cross_origin: None,
152+
top_origin: None,
153153
allow: vec![credential],
154154
user_verification: UserVerificationRequirement::Discouraged,
155155
extensions: Some(GetAssertionRequestExtensions {

libwebauthn/examples/webauthn_hid.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
121121
let make_credentials_request = MakeCredentialRequest {
122122
challenge: Vec::from(challenge),
123123
origin: "example.org".to_owned(),
124-
cross_origin: None,
124+
top_origin: None,
125125
relying_party: Ctap2PublicKeyCredentialRpEntity::new("example.org", "example.org"),
126126
user: Ctap2PublicKeyCredentialUserEntity::new(&user_id, "mario.rossi", "Mario Rossi"),
127127
resident_key: Some(ResidentKeyRequirement::Discouraged),
@@ -160,7 +160,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
160160
relying_party_id: "example.org".to_owned(),
161161
challenge: Vec::from(challenge),
162162
origin: "example.org".to_string(),
163-
cross_origin: None,
163+
top_origin: None,
164164
allow: vec![credential],
165165
user_verification: UserVerificationRequirement::Discouraged,
166166
extensions: Some(GetAssertionRequestExtensions::default()),

libwebauthn/examples/webauthn_json_hid.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use tokio::sync::broadcast::Receiver;
88
use tracing_subscriber::{self, EnvFilter};
99

1010
use libwebauthn::ops::webauthn::{
11-
GetAssertionRequest, JsonFormat, MakeCredentialRequest, RelyingPartyId, WebAuthnIDL as _,
11+
GetAssertionRequest, JsonFormat, MakeCredentialRequest, RequestOrigin, WebAuthnIDL as _,
1212
WebAuthnIDLResponse as _,
1313
};
1414
use libwebauthn::pin::PinRequestReason;
@@ -79,7 +79,8 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
7979
let mut channel = device.channel().await?;
8080
channel.wink(TIMEOUT).await?;
8181

82-
let rpid = RelyingPartyId("example.org".to_owned());
82+
let request_origin: RequestOrigin =
83+
"https://example.org".try_into().expect("Invalid origin");
8384
let request_json = r#"
8485
{
8586
"rp": {
@@ -105,7 +106,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
105106
}
106107
"#;
107108
let make_credentials_request: MakeCredentialRequest =
108-
MakeCredentialRequest::from_json(&rpid, request_json)
109+
MakeCredentialRequest::from_json(&request_origin, request_json)
109110
.expect("Failed to parse request JSON");
110111
println!(
111112
"WebAuthn MakeCredential request: {:?}",
@@ -157,7 +158,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
157158
}
158159
"#;
159160
let get_assertion: GetAssertionRequest =
160-
GetAssertionRequest::from_json(&rpid, request_json)
161+
GetAssertionRequest::from_json(&request_origin, request_json)
161162
.expect("Failed to parse request JSON");
162163
println!("WebAuthn GetAssertion request: {:?}", get_assertion);
163164

0 commit comments

Comments
 (0)