[WebAuthN] Support U2F HID Authenticators on macOS
authorjiewen_tan@apple.com <jiewen_tan@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 9 Jan 2019 00:35:39 +0000 (00:35 +0000)
committerjiewen_tan@apple.com <jiewen_tan@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 9 Jan 2019 00:35:39 +0000 (00:35 +0000)
https://bugs.webkit.org/show_bug.cgi?id=191535
<rdar://problem/47102027>

Reviewed by Brent Fulgham.

Source/WebCore:

This patch changes U2fCommandConstructor to produce register commands with
enforcing test of user presence. Otherwise, authenticators would silently
generate credentials. It also renames readFromU2fSignResponse to
readU2fSignResponse.

Tests: http/wpt/webauthn/public-key-credential-create-failure-u2f-silent.https.html
       http/wpt/webauthn/public-key-credential-create-failure-u2f.https.html
       http/wpt/webauthn/public-key-credential-create-success-u2f.https.html
       http/wpt/webauthn/public-key-credential-get-failure-u2f-silent.https.html
       http/wpt/webauthn/public-key-credential-get-failure-u2f.https.html
       http/wpt/webauthn/public-key-credential-get-success-u2f.https.html

* Modules/webauthn/fido/U2fCommandConstructor.cpp:
(fido::WebCore::constructU2fRegisterCommand):
* Modules/webauthn/fido/U2fResponseConverter.cpp:
(fido::readU2fSignResponse):
(fido::readFromU2fSignResponse): Deleted.
* Modules/webauthn/fido/U2fResponseConverter.h:

Source/WebKit:

This patch implements the support for U2F authenticators, and enables it for hid devices.
It follows the CTAP spec to map WebAuthN requests to U2F commands and return the responses:
https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html#u2f-interoperability
Most of the parts are done before this patch, this patch focues on: 7.2.2 and 7.3.2.

Besides implementing the U2fHidAuthenticator, this patch also adds support in the mocking
environment for U2F authenticators. It is done by extending the stages in MockHidConnection
from 4 to indefinite as multi-round communications are expected to map WebAuthN requests
to U2F requests.

* Sources.txt:
* UIProcess/API/C/WKWebsiteDataStoreRef.cpp:
(WKWebsiteDataStoreSetWebAuthenticationMockConfiguration):
* UIProcess/WebAuthentication/Cocoa/HidService.mm:
(WebKit::HidService::continueAddDeviceAfterGetInfo):
* UIProcess/WebAuthentication/fido/CtapHidDriver.cpp:
(WebKit::CtapHidDriver::continueAfterChannelAllocated):
* UIProcess/WebAuthentication/fido/CtapHidDriver.h:
(WebKit::CtapHidDriver::setProtocol):
* UIProcess/WebAuthentication/fido/U2fHidAuthenticator.cpp: Added.
(WebKit::U2fHidAuthenticator::U2fHidAuthenticator):
(WebKit::U2fHidAuthenticator::makeCredential):
(WebKit::U2fHidAuthenticator::checkExcludeList):
(WebKit::U2fHidAuthenticator::issueRegisterCommand):
(WebKit::U2fHidAuthenticator::getAssertion):
(WebKit::U2fHidAuthenticator::issueSignCommand):
(WebKit::U2fHidAuthenticator::issueNewCommand):
(WebKit::U2fHidAuthenticator::issueCommand):
(WebKit::U2fHidAuthenticator::responseReceived):
(WebKit::U2fHidAuthenticator::continueRegisterCommandAfterResponseReceived):
(WebKit::U2fHidAuthenticator::continueCheckOnlyCommandAfterResponseReceived):
(WebKit::U2fHidAuthenticator::continueBogusCommandAfterResponseReceived):
(WebKit::U2fHidAuthenticator::continueSignCommandAfterResponseReceived):
* UIProcess/WebAuthentication/fido/U2fHidAuthenticator.h: Added.
* UIProcess/WebAuthentication/Mock/MockHidConnection.cpp:
(WebKit::MockHidConnection::parseRequest):
(WebKit::MockHidConnection::feedReports):
* UIProcess/WebAuthentication/Mock/MockHidConnection.h:
* UIProcess/WebAuthentication/Mock/MockWebAuthenticationConfiguration.h:
* WebKit.xcodeproj/project.pbxproj:

Tools:

This patch:
1) adds support for U2F mocking mechanism;
2) updates tests to reflect U2fCommandConstructor changes.

* TestWebKitAPI/Tests/WebCore/CtapResponseTest.cpp:
(TestWebKitAPI::TEST):
* TestWebKitAPI/Tests/WebCore/FidoTestData.h:
* WebKitTestRunner/InjectedBundle/TestRunner.cpp:
(WTR::TestRunner::setWebAuthenticationMockConfiguration):

LayoutTests:

Besiding adding tests for U2F authenticators, it also changes payloadBase64 from
a string to a vector of strings. New tests are skipped for iOS.

* http/wpt/webauthn/ctap-hid-failure.https.html:
* http/wpt/webauthn/ctap-hid-success.https.html:
* http/wpt/webauthn/public-key-credential-create-failure-hid-silent.https.html:
* http/wpt/webauthn/public-key-credential-create-failure-hid.https.html:
* http/wpt/webauthn/public-key-credential-create-failure-u2f-silent.https-expected.txt: Added.
* http/wpt/webauthn/public-key-credential-create-failure-u2f-silent.https.html: Added.
* http/wpt/webauthn/public-key-credential-create-failure-u2f.https-expected.txt: Added.
* http/wpt/webauthn/public-key-credential-create-failure-u2f.https.html: Added.
* http/wpt/webauthn/public-key-credential-create-success-hid.https.html:
* http/wpt/webauthn/public-key-credential-create-success-u2f.https-expected.txt: Added.
* http/wpt/webauthn/public-key-credential-create-success-u2f.https.html: Copied from LayoutTests/http/wpt/webauthn/public-key-credential-create-success-hid.https.html.
* http/wpt/webauthn/public-key-credential-get-failure-hid-silent.https.html:
* http/wpt/webauthn/public-key-credential-get-failure-hid.https.html:
* http/wpt/webauthn/public-key-credential-get-failure-u2f-silent.https-expected.txt: Added.
* http/wpt/webauthn/public-key-credential-get-failure-u2f-silent.https.html: Added.
* http/wpt/webauthn/public-key-credential-get-failure-u2f.https-expected.txt: Added.
* http/wpt/webauthn/public-key-credential-get-failure-u2f.https.html: Added.
* http/wpt/webauthn/public-key-credential-get-success-hid.https.html:
* http/wpt/webauthn/public-key-credential-get-success-u2f.https-expected.txt: Added.
* http/wpt/webauthn/public-key-credential-get-success-u2f.https.html: Added.
* http/wpt/webauthn/resources/util.js:
* platform/ios-wk2/TestExpectations:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@239752 268f45cc-cd09-0410-ab3c-d52691b4dbfc

43 files changed:
LayoutTests/ChangeLog
LayoutTests/http/wpt/webauthn/ctap-hid-failure.https.html
LayoutTests/http/wpt/webauthn/ctap-hid-success.https.html
LayoutTests/http/wpt/webauthn/public-key-credential-create-failure-hid-silent.https.html
LayoutTests/http/wpt/webauthn/public-key-credential-create-failure-hid.https.html
LayoutTests/http/wpt/webauthn/public-key-credential-create-failure-u2f-silent.https-expected.txt [new file with mode: 0644]
LayoutTests/http/wpt/webauthn/public-key-credential-create-failure-u2f-silent.https.html [new file with mode: 0644]
LayoutTests/http/wpt/webauthn/public-key-credential-create-failure-u2f.https-expected.txt [new file with mode: 0644]
LayoutTests/http/wpt/webauthn/public-key-credential-create-failure-u2f.https.html [new file with mode: 0644]
LayoutTests/http/wpt/webauthn/public-key-credential-create-success-hid.https.html
LayoutTests/http/wpt/webauthn/public-key-credential-create-success-u2f.https-expected.txt [new file with mode: 0644]
LayoutTests/http/wpt/webauthn/public-key-credential-create-success-u2f.https.html [new file with mode: 0644]
LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-hid-silent.https.html
LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-hid.https.html
LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-u2f-silent.https-expected.txt [new file with mode: 0644]
LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-u2f-silent.https.html [new file with mode: 0644]
LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-u2f.https-expected.txt [new file with mode: 0644]
LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-u2f.https.html [new file with mode: 0644]
LayoutTests/http/wpt/webauthn/public-key-credential-get-success-hid.https.html
LayoutTests/http/wpt/webauthn/public-key-credential-get-success-u2f.https-expected.txt [new file with mode: 0644]
LayoutTests/http/wpt/webauthn/public-key-credential-get-success-u2f.https.html [new file with mode: 0644]
LayoutTests/http/wpt/webauthn/resources/util.js
LayoutTests/platform/ios-wk2/TestExpectations
Source/WebCore/ChangeLog
Source/WebCore/Modules/webauthn/fido/U2fCommandConstructor.cpp
Source/WebCore/Modules/webauthn/fido/U2fResponseConverter.cpp
Source/WebCore/Modules/webauthn/fido/U2fResponseConverter.h
Source/WebKit/ChangeLog
Source/WebKit/Sources.txt
Source/WebKit/UIProcess/API/C/WKWebsiteDataStoreRef.cpp
Source/WebKit/UIProcess/WebAuthentication/Cocoa/HidService.mm
Source/WebKit/UIProcess/WebAuthentication/Mock/MockHidConnection.cpp
Source/WebKit/UIProcess/WebAuthentication/Mock/MockHidConnection.h
Source/WebKit/UIProcess/WebAuthentication/Mock/MockWebAuthenticationConfiguration.h
Source/WebKit/UIProcess/WebAuthentication/fido/CtapHidDriver.cpp
Source/WebKit/UIProcess/WebAuthentication/fido/CtapHidDriver.h
Source/WebKit/UIProcess/WebAuthentication/fido/U2fHidAuthenticator.cpp [new file with mode: 0644]
Source/WebKit/UIProcess/WebAuthentication/fido/U2fHidAuthenticator.h [new file with mode: 0644]
Source/WebKit/WebKit.xcodeproj/project.pbxproj
Tools/ChangeLog
Tools/TestWebKitAPI/Tests/WebCore/CtapResponseTest.cpp
Tools/TestWebKitAPI/Tests/WebCore/FidoTestData.h
Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp

