/* 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, CredentialProtectionPolicy, 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_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}; use thin_vec::{thin_vec, ThinVec}; use xpcom::interfaces::{
nsICredentialParameters, nsIObserverService, nsIWebAuthnAttObj,
nsIWebAuthnAutoFillEntriesCallback, 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,
}
// Returns true if the attestation object contains information that could // identify the user's authenticator and should not be exposed to the relying // party without consent. This mirrors the AttestationConveyancePreference // "none" transform in AttestationObject::anonymize: an object is identifying // if and only if anonymize would alter its attestation statement. The AAGUID // identifies the make/model of the authenticator but no individual // user/device, so it is not on its own considered identifying. fn attestation_is_identifying(att_obj: &AttestationObject) -> bool { iflet AttestationStatement::Packed(ref packed) = att_obj.att_stmt { // Self attestation ("packed" format, no x5c, zero AAGUID) is not // identifying. if packed.attestation_cert.is_empty()
&& att_obj
.auth_data
.credential_data
.as_ref()
.is_some_and(|d| d.aaguid == AAGuid::default())
{ returnfalse;
}
}
att_obj.att_stmt != AttestationStatement::None
}
#[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::LargeBlobData(_, _)) => {
debug!("STATUS: large blob data");
}
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();
debug_assert!(
guard.is_none(), "WebAuthnService should reset the platform service before dispatching MakeCredential"
);
*guard = Some(TransactionState {
tid,
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()
.inspect_err(|_| self.discard_transaction())?;
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),
)
.inspect_err(|_| self.discard_transaction())?; 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 { self.discard_transaction(); 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;
}
letmut guard = self.transaction.lock().unwrap();
debug_assert!(
guard.is_none(), "WebAuthnService should reset the platform service before dispatching GetAssertion"
);
*guard = Some(TransactionState {
tid,
promise: TransactionPromise::Sign(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 `sign()` 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.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()
.inspect_err(|_| self.discard_transaction())?;
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 active transaction state if tid matches. Returns true if // the cancel matched (or if there is no active transaction, as a // workaround for Bug 1864526), so the caller knows whether the // usb_token_manager should also be cancelled. 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
}
// Discard transaction state for a request that was never dispatched. We // don't reject the promise here; the parent WebAuthnService does that after // we return an error. fn discard_transaction(&self) {
*self.transaction.lock().unwrap() = None;
}
xpcom_method!(reset => Reset()); fn reset(&self) -> Result<(), nsresult> {
{ // Reset only affects the active transaction; pending conditional // gets coexist with the active modal flow and survive resets. iflet Some(state) = self.transaction.lock().unwrap().take() {
cancel_prompts(state.tid)?;
state.promise.reject(NS_ERROR_DOM_NOT_ALLOWED_ERR)?;
}
} // release the transaction lock so a StateCallback can take it self.usb_token_manager.lock().unwrap().cancel();
Ok(())
}
// 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()
.inspect_err(|_| self.discard_transaction())?; if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { self.usb_token_manager.lock().unwrap().manage( 60 * 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 Some(active) = guard.as_ref() else { return Err(NS_ERROR_FAILURE);
}; let puat = active.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 &active.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.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.