Bug 1391438 - Support FIDO2 for WebAuthn on Android r=snorp,keeler
authorJ.C. Jones <jjones@mozilla.com>
Fri, 10 May 2019 16:40:17 +0000
changeset 532267 d8e0bfeb5fa39545f737a6979c94a210d947ab22
parent 532266 192ba11153b4c29512a9722caf8260292601e6aa
child 532268 c6dbde54689d42c29e2ff47fee4972bbd2433840
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp, keeler
bugs1391438, 1550625
milestone68.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 1391438 - Support FIDO2 for WebAuthn on Android r=snorp,keeler Support using the Google Play-provided FIDO2 API for Web Authentication. FIDO U2F API support is being handled subsequently in Bug 1550625. This patch uses the privileged APIs and thus will only work on Fennec Nightly, Beta, and Release builds. Differential Revision: https://phabricator.services.mozilla.com/D1148
build.gradle
dom/webauthn/AndroidWebAuthnTokenManager.cpp
dom/webauthn/AndroidWebAuthnTokenManager.h
dom/webauthn/U2FTokenManager.cpp
dom/webauthn/moz.build
mobile/android/app/build.gradle
mobile/android/base/java/org/mozilla/gecko/ActivityHandlerHelper.java
mobile/android/base/java/org/mozilla/gecko/util/WebAuthnUtils.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/WebAuthnTokenManager.java
security/manager/ssl/security-prefs.js
widget/android/WebAuthnTokenManager.cpp
widget/android/moz.build
widget/android/nsAppShell.cpp
--- a/build.gradle
+++ b/build.gradle
@@ -74,16 +74,17 @@ buildscript {
     ext.kotlin_version = '1.2.41'
     ext.support_library_version = '28.0.0'
     ext.jacoco_version = '0.8.1'
     ext.lifecycle_library_version = '1.1.1'
 
     if (gradle.mozconfig.substs.MOZ_ANDROID_GOOGLE_PLAY_SERVICES) {
         ext.google_play_services_version = '15.0.1'
         ext.google_play_services_cast_version = '16.0.0'
+        ext.google_play_services_fido_version = '17.0.0'
     }
 
     dependencies {
         classpath 'org.mozilla.apilint:apilint:0.2.1'
         classpath 'com.android.tools.build:gradle:3.1.4'
         classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.8.2'
         classpath 'org.apache.commons:commons-exec:1.3'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
new file mode 100644
--- /dev/null
+++ b/dom/webauthn/AndroidWebAuthnTokenManager.cpp
@@ -0,0 +1,330 @@
+/* -*- 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 "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/jni/GeckoBundleUtils.h"
+#include "mozilla/StaticPtr.h"
+
+#include "AndroidWebAuthnTokenManager.h"
+#include "GeneratedJNIWrappers.h"
+#include "JavaBuiltins.h"
+
+namespace mozilla {
+namespace dom {
+
+static nsIThread* gAndroidPBackgroundThread;
+
+StaticRefPtr<AndroidWebAuthnTokenManager> gAndroidWebAuthnManager;
+
+/* static */ AndroidWebAuthnTokenManager*
+AndroidWebAuthnTokenManager::GetInstance() {
+  if (!gAndroidWebAuthnManager) {
+    mozilla::ipc::AssertIsOnBackgroundThread();
+    gAndroidWebAuthnManager = new AndroidWebAuthnTokenManager();
+  }
+  return gAndroidWebAuthnManager;
+}
+
+AndroidWebAuthnTokenManager::AndroidWebAuthnTokenManager() {
+  mozilla::ipc::AssertIsOnBackgroundThread();
+  MOZ_ASSERT(XRE_IsParentProcess());
+  MOZ_ASSERT(!gAndroidWebAuthnManager);
+
+  gAndroidPBackgroundThread = NS_GetCurrentThread();
+  MOZ_ASSERT(gAndroidPBackgroundThread, "This should never be null!");
+  gAndroidWebAuthnManager = this;
+}
+
+void AndroidWebAuthnTokenManager::AssertIsOnOwningThread() const {
+  mozilla::ipc::AssertIsOnBackgroundThread();
+  MOZ_ASSERT(gAndroidPBackgroundThread);
+#ifdef DEBUG
+  bool current;
+  MOZ_ASSERT(
+      NS_SUCCEEDED(gAndroidPBackgroundThread->IsOnCurrentThread(&current)));
+  MOZ_ASSERT(current);
+#endif
+}
+
+void AndroidWebAuthnTokenManager::Drop() {
+  AssertIsOnOwningThread();
+
+  ClearPromises();
+  gAndroidWebAuthnManager = nullptr;
+  gAndroidPBackgroundThread = nullptr;
+}
+
+RefPtr<U2FRegisterPromise> AndroidWebAuthnTokenManager::Register(
+    const WebAuthnMakeCredentialInfo& aInfo, bool aForceNoneAttestation) {
+  AssertIsOnOwningThread();
+
+  if (aInfo.Extra().isNothing()) {
+    // Mostly ready for U2F, but requires using a different provider
+    // at the Java-side. Finish out in Bug 1550625
+    return U2FRegisterPromise::CreateAndReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR,
+                                               __func__);
+  }
+
+  ClearPromises();
+
+  GetMainThreadEventTarget()->Dispatch(NS_NewRunnableFunction(
+      "java::WebAuthnTokenManager::WebAuthnMakeCredential",
+      [aInfo, aForceNoneAttestation]() {
+        AssertIsOnMainThread();
+
+        // Produce the credential exclusion list
+        jni::ObjectArray::LocalRef idList =
+            jni::ObjectArray::New(aInfo.ExcludeList().Length());
+
+        nsTArray<uint8_t> transportBuf;
+        int ix = 0;
+
+        for (const WebAuthnScopedCredential& cred : aInfo.ExcludeList()) {
+          jni::ByteBuffer::LocalRef id = jni::ByteBuffer::New(
+              const_cast<void*>(static_cast<const void*>(cred.id().Elements())),
+              cred.id().Length());
+
+          idList->SetElement(ix, id);
+          transportBuf.AppendElement(cred.transports());
+
+          ix += 1;
+        }
+
+        jni::ByteBuffer::LocalRef transportList = jni::ByteBuffer::New(
+            const_cast<void*>(
+                static_cast<const void*>(transportBuf.Elements())),
+            transportBuf.Length());
+
+        const nsTArray<uint8_t>& challBuf = aInfo.Challenge();
+        jni::ByteBuffer::LocalRef challenge = jni::ByteBuffer::New(
+            const_cast<void*>(static_cast<const void*>(challBuf.Elements())),
+            challBuf.Length());
+
+        nsTArray<uint8_t> uidBuf;
+
+        // Get authenticator selection criteria
+        GECKOBUNDLE_START(authSelBundle);
+        GECKOBUNDLE_START(extensionsBundle);
+        GECKOBUNDLE_START(identifierBundle);
+
+        if (aInfo.Extra().isSome()) {
+          const auto& extra = aInfo.Extra().ref();
+          const auto& rp = extra.Rp();
+          const auto& user = extra.User();
+
+          // If we have extra data, then this is WebAuthn, not U2F
+          GECKOBUNDLE_PUT(identifierBundle, "isWebAuthn",
+                          java::sdk::Integer::ValueOf(1));
+
+          // Get the attestation preference and override if the user asked
+          AttestationConveyancePreference attestation =
+              extra.attestationConveyancePreference();
+
+          if (aForceNoneAttestation) {
+            // Add UI support to trigger this, bug 1550164
+            attestation = AttestationConveyancePreference::None;
+          }
+
+          if (static_cast<uint32_t>(attestation) <
+              static_cast<uint32_t>(
+                  AttestationConveyancePreference::EndGuard_)) {
+            // Protect the direct array reference below. Note this is still a
+            // ContiguousEnumValidator type.
+            nsString attestPref;
+            const EnumEntry& attPrefEntry =
+                AttestationConveyancePreferenceValues::strings
+                    [static_cast<uint32_t>(attestation)];
+            attestPref.AssignASCII(attPrefEntry.value, attPrefEntry.length);
+            GECKOBUNDLE_PUT(authSelBundle, "attestationPreference",
+                            jni::StringParam(attestPref));
+          }
+
+          const WebAuthnAuthenticatorSelection& sel =
+              extra.AuthenticatorSelection();
+          if (sel.requireResidentKey()) {
+            GECKOBUNDLE_PUT(authSelBundle, "requireResidentKey",
+                            java::sdk::Integer::ValueOf(1));
+          }
+
+          if (sel.userVerificationRequirement() ==
+              UserVerificationRequirement::Required) {
+            GECKOBUNDLE_PUT(authSelBundle, "requireUserVerification",
+                            java::sdk::Integer::ValueOf(1));
+          }
+
+          if (sel.authenticatorAttachment().isSome()) {
+            const AuthenticatorAttachment authenticatorAttachment =
+                sel.authenticatorAttachment().value();
+            if (authenticatorAttachment == AuthenticatorAttachment::Platform) {
+              GECKOBUNDLE_PUT(authSelBundle, "requirePlatformAttachment",
+                              java::sdk::Integer::ValueOf(1));
+            }
+          }
+
+          // Get extensions
+          for (const WebAuthnExtension& ext : extra.Extensions()) {
+            if (ext.type() == WebAuthnExtension::TWebAuthnExtensionAppId) {
+              GECKOBUNDLE_PUT(
+                  extensionsBundle, "fidoAppId",
+                  jni::StringParam(
+                      ext.get_WebAuthnExtensionAppId().appIdentifier()));
+            }
+          }
+
+          uidBuf.Assign(user.Id());
+
+          GECKOBUNDLE_PUT(identifierBundle, "rpName",
+                          jni::StringParam(rp.Name()));
+          GECKOBUNDLE_PUT(identifierBundle, "rpIcon",
+                          jni::StringParam(rp.Icon()));
+          GECKOBUNDLE_PUT(identifierBundle, "userName",
+                          jni::StringParam(user.Name()));
+          GECKOBUNDLE_PUT(identifierBundle, "userIcon",
+                          jni::StringParam(user.Icon()));
+          GECKOBUNDLE_PUT(identifierBundle, "userDisplayName",
+                          jni::StringParam(user.DisplayName()));
+        }
+
+        GECKOBUNDLE_FINISH(authSelBundle);
+        GECKOBUNDLE_FINISH(extensionsBundle);
+        GECKOBUNDLE_FINISH(identifierBundle);
+
+        // For non-WebAuthn cases, uidBuf is empty (and unused)
+        jni::ByteBuffer::LocalRef uid = jni::ByteBuffer::New(
+            const_cast<void*>(static_cast<const void*>(uidBuf.Elements())),
+            uidBuf.Length());
+
+        java::WebAuthnTokenManager::WebAuthnMakeCredential(
+            aInfo.RpId(), identifierBundle, uid, challenge, aInfo.TimeoutMS(),
+            aInfo.Origin(), idList, transportList, authSelBundle,
+            extensionsBundle);
+      }));
+
+  return mRegisterPromise.Ensure(__func__);
+}
+
+void AndroidWebAuthnTokenManager::HandleRegisterResult(
+    const AndroidWebAuthnResult& aResult) {
+  // This is likely running on the main thread, so we'll always dispatch to the
+  // background for state updates.
+  if (aResult.IsError()) {
+    nsresult aError = aResult.GetError();
+
+    gAndroidPBackgroundThread->Dispatch(NS_NewRunnableFunction(
+        "AndroidWebAuthnTokenManager::RegisterAbort",
+        [self = RefPtr<AndroidWebAuthnTokenManager>(this), aError]() {
+          self->mRegisterPromise.RejectIfExists(aError, __func__);
+        }));
+  } else {
+    gAndroidPBackgroundThread->Dispatch(NS_NewRunnableFunction(
+        "AndroidWebAuthnTokenManager::RegisterComplete",
+        [self = RefPtr<AndroidWebAuthnTokenManager>(this), aResult]() {
+          CryptoBuffer emptyBuffer;
+          WebAuthnMakeCredentialResult result(aResult.mClientDataJSON,
+                                              aResult.mAttObj,
+                                              aResult.mKeyHandle, emptyBuffer);
+          self->mRegisterPromise.Resolve(std::move(result), __func__);
+        }));
+  }
+}
+
+RefPtr<U2FSignPromise> AndroidWebAuthnTokenManager::Sign(
+    const WebAuthnGetAssertionInfo& aInfo) {
+  AssertIsOnOwningThread();
+
+  ClearPromises();
+
+  GetMainThreadEventTarget()->Dispatch(NS_NewRunnableFunction(
+      "java::WebAuthnTokenManager::WebAuthnGetAssertion", [aInfo]() {
+        AssertIsOnMainThread();
+
+        jni::ObjectArray::LocalRef idList =
+            jni::ObjectArray::New(aInfo.AllowList().Length());
+
+        nsTArray<uint8_t> transportBuf;
+
+        int ix = 0;
+        for (const WebAuthnScopedCredential& cred : aInfo.AllowList()) {
+          jni::ByteBuffer::LocalRef id = jni::ByteBuffer::New(
+              const_cast<void*>(static_cast<const void*>(cred.id().Elements())),
+              cred.id().Length());
+
+          idList->SetElement(ix, id);
+          transportBuf.AppendElement(cred.transports());
+
+          ix += 1;
+        }
+
+        jni::ByteBuffer::LocalRef transportList = jni::ByteBuffer::New(
+            const_cast<void*>(
+                static_cast<const void*>(transportBuf.Elements())),
+            transportBuf.Length());
+
+        const nsTArray<uint8_t>& challBuf = aInfo.Challenge();
+        jni::ByteBuffer::LocalRef challenge = jni::ByteBuffer::New(
+            const_cast<void*>(static_cast<const void*>(challBuf.Elements())),
+            challBuf.Length());
+
+        // Get extensions
+        GECKOBUNDLE_START(extensionsBundle);
+        if (aInfo.Extra().isSome()) {
+          const auto& extra = aInfo.Extra().ref();
+
+          for (const WebAuthnExtension& ext : extra.Extensions()) {
+            if (ext.type() == WebAuthnExtension::TWebAuthnExtensionAppId) {
+              GECKOBUNDLE_PUT(
+                  extensionsBundle, "fidoAppId",
+                  jni::StringParam(
+                      ext.get_WebAuthnExtensionAppId().appIdentifier()));
+            }
+          }
+        }
+        GECKOBUNDLE_FINISH(extensionsBundle);
+
+        java::WebAuthnTokenManager::WebAuthnGetAssertion(
+            aInfo.RpId(), challenge, aInfo.TimeoutMS(), aInfo.Origin(), idList,
+            transportList, extensionsBundle);
+      }));
+
+  return mSignPromise.Ensure(__func__);
+}
+
+void AndroidWebAuthnTokenManager::HandleSignResult(
+    const AndroidWebAuthnResult& aResult) {
+  // This is likely running on the main thread, so we'll always dispatch to the
+  // background for state updates.
+  if (aResult.IsError()) {
+    nsresult aError = aResult.GetError();
+
+    gAndroidPBackgroundThread->Dispatch(NS_NewRunnableFunction(
+        "AndroidWebAuthnTokenManager::SignAbort",
+        [self = RefPtr<AndroidWebAuthnTokenManager>(this), aError]() {
+          self->mSignPromise.RejectIfExists(aError, __func__);
+        }));
+  } else {
+    gAndroidPBackgroundThread->Dispatch(NS_NewRunnableFunction(
+        "AndroidWebAuthnTokenManager::SignComplete",
+        [self = RefPtr<AndroidWebAuthnTokenManager>(this), aResult]() {
+          CryptoBuffer emptyBuffer;
+
+          nsTArray<WebAuthnExtensionResult> emptyExtensions;
+          WebAuthnGetAssertionResult result(
+              aResult.mClientDataJSON, aResult.mKeyHandle, aResult.mSignature,
+              aResult.mAuthData, emptyExtensions, emptyBuffer,
+              aResult.mUserHandle);
+          self->mSignPromise.Resolve(std::move(result), __func__);
+        }));
+  }
+}
+
+void AndroidWebAuthnTokenManager::Cancel() {
+  AssertIsOnOwningThread();
+
+  ClearPromises();
+}
+
+}  // namespace dom
+}  // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/webauthn/AndroidWebAuthnTokenManager.h
@@ -0,0 +1,129 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_AndroidWebAuthnTokenManager_h
+#define mozilla_dom_AndroidWebAuthnTokenManager_h
+
+#include "mozilla/dom/CryptoBuffer.h"
+#include "mozilla/dom/U2FTokenTransport.h"
+
+namespace mozilla {
+namespace dom {
+
+// Collected from
+// https://developers.google.com/android/reference/com/google/android/gms/fido/fido2/api/common/ErrorCode
+NS_NAMED_LITERAL_STRING(kSecurityError, "SECURITY_ERR");
+NS_NAMED_LITERAL_STRING(kConstraintError, "CONSTRAINT_ERR");
+NS_NAMED_LITERAL_STRING(kNotSupportedError, "NOT_SUPPORTED_ERR");
+NS_NAMED_LITERAL_STRING(kInvalidStateError, "INVALID_STATE_ERR");
+NS_NAMED_LITERAL_STRING(kNotAllowedError, "NOT_ALLOWED_ERR");
+NS_NAMED_LITERAL_STRING(kAbortError, "ABORT_ERR");
+NS_NAMED_LITERAL_STRING(kEncodingError, "ENCODING_ERR");
+NS_NAMED_LITERAL_STRING(kDataError, "DATA_ERR");
+NS_NAMED_LITERAL_STRING(kTimeoutError, "TIMEOUT_ERR");
+NS_NAMED_LITERAL_STRING(kNetworkError, "NETWORK_ERR");
+NS_NAMED_LITERAL_STRING(kUnknownError, "UNKNOWN_ERR");
+
+class AndroidWebAuthnResult {
+ public:
+  explicit AndroidWebAuthnResult(const nsAString& aErrorCode)
+      : mErrorCode(aErrorCode) {}
+
+  explicit AndroidWebAuthnResult() {}
+
+  bool IsError() const { return NS_FAILED(GetError()); }
+
+  nsresult GetError() const {
+    if (mErrorCode.IsEmpty()) {
+      return NS_OK;
+    } else if (mErrorCode.Equals(kSecurityError)) {
+      return NS_ERROR_DOM_SECURITY_ERR;
+    } else if (mErrorCode.Equals(kConstraintError)) {
+      // TODO: The message is right, but it's not about indexeddb.
+      // See https://heycam.github.io/webidl/#constrainterror
+      return NS_ERROR_DOM_INDEXEDDB_CONSTRAINT_ERR;
+    } else if (mErrorCode.Equals(kNotSupportedError)) {
+      return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+    } else if (mErrorCode.Equals(kInvalidStateError)) {
+      return NS_ERROR_DOM_INVALID_STATE_ERR;
+    } else if (mErrorCode.Equals(kNotAllowedError)) {
+      return NS_ERROR_DOM_NOT_ALLOWED_ERR;
+    } else if (mErrorCode.Equals(kEncodingError)) {
+      return NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR;
+    } else if (mErrorCode.Equals(kDataError)) {
+      return NS_ERROR_DOM_DATA_ERR;
+    } else if (mErrorCode.Equals(kTimeoutError)) {
+      return NS_ERROR_DOM_TIMEOUT_ERR;
+    } else if (mErrorCode.Equals(kNetworkError)) {
+      return NS_ERROR_DOM_NETWORK_ERR;
+    } else if (mErrorCode.Equals(kAbortError)) {
+      return NS_ERROR_DOM_ABORT_ERR;
+    } else if (mErrorCode.Equals(kUnknownError)) {
+      return NS_ERROR_DOM_UNKNOWN_ERR;
+    } else {
+      __android_log_print(ANDROID_LOG_ERROR, "Gecko",
+                          "RegisterAbort unknown code: %s",
+                          NS_ConvertUTF16toUTF8(mErrorCode).get());
+      return NS_ERROR_DOM_UNKNOWN_ERR;
+    }
+  }
+
+  // Attestation-only
+  CryptoBuffer mAttObj;
+
+  // Attestations and assertions
+  CryptoBuffer mKeyHandle;
+  nsCString mClientDataJSON;
+
+  // Assertions-only
+  CryptoBuffer mAuthData;
+  CryptoBuffer mSignature;
+  CryptoBuffer mUserHandle;
+
+ private:
+  const nsString mErrorCode;
+};
+
+/*
+ * WebAuthnAndroidTokenManager is a token implementation communicating with
+ * Android Fido2 APIs.
+ */
+class AndroidWebAuthnTokenManager final : public U2FTokenTransport {
+ public:
+  explicit AndroidWebAuthnTokenManager();
+  ~AndroidWebAuthnTokenManager() {}
+
+  RefPtr<U2FRegisterPromise> Register(const WebAuthnMakeCredentialInfo& aInfo,
+                                      bool aForceNoneAttestation) override;
+
+  RefPtr<U2FSignPromise> Sign(const WebAuthnGetAssertionInfo& aInfo) override;
+
+  void Cancel() override;
+
+  void Drop() override;
+
+  void HandleRegisterResult(const AndroidWebAuthnResult& aResult);
+
+  void HandleSignResult(const AndroidWebAuthnResult& aResult);
+
+  static AndroidWebAuthnTokenManager* GetInstance();
+
+ private:
+  void ClearPromises() {
+    mRegisterPromise.RejectIfExists(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
+    mSignPromise.RejectIfExists(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
+  }
+
+  void AssertIsOnOwningThread() const;
+
+  MozPromiseHolder<U2FRegisterPromise> mRegisterPromise;
+  MozPromiseHolder<U2FSignPromise> mSignPromise;
+};
+
+}  // namespace dom
+}  // namespace mozilla
+
+#endif  // mozilla_dom_AndroidWebAuthnTokenManager_h
--- a/dom/webauthn/U2FTokenManager.cpp
+++ b/dom/webauthn/U2FTokenManager.cpp
@@ -11,27 +11,32 @@
 #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 "nsTextFormatter.h"
 
+#ifdef ANDROID
+#  include "mozilla/dom/AndroidWebAuthnTokenManager.h"
+#endif
+
 // 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"
-
+#define PREF_WEBAUTHN_ANDROID_FIDO2_ENABLED \
+  "security.webauth.webauthn_enable_android_fido2"
 namespace mozilla {
 namespace dom {
 
 /***********************************************************************
  * Statics
  **********************************************************************/
 
 class U2FPrefManager;
@@ -66,16 +71,18 @@ class U2FPrefManager final : public nsIO
     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_ANDROID_FIDO2_ENABLED);
+      Preferences::AddStrongObserver(gPrefManager,
                                      PREF_WEBAUTHN_ALLOW_DIRECT_ATTESTATION);
       ClearOnShutdown(&gPrefManager, ShutdownPhase::ShutdownThreads);
     }
     return gPrefManager;
   }
 
   static U2FPrefManager* Get() { return gPrefManager; }
 
@@ -89,16 +96,21 @@ class U2FPrefManager final : public nsIO
     return mSoftTokenCounter;
   }
 
   bool GetUsbTokenEnabled() {
     MutexAutoLock lock(mPrefMutex);
     return mUsbTokenEnabled;
   }
 