index 9b6748a..6131695 100644 (file)
@@ -1,3 +1,37 @@
+2019-01-08  Jiewen Tan  <jiewen_tan@apple.com>
+
+        [WebAuthN] Support U2F HID Authenticators on macOS
+        https://bugs.webkit.org/show_bug.cgi?id=191535
+        <rdar://problem/47102027>
+
+        Reviewed by Brent Fulgham.
+
+        Besiding adding tests for U2F authenticators, it also changes payloadBase64 from
+        a string to a vector of strings. New tests are skipped for iOS.
+
+        * http/wpt/webauthn/ctap-hid-failure.https.html:
+        * http/wpt/webauthn/ctap-hid-success.https.html:
+        * http/wpt/webauthn/public-key-credential-create-failure-hid-silent.https.html:
+        * http/wpt/webauthn/public-key-credential-create-failure-hid.https.html:
+        * http/wpt/webauthn/public-key-credential-create-failure-u2f-silent.https-expected.txt: Added.
+        * http/wpt/webauthn/public-key-credential-create-failure-u2f-silent.https.html: Added.
+        * http/wpt/webauthn/public-key-credential-create-failure-u2f.https-expected.txt: Added.
+        * http/wpt/webauthn/public-key-credential-create-failure-u2f.https.html: Added.
+        * http/wpt/webauthn/public-key-credential-create-success-hid.https.html:
+        * http/wpt/webauthn/public-key-credential-create-success-u2f.https-expected.txt: Added.
+        * http/wpt/webauthn/public-key-credential-create-success-u2f.https.html: Copied from LayoutTests/http/wpt/webauthn/public-key-credential-create-success-hid.https.html.
+        * http/wpt/webauthn/public-key-credential-get-failure-hid-silent.https.html:
+        * http/wpt/webauthn/public-key-credential-get-failure-hid.https.html:
+        * http/wpt/webauthn/public-key-credential-get-failure-u2f-silent.https-expected.txt: Added.
+        * http/wpt/webauthn/public-key-credential-get-failure-u2f-silent.https.html: Added.
+        * http/wpt/webauthn/public-key-credential-get-failure-u2f.https-expected.txt: Added.
+        * http/wpt/webauthn/public-key-credential-get-failure-u2f.https.html: Added.
+        * http/wpt/webauthn/public-key-credential-get-success-hid.https.html:
+        * http/wpt/webauthn/public-key-credential-get-success-u2f.https-expected.txt: Added.
+        * http/wpt/webauthn/public-key-credential-get-success-u2f.https.html: Added.
+        * http/wpt/webauthn/resources/util.js:
+        * platform/ios-wk2/TestExpectations:
+
 2019-01-08  Youenn Fablet  <youenn@apple.com>
 
         service worker fetch handler results in bad referrer
index e24405f..c46ee39 100644 (file)
@@ -64,7 +64,7 @@
 
     promise_test(function(t) {
         if (window.testRunner)
-            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "wrong-channel-id", payloadBase64:testDummyMessagePayloadBase64 } });
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "wrong-channel-id", payloadBase64:[testDummyMessagePayloadBase64] } });
         return promiseRejects(t, "UnknownError", navigator.credentials.create(defaultOptions), "Unknown internal error. Error code: -1");
     }, "CTAP HID with request::msg stage wrong channel id error in a mock hid authenticator.");
 </script>
index db02d54..8e5d095 100644 (file)
@@ -21,7 +21,7 @@
 
     promise_test(function(t) {
         if (window.testRunner)
-            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "success", payloadBase64: testCreationMessageBase64, keepAlive: true } });
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "success", payloadBase64: [testCreationMessageBase64], keepAlive: true } });
         return navigator.credentials.create(defaultOptions).then(credential => {
             assert_not_equals(credential, undefined);
             assert_not_equals(credential, null);
@@ -30,7 +30,7 @@
 
     promise_test(function(t) {
         if (window.testRunner)
-            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "success", payloadBase64: testCreationMessageBase64, fastDataArrival: true } });
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "success", payloadBase64: [testCreationMessageBase64], fastDataArrival: true } });
         return navigator.credentials.create(defaultOptions).then(credential => {
             assert_not_equals(credential, undefined);
             assert_not_equals(credential, null);
@@ -39,7 +39,7 @@
 
     promise_test(function(t) {
         if (window.testRunner)
-            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "info", subStage: "init", error: "empty-report", payloadBase64: testCreationMessageBase64, continueAfterErrorData: true } });
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "info", subStage: "init", error: "empty-report", payloadBase64: [testCreationMessageBase64], continueAfterErrorData: true } });
         return navigator.credentials.create(defaultOptions).then(credential => {
             assert_not_equals(credential, undefined);
             assert_not_equals(credential, null);
@@ -48,7 +48,7 @@
 
     promise_test(function(t) {
         if (window.testRunner)
-            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "info", subStage: "init", error: "wrong-channel-id", payloadBase64: testCreationMessageBase64, continueAfterErrorData: true } });
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "info", subStage: "init", error: "wrong-channel-id", payloadBase64: [testCreationMessageBase64], continueAfterErrorData: true } });
         return navigator.credentials.create(defaultOptions).then(credential => {
             assert_not_equals(credential, undefined);
             assert_not_equals(credential, null);
@@ -57,7 +57,7 @@
 
     promise_test(function(t) {
         if (window.testRunner)
-            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "info", subStage: "init", error: "wrong-nonce", payloadBase64: testCreationMessageBase64, continueAfterErrorData: true } });
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "info", subStage: "init", error: "wrong-nonce", payloadBase64: [testCreationMessageBase64], continueAfterErrorData: true } });
         return navigator.credentials.create(defaultOptions).then(credential => {
             assert_not_equals(credential, undefined);
             assert_not_equals(credential, null);
index b9953f9..66004a3 100644 (file)
@@ -22,7 +22,7 @@
         };
 
         if (window.testRunner)
-            testRunner.setWebAuthenticationMockConfiguration({ silentFailure: true, hid: { stage: "request", subStage: "msg", error: "malicious-payload", payloadBase64: testDummyMessagePayloadBase64 } });
+            testRunner.setWebAuthenticationMockConfiguration({ silentFailure: true, hid: { stage: "request", subStage: "msg", error: "malicious-payload", payloadBase64: [testDummyMessagePayloadBase64] } });
         return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.");
     }, "PublicKeyCredential's [[create]] with malicious payload in a mock hid authenticator.");
 
index 2dde6d0..f163a39 100644 (file)
@@ -46,7 +46,7 @@
         };
 
         if (window.testRunner)
-            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "malicious-payload", payloadBase64: testDummyMessagePayloadBase64 } });
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "malicious-payload", payloadBase64: [testDummyMessagePayloadBase64] } });
         return promiseRejects(t, "UnknownError", navigator.credentials.create(options), "Unknown internal error. Error code: -1");
     }, "PublicKeyCredential's [[create]] with malicious payload in a mock hid authenticator.");
 
diff --git a/LayoutTests/http/wpt/webauthn/public-key-credential-create-failure-u2f-silent.https-expected.txt b/LayoutTests/http/wpt/webauthn/public-key-credential-create-failure-u2f-silent.https-expected.txt
new file mode 100644 (file)
index 0000000..1063b7a
--- /dev/null
@@ -0,0 +1,9 @@
+
+PASS PublicKeyCredential's [[create]] with malformed APDU payload in a mock hid authenticator. 
+PASS PublicKeyCredential's [[create]] with malformed U2F register response in a mock hid authenticator. 
+PASS PublicKeyCredential's [[create]] with register command error in a mock hid authenticator. 
+PASS PublicKeyCredential's [[create]] with bogus command error in a mock hid authenticator. 
+PASS PublicKeyCredential's [[create]] with first exclude credential matched in a mock hid authenticator. 
+PASS PublicKeyCredential's [[create]] with second exclude credential matched in a mock hid authenticator. 
+PASS PublicKeyCredential's [[create]] with first exclude credential matched in a mock hid authenticator. Test of user presence. 
+
diff --git a/LayoutTests/http/wpt/webauthn/public-key-credential-create-failure-u2f-silent.https.html b/LayoutTests/http/wpt/webauthn/public-key-credential-create-failure-u2f-silent.https.html
new file mode 100644 (file)
index 0000000..5417998
--- /dev/null
@@ -0,0 +1,165 @@
+<!DOCTYPE html>
+<title>Web Authentication API: PublicKeyCredential's [[create]] failure cases with a mock u2f authenticator.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/util.js"></script>
+<script>
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: asciiToUint8Array("123456"),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                timeout: 10
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ silentFailure: true, hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: ["AQ=="] } });
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.");
+    }, "PublicKeyCredential's [[create]] with malformed APDU payload in a mock hid authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: asciiToUint8Array("123456"),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                timeout: 10
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ silentFailure: true, hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: [testU2fApduNoErrorOnlyResponseBase64] } });
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.");
+    }, "PublicKeyCredential's [[create]] with malformed U2F register response in a mock hid authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: asciiToUint8Array("123456"),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                timeout: 10
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ silentFailure: true, hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: [testU2fApduInsNotSupportedOnlyResponseBase64] } });
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.");
+    }, "PublicKeyCredential's [[create]] with register command error in a mock hid authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: asciiToUint8Array("123456"),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                excludeCredentials: [{ type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }],
+                timeout: 10
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ silentFailure: true, hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: [testU2fApduNoErrorOnlyResponseBase64, testU2fApduInsNotSupportedOnlyResponseBase64] } });
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.");
+    }, "PublicKeyCredential's [[create]] with bogus command error in a mock hid authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: asciiToUint8Array("123456"),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                excludeCredentials: [{ type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }],
+                timeout: 10
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ silentFailure: true, hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: [testU2fApduNoErrorOnlyResponseBase64, testU2fApduNoErrorOnlyResponseBase64] } });
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.");
+    }, "PublicKeyCredential's [[create]] with first exclude credential matched in a mock hid authenticator.");
+
+    // Match the second exclude credential.
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: asciiToUint8Array("123456"),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                excludeCredentials: [{ type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }, { type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }], // The content doesn't matter.
+                timeout: 10
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ silentFailure: true, hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: [testU2fApduWrongDataOnlyResponseBase64, testU2fApduNoErrorOnlyResponseBase64, testU2fApduNoErrorOnlyResponseBase64] } });
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.");
+    }, "PublicKeyCredential's [[create]] with second exclude credential matched in a mock hid authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: asciiToUint8Array("123456"),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                excludeCredentials: [{ type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }],
+                timeout: 10
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ silentFailure: true, hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: [testU2fApduConditionsNotSatisfiedOnlyResponseBase64, testU2fApduConditionsNotSatisfiedOnlyResponseBase64, testU2fApduConditionsNotSatisfiedOnlyResponseBase64, testU2fApduNoErrorOnlyResponseBase64] } });
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.create(options), "Operation timed out.");
+    }, "PublicKeyCredential's [[create]] with first exclude credential matched in a mock hid authenticator. Test of user presence.");
+</script>
diff --git a/LayoutTests/http/wpt/webauthn/public-key-credential-create-failure-u2f.https-expected.txt b/LayoutTests/http/wpt/webauthn/public-key-credential-create-failure-u2f.https-expected.txt
new file mode 100644 (file)
index 0000000..1063b7a
--- /dev/null
@@ -0,0 +1,9 @@
+
+PASS PublicKeyCredential's [[create]] with malformed APDU payload in a mock hid authenticator. 
+PASS PublicKeyCredential's [[create]] with malformed U2F register response in a mock hid authenticator. 
+PASS PublicKeyCredential's [[create]] with register command error in a mock hid authenticator. 
+PASS PublicKeyCredential's [[create]] with bogus command error in a mock hid authenticator. 
+PASS PublicKeyCredential's [[create]] with first exclude credential matched in a mock hid authenticator. 
+PASS PublicKeyCredential's [[create]] with second exclude credential matched in a mock hid authenticator. 
+PASS PublicKeyCredential's [[create]] with first exclude credential matched in a mock hid authenticator. Test of user presence. 
+
diff --git a/LayoutTests/http/wpt/webauthn/public-key-credential-create-failure-u2f.https.html b/LayoutTests/http/wpt/webauthn/public-key-credential-create-failure-u2f.https.html
new file mode 100644 (file)
index 0000000..157d9e8
--- /dev/null
@@ -0,0 +1,158 @@
+<!DOCTYPE html>
+<title>Web Authentication API: PublicKeyCredential's [[create]] failure cases with a mock u2f authenticator.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/util.js"></script>
+<script>
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: asciiToUint8Array("123456"),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }]
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: ["AQ=="] } });
+        return promiseRejects(t, "UnknownError", navigator.credentials.create(options), "Couldn't parse the APDU response.");
+    }, "PublicKeyCredential's [[create]] with malformed APDU payload in a mock hid authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: asciiToUint8Array("123456"),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }]
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: [testU2fApduNoErrorOnlyResponseBase64] } });
+        return promiseRejects(t, "UnknownError", navigator.credentials.create(options), "Couldn't parse the U2F register response.");
+    }, "PublicKeyCredential's [[create]] with malformed U2F register response in a mock hid authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: asciiToUint8Array("123456"),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }]
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: [testU2fApduInsNotSupportedOnlyResponseBase64] } });
+        return promiseRejects(t, "UnknownError", navigator.credentials.create(options), "Unknown internal error. Error code: 27904");
+    }, "PublicKeyCredential's [[create]] with register command error in a mock hid authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: asciiToUint8Array("123456"),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                excludeCredentials: [{ type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }]
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: [testU2fApduNoErrorOnlyResponseBase64, testU2fApduInsNotSupportedOnlyResponseBase64] } });
+        return promiseRejects(t, "UnknownError", navigator.credentials.create(options), "Unknown internal error. Error code: 27904");
+    }, "PublicKeyCredential's [[create]] with bogus command error in a mock hid authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: asciiToUint8Array("123456"),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                excludeCredentials: [{ type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }]
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: [testU2fApduNoErrorOnlyResponseBase64, testU2fApduNoErrorOnlyResponseBase64] } });
+        return promiseRejects(t, "InvalidStateError", navigator.credentials.create(options), "At least one credential matches an entry of the excludeCredentials list in the authenticator.");
+    }, "PublicKeyCredential's [[create]] with first exclude credential matched in a mock hid authenticator.");
+
+    // Match the second exclude credential.
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: asciiToUint8Array("123456"),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                excludeCredentials: [{ type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }, { type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }] // The content doesn't matter.
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: [testU2fApduWrongDataOnlyResponseBase64, testU2fApduNoErrorOnlyResponseBase64, testU2fApduNoErrorOnlyResponseBase64] } });
+        return promiseRejects(t, "InvalidStateError", navigator.credentials.create(options), "At least one credential matches an entry of the excludeCredentials list in the authenticator.");
+    }, "PublicKeyCredential's [[create]] with second exclude credential matched in a mock hid authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "example.com"
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: asciiToUint8Array("123456"),
+                    displayName: "John",
+                },
+                challenge: asciiToUint8Array("123456"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                excludeCredentials: [{ type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }]
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: [testU2fApduConditionsNotSatisfiedOnlyResponseBase64, testU2fApduConditionsNotSatisfiedOnlyResponseBase64, testU2fApduConditionsNotSatisfiedOnlyResponseBase64, testU2fApduNoErrorOnlyResponseBase64] } });
+        return promiseRejects(t, "InvalidStateError", navigator.credentials.create(options), "At least one credential matches an entry of the excludeCredentials list in the authenticator.");
+    }, "PublicKeyCredential's [[create]] with first exclude credential matched in a mock hid authenticator. Test of user presence.");
+</script>
index 5d93b80..0d79a9a 100644 (file)
@@ -7,7 +7,7 @@
 <script>
     // Default mock configuration. Tests need to override if they need different configuration.
     if (window.testRunner)
