一、背景

项目中,客户端与服务端之间普遍使用Https协议通信,突然接到测试同事反馈Android5.0以下手机上,App测试服使用出现问题,出现SSL handshake aborted错误信息,但正式服正常。经查,普遍错误信息详情如下:

 

SSL handshake aborted: ssl=0x78f080: I/O error during system call, Connection reset by peer

或者:
javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x78a8df68: Failure in SSL library, usually a protocol error

从错误信息上粗略看上去,SSL握手阶段出现问题,连接终止。



二、分析与处理

2.1 问题分析

从总体信息上看,显然测试服与正式服环境有所不同,导致在Android 5.0以下机型上SSL握手阶段失败。很有可能是测试服改变了相关配置。网上查了一圈,很快,Android官网文档上找到了对应指引。

developer.android.com/reference/j…

文档中的图示给出了Android版本与SSL/TLS版本之间的对应关系。

Android 5.0以下系统支持TLS 1.1/1.2协议版本_第1张图片

SSLSocket来源于javax.net.ssl,实际上是java的扩展库,Android不同系统版本中引入的是不同的Open JDK版本。因此,此处的版本对应关系的背后,实际上是因为java的不同版本中,对于SSLSocket中对SSL/TLS版本的默认支持发生了变化。

同时,对于更底层的,如SSLSocketFactoryImpl、SSLSocketImpl等实现类,Oracle JD是在sun.security.ssl包中,Open JDK对其进行了自己的实现,并放在了com.android.org.conscrypt包中。并在类名前前统一加上了Open加以区分,如OpenSSLSocketFactoryImplOpenSSLSocketImpl等。

对于conscrypt的介绍,可以参考文档:
source.android.google.cn/devices/arc…

Android 5.0 API级别是21,5.0以下常用的机型是4.4.x/4.2.x等。API Level 16对应的是Android 4.1。因此,问题基本上可以定位在服务端对TLS版本做了升级。

通过Https通信,客户端与服务端在SSL/TLS层建立安全连接前,涉及到版本协商过程。SSL/TLS在客户端和服务端分别具有对应的版本,握手阶段客户端与服务端的SSL/TLS版本,会取用两者同时支持的最高版本。如在Android 4.4手机上,默认支持SSLv3,TLSv1,如果服务端配置支持的协议版本是TLSv1,TLSv1.1,TLSv1.2,则会取用TLSv1作为协商后的版本。当然,无论是客户端还是服务端,对于SSL/TLS版本,在对应系统版本所能支持的协议版本范围内,是可以人为去修改的。如4.4系统手机上,可以将客户端在请求时的支持版本改成SSLv3,TLSv1,TLSv1.1,TLSv1.2。如果此时服务端支持的协议版本是TLSv1,TLSv1.1,TLSv1.2,协商后的版本将是TLSv1.2。

对于给定的Https的服务端网址,可以检测其当前所支持的SSL/TLS版本。

推荐一个非常实用的检测网站,不仅列出了服务端当前支持的版本,还列出了具体的加密套件等有用信息。

https://www.ssllabs.com/ssltest/analyze.html

对项目中的正式服和测试服实测,结果如下:
正式服:

Android 5.0以下系统支持TLS 1.1/1.2协议版本_第2张图片

测试服:

Android 5.0以下系统支持TLS 1.1/1.2协议版本_第3张图片

显然,服务端在测试服中,将TLS1.0的版本支持给直接去掉了。这也正好与测试结果及从Android官方文档中分析结果是一致的。

经与服务端/运维等同事确认,测试服TLS版本协议确实做了修改,处于安全及升级等方面考虑,测试服运行一段时间后,后续也会同样部署到正式服中。

这也就意味着,客户端是需要适配的。


2.2 问题处理

当前项目最低支持版本是4.4,从Android官方文档中可以看出,Android 4.4默认支持的SSL/TLS版本是SSLv3,TLSv1。但TLSv1.1,TLSv1.2实际上也是在其支持范围内的,需要人为去配置。

我们的最终目标是改变改变SSLSocket实例中的enabledProtocols,具体可以通过调用其方法setEnabledProtocols(String protocols[])SSLSocket,对外,是通过SSLSocketFactory接口的方式与外部交互,其创建的调用方式,具体是通过createSocket()方法进行。

因此,我们可以通过代理模式,设置兼容的SSLSocketFactory,并重写其对应的createSocket()方法,同时,将其设置给OkHttpClientsslSocketFactory

首先实现代理类:

public class TlsCompatSocketFactory extends SSLSocketFactory {    private static final String[] TLS_VERSION_LIST = {"TLSv1", "TLSv1.1", "TLSv1.2"};    final SSLSocketFactory target;    public TlsCompatSocketFactory(SSLSocketFactory target) {        this.target = target;    }    @Override    public String[] getDefaultCipherSuites() {        return target.getDefaultCipherSuites();    }    @Override    public String[] getSupportedCipherSuites() {        return target.getSupportedCipherSuites();    }    @Override    public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {        return supportTLS(target.createSocket(s, host, port, autoClose));    }    @Override    public Socket createSocket(String host, int port) throws IOException, UnknownHostException {        return supportTLS(target.createSocket(host, port));    }    @Override    public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {        return supportTLS(target.createSocket(host, port, localHost, localPort));    }    @Override    public Socket createSocket(InetAddress host, int port) throws IOException {        return supportTLS(target.createSocket(host, port));    }    @Override    public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {        return supportTLS(target.createSocket(address, port, localAddress, localPort));    }    private Socket supportTLS(Socket s) {        if (s instanceof SSLSocket) {            ((SSLSocket) s).setEnabledProtocols(TLS_VERSION_LIST);        }        return s;    }}

