[WebAuthn] Implement SPI for the platform authenticator
authorjiewen_tan@apple.com <jiewen_tan@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 24 Feb 2020 23:51:17 +0000 (23:51 +0000)
committerjiewen_tan@apple.com <jiewen_tan@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 24 Feb 2020 23:51:17 +0000 (23:51 +0000)
https://bugs.webkit.org/show_bug.cgi?id=208087
<rdar://problem/59369305>

Reviewed by Brent Fulgham.

Source/WebCore:

Enhances AuthenticatorAssertionResponse to accommondate responses
returned from the platform authenticator.

Covered by API tests.

* Modules/webauthn/AuthenticatorAssertionResponse.cpp:
(WebCore::AuthenticatorAssertionResponse::create):
(WebCore::AuthenticatorAssertionResponse::setAuthenticatorData):
(WebCore::AuthenticatorAssertionResponse::AuthenticatorAssertionResponse):
* Modules/webauthn/AuthenticatorAssertionResponse.h:
(WebCore::AuthenticatorAssertionResponse::authenticatorData const):
(WebCore::AuthenticatorAssertionResponse::signature const):
(WebCore::AuthenticatorAssertionResponse::name const):
(WebCore::AuthenticatorAssertionResponse::displayName const):
(WebCore::AuthenticatorAssertionResponse::numberOfCredentials const):
(WebCore::AuthenticatorAssertionResponse::accessControl const):
(WebCore::AuthenticatorAssertionResponse::setSignature):
(WebCore::AuthenticatorAssertionResponse::setName):
(WebCore::AuthenticatorAssertionResponse::setDisplayName):
(WebCore::AuthenticatorAssertionResponse::setNumberOfCredentials):

Source/WebKit:

Here is the newly added SPI:
typedef NS_ENUM(NSInteger, _WKWebAuthenticationPanelUpdate) {
    ...
    _WKWebAuthenticationPanelUpdateLAError,
    _WKWebAuthenticationPanelUpdateLADuplicateCredential,
    _WKWebAuthenticationPanelUpdateLANoCredential,
};

typedef NS_ENUM(NSInteger, _WKWebAuthenticationTransport) {
    ...
    _WKWebAuthenticationTransportInternal,
};

@protocol _WKWebAuthenticationPanelDelegate <NSObject>
@optional
...
- (void)panel:(_WKWebAuthenticationPanel *)panel verifyUserWithAccessControl:(SecAccessControlRef)accessControl completionHandler:(void (^)(LAContext *))completionHandler;
@end

Illustrations:
1) _WKWebAuthenticationPanelUpdate: Three errors are added to help clients present meaningful error messages to users.
a) WKWebAuthenticationPanelUpdateLAError: An internal error, clients should inform users and terminate the platform
authentication process. This error can be returned at any time.
b) _WKWebAuthenticationPanelUpdateLADuplicateCredential: It means a credential is found to match an entry in the
excludeList. Clients should inform users and terminate the platform authentication process. This error will only be
returned during makeCredential and before verifyUserWithAccessControl delegate.
c) _WKWebAuthenticationPanelUpdateLANoCredential: It means no credentials are found. Clients should inform users and
terminate the platform authentication process. This error will only be returned during getAssertion and before
verifyUserWithAccessControl delegate.

2) _WKWebAuthenticationTransport: _WKWebAuthenticationTransportInternal is added such that clients can learn platform
authenticator will be used from _WKWebAuthenticationPanel.transports.

3) verifyUserWithAccessControl: A delegate that will be called during makeCredential or getAssertion when the platform
authenticator is involved. This delegate is used to obtain user verification from a LAContext. In addition, the LAContext
should evaluate the passed accessControl, such that the SEP protected credential private key can be used. A typical
example will be [LAContext evaluateAccessControl:accessControl operation:LAAccessControlOperationUseKeySign localizedReason:reply:].
Noted, for getAssertion, selectAssertionResponse will be called before verifyUserWithAccessControl. So users need to be
prompted to select a credential before the user verification.

In the scenario when both the platform authenticator and external authenticators are requested. Clients are advised to
wait until verifyUserWithAccessControl to show the combined UI. If any of the LAError states are received before
verifyUserWithAccessControl, clients should then only show the external authenticator UI. Also, platform authenticator and
external authenticators are being discovered at the same time, which means a user can plug in a security key at anytime.
If a valid response is received from the security key, the whole ceremony will be terminated.

Besides introducing the SPI, and all the necessary plumbing to make it happen. This patch also:
1) adds LocalAuthenticationSPI, which is used to check whether a given LAContext is unlocked or not;
2) improves MockLocalConnection such that mock testing can still be ran.

* Platform/spi/Cocoa/LocalAuthenticationSPI.h: Copied from Source/WebKit/UIProcess/WebAuthentication/Cocoa/LocalAuthenticationSoftLink.h.
* UIProcess/API/APIWebAuthenticationPanel.cpp:
(API::WebAuthenticationPanel::WebAuthenticationPanel):
* UIProcess/API/APIWebAuthenticationPanelClient.h:
(API::WebAuthenticationPanelClient::verifyUser const):
* UIProcess/API/Cocoa/_WKWebAuthenticationPanel.h:
* UIProcess/API/Cocoa/_WKWebAuthenticationPanel.mm:
(wkWebAuthenticationTransport):
* UIProcess/WebAuthentication/Authenticator.h:
* UIProcess/WebAuthentication/AuthenticatorManager.cpp:
(WebKit::AuthenticatorManager::verifyUser):
* UIProcess/WebAuthentication/AuthenticatorManager.h:
* UIProcess/WebAuthentication/Cocoa/LocalAuthenticationSoftLink.h:
* UIProcess/WebAuthentication/Cocoa/LocalAuthenticator.h:
* UIProcess/WebAuthentication/Cocoa/LocalAuthenticator.mm:
(WebKit::LocalAuthenticatorInternal::toNSData):
(WebKit::LocalAuthenticatorInternal::toArrayBuffer):
(WebKit::LocalAuthenticator::makeCredential):
(WebKit::LocalAuthenticator::continueMakeCredentialAfterUserConsented):
(WebKit::LocalAuthenticator::continueMakeCredentialAfterAttested):
(WebKit::LocalAuthenticator::getAssertion):
(WebKit::LocalAuthenticator::continueGetAssertionAfterResponseSelected):
(WebKit::LocalAuthenticator::continueGetAssertionAfterUserConsented):
(WebKit::LocalAuthenticator::receiveException const):
* UIProcess/WebAuthentication/Cocoa/LocalConnection.h:
(WebKit::LocalConnection::filterResponses const):
* UIProcess/WebAuthentication/Cocoa/LocalConnection.mm:
(WebKit::LocalConnection::isUnlocked const):
(WebKit::LocalConnection::getUserConsent const): Deleted.
(WebKit::LocalConnection::selectCredential const): Deleted.
* UIProcess/WebAuthentication/Cocoa/WebAuthenticationPanelClient.h:
* UIProcess/WebAuthentication/Cocoa/WebAuthenticationPanelClient.mm:
(WebKit::WebAuthenticationPanelClient::WebAuthenticationPanelClient):
(WebKit::wkWebAuthenticationPanelUpdate):
(WebKit::WebAuthenticationPanelClient::selectAssertionResponse const):
(WebKit::WebAuthenticationPanelClient::verifyUser const):
* UIProcess/WebAuthentication/Mock/MockLocalConnection.h:
* UIProcess/WebAuthentication/Mock/MockLocalConnection.mm:
(WebKit::MockLocalConnection::isUnlocked const):
(WebKit::MockLocalConnection::filterResponses const):
(WebKit::MockLocalConnection::getUserConsent const): Deleted.
(WebKit::MockLocalConnection::selectCredential const): Deleted.
* UIProcess/WebAuthentication/WebAuthenticationFlags.h:
* WebKit.xcodeproj/project.pbxproj:

Tools:

Besides adding API tests, this patch also teaches TestWebKitAPI to use restricted entitlements.

* TestWebKitAPI/Configurations/TestWebKitAPI-macOS.entitlements:
* TestWebKitAPI/Configurations/TestWebKitAPI.xcconfig:
* TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
* TestWebKitAPI/Tests/WebKitCocoa/_WKWebAuthenticationPanel.mm:
(-[TestWebAuthenticationPanelDelegate panel:updateWebAuthenticationPanel:]):
(-[TestWebAuthenticationPanelDelegate panel:selectAssertionResponse:completionHandler:]):
(-[TestWebAuthenticationPanelDelegate panel:verifyUserWithAccessControl:completionHandler:]):
(TestWebKitAPI::TEST):
* TestWebKitAPI/Tests/WebKitCocoa/web-authentication-get-assertion-la.html: Copied from Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-get-assertion.html.
* TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-la-duplicate-credential.html: Added.
* TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-la-error.html: Added.
* TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-la.html: Added.

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

32 files changed:
Source/WebCore/ChangeLog
Source/WebCore/Modules/webauthn/AuthenticatorAssertionResponse.cpp
Source/WebCore/Modules/webauthn/AuthenticatorAssertionResponse.h
Source/WebKit/ChangeLog
Source/WebKit/Platform/spi/Cocoa/LocalAuthenticationSPI.h [new file with mode: 0644]
Source/WebKit/UIProcess/API/APIWebAuthenticationPanel.cpp
Source/WebKit/UIProcess/API/APIWebAuthenticationPanelClient.h
Source/WebKit/UIProcess/API/Cocoa/_WKWebAuthenticationPanel.h
Source/WebKit/UIProcess/API/Cocoa/_WKWebAuthenticationPanel.mm
Source/WebKit/UIProcess/WebAuthentication/Authenticator.h
Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.cpp
Source/WebKit/UIProcess/WebAuthentication/AuthenticatorManager.h
Source/WebKit/UIProcess/WebAuthentication/Cocoa/LocalAuthenticationSoftLink.h
Source/WebKit/UIProcess/WebAuthentication/Cocoa/LocalAuthenticator.h
Source/WebKit/UIProcess/WebAuthentication/Cocoa/LocalAuthenticator.mm
Source/WebKit/UIProcess/WebAuthentication/Cocoa/LocalConnection.h
Source/WebKit/UIProcess/WebAuthentication/Cocoa/LocalConnection.mm
Source/WebKit/UIProcess/WebAuthentication/Cocoa/WebAuthenticationPanelClient.h
Source/WebKit/UIProcess/WebAuthentication/Cocoa/WebAuthenticationPanelClient.mm
Source/WebKit/UIProcess/WebAuthentication/Mock/MockLocalConnection.h
Source/WebKit/UIProcess/WebAuthentication/Mock/MockLocalConnection.mm
Source/WebKit/UIProcess/WebAuthentication/WebAuthenticationFlags.h
Source/WebKit/WebKit.xcodeproj/project.pbxproj
Tools/ChangeLog
Tools/TestWebKitAPI/Configurations/TestWebKitAPI-macOS.entitlements
Tools/TestWebKitAPI/Configurations/TestWebKitAPI.xcconfig
Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
Tools/TestWebKitAPI/Tests/WebKitCocoa/_WKWebAuthenticationPanel.mm
Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-get-assertion-la.html [new file with mode: 0644]
Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-la-duplicate-credential.html [new file with mode: 0644]
Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-la-error.html [new file with mode: 0644]
Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-la.html [new file with mode: 0644]

index 8fd33e6..e0eec1d 100644 (file)
@@ -1,3 +1,32 @@
+2020-02-24  Jiewen Tan  <jiewen_tan@apple.com>
+
+        [WebAuthn] Implement SPI for the platform authenticator
+        https://bugs.webkit.org/show_bug.cgi?id=208087
+        <rdar://problem/59369305>
+
+        Reviewed by Brent Fulgham.
+
+        Enhances AuthenticatorAssertionResponse to accommondate responses
+        returned from the platform authenticator.
+
+        Covered by API tests.
+
+        * Modules/webauthn/AuthenticatorAssertionResponse.cpp:
+        (WebCore::AuthenticatorAssertionResponse::create):
+        (WebCore::AuthenticatorAssertionResponse::setAuthenticatorData):
+        (WebCore::AuthenticatorAssertionResponse::AuthenticatorAssertionResponse):
+        * Modules/webauthn/AuthenticatorAssertionResponse.h:
+        (WebCore::AuthenticatorAssertionResponse::authenticatorData const):
+        (WebCore::AuthenticatorAssertionResponse::signature const):
+        (WebCore::AuthenticatorAssertionResponse::name const):
+        (WebCore::AuthenticatorAssertionResponse::displayName const):
+        (WebCore::AuthenticatorAssertionResponse::numberOfCredentials const):
+        (WebCore::AuthenticatorAssertionResponse::accessControl const):
+        (WebCore::AuthenticatorAssertionResponse::setSignature):
+        (WebCore::AuthenticatorAssertionResponse::setName):
+        (WebCore::AuthenticatorAssertionResponse::setDisplayName):
+        (WebCore::AuthenticatorAssertionResponse::setNumberOfCredentials):
+
 2020-02-24  Andres Gonzalez  <andresg_22@apple.com>
 
         [WebAccessibilityObjectWrapper updateObjectBackingStore] should return the backing object.