-        testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "success", payloadBase64: testCreationMessageBase64 } });
+        testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "success", payloadBase64: [testCreationMessageBase64] } });
 
     function checkResult(credential)
     {
diff --git a/LayoutTests/http/wpt/webauthn/public-key-credential-create-success-u2f.https-expected.txt b/LayoutTests/http/wpt/webauthn/public-key-credential-create-success-u2f.https-expected.txt
new file mode 100644 (file)
index 0000000..57b2617
--- /dev/null
@@ -0,0 +1,6 @@
+
+PASS PublicKeyCredential's [[create]] with minimum options in a mock u2f authenticator. 
+PASS PublicKeyCredential's [[create]] with excludeCredentials in a mock u2f authenticator. 
+PASS PublicKeyCredential's [[create]] with excludeCredentials in a mock u2f authenticator. 2 
+PASS PublicKeyCredential's [[create]] with test of user presence in a mock u2f authenticator. 
+
diff --git a/LayoutTests/http/wpt/webauthn/public-key-credential-create-success-u2f.https.html b/LayoutTests/http/wpt/webauthn/public-key-credential-create-success-u2f.https.html
new file mode 100644 (file)
index 0000000..aab18d1
--- /dev/null
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<title>Web Authentication API: PublicKeyCredential's [[create]] success cases with a mock u2f authenticator.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/util.js"></script>
+<script src="./resources/cbor.js"></script>
+<script>
+    function checkResult(credential)
+    {
+        // Check response
+        assert_array_equals(Base64URL.parse(credential.id), Base64URL.parse(testU2fCredentialIdBase64));
+        assert_equals(credential.type, 'public-key');
+        assert_array_equals(new Uint8Array(credential.rawId), Base64URL.parse(testU2fCredentialIdBase64));
+        assert_equals(bytesToASCIIString(credential.response.clientDataJSON), '{"type":"webauthn.create","challenge":"MTIzNDU2","origin":"https://localhost:9443"}');
+        assert_throws("NotSupportedError", () => { credential.getClientExtensionResults() });
+
+        // Check attestation
+        const attestationObject = CBOR.decode(credential.response.attestationObject);
+        assert_equals(attestationObject.fmt, "fido-u2f");
+        // Check authData
+        const authData = decodeAuthData(attestationObject.authData);
+        assert_equals(bytesToHexString(authData.rpIdHash), "49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763");
+        assert_equals(authData.flags, 65);
+        assert_equals(authData.counter, 0);
+        assert_equals(bytesToHexString(authData.aaguid), "00000000000000000000000000000000");
+        assert_array_equals(authData.credentialID, Base64URL.parse(testU2fCredentialIdBase64));
+        // Check fido-u2f attestation
+        assert_true(checkPublicKey(authData.publicKey));
+        assert_equals(attestationObject.attStmt.x5c.length, 1);
+    }
+
+    promise_test(t => {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "localhost",
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: Base64URL.parse(testUserhandleBase64),
+                    displayName: "Appleseed",
+                },
+                challenge: Base64URL.parse("MTIzNDU2"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                timeout: 10
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "success", isU2f: true, payloadBase64: [testU2fRegisterResponse] } });
+        return navigator.credentials.create(options).then(credential => {
+            checkResult(credential);
+        });
+    }, "PublicKeyCredential's [[create]] with minimum options in a mock u2f authenticator.");
+
+    promise_test(t => {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "localhost",
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: Base64URL.parse(testUserhandleBase64),
+                    displayName: "Appleseed",
+                },
+                challenge: Base64URL.parse("MTIzNDU2"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                excludeCredentials: [{ type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }],
+                timeout: 10
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "success", isU2f: true, payloadBase64: [testU2fApduWrongDataOnlyResponseBase64, testU2fRegisterResponse] } });
+        return navigator.credentials.create(options).then(credential => {
+            checkResult(credential);
+        });
+    }, "PublicKeyCredential's [[create]] with excludeCredentials in a mock u2f authenticator.");
+
+    promise_test(t => {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "localhost",
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: Base64URL.parse(testUserhandleBase64),
+                    displayName: "Appleseed",
+                },
+                challenge: Base64URL.parse("MTIzNDU2"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                excludeCredentials: [{ type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }, { type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }], // The content doesn't matter.
+                timeout: 10
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "success", isU2f: true, payloadBase64: [testU2fApduWrongDataOnlyResponseBase64, testU2fApduWrongDataOnlyResponseBase64, testU2fRegisterResponse] } });
+        return navigator.credentials.create(options).then(credential => {
+            checkResult(credential);
+        });
+    }, "PublicKeyCredential's [[create]] with excludeCredentials in a mock u2f authenticator. 2");
+
+    promise_test(t => {
+        const options = {
+            publicKey: {
+                rp: {
+                    name: "localhost",
+                },
+                user: {
+                    name: "John Appleseed",
+                    id: Base64URL.parse(testUserhandleBase64),
+                    displayName: "Appleseed",
+                },
+                challenge: Base64URL.parse("MTIzNDU2"),
+                pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+                timeout: 500
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "success", isU2f: true, payloadBase64: [testU2fApduConditionsNotSatisfiedOnlyResponseBase64, testU2fApduConditionsNotSatisfiedOnlyResponseBase64, testU2fRegisterResponse] } });
+        return navigator.credentials.create(options).then(credential => {
+            checkResult(credential);
+        });
+    }, "PublicKeyCredential's [[create]] with test of user presence in a mock u2f authenticator.");
+</script>
index e3636c3..144ba9d 100644 (file)
@@ -13,7 +13,7 @@
         };
 
         if (window.testRunner)
-            testRunner.setWebAuthenticationMockConfiguration({ silentFailure: true, hid: { stage: "request", subStage: "msg", error: "malicious-payload", payloadBase64: testDummyMessagePayloadBase64 } });
+            testRunner.setWebAuthenticationMockConfiguration({ silentFailure: true, hid: { stage: "request", subStage: "msg", error: "malicious-payload", payloadBase64: [testDummyMessagePayloadBase64] } });
         return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "Operation timed out.");
     }, "PublicKeyCredential's [[get]] with malicious payload in a mock hid authenticator.");
 
index ec6b0a8..5ac2139 100644 (file)
@@ -32,7 +32,7 @@
         };
 
         if (window.testRunner)
-            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "malicious-payload", payloadBase64: testDummyMessagePayloadBase64 } });
+            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");
     }, "PublicKeyCredential's [[get]] with malicious payload in a mock hid authenticator.");
 