+  bool GetAndroidFido2Enabled() {
+    MutexAutoLock lock(mPrefMutex);
+    return mAndroidFido2Enabled;
+  }
+
   bool GetAllowDirectAttestationForTesting() {
     MutexAutoLock lock(mPrefMutex);
     return mAllowDirectAttestation;
   }
 
   NS_IMETHODIMP
   Observe(nsISupports* aSubject, const char* aTopic,
           const char16_t* aData) override {
@@ -108,24 +120,27 @@ class U2FPrefManager final : public nsIO
 
  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);
+    mAndroidFido2Enabled =
+        Preferences::GetBool(PREF_WEBAUTHN_ANDROID_FIDO2_ENABLED);
     mAllowDirectAttestation =
         Preferences::GetBool(PREF_WEBAUTHN_ALLOW_DIRECT_ATTESTATION);
   }
 
   Mutex mPrefMutex;
   bool mSoftTokenEnabled;
   int mSoftTokenCounter;
   bool mUsbTokenEnabled;
+  bool mAndroidFido2Enabled;
   bool mAllowDirectAttestation;
 };
 
 NS_IMPL_ISUPPORTS(U2FPrefManager, nsIObserver);
 
 /***********************************************************************
  * U2FManager Implementation
  **********************************************************************/
@@ -248,16 +263,23 @@ RefPtr<U2FTokenTransport> U2FTokenManage
 
   if (!gBackgroundThread) {
     gBackgroundThread = NS_GetCurrentThread();
     MOZ_ASSERT(gBackgroundThread, "This should never be null!");
   }
 
   auto pm = U2FPrefManager::Get();
 
