Skip to content

Commit fcfce6b

Browse files
feat(webauthn): add OriginValidation::Trust to bypass the rp.id check
1 parent 21e1642 commit fcfce6b

11 files changed

Lines changed: 153 additions & 56 deletions

File tree

libwebauthn/examples/ceremony/webauthn_ble.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::error::Error;
22

33
use libwebauthn::ops::webauthn::{
44
DatFilePublicSuffixList, GetAssertionRequest, JsonFormat, MakeCredentialRequest,
5-
MaxRegistrableLabels, RelatedOrigins, RequestOrigin, RequestSettings,
5+
MaxRegistrableLabels, OriginValidation, RelatedOrigins, RequestOrigin, RequestSettings,
66
ReqwestRelatedOriginsSource, WebAuthnIDLResponse as _,
77
};
88
use libwebauthn::proto::ctap2::Ctap2PublicKeyCredentialDescriptor;
@@ -55,10 +55,12 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
5555
"#;
5656
let related_origins = ReqwestRelatedOriginsSource::new()?;
5757
let settings = RequestSettings {
58-
public_suffix_list: &psl,
59-
related_origins: RelatedOrigins::Enabled {
60-
source: &related_origins,
61-
max_labels: MaxRegistrableLabels::default(),
58+
origin: OriginValidation::Validate {
59+
public_suffix_list: &psl,
60+
related_origins: RelatedOrigins::Enabled {
61+
source: &related_origins,
62+
max_labels: MaxRegistrableLabels::default(),
63+
},
6264
},
6365
};
6466
let make_credentials_request: MakeCredentialRequest =

libwebauthn/examples/ceremony/webauthn_cable.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use qrcode::QrCode;
1212

1313
use libwebauthn::ops::webauthn::{
1414
DatFilePublicSuffixList, JsonFormat, MakeCredentialRequest, MaxRegistrableLabels,
15-
RelatedOrigins, RequestOrigin, RequestSettings, ReqwestRelatedOriginsSource,
15+
OriginValidation, RelatedOrigins, RequestOrigin, RequestSettings, ReqwestRelatedOriginsSource,
1616
WebAuthnIDLResponse as _,
1717
};
1818
use libwebauthn::transport::{Channel as _, Device};
@@ -61,10 +61,12 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
6161
);
6262
let related_origins = ReqwestRelatedOriginsSource::new()?;
6363
let settings = RequestSettings {
64-
public_suffix_list: &psl,
65-
related_origins: RelatedOrigins::Enabled {
66-
source: &related_origins,
67-
max_labels: MaxRegistrableLabels::default(),
64+
origin: OriginValidation::Validate {
65+
public_suffix_list: &psl,
66+
related_origins: RelatedOrigins::Enabled {
67+
source: &related_origins,
68+
max_labels: MaxRegistrableLabels::default(),
69+
},
6870
},
6971
};
7072

libwebauthn/examples/ceremony/webauthn_cable_wss.rs

Lines changed: 10 additions & 6 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, RelatedOrigins, RequestOrigin,
18-
RequestSettings, SystemPublicSuffixList, 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};
@@ -99,8 +99,10 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
9999
&request_origin,
100100
MAKE_CREDENTIAL_REQUEST,
101101
&RequestSettings {
102-
public_suffix_list: &psl,
103-
related_origins: RelatedOrigins::Disabled,
102+
origin: OriginValidation::Validate {
103+
public_suffix_list: &psl,
104+
related_origins: RelatedOrigins::Disabled,
105+
},
104106
},
105107
)
106108
.await
@@ -166,8 +168,10 @@ async fn run_get_assertion(
166168
request_origin,
167169
GET_ASSERTION_REQUEST,
168170
&RequestSettings {
169-
public_suffix_list: psl,
170-
related_origins: RelatedOrigins::Disabled,
171+
origin: OriginValidation::Validate {
172+
public_suffix_list: psl,
173+
related_origins: RelatedOrigins::Disabled,
174+
},
171175
},
172176
)
173177
.await

libwebauthn/examples/ceremony/webauthn_hid.rs

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