diff --git a/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-u2f-silent.https-expected.txt b/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-u2f-silent.https-expected.txt
new file mode 100644 (file)
index 0000000..5a06145
--- /dev/null
@@ -0,0 +1,5 @@
+
+PASS PublicKeyCredential's [[get]] with malformed sign response in a mock hid authenticator. 
+PASS PublicKeyCredential's [[get]] with no matched allow credentials in a mock hid authenticator. 
+PASS PublicKeyCredential's [[get]] with no matched allow credentials in a mock hid authenticator. 2 
+
diff --git a/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-u2f-silent.https.html b/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-u2f-silent.https.html
new file mode 100644 (file)
index 0000000..8a7cd8a
--- /dev/null
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<title>Web Authentication API: PublicKeyCredential's [[get]] failure cases with a mock u2f authenticator.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/util.js"></script>
+<script>
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                challenge: asciiToUint8Array("123456"),
+                allowCredentials: [{ type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }],
+                timeout: 10
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ silentFailure: true, hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: [testU2fApduNoErrorOnlyResponseBase64] } });
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "Operation timed out.");
+    }, "PublicKeyCredential's [[get]] with malformed sign response in a mock hid authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                challenge: asciiToUint8Array("123456"),
+                allowCredentials: [{ type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }],
+                timeout: 10
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ silentFailure: true, hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: [testU2fApduWrongDataOnlyResponseBase64] } });
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "Operation timed out.");
+    }, "PublicKeyCredential's [[get]] with no matched allow credentials in a mock hid authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                challenge: asciiToUint8Array("123456"),
+                allowCredentials: [{ type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }, { type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }],
+                timeout: 10
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ silentFailure: true, hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: [testU2fApduWrongDataOnlyResponseBase64, testU2fApduWrongDataOnlyResponseBase64] } });
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "Operation timed out.");
+    }, "PublicKeyCredential's [[get]] with no matched allow credentials in a mock hid authenticator. 2");
+</script>
diff --git a/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-u2f.https-expected.txt b/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-u2f.https-expected.txt
new file mode 100644 (file)
index 0000000..5a06145
--- /dev/null
@@ -0,0 +1,5 @@
+
+PASS PublicKeyCredential's [[get]] with malformed sign response in a mock hid authenticator. 
+PASS PublicKeyCredential's [[get]] with no matched allow credentials in a mock hid authenticator. 
+PASS PublicKeyCredential's [[get]] with no matched allow credentials in a mock hid authenticator. 2 
+
diff --git a/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-u2f.https.html b/LayoutTests/http/wpt/webauthn/public-key-credential-get-failure-u2f.https.html
new file mode 100644 (file)
index 0000000..b435b5f
--- /dev/null
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<title>Web Authentication API: PublicKeyCredential's [[get]] failure cases with a mock u2f authenticator.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/util.js"></script>
+<script>
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                challenge: asciiToUint8Array("123456"),
+                allowCredentials: [{ type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }]
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: [testU2fApduNoErrorOnlyResponseBase64] } });
+        return promiseRejects(t, "UnknownError", navigator.credentials.get(options), "Couldn't parse the U2F sign response.");
+    }, "PublicKeyCredential's [[get]] with malformed sign response in a mock hid authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                challenge: asciiToUint8Array("123456"),
+                allowCredentials: [{ type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }]
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: [testU2fApduWrongDataOnlyResponseBase64] } });
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "No credentials from the allowCredentials list is found in the authenticator.");
+    }, "PublicKeyCredential's [[get]] with no matched allow credentials in a mock hid authenticator.");
+
+    promise_test(function(t) {
+        const options = {
+            publicKey: {
+                challenge: asciiToUint8Array("123456"),
+                allowCredentials: [{ type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }, { type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }]
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "malicious-payload", isU2f: true, payloadBase64: [testU2fApduWrongDataOnlyResponseBase64, testU2fApduWrongDataOnlyResponseBase64] } });
+        return promiseRejects(t, "NotAllowedError", navigator.credentials.get(options), "No credentials from the allowCredentials list is found in the authenticator.");
+    }, "PublicKeyCredential's [[get]] with no matched allow credentials in a mock hid authenticator. 2");
+</script>
index 67db831..7436246 100644 (file)
@@ -6,7 +6,7 @@
 <script>
     // Default mock configuration. Tests need to override if they need different configuration.
     if (window.testRunner)
-        testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "success", payloadBase64: testAssertionMessageBase64 } });
+        testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "success", payloadBase64: [testAssertionMessageBase64] } });
 
     function checkResult(credential)
     {
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
new file mode 100644 (file)
index 0000000..4c4a39e
--- /dev/null
@@ -0,0 +1,5 @@
+
+PASS PublicKeyCredential's [[get]] with minimum options in a mock hid authenticator. 
+PASS PublicKeyCredential's [[get]] with more allow credentials in a mock hid authenticator. 
+PASS PublicKeyCredential's [[get]] with test of user presence in a mock hid authenticator. 
+
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
new file mode 100644 (file)
index 0000000..8a6aafc
--- /dev/null
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<title>Web Authentication API: PublicKeyCredential's [[get]] success cases with a mock u2f authenticator.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/util.js"></script>
+<script>
+    function checkResult(credential)
+    {
+        // Check respond
+        assert_array_equals(Base64URL.parse(credential.id), Base64URL.parse(testU2fCredentialIdBase64));
+        assert_equals(credential.type, 'public-key');
+        assert_array_equals(new Uint8Array(credential.rawId), Base64URL.parse(testU2fCredentialIdBase64));
+        assert_equals(bytesToASCIIString(credential.response.clientDataJSON), '{"type":"webauthn.get","challenge":"MTIzNDU2","origin":"https://localhost:9443"}');
+        assert_equals(credential.response.userHandle, null);
+
+        // Check authData
+        const authData = decodeAuthData(new Uint8Array(credential.response.authenticatorData));
+        assert_equals(bytesToHexString(authData.rpIdHash), "49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763");
+        assert_equals(authData.flags, 1);
+        assert_equals(authData.counter, 59);
+    }
+
+    promise_test(t => {
+        const options = {
+            publicKey: {
+                challenge: Base64URL.parse("MTIzNDU2"),
+                allowCredentials: [{ type: "public-key", id: Base64URL.parse(testU2fCredentialIdBase64) }],
+                timeout: 10
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "success", isU2f: true, payloadBase64: [testU2fSignResponse] } });
+        return navigator.credentials.get(options).then(credential => {
+            return checkResult(credential);
+        });
+    }, "PublicKeyCredential's [[get]] with minimum options in a mock hid authenticator.");
+
+    promise_test(t => {
+        const options = {
+            publicKey: {
+                challenge: Base64URL.parse("MTIzNDU2"),
+                allowCredentials: [{ type: "public-key", id: Base64URL.parse(testCredentialIdBase64) }, { type: "public-key", id: Base64URL.parse(testU2fCredentialIdBase64) }],
+                timeout: 10
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "success", isU2f: true, payloadBase64: [testU2fApduWrongDataOnlyResponseBase64, testU2fSignResponse] } });
+        return navigator.credentials.get(options).then(credential => {
+            return checkResult(credential);
+        });
+    }, "PublicKeyCredential's [[get]] with more allow credentials in a mock hid authenticator.");
+
+    promise_test(t => {
+        const options = {
+            publicKey: {
+                challenge: Base64URL.parse("MTIzNDU2"),
+                allowCredentials: [{ type: "public-key", id: Base64URL.parse(testU2fCredentialIdBase64) }],
+                timeout: 500
+            }
+        };
+
+        if (window.testRunner)
+            testRunner.setWebAuthenticationMockConfiguration({ hid: { stage: "request", subStage: "msg", error: "success", isU2f: true, payloadBase64: [testU2fApduConditionsNotSatisfiedOnlyResponseBase64, testU2fApduConditionsNotSatisfiedOnlyResponseBase64, testU2fSignResponse] } });
+        return navigator.credentials.get(options).then(credential => {
+            return checkResult(credential);
+        });
+    }, "PublicKeyCredential's [[get]] with test of user presence in a mock hid authenticator.");
+</script>
index 83f4b48..f60986b 100644 (file)
@@ -69,6 +69,34 @@ const testAssertionMessageBase64 =
     "Z51VstuQkuHI2eXh0Ct1gPC0gSx3CWLh5I9a2AEAAABQA1hHMEUCIQCSFTuuBWgB" +
     "4/F0VB7DlUVM09IHPmxe1MzHUwRoCRZbCAIgGKov6xoAx2MEf6/6qNs8OutzhP2C" +
     "QoJ1L7Fe64G9uBc=";
+const testU2fApduNoErrorOnlyResponseBase64 = "kAA=";
+const testU2fApduInsNotSupportedOnlyResponseBase64 = "bQA=";
+const testU2fApduWrongDataOnlyResponseBase64 = "aoA=";
+const testU2fApduConditionsNotSatisfiedOnlyResponseBase64 = "aYU=";
+const testU2fRegisterResponse =
+    "BQTodiWJbuTkbcAydm6Ah5YvNt+d/otWfzdjAVsZkKYOFCfeYS1mQYvaGVBYHrxc" +
+    "jB2tcQyxTCL4yXBF9GEvsgyRQD69ib937FCXVe6cJjXvqqx7K5xc7xc2w3F9pIU0" +
+    "yMa2VNf/lF9QtcxOeAVb3TlrZPeNosX5YgDM1BXNCP5CADgwggJKMIIBMqADAgEC" +
+    "AgQEbIgiMA0GCSqGSIb3DQEBCwUAMC4xLDAqBgNVBAMTI1l1YmljbyBVMkYgUm9v" +
+    "dCBDQSBTZXJpYWwgNDU3MjAwNjMxMCAXDTE0MDgwMTAwMDAwMFoYDzIwNTAwOTA0" +
+    "MDAwMDAwWjAsMSowKAYDVQQDDCFZdWJpY28gVTJGIEVFIFNlcmlhbCAyNDkxODIz" +
+    "MjQ3NzAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ8yrksy5cofujmOUN+IfzW" +
+    "tvFlstWj89sTHTHBa3QrtHbY0emQgOtUbJu99VbmIQ/UJ4WJnnjMWJ6+MQ9s25/0" +
+    "ozswOTAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuMjATBgsrBgEE" +
+    "AYLlHAIBAQQEAwIEMDANBgkqhkiG9w0BAQsFAAOCAQEAn5sFIki8TPQsxZkfyqus" +
+    "m2Ubvlvc3I7wrSwcH/s20YcV1C54skkiT5LH5uegXEnw5+TIgb8ulPReSiGDPXRW" +
+    "hR0PbBRaKVQMh08wksk0tD0iK4liwPQQzvHbdYkq8Ra0Spb101reo4IvxxRvYAQ4" +
+    "W8tptlyZ5+tpGXhnA8DYzUHo91zKRKqKtyWtjnmf86hpam8bJlbmMbHkAYPAj9pT" +
+    "+kqPhaBWk5RK4XmhM50ALRXKvYEAkOxyLvXe+ZZaNx1BXWJLaKJwfK2XvN0Xha+X" +
+    "4ljzPfVqAxqgNW2OjV68rcdOBxY2xrEQrOXMm5Df6srmQP8bsPH+XbTv96lfBgcz" +
+    "9TBFAiAyR3nGjzOAKIoRl7YJX3puubGxwSf2auEqmf6FMuwjuQIhAOOVFqxNYe5k" +
+    "BE1QtBWmpNTYS6bYlctat6GqfQgd40H6kAA=";
+const testU2fCredentialIdBase64 =
+    "Pr2Jv3fsUJdV7pwmNe-qrHsrnFzvFzbDcX2khTTIxrZU1_-UX1C1zE54BVvdOWtk" +
+    "942ixfliAMzUFc0I_kIAOA";
+const testU2fSignResponse =
+    "AQAAADswRAIge94KUqwfTIsn4AOjcM1mpMcRjdItVEeDX0W5nGhCP/cCIDxRe0eH" +
+    "f4V4LeEAhqeD0effTjY553H19q+jWq1Tc4WOkAA=";
 
 const RESOURCES_DIR = "/WebKit/webauthn/resources/";
 
index 08370fe..034f122 100644 (file)
@@ -1325,6 +1325,12 @@ http/wpt/webauthn/public-key-credential-create-success-hid.https.html [ Skip ]
 http/wpt/webauthn/public-key-credential-get-failure-hid-silent.https.html [ Skip ]
 http/wpt/webauthn/public-key-credential-get-failure-hid.https.html [ Skip ]
 http/wpt/webauthn/public-key-credential-get-success-hid.https.html [ Skip ]
