@@ -26,6 +26,7 @@ use crate::{
2626 CredentialRequestController ,
2727 } ,
2828 model:: { CredentialRequest , CredentialResponse } ,
29+ webauthn:: Origin ,
2930} ;
3031
3132pub const SERVICE_NAME : & str = "xyz.iinuwa.credentialsd.Credentials" ;
@@ -363,6 +364,146 @@ impl<C: CredentialRequestController + Send + Sync + 'static> CredentialGateway<C
363364 }
364365}
365366
367+ async fn validate_app_details (
368+ connection : & Connection ,
369+ header : & Header < ' _ > ,
370+ claimed_app_id : String ,
371+ claimed_app_display_name : Option < String > ,
372+ claimed_origin : Option < String > ,
373+ claimed_top_origin : Option < String > ,
374+ ) -> Result < ( RequestingApplication , Origin ) , Error > {
375+ let Some ( unique_name) = header. sender ( ) else {
376+ return Err ( Error :: SecurityError ) ;
377+ } ;
378+
379+ let Some ( pid) = query_peer_pid_via_fdinfo ( connection, unique_name) . await else {
380+ return Err ( Error :: SecurityError ) ;
381+ } ;
382+
383+ if claimed_app_id. is_empty ( ) || !should_trust_app_id ( pid) . await {
384+ tracing:: warn!( "App ID could not be determined. Rejecting request." ) ;
385+ return Err ( Error :: SecurityError ) ;
386+ }
387+ // Now we can trust these app detail parameters.
388+ let app_id = format ! ( "app:{claimed_app_id}" ) ;
389+ let display_name = claimed_app_display_name. unwrap_or_default ( ) ;
390+
391+ // 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+ ) ?;
397+ let app_details = RequestingApplication {
398+ name : display_name,
399+ path : app_id,
400+ pid,
401+ } ;
402+ Ok ( ( app_details, origin) )
403+ }
404+
405+ async fn should_trust_app_id ( pid : u32 ) -> bool {
406+ // Verify if we should trust the peer based on the file name. We verify that
407+ // we're in the same mount namespace before using the exe path.
408+
409+ // TODO: If the portal is running in a separate mount namespace for security
410+ // reasons, then this check will fail with a false negative.
411+ // In the future, we should retrieve this information from another trusted
412+ // source, e.g. check if the PID is in a cgroup managed by systemd and
413+ // corresponds to the org.freedesktop.portal.Desktop D-Bus service unit.
414+ let Ok ( my_mnt_ns) = tokio:: fs:: read_link ( "/proc/self/ns/mnt" ) . await else {
415+ tracing:: debug!( "Could not read peer mount namespace" ) ;
416+ return false ;
417+ } ;
418+ let Ok ( peer_mnt_ns) = tokio:: fs:: read_link ( format ! ( "/proc/{pid}/ns/mnt" ) ) . await else {
419+ tracing:: debug!( "Could not determine our mount namespace" ) ;
420+ return false ;
421+ } ;
422+ tracing:: debug!(
423+ "mount namespace:\n ours:\t {:?}\n theirs:\t {:?}" ,
424+ my_mnt_ns,
425+ peer_mnt_ns
426+ ) ;
427+ if my_mnt_ns != peer_mnt_ns {
428+ tracing:: warn!( "Peer mount namespace is not the same as ours, not trusting the request." ) ;
429+ return false ;
430+ }
431+
432+ let Ok ( exe_path) = tokio:: fs:: read_link ( format ! ( "/proc/{pid}/exe" ) ) . await else {
433+ return false ;
434+ } ;
435+
436+ // The target binaries are hard-coded to valid UTF-8, so it's acceptable to
437+ // lose some data here.
438+ let Some ( exe_path) = exe_path. to_str ( ) else {
439+ return false ;
440+ } ;
441+ tracing:: debug!( ?exe_path, %pid, "Found executable path:" ) ;
442+ let trusted_callers: Vec < String > = if cfg ! ( debug_assertions) {
443+ let trusted_callers_env = std:: env:: var ( "CREDSD_TRUSTED_CALLERS" ) . unwrap_or_default ( ) ;
444+ trusted_callers_env. split ( ',' ) . map ( String :: from) . collect ( )
445+ } else {
446+ vec ! [ "/usr/bin/xdg-desktop-portal" . to_string( ) ]
447+ } ;
448+ return trusted_callers. as_slice ( ) . contains ( & exe_path. to_string ( ) ) ;
449+ }
450+
451+ fn check_origin_from_app < ' a > (
452+ app_id : & str ,
453+ origin : Option < & str > ,
454+ top_origin : Option < & str > ,
455+ ) -> Result < Origin , WebAuthnError > {
456+ let trusted_clients = [
457+ "org.mozilla.firefox" ,
458+ "xyz.iinuwa.credentialsd.DemoCredentialsUi" ,
459+ ] ;
460+ let is_privileged_client = trusted_clients. contains ( & app_id. as_ref ( ) ) ;
461+ if is_privileged_client {
462+ check_origin_from_privileged_client ( origin, top_origin)
463+ } else {
464+ Ok ( Origin :: AppId ( app_id. to_string ( ) ) )
465+ }
466+ }
467+
468+ 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+ }
486+ } else {
487+ Origin :: SameOrigin ( origin. to_string ( ) )
488+ }
489+ }
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." ) ;
496+ return Err ( WebAuthnError :: SecurityError ) ;
497+ }
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+
366507async fn check_origin (
367508 origin : Option < & str > ,
368509 is_same_origin : Option < bool > ,
0 commit comments