[WebAuthN] Allow authenticators that support both CTAP and U2F to try U2F if CTAP fails in authenticatorGetAssertion
https://bugs.webkit.org/show_bug.cgi?id=197974
<rdar://problem/50879746>

Reviewed by Brent Fulgham.

Source/WebKit:

Authenticators that support both CTAP and U2F protocols can be used in a U2F enabled browser to create a credential in
U2F format. When such authenticator is used to login in WebKit, it will be treated as a CTAP authenticator. Since the
previous credential is in U2F format, the authenticator will not consider that as a valid credential when CTAP requests
come along for that U2F credential. Therefore the previous created U2F credential will not be asked at all, and users
will not be able to login. This situation is not well documented in the CTAP/WebAuthN spec yet.

To workaround the above issue, an authenticator that supports both protocols will be downgraded to a U2F authenticator
to ask a potential U2F credential once a valid error is returned regarding to the first CTAP request.

* UIProcess/API/C/WKWebsiteDataStoreRef.cpp:
(WKWebsiteDataStoreSetWebAuthenticationMockConfiguration):
* UIProcess/WebAuthentication/Authenticator.h:
* UIProcess/WebAuthentication/AuthenticatorManager.cpp:
(WebKit::AuthenticatorManager::downgrade):
* UIProcess/WebAuthentication/AuthenticatorManager.h:
* UIProcess/WebAuthentication/Mock/MockHidConnection.cpp:
(WebKit::MockHidConnection::parseRequest):
(WebKit::MockHidConnection::feedReports):
* UIProcess/WebAuthentication/Mock/MockWebAuthenticationConfiguration.h:
* UIProcess/WebAuthentication/fido/CtapHidAuthenticator.cpp:
(WebKit::CtapHidAuthenticator::makeCredential):
(WebKit::CtapHidAuthenticator::getAssertion):
(WebKit::CtapHidAuthenticator::continueGetAssertionAfterResponseReceived):
(WebKit::CtapHidAuthenticator::tryDowngrade):
(WebKit::CtapHidAuthenticator::continueGetAssertionAfterResponseReceived const): Deleted.
* UIProcess/WebAuthentication/fido/CtapHidAuthenticator.h:

Tools:

Add a canDowngrade option for mock hid devices to simulate the situation.

* WebKitTestRunner/InjectedBundle/TestRunner.cpp:
(WTR::TestRunner::setWebAuthenticationMockConfiguration):

LayoutTests:

* http/wpt/webauthn/public-key-credential-get-failure-hid-silent.https-expected.txt:
* http/wpt/webauthn/public-key-credential-get-failure-hid-silent.https.html:
* http/wpt/webauthn/public-key-credential-get-failure-hid.https-expected.txt:
* http/wpt/webauthn/public-key-credential-get-failure-hid.https.html:
* http/wpt/webauthn/public-key-credential-get-success-u2f.https-expected.txt:
* http/wpt/webauthn/public-key-credential-get-success-u2f.https.html:
* http/wpt/webauthn/resources/util.js:


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@245500 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 60a6b02..b8f7fd9 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,19 @@
+2019-05-18  Jiewen Tan  <jiewen_tan@apple.com>
+
+        [WebAuthN] Allow authenticators that support both CTAP and U2F to try U2F if CTAP fails in authenticatorGetAssertion
+        https://bugs.webkit.org/show_bug.cgi?id=197974
+        <rdar://problem/50879746>
+
+        Reviewed by Brent Fulgham.
+
+        * http/wpt/webauthn/public-key-credential-get-failure-hid-silent.https-expected.txt:
+        * http/wpt/webauthn/public-key-credential-get-failure-hid-silent.https.html:
+        * http/wpt/webauthn/public-key-credential-get-failure-hid.https-expected.txt:
+        * http/wpt/webauthn/public-key-credential-get-failure-hid.https.html:
+        * http/wpt/webauthn/public-key-credential-get-success-u2f.https-expected.txt:
+        * http/wpt/webauthn/public-key-credential-get-success-u2f.https.html:
+        * http/wpt/webauthn/resources/util.js:
+
 2019-05-17  Joonghun Park  <pjh0718@gmail.com>
 
         Implement CSS `display: flow-root` (modern clearfix)
