diff --git a/SECURITY.md b/SECURITY.md index 35f5de37f63..aceb8cc055d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,12 +10,38 @@ On Android, use the [Play Services Provider](#tls-on-android). For non-Android s ## TLS on Android -On Android we recommend the use of the [Play Services Dynamic Security Provider](http://appfoundry.be/blog/2014/11/18/Google-Play-Services-Dynamic-Security-Provider) to ensure your application has an up-to-date OpenSSL library with the necessary ciper-suites and a reliable ALPN implementation. - -You may need to [update the security provider](https://developer.android.com/training/articles/security-gms-provider.html) to enable ALPN support, especially for Android versions < 5.0. If the provider fails to update, ALPN may not work. +On Android we recommend the use of the [Play Services Dynamic Security +Provider](https://www.appfoundry.be/blog/2014/11/18/Google-Play-Services-Dynamic-Security-Provider/) +to ensure your application has an up-to-date OpenSSL library with the necessary +ciper-suites and a reliable ALPN implementation. This requires [updating the +security provider at +runtime](https://developer.android.com/training/articles/security-gms-provider.html). + +Although ALPN mostly works on newer Android releases (especially since 5.0), +there are bugs and discovered security vulnerabilities that are only fixed by +upgrading the security provider. Thus, we recommend using the Play Service +Dynamic Security Provider for all Android versions. *Note: The Dynamic Security Provider must be installed **before** creating a gRPC OkHttp channel. gRPC's OkHttpProtocolNegotiator statically initializes the security protocol(s) available to gRPC, which means that changes to the security provider after the first channel is created will not be picked up by gRPC.* +### Bundling Conscrypt + +If depending on Play Services is not an option for your app, then you may bundle +[Conscrypt](https://conscrypt.org) with your application. Binaries are available +on [Maven +Central](https://search.maven.org/#search%7Cga%7C1%7Cg%3Aorg.conscrypt%20a%3Aconscrypt-android). + +Like the Play Services Dynamic Security Provider, you must still "install" +Conscrypt before use. + +```java +import org.conscrypt.Conscrypt; +import java.security.Security; +... + +Security.insertProviderAt(Conscrypt.newProvider(), 1); +``` + ## TLS with OpenSSL This is currently the recommended approach for using gRPC over TLS (on non-Android systems). diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java index 062a7da9c88..334e0c307d8 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java @@ -21,12 +21,11 @@ import com.google.common.annotations.VisibleForTesting; import io.grpc.okhttp.internal.OptionalMethod; import io.grpc.okhttp.internal.Platform; +import io.grpc.okhttp.internal.Platform.TlsExtensionType; import io.grpc.okhttp.internal.Protocol; import io.grpc.okhttp.internal.Util; import java.io.IOException; import java.net.Socket; -import java.security.Provider; -import java.security.Security; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -42,7 +41,7 @@ class OkHttpProtocolNegotiator { private static OkHttpProtocolNegotiator NEGOTIATOR = createNegotiator(OkHttpProtocolNegotiator.class.getClassLoader()); - private final Platform platform; + protected final Platform platform; @VisibleForTesting OkHttpProtocolNegotiator(Platform platform) { @@ -73,7 +72,7 @@ static OkHttpProtocolNegotiator createNegotiator(ClassLoader loader) { } } return android - ? new AndroidNegotiator(DEFAULT_PLATFORM, AndroidNegotiator.DEFAULT_TLS_EXTENSION_TYPE) + ? new AndroidNegotiator(DEFAULT_PLATFORM) : new OkHttpProtocolNegotiator(DEFAULT_PLATFORM); } @@ -134,19 +133,8 @@ static final class AndroidNegotiator extends OkHttpProtocolNegotiator { private static final OptionalMethod SET_NPN_PROTOCOLS = new OptionalMethod(null, "setNpnProtocols", byte[].class); - private static final TlsExtensionType DEFAULT_TLS_EXTENSION_TYPE = - pickTlsExtensionType(AndroidNegotiator.class.getClassLoader()); - - enum TlsExtensionType { - ALPN_AND_NPN, - NPN, - } - - private final TlsExtensionType tlsExtensionType; - - AndroidNegotiator(Platform platform, TlsExtensionType tlsExtensionType) { + AndroidNegotiator(Platform platform) { super(platform); - this.tlsExtensionType = checkNotNull(tlsExtensionType, "Unable to pick a TLS extension"); } @Override @@ -175,11 +163,11 @@ protected void configureTlsExtensions( } Object[] parameters = {Platform.concatLengthPrefixed(protocols)}; - if (tlsExtensionType == TlsExtensionType.ALPN_AND_NPN) { + if (platform.getTlsExtensionType() == TlsExtensionType.ALPN_AND_NPN) { SET_ALPN_PROTOCOLS.invokeWithoutCheckedException(sslSocket, parameters); } - if (tlsExtensionType != null) { + if (platform.getTlsExtensionType() != TlsExtensionType.NONE) { SET_NPN_PROTOCOLS.invokeWithoutCheckedException(sslSocket, parameters); } else { throw new RuntimeException("We can not do TLS handshake on this Android version, please" @@ -189,7 +177,7 @@ protected void configureTlsExtensions( @Override public String getSelectedProtocol(SSLSocket socket) { - if (tlsExtensionType == TlsExtensionType.ALPN_AND_NPN) { + if (platform.getTlsExtensionType() == TlsExtensionType.ALPN_AND_NPN) { try { byte[] alpnResult = (byte[]) GET_ALPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket); @@ -202,7 +190,7 @@ public String getSelectedProtocol(SSLSocket socket) { } } - if (tlsExtensionType != null) { + if (platform.getTlsExtensionType() != TlsExtensionType.NONE) { try { byte[] npnResult = (byte[]) GET_NPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket); @@ -216,40 +204,5 @@ public String getSelectedProtocol(SSLSocket socket) { } return null; } - - @VisibleForTesting - static TlsExtensionType pickTlsExtensionType(ClassLoader loader) { - // Decide which TLS Extension (APLN and NPN) we will use, follow the rules: - // 1. If Google Play Services Security Provider is installed, use both - // 2. If on Android 5.0 or later, use both, else - // 3. If on Android 4.1 or later, use NPN, else - // 4. Fail. - // TODO(madongfly): Logging. - - // Check if Google Play Services Security Provider is installed. - Provider provider = Security.getProvider("GmsCore_OpenSSL"); - if (provider != null) { - return TlsExtensionType.ALPN_AND_NPN; - } - - // Check if on Android 5.0 or later. - try { - loader.loadClass("android.net.Network"); // Arbitrary class added in Android 5.0. - return TlsExtensionType.ALPN_AND_NPN; - } catch (ClassNotFoundException e) { - logger.log(Level.FINE, "Can't find class", e); - } - - // Check if on Android 4.1 or later. - try { - loader.loadClass("android.app.ActivityOptions"); // Arbitrary class added in Android 4.1. - return TlsExtensionType.NPN; - } catch (ClassNotFoundException e) { - logger.log(Level.FINE, "Can't find class", e); - } - - // This will be caught by the constructor. - return null; - } } } diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpProtocolNegotiatorTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpProtocolNegotiatorTest.java index 312494836be..2ac31ea51f5 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpProtocolNegotiatorTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpProtocolNegotiatorTest.java @@ -19,7 +19,6 @@ import static com.google.common.base.Charsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -28,17 +27,13 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import io.grpc.okhttp.OkHttpProtocolNegotiator.AndroidNegotiator; -import io.grpc.okhttp.OkHttpProtocolNegotiator.AndroidNegotiator.TlsExtensionType; import io.grpc.okhttp.internal.Platform; +import io.grpc.okhttp.internal.Platform.TlsExtensionType; import io.grpc.okhttp.internal.Protocol; import java.io.IOException; -import java.security.Provider; -import java.security.Security; import javax.net.ssl.HandshakeCompletedListener; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; -import org.junit.After; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -53,21 +48,9 @@ public class OkHttpProtocolNegotiatorTest { @Rule public final ExpectedException thrown = ExpectedException.none(); - private final Provider fakeSecurityProvider = new Provider("GmsCore_OpenSSL", 1.0, "info") {}; private final SSLSocket sock = mock(SSLSocket.class); private final Platform platform = mock(Platform.class); - @Before - public void setUp() { - // Tests that depend on android need this to know which protocol negotiation to use. - Security.addProvider(fakeSecurityProvider); - } - - @After - public void tearDown() { - Security.removeProvider(fakeSecurityProvider.getName()); - } - @Test public void createNegotiator_isAndroid() { ClassLoader cl = new ClassLoader(this.getClass().getClassLoader()) { @@ -179,78 +162,11 @@ public void negotiate_preferGrpcExp() throws Exception { verify(platform).afterHandshake(sock); } - @Test - public void pickTlsExtensionType_securityProvider() throws Exception { - assertNotNull(Security.getProvider(fakeSecurityProvider.getName())); - - AndroidNegotiator.TlsExtensionType tlsExtensionType = - AndroidNegotiator.pickTlsExtensionType(getClass().getClassLoader()); - - assertEquals(TlsExtensionType.ALPN_AND_NPN, tlsExtensionType); - } - - @Test - public void pickTlsExtensionType_android50() throws Exception { - Security.removeProvider(fakeSecurityProvider.getName()); - ClassLoader cl = new ClassLoader(this.getClass().getClassLoader()) { - @Override - protected Class findClass(String name) throws ClassNotFoundException { - // Just don't throw. - if ("android.net.Network".equals(name)) { - return null; - } - return super.findClass(name); - } - }; - - AndroidNegotiator.TlsExtensionType tlsExtensionType = - AndroidNegotiator.pickTlsExtensionType(cl); - - assertEquals(TlsExtensionType.ALPN_AND_NPN, tlsExtensionType); - } - - @Test - public void pickTlsExtensionType_android41() throws Exception { - Security.removeProvider(fakeSecurityProvider.getName()); - ClassLoader cl = new ClassLoader(this.getClass().getClassLoader()) { - @Override - protected Class findClass(String name) throws ClassNotFoundException { - // Just don't throw. - if ("android.app.ActivityOptions".equals(name)) { - return null; - } - return super.findClass(name); - } - }; - - AndroidNegotiator.TlsExtensionType tlsExtensionType = - AndroidNegotiator.pickTlsExtensionType(cl); - - assertEquals(TlsExtensionType.NPN, tlsExtensionType); - } - - @Test - public void pickTlsExtensionType_none() throws Exception { - Security.removeProvider(fakeSecurityProvider.getName()); - - AndroidNegotiator.TlsExtensionType tlsExtensionType = - AndroidNegotiator.pickTlsExtensionType(getClass().getClassLoader()); - - assertNull(tlsExtensionType); - } - - @Test - public void androidNegotiator_failsOnNull() { - thrown.expect(NullPointerException.class); - thrown.expectMessage("Unable to pick a TLS extension"); - - new AndroidNegotiator(platform, null); - } - // Checks that the super class is properly invoked. @Test public void negotiate_android_handshakeFails() throws Exception { - AndroidNegotiator negotiator = new AndroidNegotiator(platform, TlsExtensionType.ALPN_AND_NPN); + when(platform.getTlsExtensionType()).thenReturn(TlsExtensionType.ALPN_AND_NPN); + AndroidNegotiator negotiator = new AndroidNegotiator(platform); FakeAndroidSslSocket androidSock = new FakeAndroidSslSocket() { @Override @@ -275,7 +191,8 @@ public byte[] getAlpnSelectedProtocol() { @Test public void getSelectedProtocol_alpn() throws Exception { - AndroidNegotiator negotiator = new AndroidNegotiator(platform, TlsExtensionType.ALPN_AND_NPN); + when(platform.getTlsExtensionType()).thenReturn(TlsExtensionType.ALPN_AND_NPN); + AndroidNegotiator negotiator = new AndroidNegotiator(platform); FakeAndroidSslSocket androidSock = new FakeAndroidSslSocketAlpn(); String actual = negotiator.getSelectedProtocol(androidSock); @@ -293,7 +210,8 @@ public byte[] getNpnSelectedProtocol() { @Test public void getSelectedProtocol_npn() throws Exception { - AndroidNegotiator negotiator = new AndroidNegotiator(platform, TlsExtensionType.NPN); + when(platform.getTlsExtensionType()).thenReturn(TlsExtensionType.NPN); + AndroidNegotiator negotiator = new AndroidNegotiator(platform); FakeAndroidSslSocket androidSock = new FakeAndroidSslSocketNpn(); String actual = negotiator.getSelectedProtocol(androidSock); diff --git a/okhttp/third_party/okhttp/java/io/grpc/okhttp/internal/Platform.java b/okhttp/third_party/okhttp/java/io/grpc/okhttp/internal/Platform.java index e755ae7477c..fb040d01a4e 100644 --- a/okhttp/third_party/okhttp/java/io/grpc/okhttp/internal/Platform.java +++ b/okhttp/third_party/okhttp/java/io/grpc/okhttp/internal/Platform.java @@ -36,7 +36,6 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; - import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import okio.Buffer; @@ -62,15 +61,24 @@ public class Platform { public static final Logger logger = Logger.getLogger(Platform.class.getName()); + public enum TlsExtensionType { + ALPN_AND_NPN, + NPN, + NONE, + } + /** - * List of security providers to use in order of preference. + * List of recognized security providers. The first recognized security provider according to the + * preference order returned by {@link Security#getProviders} will be selected. */ - private static final String[] ANDROID_SECURITY_PROVIDERS = new String[]{ - // See https://developer.android.com/training/articles/security-gms-provider.html - "com.google.android.gms.org.conscrypt.OpenSSLProvider", - "com.android.org.conscrypt.OpenSSLProvider", - "org.conscrypt.OpenSSLProvider", - "org.apache.harmony.xnet.provider.jsse.OpenSSLProvider"}; + private static final String[] ANDROID_SECURITY_PROVIDERS = + new String[] { + // See https://developer.android.com/training/articles/security-gms-provider.html + "com.google.android.gms.org.conscrypt.OpenSSLProvider", + "org.conscrypt.OpenSSLProvider", + "com.android.org.conscrypt.OpenSSLProvider", + "org.apache.harmony.xnet.provider.jsse.OpenSSLProvider" + }; private static final Platform PLATFORM = findPlatform(); @@ -103,6 +111,11 @@ public Provider getProvider() { return sslProvider; } + /** Returns the TLS extension type available (ALPN and NPN, NPN, or None). */ + public TlsExtensionType getTlsExtensionType() { + return TlsExtensionType.NONE; + } + /** * Configure TLS extensions on {@code sslSocket} for {@code route}. * @@ -132,10 +145,9 @@ public void connectSocket(Socket socket, InetSocketAddress address, /** Attempt to match the host runtime to a capable Platform implementation. */ private static Platform findPlatform() { - Provider sslProvider = GrpcUtil.IS_RESTRICTED_APPENGINE ? - getAppEngineProvider() - : getAndroidSecurityProvider(); - if (sslProvider != null) { + Provider androidOrAppEngineProvider = + GrpcUtil.IS_RESTRICTED_APPENGINE ? getAppEngineProvider() : getAndroidSecurityProvider(); + if (androidOrAppEngineProvider != null) { // Attempt to find Android 2.3+ APIs. OptionalMethod setUseSessionTickets = new OptionalMethod(null, "setUseSessionTickets", boolean.class); @@ -156,9 +168,31 @@ private static Platform findPlatform() { } catch (ClassNotFoundException ignored) { } catch (NoSuchMethodException ignored) { } - return new Android(setUseSessionTickets, setHostname, trafficStatsTagSocket, - trafficStatsUntagSocket, getAlpnSelectedProtocol, setAlpnProtocols, sslProvider); + + TlsExtensionType tlsExtensionType; + if (GrpcUtil.IS_RESTRICTED_APPENGINE) { + tlsExtensionType = TlsExtensionType.ALPN_AND_NPN; + } else if (androidOrAppEngineProvider.getName().equals("GmsCore_OpenSSL") + || androidOrAppEngineProvider.getName().equals("Conscrypt")) { + tlsExtensionType = TlsExtensionType.ALPN_AND_NPN; + } else if (isAtLeastAndroid5()) { + tlsExtensionType = TlsExtensionType.ALPN_AND_NPN; + } else if (isAtLeastAndroid41()) { + tlsExtensionType = TlsExtensionType.NPN; + } else { + tlsExtensionType = TlsExtensionType.NONE; + } + return new Android( + setUseSessionTickets, + setHostname, + trafficStatsTagSocket, + trafficStatsUntagSocket, + getAlpnSelectedProtocol, + setAlpnProtocols, + androidOrAppEngineProvider, + tlsExtensionType); } + Provider sslProvider; try { sslProvider = SSLContext.getDefault().getProvider(); } catch (NoSuchAlgorithmException nsae) { @@ -182,9 +216,34 @@ private static Platform findPlatform() { } catch (NoSuchMethodException ignored) { } + // TODO(ericgribkoff) Return null here return new Platform(sslProvider); } + private static boolean isAtLeastAndroid5() { + try { + Platform.class + .getClassLoader() + .loadClass("android.net.Network"); // Arbitrary class added in Android 5.0. + return true; + } catch (ClassNotFoundException e) { + logger.log(Level.FINE, "Can't find class", e); + } + return false; + } + + private static boolean isAtLeastAndroid41() { + try { + Platform.class + .getClassLoader() + .loadClass("android.app.ActivityOptions"); // Arbitrary class added in Android 4.1. + return true; + } catch (ClassNotFoundException e) { + logger.log(Level.FINE, "Can't find class", e); + } + return false; + } + /** * Forcibly load the conscrypt security provider on AppEngine if it's available. If not fail. */ @@ -199,13 +258,13 @@ private static Provider getAppEngineProvider() { } /** - * Select from the available security providers in preference order. If a preferred provider - * is not found then warn but continue. + * Select the first recognized security provider according to the preference order returned by + * {@link Security#getProviders}. If a recognized provider is not found then warn but continue. */ private static Provider getAndroidSecurityProvider() { - for (String providerClassName : ANDROID_SECURITY_PROVIDERS) { - Provider[] providers = Security.getProviders(); - for (Provider availableProvider : providers) { + Provider[] providers = Security.getProviders(); + for (Provider availableProvider : providers) { + for (String providerClassName : ANDROID_SECURITY_PROVIDERS) { if (providerClassName.equals(availableProvider.getClass().getName())) { logger.log(Level.FINE, "Found registered provider {0}", providerClassName); return availableProvider; @@ -216,7 +275,7 @@ private static Provider getAndroidSecurityProvider() { return null; } - /** Android 2.3 or better. */ + /** Android 2.3 or better, or AppEngine with Conscrypt. */ private static class Android extends Platform { private final OptionalMethod setUseSessionTickets; @@ -230,10 +289,17 @@ private static class Android extends Platform { private final OptionalMethod getAlpnSelectedProtocol; private final OptionalMethod setAlpnProtocols; - public Android(OptionalMethod setUseSessionTickets, OptionalMethod setHostname, - Method trafficStatsTagSocket, Method trafficStatsUntagSocket, - OptionalMethod getAlpnSelectedProtocol, OptionalMethod setAlpnProtocols, - Provider provider) { + private final TlsExtensionType tlsExtensionType; + + public Android( + OptionalMethod setUseSessionTickets, + OptionalMethod setHostname, + Method trafficStatsTagSocket, + Method trafficStatsUntagSocket, + OptionalMethod getAlpnSelectedProtocol, + OptionalMethod setAlpnProtocols, + Provider provider, + TlsExtensionType tlsExtensionType) { super(provider); this.setUseSessionTickets = setUseSessionTickets; this.setHostname = setHostname; @@ -241,6 +307,12 @@ public Android(OptionalMethod setUseSessionTickets, OptionalMethod protocols) { List names = new ArrayList(protocols.size());