Skip to content

Commit 2fbdf14

Browse files
Derive rpId from NavigationContext in gateway
1 parent 9bb1cab commit 2fbdf14

3 files changed

Lines changed: 112 additions & 127 deletions

File tree

credentialsd/src/gateway/mod.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ impl GatewayService {
8585
context: RequestContext,
8686
parent_window: Option<WindowHandle>,
8787
) -> Result<CreateCredentialResponse, WebAuthnError> {
88-
let _request_environment = validate_request(&context)?;
88+
let request_environment = validate_request(&context)?;
8989

9090
if let ("publicKey", Some(_)) = (request.r#type.as_ref(), &request.public_key) {
9191
// TODO: assert that RP ID is bound to origin:
@@ -95,7 +95,7 @@ impl GatewayService {
9595
// - query for related origins, if supported
9696
// - fail if not supported, or if RP ID doesn't match any related origins.
9797
let (make_cred_request, client_data_json) =
98-
create_credential_request_try_into_ctap2(&request)
98+
create_credential_request_try_into_ctap2(&request, &request_environment)
9999
.inspect_err(|_| {
100100
tracing::error!(
101101
"Could not parse passkey creation request. Rejecting request."
@@ -143,7 +143,7 @@ impl GatewayService {
143143
context: RequestContext,
144144
parent_window: Option<WindowHandle>,
145145
) -> Result<GetCredentialResponse, WebAuthnError> {
146-
let _request_environment = validate_request(&context)?;
146+
let request_environment = validate_request(&context)?;
147147

148148
if request.public_key.is_some() {
149149
// Setup request
@@ -155,10 +155,12 @@ impl GatewayService {
155155
// - query for related origins, if supported
156156
// - fail if not supported, or if RP ID doesn't match any related origins.
157157
let (get_cred_request, client_data_json) =
158-
get_credential_request_try_into_ctap2(&request).map_err(|e| {
159-
tracing::error!("Could not parse passkey assertion request: {e:?}");
160-
WebAuthnError::TypeError
161-
})?;
158+
get_credential_request_try_into_ctap2(&request, &request_environment).map_err(
159+
|e| {
160+
tracing::error!("Could not parse passkey assertion request: {e:?}");
161+
WebAuthnError::TypeError
162+
},
163+
)?;
162164
let cred_request = CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request);
163165

164166
let response = self

credentialsd/src/gateway/util.rs

Lines changed: 86 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -13,87 +13,91 @@ use credentialsd_common::{
1313
use crate::model::{GetAssertionResponseInternal, MakeCredentialResponseInternal};
1414
use crate::webauthn::{
1515
self, GetAssertionRequest, GetPublicKeyCredentialUnsignedExtensionsResponse,
16-
MakeCredentialRequest, RelyingPartyId, WebAuthnIDL,
16+
MakeCredentialRequest, NavigationContext, Origin, RelyingPartyId, WebAuthnIDL,
1717
};
1818

19-
/// Parses a WebAuthn create credential request from D-Bus into a CTAP2 MakeCredentialRequest.
20-
///
21-
/// Uses libwebauthn's `WebAuthnIDL::from_json()` for parsing, which handles:
22-
/// - Challenge decoding from base64url
23-
/// - User entity parsing with base64url-encoded user ID
24-
/// - Relying party entity parsing
25-
/// - Extension parsing (credProps, credBlob, largeBlobSupport, prf, etc.)
26-
/// - Authenticator selection criteria (residentKey, userVerification)
27-
/// - Excluded credentials list
28-
/// - Public key credential parameters
19+
/// Reads the rpId from a create-credential request JSON (`rp.id`).
2920
///
30-
/// Returns the parsed request and the client data JSON (needed for response serialization).
31-
pub(super) fn create_credential_request_try_into_ctap2(
32-
request: &CreateCredentialRequest,
33-
) -> std::result::Result<(MakeCredentialRequest, String), WebAuthnError> {
34-
if request.public_key.is_none() {
35-
return Err(WebAuthnError::NotSupportedError);
36-
}
37-
let options = request.public_key.as_ref().ok_or_else(|| {
38-
tracing::info!("Invalid request: missing public_key");
21+
/// Used as a fallback when the origin is an AppId and the effective domain
22+
/// cannot be derived from the origin alone.
23+
// TODO(libwebauthn#185)
24+
fn peek_make_credential_rp_id(request_json: &str) -> Result<RelyingPartyId, WebAuthnError> {
25+
let value = serde_json::from_str::<serde_json::Value>(request_json).map_err(|err| {
26+
tracing::info!("Invalid request JSON: {err}");
3927
WebAuthnError::TypeError
4028
})?;
41-
42-
// Get origin and determine relying party ID
43-
let (origin, _is_cross_origin) =
44-
match (request.origin.as_ref(), request.is_same_origin.as_ref()) {
45-
(Some(origin), Some(is_same_origin)) => (origin.to_string(), !is_same_origin),
46-
(Some(origin), None) => (origin.to_string(), true),
47-
(None, _) => {
48-
tracing::info!("Error reading origin from request.");
49-
return Err(WebAuthnError::TypeError);
50-
}
51-
};
52-
53-
// Extract rpId from JSON for RelyingPartyId construction
54-
// libwebauthn validates that the rpId in the request matches this
55-
let request_value =
56-
serde_json::from_str::<serde_json::Value>(&options.request_json).map_err(|err| {
57-
tracing::info!("Invalid request JSON: {err}");
58-
WebAuthnError::TypeError
59-
})?;
60-
let json = request_value.as_object().ok_or_else(|| {
61-
tracing::info!("Invalid request JSON: not an object");
62-
WebAuthnError::TypeError
63-
})?;
64-
65-
// Get rpId from the request, or derive from origin
66-
let rp_id_str = json
29+
let rp_id_str = value
6730
.get("rp")
6831
.and_then(|rp| rp.get("id"))
6932
.and_then(|id| id.as_str())
70-
.map(|s| s.to_string())
71-
.unwrap_or_else(|| {
72-
// Default to effective domain from origin
73-
origin
74-
.strip_prefix("https://")
75-
.map(|rest| rest.split_once('/').map(|(d, _)| d).unwrap_or(rest))
76-
.unwrap_or(&origin)
77-
.to_string()
78-
});
33+
.ok_or_else(|| {
34+
tracing::info!("RP ID required if using app ID as origin");
35+
WebAuthnError::SecurityError
36+
})?;
37+
RelyingPartyId::try_from(rp_id_str).map_err(|_| {
38+
tracing::info!("Invalid relying party ID");
39+
WebAuthnError::TypeError
40+
})
41+
}
7942

80-
let rp_id = RelyingPartyId::try_from(rp_id_str.as_str()).map_err(|_| {
43+
/// Reads the rpId from a get-credential request JSON (`rpId`).
44+
///
45+
/// Used as a fallback when the origin is an AppId and the effective domain
46+
/// cannot be derived from the origin alone.
47+
// TODO(libwebauthn#185)
48+
fn peek_get_assertion_rp_id(request_json: &str) -> Result<RelyingPartyId, WebAuthnError> {
49+
let value = serde_json::from_str::<serde_json::Value>(request_json).map_err(|err| {
50+
tracing::info!("Invalid request JSON: {err}");
51+
WebAuthnError::TypeError
52+
})?;
53+
let rp_id_str = value
54+
.get("rpId")
55+
.and_then(|id| id.as_str())
56+
.ok_or_else(|| {
57+
tracing::info!("RP ID required if using app ID as origin");
58+
WebAuthnError::SecurityError
59+
})?;
60+
RelyingPartyId::try_from(rp_id_str).map_err(|_| {
8161
tracing::info!("Invalid relying party ID");
8262
WebAuthnError::TypeError
63+
})
64+
}
65+
66+
/// Parses a WebAuthn create credential request from D-Bus into a CTAP2 MakeCredentialRequest.
67+
///
68+
/// Uses libwebauthn's `WebAuthnIDL::from_json()` for parsing. The relying party ID is derived
69+
/// from the request's origin; libwebauthn validates that any rpId in the JSON matches it.
70+
pub(super) fn create_credential_request_try_into_ctap2(
71+
request: &CreateCredentialRequest,
72+
request_environment: &NavigationContext,
73+
) -> std::result::Result<(MakeCredentialRequest, String), WebAuthnError> {
74+
let options = request.public_key.as_ref().ok_or_else(|| {
75+
tracing::info!("Invalid request: missing public_key");
76+
WebAuthnError::NotSupportedError
8377
})?;
8478

85-
// Use libwebauthn's JSON parsing
79+
let origin = request_environment.origin();
80+
let rp_id = match origin {
81+
Origin::Https { .. } => RelyingPartyId::try_from(origin).map_err(|err| {
82+
tracing::info!("Cannot derive relying party ID from origin: {err}");
83+
WebAuthnError::SecurityError
84+
})?,
85+
Origin::AppId(_) => peek_make_credential_rp_id(&options.request_json)?,
86+
};
87+
8688
let mut make_cred_request = MakeCredentialRequest::from_json(&rp_id, &options.request_json)
8789
.map_err(|err| {
8890
tracing::info!("Failed to parse MakeCredential request JSON: {err}");
8991
WebAuthnError::TypeError
9092
})?;
9193

92-
// Set origin and cross_origin from D-Bus request context
93-
make_cred_request.origin = origin;
94-
make_cred_request.cross_origin = request.is_same_origin.as_ref().map(|same| !same);
94+
// TODO(libwebauthn#185)
95+
make_cred_request.origin = origin.to_string();
96+
make_cred_request.cross_origin = Some(matches!(
97+
request_environment,
98+
NavigationContext::CrossOrigin(_)
99+
));
95100

96-
// Get the client data JSON from the request for response serialization
97101
let client_data_json = make_cred_request.client_data_json();
98102

99103
Ok((make_cred_request, client_data_json))
@@ -143,77 +147,39 @@ pub(super) fn create_credential_response_try_from_ctap2(
143147

144148
/// Parses a WebAuthn get credential request from D-Bus into a CTAP2 GetAssertionRequest.
145149
///
146-
/// Uses libwebauthn's `WebAuthnIDL::from_json()` for parsing, which handles:
147-
/// - Challenge decoding from base64url
148-
/// - Allowed credentials list with transports
149-
/// - Extension parsing (getCredBlob, largeBlob, prf, hmac-secret)
150-
/// - User verification requirement
151-
///
152-
/// Returns the parsed request and the client data JSON (needed for response serialization).
150+
/// Uses libwebauthn's `WebAuthnIDL::from_json()` for parsing. The relying party ID is derived
151+
/// from the request's origin; libwebauthn validates that any rpId in the JSON matches it.
153152
pub(super) fn get_credential_request_try_into_ctap2(
154153
request: &GetCredentialRequest,
154+
request_environment: &NavigationContext,
155155
) -> std::result::Result<(GetAssertionRequest, String), WebAuthnError> {
156-
if request.public_key.is_none() {
157-
return Err(WebAuthnError::NotSupportedError);
158-
}
159156
let options = request.public_key.as_ref().ok_or_else(|| {
160157
tracing::info!("Invalid request: no \"publicKey\" options specified.");
161-
WebAuthnError::TypeError
158+
WebAuthnError::NotSupportedError
162159
})?;
163160

164-
// Get origin
165-
let (origin, _is_cross_origin) =
166-
match (request.origin.as_ref(), request.is_same_origin.as_ref()) {
167-
(Some(origin), Some(is_same_origin)) => (origin.to_string(), !is_same_origin),
168-
(Some(origin), None) => (origin.to_string(), true),
169-
(None, _) => {
170-
tracing::info!("Error reading origin from client request.");
171-
return Err(WebAuthnError::TypeError);
172-
}
173-
};
174-
175-
// Extract rpId from JSON for RelyingPartyId construction
176-
let request_value =
177-
serde_json::from_str::<serde_json::Value>(&options.request_json).map_err(|err| {
178-
tracing::info!("Invalid request JSON: {err}");
179-
WebAuthnError::TypeError
180-
})?;
181-
let json = request_value.as_object().ok_or_else(|| {
182-
tracing::info!("Invalid request JSON: not an object");
183-
WebAuthnError::TypeError
184-
})?;
185-
186-
// Get rpId from the request, or derive from origin
187-
let rp_id_str = json
188-
.get("rpId")
189-
.and_then(|id| id.as_str())
190-
.map(|s| s.to_string())
191-
.unwrap_or_else(|| {
192-
// Default to effective domain from origin
193-
origin
194-
.strip_prefix("https://")
195-
.map(|rest| rest.split_once('/').map(|(d, _)| d).unwrap_or(rest))
196-
.unwrap_or(&origin)
197-
.to_string()
198-
});
199-
200-
let rp_id = RelyingPartyId::try_from(rp_id_str.as_str()).map_err(|_| {
201-
tracing::info!("Invalid relying party ID");
202-
WebAuthnError::TypeError
203-
})?;
161+
let origin = request_environment.origin();
162+
let rp_id = match origin {
163+
Origin::Https { .. } => RelyingPartyId::try_from(origin).map_err(|err| {
164+
tracing::info!("Cannot derive relying party ID from origin: {err}");
165+
WebAuthnError::SecurityError
166+
})?,
167+
Origin::AppId(_) => peek_get_assertion_rp_id(&options.request_json)?,
168+
};
204169

205-
// Use libwebauthn's JSON parsing
206170
let mut get_assertion_request = GetAssertionRequest::from_json(&rp_id, &options.request_json)
207171
.map_err(|err| {
208-
tracing::info!("Failed to parse GetAssertion request JSON: {err}");
209-
WebAuthnError::TypeError
210-
})?;
172+
tracing::info!("Failed to parse GetAssertion request JSON: {err}");
173+
WebAuthnError::TypeError
174+
})?;
211175

212-
// Set origin and cross_origin from D-Bus request context
213-
get_assertion_request.origin = origin;
214-
get_assertion_request.cross_origin = request.is_same_origin.as_ref().map(|same| !same);
176+
// TODO(libwebauthn#185)
177+
get_assertion_request.origin = origin.to_string();
178+
get_assertion_request.cross_origin = Some(matches!(
179+
request_environment,
180+
NavigationContext::CrossOrigin(_)
181+
));
215182

216-
// Get the client data JSON from the request for response serialization
217183
let client_data_json = get_assertion_request.client_data_json();
218184

219185
Ok((get_assertion_request, client_data_json))

credentialsd/src/webauthn.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,23 @@ impl Display for Origin {
510510
}
511511
}
512512

513+
impl TryFrom<&Origin> for RelyingPartyId {
514+
type Error = OriginParseError;
515+
516+
/// Derives the relying party ID (effective domain) from an origin.
517+
///
518+
/// AppId origins have no effective domain and must be mapped to an rpId
519+
/// out-of-band, so this conversion fails for them.
520+
fn try_from(origin: &Origin) -> Result<Self, Self::Error> {
521+
match origin {
522+
Origin::Https { host, .. } => {
523+
RelyingPartyId::try_from(host.as_str()).map_err(|_| OriginParseError::InvalidHost)
524+
}
525+
Origin::AppId(_) => Err(OriginParseError::InvalidScheme),
526+
}
527+
}
528+
}
529+
513530
impl FromStr for Origin {
514531
type Err = OriginParseError;
515532

0 commit comments

Comments
 (0)