diff --git a/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-hid-silent.https-expected.txt b/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-hid-silent.https-expected.txt
index b462a44..aa9e639 100644
--- a/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-hid-silent.https-expected.txt
+++ b/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-hid-silent.https-expected.txt
@@ -1,4 +1,7 @@
 
 PASS PublicKeyCredential's [[get]] with malicious payload in a mock hid authenticator. 
 PASS PublicKeyCredential's [[get]] with unsupported options in a mock hid authenticator. 
+PASS PublicKeyCredential's [[get]] with invalid credential in a mock hid authenticator. 
+PASS PublicKeyCredential's [[get]] with authenticator downgrade in a mock hid authenticator. 
+PASS PublicKeyCredential's [[get]] with authenticator downgrade in a mock hid authenticator. 2 
 
diff --git a/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-hid-silent.https.html b/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-hid-silent.https.html
index 144ba9d..e85c2c8 100644
--- a/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-hid-silent.https.html
+++ b/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-hid-silent.https.html
@@ -30,4 +30,44 @@
             testRunner.setWebAuthenticationMockConfiguration({ silentFailure: true, hid: { stage: "request", subStage: "msg", error: "unsupported-options" } });
         return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "Operation timed out.");
     }, "PublicKeyCredential's [[get]] with unsupported options in a mock hid authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                challenge: asciiToUint8Array("123456"),
+                timeout: 10
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ silentFailure: true, hid: { stage: "request", subStage: "msg", error: "malicious-payload", payloadBase64: [testCtapErrInvalidCredentialResponseBase64] } });
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "Operation timed out.");
+    }, "PublicKeyCredential's [[get]] with invalid credential in a mock hid authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                challenge: asciiToUint8Array("123456"),
+                timeout: 10
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ silentFailure: true, hid: { stage: "request", subStage: "msg", error: "malicious-payload", canDowngrade: true, payloadBase64: [testCtapErrInvalidCredentialResponseBase64] } });
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "Operation timed out.");
+    }, "PublicKeyCredential's [[get]] with authenticator downgrade in a mock hid authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                challenge: asciiToUint8Array("123456"),
+                extensions: { appid: "" },
+                timeout: 10
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ silentFailure: true, hid: { stage: "request", subStage: "msg", error: "malicious-payload", canDowngrade: true, payloadBase64: [testCtapErrInvalidCredentialResponseBase64] } });
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "Operation timed out.");
+    }, "PublicKeyCredential's [[get]] with authenticator downgrade in a mock hid authenticator. 2");
 </script>
diff --git a/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-hid.https-expected.txt b/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-hid.https-expected.txt
index a53dd15..61b3afa 100644
--- a/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-hid.https-expected.txt
+++ b/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-hid.https-expected.txt
@@ -2,4 +2,6 @@
 PASS PublicKeyCredential's [[get]] with timeout in a mock hid authenticator. 
 PASS PublicKeyCredential's [[get]] with malicious payload in a mock hid authenticator. 
 PASS PublicKeyCredential's [[get]] with unsupported options in a mock hid authenticator. 
+PASS PublicKeyCredential's [[get]] with authenticator downgrade failed in a mock hid authenticator. 
+PASS PublicKeyCredential's [[get]] with authenticator downgrade succeeded and then U2F failed in a mock hid authenticator. 2 
 
diff --git a/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-hid.https.html b/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-hid.https.html
index 5ac2139..5485f37 100644
--- a/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-hid.https.html
+++ b/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-hid.https.html
@@ -33,7 +33,7 @@
 
         if (window.testRunner)
             testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "malicious-payload", payloadBase64: [testDummyMessagePayloadBase64] } });