然后在OkHttpClient的封装类中,将其设置给OkHttpClient的builder:

....@JvmStatic// 设置5.0以下机型可以支持TLS 1.1/1.2版本val sc = SSLContext.getInstance("TLS")sc.init(null, null, null)clientBuilder.sslSocketFactory(TlsCompatSocketFactory(sc.socketFactory), object : X509TrustManager {    @Throws(CertificateException::class)    override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {    }    @Throws(CertificateException::class)    override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {    }    override fun getAcceptedIssuers(): Array<X509Certificate> {        return arrayOf()    }})....

设置完成后,如下图所示,在进行Https请求时,sslSocketFactory已经被替换成了我们自己定义的代理类TlsCompatSocketFactory。其内部的target对象中的sslParametersenabledProtocols为TLSv1和SSlv3,此参数为创建SSLSocket对象时默认的SSL/TLS协议版本。现在通过代理后,SSLSocket对象中的enabledProtocols已经变更成我们自定义的TLS_VERSION_LIST,即同时包含了TLSv1、TLSv1.1、TLSv1.2协议版本。

Android 5.0以下系统支持TLS 1.1/1.2协议版本_第4张图片

另外,SSL/TLS协议版本中,还有一点需要注意的是,除了SSLSocket对象支持的协议版本外,OkHttp还通过connectionSpecs指定了一个连接规格连接规格中,包含有tlsVersions,此参数与SSLSocket中的enabledProtocols一起,用来控制实际连接建立时的终端SSL/TLS协议版本。

默认的取值逻辑如下:

static final List<ConnectionSpec> DEFAULT_CONNECTION_SPECS = Util.immutableList(      ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT);

其中,MODERN_TLS对应的实现如下:

public static final ConnectionSpec MODERN_TLS = new Builder(true)      .cipherSuites(APPROVED_CIPHER_SUITES)      .tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0)      .supportsTlsExtensions(true)      .build();

也就是说,默认情况下,ConnectionSpec中的tlsVersions对当前主流的SSL/TLS协议版本都是支持的。当然,特殊情况下,我们也可以人为去设置ConnectionSpec并指定其内部的tlsVersions

下面我们看下tlsVersionsenabledProtocols取交集的具体逻辑。

private fun supportedSpec(sslSocket: SSLSocket, isFallback: Boolean): ConnectionSpec {    ....    val tlsVersionsIntersection = if (tlsVersionsAsString != null) {      sslSocket.enabledProtocols.intersect(tlsVersionsAsString, naturalOrder())    } else {      sslSocket.enabledProtocols    }        ....        return Builder(this)        .cipherSuites(*cipherSuitesIntersection)        .tlsVersions(*tlsVersionsIntersection)        .build()} /** Applies this spec to {@code sslSocket}. */  void apply(SSLSocket sslSocket, boolean isFallback) {    ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback);    if (specToApply.tlsVersions != null) {      sslSocket.setEnabledProtocols(specToApply.tlsVersions);    }    if (specToApply.cipherSuites != null) {      sslSocket.setEnabledCipherSuites(specToApply.cipherSuites);    }  }

也就是说,最终sslSocket实例中的enabledProtocols,除了基于TlsCompatSocketFactory中对sslSocket设置的enabledProtocols外,最终还会和ConnectionSpec内部的tlsVersions取交集后,再次赋值给sslSocket实例中的enabledProtocols



三、结语

总体上来说,Https通信时,SSL/TLS的协议版本,在客户端,首先取决于Android系统默认支持下的协议版本,并与ConnectionSpec内部的tlsVersions取交集,在服务端则依赖于服务端的配置。在握手阶段,客户端会和服务端协商最终的协议版本,取用两者同时支持的最高版本。

一般情况下,终端可以尽量放宽协议版本,这样当服务端更改协议版本,甚至只支持某一个协议版本(如TLSv1.1)时,在协议版本协商阶段,都是可以有尽量匹配的版本,从而对Https通信不造成影响。当然,Android 5.0以上的系统中,默认情况下,客户端对主流的协议版本都是支持的,一般不用做特殊处理。

end~

转载于:

https://juejin.im/post/5df8c7006fb9a01606716ba9

更多相关文章

  1. 蓝牙协议
  2. android调用高版本api函数的兼容性问题
  3. Flutter之Android SDK版本与External Libraries中的Android API
  4. Android AdbCommandRejectedException和cannot bind to套接字地
  5. 字符串资源多国语言版本的出错问题

随机推荐

  1. android 使用Okhttp封装上传JSON格式数据
  2. 2013.04.16——— android 获取状态栏高
  3. android 第一次运行应用的引导界面
  4. Eclipse 中的Android 工程突然无法启动调
  5. Android基础--电话拨号器
  6. Ubuntu 12.04 安装 Tomcat8 遇到的问题
  7. Android第二十九期 - 各种Android的App框
  8. gdbserver/remote debug on android
  9. android 一行代码,快速实现图片验证码(附an
  10. android中Dialog居中显示