Apply patch. rdar://problem/62083319

Canonical link: https://commits.webkit.org/218903.521@safari-609.2.7-branch
git-svn-id: https://svn.webkit.org/repository/webkit/branches/safari-609.2.7-branch@260542 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Source/WebKit/ChangeLog b/Source/WebKit/ChangeLog
index e0931bf..709bdbf 100644
--- a/Source/WebKit/ChangeLog
+++ b/Source/WebKit/ChangeLog
@@ -1,3 +1,34 @@
+2020-04-22  Russell Epstein  <repstein@apple.com>
+
+        Apply patch. rdar://problem/62083319
+
+    2020-04-22  Alex Christensen  <achristensen@webkit.org>
+
+            NetworkSessionCocoa should request client certificate only once per host/port
+            https://bugs.webkit.org/show_bug.cgi?id=210626
+            <rdar://problem/60340449>
+
+            Reviewed by Geoffrey Garen.
+
+            NSURLSession creates more than one TCP connection to a server when using HTTP 1.1.
+            Each TCP connection with TLS generates a didReceiveChallenge to do the server trust evaluation of the certificate chain.
+            If the server requests a client certificate in the TLS handshake, it also generates a didReceiveChallenge to request client
+            certificates as well.  This is an implementation detail of our networking.  We should not actually ask the WKNavigationDelegate
+            for client certificates more than once per host/port.  We should remember the credential and give it to NSURLSession immediately
+            if we have used this credential in the past for a task that has received bytes (either a response or a redirect).  If the TLS
+            handshake fails, we should not reuse that same certificate automatically.
+
+            * NetworkProcess/cocoa/NetworkSessionCocoa.h:
+            * NetworkProcess/cocoa/NetworkSessionCocoa.mm:
+            (-[WKNetworkSessionDelegate URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:]):
+            (-[WKNetworkSessionDelegate URLSession:task:didReceiveChallenge:completionHandler:]):
+            (-[WKNetworkSessionDelegate URLSession:task:didCompleteWithError:]):
+            (-[WKNetworkSessionDelegate URLSession:dataTask:didReceiveResponse:completionHandler:]):
+            (WebKit::NetworkSessionCocoa::clientCertificateSuggestedForHost):
+            (WebKit::NetworkSessionCocoa::taskReceivedBytes):
+            (WebKit::NetworkSessionCocoa::taskFailed):
+            (WebKit::NetworkSessionCocoa::successfulClientCertificateForHost const):
+
 2020-04-16  Alan Coon  <alancoon@apple.com>
 
         Cherry-pick r259814. rdar://problem/61888315
diff --git a/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.h b/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.h
index 4ad2c10..05fc986 100644
--- a/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.h
+++ b/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.h
@@ -48,6 +48,7 @@
 enum class NegotiatedLegacyTLS : bool;
 class LegacyCustomProtocolManager;
 class NetworkSessionCocoa;
+using HostAndPort = std::pair<String, uint16_t>;
 
 struct SessionWrapper : public CanMakeWeakPtr<SessionWrapper> {
     void initialize(NSURLSessionConfiguration *, NetworkSessionCocoa&, WebCore::StoredCredentialsPolicy);
@@ -95,6 +96,11 @@
 
     SessionWrapper& sessionWrapperForTask(const WebCore::ResourceRequest&, WebCore::StoredCredentialsPolicy);
 
+    void clientCertificateSuggestedForHost(NetworkDataTaskCocoa::TaskIdentifier, NSURLCredential *, const String& host, uint16_t port);
+    void taskReceivedBytes(NetworkDataTaskCocoa::TaskIdentifier);
+    void taskFailed(NetworkDataTaskCocoa::TaskIdentifier);
+    NSURLCredential *successfulClientCertificateForHost(const String& host, uint16_t port) const;
+
 private:
     void invalidateAndCancel() override;
     void clearCredentials() override;
@@ -133,6 +139,14 @@
     Seconds m_loadThrottleLatency;
     bool m_fastServerTrustEvaluationEnabled { false };
     String m_dataConnectionServiceType;
+    
+    struct SuggestedClientCertificate {
+        String host;
+        uint16_t port { 0 };
+        RetainPtr<NSURLCredential> credential;
+    };
+    HashMap<NetworkDataTaskCocoa::TaskIdentifier, SuggestedClientCertificate> m_suggestedClientCertificates;
+    HashMap<HostAndPort, RetainPtr<NSURLCredential>> m_successfulClientCertificates;
 };
 
 } // namespace WebKit