44
use libwebauthn::ops::webauthn::{
5-
GetAssertionRequest, JsonFormat, MakeCredentialRequest, MaxRegistrableLabels, RelatedOrigins,
6-
RequestOrigin, RequestSettings, ReqwestRelatedOriginsSource, SystemPublicSuffixList,
7-
WebAuthnIDLResponse as _,
5+
GetAssertionRequest, JsonFormat, MakeCredentialRequest, MaxRegistrableLabels, OriginValidation,
6+
RelatedOrigins, RequestOrigin, RequestSettings, ReqwestRelatedOriginsSource,
7+
SystemPublicSuffixList, WebAuthnIDLResponse as _,
88
};
99
use libwebauthn::proto::ctap2::Ctap2PublicKeyCredentialDescriptor;
1010
use libwebauthn::transport::hid::list_devices;
@@ -35,10 +35,12 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
3535
);
3636
let related_origins = ReqwestRelatedOriginsSource::new()?;
3737
let settings = RequestSettings {
38-
public_suffix_list: &psl,
39-
related_origins: RelatedOrigins::Enabled {
40-
source: &related_origins,
41-
max_labels: MaxRegistrableLabels::default(),
38+
origin: OriginValidation::Validate {
39+
public_suffix_list: &psl,
40+
related_origins: RelatedOrigins::Enabled {
41+
source: &related_origins,
42+
max_labels: MaxRegistrableLabels::default(),
43+
},
4244
},
4345
};
4446
let request_json = r#"

libwebauthn/examples/ceremony/webauthn_nfc.rs

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

33
use libwebauthn::ops::webauthn::{
4-
GetAssertionRequest, JsonFormat, MakeCredentialRequest, MaxRegistrableLabels, RelatedOrigins,
5-
RequestOrigin, RequestSettings, ReqwestRelatedOriginsSource, SystemPublicSuffixList,
6-
WebAuthnIDLResponse as _,
4+
GetAssertionRequest, JsonFormat, MakeCredentialRequest, MaxRegistrableLabels, OriginValidation,
5+
RelatedOrigins, RequestOrigin, RequestSettings, ReqwestRelatedOriginsSource,
6+
SystemPublicSuffixList, WebAuthnIDLResponse as _,
77
};
88
use libwebauthn::transport::nfc::{get_nfc_device, is_nfc_available};
99
use libwebauthn::transport::{Channel as _, Device};
@@ -33,10 +33,12 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
3333
);
3434
let related_origins = ReqwestRelatedOriginsSource::new()?;
3535
let settings = RequestSettings {
36-
public_suffix_list: &psl,
37-
related_origins: RelatedOrigins::Enabled {
38-
source: &related_origins,
39-
max_labels: MaxRegistrableLabels::default(),
36+
origin: OriginValidation::Validate {
37+
public_suffix_list: &psl,
38+
related_origins: RelatedOrigins::Enabled {
39+
source: &related_origins,
40+
max_labels: MaxRegistrableLabels::default(),
41+
},
4042
},
4143
};
4244
let make_credentials_request = MakeCredentialRequest::prepare(

libwebauthn/src/lib.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
//!
3333
//! ```no_run
3434
//! use libwebauthn::ops::webauthn::{
35-
//! MakeCredentialRequest, RelatedOrigins, RequestOrigin, RequestSettings,
35+
//! MakeCredentialRequest, OriginValidation, RelatedOrigins, RequestOrigin, RequestSettings,
3636
//! SystemPublicSuffixList,
3737
//! };
3838
//! use libwebauthn::transport::hid::list_devices;
@@ -51,8 +51,10 @@
5151
//! let origin: RequestOrigin = "https://example.org".try_into().expect("invalid origin");
5252
//! let psl = SystemPublicSuffixList::auto().expect("public suffix list unavailable");
5353
//! let settings = RequestSettings {
54-
//! public_suffix_list: &psl,
55-
//! related_origins: RelatedOrigins::Disabled,
54+
//! origin: OriginValidation::Validate {
55+
//! public_suffix_list: &psl,
56+
//! related_origins: RelatedOrigins::Disabled,
57+
//! },
5658
//! };
5759
//! let request_json = r#"{ "rp": { "id": "example.org", "name": "Example" } }"#; // abbreviated
5860
//! let request =

libwebauthn/src/ops/webauthn/get_assertion.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,7 @@ mod tests {
663663
HttpClientError, MaxRegistrableLabels, RelatedOrigins, RelatedOriginsError,
664664
RelatedOriginsSource,
665665
};
666-
use crate::ops::webauthn::{GetAssertionRequest, RequestOrigin};
666+
use crate::ops::webauthn::{GetAssertionRequest, OriginValidation, RequestOrigin};
667667
use crate::proto::ctap2::Ctap2PublicKeyCredentialType;
668668

669669
use super::*;
@@ -715,8 +715,10 @@ mod tests {
715715
origin,
716716
json,
717717
&RequestSettings {
718-
public_suffix_list: psl,
719-
related_origins,
718+
origin: OriginValidation::Validate {
719+
public_suffix_list: psl,
720+
related_origins,
721+
},
720722
},
721723
)
722724
.await
@@ -834,6 +836,22 @@ mod tests {
834836
));
835837
}
836838

