Skip to content

Commit 7d26b98

Browse files
feat(webauthn): related-origins validation (WebAuthn L3 §5.11) (#219)
Implements WebAuthn L3 §5.11 "Related Origins". When a request's rp.id is not a registrable suffix of the caller's effective domain, libwebauthn resolves the relying party's allowed origins and accepts the request if one matches the caller. Origin resolution is pluggable and the matching always runs in libwebauthn. An optional reqwest-backed source fetches the RP's `.well-known/webauthn` document behind the `reqwest-related-origins-source` feature, so the core crate stays HTTP-client-free. A caller that already has the list, such as a browser, can supply its own source and skip the fetch. Related origins can also be turned off. Request building now takes its public-suffix-list and related-origins dependencies through a settings value rather than positional parameters, which keeps the API manageable as options grow. Existing call sites need a small update for the new signature. Closes #160. Based on #173 by @HarveyOrourke15.
1 parent e104a65 commit 7d26b98

25 files changed

Lines changed: 2618 additions & 298 deletions

Cargo.lock

Lines changed: 392 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,18 @@ $ git submodule update --init
6868
The basic ceremony examples (register + authenticate) cover all transports. The
6969
WebAuthn examples consume and emit JSON per the [WebAuthn IDL][webauthn].
7070

71-
| Transport | FIDO U2F | WebAuthn (FIDO2) |
72-
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
73-
| **USB (HID)** | `cargo run --example u2f_hid` | `cargo run --example webauthn_hid` |
74-
| **Bluetooth (BLE)** | `cargo run --example u2f_ble` | `cargo run --example webauthn_ble` |
75-
| **NFC** [^nfc] | `cargo run --features nfc-backend-pcsc --example u2f_nfc`<br>`cargo run --features nfc-backend-libnfc --example u2f_nfc` | `cargo run --features nfc-backend-pcsc --example webauthn_nfc`<br>`cargo run --features nfc-backend-libnfc --example webauthn_nfc` |
76-
| **Hybrid (caBLE v2 + CTAP 2.3)** || `cargo run --example webauthn_cable` |
77-
| **Hybrid (caBLE v2)** || `cargo run --example webauthn_cable_wss` |
71+
| Transport | FIDO U2F | WebAuthn (FIDO2) [^ro] |
72+
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
73+
| **USB (HID)** | `cargo run --example u2f_hid` | `cargo run --example webauthn_hid` |
74+
| **Bluetooth (BLE)** | `cargo run --example u2f_ble` | `cargo run --example webauthn_ble` |
75+
| **NFC** [^nfc] | `cargo run --features nfc-backend-pcsc --example u2f_nfc`<br>`cargo run --features nfc-backend-libnfc --example u2f_nfc` | `cargo run --features nfc-backend-pcsc --example webauthn_nfc`<br>`cargo run --features nfc-backend-libnfc --example webauthn_nfc` |
76+
| **Hybrid (caBLE v2 + CTAP 2.3)** | | `cargo run --example webauthn_cable` |
77+
| **Hybrid (caBLE v2)** || `cargo run --example webauthn_cable_wss` |
7878

7979
[^nfc]: `nfc-backend-pcsc` is pure userspace and recommended on most systems. `nfc-backend-libnfc` requires the `libnfc` system library. Both can be enabled together; the first FIDO device found by either backend is used.
8080

81+
[^ro]: The ceremony examples run with related origins disabled (they are same-origin, so it never applies). The bundled reqwest-backed [related-origins](https://www.w3.org/TR/webauthn-3/#sctn-related-origins) source is shown in the `webauthn_related_origins_hid` example below, behind the optional `reqwest-related-origins-source` feature. Consumers that ship their own HTTP stack can implement `HttpClient` or `RelatedOriginsSource` directly.
82+
8183
Additional HID-only examples cover specific FIDO2 features and authenticator management:
8284

8385
```
@@ -88,6 +90,9 @@ $ cargo run --example webauthn_prf_hid
8890
$ cargo run --example prf_replay -- CREDENTIAL_ID FIRST_PRF_INPUT
8991
$ cargo run --example device_selection_hid
9092
93+
# Related origins (reqwest-backed well-known fetch)
94+
$ cargo run --features reqwest-related-origins-source --example webauthn_related_origins_hid
95+
9196
# CTAP2 authenticator management
9297
$ cargo run --example change_pin_hid
9398
$ cargo run --example bio_enrollment_hid

libwebauthn/Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ nfc-backend-libnfc = [
3939
# external crates (e.g. libwebauthn-tests) can plug in a virtual HID transport
4040
# for end-to-end tests.
4141
virt = []
42+
# Provides the reqwest-backed HttpClient and ReqwestRelatedOriginsSource. Off by
43+
# default so the core crate stays HTTP-client-free. Consumers that want the
44+
# default fetch opt in, others implement HttpClient or RelatedOriginsSource.
45+
reqwest-related-origins-source = ["dep:reqwest"]
4246

4347
[dependencies]
4448
base64-url = "3.0.0"
@@ -47,6 +51,7 @@ tracing = "0.1.29"
4751
idna = "1.0.3"
4852
publicsuffix = "2.3"
4953
url = "2.5"
54+
http = "1"
5055
maplit = "1.0.2"
5156
sha2 = "0.10.2"
5257
uuid = { version = "1.5.0", features = ["serde", "v4"] }
@@ -100,6 +105,12 @@ apdu = { version = "0.4.0", optional = true }
100105
pcsc = { version = "2.9.0", optional = true }
101106
nfc1 = { version = "=0.6.0", optional = true, default-features = false }
102107
nfc1-sys = { version = "0.3.9", optional = true, default-features = false }
108+
reqwest = { version = "0.12", default-features = false, features = [
109+
"rustls-tls-native-roots",
110+
"http2",
111+
"stream",
112+
"charset",
113+
], optional = true }
103114

104115
[dev-dependencies]
105116
tracing-subscriber = { version = "0.3.3", features = ["env-filter"] }
@@ -161,6 +172,11 @@ path = "examples/features/webauthn_preflight_hid.rs"
161172
name = "webauthn_prf_hid"
162173
path = "examples/features/webauthn_prf_hid.rs"
163174

175+
[[example]]
176+
name = "webauthn_related_origins_hid"
177+
path = "examples/features/webauthn_related_origins_hid.rs"
178+
required-features = ["reqwest-related-origins-source"]
179+
164180
[[example]]
165181
name = "prf_replay"
166182
path = "examples/features/prf_replay.rs"

libwebauthn/examples/ceremony/webauthn_ble.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use std::error::Error;
22

33
use libwebauthn::ops::webauthn::{
4-
DatFilePublicSuffixList, GetAssertionRequest, JsonFormat, MakeCredentialRequest, RequestOrigin,
5-
WebAuthnIDL as _, WebAuthnIDLResponse as _,
4+
DatFilePublicSuffixList, GetAssertionRequest, JsonFormat, MakeCredentialRequest,
5+
OriginValidation, RelatedOrigins, RequestOrigin, RequestSettings, WebAuthnIDLResponse as _,
66
};
77
use libwebauthn::proto::ctap2::Ctap2PublicKeyCredentialDescriptor;
88
use libwebauthn::transport::ble::list_devices;
@@ -52,8 +52,15 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
5252
"attestation": "none"
5353
}
5454
"#;
55+
let settings = RequestSettings {
56+
origin: OriginValidation::Validate {
57+
public_suffix_list: &psl,
58+
related_origins: RelatedOrigins::Disabled,
59+
},
60+
};
5561
let make_credentials_request: MakeCredentialRequest =
56-
MakeCredentialRequest::from_json(&request_origin, &psl, request_json)
62+
MakeCredentialRequest::prepare(&request_origin, request_json, &settings)
63+
.await
5764
.expect("Failed to parse request JSON");
5865
println!(
5966
"WebAuthn MakeCredential request: {:?}",
@@ -97,7 +104,8 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
97104
"#
98105
);
99106
let get_assertion: GetAssertionRequest =
100-
GetAssertionRequest::from_json(&request_origin, &psl, &request_json)
107+
GetAssertionRequest::prepare(&request_origin, &request_json, &settings)
108+
.await
101109
.expect("Failed to parse request JSON");
102110
println!("WebAuthn GetAssertion request: {:?}", get_assertion);
103111

libwebauthn/examples/ceremony/webauthn_cable.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use qrcode::render::unicode;
1111
use qrcode::QrCode;
1212

1313
use libwebauthn::ops::webauthn::{
14-
DatFilePublicSuffixList, JsonFormat, MakeCredentialRequest, RequestOrigin, WebAuthnIDL as _,
15-
WebAuthnIDLResponse as _,
14+
DatFilePublicSuffixList, JsonFormat, MakeCredentialRequest, OriginValidation, RelatedOrigins,
15+
RequestOrigin, RequestSettings, WebAuthnIDLResponse as _,
1616
};
1717
use libwebauthn::transport::{Channel as _, Device};
1818
use libwebauthn::webauthn::WebAuthn;
@@ -58,6 +58,12 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
5858
let psl = DatFilePublicSuffixList::from_system_file().expect(
5959
"PSL not available; install the publicsuffix-list package or pass an explicit path",
6060
);
61+
let settings = RequestSettings {
62+
origin: OriginValidation::Validate {
63+
public_suffix_list: &psl,
64+
related_origins: RelatedOrigins::Disabled,
65+
},
66+
};
6167

6268
let mut device: CableQrCodeDevice = CableQrCodeDevice::new_transient(
6369
QrCodeOperationHint::MakeCredential,
@@ -79,8 +85,10 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
7985
let state_recv = channel.get_ux_update_receiver();
8086
tokio::spawn(common::handle_cable_updates(state_recv));
8187

82-
let request = MakeCredentialRequest::from_json(&request_origin, &psl, MAKE_CREDENTIAL_REQUEST)
83-
.expect("Failed to parse request JSON");
88+
let request =
89+
MakeCredentialRequest::prepare(&request_origin, MAKE_CREDENTIAL_REQUEST, &settings)
90+
.await
91+
.expect("Failed to parse request JSON");
8492

8593
let response = retry_user_errors!(channel.webauthn_make_credential(&request)).unwrap();
8694
let response_json = response

libwebauthn/examples/ceremony/webauthn_cable_wss.rs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ use qrcode::QrCode;
1414
use tokio::time::sleep;
1515

1616
use libwebauthn::ops::webauthn::{
17-
GetAssertionRequest, JsonFormat, MakeCredentialRequest, RequestOrigin, SystemPublicSuffixList,
18-
WebAuthnIDL as _, WebAuthnIDLResponse as _,
17+
GetAssertionRequest, JsonFormat, MakeCredentialRequest, OriginValidation, RelatedOrigins,
18+
RequestOrigin, RequestSettings, SystemPublicSuffixList, WebAuthnIDLResponse as _,
1919
};
2020
use libwebauthn::transport::cable::channel::CableChannel;
2121
use libwebauthn::transport::{Channel as _, Device};
@@ -95,9 +95,18 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
9595
let state_recv = channel.get_ux_update_receiver();
9696
tokio::spawn(common::handle_cable_updates(state_recv));
9797

98-
let request =
99-
MakeCredentialRequest::from_json(&request_origin, &psl, MAKE_CREDENTIAL_REQUEST)
100-
.expect("Failed to parse request JSON");
98+
let request = MakeCredentialRequest::prepare(
99+
&request_origin,
100+
MAKE_CREDENTIAL_REQUEST,
101+
&RequestSettings {
102+
origin: OriginValidation::Validate {
103+
public_suffix_list: &psl,
104+
related_origins: RelatedOrigins::Disabled,
105+
},
106+
},
107+
)
108+
.await
109+
.expect("Failed to parse request JSON");
101110

102111
let response = retry_user_errors!(channel.webauthn_make_credential(&request)).unwrap();
103112
let response_json = response
@@ -155,8 +164,18 @@ async fn run_get_assertion(
155164
let state_recv = channel.get_ux_update_receiver();
156165
tokio::spawn(common::handle_cable_updates(state_recv));
157166

158-
let request = GetAssertionRequest::from_json(request_origin, psl, GET_ASSERTION_REQUEST)
159-
.expect("Failed to parse request JSON");
167+
let request = GetAssertionRequest::prepare(
168+
request_origin,
169+
GET_ASSERTION_REQUEST,
170+
&RequestSettings {
171+
origin: OriginValidation::Validate {
172+
public_suffix_list: psl,
173+
related_origins: RelatedOrigins::Disabled,
174+
},
175+
},
176+
)
177+
.await
178+
.expect("Failed to parse request JSON");
160179
let response = retry_user_errors!(channel.webauthn_get_assertion(&request)).unwrap();
161180
for assertion in &response.assertions {
162181
let assertion_json = assertion

libwebauthn/examples/ceremony/webauthn_hid.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use std::error::Error;
22
use std::time::Duration;
33

44
use libwebauthn::ops::webauthn::{
5-
GetAssertionRequest, JsonFormat, MakeCredentialRequest, RequestOrigin, SystemPublicSuffixList,
6-
WebAuthnIDL as _, WebAuthnIDLResponse as _,
5+
GetAssertionRequest, JsonFormat, MakeCredentialRequest, OriginValidation, RelatedOrigins,
6+
RequestOrigin, RequestSettings, SystemPublicSuffixList, WebAuthnIDLResponse as _,
77
};
88
use libwebauthn::proto::ctap2::Ctap2PublicKeyCredentialDescriptor;
99
use libwebauthn::transport::hid::list_devices;
@@ -32,6 +32,12 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
3232
let psl = SystemPublicSuffixList::auto().expect(
3333
"PSL not available; install the publicsuffix-list (or publicsuffix-list-dafsa) package, or pass an explicit path",
3434
);
35+
let settings = RequestSettings {
36+
origin: OriginValidation::Validate {
37+
public_suffix_list: &psl,
38+
related_origins: RelatedOrigins::Disabled,
39+
},
40+
};
3541
let request_json = r#"
3642
{
3743
"rp": {
@@ -57,7 +63,8 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
5763
}
5864
"#;
5965
let make_credentials_request: MakeCredentialRequest =
60-
MakeCredentialRequest::from_json(&request_origin, &psl, request_json)
66+
MakeCredentialRequest::prepare(&request_origin, request_json, &settings)
67+
.await
6168
.expect("Failed to parse request JSON");
6269
println!(
6370
"WebAuthn MakeCredential request: {:?}",
@@ -101,7 +108,8 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
101108
"#
102109
);
103110
let get_assertion: GetAssertionRequest =
104-
GetAssertionRequest::from_json(&request_origin, &psl, &request_json)
111+
GetAssertionRequest::prepare(&request_origin, &request_json, &settings)
112+
.await
105113
.expect("Failed to parse request JSON");
106114
println!("WebAuthn GetAssertion request: {:?}", get_assertion);
107115

libwebauthn/examples/ceremony/webauthn_nfc.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use std::error::Error;
22

33
use libwebauthn::ops::webauthn::{
4-
GetAssertionRequest, JsonFormat, MakeCredentialRequest, RequestOrigin, SystemPublicSuffixList,
5-
WebAuthnIDL as _, WebAuthnIDLResponse as _,
4+
GetAssertionRequest, JsonFormat, MakeCredentialRequest, OriginValidation, RelatedOrigins,
5+
RequestOrigin, RequestSettings, SystemPublicSuffixList, WebAuthnIDLResponse as _,
66
};
77
use libwebauthn::transport::nfc::{get_nfc_device, is_nfc_available};
88
use libwebauthn::transport::{Channel as _, Device};
@@ -30,9 +30,14 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
3030
let psl = SystemPublicSuffixList::auto().expect(
3131
"PSL not available; install the publicsuffix-list (or publicsuffix-list-dafsa) package, or pass an explicit path",
3232
);
33-
let make_credentials_request = MakeCredentialRequest::from_json(
33+
let settings = RequestSettings {
34+
origin: OriginValidation::Validate {
35+
public_suffix_list: &psl,
36+
related_origins: RelatedOrigins::Disabled,
37+
},
38+
};
39+
let make_credentials_request = MakeCredentialRequest::prepare(
3440
&request_origin,
35-
&psl,
3641
r#"
3742
{
3843
"rp": {
@@ -57,7 +62,9 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
5762
"attestation": "none"
5863
}
5964
"#,
65+
&settings,
6066
)
67+
.await
6168
.expect("Failed to parse request JSON");
6269
println!(
6370
"WebAuthn MakeCredential request: {:?}",
@@ -74,9 +81,8 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
7481
.expect("Failed to serialize MakeCredential response");
7582
println!("WebAuthn MakeCredential response (JSON):\n{response_json}");
7683

77-
let get_assertion = GetAssertionRequest::from_json(
84+
let get_assertion = GetAssertionRequest::prepare(
7885
&request_origin,
79-
&psl,
8086
r#"
8187
{
8288
"challenge": "Y3JlZGVudGlhbHMtZm9yLWxpbnV4L2xpYndlYmF1dGhu",
@@ -85,7 +91,9 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
8591
"userVerification": "discouraged"
8692
}
8793
"#,
94+
&settings,
8895
)
96+
.await
8997
.expect("Failed to parse request JSON");
9098
println!("WebAuthn GetAssertion request: {:?}", get_assertion);
9199

0 commit comments

Comments
 (0)