lib.rs (68521B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 #[macro_use] 6 extern crate log; 7 8 #[macro_use] 9 extern crate xpcom; 10 11 use authenticator::{ 12 authenticatorservice::{RegisterArgs, SignArgs}, 13 ctap2::attestation::{AAGuid, AttestationObject, AttestationStatement}, 14 ctap2::commands::{get_info::AuthenticatorVersion, PinUvAuthResult}, 15 ctap2::server::{ 16 AuthenticationExtensionsClientInputs, AuthenticationExtensionsPRFInputs, 17 AuthenticationExtensionsPRFOutputs, AuthenticationExtensionsPRFValues, 18 AuthenticatorAttachment, CredentialProtectionPolicy, PublicKeyCredentialDescriptor, 19 PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty, 20 ResidentKeyRequirement, UserVerificationRequirement, 21 }, 22 errors::AuthenticatorError, 23 statecallback::StateCallback, 24 AuthenticatorInfo, BioEnrollmentResult, CredentialManagementResult, InteractiveRequest, 25 ManageResult, Pin, RegisterResult, SignResult, StateMachine, StatusPinUv, StatusUpdate, 26 }; 27 use base64::Engine; 28 use cstr::cstr; 29 use moz_task::{get_main_thread, RunnableBuilder}; 30 use nserror::{ 31 nsresult, NS_ERROR_DOM_ABORT_ERR, NS_ERROR_DOM_INVALID_STATE_ERR, NS_ERROR_DOM_NOT_ALLOWED_ERR, 32 NS_ERROR_DOM_OPERATION_ERR, NS_ERROR_FAILURE, NS_ERROR_INVALID_ARG, NS_ERROR_NOT_AVAILABLE, 33 NS_ERROR_NOT_IMPLEMENTED, NS_ERROR_NULL_POINTER, NS_OK, 34 }; 35 use nsstring::{nsACString, nsAString, nsCString, nsString}; 36 use serde::Serialize; 37 use serde_json::json; 38 use std::cell::RefCell; 39 use std::collections::HashMap; 40 use std::fmt::Write; 41 use std::sync::mpsc::{channel, Receiver, RecvError, Sender}; 42 use std::sync::{Arc, Mutex, MutexGuard}; 43 use thin_vec::{thin_vec, ThinVec}; 44 use xpcom::interfaces::{ 45 nsICredentialParameters, nsIObserverService, nsIWebAuthnAttObj, nsIWebAuthnAutoFillEntry, 46 nsIWebAuthnRegisterArgs, nsIWebAuthnRegisterPromise, nsIWebAuthnRegisterResult, 47 nsIWebAuthnService, nsIWebAuthnSignArgs, nsIWebAuthnSignPromise, nsIWebAuthnSignResult, 48 }; 49 use xpcom::{xpcom_method, RefPtr}; 50 mod about_webauthn_controller; 51 use about_webauthn_controller::*; 52 mod test_token; 53 use test_token::TestTokenManager; 54 55 fn authrs_to_nserror(e: AuthenticatorError) -> nsresult { 56 match e { 57 AuthenticatorError::CredentialExcluded => NS_ERROR_DOM_INVALID_STATE_ERR, 58 _ => NS_ERROR_DOM_NOT_ALLOWED_ERR, 59 } 60 } 61 62 fn should_cancel_prompts<T>(result: &Result<T, AuthenticatorError>) -> bool { 63 match result { 64 Err(AuthenticatorError::CredentialExcluded) | Err(AuthenticatorError::PinError(_)) => false, 65 _ => true, 66 } 67 } 68 69 // Using serde(tag="type") makes it so that, for example, BrowserPromptType::Cancel is serialized 70 // as '{ type: "cancel" }', and BrowserPromptType::PinInvalid { retries: 5 } is serialized as 71 // '{type: "pin-invalid", retries: 5}'. 72 #[derive(Serialize)] 73 #[serde(tag = "type", rename_all = "kebab-case")] 74 enum BrowserPromptType<'a> { 75 AlreadyRegistered, 76 Cancel, 77 DeviceBlocked, 78 PinAuthBlocked, 79 PinNotSet, 80 Presence, 81 SelectDevice, 82 UvBlocked, 83 PinRequired, 84 SelectedDevice { 85 auth_info: Option<AuthenticatorInfo>, 86 }, 87 PinInvalid { 88 retries: Option<u8>, 89 }, 90 PinIsTooLong, 91 PinIsTooShort, 92 UvInvalid { 93 retries: Option<u8>, 94 }, 95 SelectSignResult { 96 entities: &'a [PublicKeyCredentialUserEntity], 97 }, 98 ListenSuccess, 99 ListenError { 100 error: Box<BrowserPromptType<'a>>, 101 }, 102 CredentialManagementUpdate { 103 result: CredentialManagementResult, 104 }, 105 BioEnrollmentUpdate { 106 result: BioEnrollmentResult, 107 }, 108 UnknownError, 109 } 110 111 #[derive(Debug)] 112 enum PromptTarget { 113 Browser, 114 AboutPage, 115 } 116 117 #[derive(Serialize)] 118 struct BrowserPromptMessage<'a> { 119 prompt: BrowserPromptType<'a>, 120 tid: u64, 121 origin: Option<&'a str>, 122 #[serde(rename = "browsingContextId")] 123 browsing_context_id: Option<u64>, 124 } 125 126 fn notify_observers(prompt_target: PromptTarget, json: nsString) -> Result<(), nsresult> { 127 let main_thread = get_main_thread()?; 128 let target = match prompt_target { 129 PromptTarget::Browser => cstr!("webauthn-prompt"), 130 PromptTarget::AboutPage => cstr!("about-webauthn-prompt"), 131 }; 132 133 RunnableBuilder::new("AuthrsService::send_prompt", move || { 134 if let Ok(obs_svc) = xpcom::components::Observer::service::<nsIObserverService>() { 135 unsafe { 136 obs_svc.NotifyObservers(std::ptr::null(), target.as_ptr(), json.as_ptr()); 137 } 138 } 139 }) 140 .dispatch(main_thread.coerce()) 141 } 142 143 fn send_prompt( 144 prompt: BrowserPromptType, 145 tid: u64, 146 origin: Option<&str>, 147 browsing_context_id: Option<u64>, 148 ) -> Result<(), nsresult> { 149 let mut json = nsString::new(); 150 write!( 151 json, 152 "{}", 153 json!(&BrowserPromptMessage { 154 prompt, 155 tid, 156 origin, 157 browsing_context_id 158 }) 159 ) 160 .or(Err(NS_ERROR_FAILURE))?; 161 notify_observers(PromptTarget::Browser, json) 162 } 163 164 fn cancel_prompts(tid: u64) -> Result<(), nsresult> { 165 send_prompt(BrowserPromptType::Cancel, tid, None, None)?; 166 Ok(()) 167 } 168 169 #[xpcom(implement(nsIWebAuthnRegisterResult), atomic)] 170 pub struct WebAuthnRegisterResult { 171 // result is only borrowed mutably in `Anonymize`. 172 result: RefCell<RegisterResult>, 173 } 174 175 impl WebAuthnRegisterResult { 176 xpcom_method!(get_client_data_json => GetClientDataJSON() -> nsACString); 177 fn get_client_data_json(&self) -> Result<nsCString, nsresult> { 178 Err(NS_ERROR_NOT_AVAILABLE) 179 } 180 181 xpcom_method!(get_attestation_object => GetAttestationObject() -> ThinVec<u8>); 182 fn get_attestation_object(&self) -> Result<ThinVec<u8>, nsresult> { 183 let mut out = ThinVec::new(); 184 serde_cbor::to_writer(&mut out, &self.result.borrow().att_obj).or(Err(NS_ERROR_FAILURE))?; 185 Ok(out) 186 } 187 188 xpcom_method!(get_attestation_consent_prompt_shown => GetAttestationConsentPromptShown() -> bool); 189 fn get_attestation_consent_prompt_shown(&self) -> Result<bool, nsresult> { 190 Ok(false) 191 } 192 193 xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>); 194 fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> { 195 let Some(credential_data) = &self.result.borrow().att_obj.auth_data.credential_data else { 196 return Err(NS_ERROR_FAILURE); 197 }; 198 Ok(credential_data.credential_id.as_slice().into()) 199 } 200 201 xpcom_method!(get_transports => GetTransports() -> ThinVec<nsString>); 202 fn get_transports(&self) -> Result<ThinVec<nsString>, nsresult> { 203 // The list that we return here might be included in a future GetAssertion request as a 204 // hint as to which transports to try. In production, we only support the "usb" transport. 205 // In tests, the result is not very important, but we can at least return "internal" if 206 // we're simulating platform attachment. 207 if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") 208 && self.result.borrow().attachment == AuthenticatorAttachment::Platform 209 { 210 Ok(thin_vec![nsString::from("internal")]) 211 } else { 212 Ok(thin_vec![nsString::from("usb")]) 213 } 214 } 215 216 xpcom_method!(get_hmac_create_secret => GetHmacCreateSecret() -> bool); 217 fn get_hmac_create_secret(&self) -> Result<bool, nsresult> { 218 let Some(hmac_create_secret) = self.result.borrow().extensions.hmac_create_secret else { 219 return Err(NS_ERROR_NOT_AVAILABLE); 220 }; 221 Ok(hmac_create_secret) 222 } 223 224 xpcom_method!(get_large_blob_supported => GetLargeBlobSupported() -> bool); 225 fn get_large_blob_supported(&self) -> Result<bool, nsresult> { 226 Err(NS_ERROR_NOT_AVAILABLE) 227 } 228 229 xpcom_method!(get_prf_enabled => GetPrfEnabled() -> bool); 230 fn get_prf_enabled(&self) -> Result<bool, nsresult> { 231 match self.result.borrow().extensions.prf { 232 Some(AuthenticationExtensionsPRFOutputs { 233 enabled: Some(prf_enabled), 234 .. 235 }) => Ok(prf_enabled), 236 _ => Err(NS_ERROR_NOT_AVAILABLE), 237 } 238 } 239 240 xpcom_method!(get_prf_results_first => GetPrfResultsFirst() -> ThinVec<u8>); 241 fn get_prf_results_first(&self) -> Result<ThinVec<u8>, nsresult> { 242 match &self.result.borrow().extensions.prf { 243 Some(AuthenticationExtensionsPRFOutputs { 244 results: Some(AuthenticationExtensionsPRFValues { first, .. }), 245 .. 246 }) => Ok(first.as_slice().into()), 247 _ => Err(NS_ERROR_NOT_AVAILABLE), 248 } 249 } 250 251 xpcom_method!(get_prf_results_second => GetPrfResultsSecond() -> ThinVec<u8>); 252 fn get_prf_results_second(&self) -> Result<ThinVec<u8>, nsresult> { 253 match &self.result.borrow().extensions.prf { 254 Some(AuthenticationExtensionsPRFOutputs { 255 results: 256 Some(AuthenticationExtensionsPRFValues { 257 second: Some(second), 258 .. 259 }), 260 .. 261 }) => Ok(second.as_slice().into()), 262 _ => Err(NS_ERROR_NOT_AVAILABLE), 263 } 264 } 265 266 xpcom_method!(get_cred_props_rk => GetCredPropsRk() -> bool); 267 fn get_cred_props_rk(&self) -> Result<bool, nsresult> { 268 let Some(cred_props) = &self.result.borrow().extensions.cred_props else { 269 return Err(NS_ERROR_NOT_AVAILABLE); 270 }; 271 Ok(cred_props.rk) 272 } 273 274 xpcom_method!(set_cred_props_rk => SetCredPropsRk(aCredPropsRk: bool)); 275 fn set_cred_props_rk(&self, _cred_props_rk: bool) -> Result<(), nsresult> { 276 Err(NS_ERROR_NOT_IMPLEMENTED) 277 } 278 279 xpcom_method!(get_authenticator_attachment => GetAuthenticatorAttachment() -> nsAString); 280 fn get_authenticator_attachment(&self) -> Result<nsString, nsresult> { 281 match self.result.borrow().attachment { 282 AuthenticatorAttachment::CrossPlatform => Ok(nsString::from("cross-platform")), 283 AuthenticatorAttachment::Platform => Ok(nsString::from("platform")), 284 AuthenticatorAttachment::Unknown => Err(NS_ERROR_NOT_AVAILABLE), 285 } 286 } 287 288 xpcom_method!(has_identifying_attestation => HasIdentifyingAttestation() -> bool); 289 fn has_identifying_attestation(&self) -> Result<bool, nsresult> { 290 if self.result.borrow().att_obj.att_stmt != AttestationStatement::None { 291 return Ok(true); 292 } 293 if let Some(data) = &self.result.borrow().att_obj.auth_data.credential_data { 294 return Ok(data.aaguid != AAGuid::default()); 295 } 296 Ok(false) 297 } 298 299 xpcom_method!(anonymize => Anonymize()); 300 fn anonymize(&self) -> Result<nsresult, nsresult> { 301 self.result.borrow_mut().att_obj.anonymize(); 302 Ok(NS_OK) 303 } 304 } 305 306 #[xpcom(implement(nsIWebAuthnAttObj), atomic)] 307 pub struct WebAuthnAttObj { 308 att_obj: AttestationObject, 309 } 310 311 impl WebAuthnAttObj { 312 xpcom_method!(get_attestation_object => GetAttestationObject() -> ThinVec<u8>); 313 fn get_attestation_object(&self) -> Result<ThinVec<u8>, nsresult> { 314 let mut out = ThinVec::new(); 315 serde_cbor::to_writer(&mut out, &self.att_obj).or(Err(NS_ERROR_FAILURE))?; 316 Ok(out) 317 } 318 319 xpcom_method!(get_authenticator_data => GetAuthenticatorData() -> ThinVec<u8>); 320 fn get_authenticator_data(&self) -> Result<ThinVec<u8>, nsresult> { 321 // TODO(https://github.com/mozilla/authenticator-rs/issues/302) use to_writer 322 Ok(self.att_obj.auth_data.to_vec().into()) 323 } 324 325 xpcom_method!(get_public_key => GetPublicKey() -> ThinVec<u8>); 326 fn get_public_key(&self) -> Result<ThinVec<u8>, nsresult> { 327 let Some(credential_data) = &self.att_obj.auth_data.credential_data else { 328 return Err(NS_ERROR_FAILURE); 329 }; 330 Ok(credential_data 331 .credential_public_key 332 .der_spki() 333 .or(Err(NS_ERROR_NOT_AVAILABLE))? 334 .into()) 335 } 336 337 xpcom_method!(get_public_key_algorithm => GetPublicKeyAlgorithm() -> i32); 338 fn get_public_key_algorithm(&self) -> Result<i32, nsresult> { 339 let Some(credential_data) = &self.att_obj.auth_data.credential_data else { 340 return Err(NS_ERROR_FAILURE); 341 }; 342 // safe to cast to i32 by inspection of defined values 343 Ok(credential_data.credential_public_key.alg as i32) 344 } 345 346 xpcom_method!(is_identifying => IsIdentifying() -> bool); 347 fn is_identifying(&self) -> Result<bool, nsresult> { 348 if self.att_obj.att_stmt != AttestationStatement::None { 349 return Ok(true); 350 } 351 if let Some(data) = &self.att_obj.auth_data.credential_data { 352 return Ok(data.aaguid != AAGuid::default()); 353 } 354 Ok(false) 355 } 356 } 357 358 #[xpcom(implement(nsIWebAuthnSignResult), atomic)] 359 pub struct WebAuthnSignResult { 360 result: SignResult, 361 } 362 363 impl WebAuthnSignResult { 364 xpcom_method!(get_client_data_json => GetClientDataJSON() -> nsACString); 365 fn get_client_data_json(&self) -> Result<nsCString, nsresult> { 366 Err(NS_ERROR_NOT_AVAILABLE) 367 } 368 369 xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>); 370 fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> { 371 let Some(cred) = &self.result.assertion.credentials else { 372 return Err(NS_ERROR_FAILURE); 373 }; 374 Ok(cred.id.as_slice().into()) 375 } 376 377 xpcom_method!(get_signature => GetSignature() -> ThinVec<u8>); 378 fn get_signature(&self) -> Result<ThinVec<u8>, nsresult> { 379 Ok(self.result.assertion.signature.as_slice().into()) 380 } 381 382 xpcom_method!(get_authenticator_data => GetAuthenticatorData() -> ThinVec<u8>); 383 fn get_authenticator_data(&self) -> Result<ThinVec<u8>, nsresult> { 384 Ok(self.result.assertion.auth_data.to_vec().into()) 385 } 386 387 xpcom_method!(get_user_handle => GetUserHandle() -> ThinVec<u8>); 388 fn get_user_handle(&self) -> Result<ThinVec<u8>, nsresult> { 389 let Some(user) = &self.result.assertion.user else { 390 return Err(NS_ERROR_NOT_AVAILABLE); 391 }; 392 Ok(user.id.as_slice().into()) 393 } 394 395 xpcom_method!(get_user_name => GetUserName() -> nsACString); 396 fn get_user_name(&self) -> Result<nsCString, nsresult> { 397 let Some(user) = &self.result.assertion.user else { 398 return Err(NS_ERROR_NOT_AVAILABLE); 399 }; 400 let Some(name) = &user.name else { 401 return Err(NS_ERROR_NOT_AVAILABLE); 402 }; 403 Ok(nsCString::from(name)) 404 } 405 406 xpcom_method!(get_authenticator_attachment => GetAuthenticatorAttachment() -> nsAString); 407 fn get_authenticator_attachment(&self) -> Result<nsString, nsresult> { 408 match self.result.attachment { 409 AuthenticatorAttachment::CrossPlatform => Ok(nsString::from("cross-platform")), 410 AuthenticatorAttachment::Platform => Ok(nsString::from("platform")), 411 AuthenticatorAttachment::Unknown => Err(NS_ERROR_NOT_AVAILABLE), 412 } 413 } 414 415 xpcom_method!(get_used_app_id => GetUsedAppId() -> bool); 416 fn get_used_app_id(&self) -> Result<bool, nsresult> { 417 self.result.extensions.app_id.ok_or(NS_ERROR_NOT_AVAILABLE) 418 } 419 420 xpcom_method!(set_used_app_id => SetUsedAppId(aUsedAppId: bool)); 421 fn set_used_app_id(&self, _used_app_id: bool) -> Result<(), nsresult> { 422 Err(NS_ERROR_NOT_IMPLEMENTED) 423 } 424 425 xpcom_method!(get_large_blob_value => GetLargeBlobValue() -> ThinVec<u8>); 426 fn get_large_blob_value(&self) -> Result<ThinVec<u8>, nsresult> { 427 Err(NS_ERROR_NOT_AVAILABLE) 428 } 429 430 xpcom_method!(get_large_blob_written => GetLargeBlobWritten() -> bool); 431 fn get_large_blob_written(&self) -> Result<bool, nsresult> { 432 Err(NS_ERROR_NOT_AVAILABLE) 433 } 434 435 xpcom_method!(get_prf_maybe => GetPrfMaybe() -> bool); 436 /// Return true if a PRF output is present, even if all attributes are absent. 437 fn get_prf_maybe(&self) -> Result<bool, nsresult> { 438 Ok(self.result.extensions.prf.is_some()) 439 } 440 441 xpcom_method!(get_prf_results_first => GetPrfResultsFirst() -> ThinVec<u8>); 442 fn get_prf_results_first(&self) -> Result<ThinVec<u8>, nsresult> { 443 match &self.result.extensions.prf { 444 Some(AuthenticationExtensionsPRFOutputs { 445 results: Some(AuthenticationExtensionsPRFValues { first, .. }), 446 .. 447 }) => Ok(first.as_slice().into()), 448 _ => Err(NS_ERROR_NOT_AVAILABLE), 449 } 450 } 451 452 xpcom_method!(get_prf_results_second => GetPrfResultsSecond() -> ThinVec<u8>); 453 fn get_prf_results_second(&self) -> Result<ThinVec<u8>, nsresult> { 454 match &self.result.extensions.prf { 455 Some(AuthenticationExtensionsPRFOutputs { 456 results: 457 Some(AuthenticationExtensionsPRFValues { 458 second: Some(second), 459 .. 460 }), 461 .. 462 }) => Ok(second.as_slice().into()), 463 _ => Err(NS_ERROR_NOT_AVAILABLE), 464 } 465 } 466 } 467 468 // A transaction may create a channel to ask a user for additional input, e.g. a PIN. The Sender 469 // component of this channel is sent to an AuthrsServide in a StatusUpdate. AuthrsService 470 // caches the sender along with the expected (u64) transaction ID, which is used as a consistency 471 // check in callbacks. 472 type PinReceiver = Option<(u64, Sender<Pin>)>; 473 type SelectionReceiver = Option<(u64, Sender<Option<usize>>)>; 474 475 fn status_callback( 476 status_rx: Receiver<StatusUpdate>, 477 tid: u64, 478 origin: &String, 479 browsing_context_id: u64, 480 transaction: Arc<Mutex<Option<TransactionState>>>, /* Shared with an AuthrsService */ 481 ) -> Result<(), nsresult> { 482 let origin = Some(origin.as_str()); 483 let browsing_context_id = Some(browsing_context_id); 484 loop { 485 match status_rx.recv() { 486 Ok(StatusUpdate::SelectDeviceNotice) => { 487 debug!("STATUS: Please select a device by touching one of them."); 488 send_prompt( 489 BrowserPromptType::SelectDevice, 490 tid, 491 origin, 492 browsing_context_id, 493 )?; 494 } 495 Ok(StatusUpdate::PresenceRequired) => { 496 debug!("STATUS: Waiting for user presence"); 497 send_prompt( 498 BrowserPromptType::Presence, 499 tid, 500 origin, 501 browsing_context_id, 502 )?; 503 } 504 Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => { 505 let mut guard = transaction.lock().unwrap(); 506 let Some(transaction) = guard.as_mut() else { 507 warn!("STATUS: received status update after end of transaction."); 508 break; 509 }; 510 transaction.pin_receiver.replace((tid, sender)); 511 send_prompt( 512 BrowserPromptType::PinRequired, 513 tid, 514 origin, 515 browsing_context_id, 516 )?; 517 } 518 Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, retries))) => { 519 let mut guard = transaction.lock().unwrap(); 520 let Some(transaction) = guard.as_mut() else { 521 warn!("STATUS: received status update after end of transaction."); 522 break; 523 }; 524 transaction.pin_receiver.replace((tid, sender)); 525 send_prompt( 526 BrowserPromptType::PinInvalid { retries }, 527 tid, 528 origin, 529 browsing_context_id, 530 )?; 531 } 532 Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => { 533 send_prompt( 534 BrowserPromptType::PinAuthBlocked, 535 tid, 536 origin, 537 browsing_context_id, 538 )?; 539 } 540 Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => { 541 send_prompt( 542 BrowserPromptType::DeviceBlocked, 543 tid, 544 origin, 545 browsing_context_id, 546 )?; 547 } 548 Ok(StatusUpdate::PinUvError(StatusPinUv::PinNotSet)) => { 549 send_prompt( 550 BrowserPromptType::PinNotSet, 551 tid, 552 origin, 553 browsing_context_id, 554 )?; 555 } 556 Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(retries))) => { 557 send_prompt( 558 BrowserPromptType::UvInvalid { retries }, 559 tid, 560 origin, 561 browsing_context_id, 562 )?; 563 } 564 Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => { 565 send_prompt( 566 BrowserPromptType::UvBlocked, 567 tid, 568 origin, 569 browsing_context_id, 570 )?; 571 } 572 Ok(StatusUpdate::PinUvError(StatusPinUv::PinIsTooShort)) 573 | Ok(StatusUpdate::PinUvError(StatusPinUv::PinIsTooLong(..))) => { 574 // These should never happen. 575 warn!("STATUS: Got unexpected StatusPinUv-error."); 576 } 577 Ok(StatusUpdate::InteractiveManagement(_)) => { 578 debug!("STATUS: interactive management"); 579 } 580 Ok(StatusUpdate::SelectResultNotice(sender, entities)) => { 581 debug!("STATUS: select result notice"); 582 let mut guard = transaction.lock().unwrap(); 583 let Some(transaction) = guard.as_mut() else { 584 warn!("STATUS: received status update after end of transaction."); 585 break; 586 }; 587 transaction.selection_receiver.replace((tid, sender)); 588 send_prompt( 589 BrowserPromptType::SelectSignResult { 590 entities: &entities, 591 }, 592 tid, 593 origin, 594 browsing_context_id, 595 )?; 596 } 597 Err(RecvError) => { 598 debug!("STATUS: end"); 599 break; 600 } 601 } 602 } 603 Ok(()) 604 } 605 606 #[derive(Clone)] 607 struct RegisterPromise(RefPtr<nsIWebAuthnRegisterPromise>); 608 609 impl RegisterPromise { 610 fn resolve_or_reject(&self, result: Result<RegisterResult, nsresult>) -> Result<(), nsresult> { 611 match result { 612 Ok(result) => { 613 let wrapped_result = WebAuthnRegisterResult::allocate(InitWebAuthnRegisterResult { 614 result: RefCell::new(result), 615 }) 616 .query_interface::<nsIWebAuthnRegisterResult>() 617 .ok_or(NS_ERROR_FAILURE)?; 618 unsafe { self.0.Resolve(wrapped_result.coerce()) }; 619 } 620 Err(result) => { 621 unsafe { self.0.Reject(result) }; 622 } 623 } 624 Ok(()) 625 } 626 } 627 628 #[derive(Clone)] 629 struct SignPromise(RefPtr<nsIWebAuthnSignPromise>); 630 631 impl SignPromise { 632 fn resolve_or_reject(&self, result: Result<SignResult, nsresult>) -> Result<(), nsresult> { 633 match result { 634 Ok(result) => { 635 let wrapped_result = 636 WebAuthnSignResult::allocate(InitWebAuthnSignResult { result }) 637 .query_interface::<nsIWebAuthnSignResult>() 638 .ok_or(NS_ERROR_FAILURE)?; 639 unsafe { self.0.Resolve(wrapped_result.coerce()) }; 640 } 641 Err(result) => { 642 unsafe { self.0.Reject(result) }; 643 } 644 } 645 Ok(()) 646 } 647 } 648 649 #[derive(Clone)] 650 enum TransactionPromise { 651 Listen, 652 Register(RegisterPromise), 653 Sign(SignPromise), 654 } 655 656 impl TransactionPromise { 657 fn reject(&self, err: nsresult) -> Result<(), nsresult> { 658 match self { 659 TransactionPromise::Listen => Ok(()), 660 TransactionPromise::Register(promise) => promise.resolve_or_reject(Err(err)), 661 TransactionPromise::Sign(promise) => promise.resolve_or_reject(Err(err)), 662 } 663 } 664 } 665 666 enum TransactionArgs { 667 Sign(/* timeout */ u64, SignArgs), 668 } 669 670 struct TransactionState { 671 tid: u64, 672 browsing_context_id: u64, 673 pending_args: Option<TransactionArgs>, 674 promise: TransactionPromise, 675 pin_receiver: PinReceiver, 676 selection_receiver: SelectionReceiver, 677 interactive_receiver: InteractiveManagementReceiver, 678 puat_cache: Option<PinUvAuthResult>, // Cached credential to avoid repeated PIN-entries 679 } 680 681 // AuthrsService provides an nsIWebAuthnService built on top of authenticator-rs. 682 #[xpcom(implement(nsIWebAuthnService), atomic)] 683 pub struct AuthrsService { 684 usb_token_manager: Mutex<StateMachine>, 685 test_token_manager: TestTokenManager, 686 transaction: Arc<Mutex<Option<TransactionState>>>, 687 } 688 689 impl AuthrsService { 690 xpcom_method!(pin_callback => PinCallback(aTransactionId: u64, aPin: *const nsACString)); 691 fn pin_callback(&self, transaction_id: u64, pin: &nsACString) -> Result<(), nsresult> { 692 if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { 693 let mut guard = self.transaction.lock().unwrap(); 694 let Some(transaction) = guard.as_mut() else { 695 // No ongoing transaction 696 return Err(NS_ERROR_FAILURE); 697 }; 698 let Some((tid, channel)) = transaction.pin_receiver.take() else { 699 // We weren't expecting a pin. 700 return Err(NS_ERROR_FAILURE); 701 }; 702 if tid != transaction_id { 703 // The browser is confused about which transaction is active. 704 // This shouldn't happen 705 return Err(NS_ERROR_FAILURE); 706 } 707 channel 708 .send(Pin::new(&pin.to_string())) 709 .or(Err(NS_ERROR_FAILURE)) 710 } else { 711 // Silently accept request, if all webauthn-options are disabled. 712 // Used for testing. 713 Ok(()) 714 } 715 } 716 717 xpcom_method!(selection_callback => SelectionCallback(aTransactionId: u64, aSelection: u64)); 718 fn selection_callback(&self, transaction_id: u64, selection: u64) -> Result<(), nsresult> { 719 let mut guard = self.transaction.lock().unwrap(); 720 let Some(transaction) = guard.as_mut() else { 721 // No ongoing transaction 722 return Err(NS_ERROR_FAILURE); 723 }; 724 let Some((tid, channel)) = transaction.selection_receiver.take() else { 725 // We weren't expecting a selection. 726 return Err(NS_ERROR_FAILURE); 727 }; 728 if tid != transaction_id { 729 // The browser is confused about which transaction is active. 730 // This shouldn't happen 731 return Err(NS_ERROR_FAILURE); 732 } 733 channel 734 .send(Some(selection as usize)) 735 .or(Err(NS_ERROR_FAILURE)) 736 } 737 738 xpcom_method!(get_is_uvpaa => GetIsUVPAA() -> bool); 739 fn get_is_uvpaa(&self) -> Result<bool, nsresult> { 740 if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { 741 Ok(false) 742 } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { 743 Ok(self 744 .test_token_manager 745 .has_user_verifying_platform_authenticator()) 746 } else { 747 Err(NS_ERROR_NOT_AVAILABLE) 748 } 749 } 750 751 xpcom_method!(make_credential => MakeCredential(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsIWebAuthnRegisterArgs, aPromise: *const nsIWebAuthnRegisterPromise)); 752 fn make_credential( 753 &self, 754 tid: u64, 755 browsing_context_id: u64, 756 args: &nsIWebAuthnRegisterArgs, 757 promise: &nsIWebAuthnRegisterPromise, 758 ) -> Result<(), nsresult> { 759 self.reset()?; 760 761 let promise = RegisterPromise(RefPtr::new(promise)); 762 763 let mut origin = nsString::new(); 764 unsafe { args.GetOrigin(&mut *origin) }.to_result()?; 765 766 let mut relying_party_id = nsString::new(); 767 unsafe { args.GetRpId(&mut *relying_party_id) }.to_result()?; 768 769 let mut client_data_hash = ThinVec::new(); 770 unsafe { args.GetClientDataHash(&mut client_data_hash) }.to_result()?; 771 let mut client_data_hash_arr = [0u8; 32]; 772 client_data_hash_arr.copy_from_slice(&client_data_hash); 773 774 let mut timeout_ms = 0u32; 775 unsafe { args.GetTimeoutMS(&mut timeout_ms) }.to_result()?; 776 777 let mut exclude_list = ThinVec::new(); 778 unsafe { args.GetExcludeList(&mut exclude_list) }.to_result()?; 779 let exclude_list = exclude_list 780 .iter_mut() 781 .map(|id| PublicKeyCredentialDescriptor { 782 id: id.to_vec(), 783 transports: vec![], 784 }) 785 .collect(); 786 787 let mut relying_party_name = nsString::new(); 788 unsafe { args.GetRpName(&mut *relying_party_name) }.to_result()?; 789 790 let mut user_id = ThinVec::new(); 791 unsafe { args.GetUserId(&mut user_id) }.to_result()?; 792 793 let mut user_name = nsString::new(); 794 unsafe { args.GetUserName(&mut *user_name) }.to_result()?; 795 796 let mut user_display_name = nsString::new(); 797 unsafe { args.GetUserDisplayName(&mut *user_display_name) }.to_result()?; 798 799 let mut cose_algs = ThinVec::new(); 800 unsafe { args.GetCoseAlgs(&mut cose_algs) }.to_result()?; 801 let pub_cred_params = cose_algs 802 .iter() 803 .filter_map(|alg| PublicKeyCredentialParameters::try_from(*alg).ok()) 804 .collect(); 805 806 let mut resident_key = nsString::new(); 807 unsafe { args.GetResidentKey(&mut *resident_key) }.to_result()?; 808 let resident_key_req = if resident_key.eq("required") { 809 ResidentKeyRequirement::Required 810 } else if resident_key.eq("preferred") { 811 ResidentKeyRequirement::Preferred 812 } else if resident_key.eq("discouraged") { 813 ResidentKeyRequirement::Discouraged 814 } else { 815 return Err(NS_ERROR_FAILURE); 816 }; 817 818 let mut user_verification = nsString::new(); 819 unsafe { args.GetUserVerification(&mut *user_verification) }.to_result()?; 820 let user_verification_req = if user_verification.eq("required") { 821 UserVerificationRequirement::Required 822 } else if user_verification.eq("discouraged") { 823 UserVerificationRequirement::Discouraged 824 } else { 825 UserVerificationRequirement::Preferred 826 }; 827 828 let mut authenticator_attachment = nsString::new(); 829 if unsafe { args.GetAuthenticatorAttachment(&mut *authenticator_attachment) } 830 .to_result() 831 .is_ok() 832 { 833 if authenticator_attachment.eq("platform") { 834 return Err(NS_ERROR_FAILURE); 835 } 836 } 837 838 let mut credential_protection_policy = None; 839 let mut enforce_credential_protection_policy = None; 840 let mut cred_protect_policy_value = nsCString::new(); 841 let mut enforce_cred_protect_value = false; 842 if unsafe { args.GetCredentialProtectionPolicy(&mut *cred_protect_policy_value) } 843 .to_result() 844 .is_ok() 845 { 846 unsafe { args.GetEnforceCredentialProtectionPolicy(&mut enforce_cred_protect_value) } 847 .to_result()?; 848 credential_protection_policy = if cred_protect_policy_value 849 .eq("userVerificationOptional") 850 { 851 Some(CredentialProtectionPolicy::UserVerificationOptional) 852 } else if cred_protect_policy_value.eq("userVerificationOptionalWithCredentialIDList") { 853 Some(CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList) 854 } else if cred_protect_policy_value.eq("userVerificationRequired") { 855 Some(CredentialProtectionPolicy::UserVerificationRequired) 856 } else { 857 return Err(NS_ERROR_FAILURE); 858 }; 859 enforce_credential_protection_policy = Some(enforce_cred_protect_value); 860 } 861 862 let mut cred_props = false; 863 unsafe { args.GetCredProps(&mut cred_props) }.to_result()?; 864 865 let mut min_pin_length = false; 866 unsafe { args.GetMinPinLength(&mut min_pin_length) }.to_result()?; 867 868 let prf_input = (|| -> Option<AuthenticationExtensionsPRFInputs> { 869 let mut prf: bool = false; 870 unsafe { args.GetPrf(&mut prf) }.to_result().ok()?; 871 if !prf { 872 return None; 873 } 874 875 let eval = || -> Option<AuthenticationExtensionsPRFValues> { 876 let mut prf_eval_first: ThinVec<u8> = ThinVec::new(); 877 let mut prf_eval_second: ThinVec<u8> = ThinVec::new(); 878 unsafe { args.GetPrfEvalFirst(&mut prf_eval_first) } 879 .to_result() 880 .ok()?; 881 let has_second = unsafe { args.GetPrfEvalSecond(&mut prf_eval_second) } 882 .to_result() 883 .is_ok(); 884 Some(AuthenticationExtensionsPRFValues { 885 first: prf_eval_first.to_vec(), 886 second: has_second.then(|| prf_eval_second.to_vec()), 887 }) 888 }(); 889 890 Some(AuthenticationExtensionsPRFInputs { 891 eval, 892 eval_by_credential: None, 893 }) 894 })(); 895 896 let mut hmac_create_secret = None; 897 let mut maybe_hmac_create_secret = false; 898 if unsafe { args.GetHmacCreateSecret(&mut maybe_hmac_create_secret) } 899 .to_result() 900 .is_ok() 901 { 902 hmac_create_secret = Some(maybe_hmac_create_secret); 903 } 904 905 let origin = origin.to_string(); 906 let info = RegisterArgs { 907 client_data_hash: client_data_hash_arr, 908 relying_party: RelyingParty { 909 id: relying_party_id.to_string(), 910 name: None, 911 }, 912 origin: origin.clone(), 913 user: PublicKeyCredentialUserEntity { 914 id: user_id.to_vec(), 915 name: Some(user_name.to_string()), 916 display_name: None, 917 }, 918 pub_cred_params, 919 exclude_list, 920 user_verification_req, 921 resident_key_req, 922 extensions: AuthenticationExtensionsClientInputs { 923 credential_protection_policy, 924 enforce_credential_protection_policy, 925 cred_props: cred_props.then_some(true), 926 hmac_create_secret, 927 min_pin_length: min_pin_length.then_some(true), 928 prf: prf_input, 929 ..Default::default() 930 }, 931 pin: None, 932 use_ctap1_fallback: !static_prefs::pref!("security.webauthn.ctap2"), 933 }; 934 935 let mut guard = self.transaction.lock().unwrap(); 936 *guard = Some(TransactionState { 937 tid, 938 browsing_context_id, 939 pending_args: None, 940 promise: TransactionPromise::Register(promise), 941 pin_receiver: None, 942 selection_receiver: None, 943 interactive_receiver: None, 944 puat_cache: None, 945 }); 946 // drop the guard here to ensure we don't deadlock if the call to `register()` below 947 // hairpins the state callback. 948 drop(guard); 949 950 let (status_tx, status_rx) = channel::<StatusUpdate>(); 951 let status_transaction = self.transaction.clone(); 952 let status_origin = info.origin.clone(); 953 RunnableBuilder::new("AuthrsService::MakeCredential::StatusReceiver", move || { 954 let _ = status_callback( 955 status_rx, 956 tid, 957 &status_origin, 958 browsing_context_id, 959 status_transaction, 960 ); 961 }) 962 .may_block(true) 963 .dispatch_background_task()?; 964 965 let callback_transaction = self.transaction.clone(); 966 let callback_origin = info.origin.clone(); 967 let state_callback = StateCallback::<Result<RegisterResult, AuthenticatorError>>::new( 968 Box::new(move |result| { 969 let mut guard = callback_transaction.lock().unwrap(); 970 let Some(state) = guard.as_mut() else { 971 return; 972 }; 973 if state.tid != tid { 974 return; 975 } 976 let TransactionPromise::Register(ref promise) = state.promise else { 977 return; 978 }; 979 if let Err(AuthenticatorError::CredentialExcluded) = result { 980 let _ = send_prompt( 981 BrowserPromptType::AlreadyRegistered, 982 tid, 983 Some(&callback_origin), 984 Some(browsing_context_id), 985 ); 986 } 987 if should_cancel_prompts(&result) { 988 // Some errors are accompanied by prompts that should persist after the 989 // operation terminates. 990 let _ = cancel_prompts(tid); 991 } 992 let _ = promise.resolve_or_reject(result.map_err(authrs_to_nserror)); 993 *guard = None; 994 }), 995 ); 996 997 // The authenticator crate provides an `AuthenticatorService` which can dispatch a request 998 // in parallel to any number of transports. We only support the USB transport in production 999 // configurations, so we do not need the full generality of `AuthenticatorService` here. 1000 // We disable the USB transport in tests that use virtual devices. 1001 if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { 1002 // TODO(Bug 1855290) Remove this presence prompt 1003 send_prompt( 1004 BrowserPromptType::Presence, 1005 tid, 1006 Some(&info.origin), 1007 Some(browsing_context_id), 1008 )?; 1009 self.usb_token_manager.lock().unwrap().register( 1010 timeout_ms.into(), 1011 info, 1012 status_tx, 1013 state_callback, 1014 ); 1015 } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { 1016 self.test_token_manager 1017 .register(timeout_ms.into(), info, status_tx, state_callback); 1018 } else { 1019 return Err(NS_ERROR_FAILURE); 1020 } 1021 1022 Ok(()) 1023 } 1024 1025 xpcom_method!(set_has_attestation_consent => SetHasAttestationConsent(aTid: u64, aHasConsent: bool)); 1026 fn set_has_attestation_consent(&self, _tid: u64, _has_consent: bool) -> Result<(), nsresult> { 1027 Err(NS_ERROR_NOT_IMPLEMENTED) 1028 } 1029 1030 xpcom_method!(get_assertion => GetAssertion(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsIWebAuthnSignArgs, aPromise: *const nsIWebAuthnSignPromise)); 1031 fn get_assertion( 1032 &self, 1033 tid: u64, 1034 browsing_context_id: u64, 1035 args: &nsIWebAuthnSignArgs, 1036 promise: &nsIWebAuthnSignPromise, 1037 ) -> Result<(), nsresult> { 1038 self.reset()?; 1039 1040 let promise = SignPromise(RefPtr::new(promise)); 1041 1042 let mut origin = nsString::new(); 1043 unsafe { args.GetOrigin(&mut *origin) }.to_result()?; 1044 1045 let mut relying_party_id = nsString::new(); 1046 unsafe { args.GetRpId(&mut *relying_party_id) }.to_result()?; 1047 1048 let mut client_data_hash = ThinVec::new(); 1049 unsafe { args.GetClientDataHash(&mut client_data_hash) }.to_result()?; 1050 let mut client_data_hash_arr = [0u8; 32]; 1051 client_data_hash_arr.copy_from_slice(&client_data_hash); 1052 1053 let mut timeout_ms = 0u32; 1054 unsafe { args.GetTimeoutMS(&mut timeout_ms) }.to_result()?; 1055 1056 let mut allow_list = ThinVec::new(); 1057 unsafe { args.GetAllowList(&mut allow_list) }.to_result()?; 1058 let allow_list = allow_list 1059 .iter() 1060 .map(|id| PublicKeyCredentialDescriptor { 1061 id: id.to_vec(), 1062 transports: vec![], 1063 }) 1064 .collect(); 1065 1066 let mut user_verification = nsString::new(); 1067 unsafe { args.GetUserVerification(&mut *user_verification) }.to_result()?; 1068 let mut user_verification_req = if user_verification.eq("required") { 1069 UserVerificationRequirement::Required 1070 } else if user_verification.eq("discouraged") { 1071 UserVerificationRequirement::Discouraged 1072 } else { 1073 UserVerificationRequirement::Preferred 1074 }; 1075 1076 let mut app_id = None; 1077 let mut maybe_app_id = nsString::new(); 1078 match unsafe { args.GetAppId(&mut *maybe_app_id) }.to_result() { 1079 Ok(_) => app_id = Some(maybe_app_id.to_string()), 1080 _ => (), 1081 } 1082 1083 let prf_input = || -> Option<AuthenticationExtensionsPRFInputs> { 1084 let mut prf: bool = false; 1085 unsafe { args.GetPrf(&mut prf) }.to_result().ok()?; 1086 if !prf { 1087 return None; 1088 } 1089 1090 let eval = || -> Option<AuthenticationExtensionsPRFValues> { 1091 let mut prf_eval_first: ThinVec<u8> = ThinVec::new(); 1092 let mut prf_eval_second: ThinVec<u8> = ThinVec::new(); 1093 unsafe { args.GetPrfEvalFirst(&mut prf_eval_first) } 1094 .to_result() 1095 .ok()?; 1096 let has_second = unsafe { args.GetPrfEvalSecond(&mut prf_eval_second) } 1097 .to_result() 1098 .is_ok(); 1099 Some(AuthenticationExtensionsPRFValues { 1100 first: prf_eval_first.to_vec(), 1101 second: has_second.then(|| prf_eval_second.to_vec()), 1102 }) 1103 }(); 1104 1105 let eval_by_credential = 1106 || -> Option<HashMap<Vec<u8>, AuthenticationExtensionsPRFValues>> { 1107 let mut credential_ids: ThinVec<ThinVec<u8>> = ThinVec::new(); 1108 let mut eval_by_cred_firsts: ThinVec<ThinVec<u8>> = ThinVec::new(); 1109 let mut eval_by_cred_second_maybes: ThinVec<bool> = ThinVec::new(); 1110 let mut eval_by_cred_seconds: ThinVec<ThinVec<u8>> = ThinVec::new(); 1111 unsafe { args.GetPrfEvalByCredentialCredentialId(&mut credential_ids) } 1112 .to_result() 1113 .ok()?; 1114 unsafe { args.GetPrfEvalByCredentialEvalFirst(&mut eval_by_cred_firsts) } 1115 .to_result() 1116 .ok()?; 1117 unsafe { 1118 args.GetPrfEvalByCredentialEvalSecondMaybe(&mut eval_by_cred_second_maybes) 1119 } 1120 .to_result() 1121 .ok()?; 1122 unsafe { args.GetPrfEvalByCredentialEvalSecond(&mut eval_by_cred_seconds) } 1123 .to_result() 1124 .ok()?; 1125 if credential_ids.len() != eval_by_cred_firsts.len() 1126 || credential_ids.len() != eval_by_cred_second_maybes.len() 1127 || credential_ids.len() != eval_by_cred_seconds.len() 1128 { 1129 return None; 1130 } 1131 let mut result = HashMap::new(); 1132 for i in 0..credential_ids.len() { 1133 result.insert( 1134 credential_ids[i].to_vec(), 1135 AuthenticationExtensionsPRFValues { 1136 first: eval_by_cred_firsts[i].to_vec(), 1137 second: eval_by_cred_second_maybes[i] 1138 .then(|| eval_by_cred_seconds[i].to_vec()), 1139 }, 1140 ); 1141 } 1142 Some(result) 1143 }(); 1144 1145 Some(AuthenticationExtensionsPRFInputs { 1146 eval, 1147 eval_by_credential, 1148 }) 1149 }(); 1150 1151 // https://w3c.github.io/webauthn/#prf-extension 1152 // "The hmac-secret extension provides two PRFs per credential: one which is used for 1153 // requests where user verification is performed and another for all other requests. 1154 // This extension [PRF] only exposes a single PRF per credential and, when implementing 1155 // on top of hmac-secret, that PRF MUST be the one used for when user verification is 1156 // performed. This overrides the UserVerificationRequirement if neccessary." 1157 if prf_input.is_some() && user_verification_req == UserVerificationRequirement::Discouraged 1158 { 1159 user_verification_req = UserVerificationRequirement::Preferred; 1160 } 1161 1162 let mut conditionally_mediated = false; 1163 unsafe { args.GetConditionallyMediated(&mut conditionally_mediated) }.to_result()?; 1164 1165 let info = SignArgs { 1166 client_data_hash: client_data_hash_arr, 1167 relying_party_id: relying_party_id.to_string(), 1168 origin: origin.to_string(), 1169 allow_list, 1170 user_verification_req, 1171 user_presence_req: true, 1172 extensions: AuthenticationExtensionsClientInputs { 1173 app_id, 1174 prf: prf_input, 1175 ..Default::default() 1176 }, 1177 pin: None, 1178 use_ctap1_fallback: !static_prefs::pref!("security.webauthn.ctap2"), 1179 }; 1180 1181 let mut guard = self.transaction.lock().unwrap(); 1182 *guard = Some(TransactionState { 1183 tid, 1184 browsing_context_id, 1185 pending_args: Some(TransactionArgs::Sign(timeout_ms as u64, info)), 1186 promise: TransactionPromise::Sign(promise), 1187 pin_receiver: None, 1188 selection_receiver: None, 1189 interactive_receiver: None, 1190 puat_cache: None, 1191 }); 1192 1193 if !conditionally_mediated { 1194 // Immediately proceed to the modal UI flow. 1195 self.do_get_assertion(None, guard) 1196 } else { 1197 // Cache the request and wait for the conditional UI to request autofill entries, etc. 1198 Ok(()) 1199 } 1200 } 1201 1202 fn do_get_assertion( 1203 &self, 1204 mut selected_credential_id: Option<Vec<u8>>, 1205 mut guard: MutexGuard<Option<TransactionState>>, 1206 ) -> Result<(), nsresult> { 1207 let Some(state) = guard.as_mut() else { 1208 return Err(NS_ERROR_FAILURE); 1209 }; 1210 let browsing_context_id = state.browsing_context_id; 1211 let tid = state.tid; 1212 let (timeout_ms, mut info) = match state.pending_args.take() { 1213 Some(TransactionArgs::Sign(timeout_ms, info)) => (timeout_ms, info), 1214 _ => return Err(NS_ERROR_FAILURE), 1215 }; 1216 1217 if let Some(id) = selected_credential_id.take() { 1218 if info.allow_list.is_empty() { 1219 info.allow_list.push(PublicKeyCredentialDescriptor { 1220 id, 1221 transports: vec![], 1222 }); 1223 } else { 1224 // We need to ensure that the selected credential id 1225 // was in the original allow_list. 1226 info.allow_list.retain(|cred| cred.id == id); 1227 if info.allow_list.is_empty() { 1228 return Err(NS_ERROR_FAILURE); 1229 } 1230 } 1231 } 1232 1233 let (status_tx, status_rx) = channel::<StatusUpdate>(); 1234 let status_transaction = self.transaction.clone(); 1235 let status_origin = info.origin.to_string(); 1236 RunnableBuilder::new("AuthrsService::GetAssertion::StatusReceiver", move || { 1237 let _ = status_callback( 1238 status_rx, 1239 tid, 1240 &status_origin, 1241 browsing_context_id, 1242 status_transaction, 1243 ); 1244 }) 1245 .may_block(true) 1246 .dispatch_background_task()?; 1247 1248 let uniq_allowed_cred = if info.allow_list.len() == 1 { 1249 info.allow_list.first().cloned() 1250 } else { 1251 None 1252 }; 1253 1254 let callback_transaction = self.transaction.clone(); 1255 let state_callback = StateCallback::<Result<SignResult, AuthenticatorError>>::new( 1256 Box::new(move |mut result| { 1257 let mut guard = callback_transaction.lock().unwrap(); 1258 let Some(state) = guard.as_mut() else { 1259 return; 1260 }; 1261 if state.tid != tid { 1262 return; 1263 } 1264 let TransactionPromise::Sign(ref promise) = state.promise else { 1265 return; 1266 }; 1267 if uniq_allowed_cred.is_some() { 1268 // In CTAP 2.0, but not CTAP 2.1, the assertion object's credential field 1269 // "May be omitted if the allowList has exactly one credential." If we had 1270 // a unique allowed credential, then copy its descriptor to the output. 1271 if let Ok(inner) = result.as_mut() { 1272 inner.assertion.credentials = uniq_allowed_cred; 1273 } 1274 } 1275 if should_cancel_prompts(&result) { 1276 // Some errors are accompanied by prompts that should persist after the 1277 // operation terminates. 1278 let _ = cancel_prompts(tid); 1279 } 1280 let _ = promise.resolve_or_reject(result.map_err(authrs_to_nserror)); 1281 *guard = None; 1282 }), 1283 ); 1284 1285 // TODO(Bug 1855290) Remove this presence prompt 1286 send_prompt( 1287 BrowserPromptType::Presence, 1288 tid, 1289 Some(&info.origin), 1290 Some(browsing_context_id), 1291 )?; 1292 1293 // As in `register`, we are intentionally avoiding `AuthenticatorService` here. 1294 if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { 1295 self.usb_token_manager.lock().unwrap().sign( 1296 timeout_ms as u64, 1297 info, 1298 status_tx, 1299 state_callback, 1300 ); 1301 } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { 1302 self.test_token_manager 1303 .sign(timeout_ms as u64, info, status_tx, state_callback); 1304 } else { 1305 return Err(NS_ERROR_FAILURE); 1306 } 1307 1308 Ok(()) 1309 } 1310 1311 xpcom_method!(has_pending_conditional_get => HasPendingConditionalGet(aBrowsingContextId: u64, aOrigin: *const nsAString) -> u64); 1312 fn has_pending_conditional_get( 1313 &self, 1314 browsing_context_id: u64, 1315 origin: &nsAString, 1316 ) -> Result<u64, nsresult> { 1317 let mut guard = self.transaction.lock().unwrap(); 1318 let Some(state) = guard.as_mut() else { 1319 return Ok(0); 1320 }; 1321 let Some(TransactionArgs::Sign(_, info)) = state.pending_args.as_ref() else { 1322 return Ok(0); 1323 }; 1324 if state.browsing_context_id != browsing_context_id { 1325 return Ok(0); 1326 } 1327 if !info.origin.eq(&origin.to_string()) { 1328 return Ok(0); 1329 } 1330 Ok(state.tid) 1331 } 1332 1333 xpcom_method!(get_autofill_entries => GetAutoFillEntries(aTransactionId: u64) -> ThinVec<Option<RefPtr<nsIWebAuthnAutoFillEntry>>>); 1334 fn get_autofill_entries( 1335 &self, 1336 tid: u64, 1337 ) -> Result<ThinVec<Option<RefPtr<nsIWebAuthnAutoFillEntry>>>, nsresult> { 1338 let mut guard = self.transaction.lock().unwrap(); 1339 let Some(state) = guard.as_mut() else { 1340 return Err(NS_ERROR_NOT_AVAILABLE); 1341 }; 1342 if state.tid != tid { 1343 return Err(NS_ERROR_NOT_AVAILABLE); 1344 } 1345 let Some(TransactionArgs::Sign(_, info)) = state.pending_args.as_ref() else { 1346 return Err(NS_ERROR_NOT_AVAILABLE); 1347 }; 1348 if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { 1349 // We don't currently support silent discovery for credentials on USB tokens. 1350 return Ok(thin_vec![]); 1351 } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { 1352 return self 1353 .test_token_manager 1354 .get_autofill_entries(&info.relying_party_id, &info.allow_list); 1355 } else { 1356 return Err(NS_ERROR_FAILURE); 1357 } 1358 } 1359 1360 xpcom_method!(select_autofill_entry => SelectAutoFillEntry(aTid: u64, aCredentialId: *const ThinVec<u8>)); 1361 fn select_autofill_entry(&self, tid: u64, credential_id: &ThinVec<u8>) -> Result<(), nsresult> { 1362 let mut guard = self.transaction.lock().unwrap(); 1363 let Some(state) = guard.as_mut() else { 1364 return Err(NS_ERROR_FAILURE); 1365 }; 1366 if tid != state.tid { 1367 return Err(NS_ERROR_FAILURE); 1368 } 1369 self.do_get_assertion(Some(credential_id.to_vec()), guard) 1370 } 1371 1372 xpcom_method!(resume_conditional_get => ResumeConditionalGet(aTid: u64)); 1373 fn resume_conditional_get(&self, tid: u64) -> Result<(), nsresult> { 1374 let mut guard = self.transaction.lock().unwrap(); 1375 let Some(state) = guard.as_mut() else { 1376 return Err(NS_ERROR_FAILURE); 1377 }; 1378 if tid != state.tid { 1379 return Err(NS_ERROR_FAILURE); 1380 } 1381 self.do_get_assertion(None, guard) 1382 } 1383 1384 // Clears the transaction state if tid matches the ongoing transaction ID. 1385 // Returns whether the tid was a match. 1386 fn clear_transaction(&self, tid: u64) -> bool { 1387 let mut guard = self.transaction.lock().unwrap(); 1388 let Some(state) = guard.as_ref() else { 1389 return true; // workaround for Bug 1864526. 1390 }; 1391 if state.tid != tid { 1392 // Ignore the cancellation request if the transaction 1393 // ID does not match. 1394 return false; 1395 } 1396 // It's possible that we haven't dispatched the request to the usb_token_manager yet, 1397 // e.g. if we're waiting for resume_make_credential. So reject the promise and drop the 1398 // state here rather than from the StateCallback 1399 let _ = state.promise.reject(NS_ERROR_DOM_NOT_ALLOWED_ERR); 1400 *guard = None; 1401 true 1402 } 1403 1404 xpcom_method!(cancel => Cancel(aTransactionId: u64)); 1405 fn cancel(&self, tid: u64) -> Result<(), nsresult> { 1406 if self.clear_transaction(tid) { 1407 self.usb_token_manager.lock().unwrap().cancel(); 1408 } 1409 Ok(()) 1410 } 1411 1412 xpcom_method!(reset => Reset()); 1413 fn reset(&self) -> Result<(), nsresult> { 1414 { 1415 if let Some(state) = self.transaction.lock().unwrap().take() { 1416 cancel_prompts(state.tid)?; 1417 state.promise.reject(NS_ERROR_DOM_ABORT_ERR)?; 1418 } 1419 } // release the transaction lock so a StateCallback can take it 1420 self.usb_token_manager.lock().unwrap().cancel(); 1421 Ok(()) 1422 } 1423 1424 xpcom_method!( 1425 add_virtual_authenticator => AddVirtualAuthenticator( 1426 protocol: *const nsACString, 1427 transport: *const nsACString, 1428 hasResidentKey: bool, 1429 hasUserVerification: bool, 1430 isUserConsenting: bool, 1431 isUserVerified: bool) -> nsACString 1432 ); 1433 fn add_virtual_authenticator( 1434 &self, 1435 protocol: &nsACString, 1436 transport: &nsACString, 1437 has_resident_key: bool, 1438 has_user_verification: bool, 1439 is_user_consenting: bool, 1440 is_user_verified: bool, 1441 ) -> Result<nsCString, nsresult> { 1442 let protocol = match protocol.to_string().as_str() { 1443 "ctap1/u2f" => AuthenticatorVersion::U2F_V2, 1444 "ctap2" => AuthenticatorVersion::FIDO_2_0, 1445 "ctap2_1" => AuthenticatorVersion::FIDO_2_1, 1446 _ => return Err(NS_ERROR_INVALID_ARG), 1447 }; 1448 let transport = transport.to_string(); 1449 match transport.as_str() { 1450 "usb" | "nfc" | "ble" | "smart-card" | "hybrid" | "internal" => (), 1451 _ => return Err(NS_ERROR_INVALID_ARG), 1452 }; 1453 self.test_token_manager 1454 .add_virtual_authenticator( 1455 protocol, 1456 transport, 1457 has_resident_key, 1458 has_user_verification, 1459 is_user_consenting, 1460 is_user_verified, 1461 ) 1462 .map(nsCString::from) 1463 } 1464 1465 xpcom_method!(remove_virtual_authenticator => RemoveVirtualAuthenticator(authenticatorId: *const nsACString)); 1466 fn remove_virtual_authenticator(&self, authenticator_id: &nsACString) -> Result<(), nsresult> { 1467 self.test_token_manager 1468 .remove_virtual_authenticator(&authenticator_id.to_utf8()) 1469 } 1470 1471 xpcom_method!( 1472 add_credential => AddCredential( 1473 authenticatorId: *const nsACString, 1474 credentialId: *const nsACString, 1475 isResidentCredential: bool, 1476 rpId: *const nsACString, 1477 privateKey: *const nsACString, 1478 userHandle: *const nsACString, 1479 signCount: u32) 1480 ); 1481 fn add_credential( 1482 &self, 1483 authenticator_id: &nsACString, 1484 credential_id: &nsACString, 1485 is_resident_credential: bool, 1486 rp_id: &nsACString, 1487 private_key: &nsACString, 1488 user_handle: &nsACString, 1489 sign_count: u32, 1490 ) -> Result<(), nsresult> { 1491 let credential_id = base64::engine::general_purpose::URL_SAFE_NO_PAD 1492 .decode(credential_id) 1493 .or(Err(NS_ERROR_INVALID_ARG))?; 1494 let private_key = base64::engine::general_purpose::URL_SAFE_NO_PAD 1495 .decode(private_key) 1496 .or(Err(NS_ERROR_INVALID_ARG))?; 1497 let user_handle = base64::engine::general_purpose::URL_SAFE_NO_PAD 1498 .decode(user_handle) 1499 .or(Err(NS_ERROR_INVALID_ARG))?; 1500 self.test_token_manager.add_credential( 1501 &authenticator_id.to_utf8(), 1502 &credential_id, 1503 &private_key, 1504 &user_handle, 1505 sign_count, 1506 rp_id.to_string(), 1507 is_resident_credential, 1508 ) 1509 } 1510 1511 xpcom_method!(get_credentials => GetCredentials(authenticatorId: *const nsACString) -> ThinVec<Option<RefPtr<nsICredentialParameters>>>); 1512 fn get_credentials( 1513 &self, 1514 authenticator_id: &nsACString, 1515 ) -> Result<ThinVec<Option<RefPtr<nsICredentialParameters>>>, nsresult> { 1516 self.test_token_manager 1517 .get_credentials(&authenticator_id.to_utf8()) 1518 } 1519 1520 xpcom_method!(remove_credential => RemoveCredential(authenticatorId: *const nsACString, credentialId: *const nsACString)); 1521 fn remove_credential( 1522 &self, 1523 authenticator_id: &nsACString, 1524 credential_id: &nsACString, 1525 ) -> Result<(), nsresult> { 1526 let credential_id = base64::engine::general_purpose::URL_SAFE_NO_PAD 1527 .decode(credential_id) 1528 .or(Err(NS_ERROR_INVALID_ARG))?; 1529 self.test_token_manager 1530 .remove_credential(&authenticator_id.to_utf8(), credential_id.as_ref()) 1531 } 1532 1533 xpcom_method!(remove_all_credentials => RemoveAllCredentials(authenticatorId: *const nsACString)); 1534 fn remove_all_credentials(&self, authenticator_id: &nsACString) -> Result<(), nsresult> { 1535 self.test_token_manager 1536 .remove_all_credentials(&authenticator_id.to_utf8()) 1537 } 1538 1539 xpcom_method!(set_user_verified => SetUserVerified(authenticatorId: *const nsACString, isUserVerified: bool)); 1540 fn set_user_verified( 1541 &self, 1542 authenticator_id: &nsACString, 1543 is_user_verified: bool, 1544 ) -> Result<(), nsresult> { 1545 self.test_token_manager 1546 .set_user_verified(&authenticator_id.to_utf8(), is_user_verified) 1547 } 1548 1549 xpcom_method!(listen => Listen()); 1550 pub(crate) fn listen(&self) -> Result<(), nsresult> { 1551 // For now, we don't support softtokens 1552 if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { 1553 return Ok(()); 1554 } 1555 1556 { 1557 let mut guard = self.transaction.lock().unwrap(); 1558 if guard.as_ref().is_some() { 1559 // ignore listen() and continue with ongoing transaction 1560 return Ok(()); 1561 } 1562 *guard = Some(TransactionState { 1563 tid: 0, 1564 browsing_context_id: 0, 1565 pending_args: None, 1566 promise: TransactionPromise::Listen, 1567 pin_receiver: None, 1568 selection_receiver: None, 1569 interactive_receiver: None, 1570 puat_cache: None, 1571 }); 1572 } 1573 1574 // We may get from status_updates info about certain errors (e.g. PinErrors) 1575 // which we want to present to the user. We will ignore the following error 1576 // which is caused by us "hanging up" on the StatusUpdate-channel and return 1577 // the PinError instead, via `upcoming_error`. 1578 let upcoming_error = Arc::new(Mutex::new(None)); 1579 let upcoming_error_c = upcoming_error.clone(); 1580 let callback_transaction = self.transaction.clone(); 1581 let state_callback = StateCallback::<Result<ManageResult, AuthenticatorError>>::new( 1582 Box::new(move |result| { 1583 let mut guard = callback_transaction.lock().unwrap(); 1584 match guard.as_mut() { 1585 Some(state) => { 1586 match state.promise { 1587 TransactionPromise::Listen => (), 1588 _ => return, 1589 } 1590 *guard = None; 1591 } 1592 // We have no transaction anymore, this means cancel() was called 1593 None => (), 1594 } 1595 let msg = match result { 1596 Ok(_) => BrowserPromptType::ListenSuccess, 1597 Err(e) => { 1598 // See if we have a cached error that should replace this error 1599 let replacement = if let Ok(mut x) = upcoming_error_c.lock() { 1600 x.take() 1601 } else { 1602 None 1603 }; 1604 let replaced_err = replacement.unwrap_or(e); 1605 let err = authrs_to_prompt(replaced_err); 1606 BrowserPromptType::ListenError { 1607 error: Box::new(err), 1608 } 1609 } 1610 }; 1611 let _ = send_about_prompt(&msg); 1612 }), 1613 ); 1614 1615 // Calling `manage()` within the lock, to avoid race conditions 1616 // where we might check listen_blocked, see that it's false, 1617 // continue along, but in parallel `make_credential()` aborts the 1618 // interactive process shortly after, setting listen_blocked to true, 1619 // then accessing usb_token_manager afterwards and at the same time 1620 // we do it here, causing a runtime crash for trying to mut-borrow it twice. 1621 let (status_tx, status_rx) = channel::<StatusUpdate>(); 1622 let status_transaction = self.transaction.clone(); 1623 RunnableBuilder::new( 1624 "AuthrsTransport::AboutWebauthn::StatusReceiver", 1625 move || { 1626 let _ = interactive_status_callback(status_rx, status_transaction, upcoming_error); 1627 }, 1628 ) 1629 .may_block(true) 1630 .dispatch_background_task()?; 1631 if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { 1632 self.usb_token_manager.lock().unwrap().manage( 1633 60 * 1000 * 1000, 1634 status_tx, 1635 state_callback, 1636 ); 1637 } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { 1638 // We don't yet support softtoken 1639 } else { 1640 // Silently accept request, if all webauthn-options are disabled. 1641 // Used for testing. 1642 } 1643 Ok(()) 1644 } 1645 1646 xpcom_method!(run_command => RunCommand(c_cmd: *const nsACString)); 1647 pub fn run_command(&self, c_cmd: &nsACString) -> Result<(), nsresult> { 1648 // Always test if it can be parsed from incoming JSON (even for tests) 1649 let incoming: RequestWrapper = 1650 serde_json::from_str(&c_cmd.to_utf8()).or(Err(NS_ERROR_DOM_OPERATION_ERR))?; 1651 if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { 1652 let guard = self.transaction.lock().unwrap(); 1653 let puat = guard.as_ref().and_then(|g| g.puat_cache.clone()); 1654 let command = match incoming { 1655 RequestWrapper::Quit => InteractiveRequest::Quit, 1656 RequestWrapper::ChangePIN(a, b) => InteractiveRequest::ChangePIN(a, b), 1657 RequestWrapper::SetPIN(a) => InteractiveRequest::SetPIN(a), 1658 RequestWrapper::CredentialManagement(c) => { 1659 InteractiveRequest::CredentialManagement(c, puat) 1660 } 1661 RequestWrapper::BioEnrollment(c) => InteractiveRequest::BioEnrollment(c, puat), 1662 }; 1663 match &guard.as_ref().unwrap().interactive_receiver { 1664 Some(channel) => channel.send(command).or(Err(NS_ERROR_FAILURE)), 1665 // Either we weren't expecting a pin, or the controller is confused 1666 // about which transaction is active. Neither is recoverable, so it's 1667 // OK to drop the PinReceiver here. 1668 _ => Err(NS_ERROR_FAILURE), 1669 } 1670 } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { 1671 // We don't yet support softtoken 1672 Ok(()) 1673 } else { 1674 // Silently accept request, if all webauthn-options are disabled. 1675 // Used for testing. 1676 Ok(()) 1677 } 1678 } 1679 } 1680 1681 #[no_mangle] 1682 pub extern "C" fn authrs_service_constructor(result: *mut *const nsIWebAuthnService) -> nsresult { 1683 let wrapper = AuthrsService::allocate(InitAuthrsService { 1684 usb_token_manager: Mutex::new(StateMachine::new()), 1685 test_token_manager: TestTokenManager::new(), 1686 transaction: Arc::new(Mutex::new(None)), 1687 }); 1688 1689 #[cfg(feature = "fuzzing")] 1690 { 1691 let fuzzing_config = static_prefs::pref!("fuzzing.webauthn.authenticator_config"); 1692 if fuzzing_config != 0 { 1693 let is_user_verified = (fuzzing_config & 0x01) != 0; 1694 let is_user_consenting = (fuzzing_config & 0x02) != 0; 1695 let has_user_verification = (fuzzing_config & 0x04) != 0; 1696 let has_resident_key = (fuzzing_config & 0x08) != 0; 1697 let transport = nsCString::from(match (fuzzing_config & 0x10) >> 4 { 1698 0 => "usb", 1699 1 => "internal", 1700 _ => unreachable!(), 1701 }); 1702 let protocol = nsCString::from(match (fuzzing_config & 0x60) >> 5 { 1703 0 => "", // reserved 1704 1 => "ctap1/u2f", 1705 2 => "ctap2", 1706 3 => "ctap2_1", 1707 _ => unreachable!(), 1708 }); 1709 // If this fails it's probably because the protocol bits were zero, 1710 // we'll just ignore it. 1711 let _ = wrapper.add_virtual_authenticator( 1712 &protocol, 1713 &transport, 1714 has_resident_key, 1715 has_user_verification, 1716 is_user_consenting, 1717 is_user_verified, 1718 ); 1719 } 1720 } 1721 1722 unsafe { 1723 RefPtr::new(wrapper.coerce::<nsIWebAuthnService>()).forget(&mut *result); 1724 } 1725 NS_OK 1726 } 1727 1728 #[no_mangle] 1729 pub extern "C" fn authrs_webauthn_att_obj_constructor( 1730 att_obj_bytes: &ThinVec<u8>, 1731 anonymize: bool, 1732 result: *mut *const nsIWebAuthnAttObj, 1733 ) -> nsresult { 1734 if result.is_null() { 1735 return NS_ERROR_NULL_POINTER; 1736 } 1737 1738 let mut att_obj: AttestationObject = match serde_cbor::from_slice(att_obj_bytes) { 1739 Ok(att_obj) => att_obj, 1740 Err(_) => return NS_ERROR_INVALID_ARG, 1741 }; 1742 1743 if anonymize { 1744 att_obj.anonymize(); 1745 } 1746 1747 let wrapper = WebAuthnAttObj::allocate(InitWebAuthnAttObj { att_obj }); 1748 1749 unsafe { 1750 RefPtr::new(wrapper.coerce::<nsIWebAuthnAttObj>()).forget(&mut *result); 1751 } 1752 1753 NS_OK 1754 }