diff --git a/library/java/org/chromium/net/AndroidCertVerifyResult.java b/library/java/org/chromium/net/AndroidCertVerifyResult.java new file mode 100644 index 0000000000..ab11038e94 --- /dev/null +++ b/library/java/org/chromium/net/AndroidCertVerifyResult.java @@ -0,0 +1,63 @@ +package org.chromium.net; + +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * The result of a certification verification. + */ +public final class AndroidCertVerifyResult { + + /** + * The verification status. One of the values in CertVerifyStatusAndroid. + */ + private final int mStatus; + + /** + * True if the root CA in the chain is in the system store. + */ + private final boolean mIsIssuedByKnownRoot; + + /** + * The properly ordered certificate chain used for verification. + */ + private final List mCertificateChain; + + public AndroidCertVerifyResult(int status, boolean isIssuedByKnownRoot, + List certificateChain) { + mStatus = status; + mIsIssuedByKnownRoot = isIssuedByKnownRoot; + mCertificateChain = new ArrayList(certificateChain); + } + + public AndroidCertVerifyResult(int status) { + mStatus = status; + mIsIssuedByKnownRoot = false; + mCertificateChain = Collections.emptyList(); + } + + // TODO(stefanoduo): Hook envoy-mobile JNI. + //@CalledByNative + public int getStatus() { return mStatus; } + + // TODO(stefanoduo): Hook envoy-mobile JNI. + //@CalledByNative + public boolean isIssuedByKnownRoot() { return mIsIssuedByKnownRoot; } + + // TODO(stefanoduo): Hook envoy-mobile JNI. + //@CalledByNative + public byte[][] getCertificateChainEncoded() { + byte[][] verifiedChainArray = new byte[mCertificateChain.size()][]; + try { + for (int i = 0; i < mCertificateChain.size(); i++) { + verifiedChainArray[i] = mCertificateChain.get(i).getEncoded(); + } + } catch (CertificateEncodingException e) { + return new byte[0][]; + } + return verifiedChainArray; + } +} diff --git a/library/java/org/chromium/net/AndroidNetworkLibrary.java b/library/java/org/chromium/net/AndroidNetworkLibrary.java new file mode 100644 index 0000000000..4fe6909dfd --- /dev/null +++ b/library/java/org/chromium/net/AndroidNetworkLibrary.java @@ -0,0 +1,97 @@ +package org.chromium.net; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.TrafficStats; +import android.net.TransportInfo; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.telephony.TelephonyManager; +import android.util.Log; + +import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.net.SocketImpl; +import java.net.URLConnection; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.Enumeration; +import java.util.List; + +/** + * This class implements net utilities required by the net component. + */ +public final class AndroidNetworkLibrary { + private static final String TAG = "AndroidNetworkLibrary"; + + /** + * Validate the server's certificate chain is trusted. Note that the caller + * must still verify the name matches that of the leaf certificate. + * + * @param certChain The ASN.1 DER encoded bytes for certificates. + * @param authType The key exchange algorithm name (e.g. RSA). + * @param host The hostname of the server. + * @return Android certificate verification result code. + */ + // TODO(stefanoduo): Hook envoy-mobile JNI. + //@CalledByNative + public static AndroidCertVerifyResult verifyServerCertificates(byte[][] certChain, + String authType, String host) { + try { + return X509Util.verifyServerCertificates(certChain, authType, host); + } catch (KeyStoreException e) { + return new AndroidCertVerifyResult(CertVerifyStatusAndroid.FAILED); + } catch (NoSuchAlgorithmException e) { + return new AndroidCertVerifyResult(CertVerifyStatusAndroid.FAILED); + } catch (IllegalArgumentException e) { + return new AndroidCertVerifyResult(CertVerifyStatusAndroid.FAILED); + } + } + + /** + * Adds a test root certificate to the local trust store. + * @param rootCert DER encoded bytes of the certificate. + */ + // TODO(stefanoduo): Hook envoy-mobile JNI. + //@CalledByNativeUnchecked + public static void addTestRootCertificate(byte[] rootCert) + throws CertificateException, KeyStoreException, NoSuchAlgorithmException { + X509Util.addTestRootCertificate(rootCert); + } + + /** + * Removes all test root certificates added by |addTestRootCertificate| calls from the local + * trust store. + */ + // TODO(stefanoduo): Hook envoy-mobile JNI. + //@CalledByNativeUnchecked + public static void clearTestRootCertificates() + throws NoSuchAlgorithmException, CertificateException, KeyStoreException { + X509Util.clearTestRootCertificates(); + } +} diff --git a/library/java/org/chromium/net/CertVerifyStatusAndroid.java b/library/java/org/chromium/net/CertVerifyStatusAndroid.java new file mode 100644 index 0000000000..2757102bdf --- /dev/null +++ b/library/java/org/chromium/net/CertVerifyStatusAndroid.java @@ -0,0 +1,43 @@ +package org.chromium.net; + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@IntDef({CertVerifyStatusAndroid.OK, CertVerifyStatusAndroid.FAILED, + CertVerifyStatusAndroid.NO_TRUSTED_ROOT, CertVerifyStatusAndroid.EXPIRED, + CertVerifyStatusAndroid.NOT_YET_VALID, CertVerifyStatusAndroid.UNABLE_TO_PARSE, + CertVerifyStatusAndroid.INCORRECT_KEY_USAGE}) +@Retention(RetentionPolicy.SOURCE) +public @interface CertVerifyStatusAndroid { + /** + * Certificate is trusted. + */ + int OK = 0; + /** + * Certificate verification could not be conducted. + */ + int FAILED = -1; + /** + * Certificate is not trusted due to non-trusted root of the certificate chain. + */ + int NO_TRUSTED_ROOT = -2; + /** + * Certificate is not trusted because it has expired. + */ + int EXPIRED = -3; + /** + * Certificate is not trusted because it is not valid yet. + */ + int NOT_YET_VALID = -4; + /** + * Certificate is not trusted because it could not be parsed. + */ + int UNABLE_TO_PARSE = -5; + /** + * Certificate is not trusted because it has an extendedKeyUsage field, but its value is not + * correct for a web server. + */ + int INCORRECT_KEY_USAGE = -6; +} diff --git a/library/java/org/chromium/net/ContextUtils.java b/library/java/org/chromium/net/ContextUtils.java new file mode 100644 index 0000000000..c240df4589 --- /dev/null +++ b/library/java/org/chromium/net/ContextUtils.java @@ -0,0 +1,166 @@ +package org.chromium.net; + +import android.app.Activity; +import android.app.Application; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.res.AssetManager; +import android.os.Build; +import android.os.Handler; +import android.os.Process; +import android.preference.PreferenceManager; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +/** + * This class provides Android application context related utility methods. + */ +public final class ContextUtils { + private static final String TAG = "ContextUtils"; + private static Context sApplicationContext; + + private static boolean sSdkSandboxProcess; + + /** + * Flag for {@link Context#registerReceiver}: The receiver can receive broadcasts from other + * Apps. Has the same behavior as marking a statically registered receiver with "exported=true". + * + * TODO(mthiesse): Move to ApiHelperForT when we build against T SDK. + */ + public static final int RECEIVER_EXPORTED = 0x2; + public static final int RECEIVER_NOT_EXPORTED = 0x4; + + /** + * Initialization-on-demand holder. This exists for thread-safe lazy initialization. + */ + private static class Holder { + // Not final for tests. + private static SharedPreferences sSharedPreferences = fetchAppSharedPreferences(); + } + + /** + * Get the Android application context. + * + * Under normal circumstances there is only one application context in a process, so it's safe + * to treat this as a global. In WebView it's possible for more than one app using WebView to be + * running in a single process, but this mechanism is rarely used and this is not the only + * problem in that scenario, so we don't currently forbid using it as a global. + * + * Do not downcast the context returned by this method to Application (or any subclass). It may + * not be an Application object; it may be wrapped in a ContextWrapper. The only assumption you + * may make is that it is a Context whose lifetime is the same as the lifetime of the process. + */ + public static Context getApplicationContext() { return sApplicationContext; } + + /** + * Initializes the java application context. + * + * This should be called exactly once early on during startup, before native is loaded and + * before any other clients make use of the application context through this class. + * + * @param appContext The application context. + */ + public static void initApplicationContext(Context appContext) { + // Conceding that occasionally in tests, native is loaded before the browser process is + // started, in which case the browser process re-sets the application context. + assert sApplicationContext == null || sApplicationContext == appContext || + ((ContextWrapper)sApplicationContext).getBaseContext() == appContext; + initJavaSideApplicationContext(appContext); + } + + /** + * Only called by the static holder class and tests. + * + * @return The application-wide shared preferences. + */ + @SuppressWarnings("DefaultSharedPreferencesCheck") + private static SharedPreferences fetchAppSharedPreferences() { + // This may need to create the prefs directory if we've never used shared prefs before, so + // allow disk writes. This is rare but can happen if code used early in startup reads prefs. + try (StrictModeContext ignored = StrictModeContext.allowDiskWrites()) { + return PreferenceManager.getDefaultSharedPreferences(sApplicationContext); + } + } + + /** + * This is used to ensure that we always use the application context to fetch the default shared + * preferences. This avoids needless I/O for android N and above. It also makes it clear that + * the app-wide shared preference is desired, rather than the potentially context-specific one. + * + * @return application-wide shared preferences. + */ + public static SharedPreferences getAppSharedPreferences() { return Holder.sSharedPreferences; } + + /** + * Occasionally tests cannot ensure the application context doesn't change between tests (junit) + * and sometimes specific tests has its own special needs, initApplicationContext should be used + * as much as possible, but this method can be used to override it. + * + * @param appContext The new application context. + */ + @VisibleForTesting + public static void initApplicationContextForTests(Context appContext) { + initJavaSideApplicationContext(appContext); + Holder.sSharedPreferences = fetchAppSharedPreferences(); + } + + /** + * Tests that use the applicationContext may unintentionally use the Context + * set by a previously run test. + */ + @VisibleForTesting + public static void clearApplicationContextForTests() { + sApplicationContext = null; + Holder.sSharedPreferences = null; + } + + private static void initJavaSideApplicationContext(Context appContext) { + assert appContext != null; + // Guard against anyone trying to downcast. + if (appContext instanceof Application) { + appContext = new ContextWrapper(appContext); + } + sApplicationContext = appContext; + } + + /** + * As to Exported V.S. NonExported receiver, please refer to + * https://developer.android.com/reference/android/content/Context#registerReceiver(android.content.BroadcastReceiver,%20android.content.IntentFilter,%20int) + */ + public static Intent registerExportedBroadcastReceiver(Context context, + BroadcastReceiver receiver, + IntentFilter filter, String permission) { + return registerBroadcastReceiver(context, receiver, filter, permission, /*scheduler=*/null, + RECEIVER_EXPORTED); + } + + public static Intent registerNonExportedBroadcastReceiver(Context context, + BroadcastReceiver receiver, + IntentFilter filter) { + return registerBroadcastReceiver(context, receiver, filter, /*permission=*/null, + /*scheduler=*/null, RECEIVER_NOT_EXPORTED); + } + + public static Intent registerNonExportedBroadcastReceiver(Context context, + BroadcastReceiver receiver, + IntentFilter filter, + Handler scheduler) { + return registerBroadcastReceiver(context, receiver, filter, /*permission=*/null, scheduler, + RECEIVER_NOT_EXPORTED); + } + + private static Intent registerBroadcastReceiver(Context context, BroadcastReceiver receiver, + IntentFilter filter, String permission, + Handler scheduler, int flags) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return context.registerReceiver(receiver, filter, permission, scheduler, flags); + } else { + return context.registerReceiver(receiver, filter, permission, scheduler); + } + } +} diff --git a/library/java/org/chromium/net/StrictModeContext.java b/library/java/org/chromium/net/StrictModeContext.java new file mode 100644 index 0000000000..e9bee7f615 --- /dev/null +++ b/library/java/org/chromium/net/StrictModeContext.java @@ -0,0 +1,106 @@ +package org.chromium.net; + +import android.os.Build; +import android.os.StrictMode; + +import java.io.Closeable; + +/** + * Enables try-with-resources compatible StrictMode violation allowlisting. + * + * Prefer "ignored" as the variable name to appease Android Studio's "Unused symbol" inspection. + * + * Example: + *
+ *     try (StrictModeContext ignored = StrictModeContext.allowDiskWrites()) {
+ *         return Example.doThingThatRequiresDiskWrites();
+ *     }
+ * 
+ * + */ +public final class StrictModeContext implements Closeable { + private final StrictMode.ThreadPolicy mThreadPolicy; + private final StrictMode.VmPolicy mVmPolicy; + + private StrictModeContext(StrictMode.ThreadPolicy threadPolicy, StrictMode.VmPolicy vmPolicy) { + mThreadPolicy = threadPolicy; + mVmPolicy = vmPolicy; + } + + private StrictModeContext(StrictMode.ThreadPolicy threadPolicy) { this(threadPolicy, null); } + + private StrictModeContext(StrictMode.VmPolicy vmPolicy) { this(null, vmPolicy); } + + /** + * Convenience method for disabling all VM-level StrictMode checks with try-with-resources. + * Includes everything listed here: + * https://developer.android.com/reference/android/os/StrictMode.VmPolicy.Builder.html + */ + public static StrictModeContext allowAllVmPolicies() { + StrictMode.VmPolicy oldPolicy = StrictMode.getVmPolicy(); + StrictMode.setVmPolicy(StrictMode.VmPolicy.LAX); + return new StrictModeContext(oldPolicy); + } + + /** + * Convenience method for disabling all thread-level StrictMode checks with try-with-resources. + * Includes everything listed here: + * https://developer.android.com/reference/android/os/StrictMode.ThreadPolicy.Builder.html + */ + public static StrictModeContext allowAllThreadPolicies() { + StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); + StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.LAX); + return new StrictModeContext(oldPolicy); + } + + /** + * Convenience method for disabling StrictMode for disk-writes with try-with-resources. + */ + public static StrictModeContext allowDiskWrites() { + StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); + return new StrictModeContext(oldPolicy); + } + + /** + * Convenience method for disabling StrictMode for disk-reads with try-with-resources. + */ + public static StrictModeContext allowDiskReads() { + StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); + return new StrictModeContext(oldPolicy); + } + + /** + * Convenience method for disabling StrictMode for slow calls with try-with-resources. + */ + public static StrictModeContext allowSlowCalls() { + StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); + StrictMode.setThreadPolicy( + new StrictMode.ThreadPolicy.Builder(oldPolicy).permitCustomSlowCalls().build()); + return new StrictModeContext(oldPolicy); + } + + /** + * Convenience method for disabling StrictMode for unbuffered input/output operations with + * try-with-resources. + * For API level 25- this method will do nothing; + * because StrictMode.ThreadPolicy.Builder#permitUnbufferedIo is added in API level 26. + */ + public static StrictModeContext allowUnbufferedIo() { + StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + StrictMode.setThreadPolicy( + new StrictMode.ThreadPolicy.Builder(oldPolicy).permitUnbufferedIo().build()); + } + return new StrictModeContext(oldPolicy); + } + + @Override + public void close() { + if (mThreadPolicy != null) { + StrictMode.setThreadPolicy(mThreadPolicy); + } + if (mVmPolicy != null) { + StrictMode.setVmPolicy(mVmPolicy); + } + } +} diff --git a/library/java/org/chromium/net/X509Util.java b/library/java/org/chromium/net/X509Util.java new file mode 100644 index 0000000000..dee264cfa8 --- /dev/null +++ b/library/java/org/chromium/net/X509Util.java @@ -0,0 +1,519 @@ +package org.chromium.net; + +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.Pair; + +import android.util.Log; + +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; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +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. + */ +public final class X509Util { + private static final String TAG = "X509Util"; + + private static final class TrustStorageListener extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + boolean shouldReloadTrustManager = false; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (KeyChain.ACTION_KEYCHAIN_CHANGED.equals(intent.getAction()) || + KeyChain.ACTION_TRUST_STORE_CHANGED.equals(intent.getAction())) { + // TODO(davidben): ACTION_KEYCHAIN_CHANGED indicates client certificates + // changed, not the trust store. The two signals within CertDatabase are + // identical, so we are reloading more than needed. But note b/36492171. + shouldReloadTrustManager = true; + } else if (KeyChain.ACTION_KEY_ACCESS_CHANGED.equals(intent.getAction()) && + !intent.getBooleanExtra(KeyChain.EXTRA_KEY_ACCESSIBLE, false)) { + // We lost access to a client certificate key. Reload all client certificate + // state as we are not currently able to forget an individual identity. + shouldReloadTrustManager = true; + } + } else { + @SuppressWarnings("deprecation") String action = KeyChain.ACTION_STORAGE_CHANGED; + // Before Android O, KeyChain only emitted a coarse-grained intent. This fires much + // more often than it should (https://crbug.com/381912), but there are no APIs to + // distinguish the various cases. + shouldReloadTrustManager = action.equals(intent.getAction()); + } + + if (shouldReloadTrustManager) { + try { + reloadDefaultTrustManager(); + } catch (CertificateException e) { + Log.e(TAG, "Unable to reload the default TrustManager", e); + } catch (KeyStoreException e) { + Log.e(TAG, "Unable to reload the default TrustManager", e); + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, "Unable to reload the default TrustManager", e); + } + } + } + } + + private static List + checkServerTrustedIgnoringRuntimeException(X509TrustManagerExtensions tm, X509Certificate[] chain, + String authType, String host) + throws CertificateException { + try { + return tm.checkServerTrusted(chain, authType, host); + } catch (RuntimeException e) { + // https://crbug.com/937354: checkServerTrusted() can unexpectedly throw runtime + // exceptions, most often within conscrypt while parsing certificates. + Log.e(TAG, "checkServerTrusted() unexpectedly threw: %s", e); + throw new CertificateException(e); + } + } + + private static CertificateFactory sCertificateFactory; + + private static final String OID_TLS_SERVER_AUTH = "1.3.6.1.5.5.7.3.1"; + private static final String OID_ANY_EKU = "2.5.29.37.0"; + // Server-Gated Cryptography (necessary to support a few legacy issuers): + // Netscape: + private static final String OID_SERVER_GATED_NETSCAPE = "2.16.840.1.113730.4.1"; + // Microsoft: + private static final String OID_SERVER_GATED_MICROSOFT = "1.3.6.1.4.1.311.10.3.3"; + + /** + * Trust manager backed up by the read-only system certificate store. + */ + private static X509TrustManagerExtensions sDefaultTrustManager; + + /** + * BroadcastReceiver that listens to change in the system keystore to invalidate certificate + * caches. + */ + private static TrustStorageListener sTrustStorageListener; + + /** + * 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 X509TrustManagerExtensions sTestTrustManager; + 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> 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(); + + /** + * Ensures that the trust managers and certificate factory are initialized. + */ + private static void ensureInitialized() + throws CertificateException, KeyStoreException, NoSuchAlgorithmException { + synchronized (sLock) { ensureInitializedLocked(); } + } + + /** + * Ensures that the trust managers and certificate factory are initialized. Must be called with + * |sLock| held. + */ + // FindBugs' static field initialization warnings do not handle methods that are expected to be + // called locked. + private static void ensureInitializedLocked() + throws CertificateException, KeyStoreException, NoSuchAlgorithmException { + assert Thread.holdsLock(sLock); + + if (sCertificateFactory == null) { + sCertificateFactory = CertificateFactory.getInstance("X.509"); + } + 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. + } + sLoadedSystemKeyStore = true; + } + if (sSystemTrustAnchorCache == null) { + sSystemTrustAnchorCache = new HashSet>(); + } + if (sTestKeyStore == null) { + sTestKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + try { + sTestKeyStore.load(null); + } catch (IOException e) { + // No IO operation is attempted. + } + } + if (sTestTrustManager == null) { + sTestTrustManager = X509Util.createTrustManager(sTestKeyStore); + } + if (sTrustStorageListener == null) { + sTrustStorageListener = new TrustStorageListener(); + IntentFilter filter = new IntentFilter(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + filter.addAction(KeyChain.ACTION_KEYCHAIN_CHANGED); + filter.addAction(KeyChain.ACTION_KEY_ACCESS_CHANGED); + filter.addAction(KeyChain.ACTION_TRUST_STORE_CHANGED); + } else { + @SuppressWarnings("deprecation") String action = KeyChain.ACTION_STORAGE_CHANGED; + filter.addAction(action); + } + ContextUtils.registerNonExportedBroadcastReceiver(ContextUtils.getApplicationContext(), + sTrustStorageListener, filter); + } + } + + /** + * Creates a X509TrustManagerExtensions backed up by the given key + * store. When null is passed as a key store, system default trust store is + * used. Returns null if no created TrustManager was suitable. + * @throws KeyStoreException, NoSuchAlgorithmException on error initializing the TrustManager. + */ + private static X509TrustManagerExtensions createTrustManager(KeyStore keyStore) + throws KeyStoreException, NoSuchAlgorithmException { + String algorithm = TrustManagerFactory.getDefaultAlgorithm(); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm); + tmf.init(keyStore); + + TrustManager[] trustManagers = null; + try { + trustManagers = tmf.getTrustManagers(); + } catch (RuntimeException e) { + // https://crbug.com/937354: getTrustManagers() can unexpectedly throw runtime + // exceptions, most often while processing the network security config XML file. + Log.e(TAG, "TrustManagerFactory.getTrustManagers() unexpectedly threw: %s", e); + throw new KeyStoreException(e); + } + + for (TrustManager tm : trustManagers) { + if (tm instanceof X509TrustManager) { + try { + return new X509TrustManagerExtensions((X509TrustManager)tm); + } catch (IllegalArgumentException e) { + String className = tm.getClass().getName(); + Log.e(TAG, "Error creating trust manager (" + className + "): " + e); + } + } + } + Log.e(TAG, "Could not find suitable trust manager"); + return null; + } + + /** + * After each modification of test key store, trust manager has to be generated again. + */ + private static void reloadTestTrustManager() throws KeyStoreException, NoSuchAlgorithmException { + assert Thread.holdsLock(sLock); + + sTestTrustManager = X509Util.createTrustManager(sTestKeyStore); + } + + /** + * After each modification by the system of the key store, trust manager has to be regenerated. + */ + private static void reloadDefaultTrustManager() + throws KeyStoreException, NoSuchAlgorithmException, CertificateException { + synchronized (sLock) { + sDefaultTrustManager = null; + sSystemTrustAnchorCache = null; + ensureInitializedLocked(); + } + // TODO(stefanoduo): Hook envoy-mobile JNI + // X509UtilJni.get().notifyKeyChainChanged(); + } + + /** + * Convert a DER encoded certificate to an X509Certificate. + */ + public static X509Certificate createCertificateFromBytes(byte[] derBytes) + throws CertificateException, KeyStoreException, NoSuchAlgorithmException { + ensureInitialized(); + return (X509Certificate)sCertificateFactory.generateCertificate( + new ByteArrayInputStream(derBytes)); + } + + public static void addTestRootCertificate(byte[] rootCertBytes) + throws CertificateException, KeyStoreException, NoSuchAlgorithmException { + ensureInitialized(); + X509Certificate rootCert = createCertificateFromBytes(rootCertBytes); + synchronized (sLock) { + sTestKeyStore.setCertificateEntry("root_cert_" + Integer.toString(sTestKeyStore.size()), + rootCert); + reloadTestTrustManager(); + } + } + + public static void clearTestRootCertificates() + throws NoSuchAlgorithmException, CertificateException, KeyStoreException { + ensureInitialized(); + synchronized (sLock) { + try { + sTestKeyStore.load(null); + reloadTestTrustManager(); + } catch (IOException e) { + // No IO operation is attempted. + } + } + } + + 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 { + assert Thread.holdsLock(sLock); + + // 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 key = + new Pair(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. + * + * @return true if there is no EKU extension or if any of the EKU extensions is one of the valid + * OIDs for web server certificates. + * + * TODO(palmer): This can be removed after the equivalent change is made to the Android default + * TrustManager and that change is shipped to a large majority of Android users. + */ + static boolean verifyKeyUsage(X509Certificate certificate) throws CertificateException { + List ekuOids; + try { + ekuOids = certificate.getExtendedKeyUsage(); + } catch (NullPointerException e) { + // getExtendedKeyUsage() can crash due to an Android platform bug. This probably + // happens when the EKU extension data is malformed so return false here. + // See http://crbug.com/233610 + return false; + } + if (ekuOids == null) + return true; + + for (String ekuOid : ekuOids) { + if (ekuOid.equals(OID_TLS_SERVER_AUTH) || ekuOid.equals(OID_ANY_EKU) || + ekuOid.equals(OID_SERVER_GATED_NETSCAPE) || ekuOid.equals(OID_SERVER_GATED_MICROSOFT)) { + return true; + } + } + + return false; + } + + 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|=" + Arrays.deepToString(certChain)); + } + + try { + ensureInitialized(); + } catch (CertificateException e) { + return new AndroidCertVerifyResult(CertVerifyStatusAndroid.FAILED); + } + + List serverCertificatesList = new ArrayList(); + try { + serverCertificatesList.add(createCertificateFromBytes(certChain[0])); + } catch (CertificateException e) { + return new AndroidCertVerifyResult(CertVerifyStatusAndroid.UNABLE_TO_PARSE); + } + for (int i = 1; i < certChain.length; ++i) { + try { + serverCertificatesList.add(createCertificateFromBytes(certChain[i])); + } catch (CertificateException e) { + Log.w(TAG, "intermediate " + i + " failed parsing"); + } + } + X509Certificate[] serverCertificates = + serverCertificatesList.toArray(new X509Certificate[serverCertificatesList.size()]); + + // Expired and not yet valid certificates would be rejected by the trust managers, but the + // trust managers report all certificate errors using the general CertificateException. In + // order to get more granular error information, cert validity time range is being checked + // separately. + try { + serverCertificates[0].checkValidity(); + if (!verifyKeyUsage(serverCertificates[0])) { + return new AndroidCertVerifyResult(CertVerifyStatusAndroid.INCORRECT_KEY_USAGE); + } + } catch (CertificateExpiredException e) { + return new AndroidCertVerifyResult(CertVerifyStatusAndroid.EXPIRED); + } catch (CertificateNotYetValidException e) { + return new AndroidCertVerifyResult(CertVerifyStatusAndroid.NOT_YET_VALID); + } catch (CertificateException e) { + return new AndroidCertVerifyResult(CertVerifyStatusAndroid.FAILED); + } + + synchronized (sLock) { + // If no trust manager was found, fail without crashing on the null pointer. + if (sDefaultTrustManager == null) { + return new AndroidCertVerifyResult(CertVerifyStatusAndroid.FAILED); + } + + List verifiedChain; + try { + verifiedChain = checkServerTrustedIgnoringRuntimeException( + sDefaultTrustManager, serverCertificates, authType, host); + } catch (CertificateException eDefaultManager) { + try { + verifiedChain = checkServerTrustedIgnoringRuntimeException( + sTestTrustManager, 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 new AndroidCertVerifyResult(CertVerifyStatusAndroid.NO_TRUSTED_ROOT); + } + } + + boolean isIssuedByKnownRoot = false; + if (verifiedChain.size() > 0) { + X509Certificate root = verifiedChain.get(verifiedChain.size() - 1); + isIssuedByKnownRoot = isKnownRoot(root); + } + + return new AndroidCertVerifyResult(CertVerifyStatusAndroid.OK, isIssuedByKnownRoot, + verifiedChain); + } + } + + // TODO(stefanoduo): Hook envoy-mobile JNI + //@NativeMethods + // interface Natives { + // /** + // * Notify the native net::CertDatabase instance that the system database has been updated. + // */ + // void notifyKeyChainChanged(); + //} +}