-        return promiseRejects(t, "UnknownError", navigator.credentials.get(options), "Unknown internal error. Error code: -1");
+        return promiseRejects(t, "UnknownError", navigator.credentials.get(options), "Unknown internal error. Error code: 255");
     }, "PublicKeyCredential's [[get]] with malicious payload in a mock hid authenticator.");
 
     promise_test(function(t) {
@@ -48,4 +48,29 @@
             testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "unsupported-options" } });
         return promiseRejects(t, "UnknownError", navigator.credentials.get(options), "Unknown internal error. Error code: 43");
     }, "PublicKeyCredential's [[get]] with unsupported options in a mock hid authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                challenge: asciiToUint8Array("123456")
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "malicious-payload", payloadBase64: [testCtapErrInvalidCredentialResponseBase64] } });
+        return promiseRejects(t, "UnknownError", navigator.credentials.get(options), "Unknown internal error. Error code: 34");
+    }, "PublicKeyCredential's [[get]] with authenticator downgrade failed in a mock hid authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                challenge: asciiToUint8Array("123456"),
+                extensions: { appid: "" }
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "malicious-payload", canDowngrade: true, payloadBase64: [testCtapErrInvalidCredentialResponseBase64] } });
+        return promiseRejects(t, "NotSupportedError", navigator.credentials.get(options), "Cannot convert the request to U2F command.");
+    }, "PublicKeyCredential's [[get]] with authenticator downgrade succeeded and then U2F failed in a mock hid authenticator. 2");
 </script>
diff --git a/LayoutTests/http/wpt/webauthn/public-key-credential-get-success-u2f.https-expected.txt b/LayoutTests/http/wpt/webauthn/public-key-credential-get-success-u2f.https-expected.txt
index ab8f071..aefe75e 100644
--- a/LayoutTests/http/wpt/webauthn/public-key-credential-get-success-u2f.https-expected.txt
+++ b/LayoutTests/http/wpt/webauthn/public-key-credential-get-success-u2f.https-expected.txt
@@ -8,4 +8,6 @@
 PASS PublicKeyCredential's [[get]] with an AppID in a mock hid authenticator. 
 PASS PublicKeyCredential's [[get]] with multiple credentials and AppID is not used in a mock hid authenticator. 
 PASS PublicKeyCredential's [[get]] with multiple credentials and AppID is used in a mock hid authenticator. 
+PASS PublicKeyCredential's [[get]] with downgraded authenticator in a mock hid authenticator. 
+PASS PublicKeyCredential's [[get]] with downgraded authenticator in a mock hid authenticator. (AppID) 
 
diff --git a/LayoutTests/http/wpt/webauthn/public-key-credential-get-success-u2f.https.html b/LayoutTests/http/wpt/webauthn/public-key-credential-get-success-u2f.https.html
index a0a0f0b..c857114 100644
--- a/LayoutTests/http/wpt/webauthn/public-key-credential-get-success-u2f.https.html
+++ b/LayoutTests/http/wpt/webauthn/public-key-credential-get-success-u2f.https.html
@@ -181,4 +181,38 @@
         });
     }, "PublicKeyCredential's [[get]] with multiple credentials and AppID is used in a mock hid authenticator.");
 
+    promise_test(t => {
+        const options = {
+            publicKey: {
+                challenge: Base64URL.parse("MTIzNDU2"),
+                allowCredentials: [{ type: "public-key", id: Base64URL.parse(testU2fCredentialIdBase64) }],
+                timeout: 100,
+                extensions: { appid: "https://localhost:666/appid" }
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "success", canDowngrade: true, payloadBase64: [testCtapErrInvalidCredentialResponseBase64, testU2fSignResponse] } });
+        return navigator.credentials.get(options).then(credential => {
+            return checkResult(credential);
+        });
+    }, "PublicKeyCredential's [[get]] with downgraded authenticator in a mock hid authenticator.");
+
+    promise_test(t => {
+        const options = {
+            publicKey: {
+                challenge: Base64URL.parse("MTIzNDU2"),
+                allowCredentials: [{ type: "public-key", id: Base64URL.parse(testU2fCredentialIdBase64) }],
+                timeout: 100,
+                extensions: { appid: "https://localhost:666/appid" }
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "success", canDowngrade: true, payloadBase64: [testCtapErrInvalidCredentialResponseBase64, testU2fApduWrongDataOnlyResponseBase64, testU2fSignResponse] } });
+        return navigator.credentials.get(options).then(credential => {
+            return checkResult(credential, true, "7eabc5cc3251bdc59115ef87b5f7ee74cb03747e39ba8341748565cc129c0719");
+        });
+    }, "PublicKeyCredential's [[get]] with downgraded authenticator in a mock hid authenticator. (AppID)");
+
 </script>
