Add support for client side SSL certificates

This introduces the ability for clients (i.e. the exchange service) to
register "special connection types" that use a client certificate stored
in the system keystore. The alias is encoded into the URI scheme for
those clients, and the socket factory used for those connections will
use the approprate KeyManager.

Lots of TODO's, including bubbling a lot of this up to the higher level
and wiring the UI to actually set the alias in the HostAuth table.

Change-Id: If5e1901c5b58731fdabd3e6b6da7198134b512d2
This commit is contained in:
Ben Komalo 2011-05-04 10:15:35 -07:00
parent e57a83d39a
commit 78959916e7
3 changed files with 276 additions and 7 deletions

View File

@ -70,6 +70,9 @@
<uses-permission
android:name="com.android.email.permission.READ_ATTACHMENT"/>
<uses-permission
android:name="android.permission.USE_CREDENTIALS"/>
<!-- Grant permission to system apps to access provider (see provider below) -->
<permission
android:name="com.android.email.permission.ACCESS_PROVIDER"

View File

@ -0,0 +1,131 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.emailcommon.utility;
import com.android.emailcommon.Logging;
import com.android.emailcommon.utility.SSLUtils.KeyChainKeyManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.HttpParams;
import android.content.Context;
import android.net.SSLCertificateSocketFactory;
import android.util.Log;
import javax.net.ssl.KeyManager;
/**
* A thread-safe client connection manager that manages the use of client certificates from the
* {@link android.security.KeyChain} for SSL connections.
*/
public class EmailClientConnectionManager extends ThreadSafeClientConnManager {
private static final boolean LOG_ENABLED = false;
/**
* Not publicly instantiable except via {@link #newInstance(HttpParams)}
*/
private EmailClientConnectionManager(HttpParams params, SchemeRegistry registry) {
super(params, registry);
}
public static EmailClientConnectionManager newInstance(HttpParams params) {
// Create a registry for our three schemes; http and https will use built-in factories
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http",
PlainSocketFactory.getSocketFactory(), 80));
registry.register(new Scheme("https", SSLUtils.getHttpSocketFactory(false), 443));
// Register the httpts scheme with our insecure factory
registry.register(new Scheme("httpts",
SSLUtils.getHttpSocketFactory(true /*insecure*/), 443));
return new EmailClientConnectionManager(params, registry);
}
/**
* Ensures that a client SSL certificate is known to be used for the specified connection
* manager.
* A {@link SchemeRegistry} is used to denote which client certificates to use for a given
* connection, so clients of this connection manager should use
* {@link #makeSchemeForClientCert(String, boolean)}.
*/
public synchronized void registerClientCert(
Context context, String clientCertAlias, boolean trustAllServerCerts) {
SchemeRegistry registry = getSchemeRegistry();
String schemeName = makeSchemeForClientCert(clientCertAlias, trustAllServerCerts);
Scheme existing = registry.get(schemeName);
if (existing == null) {
if (LOG_ENABLED) {
Log.i(Logging.LOG_TAG, "Registering socket factory for certificate alias ["
+ clientCertAlias + "]");
}
KeyManager keyManager = KeyChainKeyManager.fromAlias(context, clientCertAlias);
if (keyManager == null) {
// TODO: handle failing to retrieve credentials from the keystore.
Log.e(Logging.LOG_TAG, "Unable to retrieve credentials for alias ["
+ clientCertAlias + "]");
return;
}
SSLCertificateSocketFactory underlying = SSLUtils.getSSLSocketFactory(
trustAllServerCerts);
underlying.setKeyManagers(new KeyManager[] { keyManager });
registry.register(new Scheme(schemeName, new SSLSocketFactory(underlying), 443));
}
}
/**
* Unregisters a custom connection type that uses a client certificate on the connection
* manager.
* @see #registerClientCert(Context, String, boolean)
*/
public synchronized void unregisterClientCert(
String clientCertAlias, boolean trustAllServerCerts) {
SchemeRegistry registry = getSchemeRegistry();
String schemeName = makeSchemeForClientCert(clientCertAlias, trustAllServerCerts);
Scheme existing = registry.get(schemeName);
if (existing != null) {
registry.unregister(schemeName);
}
}
/**
* Builds a custom scheme name to be used in a connection manager according to the connection
* parameters.
*/
public static String makeScheme(
boolean useSsl, boolean trustAllServerCerts, String clientCertAlias) {
if (clientCertAlias != null) {
return makeSchemeForClientCert(clientCertAlias, trustAllServerCerts);
} else {
return useSsl ? (trustAllServerCerts ? "httpts" : "https") : "http";
}
}
/**
* Builds a unique scheme name for an SSL connection that uses a client user certificate.
*/
private static String makeSchemeForClientCert(
String clientCertAlias, boolean trustAllServerCerts) {
String safeAlias = SSLUtils.escapeForSchemeName(clientCertAlias);
return (trustAllServerCerts ? "httpts" : "https") + "+clientCert+" + safeAlias;
}
}

View File