index 5d302f8..38893ee 100644 (file)
@@ -40,7 +40,7 @@ Ref<AuthenticatorAssertionResponse> AuthenticatorAssertionResponse::create(Ref<A
     return response;
 }
 
-Ref<AuthenticatorAssertionResponse> AuthenticatorAssertionResponse::create(const Vector<uint8_t>& rawId, const Vector<uint8_t>& authenticatorData, const Vector<uint8_t>& signature,  const Vector<uint8_t>& userHandle)
+Ref<AuthenticatorAssertionResponse> AuthenticatorAssertionResponse::create(const Vector<uint8_t>& rawId, const Vector<uint8_t>& authenticatorData, const Vector<uint8_t>& signature, const Vector<uint8_t>& userHandle)
 {
     RefPtr<ArrayBuffer> userhandleBuffer;
     if (!userHandle.isEmpty())
@@ -48,6 +48,16 @@ Ref<AuthenticatorAssertionResponse> AuthenticatorAssertionResponse::create(const
     return create(ArrayBuffer::create(rawId.data(), rawId.size()), ArrayBuffer::create(authenticatorData.data(), authenticatorData.size()), ArrayBuffer::create(signature.data(), signature.size()), WTFMove(userhandleBuffer), WTF::nullopt);
 }
 
+Ref<AuthenticatorAssertionResponse> AuthenticatorAssertionResponse::create(Ref<ArrayBuffer>&& rawId, Ref<ArrayBuffer>&& userHandle, SecAccessControlRef accessControl)
+{
+    return adoptRef(*new AuthenticatorAssertionResponse(WTFMove(rawId), WTFMove(userHandle), accessControl));
+}
+
+void AuthenticatorAssertionResponse::setAuthenticatorData(Vector<uint8_t>&& authenticatorData)
+{
+    m_authenticatorData = ArrayBuffer::create(authenticatorData.data(), authenticatorData.size());
+}
+
 AuthenticatorAssertionResponse::AuthenticatorAssertionResponse(Ref<ArrayBuffer>&& rawId, Ref<ArrayBuffer>&& authenticatorData, Ref<ArrayBuffer>&& signature, RefPtr<ArrayBuffer>&& userHandle)
     : AuthenticatorResponse(WTFMove(rawId))
     , m_authenticatorData(WTFMove(authenticatorData))
@@ -56,6 +66,13 @@ AuthenticatorAssertionResponse::AuthenticatorAssertionResponse(Ref<ArrayBuffer>&
 {
 }
 
+AuthenticatorAssertionResponse::AuthenticatorAssertionResponse(Ref<ArrayBuffer>&& rawId, Ref<ArrayBuffer>&& userHandle, SecAccessControlRef accessControl)
+    : AuthenticatorResponse(WTFMove(rawId))
+    , m_userHandle(WTFMove(userHandle))
+    , m_accessControl(accessControl)
+{
+}
+
 AuthenticatorResponseData AuthenticatorAssertionResponse::data() const
 {
     auto data = AuthenticatorResponse::data();
index d70a9cc..2b309cf 100644 (file)
@@ -28,6 +28,8 @@
 #if ENABLE(WEB_AUTHN)
 
 #include "AuthenticatorResponse.h"
+#include <wtf/RetainPtr.h>
+#include <wtf/spi/cocoa/SecuritySPI.h>
 
 namespace WebCore {
 
@@ -35,32 +37,38 @@ class AuthenticatorAssertionResponse : public AuthenticatorResponse {
 public:
     static Ref<AuthenticatorAssertionResponse> create(Ref<ArrayBuffer>&& rawId, Ref<ArrayBuffer>&& authenticatorData, Ref<ArrayBuffer>&& signature, RefPtr<ArrayBuffer>&& userHandle, Optional<AuthenticationExtensionsClientOutputs>&&);
     WEBCORE_EXPORT static Ref<AuthenticatorAssertionResponse> create(const Vector<uint8_t>& rawId, const Vector<uint8_t>& authenticatorData, const Vector<uint8_t>& signature,  const Vector<uint8_t>& userHandle);
+    WEBCORE_EXPORT static Ref<AuthenticatorAssertionResponse> create(Ref<ArrayBuffer>&& rawId, Ref<ArrayBuffer>&& userHandle, SecAccessControlRef);
     virtual ~AuthenticatorAssertionResponse() = default;
 
-    ArrayBuffer* authenticatorData() const { return m_authenticatorData.ptr(); }
-    ArrayBuffer* signature() const { return m_signature.ptr(); }
+    ArrayBuffer* authenticatorData() const { return m_authenticatorData.get(); }
+    ArrayBuffer* signature() const { return m_signature.get(); }
     ArrayBuffer* userHandle() const { return m_userHandle.get(); }
+    const String& name() const { return m_name; }
+    const String& displayName() const { return m_displayName; }
+    size_t numberOfCredentials() const { return m_numberOfCredentials; }
+    SecAccessControlRef accessControl() const { return m_accessControl.get(); }
 
+    WEBCORE_EXPORT void setAuthenticatorData(Vector<uint8_t>&&);
+    void setSignature(Ref<ArrayBuffer>&& signature) { m_signature = WTFMove(signature); }
     void setName(const String& name) { m_name = name; }
-    const String& name() const { return m_name; }
     void setDisplayName(const String& displayName) { m_displayName = displayName; }
-    const String& displayName() const { return m_displayName; }
     void setNumberOfCredentials(size_t numberOfCredentials) { m_numberOfCredentials = numberOfCredentials; }
-    size_t numberOfCredentials() const { return m_numberOfCredentials; }
 
 private:
     AuthenticatorAssertionResponse(Ref<ArrayBuffer>&&, Ref<ArrayBuffer>&&, Ref<ArrayBuffer>&&, RefPtr<ArrayBuffer>&&);
+    AuthenticatorAssertionResponse(Ref<ArrayBuffer>&&, Ref<ArrayBuffer>&&, SecAccessControlRef);
 
     Type type() const final { return Type::Assertion; }
     AuthenticatorResponseData data() const final;
 
-    Ref<ArrayBuffer> m_authenticatorData;
-    Ref<ArrayBuffer> m_signature;
+    RefPtr<ArrayBuffer> m_authenticatorData;
+    RefPtr<ArrayBuffer> m_signature;
     RefPtr<ArrayBuffer> m_userHandle;
 
     String m_name;
     String m_displayName;
     size_t m_numberOfCredentials { 0 };
+    RetainPtr<SecAccessControlRef> m_accessControl;
 };
 
 } // namespace WebCore
index a86e945..449081e 100644 (file)
@@ -1,3 +1,106 @@
+2020-02-24  Jiewen Tan  <jiewen_tan@apple.com>
+
+        [WebAuthn] Implement SPI for the platform authenticator
+        https://bugs.webkit.org/show_bug.cgi?id=208087
+        <rdar://problem/59369305>
+
+        Reviewed by Brent Fulgham.
+
+        Here is the newly added SPI:
+        typedef NS_ENUM(NSInteger, _WKWebAuthenticationPanelUpdate) {
+            ...
+            _WKWebAuthenticationPanelUpdateLAError,
+            _WKWebAuthenticationPanelUpdateLADuplicateCredential,
+            _WKWebAuthenticationPanelUpdateLANoCredential,
+        };
+
+        typedef NS_ENUM(NSInteger, _WKWebAuthenticationTransport) {
+            ...
+            _WKWebAuthenticationTransportInternal,
+        };
+
+        @protocol _WKWebAuthenticationPanelDelegate <NSObject>
+        @optional
+        ...
+        - (void)panel:(_WKWebAuthenticationPanel *)panel verifyUserWithAccessControl:(SecAccessControlRef)accessControl completionHandler:(void (^)(LAContext *))completionHandler;
+        @end
+
+        Illustrations:
+        1) _WKWebAuthenticationPanelUpdate: Three errors are added to help clients present meaningful error messages to users.
+        a) WKWebAuthenticationPanelUpdateLAError: An internal error, clients should inform users and terminate the platform
+        authentication process. This error can be returned at any time.
+        b) _WKWebAuthenticationPanelUpdateLADuplicateCredential: It means a credential is found to match an entry in the
+        excludeList. Clients should inform users and terminate the platform authentication process. This error will only be
+        returned during makeCredential and before verifyUserWithAccessControl delegate.
+        c) _WKWebAuthenticationPanelUpdateLANoCredential: It means no credentials are found. Clients should inform users and
+        terminate the platform authentication process. This error will only be returned during getAssertion and before
+        verifyUserWithAccessControl delegate.
+
+        2) _WKWebAuthenticationTransport: _WKWebAuthenticationTransportInternal is added such that clients can learn platform
+        authenticator will be used from _WKWebAuthenticationPanel.transports.
+
+        3) verifyUserWithAccessControl: A delegate that will be called during makeCredential or getAssertion when the platform
+        authenticator is involved. This delegate is used to obtain user verification from a LAContext. In addition, the LAContext
+        should evaluate the passed accessControl, such that the SEP protected credential private key can be used. A typical
+        example will be [LAContext evaluateAccessControl:accessControl operation:LAAccessControlOperationUseKeySign localizedReason:reply:].
+        Noted, for getAssertion, selectAssertionResponse will be called before verifyUserWithAccessControl. So users need to be
+        prompted to select a credential before the user verification.
+
+        In the scenario when both the platform authenticator and external authenticators are requested. Clients are advised to
+        wait until verifyUserWithAccessControl to show the combined UI. If any of the LAError states are received before
+        verifyUserWithAccessControl, clients should then only show the external authenticator UI. Also, platform authenticator and
+        external authenticators are being discovered at the same time, which means a user can plug in a security key at anytime.
+        If a valid response is received from the security key, the whole ceremony will be terminated.
+
+        Besides introducing the SPI, and all the necessary plumbing to make it happen. This patch also:
+        1) adds LocalAuthenticationSPI, which is used to check whether a given LAContext is unlocked or not;
+        2) improves MockLocalConnection such that mock testing can still be ran.
+
+        * Platform/spi/Cocoa/LocalAuthenticationSPI.h: Copied from Source/WebKit/UIProcess/WebAuthentication/Cocoa/LocalAuthenticationSoftLink.h.
+        * UIProcess/API/APIWebAuthenticationPanel.cpp:
+        (API::WebAuthenticationPanel::WebAuthenticationPanel):
+        * UIProcess/API/APIWebAuthenticationPanelClient.h:
+        (API::WebAuthenticationPanelClient::verifyUser const):
+        * UIProcess/API/Cocoa/_WKWebAuthenticationPanel.h:
+        * UIProcess/API/Cocoa/_WKWebAuthenticationPanel.mm:
+        (wkWebAuthenticationTransport):
+        * UIProcess/WebAuthentication/Authenticator.h:
+        * UIProcess/WebAuthentication/AuthenticatorManager.cpp:
+        (WebKit::AuthenticatorManager::verifyUser):
+        * UIProcess/WebAuthentication/AuthenticatorManager.h:
+        * UIProcess/WebAuthentication/Cocoa/LocalAuthenticationSoftLink.h:
+        * UIProcess/WebAuthentication/Cocoa/LocalAuthenticator.h:
+        * UIProcess/WebAuthentication/Cocoa/LocalAuthenticator.mm:
+        (WebKit::LocalAuthenticatorInternal::toNSData):
+        (WebKit::LocalAuthenticatorInternal::toArrayBuffer):
+        (WebKit::LocalAuthenticator::makeCredential):
+        (WebKit::LocalAuthenticator::continueMakeCredentialAfterUserConsented):
+        (WebKit::LocalAuthenticator::continueMakeCredentialAfterAttested):
+        (WebKit::LocalAuthenticator::getAssertion):
+        (WebKit::LocalAuthenticator::continueGetAssertionAfterResponseSelected):
+        (WebKit::LocalAuthenticator::continueGetAssertionAfterUserConsented):
+        (WebKit::LocalAuthenticator::receiveException const):
+        * UIProcess/WebAuthentication/Cocoa/LocalConnection.h:
+        (WebKit::LocalConnection::filterResponses const):
+        * UIProcess/WebAuthentication/Cocoa/LocalConnection.mm:
+        (WebKit::LocalConnection::isUnlocked const):
+        (WebKit::LocalConnection::getUserConsent const): Deleted.
+        (WebKit::LocalConnection::selectCredential const): Deleted.
+        * UIProcess/WebAuthentication/Cocoa/WebAuthenticationPanelClient.h:
+        * UIProcess/WebAuthentication/Cocoa/WebAuthenticationPanelClient.mm:
+        (WebKit::WebAuthenticationPanelClient::WebAuthenticationPanelClient):
+        (WebKit::wkWebAuthenticationPanelUpdate):
+        (WebKit::WebAuthenticationPanelClient::selectAssertionResponse const):
+        (WebKit::WebAuthenticationPanelClient::verifyUser const):
+        * UIProcess/WebAuthentication/Mock/MockLocalConnection.h:
+        * UIProcess/WebAuthentication/Mock/MockLocalConnection.mm:
+        (WebKit::MockLocalConnection::isUnlocked const):
+        (WebKit::MockLocalConnection::filterResponses const):
+        (WebKit::MockLocalConnection::getUserConsent const): Deleted.
+        (WebKit::MockLocalConnection::selectCredential const): Deleted.
+        * UIProcess/WebAuthentication/WebAuthenticationFlags.h:
+        * WebKit.xcodeproj/project.pbxproj:
+
 2020-02-24  Per Arne Vollan  <pvollan@apple.com>
 
         [iOS] Use one telemetry decoration for each sandbox rule