+http/wpt/webauthn/public-key-credential-create-failure-u2f-silent.https.html [ Skip ]
+http/wpt/webauthn/public-key-credential-create-failure-u2f.https.html [ Skip ]
+http/wpt/webauthn/public-key-credential-create-success-u2f.https.html [ Skip ]
+http/wpt/webauthn/public-key-credential-get-failure-u2f-silent.https.html [ Skip ]
+http/wpt/webauthn/public-key-credential-get-failure-u2f.https.html [ Skip ]
+http/wpt/webauthn/public-key-credential-get-success-u2f.https.html [ Skip ]
 
 # FIXME: Unskip these tests once we have the fix for <rdar://problem/44930119>.
 fast/forms/auto-fill-button/caps-lock-indicator-should-be-visible-after-hiding-auto-fill-strong-password-button.html [ Skip ]
index 1975dd2..50a6706 100644 (file)
@@ -1,3 +1,30 @@
+2019-01-08  Jiewen Tan  <jiewen_tan@apple.com>
+
+        [WebAuthN] Support U2F HID Authenticators on macOS
+        https://bugs.webkit.org/show_bug.cgi?id=191535
+        <rdar://problem/47102027>
+
+        Reviewed by Brent Fulgham.
+
+        This patch changes U2fCommandConstructor to produce register commands with
+        enforcing test of user presence. Otherwise, authenticators would silently
+        generate credentials. It also renames readFromU2fSignResponse to
+        readU2fSignResponse.
+
+        Tests: http/wpt/webauthn/public-key-credential-create-failure-u2f-silent.https.html
+               http/wpt/webauthn/public-key-credential-create-failure-u2f.https.html
+               http/wpt/webauthn/public-key-credential-create-success-u2f.https.html
+               http/wpt/webauthn/public-key-credential-get-failure-u2f-silent.https.html
+               http/wpt/webauthn/public-key-credential-get-failure-u2f.https.html
+               http/wpt/webauthn/public-key-credential-get-success-u2f.https.html
+
+        * Modules/webauthn/fido/U2fCommandConstructor.cpp:
+        (fido::WebCore::constructU2fRegisterCommand):
+        * Modules/webauthn/fido/U2fResponseConverter.cpp:
+        (fido::readU2fSignResponse):
+        (fido::readFromU2fSignResponse): Deleted.
+        * Modules/webauthn/fido/U2fResponseConverter.h:
+
 2019-01-08  Wenson Hsieh  <wenson_hsieh@apple.com>
 
         [iOS] Dispatch a synthetic mousedown event prior to starting drags
index 1606ab0..21ba609 100644 (file)
@@ -54,6 +54,8 @@ static Vector<uint8_t> constructU2fRegisterCommand(const Vector<uint8_t>& applic
 
     apdu::ApduCommand command;
     command.setIns(static_cast<uint8_t>(U2fApduInstruction::kRegister));
+    // This is needed for test of user presence even though the spec doesn't specify it.
+    command.setP1(kP1EnforceUserPresenceAndSign);
     command.setData(WTFMove(data));
     command.setResponseLength(apdu::ApduCommand::kApduMaxResponseLength);
     return command.getEncodedCommand();
index c45a171..cb46781 100644 (file)
@@ -173,7 +173,7 @@ Optional<PublicKeyCredentialData> readU2fRegisterResponse(const String& rpId, co
     return PublicKeyCredentialData { ArrayBuffer::create(credentialId.data(), credentialId.size()), true, nullptr, ArrayBuffer::create(attestationObject.data(), attestationObject.size()), nullptr, nullptr, nullptr };
 }
 
-Optional<PublicKeyCredentialData> readFromU2fSignResponse(const String& rpId, const Vector<uint8_t>& keyHandle, const Vector<uint8_t>& u2fData)
+Optional<PublicKeyCredentialData> readU2fSignResponse(const String& rpId, const Vector<uint8_t>& keyHandle, const Vector<uint8_t>& u2fData)
 {
     if (keyHandle.isEmpty() || u2fData.size() <= signatureIndex)
         return WTF::nullopt;
index d6a41f8..4c6d24e 100644 (file)
@@ -42,7 +42,7 @@ WEBCORE_EXPORT Optional<WebCore::PublicKeyCredentialData> readU2fRegisterRespons
 
 // Converts a U2F authentication response to WebAuthN getAssertion response.
 // https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html#u2f-authenticatorGetAssertion-interoperability
-WEBCORE_EXPORT Optional<WebCore::PublicKeyCredentialData> readFromU2fSignResponse(const String& rpId, const Vector<uint8_t>& keyHandle, const Vector<uint8_t>& u2fData);
+WEBCORE_EXPORT Optional<WebCore::PublicKeyCredentialData> readU2fSignResponse(const String& rpId, const Vector<uint8_t>& keyHandle, const Vector<uint8_t>& u2fData);
 
 } // namespace fido
 
index f6c2a34..b6fd324 100644 (file)
@@ -1,3 +1,52 @@
+2019-01-08  Jiewen Tan  <jiewen_tan@apple.com>
+
+        [WebAuthN] Support U2F HID Authenticators on macOS
+        https://bugs.webkit.org/show_bug.cgi?id=191535
+        <rdar://problem/47102027>
+
+        Reviewed by Brent Fulgham.
+
+        This patch implements the support for U2F authenticators, and enables it for hid devices.
+        It follows the CTAP spec to map WebAuthN requests to U2F commands and return the responses:
+        https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html#u2f-interoperability
+        Most of the parts are done before this patch, this patch focues on: 7.2.2 and 7.3.2.
+
+        Besides implementing the U2fHidAuthenticator, this patch also adds support in the mocking
+        environment for U2F authenticators. It is done by extending the stages in MockHidConnection
+        from 4 to indefinite as multi-round communications are expected to map WebAuthN requests
+        to U2F requests.
+
+        * Sources.txt:
+        * UIProcess/API/C/WKWebsiteDataStoreRef.cpp:
+        (WKWebsiteDataStoreSetWebAuthenticationMockConfiguration):
+        * UIProcess/WebAuthentication/Cocoa/HidService.mm:
+        (WebKit::HidService::continueAddDeviceAfterGetInfo):
+        * UIProcess/WebAuthentication/fido/CtapHidDriver.cpp:
+        (WebKit::CtapHidDriver::continueAfterChannelAllocated):
+        * UIProcess/WebAuthentication/fido/CtapHidDriver.h:
+        (WebKit::CtapHidDriver::setProtocol):
+        * UIProcess/WebAuthentication/fido/U2fHidAuthenticator.cpp: Added.
+        (WebKit::U2fHidAuthenticator::U2fHidAuthenticator):
+        (WebKit::U2fHidAuthenticator::makeCredential):
+        (WebKit::U2fHidAuthenticator::checkExcludeList):
+        (WebKit::U2fHidAuthenticator::issueRegisterCommand):
+        (WebKit::U2fHidAuthenticator::getAssertion):
+        (WebKit::U2fHidAuthenticator::issueSignCommand):
+        (WebKit::U2fHidAuthenticator::issueNewCommand):
+        (WebKit::U2fHidAuthenticator::issueCommand):
+        (WebKit::U2fHidAuthenticator::responseReceived):
+        (WebKit::U2fHidAuthenticator::continueRegisterCommandAfterResponseReceived):
+        (WebKit::U2fHidAuthenticator::continueCheckOnlyCommandAfterResponseReceived):
+        (WebKit::U2fHidAuthenticator::continueBogusCommandAfterResponseReceived):
+        (WebKit::U2fHidAuthenticator::continueSignCommandAfterResponseReceived):
+        * UIProcess/WebAuthentication/fido/U2fHidAuthenticator.h: Added.
+        * UIProcess/WebAuthentication/Mock/MockHidConnection.cpp:
+        (WebKit::MockHidConnection::parseRequest):
+        (WebKit::MockHidConnection::feedReports):
+        * UIProcess/WebAuthentication/Mock/MockHidConnection.h:
+        * UIProcess/WebAuthentication/Mock/MockWebAuthenticationConfiguration.h:
+        * WebKit.xcodeproj/project.pbxproj:
+
 2019-01-08  Youenn Fablet  <youenn@apple.com>
 
         service worker fetch handler results in bad referrer
index 496e88c..f58001e 100644 (file)
@@ -385,6 +385,10 @@ UIProcess/Plugins/PluginProcessProxy.cpp
 UIProcess/UserContent/WebScriptMessageHandler.cpp
 UIProcess/UserContent/WebUserContentControllerProxy.cpp
 
+UIProcess/WebAuthentication/fido/CtapHidAuthenticator.cpp
+UIProcess/WebAuthentication/fido/CtapHidDriver.cpp
+UIProcess/WebAuthentication/fido/U2fHidAuthenticator.cpp
+
 UIProcess/WebAuthentication/Mock/MockAuthenticatorManager.cpp
 UIProcess/WebAuthentication/Mock/MockHidConnection.cpp
 UIProcess/WebAuthentication/Mock/MockHidService.cpp
index cf6caf6..61e93c8 100644 (file)
@@ -627,8 +627,11 @@ void WKWebsiteDataStoreSetWebAuthenticationMockConfiguration(WKWebsiteDataStoreR
         if (error == "wrong-nonce")
             hid.error = WebKit::MockWebAuthenticationConfiguration::Hid::Error::WrongNonce;
 
-        if (auto payloadBase64 = static_cast<WKStringRef>(WKDictionaryGetItemForKey(hidRef, adoptWK(WKStringCreateWithUTF8CString("PayloadBase64")).get())))
-            hid.payloadBase64 = WebKit::toImpl(payloadBase64)->string();
+        if (auto payloadBase64 = static_cast<WKArrayRef>(WKDictionaryGetItemForKey(hidRef, adoptWK(WKStringCreateWithUTF8CString("PayloadBase64")).get())))
+            hid.payloadBase64 = WebKit::toImpl(payloadBase64)->toStringVector();
+
+        if (auto isU2f = static_cast<WKBooleanRef>(WKDictionaryGetItemForKey(hidRef, adoptWK(WKStringCreateWithUTF8CString("IsU2f")).get())))
+            hid.isU2f = WKBooleanGetValue(isU2f);
 
         if (auto keepAlive = static_cast<WKBooleanRef>(WKDictionaryGetItemForKey(hidRef, adoptWK(WKStringCreateWithUTF8CString("KeepAlive")).get())))
             hid.keepAlive = WKBooleanGetValue(keepAlive);
index 237b1b4..3da8706 100644 (file)
@@ -31,6 +31,7 @@
 #import "CtapHidAuthenticator.h"
 #import "CtapHidDriver.h"
 #import "HidConnection.h"
+#import "U2fHidAuthenticator.h"
 #import <WebCore/DeviceRequestConverter.h>
 #import <WebCore/DeviceResponseConverter.h>
 #import <WebCore/FidoConstants.h>
@@ -108,7 +109,7 @@ void HidService::deviceAdded(IOHIDDeviceRef device)
 void HidService::continueAddDeviceAfterGetInfo(CtapHidDriver* ptr, Vector<uint8_t>&& response)
 {
     std::unique_ptr<CtapHidDriver> driver = m_drivers.take(ptr);
-    if (!driver || !observer())
+    if (!driver || !observer() || response.isEmpty())
         return;
 
     auto info = readCTAPGetInfoResponse(response);
@@ -116,8 +117,9 @@ void HidService::continueAddDeviceAfterGetInfo(CtapHidDriver* ptr, Vector<uint8_
         observer()->authenticatorAdded(CtapHidAuthenticator::create(WTFMove(driver), WTFMove(*info)));
         return;
     }
-    // FIXME(191535): Support U2F authenticators.
     LOG_ERROR("Couldn't parse a ctap get info response.");
+    driver->setProtocol(ProtocolVersion::kU2f);
+    observer()->authenticatorAdded(U2fHidAuthenticator::create(WTFMove(driver)));
 }
 
 } // namespace WebKit
index 0b17699..d86a3a4 100644 (file)
@@ -127,43 +127,48 @@ void MockHidConnection::parseRequest()
         if (previousSubStage == Mock::SubStage::Msg)
             m_stage = Mock::Stage::Request;
     }