diff --git a/LayoutTests/http/wpt/webauthn/resources/util.js b/LayoutTests/http/wpt/webauthn/resources/util.js
index f9a3204..987d072 100644
--- a/LayoutTests/http/wpt/webauthn/resources/util.js
+++ b/LayoutTests/http/wpt/webauthn/resources/util.js
@@ -98,6 +98,7 @@
     "AQAAADswRAIge94KUqwfTIsn4AOjcM1mpMcRjdItVEeDX0W5nGhCP/cCIDxRe0eH" +
     "f4V4LeEAhqeD0effTjY553H19q+jWq1Tc4WOkAA=";
 const testCtapErrCredentialExcludedOnlyResponseBase64 = "GQ==";
+const testCtapErrInvalidCredentialResponseBase64 = "Ig==";
 
 const RESOURCES_DIR = "/WebKit/webauthn/resources/";
 
diff --git a/Source/WebKit/ChangeLog b/Source/WebKit/ChangeLog
index fb9d26c..09d4239 100644
--- a/Source/WebKit/ChangeLog
+++ b/Source/WebKit/ChangeLog
@@ -1,3 +1,38 @@
+2019-05-18  Jiewen Tan  <jiewen_tan@apple.com>
+
+        [WebAuthN] Allow authenticators that support both CTAP and U2F to try U2F if CTAP fails in authenticatorGetAssertion
+        https://bugs.webkit.org/show_bug.cgi?id=197974
+        <rdar://problem/50879746>
+
+        Reviewed by Brent Fulgham.
+
+        Authenticators that support both CTAP and U2F protocols can be used in a U2F enabled browser to create a credential in
+        U2F format. When such authenticator is used to login in WebKit, it will be treated as a CTAP authenticator. Since the
+        previous credential is in U2F format, the authenticator will not consider that as a valid credential when CTAP requests
+        come along for that U2F credential. Therefore the previous created U2F credential will not be asked at all, and users
+        will not be able to login. This situation is not well documented in the CTAP/WebAuthN spec yet.
+
+        To workaround the above issue, an authenticator that supports both protocols will be downgraded to a U2F authenticator
+        to ask a potential U2F credential once a valid error is returned regarding to the first CTAP request.
+
+        * UIProcess/API/C/WKWebsiteDataStoreRef.cpp:
+        (WKWebsiteDataStoreSetWebAuthenticationMockConfiguration):
+        * UIProcess/WebAuthentication/Authenticator.h:
+        * UIProcess/WebAuthentication/AuthenticatorManager.cpp:
+        (WebKit::AuthenticatorManager::downgrade):
+        * UIProcess/WebAuthentication/AuthenticatorManager.h:
+        * UIProcess/WebAuthentication/Mock/MockHidConnection.cpp:
+        (WebKit::MockHidConnection::parseRequest):
+        (WebKit::MockHidConnection::feedReports):
+        * UIProcess/WebAuthentication/Mock/MockWebAuthenticationConfiguration.h:
+        * UIProcess/WebAuthentication/fido/CtapHidAuthenticator.cpp:
+        (WebKit::CtapHidAuthenticator::makeCredential):
+        (WebKit::CtapHidAuthenticator::getAssertion):
+        (WebKit::CtapHidAuthenticator::continueGetAssertionAfterResponseReceived):
+        (WebKit::CtapHidAuthenticator::tryDowngrade):
+        (WebKit::CtapHidAuthenticator::continueGetAssertionAfterResponseReceived const): Deleted.
+        * UIProcess/WebAuthentication/fido/CtapHidAuthenticator.h:
+
 2019-05-17  Don Olmstead  <don.olmstead@sony.com>
 
         [CMake] Use builtin FindICU
diff --git a/Source/WebKit/UIProcess/API/C/WKWebsiteDataStoreRef.cpp b/Source/WebKit/UIProcess/API/C/WKWebsiteDataStoreRef.cpp
index 2dad564..3c9660e 100644
--- a/Source/WebKit/UIProcess/API/C/WKWebsiteDataStoreRef.cpp
+++ b/Source/WebKit/UIProcess/API/C/WKWebsiteDataStoreRef.cpp
@@ -652,6 +652,9 @@
         if (auto continueAfterErrorData = static_cast<WKBooleanRef>(WKDictionaryGetItemForKey(hidRef, adoptWK(WKStringCreateWithUTF8CString("ContinueAfterErrorData")).get())))
             hid.continueAfterErrorData = WKBooleanGetValue(continueAfterErrorData);
 
