/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#[macro_use] externcrate log;
#[macro_use] externcrate xpcom;
use authenticator::{
authenticatorservice::{RegisterArgs, SignArgs},
ctap2::attestation::{AAGuid, AttestationObject, AttestationStatement},
ctap2::commands::{get_info::AuthenticatorVersion, PinUvAuthResult},
ctap2::server::{
AuthenticationExtensionsClientInputs, AuthenticationExtensionsPRFInputs,
AuthenticationExtensionsPRFOutputs, AuthenticationExtensionsPRFValues,
AuthenticatorAttachment, PublicKeyCredentialDescriptor, PublicKeyCredentialParameters,
PublicKeyCredentialUserEntity, RelyingParty, ResidentKeyRequirement,
UserVerificationRequirement,
},
errors::AuthenticatorError,
statecallback::StateCallback,
AuthenticatorInfo, BioEnrollmentResult, CredentialManagementResult, InteractiveRequest,
ManageResult, Pin, RegisterResult, SignResult, StateMachine, StatusPinUv, StatusUpdate,
}; use base64::Engine; use cstr::cstr; use moz_task::{get_main_thread, RunnableBuilder}; use nserror::{
nsresult, NS_ERROR_DOM_ABORT_ERR, NS_ERROR_DOM_INVALID_STATE_ERR, NS_ERROR_DOM_NOT_ALLOWED_ERR,
NS_ERROR_DOM_OPERATION_ERR, NS_ERROR_FAILURE, NS_ERROR_INVALID_ARG, NS_ERROR_NOT_AVAILABLE,
NS_ERROR_NOT_IMPLEMENTED, NS_ERROR_NULL_POINTER, NS_OK,
}; use nsstring::{nsACString, nsAString, nsCString, nsString}; use serde::Serialize; use serde_json::json; use std::cell::RefCell; use std::collections::HashMap; use std::fmt::Write; use std::sync::mpsc::{channel, Receiver, RecvError, Sender}; use std::sync::{Arc, Mutex, MutexGuard}; use thin_vec::{thin_vec, ThinVec}; use xpcom::interfaces::{
nsICredentialParameters, nsIObserverService, nsIWebAuthnAttObj, nsIWebAuthnAutoFillEntry,
nsIWebAuthnRegisterArgs, nsIWebAuthnRegisterPromise, nsIWebAuthnRegisterResult,
nsIWebAuthnService, nsIWebAuthnSignArgs, nsIWebAuthnSignPromise, nsIWebAuthnSignResult,
}; use xpcom::{xpcom_method, RefPtr}; mod about_webauthn_controller; use about_webauthn_controller::*; mod test_token; use test_token::TestTokenManager;
fn authrs_to_nserror(e: AuthenticatorError) -> nsresult { match e {
AuthenticatorError::CredentialExcluded => NS_ERROR_DOM_INVALID_STATE_ERR,
_ => NS_ERROR_DOM_NOT_ALLOWED_ERR,
}
}
fn should_cancel_prompts<T>(result: &Result<T, AuthenticatorError>) -> bool { match result {
Err(AuthenticatorError::CredentialExcluded) | Err(AuthenticatorError::PinError(_)) => false,
_ => true,
}
}
// Using serde(tag="type") makes it so that, for example, BrowserPromptType::Cancel is serialized // as '{ type: "cancel" }', and BrowserPromptType::PinInvalid { retries: 5 } is serialized as // '{type: "pin-invalid", retries: 5}'. #[derive(Serialize)] #[serde(tag = "type", rename_all = "kebab-case")] enum BrowserPromptType<'a> {
AlreadyRegistered,
Cancel,
DeviceBlocked,
PinAuthBlocked,
PinNotSet,
Presence,
SelectDevice,
UvBlocked,
PinRequired,
SelectedDevice {
auth_info: Option<AuthenticatorInfo>,
},
PinInvalid {
retries: Option<u8>,
},
PinIsTooLong,
PinIsTooShort,
UvInvalid {
retries: Option<u8>,
},
SelectSignResult {
entities: &'a [PublicKeyCredentialUserEntity],
},
ListenSuccess,
ListenError {
error: Box<BrowserPromptType<'a>>,
},
CredentialManagementUpdate {
result: CredentialManagementResult,
},
BioEnrollmentUpdate {
result: BioEnrollmentResult,
},
UnknownError,
}
#[xpcom(implement(nsIWebAuthnRegisterResult), atomic)] pubstruct WebAuthnRegisterResult { // result is only borrowed mutably in `Anonymize`.
result: RefCell<RegisterResult>,
}
xpcom_method!(get_transports => GetTransports() -> ThinVec<nsString>); fn get_transports(&self) -> Result<ThinVec<nsString>, nsresult> { // The list that we return here might be included in a future GetAssertion request as a // hint as to which transports to try. In production, we only support the "usb" transport. // In tests, the result is not very important, but we can at least return "internal" if // we're simulating platform attachment. if static_prefs::pref!("security.webauth.webauthn_enable_softtoken")
&& self.result.borrow().attachment == AuthenticatorAttachment::Platform
{
Ok(thin_vec![nsString::from("internal")])
} else {
Ok(thin_vec![nsString::from("usb")])
}
}
xpcom_method!(get_public_key_algorithm => GetPublicKeyAlgorithm() -> i32); fn get_public_key_algorithm(&self) -> Result<i32, nsresult> { let Some(credential_data) = &self.att_obj.auth_data.credential_data else { return Err(NS_ERROR_FAILURE);
}; // safe to cast to i32 by inspection of defined values
Ok(credential_data.credential_public_key.alg as i32)
}
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> {
Ok(self.result.extensions.prf.is_some())
}
// A transaction may create a channel to ask a user for additional input, e.g. a PIN. The Sender // component of this channel is sent to an AuthrsServide in a StatusUpdate. AuthrsService // caches the sender along with the expected (u64) transaction ID, which is used as a consistency // check in callbacks. type PinReceiver = Option<(u64, Sender<Pin>)>; type SelectionReceiver = Option<(u64, Sender<Option<usize>>)>;
fn status_callback(
status_rx: Receiver<StatusUpdate>,
tid: u64,
origin: &String,
browsing_context_id: u64,
transaction: Arc<Mutex<Option<TransactionState>>>, /* Shared with an AuthrsService */
) -> Result<(), nsresult> { let origin = Some(origin.as_str()); let browsing_context_id = Some(browsing_context_id); loop { match status_rx.recv() {
Ok(StatusUpdate::SelectDeviceNotice) => {
debug!("STATUS: Please select a device by touching one of them.");
send_prompt(
BrowserPromptType::SelectDevice,
tid,
origin,
browsing_context_id,
)?;
}
Ok(StatusUpdate::PresenceRequired) => {
debug!("STATUS: Waiting for user presence");
send_prompt(
BrowserPromptType::Presence,
tid,
origin,
browsing_context_id,
)?;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => { letmut guard = transaction.lock().unwrap(); let Some(transaction) = guard.as_mut() else {
warn!("STATUS: received status update after end of transaction."); break;
};
transaction.pin_receiver.replace((tid, sender));
send_prompt(
BrowserPromptType::PinRequired,
tid,
origin,
browsing_context_id,
)?;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, retries))) => { letmut guard = transaction.lock().unwrap(); let Some(transaction) = guard.as_mut() else {
warn!("STATUS: received status update after end of transaction."); break;
};
transaction.pin_receiver.replace((tid, sender));
send_prompt(
BrowserPromptType::PinInvalid { retries },
tid,
origin,
browsing_context_id,
)?;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => {
send_prompt(
BrowserPromptType::PinAuthBlocked,
tid,
origin,
browsing_context_id,
)?;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => {
send_prompt(
BrowserPromptType::DeviceBlocked,
tid,
origin,
browsing_context_id,
)?;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinNotSet)) => {
send_prompt(
BrowserPromptType::PinNotSet,
tid,
origin,
browsing_context_id,
)?;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(retries))) => {
send_prompt(
BrowserPromptType::UvInvalid { retries },
tid,
origin,
browsing_context_id,
)?;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => {
send_prompt(
BrowserPromptType::UvBlocked,
tid,
origin,
browsing_context_id,
)?;
}
Ok(StatusUpdate::PinUvError(StatusPinUv::PinIsTooShort))
| Ok(StatusUpdate::PinUvError(StatusPinUv::PinIsTooLong(..))) => { // These should never happen.
warn!("STATUS: Got unexpected StatusPinUv-error.");
}
Ok(StatusUpdate::InteractiveManagement(_)) => {
debug!("STATUS: interactive management");
}
Ok(StatusUpdate::SelectResultNotice(sender, entities)) => {
debug!("STATUS: select result notice"); letmut guard = transaction.lock().unwrap(); let Some(transaction) = guard.as_mut() else {
warn!("STATUS: received status update after end of transaction."); break;
};
transaction.selection_receiver.replace((tid, sender));
send_prompt(
BrowserPromptType::SelectSignResult {
entities: &entities,
},
tid,
origin,
browsing_context_id,
)?;
}
Err(RecvError) => {
debug!("STATUS: end"); break;
}
}
}
Ok(())
}
// AuthrsService provides an nsIWebAuthnService built on top of authenticator-rs. #[xpcom(implement(nsIWebAuthnService), atomic)] pubstruct AuthrsService {
usb_token_manager: Mutex<StateMachine>,
test_token_manager: TestTokenManager,
transaction: Arc<Mutex<Option<TransactionState>>>,
}
impl AuthrsService {
xpcom_method!(pin_callback => PinCallback(aTransactionId: u64, aPin: *const nsACString)); fn pin_callback(&self, transaction_id: u64, pin: &nsACString) -> Result<(), nsresult> { if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { letmut guard = self.transaction.lock().unwrap(); let Some(transaction) = guard.as_mut() else { // No ongoing transaction return Err(NS_ERROR_FAILURE);
}; let Some((tid, channel)) = transaction.pin_receiver.take() else { // We weren't expecting a pin. return Err(NS_ERROR_FAILURE);
}; if tid != transaction_id { // The browser is confused about which transaction is active. // This shouldn't happen return Err(NS_ERROR_FAILURE);
}
channel
.send(Pin::new(&pin.to_string()))
.or(Err(NS_ERROR_FAILURE))
} else { // Silently accept request, if all webauthn-options are disabled. // Used for testing.
Ok(())
}
}
xpcom_method!(selection_callback => SelectionCallback(aTransactionId: u64, aSelection: u64)); fn selection_callback(&self, transaction_id: u64, selection: u64) -> Result<(), nsresult> { letmut guard = self.transaction.lock().unwrap(); let Some(transaction) = guard.as_mut() else { // No ongoing transaction return Err(NS_ERROR_FAILURE);
}; let Some((tid, channel)) = transaction.selection_receiver.take() else { // We weren't expecting a selection. return Err(NS_ERROR_FAILURE);
}; if tid != transaction_id { // The browser is confused about which transaction is active. // This shouldn't happen return Err(NS_ERROR_FAILURE);
}
channel
.send(Some(selection as usize))
.or(Err(NS_ERROR_FAILURE))
}
letmut guard = self.transaction.lock().unwrap();
*guard = Some(TransactionState {
tid,
browsing_context_id,
pending_args: None,
promise: TransactionPromise::Register(promise),
pin_receiver: None,
selection_receiver: None,
interactive_receiver: None,
puat_cache: None,
}); // drop the guard here to ensure we don't deadlock if the call to `register()` below // hairpins the state callback.
drop(guard);
let (status_tx, status_rx) = channel::<StatusUpdate>(); let status_transaction = self.transaction.clone(); let status_origin = info.origin.clone();
RunnableBuilder::new("AuthrsService::MakeCredential::StatusReceiver", move || { let _ = status_callback(
status_rx,
tid,
&status_origin,
browsing_context_id,
status_transaction,
);
})
.may_block(true)
.dispatch_background_task()?;
let callback_transaction = self.transaction.clone(); let callback_origin = info.origin.clone(); let state_callback = StateCallback::<Result<RegisterResult, AuthenticatorError>>::new( Box::new(move |result| { letmut 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;
}; iflet Err(AuthenticatorError::CredentialExcluded) = result { let _ = send_prompt(
BrowserPromptType::AlreadyRegistered,
tid,
Some(&callback_origin),
Some(browsing_context_id),
);
} if should_cancel_prompts(&result) { // Some errors are accompanied by prompts that should persist after the // operation terminates. let _ = cancel_prompts(tid);
} let _ = promise.resolve_or_reject(result.map_err(authrs_to_nserror));
*guard = None;
}),
);
// The authenticator crate provides an `AuthenticatorService` which can dispatch a request // in parallel to any number of transports. We only support the USB transport in production // configurations, so we do not need the full generality of `AuthenticatorService` here. // We disable the USB transport in tests that use virtual devices. if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { // TODO(Bug 1855290) Remove this presence prompt
send_prompt(
BrowserPromptType::Presence,
tid,
Some(&info.origin),
Some(browsing_context_id),
)?; self.usb_token_manager.lock().unwrap().register(
timeout_ms.into(),
info,
status_tx,
state_callback,
);
} elseif static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { self.test_token_manager
.register(timeout_ms.into(), info, status_tx, state_callback);
} else { return Err(NS_ERROR_FAILURE);
}
// 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_input.is_some() && user_verification_req == UserVerificationRequirement::Discouraged
{
user_verification_req = UserVerificationRequirement::Preferred;
}
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 browsing_context_id = state.browsing_context_id; let tid = state.tid; let (timeout_ms, mut info) = match state.pending_args.take() {
Some(TransactionArgs::Sign(timeout_ms, info)) => (timeout_ms, info),
_ => return Err(NS_ERROR_FAILURE),
};
iflet Some(id) = selected_credential_id.take() { if info.allow_list.is_empty() {
info.allow_list.push(PublicKeyCredentialDescriptor {
id,
transports: vec![],
});
} else { // We need to ensure that the selected credential id // was in the original allow_list.
info.allow_list.retain(|cred| cred.id == id); if info.allow_list.is_empty() { return Err(NS_ERROR_FAILURE);
}
}
}
let (status_tx, status_rx) = channel::<StatusUpdate>(); let status_transaction = self.transaction.clone(); let status_origin = info.origin.to_string();
RunnableBuilder::new("AuthrsService::GetAssertion::StatusReceiver", move || { let _ = status_callback(
status_rx,
tid,
&status_origin,
browsing_context_id,
status_transaction,
);
})
.may_block(true)
.dispatch_background_task()?;
let uniq_allowed_cred = if info.allow_list.len() == 1 {
info.allow_list.first().cloned()
} else {
None
};
let callback_transaction = self.transaction.clone(); let state_callback = StateCallback::<Result<SignResult, AuthenticatorError>>::new( Box::new(move |mut result| { letmut 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;
}; if uniq_allowed_cred.is_some() { // 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. iflet Ok(inner) = result.as_mut() {
inner.assertion.credentials = uniq_allowed_cred;
}
} if should_cancel_prompts(&result) { // Some errors are accompanied by prompts that should persist after the // operation terminates. let _ = cancel_prompts(tid);
} let _ = promise.resolve_or_reject(result.map_err(authrs_to_nserror));
*guard = None;
}),
);
// 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 { letmut guard = self.transaction.lock().unwrap(); let Some(state) = guard.as_ref() else { returntrue; // workaround for Bug 1864526.
}; if state.tid != tid { // Ignore the cancellation request if the transaction // ID does not match. returnfalse;
} // 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
}
// We may get from status_updates info about certain errors (e.g. PinErrors) // which we want to present to the user. We will ignore the following error // which is caused by us "hanging up" on the StatusUpdate-channel and return // the PinError instead, via `upcoming_error`. let upcoming_error = Arc::new(Mutex::new(None)); let upcoming_error_c = upcoming_error.clone(); let callback_transaction = self.transaction.clone(); let state_callback = StateCallback::<Result<ManageResult, AuthenticatorError>>::new( Box::new(move |result| { letmut guard = callback_transaction.lock().unwrap(); match guard.as_mut() {
Some(state) => { match state.promise {
TransactionPromise::Listen => (),
_ => return,
}
*guard = None;
} // We have no transaction anymore, this means cancel() was called
None => (),
} let msg = match result {
Ok(_) => BrowserPromptType::ListenSuccess,
Err(e) => { // See if we have a cached error that should replace this error let replacement = iflet Ok(mut x) = upcoming_error_c.lock() {
x.take()
} else {
None
}; let replaced_err = replacement.unwrap_or(e); let err = authrs_to_prompt(replaced_err);
BrowserPromptType::ListenError {
error: Box::new(err),
}
}
}; let _ = send_about_prompt(&msg);
}),
);
// Calling `manage()` within the lock, to avoid race conditions // where we might check listen_blocked, see that it's false, // continue along, but in parallel `make_credential()` aborts the // interactive process shortly after, setting listen_blocked to true, // then accessing usb_token_manager afterwards and at the same time // we do it here, causing a runtime crash for trying to mut-borrow it twice. let (status_tx, status_rx) = channel::<StatusUpdate>(); let status_transaction = self.transaction.clone();
RunnableBuilder::new( "AuthrsTransport::AboutWebauthn::StatusReceiver", move || { let _ = interactive_status_callback(status_rx, status_transaction, upcoming_error);
},
)
.may_block(true)
.dispatch_background_task()?; if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { self.usb_token_manager.lock().unwrap().manage( 60 * 1000 * 1000,
status_tx,
state_callback,
);
} elseif static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { // We don't yet support softtoken
} else { // Silently accept request, if all webauthn-options are disabled. // Used for testing.
}
Ok(())
}
xpcom_method!(run_command => RunCommand(c_cmd: *const nsACString)); pubfn run_command(&self, c_cmd: &nsACString) -> Result<(), nsresult> { // Always test if it can be parsed from incoming JSON (even for tests) let incoming: RequestWrapper =
serde_json::from_str(&c_cmd.to_utf8()).or(Err(NS_ERROR_DOM_OPERATION_ERR))?; if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { let guard = self.transaction.lock().unwrap(); let puat = guard.as_ref().and_then(|g| g.puat_cache.clone()); let command = match incoming {
RequestWrapper::Quit => InteractiveRequest::Quit,
RequestWrapper::ChangePIN(a, b) => InteractiveRequest::ChangePIN(a, b),
RequestWrapper::SetPIN(a) => InteractiveRequest::SetPIN(a),
RequestWrapper::CredentialManagement(c) => {
InteractiveRequest::CredentialManagement(c, puat)
}
RequestWrapper::BioEnrollment(c) => InteractiveRequest::BioEnrollment(c, puat),
}; match &guard.as_ref().unwrap().interactive_receiver {
Some(channel) => channel.send(command).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),
}
} elseif static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { // We don't yet support softtoken
Ok(())
} else { // Silently accept request, if all webauthn-options are disabled. // Used for testing.
Ok(())
}
}
}
¤ Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.0.52Bemerkung:
(vorverarbeitet am 2026-06-17)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.