-    if (m_requestMessage->cmd() == FidoHidDeviceCommand::kCbor)
+    if (m_requestMessage->cmd() == FidoHidDeviceCommand::kCbor || m_requestMessage->cmd() == FidoHidDeviceCommand::kMsg)
         m_subStage = Mock::SubStage::Msg;
 
-    // Set options.
     if (m_stage == Mock::Stage::Request && m_subStage == Mock::SubStage::Msg) {
-        m_requireResidentKey = false;
-        m_requireUserVerification = false;
-
-        auto payload = m_requestMessage->getMessagePayload();
-        ASSERT(payload.size());
-        auto cmd = static_cast<CtapRequestCommand>(payload[0]);
-        payload.remove(0);
-        auto requestMap = CBORReader::read(payload);
-        ASSERT(requestMap);
-
-        if (cmd == CtapRequestCommand::kAuthenticatorMakeCredential) {
-            auto it = requestMap->getMap().find(CBORValue(CtapMakeCredentialRequestOptionsKey)); // Find options.
-            if (it != requestMap->getMap().end()) {
-                auto& optionMap = it->second.getMap();
-
-                auto itr = optionMap.find(CBORValue(kResidentKeyMapKey));
-                if (itr != optionMap.end())
-                    m_requireResidentKey = itr->second.getBool();
-
-                itr = optionMap.find(CBORValue(kUserVerificationMapKey));
-                if (itr != optionMap.end())
-                    m_requireUserVerification = itr->second.getBool();
+        // Make sure we issue different msg cmd for CTAP and U2F.
+        ASSERT(m_configuration.hid->isU2f ^ (m_requestMessage->cmd() != FidoHidDeviceCommand::kMsg));
+
+        // Set options.
+        if (m_requestMessage->cmd() == FidoHidDeviceCommand::kCbor) {
+            m_requireResidentKey = false;
+            m_requireUserVerification = false;
+
+            auto payload = m_requestMessage->getMessagePayload();
+            ASSERT(payload.size());
+            auto cmd = static_cast<CtapRequestCommand>(payload[0]);
+            payload.remove(0);
+            auto requestMap = CBORReader::read(payload);
+            ASSERT(requestMap);
+
+            if (cmd == CtapRequestCommand::kAuthenticatorMakeCredential) {
+                auto it = requestMap->getMap().find(CBORValue(CtapMakeCredentialRequestOptionsKey)); // Find options.
+                if (it != requestMap->getMap().end()) {
+                    auto& optionMap = it->second.getMap();
+
+                    auto itr = optionMap.find(CBORValue(kResidentKeyMapKey));
+                    if (itr != optionMap.end())
+                        m_requireResidentKey = itr->second.getBool();
+
+                    itr = optionMap.find(CBORValue(kUserVerificationMapKey));
+                    if (itr != optionMap.end())
+                        m_requireUserVerification = itr->second.getBool();
+                }
             }
-        }
 
-        if (cmd == CtapRequestCommand::kAuthenticatorGetAssertion) {
-            auto it = requestMap->getMap().find(CBORValue(CtapGetAssertionRequestOptionsKey)); // Find options.
-            if (it != requestMap->getMap().end()) {
-                auto& optionMap = it->second.getMap();
-                auto itr = optionMap.find(CBORValue(kUserVerificationMapKey));
-                if (itr != optionMap.end())
-                    m_requireUserVerification = itr->second.getBool();
+            if (cmd == CtapRequestCommand::kAuthenticatorGetAssertion) {
+                auto it = requestMap->getMap().find(CBORValue(CtapGetAssertionRequestOptionsKey)); // Find options.
+                if (it != requestMap->getMap().end()) {
+                    auto& optionMap = it->second.getMap();
+                    auto itr = optionMap.find(CBORValue(kUserVerificationMapKey));
+                    if (itr != optionMap.end())
+                        m_requireUserVerification = itr->second.getBool();
+                }
             }
         }
     }
@@ -207,8 +212,12 @@ void MockHidConnection::feedReports()
         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);
-        else
-            message = FidoHidMessage::create(m_currentChannel, FidoHidDeviceCommand::kCbor, infoData);
+        else {
+            if (!m_configuration.hid->isU2f)
+                message = FidoHidMessage::create(m_currentChannel, FidoHidDeviceCommand::kCbor, infoData);
+            else
+                message = FidoHidMessage::create(m_currentChannel, FidoHidDeviceCommand::kError, { static_cast<uint8_t>(CtapDeviceResponseCode::kCtap1ErrInvalidCommand) });
+        }
     }
 
     if (m_stage == Mock::Stage::Request && m_subStage == Mock::SubStage::Msg) {
@@ -223,9 +232,14 @@ void MockHidConnection::feedReports()
             message = FidoHidMessage::create(m_currentChannel, FidoHidDeviceCommand::kCbor, { static_cast<uint8_t>(CtapDeviceResponseCode::kCtap2ErrUnsupportedOption) });
         else {
             Vector<uint8_t> payload;
-            auto status = base64Decode(m_configuration.hid->payloadBase64, payload);
+            ASSERT(!m_configuration.hid->payloadBase64.isEmpty());
+            auto status = base64Decode(m_configuration.hid->payloadBase64[0], payload);
+            m_configuration.hid->payloadBase64.remove(0);
             ASSERT_UNUSED(status, status);
-            message = FidoHidMessage::create(m_currentChannel, FidoHidDeviceCommand::kCbor, payload);
+            if (!m_configuration.hid->isU2f)
+                message = FidoHidMessage::create(m_currentChannel, FidoHidDeviceCommand::kCbor, payload);
+            else
+                message = FidoHidMessage::create(m_currentChannel, FidoHidDeviceCommand::kMsg, payload);
         }
     }
 
index e0bf86b..3dcabc4 100644 (file)
 namespace WebKit {
 
 // The following basically simulates an external HID token that:
-//    1. Only supports CTAP2 protocol,
+//    1. Supports only one protocol, either CTAP2 or U2F.
 //    2. Doesn't support resident keys,
 //    3. Doesn't support user verification.
-// There are four stages for each WebAuthN request:
+// There are four stages for each CTAP request:
 // FSM: Info::Init => Info::Msg => Request::Init => Request::Msg
+// There are indefinite stages for each U2F request:
+// FSM: Info::Init => Info::Msg => [Request::Init => Request::Msg]+
 // According to different combinations of error and stages, error will manifest differently.
 class MockHidConnection final : public CanMakeWeakPtr<MockHidConnection>, public HidConnection {
 public:
index 78867a5..be0872f 100644 (file)
@@ -61,10 +61,11 @@ struct MockWebAuthenticationConfiguration {
             WrongNonce
         };
 
-        String payloadBase64;
+        Vector<String> payloadBase64;
         Stage stage { Stage::Info };
         SubStage subStage { SubStage::Init };
         Error error { Error::Success };
+        bool isU2f { false };
         bool keepAlive { false };
         bool fastDataArrival { false };
         bool continueAfterErrorData { false };
index 6f28620..1f9ef98 100644 (file)
@@ -197,7 +197,7 @@ void CtapHidDriver::continueAfterChannelAllocated(Optional<FidoHidMessage>&& mes
     // FIXME(191534): Check the reset of the payload.
     // FIXME(192061)
     LOG_ERROR("Start sending the request.");
-    auto cmd = FidoHidMessage::create(m_channelId, FidoHidDeviceCommand::kCbor, m_requestData);
+    auto cmd = FidoHidMessage::create(m_channelId, m_protocol == ProtocolVersion::kCtap ? FidoHidDeviceCommand::kCbor : FidoHidDeviceCommand::kMsg, m_requestData);
     ASSERT(cmd);
     m_worker->transact(WTFMove(*cmd), [weakThis = makeWeakPtr(*this)](Optional<FidoHidMessage>&& response) mutable {
         ASSERT(RunLoop::isMain());
index de05026..8fb504d 100644 (file)
@@ -57,6 +57,7 @@ public:
 
     explicit CtapHidDriver(UniqueRef<HidConnection>&&);
 
+    void setProtocol(fido::ProtocolVersion protocol) { m_protocol = protocol; }
     void transact(Vector<uint8_t>&& data, ResponseCallback&&);
 
 private:
@@ -103,6 +104,7 @@ private:
     Vector<uint8_t> m_requestData;
     ResponseCallback m_responseCallback;
     Vector<uint8_t> m_nonce;
+    fido::ProtocolVersion m_protocol { fido::ProtocolVersion::kCtap };
 };
 
 } // namespace WebKit
diff --git a/Source/WebKit/UIProcess/WebAuthentication/fido/U2fHidAuthenticator.cpp b/Source/WebKit/UIProcess/WebAuthentication/fido/U2fHidAuthenticator.cpp
new file mode 100644 (file)
index 0000000..8447f42
--- /dev/null
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2019 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include "U2fHidAuthenticator.h"
+
+#if ENABLE(WEB_AUTHN) && PLATFORM(MAC)
+
+#include "CtapHidDriver.h"
+#include <WebCore/ApduResponse.h>
+#include <WebCore/ExceptionData.h>
+#include <WebCore/U2fCommandConstructor.h>
+#include <WebCore/U2fResponseConverter.h>
+#include <wtf/RunLoop.h>
+#include <wtf/text/StringConcatenateNumbers.h>
+
+namespace WebKit {
+using namespace WebCore;
+using namespace apdu;
+using namespace fido;
+
+namespace {
+const unsigned retryTimeOutValueMs = 200;
+}
+
+U2fHidAuthenticator::U2fHidAuthenticator(std::unique_ptr<CtapHidDriver>&& driver)
+    : m_driver(WTFMove(driver))
+    , m_retryTimer(RunLoop::main(), this, &U2fHidAuthenticator::retryLastCommand)
+{
+    // FIXME(191520): We need a way to convert std::unique_ptr to UniqueRef.
+    ASSERT(m_driver);
+}
+
+void U2fHidAuthenticator::makeCredential()
+{
+    if (!isConvertibleToU2fRegisterCommand(requestData().creationOptions)) {
+        receiveRespond(ExceptionData { NotSupportedError, "Cannot convert the request to U2F command."_s });
+        return;
+    }
+    if (!requestData().creationOptions.excludeCredentials.isEmpty()) {
+        ASSERT(!m_nextListIndex);
+        checkExcludeList(m_nextListIndex++);
+        return;
+    }
+    issueRegisterCommand();
+}
+
+void U2fHidAuthenticator::checkExcludeList(size_t index)
+{
+    if (index >= requestData().creationOptions.excludeCredentials.size()) {
+        issueRegisterCommand();
+        return;
+    }
+    auto u2fCmd = convertToU2fCheckOnlySignCommand(requestData().hash, requestData().creationOptions, requestData().creationOptions.excludeCredentials[index]);
+    ASSERT(u2fCmd);
+    issueNewCommand(WTFMove(*u2fCmd), CommandType::CheckOnlyCommand);
+}
+
+void U2fHidAuthenticator::issueRegisterCommand()
+{
+    auto u2fCmd = convertToU2fRegisterCommand(requestData().hash, requestData().creationOptions);
+    ASSERT(u2fCmd);
+    issueNewCommand(WTFMove(*u2fCmd), CommandType::RegisterCommand);
+}
+
+void U2fHidAuthenticator::getAssertion()
+{
+    if (!isConvertibleToU2fSignCommand(requestData().requestOptions)) {
+        receiveRespond(ExceptionData { NotSupportedError, "Cannot convert the request to U2F command."_s });
+        return;
+    }
+    ASSERT(!m_nextListIndex);
+    issueSignCommand(m_nextListIndex++);
+}
+
+void U2fHidAuthenticator::issueSignCommand(size_t index)
+{
+    if (index >= requestData().requestOptions.allowCredentials.size()) {
+        receiveRespond(ExceptionData { NotAllowedError, "No credentials from the allowCredentials list is found in the authenticator."_s });
+        return;
+    }
+    auto u2fCmd = convertToU2fSignCommand(requestData().hash, requestData().requestOptions, requestData().requestOptions.allowCredentials[index].idVector);
+    ASSERT(u2fCmd);
+    issueNewCommand(WTFMove(*u2fCmd), CommandType::SignCommand);
+}
+
+void U2fHidAuthenticator::issueNewCommand(Vector<uint8_t>&& command, CommandType type)
+{
+    m_lastCommand = WTFMove(command);
+    m_lastCommandType = type;
+    issueCommand(m_lastCommand, m_lastCommandType);
+}
+
+void U2fHidAuthenticator::issueCommand(const Vector<uint8_t>& command, CommandType type)
+{
+    m_driver->transact(Vector<uint8_t>(command), [weakThis = makeWeakPtr(*this), type](Vector<uint8_t>&& data) {
+        ASSERT(RunLoop::isMain());
+        if (!weakThis)
+            return;
+        weakThis->responseReceived(WTFMove(data), type);
+    });
+}
+
+void U2fHidAuthenticator::responseReceived(Vector<uint8_t>&& response, CommandType type)
+{
+    auto apduResponse = ApduResponse::createFromMessage(response);
+    if (!apduResponse) {
+        receiveRespond(ExceptionData { UnknownError, "Couldn't parse the APDU response."_s });
+        return;
+    }
+
+    switch (type) {
+    case CommandType::RegisterCommand:
+        continueRegisterCommandAfterResponseReceived(WTFMove(*apduResponse));
+        return;
+    case CommandType::CheckOnlyCommand:
+        continueCheckOnlyCommandAfterResponseReceived(WTFMove(*apduResponse));
+        return;
+    case CommandType::BogusCommand:
+        continueBogusCommandAfterResponseReceived(WTFMove(*apduResponse));
+        return;
+    case CommandType::SignCommand:
+        continueSignCommandAfterResponseReceived(WTFMove(*apduResponse));
+        return;
+    }
+    ASSERT_NOT_REACHED();
+}
+
+void U2fHidAuthenticator::continueRegisterCommandAfterResponseReceived(ApduResponse&& apduResponse)
+{
+    switch (apduResponse.status()) {
+    case ApduResponse::Status::SW_NO_ERROR: {
+        auto response = readU2fRegisterResponse(requestData().creationOptions.rp.id, apduResponse.data());
+        if (!response) {
+            receiveRespond(ExceptionData { UnknownError, "Couldn't parse the U2F register response."_s });
+            return;
+        }
+        receiveRespond(WTFMove(*response));
+        return;
+    }
+    case ApduResponse::Status::SW_CONDITIONS_NOT_SATISFIED:
+        // Polling is required during test of user presence.
+        m_retryTimer.startOneShot(Seconds::fromMilliseconds(retryTimeOutValueMs));
+        return;
+    default:
+        receiveRespond(ExceptionData { UnknownError, makeString("Unknown internal error. Error code: ", static_cast<unsigned>(apduResponse.status())) });
+    }
+}
+
+void U2fHidAuthenticator::continueCheckOnlyCommandAfterResponseReceived(ApduResponse&& apduResponse)
+{
+    switch (apduResponse.status()) {
+    case ApduResponse::Status::SW_NO_ERROR:
+    case ApduResponse::Status::SW_CONDITIONS_NOT_SATISFIED:
+        issueNewCommand(constructBogusU2fRegistrationCommand(), CommandType::BogusCommand);
+        return;
+    default:
+        checkExcludeList(m_nextListIndex++);
+    }
+}
+
+void U2fHidAuthenticator::continueBogusCommandAfterResponseReceived(ApduResponse&& apduResponse)
+{
+    switch (apduResponse.status()) {
+    case ApduResponse::Status::SW_NO_ERROR:
+        receiveRespond(ExceptionData { InvalidStateError, "At least one credential matches an entry of the excludeCredentials list in the authenticator."_s });
+        return;
+    case ApduResponse::Status::SW_CONDITIONS_NOT_SATISFIED:
+        // Polling is required during test of user presence.
+        m_retryTimer.startOneShot(Seconds::fromMilliseconds(retryTimeOutValueMs));
+        return;
+    default:
+        receiveRespond(ExceptionData { UnknownError, makeString("Unknown internal error. Error code: ", static_cast<unsigned>(apduResponse.status())) });
+    }
+}
+
+void U2fHidAuthenticator::continueSignCommandAfterResponseReceived(ApduResponse&& apduResponse)
+{
+    switch (apduResponse.status()) {
+    case ApduResponse::Status::SW_NO_ERROR: {
+        auto response = readU2fSignResponse(requestData().requestOptions.rpId, requestData().requestOptions.allowCredentials[m_nextListIndex - 1].idVector, apduResponse.data());
+        if (!response) {
+            receiveRespond(ExceptionData { UnknownError, "Couldn't parse the U2F sign response."_s });
+            return;
+        }
+        receiveRespond(WTFMove(*response));
+        return;
+    }
+    case ApduResponse::Status::SW_CONDITIONS_NOT_SATISFIED:
+        // Polling is required during test of user presence.
+        m_retryTimer.startOneShot(Seconds::fromMilliseconds(retryTimeOutValueMs));
+        return;
+    default:
+        issueSignCommand(m_nextListIndex++);
+    }
+}
+
+} // namespace WebKit
+
+#endif // ENABLE(WEB_AUTHN) && PLATFORM(MAC)
diff --git a/Source/WebKit/UIProcess/WebAuthentication/fido/U2fHidAuthenticator.h b/Source/WebKit/UIProcess/WebAuthentication/fido/U2fHidAuthenticator.h
new file mode 100644 (file)
index 0000000..cff2947
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2019 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#if ENABLE(WEB_AUTHN) && PLATFORM(MAC)
+
+#include "Authenticator.h"
+#include <wtf/RunLoop.h>
+
+namespace apdu {
+class ApduResponse;
+}
+
+namespace WebKit {
+
+class CtapHidDriver;
+
+class U2fHidAuthenticator final : public Authenticator {
+public:
+    static Ref<U2fHidAuthenticator> create(std::unique_ptr<CtapHidDriver>&& driver)
+    {
+        return adoptRef(*new U2fHidAuthenticator(WTFMove(driver)));
+    }
+
+private:
+    explicit U2fHidAuthenticator(std::unique_ptr<CtapHidDriver>&&);
+
+    void makeCredential() final;
+    void checkExcludeList(size_t index);
+    void issueRegisterCommand();
+    void getAssertion() final;
+    void issueSignCommand(size_t index);
+
+    enum class CommandType : uint8_t {
+        RegisterCommand,
+        CheckOnlyCommand,
+        BogusCommand,
+        SignCommand
+    };
+    void issueNewCommand(Vector<uint8_t>&& command, CommandType);
+    void retryLastCommand() { issueCommand(m_lastCommand, m_lastCommandType); }
+    void issueCommand(const Vector<uint8_t>& command, CommandType);
+    void responseReceived(Vector<uint8_t>&& response, CommandType);
+    void continueRegisterCommandAfterResponseReceived(apdu::ApduResponse&&);
+    void continueCheckOnlyCommandAfterResponseReceived(apdu::ApduResponse&&);
+    void continueBogusCommandAfterResponseReceived(apdu::ApduResponse&&);
+    void continueSignCommandAfterResponseReceived(apdu::ApduResponse&&);
+
+    std::unique_ptr<CtapHidDriver> m_driver;
+    RunLoop::Timer<U2fHidAuthenticator> m_retryTimer;
+    Vector<uint8_t> m_lastCommand;
+    CommandType m_lastCommandType;
+    size_t m_nextListIndex { 0 };
+};
+
+} // namespace WebKit
+
+#endif // ENABLE(WEB_AUTHN) && PLATFORM(MAC)
index e7e4772..9c82517 100644 (file)
                570AB8F320AE3BD700B8BE87 /* SecKeyProxyStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 570AB8F220AE3BD700B8BE87 /* SecKeyProxyStore.h */; };
                57597EB921811D9A0037F924 /* CtapHidDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = 57597EB721811D9A0037F924 /* CtapHidDriver.h */; };
                57597EBD218184900037F924 /* CtapHidAuthenticator.h in Headers */ = {isa = PBXBuildFile; fileRef = 57597EBB2181848F0037F924 /* CtapHidAuthenticator.h */; };
-               57597EBE218184900037F924 /* CtapHidAuthenticator.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 57597EBC2181848F0037F924 /* CtapHidAuthenticator.cpp */; };
-               57597EC121818BE20037F924 /* CtapHidDriver.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 57597EC021818BE20037F924 /* CtapHidDriver.cpp */; };
                5772F206217DBD6A0056BF2C /* HidService.h in Headers */ = {isa = PBXBuildFile; fileRef = 5772F204217DBD6A0056BF2C /* HidService.h */; };
                578DC2982155A0020074E815 /* LocalAuthenticationSoftLink.h in Headers */ = {isa = PBXBuildFile; fileRef = 578DC2972155A0010074E815 /* LocalAuthenticationSoftLink.h */; };
                57AC8F50217FEED90055438C /* HidConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = 57AC8F4E217FEED90055438C /* HidConnection.h */; };
                57DCEDC3214F114C0016B847 /* MockLocalService.h in Headers */ = {isa = PBXBuildFile; fileRef = 57DCEDC1214F114C0016B847 /* MockLocalService.h */; };
                57DCEDC7214F18300016B847 /* MockLocalConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = 57DCEDC5214F18300016B847 /* MockLocalConnection.h */; };
                57DCEDCB214F4E420016B847 /* MockAuthenticatorManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 57DCEDC9214F4E420016B847 /* MockAuthenticatorManager.h */; };
