Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
155e1b5
feat(deps): add optional reqwest 0.12 behind related-origins-client f…
AlfioEmanueleFresta May 17, 2026
4fb8710
feat(webauthn): add related_origins module with trait and validator
AlfioEmanueleFresta May 17, 2026
ce6fd23
feat(webauthn): add ReqwestRelatedOriginsClient behind feature flag
AlfioEmanueleFresta May 17, 2026
129d38f
refactor(webauthn): make FromIdlModel async with related-origins clie…
AlfioEmanueleFresta May 17, 2026
bd140b8
feat(webauthn): wire related-origins fallback into make_credential an…
AlfioEmanueleFresta May 17, 2026
913a744
feat(webauthn): distinguish step 2.b and 2.c errors in related-origin…
AlfioEmanueleFresta May 17, 2026
384c548
test(webauthn): add unit tests for related-origins validator
AlfioEmanueleFresta May 17, 2026
bc8219c
test(webauthn): add related-origins fallback tests for make_credentia…
AlfioEmanueleFresta May 17, 2026
d375c2d
test(webauthn): add integration test for related-origins end-to-end
AlfioEmanueleFresta May 17, 2026
b4a181f
fix(webauthn): disable referer header in default related-origins client
AlfioEmanueleFresta May 17, 2026
a8e0608
feat(webauthn): document RelatedOriginsHttpClient trait contract
AlfioEmanueleFresta May 17, 2026
7e84b8f
refactor(webauthn): make MAX_REGISTRABLE_LABELS crate-private
AlfioEmanueleFresta May 17, 2026
6223869
fix(webauthn): redact related-origins error detail in mismatch log
AlfioEmanueleFresta May 17, 2026
6bb487d
fix(webauthn): same_origin compares tuple not Origin re-parse
AlfioEmanueleFresta May 17, 2026
e20b15d
test(webauthn): pin the 5th-distinct-label cap boundary
AlfioEmanueleFresta May 17, 2026
2663a05
test(webauthn): rename ipv6 test to reflect actual assertion
AlfioEmanueleFresta May 17, 2026
181ee7a
refactor(webauthn): re-export ReqwestRelatedOriginsClient from relate…
AlfioEmanueleFresta May 17, 2026
23c7cfb
chore(webauthn): trim verbose doc comments in related_origins
AlfioEmanueleFresta May 17, 2026
a6affbc
feat(examples): switch webauthn ceremony examples to ReqwestRelatedOr…
AlfioEmanueleFresta May 18, 2026
e73052e
test(webauthn): use reserved example.* domains in related_origins int…
AlfioEmanueleFresta May 18, 2026
7af1ad0
refactor(webauthn): narrow RelatedOriginsHttpClient error to WellKnow…
AlfioEmanueleFresta May 18, 2026
fc8ebc4
refactor(webauthn): pluggable HttpClient and RelatedOriginsSource
AlfioEmanueleFresta May 28, 2026
0617c6c
feat(webauthn): prepare() request builder with RequestSettings
AlfioEmanueleFresta May 28, 2026
c3d0826
test(webauthn): adopt prepare() in examples, integration test, and RE…
AlfioEmanueleFresta May 28, 2026
902ee8a
feat(webauthn): add StaticRelatedOriginsSource for caller-provided or…
AlfioEmanueleFresta May 30, 2026
f66a860
style(webauthn): apply rustfmt
AlfioEmanueleFresta May 30, 2026
21e1642
docs: add module-level documentation (#237)
AlfioEmanueleFresta May 30, 2026
fcfce6b
feat(webauthn): add OriginValidation::Trust to bypass the rp.id check
AlfioEmanueleFresta May 30, 2026
c011930
refactor(examples): drop related origins from ceremony examples, add …
AlfioEmanueleFresta May 30, 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
392 changes: 392 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

19 changes: 12 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 --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` |

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

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

```
Expand All @@ -88,6 +90,9 @@ $ cargo run --example webauthn_prf_hid
$ cargo run --example prf_replay -- CREDENTIAL_ID FIRST_PRF_INPUT
$ cargo run --example device_selection_hid

# Related origins (reqwest-backed well-known fetch)
$ cargo run --features reqwest-related-origins-source --example webauthn_related_origins_hid

# CTAP2 authenticator management
$ cargo run --example change_pin_hid
$ cargo run --example bio_enrollment_hid
Expand Down
16 changes: 16 additions & 0 deletions libwebauthn/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,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 HttpClient and ReqwestRelatedOriginsSource. Off by
# default so the core crate stays HTTP-client-free. Consumers that want the
# default fetch opt in, others implement HttpClient or RelatedOriginsSource.
reqwest-related-origins-source = ["dep:reqwest"]

[dependencies]
base64-url = "3.0.0"
Expand All @@ -47,6 +51,7 @@ tracing = "0.1.29"
idna = "1.0.3"
publicsuffix = "2.3"
url = "2.5"
http = "1"
maplit = "1.0.2"
sha2 = "0.10.2"
uuid = { version = "1.5.0", features = ["serde", "v4"] }
Expand Down Expand Up @@ -100,6 +105,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 @@ -161,6 +172,11 @@ path = "examples/features/webauthn_preflight_hid.rs"
name = "webauthn_prf_hid"
path = "examples/features/webauthn_prf_hid.rs"

[[example]]
name = "webauthn_related_origins_hid"
path = "examples/features/webauthn_related_origins_hid.rs"
required-features = ["reqwest-related-origins-source"]

[[example]]
name = "prf_replay"
path = "examples/features/prf_replay.rs"
Expand Down
16 changes: 12 additions & 4 deletions libwebauthn/examples/ceremony/webauthn_ble.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::error::Error;

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

Expand Down
16 changes: 12 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, OriginValidation, RelatedOrigins,
RequestOrigin, RequestSettings, WebAuthnIDLResponse as _,
};
use libwebauthn::transport::{Channel as _, Device};
use libwebauthn::webauthn::WebAuthn;
Expand Down Expand Up @@ -58,6 +58,12 @@ 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 settings = RequestSettings {
origin: OriginValidation::Validate {
public_suffix_list: &psl,
related_origins: RelatedOrigins::Disabled,
},
};