diff --git a/Source/WebKit/Platform/spi/Cocoa/LocalAuthenticationSPI.h b/Source/WebKit/Platform/spi/Cocoa/LocalAuthenticationSPI.h
new file mode 100644 (file)
index 0000000..f7a0f6d
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 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
+
+#import <LocalAuthentication/LocalAuthentication.h>
+
+#if USE(APPLE_INTERNAL_SDK)
+
+#import <LocalAuthentication/LocalAuthentication_Private.h>
+
+#else
+
+typedef NS_ENUM(NSInteger, LAOption) {
+    LAOptionNotInteractive,
+};
+
+@interface LAContext(Private) <NSSecureCoding>
+
+- (NSDictionary *)evaluatePolicy:(LAPolicy)policy options:(NSDictionary *)options error:(NSError **)error;
+
+@end
+
+#endif // USE(APPLE_INTERNAL_SDK)
index 0cddbcf..e5cb473 100644 (file)
@@ -53,6 +53,8 @@ WebAuthenticationPanel::WebAuthenticationPanel(const AuthenticatorManager& manag
         m_transports.uncheckedAppend(AuthenticatorTransport::Usb);
     if (transports.contains(AuthenticatorTransport::Nfc))
         m_transports.uncheckedAppend(AuthenticatorTransport::Nfc);
+    if (transports.contains(AuthenticatorTransport::Internal))
+        m_transports.uncheckedAppend(AuthenticatorTransport::Internal);
 }
 
 WebAuthenticationPanel::~WebAuthenticationPanel() = default;
index 459644e..8c36853 100644 (file)
 #include <wtf/CompletionHandler.h>
 #include <wtf/HashSet.h>
 #include <wtf/RefCounted.h>
+#include <wtf/spi/cocoa/SecuritySPI.h>
 #include <wtf/text/WTFString.h>
 
+OBJC_CLASS LAContext;
+
 namespace WebCore {
 class AuthenticatorAssertionResponse;
 }
@@ -52,6 +55,7 @@ public:
     virtual void dismissPanel(WebKit::WebAuthenticationResult) const { }
     virtual void requestPin(uint64_t, CompletionHandler<void(const WTF::String&)>&& completionHandler) const { completionHandler(emptyString()); }
     virtual void selectAssertionResponse(Vector<Ref<WebCore::AuthenticatorAssertionResponse>>&& responses, CompletionHandler<void(const WebCore::AuthenticatorAssertionResponse&)>&& completionHandler) const { ASSERT(!responses.isEmpty()); completionHandler(responses[0]); }
+    virtual void verifyUser(SecAccessControlRef, CompletionHandler<void(LAContext *)>&& completionHandler) const { completionHandler(nullptr); }
 };
 
 } // namespace API
index 431c633..9741dd6 100644 (file)
@@ -31,6 +31,7 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
+@class LAContext;
 @class _WKWebAuthenticationAssertionResponse;
 @class _WKWebAuthenticationPanel;
 
@@ -46,6 +47,9 @@ typedef NS_ENUM(NSInteger, _WKWebAuthenticationPanelUpdate) {
     _WKWebAuthenticationPanelUpdatePINBlocked,
     _WKWebAuthenticationPanelUpdatePINAuthBlocked,
     _WKWebAuthenticationPanelUpdatePINInvalid,
+    _WKWebAuthenticationPanelUpdateLAError,
+    _WKWebAuthenticationPanelUpdateLAExcludeCredentialsMatched,
+    _WKWebAuthenticationPanelUpdateLANoCredential,
 } WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
 
 typedef NS_ENUM(NSInteger, _WKWebAuthenticationResult) {
@@ -56,6 +60,7 @@ typedef NS_ENUM(NSInteger, _WKWebAuthenticationResult) {
 typedef NS_ENUM(NSInteger, _WKWebAuthenticationTransport) {
     _WKWebAuthenticationTransportUSB,
     _WKWebAuthenticationTransportNFC,
+    _WKWebAuthenticationTransportInternal,
 } WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
 
 typedef NS_ENUM(NSInteger, _WKWebAuthenticationType) {
@@ -71,6 +76,7 @@ typedef NS_ENUM(NSInteger, _WKWebAuthenticationType) {
 - (void)panel:(_WKWebAuthenticationPanel *)panel dismissWebAuthenticationPanelWithResult:(_WKWebAuthenticationResult)result WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
 - (void)panel:(_WKWebAuthenticationPanel *)panel requestPINWithRemainingRetries:(NSUInteger)retries completionHandler:(void (^)(NSString *))completionHandler WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
 - (void)panel:(_WKWebAuthenticationPanel *)panel selectAssertionResponse:(NSArray < _WKWebAuthenticationAssertionResponse *> *)responses completionHandler:(void (^)(_WKWebAuthenticationAssertionResponse *))completionHandler WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
+- (void)panel:(_WKWebAuthenticationPanel *)panel verifyUserWithAccessControl:(SecAccessControlRef)accessControl completionHandler:(void (^)(LAContext *))completionHandler WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
 
 @end
 
index 2eb31e8..1973b0f 100644 (file)
@@ -73,6 +73,8 @@ static _WKWebAuthenticationTransport wkWebAuthenticationTransport(WebCore::Authe
         return _WKWebAuthenticationTransportUSB;
     case WebCore::AuthenticatorTransport::Nfc:
         return _WKWebAuthenticationTransportNFC;
+    case WebCore::AuthenticatorTransport::Internal:
+        return _WKWebAuthenticationTransportInternal;
     default:
         ASSERT_NOT_REACHED();
         return _WKWebAuthenticationTransportUSB;
index d16b17d..91d99ed 100644 (file)
@@ -34,6 +34,9 @@
 #include <wtf/Forward.h>
 #include <wtf/RefCounted.h>
 #include <wtf/WeakPtr.h>
+#include <wtf/spi/cocoa/SecuritySPI.h>
+
+OBJC_CLASS LAContext;
 
 namespace WebCore {
 class AuthenticatorAssertionResponse;
@@ -53,6 +56,7 @@ public:
         virtual void authenticatorStatusUpdated(WebAuthenticationStatus) = 0;
         virtual void requestPin(uint64_t retries, CompletionHandler<void(const WTF::String&)>&&) = 0;
         virtual void selectAssertionResponse(const HashSet<Ref<WebCore::AuthenticatorAssertionResponse>>&, CompletionHandler<void(const WebCore::AuthenticatorAssertionResponse&)>&&) = 0;
+        virtual void verifyUser(SecAccessControlRef, CompletionHandler<void(LAContext *)>&&) = 0;
     };
 
     virtual ~Authenticator() = default;
index 73626cc..885971f 100644 (file)
@@ -295,6 +295,14 @@ void AuthenticatorManager::selectAssertionResponse(const HashSet<Ref<Authenticat
     });
 }
 
+void AuthenticatorManager::verifyUser(SecAccessControlRef accessControlRef, CompletionHandler<void(LAContext *)>&& completionHandler)
+{
+    RetainPtr<SecAccessControlRef> accessControl = accessControlRef;
+    dispatchPanelClientCall([accessControl = WTFMove(accessControl), completionHandler = WTFMove(completionHandler)] (const API::WebAuthenticationPanel& panel) mutable {
+        panel.client().verifyUser(accessControl.get(), WTFMove(completionHandler));
+    });
+}
+
 UniqueRef<AuthenticatorTransportService> AuthenticatorManager::createService(AuthenticatorTransport transport, AuthenticatorTransportService::Observer& observer) const
 {
     return AuthenticatorTransportService::create(transport, observer);
index c151dd9..e8f2d2b 100644 (file)
@@ -83,6 +83,7 @@ private:
     void authenticatorStatusUpdated(WebAuthenticationStatus) final;
     void requestPin(uint64_t retries, CompletionHandler<void(const WTF::String&)>&&) final;
     void selectAssertionResponse(const HashSet<Ref<WebCore::AuthenticatorAssertionResponse>>&, CompletionHandler<void(const WebCore::AuthenticatorAssertionResponse&)>&&) final;
+    void verifyUser(SecAccessControlRef, CompletionHandler<void(LAContext *)>&&) final;
 
     // Overriden by MockAuthenticatorManager.
     virtual UniqueRef<AuthenticatorTransportService> createService(WebCore::AuthenticatorTransport, AuthenticatorTransportService::Observer&) const;
index 1552b27..e849102 100644 (file)
@@ -25,7 +25,7 @@
 
 #pragma once
 
-#import <LocalAuthentication/LocalAuthentication.h>
+#import "LocalAuthenticationSPI.h"
 #import <wtf/SoftLinking.h>
 
 SOFT_LINK_FRAMEWORK_FOR_HEADER(WebKit, LocalAuthentication);
index ded98f2..9a47570 100644 (file)
@@ -39,12 +39,13 @@ class LocalAuthenticator final : public Authenticator {
 public:
     // Here is the FSM.
     // MakeCredential: Init => RequestReceived => UserConsented => Attested => End
-    // GetAssertion: Init => RequestReceived => UserConsented => End
+    // GetAssertion: Init => RequestReceived => ResponseSelected => UserConsented => End
     enum class State {
         Init,
         RequestReceived,
         UserConsented,
         Attested,
+        ResponseSelected
     };
 
     static Ref<LocalAuthenticator> create(UniqueRef<LocalConnection>&& connection)
@@ -56,14 +57,18 @@ private:
     explicit LocalAuthenticator(UniqueRef<LocalConnection>&&);
 
     void makeCredential() final;
-    void continueMakeCredentialAfterUserConsented(SecAccessControlRef, LocalConnection::UserConsent, LAContext *);
+    void continueMakeCredentialAfterUserConsented(SecAccessControlRef, LAContext *);
     void continueMakeCredentialAfterAttested(SecKeyRef, Vector<uint8_t>&& credentialId, Vector<uint8_t>&& authData, NSArray *certificates, NSError *);
 
     void getAssertion() final;
-    void continueGetAssertionAfterUserConsented(LocalConnection::UserConsent, LAContext *, const Vector<uint8_t>& credentialId, const Vector<uint8_t>& userhandle);
+    void continueGetAssertionAfterResponseSelected(Ref<WebCore::AuthenticatorAssertionResponse>&&);
+    void continueGetAssertionAfterUserConsented(LAContext *, Ref<WebCore::AuthenticatorAssertionResponse>&&);
+
+    void receiveException(WebCore::ExceptionData&&, WebAuthenticationStatus = WebAuthenticationStatus::LAError) const;
 
     State m_state { State::Init };
     UniqueRef<LocalConnection> m_connection;
+    HashSet<Ref<WebCore::AuthenticatorAssertionResponse>> m_assertionResponses;
 };
 
 } // namespace WebKit
index 25dd1c6..ffc60ea 100644 (file)
@@ -86,6 +86,18 @@ static inline RetainPtr<NSData> toNSData(const Vector<uint8_t>& data)
     return adoptNS([[NSData alloc] initWithBytes:data.data() length:data.size()]);
 }
 
+static inline RetainPtr<NSData> toNSData(ArrayBuffer* buffer)
+{
+    ASSERT(buffer);
+    // FIXME(183534): Consider using initWithBytesNoCopy.
+    return adoptNS([[NSData alloc] initWithBytes:buffer->data() length:buffer->byteLength()]);
+}
+
+static inline Ref<ArrayBuffer> toArrayBuffer(NSData *data)
+{
+    return ArrayBuffer::create(reinterpret_cast<const uint8_t*>(data.bytes), data.length);
+}
+
 } // LocalAuthenticatorInternal
 
 LocalAuthenticator::LocalAuthenticator(UniqueRef<LocalConnection>&& connection)
@@ -108,7 +120,7 @@ void LocalAuthenticator::makeCredential()
     if (notFound == creationOptions.pubKeyCredParams.findMatching([] (auto& pubKeyCredParam) {
         return pubKeyCredParam.type == PublicKeyCredentialType::PublicKey && pubKeyCredParam.alg == COSE::ES256;
     })) {
-        receiveRespond(ExceptionData { NotSupportedError, "The platform attached authenticator doesn't support any provided PublicKeyCredentialParameters."_s });
+        receiveException({ NotSupportedError, "The platform attached authenticator doesn't support any provided PublicKeyCredentialParameters."_s });
         return;
     }
 
@@ -131,24 +143,22 @@ void LocalAuthenticator::makeCredential()
         CFTypeRef attributesArrayRef = nullptr;
         OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &attributesArrayRef);
         if (status && status != errSecItemNotFound) {
-            LOG_ERROR("Couldn't query Keychain: %d", status);
-            receiveRespond(ExceptionData { UnknownError, makeString("Couldn't query Keychain: ", status) });
+            receiveException({ UnknownError, makeString("Couldn't query Keychain: ", status) });
             return;
         }
         auto retainAttributesArray = adoptCF(attributesArrayRef);
 