+               57EB2E3A21E1983E00B89CDF /* U2fHidAuthenticator.h in Headers */ = {isa = PBXBuildFile; fileRef = 57EB2E3821E1983E00B89CDF /* U2fHidAuthenticator.h */; };
                587743A621C30BBE00AE9084 /* HTTPSUpgradeList.db in Resources */ = {isa = PBXBuildFile; fileRef = 587743A421C30AD800AE9084 /* HTTPSUpgradeList.db */; };
                58E977DF21C49A00005D92A6 /* NetworkHTTPSUpgradeChecker.h in Headers */ = {isa = PBXBuildFile; fileRef = 58E977DD21C49A00005D92A6 /* NetworkHTTPSUpgradeChecker.h */; };
                5C0B17781E7C880E00E9123C /* NetworkSocketStreamMessageReceiver.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 5C0B17741E7C879C00E9123C /* NetworkSocketStreamMessageReceiver.cpp */; };
                57DCEDC6214F18300016B847 /* MockLocalConnection.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MockLocalConnection.mm; sourceTree = "<group>"; };
                57DCEDC9214F4E420016B847 /* MockAuthenticatorManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockAuthenticatorManager.h; sourceTree = "<group>"; };
                57DCEDCD214F51680016B847 /* MockAuthenticatorManager.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MockAuthenticatorManager.cpp; sourceTree = "<group>"; };
+               57EB2E3821E1983E00B89CDF /* U2fHidAuthenticator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = U2fHidAuthenticator.h; sourceTree = "<group>"; };
+               57EB2E3921E1983E00B89CDF /* U2fHidAuthenticator.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = U2fHidAuthenticator.cpp; sourceTree = "<group>"; };
                587743A421C30AD800AE9084 /* HTTPSUpgradeList.db */ = {isa = PBXFileReference; lastKnownFileType = file; name = HTTPSUpgradeList.db; path = DerivedSources/WebKit2/HTTPSUpgradeList.db; sourceTree = BUILT_PRODUCTS_DIR; };
                58E977DC21C499FE005D92A6 /* NetworkHTTPSUpgradeChecker.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = NetworkHTTPSUpgradeChecker.cpp; sourceTree = "<group>"; };
                58E977DD21C49A00005D92A6 /* NetworkHTTPSUpgradeChecker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NetworkHTTPSUpgradeChecker.h; sourceTree = "<group>"; };
                                57597EBB2181848F0037F924 /* CtapHidAuthenticator.h */,
                                57597EC021818BE20037F924 /* CtapHidDriver.cpp */,
                                57597EB721811D9A0037F924 /* CtapHidDriver.h */,
