summaryrefslogtreecommitdiffstats
path: root/dom/webauthn/authrs_bridge/src/test_token.rs
diff options
context:
space:
mode:
Diffstat (limited to 'dom/webauthn/authrs_bridge/src/test_token.rs')
-rw-r--r--dom/webauthn/authrs_bridge/src/test_token.rs974
1 files changed, 974 insertions, 0 deletions
diff --git a/dom/webauthn/authrs_bridge/src/test_token.rs b/dom/webauthn/authrs_bridge/src/test_token.rs
new file mode 100644
index 0000000000..afc2ddbc75
--- /dev/null
+++ b/dom/webauthn/authrs_bridge/src/test_token.rs
@@ -0,0 +1,974 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use authenticator::authenticatorservice::{RegisterArgs, SignArgs};
+use authenticator::crypto::{ecdsa_p256_sha256_sign_raw, COSEAlgorithm, COSEKey, SharedSecret};
+use authenticator::ctap2::{
+ attestation::{
+ AAGuid, AttestationObject, AttestationStatement, AttestationStatementPacked,
+ AttestedCredentialData, AuthenticatorData, AuthenticatorDataFlags, Extension,
+ },
+ client_data::ClientDataHash,
+ commands::{
+ client_pin::{ClientPIN, ClientPinResponse, PINSubcommand},
+ get_assertion::{GetAssertion, GetAssertionResponse, GetAssertionResult},
+ get_info::{AuthenticatorInfo, AuthenticatorOptions, AuthenticatorVersion},
+ get_version::{GetVersion, U2FInfo},
+ make_credentials::{MakeCredentials, MakeCredentialsResult},
+ reset::Reset,
+ selection::Selection,
+ RequestCtap1, RequestCtap2, StatusCode,
+ },
+ preflight::CheckKeyHandle,
+ server::{
+ AuthenticatorAttachment, PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity,
+ RelyingParty,
+ },
+};
+use authenticator::errors::{AuthenticatorError, CommandError, HIDError, U2FTokenError};
+use authenticator::{ctap2, statecallback::StateCallback};
+use authenticator::{FidoDevice, FidoDeviceIO, FidoProtocol, VirtualFidoDevice};
+use authenticator::{RegisterResult, SignResult, StatusUpdate};
+use base64::Engine;
+use moz_task::RunnableBuilder;
+use nserror::{nsresult, NS_ERROR_FAILURE, NS_ERROR_INVALID_ARG, NS_OK};
+use nsstring::{nsACString, nsAString, nsCString, nsString};
+use rand::{thread_rng, RngCore};
+use std::cell::{Ref, RefCell};
+use std::collections::{hash_map::Entry, HashMap};
+use std::ops::{Deref, DerefMut};
+use std::sync::atomic::{AtomicU32, Ordering};
+use std::sync::mpsc::Sender;
+use std::sync::{Arc, Mutex};
+use thin_vec::ThinVec;
+use xpcom::interfaces::{nsICredentialParameters, nsIWebAuthnAutoFillEntry};
+use xpcom::{xpcom_method, RefPtr};
+
+// All TestTokens use this fixed, randomly generated, AAGUID
+const VIRTUAL_TOKEN_AAGUID: AAGuid = AAGuid([
+ 0x68, 0xe1, 0x00, 0xa5, 0x0b, 0x47, 0x91, 0x04, 0xb8, 0x54, 0x97, 0xa9, 0xba, 0x51, 0x06, 0x38,
+]);
+
+#[derive(Debug)]
+struct TestTokenCredential {
+ id: Vec<u8>,
+ privkey: Vec<u8>,
+ user_handle: Vec<u8>,
+ sign_count: AtomicU32,
+ is_discoverable_credential: bool,
+ rp: RelyingParty,
+}
+
+impl TestTokenCredential {
+ fn assert(
+ &self,
+ client_data_hash: &ClientDataHash,
+ flags: AuthenticatorDataFlags,
+ ) -> Result<GetAssertionResponse, HIDError> {
+ let credentials = Some(PublicKeyCredentialDescriptor {
+ id: self.id.clone(),
+ transports: vec![],
+ });
+
+ let auth_data = AuthenticatorData {
+ rp_id_hash: self.rp.hash(),
+ flags,
+ counter: self.sign_count.fetch_add(1, Ordering::Relaxed),
+ credential_data: None,
+ extensions: Extension::default(),
+ };
+
+ let user = Some(PublicKeyCredentialUserEntity {
+ id: self.user_handle.clone(),
+ ..Default::default()
+ });
+
+ let mut data = auth_data.to_vec();
+ data.extend_from_slice(client_data_hash.as_ref());
+ let signature =
+ ecdsa_p256_sha256_sign_raw(&self.privkey, &data).or(Err(HIDError::DeviceError))?;
+
+ Ok(GetAssertionResponse {
+ credentials,
+ auth_data,
+ signature,
+ user,
+ number_of_credentials: Some(1),
+ })
+ }
+}
+
+#[derive(Debug)]
+struct TestToken {
+ protocol: FidoProtocol,
+ transport: String,
+ versions: Vec<AuthenticatorVersion>,
+ has_resident_key: bool,
+ has_user_verification: bool,
+ is_user_consenting: bool,
+ is_user_verified: bool,
+ // This is modified in `make_credentials` which takes a &TestToken, but we only allow one transaction at a time.
+ credentials: RefCell<Vec<TestTokenCredential>>,
+ pin_token: [u8; 32],
+ shared_secret: Option<SharedSecret>,
+ authenticator_info: Option<AuthenticatorInfo>,
+}
+
+impl TestToken {
+ fn new(
+ versions: Vec<AuthenticatorVersion>,
+ transport: String,
+ has_resident_key: bool,
+ has_user_verification: bool,
+ is_user_consenting: bool,
+ is_user_verified: bool,
+ ) -> TestToken {
+ let mut pin_token = [0u8; 32];
+ thread_rng().fill_bytes(&mut pin_token);
+ Self {
+ protocol: FidoProtocol::CTAP2,
+ transport,
+ versions,
+ has_resident_key,
+ has_user_verification,
+ is_user_consenting,
+ is_user_verified,
+ credentials: RefCell::new(vec![]),
+ pin_token,
+ shared_secret: None,
+ authenticator_info: None,
+ }
+ }
+
+ fn insert_credential(
+ &self,
+ id: &[u8],
+ privkey: &[u8],
+ rp: &RelyingParty,
+ is_discoverable_credential: bool,
+ user_handle: &[u8],
+ sign_count: u32,
+ ) {
+ let c = TestTokenCredential {
+ id: id.to_vec(),
+ privkey: privkey.to_vec(),
+ rp: rp.clone(),
+ is_discoverable_credential,
+ user_handle: user_handle.to_vec(),
+ sign_count: AtomicU32::new(sign_count),
+ };
+
+ let mut credlist = self.credentials.borrow_mut();
+
+ match credlist.binary_search_by_key(&id, |probe| &probe.id) {
+ Ok(_) => {}
+ Err(idx) => credlist.insert(idx, c),
+ }
+ }
+
+ fn get_credentials(&self) -> Ref<Vec<TestTokenCredential>> {
+ self.credentials.borrow()
+ }
+
+ fn delete_credential(&mut self, id: &[u8]) -> bool {
+ let mut credlist = self.credentials.borrow_mut();
+ if let Ok(idx) = credlist.binary_search_by_key(&id, |probe| &probe.id) {
+ credlist.remove(idx);
+ return true;
+ }
+
+ false
+ }
+
+ fn delete_all_credentials(&mut self) {
+ self.credentials.borrow_mut().clear();
+ }
+
+ fn has_credential(&self, id: &[u8]) -> bool {
+ self.credentials
+ .borrow()
+ .binary_search_by_key(&id, |probe| &probe.id)
+ .is_ok()
+ }
+
+ fn max_supported_version(&self) -> AuthenticatorVersion {
+ self.authenticator_info
+ .as_ref()
+ .map_or(AuthenticatorVersion::U2F_V2, |info| {
+ info.max_supported_version()
+ })
+ }
+}
+
+impl FidoDevice for TestToken {
+ fn pre_init(&mut self) -> Result<(), HIDError> {
+ Ok(())
+ }
+
+ fn should_try_ctap2(&self) -> bool {
+ true
+ }
+
+ fn initialized(&self) -> bool {
+ true
+ }
+
+ fn is_u2f(&mut self) -> bool {
+ true
+ }
+
+ fn get_shared_secret(&self) -> Option<&SharedSecret> {
+ self.shared_secret.as_ref()
+ }
+
+ fn set_shared_secret(&mut self, shared_secret: SharedSecret) {
+ self.shared_secret = Some(shared_secret);
+ }
+
+ fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
+ self.authenticator_info.as_ref()
+ }
+
+ fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
+ self.authenticator_info = Some(authenticator_info);
+ }
+
+ fn get_protocol(&self) -> FidoProtocol {
+ self.protocol
+ }
+
+ fn downgrade_to_ctap1(&mut self) {
+ self.protocol = FidoProtocol::CTAP1
+ }
+}
+
+impl FidoDeviceIO for TestToken {
+ fn send_msg_cancellable<Out, Req: RequestCtap1<Output = Out> + RequestCtap2<Output = Out>>(
+ &mut self,
+ msg: &Req,
+ keep_alive: &dyn Fn() -> bool,
+ ) -> Result<Out, HIDError> {
+ if !self.initialized() {
+ return Err(HIDError::DeviceNotInitialized);
+ }
+
+ match self.get_protocol() {
+ FidoProtocol::CTAP1 => self.send_ctap1_cancellable(msg, keep_alive),
+ FidoProtocol::CTAP2 => self.send_cbor_cancellable(msg, keep_alive),
+ }
+ }
+
+ fn send_cbor_cancellable<Req: RequestCtap2>(
+ &mut self,
+ msg: &Req,
+ _keep_alive: &dyn Fn() -> bool,
+ ) -> Result<Req::Output, HIDError> {
+ msg.send_to_virtual_device(self)
+ }
+
+ fn send_ctap1_cancellable<Req: RequestCtap1>(
+ &mut self,
+ msg: &Req,
+ _keep_alive: &dyn Fn() -> bool,
+ ) -> Result<Req::Output, HIDError> {
+ msg.send_to_virtual_device(self)
+ }
+}
+
+impl VirtualFidoDevice for TestToken {
+ fn check_key_handle(&self, req: &CheckKeyHandle) -> Result<(), HIDError> {
+ let credlist = self.credentials.borrow();
+ let req_rp_hash = req.rp.hash();
+ let eligible_cred_iter = credlist.iter().filter(|x| x.rp.hash() == req_rp_hash);
+ for credential in eligible_cred_iter {
+ if req.key_handle == credential.id {
+ return Ok(());
+ }
+ }
+ Err(HIDError::DeviceError)
+ }
+
+ fn client_pin(&self, req: &ClientPIN) -> Result<ClientPinResponse, HIDError> {
+ match req.subcommand {
+ PINSubcommand::GetKeyAgreement => {
+ // We don't need to save, or even know, the private key for the public key returned
+ // here because we have access to the shared secret derived on the client side.
+ let (_private, public) = COSEKey::generate(COSEAlgorithm::ECDH_ES_HKDF256)
+ .map_err(|_| HIDError::DeviceError)?;
+ Ok(ClientPinResponse {
+ key_agreement: Some(public),
+ ..Default::default()
+ })
+ }
+ PINSubcommand::GetPinUvAuthTokenUsingUvWithPermissions => {
+ // TODO: permissions
+ if !self.is_user_consenting || !self.is_user_verified {
+ return Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::OperationDenied,
+ None,
+ )));
+ }
+ let secret = match self.shared_secret.as_ref() {
+ Some(secret) => secret,
+ _ => return Err(HIDError::DeviceError),
+ };
+ let encrypted_pin_token = match secret.encrypt(&self.pin_token) {
+ Ok(token) => token,
+ _ => return Err(HIDError::DeviceError),
+ };
+ Ok(ClientPinResponse {
+ pin_token: Some(encrypted_pin_token),
+ ..Default::default()
+ })
+ }
+ _ => Err(HIDError::UnsupportedCommand),
+ }
+ }
+
+ fn get_assertion(&self, req: &GetAssertion) -> Result<Vec<GetAssertionResult>, HIDError> {
+ // Algorithm 6.2.2 from CTAP 2.1
+ // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-makeCred-authnr-alg
+
+ // 1. zero length pinUvAuthParam
+ // (not implemented)
+
+ // 2. Validate pinUvAuthParam
+ // Handled by caller
+
+ // 3. Initialize "uv" and "up" bits to false
+ let mut flags = AuthenticatorDataFlags::empty();
+
+ // 4. Handle all options
+ // 4.1 and 4.2
+ let effective_uv_opt =
+ req.options.user_verification.unwrap_or(false) && req.pin_uv_auth_param.is_none();
+
+ // 4.3
+ if effective_uv_opt && !self.has_user_verification {
+ return Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::InvalidOption,
+ None,
+ )));
+ }
+
+ // 4.4 rk
+ // (not implemented, we don't encode it)
+
+ // 4.5
+ let effective_up_opt = req.options.user_presence.unwrap_or(true);
+
+ // 5. alwaysUv
+ // (not implemented)
+
+ // 6. User verification
+ // TODO: Permissions, (maybe) validate pinUvAuthParam
+ if self.is_user_verified && (effective_uv_opt || req.pin_uv_auth_param.is_some()) {
+ flags |= AuthenticatorDataFlags::USER_VERIFIED;
+ }
+
+ // 7. Locate credentials
+ let credlist = self.credentials.borrow();
+ let req_rp_hash = req.rp.hash();
+ let eligible_cred_iter = credlist.iter().filter(|x| x.rp.hash() == req_rp_hash);
+
+ // 8. Set up=true if evidence of user interaction was provided in step 6.
+ // (not applicable, we use pinUvAuthParam)
+
+ // 9. User presence test
+ if effective_up_opt {
+ if self.is_user_consenting {
+ flags |= AuthenticatorDataFlags::USER_PRESENT;
+ } else {
+ return Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::UpRequired,
+ None,
+ )));
+ }
+ }
+
+ // 10. Extensions
+ // (not implemented)
+
+ let mut assertions: Vec<GetAssertionResult> = vec![];
+ if !req.allow_list.is_empty() {
+ // 11. Non-discoverable credential case
+ // return at most one assertion matching an allowed credential ID
+ for credential in eligible_cred_iter {
+ if req.allow_list.iter().any(|x| x.id == credential.id) {
+ let mut assertion: GetAssertionResponse =
+ credential.assert(&req.client_data_hash, flags)?;
+ if req.allow_list.len() == 1
+ && self.max_supported_version() == AuthenticatorVersion::FIDO_2_0
+ {
+ // CTAP 2.0 authenticators are allowed to omit the credential ID in the
+ // response if the allow list contains exactly one entry. This behavior is
+ // a common source of bugs, e.g. Bug 1864504, so we'll exercise it here.
+ assertion.credentials = None;
+ }
+ assertions.push(GetAssertionResult {
+ assertion: assertion.into(),
+ attachment: AuthenticatorAttachment::Unknown,
+ extensions: Default::default(),
+ });
+ break;
+ }
+ }
+ } else {
+ // 12. Discoverable credential case
+ // return any number of assertions from credentials bound to this RP ID
+ for credential in eligible_cred_iter.filter(|x| x.is_discoverable_credential) {
+ let assertion = credential.assert(&req.client_data_hash, flags)?.into();
+ assertions.push(GetAssertionResult {
+ assertion,
+ attachment: AuthenticatorAttachment::Unknown,
+ extensions: Default::default(),
+ });
+ }
+ }
+
+ if assertions.is_empty() {
+ return Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::NoCredentials,
+ None,
+ )));
+ }
+
+ Ok(assertions)
+ }
+
+ fn get_info(&self) -> Result<AuthenticatorInfo, HIDError> {
+ // This is a CTAP2.1 device with internal user verification support
+ Ok(AuthenticatorInfo {
+ versions: self.versions.clone(),
+ options: AuthenticatorOptions {
+ platform_device: self.transport == "internal",
+ resident_key: self.has_resident_key,
+ pin_uv_auth_token: Some(self.has_user_verification),
+ user_verification: Some(self.has_user_verification),
+ ..Default::default()
+ },
+ ..Default::default()
+ })
+ }
+
+ fn get_version(&self, _req: &GetVersion) -> Result<U2FInfo, HIDError> {
+ Err(HIDError::UnsupportedCommand)
+ }
+
+ fn make_credentials(&self, req: &MakeCredentials) -> Result<MakeCredentialsResult, HIDError> {
+ // Algorithm 6.1.2 from CTAP 2.1
+ // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-makeCred-authnr-alg
+
+ // 1. zero length pinUvAuthParam
+ // (not implemented)
+
+ // 2. Validate pinUvAuthParam
+ // Handled by caller
+
+ // 3. Validate pubKeyCredParams
+ if !req
+ .pub_cred_params
+ .iter()
+ .any(|x| x.alg == COSEAlgorithm::ES256)
+ {
+ return Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::UnsupportedAlgorithm,
+ None,
+ )));
+ }
+
+ // 4. initialize "uv" and "up" bits to false
+ let mut flags = AuthenticatorDataFlags::empty();
+
+ // 5. process all options
+
+ // 5.1 and 5.2
+ let effective_uv_opt =
+ req.options.user_verification.unwrap_or(false) && req.pin_uv_auth_param.is_none();
+
+ // 5.3
+ if effective_uv_opt && !self.has_user_verification {
+ return Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::InvalidOption,
+ None,
+ )));
+ }
+
+ // 5.4
+ if req.options.resident_key.unwrap_or(false) && !self.has_resident_key {
+ return Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::UnsupportedOption,
+ None,
+ )));
+ }
+
+ // 5.6 and 5.7
+ // Nothing to do. We don't provide a way to set up=false.
+
+ // 6. alwaysUv option ID
+ // (not implemented)
+
+ // 7. and 8. makeCredUvNotRqd option ID
+ // (not implemented)
+
+ // 9. enterprise attestation
+ // (not implemented)
+
+ // 11. User verification
+ // TODO: Permissions, (maybe) validate pinUvAuthParam
+ if self.is_user_verified {
+ flags |= AuthenticatorDataFlags::USER_VERIFIED;
+ }
+
+ // 12. exclude list
+ // TODO: credProtect
+ if req.exclude_list.iter().any(|x| self.has_credential(&x.id)) {
+ return Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::CredentialExcluded,
+ None,
+ )));
+ }
+
+ // 13. Set up=true if evidence of user interaction was provided in step 11.
+ // (not applicable, we use pinUvAuthParam)
+
+ // 14. User presence test
+ if self.is_user_consenting {
+ flags |= AuthenticatorDataFlags::USER_PRESENT;
+ } else {
+ return Err(HIDError::Command(CommandError::StatusCode(
+ StatusCode::UpRequired,
+ None,
+ )));
+ }
+
+ // 15. process extensions
+ let mut extensions = Extension::default();
+ if req.extensions.min_pin_length == Some(true) {
+ // a real authenticator would
+ // 1) return an actual minimum pin length, and
+ // 2) check the RP ID against an allowlist before providing any data
+ extensions.min_pin_length = Some(4);
+ }
+
+ if extensions.has_some() {
+ flags |= AuthenticatorDataFlags::EXTENSION_DATA;
+ }
+
+ // 16. Generate a new credential.
+ let (private, public) =
+ COSEKey::generate(COSEAlgorithm::ES256).map_err(|_| HIDError::DeviceError)?;
+ let counter = 0;
+
+ // 17. and 18. Store credential
+ //
+ // All of the credentials that we create are "resident"---we store the private key locally,
+ // and use a random value for the credential ID. The `req.options.resident_key` field
+ // determines whether we make the credential "discoverable".
+ let mut id = [0u8; 32];
+ thread_rng().fill_bytes(&mut id);
+ self.insert_credential(
+ &id,
+ &private,
+ &req.rp,
+ req.options.resident_key.unwrap_or(false),
+ &req.user.clone().unwrap_or_default().id,
+ counter,
+ );
+
+ // 19. Generate attestation statement
+ flags |= AuthenticatorDataFlags::ATTESTED;
+
+ let auth_data = AuthenticatorData {
+ rp_id_hash: req.rp.hash(),
+ flags,
+ counter,
+ credential_data: Some(AttestedCredentialData {
+ aaguid: VIRTUAL_TOKEN_AAGUID,
+ credential_id: id.to_vec(),
+ credential_public_key: public,
+ }),
+ extensions,
+ };
+
+ let mut data = auth_data.to_vec();
+ data.extend_from_slice(req.client_data_hash.as_ref());
+
+ let sig = ecdsa_p256_sha256_sign_raw(&private, &data).or(Err(HIDError::DeviceError))?;
+
+ let att_stmt = AttestationStatement::Packed(AttestationStatementPacked {
+ alg: COSEAlgorithm::ES256,
+ sig: sig.as_slice().into(),
+ attestation_cert: vec![],
+ });
+
+ let result = MakeCredentialsResult {
+ attachment: AuthenticatorAttachment::Unknown,
+ att_obj: AttestationObject {
+ att_stmt,
+ auth_data,
+ },
+ extensions: Default::default(),
+ };
+ Ok(result)
+ }
+
+ fn reset(&self, _req: &Reset) -> Result<(), HIDError> {
+ Err(HIDError::UnsupportedCommand)
+ }
+
+ fn selection(&self, _req: &Selection) -> Result<(), HIDError> {
+ Err(HIDError::UnsupportedCommand)
+ }
+}
+
+#[xpcom(implement(nsICredentialParameters), atomic)]
+struct CredentialParameters {
+ credential_id: Vec<u8>,
+ is_resident_credential: bool,
+ rp_id: String,
+ private_key: Vec<u8>,
+ user_handle: Vec<u8>,
+ sign_count: u32,
+}
+
+impl CredentialParameters {
+ xpcom_method!(get_credential_id => GetCredentialId() -> nsACString);
+ fn get_credential_id(&self) -> Result<nsCString, nsresult> {
+ Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD
+ .encode(&self.credential_id)
+ .into())
+ }
+
+ xpcom_method!(get_is_resident_credential => GetIsResidentCredential() -> bool);
+ fn get_is_resident_credential(&self) -> Result<bool, nsresult> {
+ Ok(self.is_resident_credential)
+ }
+
+ xpcom_method!(get_rp_id => GetRpId() -> nsACString);
+ fn get_rp_id(&self) -> Result<nsCString, nsresult> {
+ Ok(nsCString::from(&self.rp_id))
+ }
+
+ xpcom_method!(get_private_key => GetPrivateKey() -> nsACString);
+ fn get_private_key(&self) -> Result<nsCString, nsresult> {
+ Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD
+ .encode(&self.private_key)
+ .into())
+ }
+
+ xpcom_method!(get_user_handle => GetUserHandle() -> nsACString);
+ fn get_user_handle(&self) -> Result<nsCString, nsresult> {
+ Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD
+ .encode(&self.user_handle)
+ .into())
+ }
+
+ xpcom_method!(get_sign_count => GetSignCount() -> u32);
+ fn get_sign_count(&self) -> Result<u32, nsresult> {
+ Ok(self.sign_count)
+ }
+}
+
+#[xpcom(implement(nsIWebAuthnAutoFillEntry), atomic)]
+struct WebAuthnAutoFillEntry {
+ rp: String,
+ credential_id: Vec<u8>,
+}
+
+impl WebAuthnAutoFillEntry {
+ xpcom_method!(get_provider => GetProvider() -> u8);
+ fn get_provider(&self) -> Result<u8, nsresult> {
+ Ok(nsIWebAuthnAutoFillEntry::PROVIDER_TEST_TOKEN)
+ }
+
+ xpcom_method!(get_user_name => GetUserName() -> nsAString);
+ fn get_user_name(&self) -> Result<nsString, nsresult> {
+ Ok(nsString::from("Test User"))
+ }
+
+ xpcom_method!(get_rp_id => GetRpId() -> nsAString);
+ fn get_rp_id(&self) -> Result<nsString, nsresult> {
+ Ok(nsString::from(&self.rp))
+ }
+
+ xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>);
+ fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> {
+ Ok(self.credential_id.as_slice().into())
+ }
+}
+
+#[derive(Default)]
+pub(crate) struct TestTokenManager {
+ state: Arc<Mutex<HashMap<u64, TestToken>>>,
+}
+
+impl TestTokenManager {
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ pub fn add_virtual_authenticator(
+ &self,
+ protocol: AuthenticatorVersion,
+ transport: String,
+ has_resident_key: bool,
+ has_user_verification: bool,
+ is_user_consenting: bool,
+ is_user_verified: bool,
+ ) -> Result<u64, nsresult> {
+ let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?;
+ let token = TestToken::new(
+ vec![protocol],
+ transport,
+ has_resident_key,
+ has_user_verification,
+ is_user_consenting,
+ is_user_verified,
+ );
+ loop {
+ let id = rand::random::<u64>() & 0x1f_ffff_ffff_ffffu64; // Make the id safe for JS (53 bits)
+ match guard.deref_mut().entry(id) {
+ Entry::Occupied(_) => continue,
+ Entry::Vacant(v) => {
+ v.insert(token);
+ return Ok(id);
+ }
+ };
+ }
+ }
+
+ pub fn remove_virtual_authenticator(&self, authenticator_id: u64) -> Result<(), nsresult> {
+ let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?;
+ guard
+ .deref_mut()
+ .remove(&authenticator_id)
+ .ok_or(NS_ERROR_INVALID_ARG)?;
+ Ok(())
+ }
+
+ pub fn add_credential(
+ &self,
+ authenticator_id: u64,
+ id: &[u8],
+ privkey: &[u8],
+ user_handle: &[u8],
+ sign_count: u32,
+ rp_id: String,
+ is_resident_credential: bool,
+ ) -> Result<(), nsresult> {
+ let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?;
+ let token = guard
+ .deref_mut()
+ .get_mut(&authenticator_id)
+ .ok_or(NS_ERROR_INVALID_ARG)?;
+ let rp = RelyingParty::from(rp_id);
+ token.insert_credential(
+ id,
+ privkey,
+ &rp,
+ is_resident_credential,
+ user_handle,
+ sign_count,
+ );
+ Ok(())
+ }
+
+ pub fn get_credentials(
+ &self,
+ authenticator_id: u64,
+ ) -> Result<ThinVec<Option<RefPtr<nsICredentialParameters>>>, nsresult> {
+ let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?;
+ let token = guard
+ .get_mut(&authenticator_id)
+ .ok_or(NS_ERROR_INVALID_ARG)?;
+ let credentials = token.get_credentials();
+ let mut credentials_parameters = ThinVec::with_capacity(credentials.len());
+ for credential in credentials.deref() {
+ // CTAP1 credentials are not currently supported here.
+ let credential_parameters = CredentialParameters::allocate(InitCredentialParameters {
+ credential_id: credential.id.clone(),
+ is_resident_credential: credential.is_discoverable_credential,
+ rp_id: credential.rp.id.clone(),
+ private_key: credential.privkey.clone(),
+ user_handle: credential.user_handle.clone(),
+ sign_count: credential.sign_count.load(Ordering::Relaxed),
+ })
+ .query_interface::<nsICredentialParameters>()
+ .ok_or(NS_ERROR_FAILURE)?;
+ credentials_parameters.push(Some(credential_parameters));
+ }
+ Ok(credentials_parameters)
+ }
+
+ pub fn remove_credential(&self, authenticator_id: u64, id: &[u8]) -> Result<(), nsresult> {
+ let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?;
+ let token = guard
+ .deref_mut()
+ .get_mut(&authenticator_id)
+ .ok_or(NS_ERROR_INVALID_ARG)?;
+ if token.delete_credential(id) {
+ Ok(())
+ } else {
+ Err(NS_ERROR_INVALID_ARG)
+ }
+ }
+
+ pub fn remove_all_credentials(&self, authenticator_id: u64) -> Result<(), nsresult> {
+ let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?;
+ let token = guard
+ .deref_mut()
+ .get_mut(&authenticator_id)
+ .ok_or(NS_ERROR_INVALID_ARG)?;
+ token.delete_all_credentials();
+ Ok(())
+ }
+
+ pub fn set_user_verified(
+ &self,
+ authenticator_id: u64,
+ is_user_verified: bool,
+ ) -> Result<(), nsresult> {
+ let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?;
+ let token = guard
+ .deref_mut()
+ .get_mut(&authenticator_id)
+ .ok_or(NS_ERROR_INVALID_ARG)?;
+ token.is_user_verified = is_user_verified;
+ Ok(())
+ }
+
+ pub fn register(
+ &self,
+ _timeout_ms: u64,
+ ctap_args: RegisterArgs,
+ status: Sender<StatusUpdate>,
+ callback: StateCallback<Result<RegisterResult, AuthenticatorError>>,
+ ) {
+ if !static_prefs::pref!("security.webauth.webauthn_enable_softtoken") {
+ return;
+ }
+
+ let state_obj = self.state.clone();
+
+ // Registration doesn't currently block, but it might in a future version, so we run it on
+ // a background thread.
+ let _ = RunnableBuilder::new("TestTokenManager::register", move || {
+ // TODO(Bug 1854278) We should actually run one thread per token here
+ // and attempt to fulfill this request in parallel.
+ for token in state_obj.lock().unwrap().values_mut() {
+ let _ = token.init();
+ if ctap2::register(
+ token,
+ ctap_args.clone(),
+ status.clone(),
+ callback.clone(),
+ &|| true,
+ ) {
+ // callback was called
+ return;
+ }
+ }
+
+ // Send an error, if the callback wasn't called already.
+ callback.call(Err(AuthenticatorError::U2FToken(U2FTokenError::NotAllowed)));
+ })
+ .may_block(true)
+ .dispatch_background_task();
+ }
+
+ pub fn sign(
+ &self,
+ _timeout_ms: u64,
+ ctap_args: SignArgs,
+ status: Sender<StatusUpdate>,
+ callback: StateCallback<Result<SignResult, AuthenticatorError>>,
+ ) {
+ if !static_prefs::pref!("security.webauth.webauthn_enable_softtoken") {
+ return;
+ }
+
+ let state_obj = self.state.clone();
+
+ // Signing can block during signature selection, so we need to run it on a background thread.
+ let _ = RunnableBuilder::new("TestTokenManager::sign", move || {
+ // TODO(Bug 1854278) We should actually run one thread per token here
+ // and attempt to fulfill this request in parallel.
+ for token in state_obj.lock().unwrap().values_mut() {
+ let _ = token.init();
+ if ctap2::sign(
+ token,
+ ctap_args.clone(),
+ status.clone(),
+ callback.clone(),
+ &|| true,
+ ) {
+ // callback was called
+ return;
+ }
+ }
+
+ // Send an error, if the callback wasn't called already.
+ callback.call(Err(AuthenticatorError::U2FToken(U2FTokenError::NotAllowed)));
+ })
+ .may_block(true)
+ .dispatch_background_task();
+ }
+
+ pub fn has_platform_authenticator(&self) -> bool {
+ if !static_prefs::pref!("security.webauth.webauthn_enable_softtoken") {
+ return false;
+ }
+
+ for token in self.state.lock().unwrap().values_mut() {
+ let _ = token.init();
+ if token.transport.as_str() == "internal" {
+ return true;
+ }
+ }
+
+ false
+ }
+
+ pub fn get_autofill_entries(
+ &self,
+ rp_id: &str,
+ credential_filter: &Vec<PublicKeyCredentialDescriptor>,
+ ) -> Result<ThinVec<Option<RefPtr<nsIWebAuthnAutoFillEntry>>>, nsresult> {
+ let guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?;
+ let mut entries = ThinVec::new();
+
+ for token in guard.values() {
+ let credentials = token.get_credentials();
+ for credential in credentials.deref() {
+ // The relying party ID must match.
+ if !rp_id.eq(&credential.rp.id) {
+ continue;
+ }
+ // Only discoverable credentials are admissible.
+ if !credential.is_discoverable_credential {
+ continue;
+ }
+ // Only credentials listed in the credential filter (if it is
+ // non-empty) are admissible.
+ if credential_filter.len() > 0
+ && credential_filter
+ .iter()
+ .find(|cred| cred.id == credential.id)
+ .is_none()
+ {
+ continue;
+ }
+ let entry = WebAuthnAutoFillEntry::allocate(InitWebAuthnAutoFillEntry {
+ rp: credential.rp.id.clone(),
+ credential_id: credential.id.clone(),
+ })
+ .query_interface::<nsIWebAuthnAutoFillEntry>()
+ .ok_or(NS_ERROR_FAILURE)?;
+ entries.push(Some(entry));
+ }
+ }
+ Ok(entries)
+ }
+}