839+
#[tokio::test]
840+
async fn origin_trust_accepts_mismatching_rp_id() {
841+
let request_origin: RequestOrigin = "https://app.example.org".parse().unwrap();
842+
let req_json = json_field_add(REQUEST_BASE_JSON, "rpId", r#""example.com""#);
843+
let req = GetAssertionRequest::prepare(
844+
&request_origin,
845+
&req_json,
846+
&RequestSettings {
847+
origin: OriginValidation::Trust,
848+
},
849+
)
850+
.await
851+
.unwrap();
852+
assert_eq!(req.relying_party_id, "example.com");
853+
}
854+
837855
#[tokio::test]
838856
async fn test_request_from_json_rp_id_is_parent_registrable_suffix() {
839857
// origin = login.example.org, rp.id = example.org -> accepted.

libwebauthn/src/ops/webauthn/idl/mod.rs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,23 @@ use super::related_origins::{validate_related_origins, RelatedOrigins};
2626

2727
pub type JsonError = serde_json::Error;
2828

29-
/// Dependencies for origin validation: the Public Suffix List (rp.id suffix
30-
/// check and related-origins matching) and the related-origins policy.
29+
/// Per-request settings (currently just the origin-validation policy).
3130
pub struct RequestSettings<'a> {
32-
pub public_suffix_list: &'a dyn PublicSuffixList,
33-
pub related_origins: RelatedOrigins<'a>,
31+
pub origin: OriginValidation<'a>,
32+
}
33+
34+
/// How the caller origin is validated against the request rp.id.
35+
pub enum OriginValidation<'a> {
36+
/// Trust the caller's origin to rp.id binding with no check, for callers
37+
/// that have already validated it (e.g. a browser). Misuse defeats phishing
38+
/// resistance, so the caller owns that decision.
39+
Trust,
40+
/// Validate rp.id against the caller origin: a registrable suffix of the
41+
/// effective domain, then related origins on mismatch.
42+
Validate {
43+
public_suffix_list: &'a dyn PublicSuffixList,
44+
related_origins: RelatedOrigins<'a>,
45+
},
3446
}
3547

3648
/// Builds a request from its parsed IDL model, validating origin against rp.id.
@@ -48,19 +60,26 @@ where
4860
) -> Result<Self, Self::Error>;
4961
}
5062