diff --git a/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm b/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm
index 09e388e..84dfad8 100644
--- a/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm
+++ b/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm
@@ -527,6 +527,9 @@
     if (auto* networkDataTask = [self existingTask:task]) {
         auto completionHandlerCopy = Block_copy(completionHandler);
 
+        if (auto* sessionCocoa = static_cast<NetworkSessionCocoa*>(networkDataTask->networkSession()))
+            sessionCocoa->taskReceivedBytes(taskIdentifier);
+
         bool shouldIgnoreHSTS = false;
 #if ENABLE(RESOURCE_LOAD_STATISTICS)
         if (auto* sessionCocoa = networkDataTask->networkSession()) {
@@ -677,6 +680,20 @@
 #endif
         }
     }
+
+    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate]) {
+        HostAndPort key { challenge.protectionSpace.host, challenge.protectionSpace.port };
+        if (auto* credential = sessionCocoa->successfulClientCertificateForHost(challenge.protectionSpace.host, challenge.protectionSpace.port))
+            return completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
+        sessionCocoa->continueDidReceiveChallenge(*_sessionWrapper, challenge, negotiatedLegacyTLS, taskIdentifier, [self existingTask:task], [completionHandler = makeBlockPtr(completionHandler), key, weakSessionCocoa = makeWeakPtr(sessionCocoa), taskIdentifier] (WebKit::AuthenticationChallengeDisposition disposition, const WebCore::Credential& credential) mutable {
+            NSURLCredential *nsCredential = credential.nsCredential();
+            if (disposition == WebKit::AuthenticationChallengeDisposition::UseCredential && nsCredential && weakSessionCocoa)
+                weakSessionCocoa->clientCertificateSuggestedForHost(taskIdentifier, nsCredential, key.first, key.second);
+            completionHandler(toNSURLSessionAuthChallengeDisposition(disposition), nsCredential);
+        });
+        return;
+    }
+
     sessionCocoa->continueDidReceiveChallenge(*_sessionWrapper, challenge, negotiatedLegacyTLS, taskIdentifier, [self existingTask:task], [completionHandler = makeBlockPtr(completionHandler)] (WebKit::AuthenticationChallengeDisposition disposition, const WebCore::Credential& credential) mutable {
         completionHandler(toNSURLSessionAuthChallengeDisposition(disposition), credential.nsCredential());
     });
@@ -693,9 +710,11 @@
         error = [NSError errorWithDomain:[error domain] code:[error code] userInfo:newUserInfo];
     }
 
