Bug 1430150 - Implement WebAuthentication permission prompts r=jcj,johannh
authorTim Taubert <ttaubert@mozilla.com>
Sun, 11 Mar 2018 18:47:14 +0100
changeset 462567 8d91193cd766a4dfda888de34792389601d2d6de
parent 462566 54ed9f585985091fa72251007ffd66b594b665f0
child 462568 2bf39f14c6f8772d66ee1d369bf8772b38bf3e08
push id1683
push usersfraser@mozilla.com
push dateThu, 26 Apr 2018 16:43:40 +0000
treeherdermozilla-release@5af6cb21869d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjcj, johannh
bugs1430150
milestone60.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1430150 - Implement WebAuthentication permission prompts r=jcj,johannh Reviewers: jcj, johannh Reviewed By: jcj, johannh Bug #: 1430150 Differential Revision: https://phabricator.services.mozilla.com/D638
browser/base/content/browser.css
browser/base/content/browser.js
browser/installer/package-manifest.in
browser/locales/en-US/chrome/browser/browser.properties
dom/u2f/U2F.cpp
dom/webauthn/PWebAuthnTransaction.ipdl
dom/webauthn/U2FHIDTokenManager.cpp
dom/webauthn/U2FSoftTokenManager.cpp
dom/webauthn/U2FTokenManager.cpp
dom/webauthn/U2FTokenManager.h
dom/webauthn/WebAuthnManager.cpp
dom/webauthn/moz.build
dom/webauthn/nsIU2FTokenManager.idl
dom/webauthn/tests/browser/browser.ini
dom/webauthn/tests/browser/browser_webauthn_prompts.js
dom/webauthn/tests/browser/browser_webauthn_telemetry.js
dom/webauthn/tests/browser/head.js
dom/webauthn/tests/browser/tab_webauthn_success.html
dom/webauthn/tests/cbor.js
dom/webauthn/tests/cbor/cbor.js
dom/webauthn/tests/mochitest.ini
dom/webauthn/tests/test_webauthn_attestation_conveyance.html
dom/webauthn/tests/test_webauthn_loopback.html
dom/webauthn/tests/u2futil.js
toolkit/content/widgets/notification.xml
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -1362,16 +1362,20 @@ toolbarpaletteitem[place="palette"][hidd
 .popup-notification-invalid-input {
   box-shadow: 0 0 1.5px 1px red;
 }
 
 .popup-notification-invalid-input[focused] {
   box-shadow: 0 0 2px 2px rgba(255,0,0,0.4);
 }
 
+.popup-notification-description[popupid=webauthn-prompt-register-direct] {
+  white-space: pre-line;
+}
+
 .dragfeedback-tab {
   -moz-appearance: none;
   opacity: 0.65;
   -moz-window-shadow: none;
 }
 
 /* Page action panel */
 #pageAction-panel-sendToDevice-subview-body:not([state="notready"]) > #pageAction-panel-sendToDevice-notReady,
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1462,16 +1462,17 @@ var gBrowserInit = {
     Services.obs.addObserver(gXPInstallObserver, "addon-install-failed");
     Services.obs.addObserver(gXPInstallObserver, "addon-install-confirmation");
     Services.obs.addObserver(gXPInstallObserver, "addon-install-complete");
     window.messageManager.addMessageListener("Browser:URIFixup", gKeywordURIFixup);
 
     BrowserOffline.init();
     IndexedDBPromptHelper.init();
     CanvasPermissionPromptHelper.init();
+    WebAuthnPromptHelper.init();
 
     // Initialize the full zoom setting.
     // We do this before the session restore service gets initialized so we can
     // apply full zoom settings to tabs restored by the session restore service.
     FullZoom.init();
     PanelUI.init();
 
     UpdateUrlbarSearchSplitterState();
@@ -1932,16 +1933,17 @@ var gBrowserInit = {
       }
 
       if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
         MenuTouchModeObserver.uninit();
       }
       BrowserOffline.uninit();
       IndexedDBPromptHelper.uninit();
       CanvasPermissionPromptHelper.uninit();
+      WebAuthnPromptHelper.uninit();
       PanelUI.uninit();
       AutoShowBookmarksToolbar.uninit();
     }
 
     // Final window teardown, do this last.
     gBrowser.destroy();
     window.XULBrowserWindow = null;
     window.QueryInterface(Ci.nsIInterfaceRequestor)
@@ -6764,16 +6766,143 @@ var CanvasPermissionPromptHelper = {
       checkbox,
       name: uri.asciiHost,
     };
     PopupNotifications.show(browser, aTopic, message, this._notificationIcon,
                             mainAction, secondaryActions, options);
   }
 };
 