-        // FIXME(rdar://problem/35900593): Need to obtain user consent and then return different error according to the result.
+        // FIXME: Need to obtain user consent and then return different error according to the result.
         for (NSDictionary *nsAttributes in (NSArray *)attributesArrayRef) {
             NSData *nsCredentialId = nsAttributes[(id)kSecAttrApplicationLabel];
             if (excludeCredentialIds.contains(String(reinterpret_cast<const char*>(nsCredentialId.bytes), nsCredentialId.length))) {
-                receiveRespond(ExceptionData { NotAllowedError, "At least one credential matches an entry of the excludeCredentials list in the platform attached authenticator."_s });
+                receiveException({ NotAllowedError, "At least one credential matches an entry of the excludeCredentials list in the platform attached authenticator."_s }, WebAuthenticationStatus::LAExcludeCredentialsMatched);
                 return;
             }
         }
     }
 
     // Step 6.
-    // FIXME(rdar://problem/35900593): Update to a formal UI.
     // Get user consent.
     RetainPtr<SecAccessControlRef> accessControl;
     {
@@ -156,27 +166,25 @@ void LocalAuthenticator::makeCredential()
         accessControl = adoptCF(SecAccessControlCreateWithFlags(NULL, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlPrivateKeyUsage | kSecAccessControlUserPresence, &errorRef));
         auto retainError = adoptCF(errorRef);
         if (errorRef) {
-            LOG_ERROR("Couldn't create access control: %@", (NSError *)errorRef);
-            receiveRespond(ExceptionData { UnknownError, makeString("Couldn't create access control: ", String(((NSError*)errorRef).localizedDescription)) });
+            receiveException({ UnknownError, makeString("Couldn't create access control: ", String(((NSError*)errorRef).localizedDescription)) });
             return;
         }
     }
 
-    SecAccessControlRef accessControlRef = accessControl.get();
-    auto callback = [accessControl = WTFMove(accessControl), weakThis = makeWeakPtr(*this)] (LocalConnection::UserConsent consent, LAContext *context) {
-        ASSERT(RunLoop::isMain());
-        if (!weakThis)
-            return;
+    if (auto* observer = this->observer()) {
+        SecAccessControlRef accessControlRef = accessControl.get();
+        auto callback = [accessControl = WTFMove(accessControl), weakThis = makeWeakPtr(*this)] (LAContext *context) {
+            ASSERT(RunLoop::isMain());
+            if (!weakThis)
+                return;
 
-        weakThis->continueMakeCredentialAfterUserConsented(accessControl.get(), consent, context);
-    };
-    m_connection->getUserConsent(
-        makeString("allow "_s, creationOptions.rp.id, " to create a public key credential for "_s, creationOptions.user.name),
-        accessControlRef,
-        WTFMove(callback));
+            weakThis->continueMakeCredentialAfterUserConsented(accessControl.get(), context);
+        };
+        observer->verifyUser(accessControlRef, WTFMove(callback));
+    }
 }
 
-void LocalAuthenticator::continueMakeCredentialAfterUserConsented(SecAccessControlRef accessControlRef, LocalConnection::UserConsent consent, LAContext *context)
+void LocalAuthenticator::continueMakeCredentialAfterUserConsented(SecAccessControlRef accessControlRef, LAContext *context)
 {
     using namespace LocalAuthenticatorInternal;
 
@@ -184,7 +192,7 @@ void LocalAuthenticator::continueMakeCredentialAfterUserConsented(SecAccessContr
     m_state = State::UserConsented;
     auto& creationOptions = WTF::get<PublicKeyCredentialCreationOptions>(requestData().options);
 
-    if (consent == LocalConnection::UserConsent::No) {
+    if (!m_connection->isUnlocked(context)) {
         receiveRespond(ExceptionData { NotAllowedError, "Couldn't get user consent."_s });
         return;
     }
@@ -213,8 +221,7 @@ void LocalAuthenticator::continueMakeCredentialAfterUserConsented(SecAccessContr
     };
     OSStatus status = SecItemDelete((__bridge CFDictionaryRef)deleteQuery);
     if (status && status != errSecItemNotFound) {
-        LOG_ERROR("Couldn't delete older credential: %d", status);
-        receiveRespond(ExceptionData { UnknownError, makeString("Couldn't delete older credential: ", status) });
+        receiveException({ UnknownError, makeString("Couldn't delete older credential: ", status) });
         return;
     }
 
@@ -222,7 +229,7 @@ void LocalAuthenticator::continueMakeCredentialAfterUserConsented(SecAccessContr
     // The above-to-create private key will be inserted into keychain while using SEP.
     auto privateKey = m_connection->createCredentialPrivateKey(context, accessControlRef, secAttrLabel, secAttrApplicationTag.get());
     if (!privateKey) {
-        receiveRespond(ExceptionData { UnknownError, "Couldn't create private key."_s });
+        receiveException({ UnknownError, "Couldn't create private key."_s });
         return;
     }
 
@@ -243,8 +250,7 @@ void LocalAuthenticator::continueMakeCredentialAfterUserConsented(SecAccessContr
         CFTypeRef attributesRef = nullptr;
         OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)credentialIdQuery, &attributesRef);
         if (status) {
-            LOG_ERROR("Couldn't get Credential ID: %d", status);
-            receiveRespond(ExceptionData { UnknownError, makeString("Couldn't get Credential ID: ", status) });
+            receiveException({ UnknownError, makeString("Couldn't get Credential ID: ", status) });
             return;
         }
         auto retainAttributes = adoptCF(attributesRef);
@@ -268,8 +274,7 @@ void LocalAuthenticator::continueMakeCredentialAfterUserConsented(SecAccessContr
             publicKeyDataRef = adoptCF(SecKeyCopyExternalRepresentation(publicKey.get(), &errorRef));
             auto retainError = adoptCF(errorRef);
             if (errorRef) {
-                LOG_ERROR("Couldn't export the public key: %@", (NSError*)errorRef);
-                receiveRespond(ExceptionData { UnknownError, makeString("Couldn't export the public key: ", String(((NSError*)errorRef).localizedDescription)) });
+                receiveException({ UnknownError, makeString("Couldn't export the public key: ", String(((NSError*)errorRef).localizedDescription)) });
                 return;
             }
             ASSERT(((NSData *)publicKeyDataRef.get()).length == (1 + 2 * ES256FieldElementLength)); // 04 | X | Y
@@ -309,8 +314,7 @@ void LocalAuthenticator::continueMakeCredentialAfterAttested(SecKeyRef privateKe
     auto& creationOptions = WTF::get<PublicKeyCredentialCreationOptions>(requestData().options);
 
     if (error) {
-        LOG_ERROR("Couldn't attest: %@", error);
-        receiveRespond(ExceptionData { UnknownError, makeString("Couldn't attest: ", String(error.localizedDescription)) });
+        receiveException({ UnknownError, makeString("Couldn't attest: ", String(error.localizedDescription)) });
         return;
     }
     // Attestation Certificate and Attestation Issuing CA
@@ -344,10 +348,9 @@ void LocalAuthenticator::getAssertion()
     // Skip Step 8 as extensions are not supported yet.
     // Step 12 is implicitly captured by all UnknownError exception callbacks.
     // Step 3-5. Unlike the spec, if an allow list is provided and there is no intersection between existing ones and the allow list, we always return NotAllowedError.
-    // FIXME(rdar://problem/35900593): Need to inform users.
     auto allowCredentialIds = produceHashSet(requestOptions.allowCredentials);
     if (!requestOptions.allowCredentials.isEmpty() && allowCredentialIds.isEmpty()) {
-        receiveRespond(ExceptionData { NotAllowedError, "No matched credentials are found in the platform attached authenticator."_s });
+        receiveException({ NotAllowedError, "No matched credentials are found in the platform attached authenticator."_s }, WebAuthenticationStatus::LANoCredential);
         return;
     }
 
@@ -367,8 +370,7 @@ void LocalAuthenticator::getAssertion()
     CFTypeRef attributesArrayRef = nullptr;
     OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &attributesArrayRef);
     if (status && status != errSecItemNotFound) {
-        LOG_ERROR("Couldn't query Keychain: %d", status);
-        receiveRespond(ExceptionData { UnknownError, makeString("Couldn't query Keychain: ", status) });
+        receiveException({ UnknownError, makeString("Couldn't query Keychain: ", status) });
         return;
     }
     auto retainAttributesArray = adoptCF(attributesArrayRef);
@@ -386,41 +388,61 @@ void LocalAuthenticator::getAssertion()
         intersectedCredentialsAttributes = result;
     }
     if (!intersectedCredentialsAttributes.count) {
-        receiveRespond(ExceptionData { NotAllowedError, "No matched credentials are found in the platform attached authenticator."_s });
+        receiveException({ NotAllowedError, "No matched credentials are found in the platform attached authenticator."_s }, WebAuthenticationStatus::LANoCredential);
         return;
     }
 
     // Step 6.
-    auto *selectedCredentialAttributes = m_connection->selectCredential(intersectedCredentialsAttributes);
+    for (NSDictionary *attribute : intersectedCredentialsAttributes) {
+        auto addResult = m_assertionResponses.add(AuthenticatorAssertionResponse::create(
+            toArrayBuffer(attribute[(id)kSecAttrApplicationLabel]),
+            toArrayBuffer(attribute[(id)kSecAttrApplicationTag]),
+            (__bridge SecAccessControlRef)attribute[(id)kSecAttrAccessControl]));
+        ASSERT_UNUSED(addResult, addResult.isNewEntry);
+    }
+    m_connection->filterResponses(m_assertionResponses);
+
+    if (auto* observer = this->observer()) {
+        auto callback = [this, weakThis = makeWeakPtr(*this)] (const AuthenticatorAssertionResponse& response) {
+            ASSERT(RunLoop::isMain());
+            if (!weakThis)
+                return;
+
+            auto returnResponse = m_assertionResponses.take(const_cast<AuthenticatorAssertionResponse*>(&response));
+            if (!returnResponse)
+                return;
+            continueGetAssertionAfterResponseSelected(WTFMove(*returnResponse));
+        };
+        observer->selectAssertionResponse(m_assertionResponses, WTFMove(callback));
+    }
+}
+
+void LocalAuthenticator::continueGetAssertionAfterResponseSelected(Ref<WebCore::AuthenticatorAssertionResponse>&& response)
+{
+    ASSERT(m_state == State::RequestReceived);
+    m_state = State::ResponseSelected;
 
     // Step 7. Get user consent.
-    // FIXME(rdar://problem/35900593): Update to a formal UI.
-    auto callback = [
-        weakThis = makeWeakPtr(*this),
-        credentialId = toVector(selectedCredentialAttributes[(id)kSecAttrApplicationLabel]),
-        userhandle = toVector(selectedCredentialAttributes[(id)kSecAttrApplicationTag])
-    ](LocalConnection::UserConsent consent, LAContext *context) {
-        ASSERT(RunLoop::isMain());
-        if (!weakThis)
-            return;
+    if (auto* observer = this->observer()) {
+        auto accessControlRef = response->accessControl();
+        auto callback = [weakThis = makeWeakPtr(*this), response = WTFMove(response)] (LAContext *context) mutable {
+            ASSERT(RunLoop::isMain());
+            if (!weakThis)
+                return;
 
-        weakThis->continueGetAssertionAfterUserConsented(consent, context, credentialId, userhandle);
-    };
-    NSData *idData = selectedCredentialAttributes[(id)kSecAttrApplicationTag];
-    StringView idStringView { static_cast<const UChar*>([idData bytes]), static_cast<unsigned>([idData length]) };
-    m_connection->getUserConsent(
-        makeString("log into ", requestOptions.rpId, " with ", idStringView),
-        (__bridge SecAccessControlRef)selectedCredentialAttributes[(id)kSecAttrAccessControl],
-        WTFMove(callback));
+            weakThis->continueGetAssertionAfterUserConsented(context, WTFMove(response));
+        };
+        observer->verifyUser(accessControlRef, WTFMove(callback));
+    }
 }
 
-void LocalAuthenticator::continueGetAssertionAfterUserConsented(LocalConnection::UserConsent consent, LAContext *context, const Vector<uint8_t>& credentialId, const Vector<uint8_t>& userhandle)
+void LocalAuthenticator::continueGetAssertionAfterUserConsented(LAContext *context, Ref<WebCore::AuthenticatorAssertionResponse>&& response)
 {
     using namespace LocalAuthenticatorInternal;
-    ASSERT(m_state == State::RequestReceived);
+    ASSERT(m_state == State::ResponseSelected);
     m_state = State::UserConsented;
 
-    if (consent == LocalConnection::UserConsent::No) {
+    if (!m_connection->isUnlocked(context)) {
         receiveRespond(ExceptionData { NotAllowedError, "Couldn't get user consent."_s });
         return;
     }
@@ -432,25 +454,27 @@ void LocalAuthenticator::continueGetAssertionAfterUserConsented(LocalConnection:
     auto authData = buildAuthData(WTF::get<PublicKeyCredentialRequestOptions>(requestData().options).rpId, getAssertionFlags, counter, { });
 
     // Step 11.
-    Vector<uint8_t> signature;
+    RetainPtr<CFDataRef> signature;
     {
-        NSDictionary *query = @{
+        auto query = adoptNS([[NSMutableDictionary alloc] init]);
+        [query addEntriesFromDictionary:@{
             (id)kSecClass: (id)kSecClassKey,
             (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate,
-            (id)kSecAttrApplicationLabel: toNSData(credentialId).get(),
-            (id)kSecUseAuthenticationContext: context,
+            (id)kSecAttrApplicationLabel: toNSData(response->rawId()).get(),
             (id)kSecReturnRef: @YES,
 #if HAVE(DATA_PROTECTION_KEYCHAIN)
             (id)kSecUseDataProtectionKeychain: @YES
 #else
             (id)kSecAttrNoLegacy: @YES
 #endif
-        };
+        }];
+        // context is nullptr in mock testing.
+        if (context)
+            [query setObject:context forKey:(id)kSecUseAuthenticationContext];
         CFTypeRef privateKeyRef = nullptr;
-        OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &privateKeyRef);
+        OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query.get(), &privateKeyRef);
         if (status) {
-            LOG_ERROR("Couldn't get the private key reference: %d", status);
-            receiveRespond(ExceptionData { UnknownError, makeString("Couldn't get the private key reference: ", status) });
+            receiveException({ UnknownError, makeString("Couldn't get the private key reference: ", status) });
             return;
         }
         auto privateKey = adoptCF(privateKeyRef);
@@ -460,18 +484,27 @@ void LocalAuthenticator::continueGetAssertionAfterUserConsented(LocalConnection:
 
         CFErrorRef errorRef = nullptr;
         // FIXME: Converting CFTypeRef to SecKeyRef is quite subtle here.
-        auto signatureRef = adoptCF(SecKeyCreateSignature((__bridge SecKeyRef)((id)privateKeyRef), kSecKeyAlgorithmECDSASignatureMessageX962SHA256, (__bridge CFDataRef)dataToSign, &errorRef));
+        signature = adoptCF(SecKeyCreateSignature((__bridge SecKeyRef)((id)privateKeyRef), kSecKeyAlgorithmECDSASignatureMessageX962SHA256, (__bridge CFDataRef)dataToSign, &errorRef));
         auto retainError = adoptCF(errorRef);
         if (errorRef) {
-            LOG_ERROR("Couldn't generate the signature: %@", (NSError*)errorRef);
-            receiveRespond(ExceptionData { UnknownError, makeString("Couldn't generate the signature: ", String(((NSError*)errorRef).localizedDescription)) });
+            receiveException({ UnknownError, makeString("Couldn't generate the signature: ", String(((NSError*)errorRef).localizedDescription)) });
             return;
         }
-        signature = toVector((NSData *)signatureRef.get());
     }
 
     // Step 13.
-    receiveRespond(AuthenticatorAssertionResponse::create(credentialId, authData, signature, userhandle));
+    response->setAuthenticatorData(WTFMove(authData));
+    response->setSignature(toArrayBuffer((NSData *)signature.get()));
+    receiveRespond(WTFMove(response));
+}
+
+void LocalAuthenticator::receiveException(ExceptionData&& exception, WebAuthenticationStatus status) const
+{
+    LOG_ERROR(exception.message.utf8().data());
+    if (auto* observer = this->observer())
+        observer->authenticatorStatusUpdated(status);
+    receiveRespond(WTFMove(exception));
+    return;
 }
 
 } // namespace WebKit
index 9191b2c..545ac5f 100644 (file)
@@ -46,23 +46,16 @@ class LocalConnection {
     WTF_MAKE_FAST_ALLOCATED;
     WTF_MAKE_NONCOPYABLE(LocalConnection);
 public:
-    enum class UserConsent {
-        No,
-        Yes
-    };
-
     using AttestationCallback = CompletionHandler<void(NSArray *, NSError *)>;
-    using UserConsentCallback = CompletionHandler<void(UserConsent)>;
-    using UserConsentContextCallback = CompletionHandler<void(UserConsent, LAContext *)>;
 
     LocalConnection() = default;
     virtual ~LocalConnection() = default;
 
     // Overrided by MockLocalConnection.
-    virtual void getUserConsent(const String& reason, SecAccessControlRef, UserConsentContextCallback&&) const;
+    virtual bool isUnlocked(LAContext *) const;
     virtual RetainPtr<SecKeyRef> createCredentialPrivateKey(LAContext *, SecAccessControlRef, const String& secAttrLabel, NSData *secAttrApplicationTag) const;
     virtual void getAttestation(SecKeyRef, NSData *authData, NSData *hash, AttestationCallback&&) const;
-    virtual NSDictionary *selectCredential(const NSArray *) const;
+    virtual void filterResponses(HashSet<Ref<WebCore::AuthenticatorAssertionResponse>>&) const { };
 };
 
 } // namespace WebKit
index 8baeee7..53e0123 100644 (file)
 
 namespace WebKit {
 
-void LocalConnection::getUserConsent(const String& reason, SecAccessControlRef accessControl, UserConsentContextCallback&& completionHandler) const
+bool LocalConnection::isUnlocked(LAContext *context) const
 {
-    auto context = adoptNS([allocLAContextInstance() init]);
-    auto reply = makeBlockPtr([context, completionHandler = WTFMove(completionHandler)] (BOOL success, NSError *error) mutable {
-        ASSERT(!RunLoop::isMain());
-
-        UserConsent consent = UserConsent::Yes;
-        if (!success || error) {
-            LOG_ERROR("Couldn't authenticate with biometrics: %@", error);
-            consent = UserConsent::No;
-        }
-        RunLoop::main().dispatch([completionHandler = WTFMove(completionHandler), consent, context = WTFMove(context)]() mutable {
-            completionHandler(consent, context.get());
-        });
-    });
-    [context evaluateAccessControl:accessControl operation:LAAccessControlOperationUseKeySign localizedReason:reason reply:reply.get()];
+    NSError *error = nil;
+    auto result = [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics options:@{ @(LAOptionNotInteractive): @YES } error:&error];
+    if (!result)
+        LOG_ERROR("Couldn't get user consent: %@", error);
+    return !!result;
 }
 
 RetainPtr<SecKeyRef> LocalConnection::createCredentialPrivateKey(LAContext *context, SecAccessControlRef accessControlRef, const String& secAttrLabel, NSData *secAttrApplicationTag) const
@@ -88,12 +79,6 @@ LOCALCONNECTION_ADDITIONS
 #endif
 }
 
-NSDictionary *LocalConnection::selectCredential(const NSArray *credentials) const
-{
-    // FIXME(rdar://problem/35900534): We don't have an UI to prompt users for selecting intersectedCredentials, and therefore we always use the first one for now.
-    return credentials[0];
-}
-
 } // namespace WebKit
 
 #endif // ENABLE(WEB_AUTHN)
index 3c6b3d7..df3d95a 100644 (file)
@@ -50,6 +50,7 @@ private:
     void dismissPanel(WebAuthenticationResult) const final;
     void requestPin(uint64_t, CompletionHandler<void(const WTF::String&)>&&) const final;
     void selectAssertionResponse(Vector<Ref<WebCore::AuthenticatorAssertionResponse>>&&, CompletionHandler<void(const WebCore::AuthenticatorAssertionResponse&)>&&) const final;
+    void verifyUser(SecAccessControlRef, CompletionHandler<void(LAContext *)>&&) const final;
 
     _WKWebAuthenticationPanel *m_panel;
     WeakObjCPtr<id <_WKWebAuthenticationPanelDelegate> > m_delegate;
@@ -58,7 +59,8 @@ private:
         bool panelUpdateWebAuthenticationPanel : 1;
         bool panelDismissWebAuthenticationPanelWithResult : 1;
         bool panelRequestPinWithRemainingRetriesCompletionHandler : 1;
-        bool panelselectAssertionResponseCompletionHandler : 1;
+        bool panelSelectAssertionResponseCompletionHandler : 1;
+        bool panelVerifyUserWithAccessControlCompletionHandler : 1;
     } m_delegateMethods;
 };
 
