File webauthn_xdg_portal.patch of Package MozillaFirefox

Index: firefox-141.0.2/Cargo.lock
===================================================================
--- firefox-141.0.2.orig/Cargo.lock
+++ firefox-141.0.2/Cargo.lock
@@ -2654,6 +2654,7 @@ dependencies = [
  "wgpu_bindings",
  "widget_windows",
  "wpf-gpu-raster",
+ "xdg_portal_auth_service",
  "xpcom",
 ]
 
@@ -7947,6 +7948,26 @@ dependencies = [
 ]
 
 [[package]]
+name = "xdg_portal_auth_service"
+version = "0.1.0"
+dependencies = [
+ "base64 0.22.1",
+ "cstr",
+ "dbus",
+ "log",
+ "moz_task",
+ "nserror",
+ "nsstring",
+ "rand",
+ "serde",
+ "serde_cbor",
+ "serde_json",
+ "static_prefs",
+ "thin-vec",
+ "xpcom",
+]
+
+[[package]]
 name = "zeitstempel"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
Index: firefox-141.0.2/dom/webauthn/AuthrsBridge_ffi.h
===================================================================
--- firefox-141.0.2.orig/dom/webauthn/AuthrsBridge_ffi.h
+++ firefox-141.0.2/dom/webauthn/AuthrsBridge_ffi.h
@@ -20,6 +20,7 @@ nsresult authrs_webauthn_att_obj_constru
     const nsTArray<uint8_t>& attestation, bool anonymize,
     nsIWebAuthnAttObj** result);
 
+nsresult xdg_portal_auth_service_if_available(nsIWebAuthnService** result);
 }  // extern "C"
 
 #endif  // mozilla_dom_AuthrsBridge_ffi_h
Index: firefox-141.0.2/dom/webauthn/WebAuthnService.h
===================================================================
--- firefox-141.0.2.orig/dom/webauthn/WebAuthnService.h
+++ firefox-141.0.2/dom/webauthn/WebAuthnService.h
@@ -48,8 +48,17 @@ class WebAuthnService final : public nsI
     if (!mPlatformService) {
       mPlatformService = mAuthrsService;
     }
+#elif defined(MOZ_WIDGET_GTK)
+
+    if (StaticPrefs::security_webauth_webauthn_enable_xdg_portal()) {
+      xdg_portal_auth_service_if_available(getter_AddRefs(mPlatformService));
+    }
+
+    if (!mPlatformService) {
+      mPlatformService = mAuthrsService;
+    }
 #else
-    mPlatformService = mAuthrsService;
+      mPlatformService = mAuthrsService;
 #endif
   }
 
