• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #20258

01 May 2026 01:55PM UTC coverage: 88.797% (-0.03%) from 88.827%
#20258

push

github

web-flow
okhttp: enable TLS 1.3 on Android, retain TLS 1.2-only for desktop JVM

The ConnectionSpec used by OkHttpChannelBuilder had TLS 1.3 explicitly
disabled since Dec 2020 due to a Conscrypt/SunJSSE incompatibility.
However, this incompatibility does not affect Android. The previous code
applied the TLS 1.2-only restriction unconditionally to all platforms.

Regulatory impact: TLS 1.2 is classified as a legacy mechanism in ENISA
Agreed Cryptographic Mechanisms v2.0 (April 2025), with TLS 1.3 listed
as the recommended protocol. This limitation has been forcing all
downstream components using grpc-okhttp on Android to operate with a
legacy protocol, creating compliance friction with the EU Radio
Equipment Directive (RED) and EU Cyber Resilience Act (CRA)
certification requirements.

Fixes: https://github.com/grpc/grpc-java/issues/7431 (Android only)
Fixes: https://github.com/grpc/grpc-java/issues/7765 (Android only)

36136 of 40695 relevant lines covered (88.8%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.07
/../okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java
1
/*
2
 * Copyright 2015 The gRPC Authors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package io.grpc.okhttp;
18

19
import static com.google.common.base.Preconditions.checkNotNull;
20

21
import com.google.common.annotations.VisibleForTesting;
22
import com.google.common.net.HostAndPort;
23
import com.google.common.net.InetAddresses;
24
import io.grpc.internal.GrpcUtil;
25
import io.grpc.okhttp.internal.OptionalMethod;
26
import io.grpc.okhttp.internal.Platform;
27
import io.grpc.okhttp.internal.Platform.TlsExtensionType;
28
import io.grpc.okhttp.internal.Protocol;
29
import io.grpc.okhttp.internal.Util;
30
import java.io.IOException;
31
import java.lang.reflect.Constructor;
32
import java.lang.reflect.InvocationTargetException;
33
import java.lang.reflect.Method;
34
import java.net.Socket;
35
import java.util.ArrayList;
36
import java.util.Arrays;
37
import java.util.Collections;
38
import java.util.List;
39
import java.util.logging.Level;
40
import java.util.logging.Logger;
41
import javax.annotation.Nullable;
42
import javax.net.ssl.SSLParameters;
43
import javax.net.ssl.SSLSocket;
44

45
/**
46
 * A helper class located in package com.squareup.okhttp.internal for TLS negotiation.
47
 */
48
class OkHttpProtocolNegotiator {
49
  private static final Logger logger = Logger.getLogger(OkHttpProtocolNegotiator.class.getName());
1✔
50
  private static final Platform DEFAULT_PLATFORM = Platform.get();
1✔
51
  private static OkHttpProtocolNegotiator NEGOTIATOR =
1✔
52
      createNegotiator(OkHttpProtocolNegotiator.class.getClassLoader());
1✔
53

54
  protected final Platform platform;
55

56
  @VisibleForTesting
57
  OkHttpProtocolNegotiator(Platform platform) {
1✔
58
    this.platform = checkNotNull(platform, "platform");
1✔
59
  }
1✔
60

61
  public static OkHttpProtocolNegotiator get() {
62
    return NEGOTIATOR;
1✔
63
  }
64

65
  /**
66
   * Creates corresponding negotiator according to whether on Android.
67
   */
68
  @VisibleForTesting
69
  static OkHttpProtocolNegotiator createNegotiator(ClassLoader loader) {
70
    boolean android = true;
1✔
71
    try {
72
      // Attempt to find Android 2.3+ APIs.
73
      loader.loadClass("com.android.org.conscrypt.OpenSSLSocketImpl");
1✔
74
    } catch (ClassNotFoundException e1) {
1✔
75
      logger.log(Level.FINE, "Unable to find Conscrypt. Skipping", e1);
1✔
76
      try {
77
        // Older platform before being unbundled.
78
        loader.loadClass("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
1✔
79
      } catch (ClassNotFoundException e2) {
1✔
80
        logger.log(Level.FINE, "Unable to find any OpenSSLSocketImpl. Skipping", e2);
1✔
81
        android = false;
1✔
82
      }
1✔
83
    }
1✔
84
    return android
1✔
85
        ? new AndroidNegotiator(DEFAULT_PLATFORM)
1✔
86
        : new OkHttpProtocolNegotiator(DEFAULT_PLATFORM);
1✔
87
  }
88

89
  /**
90
   * Start and wait until the negotiation is done, returns the negotiated protocol.
91
   *
92
   * @throws IOException if an IO error was encountered during the handshake.
93
   * @throws RuntimeException if the negotiation completed, but no protocol was selected.
94
   */
95
  public String negotiate(
96
      SSLSocket sslSocket, String hostname, @Nullable List<Protocol> protocols) throws IOException {
97
    if (protocols != null) {
1✔
98
      configureTlsExtensions(sslSocket, hostname, protocols);
1✔
99
    }
100
    try {
101
      // Force handshake.
102
      sslSocket.startHandshake();
1✔
103

104
      String negotiatedProtocol = getSelectedProtocol(sslSocket);
1✔
105
      if (negotiatedProtocol == null) {
1✔
106
        throw new RuntimeException("TLS ALPN negotiation failed with protocols: " + protocols);
1✔
107
      }
108
      return negotiatedProtocol;
1✔
109
    } finally {
110
      platform.afterHandshake(sslSocket);
1✔
111
    }
112
  }
113

114
  /** Configure TLS extensions. */
115
  protected void configureTlsExtensions(
116
      SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
117
    platform.configureTlsExtensions(sslSocket, hostname, protocols);
1✔
118
  }
1✔
119

120
  /** Returns the negotiated protocol, or null if no protocol was negotiated. */
121
  public String getSelectedProtocol(SSLSocket socket) {
122
    return platform.getSelectedProtocol(socket);
1✔
123
  }
124

125
  static final class AndroidNegotiator extends OkHttpProtocolNegotiator {
126
    // setUseSessionTickets(boolean)
127
    private static final OptionalMethod<Socket> SET_USE_SESSION_TICKETS =
1✔
128
        new OptionalMethod<>(null, "setUseSessionTickets", Boolean.TYPE);
129
    // setHostname(String)
130
    private static final OptionalMethod<Socket> SET_HOSTNAME =
1✔
131
        new OptionalMethod<>(null, "setHostname", String.class);
132
    // byte[] getAlpnSelectedProtocol()
133
    private static final OptionalMethod<Socket> GET_ALPN_SELECTED_PROTOCOL =
1✔
134
        new OptionalMethod<>(byte[].class, "getAlpnSelectedProtocol");
135
    // setAlpnProtocol(byte[])
136
    private static final OptionalMethod<Socket> SET_ALPN_PROTOCOLS =
1✔
137
        new OptionalMethod<>(null, "setAlpnProtocols", byte[].class);
138
    // byte[] getNpnSelectedProtocol()
139
    private static final OptionalMethod<Socket> GET_NPN_SELECTED_PROTOCOL =
1✔
140
        new OptionalMethod<>(byte[].class, "getNpnSelectedProtocol");
141
    // setNpnProtocol(byte[])
142
    private static final OptionalMethod<Socket> SET_NPN_PROTOCOLS =
1✔
143
        new OptionalMethod<>(null, "setNpnProtocols", byte[].class);
144

145
    // Non-null on Android 10.0+.
146
    // SSLSockets.isSupportedSocket(SSLSocket)
147
    private static final Method SSL_SOCKETS_IS_SUPPORTED_SOCKET;
148
    // SSLSockets.setUseSessionTickets(SSLSocket, boolean)
149
    private static final Method SSL_SOCKETS_SET_USE_SESSION_TICKET;
150
    // SSLParameters.setApplicationProtocols(String[])
151
    private static final Method SET_APPLICATION_PROTOCOLS;
152
    // SSLParameters.getApplicationProtocols()
153
    private static final Method GET_APPLICATION_PROTOCOLS;
154
    // SSLSocket.getApplicationProtocol()
155
    private static final Method GET_APPLICATION_PROTOCOL;
156

157
    // Non-null on Android 7.0+.
158
    // SSLParameters.setServerNames(List<SNIServerName>)
159
    private static final Method SET_SERVER_NAMES;
160
    // SNIHostName(String)
161
    private static final Constructor<?> SNI_HOST_NAME;
162

163
    static {
164
      // Attempt to find Android 10.0+ APIs.
165
      Method setApplicationProtocolsMethod = null;
1✔
166
      Method getApplicationProtocolsMethod = null;
1✔
167
      Method getApplicationProtocolMethod = null;
1✔
168
      Method sslSocketsIsSupportedSocketMethod = null;
1✔
169
      Method sslSocketsSetUseSessionTicketsMethod = null;
1✔
170
      try {
171
        Class<?> sslParameters = SSLParameters.class;
1✔
172
        setApplicationProtocolsMethod =
1✔
173
            sslParameters.getMethod("setApplicationProtocols", String[].class);
1✔
174
        getApplicationProtocolsMethod = sslParameters.getMethod("getApplicationProtocols");
1✔
175
        getApplicationProtocolMethod = SSLSocket.class.getMethod("getApplicationProtocol");
1✔
176
        Class<?> sslSockets = Class.forName("android.net.ssl.SSLSockets");
×
177
        sslSocketsIsSupportedSocketMethod =
×
178
            sslSockets.getMethod("isSupportedSocket", SSLSocket.class);
×
179
        sslSocketsSetUseSessionTicketsMethod =
×
180
            sslSockets.getMethod("setUseSessionTickets", SSLSocket.class, boolean.class);
×
181
      } catch (ClassNotFoundException e) {
1✔
182
        logger.log(Level.FINER, "Failed to find Android 10.0+ APIs", e);
1✔
183
      } catch (NoSuchMethodException e) {
×
184
        logger.log(Level.FINER, "Failed to find Android 10.0+ APIs", e);
×
185
      }
1✔
186
      SET_APPLICATION_PROTOCOLS = setApplicationProtocolsMethod;
1✔
187
      GET_APPLICATION_PROTOCOLS = getApplicationProtocolsMethod;
1✔
188
      GET_APPLICATION_PROTOCOL = getApplicationProtocolMethod;
1✔
189
      SSL_SOCKETS_IS_SUPPORTED_SOCKET = sslSocketsIsSupportedSocketMethod;
1✔
190
      SSL_SOCKETS_SET_USE_SESSION_TICKET = sslSocketsSetUseSessionTicketsMethod;
1✔
191

192
      // Attempt to find Android 7.0+ APIs.
193
      Method setServerNamesMethod = null;
1✔
194
      Constructor<?> sniHostNameConstructor = null;
1✔
195
      try {
196
        setServerNamesMethod = SSLParameters.class.getMethod("setServerNames", List.class);
1✔
197
        sniHostNameConstructor =
1✔
198
            Class.forName("javax.net.ssl.SNIHostName").getConstructor(String.class);
1✔
199
      } catch (ClassNotFoundException e) {
×
200
        logger.log(Level.FINER, "Failed to find Android 7.0+ APIs", e);
×
201
      } catch (NoSuchMethodException e) {
×
202
        logger.log(Level.FINER, "Failed to find Android 7.0+ APIs", e);
×
203
      }
1✔
204
      SET_SERVER_NAMES = setServerNamesMethod;
1✔
205
      SNI_HOST_NAME = sniHostNameConstructor;
1✔
206
    }
1✔
207

208
    AndroidNegotiator(Platform platform) {
209
      super(platform);
1✔
210
    }
1✔
211

212
    @Override
213
    public String negotiate(SSLSocket sslSocket, String hostname, List<Protocol> protocols)
214
        throws IOException {
215
      // First check if a protocol has already been selected, since it's possible that the user
216
      // provided SSLSocketFactory has already done the handshake when creates the SSLSocket.
217
      String negotiatedProtocol = getSelectedProtocol(sslSocket);
1✔
218
      if (negotiatedProtocol == null) {
1✔
219
        negotiatedProtocol = super.negotiate(sslSocket, hostname, protocols);
×
220
      }
221
      return negotiatedProtocol;
×
222
    }
223

224
    /**
225
     * Override {@link Platform}'s configureTlsExtensions for Android older than 5.0, since OkHttp
226
     * (2.3+) only support such function for Android 5.0+.
227
     *
228
     * <p>Note: Prior to Android Q, the standard way of accessing some Conscrypt features was to
229
     * use reflection to call hidden APIs. Beginning in Q, there is public API for all of these
230
     * features. We attempt to use the public API where possible. Otherwise, fall back to use the
231
     * old reflective API.
232
     */
233
    @Override
234
    protected void configureTlsExtensions(
235
        SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
236
      String[] protocolNames = protocolIds(protocols);
1✔
237
      SSLParameters sslParams = sslSocket.getSSLParameters();
1✔
238
      try {
239
        // Enable SNI and session tickets.
240
        // Hostname is normally validated in the builder (see checkAuthority) and it should
241
        // virtually always succeed. Check again here to avoid troubles (e.g., hostname with
242
        // underscore) enabling SNI, which works around cases where checkAuthority is disabled.
243
        // See b/154375837.
244
        if (hostname != null && isValidHostName(hostname)) {
1✔
245
          if (SSL_SOCKETS_IS_SUPPORTED_SOCKET != null
1✔
246
              && (boolean) SSL_SOCKETS_IS_SUPPORTED_SOCKET.invoke(null, sslSocket)) {
×
247
            SSL_SOCKETS_SET_USE_SESSION_TICKET.invoke(null, sslSocket, true);
×
248
          } else {
249
            SET_USE_SESSION_TICKETS.invokeOptionalWithoutCheckedException(sslSocket, true);
1✔
250
          }
251
          if (SET_SERVER_NAMES != null
1✔
252
              && SNI_HOST_NAME != null
253
              && !InetAddresses.isInetAddress(HostAndPort.fromString(hostname).getHost())) {
1✔
254
            SET_SERVER_NAMES
1✔
255
                .invoke(sslParams, Collections.singletonList(SNI_HOST_NAME.newInstance(hostname)));
1✔
256
          } else {
257
            SET_HOSTNAME.invokeOptionalWithoutCheckedException(sslSocket, hostname);
×
258
          }
259
        }
260
        boolean alpnEnabled = false;
1✔
261
        if (GET_APPLICATION_PROTOCOL != null) {
1✔
262
          try {
263
            // If calling SSLSocket.getApplicationProtocol() throws UnsupportedOperationException,
264
            // the underlying provider does not implement operations for enabling
265
            // ALPN in the fashion of SSLParameters.setApplicationProtocols(). Fall back to
266
            // use old hidden methods.
267
            GET_APPLICATION_PROTOCOL.invoke(sslSocket);
×
268
            SET_APPLICATION_PROTOCOLS.invoke(sslParams, (Object) protocolNames);
×
269
            alpnEnabled = true;
×
270
          } catch (InvocationTargetException e) {
1✔
271
            Throwable targetException = e.getTargetException();
1✔
272
            if (targetException instanceof UnsupportedOperationException) {
1✔
273
              logger.log(Level.FINER, "setApplicationProtocol unsupported, will try old methods");
1✔
274
            } else {
275
              throw e;
×
276
            }
277
          }
×
278
        }
279
        sslSocket.setSSLParameters(sslParams);
1✔
280
        // Check application protocols are configured correctly. If not, configure again with
281
        // old methods.
282
        // Workaround for Conscrypt bug: https://github.com/google/conscrypt/issues/832
283
        if (alpnEnabled && GET_APPLICATION_PROTOCOLS != null) {
1✔
284
          String[] configuredProtocols =
×
285
              (String[]) GET_APPLICATION_PROTOCOLS.invoke(sslSocket.getSSLParameters());
×
286
          if (Arrays.equals(protocolNames, configuredProtocols)) {
×
287
            return;
×
288
          }
289
        }
290
      } catch (IllegalAccessException e) {
×
291
        throw new RuntimeException(e);
×
292
      } catch (InvocationTargetException e) {
×
293
        throw new RuntimeException(e);
×
294
      } catch (InstantiationException e) {
×
295
        throw new RuntimeException(e);
×
296
      }
1✔
297

298
      Object[] parameters = {Platform.concatLengthPrefixed(protocols)};
1✔
299
      if (platform.getTlsExtensionType() == TlsExtensionType.ALPN_AND_NPN) {
1✔
300
        SET_ALPN_PROTOCOLS.invokeWithoutCheckedException(sslSocket, parameters);
1✔
301
      }
302
      if (platform.getTlsExtensionType() != TlsExtensionType.NONE) {
1✔
303
        SET_NPN_PROTOCOLS.invokeWithoutCheckedException(sslSocket, parameters);
1✔
304
      } else {
305
        throw new RuntimeException("We can not do TLS handshake on this Android version, please"
×
306
            + " install the Google Play Services Dynamic Security Provider to use TLS");
307
      }
308
    }
1✔
309

310
    @Override
311
    public String getSelectedProtocol(SSLSocket socket) {
312
      if (GET_APPLICATION_PROTOCOL != null) {
1✔
313
        try {
314
          return (String) GET_APPLICATION_PROTOCOL.invoke(socket);
×
315
        } catch (IllegalAccessException e) {
×
316
          throw new RuntimeException(e);
×
317
        } catch (InvocationTargetException e) {
1✔
318
          Throwable targetException = e.getTargetException();
1✔
319
          if (targetException instanceof UnsupportedOperationException) {
1✔
320
            logger.log(
1✔
321
                Level.FINER,
322
                "Socket unsupported for getApplicationProtocol, will try old methods");
323
          } else {
324
            throw new RuntimeException(e);
×
325
          }
326
        }
327
      }
328

329
      if (platform.getTlsExtensionType() == TlsExtensionType.ALPN_AND_NPN) {
1✔
330
        try {
331
          byte[] alpnResult =
1✔
332
              (byte[]) GET_ALPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket);
1✔
333
          if (alpnResult != null) {
1✔
334
            return new String(alpnResult, Util.UTF_8);
1✔
335
          }
336
        } catch (Exception e) {
×
337
          logger.log(Level.FINE, "Failed calling getAlpnSelectedProtocol()", e);
×
338
          // In some implementations, querying selected protocol before the handshake will fail with
339
          // exception.
340
        }
1✔
341
      }
342

343
      if (platform.getTlsExtensionType() != TlsExtensionType.NONE) {
1✔
344
        try {
345
          byte[] npnResult =
1✔
346
              (byte[]) GET_NPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket);
1✔
347
          if (npnResult != null) {
1✔
348
            return new String(npnResult, Util.UTF_8);
1✔
349
          }
350
        } catch (Exception e) {
×
351
          logger.log(Level.FINE, "Failed calling getNpnSelectedProtocol()", e);
×
352
          // In some implementations, querying selected protocol before the handshake will fail with
353
          // exception.
354
        }
1✔
355
      }
356
      return null;
1✔
357
    }
358
  }
359

360
  private static String[] protocolIds(List<Protocol> protocols) {
361
    List<String> result = new ArrayList<>();
1✔
362
    for (Protocol protocol : protocols) {
1✔
363
      result.add(protocol.toString());
1✔
364
    }
1✔
365
    return result.toArray(new String[0]);
1✔
366
  }
367

368
  @VisibleForTesting
369
  static boolean isValidHostName(String name) {
370
    // GrpcUtil.checkAuthority() depends on URI implementation, while Android's URI implementation
371
    // allows underscore in hostname. Manually disallow hostname with underscore to avoid troubles.
372
    // See b/154375837.
373
    if (name.contains("_")) {
1✔
374
      return false;
1✔
375
    }
376
    try {
377
      GrpcUtil.checkAuthority(name);
1✔
378
      return true;
1✔
379
    } catch (IllegalArgumentException e) {
×
380
      return false;
×
381
    }
382
  }
383
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc