Chromium Code Reviews| Index: net/android/java/src/org/chromium/net/X509Util.java |
| diff --git a/net/android/java/src/org/chromium/net/X509Util.java b/net/android/java/src/org/chromium/net/X509Util.java |
| index 76adb899bdff33af717711f17fa445031a1de14a..788f226b922a54e5f41eda91e6cbe9ec1aa8191a 100644 |
| --- a/net/android/java/src/org/chromium/net/X509Util.java |
| +++ b/net/android/java/src/org/chromium/net/X509Util.java |
| @@ -8,8 +8,11 @@ import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| +import android.net.http.X509TrustManagerExtensions; |
| +import android.os.Build; |
| import android.security.KeyChain; |
| import android.util.Log; |
| +import android.util.Pair; |
| import org.chromium.base.JNINamespace; |
| @@ -18,23 +21,34 @@ import java.io.IOException; |
| import java.security.KeyStore; |
| import java.security.KeyStoreException; |
| import java.security.NoSuchAlgorithmException; |
| +import java.security.PublicKey; |
| +import java.security.cert.Certificate; |
| import java.security.cert.CertificateException; |
| import java.security.cert.CertificateExpiredException; |
| import java.security.cert.CertificateFactory; |
| import java.security.cert.CertificateNotYetValidException; |
| import java.security.cert.X509Certificate; |
| +import java.util.Arrays; |
| +import java.util.Collections; |
| +import java.util.Enumeration; |
| +import java.util.HashSet; |
| import java.util.List; |
| +import java.util.Set; |
| import javax.net.ssl.TrustManager; |
| import javax.net.ssl.TrustManagerFactory; |
| import javax.net.ssl.X509TrustManager; |
| +import javax.security.auth.x500.X500Principal; |
| +/** |
| + * Utility functions for verifying X.509 certificates. |
| + */ |
| @JNINamespace("net") |
| public class X509Util { |
| private static final String TAG = "X509Util"; |
| - public static final class TrustStorageListener extends BroadcastReceiver { |
| + private static final class TrustStorageListener extends BroadcastReceiver { |
| @Override public void onReceive(Context context, Intent intent) { |
| if (intent.getAction().equals(KeyChain.ACTION_STORAGE_CHANGED)) { |
| try { |
| @@ -53,6 +67,49 @@ public class X509Util { |
| } |
| } |
| + /** |
| + * Interface that wraps one of X509TrustManager or |
| + * X509TrustManagerExtensions to support platforms before the latter was |
| + * added. |
| + */ |
| + private static interface X509TrustManagerImplementation { |
| + public List<X509Certificate> checkServerTrusted(X509Certificate[] chain, |
| + String authType, |
| + String host) throws CertificateException; |
| + } |
| + |
| + private static final class X509TrustManagerIceCreamSandwich implements |
| + X509TrustManagerImplementation { |
| + private final X509TrustManager mTrustManager; |
| + |
| + public X509TrustManagerIceCreamSandwich(X509TrustManager trustManager) { |
| + mTrustManager = trustManager; |
| + } |
| + |
| + @Override |
| + public List<X509Certificate> checkServerTrusted(X509Certificate[] chain, |
| + String authType, |
| + String host) throws CertificateException { |
| + mTrustManager.checkServerTrusted(chain, authType); |
| + return Collections.<X509Certificate>emptyList(); |
| + } |
| + } |
| + |
| + private static final class X509TrustManagerJellyBean implements X509TrustManagerImplementation { |
| + private final X509TrustManagerExtensions mTrustManagerExtensions; |
| + |
| + public X509TrustManagerJellyBean(X509TrustManager trustManager) { |
| + mTrustManagerExtensions = new X509TrustManagerExtensions(trustManager); |
| + } |
| + |
| + @Override |
| + public List<X509Certificate> checkServerTrusted(X509Certificate[] chain, |
| + String authType, |
| + String host) throws CertificateException { |
| + return mTrustManagerExtensions.checkServerTrusted(chain, authType, host); |
| + } |
| + } |
| + |
| private static CertificateFactory sCertificateFactory; |
| private static final String OID_TLS_SERVER_AUTH = "1.3.6.1.5.5.7.3.1"; |
| @@ -66,7 +123,7 @@ public class X509Util { |
| /** |
| * Trust manager backed up by the read-only system certificate store. |
| */ |
| - private static X509TrustManager sDefaultTrustManager; |
| + private static X509TrustManagerImplementation sDefaultTrustManager; |
| /** |
| * BroadcastReceiver that listens to change in the system keystore to invalidate certificate |
| @@ -78,10 +135,23 @@ public class X509Util { |
| * Trust manager backed up by a custom certificate store. We need such manager to plant test |
| * root CA to the trust store in testing. |
| */ |
| - private static X509TrustManager sTestTrustManager; |
| + private static X509TrustManagerImplementation sTestTrustManager; |
| private static KeyStore sTestKeyStore; |
| /** |
| + * Hash set of the subject and public key of system roots. This is used to |
| + * determine whether a chain ends at a well-known root or not. |
| + * |
| + * Querying the system KeyStore for the root directly doesn't work as the |
| + * root of the verified chain may be the server's version of a root rather |
| + * than the system one. For instance, the server may send a certificate |
| + * signed by another CA, while the system store contains a self-signed root |
| + * with the same subject and SPKI. The chain will terminate at that root |
| + * but X509TrustManagerExtensions will return the server's version. |
| + */ |
| + private static Set<Pair<X500Principal, PublicKey>> sSystemTrustRoots; |
| + |
| + /** |
| * Lock object used to synchronize all calls that modify or depend on the trust managers. |
| */ |
| private static final Object sLock = new Object(); |
| @@ -105,6 +175,9 @@ public class X509Util { |
| if (sDefaultTrustManager == null) { |
| sDefaultTrustManager = X509Util.createTrustManager(null); |
| } |
| + if (sSystemTrustRoots == null) { |
| + sSystemTrustRoots = buildSystemTrustRootSet(); |
| + } |
| if (sTestKeyStore == null) { |
| sTestKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); |
| try { |
| @@ -125,20 +198,56 @@ public class X509Util { |
| } |
| } |
| + private static Set<Pair<X500Principal, PublicKey>> buildSystemTrustRootSet() throws |
| + CertificateException, KeyStoreException, NoSuchAlgorithmException { |
| + // Load the Android CA store. |
| + KeyStore systemKeyStore = KeyStore.getInstance("AndroidCAStore"); |
|
joth
2014/01/18 00:23:13
Note "AndroidCAStore" is really an internal detail
davidben
2014/01/21 17:28:57
Oh hrm. I'll defer to Ryan, but defaulting to fals
|
| + try { |
| + systemKeyStore.load(null); |
| + } catch (IOException e) { |
| + // No IO operation is attempted. |
| + } |
| + |
| + // System trust roots have prefix of "system:". |
| + Set<Pair<X500Principal, PublicKey>> roots = new HashSet<Pair<X500Principal, PublicKey>>(); |
| + Enumeration<String> aliases = systemKeyStore.aliases(); |
| + while (aliases.hasMoreElements()) { |
| + String alias = aliases.nextElement(); |
| + if (!alias.startsWith("system:")) |
| + continue; |
| + Certificate cert = systemKeyStore.getCertificate(alias); |
| + if (cert != null && cert instanceof X509Certificate) { |
| + X509Certificate x509Cert = (X509Certificate)cert; |
| + roots.add(new Pair<X500Principal, PublicKey>(x509Cert.getSubjectX500Principal(), |
| + x509Cert.getPublicKey())); |
| + } |
| + } |
| + return roots; |
| + } |
| + |
| /** |
| - * Creates a X509TrustManager backed up by the given key store. When null is passed as a key |
| - * store, system default trust store is used. |
| + * Creates a X509TrustManagerImplementation backed up by the given key |
| + * store. When null is passed as a key store, system default trust store is |
| + * used. |
| * @throws KeyStoreException, NoSuchAlgorithmException on error initializing the TrustManager. |
| */ |
| - private static X509TrustManager createTrustManager(KeyStore keyStore) throws KeyStoreException, |
| - NoSuchAlgorithmException { |
| + private static X509TrustManagerImplementation createTrustManager(KeyStore keyStore) throws |
| + KeyStoreException, NoSuchAlgorithmException { |
| String algorithm = TrustManagerFactory.getDefaultAlgorithm(); |
| TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm); |
| tmf.init(keyStore); |
| for (TrustManager tm : tmf.getTrustManagers()) { |
| if (tm instanceof X509TrustManager) { |
| - return (X509TrustManager) tm; |
| + try { |
| + if (Build.VERSION.SDK_INT >= 17) { |
|
joth
2014/01/18 00:23:13
nit: you can use Build.VERSION_CODES.JELLY_BEAN in
|
| + return new X509TrustManagerJellyBean((X509TrustManager) tm); |
| + } else { |
| + return new X509TrustManagerIceCreamSandwich((X509TrustManager) tm); |
| + } |
| + } catch (IllegalArgumentException e) { |
| + Log.e(TAG, "Error creating trust manager: " + e); |
| + } |
| } |
| } |
| return null; |
| @@ -158,6 +267,7 @@ public class X509Util { |
| private static void reloadDefaultTrustManager() throws KeyStoreException, |
| NoSuchAlgorithmException, CertificateException { |
| sDefaultTrustManager = null; |
| + sSystemTrustRoots = null; |
| nativeNotifyKeyChainChanged(); |
| ensureInitialized(); |
| } |
| @@ -231,17 +341,20 @@ public class X509Util { |
| return false; |
| } |
| - public static int verifyServerCertificates(byte[][] certChain, String authType) |
| + public static AndroidCertVerifyResult verifyServerCertificates(byte[][] certChain, |
| + String authType, |
| + String host) |
| throws KeyStoreException, NoSuchAlgorithmException { |
| if (certChain == null || certChain.length == 0 || certChain[0] == null) { |
| throw new IllegalArgumentException("Expected non-null and non-empty certificate " + |
| - "chain passed as |certChain|. |certChain|=" + certChain); |
| + "chain passed as |certChain|. |certChain|=" + Arrays.deepToString(certChain)); |
| } |
| + |
| try { |
| ensureInitialized(); |
| } catch (CertificateException e) { |
| - return CertVerifyResultAndroid.VERIFY_FAILED; |
| + return new AndroidCertVerifyResult(CertVerifyStatusAndroid.VERIFY_FAILED); |
| } |
| X509Certificate[] serverCertificates = new X509Certificate[certChain.length]; |
| @@ -250,7 +363,7 @@ public class X509Util { |
| serverCertificates[i] = createCertificateFromBytes(certChain[i]); |
| } |
| } catch (CertificateException e) { |
| - return CertVerifyResultAndroid.VERIFY_UNABLE_TO_PARSE; |
| + return new AndroidCertVerifyResult(CertVerifyStatusAndroid.VERIFY_UNABLE_TO_PARSE); |
| } |
| // Expired and not yet valid certificates would be rejected by the trust managers, but the |
| @@ -259,32 +372,47 @@ public class X509Util { |
| // separately. |
| try { |
| serverCertificates[0].checkValidity(); |
| - if (!verifyKeyUsage(serverCertificates[0])) |
| - return CertVerifyResultAndroid.VERIFY_INCORRECT_KEY_USAGE; |
| + if (!verifyKeyUsage(serverCertificates[0])) { |
| + return new AndroidCertVerifyResult( |
| + CertVerifyStatusAndroid.VERIFY_INCORRECT_KEY_USAGE); |
| + } |
| } catch (CertificateExpiredException e) { |
| - return CertVerifyResultAndroid.VERIFY_EXPIRED; |
| + return new AndroidCertVerifyResult(CertVerifyStatusAndroid.VERIFY_EXPIRED); |
| } catch (CertificateNotYetValidException e) { |
| - return CertVerifyResultAndroid.VERIFY_NOT_YET_VALID; |
| + return new AndroidCertVerifyResult(CertVerifyStatusAndroid.VERIFY_NOT_YET_VALID); |
| } catch (CertificateException e) { |
| - return CertVerifyResultAndroid.VERIFY_FAILED; |
| + return new AndroidCertVerifyResult(CertVerifyStatusAndroid.VERIFY_FAILED); |
| } |
| synchronized (sLock) { |
| + List<X509Certificate> verifiedChain; |
| try { |
| - sDefaultTrustManager.checkServerTrusted(serverCertificates, authType); |
| - return CertVerifyResultAndroid.VERIFY_OK; |
| + verifiedChain = sDefaultTrustManager.checkServerTrusted(serverCertificates, |
| + authType, host); |
| } catch (CertificateException eDefaultManager) { |
| try { |
| - sTestTrustManager.checkServerTrusted(serverCertificates, authType); |
| - return CertVerifyResultAndroid.VERIFY_OK; |
| + verifiedChain = sTestTrustManager.checkServerTrusted(serverCertificates, |
| + authType, host); |
| } catch (CertificateException eTestManager) { |
| // Neither of the trust managers confirms the validity of the certificate chain, |
| // log the error message returned by the system trust manager. |
| Log.i(TAG, "Failed to validate the certificate chain, error: " + |
| eDefaultManager.getMessage()); |
| - return CertVerifyResultAndroid.VERIFY_NO_TRUSTED_ROOT; |
| + return new AndroidCertVerifyResult( |
| + CertVerifyStatusAndroid.VERIFY_NO_TRUSTED_ROOT); |
| } |
| } |
| + |
| + boolean isIssuedByKnownRoot = false; |
| + if (verifiedChain.size() > 0) { |
| + X509Certificate root = verifiedChain.get(verifiedChain.size() - 1); |
| + isIssuedByKnownRoot = sSystemTrustRoots.contains( |
| + new Pair<X500Principal, PublicKey>(root.getSubjectX500Principal(), |
| + root.getPublicKey())); |
| + } |
| + |
| + return new AndroidCertVerifyResult(CertVerifyStatusAndroid.VERIFY_OK, |
| + isIssuedByKnownRoot, verifiedChain); |
| } |
| } |