index d516632..0e973fb 100644 (file)
@@ -38,6 +38,8 @@
 #import <wtf/BlockPtr.h>
 #import <wtf/RunLoop.h>
 
+#import "LocalAuthenticationSoftLink.h"
+
 namespace WebKit {
 
 WebAuthenticationPanelClient::WebAuthenticationPanelClient(_WKWebAuthenticationPanel *panel, id <_WKWebAuthenticationPanelDelegate> delegate)
@@ -47,7 +49,8 @@ WebAuthenticationPanelClient::WebAuthenticationPanelClient(_WKWebAuthenticationP
     m_delegateMethods.panelUpdateWebAuthenticationPanel = [delegate respondsToSelector:@selector(panel:updateWebAuthenticationPanel:)];
     m_delegateMethods.panelDismissWebAuthenticationPanelWithResult = [delegate respondsToSelector:@selector(panel:dismissWebAuthenticationPanelWithResult:)];
     m_delegateMethods.panelRequestPinWithRemainingRetriesCompletionHandler = [delegate respondsToSelector:@selector(panel:requestPINWithRemainingRetries:completionHandler:)];
-    m_delegateMethods.panelselectAssertionResponseCompletionHandler = [delegate respondsToSelector:@selector(panel:selectAssertionResponse:completionHandler:)];
+    m_delegateMethods.panelSelectAssertionResponseCompletionHandler = [delegate respondsToSelector:@selector(panel:selectAssertionResponse:completionHandler:)];
+    m_delegateMethods.panelVerifyUserWithAccessControlCompletionHandler = [delegate respondsToSelector:@selector(panel:verifyUserWithAccessControl:completionHandler:)];
 }
 
 RetainPtr<id <_WKWebAuthenticationPanelDelegate> > WebAuthenticationPanelClient::delegate()
@@ -67,6 +70,12 @@ static _WKWebAuthenticationPanelUpdate wkWebAuthenticationPanelUpdate(WebAuthent
         return _WKWebAuthenticationPanelUpdatePINAuthBlocked;
     if (status == WebAuthenticationStatus::PinInvalid)
         return _WKWebAuthenticationPanelUpdatePINInvalid;
+    if (status == WebAuthenticationStatus::LAError)
+        return _WKWebAuthenticationPanelUpdateLAError;
+    if (status == WebAuthenticationStatus::LAExcludeCredentialsMatched)
+        return _WKWebAuthenticationPanelUpdateLAExcludeCredentialsMatched;
+    if (status == WebAuthenticationStatus::LANoCredential)
+        return _WKWebAuthenticationPanelUpdateLANoCredential;
     ASSERT_NOT_REACHED();
     return _WKWebAuthenticationPanelUpdateMultipleNFCTagsPresent;
 }
@@ -133,7 +142,7 @@ void WebAuthenticationPanelClient::selectAssertionResponse(Vector<Ref<WebCore::A
 {
     ASSERT(!responses.isEmpty());
 
-    if (!m_delegateMethods.panelselectAssertionResponseCompletionHandler) {
+    if (!m_delegateMethods.panelSelectAssertionResponseCompletionHandler) {
         completionHandler(responses[0]);
         return;
     }
@@ -158,6 +167,28 @@ void WebAuthenticationPanelClient::selectAssertionResponse(Vector<Ref<WebCore::A
     }).get()];
 }
 
+void WebAuthenticationPanelClient::verifyUser(SecAccessControlRef accessControl, CompletionHandler<void(LAContext *)>&& completionHandler) const
+{
+    if (!m_delegateMethods.panelVerifyUserWithAccessControlCompletionHandler) {
+        completionHandler(adoptNS([allocLAContextInstance() init]).get());
+        return;
+    }
+
+    auto delegate = m_delegate.get();
+    if (!delegate) {
+        completionHandler(adoptNS([allocLAContextInstance() init]).get());
+        return;
+    }
+
+    auto checker = CompletionHandlerCallChecker::create(delegate.get(), @selector(panel:verifyUserWithAccessControl:completionHandler:));
+    [delegate panel:m_panel verifyUserWithAccessControl:accessControl completionHandler:makeBlockPtr([completionHandler = WTFMove(completionHandler), checker = WTFMove(checker)](LAContext *context) mutable {
+        if (checker->completionHandlerHasBeenCalled())
+            return;
+        checker->didCallCompletionHandler();
+        completionHandler(context);
+    }).get()];
+}
+
 } // namespace WebKit
 
 #endif // ENABLE(WEB_AUTHN)
index 720158a..d0dd9b1 100644 (file)
@@ -37,10 +37,10 @@ public:
     explicit MockLocalConnection(const WebCore::MockWebAuthenticationConfiguration&);
 
 private:
-    void getUserConsent(const String& reason, SecAccessControlRef, UserConsentContextCallback&&) const final;
+    bool isUnlocked(LAContext *) const final;
     RetainPtr<SecKeyRef> createCredentialPrivateKey(LAContext *, SecAccessControlRef, const String& secAttrLabel, NSData *secAttrApplicationTag) const final;
     void getAttestation(SecKeyRef, NSData *authData, NSData *hash, AttestationCallback&&) const final;
