From 5827b9e8200c3a34394280388775cd72b4b95d5f Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus Date: Tue, 20 May 2025 15:40:12 +0200 Subject: [PATCH 1/5] Implement getClientCapabilities (all false for now) --- ...z.iinuwa.credentials.CredentialManager.xml | 3 ++ webext/add-on/background.js | 16 +++++++---- webext/add-on/content.js | 12 ++++++++ webext/app/credential_manager_shim.py | 13 +++++++-- .../src/dbus.rs | 28 +++++++++++++++++++ 5 files changed, 64 insertions(+), 8 deletions(-) diff --git a/contrib/xyz.iinuwa.credentials.CredentialManager.xml b/contrib/xyz.iinuwa.credentials.CredentialManager.xml index 534c798..056fcd8 100644 --- a/contrib/xyz.iinuwa.credentials.CredentialManager.xml +++ b/contrib/xyz.iinuwa.credentials.CredentialManager.xml @@ -16,6 +16,9 @@ + + + diff --git a/webext/add-on/background.js b/webext/add-on/background.js index 9c22889..863ad50 100644 --- a/webext/add-on/background.js +++ b/webext/add-on/background.js @@ -36,12 +36,16 @@ function rcvFromContent(msg) { // const isCrossOrigin = origin === topOrigin // const isTopLevel = contentPort.sender.frameId === 0; - - const serializedOptions = serializeRequest(options) - - console.debug(options.publicKey.challenge) - console.debug("background script received options, passing onto native app") - nativePort.postMessage({ requestId, cmd, options: serializedOptions, origin, topOrigin }) + if (options) { + const serializedOptions = serializeRequest(options) + + console.debug(options.publicKey.challenge) + console.debug("background script received options, passing onto native app") + nativePort.postMessage({ requestId, cmd, options: serializedOptions, origin, topOrigin }) + } else { + console.debug("background script received message without arguments, passing onto native app") + nativePort.postMessage({ requestId, cmd, origin, topOrigin }) + } } function rcvFromNative(msg) { diff --git a/webext/add-on/content.js b/webext/add-on/content.js index 2ff5111..2134540 100644 --- a/webext/add-on/content.js +++ b/webext/add-on/content.js @@ -14,6 +14,11 @@ exportFunction(createCredential, navigator.credentials, { defineAs: "create"}) exportFunction(getCredential, navigator.credentials, { defineAs: "get"}) +if (window.PublicKeyCredential) { + console.log("overriding PublicKeyCredential.getClientCapabilities() in content script"); + exportFunction(getClientCapabilities, PublicKeyCredential, { defineAs: "getClientCapabilities"}) +} + function startRequest() { const requestId = requestCounter++; const {promise, resolve, reject } = window.Promise.withResolvers(); @@ -182,3 +187,10 @@ function getCredential(request) { webauthnPort.postMessage({ requestId, cmd: 'get', options, }) return promise.then(cloneCredentialResponse) }; + +function getClientCapabilities() { + console.log("forwarding getClientCapabilities call from content script to background script") + const { requestId, promise } = startRequest(); + webauthnPort.postMessage({ requestId, cmd: 'getClientCapabilities', }) + return promise +}; diff --git a/webext/app/credential_manager_shim.py b/webext/app/credential_manager_shim.py index 2f903bf..109cc55 100755 --- a/webext/app/credential_manager_shim.py +++ b/webext/app/credential_manager_shim.py @@ -343,7 +343,7 @@ async def run(cmd, options, origin, top_origin): interface = proxy_object.get_interface( 'xyz.iinuwa.credentials.CredentialManagerUi1') - logging.debug(f"COnnected to interface at {interface.path}") + logging.debug(f"Connected to interface at {interface.path}") if cmd == 'create': if 'publicKey' in options: @@ -355,6 +355,12 @@ async def run(cmd, options, origin, top_origin): return await get_passkey(interface, options['publicKey'], origin, top_origin) else: raise Exception(f"Could not get unknown credential type: {options.keys()[0]}") + elif cmd == 'getClientCapabilities': + rsp = await interface.call_get_client_capabilities() + response = {} + for name, val in rsp.items(): + response[name] = val.value + return response else: raise Exception(f"unknown cmd: {cmd}") @@ -366,7 +372,10 @@ async def run(cmd, options, origin, top_origin): request_id = receivedMessage['requestId'] try: cmd = receivedMessage['cmd'] - options = receivedMessage['options'] + + options = None + if 'options' in receivedMessage: + options = receivedMessage['options'] origin = receivedMessage['origin'] top_origin = receivedMessage['topOrigin'] loop = asyncio.get_event_loop() diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index 2ba2572..bf1bb1b 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -241,6 +241,20 @@ impl CredentialManager { )) } } + + async fn get_client_capabilities(&self) -> fdo::Result { + Ok(GetClientCapabilitiesResponse { + conditional_create: false, + conditional_get: false, + hybrid_transport: false, + 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 create_password( @@ -838,6 +852,20 @@ impl From for GetCredentialResponse { } } +#[derive(SerializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct GetClientCapabilitiesResponse { + conditional_create: bool, + conditional_get: bool, + hybrid_transport: bool, + passkey_platform_authenticator: bool, + user_verifying_platform_authenticator: bool, + related_origins: bool, + signal_all_accepted_credentials: bool, + signal_current_user_details: bool, + signal_unknown_credential: bool, +} + fn format_client_data_json( op: Operation, challenge: &str, From 2625bf2b25f5700567bec09bf2561acdb754c72a Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 20 May 2025 23:31:27 -0500 Subject: [PATCH 2/5] Use camelCase property names on capabilities --- xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs index bf1bb1b..a43b006 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/dbus.rs @@ -853,7 +853,7 @@ impl From for GetCredentialResponse { } #[derive(SerializeDict, Type)] -#[zvariant(signature = "dict")] +#[zvariant(signature = "dict", rename_all = "camelCase")] pub struct GetClientCapabilitiesResponse { conditional_create: bool, conditional_get: bool, From dcfc6190526f420179c6bd4ab8eba9b456d49ed3 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 20 May 2025 23:32:03 -0500 Subject: [PATCH 3/5] Clone response from capabilities object --- webext/add-on/content.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webext/add-on/content.js b/webext/add-on/content.js index 2134540..7ddcb6a 100644 --- a/webext/add-on/content.js +++ b/webext/add-on/content.js @@ -192,5 +192,5 @@ function getClientCapabilities() { console.log("forwarding getClientCapabilities call from content script to background script") const { requestId, promise } = startRequest(); webauthnPort.postMessage({ requestId, cmd: 'getClientCapabilities', }) - return promise + return promise.then((capabilities) => cloneInto(capabilities, window)) }; From 9f3f2a75ff94245b7801116321ae2c1b23a2bd3e Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 20 May 2025 23:32:33 -0500 Subject: [PATCH 4/5] Fix cargo test build --- .../src/platform_authenticator/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xyz-iinuwa-credential-manager-portal-gtk/src/platform_authenticator/mod.rs b/xyz-iinuwa-credential-manager-portal-gtk/src/platform_authenticator/mod.rs index 6b401a9..8f49c64 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/src/platform_authenticator/mod.rs +++ b/xyz-iinuwa-credential-manager-portal-gtk/src/platform_authenticator/mod.rs @@ -715,7 +715,7 @@ mod test { PublicKeyCredentialParameters, PublicKeyCredentialType, }; - use super::sign_attestation; + use super::{create_attested_credential_data, create_authenticator_data, sign_attestation}; #[test] fn test_attestation() { From fa217040806eab3996d678c99fbcf7a34131d4d4 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 21 May 2025 01:07:09 -0500 Subject: [PATCH 5/5] Add D-Bus tests --- .../.gitignore | 1 + .../meson.build | 1 + .../tests/config/mod.rs.in | 4 ++ .../tests/dbus.rs | 70 +++++++++++++++++++ .../tests/meson.build | 43 ++++++++++++ .../xyz.iinuwa.CredentialManagerUi.service.in | 3 + 6 files changed, 122 insertions(+) create mode 100644 xyz-iinuwa-credential-manager-portal-gtk/tests/config/mod.rs.in create mode 100644 xyz-iinuwa-credential-manager-portal-gtk/tests/dbus.rs create mode 100644 xyz-iinuwa-credential-manager-portal-gtk/tests/meson.build create mode 100644 xyz-iinuwa-credential-manager-portal-gtk/tests/services/xyz.iinuwa.CredentialManagerUi.service.in diff --git a/xyz-iinuwa-credential-manager-portal-gtk/.gitignore b/xyz-iinuwa-credential-manager-portal-gtk/.gitignore index f0c26c5..95d99ef 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/.gitignore +++ b/xyz-iinuwa-credential-manager-portal-gtk/.gitignore @@ -1 +1,2 @@ src/config.rs +tests/config/mod.rs \ No newline at end of file diff --git a/xyz-iinuwa-credential-manager-portal-gtk/meson.build b/xyz-iinuwa-credential-manager-portal-gtk/meson.build index a39e43f..50339cd 100644 --- a/xyz-iinuwa-credential-manager-portal-gtk/meson.build +++ b/xyz-iinuwa-credential-manager-portal-gtk/meson.build @@ -65,6 +65,7 @@ endif subdir('data') subdir('po') subdir('src') +subdir('tests') gnome.post_install( gtk_update_icon_cache: true, diff --git a/xyz-iinuwa-credential-manager-portal-gtk/tests/config/mod.rs.in b/xyz-iinuwa-credential-manager-portal-gtk/tests/config/mod.rs.in new file mode 100644 index 0000000..b4e7d96 --- /dev/null +++ b/xyz-iinuwa-credential-manager-portal-gtk/tests/config/mod.rs.in @@ -0,0 +1,4 @@ +pub const SERVICE_DIR: &'static str = @SERVICE_DIR@; +pub const SERVICE_NAME: &'static str = "xyz.iinuwa.credentials.CredentialManagerUi"; +pub const PATH: &'static str = "/xyz/iinuwa/credentials/CredentialManagerUi"; +pub const INTERFACE: &'static str = "xyz.iinuwa.credentials.CredentialManagerUi1"; diff --git a/xyz-iinuwa-credential-manager-portal-gtk/tests/dbus.rs b/xyz-iinuwa-credential-manager-portal-gtk/tests/dbus.rs new file mode 100644 index 0000000..b65918c --- /dev/null +++ b/xyz-iinuwa-credential-manager-portal-gtk/tests/dbus.rs @@ -0,0 +1,70 @@ +mod config; + +use std::collections::HashMap; + +use client::DbusClient; +use zbus::zvariant::Value; + +#[test] +fn test_client_capabilities() { + let client = DbusClient::new(); + let msg = client.call_method("GetClientCapabilities", &()).unwrap(); + let body = msg.body(); + let rsp: HashMap = body.deserialize().unwrap(); + + let capabilities = HashMap::from([ + ("conditionalCreate", false), + ("conditionalGet", false), + ("hybridTransport", false), + ("passkeyPlatformAuthenticator", false), + ("userVerifyingPlatformAuthenticator", false), + ("relatedOrigins", false), + ("signalAllAcceptedCredentials", false), + ("signalCurrentUserDetails", false), + ("signalUnknownCredential", false), + ]); + for (key, expected) in capabilities.iter() { + let value: &Value = rsp.get(*key).unwrap(); + assert_eq!(*expected, value.try_into().unwrap()); + } +} + +mod client { + use crate::config::{INTERFACE, PATH, SERVICE_DIR, SERVICE_NAME}; + use gtk::gio::{TestDBus, TestDBusFlags}; + use serde::Serialize; + use zbus::{blocking::Connection, zvariant::DynamicType, Message}; + + fn init_test_dbus() -> TestDBus { + let dbus = TestDBus::new(TestDBusFlags::NONE); + + // assumes this runs in root of Cargo project. + let current_dir = std::env::current_dir().unwrap(); + let service_dir = current_dir.join(SERVICE_DIR); + println!("{:?}", service_dir); + dbus.add_service_dir(service_dir.to_str().unwrap()); + + dbus.up(); + dbus + } + + pub(super) struct DbusClient { + _bus: TestDBus, + } + + impl DbusClient { + pub fn new() -> Self { + Self { + _bus: init_test_dbus(), + } + } + + pub fn call_method(&self, method_name: &str, body: &B) -> zbus::Result + where + B: Serialize + DynamicType, + { + let connection = Connection::session().unwrap(); + connection.call_method(Some(SERVICE_NAME), PATH, Some(INTERFACE), method_name, body) + } + } +} diff --git a/xyz-iinuwa-credential-manager-portal-gtk/tests/meson.build b/xyz-iinuwa-credential-manager-portal-gtk/tests/meson.build new file mode 100644 index 0000000..549b598 --- /dev/null +++ b/xyz-iinuwa-credential-manager-portal-gtk/tests/meson.build @@ -0,0 +1,43 @@ +test_config = configuration_data() +test_config.set_quoted( + 'SERVICE_DIR', + meson.project_build_root() / backend_executable_name / 'tests', +) +test_config.set( + 'DBUS_EXECUTABLE', + meson.project_build_root() / backend_executable_name / 'src' / backend_executable_name, +) +configure_file( + input: 'config' / 'mod.rs.in', + output: 'config.rs', + configuration: test_config, +) + +# Copy the config output to the source directory. +run_command( + 'cp', + meson.project_build_root() / backend_executable_name / 'tests' / 'config.rs', + meson.project_source_root() / backend_executable_name / 'tests' / 'config' / 'mod.rs', + check: true, +) + +configure_file( + input: 'services' / 'xyz.iinuwa.CredentialManagerUi.service.in', + output: 'xyz.iinuwa.CredentialManagerUi.service', + configuration: test_config, +) + +test( + 'cargo dbus tests', + cargo, + env: [cargo_env], + args: [ + 'test', + '--test', 'dbus', + '--no-fail-fast', cargo_options, + '--', + '--nocapture', + ], + protocol: 'exitcode', + verbose: true, +) \ No newline at end of file diff --git a/xyz-iinuwa-credential-manager-portal-gtk/tests/services/xyz.iinuwa.CredentialManagerUi.service.in b/xyz-iinuwa-credential-manager-portal-gtk/tests/services/xyz.iinuwa.CredentialManagerUi.service.in new file mode 100644 index 0000000..9fc84ea --- /dev/null +++ b/xyz-iinuwa-credential-manager-portal-gtk/tests/services/xyz.iinuwa.CredentialManagerUi.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=xyz.iinuwa.credentials.CredentialManagerUi +Exec=@DBUS_EXECUTABLE@