Android 5.0以下系統支持TLS 1.1/1.2協議版本


一、背景

項目中,客戶端與服務端之間普遍使用Https協議通信,突然接到測試同事反饋Android5.0以下手機上,App測試服使用出現問題,出現SSL handshake aborted錯誤信息,但正式服正常。經查,普遍錯誤信息詳情如下:

SSL handshake aborted: ssl=0x78f08cd0: I/O error during system call, Connection reset by peer .... 復制代碼

從錯誤信息上粗略看上去,SSL握手階段出現問題,連接終止。


二、分析與處理

2.1 問題分析

從總體信息上看,顯然測試服與正式服環境有所不同,導致在Android 5.0以下機型上SSL握手階段失敗。很有可能是測試服改變了相關配置。網上查了一圈,很快,Android官網文檔上找到了對應指引。

developer.android.com/reference/j…

文檔中的圖示給出了Android版本與SSL/TLS版本之間的對應關系。

 

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版本。

推薦一個非常實用的檢測網站,不僅列出了服務端當前支持的版本,還列出了具體的加密套件等有用信息。

www.ssllabs.com/ssltest/ana…

對項目中的正式服和測試服實測,結果如下:
正式服:

 

 

測試服:

 

 

顯然,服務端在測試服中,將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協議版本。

 

 

另外,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~


作者:HappyCorn
鏈接:https://juejin.im/post/5df8c7006fb9a01606716ba9
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM