From 60d05bafc140e9034d1c7dd34750e4e8d11e732e Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Sat, 28 Feb 2026 09:09:48 -0600 Subject: [PATCH 1/7] Extract request handling out of D-Bus methods --- credentialsd/src/dbus/gateway.rs | 232 ++++++++++++++++++------------- 1 file changed, 132 insertions(+), 100 deletions(-) diff --git a/credentialsd/src/dbus/gateway.rs b/credentialsd/src/dbus/gateway.rs index 32af944..fd43d4b 100644 --- a/credentialsd/src/dbus/gateway.rs +++ b/credentialsd/src/dbus/gateway.rs @@ -250,58 +250,17 @@ impl CredentialGateway CredentialGateway fdo::Result { @@ -401,6 +324,115 @@ impl CredentialGateway( + controller: &AsyncMutex, + request: CreateCredentialRequest, + request_environment: NavigationContext, + requesting_app: Option, + parent_window: Option, +) -> Result { + if let ("publicKey", Some(_)) = (request.r#type.as_ref(), &request.public_key) { + // TODO: assert that RP ID is bound to origin: + // - if RP ID is not set, set the RP ID to the origin's effective domain + // - if RP ID is set, assert that it matches origin's effective domain + // - if RP ID is set, but origin's effective domain doesn't match + // - query for related origins, if supported + // - fail if not supported, or if RP ID doesn't match any related origins. + let (make_cred_request, client_data_json) = + create_credential_request_try_into_ctap2(&request, &request_environment).inspect_err( + |_| { + tracing::error!("Could not parse passkey creation request. Rejecting request."); + }, + )?; + if make_cred_request.algorithms.is_empty() { + tracing::info!("No supported algorithms given in request. Rejecting request."); + return Err(WebAuthnError::NotSupportedError); + } + let cred_request = CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request); + + let response = controller + .lock() + .await + .request_credential(requesting_app, cred_request, parent_window.into()) + .await?; + + if let CredentialResponse::CreatePublicKeyCredentialResponse(cred_response) = response { + let public_key_response = + create_credential_response_try_from_ctap2(&cred_response, client_data_json) + .map_err(|err| { + tracing::error!( + "Failed to parse credential response from authenticator: {err}" + ); + // Using NotAllowedError as a catch-all error. + WebAuthnError::NotAllowedError + })?; + Ok(public_key_response.into()) + } else { + // TODO: is response safe to log here? + // tracing::error!("Expected create public key credential response, received {response:?}"); + tracing::error!("Did not receive expected create public key credential response."); + // Using NotAllowedError as a catch-all error. + Err(WebAuthnError::NotAllowedError.into()) + } + } else { + tracing::error!("Unknown credential type request: {}", request.r#type); + Err(WebAuthnError::TypeError) + } +} + +async fn handle_get_credential( + controller: &AsyncMutex, + request: GetCredentialRequest, + request_environment: NavigationContext, + requesting_app: Option, + parent_window: Option, +) -> Result { + if let ("publicKey", Some(_)) = (request.r#type.as_ref(), &request.public_key) { + // Setup request + + // TODO: assert that RP ID is bound to origin: + // - if RP ID is not set, set the RP ID to the origin's effective domain + // - if RP ID is set, assert that it matches origin's effective domain + // - if RP ID is set, but origin's effective domain doesn't match + // - query for related origins, if supported + // - fail if not supported, or if RP ID doesn't match any related origins. + let (get_cred_request, client_data_json) = + get_credential_request_try_into_ctap2(&request, &request_environment).map_err(|e| { + tracing::error!("Could not parse passkey assertion request: {e:?}"); + WebAuthnError::TypeError + })?; + let cred_request = CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request); + + let response = controller + .lock() + .await + .request_credential(requesting_app, cred_request, parent_window.into()) + .await?; + + if let CredentialResponse::GetPublicKeyCredentialResponse(cred_response) = response { + let public_key_response = get_credential_response_try_from_ctap2( + &cred_response, + client_data_json, + ) + .map_err(|err| { + tracing::error!("Failed to parse credential response from authenticator: {err}"); + // Using NotAllowedError as a catch-all error. + WebAuthnError::NotAllowedError + })?; + Ok(public_key_response.into()) + } else { + // TODO: is response safe to log here? + // tracing::error!("Expected get public key credential response, received {response:?}"); + tracing::error!("Did not receive expected get public key credential response."); + // Using NotAllowedError as a catch-all error. + Err(WebAuthnError::NotAllowedError.into()) + } + } else { + tracing::error!("Unknown credential type request: {}", request.r#type); + Err(WebAuthnError::TypeError.into()) + } +} + async fn validate_app_details( connection: &Connection, header: &Header<'_>, From f2b5d72ed5101a6897de7bc7b1fc5a07d305d4cd Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 11 Mar 2026 21:14:26 -0500 Subject: [PATCH 2/7] Move Gateway service to its own module --- credentialsd/src/dbus/mod.rs | 8 -------- credentialsd/src/{dbus/gateway.rs => gateway/mod.rs} | 12 +++++++----- credentialsd/src/{dbus/model.rs => gateway/util.rs} | 0 credentialsd/src/main.rs | 3 ++- 4 files changed, 9 insertions(+), 14 deletions(-) rename credentialsd/src/{dbus/gateway.rs => gateway/mod.rs} (99%) rename credentialsd/src/{dbus/model.rs => gateway/util.rs} (100%) diff --git a/credentialsd/src/dbus/mod.rs b/credentialsd/src/dbus/mod.rs index 3ed09e0..9df11af 100644 --- a/credentialsd/src/dbus/mod.rs +++ b/credentialsd/src/dbus/mod.rs @@ -10,20 +10,12 @@ //! There is also a client to reach out to the UI controller hosted by the trusted UI. mod flow_control; -mod gateway; -mod model; mod ui_control; -use self::model::{ - create_credential_request_try_into_ctap2, create_credential_response_try_from_ctap2, - get_credential_request_try_into_ctap2, get_credential_response_try_from_ctap2, -}; - pub use self::{ flow_control::{ start_flow_control_service, CredentialRequestController, CredentialRequestControllerClient, }, - gateway::start_gateway, ui_control::UiControlServiceClient, }; diff --git a/credentialsd/src/dbus/gateway.rs b/credentialsd/src/gateway/mod.rs similarity index 99% rename from credentialsd/src/dbus/gateway.rs rename to credentialsd/src/gateway/mod.rs index fd43d4b..25e7e5d 100644 --- a/credentialsd/src/dbus/gateway.rs +++ b/credentialsd/src/gateway/mod.rs @@ -1,6 +1,8 @@ //! Implements the service that public clients can connect to. Responsible for //! authorizing clients for origins and validating request parameters. +mod util; + use std::{os::fd::AsRawFd, sync::Arc}; use credentialsd_common::{ @@ -20,14 +22,14 @@ use zbus::{ }; use crate::{ - dbus::{ - create_credential_request_try_into_ctap2, create_credential_response_try_from_ctap2, - get_credential_request_try_into_ctap2, get_credential_response_try_from_ctap2, - CredentialRequestController, - }, + dbus::CredentialRequestController, model::{CredentialRequest, CredentialResponse}, webauthn::{AppId, NavigationContext, Origin}, }; +use util::{ + create_credential_request_try_into_ctap2, create_credential_response_try_from_ctap2, + get_credential_request_try_into_ctap2, get_credential_response_try_from_ctap2, +}; pub const SERVICE_NAME: &str = "xyz.iinuwa.credentialsd.Credentials"; pub const SERVICE_PATH: &str = "/xyz/iinuwa/credentialsd/Credentials"; diff --git a/credentialsd/src/dbus/model.rs b/credentialsd/src/gateway/util.rs similarity index 100% rename from credentialsd/src/dbus/model.rs rename to credentialsd/src/gateway/util.rs diff --git a/credentialsd/src/main.rs b/credentialsd/src/main.rs index d72229a..ea920e8 100644 --- a/credentialsd/src/main.rs +++ b/credentialsd/src/main.rs @@ -2,6 +2,7 @@ mod cbor; mod cose; mod credential_service; mod dbus; +mod gateway; mod model; mod serde; mod webauthn; @@ -45,7 +46,7 @@ async fn run() -> Result<(), Box> { print!("Starting D-Bus public client service..."); let initiator = CredentialRequestControllerClient { initiator }; - let _gateway_conn = dbus::start_gateway(initiator).await?; + let _gateway_conn = gateway::start_gateway(initiator).await?; println!(" ✅"); println!("Waiting for messages..."); From 61ad08d0e775a8c717d7fd45c98ada506743628d Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 11 Mar 2026 21:47:14 -0500 Subject: [PATCH 3/7] Split out core gateway functions from D-Bus interface --- credentialsd/src/gateway/dbus.rs | 393 +++++++++++++++++++++++++++ credentialsd/src/gateway/mod.rs | 445 ++++--------------------------- 2 files changed, 438 insertions(+), 400 deletions(-) create mode 100644 credentialsd/src/gateway/dbus.rs diff --git a/credentialsd/src/gateway/dbus.rs b/credentialsd/src/gateway/dbus.rs new file mode 100644 index 0000000..3d33c3d --- /dev/null +++ b/credentialsd/src/gateway/dbus.rs @@ -0,0 +1,393 @@ +use std::{os::fd::AsRawFd, sync::Arc}; + +use tokio::sync::Mutex as AsyncMutex; +use zbus::{ + fdo, interface, + message::Header, + names::{BusName, UniqueName}, + zvariant::Optional, + Connection, DBusError, +}; + +use credentialsd_common::{ + model::{GetClientCapabilitiesResponse, RequestingApplication, WebAuthnError}, + server::{ + CreateCredentialRequest, CreateCredentialResponse, GetCredentialRequest, + GetCredentialResponse, WindowHandle, + }, +}; + +use crate::{ + dbus::CredentialRequestController, + webauthn::{AppId, NavigationContext, Origin}, +}; + +use super::{ + check_origin_from_app, check_origin_from_privileged_client, get_app_info_from_pid, + handle_create_credential, handle_get_client_capabilities, handle_get_credential, + CredentialGateway, +}; + +pub const SERVICE_NAME: &str = "xyz.iinuwa.credentialsd.Credentials"; +pub const SERVICE_PATH: &str = "/xyz/iinuwa/credentialsd/Credentials"; + +pub(super) async fn start_dbus_gateway( + controller: C, +) -> Result { + zbus::connection::Builder::session() + .inspect_err(|err| { + tracing::error!("Failed to connect to D-Bus session: {err}"); + })? + .name(SERVICE_NAME)? + .serve_at( + SERVICE_PATH, + CredentialGateway { + controller: Arc::new(AsyncMutex::new(controller)), + }, + )? + .build() + .await +} +/// These are public methods that can be called by arbitrary clients to begin a credential flow. +#[interface(name = "xyz.iinuwa.credentialsd.Credentials1")] +impl super::CredentialGateway { + async fn create_credential( + &self, + #[zbus(header)] header: Header<'_>, + #[zbus(connection)] connection: &Connection, + parent_window: Optional, + request: CreateCredentialRequest, + ) -> Result { + // TODO: Add authorization check for privileged client. + let top_origin = if request.is_same_origin.unwrap_or_default() { + None + } else { + // TODO: Once we modify the models to convey the top-origin in cross origin requests to the UI, we can remove this error message. + // We should still reject cross-origin requests for conditionally-mediated requests. + tracing::warn!("Client attempted to issue cross-origin request for credentials, which are not supported by this platform."); + return Err(WebAuthnError::NotAllowedError.into()); + }; + let Some(origin) = request + .origin + .as_ref() + .map(|o| { + o.parse::().map_err(|_| { + tracing::warn!("Invalid origin specified: {:?}", request.origin); + Error::SecurityError + }) + }) + .transpose()? + else { + tracing::warn!( + "Caller requested implicit origin, which is not yet implemented. Rejecting request." + ); + return Err(Error::SecurityError); + }; + let request_environment = check_origin_from_privileged_client(origin, top_origin)?; + // Find out where this request is coming from (which application is requesting this) + let requesting_app = query_connection_peer_binary(header, connection).await; + let response = handle_create_credential( + &self.controller, + request, + request_environment, + requesting_app, + parent_window.into(), + ) + .await?; + Ok(response) + } + + async fn get_credential( + &self, + #[zbus(header)] header: Header<'_>, + #[zbus(connection)] connection: &Connection, + parent_window: Optional, + request: GetCredentialRequest, + ) -> Result { + // TODO: Add authorization check for privileged client. + let top_origin = if request.is_same_origin.unwrap_or_default() { + None + } else { + // TODO: Once we modify the models to convey the top-origin in cross origin requests to the UI, we can remove this error message. + // We should still reject cross-origin requests for conditionally-mediated requests. + tracing::warn!("Client attempted to issue cross-origin request for credentials, which are not supported by this platform."); + return Err(WebAuthnError::NotAllowedError.into()); + }; + let Some(origin) = request + .origin + .as_ref() + .map(|o| { + o.parse::().map_err(|_| { + tracing::warn!("Invalid origin specified: {:?}", request.origin); + Error::SecurityError + }) + }) + .transpose()? + else { + tracing::warn!( + "Caller requested implicit origin, which is not yet implemented. Rejecting request." + ); + return Err(Error::SecurityError); + }; + let request_environment = check_origin_from_privileged_client(origin, top_origin)?; + // Find out where this request is coming from (which application is requesting this) + let requesting_app = query_connection_peer_binary(header, connection).await; + let response = handle_get_credential( + &self.controller, + request, + request_environment, + requesting_app, + parent_window.into(), + ) + .await?; + Ok(response) + } + + async fn get_client_capabilities(&self) -> fdo::Result { + let capabilities = handle_get_client_capabilities(); + Ok(capabilities) + } +} + +#[allow(clippy::enum_variant_names)] +#[derive(DBusError, Debug)] +#[zbus(prefix = "xyz.iinuwa.credentialsd")] +enum Error { + #[zbus(error)] + ZBus(zbus::Error), + + /// The ceremony was cancelled by an AbortController. See § 5.6 Abort + /// Operations with AbortSignal and § 1.3.4 Aborting Authentication + /// Operations. + AbortError, + + /// Either `residentKey` was set to required and no available authenticator + /// supported resident keys, or `userVerification` was set to required and no + /// available authenticator could perform user verification. + ConstraintError, + + /// The authenticator used in the ceremony recognized an entry in + /// `excludeCredentials` after the user consented to registering a credential. + InvalidStateError, + + /// No entry in `pubKeyCredParams` had a type property of `public-key`, or the + /// authenticator did not support any of the signature algorithms specified + /// in `pubKeyCredParams`. + NotSupportedError, + + /// The effective domain was not a valid domain, or `rp.id` was not equal to + /// or a registrable domain suffix of the effective domain. In the latter + /// case, the client does not support related origin requests or the related + /// origins validation procedure failed. + SecurityError, + + /// A catch-all error covering a wide range of possible reasons, including + /// common ones like the user canceling out of the ceremony. Some of these + /// causes are documented throughout this spec, while others are + /// client-specific. + NotAllowedError, + + /// The options argument was not a valid `CredentialCreationOptions` value, or + /// the value of `user.id` was empty or was longer than 64 bytes. + TypeError, +} + +impl From for Error { + fn from(value: WebAuthnError) -> Self { + match value { + WebAuthnError::AbortError => Self::AbortError, + WebAuthnError::ConstraintError => Self::ConstraintError, + WebAuthnError::InvalidStateError => Self::InvalidStateError, + WebAuthnError::NotSupportedError => Self::NotSupportedError, + WebAuthnError::SecurityError => Self::SecurityError, + WebAuthnError::NotAllowedError => Self::NotAllowedError, + WebAuthnError::TypeError => Self::TypeError, + } + } +} + +async fn validate_app_details( + connection: &Connection, + header: &Header<'_>, + claimed_app_id: String, + claimed_app_display_name: Option, + claimed_origin: Option, + claimed_top_origin: Option, +) -> Result<(RequestingApplication, NavigationContext), Error> { + let Some(unique_name) = header.sender() else { + return Err(Error::SecurityError); + }; + + let Some(pid) = query_peer_pid_via_fdinfo(connection, unique_name).await else { + return Err(Error::SecurityError); + }; + + if claimed_app_id.is_empty() || !super::should_trust_app_id(pid).await { + tracing::warn!("App ID could not be determined. Rejecting request."); + return Err(Error::SecurityError); + } + // Now we can trust these app detail parameters. + let Ok(app_id) = claimed_app_id.parse::() else { + tracing::warn!("Invalid app ID passed: {claimed_app_id}"); + return Err(Error::SecurityError); + }; + let display_name = claimed_app_display_name.unwrap_or_default(); + + // Verify that the origin is valid for the given app ID. + let claimed_origin = claimed_origin + .map(|o| { + o.parse().map_err(|_| { + tracing::warn!("Invalid origin passed: {o}"); + Error::SecurityError + }) + }) + .transpose()?; + let request_env = if let Some(claimed_origin) = claimed_origin { + let claimed_top_origin = claimed_top_origin + .map(|o| { + o.parse().map_err(|_| { + tracing::warn!("Invalid origin passed: {o}"); + Error::SecurityError + }) + }) + .transpose()?; + check_origin_from_app(&app_id, claimed_origin, claimed_top_origin)? + } else { + NavigationContext::SameOrigin(Origin::AppId(app_id)) + }; + let app_details = RequestingApplication { + name: Some(display_name).into(), + path_or_app_id: claimed_app_id, + pid, + }; + Ok((app_details, request_env)) +} + +async fn query_peer_pid_via_fdinfo( + connection: &Connection, + sender_unique_name: &UniqueName<'_>, +) -> Option { + let dbus_proxy = match zbus::fdo::DBusProxy::new(connection).await { + Ok(p) => p, + Err(e) => { + tracing::error!("Failed to establish DBus proxy to query peer info: {e:?}"); + return None; + } + }; + + let peer_credentials = match dbus_proxy + .get_connection_credentials(BusName::from(sender_unique_name.to_owned())) + .await + { + Ok(c) => c, + Err(e) => { + tracing::error!("Failed to get peer credentials: {e:?}"); + return None; + } + }; + + let pidfd = match peer_credentials.process_fd() { + Some(p) => p.as_raw_fd(), + None => { + tracing::error!("Failed to get process fd from peer credentials"); + return None; + } + }; + + let fdinfo_str = match std::fs::read_to_string(format!("/proc/self/fdinfo/{pidfd}")) { + Ok(fdinfo) => fdinfo, + Err(e) => { + tracing::error!("Failed to read fdinfo from procfs: {e}"); + return None; + } + }; + + // Find the line that starts with "Pid:" + let pid_line = match fdinfo_str.lines().find(|line| line.starts_with("Pid:")) { + Some(line) => line, + None => { + tracing::error!("Failed to read PID from fdinfo"); + return None; + } + }; + + let pid_str = pid_line[4..].trim(); + + // std::process::id() also returns u32 + let pid: u32 = match pid_str.parse() { + Ok(id) => id, + Err(e) => { + tracing::error!("Failed to parse PID from fdinfo entry: {e}"); + return None; + } + }; + + Some(pid) +} + +async fn query_peer_pid_via_dbus( + connection: &Connection, + sender_unique_name: &UniqueName<'_>, +) -> Option { + // Use the connection to query the D-Bus daemon for more info + let proxy = match zbus::Proxy::new( + connection, + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + ) + .await + { + Ok(p) => p, + Err(e) => { + tracing::error!("Failed to establish DBus proxy to query peer info: {e:?}"); + return None; + } + }; + + // Get the Process ID (PID) of the peer + let pid_result = match proxy + .call_method("GetConnectionUnixProcessID", &(sender_unique_name)) + .await + { + Ok(pid) => pid, + Err(e) => { + tracing::error!("Failed to get peer PID via DBus: {e:?}"); + return None; + } + }; + let pid: u32 = match pid_result.body().deserialize() { + Ok(pid) => pid, + Err(e) => { + tracing::error!("Retrieved peer PID is not an integer: {e:?}"); + return None; + } + }; + Some(pid) +} + +async fn query_connection_peer_binary( + header: Header<'_>, + connection: &Connection, +) -> Option { + // Get the sender's unique bus name + let sender_unique_name = header.sender()?; + + tracing::debug!("Received request from sender: {}", sender_unique_name); + + // Get the senders PID. + // + // First, try to get the PID via the more secure fdinfo + let mut pid = query_peer_pid_via_fdinfo(connection, sender_unique_name).await; + // If that fails, we fall back to asking dbus directly for the peers PID + if pid.is_none() { + pid = query_peer_pid_via_dbus(connection, sender_unique_name).await; + } + + let Some(pid) = pid else { + tracing::error!("Failed to determine peers PID. Skipping application details query."); + return None; + }; + + get_app_info_from_pid(pid) +} diff --git a/credentialsd/src/gateway/mod.rs b/credentialsd/src/gateway/mod.rs index 25e7e5d..baa5bf7 100644 --- a/credentialsd/src/gateway/mod.rs +++ b/credentialsd/src/gateway/mod.rs @@ -1,9 +1,10 @@ //! Implements the service that public clients can connect to. Responsible for //! authorizing clients for origins and validating request parameters. +mod dbus; mod util; -use std::{os::fd::AsRawFd, sync::Arc}; +use std::sync::Arc; use credentialsd_common::{ model::{GetClientCapabilitiesResponse, RequestingApplication, WebAuthnError}, @@ -13,13 +14,7 @@ use credentialsd_common::{ }, }; use tokio::sync::Mutex as AsyncMutex; -use zbus::{ - fdo, interface, - message::Header, - names::{BusName, UniqueName}, - zvariant::Optional, - Connection, DBusError, -}; +use zbus::Connection; use crate::{ dbus::CredentialRequestController, @@ -31,301 +26,16 @@ use util::{ get_credential_request_try_into_ctap2, get_credential_response_try_from_ctap2, }; -pub const SERVICE_NAME: &str = "xyz.iinuwa.credentialsd.Credentials"; -pub const SERVICE_PATH: &str = "/xyz/iinuwa/credentialsd/Credentials"; - pub async fn start_gateway( controller: C, ) -> Result { - zbus::connection::Builder::session() - .inspect_err(|err| { - tracing::error!("Failed to connect to D-Bus session: {err}"); - })? - .name(SERVICE_NAME)? - .serve_at( - SERVICE_PATH, - CredentialGateway { - controller: Arc::new(AsyncMutex::new(controller)), - }, - )? - .build() - .await + dbus::start_dbus_gateway(controller).await } struct CredentialGateway { controller: Arc>, } -async fn query_peer_pid_via_fdinfo( - connection: &Connection, - sender_unique_name: &UniqueName<'_>, -) -> Option { - let dbus_proxy = match zbus::fdo::DBusProxy::new(connection).await { - Ok(p) => p, - Err(e) => { - tracing::error!("Failed to establish DBus proxy to query peer info: {e:?}"); - return None; - } - }; - - let peer_credentials = match dbus_proxy - .get_connection_credentials(BusName::from(sender_unique_name.to_owned())) - .await - { - Ok(c) => c, - Err(e) => { - tracing::error!("Failed to get peer credentials: {e:?}"); - return None; - } - }; - - let pidfd = match peer_credentials.process_fd() { - Some(p) => p.as_raw_fd(), - None => { - tracing::error!("Failed to get process fd from peer credentials"); - return None; - } - }; - - let fdinfo_str = match std::fs::read_to_string(format!("/proc/self/fdinfo/{pidfd}")) { - Ok(fdinfo) => fdinfo, - Err(e) => { - tracing::error!("Failed to read fdinfo from procfs: {e}"); - return None; - } - }; - - // Find the line that starts with "Pid:" - let pid_line = match fdinfo_str.lines().find(|line| line.starts_with("Pid:")) { - Some(line) => line, - None => { - tracing::error!("Failed to read PID from fdinfo"); - return None; - } - }; - - let pid_str = pid_line[4..].trim(); - - // std::process::id() also returns u32 - let pid: u32 = match pid_str.parse() { - Ok(id) => id, - Err(e) => { - tracing::error!("Failed to parse PID from fdinfo entry: {e}"); - return None; - } - }; - - Some(pid) -} - -async fn query_peer_pid_via_dbus( - connection: &Connection, - sender_unique_name: &UniqueName<'_>, -) -> Option { - // Use the connection to query the D-Bus daemon for more info - let proxy = match zbus::Proxy::new( - connection, - "org.freedesktop.DBus", - "/org/freedesktop/DBus", - "org.freedesktop.DBus", - ) - .await - { - Ok(p) => p, - Err(e) => { - tracing::error!("Failed to establish DBus proxy to query peer info: {e:?}"); - return None; - } - }; - - // Get the Process ID (PID) of the peer - let pid_result = match proxy - .call_method("GetConnectionUnixProcessID", &(sender_unique_name)) - .await - { - Ok(pid) => pid, - Err(e) => { - tracing::error!("Failed to get peer PID via DBus: {e:?}"); - return None; - } - }; - let pid: u32 = match pid_result.body().deserialize() { - Ok(pid) => pid, - Err(e) => { - tracing::error!("Retrieved peer PID is not an integer: {e:?}"); - return None; - } - }; - Some(pid) -} - -async fn query_connection_peer_binary( - header: Header<'_>, - connection: &Connection, -) -> Option { - // Get the sender's unique bus name - let sender_unique_name = header.sender()?; - - tracing::debug!("Received request from sender: {}", sender_unique_name); - - // Get the senders PID. - // - // First, try to get the PID via the more secure fdinfo - let mut pid = query_peer_pid_via_fdinfo(connection, sender_unique_name).await; - // If that fails, we fall back to asking dbus directly for the peers PID - if pid.is_none() { - pid = query_peer_pid_via_dbus(connection, sender_unique_name).await; - } - - let Some(pid) = pid else { - tracing::error!("Failed to determine peers PID. Skipping application details query."); - return None; - }; - - // Get binary path via PID from /proc file-system - // TODO: To be REALLY sure, we may want to look at /proc/PID/exe instead. It is a symlink to - // the actual binary, giving a full path instead of only the command name. - // This should in theory be "more secure", but also may disconcert novice users with no - // technical background. - let command_name = match std::fs::read_to_string(format!("/proc/{pid}/comm")) { - Ok(c) => c.trim().to_string(), - Err(e) => { - tracing::error!( - "Failed to read /proc/{pid}/comm, so we don't know the command name of peer: {e:?}" - ); - return None; - } - }; - tracing::debug!("Request is from: {command_name}"); - - let exe_path = match std::fs::read_link(format!("/proc/{pid}/exe")) { - Ok(p) => p, - Err(e) => { - tracing::error!( - "Failed to follow link of /proc/{pid}/exe, so we don't know the executable path of peer: {e:?}" - ); - return None; - } - }; - tracing::debug!("Request is from: {exe_path:?}"); - - Some(RequestingApplication { - name: Some(command_name).into(), - path_or_app_id: exe_path.to_string_lossy().to_string(), - pid, - }) -} - -/// These are public methods that can be called by arbitrary clients to begin a credential flow. -#[interface(name = "xyz.iinuwa.credentialsd.Credentials1")] -impl CredentialGateway { - async fn create_credential( - &self, - #[zbus(header)] header: Header<'_>, - #[zbus(connection)] connection: &Connection, - parent_window: Optional, - request: CreateCredentialRequest, - ) -> Result { - // TODO: Add authorization check for privileged client. - let top_origin = if request.is_same_origin.unwrap_or_default() { - None - } else { - // TODO: Once we modify the models to convey the top-origin in cross origin requests to the UI, we can remove this error message. - // We should still reject cross-origin requests for conditionally-mediated requests. - tracing::warn!("Client attempted to issue cross-origin request for credentials, which are not supported by this platform."); - return Err(WebAuthnError::NotAllowedError.into()); - }; - let Some(origin) = request - .origin - .as_ref() - .map(|o| { - o.parse::().map_err(|_| { - tracing::warn!("Invalid origin specified: {:?}", request.origin); - Error::SecurityError - }) - }) - .transpose()? - else { - tracing::warn!( - "Caller requested implicit origin, which is not yet implemented. Rejecting request." - ); - return Err(Error::SecurityError); - }; - let request_environment = check_origin_from_privileged_client(origin, top_origin)?; - // Find out where this request is coming from (which application is requesting this) - let requesting_app = query_connection_peer_binary(header, connection).await; - let response = handle_create_credential( - &self.controller, - request, - request_environment, - requesting_app, - parent_window.into(), - ) - .await?; - Ok(response) - } - - async fn get_credential( - &self, - #[zbus(header)] header: Header<'_>, - #[zbus(connection)] connection: &Connection, - parent_window: Optional, - request: GetCredentialRequest, - ) -> Result { - // TODO: Add authorization check for privileged client. - let top_origin = if request.is_same_origin.unwrap_or_default() { - None - } else { - // TODO: Once we modify the models to convey the top-origin in cross origin requests to the UI, we can remove this error message. - // We should still reject cross-origin requests for conditionally-mediated requests. - tracing::warn!("Client attempted to issue cross-origin request for credentials, which are not supported by this platform."); - return Err(WebAuthnError::NotAllowedError.into()); - }; - let Some(origin) = request - .origin - .as_ref() - .map(|o| { - o.parse::().map_err(|_| { - tracing::warn!("Invalid origin specified: {:?}", request.origin); - Error::SecurityError - }) - }) - .transpose()? - else { - tracing::warn!( - "Caller requested implicit origin, which is not yet implemented. Rejecting request." - ); - return Err(Error::SecurityError); - }; - let request_environment = check_origin_from_privileged_client(origin, top_origin)?; - // Find out where this request is coming from (which application is requesting this) - let requesting_app = query_connection_peer_binary(header, connection).await; - let response = handle_get_credential( - &self.controller, - request, - request_environment, - requesting_app, - parent_window.into(), - ) - .await?; - Ok(response) - } - - async fn get_client_capabilities(&self) -> fdo::Result { - Ok(GetClientCapabilitiesResponse { - conditional_create: false, - conditional_get: false, - hybrid_transport: true, - passkey_platform_authenticator: false, - user_verifying_platform_authenticator: false, - related_origins: false, - signal_all_accepted_credentials: false, - signal_current_user_details: false, - signal_unknown_credential: false, - }) - } -} - async fn handle_create_credential( controller: &AsyncMutex, request: CreateCredentialRequest, @@ -435,61 +145,53 @@ async fn handle_get_credential( } } -async fn validate_app_details( - connection: &Connection, - header: &Header<'_>, - claimed_app_id: String, - claimed_app_display_name: Option, - claimed_origin: Option, - claimed_top_origin: Option, -) -> Result<(RequestingApplication, NavigationContext), Error> { - let Some(unique_name) = header.sender() else { - return Err(Error::SecurityError); - }; +fn handle_get_client_capabilities() -> GetClientCapabilitiesResponse { + GetClientCapabilitiesResponse { + conditional_create: false, + conditional_get: false, + hybrid_transport: true, + passkey_platform_authenticator: false, + user_verifying_platform_authenticator: false, + related_origins: false, + signal_all_accepted_credentials: false, + signal_current_user_details: false, + signal_unknown_credential: false, + } +} - let Some(pid) = query_peer_pid_via_fdinfo(connection, unique_name).await else { - return Err(Error::SecurityError); +fn get_app_info_from_pid(pid: u32) -> Option { + // Get binary path via PID from /proc file-system + // TODO: To be REALLY sure, we may want to look at /proc/PID/exe instead. It is a symlink to + // the actual binary, giving a full path instead of only the command name. + // This should in theory be "more secure", but also may disconcert novice users with no + // technical background. + let command_name = match std::fs::read_to_string(format!("/proc/{pid}/comm")) { + Ok(c) => c.trim().to_string(), + Err(e) => { + tracing::error!( + "Failed to read /proc/{pid}/comm, so we don't know the command name of peer: {e:?}" + ); + return None; + } }; + tracing::debug!("Request is from: {command_name}"); - if claimed_app_id.is_empty() || !should_trust_app_id(pid).await { - tracing::warn!("App ID could not be determined. Rejecting request."); - return Err(Error::SecurityError); - } - // Now we can trust these app detail parameters. - let Ok(app_id) = claimed_app_id.parse::() else { - tracing::warn!("Invalid app ID passed: {claimed_app_id}"); - return Err(Error::SecurityError); + let exe_path = match std::fs::read_link(format!("/proc/{pid}/exe")) { + Ok(p) => p, + Err(e) => { + tracing::error!( + "Failed to follow link of /proc/{pid}/exe, so we don't know the executable path of peer: {e:?}" + ); + return None; + } }; - let display_name = claimed_app_display_name.unwrap_or_default(); + tracing::debug!("Request is from: {exe_path:?}"); - // Verify that the origin is valid for the given app ID. - let claimed_origin = claimed_origin - .map(|o| { - o.parse().map_err(|_| { - tracing::warn!("Invalid origin passed: {o}"); - Error::SecurityError - }) - }) - .transpose()?; - let request_env = if let Some(claimed_origin) = claimed_origin { - let claimed_top_origin = claimed_top_origin - .map(|o| { - o.parse().map_err(|_| { - tracing::warn!("Invalid origin passed: {o}"); - Error::SecurityError - }) - }) - .transpose()?; - check_origin_from_app(&app_id, claimed_origin, claimed_top_origin)? - } else { - NavigationContext::SameOrigin(Origin::AppId(app_id)) - }; - let app_details = RequestingApplication { - name: Some(display_name).into(), - path_or_app_id: claimed_app_id, + Some(RequestingApplication { + name: Some(command_name).into(), + path_or_app_id: exe_path.to_string_lossy().to_string(), pid, - }; - Ok((app_details, request_env)) + }) } async fn should_trust_app_id(pid: u32) -> bool { @@ -575,63 +277,6 @@ fn check_origin_from_privileged_client( } } -#[allow(clippy::enum_variant_names)] -#[derive(DBusError, Debug)] -#[zbus(prefix = "xyz.iinuwa.credentialsd")] -enum Error { - #[zbus(error)] - ZBus(zbus::Error), - - /// The ceremony was cancelled by an AbortController. See § 5.6 Abort - /// Operations with AbortSignal and § 1.3.4 Aborting Authentication - /// Operations. - AbortError, - - /// Either `residentKey` was set to required and no available authenticator - /// supported resident keys, or `userVerification` was set to required and no - /// available authenticator could perform user verification. - ConstraintError, - - /// The authenticator used in the ceremony recognized an entry in - /// `excludeCredentials` after the user consented to registering a credential. - InvalidStateError, - - /// No entry in `pubKeyCredParams` had a type property of `public-key`, or the - /// authenticator did not support any of the signature algorithms specified - /// in `pubKeyCredParams`. - NotSupportedError, - - /// The effective domain was not a valid domain, or `rp.id` was not equal to - /// or a registrable domain suffix of the effective domain. In the latter - /// case, the client does not support related origin requests or the related - /// origins validation procedure failed. - SecurityError, - - /// A catch-all error covering a wide range of possible reasons, including - /// common ones like the user canceling out of the ceremony. Some of these - /// causes are documented throughout this spec, while others are - /// client-specific. - NotAllowedError, - - /// The options argument was not a valid `CredentialCreationOptions` value, or - /// the value of `user.id` was empty or was longer than 64 bytes. - TypeError, -} - -impl From for Error { - fn from(value: WebAuthnError) -> Self { - match value { - WebAuthnError::AbortError => Self::AbortError, - WebAuthnError::ConstraintError => Self::ConstraintError, - WebAuthnError::InvalidStateError => Self::InvalidStateError, - WebAuthnError::NotSupportedError => Self::NotSupportedError, - WebAuthnError::SecurityError => Self::SecurityError, - WebAuthnError::NotAllowedError => Self::NotAllowedError, - WebAuthnError::TypeError => Self::TypeError, - } - } -} - #[cfg(test)] mod test { use credentialsd_common::model::WebAuthnError; From 3d776b143cfd21de42412cfa5ceb2f44e618ca73 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 11 Mar 2026 21:52:16 -0500 Subject: [PATCH 4/7] Address clippy lints in gateway module --- credentialsd/src/gateway/mod.rs | 16 ++++++++-------- credentialsd/src/gateway/util.rs | 5 ++--- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/credentialsd/src/gateway/mod.rs b/credentialsd/src/gateway/mod.rs index baa5bf7..6f59066 100644 --- a/credentialsd/src/gateway/mod.rs +++ b/credentialsd/src/gateway/mod.rs @@ -65,7 +65,7 @@ async fn handle_create_credential( let response = controller .lock() .await - .request_credential(requesting_app, cred_request, parent_window.into()) + .request_credential(requesting_app, cred_request, parent_window) .await?; if let CredentialResponse::CreatePublicKeyCredentialResponse(cred_response) = response { @@ -84,7 +84,7 @@ async fn handle_create_credential( // tracing::error!("Expected create public key credential response, received {response:?}"); tracing::error!("Did not receive expected create public key credential response."); // Using NotAllowedError as a catch-all error. - Err(WebAuthnError::NotAllowedError.into()) + Err(WebAuthnError::NotAllowedError) } } else { tracing::error!("Unknown credential type request: {}", request.r#type); @@ -118,7 +118,7 @@ async fn handle_get_credential( let response = controller .lock() .await - .request_credential(requesting_app, cred_request, parent_window.into()) + .request_credential(requesting_app, cred_request, parent_window) .await?; if let CredentialResponse::GetPublicKeyCredentialResponse(cred_response) = response { @@ -137,11 +137,11 @@ async fn handle_get_credential( // tracing::error!("Expected get public key credential response, received {response:?}"); tracing::error!("Did not receive expected get public key credential response."); // Using NotAllowedError as a catch-all error. - Err(WebAuthnError::NotAllowedError.into()) + Err(WebAuthnError::NotAllowedError) } } else { tracing::error!("Unknown credential type request: {}", request.r#type); - Err(WebAuthnError::TypeError.into()) + Err(WebAuthnError::TypeError) } } @@ -237,10 +237,10 @@ async fn should_trust_app_id(pid: u32) -> bool { } else { vec!["/usr/bin/xdg-desktop-portal".to_string()] }; - return trusted_callers.as_slice().contains(&exe_path.to_string()); + trusted_callers.as_slice().contains(&exe_path.to_string()) } -fn check_origin_from_app<'a>( +fn check_origin_from_app( app_id: &AppId, origin: Origin, top_origin: Option, @@ -272,7 +272,7 @@ fn check_origin_from_privileged_client( } _ => { tracing::warn!("Caller requested non-HTTPS schemed origin, which is not supported."); - return Err(WebAuthnError::SecurityError); + Err(WebAuthnError::SecurityError) } } } diff --git a/credentialsd/src/gateway/util.rs b/credentialsd/src/gateway/util.rs index b61f88d..57e1524 100644 --- a/credentialsd/src/gateway/util.rs +++ b/credentialsd/src/gateway/util.rs @@ -183,8 +183,7 @@ pub(super) fn create_credential_request_try_into_ctap2( .filter_map(|e| e.ok()) .collect() }); - let client_data_json = - webauthn::format_client_data_json(Operation::Create, &challenge, &origin); + let client_data_json = webauthn::format_client_data_json(Operation::Create, &challenge, origin); let client_data_hash = webauthn::create_client_data_hash(&client_data_json); Ok(( MakeCredentialRequest { @@ -284,7 +283,7 @@ pub(super) fn get_credential_request_try_into_ctap2( } let client_data_json = - webauthn::format_client_data_json(Operation::Get, &options.challenge, &request_env); + webauthn::format_client_data_json(Operation::Get, &options.challenge, request_env); let client_data_hash = webauthn::create_client_data_hash(&client_data_json); // TODO: actually calculate correct effective domain, and use fallback to related origin requests to fill this in. For now, just default to origin. let user_verification = match options From b02efb7fac1b5e4719aa41eea337a33e85aed33e Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 11 Mar 2026 21:52:16 -0500 Subject: [PATCH 5/7] docs: Update reference to gateway module in architecture doc --- ARCHITECTURE.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 99e5063..35b2193 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -103,11 +103,15 @@ Actual interaction I/O is performed in the [libwebauthn][libwebauthn] library. [libwebauthn]: https://github.com/linux-credentials/libwebauthn +### `credentialsd/src/gateway/` + +The Gateway service is defined here, along with its D-Bus interface. + ### `credentialsd/src/dbus/` D-Bus clients and services. -The Gateway and Flow Controller services are defined here, as well as a client +The Flow Controller services are defined here, as well as a client for the UI Controller. The `model` module contains some methods to convert from D-Bus types to internal From b3462cf27bc0220162fafc7faa3ea54c0fa45ffe Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 11 Mar 2026 22:49:28 -0500 Subject: [PATCH 6/7] daemon: Use async_trait to prevent generics sprawl --- .vscode/tasks.json | 4 +- Cargo.lock | 1 + credentialsd/Cargo.toml | 1 + credentialsd/src/dbus/flow_control.rs | 8 +- credentialsd/src/gateway/dbus.rs | 66 ++++---- credentialsd/src/gateway/mod.rs | 223 +++++++++++++------------- credentialsd/src/webauthn.rs | 10 +- 7 files changed, 161 insertions(+), 152 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 718a183..bab3e5b 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -3,7 +3,7 @@ "tasks": [ { "type": "meson", - "target": "credentialsd", + "target": "credentialsd/src/credentialsd:custom", "mode": "build", "problemMatcher": [ "$meson-gcc" @@ -13,7 +13,7 @@ }, { "type": "meson", - "target": "credentialsd-ui", + "target": "credentialsd-ui/src/credentialsd-ui:custom", "mode": "build", "problemMatcher": [ "$meson-gcc" diff --git a/Cargo.lock b/Cargo.lock index 6da6154..0626a7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -745,6 +745,7 @@ name = "credentialsd" version = "0.2.0" dependencies = [ "async-stream", + "async-trait", "base64", "credentialsd-common", "futures-lite", diff --git a/credentialsd/Cargo.toml b/credentialsd/Cargo.toml index af5a9a9..07b3d04 100644 --- a/credentialsd/Cargo.toml +++ b/credentialsd/Cargo.toml @@ -7,6 +7,7 @@ license = "LGPL-3.0-only" [dependencies] async-stream = "0.3.6" +async-trait = "0.1.89" base64 = "0.22.1" credentialsd-common = { path = "../credentialsd-common" } futures-lite.workspace = true diff --git a/credentialsd/src/dbus/flow_control.rs b/credentialsd/src/dbus/flow_control.rs index 737ac35..8a1450d 100644 --- a/credentialsd/src/dbus/flow_control.rs +++ b/credentialsd/src/dbus/flow_control.rs @@ -1,9 +1,9 @@ //! This module implements the service to allow the user to control the flow of //! the credential request through the trusted UI. -use std::future::Future; use std::{collections::VecDeque, fmt::Debug, sync::Arc}; +use async_trait::async_trait; use credentialsd_common::model::{ BackgroundEvent, Device, Error as CredentialServiceError, RequestingApplication, WebAuthnError, }; @@ -341,13 +341,14 @@ enum SignalState { Active, } +#[async_trait] pub trait CredentialRequestController { - fn request_credential( + async fn request_credential( &self, requesting_app: Option, request: CredentialRequest, window_handle: Option, - ) -> impl Future> + Send; + ) -> Result; } pub struct CredentialRequestControllerClient { @@ -359,6 +360,7 @@ pub struct CredentialRequestControllerClient { )>, } +#[async_trait] impl CredentialRequestController for CredentialRequestControllerClient { async fn request_credential( &self, diff --git a/credentialsd/src/gateway/dbus.rs b/credentialsd/src/gateway/dbus.rs index 3d33c3d..b2e5d8e 100644 --- a/credentialsd/src/gateway/dbus.rs +++ b/credentialsd/src/gateway/dbus.rs @@ -17,40 +17,36 @@ use credentialsd_common::{ }, }; -use crate::{ - dbus::CredentialRequestController, - webauthn::{AppId, NavigationContext, Origin}, -}; +use crate::webauthn::{AppId, NavigationContext, Origin}; use super::{ check_origin_from_app, check_origin_from_privileged_client, get_app_info_from_pid, - handle_create_credential, handle_get_client_capabilities, handle_get_credential, - CredentialGateway, + GatewayService, }; pub const SERVICE_NAME: &str = "xyz.iinuwa.credentialsd.Credentials"; pub const SERVICE_PATH: &str = "/xyz/iinuwa/credentialsd/Credentials"; -pub(super) async fn start_dbus_gateway( - controller: C, +pub(super) async fn start_dbus_gateway( + svc: Arc>, ) -> Result { zbus::connection::Builder::session() .inspect_err(|err| { tracing::error!("Failed to connect to D-Bus session: {err}"); })? .name(SERVICE_NAME)? - .serve_at( - SERVICE_PATH, - CredentialGateway { - controller: Arc::new(AsyncMutex::new(controller)), - }, - )? + .serve_at(SERVICE_PATH, CredentialGateway { svc: svc.clone() })? .build() .await } + +struct CredentialGateway { + svc: Arc>, +} + /// These are public methods that can be called by arbitrary clients to begin a credential flow. #[interface(name = "xyz.iinuwa.credentialsd.Credentials1")] -impl super::CredentialGateway { +impl CredentialGateway { async fn create_credential( &self, #[zbus(header)] header: Header<'_>, @@ -86,14 +82,17 @@ impl super::CredentialGa let request_environment = check_origin_from_privileged_client(origin, top_origin)?; // Find out where this request is coming from (which application is requesting this) let requesting_app = query_connection_peer_binary(header, connection).await; - let response = handle_create_credential( - &self.controller, - request, - request_environment, - requesting_app, - parent_window.into(), - ) - .await?; + let response = self + .svc + .lock() + .await + .handle_create_credential( + request, + request_environment, + requesting_app, + parent_window.into(), + ) + .await?; Ok(response) } @@ -132,19 +131,22 @@ impl super::CredentialGa let request_environment = check_origin_from_privileged_client(origin, top_origin)?; // Find out where this request is coming from (which application is requesting this) let requesting_app = query_connection_peer_binary(header, connection).await; - let response = handle_get_credential( - &self.controller, - request, - request_environment, - requesting_app, - parent_window.into(), - ) - .await?; + let response = self + .svc + .lock() + .await + .handle_get_credential( + request, + request_environment, + requesting_app, + parent_window.into(), + ) + .await?; Ok(response) } async fn get_client_capabilities(&self) -> fdo::Result { - let capabilities = handle_get_client_capabilities(); + let capabilities = self.svc.lock().await.handle_get_client_capabilities(); Ok(capabilities) } } diff --git a/credentialsd/src/gateway/mod.rs b/credentialsd/src/gateway/mod.rs index 6f59066..659f27b 100644 --- a/credentialsd/src/gateway/mod.rs +++ b/credentialsd/src/gateway/mod.rs @@ -29,133 +29,140 @@ use util::{ pub async fn start_gateway( controller: C, ) -> Result { - dbus::start_dbus_gateway(controller).await + let svc = Arc::new(AsyncMutex::new(GatewayService { + request_controller: Box::new(controller), + })); + dbus::start_dbus_gateway(svc).await } -struct CredentialGateway { - controller: Arc>, +struct GatewayService { + request_controller: Box, } -async fn handle_create_credential( - controller: &AsyncMutex, - request: CreateCredentialRequest, - request_environment: NavigationContext, - requesting_app: Option, - parent_window: Option, -) -> Result { - if let ("publicKey", Some(_)) = (request.r#type.as_ref(), &request.public_key) { - // TODO: assert that RP ID is bound to origin: - // - if RP ID is not set, set the RP ID to the origin's effective domain - // - if RP ID is set, assert that it matches origin's effective domain - // - if RP ID is set, but origin's effective domain doesn't match - // - query for related origins, if supported - // - fail if not supported, or if RP ID doesn't match any related origins. - let (make_cred_request, client_data_json) = - create_credential_request_try_into_ctap2(&request, &request_environment).inspect_err( - |_| { - tracing::error!("Could not parse passkey creation request. Rejecting request."); - }, - )?; - if make_cred_request.algorithms.is_empty() { - tracing::info!("No supported algorithms given in request. Rejecting request."); - return Err(WebAuthnError::NotSupportedError); - } - let cred_request = CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request); - - let response = controller - .lock() - .await - .request_credential(requesting_app, cred_request, parent_window) - .await?; - - if let CredentialResponse::CreatePublicKeyCredentialResponse(cred_response) = response { - let public_key_response = - create_credential_response_try_from_ctap2(&cred_response, client_data_json) - .map_err(|err| { +impl GatewayService { + async fn handle_create_credential( + &self, + request: CreateCredentialRequest, + request_environment: NavigationContext, + requesting_app: Option, + parent_window: Option, + ) -> Result { + if let ("publicKey", Some(_)) = (request.r#type.as_ref(), &request.public_key) { + // TODO: assert that RP ID is bound to origin: + // - if RP ID is not set, set the RP ID to the origin's effective domain + // - if RP ID is set, assert that it matches origin's effective domain + // - if RP ID is set, but origin's effective domain doesn't match + // - query for related origins, if supported + // - fail if not supported, or if RP ID doesn't match any related origins. + let (make_cred_request, client_data_json) = + create_credential_request_try_into_ctap2(&request, &request_environment) + .inspect_err(|_| { tracing::error!( - "Failed to parse credential response from authenticator: {err}" + "Could not parse passkey creation request. Rejecting request." ); - // Using NotAllowedError as a catch-all error. - WebAuthnError::NotAllowedError })?; - Ok(public_key_response.into()) + if make_cred_request.algorithms.is_empty() { + tracing::info!("No supported algorithms given in request. Rejecting request."); + return Err(WebAuthnError::NotSupportedError); + } + let cred_request = + CredentialRequest::CreatePublicKeyCredentialRequest(make_cred_request); + + let response = self + .request_controller + .request_credential(requesting_app, cred_request, parent_window) + .await?; + + if let CredentialResponse::CreatePublicKeyCredentialResponse(cred_response) = response { + let public_key_response = + create_credential_response_try_from_ctap2(&cred_response, client_data_json) + .map_err(|err| { + tracing::error!( + "Failed to parse credential response from authenticator: {err}" + ); + // Using NotAllowedError as a catch-all error. + WebAuthnError::NotAllowedError + })?; + Ok(public_key_response.into()) + } else { + // TODO: is response safe to log here? + // tracing::error!("Expected create public key credential response, received {response:?}"); + tracing::error!("Did not receive expected create public key credential response."); + // Using NotAllowedError as a catch-all error. + Err(WebAuthnError::NotAllowedError) + } } else { - // TODO: is response safe to log here? - // tracing::error!("Expected create public key credential response, received {response:?}"); - tracing::error!("Did not receive expected create public key credential response."); - // Using NotAllowedError as a catch-all error. - Err(WebAuthnError::NotAllowedError) + tracing::error!("Unknown credential type request: {}", request.r#type); + Err(WebAuthnError::TypeError) } - } else { - tracing::error!("Unknown credential type request: {}", request.r#type); - Err(WebAuthnError::TypeError) } -} -async fn handle_get_credential( - controller: &AsyncMutex, - request: GetCredentialRequest, - request_environment: NavigationContext, - requesting_app: Option, - parent_window: Option, -) -> Result { - if let ("publicKey", Some(_)) = (request.r#type.as_ref(), &request.public_key) { - // Setup request + async fn handle_get_credential( + &self, + request: GetCredentialRequest, + request_environment: NavigationContext, + requesting_app: Option, + parent_window: Option, + ) -> Result { + if let ("publicKey", Some(_)) = (request.r#type.as_ref(), &request.public_key) { + // Setup request - // TODO: assert that RP ID is bound to origin: - // - if RP ID is not set, set the RP ID to the origin's effective domain - // - if RP ID is set, assert that it matches origin's effective domain - // - if RP ID is set, but origin's effective domain doesn't match - // - query for related origins, if supported - // - fail if not supported, or if RP ID doesn't match any related origins. - let (get_cred_request, client_data_json) = - get_credential_request_try_into_ctap2(&request, &request_environment).map_err(|e| { - tracing::error!("Could not parse passkey assertion request: {e:?}"); - WebAuthnError::TypeError - })?; - let cred_request = CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request); + // TODO: assert that RP ID is bound to origin: + // - if RP ID is not set, set the RP ID to the origin's effective domain + // - if RP ID is set, assert that it matches origin's effective domain + // - if RP ID is set, but origin's effective domain doesn't match + // - query for related origins, if supported + // - fail if not supported, or if RP ID doesn't match any related origins. + let (get_cred_request, client_data_json) = + get_credential_request_try_into_ctap2(&request, &request_environment).map_err( + |e| { + tracing::error!("Could not parse passkey assertion request: {e:?}"); + WebAuthnError::TypeError + }, + )?; + let cred_request = CredentialRequest::GetPublicKeyCredentialRequest(get_cred_request); - let response = controller - .lock() - .await - .request_credential(requesting_app, cred_request, parent_window) - .await?; + let response = self + .request_controller + .request_credential(requesting_app, cred_request, parent_window) + .await?; - if let CredentialResponse::GetPublicKeyCredentialResponse(cred_response) = response { - let public_key_response = get_credential_response_try_from_ctap2( - &cred_response, - client_data_json, - ) - .map_err(|err| { - tracing::error!("Failed to parse credential response from authenticator: {err}"); + if let CredentialResponse::GetPublicKeyCredentialResponse(cred_response) = response { + let public_key_response = + get_credential_response_try_from_ctap2(&cred_response, client_data_json) + .map_err(|err| { + tracing::error!( + "Failed to parse credential response from authenticator: {err}" + ); + // Using NotAllowedError as a catch-all error. + WebAuthnError::NotAllowedError + })?; + Ok(public_key_response.into()) + } else { + // TODO: is response safe to log here? + // tracing::error!("Expected get public key credential response, received {response:?}"); + tracing::error!("Did not receive expected get public key credential response."); // Using NotAllowedError as a catch-all error. - WebAuthnError::NotAllowedError - })?; - Ok(public_key_response.into()) + Err(WebAuthnError::NotAllowedError) + } } else { - // TODO: is response safe to log here? - // tracing::error!("Expected get public key credential response, received {response:?}"); - tracing::error!("Did not receive expected get public key credential response."); - // Using NotAllowedError as a catch-all error. - Err(WebAuthnError::NotAllowedError) + tracing::error!("Unknown credential type request: {}", request.r#type); + Err(WebAuthnError::TypeError) } - } else { - tracing::error!("Unknown credential type request: {}", request.r#type); - Err(WebAuthnError::TypeError) } -} -fn handle_get_client_capabilities() -> GetClientCapabilitiesResponse { - GetClientCapabilitiesResponse { - conditional_create: false, - conditional_get: false, - hybrid_transport: true, - passkey_platform_authenticator: false, - user_verifying_platform_authenticator: false, - related_origins: false, - signal_all_accepted_credentials: false, - signal_current_user_details: false, - signal_unknown_credential: false, + fn handle_get_client_capabilities(&self) -> GetClientCapabilitiesResponse { + GetClientCapabilitiesResponse { + conditional_create: false, + conditional_get: false, + hybrid_transport: true, + passkey_platform_authenticator: false, + user_verifying_platform_authenticator: false, + related_origins: false, + signal_all_accepted_credentials: false, + signal_current_user_details: false, + signal_unknown_credential: false, + } } } diff --git a/credentialsd/src/webauthn.rs b/credentialsd/src/webauthn.rs index eeb6985..905fccf 100644 --- a/credentialsd/src/webauthn.rs +++ b/credentialsd/src/webauthn.rs @@ -247,18 +247,14 @@ impl TryFrom<&CredentialDescriptor> for Ctap2PublicKeyCredentialDescriptor { let transports = value.transports.as_ref().filter(|t| !t.is_empty()); let transports = match transports { Some(transports) => { - let mut transport_list = transports.iter().map(|t| match t.as_ref() { + let transport_list = transports.iter().map(|t| match t.as_ref() { "ble" => Some(Ctap2Transport::Ble), + "hybrid" => Some(Ctap2Transport::Hybrid), + "internal" => Some(Ctap2Transport::Internal), "nfc" => Some(Ctap2Transport::Nfc), "usb" => Some(Ctap2Transport::Usb), - "internal" => Some(Ctap2Transport::Internal), _ => None, }); - if transport_list.any(|t| t.is_none()) { - return Err(Error::Internal( - "Invalid transport type specified".to_owned(), - )); - } transport_list.collect() } None => None, From b177ddbcabfe66c53b5e0fda6403a953865e2f53 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Fri, 13 Mar 2026 07:53:57 -0500 Subject: [PATCH 7/7] daemon: Add some documentation to D-Bus gateway service --- credentialsd/src/dbus/flow_control.rs | 2 ++ credentialsd/src/gateway/dbus.rs | 28 +++++++++++++++++++++------ credentialsd/src/gateway/mod.rs | 4 ++++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/credentialsd/src/dbus/flow_control.rs b/credentialsd/src/dbus/flow_control.rs index 8a1450d..a6ed555 100644 --- a/credentialsd/src/dbus/flow_control.rs +++ b/credentialsd/src/dbus/flow_control.rs @@ -341,6 +341,8 @@ enum SignalState { Active, } +/// Coordinates between user and various devices connected to the machine to +/// fulfill credential requests. #[async_trait] pub trait CredentialRequestController { async fn request_credential( diff --git a/credentialsd/src/gateway/dbus.rs b/credentialsd/src/gateway/dbus.rs index b2e5d8e..8bf7e7d 100644 --- a/credentialsd/src/gateway/dbus.rs +++ b/credentialsd/src/gateway/dbus.rs @@ -35,16 +35,28 @@ pub(super) async fn start_dbus_gateway( tracing::error!("Failed to connect to D-Bus session: {err}"); })? .name(SERVICE_NAME)? - .serve_at(SERVICE_PATH, CredentialGateway { svc: svc.clone() })? + .serve_at( + SERVICE_PATH, + CredentialGateway { + gateway_service: svc.clone(), + }, + )? .build() .await } +/// Struct to hold state for the D-Bus interface. struct CredentialGateway { - svc: Arc>, + /// Service responsible for processing credential requests. + gateway_service: Arc>, } -/// These are public methods that can be called by arbitrary clients to begin a credential flow. +/// These are public methods that can be called by arbitrary clients to begin a +/// credential flow. +/// +/// The D-Bus interface is responsible for authorizing the client and collecting +/// the contextual information about the client to pass onto the GatewayService +/// for evaluation. #[interface(name = "xyz.iinuwa.credentialsd.Credentials1")] impl CredentialGateway { async fn create_credential( @@ -83,7 +95,7 @@ impl CredentialGateway { // Find out where this request is coming from (which application is requesting this) let requesting_app = query_connection_peer_binary(header, connection).await; let response = self - .svc + .gateway_service .lock() .await .handle_create_credential( @@ -132,7 +144,7 @@ impl CredentialGateway { // Find out where this request is coming from (which application is requesting this) let requesting_app = query_connection_peer_binary(header, connection).await; let response = self - .svc + .gateway_service .lock() .await .handle_get_credential( @@ -146,7 +158,11 @@ impl CredentialGateway { } async fn get_client_capabilities(&self) -> fdo::Result { - let capabilities = self.svc.lock().await.handle_get_client_capabilities(); + let capabilities = self + .gateway_service + .lock() + .await + .handle_get_client_capabilities(); Ok(capabilities) } } diff --git a/credentialsd/src/gateway/mod.rs b/credentialsd/src/gateway/mod.rs index 659f27b..6c06f70 100644 --- a/credentialsd/src/gateway/mod.rs +++ b/credentialsd/src/gateway/mod.rs @@ -35,7 +35,11 @@ pub async fn start_gateway, }