summaryrefslogtreecommitdiff
path: root/chromium/components/browser_ui/client_certificate
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@qt.io>2020-10-12 14:27:29 +0200
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2020-10-13 09:35:20 +0000
commitc30a6232df03e1efbd9f3b226777b07e087a1122 (patch)
treee992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/components/browser_ui/client_certificate
parent7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff)
downloadqtwebengine-chromium-85-based.tar.gz
BASELINE: Update Chromium to 85.0.4183.14085-based
Change-Id: Iaa42f4680837c57725b1344f108c0196741f6057 Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/components/browser_ui/client_certificate')
-rw-r--r--chromium/components/browser_ui/client_certificate/OWNERS7
-rw-r--r--chromium/components/browser_ui/client_certificate/android/BUILD.gn58
-rw-r--r--chromium/components/browser_ui/client_certificate/android/DEPS6
-rw-r--r--chromium/components/browser_ui/client_certificate/android/java/src/org/chromium/components/browser_ui/client_certificate/SSLClientCertificateRequest.java320
-rw-r--r--chromium/components/browser_ui/client_certificate/android/java/src/org/chromium/components/browser_ui/client_certificate/SSLClientCertificateRequestTest.java63
-rw-r--r--chromium/components/browser_ui/client_certificate/android/ssl_client_certificate_request.cc394
-rw-r--r--chromium/components/browser_ui/client_certificate/android/ssl_client_certificate_request.h32
7 files changed, 880 insertions, 0 deletions
diff --git a/chromium/components/browser_ui/client_certificate/OWNERS b/chromium/components/browser_ui/client_certificate/OWNERS
new file mode 100644
index 00000000000..bd8c1538b60
--- /dev/null
+++ b/chromium/components/browser_ui/client_certificate/OWNERS
@@ -0,0 +1,7 @@
+# Primary:
+dmcardle@chromium.org
+
+# Secondary:
+file://chrome/android/OWNERS
+
+# COMPONENT: Internals>Network>Auth
diff --git a/chromium/components/browser_ui/client_certificate/android/BUILD.gn b/chromium/components/browser_ui/client_certificate/android/BUILD.gn
new file mode 100644
index 00000000000..68a7aaf93bf
--- /dev/null
+++ b/chromium/components/browser_ui/client_certificate/android/BUILD.gn
@@ -0,0 +1,58 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/config/android/rules.gni")
+
+android_resources("java_resources") {
+ custom_package = "org.chromium.components.browser_ui.client_certificate"
+ deps = [
+ "//components/browser_ui/strings/android:browser_ui_strings_grd",
+ "//components/browser_ui/styles/android:java_resources",
+ ]
+}
+
+generate_jni("jni_headers") {
+ sources = [ "java/src/org/chromium/components/browser_ui/client_certificate/SSLClientCertificateRequest.java" ]
+}
+
+android_library("java") {
+ sources = [ "java/src/org/chromium/components/browser_ui/client_certificate/SSLClientCertificateRequest.java" ]
+ deps = [
+ ":java_resources",
+ "//base:base_java",
+ "//base:jni_java",
+ "//content/public/android:content_java",
+ "//third_party/android_deps:androidx_appcompat_appcompat_java",
+ "//ui/android:ui_java",
+ ]
+ annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ]
+}
+
+source_set("android") {
+ sources = [
+ "ssl_client_certificate_request.cc",
+ "ssl_client_certificate_request.h",
+ ]
+ deps = [
+ ":jni_headers",
+ "//base",
+ "//content/public/browser",
+ "//net",
+ "//ui/android",
+ ]
+}
+
+java_library("junit") {
+ # Platform checks are broken for Robolectric. See https://crbug.com/1071638.
+ bypass_platform_checks = true
+ testonly = true
+ sources = [ "java/src/org/chromium/components/browser_ui/client_certificate/SSLClientCertificateRequestTest.java" ]
+ deps = [
+ ":java",
+ "//base:base_junit_test_support",
+ "//third_party/android_deps:robolectric_all_java",
+ "//third_party/junit",
+ "//third_party/mockito:mockito_java",
+ ]
+}
diff --git a/chromium/components/browser_ui/client_certificate/android/DEPS b/chromium/components/browser_ui/client_certificate/android/DEPS
new file mode 100644
index 00000000000..20a2f7177eb
--- /dev/null
+++ b/chromium/components/browser_ui/client_certificate/android/DEPS
@@ -0,0 +1,6 @@
+include_rules = [
+ "+content/public/android/java/src/org/chromium/content_public",
+ "+content/public/browser",
+ "+net",
+ "+ui/android",
+]
diff --git a/chromium/components/browser_ui/client_certificate/android/java/src/org/chromium/components/browser_ui/client_certificate/SSLClientCertificateRequest.java b/chromium/components/browser_ui/client_certificate/android/java/src/org/chromium/components/browser_ui/client_certificate/SSLClientCertificateRequest.java
new file mode 100644
index 00000000000..800cf88e521
--- /dev/null
+++ b/chromium/components/browser_ui/client_certificate/android/java/src/org/chromium/components/browser_ui/client_certificate/SSLClientCertificateRequest.java
@@ -0,0 +1,320 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.components.browser_ui.client_certificate;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.DialogInterface.OnClickListener;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
+import android.security.KeyChainException;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.app.AlertDialog;
+
+import org.chromium.base.ContextUtils;
+import org.chromium.base.ThreadUtils;
+import org.chromium.base.annotations.CalledByNative;
+import org.chromium.base.annotations.JNINamespace;
+import org.chromium.base.annotations.NativeMethods;
+import org.chromium.base.task.AsyncTask;
+import org.chromium.base.task.PostTask;
+import org.chromium.content_public.browser.UiThreadTaskTraits;
+import org.chromium.ui.UiUtils;
+import org.chromium.ui.base.WindowAndroid;
+
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * Handles selection of client certificate on the Java side. This class is responsible for selection
+ * of the client certificate to be used for authentication and retrieval of the private key and full
+ * certificate chain.
+ *
+ * The entry point is selectClientCertificate() and it will be called on the UI thread. Then the
+ * class will construct and run an appropriate CertAsyncTask, that will run in background, and
+ * finally pass the results back to the UI thread, which will return to the native code.
+ */
+@JNINamespace("browser_ui")
+public class SSLClientCertificateRequest {
+ static final String TAG = "SSLClientCertificateRequest";
+
+ /**
+ * Implementation for anynchronous task of handling the certificate request. This
+ * AsyncTask retrieves the authentication material from the system key store.
+ * The key store is accessed in background, as the APIs being exercised
+ * may be blocking. The results are posted back to native on the UI thread.
+ */
+ private static class CertAsyncTaskKeyChain extends AsyncTask<Void> {
+ // These fields will store the results computed in doInBackground so that they can be posted
+ // back in onPostExecute.
+ private byte[][] mEncodedChain;
+ private PrivateKey mPrivateKey;
+
+ // Pointer to the native certificate request needed to return the results.
+ private final long mNativePtr;
+
+ @SuppressLint("StaticFieldLeak") // TODO(crbug.com/807729): Remove and fix.
+ final Context mContext;
+ final String mAlias;
+
+ CertAsyncTaskKeyChain(Context context, long nativePtr, String alias) {
+ mNativePtr = nativePtr;
+ mContext = context;
+ assert alias != null;
+ mAlias = alias;
+ }
+
+ @Override
+ protected Void doInBackground() {
+ String alias = getAlias();
+ if (alias == null) return null;
+
+ PrivateKey key = getPrivateKey(alias);
+ X509Certificate[] chain = getCertificateChain(alias);
+
+ if (key == null || chain == null || chain.length == 0) {
+ Log.w(TAG, "Empty client certificate chain?");
+ return null;
+ }
+
+ // Encode the certificate chain.
+ byte[][] encodedChain = new byte[chain.length][];
+ try {
+ for (int i = 0; i < chain.length; ++i) {
+ encodedChain[i] = chain[i].getEncoded();
+ }
+ } catch (CertificateEncodingException e) {
+ Log.w(TAG, "Could not retrieve encoded certificate chain: " + e);
+ return null;
+ }
+
+ mEncodedChain = encodedChain;
+ mPrivateKey = key;
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ ThreadUtils.assertOnUiThread();
+ SSLClientCertificateRequestJni.get().onSystemRequestCompletion(
+ mNativePtr, mEncodedChain, mPrivateKey);
+ }
+
+ private String getAlias() {
+ return mAlias;
+ }
+
+ private PrivateKey getPrivateKey(String alias) {
+ try {
+ return KeyChain.getPrivateKey(mContext, alias);
+ } catch (KeyChainException e) {
+ Log.w(TAG, "KeyChainException when looking for '" + alias + "' certificate");
+ return null;
+ } catch (InterruptedException e) {
+ Log.w(TAG, "InterruptedException when looking for '" + alias + "'certificate");
+ return null;
+ }
+ }
+
+ private X509Certificate[] getCertificateChain(String alias) {
+ try {
+ return KeyChain.getCertificateChain(mContext, alias);
+ } catch (KeyChainException e) {
+ Log.w(TAG, "KeyChainException when looking for '" + alias + "' certificate");
+ return null;
+ } catch (InterruptedException e) {
+ Log.w(TAG, "InterruptedException when looking for '" + alias + "'certificate");
+ return null;
+ }
+ }
+ }
+
+ /**
+ * The system KeyChain API will call us back on the alias() method, passing the alias of the
+ * certificate selected by the user.
+ */
+ private static class KeyChainCertSelectionCallback implements KeyChainAliasCallback {
+ private final long mNativePtr;
+ private final Context mContext;
+
+ KeyChainCertSelectionCallback(Context context, long nativePtr) {
+ mContext = context;
+ mNativePtr = nativePtr;
+ }
+
+ @Override
+ public void alias(final String alias) {
+ // This is called by KeyChainActivity in a background thread. Post task to
+ // handle the certificate selection on the UI thread.
+ PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT, () -> {
+ if (alias == null) {
+ // No certificate was selected.
+ PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT,
+ ()
+ -> SSLClientCertificateRequestJni.get()
+ .onSystemRequestCompletion(mNativePtr, null, null));
+ } else {
+ new CertAsyncTaskKeyChain(mContext, mNativePtr, alias)
+ .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ });
+ }
+ }
+
+ /**
+ * Wrapper class for the static KeyChain#choosePrivateKeyAlias method to facilitate testing.
+ */
+ @VisibleForTesting
+ static class KeyChainCertSelectionWrapper {
+ private final Activity mActivity;
+ private final KeyChainAliasCallback mCallback;
+ private final String[] mKeyTypes;
+ private final Principal[] mPrincipalsForCallback;
+ private final String mHostName;
+ private final int mPort;
+ private final String mAlias;
+
+ public KeyChainCertSelectionWrapper(Activity activity, KeyChainAliasCallback callback,
+ String[] keyTypes, Principal[] principalsForCallback, String hostName, int port,
+ String alias) {
+ mActivity = activity;
+ mCallback = callback;
+ mKeyTypes = keyTypes;
+ mPrincipalsForCallback = principalsForCallback;
+ mHostName = hostName;
+ mPort = port;
+ mAlias = alias;
+ }
+
+ /**
+ * Calls KeyChain#choosePrivateKeyAlias with the provided arguments.
+ */
+ public void choosePrivateKeyAlias() throws ActivityNotFoundException {
+ KeyChain.choosePrivateKeyAlias(mActivity, mCallback, mKeyTypes, mPrincipalsForCallback,
+ mHostName, mPort, mAlias);
+ }
+ }
+
+ /**
+ * Dialog that explains to the user that client certificates aren't supported on their operating
+ * system. Separated out into its own class to allow Robolectric unit testing of
+ * maybeShowCertSelection without depending on Chrome resources.
+ */
+ @VisibleForTesting
+ static class CertSelectionFailureDialog {
+ private final Context mContext;
+
+ public CertSelectionFailureDialog(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Builds and shows the dialog.
+ */
+ public void show() {
+ final AlertDialog.Builder builder = new UiUtils.CompatibleAlertDialogBuilder(
+ mContext, R.style.Theme_Chromium_AlertDialog);
+ builder.setTitle(R.string.client_cert_unsupported_title)
+ .setMessage(R.string.client_cert_unsupported_message)
+ .setNegativeButton(R.string.close,
+ (OnClickListener) (dialog, which)
+ -> {
+ // Do nothing
+ });
+ builder.show();
+ }
+ }
+
+ /**
+ * Create a new asynchronous request to select a client certificate.
+ *
+ * @param nativePtr The native object responsible for this request.
+ * @param window A WindowAndroid instance.
+ * @param keyTypes The list of supported key exchange types.
+ * @param encodedPrincipals The list of CA DistinguishedNames.
+ * @param hostName The server host name is available (empty otherwise).
+ * @param port The server port if available (0 otherwise).
+ * @return true on success.
+ * Note that nativeOnSystemRequestComplete will be called iff this method returns true.
+ */
+ @CalledByNative
+ private static boolean selectClientCertificate(final long nativePtr, final WindowAndroid window,
+ final String[] keyTypes, byte[][] encodedPrincipals, final String hostName,
+ final int port) {
+ ThreadUtils.assertOnUiThread();
+
+ // Use the context for the failure dialog in case the activity doesn't have the correct
+ // resources.
+ final Context context = window.getContext().get();
+ final Activity activity = ContextUtils.activityFromContext(context);
+ if (activity == null) {
+ Log.w(TAG, "Certificate request on GC'd activity.");
+ return false;
+ }
+
+ // Build the list of principals from encoded versions.
+ Principal[] principals = null;
+ if (encodedPrincipals.length > 0) {
+ principals = new X500Principal[encodedPrincipals.length];
+ try {
+ for (int n = 0; n < encodedPrincipals.length; n++) {
+ principals[n] = new X500Principal(encodedPrincipals[n]);
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Exception while decoding issuers list: " + e);
+ return false;
+ }
+ }
+
+ KeyChainCertSelectionCallback callback =
+ new KeyChainCertSelectionCallback(activity.getApplicationContext(), nativePtr);
+ KeyChainCertSelectionWrapper keyChain = new KeyChainCertSelectionWrapper(
+ activity, callback, keyTypes, principals, hostName, port, null);
+ maybeShowCertSelection(keyChain, callback, new CertSelectionFailureDialog(context));
+
+ // We've taken ownership of the native ssl request object.
+ return true;
+ }
+
+ /**
+ * Attempt to show the certificate selection dialog and shows the provided
+ * CertSelectionFailureDialog if the platform's cert selection activity can't be found.
+ */
+ @VisibleForTesting
+ static void maybeShowCertSelection(KeyChainCertSelectionWrapper keyChain,
+ KeyChainAliasCallback callback, CertSelectionFailureDialog failureDialog) {
+ try {
+ keyChain.choosePrivateKeyAlias();
+ } catch (ActivityNotFoundException e) {
+ // This exception can be hit when a platform is missing the activity to select
+ // a client certificate. It gets handled here to avoid a crash.
+ // Complete the callback without selecting a certificate.
+ callback.alias(null);
+ // Show a dialog letting the user know that the system does not support
+ // client certificate selection.
+ failureDialog.show();
+ }
+ }
+
+ public static void notifyClientCertificatesChangedOnIOThread() {
+ Log.d(TAG, "ClientCertificatesChanged!");
+ SSLClientCertificateRequestJni.get().notifyClientCertificatesChangedOnIOThread();
+ }
+
+ @NativeMethods
+ interface Natives {
+ void notifyClientCertificatesChangedOnIOThread();
+ // Called to pass request results to native side.
+ void onSystemRequestCompletion(long requestPtr, byte[][] certChain, PrivateKey privateKey);
+ }
+}
diff --git a/chromium/components/browser_ui/client_certificate/android/java/src/org/chromium/components/browser_ui/client_certificate/SSLClientCertificateRequestTest.java b/chromium/components/browser_ui/client_certificate/android/java/src/org/chromium/components/browser_ui/client_certificate/SSLClientCertificateRequestTest.java
new file mode 100644
index 00000000000..2f3135f4106
--- /dev/null
+++ b/chromium/components/browser_ui/client_certificate/android/java/src/org/chromium/components/browser_ui/client_certificate/SSLClientCertificateRequestTest.java
@@ -0,0 +1,63 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.components.browser_ui.client_certificate;
+
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.content.ActivityNotFoundException;
+import android.security.KeyChainAliasCallback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.annotation.Config;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.components.browser_ui.client_certificate.SSLClientCertificateRequest.CertSelectionFailureDialog;
+import org.chromium.components.browser_ui.client_certificate.SSLClientCertificateRequest.KeyChainCertSelectionWrapper;
+
+/**
+ * Unit tests for the SSLClientCertificateRequest class.
+ */
+@RunWith(BaseRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class SSLClientCertificateRequestTest {
+ @Mock
+ private KeyChainCertSelectionWrapper mKeyChainMock;
+ @Mock
+ private KeyChainAliasCallback mCallbackMock;
+ @Mock
+ private CertSelectionFailureDialog mFailureDialogMock;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void testSelectCertActivityNotFound() {
+ doThrow(new ActivityNotFoundException()).when(mKeyChainMock).choosePrivateKeyAlias();
+
+ SSLClientCertificateRequest.maybeShowCertSelection(
+ mKeyChainMock, mCallbackMock, mFailureDialogMock);
+
+ verify(mKeyChainMock).choosePrivateKeyAlias();
+ verify(mCallbackMock).alias(null);
+ verify(mFailureDialogMock).show();
+ }
+
+ @Test
+ public void testSelectCertActivityFound() {
+ SSLClientCertificateRequest.maybeShowCertSelection(
+ mKeyChainMock, mCallbackMock, mFailureDialogMock);
+
+ verify(mKeyChainMock).choosePrivateKeyAlias();
+ verifyZeroInteractions(mFailureDialogMock);
+ }
+}
diff --git a/chromium/components/browser_ui/client_certificate/android/ssl_client_certificate_request.cc b/chromium/components/browser_ui/client_certificate/android/ssl_client_certificate_request.cc
new file mode 100644
index 00000000000..13cb4583889
--- /dev/null
+++ b/chromium/components/browser_ui/client_certificate/android/ssl_client_certificate_request.cc
@@ -0,0 +1,394 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/browser_ui/client_certificate/android/ssl_client_certificate_request.h"
+
+#include <stddef.h>
+#include <utility>
+
+#include "base/android/jni_array.h"
+#include "base/android/jni_string.h"
+#include "base/android/scoped_java_ref.h"
+#include "base/bind.h"
+#include "base/compiler_specific.h"
+#include "base/containers/queue.h"
+#include "base/logging.h"
+#include "base/memory/ref_counted.h"
+#include "components/browser_ui/client_certificate/android/jni_headers/SSLClientCertificateRequest_jni.h"
+#include "content/public/browser/browser_task_traits.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/client_certificate_delegate.h"
+#include "content/public/browser/navigation_handle.h"
+#include "content/public/browser/web_contents_observer.h"
+#include "content/public/browser/web_contents_user_data.h"
+#include "net/base/host_port_pair.h"
+#include "net/cert/cert_database.h"
+#include "net/cert/x509_certificate.h"
+#include "net/ssl/ssl_cert_request_info.h"
+#include "net/ssl/ssl_client_cert_type.h"
+#include "net/ssl/ssl_platform_key_android.h"
+#include "net/ssl/ssl_private_key.h"
+#include "ui/android/view_android.h"
+#include "ui/android/window_android.h"
+
+namespace browser_ui {
+namespace {
+using base::android::JavaParamRef;
+using base::android::ScopedJavaLocalRef;
+
+class SSLClientCertPendingRequests;
+
+class ClientCertRequest {
+ public:
+ ClientCertRequest(
+ base::WeakPtr<SSLClientCertPendingRequests> pending_requests,
+ const scoped_refptr<net::SSLCertRequestInfo>& cert_request_info,
+ std::unique_ptr<content::ClientCertificateDelegate> delegate)
+ : pending_requests_(pending_requests),
+ cert_request_info_(cert_request_info),
+ delegate_(std::move(delegate)) {}
+
+ base::OnceClosure GetCancellationCallback() {
+ return base::BindOnce(&ClientCertRequest::OnCancel,
+ weak_factory_.GetWeakPtr());
+ }
+
+ void CertificateSelected(scoped_refptr<net::X509Certificate> cert,
+ scoped_refptr<net::SSLPrivateKey> key);
+
+ void OnCancel();
+
+ net::SSLCertRequestInfo* cert_request_info() const {
+ return cert_request_info_.get();
+ }
+
+ private:
+ base::WeakPtr<SSLClientCertPendingRequests> pending_requests_;
+ scoped_refptr<net::SSLCertRequestInfo> cert_request_info_;
+ std::unique_ptr<content::ClientCertificateDelegate> delegate_;
+ base::WeakPtrFactory<ClientCertRequest> weak_factory_{this};
+
+ DISALLOW_COPY_AND_ASSIGN(ClientCertRequest);
+};
+
+class SSLClientCertPendingRequests
+ : public content::WebContentsUserData<SSLClientCertPendingRequests>,
+ public content::WebContentsObserver {
+ public:
+ explicit SSLClientCertPendingRequests(content::WebContents* web_contents)
+ : content::WebContentsObserver(web_contents) {}
+ ~SSLClientCertPendingRequests() override {}
+
+ void AddRequest(std::unique_ptr<ClientCertRequest> request);
+
+ void RequestComplete(net::SSLCertRequestInfo* info,
+ scoped_refptr<net::X509Certificate> cert,
+ scoped_refptr<net::SSLPrivateKey> key);
+
+ // Remove pending requests when |should_keep| returns false. Calls |on_drop|
+ // before dropping a request.
+ void FilterPendingRequests(
+ std::function<bool(ClientCertRequest*)> should_keep,
+ std::function<void(ClientCertRequest*)> on_drop);
+
+ base::WeakPtr<SSLClientCertPendingRequests> GetWeakPtr() {
+ return weak_factory_.GetWeakPtr();
+ }
+
+ void ReadyToCommitNavigation(
+ content::NavigationHandle* navigation_handle) override;
+
+ class CertificateDialogPolicy {
+ public:
+ // Has the maximum number of cert dialogs been exceeded?
+ bool MaxExceeded() { return count_ >= k_max_displayed_dialogs; }
+ // Resets counter. Should be called on navigation.
+ void ResetCount() { count_ = 0; }
+ // Increment the counter.
+ void IncrementCount() { count_++; }
+
+ private:
+ size_t count_ = 0;
+ const size_t k_max_displayed_dialogs = 5;
+ };
+
+ private:
+ void PumpRequests();
+
+ bool active_request_ = false;
+
+ CertificateDialogPolicy dialog_policy_;
+ base::queue<std::unique_ptr<ClientCertRequest>> pending_requests_;
+ base::WeakPtrFactory<SSLClientCertPendingRequests> weak_factory_{this};
+
+ friend class content::WebContentsUserData<SSLClientCertPendingRequests>;
+
+ WEB_CONTENTS_USER_DATA_KEY_DECL();
+};
+
+ui::WindowAndroid* GetWindowFromWebContents(
+ content::WebContents* web_contents) {
+ ui::ViewAndroid* view = web_contents->GetNativeView();
+ if (view == nullptr) {
+ LOG(ERROR) << "Could not get ViewAndroid";
+ return nullptr;
+ }
+ // May return nullptr.
+ return view->GetWindowAndroid();
+}
+
+WEB_CONTENTS_USER_DATA_KEY_IMPL(SSLClientCertPendingRequests)
+
+static void StartClientCertificateRequest(
+ std::unique_ptr<ClientCertRequest> request,
+ content::WebContents* web_contents) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+ ui::WindowAndroid* window = GetWindowFromWebContents(web_contents);
+ if (window == nullptr) {
+ LOG(ERROR) << "Could not get Window";
+ return;
+ }
+
+ // Build the |key_types| JNI parameter, as a String[]
+ std::vector<std::string> key_types;
+ for (size_t n = 0; n < request->cert_request_info()->cert_key_types.size();
+ ++n) {
+ switch (request->cert_request_info()->cert_key_types[n]) {
+ case net::CLIENT_CERT_RSA_SIGN:
+ key_types.push_back("RSA");
+ break;
+ case net::CLIENT_CERT_ECDSA_SIGN:
+ key_types.push_back("EC");
+ break;
+ default:
+ // Ignore unknown types.
+ break;
+ }
+ }
+
+ JNIEnv* env = base::android::AttachCurrentThread();
+ ScopedJavaLocalRef<jobjectArray> key_types_ref =
+ base::android::ToJavaArrayOfStrings(env, key_types);
+ if (key_types_ref.is_null()) {
+ LOG(ERROR) << "Could not create key types array (String[])";
+ return;
+ }
+
+ // Build the |encoded_principals| JNI parameter, as a byte[][]
+ ScopedJavaLocalRef<jobjectArray> principals_ref =
+ base::android::ToJavaArrayOfByteArray(
+ env, request->cert_request_info()->cert_authorities);
+ if (principals_ref.is_null()) {
+ LOG(ERROR) << "Could not create principals array (byte[][])";
+ return;
+ }
+
+ // Build the |host_name| and |port| JNI parameters, as a String and
+ // a jint.
+ ScopedJavaLocalRef<jstring> host_name_ref =
+ base::android::ConvertUTF8ToJavaString(
+ env, request->cert_request_info()->host_and_port.host());
+
+ // Pass the address of the delegate through to Java.
+ jlong request_id = reinterpret_cast<intptr_t>(request.get());
+
+ if (!Java_SSLClientCertificateRequest_selectClientCertificate(
+ env, request_id, window->GetJavaObject(), key_types_ref,
+ principals_ref, host_name_ref,
+ request->cert_request_info()->host_and_port.port())) {
+ return;
+ }
+
+ // Ownership was transferred to Java.
+ ignore_result(request.release());
+}
+
+void SSLClientCertPendingRequests::AddRequest(
+ std::unique_ptr<ClientCertRequest> request) {
+ pending_requests_.push(std::move(request));
+ PumpRequests();
+}
+
+// Note that the default value for |on_drop| is a no-op.
+void SSLClientCertPendingRequests::FilterPendingRequests(
+ std::function<bool(ClientCertRequest*)> should_keep,
+ std::function<void(ClientCertRequest*)> on_drop = [](auto* unused) {}) {
+ base::queue<std::unique_ptr<ClientCertRequest>> new_pending_requests;
+ while (!pending_requests_.empty()) {
+ std::unique_ptr<ClientCertRequest> next =
+ std::move(pending_requests_.front());
+ pending_requests_.pop();
+ if (should_keep(next.get())) {
+ new_pending_requests.push(std::move(next));
+ } else {
+ on_drop(next.get());
+ }
+ }
+ pending_requests_.swap(new_pending_requests);
+}
+
+void SSLClientCertPendingRequests::RequestComplete(
+ net::SSLCertRequestInfo* info,
+ scoped_refptr<net::X509Certificate> cert,
+ scoped_refptr<net::SSLPrivateKey> key) {
+ active_request_ = false;
+
+ // Deduplicate pending requests. Only keep pending requests whose host and
+ // port differ from those of the completed request.
+ const std::string host_and_port = info->host_and_port.ToString();
+ auto should_keep = [host_and_port](ClientCertRequest* req) {
+ return host_and_port != req->cert_request_info()->host_and_port.ToString();
+ };
+ auto on_drop = [cert, key](ClientCertRequest* req) {
+ req->CertificateSelected(cert, key);
+ };
+ FilterPendingRequests(should_keep, on_drop);
+
+ PumpRequests();
+}
+
+void SSLClientCertPendingRequests::PumpRequests() {
+ if (active_request_ || pending_requests_.empty()) {
+ return;
+ }
+
+ active_request_ = true;
+ std::unique_ptr<ClientCertRequest> next =
+ std::move(pending_requests_.front());
+ pending_requests_.pop();
+
+ // Check if this page is allowed to show any more client cert dialogs.
+ if (!dialog_policy_.MaxExceeded()) {
+ dialog_policy_.IncrementCount();
+ StartClientCertificateRequest(std::move(next), web_contents());
+ }
+}
+
+void SSLClientCertPendingRequests::ReadyToCommitNavigation(
+ content::NavigationHandle* navigation_handle) {
+ // Be careful to only reset the the client certificate dialog counter when the
+ // navigation is user-initiated. Note that |HasUserGesture| does not capture
+ // browser-initiated navigations. The negation of |IsRendererInitiated| tells
+ // us whether the navigation is browser-generated.
+ if (navigation_handle->IsInMainFrame() &&
+ (navigation_handle->HasUserGesture() ||
+ !navigation_handle->IsRendererInitiated())) {
+ // Flush any remaining dialogs before resetting the counter.
+ auto should_keep = [](auto* req) { return false; };
+ FilterPendingRequests(should_keep);
+ dialog_policy_.ResetCount();
+ }
+}
+
+void ClientCertRequest::CertificateSelected(
+ scoped_refptr<net::X509Certificate> cert,
+ scoped_refptr<net::SSLPrivateKey> key) {
+ delegate_->ContinueWithCertificate(cert, key);
+ if (pending_requests_) {
+ pending_requests_->RequestComplete(cert_request_info(), cert, key);
+ }
+}
+
+void ClientCertRequest::OnCancel() {
+ // When we receive an OnCancel message, we remove this ClientCertRequest from
+ // the queue of pending requests.
+ auto should_keep = [this](auto* req) { return req != this; };
+ if (pending_requests_) {
+ pending_requests_->FilterPendingRequests(should_keep);
+ }
+}
+
+} // namespace
+
+// Called from JNI on request completion/result.
+// |env| is the current thread's JNIEnv.
+// |clazz| is the SSLClientCertificateRequest JNI class reference.
+// |request_id| is the id passed to
+// Java_SSLClientCertificateRequest_selectClientCertificate() in Start().
+// |encoded_chain_ref| is a JNI reference to a Java array of byte arrays,
+// each item holding a DER-encoded X.509 certificate.
+// |private_key_ref| is the platform PrivateKey object JNI reference for
+// the client certificate.
+// Note: both |encoded_chain_ref| and |private_key_ref| will be NULL if
+// the user didn't select a certificate.
+static void JNI_SSLClientCertificateRequest_OnSystemRequestCompletion(
+ JNIEnv* env,
+ jlong request_id,
+ const JavaParamRef<jobjectArray>& encoded_chain_ref,
+ const JavaParamRef<jobject>& private_key_ref) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+ // Take back ownership of the request object.
+ std::unique_ptr<ClientCertRequest> request(
+ reinterpret_cast<ClientCertRequest*>(request_id));
+
+ if (encoded_chain_ref == NULL || private_key_ref == NULL) {
+ LOG(ERROR) << "No client certificate selected";
+ request->CertificateSelected(nullptr, nullptr);
+ return;
+ }
+
+ // Convert the encoded chain to a vector of strings.
+ std::vector<std::string> encoded_chain_strings;
+ if (encoded_chain_ref) {
+ base::android::JavaArrayOfByteArrayToStringVector(env, encoded_chain_ref,
+ &encoded_chain_strings);
+ }
+
+ std::vector<base::StringPiece> encoded_chain;
+ for (size_t n = 0; n < encoded_chain_strings.size(); ++n)
+ encoded_chain.push_back(encoded_chain_strings[n]);
+
+ // Create the X509Certificate object from the encoded chain.
+ scoped_refptr<net::X509Certificate> client_cert(
+ net::X509Certificate::CreateFromDERCertChain(encoded_chain));
+ if (!client_cert.get()) {
+ LOG(ERROR) << "Could not decode client certificate chain";
+ return;
+ }
+
+ // Create an SSLPrivateKey wrapper for the private key JNI reference.
+ scoped_refptr<net::SSLPrivateKey> private_key =
+ net::WrapJavaPrivateKey(client_cert.get(), private_key_ref);
+ if (!private_key) {
+ LOG(ERROR) << "Could not create OpenSSL wrapper for private key";
+ return;
+ }
+
+ request->CertificateSelected(std::move(client_cert), std::move(private_key));
+}
+
+static void NotifyClientCertificatesChanged() {
+ net::CertDatabase::GetInstance()->NotifyObserversCertDBChanged();
+}
+
+static void
+JNI_SSLClientCertificateRequest_NotifyClientCertificatesChangedOnIOThread(
+ JNIEnv* env) {
+ if (content::BrowserThread::CurrentlyOn(content::BrowserThread::IO)) {
+ NotifyClientCertificatesChanged();
+ } else {
+ content::GetIOThreadTaskRunner({})->PostTask(
+ FROM_HERE, base::BindOnce(&NotifyClientCertificatesChanged));
+ }
+}
+
+base::OnceClosure ShowSSLClientCertificateSelector(
+ content::WebContents* contents,
+ net::SSLCertRequestInfo* cert_request_info,
+ std::unique_ptr<content::ClientCertificateDelegate> delegate) {
+ SSLClientCertPendingRequests::CreateForWebContents(contents);
+ SSLClientCertPendingRequests* active_requests =
+ SSLClientCertPendingRequests::FromWebContents(contents);
+
+ auto client_cert_request = std::make_unique<ClientCertRequest>(
+ active_requests->GetWeakPtr(), cert_request_info, std::move(delegate));
+ base::OnceClosure cancellation_callback =
+ client_cert_request->GetCancellationCallback();
+ active_requests->AddRequest(std::move(client_cert_request));
+ return cancellation_callback;
+}
+
+} // namespace browser_ui
diff --git a/chromium/components/browser_ui/client_certificate/android/ssl_client_certificate_request.h b/chromium/components/browser_ui/client_certificate/android/ssl_client_certificate_request.h
new file mode 100644
index 00000000000..1ac2090b23c
--- /dev/null
+++ b/chromium/components/browser_ui/client_certificate/android/ssl_client_certificate_request.h
@@ -0,0 +1,32 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_BROWSER_UI_CLIENT_CERTIFICATE_ANDROID_SSL_CLIENT_CERTIFICATE_REQUEST_H_
+#define COMPONENTS_BROWSER_UI_CLIENT_CERTIFICATE_ANDROID_SSL_CLIENT_CERTIFICATE_REQUEST_H_
+
+#include <memory>
+
+#include "base/callback.h"
+
+namespace content {
+class ClientCertificateDelegate;
+class WebContents;
+} // namespace content
+
+namespace net {
+class SSLCertRequestInfo;
+}
+
+namespace browser_ui {
+
+// Opens a SSL client certificate selection dialog. Returns a callback that will
+// cancel the dialog.
+base::OnceClosure ShowSSLClientCertificateSelector(
+ content::WebContents* contents,
+ net::SSLCertRequestInfo* cert_request_info,
+ std::unique_ptr<content::ClientCertificateDelegate> delegate);
+
+} // namespace browser_ui
+
+#endif // COMPONENTS_BROWSER_UI_CLIENT_CERTIFICATE_ANDROID_SSL_CLIENT_CERTIFICATE_REQUEST_H_