summaryrefslogtreecommitdiffstats
path: root/dom/webauthn/WebAuthnController.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /dom/webauthn/WebAuthnController.cpp
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/webauthn/WebAuthnController.cpp')
-rw-r--r--dom/webauthn/WebAuthnController.cpp739
1 files changed, 739 insertions, 0 deletions
diff --git a/dom/webauthn/WebAuthnController.cpp b/dom/webauthn/WebAuthnController.cpp
new file mode 100644
index 0000000000..c089baaca3
--- /dev/null
+++ b/dom/webauthn/WebAuthnController.cpp
@@ -0,0 +1,739 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "json/json.h"
+#include "mozilla/dom/WebAuthnController.h"
+#include "mozilla/dom/PWebAuthnTransactionParent.h"
+#include "mozilla/dom/WebAuthnUtil.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_security.h"
+#include "mozilla/Unused.h"
+#include "nsEscape.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+#include "nsIThread.h"
+#include "nsTextFormatter.h"
+#include "mozilla/Telemetry.h"
+
+#include "AuthrsTransport.h"
+#include "CtapArgs.h"
+#include "CtapResults.h"
+#include "U2FSoftTokenTransport.h"
+#include "WebAuthnEnumStrings.h"
+
+#ifdef MOZ_WIDGET_ANDROID
+# include "mozilla/dom/AndroidWebAuthnTokenManager.h"
+#endif
+
+namespace mozilla::dom {
+
+/***********************************************************************
+ * Statics
+ **********************************************************************/
+
+namespace {
+static mozilla::LazyLogModule gWebAuthnControllerLog("webauthncontroller");
+StaticRefPtr<WebAuthnController> gWebAuthnController;
+static nsIThread* gWebAuthnBackgroundThread;
+} // namespace
+
+// Data for WebAuthn UI prompt notifications.
+static const char16_t kPresencePromptNotification[] =
+ u"{\"is_ctap2\":true,\"action\":\"presence\",\"tid\":%llu,"
+ u"\"origin\":\"%s\",\"browsingContextId\":%llu}";
+static const char16_t kRegisterDirectPromptNotification[] =
+ u"{\"is_ctap2\":true,\"action\":\"register-direct\",\"tid\":%llu,"
+ u"\"origin\":\"%s\",\"browsingContextId\":%llu}";
+static const char16_t kCancelPromptNotification[] =
+ u"{\"is_ctap2\":true,\"action\":\"cancel\",\"tid\":%llu}";
+static const char16_t kSelectSignResultNotification[] =
+ u"{\"is_ctap2\":true,\"action\":\"select-sign-result\",\"tid\":%llu,"
+ u"\"origin\":\"%s\",\"browsingContextId\":%llu,\"usernames\":[%s]}";
+
+/***********************************************************************
+ * U2FManager Implementation
+ **********************************************************************/
+
+NS_IMPL_ISUPPORTS(WebAuthnController, nsIWebAuthnController,
+ nsIU2FTokenManager);
+
+WebAuthnController::WebAuthnController() : mTransactionParent(nullptr) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ // Create on the main thread to make sure ClearOnShutdown() works.
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+// static
+void WebAuthnController::Initialize() {
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!gWebAuthnController);
+ gWebAuthnController = new WebAuthnController();
+ ClearOnShutdown(&gWebAuthnController);
+}
+
+// static
+WebAuthnController* WebAuthnController::Get() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ mozilla::ipc::AssertIsOnBackgroundThread();
+ return gWebAuthnController;
+}
+
+void WebAuthnController::AbortTransaction(const uint64_t& aTransactionId,
+ const nsresult& aError,
+ bool shouldCancelActiveDialog) {
+ if (mTransactionParent && mTransaction.isSome() && aTransactionId > 0 &&
+ aTransactionId == mTransaction.ref().mTransactionId) {
+ Unused << mTransactionParent->SendAbort(aTransactionId, aError);
+ ClearTransaction(shouldCancelActiveDialog);
+ }
+}
+
+void WebAuthnController::AbortOngoingTransaction() {
+ if (mTransaction.isSome()) {
+ AbortTransaction(mTransaction.ref().mTransactionId, NS_ERROR_DOM_ABORT_ERR,
+ true);
+ }
+}
+
+void WebAuthnController::MaybeClearTransaction(
+ PWebAuthnTransactionParent* aParent) {
+ // Only clear if we've been requested to do so by our current transaction
+ // parent.
+ if (mTransactionParent == aParent) {
+ ClearTransaction(true);
+ }
+}
+
+void WebAuthnController::ClearTransaction(bool cancel_prompt) {
+ if (cancel_prompt && mTransaction.isSome() &&
+ mTransaction.ref().mTransactionId > 0) {
+ // Remove any prompts we might be showing for the current transaction.
+ SendPromptNotification(kCancelPromptNotification,
+ mTransaction.ref().mTransactionId);
+ }
+ mTransactionParent = nullptr;
+ mTransportImpl = nullptr;
+
+ // Forget any pending registration.
+ mPendingRegisterInfo.reset();
+ mPendingSignInfo.reset();
+ mPendingSignResults.Clear();
+ mTransaction.reset();
+}
+
+template <typename... T>
+void WebAuthnController::SendPromptNotification(const char16_t* aFormat,
+ T... aArgs) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ nsAutoString json;
+ nsTextFormatter::ssprintf(json, aFormat, aArgs...);
+
+ nsCOMPtr<nsIRunnable> r(NewRunnableMethod<nsString>(
+ "WebAuthnController::RunSendPromptNotification", this,
+ &WebAuthnController::RunSendPromptNotification, json));
+
+ MOZ_ALWAYS_SUCCEEDS(GetMainThreadSerialEventTarget()->Dispatch(
+ r.forget(), NS_DISPATCH_NORMAL));
+}
+
+NS_IMETHODIMP
+WebAuthnController::SendPromptNotificationPreformatted(
+ uint64_t aTransactionId, const nsACString& aJson) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ nsCOMPtr<nsIRunnable> r(NewRunnableMethod<nsString>(
+ "WebAuthnController::RunSendPromptNotification", this,
+ &WebAuthnController::RunSendPromptNotification,
+ NS_ConvertUTF8toUTF16(aJson)));
+ MOZ_ALWAYS_SUCCEEDS(GetMainThreadSerialEventTarget()->Dispatch(
+ r.forget(), NS_DISPATCH_NORMAL));
+ return NS_OK;
+}
+
+void WebAuthnController::RunSendPromptNotification(const nsString& aJSON) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ if (NS_WARN_IF(!os)) {
+ return;
+ }
+
+ nsCOMPtr<nsIWebAuthnController> self = this;
+ MOZ_ALWAYS_SUCCEEDS(
+ os->NotifyObservers(self, "webauthn-prompt", aJSON.get()));
+}
+
+nsCOMPtr<nsIWebAuthnTransport> WebAuthnController::GetTransportImpl() {
+ mozilla::ipc::AssertIsOnBackgroundThread();
+
+ if (mTransportImpl) {
+ return mTransportImpl;
+ }
+
+/* Enable in Bug 1819414 */
+#if 0
+# ifdef MOZ_WIDGET_ANDROID
+ // On Android, prefer the platform support if enabled.
+ if (StaticPrefs::security_webauth_webauthn_enable_android_fido2()) {
+ nsCOMPtr<nsIWebAuthnTransport> transport = AndroidWebAuthnTokenManager::GetInstance();
+ transport->SetController(this);
+ return transport;
+ }
+# endif
+#endif
+
+ // Prefer the HW token, even if the softtoken is enabled too.
+ // We currently don't support soft and USB tokens enabled at the
+ // same time as the softtoken would always win the race to register.
+ // We could support it for signing though...
+ if (StaticPrefs::security_webauth_webauthn_enable_usbtoken()) {
+ nsCOMPtr<nsIWebAuthnTransport> transport = NewAuthrsTransport();
+ transport->SetController(this);
+ return transport;
+ }
+
+ if (StaticPrefs::security_webauth_webauthn_enable_softtoken()) {
+ nsCOMPtr<nsIWebAuthnTransport> transport = new U2FSoftTokenTransport(
+ StaticPrefs::security_webauth_softtoken_counter());
+ transport->SetController(this);
+ return transport;
+ }
+
+ // TODO Use WebAuthnRequest to aggregate results from all transports,
+ // once we have multiple HW transport types.
+
+ return nullptr;
+}
+
+void WebAuthnController::Cancel(PWebAuthnTransactionParent* aTransactionParent,
+ const Tainted<uint64_t>& aTransactionId) {
+ // The last transaction ID also suffers from the issue described in Bug
+ // 1696159. A content process could cancel another content processes
+ // transaction by guessing the last transaction ID.
+ if (mTransactionParent != aTransactionParent || mTransaction.isNothing() ||
+ !MOZ_IS_VALID(aTransactionId,
+ mTransaction.ref().mTransactionId == aTransactionId)) {
+ return;
+ }
+
+ if (mTransportImpl) {
+ mTransportImpl->Cancel();
+ }
+
+ ClearTransaction(true);
+}
+
+void WebAuthnController::Register(
+ PWebAuthnTransactionParent* aTransactionParent,
+ const uint64_t& aTransactionId, const WebAuthnMakeCredentialInfo& aInfo) {
+ mozilla::ipc::AssertIsOnBackgroundThread();
+ MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug,
+ ("WebAuthnController::Register"));
+ MOZ_ASSERT(aTransactionId > 0);
+
+ if (!gWebAuthnBackgroundThread) {
+ gWebAuthnBackgroundThread = NS_GetCurrentThread();
+ MOZ_ASSERT(gWebAuthnBackgroundThread, "This should never be null!");
+ }
+
+ AbortOngoingTransaction();
+ mTransactionParent = aTransactionParent;
+
+ /* We could refactor to defer the hashing here */
+ CryptoBuffer rpIdHash, clientDataHash;
+ NS_ConvertUTF16toUTF8 rpId(aInfo.RpId());
+ nsresult rv = BuildTransactionHashes(rpId, aInfo.ClientDataJSON(), rpIdHash,
+ clientDataHash);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // We haven't set mTransaction yet, so we can't use AbortTransaction
+ Unused << mTransactionParent->SendAbort(aTransactionId,
+ NS_ERROR_DOM_UNKNOWN_ERR);
+ return;
+ }
+
+ // Hold on to any state that we need to finish the transaction.
+ mTransaction = Some(
+ Transaction(aTransactionId, rpIdHash, Nothing(), aInfo.ClientDataJSON()));
+
+ MOZ_ASSERT(mPendingRegisterInfo.isNothing());
+ mPendingRegisterInfo = Some(aInfo);
+
+ // Determine whether direct attestation was requested.
+ bool noneAttestationRequested = true;
+
+// On Android, let's always reject direct attestations until we have a
+// mechanism to solicit user consent, from Bug 1550164
+#ifndef MOZ_WIDGET_ANDROID
+ // The default attestation type is "none", so set
+ // noneAttestationRequested=false only if the RP's preference matches one of
+ // the other known types. This needs to be reviewed if values are added to
+ // the AttestationConveyancePreference enum.
+ const nsString& attestation = aInfo.attestationConveyancePreference();
+ static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 2);
+ if (attestation.EqualsLiteral(
+ MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT) ||
+ attestation.EqualsLiteral(
+ MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT) ||
+ attestation.EqualsLiteral(
+ MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_ENTERPRISE)) {
+ noneAttestationRequested = false;
+ }
+#endif // not MOZ_WIDGET_ANDROID
+
+ // Start a register request immediately if direct attestation
+ // wasn't requested or the test pref is set.
+ if (noneAttestationRequested ||
+ StaticPrefs::
+ security_webauth_webauthn_testing_allow_direct_attestation()) {
+ DoRegister(aInfo, noneAttestationRequested);
+ return;
+ }
+
+ // If the RP request direct attestation, ask the user for permission and
+ // store the transaction info until the user proceeds or cancels.
+ NS_ConvertUTF16toUTF8 origin(aInfo.Origin());
+ SendPromptNotification(kRegisterDirectPromptNotification, aTransactionId,
+ origin.get(), aInfo.BrowsingContextId());
+}
+
+void WebAuthnController::DoRegister(const WebAuthnMakeCredentialInfo& aInfo,
+ bool aForceNoneAttestation) {
+ mozilla::ipc::AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mTransaction.isSome());
+ if (NS_WARN_IF(mTransaction.isNothing())) {
+ // Clear prompt?
+ return;
+ }
+
+ // Show a prompt that lets the user cancel the ongoing transaction.
+ NS_ConvertUTF16toUTF8 origin(aInfo.Origin());
+ SendPromptNotification(kPresencePromptNotification,
+ mTransaction.ref().mTransactionId, origin.get(),
+ aInfo.BrowsingContextId(), "false");
+
+ RefPtr<CtapRegisterArgs> args(
+ new CtapRegisterArgs(aInfo, aForceNoneAttestation));
+
+ mTransportImpl = GetTransportImpl();
+ if (!mTransportImpl) {
+ AbortTransaction(mTransaction.ref().mTransactionId,
+ NS_ERROR_DOM_NOT_ALLOWED_ERR, true);
+ return;
+ }
+ nsresult rv = mTransportImpl->MakeCredential(
+ mTransaction.ref().mTransactionId, aInfo.BrowsingContextId(), args);
+ if (NS_FAILED(rv)) {
+ AbortTransaction(mTransaction.ref().mTransactionId, rv, true);
+ return;
+ }
+}
+
+NS_IMETHODIMP
+WebAuthnController::ResumeRegister(uint64_t aTransactionId,
+ bool aForceNoneAttestation) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!gWebAuthnBackgroundThread) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIRunnable> r(NewRunnableMethod<uint64_t, bool>(
+ "WebAuthnController::RunResumeRegister", this,
+ &WebAuthnController::RunResumeRegister, aTransactionId,
+ aForceNoneAttestation));
+
+ return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
+}
+
+void WebAuthnController::RunResumeRegister(uint64_t aTransactionId,
+ bool aForceNoneAttestation) {
+ mozilla::ipc::AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(mPendingRegisterInfo.isNothing())) {
+ return;
+ }
+
+ if (mTransaction.isNothing() ||
+ mTransaction.ref().mTransactionId != aTransactionId) {
+ return;
+ }
+
+ // Resume registration and cleanup.
+ DoRegister(mPendingRegisterInfo.ref(), aForceNoneAttestation);
+}
+
+NS_IMETHODIMP
+WebAuthnController::FinishRegister(uint64_t aTransactionId,
+ nsICtapRegisterResult* aResult) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ nsCOMPtr<nsIRunnable> r(
+ NewRunnableMethod<uint64_t, RefPtr<nsICtapRegisterResult>>(
+ "WebAuthnController::RunFinishRegister", this,
+ &WebAuthnController::RunFinishRegister, aTransactionId, aResult));
+
+ if (!gWebAuthnBackgroundThread) {
+ return NS_ERROR_FAILURE;
+ }
+ return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
+}
+
+void WebAuthnController::RunFinishRegister(
+ uint64_t aTransactionId, const RefPtr<nsICtapRegisterResult>& aResult) {
+ mozilla::ipc::AssertIsOnBackgroundThread();
+ if (mTransaction.isNothing() ||
+ aTransactionId != mTransaction.ref().mTransactionId) {
+ // The previous transaction was likely cancelled from the prompt.
+ return;
+ }
+
+ nsresult status;
+ nsresult rv = aResult->GetStatus(&status);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true);
+ return;
+ }
+ if (NS_FAILED(status)) {
+ bool shouldCancelActiveDialog = true;
+ if (status == NS_ERROR_DOM_OPERATION_ERR) {
+ // PIN-related errors. Let the dialog show to inform the user
+ shouldCancelActiveDialog = false;
+ }
+ Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
+ u"CTAPRegisterAbort"_ns, 1);
+ AbortTransaction(aTransactionId, status, shouldCancelActiveDialog);
+ return;
+ }
+
+ nsCString clientDataJson = mPendingRegisterInfo.ref().ClientDataJSON();
+
+ nsTArray<uint8_t> attObj;
+ rv = aResult->GetAttestationObject(attObj);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true);
+ return;
+ }
+
+ nsTArray<uint8_t> credentialId;
+ rv = aResult->GetCredentialId(credentialId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true);
+ return;
+ }
+
+ nsTArray<WebAuthnExtensionResult> extensions;
+ WebAuthnMakeCredentialResult result(clientDataJson, attObj, credentialId,
+ extensions);
+
+ Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
+ u"CTAPRegisterFinish"_ns, 1);
+ Unused << mTransactionParent->SendConfirmRegister(aTransactionId, result);
+ ClearTransaction(true);
+}
+
+void WebAuthnController::Sign(PWebAuthnTransactionParent* aTransactionParent,
+ const uint64_t& aTransactionId,
+ const WebAuthnGetAssertionInfo& aInfo) {
+ mozilla::ipc::AssertIsOnBackgroundThread();
+ MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug, ("WebAuthnSign"));
+ MOZ_ASSERT(aTransactionId > 0);
+
+ if (!gWebAuthnBackgroundThread) {
+ gWebAuthnBackgroundThread = NS_GetCurrentThread();
+ MOZ_ASSERT(gWebAuthnBackgroundThread, "This should never be null!");
+ }
+
+ AbortOngoingTransaction();
+ mTransactionParent = aTransactionParent;
+
+ /* We could refactor to defer the hashing here */
+ CryptoBuffer rpIdHash, clientDataHash;
+ NS_ConvertUTF16toUTF8 rpId(aInfo.RpId());
+ nsresult rv = BuildTransactionHashes(rpId, aInfo.ClientDataJSON(), rpIdHash,
+ clientDataHash);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // We haven't set mTransaction yet, so we can't use AbortTransaction
+ Unused << mTransactionParent->SendAbort(aTransactionId,
+ NS_ERROR_DOM_UNKNOWN_ERR);
+ return;
+ }
+
+ Maybe<nsTArray<uint8_t>> appIdHash = Nothing();
+ for (const WebAuthnExtension& ext : aInfo.Extensions()) {
+ if (ext.type() == WebAuthnExtension::TWebAuthnExtensionAppId) {
+ appIdHash = Some(ext.get_WebAuthnExtensionAppId().AppId().Clone());
+ }
+ }
+
+ // Hold on to any state that we need to finish the transaction.
+ mTransaction = Some(
+ Transaction(aTransactionId, rpIdHash, appIdHash, aInfo.ClientDataJSON()));
+
+ mPendingSignInfo = Some(aInfo);
+
+ // Show a prompt that lets the user cancel the ongoing transaction.
+ NS_ConvertUTF16toUTF8 origin(aInfo.Origin());
+ SendPromptNotification(kPresencePromptNotification,
+ mTransaction.ref().mTransactionId, origin.get(),
+ aInfo.BrowsingContextId(), "false");
+
+ RefPtr<CtapSignArgs> args(new CtapSignArgs(aInfo));
+
+ mTransportImpl = GetTransportImpl();
+ if (!mTransportImpl) {
+ AbortTransaction(mTransaction.ref().mTransactionId,
+ NS_ERROR_DOM_NOT_ALLOWED_ERR, true);
+ return;
+ }
+
+ rv = mTransportImpl->GetAssertion(mTransaction.ref().mTransactionId,
+ aInfo.BrowsingContextId(), args.get());
+ if (NS_FAILED(rv)) {
+ AbortTransaction(mTransaction.ref().mTransactionId, rv, true);
+ return;
+ }
+}
+
+NS_IMETHODIMP
+WebAuthnController::FinishSign(
+ uint64_t aTransactionId,
+ const nsTArray<RefPtr<nsICtapSignResult>>& aResult) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ nsTArray<RefPtr<nsICtapSignResult>> ownedResult = aResult.Clone();
+
+ nsCOMPtr<nsIRunnable> r(
+ NewRunnableMethod<uint64_t, nsTArray<RefPtr<nsICtapSignResult>>>(
+ "WebAuthnController::RunFinishSign", this,
+ &WebAuthnController::RunFinishSign, aTransactionId,
+ std::move(ownedResult)));
+
+ if (!gWebAuthnBackgroundThread) {
+ return NS_ERROR_FAILURE;
+ }
+ return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
+}
+
+void WebAuthnController::RunFinishSign(
+ uint64_t aTransactionId,
+ const nsTArray<RefPtr<nsICtapSignResult>>& aResult) {
+ mozilla::ipc::AssertIsOnBackgroundThread();
+ if (mTransaction.isNothing() ||
+ aTransactionId != mTransaction.ref().mTransactionId) {
+ return;
+ }
+
+ if (aResult.Length() == 0) {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
+ u"CTAPSignAbort"_ns, 1);
+ AbortTransaction(aTransactionId, NS_ERROR_DOM_UNKNOWN_ERR, true);
+ return;
+ }
+
+ if (aResult.Length() == 1) {
+ nsresult status;
+ nsresult rv = aResult[0]->GetStatus(&status);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true);
+ return;
+ }
+ if (NS_FAILED(status)) {
+ bool shouldCancelActiveDialog = true;
+ if (status == NS_ERROR_DOM_OPERATION_ERR) {
+ // PIN-related errors, e.g. blocked token. Let the dialog show to inform
+ // the user
+ shouldCancelActiveDialog = false;
+ }
+ Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
+ u"CTAPSignAbort"_ns, 1);
+ AbortTransaction(aTransactionId, status, shouldCancelActiveDialog);
+ return;
+ }
+ mPendingSignResults = aResult.Clone();
+ RunResumeWithSelectedSignResult(aTransactionId, 0);
+ return;
+ }
+
+ // If we more than one assertion, all of them should have OK status.
+ for (const auto& assertion : aResult) {
+ nsresult status;
+ nsresult rv = assertion->GetStatus(&status);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true);
+ return;
+ }
+ if (NS_WARN_IF(NS_FAILED(status))) {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
+ u"CTAPSignAbort"_ns, 1);
+ AbortTransaction(aTransactionId, status, true);
+ return;
+ }
+ }
+
+ nsCString usernames;
+ StringJoinAppend(
+ usernames, ","_ns, aResult,
+ [](nsACString& dst, const RefPtr<nsICtapSignResult>& assertion) {
+ nsCString username;
+ nsresult rv = assertion->GetUserName(username);
+ if (NS_FAILED(rv)) {
+ username.Assign("<Unknown username>");
+ }
+ nsCString escaped_username;
+ NS_Escape(username, escaped_username, url_XAlphas);
+ dst.Append("\""_ns + escaped_username + "\""_ns);
+ });
+
+ mPendingSignResults = aResult.Clone();
+ NS_ConvertUTF16toUTF8 origin(mPendingSignInfo.ref().Origin());
+ SendPromptNotification(kSelectSignResultNotification,
+ mTransaction.ref().mTransactionId, origin.get(),
+ mPendingSignInfo.ref().BrowsingContextId(),
+ usernames.get());
+}
+
+NS_IMETHODIMP
+WebAuthnController::SignatureSelectionCallback(uint64_t aTransactionId,
+ uint64_t idx) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIRunnable> r(NewRunnableMethod<uint64_t, uint64_t>(
+ "WebAuthnController::RunResumeWithSelectedSignResult", this,
+ &WebAuthnController::RunResumeWithSelectedSignResult, aTransactionId,
+ idx));
+
+ if (!gWebAuthnBackgroundThread) {
+ return NS_ERROR_FAILURE;
+ }
+ return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
+}
+
+void WebAuthnController::RunResumeWithSelectedSignResult(
+ uint64_t aTransactionId, uint64_t idx) {
+ mozilla::ipc::AssertIsOnBackgroundThread();
+ if (mTransaction.isNothing() ||
+ mTransaction.ref().mTransactionId != aTransactionId) {
+ return;
+ }
+
+ if (NS_WARN_IF(mPendingSignResults.Length() <= idx)) {
+ return;
+ }
+
+ RefPtr<nsICtapSignResult>& selected = mPendingSignResults[idx];
+
+ nsTArray<uint8_t> credentialId;
+ nsresult rv = selected->GetCredentialId(credentialId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true);
+ return;
+ }
+
+ nsTArray<uint8_t> signature;
+ rv = selected->GetSignature(signature);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true);
+ return;
+ }
+
+ nsTArray<uint8_t> authenticatorData;
+ rv = selected->GetAuthenticatorData(authenticatorData);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true);
+ return;
+ }
+
+ nsTArray<uint8_t> rpIdHash;
+ rv = selected->GetRpIdHash(rpIdHash);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true);
+ return;
+ }
+
+ nsTArray<uint8_t> userHandle;
+ Unused << selected->GetUserHandle(userHandle); // optional
+
+ nsTArray<WebAuthnExtensionResult> extensions;
+ if (mTransaction.ref().mAppIdHash.isSome()) {
+ bool usedAppId = (rpIdHash == mTransaction.ref().mAppIdHash.ref());
+ extensions.AppendElement(WebAuthnExtensionResultAppId(usedAppId));
+ }
+
+ WebAuthnGetAssertionResult result(mTransaction.ref().mClientDataJSON,
+ credentialId, signature, authenticatorData,
+ extensions, userHandle);
+
+ Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
+ u"CTAPSignFinish"_ns, 1);
+ Unused << mTransactionParent->SendConfirmSign(aTransactionId, result);
+ ClearTransaction(true);
+}
+
+NS_IMETHODIMP
+WebAuthnController::PinCallback(uint64_t aTransactionId,
+ const nsACString& aPin) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIRunnable> r(NewRunnableMethod<uint64_t, nsCString>(
+ "WebAuthnController::RunPinCallback", this,
+ &WebAuthnController::RunPinCallback, aTransactionId, aPin));
+
+ if (!gWebAuthnBackgroundThread) {
+ return NS_ERROR_FAILURE;
+ }
+ return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
+}
+
+void WebAuthnController::RunPinCallback(uint64_t aTransactionId,
+ const nsCString& aPin) {
+ mozilla::ipc::AssertIsOnBackgroundThread();
+
+ if (mTransportImpl) {
+ mTransportImpl->PinCallback(aTransactionId, aPin);
+ }
+}
+
+NS_IMETHODIMP
+WebAuthnController::Cancel(uint64_t aTransactionId) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIRunnable> r(NewRunnableMethod<uint64_t>(
+ "WebAuthnController::RunCancel", this, &WebAuthnController::RunCancel,
+ aTransactionId));
+
+ if (!gWebAuthnBackgroundThread) {
+ return NS_ERROR_FAILURE;
+ }
+ return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
+}
+
+void WebAuthnController::RunCancel(uint64_t aTransactionId) {
+ mozilla::ipc::AssertIsOnBackgroundThread();
+
+ if (mTransaction.isNothing() ||
+ mTransaction.ref().mTransactionId != aTransactionId) {
+ return;
+ }
+
+ // Cancel the request.
+ if (mTransportImpl) {
+ mTransportImpl->Cancel();
+ }
+
+ // Reject the promise.
+ AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR, true);
+}
+
+} // namespace mozilla::dom