Skip to content

Commit

Permalink
Add client cert support to android_webview
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sgurun@chromium.org committed Apr 22, 2014
1 parent 756cbc9 commit 353539b
Show file tree
Hide file tree
Showing 17 changed files with 687 additions and 13 deletions.
2 changes: 2 additions & 0 deletions android_webview/android_webview_tests.gypi
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
Expand All @@ -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',
Expand Down
11 changes: 7 additions & 4 deletions android_webview/browser/aw_content_browser_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions android_webview/browser/aw_contents_client_bridge_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class WebContents;
}

namespace net {
class SSLCertRequestInfo;
class X509Certificate;
}

Expand All @@ -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);
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@
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
* browing session (i.e. profile).
* 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 {

Expand All @@ -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;
Expand Down Expand Up @@ -61,6 +62,20 @@ public HttpAuthDatabase getHttpAuthDatabase(Context context) {
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()
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,8 @@ public AwContents(AwBrowserContext browserContext, ViewGroup containerView,
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();
Expand Down Expand Up @@ -1368,6 +1369,15 @@ public void clearSslPreferences() {
mContentViewCore.clearSslPreferences();
}

/**
* @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.
Expand Down Expand Up @@ -2140,4 +2150,5 @@ private native void nativeTrimMemoryOnRenderThread(long nativeAwContents, int le

private native void nativeCreatePdfExporter(long nativeAwContents, AwPdfExporter awPdfExporter);

private native void nativeClearClientCertPreferences(long nativeAwContents);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
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
Expand Down Expand Up @@ -165,6 +167,12 @@ public abstract void onReceivedHttpAuthRequest(AwHttpAuthHandler handler,

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,21 @@

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.
Expand All @@ -20,14 +31,99 @@
*/
@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.
Expand Down Expand Up @@ -66,6 +162,43 @@ private void proceedSslError(boolean proceed, int id) {
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);
Expand Down Expand Up @@ -110,6 +243,8 @@ void cancelJsResult(int id) {
//--------------------------------------------------------------------------------------------
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);
Expand Down
Loading

0 comments on commit 353539b

Please sign in to comment.