diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-12 14:27:29 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-13 09:35:20 +0000 |
commit | c30a6232df03e1efbd9f3b226777b07e087a1122 (patch) | |
tree | e992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/components/browser_ui/client_certificate | |
parent | 7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff) | |
download | qtwebengine-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')
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_ |