-    NSDictionary *selectCredential(const NSArray *) const final;
+    void filterResponses(HashSet<Ref<WebCore::AuthenticatorAssertionResponse>>&) const final;
 
     WebCore::MockWebAuthenticationConfiguration m_configuration;
 };
index 915b594..8b8d824 100644 (file)
@@ -32,6 +32,7 @@
 #import <WebCore/ExceptionData.h>
 #import <wtf/RunLoop.h>
 #import <wtf/spi/cocoa/SecuritySPI.h>
+#import <wtf/text/Base64.h>
 #import <wtf/text/WTFString.h>
 
 #import "LocalAuthenticationSoftLink.h"
@@ -43,17 +44,9 @@ MockLocalConnection::MockLocalConnection(const WebCore::MockWebAuthenticationCon
 {
 }
 
-void MockLocalConnection::getUserConsent(const String&, SecAccessControlRef, UserConsentContextCallback&& callback) const
+bool MockLocalConnection::isUnlocked(LAContext *context) const
 {
-    // Mock async operations.
-    RunLoop::main().dispatch([configuration = m_configuration, callback = WTFMove(callback)]() mutable {
-        ASSERT(configuration.local);
-        if (!configuration.local->acceptAuthentication) {
-            callback(UserConsent::No, nil);
-            return;
-        }
-        callback(UserConsent::Yes, adoptNS([allocLAContextInstance() init]).get());
-    });
+    return m_configuration.local->acceptAuthentication;
 }
 
 RetainPtr<SecKeyRef> MockLocalConnection::createCredentialPrivateKey(LAContext *, SecAccessControlRef, const String& secAttrLabel, NSData *secAttrApplicationTag) const
@@ -112,15 +105,24 @@ void MockLocalConnection::getAttestation(SecKeyRef, NSData *, NSData *, Attestat
     });
 }
 
-NSDictionary *MockLocalConnection::selectCredential(const NSArray *credentials) const
+void MockLocalConnection::filterResponses(HashSet<Ref<WebCore::AuthenticatorAssertionResponse>>& responses) const
 {
-    auto preferredUserhandle = adoptNS([[NSData alloc] initWithBase64EncodedString:m_configuration.local->preferredUserhandleBase64 options:0]);
-    for (NSDictionary *credential : credentials) {
-        if ([credential[(id)kSecAttrApplicationTag] isEqualToData:preferredUserhandle.get()])
-            return credential;
+    const auto& preferredUserhandleBase64 = m_configuration.local->preferredUserhandleBase64;
+    if (preferredUserhandleBase64.isEmpty())
+        return;
+
+    auto itr = responses.begin();
+    for (; itr != responses.end(); ++itr) {
+        auto* userHandle = itr->get().userHandle();
+        ASSERT(userHandle);
+        auto userhandleBase64 = base64Encode(userHandle->data(), userHandle->byteLength());
+        if (userhandleBase64 == preferredUserhandleBase64)
+            break;
     }
-    ASSERT_NOT_REACHED();
-    return nil;
+    auto response = responses.take(itr);
+    ASSERT(response);
+    responses.clear();
+    responses.add(WTFMove(*response));
 }
 
 } // namespace WebKit
index 5488a21..5092d93 100644 (file)
@@ -45,7 +45,10 @@ enum class WebAuthenticationStatus : uint8_t {
     NoCredentialsFound,
     PinBlocked,
     PinAuthBlocked,
-    PinInvalid
+    PinInvalid,
+    LAError,
+    LAExcludeCredentialsMatched,
+    LANoCredential,
 };
 
 } // namespace WebKit
index 2ccfb65..45eccc5 100644 (file)
                570DAAC623037F7F00E8FC04 /* WKNFReaderSessionDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 570DAAC423037F7E00E8FC04 /* WKNFReaderSessionDelegate.h */; };
                570DAACA230385FD00E8FC04 /* CtapNfcDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = 570DAAC8230385FD00E8FC04 /* CtapNfcDriver.h */; };
                572FD44322265CE200A1ECC3 /* WebViewDidMoveToWindowObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = 572FD44122265CE200A1ECC3 /* WebViewDidMoveToWindowObserver.h */; };
+               574217922400E286002B303D /* LocalAuthenticationSPI.h in Headers */ = {isa = PBXBuildFile; fileRef = 574217912400E098002B303D /* LocalAuthenticationSPI.h */; };
                574728D123456E98001700AF /* _WKWebAuthenticationPanel.h in Headers */ = {isa = PBXBuildFile; fileRef = 574728CF23456E98001700AF /* _WKWebAuthenticationPanel.h */; settings = {ATTRIBUTES = (Private, ); }; };
                574728D4234570AE001700AF /* _WKWebAuthenticationPanelInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 574728D3234570AE001700AF /* _WKWebAuthenticationPanelInternal.h */; };
                57597EB921811D9A0037F924 /* CtapHidDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = 57597EB721811D9A0037F924 /* CtapHidDriver.h */; };
                570DAAC8230385FD00E8FC04 /* CtapNfcDriver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CtapNfcDriver.h; sourceTree = "<group>"; };
                570DAAC9230385FD00E8FC04 /* CtapNfcDriver.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = CtapNfcDriver.cpp; sourceTree = "<group>"; };
                572FD44122265CE200A1ECC3 /* WebViewDidMoveToWindowObserver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WebViewDidMoveToWindowObserver.h; sourceTree = "<group>"; };
+               574217912400E098002B303D /* LocalAuthenticationSPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LocalAuthenticationSPI.h; sourceTree = "<group>"; };
                574728CF23456E98001700AF /* _WKWebAuthenticationPanel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = _WKWebAuthenticationPanel.h; sourceTree = "<group>"; };
                574728D023456E98001700AF /* _WKWebAuthenticationPanel.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = _WKWebAuthenticationPanel.mm; sourceTree = "<group>"; };
                574728D3234570AE001700AF /* _WKWebAuthenticationPanelInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = _WKWebAuthenticationPanelInternal.h; sourceTree = "<group>"; };
                                1A5705101BE410E500874AF1 /* BlockSPI.h */,
                                37C21CAD1E994C0C0029D5F9 /* CorePredictionSPI.h */,
                                2DAADA8E2298C21000E36B0C /* DeviceManagementSPI.h */,
+                               574217912400E098002B303D /* LocalAuthenticationSPI.h */,
                                57B826402304EB3E00B72EB0 /* NearFieldSPI.h */,
                                3754D5441B3A29FD003A4C7F /* NSInvocationSPI.h */,
                                37B47E2C1D64DB76005F4EFF /* objcSPI.h */,
                                413075B21DE85F580039EC69 /* LibWebRTCSocketFactory.h in Headers */,
                                2D1087611D2C573E00B85F82 /* LoadParameters.h in Headers */,
                                578DC2982155A0020074E815 /* LocalAuthenticationSoftLink.h in Headers */,
+                               574217922400E286002B303D /* LocalAuthenticationSPI.h in Headers */,
                                57DCEDAC214C60270016B847 /* LocalAuthenticator.h in Headers */,
                                57DCEDAD214C602C0016B847 /* LocalConnection.h in Headers */,
                                57DCEDAE214C60330016B847 /* LocalService.h in Headers */,
index a9d5ae1..ed499a3 100644 (file)
@@ -1,3 +1,26 @@
+2020-02-24  Jiewen Tan  <jiewen_tan@apple.com>
+
+        [WebAuthn] Implement SPI for the platform authenticator
+        https://bugs.webkit.org/show_bug.cgi?id=208087
+        <rdar://problem/59369305>
+
+        Reviewed by Brent Fulgham.
+
+        Besides adding API tests, this patch also teaches TestWebKitAPI to use restricted entitlements.
+
+        * TestWebKitAPI/Configurations/TestWebKitAPI-macOS.entitlements:
+        * TestWebKitAPI/Configurations/TestWebKitAPI.xcconfig:
+        * TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
+        * TestWebKitAPI/Tests/WebKitCocoa/_WKWebAuthenticationPanel.mm:
+        (-[TestWebAuthenticationPanelDelegate panel:updateWebAuthenticationPanel:]):
+        (-[TestWebAuthenticationPanelDelegate panel:selectAssertionResponse:completionHandler:]):
+        (-[TestWebAuthenticationPanelDelegate panel:verifyUserWithAccessControl:completionHandler:]):
+        (TestWebKitAPI::TEST):
+        * TestWebKitAPI/Tests/WebKitCocoa/web-authentication-get-assertion-la.html: Copied from Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-get-assertion.html.
+        * TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-la-duplicate-credential.html: Added.
+        * TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-la-error.html: Added.
+        * TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-la.html: Added.
+
 2020-02-24  Yusuke Suzuki  <ysuzuki@apple.com>
 
         Unreviewed, updating LLDB test for CompactPointerTuple.
index ea0c1f7..a9f9cdf 100644 (file)
@@ -2,6 +2,10 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
+       <key>keychain-access-groups</key>
+       <array>
+               <string>com.apple.TestWebKitAPI</string>
+       </array>
        <key>com.apple.security.temporary-exception.sbpl</key>
        <array>
                <string>(allow mach-issue-extension (require-all (extension-class &quot;com.apple.webkit.extension.mach&quot;)))</string>
index 30a8caf..ca8e171 100644 (file)
@@ -69,3 +69,10 @@ CODE_SIGN_ENTITLEMENTS[sdk=iphone*] = Configurations/TestWebKitAPI-iOS.entitleme
 CODE_SIGN_ENTITLEMENTS[sdk=macosx*] = Configurations/TestWebKitAPI-macOS.entitlements;
 
 STRIP_STYLE = debugging;
+
+CODE_SIGN_IDENTITY[sdk=macosx*] = $(CODE_SIGN_IDENTITY_$(CONFIGURATION));
+CODE_SIGN_IDENTITY_Debug = $(CODE_SIGN_IDENTITY_$(USE_INTERNAL_SDK));
+CODE_SIGN_IDENTITY_Release = $(CODE_SIGN_IDENTITY_$(USE_INTERNAL_SDK));
+CODE_SIGN_IDENTITY_YES = $(WK_ENGINEERING_CODE_SIGN_IDENTITY);
+CODE_SIGN_IDENTITY_Production = $(CODE_SIGN_IDENTITY_Production_$(USE_INTERNAL_SDK));
+CODE_SIGN_IDENTITY_Production_YES = -;
index 033208f..f022d1e 100644 (file)
                573255A522139BC700396AE8 /* helloworld.webarchive in Copy Resources */ = {isa = PBXBuildFile; fileRef = 573255A422139B9000396AE8 /* helloworld.webarchive */; };
                573255A622139BC700396AE8 /* load-web-archive-1.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 573255A222139B8F00396AE8 /* load-web-archive-1.html */; };
                573255A722139BC700396AE8 /* load-web-archive-2.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 573255A322139B9000396AE8 /* load-web-archive-2.html */; };
+               574217882400AC25002B303D /* web-authentication-make-credential-la-error.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 574217872400ABFD002B303D /* web-authentication-make-credential-la-error.html */; };
+               5742178A2400AED8002B303D /* web-authentication-make-credential-la-duplicate-credential.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 574217892400AED0002B303D /* web-authentication-make-credential-la-duplicate-credential.html */; };
+               5742178C2400CD47002B303D /* web-authentication-get-assertion-la.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 5742178B2400CD2D002B303D /* web-authentication-get-assertion-la.html */; };
+               5742178E2400D2DF002B303D /* web-authentication-make-credential-la.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 5742178D2400D26C002B303D /* web-authentication-make-credential-la.html */; };
                574F55D2204D47F0002948C6 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 574F55D0204D471C002948C6 /* Security.framework */; };
                5758597F23A2527A00C74572 /* CtapPinTest.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 5758597E23A2527A00C74572 /* CtapPinTest.cpp */; };
                5758598423C3C3A400C74572 /* web-authentication-make-credential-hid-pin-get-retries-error.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 5758598323C3C36200C74572 /* web-authentication-make-credential-hid-pin-get-retries-error.html */; };
                                578DA44E23ECD28B00246010 /* web-authentication-get-assertion-hid-pin-invalid-error-retry.html in Copy Resources */,
                                570D26FC23C3F87000D5CF67 /* web-authentication-get-assertion-hid-pin.html in Copy Resources */,
                                57663DEC234F1F9300E85E09 /* web-authentication-get-assertion-hid.html in Copy Resources */,
+                               5742178C2400CD47002B303D /* web-authentication-get-assertion-la.html in Copy Resources */,
                                579833922368FA37008E5547 /* web-authentication-get-assertion-nfc-multiple-tags.html in Copy Resources */,
                                57663DEA234EA66D00E85E09 /* web-authentication-get-assertion-nfc.html in Copy Resources */,
                                577454D22359BB01008E1ED7 /* web-authentication-get-assertion-u2f-no-credentials.html in Copy Resources */,
                                578DA44A23ECD18600246010 /* web-authentication-make-credential-hid-pin-invalid-error-retry.html in Copy Resources */,
                                570D26F623C3D33000D5CF67 /* web-authentication-make-credential-hid-pin.html in Copy Resources */,
                                5798337E236019A4008E5547 /* web-authentication-make-credential-hid.html in Copy Resources */,
