Skip to content

Commit c3f8597

Browse files
fix(webauthn): default rp.id to effective domain and reject ip-literal rp ids (#285)
Registration now defaults a missing RP ID to the caller effective domain, matching the assertion ceremony and the spec, instead of failing. Origins whose host is an IP address are rejected rather than used as an RP ID. This removes an inconsistency between the create and get paths and closes a malformed RP ID case.
1 parent ac426c2 commit c3f8597

3 files changed

Lines changed: 105 additions & 6 deletions

File tree

libwebauthn/src/ops/webauthn/get_assertion.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,9 @@ impl FromIdlModel<PublicKeyCredentialRequestOptionsJSON> for GetAssertionRequest
192192
}
193193
parsed.0
194194
} else {
195-
effective_rp_id.to_string()
195+
RelyingPartyId::try_from(effective_rp_id)
196+
.map_err(|err| GetAssertionPrepareError::InvalidRelyingPartyId(err.to_string()))?
197+
.0
196198
};
197199

198200
let prf = match inner.extensions.as_ref() {
@@ -906,6 +908,42 @@ mod tests {
906908
));
907909
}
908910

911+
#[tokio::test]
912+
async fn test_request_from_json_rejects_ip_effective_domain() {
913+
let request_origin: RequestOrigin = "https://127.0.0.1:8443".parse().unwrap();
914+
let req_json = json_field_rm(REQUEST_BASE_JSON, "rpId");
915+
916+
let result = from_json(
917+
&request_origin,
918+
&MockPublicSuffixList,
919+
RelatedOrigins::Disabled,
920+
&req_json,
921+
)
922+
.await;
923+
assert!(matches!(
924+
result,
925+
Err(GetAssertionPrepareError::InvalidRelyingPartyId(_))
926+
));
927+
}
928+
929+
#[tokio::test]
930+
async fn test_request_from_json_rejects_ipv6_effective_domain() {
931+
let request_origin: RequestOrigin = "https://[::1]:8443".parse().unwrap();
932+
let req_json = json_field_rm(REQUEST_BASE_JSON, "rpId");
933+
934+
let result = from_json(
935+
&request_origin,
936+
&MockPublicSuffixList,
937+
RelatedOrigins::Disabled,
938+
&req_json,
939+
)
940+
.await;
941+
assert!(matches!(
942+
result,
943+
Err(GetAssertionPrepareError::InvalidRelyingPartyId(_))
944+
));
945+
}
946+
909947
#[tokio::test]
910948
async fn test_request_from_json_mismatching_rp_id() {
911949
let request_origin: RequestOrigin = "https://example.org".parse().unwrap();

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::{
44
ops::webauthn::{
55
MakeCredentialsRequestExtensions, ResidentKeyRequirement, UserVerificationRequirement,
66
},
7-
proto::ctap2::{Ctap2CredentialType, Ctap2PublicKeyCredentialRpEntity},
7+
proto::ctap2::Ctap2CredentialType,
88
};
99

1010
use serde::Deserialize;
@@ -28,6 +28,13 @@ fn default_user_verification() -> UserVerificationRequirement {
2828
UserVerificationRequirement::Preferred
2929
}
3030

31+
#[derive(Debug, Clone, Deserialize)]
32+
#[serde(rename_all = "camelCase")]
33+
pub struct PublicKeyCredentialRpEntityJSON {
34+
pub id: Option<String>,
35+
pub name: Option<String>,
36+
}
37+
3138
#[derive(Debug, Clone, PartialEq, Deserialize)]
3239
#[serde(rename_all = "camelCase")]
3340
pub struct PublicKeyCredentialUserEntity {
@@ -39,7 +46,7 @@ pub struct PublicKeyCredentialUserEntity {
3946
#[derive(Debug, Clone, Deserialize)]
4047
#[serde(rename_all = "camelCase")]
4148
pub struct PublicKeyCredentialCreationOptionsJSON {
42-
pub rp: Ctap2PublicKeyCredentialRpEntity,
49+
pub rp: PublicKeyCredentialRpEntityJSON,
4350
pub user: PublicKeyCredentialUserEntity,
4451
pub challenge: Base64UrlString,
4552
#[serde(rename = "pubKeyCredParams")]

libwebauthn/src/ops/webauthn/make_credential.rs

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -409,16 +409,18 @@ impl FromIdlModel<PublicKeyCredentialCreationOptionsJSON> for MakeCredentialRequ
409409
inner: PublicKeyCredentialCreationOptionsJSON,
410410
) -> Result<Self, MakeCredentialPrepareError> {
411411
let effective_rp_id = request_origin.origin.host.as_str();
412-
let rp_id = RelyingPartyId::try_from(inner.rp.id.as_str())
412+
let rp_id = RelyingPartyId::try_from(inner.rp.id.as_deref().unwrap_or(effective_rp_id))
413413
.map_err(|err| MakeCredentialPrepareError::InvalidRelyingPartyId(err.to_string()))?;
414414
if !rp_id_authorised(request_origin, &rp_id, settings).await {
415415
return Err(MakeCredentialPrepareError::MismatchingRelyingPartyId(
416416
rp_id.0,
417417
effective_rp_id.to_string(),
418418
));
419419
}
420-
let mut relying_party = inner.rp;
421-
relying_party.id = rp_id.0;
420+
let relying_party = Ctap2PublicKeyCredentialRpEntity {
421+
id: rp_id.0,
422+
name: inner.rp.name,
423+
};
422424
let resident_key = if inner
423425
.authenticator_selection
424426
.as_ref()
@@ -1138,6 +1140,58 @@ mod tests {
11381140
));
11391141
}
11401142

1143+
#[tokio::test]
1144+
async fn test_request_from_json_rp_id_defaults_to_effective_domain() {
1145+
let request_origin: RequestOrigin = "https://example.org".parse().unwrap();
1146+
let req_json = json_field_add(REQUEST_BASE_JSON, "rp", r#"{"name": "example.org"}"#);
1147+
1148+
let req = from_json(
1149+
&request_origin,
1150+
&MockPublicSuffixList,
1151+
RelatedOrigins::Disabled,
1152+
&req_json,
1153+
)
1154+
.await
1155+
.unwrap();
1156+
assert_eq!(req.relying_party.id, "example.org");
1157+
}
1158+
1159+
#[tokio::test]
1160+
async fn test_request_from_json_rejects_ipv4_effective_domain() {
1161+
let request_origin: RequestOrigin = "https://127.0.0.1:8443".parse().unwrap();
1162+
let req_json = json_field_add(REQUEST_BASE_JSON, "rp", r#"{"name": "example.org"}"#);
1163+
1164+
let result = from_json(
1165+
&request_origin,
1166+
&MockPublicSuffixList,
1167+
RelatedOrigins::Disabled,
1168+
&req_json,
1169+
)
1170+
.await;
1171+
assert!(matches!(
1172+
result,
1173+
Err(MakeCredentialPrepareError::InvalidRelyingPartyId(_))
1174+
));
1175+
}
1176+
1177+
#[tokio::test]
1178+
async fn test_request_from_json_rejects_ipv6_effective_domain() {
1179+
let request_origin: RequestOrigin = "https://[::1]:8443".parse().unwrap();
1180+
let req_json = json_field_add(REQUEST_BASE_JSON, "rp", r#"{"name": "example.org"}"#);
1181+
1182+
let result = from_json(
1183+
&request_origin,
1184+
&MockPublicSuffixList,
1185+
RelatedOrigins::Disabled,
1186+
&req_json,
1187+
)
1188+
.await;
1189+
assert!(matches!(
1190+
result,
1191+
Err(MakeCredentialPrepareError::InvalidRelyingPartyId(_))
1192+
));
1193+
}
1194+
11411195
#[tokio::test]
11421196
async fn origin_trust_accepts_mismatching_rp_id() {
11431197
let request_origin: RequestOrigin = "https://app.example.org".parse().unwrap();

0 commit comments

Comments
 (0)