diff options
author | sgurun@chromium.org <sgurun@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-04-22 22:22:44 +0000 |
---|---|---|
committer | sgurun@chromium.org <sgurun@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-04-22 22:22:44 +0000 |
commit | 353539b10270cbd21ef9510d383f80a05e5e66dc (patch) | |
tree | 98c042942b5745f73a5fc8ea26a4c77815121079 | |
parent | 756cbc945a29f68e9a42c2630c5829ec9b82e68e (diff) | |
download | chromium_src-353539b10270cbd21ef9510d383f80a05e5e66dc.zip chromium_src-353539b10270cbd21ef9510d383f80a05e5e66dc.tar.gz chromium_src-353539b10270cbd21ef9510d383f80a05e5e66dc.tar.bz2 |
Add client cert support to android_webview
This CL implements client certs backend for android_webview. Most of the code path is similar to how chrome handles client certs. the callbacks are eventually plumbed to webview as APIs.
We still need to answer the question that if ClientCert cache is needed at the android_webview layer.
BUG=b/12983007
TBR=jcivelli@chromium.org
Review URL: https://codereview.chromium.org/235563005
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@265380 0039d316-1c4b-4281-b951-d872f2087c98
17 files changed, 687 insertions, 13 deletions
diff --git a/android_webview/android_webview_tests.gypi b/android_webview/android_webview_tests.gypi index 9ead6fe..f3e8cbb5 100644 --- a/android_webview/android_webview_tests.gypi +++ b/android_webview/android_webview_tests.gypi @@ -100,6 +100,7 @@ 'browser/net/android_stream_reader_url_request_job_unittest.cc', 'browser/net/input_stream_reader_unittest.cc', 'lib/main/webview_tests.cc', + 'native/aw_contents_client_bridge_unittest.cc', 'native/input_stream_unittest.cc', 'native/state_serializer_unittest.cc', ], @@ -122,6 +123,7 @@ 'type': 'none', 'sources': [ '../android_webview/unittestjava/src/org/chromium/android_webview/unittest/InputStreamUnittest.java', + '../android_webview/unittestjava/src/org/chromium/android_webview/unittest/MockAwContentsClientBridge.java', ], 'variables': { 'jni_gen_package': 'android_webview_unittests', diff --git a/android_webview/browser/aw_content_browser_client.cc b/android_webview/browser/aw_content_browser_client.cc index f836fde..db2dfa9 100644 --- a/android_webview/browser/aw_content_browser_client.cc +++ b/android_webview/browser/aw_content_browser_client.cc @@ -359,10 +359,13 @@ void AwContentBrowserClient::SelectClientCertificate( const net::HttpNetworkSession* network_session, net::SSLCertRequestInfo* cert_request_info, const base::Callback<void(net::X509Certificate*)>& callback) { - LOG(WARNING) << "Client certificate request from " - << cert_request_info->host_and_port.ToString() - << " rejected. (Client certificates not supported in WebView)"; - callback.Run(NULL); + AwContentsClientBridgeBase* client = + AwContentsClientBridgeBase::FromID(render_process_id, render_frame_id); + if (client) { + client->SelectClientCertificate(cert_request_info, callback); + } else { + callback.Run(NULL); + } } blink::WebNotificationPresenter::Permission diff --git a/android_webview/browser/aw_contents_client_bridge_base.h b/android_webview/browser/aw_contents_client_bridge_base.h index a1b3b7f..a24aa4b 100644 --- a/android_webview/browser/aw_contents_client_bridge_base.h +++ b/android_webview/browser/aw_contents_client_bridge_base.h @@ -16,6 +16,7 @@ class WebContents; } namespace net { +class SSLCertRequestInfo; class X509Certificate; } @@ -28,6 +29,8 @@ namespace android_webview { // native/ from browser/ layer. class AwContentsClientBridgeBase { public: + typedef base::Callback<void(net::X509Certificate*)> SelectCertificateCallback; + // Adds the handler to the UserData registry. static void Associate(content::WebContents* web_contents, AwContentsClientBridgeBase* handler); @@ -43,6 +46,9 @@ class AwContentsClientBridgeBase { const GURL& request_url, const base::Callback<void(bool)>& callback, bool* cancel_request) = 0; + virtual void SelectClientCertificate( + net::SSLCertRequestInfo* cert_request_info, + const SelectCertificateCallback& callback) = 0; virtual void RunJavaScriptDialog( content::JavaScriptMessageType message_type, diff --git a/android_webview/java/src/org/chromium/android_webview/AwBrowserContext.java b/android_webview/java/src/org/chromium/android_webview/AwBrowserContext.java index fc991f8..d52ad4e 100644 --- a/android_webview/java/src/org/chromium/android_webview/AwBrowserContext.java +++ b/android_webview/java/src/org/chromium/android_webview/AwBrowserContext.java @@ -8,6 +8,7 @@ import android.content.Context; import android.content.SharedPreferences; import org.chromium.content.browser.ContentViewStatics; +import org.chromium.net.DefaultAndroidKeyStore; /** * Java side of the Browser Context: contains all the java side objects needed to host one @@ -15,8 +16,6 @@ import org.chromium.content.browser.ContentViewStatics; * Note that due to running in single process mode, and limitations on renderer process only * being able to use a single browser context, currently there can only be one AwBrowserContext * instance, so at this point the class mostly exists for conceptual clarity. - * - * Obtain the default (singleton) instance with AwBrowserProcess.getDefaultBrowserContext(). */ public class AwBrowserContext { @@ -28,6 +27,8 @@ public class AwBrowserContext { private AwCookieManager mCookieManager; private AwFormDatabase mFormDatabase; private HttpAuthDatabase mHttpAuthDatabase; + private DefaultAndroidKeyStore mLocalKeyStore; + private ClientCertLookupTable mClientCertLookupTable; public AwBrowserContext(SharedPreferences sharedPreferences) { mSharedPreferences = sharedPreferences; @@ -61,6 +62,20 @@ public class AwBrowserContext { return mHttpAuthDatabase; } + public DefaultAndroidKeyStore getKeyStore() { + if (mLocalKeyStore == null) { + mLocalKeyStore = new DefaultAndroidKeyStore(); + } + return mLocalKeyStore; + } + + public ClientCertLookupTable getClientCertLookupTable() { + if (mClientCertLookupTable == null) { + mClientCertLookupTable = new ClientCertLookupTable(); + } + return mClientCertLookupTable; + } + /** * @see android.webkit.WebView#pauseTimers() */ diff --git a/android_webview/java/src/org/chromium/android_webview/AwContents.java b/android_webview/java/src/org/chromium/android_webview/AwContents.java index c8796ac..832e907 100644 --- a/android_webview/java/src/org/chromium/android_webview/AwContents.java +++ b/android_webview/java/src/org/chromium/android_webview/AwContents.java @@ -508,7 +508,8 @@ public class AwContents { mLayoutSizer.setDelegate(new AwLayoutSizerDelegate()); mLayoutSizer.setDIPScale(mDIPScale); mWebContentsDelegate = new AwWebContentsDelegateAdapter(contentsClient, mContainerView); - mContentsClientBridge = new AwContentsClientBridge(contentsClient); + mContentsClientBridge = new AwContentsClientBridge(contentsClient, + mBrowserContext.getKeyStore(), mBrowserContext.getClientCertLookupTable()); mZoomControls = new AwZoomControls(this); mIoThreadClient = new IoThreadClientImpl(); mInterceptNavigationDelegate = new InterceptNavigationDelegateImpl(); @@ -1369,6 +1370,15 @@ public class AwContents { } /** + * @see android.webkit.WebView#clearClientCertPreferences() + */ + public void clearClientCertPreferences() { + mBrowserContext.getClientCertLookupTable().clear(); + if (mNativeAwContents == 0) return; + nativeClearClientCertPreferences(mNativeAwContents); + } + + /** * Method to return all hit test values relevant to public WebView API. * Note that this expose more data than needed for WebView.getHitTestResult. * Unsafely returning reference to mutable internal object to avoid excessive @@ -2140,4 +2150,5 @@ public class AwContents { private native void nativeCreatePdfExporter(long nativeAwContents, AwPdfExporter awPdfExporter); + private native void nativeClearClientCertPreferences(long nativeAwContents); } diff --git a/android_webview/java/src/org/chromium/android_webview/AwContentsClient.java b/android_webview/java/src/org/chromium/android_webview/AwContentsClient.java index 22e7c7d..fa59347 100644 --- a/android_webview/java/src/org/chromium/android_webview/AwContentsClient.java +++ b/android_webview/java/src/org/chromium/android_webview/AwContentsClient.java @@ -21,6 +21,8 @@ import org.chromium.content.browser.ContentViewCore; import org.chromium.content.browser.WebContentsObserverAndroid; import org.chromium.net.NetError; +import java.security.Principal; + /** * Base-class that an AwContents embedder derives from to receive callbacks. * This extends ContentViewClient, as in many cases we want to pass-thru ContentViewCore @@ -165,6 +167,12 @@ public abstract class AwContentsClient { public abstract void onReceivedSslError(ValueCallback<Boolean> callback, SslError error); + // TODO(sgurun): Make abstract once this has rolled in downstream. + public void onReceivedClientCertRequest( + final AwContentsClientBridge.ClientCertificateRequestCallback callback, + final String[] keyTypes, final Principal[] principals, final String host, + final int port) { } + public abstract void onReceivedLoginRequest(String realm, String account, String args); public abstract void onFormResubmission(Message dontResend, Message resend); diff --git a/android_webview/java/src/org/chromium/android_webview/AwContentsClientBridge.java b/android_webview/java/src/org/chromium/android_webview/AwContentsClientBridge.java index 66e4f1e..6ebfe8a 100644 --- a/android_webview/java/src/org/chromium/android_webview/AwContentsClientBridge.java +++ b/android_webview/java/src/org/chromium/android_webview/AwContentsClientBridge.java @@ -6,10 +6,21 @@ package org.chromium.android_webview; import android.net.http.SslCertificate; import android.net.http.SslError; +import android.util.Log; import android.webkit.ValueCallback; import org.chromium.base.CalledByNative; import org.chromium.base.JNINamespace; +import org.chromium.base.ThreadUtils; +import org.chromium.net.AndroidPrivateKey; +import org.chromium.net.DefaultAndroidKeyStore; + +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; + +import javax.security.auth.x500.X500Principal; /** * This class handles the JNI communication logic for the the AwContentsClient class. @@ -20,14 +31,99 @@ import org.chromium.base.JNINamespace; */ @JNINamespace("android_webview") public class AwContentsClientBridge { + static final String TAG = "AwContentsClientBridge"; private AwContentsClient mClient; // The native peer of this object. private long mNativeContentsClientBridge; - public AwContentsClientBridge(AwContentsClient client) { + private DefaultAndroidKeyStore mLocalKeyStore; + + private ClientCertLookupTable mLookupTable; + + // Used for mocking this class in tests. + protected AwContentsClientBridge(DefaultAndroidKeyStore keyStore, + ClientCertLookupTable table) { + mLocalKeyStore = keyStore; + mLookupTable = table; + } + + public AwContentsClientBridge(AwContentsClient client, DefaultAndroidKeyStore keyStore, + ClientCertLookupTable table) { assert client != null; mClient = client; + mLocalKeyStore = keyStore; + mLookupTable = table; + } + + /** + * Callback to communicate clientcertificaterequest back to the AwContentsClientBridge. + * The public methods should be called on UI thread. + * A request can not be proceeded, ignored or canceled more than once. Doing this + * is a programming error and causes an exception. + */ + public class ClientCertificateRequestCallback { + + private int mId; + private String mHost; + private int mPort; + private boolean mIsCalled; + + public ClientCertificateRequestCallback(int id, String host, int port) { + mId = id; + mHost = host; + mPort = port; + } + + public void proceed(PrivateKey privateKey, X509Certificate[] chain) { + ThreadUtils.assertOnUiThread(); + checkIfCalled(); + + AndroidPrivateKey key = mLocalKeyStore.createKey(privateKey); + + if (key == null || chain == null || chain.length == 0) { + Log.w(TAG, "Empty client certificate chain?"); + provideResponse(null, null); + return; + } + // 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); + provideResponse(null, null); + return; + } + mLookupTable.allow(mHost, mPort, key, encodedChain); + provideResponse(key, encodedChain); + } + + public void ignore() { + ThreadUtils.assertOnUiThread(); + checkIfCalled(); + provideResponse(null, null); + } + + public void cancel() { + ThreadUtils.assertOnUiThread(); + mLookupTable.deny(mHost, mPort); + provideResponse(null, null); + } + + public void checkIfCalled() { + if (mIsCalled) { + throw new IllegalStateException("The callback was already called."); + } + mIsCalled = true; + } + + private void provideResponse(AndroidPrivateKey androidKey, byte[][] certChain) { + nativeProvideClientCertificateResponse(mNativeContentsClientBridge, mId, + certChain, androidKey); + } } // Used by the native peer to set/reset a weak ref to the native peer. @@ -66,6 +162,43 @@ public class AwContentsClientBridge { nativeProceedSslError(mNativeContentsClientBridge, proceed, id); } + // Intentionally not private for testing the native peer of this class. + @CalledByNative + protected void selectClientCertificate(final int id, final String[] keyTypes, + byte[][] encodedPrincipals, final String host, final int port) { + ClientCertLookupTable.Cert cert = mLookupTable.getCertData(host, port); + if (mLookupTable.isDenied(host, port)) { + nativeProvideClientCertificateResponse(mNativeContentsClientBridge, id, + null, null); + return; + } + if (cert != null) { + nativeProvideClientCertificateResponse(mNativeContentsClientBridge, id, + cert.certChain, cert.privateKey); + return; + } + // Build the list of principals from encoded versions. + Principal[] principals = null; + if (encodedPrincipals.length > 0) { + principals = new X500Principal[encodedPrincipals.length]; + for (int n = 0; n < encodedPrincipals.length; n++) { + try { + principals[n] = new X500Principal(encodedPrincipals[n]); + } catch (IllegalArgumentException e) { + Log.w(TAG, "Exception while decoding issuers list: " + e); + nativeProvideClientCertificateResponse(mNativeContentsClientBridge, id, + null, null); + return; + } + } + + } + + final ClientCertificateRequestCallback callback = + new ClientCertificateRequestCallback(id, host, port); + mClient.onReceivedClientCertRequest(callback, keyTypes, principals, host, port); + } + @CalledByNative private void handleJsAlert(String url, String message, int id) { JsResultHandler handler = new JsResultHandler(this, id); @@ -110,6 +243,8 @@ public class AwContentsClientBridge { //-------------------------------------------------------------------------------------------- private native void nativeProceedSslError(long nativeAwContentsClientBridge, boolean proceed, int id); + private native void nativeProvideClientCertificateResponse(long nativeAwContentsClientBridge, + int id, byte[][] certChain, AndroidPrivateKey androidKey); private native void nativeConfirmJsResult(long nativeAwContentsClientBridge, int id, String prompt); diff --git a/android_webview/java/src/org/chromium/android_webview/ClientCertLookupTable.java b/android_webview/java/src/org/chromium/android_webview/ClientCertLookupTable.java new file mode 100644 index 0000000..65ef44ac --- /dev/null +++ b/android_webview/java/src/org/chromium/android_webview/ClientCertLookupTable.java @@ -0,0 +1,76 @@ +// Copyright 2014 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.android_webview; + +import org.chromium.net.AndroidPrivateKey; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Store user's client certificate decision for a host and port pair. Not + * thread-safe. All accesses are done on UI thread. + */ +public class ClientCertLookupTable { + + /** + * A container for the certificate data. + */ + public static class Cert { + AndroidPrivateKey privateKey; + byte[][] certChain; + public Cert(AndroidPrivateKey privateKey, byte[][] certChain) { + this.privateKey = privateKey; + byte[][] newChain = new byte[certChain.length][]; + for (int i = 0; i < certChain.length; i++) { + newChain[i] = Arrays.copyOf(certChain[i], certChain[i].length); + } + this.certChain = newChain; + } + }; + + private final Map<String, Cert> mCerts; + private final Set<String> mDenieds; + + // Clear client certificate preferences + public void clear() { + mCerts.clear(); + mDenieds.clear(); + } + + public ClientCertLookupTable() { + mCerts = new HashMap<String, Cert>(); + mDenieds = new HashSet<String>(); + } + + public void allow(String host, int port, AndroidPrivateKey privateKey, byte[][] chain) { + String host_and_port = hostAndPort(host, port); + mCerts.put(host_and_port, new Cert(privateKey, chain)); + mDenieds.remove(host_and_port); + } + + public void deny(String host, int port) { + String host_and_port = hostAndPort(host, port); + mCerts.remove(host_and_port); + mDenieds.add(host_and_port); + } + + public Cert getCertData(String host, int port) { + return mCerts.get(hostAndPort(host, port)); + } + + public boolean isDenied(String host, int port) { + return mDenieds.contains(hostAndPort(host, port)); + } + + // TODO(sgurun) add a test for this. Not separating host and pair properly will be + // a security issue. + private static String hostAndPort(String host, int port) { + return host + ":" + port; + } +} diff --git a/android_webview/native/DEPS b/android_webview/native/DEPS index 0c49faf..d221dcd 100644 --- a/android_webview/native/DEPS +++ b/android_webview/native/DEPS @@ -1,5 +1,6 @@ include_rules = [ "+content/public/browser", + "+content/public/test", "+ui/gfx", "+ui/shell_dialogs", diff --git a/android_webview/native/aw_contents.cc b/android_webview/native/aw_contents.cc index eb009e9..c7b07f8 100644 --- a/android_webview/native/aw_contents.cc +++ b/android_webview/native/aw_contents.cc @@ -54,6 +54,7 @@ #include "content/public/common/renderer_preferences.h" #include "content/public/common/ssl_status.h" #include "jni/AwContents_jni.h" +#include "net/cert/cert_database.h" #include "net/cert/x509_certificate.h" #include "third_party/skia/include/core/SkPicture.h" #include "ui/base/l10n/l10n_util_android.h" @@ -165,6 +166,11 @@ void OnIoThreadClientReady(content::RenderFrameHost* rfh) { render_process_id, render_frame_id); } +void NotifyClientCertificatesChanged() { + DCHECK_CURRENTLY_ON(BrowserThread::IO); + net::CertDatabase::GetInstance()->OnAndroidKeyStoreChanged(); +} + } // namespace // static @@ -1038,6 +1044,13 @@ void AwContents::SetExtraHeadersForUrl(JNIEnv* env, jobject obj, extra_headers); } +void AwContents::ClearClientCertPreferences(JNIEnv* env, jobject obj) { + content::BrowserThread::PostTask( + BrowserThread::IO, + FROM_HERE, + base::Bind(&NotifyClientCertificatesChanged)); +} + void AwContents::SetJsOnlineProperty(JNIEnv* env, jobject obj, jboolean network_up) { diff --git a/android_webview/native/aw_contents.h b/android_webview/native/aw_contents.h index acae893..bdcede7 100644 --- a/android_webview/native/aw_contents.h +++ b/android_webview/native/aw_contents.h @@ -129,6 +129,9 @@ class AwContents : public FindHelper::Listener, void DrawGL(AwDrawGLInfo* draw_info); + // TODO(sgurun) test this. + void ClearClientCertPreferences(JNIEnv* env, jobject obj); + // Geolocation API support void ShowGeolocationPrompt(const GURL& origin, base::Callback<void(bool)>); void HideGeolocationPrompt(const GURL& origin); diff --git a/android_webview/native/aw_contents_client_bridge.cc b/android_webview/native/aw_contents_client_bridge.cc index 5f9e299..0703c81 100644 --- a/android_webview/native/aw_contents_client_bridge.cc +++ b/android_webview/native/aw_contents_client_bridge.cc @@ -8,10 +8,14 @@ #include "base/android/jni_android.h" #include "base/android/jni_array.h" #include "base/android/jni_string.h" -#include "base/callback.h" +#include "base/callback_helpers.h" #include "content/public/browser/browser_thread.h" #include "jni/AwContentsClientBridge_jni.h" +#include "net/android/keystore_openssl.h" #include "net/cert/x509_certificate.h" +#include "net/ssl/openssl_client_key_store.h" +#include "net/ssl/ssl_cert_request_info.h" +#include "net/ssl/ssl_client_cert_type.h" #include "url/gurl.h" using base::android::AttachCurrentThread; @@ -24,6 +28,21 @@ using content::BrowserThread; namespace android_webview { +typedef net::OpenSSLClientKeyStore::ScopedEVP_PKEY ScopedEVP_PKEY; + +namespace { +// Must be called on the I/O thread to record a client certificate +// and its private key in the OpenSSLClientKeyStore. +void RecordClientCertificateKey( + const scoped_refptr<net::X509Certificate>& client_cert, + ScopedEVP_PKEY private_key) { + DCHECK_CURRENTLY_ON(content::BrowserThread::IO); + net::OpenSSLClientKeyStore::GetInstance()->RecordClientCertPrivateKey( + client_cert.get(), private_key.get()); +} + +} // namespace + AwContentsClientBridge::AwContentsClientBridge(JNIEnv* env, jobject obj) : java_ref_(env, obj) { DCHECK(obj); @@ -49,7 +68,7 @@ void AwContentsClientBridge::AllowCertificateError( const base::Callback<void(bool)>& callback, bool* cancel_request) { - DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + DCHECK_CURRENTLY_ON(BrowserThread::UI); JNIEnv* env = AttachCurrentThread(); ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); @@ -73,12 +92,12 @@ void AwContentsClientBridge::AllowCertificateError( // if the request is cancelled, then cancel the stored callback if (*cancel_request) { pending_cert_error_callbacks_.Remove(request_id); - } + } } void AwContentsClientBridge::ProceedSslError(JNIEnv* env, jobject obj, jboolean proceed, jint id) { - DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + DCHECK_CURRENTLY_ON(BrowserThread::UI); CertErrorCallback* callback = pending_cert_error_callbacks_.Lookup(id); if (!callback || callback->is_null()) { LOG(WARNING) << "Ignoring unexpected ssl error proceed callback"; @@ -88,6 +107,147 @@ void AwContentsClientBridge::ProceedSslError(JNIEnv* env, jobject obj, pending_cert_error_callbacks_.Remove(id); } +// This method is inspired by SelectClientCertificate() in +// chrome/browser/ui/android/ssl_client_certificate_request.cc +void AwContentsClientBridge::SelectClientCertificate( + net::SSLCertRequestInfo* cert_request_info, + const SelectCertificateCallback& callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + // Add the callback to id map. + int request_id = pending_client_cert_request_callbacks_.Add( + new SelectCertificateCallback(callback)); + // Make sure callback is run on error. + base::ScopedClosureRunner guard(base::Bind( + &AwContentsClientBridge::HandleErrorInClientCertificateResponse, + base::Unretained(this), + request_id)); + + JNIEnv* env = base::android::AttachCurrentThread(); + ScopedJavaLocalRef<jobject> obj = java_ref_.get(env); + if (obj.is_null()) + return; + + // Build the |key_types| JNI parameter, as a String[] + std::vector<std::string> key_types; + for (size_t n = 0; n < cert_request_info->cert_key_types.size(); ++n) { + switch (cert_request_info->cert_key_types[n]) { + case net::CLIENT_CERT_RSA_SIGN: + key_types.push_back("RSA"); + break; + case net::CLIENT_CERT_DSS_SIGN: + key_types.push_back("DSA"); + break; + case net::CLIENT_CERT_ECDSA_SIGN: + key_types.push_back("ECDSA"); + break; + default: + // Ignore unknown types. + break; + } + } + + 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, 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, cert_request_info->host_and_port.host()); + + Java_AwContentsClientBridge_selectClientCertificate( + env, + obj.obj(), + request_id, + key_types_ref.obj(), + principals_ref.obj(), + host_name_ref.obj(), + cert_request_info->host_and_port.port()); + + // Release the guard. + ignore_result(guard.Release()); +} + +// This method is inspired by OnSystemRequestCompletion() in +// chrome/browser/ui/android/ssl_client_certificate_request.cc +void AwContentsClientBridge::ProvideClientCertificateResponse( + JNIEnv* env, + jobject obj, + int request_id, + jobjectArray encoded_chain_ref, + jobject private_key_ref) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + SelectCertificateCallback* callback = + pending_client_cert_request_callbacks_.Lookup(request_id); + DCHECK(callback); + + // Make sure callback is run on error. + base::ScopedClosureRunner guard(base::Bind( + &AwContentsClientBridge::HandleErrorInClientCertificateResponse, + base::Unretained(this), + request_id)); + if (encoded_chain_ref == NULL || private_key_ref == NULL) { + LOG(ERROR) << "Client certificate request cancelled"; + 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 EVP_PKEY wrapper for the private key JNI reference. + ScopedEVP_PKEY private_key( + net::android::GetOpenSSLPrivateKeyWrapper(private_key_ref)); + if (!private_key.get()) { + LOG(ERROR) << "Could not create OpenSSL wrapper for private key"; + return; + } + + // RecordClientCertificateKey() must be called on the I/O thread, + // before the callback is called with the selected certificate on + // the UI thread. + content::BrowserThread::PostTaskAndReply( + content::BrowserThread::IO, + FROM_HERE, + base::Bind(&RecordClientCertificateKey, + client_cert, + base::Passed(&private_key)), + base::Bind(*callback, client_cert)); + pending_client_cert_request_callbacks_.Remove(request_id); + + // Release the guard. + ignore_result(guard.Release()); +} + void AwContentsClientBridge::RunJavaScriptDialog( content::JavaScriptMessageType message_type, const GURL& origin_url, @@ -206,6 +366,15 @@ void AwContentsClientBridge::CancelJsResult(JNIEnv*, jobject, int id) { pending_js_dialog_callbacks_.Remove(id); } +// Use to cleanup if there is an error in client certificate response. +void AwContentsClientBridge::HandleErrorInClientCertificateResponse( + int request_id) { + SelectCertificateCallback* callback = + pending_client_cert_request_callbacks_.Lookup(request_id); + callback->Run(scoped_refptr<net::X509Certificate>()); + pending_client_cert_request_callbacks_.Remove(request_id); +} + bool RegisterAwContentsClientBridge(JNIEnv* env) { return RegisterNativesImpl(env); } diff --git a/android_webview/native/aw_contents_client_bridge.h b/android_webview/native/aw_contents_client_bridge.h index 4cd6c6c..69055fd 100644 --- a/android_webview/native/aw_contents_client_bridge.h +++ b/android_webview/native/aw_contents_client_bridge.h @@ -29,7 +29,6 @@ namespace android_webview { // any references. class AwContentsClientBridge : public AwContentsClientBridgeBase { public: - AwContentsClientBridge(JNIEnv* env, jobject obj); virtual ~AwContentsClientBridge(); @@ -39,6 +38,9 @@ class AwContentsClientBridge : public AwContentsClientBridgeBase { const GURL& request_url, const base::Callback<void(bool)>& callback, bool* cancel_request) OVERRIDE; + virtual void SelectClientCertificate( + net::SSLCertRequestInfo* cert_request_info, + const SelectCertificateCallback& callback) OVERRIDE; virtual void RunJavaScriptDialog( content::JavaScriptMessageType message_type, @@ -56,16 +58,23 @@ class AwContentsClientBridge : public AwContentsClientBridgeBase { // Methods called from Java. void ProceedSslError(JNIEnv* env, jobject obj, jboolean proceed, jint id); + void ProvideClientCertificateResponse(JNIEnv* env, jobject object, + jint request_id, jobjectArray encoded_chain_ref, + jobject private_key_ref); void ConfirmJsResult(JNIEnv*, jobject, int id, jstring prompt); void CancelJsResult(JNIEnv*, jobject, int id); private: + void HandleErrorInClientCertificateResponse(int id); + JavaObjectWeakGlobalRef java_ref_; typedef const base::Callback<void(bool)> CertErrorCallback; IDMap<CertErrorCallback, IDMapOwnPointer> pending_cert_error_callbacks_; IDMap<content::JavaScriptDialogManager::DialogClosedCallback, IDMapOwnPointer> pending_js_dialog_callbacks_; + IDMap<SelectCertificateCallback, IDMapOwnPointer> + pending_client_cert_request_callbacks_; }; bool RegisterAwContentsClientBridge(JNIEnv* env); diff --git a/android_webview/native/aw_contents_client_bridge_unittest.cc b/android_webview/native/aw_contents_client_bridge_unittest.cc new file mode 100644 index 0000000..e6714bb --- /dev/null +++ b/android_webview/native/aw_contents_client_bridge_unittest.cc @@ -0,0 +1,152 @@ +// Copyright 2014 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 "android_webview/native/aw_contents_client_bridge.h" + +#include "base/android/jni_android.h" +#include "base/android/jni_array.h" +#include "base/android/scoped_java_ref.h" +#include "base/bind.h" +#include "base/memory/scoped_ptr.h" +#include "base/run_loop.h" +#include "content/public/test/test_browser_thread_bundle.h" +#include "jni/MockAwContentsClientBridge_jni.h" +#include "net/android/net_jni_registrar.h" +#include "net/ssl/ssl_cert_request_info.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + + +using base::android::AttachCurrentThread; +using base::android::ScopedJavaLocalRef; +using net::SSLCertRequestInfo; +using net::SSLClientCertType; +using net::X509Certificate; +using testing::NotNull; +using testing::Test; + +namespace android_webview { + +namespace { + +// Tests the android_webview contents client bridge. +class AwContentsClientBridgeTest : public Test { + public: + typedef AwContentsClientBridge::SelectCertificateCallback + SelectCertificateCallback; + + AwContentsClientBridgeTest() { } + + // Callback method called when a cert is selected. + void CertSelected(X509Certificate* cert); + protected: + virtual void SetUp(); + void TestCertType(SSLClientCertType type, const std::string& expected_name); + // Create the TestBrowserThreads. Just instantiate the member variable. + content::TestBrowserThreadBundle thread_bundle_; + base::android::ScopedJavaGlobalRef<jobject> jbridge_; + scoped_ptr<AwContentsClientBridge> bridge_; + scoped_refptr<SSLCertRequestInfo> cert_request_info_; + X509Certificate* selected_cert_; + int cert_selected_callbacks_; + JNIEnv* env_; +}; + +} // namespace + +void AwContentsClientBridgeTest::SetUp() { + env_ = AttachCurrentThread(); + ASSERT_THAT(env_, NotNull()); + ASSERT_TRUE(android_webview::RegisterAwContentsClientBridge(env_)); + ASSERT_TRUE(RegisterNativesImpl(env_)); + ASSERT_TRUE(net::android::RegisterJni(env_)); + jbridge_.Reset(env_, + Java_MockAwContentsClientBridge_getAwContentsClientBridge(env_).obj()); + bridge_.reset(new AwContentsClientBridge(env_, jbridge_.obj())); + selected_cert_ = NULL; + cert_selected_callbacks_ = 0; + cert_request_info_ = new net::SSLCertRequestInfo; +} + +void AwContentsClientBridgeTest::CertSelected(X509Certificate* cert) { + selected_cert_ = cert; + cert_selected_callbacks_++; +} + +TEST_F(AwContentsClientBridgeTest, TestClientCertKeyTypesCorrectlyEncoded) { + SSLClientCertType cert_types[3] = {net::CLIENT_CERT_RSA_SIGN, + net::CLIENT_CERT_DSS_SIGN, net::CLIENT_CERT_ECDSA_SIGN}; + std::string expected_names[3] = {"RSA", "DSA" ,"ECDSA"}; + + for(int i = 0; i < 3; i++) { + TestCertType(cert_types[i], expected_names[i]); + } +} + +void AwContentsClientBridgeTest::TestCertType(SSLClientCertType type, + const std::string& expected_name) { + cert_request_info_->cert_key_types.clear(); + cert_request_info_->cert_key_types.push_back(type); + bridge_->SelectClientCertificate( + cert_request_info_.get(), + base::Bind( + &AwContentsClientBridgeTest::CertSelected, + base::Unretained(static_cast<AwContentsClientBridgeTest*>(this)))); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(0, cert_selected_callbacks_); + ScopedJavaLocalRef<jobjectArray> key_types = + Java_MockAwContentsClientBridge_getKeyTypes(env_, jbridge_.obj()); + std::vector<std::string> vec; + base::android::AppendJavaStringArrayToStringVector(env_, + key_types.obj(), + &vec); + EXPECT_EQ(1u, vec.size()); + EXPECT_EQ(expected_name, vec[0]); +} + +// Verify that ProvideClientCertificateResponse works properly when the client +// responds with a null key. +TEST_F(AwContentsClientBridgeTest, + TestProvideClientCertificateResponseCallsCallbackOnNullKey) { + // Call SelectClientCertificate to create a callback id that mock java object + // can call on. + bridge_->SelectClientCertificate( + cert_request_info_.get(), + base::Bind( + &AwContentsClientBridgeTest::CertSelected, + base::Unretained(static_cast<AwContentsClientBridgeTest*>(this)))); + bridge_->ProvideClientCertificateResponse(env_, jbridge_.obj(), + Java_MockAwContentsClientBridge_getRequestId(env_, jbridge_.obj()), + Java_MockAwContentsClientBridge_createTestCertChain( + env_, jbridge_.obj()).obj(), + NULL); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(NULL, selected_cert_); + EXPECT_EQ(1, cert_selected_callbacks_); +} + +// Verify that ProvideClientCertificateResponse calls the callback with +// NULL parameters when private key is not provided. +TEST_F(AwContentsClientBridgeTest, + TestProvideClientCertificateResponseCallsCallbackOnNullChain) { + // Call SelectClientCertificate to create a callback id that mock java object + // can call on. + bridge_->SelectClientCertificate( + cert_request_info_.get(), + base::Bind( + &AwContentsClientBridgeTest::CertSelected, + base::Unretained(static_cast<AwContentsClientBridgeTest*>(this)))); + int requestId = + Java_MockAwContentsClientBridge_getRequestId(env_, jbridge_.obj()); + bridge_->ProvideClientCertificateResponse(env_, jbridge_.obj(), + requestId, + NULL, + Java_MockAwContentsClientBridge_createTestPrivateKey( + env_, jbridge_.obj()).obj()); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(NULL, selected_cert_); + EXPECT_EQ(1, cert_selected_callbacks_); +} + +} // android_webview diff --git a/android_webview/native/webview_native.gyp b/android_webview/native/webview_native.gyp index 27d599a..9a1761b 100644 --- a/android_webview/native/webview_native.gyp +++ b/android_webview/native/webview_native.gyp @@ -24,6 +24,7 @@ '../../webkit/common/webkit_common.gyp:webkit_common', '../../webkit/storage_browser.gyp:webkit_storage_browser', '../../webkit/storage_common.gyp:webkit_storage_common', + '../../third_party/openssl/openssl.gyp:openssl', 'android_webview_native_jni', ], 'include_dirs': [ diff --git a/android_webview/test/shell/src/org/chromium/android_webview/test/NullContentsClient.java b/android_webview/test/shell/src/org/chromium/android_webview/test/NullContentsClient.java index 34e6ca5..d1dc1be 100644 --- a/android_webview/test/shell/src/org/chromium/android_webview/test/NullContentsClient.java +++ b/android_webview/test/shell/src/org/chromium/android_webview/test/NullContentsClient.java @@ -17,12 +17,15 @@ import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import org.chromium.android_webview.AwContentsClient; +import org.chromium.android_webview.AwContentsClientBridge; import org.chromium.android_webview.AwHttpAuthHandler; import org.chromium.android_webview.InterceptedRequestData; import org.chromium.android_webview.JsPromptResultReceiver; import org.chromium.android_webview.JsResultReceiver; import org.chromium.base.ThreadUtils; +import java.security.Principal; + /** * As a convience for tests that only care about specefic callbacks, this class provides * empty implementations of all abstract methods. @@ -87,6 +90,14 @@ public class NullContentsClient extends AwContentsClient { } @Override + public void onReceivedClientCertRequest( + final AwContentsClientBridge.ClientCertificateRequestCallback callback, + final String[] keyTypes, final Principal[] principals, final String host, + final int port) { + callback.proceed(null, null); + } + + @Override public void onReceivedLoginRequest(String realm, String account, String args) { } diff --git a/android_webview/unittestjava/src/org/chromium/android_webview/unittest/MockAwContentsClientBridge.java b/android_webview/unittestjava/src/org/chromium/android_webview/unittest/MockAwContentsClientBridge.java new file mode 100644 index 0000000..5c4238d --- /dev/null +++ b/android_webview/unittestjava/src/org/chromium/android_webview/unittest/MockAwContentsClientBridge.java @@ -0,0 +1,59 @@ +// Copyright 2014 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.android_webview.unittest; + +import org.chromium.android_webview.AwContentsClientBridge; +import org.chromium.android_webview.ClientCertLookupTable; +import org.chromium.base.CalledByNative; +import org.chromium.net.AndroidKeyStore; +import org.chromium.net.AndroidPrivateKey; +import org.chromium.net.DefaultAndroidKeyStore; + +class MockAwContentsClientBridge extends AwContentsClientBridge { + + private int mId; + private String[] mKeyTypes; + + public MockAwContentsClientBridge() { + super(new DefaultAndroidKeyStore(), new ClientCertLookupTable()); + } + + @Override + protected void selectClientCertificate(final int id, final String[] keyTypes, + byte[][] encodedPrincipals, final String host, final int port) { + mId = id; + mKeyTypes = keyTypes; + } + + @CalledByNative + private static MockAwContentsClientBridge getAwContentsClientBridge() { + return new MockAwContentsClientBridge(); + } + + @CalledByNative + private String[] getKeyTypes() { + return mKeyTypes; + } + + @CalledByNative + private int getRequestId() { + return mId; + } + + @CalledByNative + private AndroidPrivateKey createTestPrivateKey() { + return new AndroidPrivateKey() { + @Override + public AndroidKeyStore getKeyStore() { + return null; + } + }; + } + + @CalledByNative + private byte[][] createTestCertChain() { + return new byte[][]{{1}}; + } +} |