51-
/// Whether `request_origin` may act for `rp_id`: a registrable suffix of the
52-
/// caller's effective domain, or a matching related origin when enabled.
63+
/// Whether `request_origin` may act for `rp_id`. `Trust` accepts any rp.id;
64+
/// `Validate` requires a registrable suffix of the caller's effective domain or
65+
/// a matching related origin.
5366
pub(crate) async fn rp_id_authorised(
5467
request_origin: &RequestOrigin,
5568
rp_id: &RelyingPartyId,
5669
settings: &RequestSettings<'_>,
5770
) -> bool {
71+
let (public_suffix_list, related_origins) = match &settings.origin {
72+
OriginValidation::Trust => return true,
73+
OriginValidation::Validate {
74+
public_suffix_list,
75+
related_origins,
76+
} => (*public_suffix_list, related_origins),
77+
};
5878
let effective_rp_id = request_origin.origin.host.as_str();
59-
if is_registrable_domain_suffix_or_equal(&rp_id.0, effective_rp_id, settings.public_suffix_list)
60-
{
79+
if is_registrable_domain_suffix_or_equal(&rp_id.0, effective_rp_id, public_suffix_list) {
6180
return true;
6281
}
63-
match &settings.related_origins {
82+
match related_origins {
6483
RelatedOrigins::Disabled => false,
6584
RelatedOrigins::Enabled { source, max_labels } => {
6685
match source.allowed_origins(rp_id).await {
@@ -71,7 +90,7 @@ pub(crate) async fn rp_id_authorised(
7190
Ok(origins) => match validate_related_origins(
7291
&request_origin.origin,
7392
&origins,
74-
settings.public_suffix_list,
93+
public_suffix_list,
7594
*max_labels,
7695
) {
7796
Ok(()) => true,

libwebauthn/src/ops/webauthn/make_credential.rs

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -702,7 +702,7 @@ mod tests {
702702
HttpClientError, MaxRegistrableLabels, RelatedOrigins, RelatedOriginsError,
703703
RelatedOriginsSource,
704704
};
705-
use crate::ops::webauthn::{MakeCredentialRequest, RequestOrigin};
705+
use crate::ops::webauthn::{MakeCredentialRequest, OriginValidation, RequestOrigin};
706706
use crate::proto::ctap2::Ctap2PublicKeyCredentialType;
707707

708708
use super::*;
@@ -754,8 +754,10 @@ mod tests {
754754
origin,
755755
json,
756756
&RequestSettings {
757-
public_suffix_list: psl,
758-
related_origins,
757+
origin: OriginValidation::Validate {
758+
public_suffix_list: psl,
759+
related_origins,
760+
},
759761
},
760762
)
761763
.await
@@ -1095,6 +1097,48 @@ mod tests {
10951097
));
10961098
}
10971099

1100+
#[tokio::test]
1101+
async fn origin_trust_accepts_mismatching_rp_id() {
1102+
let request_origin: RequestOrigin = "https://app.example.org".parse().unwrap();
1103+
let req_json = json_field_add(
1104+
REQUEST_BASE_JSON,
1105+
"rp",
1106+
r#"{"id": "example.com", "name": "example.com"}"#,
1107+
);
1108+
let req = MakeCredentialRequest::prepare(
1109+
&request_origin,
1110+
&req_json,
1111+
&RequestSettings {
1112+
origin: OriginValidation::Trust,
1113+
},
1114+
)
1115+
.await
1116+
.unwrap();
1117+
assert_eq!(req.relying_party.id, "example.com");
1118+
}
1119+
1120+
#[tokio::test]
1121+
async fn origin_trust_still_rejects_invalid_rp_id() {
1122+
let request_origin: RequestOrigin = "https://example.org".parse().unwrap();
1123+
let req_json = json_field_add(
1124+
REQUEST_BASE_JSON,
1125+
"rp",
1126+
r#"{"id": "example.org.", "name": "example.org"}"#,
1127+
);
1128+
let result = MakeCredentialRequest::prepare(
1129+
&request_origin,
1130+
&req_json,
1131+
&RequestSettings {
1132+
origin: OriginValidation::Trust,
1133+
},
1134+
)
1135+
.await;
1136+
assert!(matches!(
1137+
result,
1138+
Err(MakeCredentialPrepareError::InvalidRelyingPartyId(_))
1139+
));
1140+
}
1141+
10981142
#[tokio::test]
10991143
async fn test_request_from_json_mismatching_rp_id() {
11001144
let request_origin: RequestOrigin = "https://example.org".parse().unwrap();

libwebauthn/src/ops/webauthn/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ pub use idl::{
2121
rpid::RelyingPartyId,
2222
AuthenticationExtensionsClientOutputsJSON, AuthenticationResponseJSON,
2323
AuthenticatorAssertionResponseJSON, AuthenticatorAttestationResponseJSON, Base64UrlString,
24-
JsonFormat, RegistrationResponseJSON, RequestSettings, ResponseSerializationError,
25-
WebAuthnIDLResponse,
24+
JsonFormat, OriginValidation, RegistrationResponseJSON, RequestSettings,
25+
ResponseSerializationError, WebAuthnIDLResponse,
2626
};
2727
pub use make_credential::{
2828
CredentialPropsExtension, CredentialProtectionExtension, CredentialProtectionPolicy,

0 commit comments

Comments
 (0)