Skip to content

Commit 2fb50ad

Browse files
authored
Merge pull request #129 from linux-credentials/push-vsrpuwoxummx
Add security checks based on proxy PID and app ID
2 parents 5d4c32c + 2550c20 commit 2fb50ad

2 files changed

Lines changed: 166 additions & 0 deletions

File tree

credentialsd/src/dbus/gateway.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use crate::{
2626
CredentialRequestController,
2727
},
2828
model::{CredentialRequest, CredentialResponse},
29+
webauthn::Origin,
2930
};
3031

3132
pub 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+
366507
async fn check_origin(
367508
origin: Option<&str>,
368509
is_same_origin: Option<bool>,

credentialsd/src/webauthn.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,3 +691,28 @@ pub fn format_client_data_json(
691691
let cross_origin_str = if is_cross_origin { "true" } else { "false" };
692692
format!("{{\"type\":\"{op_str}\",\"challenge\":\"{challenge}\",\"origin\":\"{origin}\",\"crossOrigin\":{cross_origin_str}}}")
693693
}
694+
695+
#[derive(Debug)]
696+
pub(crate) enum Origin {
697+
AppId(String),
698+
SameOrigin(String),
699+
CrossOrigin((String, String)),
700+
}
701+
702+
impl Origin {
703+
pub(crate) fn origin(&self) -> &str {
704+
&match self {
705+
Origin::AppId(app_id) => app_id,
706+
Origin::SameOrigin(origin) => origin,
707+
Origin::CrossOrigin((origin, _)) => origin,
708+
}
709+
}
710+
711+
pub(crate) fn top_origin(&self) -> Option<&str> {
712+
match self {
713+
Origin::AppId(_) => None,
714+
Origin::SameOrigin(_) => None,
715+
Origin::CrossOrigin((_, ref top_origin)) => Some(top_origin),
716+
}
717+
}
718+
}

0 commit comments

Comments
 (0)