+                               5742178A2400AED8002B303D /* web-authentication-make-credential-la-duplicate-credential.html in Copy Resources */,
+                               574217882400AC25002B303D /* web-authentication-make-credential-la-error.html in Copy Resources */,
+                               5742178E2400D2DF002B303D /* web-authentication-make-credential-la.html in Copy Resources */,
                                1C2B81861C89259D00A5529F /* webfont.html in Copy Resources */,
                                51714EB41CF8C78C004723C4 /* WebProcessKillIDBCleanup-1.html in Copy Resources */,
                                51714EB51CF8C78C004723C4 /* WebProcessKillIDBCleanup-2.html in Copy Resources */,
                573255A322139B9000396AE8 /* load-web-archive-2.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "load-web-archive-2.html"; sourceTree = "<group>"; };
                573255A422139B9000396AE8 /* helloworld.webarchive */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = helloworld.webarchive; sourceTree = "<group>"; };
                5735F0251F3A4EA6000EE801 /* TestWebKitAPI-iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "TestWebKitAPI-iOS.entitlements"; sourceTree = "<group>"; };
+               574217872400ABFD002B303D /* web-authentication-make-credential-la-error.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "web-authentication-make-credential-la-error.html"; sourceTree = "<group>"; };
+               574217892400AED0002B303D /* web-authentication-make-credential-la-duplicate-credential.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "web-authentication-make-credential-la-duplicate-credential.html"; sourceTree = "<group>"; };
+               5742178B2400CD2D002B303D /* web-authentication-get-assertion-la.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "web-authentication-get-assertion-la.html"; sourceTree = "<group>"; };
+               5742178D2400D26C002B303D /* web-authentication-make-credential-la.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "web-authentication-make-credential-la.html"; sourceTree = "<group>"; };
+               5742178F2400D54D002B303D /* web-authentication-make-credential.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "web-authentication-make-credential.html"; sourceTree = "<group>"; };
                574F55D0204D471C002948C6 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
                5758597D23A2527A00C74572 /* CtapPinTest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CtapPinTest.h; sourceTree = "<group>"; };
                5758597E23A2527A00C74572 /* CtapPinTest.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = CtapPinTest.cpp; sourceTree = "<group>"; };
                                578DA44D23ECD26100246010 /* web-authentication-get-assertion-hid-pin-invalid-error-retry.html */,
                                570D26FB23C3F86500D5CF67 /* web-authentication-get-assertion-hid-pin.html */,
                                57663DEB234F1F8000E85E09 /* web-authentication-get-assertion-hid.html */,
+                               5742178B2400CD2D002B303D /* web-authentication-get-assertion-la.html */,
                                5798337B235EB65C008E5547 /* web-authentication-get-assertion-nfc-multiple-tags.html */,
                                57663DE9234EA60B00E85E09 /* web-authentication-get-assertion-nfc.html */,
                                577454D12359BAD5008E1ED7 /* web-authentication-get-assertion-u2f-no-credentials.html */,
                                578DA44923ECD15500246010 /* web-authentication-make-credential-hid-pin-invalid-error-retry.html */,
                                570D26F523C3D32700D5CF67 /* web-authentication-make-credential-hid-pin.html */,
                                5798337D2360196D008E5547 /* web-authentication-make-credential-hid.html */,
+                               574217892400AED0002B303D /* web-authentication-make-credential-la-duplicate-credential.html */,
+                               574217872400ABFD002B303D /* web-authentication-make-credential-la-error.html */,
+                               5742178D2400D26C002B303D /* web-authentication-make-credential-la.html */,
+                               5742178F2400D54D002B303D /* web-authentication-make-credential.html */,
                                51714EB21CF8C761004723C4 /* WebProcessKillIDBCleanup-1.html */,
                                51714EB31CF8C761004723C4 /* WebProcessKillIDBCleanup-2.html */,
                                5120C83B1E674E350025B250 /* WebsiteDataStoreCustomPaths.html */,
index 0833c49..f2fb452 100644 (file)
@@ -32,6 +32,7 @@
 #import "TCPServer.h"
 #import "TestWKWebView.h"
 #import "WKWebViewConfigurationExtras.h"
+#import <LocalAuthentication/LocalAuthentication.h>
 #import <WebKit/WKPreferencesPrivate.h>
 #import <WebKit/WKUIDelegatePrivate.h>
 #import <WebKit/_WKExperimentalFeature.h>
@@ -39,6 +40,7 @@
 #import <WebKit/_WKWebAuthenticationPanel.h>
 #import <wtf/BlockPtr.h>
 #import <wtf/RandomNumber.h>
+#import <wtf/spi/cocoa/SecuritySPI.h>
 #import <wtf/text/StringConcatenateNumbers.h>
 
 static bool webAuthenticationPanelRan = false;
@@ -49,9 +51,18 @@ static bool webAuthenticationPanelUpdateNoCredentialsFound = false;
 static bool webAuthenticationPanelUpdatePINBlocked = false;
 static bool webAuthenticationPanelUpdatePINAuthBlocked = false;
 static bool webAuthenticationPanelUpdatePINInvalid = false;
+static bool webAuthenticationPanelUpdateLAError = false;
+static bool webAuthenticationPanelUpdateLAExcludeCredentialsMatched = false;
+static bool webAuthenticationPanelUpdateLANoCredential = false;
 static bool webAuthenticationPanelCancelImmediately = false;
+static bool webAuthenticationPanelVerifyUser = false;
 static String webAuthenticationPanelPin;
 static BOOL webAuthenticationPanelNullUserHandle = NO;
+static String testES256PrivateKeyBase64 =
+    "BDj/zxSkzKgaBuS3cdWDF558of8AaIpgFpsjF/Qm1749VBJPgqUIwfhWHJ91nb7U"
+    "PH76c0+WFOzZKslPyyFse4goGIW2R7k9VHLPEZl5nfnBgEVFh5zev+/xpHQIvuq6"
+    "RQ==";
+static String testUserhandleBase64 = "AAECAwQFBgcICQ==";
 
 @interface TestWebAuthenticationPanelDelegate : NSObject <_WKWebAuthenticationPanelDelegate>
 @end
@@ -84,6 +95,18 @@ static BOOL webAuthenticationPanelNullUserHandle = NO;
         webAuthenticationPanelUpdatePINInvalid = true;
         return;
     }
+    if (update == _WKWebAuthenticationPanelUpdateLAError) {
+        webAuthenticationPanelUpdateLAError = true;
+        return;
+    }
+    if (update == _WKWebAuthenticationPanelUpdateLAExcludeCredentialsMatched) {
+        webAuthenticationPanelUpdateLAExcludeCredentialsMatched = true;
+        return;
+    }
+    if (update == _WKWebAuthenticationPanelUpdateLANoCredential) {
+        webAuthenticationPanelUpdateLANoCredential = true;
+        return;
+    }
 }
 
 - (void)panel:(_WKWebAuthenticationPanel *)panel dismissWebAuthenticationPanelWithResult:(_WKWebAuthenticationResult)result
@@ -111,6 +134,11 @@ static BOOL webAuthenticationPanelNullUserHandle = NO;
 
 - (void)panel:(_WKWebAuthenticationPanel *)panel selectAssertionResponse:(NSArray < _WKWebAuthenticationAssertionResponse *> *)responses completionHandler:(void (^)(_WKWebAuthenticationAssertionResponse *))completionHandler
 {
+    if (responses.count == 1) {
+        completionHandler(responses[0]);
+        return;
+    }
+
     EXPECT_EQ(responses.count, 2ul);
     for (_WKWebAuthenticationAssertionResponse *response in responses) {
         EXPECT_TRUE([response.name isEqual:@"johnpsmith@example.com"] || [response.name isEqual:@""]);
@@ -123,6 +151,13 @@ static BOOL webAuthenticationPanelNullUserHandle = NO;
     completionHandler(responses[index]);
 }
 
+- (void)panel:(_WKWebAuthenticationPanel *)panel verifyUserWithAccessControl:(SecAccessControlRef)accessControl completionHandler:(void (^)(LAContext *))completionHandler
+{
+    webAuthenticationPanelVerifyUser = true;
+    auto context = adoptNS([[LAContext alloc] init]);
+    completionHandler(context.get());
+}
+
 @end
 
 @interface TestWebAuthenticationPanelFakeDelegate : NSObject <_WKWebAuthenticationPanelDelegate>
@@ -231,6 +266,24 @@ static _WKExperimentalFeature *webAuthenticationExperimentalFeature()
     return theFeature.get();
 }
 
+#if USE(APPLE_INTERNAL_SDK) || PLATFORM(IOS)
+static _WKExperimentalFeature *webAuthenticationLocalAuthenticatorExperimentalFeature()
+{
+    static RetainPtr<_WKExperimentalFeature> theFeature;
+    if (theFeature)
+        return theFeature.get();
+
+    NSArray *features = [WKPreferences _experimentalFeatures];
+    for (_WKExperimentalFeature *feature in features) {
+        if ([feature.key isEqual:@"WebAuthenticationLocalAuthenticatorEnabled"]) {
+            theFeature = feature;
+            break;
+        }
+    }
+    return theFeature.get();
+}
+#endif // USE(APPLE_INTERNAL_SDK) || PLATFORM(IOS)
+
 static void reset()
 {
     webAuthenticationPanelRan = false;
@@ -241,9 +294,13 @@ static void reset()
     webAuthenticationPanelUpdatePINBlocked = false;
     webAuthenticationPanelUpdatePINAuthBlocked = false;
     webAuthenticationPanelUpdatePINInvalid = false;
+    webAuthenticationPanelUpdateLAError = false;
+    webAuthenticationPanelUpdateLAExcludeCredentialsMatched = false;
+    webAuthenticationPanelUpdateLANoCredential = false;
     webAuthenticationPanelCancelImmediately = false;
     webAuthenticationPanelPin = emptyString();
     webAuthenticationPanelNullUserHandle = NO;
+    webAuthenticationPanelVerifyUser = false;
 }
 
 static void checkPanel(_WKWebAuthenticationPanel *panel, NSString *relyingPartyID, NSArray *transports, _WKWebAuthenticationType type)
@@ -277,6 +334,60 @@ static void checkFrameInfo(WKFrameInfo *frame, bool isMainFrame, NSString *url,
     EXPECT_EQ(frame.webView, webView);
 }
 