+                               57EB2E3921E1983E00B89CDF /* U2fHidAuthenticator.cpp */,
+                               57EB2E3821E1983E00B89CDF /* U2fHidAuthenticator.h */,
                        );
                        path = fido;
                        sourceTree = "<group>";
                                1AAF263914687C39004A1E8A /* TiledCoreAnimationDrawingArea.h in Headers */,
                                1AF05D8714688348008B1E81 /* TiledCoreAnimationDrawingAreaProxy.h in Headers */,
                                2F8336861FA139DF00C6E080 /* TouchBarMenuData.h in Headers */,
+                               57EB2E3A21E1983E00B89CDF /* U2fHidAuthenticator.h in Headers */,
                                1AFE436618B6C081009C7A48 /* UIDelegate.h in Headers */,
                                515BE1B51D5917FF00DD7C68 /* UIGamepad.h in Headers */,
                                515BE1A91D55293400DD7C68 /* UIGamepadProvider.h in Headers */,
                                2D92A786212B6AB100F493FD /* ChildProcess.cpp in Sources */,
                                51FAEC3B1B0657680009C4E7 /* ChildProcessMessageReceiver.cpp in Sources */,
                                2D92A77D212B6A7100F493FD /* Connection.cpp in Sources */,
-                               57597EBE218184900037F924 /* CtapHidAuthenticator.cpp in Sources */,
-                               57597EC121818BE20037F924 /* CtapHidDriver.cpp in Sources */,
                                2D92A77E212B6A7100F493FD /* DataReference.cpp in Sources */,
                                2D92A77F212B6A7100F493FD /* Decoder.cpp in Sources */,
                                1AB7D6191288B9D900CFD08C /* DownloadProxyMessageReceiver.cpp in Sources */,
index 715eb47..61bf0d6 100644 (file)
@@ -1,3 +1,21 @@
+2019-01-08  Jiewen Tan  <jiewen_tan@apple.com>
+
+        [WebAuthN] Support U2F HID Authenticators on macOS
+        https://bugs.webkit.org/show_bug.cgi?id=191535
+        <rdar://problem/47102027>
+
+        Reviewed by Brent Fulgham.
+
+        This patch:
+        1) adds support for U2F mocking mechanism;
+        2) updates tests to reflect U2fCommandConstructor changes.
+
+        * TestWebKitAPI/Tests/WebCore/CtapResponseTest.cpp:
+        (TestWebKitAPI::TEST):
+        * TestWebKitAPI/Tests/WebCore/FidoTestData.h:
+        * WebKitTestRunner/InjectedBundle/TestRunner.cpp:
+        (WTR::TestRunner::setWebAuthenticationMockConfiguration):
+
 2019-01-08  Wenson Hsieh  <wenson_hsieh@apple.com>
 
         [iOS] Dispatch a synthetic mousedown event prior to starting drags
index de3f209..a7dcab0 100644 (file)
@@ -520,7 +520,7 @@ TEST(CTAPResponseTest, TestParseIncorrectRegisterResponseData5)
 // Tests that U2F authenticator data is properly serialized.
 TEST(CTAPResponseTest, TestParseSignResponseData)
 {
-    auto response = readFromU2fSignResponse(TestData::kRelyingPartyId, getTestCredentialRawIdBytes(), getTestSignResponse());
+    auto response = readU2fSignResponse(TestData::kRelyingPartyId, getTestCredentialRawIdBytes(), getTestSignResponse());
     ASSERT_TRUE(response);
     EXPECT_EQ(response->rawId->byteLength(), sizeof(TestData::kU2fSignKeyHandle));
     EXPECT_EQ(memcmp(response->rawId->data(), TestData::kU2fSignKeyHandle, sizeof(TestData::kU2fSignKeyHandle)), 0);
@@ -533,27 +533,27 @@ TEST(CTAPResponseTest, TestParseSignResponseData)
 
 TEST(CTAPResponseTest, TestParseU2fSignWithNullKeyHandle)
 {
-    auto response = readFromU2fSignResponse(TestData::kRelyingPartyId, Vector<uint8_t>(), getTestSignResponse());
+    auto response = readU2fSignResponse(TestData::kRelyingPartyId, Vector<uint8_t>(), getTestSignResponse());
     EXPECT_FALSE(response);
 }
 
 TEST(CTAPResponseTest, TestParseU2fSignWithNullResponse)
 {
-    auto response = readFromU2fSignResponse(TestData::kRelyingPartyId, getTestCredentialRawIdBytes(), Vector<uint8_t>());
+    auto response = readU2fSignResponse(TestData::kRelyingPartyId, getTestCredentialRawIdBytes(), Vector<uint8_t>());
     EXPECT_FALSE(response);
 }
 
 TEST(CTAPResponseTest, TestParseU2fSignWithCorruptedCounter)
 {
     // A sign response of less than 5 bytes.
-    auto response = readFromU2fSignResponse(TestData::kRelyingPartyId, getTestCredentialRawIdBytes(), getTestCorruptedSignResponse(3));
+    auto response = readU2fSignResponse(TestData::kRelyingPartyId, getTestCredentialRawIdBytes(), getTestCorruptedSignResponse(3));
     EXPECT_FALSE(response);
 }
 
 TEST(CTAPResponseTest, TestParseU2fSignWithCorruptedSignature)
 {
     // A sign response no more than 5 bytes.
-    auto response = readFromU2fSignResponse(TestData::kRelyingPartyId, getTestCredentialRawIdBytes(), getTestCorruptedSignResponse(5));
+    auto response = readU2fSignResponse(TestData::kRelyingPartyId, getTestCredentialRawIdBytes(), getTestCorruptedSignResponse(5));
     EXPECT_FALSE(response);
 }
 
index 38e03c8..b682fb7 100644 (file)
@@ -51,7 +51,7 @@ constexpr char kRelyingPartyId[] = "acme.com";
 
 constexpr uint8_t kU2fRegisterCommandApdu[] = {
     // CLA, INS, P1, P2 APDU instructions
-    0x00, 0x01, 0x00, 0x00,
+    0x00, 0x01, 0x03, 0x00,
     // Data length in 3 bytes in big endian order.
     0x00, 0x00, 0x40,
     // Challenge parameter -- see kClientDataHash
@@ -141,7 +141,7 @@ constexpr uint8_t kU2fCheckOnlySignCommandApdu[] = {
 
 constexpr uint8_t kU2fFakeRegisterCommand[] = {
     // CLA, INS, P1, P2 APDU instructions
-    0x00, 0x01, 0x00, 0x00,
+    0x00, 0x01, 0x03, 0x00,
     // Data length in 3 bytes in big endian order.
     0x00, 0x00, 0x40,
     // Bogus challenge parameter
index 7c8213e..24d340f 100644 (file)
@@ -2509,10 +2509,36 @@ void TestRunner::setWebAuthenticationMockConfiguration(JSValueRef configurationV
         JSRetainPtr<JSStringRef> payloadBase64PropertyName(Adopt, JSStringCreateWithUTF8CString("payloadBase64"));
         JSValueRef payloadBase64Value = JSObjectGetProperty(context, hid, payloadBase64PropertyName.get(), 0);
         if (!JSValueIsUndefined(context, payloadBase64Value) && !JSValueIsNull(context, payloadBase64Value)) {
-            if (!JSValueIsString(context, payloadBase64Value))
+            if (!JSValueIsArray(context, payloadBase64Value))
                 return;
+
+            JSObjectRef payloadBase64 = JSValueToObject(context, payloadBase64Value, nullptr);
+            static auto lengthProperty = adopt(JSStringCreateWithUTF8CString("length"));
+            JSValueRef payloadBase64LengthValue = JSObjectGetProperty(context, payloadBase64, lengthProperty.get(), nullptr);
+            if (!JSValueIsNumber(context, payloadBase64LengthValue))
+                return;
+
+            auto payloadBase64s = adoptWK(WKMutableArrayCreate());
+            auto payloadBase64Length = static_cast<size_t>(JSValueToNumber(context, payloadBase64LengthValue, nullptr));
+            for (size_t i = 0; i < payloadBase64Length; ++i) {
+                JSValueRef payloadBase64Value = JSObjectGetPropertyAtIndex(context, payloadBase64, i, nullptr);
+                if (!JSValueIsString(context, payloadBase64Value))
+                    continue;
+                WKArrayAppendItem(payloadBase64s.get(), toWK(adopt(JSValueToStringCopy(context, payloadBase64Value, 0)).get()).get());
+            }
+
             hidKeys.append({ AdoptWK, WKStringCreateWithUTF8CString("PayloadBase64") });
-            hidValues.append(toWK(adopt(JSValueToStringCopy(context, payloadBase64Value, 0)).get()));
+            hidValues.append(payloadBase64s);
+        }
+
+        JSRetainPtr<JSStringRef> isU2fPropertyName(Adopt, JSStringCreateWithUTF8CString("isU2f"));
+        JSValueRef isU2fValue = JSObjectGetProperty(context, hid, isU2fPropertyName.get(), 0);
+        if (!JSValueIsUndefined(context, isU2fValue) && !JSValueIsNull(context, isU2fValue)) {
+            if (!JSValueIsBoolean(context, isU2fValue))
+                return;
+            bool isU2f = JSValueToBoolean(context, isU2fValue);
+            hidKeys.append({ AdoptWK, WKStringCreateWithUTF8CString("IsU2f") });
+            hidValues.append(adoptWK(WKBooleanCreate(isU2f)).get());
         }
 
         JSRetainPtr<JSStringRef> keepAlivePropertyName(Adopt, JSStringCreateWithUTF8CString("keepAlive"));