diff options
author | davidben@chromium.org <davidben@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-05-14 00:51:09 +0000 |
---|---|---|
committer | davidben@chromium.org <davidben@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-05-14 00:51:09 +0000 |
commit | 981f6c4e7458664fdb9200fa4e1916b0389d70fd (patch) | |
tree | a447afbaba312f08d9ad7874720df2100e4dad6a /net/android | |
parent | b2a7a8052690922498512a6a45764e09b3b15da7 (diff) | |
download | chromium_src-981f6c4e7458664fdb9200fa4e1916b0389d70fd.zip chromium_src-981f6c4e7458664fdb9200fa4e1916b0389d70fd.tar.gz chromium_src-981f6c4e7458664fdb9200fa4e1916b0389d70fd.tar.bz2 |
Detect user-installed roots more efficiently on Android.
This makes assumptions about how Android stores system certificates on disk;
KeyStore's API is not sufficient to look up a trust anchor by subject and key.
BUG=361166
Review URL: https://codereview.chromium.org/228293012
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@270275 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'net/android')
-rw-r--r-- | net/android/java/src/org/chromium/net/X509Util.java | 138 |
1 files changed, 135 insertions, 3 deletions
diff --git a/net/android/java/src/org/chromium/net/X509Util.java b/net/android/java/src/org/chromium/net/X509Util.java index 5c478a2..c583465 100644 --- a/net/android/java/src/org/chromium/net/X509Util.java +++ b/net/android/java/src/org/chromium/net/X509Util.java @@ -13,14 +13,19 @@ 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; import java.io.ByteArrayInputStream; +import java.io.File; import java.io.IOException; import java.security.KeyStore; import java.security.KeyStoreException; +import java.security.MessageDigest; 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; @@ -28,11 +33,14 @@ import java.security.cert.CertificateNotYetValidException; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Collections; +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. @@ -134,6 +142,33 @@ public class X509Util { private static KeyStore sTestKeyStore; /** + * The system key store. This is used to determine whether a trust anchor is a system trust + * anchor or user-installed. + */ + private static KeyStore sSystemKeyStore; + + /** + * The directory where system certificates are stored. This is used to determine whether a + * trust anchor is a system trust anchor or user-installed. The KeyStore API alone is not + * sufficient to efficiently query whether a given X500Principal, PublicKey pair is a trust + * anchor. + */ + private static File sSystemCertificateDirectory; + + /** + * An in-memory cache of which trust anchors are system trust roots. This avoids reading and + * decoding the root from disk on every verification. Mirrors a similar in-memory cache in + * Conscrypt's X509TrustManager implementation. + */ + private static Set<Pair<X500Principal, PublicKey>> sSystemTrustAnchorCache; + + /** + * True if the system key store has been loaded. If the "AndroidCAStore" KeyStore instance + * was not found, sSystemKeyStore may be null while sLoadedSystemKeyStore is true. + */ + private static boolean sLoadedSystemKeyStore; + + /** * Lock object used to synchronize all calls that modify or depend on the trust managers. */ private static final Object sLock = new Object(); @@ -157,6 +192,27 @@ public class X509Util { if (sDefaultTrustManager == null) { sDefaultTrustManager = X509Util.createTrustManager(null); } + if (!sLoadedSystemKeyStore) { + try { + sSystemKeyStore = KeyStore.getInstance("AndroidCAStore"); + try { + sSystemKeyStore.load(null); + } catch (IOException e) { + // No IO operation is attempted. + } + sSystemCertificateDirectory = + new File(System.getenv("ANDROID_ROOT") + "/etc/security/cacerts"); + } catch (KeyStoreException e) { + // Could not load AndroidCAStore. Continue anyway; isKnownRoot will always + // return false. + } + if (!sDisableNativeCodeForTest) + nativeRecordCertVerifyCapabilitiesHistogram(sSystemKeyStore != null); + sLoadedSystemKeyStore = true; + } + if (sSystemTrustAnchorCache == null) { + sSystemTrustAnchorCache = new HashSet<Pair<X500Principal, PublicKey>>(); + } if (sTestKeyStore == null) { sTestKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); try { @@ -218,6 +274,7 @@ public class X509Util { private static void reloadDefaultTrustManager() throws KeyStoreException, NoSuchAlgorithmException, CertificateException { sDefaultTrustManager = null; + sSystemTrustAnchorCache = null; nativeNotifyKeyChainChanged(); ensureInitialized(); } @@ -256,6 +313,79 @@ public class X509Util { } } + private static final char[] HEX_DIGITS = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'a', 'b', 'c', 'd', 'e', 'f', + }; + + private static String hashPrincipal(X500Principal principal) throws NoSuchAlgorithmException { + // Android hashes a principal as the first four bytes of its MD5 digest, encoded in + // lowercase hex and reversed. Verified in 4.2, 4.3, and 4.4. + byte[] digest = MessageDigest.getInstance("MD5").digest(principal.getEncoded()); + char[] hexChars = new char[8]; + for (int i = 0; i < 4; i++) { + hexChars[2 * i] = HEX_DIGITS[(digest[3 - i] >> 4) & 0xf]; + hexChars[2 * i + 1] = HEX_DIGITS[digest[3 - i] & 0xf]; + } + return new String(hexChars); + } + + private static boolean isKnownRoot(X509Certificate root) + throws NoSuchAlgorithmException, KeyStoreException { + // Could not find the system key store. Conservatively report false. + if (sSystemKeyStore == null) + return false; + + // Check the in-memory cache first; avoid decoding the anchor from disk + // if it has been seen before. + Pair<X500Principal, PublicKey> key = + new Pair<X500Principal, PublicKey>(root.getSubjectX500Principal(), root.getPublicKey()); + if (sSystemTrustAnchorCache.contains(key)) + return true; + + // Note: It is not sufficient to call sSystemKeyStore.getCertificiateAlias. If the server + // supplies a copy of a trust anchor, X509TrustManagerExtensions returns the server's + // version rather than the system one. getCertificiateAlias will then fail to find an anchor + // name. This is fixed upstream in https://android-review.googlesource.com/#/c/91605/ + // + // TODO(davidben): When the change trickles into an Android release, query sSystemKeyStore + // directly. + + // System trust anchors are stored under a hash of the principal. In case of collisions, + // a number is appended. + String hash = hashPrincipal(root.getSubjectX500Principal()); + for (int i = 0; true; i++) { + String alias = hash + '.' + i; + if (!new File(sSystemCertificateDirectory, alias).exists()) + break; + + Certificate anchor = sSystemKeyStore.getCertificate("system:" + alias); + // It is possible for this to return null if the user deleted a trust anchor. In + // that case, the certificate remains in the system directory but is also added to + // another file. Continue iterating as there may be further collisions after the + // deleted anchor. + if (anchor == null) + continue; + + if (!(anchor instanceof X509Certificate)) { + // This should never happen. + String className = anchor.getClass().getName(); + Log.e(TAG, "Anchor " + alias + " not an X509Certificate: " + className); + continue; + } + + // If the subject and public key match, this is a system root. + X509Certificate anchorX509 = (X509Certificate)anchor; + if (root.getSubjectX500Principal().equals(anchorX509.getSubjectX500Principal()) && + root.getPublicKey().equals(anchorX509.getPublicKey())) { + sSystemTrustAnchorCache.add(key); + return true; + } + } + + return false; + } + /** * If an EKU extension is present in the end-entity certificate, it MUST contain either the * anyEKU or serverAuth or netscapeSGC or Microsoft SGC EKUs. @@ -353,10 +483,12 @@ public class X509Util { } } - // TODO(davidben): This code was removed for - // http://crbug.com/361166. Fix the performance regression and - // export it again. boolean isIssuedByKnownRoot = false; + if (verifiedChain.size() > 0) { + X509Certificate root = verifiedChain.get(verifiedChain.size() - 1); + isIssuedByKnownRoot = isKnownRoot(root); + } + return new AndroidCertVerifyResult(CertVerifyStatusAndroid.VERIFY_OK, isIssuedByKnownRoot, verifiedChain); } |