From 9617eb1eb43ed16ad7f2cbd45a78770bbda32ddf Mon Sep 17 00:00:00 2001 From: Chengyuan Zhang Date: Fri, 17 Apr 2020 18:44:08 +0000 Subject: [PATCH] okhttp: use new APIs for configuring TLS whenever possible (Android Q+) (#6912) Use new APIs for configuring TLS in Android environment. Starting from Android 29, there is a new set of public APIs for configuring ALPN (and starting from Android 24, there is API for enabling SNI). This change migrates to use these new APIs whenever possible. Only fallback to call the old hidden APIs if new ones do not exist (or do not work). --- .../grpc/okhttp/OkHttpProtocolNegotiator.java | 159 +++++++++++++++++- 1 file changed, 154 insertions(+), 5 deletions(-) diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java index d981ee2845ff..40d19db0e26b 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java @@ -25,11 +25,18 @@ import io.grpc.okhttp.internal.Protocol; import io.grpc.okhttp.internal.Util; import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.Socket; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; +import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSocket; /** @@ -133,6 +140,69 @@ static final class AndroidNegotiator extends OkHttpProtocolNegotiator { private static final OptionalMethod SET_NPN_PROTOCOLS = new OptionalMethod<>(null, "setNpnProtocols", byte[].class); + // Non-null on Android 10.0+. + // SSLSockets.isSupportedSocket(SSLSocket) + private static final Method SSL_SOCKETS_IS_SUPPORTED_SOCKET; + // SSLSockets.setUseSessionTickets(SSLSocket, boolean) + private static final Method SSL_SOCKETS_SET_USE_SESSION_TICKET; + // SSLParameters.setApplicationProtocols(String[]) + private static final Method SET_APPLICATION_PROTOCOLS; + // SSLParameters.getApplicationProtocols() + private static final Method GET_APPLICATION_PROTOCOLS; + // SSLSocket.getApplicationProtocol() + private static final Method GET_APPLICATION_PROTOCOL; + + // Non-null on Android 7.0+. + // SSLParameters.setServerNames(List) + private static final Method SET_SERVER_NAMES; + // SNIHostName(String) + private static final Constructor SNI_HOST_NAME; + + static { + // Attempt to find Android 10.0+ APIs. + Method setApplicationProtocolsMethod = null; + Method getApplicationProtocolsMethod = null; + Method getApplicationProtocolMethod = null; + Method sslSocketsIsSupportedSocketMethod = null; + Method sslSocketsSetUseSessionTicketsMethod = null; + try { + Class sslParameters = SSLParameters.class; + setApplicationProtocolsMethod = + sslParameters.getMethod("setApplicationProtocols", String[].class); + getApplicationProtocolsMethod = sslParameters.getMethod("getApplicationProtocols"); + getApplicationProtocolMethod = SSLSocket.class.getMethod("getApplicationProtocol"); + Class sslSockets = Class.forName("android.net.ssl.SSLSockets"); + sslSocketsIsSupportedSocketMethod = + sslSockets.getMethod("isSupportedSocket", SSLSocket.class); + sslSocketsSetUseSessionTicketsMethod = + sslSockets.getMethod("setUseSessionTickets", SSLSocket.class, boolean.class); + } catch (ClassNotFoundException e) { + logger.log(Level.FINER, "Failed to find Android 10.0+ APIs", e); + } catch (NoSuchMethodException e) { + logger.log(Level.FINER, "Failed to find Android 10.0+ APIs", e); + } + SET_APPLICATION_PROTOCOLS = setApplicationProtocolsMethod; + GET_APPLICATION_PROTOCOLS = getApplicationProtocolsMethod; + GET_APPLICATION_PROTOCOL = getApplicationProtocolMethod; + SSL_SOCKETS_IS_SUPPORTED_SOCKET = sslSocketsIsSupportedSocketMethod; + SSL_SOCKETS_SET_USE_SESSION_TICKET = sslSocketsSetUseSessionTicketsMethod; + + // Attempt to find Android 7.0+ APIs. + Method setServerNamesMethod = null; + Constructor sniHostNameConstructor = null; + try { + setServerNamesMethod = SSLParameters.class.getMethod("setServerNames", List.class); + sniHostNameConstructor = + Class.forName("javax.net.ssl.SNIHostName").getConstructor(String.class); + } catch (ClassNotFoundException e) { + logger.log(Level.FINER, "Failed to find Android 7.0+ APIs", e); + } catch (NoSuchMethodException e) { + logger.log(Level.FINER, "Failed to find Android 7.0+ APIs", e); + } + SET_SERVER_NAMES = setServerNamesMethod; + SNI_HOST_NAME = sniHostNameConstructor; + } + AndroidNegotiator(Platform platform) { super(platform); } @@ -152,21 +222,75 @@ public String negotiate(SSLSocket sslSocket, String hostname, List pro /** * Override {@link Platform}'s configureTlsExtensions for Android older than 5.0, since OkHttp * (2.3+) only support such function for Android 5.0+. + * + *

Note: Prior to Android Q, the standard way of accessing some Conscrypt features was to + * use reflection to call hidden APIs. Beginning in Q, there is public API for all of these + * features. We attempt to use the public API where possible. Otherwise, fall back to use the + * old reflective API. */ @Override protected void configureTlsExtensions( SSLSocket sslSocket, String hostname, List protocols) { - // Enable SNI and session tickets. - if (hostname != null) { - SET_USE_SESSION_TICKETS.invokeOptionalWithoutCheckedException(sslSocket, true); - SET_HOSTNAME.invokeOptionalWithoutCheckedException(sslSocket, hostname); + String[] protocolNames = protocolIds(protocols); + SSLParameters sslParams = sslSocket.getSSLParameters(); + try { + // Enable SNI and session tickets. + if (hostname != null) { + if (SSL_SOCKETS_IS_SUPPORTED_SOCKET != null + && (boolean) SSL_SOCKETS_IS_SUPPORTED_SOCKET.invoke(null, sslSocket)) { + SSL_SOCKETS_SET_USE_SESSION_TICKET.invoke(null, sslSocket, true); + } else { + SET_USE_SESSION_TICKETS.invokeOptionalWithoutCheckedException(sslSocket, true); + } + if (SET_SERVER_NAMES != null && SNI_HOST_NAME != null) { + SET_SERVER_NAMES + .invoke(sslParams, Collections.singletonList(SNI_HOST_NAME.newInstance(hostname))); + } else { + SET_HOSTNAME.invokeOptionalWithoutCheckedException(sslSocket, hostname); + } + } + boolean alpnEnabled = false; + if (GET_APPLICATION_PROTOCOL != null) { + try { + // If calling SSLSocket.getApplicationProtocol() throws UnsupportedOperationException, + // the underlying provider does not implement operations for enabling + // ALPN in the fashion of SSLParameters.setApplicationProtocols(). Fall back to + // use old hidden methods. + GET_APPLICATION_PROTOCOL.invoke(sslSocket); + SET_APPLICATION_PROTOCOLS.invoke(sslParams, (Object) protocolNames); + alpnEnabled = true; + } catch (InvocationTargetException e) { + Throwable targetException = e.getTargetException(); + if (targetException instanceof UnsupportedOperationException) { + logger.log(Level.FINER, "setApplicationProtocol unsupported, will try old methods"); + } else { + throw e; + } + } + } + sslSocket.setSSLParameters(sslParams); + // Check application protocols are configured correctly. If not, configure again with + // old methods. + // Workaround for Conscrypt bug: https://github.com/google/conscrypt/issues/832 + if (alpnEnabled && GET_APPLICATION_PROTOCOLS != null) { + String[] configuredProtocols = + (String[]) GET_APPLICATION_PROTOCOLS.invoke(sslSocket.getSSLParameters()); + if (Arrays.equals(protocolNames, configuredProtocols)) { + return; + } + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } catch (InstantiationException e) { + throw new RuntimeException(e); } Object[] parameters = {Platform.concatLengthPrefixed(protocols)}; if (platform.getTlsExtensionType() == TlsExtensionType.ALPN_AND_NPN) { SET_ALPN_PROTOCOLS.invokeWithoutCheckedException(sslSocket, parameters); } - if (platform.getTlsExtensionType() != TlsExtensionType.NONE) { SET_NPN_PROTOCOLS.invokeWithoutCheckedException(sslSocket, parameters); } else { @@ -177,6 +301,23 @@ protected void configureTlsExtensions( @Override public String getSelectedProtocol(SSLSocket socket) { + if (GET_APPLICATION_PROTOCOL != null) { + try { + return (String) GET_APPLICATION_PROTOCOL.invoke(socket); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + Throwable targetException = e.getTargetException(); + if (targetException instanceof UnsupportedOperationException) { + logger.log( + Level.FINER, + "Socket unsupported for getApplicationProtocol, will try old methods"); + } else { + throw new RuntimeException(e); + } + } + } + if (platform.getTlsExtensionType() == TlsExtensionType.ALPN_AND_NPN) { try { byte[] alpnResult = @@ -207,4 +348,12 @@ public String getSelectedProtocol(SSLSocket socket) { return null; } } + + private static String[] protocolIds(List protocols) { + List result = new ArrayList<>(); + for (Protocol protocol : protocols) { + result.add(protocol.toString()); + } + return result.toArray(new String[0]); + } }