diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/webauthn/authrs_bridge | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/webauthn/authrs_bridge')
-rw-r--r-- | dom/webauthn/authrs_bridge/Cargo.toml | 16 | ||||
-rw-r--r-- | dom/webauthn/authrs_bridge/src/lib.rs | 749 |
2 files changed, 765 insertions, 0 deletions
diff --git a/dom/webauthn/authrs_bridge/Cargo.toml b/dom/webauthn/authrs_bridge/Cargo.toml new file mode 100644 index 0000000000..ea549d3521 --- /dev/null +++ b/dom/webauthn/authrs_bridge/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "authrs_bridge" +version = "0.1.0" +edition = "2021" +authors = ["Martin Sirringhaus", "John Schanck"] + +[dependencies] +authenticator = { version = "0.4.0-alpha.15", features = ["gecko"] } +log = "0.4" +moz_task = { path = "../../../xpcom/rust/moz_task" } +nserror = { path = "../../../xpcom/rust/nserror" } +nsstring = { path = "../../../xpcom/rust/nsstring" } +serde_cbor = "0.11" +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } +static_prefs = { path = "../../../modules/libpref/init/static_prefs" } +xpcom = { path = "../../../xpcom/rust/xpcom" } diff --git a/dom/webauthn/authrs_bridge/src/lib.rs b/dom/webauthn/authrs_bridge/src/lib.rs new file mode 100644 index 0000000000..30ee54beb9 --- /dev/null +++ b/dom/webauthn/authrs_bridge/src/lib.rs @@ -0,0 +1,749 @@ +/* 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 log; + +#[macro_use] +extern crate xpcom; + +use authenticator::{ + authenticatorservice::{AuthenticatorService, RegisterArgs, SignArgs}, + ctap2::attestation::AttestationStatement, + ctap2::server::{ + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, + ResidentKeyRequirement, User, UserVerificationRequirement, + }, + errors::{AuthenticatorError, PinError, U2FTokenError}, + statecallback::StateCallback, + Assertion, Pin, RegisterResult, SignResult, StatusPinUv, StatusUpdate, +}; +use moz_task::RunnableBuilder; +use nserror::{ + nsresult, NS_ERROR_DOM_INVALID_STATE_ERR, NS_ERROR_DOM_NOT_ALLOWED_ERR, + NS_ERROR_DOM_NOT_SUPPORTED_ERR, NS_ERROR_DOM_OPERATION_ERR, NS_ERROR_DOM_UNKNOWN_ERR, + NS_ERROR_FAILURE, NS_ERROR_NOT_AVAILABLE, NS_ERROR_NOT_IMPLEMENTED, NS_ERROR_NULL_POINTER, + NS_OK, +}; +use nsstring::{nsACString, nsCString, nsString}; +use serde_cbor; +use std::cell::RefCell; +use std::sync::mpsc::{channel, Receiver, RecvError, Sender}; +use std::sync::{Arc, Mutex}; +use thin_vec::ThinVec; +use xpcom::interfaces::{ + nsICtapRegisterArgs, nsICtapRegisterResult, nsICtapSignArgs, nsICtapSignResult, + nsIWebAuthnController, nsIWebAuthnTransport, +}; +use xpcom::{xpcom_method, RefPtr}; + +fn make_prompt(action: &str, tid: u64, origin: &str, browsing_context_id: u64) -> String { + format!( + r#"{{"is_ctap2":true,"action":"{action}","tid":{tid},"origin":"{origin}","browsingContextId":{browsing_context_id}}}"#, + ) +} + +fn make_pin_required_prompt( + tid: u64, + origin: &str, + browsing_context_id: u64, + was_invalid: bool, + retries: i64, +) -> String { + format!( + r#"{{"is_ctap2":true,"action":"pin-required","tid":{tid},"origin":"{origin}","browsingContextId":{browsing_context_id},"wasInvalid":{was_invalid},"retriesLeft":{retries}}}"#, + ) +} + +fn authrs_to_nserror(e: &AuthenticatorError) -> nsresult { + match e { + AuthenticatorError::U2FToken(U2FTokenError::NotSupported) => NS_ERROR_DOM_NOT_SUPPORTED_ERR, + AuthenticatorError::U2FToken(U2FTokenError::InvalidState) => NS_ERROR_DOM_INVALID_STATE_ERR, + AuthenticatorError::U2FToken(U2FTokenError::NotAllowed) => NS_ERROR_DOM_NOT_ALLOWED_ERR, + AuthenticatorError::PinError(PinError::PinRequired) => NS_ERROR_DOM_OPERATION_ERR, + AuthenticatorError::PinError(PinError::InvalidPin(_)) => NS_ERROR_DOM_OPERATION_ERR, + AuthenticatorError::PinError(PinError::PinAuthBlocked) => NS_ERROR_DOM_OPERATION_ERR, + AuthenticatorError::PinError(PinError::PinBlocked) => NS_ERROR_DOM_OPERATION_ERR, + AuthenticatorError::PinError(PinError::PinNotSet) => NS_ERROR_DOM_OPERATION_ERR, + AuthenticatorError::CredentialExcluded => NS_ERROR_DOM_OPERATION_ERR, + _ => NS_ERROR_DOM_UNKNOWN_ERR, + } +} + +#[xpcom(implement(nsICtapRegisterResult), atomic)] +pub struct CtapRegisterResult { + result: Result<RegisterResult, AuthenticatorError>, +} + +impl CtapRegisterResult { + xpcom_method!(get_attestation_object => GetAttestationObject() -> ThinVec<u8>); + fn get_attestation_object(&self) -> Result<ThinVec<u8>, nsresult> { + let mut out = ThinVec::new(); + if let Ok(RegisterResult::CTAP2(attestation)) = &self.result { + if let Ok(encoded_att_obj) = serde_cbor::to_vec(&attestation) { + out.extend_from_slice(&encoded_att_obj); + return Ok(out); + } + } + Err(NS_ERROR_FAILURE) + } + + xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>); + fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> { + let mut out = ThinVec::new(); + if let Ok(RegisterResult::CTAP2(attestation)) = &self.result { + if let Some(credential_data) = &attestation.auth_data.credential_data { + out.extend(credential_data.credential_id.clone()); + return Ok(out); + } + } + Err(NS_ERROR_FAILURE) + } + + xpcom_method!(get_status => GetStatus() -> nsresult); + fn get_status(&self) -> Result<nsresult, nsresult> { + match &self.result { + Ok(_) => Ok(NS_OK), + Err(e) => Ok(authrs_to_nserror(e)), + } + } +} + +#[xpcom(implement(nsICtapSignResult), atomic)] +pub struct CtapSignResult { + result: Result<Assertion, AuthenticatorError>, +} + +impl CtapSignResult { + xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>); + fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> { + let mut out = ThinVec::new(); + if let Ok(assertion) = &self.result { + if let Some(cred) = &assertion.credentials { + out.extend_from_slice(&cred.id); + return Ok(out); + } + } + Err(NS_ERROR_FAILURE) + } + + xpcom_method!(get_signature => GetSignature() -> ThinVec<u8>); + fn get_signature(&self) -> Result<ThinVec<u8>, nsresult> { + let mut out = ThinVec::new(); + if let Ok(assertion) = &self.result { + out.extend_from_slice(&assertion.signature); + return Ok(out); + } + Err(NS_ERROR_FAILURE) + } + + xpcom_method!(get_authenticator_data => GetAuthenticatorData() -> ThinVec<u8>); + fn get_authenticator_data(&self) -> Result<ThinVec<u8>, nsresult> { + let mut out = ThinVec::new(); + if let Ok(assertion) = &self.result { + if let Ok(encoded_auth_data) = assertion.auth_data.to_vec() { + out.extend_from_slice(&encoded_auth_data); + return Ok(out); + } + } + Err(NS_ERROR_FAILURE) + } + + xpcom_method!(get_user_handle => GetUserHandle() -> ThinVec<u8>); + fn get_user_handle(&self) -> Result<ThinVec<u8>, nsresult> { + let mut out = ThinVec::new(); + if let Ok(assertion) = &self.result { + if let Some(user) = &assertion.user { + out.extend_from_slice(&user.id); + return Ok(out); + } + } + Err(NS_ERROR_FAILURE) + } + + xpcom_method!(get_user_name => GetUserName() -> nsACString); + fn get_user_name(&self) -> Result<nsCString, nsresult> { + if let Ok(assertion) = &self.result { + if let Some(user) = &assertion.user { + if let Some(name) = &user.name { + return Ok(nsCString::from(name)); + } + } + } + Err(NS_ERROR_NOT_AVAILABLE) + } + + xpcom_method!(get_rp_id_hash => GetRpIdHash() -> ThinVec<u8>); + fn get_rp_id_hash(&self) -> Result<ThinVec<u8>, nsresult> { + // assertion.auth_data.rp_id_hash + let mut out = ThinVec::new(); + if let Ok(assertion) = &self.result { + out.extend(assertion.auth_data.rp_id_hash.0.clone()); + return Ok(out); + } + Err(NS_ERROR_FAILURE) + } + + xpcom_method!(get_status => GetStatus() -> nsresult); + fn get_status(&self) -> Result<nsresult, nsresult> { + match &self.result { + Ok(_) => Ok(NS_OK), + Err(e) => Ok(authrs_to_nserror(e)), + } + } +} + +/// Controller wraps a raw pointer to an nsIWebAuthnController. The AuthrsTransport struct holds a +/// Controller which we need to initialize from the SetController XPCOM method. Since XPCOM +/// methods take &self, Controller must have interior mutability. +#[derive(Clone)] +struct Controller(RefCell<*const nsIWebAuthnController>); + +/// Our implementation of nsIWebAuthnController in WebAuthnController.cpp has the property that its +/// XPCOM methods are safe to call from any thread, hence a raw pointer to an nsIWebAuthnController +/// is Send. +unsafe impl Send for Controller {} + +impl Controller { + fn init(&self, ptr: *const nsIWebAuthnController) -> Result<(), nsresult> { + self.0.replace(ptr); + Ok(()) + } + + fn send_prompt(&self, tid: u64, msg: &str) { + if (*self.0.borrow()).is_null() { + warn!("Controller not initialized"); + return; + } + let notification_str = nsCString::from(msg); + unsafe { + (**(self.0.borrow())).SendPromptNotificationPreformatted(tid, &*notification_str); + } + } + + fn finish_register( + &self, + tid: u64, + result: Result<RegisterResult, AuthenticatorError>, + ) -> Result<(), nsresult> { + if (*self.0.borrow()).is_null() { + return Err(NS_ERROR_FAILURE); + } + let wrapped_result = CtapRegisterResult::allocate(InitCtapRegisterResult { result }) + .query_interface::<nsICtapRegisterResult>() + .ok_or(NS_ERROR_FAILURE)?; + unsafe { + (**(self.0.borrow())).FinishRegister(tid, wrapped_result.coerce()); + } + Ok(()) + } + + fn finish_sign( + &self, + tid: u64, + result: Result<SignResult, AuthenticatorError>, + ) -> Result<(), nsresult> { + if (*self.0.borrow()).is_null() { + return Err(NS_ERROR_FAILURE); + } + + // If result is an error, we return a single CtapSignResult that has its status field set + // to an error. Otherwise we convert the entries of SignResult (= Vec<Assertion>) into + // CtapSignResults with OK statuses. + let mut assertions: ThinVec<Option<RefPtr<nsICtapSignResult>>> = ThinVec::new(); + match result { + Err(e) => assertions.push( + CtapSignResult::allocate(InitCtapSignResult { result: Err(e) }) + .query_interface::<nsICtapSignResult>(), + ), + Ok(SignResult::CTAP2(assertion_vec)) => { + for assertion in assertion_vec.0 { + assertions.push( + CtapSignResult::allocate(InitCtapSignResult { + result: Ok(assertion), + }) + .query_interface::<nsICtapSignResult>(), + ); + } + } + _ => return Err(NS_ERROR_NOT_IMPLEMENTED), // SignResult::CTAP1 shouldn't happen. + } + + unsafe { + (**(self.0.borrow())).FinishSign(tid, &mut assertions); + } + Ok(()) + } +} + +// The state machine creates a Sender<Pin>/Receiver<Pin> channel in ask_user_for_pin. It passes the +// Sender through status_callback, which stores the Sender in the pin_receiver field of an +// AuthrsTransport. The u64 in PinReceiver is a transaction ID, which the AuthrsTransport uses the +// transaction ID as a consistency check. +type PinReceiver = Option<(u64, Sender<Pin>)>; + +fn status_callback( + status_rx: Receiver<StatusUpdate>, + tid: u64, + origin: &String, + browsing_context_id: u64, + controller: Controller, + pin_receiver: Arc<Mutex<PinReceiver>>, /* Shared with an AuthrsTransport */ +) { + loop { + match status_rx.recv() { + Ok(StatusUpdate::DeviceAvailable { dev_info }) => { + debug!("STATUS: device available: {}", dev_info) + } + Ok(StatusUpdate::DeviceUnavailable { dev_info }) => { + debug!("STATUS: device unavailable: {}", dev_info) + } + Ok(StatusUpdate::Success { dev_info }) => { + debug!("STATUS: success using device: {}", dev_info); + } + Ok(StatusUpdate::SelectDeviceNotice) => { + debug!("STATUS: Please select a device by touching one of them."); + let notification_str = + make_prompt("select-device", tid, origin, browsing_context_id); + controller.send_prompt(tid, ¬ification_str); + } + Ok(StatusUpdate::DeviceSelected(dev_info)) => { + debug!("STATUS: Continuing with device: {}", dev_info); + } + Ok(StatusUpdate::PresenceRequired) => { + debug!("STATUS: Waiting for user presence"); + let notification_str = make_prompt("presence", tid, origin, browsing_context_id); + controller.send_prompt(tid, ¬ification_str); + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => { + let guard = pin_receiver.lock(); + if let Ok(mut entry) = guard { + entry.replace((tid, sender)); + } else { + return; + } + let notification_str = + make_pin_required_prompt(tid, origin, browsing_context_id, false, -1); + controller.send_prompt(tid, ¬ification_str); + } + Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, attempts))) => { + let guard = pin_receiver.lock(); + if let Ok(mut entry) = guard { + entry.replace((tid, sender)); + } else { + return; + } + let notification_str = make_pin_required_prompt( + tid, + origin, + browsing_context_id, + true, + attempts.map_or(-1, |x| x as i64), + ); + controller.send_prompt(tid, ¬ification_str); + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => { + let notification_str = + make_prompt("pin-auth-blocked", tid, origin, browsing_context_id); + controller.send_prompt(tid, ¬ification_str); + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => { + let notification_str = + make_prompt("device-blocked", tid, origin, browsing_context_id); + controller.send_prompt(tid, ¬ification_str); + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinNotSet)) => { + let notification_str = make_prompt("pin-not-set", tid, origin, browsing_context_id); + controller.send_prompt(tid, ¬ification_str); + } + Ok(StatusUpdate::PinUvError(e)) => { + warn!("Unexpected error: {:?}", e) + } + Ok(StatusUpdate::InteractiveManagement((_, dev_info, auth_info))) => { + debug!( + "STATUS: interactive management: {}, {:?}", + dev_info, auth_info + ); + } + Err(RecvError) => { + debug!("STATUS: end"); + return; + } + } + } +} + +// AuthrsTransport provides an nsIWebAuthnTransport interface to an AuthenticatorService. This +// allows an nsIWebAuthnController to dispatch MakeCredential and GetAssertion requests to USB HID +// tokens. The AuthrsTransport struct also keeps 1) a pointer to the active nsIWebAuthnController, +// which is used to prompt the user for input and to return results to the controller; and +// 2) a channel through which to receive a pin callback. +#[xpcom(implement(nsIWebAuthnTransport), atomic)] +pub struct AuthrsTransport { + auth_service: RefCell<AuthenticatorService>, // interior mutable for use in XPCOM methods + controller: Controller, + pin_receiver: Arc<Mutex<PinReceiver>>, +} + +impl AuthrsTransport { + xpcom_method!(get_controller => GetController() -> *const nsIWebAuthnController); + fn get_controller(&self) -> Result<RefPtr<nsIWebAuthnController>, nsresult> { + Err(NS_ERROR_NOT_IMPLEMENTED) + } + + xpcom_method!(set_controller => SetController(aController: *const nsIWebAuthnController)); + fn set_controller(&self, controller: *const nsIWebAuthnController) -> Result<(), nsresult> { + self.controller.init(controller) + } + + xpcom_method!(pin_callback => PinCallback(aTransactionId: u64, aPin: *const nsACString)); + fn pin_callback(&self, transaction_id: u64, pin: &nsACString) -> Result<(), nsresult> { + let mut guard = self.pin_receiver.lock().or(Err(NS_ERROR_FAILURE))?; + match guard.take() { + // The pin_receiver is single-use. + Some((tid, channel)) if tid == transaction_id => channel + .send(Pin::new(&pin.to_string())) + .or(Err(NS_ERROR_FAILURE)), + // Either we weren't expecting a pin, or the controller is confused + // about which transaction is active. Neither is recoverable, so it's + // OK to drop the PinReceiver here. + _ => Err(NS_ERROR_FAILURE), + } + } + + xpcom_method!(make_credential => MakeCredential(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsICtapRegisterArgs)); + fn make_credential( + &self, + tid: u64, + browsing_context_id: u64, + args: *const nsICtapRegisterArgs, + ) -> Result<(), nsresult> { + if args.is_null() { + return Err(NS_ERROR_NULL_POINTER); + } + let args = unsafe { &*args }; + + 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_hash = ThinVec::new(); + unsafe { args.GetClientDataHash(&mut client_data_hash) }.to_result()?; + let mut client_data_hash_arr = [0u8; 32]; + client_data_hash_arr.copy_from_slice(&client_data_hash); + + let mut timeout_ms = 0u32; + unsafe { args.GetTimeoutMS(&mut timeout_ms) }.to_result()?; + + let mut exclude_list = ThinVec::new(); + unsafe { args.GetExcludeList(&mut exclude_list) }.to_result()?; + let exclude_list = exclude_list + .iter_mut() + .map(|id| PublicKeyCredentialDescriptor { + id: id.to_vec(), + transports: vec![], + }) + .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 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_cred_params = cose_algs + .iter() + .map(|alg| PublicKeyCredentialParameters::try_from(*alg).unwrap()) + .collect(); + + let mut resident_key = nsString::new(); + unsafe { args.GetResidentKey(&mut *resident_key) }.to_result()?; + let resident_key_req = if resident_key.eq("required") { + ResidentKeyRequirement::Required + } else if resident_key.eq("preferred") { + ResidentKeyRequirement::Preferred + } else if resident_key.eq("discouraged") { + ResidentKeyRequirement::Discouraged + } else { + return Err(NS_ERROR_FAILURE); + }; + + let mut user_verification = nsString::new(); + unsafe { args.GetUserVerification(&mut *user_verification) }.to_result()?; + let user_verification_req = if user_verification.eq("required") { + UserVerificationRequirement::Required + } else if user_verification.eq("preferred") { + UserVerificationRequirement::Preferred + } else if user_verification.eq("discouraged") { + UserVerificationRequirement::Discouraged + } else { + return Err(NS_ERROR_FAILURE); + }; + + let mut attestation_conveyance_preference = nsString::new(); + unsafe { args.GetAttestationConveyancePreference(&mut *attestation_conveyance_preference) } + .to_result()?; + let none_attestation = attestation_conveyance_preference.eq("none"); + + // TODO(Bug 1593571) - Add this to the extensions + // let mut hmac_create_secret = None; + // let mut maybe_hmac_create_secret = false; + // match unsafe { args.GetHmacCreateSecret(&mut maybe_hmac_create_secret) }.to_result() { + // Ok(_) => hmac_create_secret = Some(maybe_hmac_create_secret), + // _ => (), + // } + + let info = RegisterArgs { + client_data_hash: client_data_hash_arr, + relying_party: RelyingParty { + id: relying_party_id.to_string(), + name: None, + icon: None, + }, + origin: origin.to_string(), + user: User { + id: user_id.to_vec(), + icon: None, + name: Some(user_name.to_string()), + display_name: None, + }, + pub_cred_params, + exclude_list, + user_verification_req, + resident_key_req, + extensions: Default::default(), + pin: None, + use_ctap1_fallback: static_prefs::pref!("security.webauthn.ctap2") == false, + }; + + let (status_tx, status_rx) = channel::<StatusUpdate>(); + let pin_receiver = self.pin_receiver.clone(); + let controller = self.controller.clone(); + let status_origin = origin.to_string(); + RunnableBuilder::new( + "AuthrsTransport::MakeCredential::StatusReceiver", + move || { + status_callback( + status_rx, + tid, + &status_origin, + browsing_context_id, + controller, + pin_receiver, + ) + }, + ) + .may_block(true) + .dispatch_background_task()?; + + let controller = self.controller.clone(); + let callback_origin = origin.to_string(); + let state_callback = StateCallback::<Result<RegisterResult, AuthenticatorError>>::new( + Box::new(move |result| { + let result = match result { + Ok(RegisterResult::CTAP1(..)) => Err(AuthenticatorError::VersionMismatch( + "AuthrsTransport::MakeCredential", + 2, + )), + Ok(RegisterResult::CTAP2(mut attestation_object)) => { + // Tokens always provide attestation, but the user may have asked we not + // include the attestation statement in the response. + if none_attestation { + attestation_object.att_statement = AttestationStatement::None; + } + Ok(RegisterResult::CTAP2(attestation_object)) + } + Err(e @ AuthenticatorError::CredentialExcluded) => { + let notification_str = make_prompt( + "already-registered", + tid, + &callback_origin, + browsing_context_id, + ); + controller.send_prompt(tid, ¬ification_str); + Err(e) + } + Err(e) => Err(e), + }; + let _ = controller.finish_register(tid, result); + }), + ); + + self.auth_service + .borrow_mut() + .register(timeout_ms as u64, info.into(), status_tx, state_callback) + .or(Err(NS_ERROR_FAILURE)) + } + + xpcom_method!(get_assertion => GetAssertion(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsICtapSignArgs)); + fn get_assertion( + &self, + tid: u64, + browsing_context_id: u64, + args: *const nsICtapSignArgs, + ) -> Result<(), nsresult> { + if args.is_null() { + return Err(NS_ERROR_NULL_POINTER); + } + let args = unsafe { &*args }; + + 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_hash = ThinVec::new(); + unsafe { args.GetClientDataHash(&mut client_data_hash) }.to_result()?; + let mut client_data_hash_arr = [0u8; 32]; + client_data_hash_arr.copy_from_slice(&client_data_hash); + + let mut timeout_ms = 0u32; + unsafe { args.GetTimeoutMS(&mut timeout_ms) }.to_result()?; + + let mut allow_list = ThinVec::new(); + unsafe { args.GetAllowList(&mut allow_list) }.to_result()?; + let allow_list: Vec<_> = allow_list + .iter_mut() + .map(|id| PublicKeyCredentialDescriptor { + id: id.to_vec(), + transports: vec![], + }) + .collect(); + + let mut user_verification = nsString::new(); + unsafe { args.GetUserVerification(&mut *user_verification) }.to_result()?; + let user_verification_req = if user_verification.eq("required") { + UserVerificationRequirement::Required + } else if user_verification.eq("preferred") { + UserVerificationRequirement::Preferred + } else if user_verification.eq("discouraged") { + UserVerificationRequirement::Discouraged + } else { + return Err(NS_ERROR_FAILURE); + }; + + let mut alternate_rp_id = None; + let mut maybe_alternate_rp_id = nsString::new(); + match unsafe { args.GetAppId(&mut *maybe_alternate_rp_id) }.to_result() { + Ok(_) => alternate_rp_id = Some(maybe_alternate_rp_id.to_string()), + _ => (), + } + + let (status_tx, status_rx) = channel::<StatusUpdate>(); + let pin_receiver = self.pin_receiver.clone(); + let controller = self.controller.clone(); + let status_origin = origin.to_string(); + RunnableBuilder::new("AuthrsTransport::GetAssertion::StatusReceiver", move || { + status_callback( + status_rx, + tid, + &status_origin, + browsing_context_id, + controller, + pin_receiver, + ) + }) + .may_block(true) + .dispatch_background_task()?; + + let uniq_allowed_cred = if allow_list.len() == 1 { + allow_list.first().cloned() + } else { + None + }; + + let controller = self.controller.clone(); + let state_callback = + StateCallback::<Result<SignResult, AuthenticatorError>>::new(Box::new(move |result| { + let result = match result { + Ok(SignResult::CTAP1(..)) => Err(AuthenticatorError::VersionMismatch( + "AuthrsTransport::GetAssertion", + 2, + )), + Ok(SignResult::CTAP2(mut assertion_object)) => { + // In CTAP 2.0, but not CTAP 2.1, the assertion object's credential field + // "May be omitted if the allowList has exactly one Credential." If we had + // a unique allowed credential, then copy its descriptor to the output. + if uniq_allowed_cred.is_some() { + if let Some(assertion) = assertion_object.0.first_mut() { + if assertion.credentials.is_none() { + assertion.credentials = uniq_allowed_cred.clone(); + } + } + } + Ok(SignResult::CTAP2(assertion_object)) + } + Err(e) => Err(e), + }; + let _ = controller.finish_sign(tid, result); + })); + + // Bug 1834771 - Pre-filtering allowlists broke AppID support. As a temporary + // workaround, we will fallback to CTAP1 when the request includes the AppID + // extension and the allowlist is non-empty. + let use_ctap1_fallback = static_prefs::pref!("security.webauthn.ctap2") == false + || (alternate_rp_id.is_some() && !allow_list.is_empty()); + + let info = SignArgs { + client_data_hash: client_data_hash_arr, + relying_party_id: relying_party_id.to_string(), + origin: origin.to_string(), + allow_list, + user_verification_req, + user_presence_req: true, + extensions: Default::default(), + pin: None, + alternate_rp_id, + use_ctap1_fallback, + }; + + self.auth_service + .borrow_mut() + .sign(timeout_ms as u64, info.into(), status_tx, state_callback) + .or(Err(NS_ERROR_FAILURE)) + } + + xpcom_method!(cancel => Cancel()); + fn cancel(&self) -> Result<nsresult, nsresult> { + // We may be waiting for a pin. Drop the channel to release the + // state machine from `ask_user_for_pin`. + drop(self.pin_receiver.lock().or(Err(NS_ERROR_FAILURE))?.take()); + + match &self.auth_service.borrow_mut().cancel() { + Ok(_) => Ok(NS_OK), + Err(e) => Err(authrs_to_nserror(e)), + } + } +} + +#[no_mangle] +pub extern "C" fn authrs_transport_constructor( + result: *mut *const nsIWebAuthnTransport, +) -> nsresult { + let mut auth_service = match AuthenticatorService::new() { + Ok(auth_service) => auth_service, + _ => return NS_ERROR_FAILURE, + }; + auth_service.add_detected_transports(); + let wrapper = AuthrsTransport::allocate(InitAuthrsTransport { + auth_service: RefCell::new(auth_service), + controller: Controller(RefCell::new(std::ptr::null())), + pin_receiver: Arc::new(Mutex::new(None)), + }); + unsafe { + RefPtr::new(wrapper.coerce::<nsIWebAuthnTransport>()).forget(&mut *result); + } + NS_OK +} |