+#ifdef ANDROID
+  // On Android, prefer the platform support if enabled.
+  if (pm->GetAndroidFido2Enabled()) {
+    return AndroidWebAuthnTokenManager::GetInstance();
+  }
+#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 (pm->GetUsbTokenEnabled()) {
     return new U2FHIDTokenManager();
   }
 
--- a/dom/webauthn/moz.build
+++ b/dom/webauthn/moz.build
@@ -58,16 +58,24 @@ include('/ipc/chromium/chromium-config.m
 FINAL_LIBRARY = 'xul'
 
 LOCAL_INCLUDES += [
     '/dom/base',
     '/dom/crypto',
     '/security/manager/ssl',
 ]
 
+if CONFIG['OS_TARGET'] == 'Android':
+    EXPORTS.mozilla.dom += [
+        'AndroidWebAuthnTokenManager.h',
+    ]
+    UNIFIED_SOURCES += [
+        'AndroidWebAuthnTokenManager.cpp',
+    ]
+
 if CONFIG['OS_ARCH'] == 'WINNT':
     OS_LIBS += [
         'hid',
     ]
 
 if CONFIG['OS_TARGET'] == 'WINNT':
     EXPORTS.mozilla.dom += [
         'WinWebAuthnManager.h',
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -143,16 +143,20 @@ android {
                     exclude 'org/mozilla/gecko/mma/MmaLeanplumImp.java'
                     exclude 'org/mozilla/gecko/mma/LeanplumVariables.java'
                 }
 
                 if (!mozconfig.substs.MOZ_ANDROID_GCM) {
                     exclude 'org/mozilla/gecko/gcm/**/*.java'
                     exclude 'org/mozilla/gecko/push/**/*.java'
                 }
+
+                if (!mozconfig.substs.MOZ_ANDROID_GOOGLE_PLAY_SERVICES) {
+                    exclude 'org/mozilla/gecko/util/WebAuthnUtils.java'
+                }
             }
 
             res {
                 srcDir "${topsrcdir}/${mozconfig.substs.MOZ_BRANDING_DIRECTORY}/res"
                 srcDir "${topsrcdir}/mobile/android/services/src/main/res"
                 if (mozconfig.substs.MOZ_CRASHREPORTER) {
                     srcDir "${topsrcdir}/mobile/android/base/crashreporter/res"
                 }
@@ -237,16 +241,20 @@ dependencies {
     }
 
     if (mozconfig.substs.MOZ_ANDROID_GCM) {
         implementation "com.google.android.gms:play-services-basement:$google_play_services_version"
         implementation "com.google.android.gms:play-services-base:$google_play_services_version"
         implementation "com.google.android.gms:play-services-gcm:$google_play_services_version"
     }
 
+    if (mozconfig.substs.MOZ_ANDROID_GOOGLE_PLAY_SERVICES) {
+        implementation "com.google.android.gms:play-services-fido:$google_play_services_fido_version"
+    }
+
     // Include LeakCanary in local builds, but not in official builds.
     if (mozconfig.substs.MOZILLA_OFFICIAL) {
         implementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
     } else {
         implementation 'com.squareup.leakcanary:leakcanary-android:1.4-beta1'
     }
     testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
 
@@ -466,17 +474,18 @@ import groovy.xml.XmlUtil
 // All gms/firebase (except play-services-cast since it has sub-dependencies in 15.0.0)
 // libraries must use the exact same version specification (mixing versions can
 // lead to runtime crashes). Found versions 15.0.1, 15.0.0. Examples include
 // com.google.android.gms:play-services-base:15.0.1 and com.google.android.gms:play-services-basement:15.0.0
 configurations.all {
     resolutionStrategy {
         eachDependency { DependencyResolveDetails details ->
             if (details.requested.group == 'com.google.android.gms'
-                    && details.requested.name != 'play-services-cast') {
+                    && details.requested.name != 'play-services-cast'
+                    && details.requested.name != 'play-services-fido') {
                 details.useVersion "$google_play_services_version"
             }
         }
     }
 }
 
 android.applicationVariants.all { variant ->
     if (!mozconfig.substs.MOZILLA_OFFICIAL) {
--- a/mobile/android/base/java/org/mozilla/gecko/ActivityHandlerHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/ActivityHandlerHelper.java
@@ -12,17 +12,23 @@ import android.content.ActivityNotFoundE
 import android.content.Context;
 import android.content.Intent;
 import android.util.Log;
 
 public class ActivityHandlerHelper {
     private static final String LOGTAG = "GeckoActivityHandlerHelper";
     private static final ActivityResultHandlerMap mActivityResultHandlerMap = new ActivityResultHandlerMap();
 
-    private static int makeRequestCode(ActivityResultHandler aHandler) {
+    /**
+     * If you need to launch a PendingIntent from a Gecko activity but still collect
+     * the result, then you can use registerActivityHandler() to observe the
+     * result when it comes, as long as the Gecko activity is calling
+     * handleActivityResult(), which they do.
+     */
+    public static int registerActivityHandler(ActivityResultHandler aHandler) {
         return mActivityResultHandlerMap.put(aHandler);
     }
 
     /**
      * Starts the Activity, catching & logging if the Activity fails to start.
      *
      * We catch to prevent callers from passing in invalid Intents and crashing the browser.
      *
@@ -40,17 +46,16 @@ public class ActivityHandlerHelper {
             return false;
         }
     }
 
     public static void startIntentForActivity(Activity activity, Intent intent, ActivityResultHandler activityResultHandler) {
         activity.startActivityForResult(intent, mActivityResultHandlerMap.put(activityResultHandler));
     }
 
-
     public static boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
         ActivityResultHandler handler = mActivityResultHandlerMap.getAndRemove(requestCode);
         if (handler != null) {
             handler.onActivityResult(resultCode, data);
             return true;
         }
         return false;
     }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/util/WebAuthnUtils.java
@@ -0,0 +1,404 @@
+/* -*- Mode: Java; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+package org.mozilla.gecko.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.mozilla.gecko.ActivityHandlerHelper;
+import org.mozilla.gecko.WebAuthnTokenManager;
+import org.mozilla.gecko.GeckoActivityMonitor;
+import org.mozilla.gecko.util.ActivityResultHandler;
+import org.mozilla.gecko.util.GeckoBundle;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.net.Uri;
+import android.util.Log;
+import android.util.Base64;
+
+import com.google.android.gms.fido.Fido;
+import com.google.android.gms.fido.common.Transport;
+import com.google.android.gms.fido.fido2.Fido2PendingIntent;
+import com.google.android.gms.fido.fido2.Fido2PrivilegedApiClient;
+import com.google.android.gms.fido.fido2.api.common.Algorithm;
+import com.google.android.gms.fido.fido2.api.common.Attachment;
+import com.google.android.gms.fido.fido2.api.common.AttestationConveyancePreference;
+import com.google.android.gms.fido.fido2.api.common.AuthenticationExtensions;
+import com.google.android.gms.fido.fido2.api.common.AuthenticatorAssertionResponse;
+import com.google.android.gms.fido.fido2.api.common.AuthenticatorAttestationResponse;
+import com.google.android.gms.fido.fido2.api.common.AuthenticatorErrorResponse;
+import com.google.android.gms.fido.fido2.api.common.AuthenticatorSelectionCriteria;
+import com.google.android.gms.fido.fido2.api.common.BrowserPublicKeyCredentialCreationOptions;
+import com.google.android.gms.fido.fido2.api.common.BrowserPublicKeyCredentialRequestOptions;
+import com.google.android.gms.fido.fido2.api.common.EC2Algorithm;
+import com.google.android.gms.fido.fido2.api.common.FidoAppIdExtension;
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialCreationOptions;
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialDescriptor;
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialParameters;
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRequestOptions;
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRpEntity;
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialType;
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialUserEntity;
+import com.google.android.gms.fido.fido2.api.common.RSAAlgorithm;
+import com.google.android.gms.tasks.Task;
+import com.google.android.gms.tasks.OnSuccessListener;
+import com.google.android.gms.tasks.OnFailureListener;
+
+public class WebAuthnUtils
+{
+    private static final String LOG_TAG = "WebAuthnUtils";
+
+    // from u2fhid-capi.h
+    private static final byte AUTHENTICATOR_TRANSPORT_USB = 1;
+    private static final byte AUTHENTICATOR_TRANSPORT_NFC = 2;
+    private static final byte AUTHENTICATOR_TRANSPORT_BLE = 4;
+
+    private static List<Transport> getTransportsForByte(final byte transports) {
+        ArrayList<Transport> result = new ArrayList<Transport>();
+        if ((transports & AUTHENTICATOR_TRANSPORT_USB) == AUTHENTICATOR_TRANSPORT_USB) {
+            result.add(Transport.USB);
+        }
+        if ((transports & AUTHENTICATOR_TRANSPORT_NFC) == AUTHENTICATOR_TRANSPORT_NFC) {
+            result.add(Transport.NFC);
+        }
+        if ((transports & AUTHENTICATOR_TRANSPORT_BLE) == AUTHENTICATOR_TRANSPORT_BLE) {
+            result.add(Transport.BLUETOOTH_LOW_ENERGY);
+        }
+
+        return result;
+    }
+
+    public static void makeCredential(final String rpId, final GeckoBundle identifiers,
+                                      final byte[] userId, final byte[] challenge,
+                                      final long timeoutMs, final String originStr,
+                                      final WebAuthnTokenManager.WebAuthnPublicCredential[] excludeList,
+                                      final GeckoBundle authenticatorSelection,
+                                      final GeckoBundle extensions,
+                                      WebAuthnTokenManager.WebAuthnMakeCredentialResponse handler) {
+        final Activity currentActivity =
+            GeckoActivityMonitor.getInstance().getCurrentActivity();
+
+        if (currentActivity == null) {
+            handler.onFailure("UNKNOWN_ERR");
+            return;
+        }
+
+        PublicKeyCredentialCreationOptions.Builder requestBuilder =
+            new PublicKeyCredentialCreationOptions.Builder();
+
+        Fido2PrivilegedApiClient fidoClient = // Only works in released builds
+            Fido.getFido2PrivilegedApiClient(currentActivity.getApplicationContext());
+
+        List<PublicKeyCredentialParameters> params =
+            new ArrayList<PublicKeyCredentialParameters>();
+
+        // WebAuthn suuports more algorithms
+        for (Algorithm algo : new Algorithm[]{
+                EC2Algorithm.ES256, EC2Algorithm.ES384, EC2Algorithm.ES512,
+                EC2Algorithm.ED256, /* no ED384 */      EC2Algorithm.ED512,
+                RSAAlgorithm.PS256, RSAAlgorithm.PS384, RSAAlgorithm.PS512,
+                RSAAlgorithm.RS256, RSAAlgorithm.RS384, RSAAlgorithm.RS512
+            }) {
+            params.add(new PublicKeyCredentialParameters(
+                PublicKeyCredentialType.PUBLIC_KEY.toString(),
+                algo.getAlgoValue()));
+        }
+
+        PublicKeyCredentialUserEntity user =
+            new PublicKeyCredentialUserEntity(userId,
+                    identifiers.getString("userName", ""),
+                    identifiers.getString("userIcon", ""),
+                    identifiers.getString("userDisplayName", ""));
+
+        AttestationConveyancePreference pref =
+            AttestationConveyancePreference.NONE;
+        String attestationPreference =
+            authenticatorSelection.getString("attestationPreference", "NONE");
+        if (attestationPreference.equalsIgnoreCase(
+                              AttestationConveyancePreference.DIRECT.name())) {
+            pref = AttestationConveyancePreference.DIRECT;
+        } else if (attestationPreference.equalsIgnoreCase(
+                            AttestationConveyancePreference.INDIRECT.name())) {
+            pref = AttestationConveyancePreference.INDIRECT;
+        }
+
+        AuthenticatorSelectionCriteria.Builder selBuild =
+            new AuthenticatorSelectionCriteria.Builder();
+        if (extensions.containsKey("requirePlatformAttachment")) {
+            if (authenticatorSelection.getInt("requirePlatformAttachment") == 1) {
+                selBuild.setAttachment(Attachment.PLATFORM);
+            }
+        }
+        AuthenticatorSelectionCriteria sel = selBuild.build();
+
+        AuthenticationExtensions.Builder extBuilder =
+            new AuthenticationExtensions.Builder();
+        if (extensions.containsKey("fidoAppId")) {
+            extBuilder.setFido2Extension(
+                new FidoAppIdExtension(extensions.getString("fidoAppId")));
+        }
+        AuthenticationExtensions ext = extBuilder.build();
+
+        // requireResidentKey andrequireUserVerification are not yet
+        // consumed by Android's API
+
+        List<PublicKeyCredentialDescriptor> excludedList =
+            new ArrayList<PublicKeyCredentialDescriptor>();
+        for (WebAuthnTokenManager.WebAuthnPublicCredential cred : excludeList) {
+            excludedList.add(
+                new PublicKeyCredentialDescriptor(
+                                    PublicKeyCredentialType.PUBLIC_KEY.toString(),
+                                    cred.mId,
+                                    getTransportsForByte(cred.mTransports)));
+        }
+
+        PublicKeyCredentialRpEntity rp =
+            new PublicKeyCredentialRpEntity(rpId,
+                    identifiers.getString("rpName", ""),
+                    identifiers.getString("rpIcon", ""));
+
+        PublicKeyCredentialCreationOptions requestOptions =
+            requestBuilder
+                .setUser(user)
+                .setAttestationConveyancePreference(pref)
+                .setAuthenticatorSelection(sel)
+                .setAuthenticationExtensions(ext)
+                .setChallenge(challenge)
+                .setRp(rp)
+                .setParameters(params)
+                .setTimeoutSeconds(timeoutMs / 1000.0)
+                .setExcludeList(excludedList)
+                .build();
+
+        Uri origin = Uri.parse(originStr);
+
+        BrowserPublicKeyCredentialCreationOptions browserOptions =
+            new BrowserPublicKeyCredentialCreationOptions.Builder()
+                .setPublicKeyCredentialCreationOptions(requestOptions)
+                .setOrigin(origin)
+                .build();
+
+        Task<Fido2PendingIntent> result = fidoClient.getRegisterIntent(browserOptions);
+
+        result.addOnSuccessListener(new OnSuccessListener<Fido2PendingIntent>() {
+            @Override
+            public void onSuccess(Fido2PendingIntent pendingIntent) {
+                if (pendingIntent.hasPendingIntent()) {
+                    final WebAuthnMakeCredentialResult resultHandler =
+                        new WebAuthnMakeCredentialResult(handler);
+
+                    try {
+                        pendingIntent.launchPendingIntent(currentActivity,
+                            ActivityHandlerHelper.registerActivityHandler(resultHandler));
+                    } catch (IntentSender.SendIntentException e) {
+                        handler.onFailure("UNKNOWN_ERR");
+                    }
+                }
+            }
+        });
+        result.addOnFailureListener(new OnFailureListener() {
+                @Override
+                public void onFailure(Exception e) {
+                    Log.w(LOG_TAG, "onFailure=" + e);
+                    e.printStackTrace();
+                    handler.onFailure("UNKNOWN_ERR");
+                }
+        });
+    }
+
+    private static class WebAuthnMakeCredentialResult implements ActivityResultHandler {
+        private WebAuthnTokenManager.WebAuthnMakeCredentialResponse mHandler;
+
+        WebAuthnMakeCredentialResult(WebAuthnTokenManager.WebAuthnMakeCredentialResponse handler) {
+            this.mHandler = handler;
+        }
+
+        @Override
+        public void onActivityResult(final int resultCode, final Intent data) {
+            if (resultCode == Activity.RESULT_OK) {
+                if (data.hasExtra(Fido.FIDO2_KEY_ERROR_EXTRA)) {
+                    byte[] errData = data.getByteArrayExtra(Fido.FIDO2_KEY_ERROR_EXTRA);
+                    AuthenticatorErrorResponse responseData =
+                        AuthenticatorErrorResponse.deserializeFromBytes(errData);
+
+                    Log.e(LOG_TAG, "errorCode.name: " + responseData.getErrorCode());
+                    Log.e(LOG_TAG, "errorMessage: " + responseData.getErrorMessage());
+
+                    mHandler.onFailure(responseData.getErrorCode().name());
+                    return;
+                }
+
+                if (data.hasExtra(Fido.FIDO2_KEY_RESPONSE_EXTRA)) {
+                    byte[] rspData = data.getByteArrayExtra(Fido.FIDO2_KEY_RESPONSE_EXTRA);
+                    AuthenticatorAttestationResponse responseData =
+                        AuthenticatorAttestationResponse.deserializeFromBytes(rspData);
+
+                    Log.e(LOG_TAG, "key handle: " + Base64.encodeToString(responseData.getKeyHandle(), Base64.DEFAULT));
+                    Log.e(LOG_TAG, "clientDataJSON: " + Base64.encodeToString(responseData.getClientDataJSON(), Base64.DEFAULT));
+                    Log.e(LOG_TAG, "attestation Object: " + Base64.encodeToString(responseData.getAttestationObject(), Base64.DEFAULT));
+
+                    mHandler.onSuccess(
+                        responseData.getClientDataJSON(),
+                        responseData.getKeyHandle(),
+                        responseData.getAttestationObject());
+                    return;
+                }
+            }
+
+            if (resultCode == Activity.RESULT_CANCELED) {
+                Log.w(LOG_TAG, "RESULT_CANCELED" + resultCode);
+                mHandler.onFailure("ABORT_ERR");
+                return;
+            }
+
+            mHandler.onFailure("UNKNOWN_ERR");
+        }
+    }
+
+    public static void getAssertion(final String rpId, final byte[] challenge,
+                                    final long timeoutMs, final String originStr,
+                                    final WebAuthnTokenManager.WebAuthnPublicCredential[] allowList,
+                                    final GeckoBundle extensions,
+                                    WebAuthnTokenManager.WebAuthnGetAssertionResponse handler) {
+        final Activity currentActivity =
+            GeckoActivityMonitor.getInstance().getCurrentActivity();
+
+        if (currentActivity == null) {
+            handler.onFailure("UNKNOWN_ERR");
+            return;
+        }
+
+        List<PublicKeyCredentialDescriptor> allowedList =
+            new ArrayList<PublicKeyCredentialDescriptor>();
+        for (WebAuthnTokenManager.WebAuthnPublicCredential cred : allowList) {
+            allowedList.add(
+                new PublicKeyCredentialDescriptor(
+                                    PublicKeyCredentialType.PUBLIC_KEY.toString(),
+                                    cred.mId,
+                                    getTransportsForByte(cred.mTransports)));
+        }
+
+        Fido2PrivilegedApiClient fidoClient = // Only works in released builds
+            Fido.getFido2PrivilegedApiClient(currentActivity.getApplicationContext());
+
+        AuthenticationExtensions.Builder extBuilder =
+            new AuthenticationExtensions.Builder();
+        if (extensions.containsKey("fidoAppId")) {
+            extBuilder.setFido2Extension(
+                new FidoAppIdExtension(extensions.getString("fidoAppId")));
+        }
+        AuthenticationExtensions ext = extBuilder.build();
+
+        PublicKeyCredentialRequestOptions requestOptions =
+            new PublicKeyCredentialRequestOptions.Builder()
+                .setChallenge(challenge)
+                .setAllowList(allowedList)
+                .setTimeoutSeconds(timeoutMs / 1000.0)
+                .setRpId(rpId)
+                .setAuthenticationExtensions(ext)
+                .build();
+
+        Uri origin = Uri.parse(originStr);
+        BrowserPublicKeyCredentialRequestOptions browserOptions =
+            new BrowserPublicKeyCredentialRequestOptions.Builder()
+                .setPublicKeyCredentialRequestOptions(requestOptions)
+                .setOrigin(origin)
+                .build();
+
+        Task<Fido2PendingIntent> result = fidoClient.getSignIntent(browserOptions);
+
+        result.addOnSuccessListener(new OnSuccessListener<Fido2PendingIntent>() {
+            @Override
+            public void onSuccess(Fido2PendingIntent pendingIntent) {
+                if (pendingIntent.hasPendingIntent()) {
+                    final WebAuthnGetAssertionResult resultHandler =
+                        new WebAuthnGetAssertionResult(handler);
+
+                    try {
+                        pendingIntent.launchPendingIntent(currentActivity,
+                            ActivityHandlerHelper.registerActivityHandler(resultHandler));
+                    } catch (IntentSender.SendIntentException e) {
+                        Log.w(LOG_TAG, "pendingIntent failure", e);
+                        handler.onFailure("UNKNOWN_ERR");
+                    }
+                }
+            }
+        });
+        result.addOnFailureListener(new OnFailureListener() {
+                @Override
+                public void onFailure(Exception e) {
+                    Log.w(LOG_TAG, "onFailure=" + e);
+                    e.printStackTrace();
+                    handler.onFailure("UNKNOWN_ERR");
+                }
+        });
+    }
+
+    private static class WebAuthnGetAssertionResult implements ActivityResultHandler {
+        private WebAuthnTokenManager.WebAuthnGetAssertionResponse mHandler;
+
+        WebAuthnGetAssertionResult(WebAuthnTokenManager.WebAuthnGetAssertionResponse handler) {
+            this.mHandler = handler;
+        }
+
+        @Override
+        public void onActivityResult(final int resultCode, Intent data) {
+            if (resultCode == Activity.RESULT_OK) {
+
+                if (data.hasExtra(Fido.FIDO2_KEY_ERROR_EXTRA)) {
+                    Log.w(LOG_TAG, "FIDO2_KEY_ERROR_EXTRA and right");
+                    byte[] errData = data.getByteArrayExtra(Fido.FIDO2_KEY_ERROR_EXTRA);
+                    AuthenticatorErrorResponse responseData =
+                        AuthenticatorErrorResponse.deserializeFromBytes(errData);
+
+                    Log.e(LOG_TAG, "errorCode.name: " + responseData.getErrorCode());
+                    Log.e(LOG_TAG, "errorMessage: " + responseData.getErrorMessage());
+
+                    mHandler.onFailure(responseData.getErrorCode().name());
+                    return;
+                }
+
+                if (data.hasExtra(Fido.FIDO2_KEY_RESPONSE_EXTRA)) {
+                    Log.w(LOG_TAG, "FIDO2_KEY_RESPONSE_EXTRA and right");
+                    byte[] rspData = data.getByteArrayExtra(Fido.FIDO2_KEY_RESPONSE_EXTRA);
+                    AuthenticatorAssertionResponse responseData =
+                        AuthenticatorAssertionResponse.deserializeFromBytes(rspData);
+
+                    Log.e(LOG_TAG, "key handle: " + Base64.encodeToString(responseData.getKeyHandle(), Base64.DEFAULT));
+                    Log.e(LOG_TAG, "clientDataJSON: " + Base64.encodeToString(responseData.getClientDataJSON(), Base64.DEFAULT));
+                    Log.e(LOG_TAG, "auth data: " + Base64.encodeToString(responseData.getAuthenticatorData(), Base64.DEFAULT));
+                    Log.e(LOG_TAG, "signature: " + Base64.encodeToString(responseData.getSignature(), Base64.DEFAULT));
+
+                    // Nullable field
+                    byte[] userHandle = responseData.getUserHandle();
+                    if (userHandle == null) {
+                        userHandle = new byte[0];
+                    }
+
+                    mHandler.onSuccess(
+                        responseData.getClientDataJSON(),
+                        responseData.getKeyHandle(),
+                        responseData.getAuthenticatorData(),
+                        responseData.getSignature(),
+                        userHandle);
+                    return;
+                }
+            }
+
+            if (resultCode == Activity.RESULT_CANCELED) {
+                Log.w(LOG_TAG, "RESULT_CANCELED" + resultCode);
+                mHandler.onFailure("ABORT_ERR");
+                return;
+            }
+
+            mHandler.onFailure("UNKNOWN_ERR");
+        }
+    }
+
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/WebAuthnTokenManager.java
@@ -0,0 +1,212 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+package org.mozilla.gecko;
+
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.GeckoBundle;
+
+import android.util.Log;
+
+public class WebAuthnTokenManager {
+    private static final String LOGTAG = "WebAuthnTokenManager";
+
+    public static class WebAuthnPublicCredential {
+        public final byte[] mId;
+        public final byte mTransports;
+
+        public WebAuthnPublicCredential(final byte[] aId, final byte aTransports) {
+            this.mId = aId;
+            this.mTransports = aTransports;
+        }
+
+        static ArrayList<WebAuthnPublicCredential> CombineBuffers(
+                final Object[] idObjectList, final ByteBuffer transportList) {
+            if (idObjectList.length != transportList.remaining()) {
+                throw new RuntimeException("Couldn't extract allowed list!");
+            }
+
+            ArrayList<WebAuthnPublicCredential> credList =
+                new ArrayList<WebAuthnPublicCredential>();
+
+            byte[] transportBytes = new byte[transportList.remaining()];
+            transportList.get(transportBytes);
+
+            for (int i = 0; i < idObjectList.length; i++) {
+                final ByteBuffer id = (ByteBuffer)idObjectList[i];
+                byte[] idBytes = new byte[id.remaining()];
+                id.get(idBytes);
+
+                credList.add(new WebAuthnPublicCredential(idBytes, transportBytes[i]));
+            }
+            return credList;
+        }
+    }
+
+    // From WebAuthentication.webidl
+    public enum AttestationPreference {
+      NONE,
+      INDIRECT,
+      DIRECT,
+    }
+
+    public interface WebAuthnMakeCredentialResponse {
+        void onSuccess(final byte[] clientDataJson, final byte[] keyHandle,
+                       final byte[] attestationObject);
+        void onFailure(String errorCode);
+    }
+
+    @WrapForJNI(calledFrom = "gecko")
+    private static void webAuthnMakeCredential(final String rpId, final GeckoBundle identifiers,
+                                               final ByteBuffer userId,
+                                               final ByteBuffer challenge,
+                                               final long timeoutMs, final String origin,
+                                               final Object[] idList,
+                                               final ByteBuffer transportList,
+                                               final GeckoBundle authenticatorSelection,
+                                               final GeckoBundle extensions) {
+        ArrayList<WebAuthnPublicCredential> excludeList;
+
+        // TODO: Return a GeckoResult instead, Bug 1550116
+
+        if (!GeckoAppShell.isFennec()) {
+            Log.w(LOGTAG, "Currently only supported on Fennec");
+            webAuthnMakeCredentialReturnError("NOT_SUPPORTED_ERR");
+            return;
+        }
+
+        byte[] challBytes = new byte[challenge.remaining()];
+        byte[] userBytes = new byte[userId.remaining()];
+        try {
+            challenge.get(challBytes);
+            userId.get(userBytes);
+
+            excludeList = WebAuthnPublicCredential.CombineBuffers(idList,
+                                                                  transportList);
+        } catch (RuntimeException e) {
+            Log.w(LOGTAG, "Couldn't extract nio byte arrays!", e);
+            webAuthnMakeCredentialReturnError("UNKNOWN_ERR");
+            return;
+        }
+
+        WebAuthnMakeCredentialResponse handler = new WebAuthnMakeCredentialResponse() {
+            @Override
+            public void onSuccess(final byte[] clientDataJson, final byte[] keyHandle,
+                                  final byte[] attestationObject) {
+                webAuthnMakeCredentialFinish(clientDataJson, keyHandle,
+                                             attestationObject);
+            }
+            @Override
+            public void onFailure(final String errorCode) {
+                webAuthnMakeCredentialReturnError(errorCode);
+            }
+        };
+
+        try {
+            final Class<?> cls = Class.forName("org.mozilla.gecko.util.WebAuthnUtils");
+            Class<?>[] argTypes = new Class<?>[] { String.class, GeckoBundle.class,
+                                                   byte[].class, byte[].class, long.class,
+                                                   String.class, WebAuthnPublicCredential[].class,
+                                                   GeckoBundle.class, GeckoBundle.class,
+                                                   WebAuthnMakeCredentialResponse.class };
+            Method make = cls.getDeclaredMethod("makeCredential", argTypes);
+
+            make.invoke(null, rpId, identifiers, userBytes, challBytes,
+                        timeoutMs, origin,
+                        excludeList.toArray(new WebAuthnPublicCredential[0]),
+                        authenticatorSelection, extensions, handler);
+        } catch (Exception e) {
+            Log.w(LOGTAG, "Couldn't run WebAuthnUtils", e);
+            webAuthnMakeCredentialReturnError("UNKNOWN_ERR");
+            return;
+        }
+    }
+
+    @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+    /* package */ static native void webAuthnMakeCredentialFinish(final byte[] clientDataJson,
+                                                                  final byte[] keyHandle,
+                                                                  final byte[] attestationObject);
+    @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+    /* package */ static native void webAuthnMakeCredentialReturnError(String errorCode);
+
+    public interface WebAuthnGetAssertionResponse {
+        void onSuccess(final byte[] clientDataJson, final byte[] keyHandle,
+                       final byte[] authData, final byte[] signature,
+                       final byte[] userHandle);
+        void onFailure(String errorCode);
+    }
+
+    @WrapForJNI(calledFrom = "gecko")
+    private static void webAuthnGetAssertion(final String rpId, final ByteBuffer challenge,
+                                             final long timeoutMs, final String origin,
+                                             final Object[] idList,
+                                             final ByteBuffer transportList,
+                                             final GeckoBundle extensions) {
+        ArrayList<WebAuthnPublicCredential> allowList;
+
+        // TODO: Return a GeckoResult instead, Bug 1550116
+
+        if (!GeckoAppShell.isFennec()) {
+            Log.w(LOGTAG, "Currently only supported on Fennec");
+            webAuthnGetAssertionReturnError("NOT_SUPPORTED_ERR");
+            return;
+        }
+
+        byte[] challBytes = new byte[challenge.remaining()];
+        try {
+            challenge.get(challBytes);
+            allowList = WebAuthnPublicCredential.CombineBuffers(idList,
+                                                                transportList);
+        } catch (RuntimeException e) {
+            Log.w(LOGTAG, "Couldn't extract nio byte arrays!", e);
+            webAuthnGetAssertionReturnError("UNKNOWN_ERR");
+            return;
+        }
+
+        WebAuthnGetAssertionResponse handler = new WebAuthnGetAssertionResponse() {
+            @Override
+            public void onSuccess(final byte[] clientDataJson, final byte[] keyHandle,
+                                  final byte[] authData, final byte[] signature,
+                                  final byte[] userHandle) {
+                webAuthnGetAssertionFinish(clientDataJson, keyHandle, authData,
+                                           signature, userHandle);
+            }
+            @Override
+            public void onFailure(final String errorCode) {
+                webAuthnGetAssertionReturnError(errorCode);
+            }
+        };
+
+        try {
+            final Class<?> cls = Class.forName("org.mozilla.gecko.util.WebAuthnUtils");
+            Class<?>[] argTypes = new Class<?>[] { String.class, byte[].class, long.class,
+                                                   String.class, WebAuthnPublicCredential[].class,
+                                                   GeckoBundle.class,
+                                                   WebAuthnGetAssertionResponse.class };
+            Method make = cls.getDeclaredMethod("getAssertion", argTypes);
+
+            make.invoke(null, rpId, challBytes, timeoutMs, origin,
+                        allowList.toArray(new WebAuthnPublicCredential[0]),
+                        extensions, handler);
+        } catch (Exception e) {
+            Log.w(LOGTAG, "Couldn't run WebAuthnUtils", e);
+            webAuthnGetAssertionReturnError("UNKNOWN_ERR");
+            return;
+        }
+    }
+
+    @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+    /* package */ static native void webAuthnGetAssertionFinish(final byte[] clientDataJson,
+                                                                final byte[] keyHandle,
+                                                                final byte[] authData,
+                                                                final byte[] signature,
+                                                                final byte[] userHandle);
+    @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+    /* package */ static native void webAuthnGetAssertionReturnError(String errorCode);
+}
--- a/security/manager/ssl/security-prefs.js
+++ b/security/manager/ssl/security-prefs.js
@@ -116,16 +116,17 @@ pref("security.pki.netscape_step_up_poli
 // Configures Certificate Transparency support mode:
 // 0: Fully disabled.
 // 1: Only collect telemetry. CT qualification checks are not performed.
 pref("security.pki.certificate_transparency.mode", 0);
 
 // Hardware Origin-bound Second Factor Support
 pref("security.webauth.u2f", true);
 pref("security.webauth.webauthn", true);
+pref("security.webauth.webauthn_enable_android_fido2", false);
 // Only one of "enable_softtoken" and "enable_usbtoken" can be true
 // at a time.
 pref("security.webauth.webauthn_enable_softtoken", false);
 pref("security.webauth.webauthn_enable_usbtoken", true);
 
 pref("security.ssl.errorReporting.enabled", true);
 pref("security.ssl.errorReporting.url", "https://incoming.telemetry.mozilla.org/submit/sslreports/");
 pref("security.ssl.errorReporting.automatic", false);
new file mode 100644
--- /dev/null
+++ b/widget/android/WebAuthnTokenManager.cpp
@@ -0,0 +1,72 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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 "mozilla/dom/AndroidWebAuthnTokenManager.h"
+
+namespace mozilla {
+class WebAuthnTokenManager final
+    : public java::WebAuthnTokenManager::Natives<WebAuthnTokenManager> {
+ public:
+  static void WebAuthnMakeCredentialFinish(
+      jni::ByteArray::Param aClientDataJson, jni::ByteArray::Param aKeyHandle,
+      jni::ByteArray::Param aAttestationObject) {
+    mozilla::dom::AndroidWebAuthnResult result;
+
+    result.mClientDataJSON.Assign(
+        reinterpret_cast<const char*>(
+            aClientDataJson->GetElements().Elements()),
+        aClientDataJson->Length());
+    result.mKeyHandle.Assign(
+        reinterpret_cast<uint8_t*>(aKeyHandle->GetElements().Elements()),
+        aKeyHandle->Length());
+    result.mAttObj.Assign(reinterpret_cast<uint8_t*>(
+                              aAttestationObject->GetElements().Elements()),
+                          aAttestationObject->Length());
+
+    mozilla::dom::AndroidWebAuthnTokenManager::GetInstance()
+        ->HandleRegisterResult(std::move(result));
+  }
+
+  static void WebAuthnMakeCredentialReturnError(jni::String::Param aErrorCode) {
+    mozilla::dom::AndroidWebAuthnResult result(aErrorCode->ToString());
+    mozilla::dom::AndroidWebAuthnTokenManager::GetInstance()
+        ->HandleRegisterResult(std::move(result));
+  }
+
+  static void WebAuthnGetAssertionFinish(jni::ByteArray::Param aClientDataJson,
+                                         jni::ByteArray::Param aKeyHandle,
+                                         jni::ByteArray::Param aAuthData,
+                                         jni::ByteArray::Param aSignature,
+                                         jni::ByteArray::Param aUserHandle) {
+    mozilla::dom::AndroidWebAuthnResult result;
+
+    result.mClientDataJSON.Assign(
+        reinterpret_cast<const char*>(
+            aClientDataJson->GetElements().Elements()),
+        aClientDataJson->Length());
+    result.mKeyHandle.Assign(
+        reinterpret_cast<uint8_t*>(aKeyHandle->GetElements().Elements()),
+        aKeyHandle->Length());
+    result.mAuthData.Assign(
+        reinterpret_cast<uint8_t*>(aAuthData->GetElements().Elements()),
+        aAuthData->Length());
+    result.mSignature.Assign(
+        reinterpret_cast<uint8_t*>(aSignature->GetElements().Elements()),
+        aSignature->Length());
+    result.mUserHandle.Assign(
+        reinterpret_cast<uint8_t*>(aUserHandle->GetElements().Elements()),
+        aUserHandle->Length());
+
+    mozilla::dom::AndroidWebAuthnTokenManager::GetInstance()->HandleSignResult(
+        std::move(result));
+  }
+
+  static void WebAuthnGetAssertionReturnError(jni::String::Param aErrorCode) {
+    mozilla::dom::AndroidWebAuthnResult result(aErrorCode->ToString());
+    mozilla::dom::AndroidWebAuthnTokenManager::GetInstance()->HandleSignResult(
+        std::move(result));
+  }
+};
+}  // namespace mozilla
--- a/widget/android/moz.build
+++ b/widget/android/moz.build
@@ -60,16 +60,17 @@ UNIFIED_SOURCES += [
     'nsIdleServiceAndroid.cpp',
     'nsLookAndFeel.cpp',
     'nsNativeThemeAndroid.cpp',
     'nsPrintSettingsServiceAndroid.cpp',
     'nsWidgetFactory.cpp',
     'nsWindow.cpp',
     'ProcInfo.cpp',
     'ScreenHelperAndroid.cpp',
+    'WebAuthnTokenManager.cpp',
     'WebExecutorSupport.cpp',
 ]
 
 XPCOM_MANIFESTS += [
     'components.conf',
 ]
 
 include('/ipc/chromium/chromium-config.mozbuild')
--- a/widget/android/nsAppShell.cpp
+++ b/widget/android/nsAppShell.cpp
@@ -407,16 +407,17 @@ nsAppShell::nsAppShell()
     mozilla::GeckoProcessManager::Init();
     mozilla::GeckoScreenOrientation::Init();
     mozilla::GeckoSystemStateListener::Init();
     mozilla::PrefsHelper::Init();
     mozilla::widget::Telemetry::Init();
     mozilla::widget::WebExecutorSupport::Init();
     nsWindow::InitNatives();
     mozilla::gl::AndroidSurfaceTexture::Init();
+    mozilla::WebAuthnTokenManager::Init();
 
     if (jni::IsFennec()) {
       BrowserLocaleManagerSupport::Init();
       mozilla::ANRReporter::Init();
       mozilla::MemoryMonitor::Init();
       mozilla::ThumbnailHelper::Init();
     }