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); |
} |
} |