let mut device: CableQrCodeDevice = CableQrCodeDevice::new_transient(
QrCodeOperationHint::MakeCredential,
Expand All @@ -79,8 +85,10 @@ 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::prepare(&request_origin, MAKE_CREDENTIAL_REQUEST, &settings)
.await
.expect("Failed to parse request JSON");

let response = retry_user_errors!(channel.webauthn_make_credential(&request)).unwrap();
let response_json = response
Expand Down
33 changes: 26 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, OriginValidation, RelatedOrigins,
RequestOrigin, RequestSettings, SystemPublicSuffixList, WebAuthnIDLResponse as _,
};
use libwebauthn::transport::cable::channel::CableChannel;
use libwebauthn::transport::{Channel as _, Device};
Expand Down Expand Up @@ -95,9 +95,18 @@ 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::prepare(
&request_origin,
MAKE_CREDENTIAL_REQUEST,
&RequestSettings {
origin: OriginValidation::Validate {
public_suffix_list: &psl,
related_origins: RelatedOrigins::Disabled,
},
},
)
.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 +164,18 @@ 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::prepare(
request_origin,
GET_ASSERTION_REQUEST,
&RequestSettings {
origin: OriginValidation::Validate {
public_suffix_list: psl,
related_origins: RelatedOrigins::Disabled,
},
},
)
.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
16 changes: 12 additions & 4 deletions libwebauthn/examples/ceremony/webauthn_hid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use std::error::Error;
use std::time::Duration;

use libwebauthn::ops::webauthn::{
GetAssertionRequest, JsonFormat, MakeCredentialRequest, RequestOrigin, SystemPublicSuffixList,
WebAuthnIDL as _, WebAuthnIDLResponse as _,
GetAssertionRequest, JsonFormat, MakeCredentialRequest, OriginValidation, RelatedOrigins,
RequestOrigin, RequestSettings, SystemPublicSuffixList, WebAuthnIDLResponse as _,
};
use libwebauthn::proto::ctap2::Ctap2PublicKeyCredentialDescriptor;
use libwebauthn::transport::hid::list_devices;
Expand Down Expand Up @@ -32,6 +32,12 @@ 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 settings = RequestSettings {
origin: OriginValidation::Validate {
public_suffix_list: &psl,
related_origins: RelatedOrigins::Disabled,
},
};
let request_json = r#"
{
"rp": {
Expand All @@ -57,7 +63,8 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
}
"#;
let make_credentials_request: MakeCredentialRequest =
MakeCredentialRequest::from_json(&request_origin, &psl, request_json)
MakeCredentialRequest::prepare(&request_origin, request_json, &settings)
.await
.expect("Failed to parse request JSON");
println!(
"WebAuthn MakeCredential request: {:?}",
Expand Down Expand Up @@ -101,7 +108,8 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
"#
);
let get_assertion: GetAssertionRequest =
GetAssertionRequest::from_json(&request_origin, &psl, &request_json)
GetAssertionRequest::prepare(&request_origin, &request_json, &settings)
.await
.expect("Failed to parse request JSON");
println!("WebAuthn GetAssertion request: {:?}", get_assertion);

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

use libwebauthn::ops::webauthn::{
GetAssertionRequest, JsonFormat, MakeCredentialRequest, RequestOrigin, SystemPublicSuffixList,
WebAuthnIDL as _, WebAuthnIDLResponse as _,
GetAssertionRequest, JsonFormat, MakeCredentialRequest, OriginValidation, RelatedOrigins,
RequestOrigin, RequestSettings, SystemPublicSuffixList, WebAuthnIDLResponse as _,
};
use libwebauthn::transport::nfc::{get_nfc_device, is_nfc_available};
use libwebauthn::transport::{Channel as _, Device};
Expand Down Expand Up @@ -30,9 +30,14 @@ 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 make_credentials_request = MakeCredentialRequest::from_json(
let settings = RequestSettings {
origin: OriginValidation::Validate {
public_suffix_list: &psl,
related_origins: RelatedOrigins::Disabled,
},
};
let make_credentials_request = MakeCredentialRequest::prepare(
&request_origin,
&psl,
r#"
{
"rp": {
Expand All @@ -57,7 +62,9 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
"attestation": "none"
}
"#,
&settings,
)
.await
.expect("Failed to parse request JSON");
println!(
"WebAuthn MakeCredential request: {:?}",
Expand All @@ -74,9 +81,8 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
.expect("Failed to serialize MakeCredential response");
println!("WebAuthn MakeCredential response (JSON):\n{response_json}");

let get_assertion = GetAssertionRequest::from_json(
let get_assertion = GetAssertionRequest::prepare(
&request_origin,
&psl,
r#"
{
"challenge": "Y3JlZGVudGlhbHMtZm9yLWxpbnV4L2xpYndlYmF1dGhu",
Expand All @@ -85,7 +91,9 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
"userVerification": "discouraged"
}
"#,
&settings,
)
.await
.expect("Failed to parse request JSON");
println!("WebAuthn GetAssertion request: {:?}", get_assertion);

Expand Down
Loading
Loading