Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c23423f
feat(deps): add optional reqwest 0.12 behind related-origins-client f…
AlfioEmanueleFresta May 17, 2026
0dcafcd
feat(webauthn): add related_origins module with trait and validator
AlfioEmanueleFresta May 17, 2026
d84b0da
feat(webauthn): add ReqwestRelatedOriginsClient behind feature flag
AlfioEmanueleFresta May 17, 2026
f6d63b7
refactor(webauthn): make FromIdlModel async with related-origins clie…
AlfioEmanueleFresta May 17, 2026
9e7edb2
feat(webauthn): wire related-origins fallback into make_credential an…
AlfioEmanueleFresta May 17, 2026
107db92
feat(webauthn): distinguish step 2.b and 2.c errors in related-origin…
AlfioEmanueleFresta May 17, 2026
cc6f44c
test(webauthn): add unit tests for related-origins validator
AlfioEmanueleFresta May 17, 2026
2051926
test(webauthn): add related-origins fallback tests for make_credentia…
AlfioEmanueleFresta May 17, 2026
bb568ae
test(webauthn): add integration test for related-origins end-to-end
AlfioEmanueleFresta May 17, 2026
f23909d
fix(webauthn): disable referer header in default related-origins client
AlfioEmanueleFresta May 17, 2026
33b00a4
feat(webauthn): document RelatedOriginsHttpClient trait contract
AlfioEmanueleFresta May 17, 2026
35b1b94
refactor(webauthn): make MAX_REGISTRABLE_LABELS crate-private
AlfioEmanueleFresta May 17, 2026
151eae9
fix(webauthn): redact related-origins error detail in mismatch log
AlfioEmanueleFresta May 17, 2026
e10f762
fix(webauthn): same_origin compares tuple not Origin re-parse
AlfioEmanueleFresta May 17, 2026
05a9686
test(webauthn): pin the 5th-distinct-label cap boundary
AlfioEmanueleFresta May 17, 2026
d978d57
test(webauthn): rename ipv6 test to reflect actual assertion
AlfioEmanueleFresta May 17, 2026
134c116
refactor(webauthn): re-export ReqwestRelatedOriginsClient from relate…
AlfioEmanueleFresta May 17, 2026
8260a13
chore(webauthn): trim verbose doc comments in related_origins
AlfioEmanueleFresta May 17, 2026
12f716a
feat(examples): switch webauthn ceremony examples to ReqwestRelatedOr…
AlfioEmanueleFresta May 18, 2026
41ef219
test(webauthn): use reserved example.* domains in related_origins int…
AlfioEmanueleFresta May 18, 2026
2a4d4e5
refactor(webauthn): narrow RelatedOriginsHttpClient error to WellKnow…
AlfioEmanueleFresta May 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
391 changes: 391 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,18 @@ $ git submodule update --init
The basic ceremony examples (register + authenticate) cover all transports. The
WebAuthn examples consume and emit JSON per the [WebAuthn IDL][webauthn].

| Transport | FIDO U2F | WebAuthn (FIDO2) |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| **USB (HID)** | `cargo run --example u2f_hid` | `cargo run --example webauthn_hid` |
| **Bluetooth (BLE)** | `cargo run --example u2f_ble` | `cargo run --example webauthn_ble` |
| **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` |
| **Hybrid (caBLE v2 + CTAP 2.3)** | — | `cargo run --example webauthn_cable` |
| **Hybrid (caBLE v2)** | — | `cargo run --example webauthn_cable_wss` |
| Transport | FIDO U2F | WebAuthn (FIDO2) [^ro] |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **USB (HID)** | `cargo run --example u2f_hid` | `cargo run --features related-origins-client --example webauthn_hid` |
| **Bluetooth (BLE)** | `cargo run --example u2f_ble` | `cargo run --features related-origins-client --example webauthn_ble` |
| **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,related-origins-client --example webauthn_nfc`<br>`cargo run --features nfc-backend-libnfc,related-origins-client --example webauthn_nfc` |
| **Hybrid (caBLE v2 + CTAP 2.3)** | — | `cargo run --features related-origins-client --example webauthn_cable` |
| **Hybrid (caBLE v2)** | — | `cargo run --features related-origins-client --example webauthn_cable_wss` |