+        if (auto canDowngrade = static_cast<WKBooleanRef>(WKDictionaryGetItemForKey(hidRef, adoptWK(WKStringCreateWithUTF8CString("CanDowngrade")).get())))
+            hid.canDowngrade = WKBooleanGetValue(canDowngrade);
+
         configuration.hid = WTFMove(hid);
     }
 
diff --git a/Source/WebKit/UIProcess/WebAuthentication/Authenticator.h b/Source/WebKit/UIProcess/WebAuthentication/Authenticator.h
index a2aa27a..cb0111a 100644
--- a/Source/WebKit/UIProcess/WebAuthentication/Authenticator.h
+++ b/Source/WebKit/UIProcess/WebAuthentication/Authenticator.h
@@ -44,6 +44,7 @@
     public:
         virtual ~Observer() = default;
         virtual void respondReceived(Respond&&) = 0;
+        virtual void downgrade(Authenticator* id, Ref<Authenticator>&& downgradedAuthenticator) = 0;
     };
 
     virtual ~Authenticator() = default;
diff --git a/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.cpp b/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.cpp
index 0c5d25c..913bc8a 100644
--- a/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.cpp
+++ b/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.cpp
@@ -208,6 +208,17 @@
     respondReceivedInternal(WTFMove(respond));
 }
 