-    if (auto* networkDataTask = [self existingTask:task])
+    if (auto* networkDataTask = [self existingTask:task]) {
+        if (auto* sessionCocoa = static_cast<NetworkSessionCocoa*>(networkDataTask->networkSession()))
+            sessionCocoa->taskFailed(task.taskIdentifier);
         networkDataTask->didCompleteWithError(error, networkDataTask->networkLoadMetrics());
-    else if (error) {
+    } else if (error) {
         if (!_sessionWrapper)
             return;
         auto downloadID = _sessionWrapper->downloadMap.take(task.taskIdentifier);
@@ -819,6 +838,9 @@
     if (auto* networkDataTask = [self existingTask:dataTask]) {
         ASSERT(RunLoop::isMain());
 
+        if (auto* sessionCocoa = static_cast<NetworkSessionCocoa*>(networkDataTask->networkSession()))
+            sessionCocoa->taskReceivedBytes(taskIdentifier);
+
         NegotiatedLegacyTLS negotiatedLegacyTLS = NegotiatedLegacyTLS::No;
 #if HAVE(TLS_PROTOCOL_VERSION_T)
         NSURLSessionTaskTransactionMetrics *metrics = dataTask._incompleteTaskMetrics.transactionMetrics.lastObject;
@@ -982,6 +1004,38 @@
     return [NSURLSessionConfiguration defaultSessionConfiguration];
 }
 
+void NetworkSessionCocoa::clientCertificateSuggestedForHost(NetworkDataTaskCocoa::TaskIdentifier taskID, NSURLCredential *credential, const String& host, uint16_t port)
+{
+    m_suggestedClientCertificates.set(taskID, SuggestedClientCertificate { host, port, credential });
+}
+
+void NetworkSessionCocoa::taskReceivedBytes(NetworkDataTaskCocoa::TaskIdentifier identifier)
+{
+    if (LIKELY(m_suggestedClientCertificates.isEmpty()))
+        return;
+
+    auto suggestedClientCertificate = m_suggestedClientCertificates.take(identifier);
+    HostAndPort key { suggestedClientCertificate.host, suggestedClientCertificate.port };
+    if (suggestedClientCertificate.credential && decltype(m_successfulClientCertificates)::isValidKey(key))
+        m_successfulClientCertificates.add(key, suggestedClientCertificate.credential);
+}
+
+void NetworkSessionCocoa::taskFailed(NetworkDataTaskCocoa::TaskIdentifier identifier)
+{
+    if (LIKELY(m_suggestedClientCertificates.isEmpty()))
+        return;
+
+    m_suggestedClientCertificates.take(identifier);
+}
+
+NSURLCredential *NetworkSessionCocoa::successfulClientCertificateForHost(const String& host, uint16_t port) const
+{
+    HostAndPort key { host, port };
+    if (!decltype(m_successfulClientCertificates)::isValidKey(key))
+        return nil;
+    return m_successfulClientCertificates.get(key).get();
+}
+
 const String& NetworkSessionCocoa::boundInterfaceIdentifier() const
 {
     return m_boundInterfaceIdentifier;
diff --git a/Tools/ChangeLog b/Tools/ChangeLog
index 30f3772..673c5c3 100644
--- a/Tools/ChangeLog
+++ b/Tools/ChangeLog
@@ -1,3 +1,23 @@
+2020-04-22  Russell Epstein  <repstein@apple.com>
+
+        Apply patch. rdar://problem/62083319
+
+    2020-04-22  Alex Christensen  <achristensen@webkit.org>
+
+            NetworkSessionCocoa should request client certificate only once per host/port
+            https://bugs.webkit.org/show_bug.cgi?id=210626
+            <rdar://problem/60340449>
+
+            Reviewed by Geoffrey Garen.
+
+            * TestWebKitAPI/Tests/WebKitCocoa/Challenge.mm:
+            (TestWebKitAPI::clientCertServerWithCertVerifier):
+            (TestWebKitAPI::TEST):
+            * TestWebKitAPI/cocoa/HTTPServer.h:
+            (TestWebKitAPI::HTTPServer::HTTPResponse::HTTPResponse):
+            * TestWebKitAPI/cocoa/HTTPServer.mm:
+            (TestWebKitAPI::HTTPServer::HTTPServer):
+
 2020-04-10  Ryan Haddad  <ryanhaddad@apple.com>
 
         Cherry-pick r259099. rdar://problem/59610140
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/Challenge.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/Challenge.mm
index 9acd174..d9a8b92 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/Challenge.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/Challenge.mm
@@ -29,6 +29,7 @@
 #import "PlatformUtilities.h"
 #import "TCPServer.h"
 #import "Test.h"
+#import "TestNavigationDelegate.h"
 #import "TestWKWebView.h"
 #import "WKWebViewConfigurationExtras.h"
 #import <WebKit/WKNavigationDelegate.h>
@@ -38,6 +39,7 @@
 #import <WebKit/WebKit.h>
 #import <WebKit/_WKErrorRecoveryAttempting.h>
 #import <WebKit/_WKWebsiteDataStoreConfiguration.h>
+#import <wtf/BlockPtr.h>
 #import <wtf/Platform.h>
 #import <wtf/RetainPtr.h>
 #import <wtf/spi/cocoa/SecuritySPI.h>
@@ -399,6 +401,102 @@
 #endif
 }
 
+// FIXME: Find out why these tests time out on Mojave.
+#if HAVE(NETWORK_FRAMEWORK) && (!PLATFORM(MAC) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
+
+static HTTPServer clientCertServer()
+{
+    Vector<LChar> chars(50000, 'a');
+    String longString(chars.data(), chars.size());
+    return HTTPServer({
+        { "/", { "<html><img src='1.png'/><img src='2.png'/><img src='3.png'/><img src='4.png'/><img src='5.png'/><img src='6.png'/></html>" } },
+        { "/1.png", { longString } },
+        { "/2.png", { longString } },
+        { "/3.png", { longString } },
+        { "/4.png", { longString } },
+        { "/5.png", { longString } },
+        { "/6.png", { longString } },
+        { "/redirectToError", { 301, {{ "Location", "/error" }} } },
+        { "/error", { HTTPServer::HTTPResponse::TerminateConnection::Yes } },
+    }, HTTPServer::Protocol::Https, [] (auto, auto, auto certificateAllowed) {
+        certificateAllowed(true);
+    });
+}
+
+static BlockPtr<void(WKWebView *, NSURLAuthenticationChallenge *, void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))> challengeHandler(Vector<RetainPtr<NSString>>& vector)
+{
+    return makeBlockPtr([&] (WKWebView *webView, NSURLAuthenticationChallenge *challenge, void (^completionHandler)(NSURLSessionAuthChallengeDisposition, NSURLCredential *)) {
+        NSString *method = challenge.protectionSpace.authenticationMethod;
+        vector.append(method);
+        if ([method isEqualToString:NSURLAuthenticationMethodClientCertificate])
+            return completionHandler(NSURLSessionAuthChallengeUseCredential, credentialWithIdentity().get());
+        if ([method isEqualToString:NSURLAuthenticationMethodServerTrust])
+            return completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
+        ASSERT_NOT_REACHED();
+    }).get();
+}
+
+static size_t countClientCertChallenges(Vector<RetainPtr<NSString>>& vector)
+{
+    vector.removeAllMatching([](auto& method) {
+        return ![method isEqualToString:NSURLAuthenticationMethodClientCertificate];
+    });
+    return vector.size();
+};
+
+TEST(MultipleClientCertificateConnections, Basic)
+{
+    auto server = clientCertServer();
+
+    Vector<RetainPtr<NSString>> methods;
+    auto delegate = adoptNS([TestNavigationDelegate new]);
+    delegate.get().didReceiveAuthenticationChallenge = challengeHandler(methods).get();
+
+    auto webView = adoptNS([WKWebView new]);
+    [webView setNavigationDelegate:delegate.get()];
+    [webView loadRequest:server.request()];
+    [delegate waitForDidFinishNavigation];
+    EXPECT_EQ(countClientCertChallenges(methods), 1u);
+}
+
+TEST(MultipleClientCertificateConnections, Redirect)
+{
+    auto server = clientCertServer();
+
+    Vector<RetainPtr<NSString>> methods;
+    auto delegate = adoptNS([TestNavigationDelegate new]);
+    delegate.get().didReceiveAuthenticationChallenge = challengeHandler(methods).get();
+
+    auto webView = adoptNS([WKWebView new]);
+    [webView setNavigationDelegate:delegate.get()];
+    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"https://127.0.0.1:%d/redirectToError", server.port()]]]];
+    [delegate waitForDidFailProvisionalNavigation];
+    EXPECT_EQ(countClientCertChallenges(methods), 1u);
+    [webView loadRequest:server.request()];
+    [delegate waitForDidFinishNavigation];
+    EXPECT_EQ(countClientCertChallenges(methods), 1u);
+}
+
+TEST(MultipleClientCertificateConnections, Failure)
+{
+    auto server = clientCertServer();
+
+    Vector<RetainPtr<NSString>> methods;
+    auto delegate = adoptNS([TestNavigationDelegate new]);
+    delegate.get().didReceiveAuthenticationChallenge = challengeHandler(methods).get();
+
+    auto webView = adoptNS([WKWebView new]);
+    [webView setNavigationDelegate:delegate.get()];
+    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"https://127.0.0.1:%d/error", server.port()]]]];
+    [delegate waitForDidFailProvisionalNavigation];
+    size_t certChallengesAfterInitialFailure = countClientCertChallenges(methods);
+    [webView loadRequest:server.request()];
+    [delegate waitForDidFinishNavigation];
+    EXPECT_EQ(countClientCertChallenges(methods), certChallengesAfterInitialFailure + 1);
+}
+
+#endif // HAVE(NETWORK_FRAMEWORK)
+
 } // namespace TestWebKitAPI
 
 #endif // HAVE(SSL)
