Skip to content

Commit f40fa6a

Browse files
authored
Merge pull request #133 from linux-credentials/iinuwa/stricter-origin-parsing
Stricter origin parsing and validation
2 parents 72de3e6 + bd8d501 commit f40fa6a

File tree

4 files changed

+392
-173
lines changed

4 files changed

+392
-173
lines changed

credentialsd/src/credential_service/mod.rs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ mod test {
392392
credential_service::usb::InProcessUsbHandler,
393393
dbus::test::{DummyFlowServer, DummyUiServer},
394394
model::CredentialRequest,
395-
webauthn,
395+
webauthn::{self, NavigationContext},
396396
};
397397
use credentialsd_common::model::Operation;
398398

@@ -452,18 +452,13 @@ mod test {
452452

453453
fn create_credential_request() -> CredentialRequest {
454454
let challenge = "Ox0AXQz7WUER7BGQFzvVrQbReTkS3sepVGj26qfUhhrWSarkDbGF4T4NuCY1aAwHYzOzKMJJ2YRSatetl0D9bQ";
455-
let origin = "webauthn.io".to_string();
456-
let is_cross_origin = false;
457-
let client_data_json = webauthn::format_client_data_json(
458-
Operation::Create,
459-
challenge,
460-
&origin,
461-
is_cross_origin,
462-
);
455+
let origin = NavigationContext::SameOrigin("https://webauthn.io".parse().unwrap());
456+
let client_data_json =
457+
webauthn::format_client_data_json(Operation::Create, challenge, &origin);
463458
let client_data_hash = webauthn::create_client_data_hash(&client_data_json);
464459
let make_request = MakeCredentialRequest {
465460
hash: client_data_hash,
466-
origin: "webauthn.io".to_string(),
461+
origin: "https://webauthn.io".to_string(),
467462
relying_party: Ctap2PublicKeyCredentialRpEntity {
468463
id: "webauthn.io".to_string(),
469464
name: Some("webauthn.io".to_string()),

credentialsd/src/dbus/gateway.rs

Lines changed: 125 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use crate::{
2626
CredentialRequestController,
2727
},
2828
model::{CredentialRequest, CredentialResponse},
29-
webauthn::Origin,
29+
webauthn::{AppId, NavigationContext, Origin},
3030
};
3131

3232
pub const SERVICE_NAME: &str = "xyz.iinuwa.credentialsd.Credentials";
@@ -224,26 +224,46 @@ impl<C: CredentialRequestController + Send + Sync + 'static> CredentialGateway<C
224224
parent_window: Optional<WindowHandle>,
225225
request: CreateCredentialRequest,
226226
) -> Result<CreateCredentialResponse, Error> {
227-
let (_origin, is_same_origin, _top_origin) =
228-
check_origin(request.origin.as_deref(), request.is_same_origin)
229-
.await
230-
.map_err(Error::from)?;
227+
// TODO: Add authorization check for privileged client.
228+
let top_origin = if request.is_same_origin.unwrap_or_default() {
229+
None
230+
} else {
231+
// TODO: Once we modify the models to convey the top-origin in cross origin requests to the UI, we can remove this error message.
232+
// We should still reject cross-origin requests for conditionally-mediated requests.
233+
tracing::warn!("Client attempted to issue cross-origin request for credentials, which are not supported by this platform.");
234+
return Err(WebAuthnError::NotAllowedError.into());
235+
};
236+
let Some(origin) = request
237+
.origin
238+
.as_ref()
239+
.map(|o| {
240+
o.parse::<Origin>().map_err(|_| {
241+
tracing::warn!("Invalid origin specified: {:?}", request.origin);
242+
Error::SecurityError
243+
})
244+
})
245+
.transpose()?
246+
else {
247+
tracing::warn!(
248+
"Caller requested implicit origin, which is not yet implemented. Rejecting request."
249+
);
250+
return Err(Error::SecurityError);
251+
};
252+
let request_environment = check_origin_from_privileged_client(origin, top_origin)?;
231253
if let ("publicKey", Some(_)) = (request.r#type.as_ref(), &request.public_key) {
232-
if !is_same_origin {
233-
// TODO: Once we modify the models to convey the top-origin in cross origin requests to the UI, we can remove this error message.
234-
// We should still reject cross-origin requests for conditionally-mediated requests.
235-
tracing::warn!("Client attempted to issue cross-origin request for credentials, which are not supported by this platform.");
236-
return Err(WebAuthnError::NotAllowedError.into());
237-
}
254+
// TODO: assert that RP ID is bound to origin:
255+
// - if RP ID is not set, set the RP ID to the origin's effective domain
256+
// - if RP ID is set, assert that it matches origin's effective domain
257+
// - if RP ID is set, but origin's effective domain doesn't match
258+
// - query for related origins, if supported
259+
// - fail if not supported, or if RP ID doesn't match any related origins.
238260
let (make_cred_request, client_data_json) =
239-
create_credential_request_try_into_ctap2(&request).map_err(|e| {
240-
if let WebAuthnError::TypeError = e {
261+
create_credential_request_try_into_ctap2(&request, &request_environment)
262+
.inspect_err(|_| {
241263
tracing::error!(
242264
"Could not parse passkey creation request. Rejecting request."
243265
);
244-
}
245-
e
246-
})?;
266+
})?;
247267
if make_cred_request.algorithms.is_empty() {
248268
tracing::info!("No supported algorithms given in request. Rejecting request.");
249269
return Err(Error::NotSupportedError);
@@ -291,16 +311,33 @@ impl<C: CredentialRequestController + Send + Sync + 'static> CredentialGateway<C
291311
parent_window: Optional<WindowHandle>,
292312
request: GetCredentialRequest,
293313
) -> Result<GetCredentialResponse, Error> {
294-
let (_origin, is_same_origin, _top_origin) =
295-
check_origin(request.origin.as_deref(), request.is_same_origin)
296-
.await
297-
.map_err(Error::from)?;
314+
// TODO: Add authorization check for privileged client.
315+
let top_origin = if request.is_same_origin.unwrap_or_default() {
316+
None
317+
} else {
318+
// TODO: Once we modify the models to convey the top-origin in cross origin requests to the UI, we can remove this error message.
319+
// We should still reject cross-origin requests for conditionally-mediated requests.
320+
tracing::warn!("Client attempted to issue cross-origin request for credentials, which are not supported by this platform.");
321+
return Err(WebAuthnError::NotAllowedError.into());
322+
};
323+
let Some(origin) = request
324+
.origin
325+
.as_ref()
326+
.map(|o| {
327+
o.parse::<Origin>().map_err(|_| {
328+
tracing::warn!("Invalid origin specified: {:?}", request.origin);
329+
Error::SecurityError
330+
})
331+
})
332+
.transpose()?
333+
else {
334+
tracing::warn!(
335+
"Caller requested implicit origin, which is not yet implemented. Rejecting request."
336+
);
337+
return Err(Error::SecurityError);
338+
};
339+
let request_env = check_origin_from_privileged_client(origin, top_origin)?;
298340
if let ("publicKey", Some(_)) = (request.r#type.as_ref(), &request.public_key) {
299-
if !is_same_origin {
300-
// TODO: Once we modify the models to convey the top-origin in cross origin requests to the UI, we can remove this error message.
301-
tracing::warn!("Client attempted to issue cross-origin request for credentials, which are not supported by this platform.");
302-
return Err(WebAuthnError::NotAllowedError.into());
303-
}
304341
// Setup request
305342

306343
// TODO: assert that RP ID is bound to origin:
@@ -310,7 +347,7 @@ impl<C: CredentialRequestController + Send + Sync + 'static> CredentialGateway<C
310347
// - query for related origins, if supported
311348
// - fail if not supported, or if RP ID doesn't match any related origins.
312349
let (get_cred_request, client_data_json) =
313-
get_credential_request_try_into_ctap2(&request).map_err(|e| {
350+
get_credential_request_try_into_ctap2(&request, &request_env).map_err(|e| {
314351
tracing::error!("Could not parse passkey assertion request: {e:?}");
315352
WebAuthnError::TypeError
316353
})?;
@@ -371,7 +408,7 @@ async fn validate_app_details(
371408
claimed_app_display_name: Option<String>,
372409
claimed_origin: Option<String>,
373410
claimed_top_origin: Option<String>,
374-
) -> Result<(RequestingApplication, Origin), Error> {
411+
) -> Result<(RequestingApplication, NavigationContext), Error> {
375412
let Some(unique_name) = header.sender() else {
376413
return Err(Error::SecurityError);
377414
};
@@ -385,21 +422,40 @@ async fn validate_app_details(
385422
return Err(Error::SecurityError);
386423
}
387424
// Now we can trust these app detail parameters.
388-
let app_id = format!("app:{claimed_app_id}");
425+
let Ok(app_id) = claimed_app_id.parse::<AppId>() else {
426+
tracing::warn!("Invalid app ID passed: {claimed_app_id}");
427+
return Err(Error::SecurityError);
428+
};
389429
let display_name = claimed_app_display_name.unwrap_or_default();
390430

391431
// Verify that the origin is valid for the given app ID.
392-
let origin = check_origin_from_app(
393-
&app_id,
394-
claimed_origin.as_deref(),
395-
claimed_top_origin.as_deref(),
396-
)?;
432+
let claimed_origin = claimed_origin
433+
.map(|o| {
434+
o.parse().map_err(|_| {
435+
tracing::warn!("Invalid origin passed: {o}");
436+
Error::SecurityError
437+
})
438+
})
439+
.transpose()?;
440+
let request_env = if let Some(claimed_origin) = claimed_origin {
441+
let claimed_top_origin = claimed_top_origin
442+
.map(|o| {
443+
o.parse().map_err(|_| {
444+
tracing::warn!("Invalid origin passed: {o}");
445+
Error::SecurityError
446+
})
447+
})
448+
.transpose()?;
449+
check_origin_from_app(&app_id, claimed_origin, claimed_top_origin)?
450+
} else {
451+
NavigationContext::SameOrigin(Origin::AppId(app_id))
452+
};
397453
let app_details = RequestingApplication {
398454
name: display_name,
399-
path: app_id,
455+
path: claimed_app_id,
400456
pid,
401457
};
402-
Ok((app_details, origin))
458+
Ok((app_details, request_env))
403459
}
404460

405461
async fn should_trust_app_id(pid: u32) -> bool {
@@ -449,10 +505,10 @@ async fn should_trust_app_id(pid: u32) -> bool {
449505
}
450506

451507
fn check_origin_from_app<'a>(
452-
app_id: &str,
453-
origin: Option<&str>,
454-
top_origin: Option<&str>,
455-
) -> Result<Origin, WebAuthnError> {
508+
app_id: &AppId,
509+
origin: Origin,
510+
top_origin: Option<Origin>,
511+
) -> Result<NavigationContext, WebAuthnError> {
456512
let trusted_clients = [
457513
"org.mozilla.firefox",
458514
"xyz.iinuwa.credentialsd.DemoCredentialsUi",
@@ -461,75 +517,28 @@ fn check_origin_from_app<'a>(
461517
if is_privileged_client {
462518
check_origin_from_privileged_client(origin, top_origin)
463519
} else {
464-
Ok(Origin::AppId(app_id.to_string()))
520+
Ok(NavigationContext::SameOrigin(Origin::AppId(app_id.clone())))
465521
}
466522
}
467523

468524
fn check_origin_from_privileged_client(
469-
origin: Option<&str>,
470-
top_origin: Option<&str>,
471-
) -> Result<Origin, WebAuthnError> {
472-
let origin = match (origin, top_origin) {
473-
(Some(origin), top_origin) => {
474-
if !origin.starts_with("https://") {
475-
tracing::warn!(
476-
"Caller requested non-HTTPS schemed origin, which is not supported."
477-
);
478-
return Err(WebAuthnError::SecurityError);
479-
}
480-
if let Some(top_origin) = top_origin {
481-
if origin == top_origin {
482-
Origin::SameOrigin(origin.to_string())
483-
} else {
484-
Origin::CrossOrigin((origin.to_string(), top_origin.to_string()))
485-
}
525+
origin: Origin,
526+
top_origin: Option<Origin>,
527+
) -> Result<NavigationContext, WebAuthnError> {
528+
match (origin, top_origin) {
529+
(origin @ Origin::Https { .. }, None) => Ok(NavigationContext::SameOrigin(origin)),
530+
(origin @ Origin::Https { .. }, Some(top_origin @ Origin::Https { .. })) => {
531+
if origin == top_origin {
532+
Ok(NavigationContext::SameOrigin(origin))
486533
} else {
487-
Origin::SameOrigin(origin.to_string())
534+
Ok(NavigationContext::CrossOrigin((origin, top_origin)))
488535
}
489536
}
490-
(None, Some(_)) => {
491-
tracing::warn!("Top origin cannot be set if origin is not set.");
492-
return Err(WebAuthnError::SecurityError);
493-
}
494-
(None, None) => {
495-
tracing::warn!("No origin given. Rejecting request.");
537+
_ => {
538+
tracing::warn!("Caller requested non-HTTPS schemed origin, which is not supported.");
496539
return Err(WebAuthnError::SecurityError);
497540
}
498-
};
499-
500-
if let Origin::CrossOrigin(_) = origin {
501-
tracing::warn!("Client attempted to issue cross-origin request for credentials, which are not supported by this platform.");
502-
return Err(WebAuthnError::NotAllowedError);
503-
};
504-
Ok(origin)
505-
}
506-
507-
async fn check_origin(
508-
origin: Option<&str>,
509-
is_same_origin: Option<bool>,
510-
// TODO: Replace is_same_origin with explicit top_origin
511-
// top_origin: Option<&str>,
512-
) -> Result<(String, bool, String), WebAuthnError> {
513-
let origin = if let Some(origin) = origin {
514-
origin.to_string()
515-
} else {
516-
tracing::warn!(
517-
"Caller requested implicit origin, which is not yet implemented. Rejecting request."
518-
);
519-
return Err(WebAuthnError::SecurityError);
520-
};
521-
if !origin.starts_with("https://") {
522-
tracing::warn!("Caller requested non-HTTPS schemed origin, which is not supported.");
523-
return Err(WebAuthnError::SecurityError);
524541
}
525-
let is_same_origin = is_same_origin.unwrap_or(false);
526-
let top_origin = if is_same_origin {
527-
origin.clone()
528-
} else {
529-
tracing::warn!("Client attempted to issue cross-origin request for credentials, which are not supported by this platform.");
530-
return Err(WebAuthnError::NotAllowedError);
531-
};
532-
Ok((origin, true, top_origin))
533542
}
534543

535544
#[allow(clippy::enum_variant_names)]
@@ -593,18 +602,27 @@ impl From<WebAuthnError> for Error {
593602
mod test {
594603
use credentialsd_common::model::WebAuthnError;
595604

596-
use crate::dbus::gateway::check_origin;
605+
use crate::webauthn::{AppId, NavigationContext, Origin};
606+
607+
use super::check_origin_from_privileged_client;
608+
fn check_same_origin(origin: &str) -> Result<NavigationContext, WebAuthnError> {
609+
let origin = origin.parse().unwrap();
610+
check_origin_from_privileged_client(origin, None)
611+
}
597612

598-
#[tokio::test]
599-
async fn test_only_https_origins() {
600-
let check = |origin: &'static str| async { check_origin(Some(origin), Some(true)).await };
613+
#[test]
614+
fn test_https_origin_returns_success() {
601615
assert!(matches!(
602-
check("https://example.com").await,
603-
Ok((o, ..)) if o == "https://example.com"
604-
));
616+
check_same_origin("https://example.com"),
617+
Ok(NavigationContext::SameOrigin(Origin::Https { host, .. })) if host == "example.com"
618+
))
619+
}
620+
621+
#[test]
622+
fn test_throws_security_error_when_passing_app_id_origin() {
605623
assert!(matches!(
606-
check("http://example.com").await,
624+
check_same_origin("app:com.example.App"),
607625
Err(WebAuthnError::SecurityError)
608-
));
626+
))
609627
}
610628
}

0 commit comments

Comments
 (0)