[^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.

[^ro]: The WebAuthn ceremony examples wire up the bundled reqwest-backed [related-origins](https://www.w3.org/TR/webauthn-3/#sctn-related-origins) client, which lives behind the optional `related-origins-client` feature. Consumers that already ship their own HTTP stack can implement `RelatedOriginsHttpClient` directly and omit the feature.

Additional HID-only examples cover specific FIDO2 features and authenticator management:

```
Expand Down
16 changes: 15 additions & 1 deletion libwebauthn/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ nfc-backend-libnfc = [
# external crates (e.g. libwebauthn-tests) can plug in a virtual HID transport
# for end-to-end tests.
virt = []
# Provides the reqwest-backed default RelatedOriginsHttpClient. Off by default so
# the core crate stays HTTP-client-free; consumers that want the default impl
# opt in, others bring their own client.
related-origins-client = ["dep:reqwest"]

[dependencies]
base64-url = "3.0.0"
Expand Down Expand Up @@ -85,6 +89,12 @@ apdu = { version = "0.4.0", optional = true }
pcsc = { version = "2.9.0", optional = true }
nfc1 = { version = "=0.6.0", optional = true, default-features = false }
nfc1-sys = { version = "0.3.9", optional = true, default-features = false }
reqwest = { version = "0.12", default-features = false, features = [
"rustls-tls-native-roots",
"http2",
"stream",
"charset",
], optional = true }

[dev-dependencies]
tracing-subscriber = { version = "0.3.3", features = ["env-filter"] }
Expand Down Expand Up @@ -116,23 +126,27 @@ required-features = ["nfc"]
[[example]]
name = "webauthn_hid"
path = "examples/ceremony/webauthn_hid.rs"
required-features = ["related-origins-client"]

[[example]]
name = "webauthn_ble"
path = "examples/ceremony/webauthn_ble.rs"
required-features = ["related-origins-client"]

[[example]]
name = "webauthn_nfc"
path = "examples/ceremony/webauthn_nfc.rs"
required-features = ["nfc"]
required-features = ["nfc", "related-origins-client"]

[[example]]
name = "webauthn_cable"
path = "examples/ceremony/webauthn_cable.rs"
required-features = ["related-origins-client"]

[[example]]
name = "webauthn_cable_wss"
path = "examples/ceremony/webauthn_cable_wss.rs"
required-features = ["related-origins-client"]

[[example]]
name = "webauthn_extensions_hid"
Expand Down
9 changes: 6 additions & 3 deletions libwebauthn/examples/ceremony/webauthn_ble.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::error::Error;

use libwebauthn::ops::webauthn::{
DatFilePublicSuffixList, GetAssertionRequest, JsonFormat, MakeCredentialRequest, RequestOrigin,
WebAuthnIDL as _, WebAuthnIDLResponse as _,
ReqwestRelatedOriginsClient, WebAuthnIDL as _, WebAuthnIDLResponse as _,
};
use libwebauthn::proto::ctap2::Ctap2PublicKeyCredentialDescriptor;
use libwebauthn::transport::ble::list_devices;
Expand Down Expand Up @@ -52,8 +52,10 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
"attestation": "none"
}
"#;
let related_origins = ReqwestRelatedOriginsClient::new()?;
let make_credentials_request: MakeCredentialRequest =
MakeCredentialRequest::from_json(&request_origin, &psl, request_json)
MakeCredentialRequest::from_json(&request_origin, &psl, &related_origins, request_json)
.await
.expect("Failed to parse request JSON");
println!(
"WebAuthn MakeCredential request: {:?}",
Expand Down Expand Up @@ -97,7 +99,8 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
"#
);
let get_assertion: GetAssertionRequest =
GetAssertionRequest::from_json(&request_origin, &psl, &request_json)
GetAssertionRequest::from_json(&request_origin, &psl, &related_origins, &request_json)
.await
.expect("Failed to parse request JSON");
println!("WebAuthn GetAssertion request: {:?}", get_assertion);

Expand Down
15 changes: 11 additions & 4 deletions libwebauthn/examples/ceremony/webauthn_cable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use qrcode::render::unicode;
use qrcode::QrCode;

use libwebauthn::ops::webauthn::{
DatFilePublicSuffixList, JsonFormat, MakeCredentialRequest, RequestOrigin, WebAuthnIDL as _,
WebAuthnIDLResponse as _,
DatFilePublicSuffixList, JsonFormat, MakeCredentialRequest, RequestOrigin,
ReqwestRelatedOriginsClient, WebAuthnIDL as _, WebAuthnIDLResponse as _,
};
use libwebauthn::transport::{Channel as _, Device};
use libwebauthn::webauthn::WebAuthn;
Expand Down Expand Up @@ -58,6 +58,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
let psl = DatFilePublicSuffixList::from_system_file().expect(
"PSL not available; install the publicsuffix-list package or pass an explicit path",
);
let related_origins = ReqwestRelatedOriginsClient::new()?;

let mut device: CableQrCodeDevice = CableQrCodeDevice::new_transient(
QrCodeOperationHint::MakeCredential,
Expand All @@ -79,8 +80,14 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
let state_recv = channel.get_ux_update_receiver();
tokio::spawn(common::handle_cable_updates(state_recv));

let request = MakeCredentialRequest::from_json(&request_origin, &psl, MAKE_CREDENTIAL_REQUEST)
.expect("Failed to parse request JSON");
let request = MakeCredentialRequest::from_json(
&request_origin,
&psl,
&related_origins,
MAKE_CREDENTIAL_REQUEST,
)
.await
.expect("Failed to parse request JSON");

let response = retry_user_errors!(channel.webauthn_make_credential(&request)).unwrap();
let response_json = response
Expand Down
25 changes: 18 additions & 7 deletions libwebauthn/examples/ceremony/webauthn_cable_wss.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ use qrcode::QrCode;
use tokio::time::sleep;

use libwebauthn::ops::webauthn::{
GetAssertionRequest, JsonFormat, MakeCredentialRequest, RequestOrigin, SystemPublicSuffixList,
WebAuthnIDL as _, WebAuthnIDLResponse as _,
GetAssertionRequest, JsonFormat, MakeCredentialRequest, NoRelatedOriginsClient, RequestOrigin,
SystemPublicSuffixList, WebAuthnIDL as _, WebAuthnIDLResponse as _,
};
use libwebauthn::transport::cable::channel::CableChannel;
use libwebauthn::transport::{Channel as _, Device};
Expand Down Expand Up @@ -95,9 +95,14 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
let state_recv = channel.get_ux_update_receiver();
tokio::spawn(common::handle_cable_updates(state_recv));

let request =
MakeCredentialRequest::from_json(&request_origin, &psl, MAKE_CREDENTIAL_REQUEST)
.expect("Failed to parse request JSON");
let request = MakeCredentialRequest::from_json(
&request_origin,
&psl,
&NoRelatedOriginsClient,
MAKE_CREDENTIAL_REQUEST,
)
.await
.expect("Failed to parse request JSON");

let response = retry_user_errors!(channel.webauthn_make_credential(&request)).unwrap();
let response_json = response
Expand Down Expand Up @@ -155,8 +160,14 @@ async fn run_get_assertion(
let state_recv = channel.get_ux_update_receiver();
tokio::spawn(common::handle_cable_updates(state_recv));

let request = GetAssertionRequest::from_json(request_origin, psl, GET_ASSERTION_REQUEST)
.expect("Failed to parse request JSON");
let request = GetAssertionRequest::from_json(
request_origin,
psl,
&NoRelatedOriginsClient,
GET_ASSERTION_REQUEST,
)
.await
.expect("Failed to parse request JSON");
let response = retry_user_errors!(channel.webauthn_get_assertion(&request)).unwrap();
for assertion in &response.assertions {
let assertion_json = assertion
Expand Down
12 changes: 8 additions & 4 deletions libwebauthn/examples/ceremony/webauthn_hid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ use std::error::Error;
use std::time::Duration;

use libwebauthn::ops::webauthn::{
GetAssertionRequest, JsonFormat, MakeCredentialRequest, RequestOrigin, SystemPublicSuffixList,
WebAuthnIDL as _, WebAuthnIDLResponse as _,
GetAssertionRequest, JsonFormat, MakeCredentialRequest, RequestOrigin,
ReqwestRelatedOriginsClient, SystemPublicSuffixList, WebAuthnIDL as _,
WebAuthnIDLResponse as _,
};
use libwebauthn::proto::ctap2::Ctap2PublicKeyCredentialDescriptor;
use libwebauthn::transport::hid::list_devices;
Expand Down Expand Up @@ -32,6 +33,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
let psl = SystemPublicSuffixList::auto().expect(
"PSL not available; install the publicsuffix-list (or publicsuffix-list-dafsa) package, or pass an explicit path",
);
let related_origins = ReqwestRelatedOriginsClient::new()?;
let request_json = r#"
{
"rp": {
Expand All @@ -57,7 +59,8 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
}
"#;
let make_credentials_request: MakeCredentialRequest =
MakeCredentialRequest::from_json(&request_origin, &psl, request_json)
MakeCredentialRequest::from_json(&request_origin, &psl, &related_origins, request_json)
.await
.expect("Failed to parse request JSON");
println!(
"WebAuthn MakeCredential request: {:?}",
Expand Down Expand Up @@ -101,7 +104,8 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
"#
);
let get_assertion: GetAssertionRequest =
GetAssertionRequest::from_json(&request_origin, &psl, &request_json)
GetAssertionRequest::from_json(&request_origin, &psl, &related_origins, &request_json)
.await
.expect("Failed to parse request JSON");
println!("WebAuthn GetAssertion request: {:?}", get_assertion);

Expand Down
10 changes: 8 additions & 2 deletions libwebauthn/examples/ceremony/webauthn_nfc.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use std::error::Error;

use libwebauthn::ops::webauthn::{
GetAssertionRequest, JsonFormat, MakeCredentialRequest, RequestOrigin, SystemPublicSuffixList,
WebAuthnIDL as _, WebAuthnIDLResponse as _,
GetAssertionRequest, JsonFormat, MakeCredentialRequest, RequestOrigin,
ReqwestRelatedOriginsClient, SystemPublicSuffixList, WebAuthnIDL as _,
WebAuthnIDLResponse as _,
};
use libwebauthn::transport::nfc::{get_nfc_device, is_nfc_available};
use libwebauthn::transport::{Channel as _, Device};
Expand Down Expand Up @@ -30,9 +31,11 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
let psl = SystemPublicSuffixList::auto().expect(
"PSL not available; install the publicsuffix-list (or publicsuffix-list-dafsa) package, or pass an explicit path",
);
let related_origins = ReqwestRelatedOriginsClient::new()?;
let make_credentials_request = MakeCredentialRequest::from_json(
&request_origin,
&psl,
&related_origins,
r#"
{
"rp": {
Expand All @@ -58,6 +61,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
}
"#,
)
.await
.expect("Failed to parse request JSON");
println!(
"WebAuthn MakeCredential request: {:?}",
Expand All @@ -77,6 +81,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
let get_assertion = GetAssertionRequest::from_json(
&request_origin,
&psl,
&related_origins,
r#"
{
"challenge": "Y3JlZGVudGlhbHMtZm9yLWxpbnV4L2xpYndlYmF1dGhu",
Expand All @@ -86,6 +91,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
}
"#,
)
.await
.expect("Failed to parse request JSON");
println!("WebAuthn GetAssertion request: {:?}", get_assertion);

Expand Down
Loading
Loading