diff --git a/Tools/TestWebKitAPI/cocoa/HTTPServer.h b/Tools/TestWebKitAPI/cocoa/HTTPServer.h
index 0070485..60ea630 100644
--- a/Tools/TestWebKitAPI/cocoa/HTTPServer.h
+++ b/Tools/TestWebKitAPI/cocoa/HTTPServer.h
@@ -40,7 +40,7 @@
 public:
     struct HTTPResponse;
     enum class Protocol : uint8_t { Http, Https, HttpsWithLegacyTLS };
-    HTTPServer(std::initializer_list<std::pair<String, HTTPResponse>>, Protocol = Protocol::Http);
+    HTTPServer(std::initializer_list<std::pair<String, HTTPResponse>>, Protocol = Protocol::Http, Function<void(sec_protocol_metadata_t, sec_trust_t, sec_protocol_verify_complete_t)>&& = nullptr);
     uint16_t port() const;
     NSURLRequest *request() const;
     
@@ -49,15 +49,23 @@
     
     RetainPtr<nw_listener_t> m_listener;
     const Protocol m_protocol;
+    const Function<void(sec_protocol_metadata_t, sec_trust_t, sec_protocol_verify_complete_t)> m_certVerifier;
     const HashMap<String, HTTPResponse> m_requestResponseMap;
 };
 
 struct HTTPServer::HTTPResponse {
-    HTTPResponse(String&& body)
-        : body(WTFMove(body)) { }
+    enum class TerminateConnection { No, Yes };
+
+    HTTPResponse(const String& body)
+        : body(body) { }
     HTTPResponse(String&& body, HashMap<String, String>&& headerFields)
         : body(WTFMove(body))
         , headerFields(WTFMove(headerFields)) { }
+    HTTPResponse(unsigned statusCode, HashMap<String, String>&& headerFields)
+        : headerFields(WTFMove(headerFields))
+        , statusCode(statusCode) { }
+    HTTPResponse(TerminateConnection terminateConnection)
+        : terminateConnection(terminateConnection) { }
 
     HTTPResponse(const HTTPResponse&) = default;
     HTTPResponse(HTTPResponse&&) = default;
@@ -67,6 +75,8 @@
     
     String body;
     HashMap<String, String> headerFields;
+    unsigned statusCode { 200 };
+    TerminateConnection terminateConnection { TerminateConnection::No };
 };
 
 } // namespace TestWebKitAPI