+#if USE(APPLE_INTERNAL_SDK) || PLATFORM(IOS)
+
+bool addKeyToKeychain(const String& privateKeyBase64, const String& rpId, const String& userHandleBase64)
+{
+    NSDictionary* options = @{
+        (id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
+        (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate,
+        (id)kSecAttrKeySizeInBits: @256,
+    };
+    CFErrorRef errorRef = nullptr;
+    auto key = adoptCF(SecKeyCreateWithData(
+        (__bridge CFDataRef)adoptNS([[NSData alloc] initWithBase64EncodedString:privateKeyBase64 options:NSDataBase64DecodingIgnoreUnknownCharacters]).get(),
+        (__bridge CFDictionaryRef)options,
+        &errorRef
+    ));
+    if (errorRef)
+        return false;
+
+    NSDictionary* addQuery = @{
+        (id)kSecValueRef: (id)key.get(),
+        (id)kSecClass: (id)kSecClassKey,
+        (id)kSecAttrLabel: rpId,
+        (id)kSecAttrApplicationTag: adoptNS([[NSData alloc] initWithBase64EncodedString:userHandleBase64 options:NSDataBase64DecodingIgnoreUnknownCharacters]).get(),
+        (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
+#if HAVE(DATA_PROTECTION_KEYCHAIN)
+        (id)kSecUseDataProtectionKeychain: @YES
+#else
+        (id)kSecAttrNoLegacy: @YES
+#endif
+    };
+    OSStatus status = SecItemAdd((__bridge CFDictionaryRef)addQuery, NULL);
+    if (status)
+        return false;
+
+    return true;
+}
+
+void cleanUpKeychain(const String& rpId)
+{
+    NSDictionary* deleteQuery = @{
+        (id)kSecClass: (id)kSecClassKey,
+        (id)kSecAttrLabel: rpId,
+        (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
+#if HAVE(DATA_PROTECTION_KEYCHAIN)
+        (id)kSecUseDataProtectionKeychain: @YES
+#else
+        (id)kSecAttrNoLegacy: @YES
+#endif
+    };
+    SecItemDelete((__bridge CFDictionaryRef)deleteQuery);
+}
+
+#endif // USE(APPLE_INTERNAL_SDK) || PLATFORM(IOS)
+
 } // namesapce;
 
 TEST(WebAuthenticationPanel, NoPanelTimeout)
@@ -1076,6 +1187,124 @@ TEST(WebAuthenticationPanel, MultipleAccounts)
     EXPECT_EQ([[webView stringByEvaluatingJavaScript:@"userHandle"] isEqualToString:@"<null>"], webAuthenticationPanelNullUserHandle);
 }
 
+// For macOS, only internal builds can sign keychain entitlemnets
+// which are required to run local authenticator tests.
+#if USE(APPLE_INTERNAL_SDK) || PLATFORM(IOS)
+
+TEST(WebAuthenticationPanel, LAError)
+{
+    reset();
+    RetainPtr<NSURL> testURL = [[NSBundle mainBundle] URLForResource:@"web-authentication-make-credential-la-error" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
+
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationExperimentalFeature()];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationLocalAuthenticatorExperimentalFeature()];
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect configuration:configuration]);
+    auto delegate = adoptNS([[TestWebAuthenticationPanelUIDelegate alloc] init]);
+    [webView setUIDelegate:delegate.get()];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+    Util::run(&webAuthenticationPanelUpdateLAError);
+}
+
+TEST(WebAuthenticationPanel, LADuplicateCredential)
+{
+    reset();
+    RetainPtr<NSURL> testURL = [[NSBundle mainBundle] URLForResource:@"web-authentication-make-credential-la-duplicate-credential" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
+
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationExperimentalFeature()];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationLocalAuthenticatorExperimentalFeature()];
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect configuration:configuration]);
+    auto delegate = adoptNS([[TestWebAuthenticationPanelUIDelegate alloc] init]);
+    [webView setUIDelegate:delegate.get()];
+
+    ASSERT_TRUE(addKeyToKeychain(testES256PrivateKeyBase64, "", testUserhandleBase64));
+    [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+    Util::run(&webAuthenticationPanelUpdateLAExcludeCredentialsMatched);
+    cleanUpKeychain("");
+}
+
+TEST(WebAuthenticationPanel, LANoCredential)
+{
+    reset();
+    RetainPtr<NSURL> testURL = [[NSBundle mainBundle] URLForResource:@"web-authentication-get-assertion-la" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
+
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationExperimentalFeature()];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationLocalAuthenticatorExperimentalFeature()];
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect configuration:configuration]);
+    auto delegate = adoptNS([[TestWebAuthenticationPanelUIDelegate alloc] init]);
+    [webView setUIDelegate:delegate.get()];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+    Util::run(&webAuthenticationPanelUpdateLANoCredential);
+}
+
+TEST(WebAuthenticationPanel, LAMakeCredentialNullDelegate)
+{
+    reset();
+    RetainPtr<NSURL> testURL = [[NSBundle mainBundle] URLForResource:@"web-authentication-make-credential-la" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
+
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationExperimentalFeature()];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationLocalAuthenticatorExperimentalFeature()];
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect configuration:configuration]);
+    auto delegate = adoptNS([[TestWebAuthenticationPanelUIDelegate alloc] init]);
+    [delegate setIsNull:true];
+    [webView setUIDelegate:delegate.get()];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+    [webView waitForMessage:@"Succeeded!"];
+    cleanUpKeychain("");
+}
+
+TEST(WebAuthenticationPanel, LAMakeCredential)
+{
+    reset();
+    RetainPtr<NSURL> testURL = [[NSBundle mainBundle] URLForResource:@"web-authentication-make-credential-la" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
+
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationExperimentalFeature()];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationLocalAuthenticatorExperimentalFeature()];
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect configuration:configuration]);
+    auto delegate = adoptNS([[TestWebAuthenticationPanelUIDelegate alloc] init]);
+    [webView setUIDelegate:delegate.get()];
+
+    [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+    Util::run(&webAuthenticationPanelVerifyUser);
+    checkPanel([delegate panel], @"", @[adoptNS([[NSNumber alloc] initWithInt:_WKWebAuthenticationTransportUSB]).get(), adoptNS([[NSNumber alloc] initWithInt:_WKWebAuthenticationTransportInternal]).get()], _WKWebAuthenticationTypeCreate);
+    [webView waitForMessage:@"Succeeded!"];
+}
+
+TEST(WebAuthenticationPanel, LAGetAssertion)
+{
+    reset();
+    RetainPtr<NSURL> testURL = [[NSBundle mainBundle] URLForResource:@"web-authentication-get-assertion-la" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
+
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationExperimentalFeature()];
+    [[configuration preferences] _setEnabled:YES forExperimentalFeature:webAuthenticationLocalAuthenticatorExperimentalFeature()];
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect configuration:configuration]);
+    auto delegate = adoptNS([[TestWebAuthenticationPanelUIDelegate alloc] init]);
+    [webView setUIDelegate:delegate.get()];
+
+    ASSERT_TRUE(addKeyToKeychain(testES256PrivateKeyBase64, "", testUserhandleBase64));
+    [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+    Util::run(&webAuthenticationPanelVerifyUser);
+    checkPanel([delegate panel], @"", @[adoptNS([[NSNumber alloc] initWithInt:_WKWebAuthenticationTransportUSB]).get(), adoptNS([[NSNumber alloc] initWithInt:_WKWebAuthenticationTransportInternal]).get()], _WKWebAuthenticationTypeGet);
+    [webView waitForMessage:@"Succeeded!"];
+    cleanUpKeychain("");
+}
+
+#endif // USE(APPLE_INTERNAL_SDK) || PLATFORM(IOS)
+
 } // namespace TestWebKitAPI
 
 #endif // ENABLE(WEB_AUTHN)
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-get-assertion-la.html b/Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-get-assertion-la.html
new file mode 100644 (file)
index 0000000..efa3c5b
--- /dev/null
@@ -0,0 +1,22 @@
+<input type="text" id="input">
+<script>
+    if (window.internals) {
+        internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: true, acceptAttestation: false } });
+        internals.withUserGesture(() => { input.focus(); });
+    }
+
+    const options = {
+        publicKey: {
+            challenge: new Uint8Array(16),
+            timeout: 100
+        }
+    };
+
+    navigator.credentials.get(options).then(credential => {
+        // console.log("Succeeded!");
+        window.webkit.messageHandlers.testHandler.postMessage("Succeeded!");
+    }, error => {
+        // console.log(error.message);
+        window.webkit.messageHandlers.testHandler.postMessage(error.message);
+    });
+</script>
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-la-duplicate-credential.html b/Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-la-duplicate-credential.html
new file mode 100644 (file)
index 0000000..aaa5965
--- /dev/null
@@ -0,0 +1,43 @@
+<input type="text" id="input">
+<script>
+    const testCredentialIdHex = "48C4971E7805EE110EB04940EF70B7458FBC6D1E";
+    function hexStringToUint8Array(hexString)
+    {
+        if (hexString.length % 2 != 0)
+            throw "Invalid hexString";
+        var arrayBuffer = new Uint8Array(hexString.length / 2);
+
+        for (var i = 0; i < hexString.length; i += 2) {
+            var byteValue = parseInt(hexString.substr(i, 2), 16);
+            if (byteValue == NaN)
+                throw "Invalid hexString";
+            arrayBuffer[i/2] = byteValue;
+        }
+
+        return arrayBuffer;
+    }
+
+    if (window.internals) {
+        internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: false, acceptAttestation: false } });
+        internals.withUserGesture(() => { input.focus(); });
+    }
+
+    const options = {
+        publicKey: {
+            rp: {
+                name: "localhost",
+            },
+            user: {
+                name: "John Appleseed",
+                id: new Uint8Array(16),
+                displayName: "Appleseed",
+            },
+            challenge: new Uint8Array(16),
+            pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+            excludeCredentials: [{ type: "public-key", id: hexStringToUint8Array(testCredentialIdHex) }],
+            timeout: 100,
+        }
+    };
+
+    navigator.credentials.create(options);
+</script>
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-la-error.html b/Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-la-error.html
new file mode 100644 (file)
index 0000000..5f16121
--- /dev/null
@@ -0,0 +1,25 @@
+<input type="text" id="input">
+<script>
+    if (window.internals) {
+        internals.setMockWebAuthenticationConfiguration({ local: { acceptAuthentication: false, acceptAttestation: false } });
+        internals.withUserGesture(() => { input.focus(); });
+    }
+
+    const options = {
+        publicKey: {
+            rp: {
+                name: "localhost",
+            },
+            user: {
+                name: "John Appleseed",
+                id: new Uint8Array(16),
+                displayName: "Appleseed",
+            },
+            challenge: new Uint8Array(16),
+            pubKeyCredParams: [{ type: "public-key", alg: -35 }, { type: "public-key", alg: -257 }], // ES384, RS256
+            timeout: 100,
+        }
+    };
+
+    navigator.credentials.create(options);
+</script>
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-la.html b/Tools/TestWebKitAPI/Tests/WebKitCocoa/web-authentication-make-credential-la.html
new file mode 100644 (file)
index 0000000..927e25c
--- /dev/null
@@ -0,0 +1,66 @@
+<input type="text" id="input">
+<script>
+    const testES256PrivateKeyBase64 =
+        "BDj/zxSkzKgaBuS3cdWDF558of8AaIpgFpsjF/Qm1749VBJPgqUIwfhWHJ91nb7U" +
+        "PH76c0+WFOzZKslPyyFse4goGIW2R7k9VHLPEZl5nfnBgEVFh5zev+/xpHQIvuq6" +
+        "RQ==";
+    const testAttestationCertificateBase64 =
+        "MIIB6jCCAZCgAwIBAgIGAWHAxcjvMAoGCCqGSM49BAMCMFMxJzAlBgNVBAMMHkJh" +
+        "c2ljIEF0dGVzdGF0aW9uIFVzZXIgU3ViIENBMTETMBEGA1UECgwKQXBwbGUgSW5j" +
+        "LjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0xODAyMjMwMzM3MjJaFw0xODAyMjQw" +
+        "MzQ3MjJaMGoxIjAgBgNVBAMMGTAwMDA4MDEwLTAwMEE0OUEyMzBBMDIxM0ExGjAY" +
+        "BgNVBAsMEUJBQSBDZXJ0aWZpY2F0aW9uMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMw" +
+        "EQYDVQQIDApDYWxpZm9ybmlhMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvCje" +
+        "Pzr6Sg76XMoHuGabPaG6zjpLFL8Zd8/74Hh5PcL2Zq+o+f7ENXX+7nEXXYt0S8Ux" +
+        "5TIRw4hgbfxXQbWLEqM5MDcwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBPAw" +
+        "FwYJKoZIhvdjZAgCBAowCKEGBAR0ZXN0MAoGCCqGSM49BAMCA0gAMEUCIAlK8A8I" +
+        "k43TbvKuYGHZs1DTgpTwmKTBvIUw5bwgZuYnAiEAtuJjDLKbGNJAJFMi5deEBqno" +
+        "pBTCqbfbDJccfyQpjnY=";
+    const testAttestationIssuingCACertificateBase64 =
+        "MIICIzCCAaigAwIBAgIIeNjhG9tnDGgwCgYIKoZIzj0EAwIwUzEnMCUGA1UEAwwe" +
+        "QmFzaWMgQXR0ZXN0YXRpb24gVXNlciBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ" +
+        "bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTE3MDQyMDAwNDIwMFoXDTMyMDMy" +
+        "MjAwMDAwMFowUzEnMCUGA1UEAwweQmFzaWMgQXR0ZXN0YXRpb24gVXNlciBTdWIg" +
+        "Q0ExMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMFkw" +
+        "EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoSZ/1t9eBAEVp5a8PrXacmbGb8zNC1X3" +
+        "StLI9YO6Y0CL7blHmSGmjGWTwD4Q+i0J2BY3+bPHTGRyA9jGB3MSbaNmMGQwEgYD" +
+        "VR0TAQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBSD5aMhnrB0w/lhkP2XTiMQdqSj" +
+        "8jAdBgNVHQ4EFgQU5mWf1DYLTXUdQ9xmOH/uqeNSD80wDgYDVR0PAQH/BAQDAgEG" +
+        "MAoGCCqGSM49BAMCA2kAMGYCMQC3M360LLtJS60Z9q3vVjJxMgMcFQ1roGTUcKqv" +
+        "W+4hJ4CeJjySXTgq6IEHn/yWab4CMQCm5NnK6SOSK+AqWum9lL87W3E6AA1f2TvJ" +
+        "/hgok/34jr93nhS87tOQNdxDS8zyiqw=";
+    if (window.internals) {
+        internals.setMockWebAuthenticationConfiguration({
+            local: {
+                acceptAuthentication: true,
+                acceptAttestation: true,
+                privateKeyBase64: testES256PrivateKeyBase64,
+                userCertificateBase64: testAttestationCertificateBase64,
+                intermediateCACertificateBase64: testAttestationIssuingCACertificateBase64 }});
+        internals.withUserGesture(() => { input.focus(); });
+    }
+
+    const options = {
+        publicKey: {
+            rp: {
+                name: "localhost",
+            },
+            user: {
+                name: "John Appleseed",
+                id: new Uint8Array(16),
+                displayName: "Appleseed",
+            },
+            challenge: new Uint8Array(16),
+            pubKeyCredParams: [{ type: "public-key", alg: -7 }],
+            timeout: 100,
+        }
+    };
+
+    navigator.credentials.create(options).then(credential => {
+        // console.log("Succeeded!");
+        window.webkit.messageHandlers.testHandler.postMessage("Succeeded!");
+    }, error => {
+        // console.log(error.message);
+        window.webkit.messageHandlers.testHandler.postMessage(error.message);
+    });
+</script>