@@ -72,7 +81,7 @@ class WebAuthnService final : public nsI
   void ResetLocked(const TransactionStateMutex::AutoLock& aGuard);
 
   nsIWebAuthnService* DefaultService() {
-    if (StaticPrefs::security_webauth_webauthn_enable_softtoken()) {
+    if (StaticPrefs::security_webauth_webauthn_enable_softtoken() && !StaticPrefs::security_webauth_webauthn_enable_xdg_portal()) {
       return mAuthrsService;
     }
     return mPlatformService;
Index: firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/Cargo.toml
===================================================================
--- /dev/null
+++ firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "xdg_portal_auth_service"
+version = "0.1.0"
+edition = "2021"
+authors = ["Martin Sirringhaus", "John Schanck"]
+
+[dependencies]
+base64 = "^0.22"
+cstr = "0.2"
+log = "0.4"
+moz_task = { path = "../../../xpcom/rust/moz_task" }
+nserror = { path = "../../../xpcom/rust/nserror" }
+nsstring = { path = "../../../xpcom/rust/nsstring" }
+rand = "0.8"
+serde = { version = "1", features = ["derive"] }
+serde_cbor = "0.11"
+serde_json = "1.0"
+static_prefs = { path = "../../../modules/libpref/init/static_prefs" }
+thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
+xpcom = { path = "../../../xpcom/rust/xpcom" }
+dbus = "0.6.5"
+
+[features]
+fuzzing = []
Index: firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/src/dbus_controller.rs
===================================================================
--- /dev/null
+++ firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/src/dbus_controller.rs
@@ -0,0 +1,119 @@
+use super::NS_ERROR_DOM_NOT_ALLOWED_ERR;
+use dbus::{
+    arg::{IterAppend, RefArg, Variant},
+    BusType, Connection, Error, Message,
+};
+use nserror::nsresult;
+use std::collections::HashMap;
+
+use crate::{register::RegisterResult, sign::SignResult};
+
+pub(crate) struct DbusController {
+    conn: Connection,
+}
+
+impl DbusController {
+    const SERVICE_NAME: &'static str = "xyz.iinuwa.credentialsd.Credentials";
+    const PATH: &'static str = "/xyz/iinuwa/credentialsd/Credentials";
+    const INTERFACE: &'static str = "xyz.iinuwa.credentialsd.Credentials1";
+    const CREATE_CREDENTIAL_FUNCTION: &'static str = "CreateCredential";
+    const GET_CREDENTIAL_FUNCTION: &'static str = "GetCredential";
+
+    pub(crate) fn new() -> Result<Self, Error> {
+        let conn = Connection::get_private(BusType::Session)?;
+        Ok(Self { conn })
+    }
+
+    pub(crate) fn is_service_active(&self) -> bool {
+        // Create a proxy for the D-Bus daemon itself.
+        let proxy = self
+            .conn
+            .with_path("org.freedesktop.DBus", "/org/freedesktop/DBus", 5000);
+
+        // First, check if the portal is already running, by calling the "NameHasOwner" method.
+        let m = match proxy.method_call_with_args(
+            &"org.freedesktop.DBus".into(),
+            &"NameHasOwner".into(),
+            |msg| {
+                let mut i = IterAppend::new(msg);
+                i.append(Self::SERVICE_NAME);
+            },
+        ) {
+            Ok(m) => m,
+            Err(e) => {
+                log::info!("Failed to send NameHasOwner via D-Bus: {e:?}");
+                return false;
+            }
+        };
+
+        let has_owner: Option<bool> = m.get1();
+        let is_running = has_owner.unwrap_or_default();
+
+        if is_running {
+            return true;
+        }
+
+        // If it's not running, check if it is activatable
+        let m = match proxy.method_call_with_args(
+            &"org.freedesktop.DBus".into(),
+            &"ListActivatableNames".into(),
+            |_| { /* Nothing to do */ },
+        ) {
+            Ok(m) => m,
+            Err(e) => {
+                log::info!("Failed to send ListActivatableNames via D-Bus: {e:?}");
+                return false;
+            }
+        };
+
+        let activatable_services: Option<Vec<String>> = m.get1();
+        let services = activatable_services.unwrap_or_default();
+        services.iter().any(|name| name == Self::SERVICE_NAME)
+    }
+
+    pub(crate) fn send_message(
+        &self,
+        function: &str,
+        msg: HashMap<String, Variant<Box<dyn RefArg>>>,
+    ) -> Result<Message, Error> {
+        let m = Message::new_method_call(Self::SERVICE_NAME, Self::PATH, Self::INTERFACE, function)
+            // I don't know why this method returns a string instead of a DBUS-Error...
+            // Let's wrap it
+            .map_err(|e| Error::new_custom("new_method_call", &e))?
+            .append1(msg);
+
+        self.conn.send_with_reply_and_block(m, 300000)
+    }
+
+    pub(crate) fn send_create_credential(
+        &self,
+        msg: HashMap<String, Variant<Box<dyn RefArg>>>,
+    ) -> Result<RegisterResult, nsresult> {
+        let resp = self
+            .send_message(Self::CREATE_CREDENTIAL_FUNCTION, msg)
+            .map_err(|e| {
+                log::error!("Failed to send webauthn request via DBUS: {e:?}");
+                NS_ERROR_DOM_NOT_ALLOWED_ERR
+            })?;
+        RegisterResult::parse_from_dbus(resp).map_err(|e| {
+            log::error!("Failed parse webauthn reply from XDG portal: {e:?}");
+            NS_ERROR_DOM_NOT_ALLOWED_ERR
+        })
+    }
+
+    pub(crate) fn send_get_credential(
+        &self,
+        msg: HashMap<String, Variant<Box<dyn RefArg>>>,
+    ) -> Result<SignResult, nsresult> {
+        let resp = self
+            .send_message(Self::GET_CREDENTIAL_FUNCTION, msg)
+            .map_err(|e| {
+                log::error!("Failed to send webauthn request via DBUS: {e:?}");
+                NS_ERROR_DOM_NOT_ALLOWED_ERR
+            })?;
+        SignResult::parse_from_dbus(resp).map_err(|e| {
+            log::error!("Failed parse webauthn reply from XDG portal: {e:?}");
+            NS_ERROR_DOM_NOT_ALLOWED_ERR
+        })
+    }
+}
Index: firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/src/lib.rs
===================================================================
--- /dev/null
+++ firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/src/lib.rs
@@ -0,0 +1,887 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#[macro_use]
+extern crate xpcom;
+
+use base64::Engine;
+use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD};
+use dbus::arg::{RefArg, Variant};
+use moz_task::RunnableBuilder;
+use nserror::{
+    nsresult, NS_ERROR_DOM_ABORT_ERR, NS_ERROR_DOM_NOT_ALLOWED_ERR, NS_ERROR_FAILURE,
+    NS_ERROR_NOT_AVAILABLE, NS_ERROR_NOT_IMPLEMENTED, NS_OK,
+};
+use nsstring::{nsACString, nsAString, nsCString, nsString};
+use serde_json::json;
+use std::collections::HashMap;
+use std::sync::{Arc, Mutex, MutexGuard};
+use thin_vec::{thin_vec, ThinVec};
+use xpcom::interfaces::{
+    nsICredentialParameters, nsIWebAuthnAutoFillEntry, nsIWebAuthnRegisterArgs,
+    nsIWebAuthnRegisterPromise, nsIWebAuthnService, nsIWebAuthnSignArgs, nsIWebAuthnSignPromise,
+};
+use xpcom::{xpcom_method, RefPtr};
+mod register;
+use register::RegisterPromise;
+pub use register::WebAuthnRegisterResult;
+
+mod sign;
+use sign::{PendingSignArgs, SignPromise};
+
+mod dbus_controller;
+use dbus_controller::DbusController;
+
+// fn authrs_to_nserror(e: AuthenticatorError) -> nsresult {
+//     match e {
+//         AuthenticatorError::CredentialExcluded => NS_ERROR_DOM_INVALID_STATE_ERR,
+//         _ => NS_ERROR_DOM_NOT_ALLOWED_ERR,
+//     }
+// }
+
+#[derive(Clone)]
+enum TransactionPromise {
+    Register(RegisterPromise),
+    Sign(SignPromise),
+}
+
+impl TransactionPromise {
+    fn reject(&self, err: nsresult) -> Result<(), nsresult> {
+        match self {
+            TransactionPromise::Register(promise) => promise.resolve_or_reject(Err(err)),
+            TransactionPromise::Sign(promise) => promise.resolve_or_reject(Err(err)),
+        }
+    }
+}
+
+// enum TransactionArgs {
+//     Sign(/* timeout */ u64),
+// }
+
+struct TransactionState {
+    tid: u64,
+    browsing_context_id: u64,
+    pending_args: Option<PendingSignArgs>,
+    promise: TransactionPromise,
+}
+
+// XdgPortalAuthService provides an nsIWebAuthnService built on top of authenticator-rs.
+#[xpcom(implement(nsIWebAuthnService), atomic)]
+pub struct XdgPortalAuthService {
+    transaction: Arc<Mutex<Option<TransactionState>>>,
+}
+
+impl XdgPortalAuthService {
+    xpcom_method!(get_is_uvpaa => GetIsUVPAA() -> bool);
+    fn get_is_uvpaa(&self) -> Result<bool, nsresult> {
+        // For now, xdg-portal doesn't offer a platform authenticator
+        Ok(false)
+    }
+
+    xpcom_method!(make_credential => MakeCredential(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsIWebAuthnRegisterArgs, aPromise: *const nsIWebAuthnRegisterPromise));
+    fn make_credential(
+        &self,
+        tid: u64,
+        browsing_context_id: u64,
+        args: &nsIWebAuthnRegisterArgs,
+        promise: &nsIWebAuthnRegisterPromise,
+    ) -> Result<(), nsresult> {
+        self.reset()?;
+
+        let promise = RegisterPromise(RefPtr::new(promise));
+
+        let mut challenge = ThinVec::new();
+        unsafe { args.GetChallenge(&mut challenge) }.to_result()?;
+        let challenge_str = URL_SAFE_NO_PAD.encode(challenge);
+
+        let mut origin = nsString::new();
+        unsafe { args.GetOrigin(&mut *origin) }.to_result()?;
+
+        let mut relying_party_id = nsString::new();
+        unsafe { args.GetRpId(&mut *relying_party_id) }.to_result()?;
+
+        let mut client_data = nsCString::new();
+        unsafe { args.GetClientDataJSON(&mut *client_data) }.to_result()?;
+        let client_data_json: Option<serde_json::Map<_, _>> =
+            serde_json::from_str(&client_data.to_string()).ok();
+        let is_cross_origin = client_data_json
+            .and_then(|o| o.get("crossOrigin").and_then(|c| c.as_bool()))
+            .unwrap_or_default();
+
+        let mut timeout_ms = 0u32;
+        unsafe { args.GetTimeoutMS(&mut timeout_ms) }.to_result()?;
+
+        let mut exclude_list_ids = ThinVec::new();
+        unsafe { args.GetExcludeList(&mut exclude_list_ids) }.to_result()?;
+        let exclude_list: Vec<_> = exclude_list_ids
+            .iter()
+            .map(|id| {
+                json!({
+                    "id": URL_SAFE_NO_PAD.encode(&id),
+                    "type": "public-key",
+                })
+            })
+            .collect();
+
+        let mut relying_party_name = nsString::new();
+        unsafe { args.GetRpName(&mut *relying_party_name) }.to_result()?;
+
+        let mut user_id = ThinVec::new();
+        unsafe { args.GetUserId(&mut user_id) }.to_result()?;
+        let user_id_str = URL_SAFE_NO_PAD.encode(user_id);
+
+        let mut user_name = nsString::new();
+        unsafe { args.GetUserName(&mut *user_name) }.to_result()?;
+
+        let mut user_display_name = nsString::new();
+        unsafe { args.GetUserDisplayName(&mut *user_display_name) }.to_result()?;
+
+        let mut cose_algs = ThinVec::new();
+        unsafe { args.GetCoseAlgs(&mut cose_algs) }.to_result()?;
+        let pub_key_cred_params: Vec<_> = cose_algs
+            .iter()
+            .map(|alg| {
+                json!({
+                    "alg": alg,
+                    "type": "public-key",
+                })
+            })
+            .collect();
+
+        let mut resident_key = nsString::new();
+        unsafe { args.GetResidentKey(&mut *resident_key) }.to_result()?;
+
+        let mut user_verification = nsString::new();
+        unsafe { args.GetUserVerification(&mut *user_verification) }.to_result()?;
+
+        let mut authenticator_attachment = nsString::new();
+        if unsafe { args.GetAuthenticatorAttachment(&mut *authenticator_attachment) }
+            .to_result()
+            .is_ok()
+        {
+            // For now, xdg-portal doesn't offer a platform authenticator
+            if authenticator_attachment.eq("platform") {
+                return Err(NS_ERROR_NOT_AVAILABLE);
+            }
+        }
+
+        let mut extensions = serde_json::Map::new();
+        let mut cred_protect_policy_value = nsCString::new();
+        if unsafe { args.GetCredentialProtectionPolicy(&mut *cred_protect_policy_value) }
+            .to_result()
+            .is_ok()
+        {
+            extensions.insert(
+                "credentialProtectionPolicy".to_string(),
+                json!(cred_protect_policy_value.to_string()),
+            );
+            let mut enforce_cred_protect_value = false;
+            unsafe { args.GetEnforceCredentialProtectionPolicy(&mut enforce_cred_protect_value) }
+                .to_result()?;
+            extensions.insert(
+                "enforceCredentialProtectionPolicy".to_string(),
+                json!(enforce_cred_protect_value),
+            );
+        }
+
+        // Not yet supported by Firefox. See bmo#1844448
+        // let mut cred_blob = false;
+        // unsafe { args.GetCredBlob(&mut cred_blob) }.to_result()?;
+
+        let mut cred_props = false;
+        unsafe { args.GetCredProps(&mut cred_props) }.to_result()?;
+        if cred_props {
+            extensions.insert("credProps".to_string(), json!(cred_props));
+        }
+
+        let mut min_pin_length = false;
+        unsafe { args.GetMinPinLength(&mut min_pin_length) }.to_result()?;
+        if min_pin_length {
+            extensions.insert("minPinLength".to_string(), json!(min_pin_length));
+        }
+
+        // Firefox currently doesn't support largeBlob.support == "preferred", only "required"
+        let mut large_blob_support_required = false;
+        if unsafe { args.GetLargeBlobSupportRequired(&mut large_blob_support_required) }
+            .to_result()
+            .is_ok()
+        {
+            if large_blob_support_required {
+                extensions.insert("largeBlob".to_string(), json!({"support": "required"}));
+            }
+        }
+
+        let mut prf = false;
+        if unsafe { args.GetPrf(&mut prf) }.to_result().is_ok() && prf {
+            let mut prf_map = serde_json::Map::new();
+            let mut prf_eval_first: ThinVec<u8> = ThinVec::new();
+            if unsafe { args.GetPrfEvalFirst(&mut prf_eval_first) }
+                .to_result()
+                .is_ok()
+            {
+                prf_map.insert(
+                    "first".to_string(),
+                    json!(URL_SAFE_NO_PAD.encode(prf_eval_first)),
+                );
+            }
+
+            let mut prf_eval_second: ThinVec<u8> = ThinVec::new();
+            if unsafe { args.GetPrfEvalSecond(&mut prf_eval_second) }
+                .to_result()
+                .is_ok()
+            {
+                prf_map.insert(
+                    "second".to_string(),
+                    json!(URL_SAFE_NO_PAD.encode(prf_eval_second)),
+                );
+            }
+            extensions.insert("prf".to_string(), json!(prf_map));
+        }
+
+        let mut maybe_hmac_create_secret = false;
+        if unsafe { args.GetHmacCreateSecret(&mut maybe_hmac_create_secret) }
+            .to_result()
+            .is_ok()
+        {
+            extensions.insert(
+                "hmacCreateSecret".to_string(),
+                json!(maybe_hmac_create_secret),
+            );
+        }
+
+        let json_str = json!({
+            "challenge": challenge_str,
+            "rp": {
+                "id": relying_party_id.to_string(),
+                "name": relying_party_name.to_string(),
+            },
+            "user": {
+                "id": user_id_str,
+                "name": user_name.to_string(),
+                "display_name": user_display_name.to_string(),
+            },
+            "timeout": timeout_ms,
+            "excludeCredentials": exclude_list,
+            "pubKeyCredParams": pub_key_cred_params,
+            "extensions": extensions,
+            "authenticatorSelection": {
+                "residentKey": resident_key.to_string(),
+                "userVerification": user_verification.to_string(),
+            },
+        })
+        .to_string();
+
+        let mut guard = self.transaction.lock().unwrap();
+        *guard = Some(TransactionState {
+            tid,
+            browsing_context_id,
+            pending_args: None,
+            promise: TransactionPromise::Register(promise),
+        });
+        // drop the guard here to ensure we don't deadlock if the call to `register()` below
+        // hairpins the state callback.
+        drop(guard);
+
+        let callback_transaction = self.transaction.clone();
+        RunnableBuilder::new("XdgPortalService::MakeCredential::DbusSend", move || {
+            // We need to craft a message like this:
+            // req = {
+            //     "type": Variant('s', "publicKey"),
+            //     "origin": Variant('s', origin),
+            //     "is_same_origin": Variant('b', is_same_origin),
+            //     "publicKey": Variant('a{sv}', {
+            //         "request_json": Variant('s', req_json)
+            //     })
+            // }
+            //
+            // --- Build the inner dictionary for "publicKey" ---
+            // This corresponds to the a{sv} value of the "publicKey" key.
+            let mut public_key_dict = HashMap::<String, Variant<Box<dyn RefArg>>>::new();
+            public_key_dict.insert("request_json".to_string(), Variant(Box::new(json_str)));
+
+            // --- Build the main dictionary payload ---
+            // This is the top-level a{sv} structure.
+            let mut req = HashMap::<String, Variant<Box<dyn RefArg>>>::new();
+            req.insert(
+                "type".to_string(),
+                Variant(Box::new("publicKey".to_string())),
+            );
+            req.insert("origin".to_string(), Variant(Box::new(origin.to_string())));
+            req.insert(
+                "is_same_origin".to_string(),
+                Variant(Box::new(!is_cross_origin)),
+            );
+            req.insert(
+                "publicKey".to_string(),
+                // The inner dictionary must also be wrapped in a Variant
+                Variant(Box::new(public_key_dict)),
+            );
+
+            // Create a new dbus connection and send the message
+            let dbus_controller = match DbusController::new() {
+                Ok(c) => c,
+                Err(e) => {
+                    log::warn!("Failed to create DBUS connection {e:?}");
+                    return;
+                }
+            };
+            let result = dbus_controller.send_create_credential(req);
+
+            let mut guard = callback_transaction.lock().unwrap();
+            let Some(state) = guard.as_mut() else {
+                return;
+            };
+            if state.tid != tid {
+                return;
+            }
+            let TransactionPromise::Register(ref promise) = state.promise else {
+                return;
+            };
+
+            let _ = promise.resolve_or_reject(result); // TODO: Do correct error mapping here
+            *guard = None;
+        })
+        .may_block(true)
+        .dispatch_background_task()?;
+
+        Ok(())
+    }
+
+    xpcom_method!(set_has_attestation_consent => SetHasAttestationConsent(aTid: u64, aHasConsent: bool));
+    fn set_has_attestation_consent(&self, _tid: u64, _has_consent: bool) -> Result<(), nsresult> {
+        Err(NS_ERROR_NOT_IMPLEMENTED)
+    }
+
+    xpcom_method!(get_assertion => GetAssertion(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsIWebAuthnSignArgs, aPromise: *const nsIWebAuthnSignPromise));
+    fn get_assertion(
+        &self,
+        tid: u64,
+        browsing_context_id: u64,
+        args: &nsIWebAuthnSignArgs,
+        promise: &nsIWebAuthnSignPromise,
+    ) -> Result<(), nsresult> {
+        self.reset()?;
+
+        let promise = SignPromise(RefPtr::new(promise));
+
+        let mut challenge = ThinVec::new();
+        unsafe { args.GetChallenge(&mut challenge) }.to_result()?;
+        let challenge_str = URL_SAFE_NO_PAD.encode(challenge);
+
+        let mut origin = nsString::new();
+        unsafe { args.GetOrigin(&mut *origin) }.to_result()?;
+
+        let mut relying_party_id = nsString::new();
+        unsafe { args.GetRpId(&mut *relying_party_id) }.to_result()?;
+
+        let mut client_data = nsCString::new();
+        unsafe { args.GetClientDataJSON(&mut *client_data) }.to_result()?;
+        let client_data_json: Option<serde_json::Map<_, _>> =
+            serde_json::from_str(&client_data.to_string()).ok();
+        let is_cross_origin = client_data_json
+            .and_then(|o| o.get("crossOrigin").and_then(|c| c.as_bool()))
+            .unwrap_or_default();
+
+        let mut timeout_ms = 0u32;
+        unsafe { args.GetTimeoutMS(&mut timeout_ms) }.to_result()?;
+
+        let mut allow_list_ids = ThinVec::new();
+        unsafe { args.GetAllowList(&mut allow_list_ids) }.to_result()?;
+        let allow_list: Vec<_> = allow_list_ids.iter().map(|id| id.to_vec()).collect();
+
+        let mut user_verification = nsString::new();
+        unsafe { args.GetUserVerification(&mut *user_verification) }.to_result()?;
+
+        // let mut app_id = None;
+        // let mut maybe_app_id = nsString::new();
+        // match unsafe { args.GetAppId(&mut *maybe_app_id) }.to_result() {
+        //     Ok(_) => app_id = Some(maybe_app_id.to_string()),
+        //     _ => (),
+        // }
+
+        let mut extensions = serde_json::Map::new();
+
+        let mut prf = false;
+        if unsafe { args.GetPrf(&mut prf) }.to_result().is_ok() && prf {
+            let mut prf_map = serde_json::Map::new();
+            let mut prf_eval_map = serde_json::Map::new();
+            let mut prf_eval_first: ThinVec<u8> = ThinVec::new();
+            unsafe { args.GetPrfEvalFirst(&mut prf_eval_first) }.to_result()?;
+            prf_eval_map.insert(
+                "first".to_string(),
+                json!(URL_SAFE_NO_PAD.encode(prf_eval_first)),
+            );
+
+            let mut prf_eval_second: ThinVec<u8> = ThinVec::new();
+            if unsafe { args.GetPrfEvalSecond(&mut prf_eval_second) }
+                .to_result()
+                .is_ok()
+            {
+                prf_eval_map.insert(
+                    "second".to_string(),
+                    json!(URL_SAFE_NO_PAD.encode(prf_eval_second)),
+                );
+            }
+            prf_map.insert("eval".to_string(), json!(prf_eval_map));
+
+            let mut prf_eval_by_creds = serde_json::Map::new();
+            let mut credential_ids: ThinVec<ThinVec<u8>> = ThinVec::new();
+            let mut eval_by_cred_firsts: ThinVec<ThinVec<u8>> = ThinVec::new();
+            let mut eval_by_cred_second_maybes: ThinVec<bool> = ThinVec::new();
+            let mut eval_by_cred_seconds: ThinVec<ThinVec<u8>> = ThinVec::new();
+            if unsafe { args.GetPrfEvalByCredentialCredentialId(&mut credential_ids) }
+                .to_result()
+                .is_ok()
+                && !credential_ids.is_empty()
+            {
+                // All three functions are guaranteed to return arrays of the same length.
+                // If seconds are missing (because they are optional), then
+                // eval_by_cred_second_maybes[i] will have `false`, and eval_by_cred_seconds[i]
+                // an empty array
+                unsafe { args.GetPrfEvalByCredentialEvalFirst(&mut eval_by_cred_firsts) }
+                    .to_result()?;
+                unsafe {
+                    args.GetPrfEvalByCredentialEvalSecondMaybe(&mut eval_by_cred_second_maybes)
+                }
+                .to_result()?;
+                unsafe { args.GetPrfEvalByCredentialEvalSecond(&mut eval_by_cred_seconds) }
+                    .to_result()?;
+
+                for i in 0..credential_ids.len() {
+                    let mut prf_eval_by_cred_map = serde_json::Map::new();
+                    prf_eval_by_cred_map.insert(
+                        "first".to_string(),
+                        json!(URL_SAFE_NO_PAD.encode(&eval_by_cred_firsts[i])),
+                    );
+
+                    if eval_by_cred_second_maybes[i] {
+                        prf_eval_by_cred_map.insert(
+                            "second".to_string(),
+                            json!(URL_SAFE_NO_PAD.encode(&eval_by_cred_seconds[i])),
+                        );
+                    }
+                    prf_eval_by_creds.insert(
+                        URL_SAFE_NO_PAD.encode(&credential_ids[i]),
+                        json!(prf_eval_by_cred_map),
+                    );
+                }
+                prf_map.insert("evalByCredential".to_string(), json!(prf_eval_by_creds));
+            }
+            extensions.insert("prf".to_string(), json!(prf_map));
+        }
+
+        let mut large_blob_map = serde_json::Map::new();
+        let mut large_blob_read = false;
+        if unsafe { args.GetLargeBlobRead(&mut large_blob_read) }
+            .to_result()
+            .is_ok()
+        {
+            large_blob_map.insert("support".to_string(), json!("required"));
+        }
+        let mut large_blob_write: ThinVec<u8> = ThinVec::new();
+        if unsafe { args.GetLargeBlobWrite(&mut large_blob_write) }
+            .to_result()
+            .is_ok()
+        {
+            large_blob_map.insert(
+                "write".to_string(),
+                json!(URL_SAFE_NO_PAD.encode(&large_blob_write)),
+            );
+        }
+        if !large_blob_map.is_empty() {
+            extensions.insert("largeBlob".to_string(), json!(large_blob_map));
+        }
+
+        // https://w3c.github.io/webauthn/#prf-extension
+        // "The hmac-secret extension provides two PRFs per credential: one which is used for
+        // requests where user verification is performed and another for all other requests.
+        // This extension [PRF] only exposes a single PRF per credential and, when implementing
+        // on top of hmac-secret, that PRF MUST be the one used for when user verification is
+        // performed. This overrides the UserVerificationRequirement if neccessary."
+        if prf && user_verification == "discouraged" {
+            user_verification = "preferred".into();
+        }
+
+        let mut conditionally_mediated = false;
+        unsafe { args.GetConditionallyMediated(&mut conditionally_mediated) }.to_result()?;
+
+        let mut guard = self.transaction.lock().unwrap();
+        *guard = Some(TransactionState {
+            tid,
+            browsing_context_id,
+            pending_args: Some(PendingSignArgs {
+                origin: origin.to_string(),
+                challenge_str,
+                timeout_ms,
+                rp_id: relying_party_id.to_string(),
+                allow_credential_ids: allow_list,
+                user_verification: user_verification.to_string(),
+                extensions,
+                is_same_origin: !is_cross_origin,
+            }),
+            promise: TransactionPromise::Sign(promise),
+        });
+
+        if !conditionally_mediated {
+            // Immediately proceed to the modal UI flow.
+            self.do_get_assertion(None, guard)
+        } else {
+            // Cache the request and wait for the conditional UI to request autofill entries, etc.
+            Ok(())
+        }
+    }
+
+    fn do_get_assertion(
+        &self,
+        mut selected_credential_id: Option<Vec<u8>>,
+        mut guard: MutexGuard<Option<TransactionState>>,
+    ) -> Result<(), nsresult> {
+        let Some(state) = guard.as_mut() else {
+            return Err(NS_ERROR_FAILURE);
+        };
+        let tid = state.tid;
+        let mut pending_args = match state.pending_args.take() {
+            Some(args) => args,
+            None => return Err(NS_ERROR_FAILURE),
+        };
+
+        if let Some(id) = selected_credential_id.take() {
+            if pending_args.allow_credential_ids.is_empty() {
+                pending_args.allow_credential_ids.push(id);
+            } else {
+                // We need to ensure that the selected credential id
+                // was in the original allow_list.
+                pending_args.allow_credential_ids.retain(|i| i == &id);
+                if pending_args.allow_credential_ids.is_empty() {
+                    return Err(NS_ERROR_FAILURE);
+                }
+            }
+        }
+
+        let allow_list: Vec<_> = pending_args
+            .allow_credential_ids
+            .iter()
+            .map(|id| {
+                json!({
+                    "id": URL_SAFE_NO_PAD.encode(&id),
+                    "type": "public-key",
+                })
+            })
+            .collect();
+
+        let json_str = json!({
+            "challenge": pending_args.challenge_str,
+            "timeout": pending_args.timeout_ms,
+            "rpId": pending_args.rp_id,
+            "allowCredentials": allow_list,
+            "userVerification": pending_args.user_verification,
+            // "hints": [],
+            "extensions": pending_args.extensions,
+        })
+        .to_string();
+
+        let callback_transaction = self.transaction.clone();
+        RunnableBuilder::new("XdgPortalService::GetCredential::DbusSend", move || {
+            // We need to craft a message like this:
+            // req = {
+            //     "type": Variant('s', "publicKey"),
+            //     "origin": Variant('s', origin),
+            //     "is_same_origin": Variant('b', is_same_origin),
+            //     "publicKey": Variant('a{sv}', {
+            //         "request_json": Variant('s', req_json)
+            //     })
+            // }
+            // --- Build the inner dictionary for "publicKey" ---
+            // This corresponds to the a{sv} value of the "publicKey" key.
+            let mut public_key_dict = HashMap::<String, Variant<Box<dyn RefArg>>>::new();
+            public_key_dict.insert("request_json".to_string(), Variant(Box::new(json_str)));
+
+            // --- Build the main dictionary payload ---
+            // This is the top-level a{sv} structure.
+            let mut req = HashMap::<String, Variant<Box<dyn RefArg>>>::new();
+            req.insert(
+                "type".to_string(),
+                Variant(Box::new("publicKey".to_string())),
+            );
+            req.insert(
+                "origin".to_string(),
+                Variant(Box::new(pending_args.origin.clone())),
+            );
+            req.insert(
+                "is_same_origin".to_string(),
+                Variant(Box::new(pending_args.is_same_origin)),
+            );
+            req.insert(
+                "publicKey".to_string(),
+                // The inner dictionary must also be wrapped in a Variant
+                Variant(Box::new(public_key_dict)),
+            );
+
+            // Create a new dbus connection and send the message
+            let dbus_controller = match DbusController::new() {
+                Ok(c) => c,
+                Err(e) => {
+                    log::warn!("Failed to create DBUS connection {e:?}");
+                    return;
+                }
+            };
+            let result = dbus_controller.send_get_credential(req);
+
+            let mut guard = callback_transaction.lock().unwrap();
+            let Some(state) = guard.as_mut() else {
+                return;
+            };
+            if state.tid != tid {
+                return;
+            }
+            let TransactionPromise::Sign(ref promise) = state.promise else {
+                return;
+            };
+            let _ = promise.resolve_or_reject(result); // TODO: Do correct error mapping here
+            *guard = None;
+        })
+        .may_block(true)
+        .dispatch_background_task()?;
+        Ok(())
+    }
+
+    xpcom_method!(has_pending_conditional_get => HasPendingConditionalGet(aBrowsingContextId: u64, aOrigin: *const nsAString) -> u64);
+    fn has_pending_conditional_get(
+        &self,
+        browsing_context_id: u64,
+        origin: &nsAString,
+    ) -> Result<u64, nsresult> {
+        let mut guard = self.transaction.lock().unwrap();
+        let Some(state) = guard.as_mut() else {
+            return Ok(0);
+        };
+        let Some(pending_args) = state.pending_args.as_ref() else {
+            return Ok(0);
+        };
+        if state.browsing_context_id != browsing_context_id {
+            return Ok(0);
+        }
+        if !pending_args.origin.eq(&origin.to_string()) {
+            return Ok(0);
+        }
+        Ok(state.tid)
+    }
+
+    xpcom_method!(get_autofill_entries => GetAutoFillEntries(aTransactionId: u64) -> ThinVec<Option<RefPtr<nsIWebAuthnAutoFillEntry>>>);
+    fn get_autofill_entries(
+        &self,
+        tid: u64,
+    ) -> Result<ThinVec<Option<RefPtr<nsIWebAuthnAutoFillEntry>>>, nsresult> {
+        let mut guard = self.transaction.lock().unwrap();
+        let Some(state) = guard.as_mut() else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+        if state.tid != tid {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        }
+        let Some(_pending_args) = state.pending_args.as_ref() else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+        // We don't currently support silent discovery for credentials via xdg portal, YET.
+        // But we would have everything we need here.
+        return Ok(thin_vec![]);
+    }
+
+    xpcom_method!(select_autofill_entry => SelectAutoFillEntry(aTid: u64, aCredentialId: *const ThinVec<u8>));
+    fn select_autofill_entry(&self, tid: u64, credential_id: &ThinVec<u8>) -> Result<(), nsresult> {
+        // As we don't yet support silent discovery, this would never be called, but
+        // it doesn't hurt to have it implemented already for the future
+        let mut guard = self.transaction.lock().unwrap();
+        let Some(state) = guard.as_mut() else {
+            return Err(NS_ERROR_FAILURE);
+        };
+        if tid != state.tid {
+            return Err(NS_ERROR_FAILURE);
+        }
+        self.do_get_assertion(Some(credential_id.to_vec()), guard)
+    }
+
+    xpcom_method!(resume_conditional_get => ResumeConditionalGet(aTid: u64));
+    fn resume_conditional_get(&self, tid: u64) -> Result<(), nsresult> {
+        let mut guard = self.transaction.lock().unwrap();
+        let Some(state) = guard.as_mut() else {
+            return Err(NS_ERROR_FAILURE);
+        };
+        if tid != state.tid {
+            return Err(NS_ERROR_FAILURE);
+        }
+        self.do_get_assertion(None, guard)
+    }
+
+    // Clears the transaction state if tid matches the ongoing transaction ID.
+    // Returns whether the tid was a match.
+    fn clear_transaction(&self, tid: u64) -> bool {
+        let mut guard = self.transaction.lock().unwrap();
+        let Some(state) = guard.as_ref() else {
+            return true; // workaround for Bug 1864526.
+        };
+        if state.tid != tid {
+            // Ignore the cancellation request if the transaction
+            // ID does not match.
+            return false;
+        }
+        // It's possible that we haven't dispatched the request to the usb_token_manager yet,
+        // e.g. if we're waiting for resume_make_credential. So reject the promise and drop the
+        // state here rather than from the StateCallback
+        let _ = state.promise.reject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
+        *guard = None;
+        true
+    }
+
+    xpcom_method!(cancel => Cancel(aTransactionId: u64));
+    fn cancel(&self, tid: u64) -> Result<(), nsresult> {
+        self.clear_transaction(tid);
+        // TODO: Cancel dbus controller?
+        Ok(())
+    }
+
+    xpcom_method!(reset => Reset());
+    fn reset(&self) -> Result<(), nsresult> {
+        {
+            if let Some(state) = self.transaction.lock().unwrap().take() {
+                // cancel_prompts(state.tid)?;
+                state.promise.reject(NS_ERROR_DOM_ABORT_ERR)?;
+            }
+        } // release the transaction lock so a StateCallback can take it
+          // TODO!;
+          // self.usb_token_manager.lock().unwrap().cancel();
+        Ok(())
+    }
+
+    xpcom_method!(
+        add_virtual_authenticator => AddVirtualAuthenticator(
+            protocol: *const nsACString,
+            transport: *const nsACString,
+            has_resident_key: bool,
+            has_user_verification: bool,
+            is_user_consenting: bool,
+            is_user_verified: bool) -> nsACString
+    );
+    fn add_virtual_authenticator(
+        &self,
+        _protocol: &nsACString,
+        _transport: &nsACString,
+        _has_resident_key: bool,
+        _has_user_verification: bool,
+        _is_user_consenting: bool,
+        _is_user_verified: bool,
+    ) -> Result<nsCString, nsresult> {
+        Err(NS_ERROR_NOT_IMPLEMENTED)
+    }
+
+    xpcom_method!(remove_virtual_authenticator => RemoveVirtualAuthenticator(authenticatorId: *const nsACString));
+    fn remove_virtual_authenticator(&self, _authenticator_id: &nsACString) -> Result<(), nsresult> {
+        Err(NS_ERROR_NOT_IMPLEMENTED)
+    }
+
+    xpcom_method!(
+        add_credential => AddCredential(
+            authenticatorId: *const nsACString,
+            credential_id: *const nsACString,
+            is_resident_credential: bool,
+            rp_id: *const nsACString,
+            private_key: *const nsACString,
+            user_handle: *const nsACString,
+            sign_count: u32)
+    );
+    fn add_credential(
+        &self,
+        _authenticator_id: &nsACString,
+        _credential_id: &nsACString,
+        _is_resident_credential: bool,
+        _rp_id: &nsACString,
+        _private_key: &nsACString,
+        _user_handle: &nsACString,
+        _sign_count: u32,
+    ) -> Result<(), nsresult> {
+        Err(NS_ERROR_NOT_IMPLEMENTED)
+    }
+
+    xpcom_method!(get_credentials => GetCredentials(authenticatorId: *const nsACString) -> ThinVec<Option<RefPtr<nsICredentialParameters>>>);
+    fn get_credentials(
+        &self,
+        _authenticator_id: &nsACString,
+    ) -> Result<ThinVec<Option<RefPtr<nsICredentialParameters>>>, nsresult> {
+        Err(NS_ERROR_NOT_IMPLEMENTED)
+    }
+
+    xpcom_method!(remove_credential => RemoveCredential(authenticatorId: *const nsACString, credentialId: *const nsACString));
+    fn remove_credential(
+        &self,
+        _authenticator_id: &nsACString,
+        _credential_id: &nsACString,
+    ) -> Result<(), nsresult> {
+        Err(NS_ERROR_NOT_IMPLEMENTED)
+    }
+
+    xpcom_method!(remove_all_credentials => RemoveAllCredentials(authenticatorId: *const nsACString));
+    fn remove_all_credentials(&self, _authenticator_id: &nsACString) -> Result<(), nsresult> {
+        Err(NS_ERROR_NOT_IMPLEMENTED)
+    }
+
+    xpcom_method!(set_user_verified => SetUserVerified(authenticatorId: *const nsACString, isUserVerified: bool));
+    fn set_user_verified(
+        &self,
+        _authenticator_id: &nsACString,
+        _is_user_verified: bool,
+    ) -> Result<(), nsresult> {
+        Err(NS_ERROR_NOT_IMPLEMENTED)
+    }
+
+    xpcom_method!(listen => Listen());
+    pub(crate) fn listen(&self) -> Result<(), nsresult> {
+        Err(NS_ERROR_NOT_IMPLEMENTED)
+    }
+
+    xpcom_method!(run_command => RunCommand(c_cmd: *const nsACString));
+    pub fn run_command(&self, _c_cmd: &nsACString) -> Result<(), nsresult> {
+        Err(NS_ERROR_NOT_IMPLEMENTED)
+    }
+
+    xpcom_method!(pin_callback => PinCallback(aTransactionId: u64, aPin: *const nsACString));
+    fn pin_callback(&self, _transaction_id: u64, _pin: &nsACString) -> Result<(), nsresult> {
+        Err(NS_ERROR_NOT_IMPLEMENTED)
+    }
+
+    xpcom_method!(selection_callback => SelectionCallback(aTransactionId: u64, aSelection: u64));
+    fn selection_callback(&self, _transaction_id: u64, _selection: u64) -> Result<(), nsresult> {
+        Err(NS_ERROR_NOT_IMPLEMENTED)
+    }
+}
+
+#[no_mangle]
+pub extern "C" fn xdg_portal_auth_service_if_available(
+    result: *mut *const nsIWebAuthnService,
+) -> nsresult {
+    // This is not send-able to other threads because it contains a raw pointer,
+    // but we don't need to keep it. We can simply create a new one on the fly
+    let dbus_controller = match DbusController::new() {
+        Ok(c) => c,
+        Err(e) => {
+            log::warn!(
+                "Failed to create DBUS connection. Assuming XDG portal is not available. {e:?}"
+            );
+            return NS_ERROR_NOT_AVAILABLE;
+        }
+    };
+
+    if !dbus_controller.is_service_active() {
+        log::warn!("Failed to find XDG portal.");
+        return NS_ERROR_NOT_AVAILABLE;
+    }
+
+    let wrapper = XdgPortalAuthService::allocate(InitXdgPortalAuthService {
+        transaction: Arc::new(Mutex::new(None)),
+    });
+
+    unsafe {
+        RefPtr::new(wrapper.coerce::<nsIWebAuthnService>()).forget(&mut *result);
+    }
+    NS_OK
+}
Index: firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/src/register.rs
===================================================================
--- /dev/null
+++ firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/src/register.rs
@@ -0,0 +1,339 @@
+use base64::Engine;
+use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD};
+use dbus::arg::{ArgType, RefArg, Variant};
+use dbus::Message;
+use nserror::{
+    nsresult, NS_ERROR_FAILURE, NS_ERROR_NOT_AVAILABLE, NS_ERROR_NOT_IMPLEMENTED, NS_OK,
+};
+use nsstring::{nsACString, nsAString, nsCString, nsString};
+use std::cell::RefCell;
+use std::collections::HashMap;
+use thin_vec::ThinVec;
+use xpcom::interfaces::{nsIWebAuthnRegisterPromise, nsIWebAuthnRegisterResult};
+use xpcom::{xpcom_method, RefPtr};
+
+#[derive(Debug, Clone)]
+pub(crate) struct RegisterExtensionsResult {
+    pub(crate) hmac_create_secret: Option<bool>,
+    pub(crate) large_blob_supported: Option<bool>,
+    pub(crate) prf_enabled: Option<bool>,
+    pub(crate) prf_results_first: Option<Vec<u8>>,
+    pub(crate) prf_results_second: Option<Vec<u8>>,
+    pub(crate) cred_props_rk: Option<bool>,
+}
+
+#[derive(Debug, Clone)]
+pub(crate) struct RegisterResult {
+    pub(crate) client_data_json: Option<String>,
+    pub(crate) transports: Option<Vec<String>>,
+    pub(crate) attestation_object: Option<Vec<u8>>,
+    pub(crate) credential_id: Option<Vec<u8>>,
+    pub(crate) extensions: Option<RegisterExtensionsResult>,
+    pub(crate) authenticator_attachment: Option<String>,
+}
+
+impl RegisterResult {
+    pub(crate) fn parse_from_dbus(reply: Message) -> Result<Self, Box<dyn std::error::Error>> {
+        // Extract the main dictionary `a{sv}` from the message.
+        let mut iter = reply.iter_init();
+
+        // dbus-rs is a bit of a mess with respect to parsing nested messages.
+        // We have to iterate over the Dicts (and Dicts are Arrays with DictEntry-values)
+        // and then parse first the key, and second the value, to be able to
+        // re-parse the nested public_key-dict.
+        let mut public_key_dict = None;
+        let mut iter = iter.recurse(ArgType::Array).ok_or("Empty reply")?;
+        // Loop through the key-value pairs of the main dictionary.
+        while iter.arg_type() == ArgType::DictEntry {
+            // To read a key-value pair, recurse into the DictEntry itself.
+            let mut dict_entry_iter = iter
+                .recurse(ArgType::DictEntry)
+                .ok_or("Reply is not a dict")?;
+            let key: String = dict_entry_iter.read()?;
+
+            // Check the "type" field, if it is correct.
+            if key == "type" {
+                let mut dict_entry_iter = dict_entry_iter
+                    .recurse(ArgType::Variant)
+                    .ok_or("Dict value missing")?;
+                let cred_type: String = dict_entry_iter.read()?;
+                if cred_type != "public-key" {
+                    return Err(format!("Invalid credential type: {}", cred_type).into());
+                }
+            } else if key == "public_key" {
+                // The value is a Variant that wraps the target dictionary.
+                // First, recurse into the Variant wrapper.
+                let mut dict_entry_iter = dict_entry_iter
+                    .recurse(ArgType::Variant)
+                    .ok_or("Dict value missing")?;
+
+                // Now the iterator is positioned at the start of the inner value.
+                // We can now read the entire inner dictionary directly.
+                let public_key: HashMap<String, Variant<Box<dyn RefArg>>> =
+                    dict_entry_iter.read()?;
+
+                public_key_dict = Some(public_key);
+            }
+
+            iter.next();
+        }
+        let public_key_dict = public_key_dict.ok_or("Missing public_key dict from response")?;
+
+        // Extract and parse the response JSON string.
+        let reg_response_str = public_key_dict
+            .get("registration_response_json")
+            .and_then(|v| v.as_str())
+            .ok_or("Missing 'registration_response_json'")?;
+
+        let response_json: serde_json::Value = serde_json::from_str(reg_response_str)?;
+
+        let credential_id = response_json["id"]
+            .as_str()
+            .map(|x| URL_SAFE_NO_PAD.decode(x).ok())
+            .flatten();
+
+        let authenticator_attachment = response_json["authenticatorAttachment"]
+            .as_str()
+            .map(String::from);
+
+        let extensions = if let Some(extensions) =
+            response_json["clientExtensionResults"].as_object()
+        {
+            let hmac_create_secret = extensions.get("hmacCreateSecret").and_then(|x| x.as_bool());
+            let large_blob_supported = extensions.get("largeBlob").and_then(|b| {
+                b.as_object()
+                    .and_then(|blob| blob.get("supported").and_then(|s| s.as_bool()))
+            });
+            let prf_enabled = extensions.get("prf").and_then(|p| {
+                p.as_object()
+                    .and_then(|blob| blob.get("enabled").and_then(|s| s.as_bool()))
+            });
+            let prf_results_first = None; // Currently not supported by the spec, but may come in the future
+            let prf_results_second = None; // Currently not supported by the spec, but may come in the future
+            let cred_props_rk = extensions.get("credProps").and_then(|p| {
+                p.as_object()
+                    .and_then(|blob| blob.get("rk").and_then(|s| s.as_bool()))
+            });
+            Some(RegisterExtensionsResult {
+                hmac_create_secret,
+                large_blob_supported,
+                prf_enabled,
+                prf_results_first,
+                prf_results_second,
+                cred_props_rk,
+            })
+        } else {
+            None
+        };
+
+        let attestation_object = response_json["response"]["attestationObject"]
+            .as_str()
+            .map(|x| URL_SAFE_NO_PAD.decode(x).ok())
+            .flatten();
+
+        let client_data_json = response_json["response"]["clientDataJSON"]
+            .as_str()
+            .map(|x| URL_SAFE_NO_PAD.decode(x).ok())
+            .flatten()
+            .map(|b| String::from_utf8(b).ok())
+            .unwrap_or_default();
+
+        let transports = response_json["response"]["transports"]
+            .as_array()
+            .map(|ts| {
+                ts.iter()
+                    .filter_map(|t| t.as_str())
+                    .map(String::from)
+                    .collect()
+            });
+        Ok(Self {
+            client_data_json,
+            transports,
+            attestation_object,
+            credential_id,
+            extensions,
+            authenticator_attachment,
+        })
+    }
+}
+
+#[derive(Clone)]
+pub(crate) struct RegisterPromise(pub(crate) RefPtr<nsIWebAuthnRegisterPromise>);
+
+impl RegisterPromise {
+    pub(crate) fn resolve_or_reject(
+        &self,
+        result: Result<RegisterResult, nsresult>,
+    ) -> Result<(), nsresult> {
+        match result {
+            Ok(result) => {
+                let wrapped_result = WebAuthnRegisterResult::allocate(InitWebAuthnRegisterResult {
+                    result: RefCell::new(result),
+                })
+                .query_interface::<nsIWebAuthnRegisterResult>()
+                .ok_or(NS_ERROR_FAILURE)?;
+                unsafe { self.0.Resolve(wrapped_result.coerce()) };
+            }
+            Err(result) => {
+                unsafe { self.0.Reject(result) };
+            }
+        }
+        Ok(())
+    }
+}
+
+#[xpcom(implement(nsIWebAuthnRegisterResult), atomic)]
+pub struct WebAuthnRegisterResult {
+    // result is only borrowed mutably in `Anonymize`.
+    result: RefCell<RegisterResult>,
+}
+
+impl WebAuthnRegisterResult {
+    xpcom_method!(get_client_data_json => GetClientDataJSON() -> nsACString);
+    fn get_client_data_json(&self) -> Result<nsCString, nsresult> {
+        let Some(client_data_json) = &self.result.borrow().client_data_json else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+        Ok(client_data_json.into())
+    }
+
+    xpcom_method!(get_attestation_object => GetAttestationObject() -> ThinVec<u8>);
+    fn get_attestation_object(&self) -> Result<ThinVec<u8>, nsresult> {
+        let Some(attestation_object) = &self.result.borrow().attestation_object else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+        Ok(ThinVec::from(attestation_object.as_slice()))
+    }
+
+    xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>);
+    fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> {
+        let Some(credential_id) = &self.result.borrow().credential_id else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+        Ok(credential_id.as_slice().into())
+    }
+
+    xpcom_method!(get_transports => GetTransports() -> ThinVec<nsString>);
+    fn get_transports(&self) -> Result<ThinVec<nsString>, nsresult> {
+        Ok(self
+            .result
+            .borrow()
+            .transports
+            .as_ref()
+            .map(|ts| ts.iter().map(|t| t.into()).collect())
+            .unwrap_or_default())
+    }
+
+    xpcom_method!(get_hmac_create_secret => GetHmacCreateSecret() -> bool);
+    fn get_hmac_create_secret(&self) -> Result<bool, nsresult> {
+        let Some(hmac_create_secret) = self
+            .result
+            .borrow()
+            .extensions
+            .as_ref()
+            .and_then(|e| e.hmac_create_secret)
+        else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+        Ok(hmac_create_secret)
+    }
+
+    xpcom_method!(get_large_blob_supported => GetLargeBlobSupported() -> bool);
+    fn get_large_blob_supported(&self) -> Result<bool, nsresult> {
+        let Some(large_blob_supported) = self
+            .result
+            .borrow()
+            .extensions
+            .as_ref()
+            .and_then(|e| e.large_blob_supported)
+        else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+        Ok(large_blob_supported)
+    }
+
+    xpcom_method!(get_prf_enabled => GetPrfEnabled() -> bool);
+    fn get_prf_enabled(&self) -> Result<bool, nsresult> {
+        let Some(prf_enabled) = self
+            .result
+            .borrow()
+            .extensions
+            .as_ref()
+            .and_then(|e| e.prf_enabled)
+        else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+        Ok(prf_enabled)
+    }
+
+    xpcom_method!(get_prf_results_first => GetPrfResultsFirst() -> ThinVec<u8>);
+    fn get_prf_results_first(&self) -> Result<ThinVec<u8>, nsresult> {
+        let Some(extensions) = &self.result.borrow().extensions else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+
+        let Some(prf_results_first) = &extensions.prf_results_first else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+
+        Ok(prf_results_first.as_slice().into())
+    }
+
+    xpcom_method!(get_prf_results_second => GetPrfResultsSecond() -> ThinVec<u8>);
+    fn get_prf_results_second(&self) -> Result<ThinVec<u8>, nsresult> {
+        let Some(extensions) = &self.result.borrow().extensions else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+
+        let Some(prf_results_second) = &extensions.prf_results_second else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+
+        Ok(prf_results_second.as_slice().into())
+    }
+
+    xpcom_method!(get_cred_props_rk => GetCredPropsRk() -> bool);
+    fn get_cred_props_rk(&self) -> Result<bool, nsresult> {
+        let Some(cred_props_rk) = self
+            .result
+            .borrow()
+            .extensions
+            .as_ref()
+            .and_then(|e| e.cred_props_rk)
+        else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+        Ok(cred_props_rk)
+    }
+
+    xpcom_method!(set_cred_props_rk => SetCredPropsRk(aCredPropsRk: bool));
+    fn set_cred_props_rk(&self, _cred_props_rk: bool) -> Result<(), nsresult> {
+        Err(NS_ERROR_NOT_IMPLEMENTED)
+    }
+
+    xpcom_method!(get_authenticator_attachment => GetAuthenticatorAttachment() -> nsAString);
+    fn get_authenticator_attachment(&self) -> Result<nsString, nsresult> {
+        let Some(authenticator_attachment) = &self.result.borrow().authenticator_attachment else {
+            return Err(NS_ERROR_FAILURE);
+        };
+        Ok(authenticator_attachment.into())
+    }
+
+    xpcom_method!(has_identifying_attestation => HasIdentifyingAttestation() -> bool);
+    fn has_identifying_attestation(&self) -> Result<bool, nsresult> {
+        // if self.result.borrow().att_obj.att_stmt != AttestationStatement::None {
+        //     return Ok(true);
+        // }
+        // if let Some(data) = &self.result.borrow().att_obj.auth_data.credential_data {
+        //     return Ok(data.aaguid != AAGuid::default());
+        // }
+        Ok(false)
+    }
+
+    xpcom_method!(anonymize => Anonymize());
+    fn anonymize(&self) -> Result<nsresult, nsresult> {
+        // self.result.borrow_mut().att_obj.anonymize();
+        // Ok(NS_OK)
+        Err(NS_ERROR_NOT_AVAILABLE)
+    }
+}
Index: firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/src/sign.rs
===================================================================
--- /dev/null
+++ firefox-141.0.2/dom/webauthn/linux_xdg_portal_service/src/sign.rs
@@ -0,0 +1,364 @@
+use base64::Engine;
+use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD};
+use dbus::arg::{ArgType, RefArg, Variant};
+use dbus::Message;
+use nserror::{
+    nsresult, NS_ERROR_FAILURE, NS_ERROR_NOT_AVAILABLE, NS_ERROR_NOT_IMPLEMENTED, NS_OK,
+};
+use nsstring::{nsACString, nsAString, nsCString, nsString};
+use serde_json::{Map, Value};
+use std::collections::HashMap;
+use thin_vec::ThinVec;
+use xpcom::interfaces::{nsIWebAuthnSignPromise, nsIWebAuthnSignResult};
+use xpcom::{xpcom_method, RefPtr};
+
+// Flattened struct of this:
+// let response = json!({
+//     "clientDataJSON": URL_SAFE_NO_PAD.encode(self.client_data_json.as_bytes()),
+//     "authenticatorData": URL_SAFE_NO_PAD.encode(&self.authenticator_data),
+//     "signature": URL_SAFE_NO_PAD.encode(&self.signature),
+//     "userHandle": self.user_handle.as_ref().map(|h| URL_SAFE_NO_PAD.encode(h))
+// });
+// let output = json!({
+//     "id": id,
+//     "rawId": id,
+//     "authenticatorAttachment": self.attachment_modality,
+//     "response": response,
+//     "clientExtensionResults": self.extensions,
+// });
+//
+#[derive(Debug, Clone)]
+pub(crate) struct SignResult {
+    pub(crate) credential_id: Option<Vec<u8>>,
+    pub(crate) authenticator_attachment: Option<String>,
+    pub(crate) client_data_json: Option<String>,
+    pub(crate) authenticator_data: Option<Vec<u8>>,
+    pub(crate) signature: Option<Vec<u8>>,
+    pub(crate) user_handle: Option<Vec<u8>>,
+    pub(crate) extensions: Option<SignExtensionsResult>,
+}
+
+#[derive(Debug, Clone)]
+pub(crate) struct SignExtensionsResult {
+    pub(crate) large_blob_value: Option<Vec<u8>>,
+    pub(crate) large_blob_written: Option<bool>,
+    pub(crate) prf_maybe: bool,
+    pub(crate) prf_results_first: Option<Vec<u8>>,
+    pub(crate) prf_results_second: Option<Vec<u8>>,
+}
+
+impl SignResult {
+    pub(crate) fn parse_from_dbus(reply: Message) -> Result<Self, Box<dyn std::error::Error>> {
+        // Extract the main dictionary `a{sv}` from the message.
+        let mut iter = reply.iter_init();
+
+        // dbus-rs is a bit of a mess with respect to parsing nested messages.
+        // We have to iterate over the Dicts (and Dicts are Arrays with DictEntry-values)
+        // and then parse first the key, and second the value, to be able to
+        // re-parse the nested public_key-dict.
+        let mut public_key_dict = None;
+        let mut iter = iter.recurse(ArgType::Array).ok_or("Empty reply")?;
+        // Loop through the key-value pairs of the main dictionary.
+        while iter.arg_type() == ArgType::DictEntry {
+            // To read a key-value pair, recurse into the DictEntry itself.
+            let mut dict_entry_iter = iter
+                .recurse(ArgType::DictEntry)
+                .ok_or("Reply is not a dict")?;
+            let key: String = dict_entry_iter.read()?;
+
+            // Check the "type" field, if it is correct.
+            if key == "type" {
+                let mut dict_entry_iter = dict_entry_iter
+                    .recurse(ArgType::Variant)
+                    .ok_or("Dict value missing")?;
+                let cred_type: String = dict_entry_iter.read()?;
+                if cred_type != "public-key" {
+                    return Err(format!("Invalid credential type: {}", cred_type).into());
+                }
+            } else if key == "public_key" {
+                // The value is a Variant that wraps the target dictionary.
+                // First, recurse into the Variant wrapper.
+                let mut dict_entry_iter = dict_entry_iter
+                    .recurse(ArgType::Variant)
+                    .ok_or("Dict value missing")?;
+
+                // Now the iterator is positioned at the start of the inner value.
+                // We can now read the entire inner dictionary directly.
+                let public_key: HashMap<String, Variant<Box<dyn RefArg>>> =
+                    dict_entry_iter.read()?;
+
+                public_key_dict = Some(public_key);
+            }
+
+            iter.next();
+        }
+        let public_key_dict = public_key_dict.ok_or("Missing public_key dict from response")?;
+
+        // Extract and parse the response JSON string.
+        let auth_response_str = public_key_dict
+            .get("authentication_response_json")
+            .and_then(|v| v.as_str())
+            .ok_or("Missing 'authentication_response_json'")?;
+
+        let response_json: serde_json::Value = serde_json::from_str(auth_response_str)?;
+
+        let credential_id = response_json["id"]
+            .as_str()
+            .map(|x| URL_SAFE_NO_PAD.decode(x).ok())
+            .flatten();
+
+        let authenticator_attachment = response_json["authenticatorAttachment"]
+            .as_str()
+            .map(String::from);
+
+        let extensions =
+            if let Some(extensions) = response_json["clientExtensionResults"].as_object() {
+                let mut large_blob_written = None;
+                let mut large_blob_value = None;
+                if let Some(large_blob) = extensions.get("largeBlob") {
+                    large_blob_written = large_blob.get("written").and_then(|w| w.as_bool());
+                    large_blob_value = large_blob
+                        .get("blob")
+                        .and_then(|b| b.as_str().and_then(|v| URL_SAFE_NO_PAD.decode(v).ok()));
+                }
+                let mut prf_maybe = false;
+                let mut prf_results_first = None;
+                let mut prf_results_second = None;
+
+                if let Some(prf) = extensions.get("prf") {
+                    prf_maybe = true;
+                    if let Some(results) = prf.get("results").and_then(|r| r.as_object()) {
+                        prf_results_first = results
+                            .get("first")
+                            .and_then(|f| f.as_str().and_then(|f| URL_SAFE_NO_PAD.decode(f).ok()));
+                        prf_results_second = results
+                            .get("second")
+                            .and_then(|s| s.as_str().and_then(|s| URL_SAFE_NO_PAD.decode(s).ok()));
+                    }
+                }
+                Some(SignExtensionsResult {
+                    large_blob_value,
+                    large_blob_written,
+                    prf_maybe,
+                    prf_results_first,
+                    prf_results_second,
+                })
+            } else {
+                None
+            };
+
+        let client_data_json = response_json["response"]["clientDataJSON"]
+            .as_str()
+            .map(|x| URL_SAFE_NO_PAD.decode(x).ok())
+            .flatten()
+            .map(|b| String::from_utf8(b).ok())
+            .unwrap_or_default();
+
+        let authenticator_data = response_json["response"]["authenticatorData"]
+            .as_str()
+            .map(|x| URL_SAFE_NO_PAD.decode(x).ok())
+            .flatten();
+
+        let signature = response_json["response"]["signature"]
+            .as_str()
+            .map(|x| URL_SAFE_NO_PAD.decode(x).ok())
+            .flatten();
+
+        let user_handle = response_json["response"]["userHandle"]
+            .as_str()
+            .map(|x| URL_SAFE_NO_PAD.decode(x).ok())
+            .flatten();
+
+        Ok(Self {
+            client_data_json,
+            credential_id,
+            extensions,
+            authenticator_attachment,
+            authenticator_data,
+            signature,
+            user_handle,
+        })
+    }
+}
+
+#[xpcom(implement(nsIWebAuthnSignResult), atomic)]
+pub struct WebAuthnSignResult {
+    pub result: SignResult,
+}
+
+impl WebAuthnSignResult {
+    xpcom_method!(get_client_data_json => GetClientDataJSON() -> nsACString);
+    fn get_client_data_json(&self) -> Result<nsCString, nsresult> {
+        let Some(client_data_json) = &self.result.client_data_json else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+        Ok(client_data_json.into())
+    }
+
+    xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>);
+    fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> {
+        let Some(credential_id) = &self.result.credential_id else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+        Ok(credential_id.as_slice().into())
+    }
+
+    xpcom_method!(get_signature => GetSignature() -> ThinVec<u8>);
+    fn get_signature(&self) -> Result<ThinVec<u8>, nsresult> {
+        let Some(signature) = &self.result.signature else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+        Ok(signature.as_slice().into())
+    }
+
+    xpcom_method!(get_authenticator_data => GetAuthenticatorData() -> ThinVec<u8>);
+    fn get_authenticator_data(&self) -> Result<ThinVec<u8>, nsresult> {
+        let Some(authenticator_data) = &self.result.authenticator_data else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+        Ok(authenticator_data.as_slice().into())
+    }
+
+    xpcom_method!(get_user_handle => GetUserHandle() -> ThinVec<u8>);
+    fn get_user_handle(&self) -> Result<ThinVec<u8>, nsresult> {
+        let Some(user_handle) = &self.result.user_handle else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+        Ok(user_handle.as_slice().into())
+    }
+
+    xpcom_method!(get_user_name => GetUserName() -> nsACString);
+    fn get_user_name(&self) -> Result<nsCString, nsresult> {
+        // xdg-portal does not send back the username, only user_handle
+        // let Some(user) = &self.result.assertion.user else {
+        //     return Err(NS_ERROR_NOT_AVAILABLE);
+        // };
+        // let Some(name) = &user.name else {
+        //     return Err(NS_ERROR_NOT_AVAILABLE);
+        // };
+        // Ok(nsCString::from(name))
+        Err(NS_ERROR_NOT_AVAILABLE)
+    }
+
+    xpcom_method!(get_authenticator_attachment => GetAuthenticatorAttachment() -> nsAString);
+    fn get_authenticator_attachment(&self) -> Result<nsString, nsresult> {
+        let Some(authenticator_attachment) = &self.result.authenticator_attachment else {
+            return Err(NS_ERROR_FAILURE);
+        };
+        Ok(authenticator_attachment.into())
+    }
+
+    xpcom_method!(get_used_app_id => GetUsedAppId() -> bool);
+    fn get_used_app_id(&self) -> Result<bool, nsresult> {
+        // self.result.extensions.app_id.ok_or(NS_ERROR_NOT_AVAILABLE)
+        Err(NS_ERROR_NOT_AVAILABLE)
+    }
+
+    xpcom_method!(set_used_app_id => SetUsedAppId(aUsedAppId: bool));
+    fn set_used_app_id(&self, _used_app_id: bool) -> Result<(), nsresult> {
+        Err(NS_ERROR_NOT_IMPLEMENTED)
+    }
+
+    xpcom_method!(get_large_blob_value => GetLargeBlobValue() -> ThinVec<u8>);
+    fn get_large_blob_value(&self) -> Result<ThinVec<u8>, nsresult> {
+        let Some(extensions) = &self.result.extensions else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+
+        let Some(large_blob_value) = &extensions.large_blob_value else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+
+        Ok(large_blob_value.as_slice().into())
+    }
+
+    xpcom_method!(get_large_blob_written => GetLargeBlobWritten() -> bool);
+    fn get_large_blob_written(&self) -> Result<bool, nsresult> {
+        let Some(large_blob_written) = self
+            .result
+            .extensions
+            .as_ref()
+            .map(|e| e.large_blob_written)
+            .flatten()
+        else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+        Ok(large_blob_written)
+    }
+
+    xpcom_method!(get_prf_maybe => GetPrfMaybe() -> bool);
+    /// Return true if a PRF output is present, even if all attributes are absent.
+    fn get_prf_maybe(&self) -> Result<bool, nsresult> {
+        let Some(prf_maybe) = self.result.extensions.as_ref().map(|e| e.prf_maybe) else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+        Ok(prf_maybe)
+    }
+
+    xpcom_method!(get_prf_results_first => GetPrfResultsFirst() -> ThinVec<u8>);
+    fn get_prf_results_first(&self) -> Result<ThinVec<u8>, nsresult> {
+        let Some(extensions) = &self.result.extensions else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+
+        let Some(prf_results_first) = &extensions.prf_results_first else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+
+        Ok(prf_results_first.as_slice().into())
+    }
+
+    xpcom_method!(get_prf_results_second => GetPrfResultsSecond() -> ThinVec<u8>);
+    fn get_prf_results_second(&self) -> Result<ThinVec<u8>, nsresult> {
+        let Some(extensions) = &self.result.extensions else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+
+        let Some(prf_results_second) = &extensions.prf_results_second else {
+            return Err(NS_ERROR_NOT_AVAILABLE);
+        };
+
+        Ok(prf_results_second.as_slice().into())
+    }
+}
+
+// Used for conditional mediation:
+// The webpage runs a get_assertion()-request, but adds mediation: 'conditional' to it.
+// If this is the case, we cache the incoming request and execute it at a later point
+// in time, when the user selects it, after clicking into the input-field.
+#[derive(Debug, Clone)]
+pub(crate) struct PendingSignArgs {
+    pub(crate) origin: String,
+    /// base64 encoded challenge
+    pub(crate) challenge_str: String,
+    pub(crate) timeout_ms: u32,
+    pub(crate) rp_id: String,
+    pub(crate) allow_credential_ids: Vec<Vec<u8>>,
+    pub(crate) user_verification: String,
+    pub(crate) extensions: Map<String, Value>,
+    pub(crate) is_same_origin: bool,
+}
+
+#[derive(Clone)]
+pub(crate) struct SignPromise(pub(crate) RefPtr<nsIWebAuthnSignPromise>);
+
+impl SignPromise {
+    pub(crate) fn resolve_or_reject(
+        &self,
+        result: Result<SignResult, nsresult>,
+    ) -> Result<(), nsresult> {
+        match result {
+            Ok(result) => {
+                let wrapped_result =
+                    WebAuthnSignResult::allocate(InitWebAuthnSignResult { result })
+                        .query_interface::<nsIWebAuthnSignResult>()
+                        .ok_or(NS_ERROR_FAILURE)?;
+                unsafe { self.0.Resolve(wrapped_result.coerce()) };
+            }
+            Err(result) => {
+                unsafe { self.0.Reject(result) };
+            }
+        }
+        Ok(())
+    }
+}
Index: firefox-141.0.2/modules/libpref/init/StaticPrefList.yaml
===================================================================
--- firefox-141.0.2.orig/modules/libpref/init/StaticPrefList.yaml
+++ firefox-141.0.2/modules/libpref/init/StaticPrefList.yaml
@@ -16693,6 +16693,15 @@
   mirror: always
   rust: true
 