diff --git a/Tools/TestWebKitAPI/cocoa/HTTPServer.mm b/Tools/TestWebKitAPI/cocoa/HTTPServer.mm
index 4cee389..d527b87 100644
--- a/Tools/TestWebKitAPI/cocoa/HTTPServer.mm
+++ b/Tools/TestWebKitAPI/cocoa/HTTPServer.mm
@@ -34,8 +34,9 @@
 
 namespace TestWebKitAPI {
 
-HTTPServer::HTTPServer(std::initializer_list<std::pair<String, HTTPResponse>> responses, Protocol protocol)
+HTTPServer::HTTPServer(std::initializer_list<std::pair<String, HTTPResponse>> responses, Protocol protocol, Function<void(sec_protocol_metadata_t, sec_trust_t, sec_protocol_verify_complete_t)>&& verify)
     : m_protocol(protocol)
+    , m_certVerifier(WTFMove(verify))
     , m_requestResponseMap([](std::initializer_list<std::pair<String, HTTPServer::HTTPResponse>> list) {
         HashMap<String, HTTPServer::HTTPResponse> map;
         for (auto& pair : list)
@@ -50,9 +51,15 @@
         sec_protocol_options_set_local_identity(options.get(), identity.get());
         if (protocol == Protocol::HttpsWithLegacyTLS)
             sec_protocol_options_set_max_tls_protocol_version(options.get(), tls_protocol_version_TLSv10);
+        if (m_certVerifier) {
+            sec_protocol_options_set_peer_authentication_required(options.get(), true);
+            sec_protocol_options_set_verify_block(options.get(), ^(sec_protocol_metadata_t metadata, sec_trust_t trust, sec_protocol_verify_complete_t completion) {
+                m_certVerifier(metadata, trust, completion);
+            }, dispatch_get_main_queue());
+        }
 #else
         UNUSED_PARAM(protocolOptions);
-        ASSERT(protocol != Protocol::HttpsWithLegacyTLS);
+        ASSERT_UNUSED(protocol, protocol != Protocol::HttpsWithLegacyTLS);
 #endif
     };
     auto parameters = adoptNS(nw_parameters_create_secure_tcp(configureTLS, NW_PARAMETERS_DEFAULT_CONFIGURATION));
@@ -73,6 +80,19 @@
     Util::run(&ready);
 }
 
+static const char* statusText(unsigned statusCode)
+{
+    switch (statusCode) {
+    case 200:
+        return "OK";
+    case 301:
+        return "Moved Permanently";
+    default:
+        ASSERT_NOT_REACHED();
+        return "Unrecognized status";
+    }
+}
+
 void HTTPServer::respondToRequests(nw_connection_t connection)
 {
     nw_connection_receive(connection, 1, std::numeric_limits<uint32_t>::max(), ^(dispatch_data_t content, nw_content_context_t context, bool complete, nw_error_t error) {
@@ -94,10 +114,18 @@
         size_t pathLength = pathEnd - request.data() - strlen(pathPrefix);
         String path(request.data() + strlen(pathPrefix), pathLength);
         ASSERT_WITH_MESSAGE(m_requestResponseMap.contains(path), "This HTTPServer does not know how to respond to a request for %s", path.utf8().data());
-        
+
         auto response = m_requestResponseMap.get(path);
+        if (response.terminateConnection == HTTPResponse::TerminateConnection::Yes) {
+            nw_connection_cancel(connection);
+            return;
+        }
         StringBuilder responseBuilder;
-        responseBuilder.append("HTTP/1.1 200 OK\r\nContent-Length: ");
+        responseBuilder.append("HTTP/1.1 ");
+        responseBuilder.appendNumber(response.statusCode);
+        responseBuilder.append(" ");
+        responseBuilder.append(statusText(response.statusCode));
+        responseBuilder.append("\r\nContent-Length: ");
         responseBuilder.appendNumber(response.body.length());
         responseBuilder.append("\r\n");
         for (auto& pair : response.headerFields) {