@@ -26,7 +26,7 @@ use crate::{
2626 CredentialRequestController ,
2727 } ,
2828 model:: { CredentialRequest , CredentialResponse } ,
29- webauthn:: Origin ,
29+ webauthn:: { AppId , NavigationContext , Origin } ,
3030} ;
3131
3232pub 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
405461async fn should_trust_app_id ( pid : u32 ) -> bool {
@@ -449,10 +505,10 @@ async fn should_trust_app_id(pid: u32) -> bool {
449505}
450506
451507fn 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
468524fn 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 {
593602mod 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