@ -16,33 +16,62 @@
package com.android.emailcommon.utility;
import android.content.Context;
import android.net.SSLCertificateSocketFactory;
import android.security.KeyChain;
import android.security.KeyChainException;
import android.util.Log;
import javax.net.ssl.SSLSocketFactory;
import java.net.Socket;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import javax.net.ssl.KeyManager;
import javax.net.ssl.X509ExtendedKeyManager;
public class SSLUtils {
private static SSLSocketFactory sInsecureFactory;
private static SSLSocketFactory sSecureFactory;
private static SSLCertificateSocketFactory sInsecureFactory;
private static SSLCertificateSocketFactory sSecureFactory;
private static final boolean LOG_ENABLED = false;
private static final String TAG = "Email.Ssl";
/**
* Returns a {@link SSLSocketFactory}. Optionally bypass all SSL certificate checks.
* Returns a {@link javax.net.ssl.SSLSocketFactory}.
* Optionally bypass all SSL certificate checks.
*
* @param insecure if true, bypass all SSL certificate checks
*/
public synchronized static final SSLSocketFactory getSSLSocketFactory(boolean insecure) {
public synchronized static final SSLCertificateSocketFactory getSSLSocketFactory(
boolean insecure) {
if (insecure) {
if (sInsecureFactory == null) {
sInsecureFactory = SSLCertificateSocketFactory.getInsecure(0, null);
sInsecureFactory = (SSLCertificateSocketFactory)
SSLCertificateSocketFactory.getInsecure(0, null);
}
return sInsecureFactory;
} else {
if (sSecureFactory == null) {
sSecureFactory = SSLCertificateSocketFactory.getDefault(0, null);
sSecureFactory = (SSLCertificateSocketFactory)
SSLCertificateSocketFactory.getDefault(0, null);
}
return sSecureFactory;
}
}
/**
* Returns a {@link org.apache.http.conn.ssl.SSLSocketFactory SSLSocketFactory} for use with the
* Apache HTTP stack.
*/
public static SSLSocketFactory getHttpSocketFactory(boolean insecure) {
SSLCertificateSocketFactory underlying = getSSLSocketFactory(insecure);
// TODO: register a keymanager that will simply listen for requests for a client
// certificate so that higher levels know to ask the user for such credentials.
return new SSLSocketFactory(underlying);
}
/**
* Escapes the contents a string to be used as a safe scheme name in the URI according to
* http://tools.ietf.org/html/rfc3986#section-3.1
@ -70,4 +99,110 @@ public class SSLUtils {
}
return sb.toString();
}
@SuppressWarnings("unused")
private static abstract class StubKeyManager extends X509ExtendedKeyManager {
@Override public abstract String chooseClientAlias(
String[] keyTypes, Principal[] issuers, Socket socket);
@Override public abstract X509Certificate[] getCertificateChain(String alias);
@Override public abstract PrivateKey getPrivateKey(String alias);
// The following methods are unused.
@Override
public final String chooseServerAlias(
String keyType, Principal[] issuers, Socket socket) {
// not a client SSLSocket callback
throw new UnsupportedOperationException();
}
@Override
public final String[] getClientAliases(String keyType, Principal[] issuers) {
// not a client SSLSocket callback
throw new UnsupportedOperationException();
}
@Override
public final String[] getServerAliases(String keyType, Principal[] issuers) {
// not a client SSLSocket callback
throw new UnsupportedOperationException();
}
}
/**
* A {@link KeyManager} that reads uses credentials stored in the system {@link KeyChain}.
*/
public static class KeyChainKeyManager extends StubKeyManager {
private final String mClientAlias;
private final X509Certificate[] mCertificateChain;
private final PrivateKey mPrivateKey;
/**
* Builds an instance of a KeyChainKeyManager using the given certificate alias.
* If for any reason retrieval of the credentials from the system {@link KeyChain} fails,
* a {@code null} value will be returned.
*/
public static KeyChainKeyManager fromAlias(Context context, String alias) {
X509Certificate[] certificateChain;
try {
certificateChain = KeyChain.getCertificateChain(context, alias);
} catch (KeyChainException e) {
Log.e(TAG, "Unable to retrieve certificate chain for [" + alias + "] due to "
+ e);
return null;
} catch (InterruptedException e) {
Log.e(TAG, "Unable to retrieve certificate chain for [" + alias + "] due to "
+ e);
return null;
}
PrivateKey privateKey;
try {
privateKey = KeyChain.getPrivateKey(context, alias);
} catch (KeyChainException e) {
Log.e(TAG, "Unable to retrieve private key for [" + alias + "] due to " + e);
return null;
} catch (InterruptedException e) {
Log.e(TAG, "Unable to retrieve private key for [" + alias + "] due to " + e);
return null;
}
return new KeyChainKeyManager(alias, certificateChain, privateKey);
}
private KeyChainKeyManager(
String clientAlias, X509Certificate[] certificateChain, PrivateKey privateKey) {
mClientAlias = clientAlias;
mCertificateChain = certificateChain;
mPrivateKey = privateKey;
}
@Override
public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
if (LOG_ENABLED) {
Log.i(TAG, "Requesting a client cert alias for " + Arrays.toString(keyTypes));
}
return mClientAlias;
}
@Override
public X509Certificate[] getCertificateChain(String alias) {
if (LOG_ENABLED) {
Log.i(TAG, "Requesting a client certificate chain for alias [" + alias + "]");
}
return mCertificateChain;
}
@Override
public PrivateKey getPrivateKey(String alias) {
if (LOG_ENABLED) {
Log.i(TAG, "Requesting a client private key for alias [" + alias + "]");
}
return mPrivateKey;
}
}
}