+var WebAuthnPromptHelper = {
+  _icon: "default-notification-icon",
+  _topic: "webauthn-prompt",
+
+  // The current notification, if any. The U2F manager is a singleton, we will
+  // never allow more than one active request. And thus we'll never have more
+  // than one notification either.
+  _current: null,
+
+  // The current transaction ID. Will be checked when we're notified of the
+  // cancellation of an ongoing WebAuthhn request.
+  _tid: 0,
+
+  init() {
+    Services.obs.addObserver(this, this._topic);
+  },
+
+  uninit() {
+    Services.obs.removeObserver(this, this._topic);
+  },
+
+  observe(aSubject, aTopic, aData) {
+    let mgr = aSubject.QueryInterface(Ci.nsIU2FTokenManager);
+    let data = JSON.parse(aData);
+
+    if (data.action == "register") {
+      this.register(mgr, data);
+    } else if (data.action == "register-direct") {
+      this.registerDirect(mgr, data);
+    } else if (data.action == "sign") {
+      this.sign(mgr, data);
+    } else if (data.action == "cancel") {
+      this.cancel(data);
+    }
+  },
+
+  register(mgr, {origin, tid}) {
+    let mainAction = this.buildCancelAction(mgr, tid);
+    this.show(tid, "register", "webauthn.registerPrompt", origin, mainAction);
+  },
+
+  registerDirect(mgr, {origin, tid}) {
+    let mainAction = this.buildProceedAction(mgr, tid);
+    let secondaryActions = [this.buildCancelAction(mgr, tid)];
+
+    let learnMoreURL =
+      Services.urlFormatter.formatURLPref("app.support.baseURL") +
+      "webauthn-direct-attestation";
+
+    let options = {
+      learnMoreURL,
+      checkbox: {
+        label: gNavigatorBundle.getString("webauthn.anonymize")
+      }
+    };
+
+    this.show(tid, "register-direct", "webauthn.registerDirectPrompt",
+              origin, mainAction, secondaryActions, options);
+  },
+
+  sign(mgr, {origin, tid}) {
+    let mainAction = this.buildCancelAction(mgr, tid);
+    this.show(tid, "sign", "webauthn.signPrompt", origin, mainAction);
+  },
+
+  show(tid, id, stringId, origin, mainAction, secondaryActions = [], options = {}) {
+    this.reset();
+
+    try {
+      origin = Services.io.newURI(origin).asciiHost;
+    } catch (e) {
+      /* Might fail for arbitrary U2F RP IDs. */
+    }
+
+    let brandShortName =
+      document.getElementById("bundle_brand").getString("brandShortName");
+    let message =
+      gNavigatorBundle.getFormattedString(stringId, ["<>", brandShortName], 1);
+
+    options.name = origin;
+    options.hideClose = true;
+    options.eventCallback = event => {
+      if (event == "removed") {
+        this._current = null;
+        this._tid = 0;
+      }
+    };
+
+    this._tid = tid;
+    this._current = PopupNotifications.show(
+      gBrowser.selectedBrowser, `webauthn-prompt-${id}`, message,
+      this._icon, mainAction, secondaryActions, options);
+  },
+
+  cancel({tid}) {
+    if (this._tid == tid) {
+      this.reset();
+    }
+  },
+
+  reset() {
+    if (this._current) {
+      this._current.remove();
+    }
+  },
+
+  buildProceedAction(mgr, tid) {
+    return {
+      label: gNavigatorBundle.getString("webauthn.proceed"),
+      accessKey: gNavigatorBundle.getString("webauthn.proceed.accesskey"),
+      callback(state) {
+        mgr.resumeRegister(tid, !state.checkboxChecked);
+      }
+    };
+  },
+
+  buildCancelAction(mgr, tid) {
+    return {
+      label: gNavigatorBundle.getString("webauthn.cancel"),
+      accessKey: gNavigatorBundle.getString("webauthn.cancel.accesskey"),
+      callback() {
+        mgr.cancel(tid);
+      }
+    };
+  },
+};
+
 function CanCloseWindow() {
   // Avoid redundant calls to canClose from showing multiple
   // PermitUnload dialogs.
   if (Services.startup.shuttingDown || window.skipNextCanClose) {
     return true;
   }
 
   let timedOutProcesses = new WeakSet();
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -211,16 +211,17 @@
 @RESPATH@/components/dom_payments.xpt
 @RESPATH@/components/dom_power.xpt
 @RESPATH@/components/dom_push.xpt
 @RESPATH@/components/dom_quota.xpt
 @RESPATH@/components/dom_range.xpt
 @RESPATH@/components/dom_security.xpt
 @RESPATH@/components/dom_sidebar.xpt
 @RESPATH@/components/dom_storage.xpt
+@RESPATH@/components/dom_webauthn.xpt
 #ifdef MOZ_WEBSPEECH
 @RESPATH@/components/dom_webspeechrecognition.xpt
 #endif
 @RESPATH@/components/dom_workers.xpt
 @RESPATH@/components/dom_xul.xpt
 @RESPATH@/components/dom_presentation.xpt
 @RESPATH@/components/downloads.xpt
 @RESPATH@/components/editor.xpt
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -499,16 +499,38 @@ offlineApps.manageUsageAccessKey=S
 # LOCALIZATION NOTE (canvas.siteprompt): %S is hostname
 canvas.siteprompt=Will you allow %S to use your HTML5 canvas image data? This may be used to uniquely identify your computer.
 canvas.notAllow=Don’t Allow
 canvas.notAllow.accesskey=n
 canvas.allow=Allow Data Access
 canvas.allow.accesskey=A
 canvas.remember=Always remember my decision
 
+# WebAuthn prompts
+# LOCALIZATION NOTE (webauthn.registerPrompt): %S is hostname
+webauthn.registerPrompt=%S wants to register an account with one of your security tokens. You can connect and authorize one now, or cancel.
+# LOCALIZATION NOTE (webauthn.registerDirectPrompt):
+# %1$S is hostname. %2$S is brandShortName.
+# The website is asking for extended information about your
+# hardware authenticator that shouldn't be generally necessary. Permitting
+# this is safe if you only use one account at this website. If you have
+# multiple accounts at this website, and you use the same hardware
+# authenticator, then the website could link those accounts together.
+# And this is true even if you use a different profile / browser (or even Tor
+# Browser). To avoid this, you should use different hardware authenticators
+# for different accounts on this website.
+webauthn.registerDirectPrompt=%1$S is requesting extended information about your authenticator, which may affect your privacy.\n\n%2$S can anonymize this for you, but the website might decline this authenticator. If declined, you can try again.
+# LOCALIZATION NOTE (webauthn.signPrompt): %S is hostname
+webauthn.signPrompt=%S wants to authenticate you using a registered security token. You can connect and authorize one now, or cancel.
+webauthn.cancel=Cancel
+webauthn.cancel.accesskey=c
+webauthn.proceed=Proceed
+webauthn.proceed.accesskey=p
+webauthn.anonymize=Anonymize anyway
+
 # Spoof Accept-Language prompt
 privacy.spoof_english=Changing your language setting to English will make you more difficult to identify and enhance your privacy. Do you want to request English language versions of web pages?
 
 identity.identified.verifier=Verified by: %S
 identity.identified.verified_by_you=You have added a security exception for this site.
 identity.identified.state_and_country=%S, %S
 
 # LOCALIZATION NOTE (identity.notSecure.label):
--- a/dom/u2f/U2F.cpp
+++ b/dom/u2f/U2F.cpp
@@ -337,22 +337,24 @@ U2F::Register(const nsAString& aAppId,
 
   // Default values for U2F.
   WebAuthnAuthenticatorSelection authSelection(false /* requireResidentKey */,
                                                false /* requireUserVerification */,
                                                false /* requirePlatformAttachment */);
 
   uint32_t adjustedTimeoutMillis = AdjustedTimeoutMillis(opt_aTimeoutSeconds);
 
-  WebAuthnMakeCredentialInfo info(rpIdHash,
+  WebAuthnMakeCredentialInfo info(mOrigin,
+                                  rpIdHash,
                                   clientDataHash,
                                   adjustedTimeoutMillis,
                                   excludeList,
                                   extensions,
-                                  authSelection);
+                                  authSelection,
+                                  false /* RequestDirectAttestation */);
 
   MOZ_ASSERT(mTransaction.isNothing());
   mTransaction = Some(U2FTransaction(clientData, Move(AsVariant(callback))));
   mChild->SendRequestRegister(mTransaction.ref().mId, info);
 }
 
 void
 U2F::FinishMakeCredential(const uint64_t& aTransactionId,
@@ -478,17 +480,18 @@ U2F::Sign(const nsAString& aAppId,
 
   ListenForVisibilityEvents();
 
   // Always blank for U2F
   nsTArray<WebAuthnExtension> extensions;
 
   uint32_t adjustedTimeoutMillis = AdjustedTimeoutMillis(opt_aTimeoutSeconds);
 
-  WebAuthnGetAssertionInfo info(rpIdHash,
+  WebAuthnGetAssertionInfo info(mOrigin,
+                                rpIdHash,
                                 clientDataHash,
                                 adjustedTimeoutMillis,
                                 permittedList,
                                 false, /* requireUserVerification */
                                 extensions);
 
   MOZ_ASSERT(mTransaction.isNothing());
   mTransaction = Some(U2FTransaction(clientData, Move(AsVariant(callback))));
--- a/dom/webauthn/PWebAuthnTransaction.ipdl
+++ b/dom/webauthn/PWebAuthnTransaction.ipdl
@@ -42,29 +42,33 @@ struct WebAuthnExtensionResultAppId {
   bool AppId;
 };
 
 union WebAuthnExtensionResult {
   WebAuthnExtensionResultAppId;
 };
 
 struct WebAuthnMakeCredentialInfo {
+  nsString Origin;
   uint8_t[] RpIdHash;
   uint8_t[] ClientDataHash;
   uint32_t TimeoutMS;
   WebAuthnScopedCredential[] ExcludeList;
   WebAuthnExtension[] Extensions;
   WebAuthnAuthenticatorSelection AuthenticatorSelection;
+  bool RequestDirectAttestation;
 };
 
 struct WebAuthnMakeCredentialResult {
   uint8_t[] RegBuffer;
+  bool DirectAttestationPermitted;
 };
 
 struct WebAuthnGetAssertionInfo {
+  nsString Origin;
   uint8_t[] RpIdHash;
   uint8_t[] ClientDataHash;
   uint32_t TimeoutMS;
   WebAuthnScopedCredential[] AllowList;
   bool RequireUserVerification;
   WebAuthnExtension[] Extensions;
 };
 
--- a/dom/webauthn/U2FHIDTokenManager.cpp
+++ b/dom/webauthn/U2FHIDTokenManager.cpp
@@ -219,17 +219,19 @@ U2FHIDTokenManager::HandleRegisterResult
   MOZ_ASSERT(!mRegisterPromise.IsEmpty());
 
   nsTArray<uint8_t> registration;
   if (!aResult->CopyRegistration(registration)) {
     mRegisterPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
     return;
   }
 
-  WebAuthnMakeCredentialResult result(registration);
+  // Will be set by the U2FTokenManager.
+  bool directAttestationPermitted = false;
+  WebAuthnMakeCredentialResult result(registration, directAttestationPermitted);
   mRegisterPromise.Resolve(Move(result), __func__);
 }
 
 void
 U2FHIDTokenManager::HandleSignResult(UniquePtr<U2FResult>&& aResult)
 {
   mozilla::ipc::AssertIsOnBackgroundThread();
 
--- a/dom/webauthn/U2FSoftTokenManager.cpp
+++ b/dom/webauthn/U2FSoftTokenManager.cpp
@@ -683,17 +683,20 @@ U2FSoftTokenManager::Register(const WebA
   }
   registrationBuf.AppendElement(0x05, mozilla::fallible);
   registrationBuf.AppendSECItem(pubKey->u.ec.publicValue);
   registrationBuf.AppendElement(keyHandleItem->len, mozilla::fallible);
   registrationBuf.AppendSECItem(keyHandleItem.get());
   registrationBuf.AppendSECItem(attestCert.get()->derCert);
   registrationBuf.AppendSECItem(signatureItem);
 
-  WebAuthnMakeCredentialResult result((nsTArray<uint8_t>(registrationBuf)));
+  // Will be set by the U2FTokenManager.
+  bool directAttestationPermitted = false;
+  WebAuthnMakeCredentialResult result((nsTArray<uint8_t>(registrationBuf)),
+                                      directAttestationPermitted);
   return U2FRegisterPromise::CreateAndResolve(Move(result), __func__);
 }
 
 bool
 U2FSoftTokenManager::FindRegisteredKeyHandle(const nsTArray<nsTArray<uint8_t>>& aAppIds,
                                              const nsTArray<WebAuthnScopedCredential>& aCredentials,
                                              /*out*/ nsTArray<uint8_t>& aKeyHandle,
                                              /*out*/ nsTArray<uint8_t>& aAppId)
--- a/dom/webauthn/U2FTokenManager.cpp
+++ b/dom/webauthn/U2FTokenManager.cpp
@@ -6,45 +6,59 @@
 
 #include "mozilla/dom/U2FTokenManager.h"
 #include "mozilla/dom/U2FTokenTransport.h"
 #include "mozilla/dom/U2FHIDTokenManager.h"
 #include "mozilla/dom/U2FSoftTokenManager.h"
 #include "mozilla/dom/PWebAuthnTransactionParent.h"
 #include "mozilla/MozPromise.h"
 #include "mozilla/dom/WebAuthnUtil.h"
+#include "mozilla/ipc/BackgroundParent.h"
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/Unused.h"
 #include "hasht.h"
 #include "nsICryptoHash.h"
+#include "nsTextFormatter.h"
 #include "pkix/Input.h"
 #include "pkixutil.h"
 
 // Not named "security.webauth.u2f_softtoken_counter" because setting that
 // name causes the window.u2f object to disappear until preferences get
 // reloaded, as its pref is a substring!
 #define PREF_U2F_NSSTOKEN_COUNTER "security.webauth.softtoken_counter"
 #define PREF_WEBAUTHN_SOFTTOKEN_ENABLED "security.webauth.webauthn_enable_softtoken"
 #define PREF_WEBAUTHN_USBTOKEN_ENABLED "security.webauth.webauthn_enable_usbtoken"
+#define PREF_WEBAUTHN_ALLOW_DIRECT_ATTESTATION "security.webauth.webauthn_testing_allow_direct_attestation"
 
 namespace mozilla {
 namespace dom {
 
 /***********************************************************************
  * Statics
  **********************************************************************/
 
 class U2FPrefManager;
 
 namespace {
 static mozilla::LazyLogModule gU2FTokenManagerLog("u2fkeymanager");
 StaticRefPtr<U2FTokenManager> gU2FTokenManager;
 StaticRefPtr<U2FPrefManager> gPrefManager;
+static nsIThread* gBackgroundThread;
 }
 
+// Data for WebAuthn UI prompt notifications.
+static const char16_t kRegisterPromptNotifcation[] =
+  u"{\"action\":\"register\",\"tid\":%llu,\"origin\":\"%s\"}";
+static const char16_t kRegisterDirectPromptNotifcation[] =
+  u"{\"action\":\"register-direct\",\"tid\":%llu,\"origin\":\"%s\"}";
+static const char16_t kSignPromptNotifcation[] =
+  u"{\"action\":\"sign\",\"tid\":%llu,\"origin\":\"%s\"}";
+static const char16_t kCancelPromptNotifcation[] =
+  u"{\"action\":\"cancel\",\"tid\":%llu}";
+
 class U2FPrefManager final : public nsIObserver
 {
 private:
   U2FPrefManager() :
     mPrefMutex("U2FPrefManager Mutex")
   {
     UpdateValues();
   }
@@ -56,16 +70,17 @@ public:
   static U2FPrefManager* GetOrCreate()
   {
     MOZ_ASSERT(NS_IsMainThread());
     if (!gPrefManager) {
       gPrefManager = new U2FPrefManager();
       Preferences::AddStrongObserver(gPrefManager, PREF_WEBAUTHN_SOFTTOKEN_ENABLED);
       Preferences::AddStrongObserver(gPrefManager, PREF_U2F_NSSTOKEN_COUNTER);
       Preferences::AddStrongObserver(gPrefManager, PREF_WEBAUTHN_USBTOKEN_ENABLED);
+      Preferences::AddStrongObserver(gPrefManager, PREF_WEBAUTHN_ALLOW_DIRECT_ATTESTATION);
       ClearOnShutdown(&gPrefManager, ShutdownPhase::ShutdownThreads);
     }
     return gPrefManager;
   }
 
   static U2FPrefManager* Get()
   {
     return gPrefManager;
@@ -84,61 +99,66 @@ public:
   }
 
   bool GetUsbTokenEnabled()
   {
     MutexAutoLock lock(mPrefMutex);
     return mUsbTokenEnabled;
   }
 
+  bool GetAllowDirectAttestationForTesting()
+  {
+    MutexAutoLock lock(mPrefMutex);
+    return mAllowDirectAttestation;
+  }
+
   NS_IMETHODIMP
   Observe(nsISupports* aSubject,
           const char* aTopic,
           const char16_t* aData) override
   {
     UpdateValues();
     return NS_OK;
   }
 private:
   void UpdateValues() {
     MOZ_ASSERT(NS_IsMainThread());
     MutexAutoLock lock(mPrefMutex);
     mSoftTokenEnabled = Preferences::GetBool(PREF_WEBAUTHN_SOFTTOKEN_ENABLED);
     mSoftTokenCounter = Preferences::GetUint(PREF_U2F_NSSTOKEN_COUNTER);
     mUsbTokenEnabled = Preferences::GetBool(PREF_WEBAUTHN_USBTOKEN_ENABLED);
+    mAllowDirectAttestation = Preferences::GetBool(PREF_WEBAUTHN_ALLOW_DIRECT_ATTESTATION);
   }
 
   Mutex mPrefMutex;
   bool mSoftTokenEnabled;
   int mSoftTokenCounter;
   bool mUsbTokenEnabled;
+  bool mAllowDirectAttestation;
 };
 
 NS_IMPL_ISUPPORTS(U2FPrefManager, nsIObserver);
 
 /***********************************************************************
  * U2FManager Implementation
  **********************************************************************/
 
+NS_IMPL_ISUPPORTS(U2FTokenManager, nsIU2FTokenManager);
+
 U2FTokenManager::U2FTokenManager()
   : mTransactionParent(nullptr)
   , mLastTransactionId(0)
 {
   MOZ_ASSERT(XRE_IsParentProcess());
   // Create on the main thread to make sure ClearOnShutdown() works.
   MOZ_ASSERT(NS_IsMainThread());
   // Create the preference manager while we're initializing.
   U2FPrefManager::GetOrCreate();
 }
 
-U2FTokenManager::~U2FTokenManager()
-{
-  MOZ_ASSERT(NS_IsMainThread());
-}
-
 //static
 void
 U2FTokenManager::Initialize()
 {
   if (!XRE_IsParentProcess()) {
     return;
   }
   MOZ_ASSERT(NS_IsMainThread());
@@ -173,38 +193,85 @@ U2FTokenManager::MaybeClearTransaction(P
   if (mTransactionParent == aParent) {
     ClearTransaction();
   }
 }
 
 void
 U2FTokenManager::ClearTransaction()
 {
+  if (mLastTransactionId > 0) {
+    // Remove any prompts we might be showing for the current transaction.
+    SendPromptNotification(kCancelPromptNotifcation, mLastTransactionId);
+  }
+
   mTransactionParent = nullptr;
+
   // Drop managers at the end of all transactions
   if (mTokenManagerImpl) {
     mTokenManagerImpl->Drop();
     mTokenManagerImpl = nullptr;
   }
+
   // Forget promises, if necessary.
   mRegisterPromise.DisconnectIfExists();
   mSignPromise.DisconnectIfExists();
+
   // Clear transaction id.
   mLastTransactionId = 0;
+
+  // Forget any pending registration.
+  mPendingRegisterInfo.reset();
+}
+
+template<typename ...T> void
+U2FTokenManager::SendPromptNotification(const char16_t* aFormat, T... aArgs)
+{
+  mozilla::ipc::AssertIsOnBackgroundThread();
+
+  nsAutoString json;
+  nsTextFormatter::ssprintf(json, aFormat, aArgs...);
+
+  nsCOMPtr<nsIRunnable> r(NewRunnableMethod<nsString>(
+      "U2FTokenManager::RunSendPromptNotification", this,
+      &U2FTokenManager::RunSendPromptNotification, json));
+
+  MOZ_ALWAYS_SUCCEEDS(
+    GetMainThreadEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL));
+}
+
+void
+U2FTokenManager::RunSendPromptNotification(nsString aJSON)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+  if (NS_WARN_IF(!os)) {
+    return;
+  }
+
+  nsCOMPtr<nsIU2FTokenManager> self = do_QueryInterface(this);
+  MOZ_ALWAYS_SUCCEEDS(os->NotifyObservers(self, "webauthn-prompt", aJSON.get()));
 }
 
 RefPtr<U2FTokenTransport>
 U2FTokenManager::GetTokenManagerImpl()
 {
   MOZ_ASSERT(U2FPrefManager::Get());
+  mozilla::ipc::AssertIsOnBackgroundThread();
 
   if (mTokenManagerImpl) {
     return mTokenManagerImpl;
   }
 
+  if (!gBackgroundThread) {
+    gBackgroundThread = NS_GetCurrentThread();
+    MOZ_ASSERT(gBackgroundThread, "This should never be null!");
+  }
+
   auto pm = U2FPrefManager::Get();
 
   // 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 (pm->GetUsbTokenEnabled()) {
     return new U2FHIDTokenManager();
@@ -241,23 +308,60 @@ U2FTokenManager::Register(PWebAuthnTrans
   // UnknownError and terminate the operation.
 
   if ((aTransactionInfo.RpIdHash().Length() != SHA256_LENGTH) ||
       (aTransactionInfo.ClientDataHash().Length() != SHA256_LENGTH)) {
     AbortTransaction(aTransactionId, NS_ERROR_DOM_UNKNOWN_ERR);
     return;
   }
 
-  uint64_t tid = mLastTransactionId = aTransactionId;
+  mLastTransactionId = aTransactionId;
+
+  // If the RP request direct attestation, ask the user for permission and
+  // store the transaction info until the user proceeds or cancels.
+  // Might be overriden by a pref for testing purposes.
+  if (aTransactionInfo.RequestDirectAttestation() &&
+      !U2FPrefManager::Get()->GetAllowDirectAttestationForTesting()) {
+    NS_ConvertUTF16toUTF8 origin(aTransactionInfo.Origin());
+    SendPromptNotification(kRegisterDirectPromptNotifcation,
+                           aTransactionId,
+                           origin.get());
+
+    MOZ_ASSERT(mPendingRegisterInfo.isNothing());
+    mPendingRegisterInfo = Some(aTransactionInfo);
+  } else {
+    DoRegister(aTransactionInfo);
+  }
+}
+
+void
+U2FTokenManager::DoRegister(const WebAuthnMakeCredentialInfo& aInfo)
+{
+  mozilla::ipc::AssertIsOnBackgroundThread();
+  MOZ_ASSERT(mLastTransactionId > 0);
+
+  // Show a prompt that lets the user cancel the ongoing transaction.
+  NS_ConvertUTF16toUTF8 origin(aInfo.Origin());
+  SendPromptNotification(kRegisterPromptNotifcation,
+                         mLastTransactionId,
+                         origin.get());
+
+  uint64_t tid = mLastTransactionId;
   mozilla::TimeStamp startTime = mozilla::TimeStamp::Now();
+  bool requestDirectAttestation = aInfo.RequestDirectAttestation();
+
   mTokenManagerImpl
-    ->Register(aTransactionInfo)
+    ->Register(aInfo)
     ->Then(GetCurrentThreadSerialEventTarget(), __func__,
-          [tid, startTime](WebAuthnMakeCredentialResult&& aResult) {
+          [tid, startTime, requestDirectAttestation](WebAuthnMakeCredentialResult&& aResult) {
             U2FTokenManager* mgr = U2FTokenManager::Get();
+            // The token manager implementations set DirectAttestationPermitted
+            // to false by default. Override this here with information from
+            // the JS prompt.
+            aResult.DirectAttestationPermitted() = requestDirectAttestation;
             mgr->MaybeConfirmRegister(tid, aResult);
             Telemetry::ScalarAdd(
               Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
               NS_LITERAL_STRING("U2FRegisterFinish"), 1);
             Telemetry::AccumulateTimeDelta(
               Telemetry::WEBAUTHN_CREATE_CREDENTIAL_MS,
               startTime);
           },
@@ -309,18 +413,25 @@ U2FTokenManager::Sign(PWebAuthnTransacti
   }
 
   if ((aTransactionInfo.RpIdHash().Length() != SHA256_LENGTH) ||
       (aTransactionInfo.ClientDataHash().Length() != SHA256_LENGTH)) {
     AbortTransaction(aTransactionId, NS_ERROR_DOM_UNKNOWN_ERR);
     return;
   }
 
+  // Show a prompt that lets the user cancel the ongoing transaction.
+  NS_ConvertUTF16toUTF8 origin(aTransactionInfo.Origin());
+  SendPromptNotification(kSignPromptNotifcation,
+                         aTransactionId,
+                         origin.get());
+
   uint64_t tid = mLastTransactionId = aTransactionId;
   mozilla::TimeStamp startTime = mozilla::TimeStamp::Now();
+
   mTokenManagerImpl
     ->Sign(aTransactionInfo)
     ->Then(GetCurrentThreadSerialEventTarget(), __func__,
       [tid, startTime](WebAuthnGetAssertionResult&& aResult) {
         U2FTokenManager* mgr = U2FTokenManager::Get();
         mgr->MaybeConfirmSign(tid, aResult);
         Telemetry::ScalarAdd(
           Telemetry::ScalarID::SECURITY_WEBAUTHN_USED,
@@ -367,10 +478,87 @@ U2FTokenManager::Cancel(PWebAuthnTransac
   if (mTransactionParent != aParent || mLastTransactionId != aTransactionId) {
     return;
   }
 
   mTokenManagerImpl->Cancel();
   ClearTransaction();
 }
 
+// nsIU2FTokenManager
+
+NS_IMETHODIMP
+U2FTokenManager::ResumeRegister(uint64_t aTransactionId,
+                                bool aPermitDirectAttestation)
+{
+  MOZ_ASSERT(XRE_IsParentProcess());
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (!gBackgroundThread) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsCOMPtr<nsIRunnable> r(NewRunnableMethod<uint64_t, bool>(
+      "U2FTokenManager::RunResumeRegister", this,
+      &U2FTokenManager::RunResumeRegister, aTransactionId,
+      aPermitDirectAttestation));
+
+  return gBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
+}
+
+void
+U2FTokenManager::RunResumeRegister(uint64_t aTransactionId,
+                                   bool aPermitDirectAttestation)
+{
+  mozilla::ipc::AssertIsOnBackgroundThread();
+
+  if (NS_WARN_IF(mPendingRegisterInfo.isNothing())) {
+    return;
+  }
+
+  if (mLastTransactionId != aTransactionId) {
+    return;
+  }
+
+  // Forward whether the user opted into direct attestation.
+  mPendingRegisterInfo.ref().RequestDirectAttestation() =
+    aPermitDirectAttestation;
+
+  // Resume registration and cleanup.
+  DoRegister(mPendingRegisterInfo.ref());
+  mPendingRegisterInfo.reset();
+}
+
+NS_IMETHODIMP
+U2FTokenManager::Cancel(uint64_t aTransactionId)
+{
+  MOZ_ASSERT(XRE_IsParentProcess());
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (!gBackgroundThread) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsCOMPtr<nsIRunnable> r(NewRunnableMethod<uint64_t>(
+      "U2FTokenManager::RunCancel", this,
+      &U2FTokenManager::RunCancel, aTransactionId));
+
+  return gBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
+}
+
+void
+U2FTokenManager::RunCancel(uint64_t aTransactionId)
+{
+  mozilla::ipc::AssertIsOnBackgroundThread();
+
+  if (mLastTransactionId != aTransactionId) {
+    return;
+  }
+
+  // Cancel the request.
+  mTokenManagerImpl->Cancel();
+
+  // Reject the promise.
+  AbortTransaction(aTransactionId, NS_ERROR_DOM_ABORT_ERR);
+}
+
 }
 }
--- a/dom/webauthn/U2FTokenManager.h
+++ b/dom/webauthn/U2FTokenManager.h
@@ -2,16 +2,17 @@
 /* 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/. */
 
 #ifndef mozilla_dom_U2FTokenManager_h
 #define mozilla_dom_U2FTokenManager_h
 
+#include "nsIU2FTokenManager.h"
 #include "mozilla/dom/U2FTokenTransport.h"
 #include "mozilla/dom/PWebAuthnTransaction.h"
 
 /*
  * Parent process manager for U2F and WebAuthn API transactions. Handles process
  * transactions from all content processes, make sure only one transaction is
  * live at any time. Manages access to hardware and software based key systems.
  *
@@ -21,52 +22,67 @@
  */
 
 namespace mozilla {
 namespace dom {
 
 class U2FSoftTokenManager;
 class WebAuthnTransactionParent;
 
-class U2FTokenManager final
+class U2FTokenManager final : public nsIU2FTokenManager
 {
 public:
-  NS_INLINE_DECL_REFCOUNTING(U2FTokenManager)
+  NS_DECL_THREADSAFE_ISUPPORTS
+  NS_DECL_NSIU2FTOKENMANAGER
+
   static U2FTokenManager* Get();
   void Register(PWebAuthnTransactionParent* aTransactionParent,
                 const uint64_t& aTransactionId,
                 const WebAuthnMakeCredentialInfo& aTransactionInfo);
   void Sign(PWebAuthnTransactionParent* aTransactionParent,
             const uint64_t& aTransactionId,
             const WebAuthnGetAssertionInfo& aTransactionInfo);
   void Cancel(PWebAuthnTransactionParent* aTransactionParent,
               const uint64_t& aTransactionId);
   void MaybeClearTransaction(PWebAuthnTransactionParent* aParent);
   static void Initialize();
 private:
   U2FTokenManager();
-  ~U2FTokenManager();
+  ~U2FTokenManager() { }
   RefPtr<U2FTokenTransport> GetTokenManagerImpl();
   void AbortTransaction(const uint64_t& aTransactionId, const nsresult& aError);
   void ClearTransaction();
+  // Step two of "Register", kicking off the actual transaction.
+  void DoRegister(const WebAuthnMakeCredentialInfo& aInfo);
   void MaybeConfirmRegister(const uint64_t& aTransactionId,
                             const WebAuthnMakeCredentialResult& aResult);
   void MaybeAbortRegister(const uint64_t& aTransactionId, const nsresult& aError);
   void MaybeConfirmSign(const uint64_t& aTransactionId,
                         const WebAuthnGetAssertionResult& aResult);
   void MaybeAbortSign(const uint64_t& aTransactionId, const nsresult& aError);
+  // The main thread runnable function for "nsIU2FTokenManager.ResumeRegister".
+  void RunResumeRegister(uint64_t aTransactionId, bool aPermitDirectAttestation);
+  // The main thread runnable function for "nsIU2FTokenManager.Cancel".
+  void RunCancel(uint64_t aTransactionId);
+  // Sends a "webauthn-prompt" observer notification with the given data.
+  template<typename ...T>
+  void SendPromptNotification(const char16_t* aFormat, T... aArgs);
+  // The main thread runnable function for "SendPromptNotification".
+  void RunSendPromptNotification(nsString aJSON);
   // Using a raw pointer here, as the lifetime of the IPC object is managed by
   // the PBackground protocol code. This means we cannot be left holding an
   // invalid IPC protocol object after the transaction is finished.
   PWebAuthnTransactionParent* mTransactionParent;
   RefPtr<U2FTokenTransport> mTokenManagerImpl;
   MozPromiseRequestHolder<U2FRegisterPromise> mRegisterPromise;
   MozPromiseRequestHolder<U2FSignPromise> mSignPromise;
   // The last transaction id, non-zero if there's an active transaction. This
   // guards any cancel messages to ensure we don't cancel newer transactions
   // due to a stale message.
   uint64_t mLastTransactionId;
+  // Pending registration info while we wait for user input.
+  Maybe<WebAuthnMakeCredentialInfo> mPendingRegisterInfo;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_U2FTokenManager_h
--- a/dom/webauthn/WebAuthnManager.cpp
+++ b/dom/webauthn/WebAuthnManager.cpp
@@ -403,46 +403,29 @@ WebAuthnManager::MakeCredential(const Pu
   bool requireUserVerification =
     selection.mUserVerification == UserVerificationRequirement::Required;
 
   // Does the RP desire direct attestation? Indirect attestation is not
   // implemented, and thus is equivilent to None.
   bool requestDirectAttestation =
     attestation == AttestationConveyancePreference::Direct;
 
-  // XXX Bug 1430150. Need something that allows direct attestation
-  // for tests until we implement a permission dialog we can click.
-  if (requestDirectAttestation) {
-    nsresult rv;
-    nsCOMPtr<nsIPrefService> prefService = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
-
-    if (NS_SUCCEEDED(rv)) {
-      nsCOMPtr<nsIPrefBranch> branch;
-      rv = prefService->GetBranch("security.webauth.", getter_AddRefs(branch));
-
-      if (NS_SUCCEEDED(rv)) {
-        rv = branch->GetBoolPref("webauthn_testing_allow_direct_attestation",
-                                 &requestDirectAttestation);
-      }
-    }
-
-    requestDirectAttestation &= NS_SUCCEEDED(rv);
-  }
-
   // Create and forward authenticator selection criteria.
   WebAuthnAuthenticatorSelection authSelection(selection.mRequireResidentKey,
                                                requireUserVerification,
                                                requirePlatformAttachment);
 
-  WebAuthnMakeCredentialInfo info(rpIdHash,
+  WebAuthnMakeCredentialInfo info(origin,
+                                  rpIdHash,
                                   clientDataHash,
                                   adjustedTimeout,
                                   excludeList,
                                   extensions,
-                                  authSelection);
+                                  authSelection,
+                                  requestDirectAttestation);
 
   ListenForVisibilityEvents();
 
   AbortSignal* signal = nullptr;
   if (aSignal.WasPassed()) {
     signal = &aSignal.Value();
     Follow(signal);
   }
@@ -450,16 +433,17 @@ WebAuthnManager::MakeCredential(const Pu
   MOZ_ASSERT(mTransaction.isNothing());
   mTransaction = Some(WebAuthnTransaction(promise,
                                           rpIdHash,
                                           clientDataJSON,
                                           requestDirectAttestation,
                                           signal));
 
   mChild->SendRequestRegister(mTransaction.ref().mId, info);
+
   return promise.forget();
 }
 
 already_AddRefed<Promise>
 WebAuthnManager::GetAssertion(const PublicKeyCredentialRequestOptions& aOptions,
                               const Optional<OwningNonNull<AbortSignal>>& aSignal)
 {
   MOZ_ASSERT(NS_IsMainThread());
@@ -633,17 +617,18 @@ WebAuthnManager::GetAssertion(const Publ
       promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
       return promise.forget();
     }
 
     // Append the hash and send it to the backend.
     extensions.AppendElement(WebAuthnExtensionAppId(appIdHash));
   }
 
-  WebAuthnGetAssertionInfo info(rpIdHash,
+  WebAuthnGetAssertionInfo info(origin,
+                                rpIdHash,
                                 clientDataHash,
                                 adjustedTimeout,
                                 allowList,
                                 requireUserVerification,
                                 extensions);
 
   ListenForVisibilityEvents();
 
@@ -780,20 +765,20 @@ WebAuthnManager::FinishMakeCredential(co
   mozilla::dom::CryptoBuffer authDataBuf;
   rv = AssembleAuthenticatorData(rpIdHashBuf, FLAG_TUP, counterBuf, attDataBuf,
                                  authDataBuf);
   if (NS_FAILED(rv)) {
     RejectTransaction(rv);
     return;
   }
 
-  // Direct attestation might have been requested by the RP. mDirectAttestation
-  // will be true only if the user consented via the permission UI.
+  // Direct attestation might have been requested by the RP. This will
+  // be true only if the user consented via the permission UI.
   CryptoBuffer attObj;
-  if (mTransaction.ref().mDirectAttestation) {
+  if (aResult.DirectAttestationPermitted()) {
     rv = CBOREncodeFidoU2FAttestationObj(authDataBuf, attestationCertBuf,
                                          signatureBuf, attObj);
   } else {
     rv = CBOREncodeNoneAttestationObj(authDataBuf, attObj);
   }
 
   if (NS_FAILED(rv)) {
     RejectTransaction(rv);
--- a/dom/webauthn/moz.build
+++ b/dom/webauthn/moz.build
@@ -6,16 +6,22 @@
 
 with Files("**"):
     BUG_COMPONENT = ("Core", "DOM: Device Interfaces")
 
 IPDL_SOURCES += [
     'PWebAuthnTransaction.ipdl'
 ]
 
+XPIDL_SOURCES += [
+    'nsIU2FTokenManager.idl'
+]
+
+XPIDL_MODULE = 'dom_webauthn'
+
 EXPORTS.mozilla.dom += [
     'AuthenticatorAssertionResponse.h',
     'AuthenticatorAttestationResponse.h',
     'AuthenticatorResponse.h',
     'PublicKeyCredential.h',
     'U2FHIDTokenManager.h',
     'U2FSoftTokenManager.h',
     'U2FTokenManager.h',
new file mode 100644
--- /dev/null
+++ b/dom/webauthn/nsIU2FTokenManager.idl
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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 "nsISupports.idl"
+
+/**
+ * nsIU2FTokenManager
+ *
+ * An interface to the U2FTokenManager singleton.
+ *
+ * This should be used only by the WebAuthn browser UI prompts.
+ */
+
+[scriptable, uuid(745e1eac-e449-4342-bca1-ee0e6ead09fc)]
+interface nsIU2FTokenManager : nsISupports
+{
+    /**
+     * Resumes the current WebAuthn/U2F transaction if that matches the given
+     * transaction ID. This is used only when direct attestation was requested
+     * and we have to wait for user input to proceed.
+     *
+     * @param aTransactionID : The ID of the transaction to resume.
+     * @param aPermitDirectAttestation : Whether direct attestation was
+     *                                   permitted by the user.
+     */
+    void resumeRegister(in uint64_t aTransactionID,
+                        in bool aPermitDirectAttestation);
+
+    /**
+     * Cancels the current WebAuthn/U2F transaction if that matches the given
+     * transaction ID.
+     *
+     * @param aTransactionID : The ID of the transaction to cancel.
+     */
+    void cancel(in uint64_t aTransactionID);
+};
--- a/dom/webauthn/tests/browser/browser.ini
+++ b/dom/webauthn/tests/browser/browser.ini
@@ -1,13 +1,14 @@
 [DEFAULT]
 support-files =
   head.js
   tab_webauthn_result.html
   tab_webauthn_success.html
-  ../cbor/*
   ../pkijs/*
+  ../cbor.js
   ../u2futil.js
 skip-if = !e10s
 
 [browser_abort_visibility.js]
 [browser_fido_appid_extension.js]
+[browser_webauthn_prompts.js]
 [browser_webauthn_telemetry.js]
new file mode 100644
--- /dev/null
+++ b/dom/webauthn/tests/browser/browser_webauthn_prompts.js
@@ -0,0 +1,215 @@
+/* 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 strict";
+
+const TEST_URL = "https://example.com/";
+
+function promiseNotification(id) {
+  return new Promise(resolve => {
+    PopupNotifications.panel.addEventListener("popupshown", function shown() {
+      let notification = PopupNotifications.getNotification(id);
+      if (notification) {
+        ok(true, `${id} prompt visible`);
+        PopupNotifications.panel.removeEventListener("popupshown", shown);
+        resolve();
+      }
+    });
+  });
+}
+
+function arrivingHereIsBad(aResult) {
+  ok(false, "Bad result! Received a: " + aResult);
+}
+
+function expectAbortError(aResult) {
+  let expected = "AbortError";
+  is(aResult.slice(0, expected.length), expected, `Expecting a ${expected}`);
+}
+
+function verifyAnonymizedCertificate(attestationObject) {
+  return webAuthnDecodeCBORAttestation(attestationObject)
+    .then(({fmt, attStmt}) => {
+      is("none", fmt, "Is a None Attestation");
+      is("object", typeof(attStmt), "attStmt is a map");
+      is(0, Object.keys(attStmt).length, "attStmt is empty");
+    });
+}
+
+function verifyDirectCertificate(attestationObject) {
+  return webAuthnDecodeCBORAttestation(attestationObject)
+    .then(({fmt, attStmt}) => {
+      is("fido-u2f", fmt, "Is a FIDO U2F Attestation");
+      is("object", typeof(attStmt), "attStmt is a map");
+      ok(attStmt.hasOwnProperty("x5c"), "attStmt.x5c exists");
+      ok(attStmt.hasOwnProperty("sig"), "attStmt.sig exists");
+    });
+}
+
+function promiseWebAuthnRegister(tab, attestation = "indirect") {
+  return ContentTask.spawn(tab.linkedBrowser, [attestation],
+    ([attestation]) => {
+      const cose_alg_ECDSA_w_SHA256 = -7;
+
+      let challenge = content.crypto.getRandomValues(new Uint8Array(16));
+
+      let pubKeyCredParams = [{
+        type: "public-key",
+        alg: cose_alg_ECDSA_w_SHA256
+      }];
+
+      let publicKey = {
+        rp: {id: content.document.domain, name: "none", icon: "none"},
+        user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"},
+        pubKeyCredParams,
+        attestation,
+        challenge
+      };
+
+      return content.navigator.credentials.create({publicKey})
+        .then(cred => cred.response.attestationObject);
+    });
+}
+
+function promiseWebAuthnSign(tab) {
+  return ContentTask.spawn(tab.linkedBrowser, [], () => {
+    let challenge = content.crypto.getRandomValues(new Uint8Array(16));
+    let key_handle = content.crypto.getRandomValues(new Uint8Array(16));
+
+    let credential = {
+      id: key_handle,
+      type: "public-key",
+      transports: ["usb"]
+    };
+
+    let publicKey = {
+      challenge,
+      rpId: content.document.domain,
+      allowCredentials: [credential],
+    };
+
+    return content.navigator.credentials.get({publicKey});
+  });
+}
+
+add_task(async function test_setup_usbtoken() {
+  await SpecialPowers.pushPrefEnv({
+    "set": [
+      ["security.webauth.u2f", false],
+      ["security.webauth.webauthn", true],
+      ["security.webauth.webauthn_enable_softtoken", false],
+      ["security.webauth.webauthn_enable_usbtoken", true]
+    ]
+  });
+});
+
+add_task(async function test_register() {
+  // Open a new tab.
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+  // Request a new credential and wait for the prompt.
+  let active = true;
+  let request = promiseWebAuthnRegister(tab)
+    .then(arrivingHereIsBad)
+    .catch(expectAbortError)
+    .then(() => active = false);
+  await promiseNotification("webauthn-prompt-register");
+
+  // Cancel the request.
+  ok(active, "request should still be active");
+  PopupNotifications.panel.firstChild.button.click();
+  await request;
+
+  // Close tab.
+  await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_sign() {
+  // Open a new tab.
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+  // Request a new assertion and wait for the prompt.
+  let active = true;
+  let request = promiseWebAuthnSign(tab)
+    .then(arrivingHereIsBad)
+    .catch(expectAbortError)
+    .then(() => active = false);
+  await promiseNotification("webauthn-prompt-sign");
+
+  // Cancel the request.
+  ok(active, "request should still be active");
+  PopupNotifications.panel.firstChild.button.click();
+  await request;
+
+  // Close tab.
+  await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_register_direct_cancel() {
+  // Open a new tab.
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+  // Request a new credential with direct attestation and wait for the prompt.
+  let active = true;
+  let promise = promiseWebAuthnRegister(tab, "direct")
+    .then(arrivingHereIsBad).catch(expectAbortError)
+    .then(() => active = false);
+  await promiseNotification("webauthn-prompt-register-direct");
+
+  // Cancel the request.
+  ok(active, "request should still be active");
+  PopupNotifications.panel.firstChild.secondaryButton.click();
+  await promise;
+
+  // Close tab.
+  await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_setup_softtoken() {
+  await SpecialPowers.pushPrefEnv({
+    "set": [
+      ["security.webauth.u2f", false],
+      ["security.webauth.webauthn", true],
+      ["security.webauth.webauthn_enable_softtoken", true],
+      ["security.webauth.webauthn_enable_usbtoken", false]
+    ]
+  })
+});
+
+add_task(async function test_register_direct_proceed() {
+  // Open a new tab.
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+  // Request a new credential with direct attestation and wait for the prompt.
+  let request = promiseWebAuthnRegister(tab, "direct");
+  await promiseNotification("webauthn-prompt-register-direct");
+
+  // Proceed.
+  PopupNotifications.panel.firstChild.button.click();
+
+  // Ensure we got "direct" attestation.
+  await request.then(verifyDirectCertificate);
+
+  // Close tab.
+  await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_register_direct_proceed_anon() {
+  // Open a new tab.
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+  // Request a new credential with direct attestation and wait for the prompt.
+  let request = promiseWebAuthnRegister(tab, "direct");
+  await promiseNotification("webauthn-prompt-register-direct");
+
+  // Check "anonymize anyway" and proceed.
+  PopupNotifications.panel.firstChild.checkbox.checked = true;
+  PopupNotifications.panel.firstChild.button.click();
+
+  // Ensure we got "none" attestation.
+  await request.then(verifyAnonymizedCertificate);
+
+  // Close tab.
+  await BrowserTestUtils.removeTab(tab);
+});
--- a/dom/webauthn/tests/browser/browser_webauthn_telemetry.js
+++ b/dom/webauthn/tests/browser/browser_webauthn_telemetry.js
@@ -52,29 +52,34 @@ async function executeTestPage(aUri) {
   } catch(e) {
     ok(false, "Exception thrown executing test page: " + e);
   } finally {
     // Remove all the extra windows and tabs.
     return BrowserTestUtils.removeTab(gBrowser.selectedTab);
   }
 }
 
+add_task(async function test_setup() {
+  await SpecialPowers.pushPrefEnv({
+    "set": [
+      ["security.webauth.webauthn", true],
+      ["security.webauth.webauthn_enable_softtoken", true],
+      ["security.webauth.webauthn_enable_usbtoken", false],
+      ["security.webauth.webauthn_testing_allow_direct_attestation", true]
+    ]
+  });
+});
+
 add_task(async function test_loopback() {
   // These tests can't run simultaneously as the preference changes will race.
   // So let's run them sequentially here, but in an async function so we can
   // use await.
   const testPage = "https://example.com/browser/dom/webauthn/tests/browser/tab_webauthn_success.html";
   {
     cleanupTelemetry();
-    // Enable the soft token, and execute a simple end-to-end test
-    Services.prefs.setBoolPref("security.webauth.webauthn", true);
-    Services.prefs.setBoolPref("security.webauth.webauthn_enable_softtoken", true);
-    Services.prefs.setBoolPref("security.webauth.webauthn_enable_usbtoken", false);
-    Services.prefs.setBoolPref("security.webauth.webauthn_testing_allow_direct_attestation", true);
-
     await executeTestPage(testPage);
 
     let webauthn_used = getTelemetryForScalar("security.webauthn_used");
     ok(webauthn_used, "Scalar keys are set: " + Object.keys(webauthn_used).join(", "));
     is(webauthn_used["U2FRegisterFinish"], 1, "webauthn_used U2FRegisterFinish scalar should be 1");
     is(webauthn_used["U2FSignFinish"], 1, "webauthn_used U2FSignFinish scalar should be 1");
     is(webauthn_used["U2FSignAbort"], undefined, "webauthn_used U2FSignAbort scalar must be unset");
     is(webauthn_used["U2FRegisterAbort"], undefined, "webauthn_used U2FRegisterAbort scalar must be unset");
--- a/dom/webauthn/tests/browser/head.js
+++ b/dom/webauthn/tests/browser/head.js
@@ -1,56 +1,20 @@
 /* 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 strict";
 
-function bytesToBase64(u8a){
-  let CHUNK_SZ = 0x8000;
-  let c = [];
-  for (let i = 0; i < u8a.length; i += CHUNK_SZ) {
-    c.push(String.fromCharCode.apply(null, u8a.subarray(i, i + CHUNK_SZ)));
-  }
-  return window.btoa(c.join(""));
-}
-
-function bytesToBase64UrlSafe(buf) {
-  return bytesToBase64(buf)
-                 .replace(/\+/g, "-")
-                 .replace(/\//g, "_")
-                 .replace(/=/g, "");
-}
-
-function base64ToBytes(b64encoded) {
-  return new Uint8Array(window.atob(b64encoded).split("").map(function(c) {
-    return c.charCodeAt(0);
-  }));
-}
-
-function base64ToBytesUrlSafe(str) {
-  if (!str || str.length % 4 == 1) {
-    throw "Improper b64 string";
-  }
-
-  var b64 = str.replace(/\-/g, "+").replace(/\_/g, "/");
-  while (b64.length % 4 != 0) {
-    b64 += "=";
-  }
-  return base64ToBytes(b64);
-}
-
-function buffer2string(buf) {
-  let str = "";
-  if (!(buf.constructor === Uint8Array)) {
-    buf = new Uint8Array(buf);
-  }
-  buf.map(function(x){ return str += String.fromCharCode(x) });
-  return str;
-}
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/dom/webauthn/tests/browser/cbor.js",
+  this);
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/dom/webauthn/tests/browser/u2futil.js",
+  this);
 
 function memcmp(x, y) {
   let xb = new Uint8Array(x);
   let yb = new Uint8Array(y);
 
   if (x.byteLength != y.byteLength) {
     return false;
   }
--- a/dom/webauthn/tests/browser/tab_webauthn_success.html
+++ b/dom/webauthn/tests/browser/tab_webauthn_success.html
@@ -2,17 +2,17 @@
 <meta charset=utf-8>
 <head>
   <title>Full-run test for MakeCredential/GetAssertion for W3C Web Authentication</title>
   <script type="text/javascript" src="u2futil.js"></script>
   <script type="text/javascript" src="../pkijs/common.js"></script>
   <script type="text/javascript" src="../pkijs/asn1.js"></script>
   <script type="text/javascript" src="../pkijs/x509_schema.js"></script>
   <script type="text/javascript" src="../pkijs/x509_simpl.js"></script>
-  <script type="text/javascript" src="../cbor/cbor.js"></script>
+  <script type="text/javascript" src="cbor.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 
 <h1>Full-run test for MakeCredential/GetAssertion for W3C Web Authentication</h1>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1265472">Mozilla Bug 1265472</a>
 
 <script class="testbody" type="text/javascript">
rename from dom/webauthn/tests/cbor/cbor.js
rename to dom/webauthn/tests/cbor.js
--- a/dom/webauthn/tests/mochitest.ini
+++ b/dom/webauthn/tests/mochitest.ini
@@ -1,13 +1,13 @@
 [DEFAULT]
 support-files =
-  cbor/*
+  cbor.js
+  u2futil.js
   pkijs/*
-  u2futil.js
 skip-if = !e10s
 scheme = https
 
 [test_webauthn_abort_signal.html]
 [test_webauthn_attestation_conveyance.html]
 [test_webauthn_authenticator_selection.html]
 [test_webauthn_authenticator_transports.html]
 [test_webauthn_loopback.html]
--- a/dom/webauthn/tests/test_webauthn_attestation_conveyance.html
+++ b/dom/webauthn/tests/test_webauthn_attestation_conveyance.html
@@ -4,17 +4,17 @@
   <title>W3C Web Authentication - Attestation Conveyance</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
   <script type="text/javascript" src="u2futil.js"></script>
   <script type="text/javascript" src="pkijs/common.js"></script>
   <script type="text/javascript" src="pkijs/asn1.js"></script>
   <script type="text/javascript" src="pkijs/x509_schema.js"></script>
   <script type="text/javascript" src="pkijs/x509_simpl.js"></script>
-  <script type="text/javascript" src="cbor/cbor.js"></script>
+  <script type="text/javascript" src="cbor.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 
   <h1>W3C Web Authentication - Attestation Conveyance</h1>
   <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1428916">Mozilla Bug 1428916</a>
   <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1416056">Mozilla Bug 1416056</a>
 
--- a/dom/webauthn/tests/test_webauthn_loopback.html
+++ b/dom/webauthn/tests/test_webauthn_loopback.html
@@ -3,17 +3,17 @@
 <head>
   <title>Full-run test for MakeCredential/GetAssertion for W3C Web Authentication</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="u2futil.js"></script>
   <script type="text/javascript" src="pkijs/common.js"></script>
   <script type="text/javascript" src="pkijs/asn1.js"></script>
   <script type="text/javascript" src="pkijs/x509_schema.js"></script>
   <script type="text/javascript" src="pkijs/x509_simpl.js"></script>
-  <script type="text/javascript" src="cbor/cbor.js"></script>
+  <script type="text/javascript" src="cbor.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 
 <h1>Full-run test for MakeCredential/GetAssertion for W3C Web Authentication</h1>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a>
 
 <script class="testbody" type="text/javascript">
--- a/dom/webauthn/tests/u2futil.js
+++ b/dom/webauthn/tests/u2futil.js
@@ -197,17 +197,17 @@ function webAuthnDecodeAuthDataArray(aAu
   let attData = {};
   attData.aaguid = aAuthData.slice(37, 53);
   attData.credIdLen = (aAuthData[53] << 8) + aAuthData[54];
   attData.credId = aAuthData.slice(55, 55 + attData.credIdLen);
 
   console.log(":: Authenticator Data ::");
   console.log("AAGUID: " + hexEncode(attData.aaguid));
 
-  cborPubKey = aAuthData.slice(55 + attData.credIdLen);
+  let cborPubKey = aAuthData.slice(55 + attData.credIdLen);
   var pubkeyObj = CBOR.decode(cborPubKey.buffer);
   if (!(cose_kty in pubkeyObj && cose_alg in pubkeyObj && cose_crv in pubkeyObj
         && cose_crv_x in pubkeyObj && cose_crv_y in pubkeyObj)) {
     throw "Invalid CBOR Public Key Object";
   }
   if (pubkeyObj[cose_kty] != cose_kty_ec2) {
     throw "Unexpected key type";
   }
--- a/toolkit/content/widgets/notification.xml
+++ b/toolkit/content/widgets/notification.xml
@@ -507,21 +507,22 @@
                    xbl:inherits="popupid,src=icon,class=iconclass"/>
         <xul:vbox flex="1" pack="start"
                   class="popup-notification-body" xbl:inherits="popupid">
           <xul:hbox align="start">
             <xul:vbox flex="1">
               <xul:label class="popup-notification-origin header"
                          xbl:inherits="value=origin,tooltiptext=origin"
                          crop="center"/>
-              <xul:description class="popup-notification-description"
-                               xbl:inherits="popupid">
-                <!-- These need to be on the same line to avoid creating whitespace between them (whitespace is added in the localization file, if necessary). -->
-                <html:span xbl:inherits="xbl:text=label,popupid"/><html:b xbl:inherits="xbl:text=name,popupid"/><html:span xbl:inherits="xbl:text=endlabel,popupid"/>
-              </xul:description>
+              <!-- These need to be on the same line to avoid creating
+                   whitespace between them (whitespace is added in the
+                   localization file, if necessary). -->
+              <xul:description class="popup-notification-description" xbl:inherits="popupid"><html:span
+                xbl:inherits="xbl:text=label,popupid"/><html:b xbl:inherits="xbl:text=name,popupid"/><html:span
+              xbl:inherits="xbl:text=endlabel,popupid"/></xul:description>
             </xul:vbox>
             <xul:toolbarbutton anonid="closebutton"
                                class="messageCloseButton close-icon popup-notification-closebutton tabbable"
                                xbl:inherits="oncommand=closebuttoncommand,hidden=closebuttonhidden"
                                tooltiptext="&closeNotification.tooltip;"/>
           </xul:hbox>
           <children includes="popupnotificationcontent"/>
           <xul:label class="text-link popup-notification-learnmore-link"