+void AuthenticatorManager::downgrade(Authenticator* id, Ref<Authenticator>&& downgradedAuthenticator)
+{
+    RunLoop::main().dispatch([weakThis = makeWeakPtr(*this), id] {
+        if (!weakThis)
+            return;
+        auto removed = weakThis->m_authenticators.remove(id);
+        ASSERT_UNUSED(removed, removed);
+    });
+    authenticatorAdded(WTFMove(downgradedAuthenticator));
+}
+
 UniqueRef<AuthenticatorTransportService> AuthenticatorManager::createService(WebCore::AuthenticatorTransport transport, AuthenticatorTransportService::Observer& observer) const
 {
     return AuthenticatorTransportService::create(transport, observer);
diff --git a/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.h b/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.h
index dc459b4..b03fe57 100644
--- a/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.h
+++ b/Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.h
@@ -70,6 +70,7 @@
 
     // Authenticator::Observer
     void respondReceived(Respond&&) final;
+    void downgrade(Authenticator* id, Ref<Authenticator>&& downgradedAuthenticator) final;
 
     // Overriden by MockAuthenticatorManager.
     virtual UniqueRef<AuthenticatorTransportService> createService(WebCore::AuthenticatorTransport, AuthenticatorTransportService::Observer&) const;
diff --git a/Source/WebKit/UIProcess/WebAuthentication/Mock/MockHidConnection.cpp b/Source/WebKit/UIProcess/WebAuthentication/Mock/MockHidConnection.cpp
index d86a3a4..39dd752 100644
--- a/Source/WebKit/UIProcess/WebAuthentication/Mock/MockHidConnection.cpp
+++ b/Source/WebKit/UIProcess/WebAuthentication/Mock/MockHidConnection.cpp
@@ -132,6 +132,8 @@
 
     if (m_stage == Mock::Stage::Request && m_subStage == Mock::SubStage::Msg) {
         // Make sure we issue different msg cmd for CTAP and U2F.
+        if (m_configuration.hid->canDowngrade && !m_configuration.hid->isU2f)
+            m_configuration.hid->isU2f = m_requestMessage->cmd() == FidoHidDeviceCommand::kMsg;
         ASSERT(m_configuration.hid->isU2f ^ (m_requestMessage->cmd() != FidoHidDeviceCommand::kMsg));
 
         // Set options.
@@ -208,7 +210,11 @@
 
     Optional<FidoHidMessage> message;
     if (m_stage == Mock::Stage::Info && m_subStage == Mock::SubStage::Msg) {
-        auto infoData = encodeAsCBOR(AuthenticatorGetInfoResponse({ ProtocolVersion::kCtap }, Vector<uint8_t>(aaguidLength, 0u)));
+        Vector<uint8_t> infoData;
+        if (m_configuration.hid->canDowngrade)
+            infoData = encodeAsCBOR(AuthenticatorGetInfoResponse({ ProtocolVersion::kCtap, ProtocolVersion::kU2f }, Vector<uint8_t>(aaguidLength, 0u)));
+        else
+            infoData = encodeAsCBOR(AuthenticatorGetInfoResponse({ ProtocolVersion::kCtap }, Vector<uint8_t>(aaguidLength, 0u)));
         infoData.insert(0, static_cast<uint8_t>(CtapDeviceResponseCode::kSuccess)); // Prepend status code.
         if (stagesMatch() && m_configuration.hid->error == Mock::Error::WrongChannelId)
             message = FidoHidMessage::create(m_currentChannel - 1, FidoHidDeviceCommand::kCbor, infoData);
diff --git a/Source/WebKit/UIProcess/WebAuthentication/Mock/MockWebAuthenticationConfiguration.h b/Source/WebKit/UIProcess/WebAuthentication/Mock/MockWebAuthenticationConfiguration.h
index be0872f..f7c3969 100644
--- a/Source/WebKit/UIProcess/WebAuthentication/Mock/MockWebAuthenticationConfiguration.h
+++ b/Source/WebKit/UIProcess/WebAuthentication/Mock/MockWebAuthenticationConfiguration.h
@@ -69,6 +69,7 @@
         bool keepAlive { false };
         bool fastDataArrival { false };
         bool continueAfterErrorData { false };
+        bool canDowngrade { false };
     };
 
     bool silentFailure { false };
diff --git a/Source/WebKit/UIProcess/WebAuthentication/fido/CtapHidAuthenticator.cpp b/Source/WebKit/UIProcess/WebAuthentication/fido/CtapHidAuthenticator.cpp
index 08cd5a3..3541a8c 100644
--- a/Source/WebKit/UIProcess/WebAuthentication/fido/CtapHidAuthenticator.cpp
+++ b/Source/WebKit/UIProcess/WebAuthentication/fido/CtapHidAuthenticator.cpp
@@ -29,6 +29,7 @@
 #if ENABLE(WEB_AUTHN) && PLATFORM(MAC)
 
 #include "CtapHidDriver.h"
+#include "U2fHidAuthenticator.h"
 #include <WebCore/DeviceRequestConverter.h>
 #include <WebCore/DeviceResponseConverter.h>
 #include <WebCore/ExceptionData.h>
@@ -49,6 +50,7 @@
 
 void CtapHidAuthenticator::makeCredential()
 {
+    ASSERT(!m_isDowngraded);
     auto cborCmd = encodeMakeCredenitalRequestAsCBOR(requestData().hash, requestData().creationOptions, m_info.options().userVerificationAvailability());
     m_driver->transact(WTFMove(cborCmd), [weakThis = makeWeakPtr(*this)](Vector<uint8_t>&& data) {
         ASSERT(RunLoop::isMain());
@@ -74,6 +76,7 @@
 
 void CtapHidAuthenticator::getAssertion()
 {
+    ASSERT(!m_isDowngraded);
     auto cborCmd = encodeGetAssertionRequestAsCBOR(requestData().hash, requestData().requestOptions, m_info.options().userVerificationAvailability());
     m_driver->transact(WTFMove(cborCmd), [weakThis = makeWeakPtr(*this)](Vector<uint8_t>&& data) {
         ASSERT(RunLoop::isMain());
@@ -83,16 +86,32 @@
     });
 }
 
-void CtapHidAuthenticator::continueGetAssertionAfterResponseReceived(Vector<uint8_t>&& data) const
+void CtapHidAuthenticator::continueGetAssertionAfterResponseReceived(Vector<uint8_t>&& data)
 {
     auto response = readCTAPGetAssertionResponse(data);
     if (!response) {
-        receiveRespond(ExceptionData { UnknownError, makeString("Unknown internal error. Error code: ", data.size() == 1 ? data[0] : -1) });
+        auto error = getResponseCode(data);
+        if (error != CtapDeviceResponseCode::kCtap2ErrInvalidCBOR && tryDowngrade())
+            return;
+        receiveRespond(ExceptionData { UnknownError, makeString("Unknown internal error. Error code: ", static_cast<uint8_t>(error)) });
         return;
     }
     receiveRespond(WTFMove(*response));
 }
 
+bool CtapHidAuthenticator::tryDowngrade()
+{
+    if (m_info.versions().find(ProtocolVersion::kU2f) == m_info.versions().end())
+        return false;
+    if (!observer())
+        return false;
+
+    m_isDowngraded = true;
+    m_driver->setProtocol(ProtocolVersion::kU2f);
+    observer()->downgrade(this, U2fHidAuthenticator::create(WTFMove(m_driver)));
+    return true;
+}
+
 } // namespace WebKit
 
 #endif // ENABLE(WEB_AUTHN) && PLATFORM(MAC)
diff --git a/Source/WebKit/UIProcess/WebAuthentication/fido/CtapHidAuthenticator.h b/Source/WebKit/UIProcess/WebAuthentication/fido/CtapHidAuthenticator.h
index fdf6279..63c4611 100644
--- a/Source/WebKit/UIProcess/WebAuthentication/fido/CtapHidAuthenticator.h
+++ b/Source/WebKit/UIProcess/WebAuthentication/fido/CtapHidAuthenticator.h
@@ -47,10 +47,13 @@
     void makeCredential() final;
     void continueMakeCredentialAfterResponseReceived(Vector<uint8_t>&&) const;
     void getAssertion() final;
-    void continueGetAssertionAfterResponseReceived(Vector<uint8_t>&&) const;
+    void continueGetAssertionAfterResponseReceived(Vector<uint8_t>&&);
+
+    bool tryDowngrade();
 
     std::unique_ptr<CtapHidDriver> m_driver;
     fido::AuthenticatorGetInfoResponse m_info;
+    bool m_isDowngraded { false };
 };
 
 } // namespace WebKit
diff --git a/Tools/ChangeLog b/Tools/ChangeLog
index d2b62fb..8ce5fcd 100644
--- a/Tools/ChangeLog
+++ b/Tools/ChangeLog
@@ -1,3 +1,16 @@
+2019-05-18  Jiewen Tan  <jiewen_tan@apple.com>
+
+        [WebAuthN] Allow authenticators that support both CTAP and U2F to try U2F if CTAP fails in authenticatorGetAssertion
+        https://bugs.webkit.org/show_bug.cgi?id=197974
+        <rdar://problem/50879746>
+
+        Reviewed by Brent Fulgham.
+
+        Add a canDowngrade option for mock hid devices to simulate the situation.
+
+        * WebKitTestRunner/InjectedBundle/TestRunner.cpp:
+        (WTR::TestRunner::setWebAuthenticationMockConfiguration):
+
 2019-05-18  Tadeu Zagallo  <tzagallo@apple.com>
 
         Add extra information to dumpJITMemory
diff --git a/Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp b/Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp
index b24593f..0be4c1c 100644
--- a/Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp
+++ b/Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp
@@ -2692,6 +2692,16 @@
             hidValues.append(adoptWK(WKBooleanCreate(continueAfterErrorData)).get());
         }
 
+        JSRetainPtr<JSStringRef> canDowngradePropertyName(Adopt, JSStringCreateWithUTF8CString("canDowngrade"));
+        JSValueRef canDowngradeValue = JSObjectGetProperty(context, hid, canDowngradePropertyName.get(), 0);
+        if (!JSValueIsUndefined(context, canDowngradeValue) && !JSValueIsNull(context, canDowngradeValue)) {
+            if (!JSValueIsBoolean(context, canDowngradeValue))
+                return;
+            bool canDowngrade = JSValueToBoolean(context, canDowngradeValue);
+            hidKeys.append(adoptWK(WKStringCreateWithUTF8CString("CanDowngrade")));
+            hidValues.append(adoptWK(WKBooleanCreate(canDowngrade)).get());
+        }
+
         Vector<WKStringRef> rawHidKeys;
         Vector<WKTypeRef> rawHidValues;
         rawHidKeys.resize(hidKeys.size());