+#if defined(MOZ_WIDGET_GTK)
+# Dispatch WebAuthn requests to linux xdg portal
+- name: security.webauth.webauthn_enable_xdg_portal
+  type: RelaxedAtomicBool
+  value: true
+  mirror: always
+  rust: true
+#endif
+
 # residentKey support when using Android platform API
 - name: security.webauthn.webauthn_enable_android_fido2.residentkey
   type: RelaxedAtomicBool
Index: firefox-141.0.2/toolkit/library/rust/shared/Cargo.toml
===================================================================
--- firefox-141.0.2.orig/toolkit/library/rust/shared/Cargo.toml
+++ firefox-141.0.2/toolkit/library/rust/shared/Cargo.toml
@@ -124,6 +124,9 @@ webext-storage = "0.1"
 detect_win32k_conflicts = { path = "../../../xre/detect_win32k_conflicts" }
 widget_windows = { path = "../../../../widget/windows/rust" }
 
+[target.'cfg(target_os = "linux")'.dependencies]
+xdg_portal_auth_service = { path = "../../../../dom/webauthn/linux_xdg_portal_service" }
+
 [features]
 default = []
 moz_memory = ["mozglue-static/moz_memory"]
Index: firefox-141.0.2/toolkit/library/rust/shared/lib.rs
===================================================================
--- firefox-141.0.2.orig/toolkit/library/rust/shared/lib.rs
+++ firefox-141.0.2/toolkit/library/rust/shared/lib.rs
@@ -52,6 +52,7 @@ extern crate signature_cache;
 extern crate static_prefs;
 extern crate storage;
 extern crate webrender_bindings;
+extern crate xdg_portal_auth_service;
 extern crate xpcom;
 
 extern crate audio_thread_priority;
openSUSE Build Service is sponsored by