懂了,原來 OkHttp 是這樣建立 HTTPS 連接的

目錄:

  1. 概述
  2. 基礎(chǔ)
    2.1. 加密
    2.2. 數(shù)字簽名
    2.3. 數(shù)字證書
  3. TLS 原理
  4. 主要的類和接口
    4.1. JDK
    4.2. OkHttp
  5. 源碼分析
    5.1. 創(chuàng)建安全 Socket
    5.2. 配置
    5.3. 握手
    5.4. 驗證
    5.5. 完成
  6. 應用實例
    6.1. 信任所有證書
    6.2. 信任自簽名證書
    6.4. 自定義 TLS 連接規(guī)格
    6.5. 使用證書鎖定
  7. 資料

1. 概述

TLS 是進行 HTTPS 連接的重要環(huán)節(jié),通過了 TLS 層進行協(xié)商,后續(xù)的 HTTP 請求就可以使用協(xié)商好的對稱密鑰進行加密

SSL 是 Netscape 開發(fā)的專門用來保護 Web 通訊,目前版本為 3.0。TLS 是 IETF 制定的新協(xié)議,建立在 SSL 3.0 之上。所以 TLS 1.0 可以認為是 SSL 3.1

TLS(Transport Layer Security Protocol) 協(xié)議分為兩部分

  • TLS 記錄協(xié)議
  • TLS 握手協(xié)議

2. 基礎(chǔ)

2.1. 加密

2.1.1. 對稱密鑰加密

編碼和解碼使用同一個密鑰,e = d

加密算法有

  • DES
  • Triple-DES
  • RC2
  • RC4(在 OkHttp 2.3 已經(jīng)下降支持)

位數(shù)越多,枚舉攻擊花費的時間越長

痛點:發(fā)送者和接收者建立對話前,需要一個共享密鑰

2.1.2. 非對稱密鑰加密

兩個密鑰,一個加密,一個解密。私鑰持有,公鑰公開

  • RSA

破解私鑰的難度相當于對極大數(shù)進行因式分解

RSA 加密系統(tǒng)中,D 和 E 會相互抵消

E(D(stuff)) = stuff
D(E(stuff)) = stuff

所以具體哪個是私鑰,哪個是公鑰是由用戶選擇的

2.2 數(shù)字簽名

加了密的校驗和

  • 證明是原作者,只有原作者可以私鑰來進行加密
  • 證明沒有篡改,中途篡改校驗和就不再匹配

校驗和使用摘要算法生成,比如 MD5,SHA

2.3. 數(shù)字證書

受信任組織擔保的用戶或公司的信息,沒有統(tǒng)一的標準

服務(wù)端大部分使用 x509 v3 派生證書,主要信息有

字段 舉例
證書序列號 12:34:56:78
證書過期時間 Wed,Sep 17,2017
站點組織名 StevenLee
站點DNS主機名 steven-lee.me
站點公鑰 xxxx
證書頒發(fā)者 RSA Data Security
數(shù)字簽名 xxxx

服務(wù)端把證書(內(nèi)含服務(wù)端的公鑰)發(fā)給客戶端,客戶端使用頒布證書的機構(gòu)的公鑰來解密,檢查數(shù)字簽名,取出公鑰。取出服務(wù)端的公鑰,將后面請求用的對稱密鑰 X 傳遞給服務(wù)端,后面就用該密鑰進行加密傳輸信息

3. TLS 原理

HTTPS 是在 HTTP 和 TCP 之間加了一層 TLS,這個 TLS 協(xié)商了一個對稱密鑰來進行 HTTP 加密

TLS

同時,SSL/TLS 不僅僅可以用在 HTTP,也可以用在 FTP,Telnet 等應用層協(xié)議上。

SSL/TLS 實際上混合使用了對稱和非對稱密鑰,主要分成這幾步:

使用非對稱密鑰建立安全的通道。

  • 客戶端請求 Https 連接,發(fā)送可用的 TLS 版本和可用的密碼套件
  • 服務(wù)端返回證書,密碼套件和 TLS 版本

用安全的通道產(chǎn)生并發(fā)送臨時的隨機對稱密鑰。

  • 生成隨機對稱密鑰,使用證書中的服務(wù)端公鑰加密,發(fā)送給服務(wù)端
  • 服務(wù)端使用私鑰解密獲取對稱密鑰

使用對稱密鑰加密信息,進行交互。

簡化后的流程圖如下:

TLS 握手

詳細的流程圖如下:

SSL Messages

4. 主要的類和接口

4.1. JDK

主要由 JDK 的 java.security,javax.net 和 javax.net.ssl 提供的

  • SSLSocketFactory
  • SSLSocket
  • SSLSession
  • TrustManager
    • X509TrustManager
  • Certificate
    • X509Certificate
  • HostNameVerifier

核心類的關(guān)系圖

核心類圖

4.2. OkHttp

  • RealConnection
  • ConnectionSpecSelector
  • ConnectionSpec
  • CipherSuite
  • CertificatePinner

5. 源碼分析

連接的所有實現(xiàn),在 RealConnection 中。如果沒有從 ConnectionPool 復用,創(chuàng)建新的連接過程,見 RealConnection.buildConnection:

private void buildConnection(int connectTimeout, int readTimeout, int writeTimeout, ConnectionSpecSelector connectionSpecSelector) throws IOException {
    connectSocket(connectTimeout, readTimeout);
    establishProtocol(readTimeout, writeTimeout, connectionSpecSelector);
}
  • connectSocket ,三次握手,創(chuàng)建 TCP 連接。

  • establishProtocol ,在 TCP 連接的基礎(chǔ)上,開始根據(jù)不同版本的協(xié)議,來完成連接過程。主要有 HTTP/1.1,HTTP/2 和 SPDY 協(xié)議。如果是 HTTPS 類型的,則開始 TLS 建聯(lián)。

  private void establishProtocol(int readTimeout, int writeTimeout,
      ConnectionSpecSelector connectionSpecSelector) throws IOException {
    if (route.address().sslSocketFactory() != null) {
      connectTls(readTimeout, writeTimeout, connectionSpecSelector);
    } else {
      protocol = Protocol.HTTP_1_1;
      socket = rawSocket;
    }
    ... 
  }

只關(guān)注 TLS 連接過程

 private void connectTls(int readTimeout, int writeTimeout,
      ConnectionSpecSelector connectionSpecSelector) throws IOException {
    Address address = route.address();
    SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
    boolean success = false;
    SSLSocket sslSocket = null;
    try {
      // Create the wrapper over the connected socket.
      sslSocket = (SSLSocket) sslSocketFactory.createSocket(
          rawSocket, address.url().host(), address.url().port(), true /* autoClose */);

      // Configure the socket's ciphers, TLS versions, and extensions.
      ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
      if (connectionSpec.supportsTlsExtensions()) {
        Platform.get().configureTlsExtensions(
            sslSocket, address.url().host(), address.protocols());
      }

      // Force handshake. This can throw!
      sslSocket.startHandshake();
      Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());

      // Verify that the socket's certificates are acceptable for the target host.
      if (!address.hostnameVerifier().verify(address.url().host(), sslSocket.getSession())) {
        X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
        throw new SSLPeerUnverifiedException("Hostname " + address.url().host() + " not verified:"
            + "\n    certificate: " + CertificatePinner.pin(cert)
            + "\n    DN: " + cert.getSubjectDN().getName()
            + "\n    subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
      }

      // Check that the certificate pinner is satisfied by the certificates presented.
      address.certificatePinner().check(address.url().host(),
          unverifiedHandshake.peerCertificates());

      // Success! Save the handshake and the ALPN protocol.
      String maybeProtocol = connectionSpec.supportsTlsExtensions()
          ? Platform.get().getSelectedProtocol(sslSocket)
          : null;
      socket = sslSocket;
      source = Okio.buffer(Okio.source(socket));
      sink = Okio.buffer(Okio.sink(socket));
      handshake = unverifiedHandshake;
      protocol = maybeProtocol != null
          ? Protocol.get(maybeProtocol)
          : Protocol.HTTP_1_1;
      success = true;
    } catch (AssertionError e) {
      if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
      throw e;
    } finally {
      if (sslSocket != null) {
        Platform.get().afterHandshake(sslSocket);
      }
      if (!success) {
        closeQuietly(sslSocket);
      }
    }
  }

5.1. 創(chuàng)建安全 Socket

這里的安全 Socket 就是 SSLSocket,是握手成功后的 TCP Socket 進行的封裝。

如果 SSLSocketFactory 沒有自定義配置的話,會使用 OkHttp 的默認創(chuàng)建。比如在 OkHttpClient 中有這樣的代碼來構(gòu)造默認的 SSLSocketFactory

      X509TrustManager trustManager = systemDefaultTrustManager();
      this.sslSocketFactory = systemDefaultSslSocketFactory(trustManager);
      this.certificateChainCleaner = CertificateChainCleaner.get(trustManager);

systemDefaultSslSocketFactory 方法使用 SSLContext 來構(gòu)造 SSLSocketFactory

  private SSLSocketFactory systemDefaultSslSocketFactory(X509TrustManager trustManager) {
    try {
      SSLContext sslContext = SSLContext.getInstance("TLS");
      sslContext.init(null, new TrustManager[] { trustManager }, null);
      return sslContext.getSocketFactory();
    } catch (GeneralSecurityException e) {
      throw new AssertionError(); // The system has no TLS. Just give up.
    }
  }

這樣就是用了系統(tǒng)默認的 X509TrustManager。

該 SSLSocketFactory 為系統(tǒng) SDK 提供,包括它生產(chǎn)的 SSLSocket,所以和系統(tǒng)平臺版本強相關(guān),底層為 OpenSSL 庫。對 TLS 版本的支持情況不一樣,接口也有所不同。

SSLSocket 配置信息有兩大類:

  • 支持的 TLS 協(xié)議
  • 支持的密碼套件(CipherSuite)

OkHttp 不包括自己的 SSL/TLS 庫,所以 SSLSocket 使用 Android 提供的標準 SSLSocket

5.2. 配置

經(jīng)過上面創(chuàng)建過程后,SSLSocket 已經(jīng)有了一些操作系統(tǒng)提供的默認配置。但不完全安全,OkHttp 會有自己的連接規(guī)格,來過濾掉過時的 TLS 版本和弱密碼套件。

OkHttp 內(nèi)置了三套規(guī)格,

  • ConnectionSepc.MODEN_TLS, 現(xiàn)代的 TLS 配置。
  • ConnectionSpec.COMPATIABLE_TLS,不是現(xiàn)代的,但安全 TLS 配置。
  • ConnectionSpec.CLEARTEXT, 不安全的 TLS 配置。

這三套規(guī)格跟著版本走,例如,在OkHttp 2.2,下降支持響應POODLE攻擊的SSL 3.0。而在OkHttp 2.3 下降的支持RC4

所以與桌面Web瀏覽器,保持最新的OkHttp是保持安全的最好辦法

OkHttp 還會通過反射的方式,來對 SSLSocket 的 TLS 的擴展功能進行配置

  • SNI 和 Session tickets
  • ALPN

OkHttp 會先使用現(xiàn)代的規(guī)格(ConnectionSepc.MODEN_TLS)進行連接,如果失敗會采用回退策略選擇下一個。

5.2.1. TLS 連接規(guī)格選擇

該步驟選擇適合客戶端的 TLS連接規(guī)格。一個很大的作用,就是盡可能地使用高版本的 TLS,和最新的密碼套件,來提供最安全的連接。

連接規(guī)格都封裝在 ConnectionSpec 中,主要內(nèi)容就是 TLS 版本和密碼套件

連接規(guī)格選擇的策略由 ConnectSpecSelector 進行,默認使用 OkHttp 的三套規(guī)格

最后會調(diào)用 ConnectionSpec 的 apply 方法,來配置 SSLSocket

/** 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);
    }
}

在 supportedSpec 方法中,會對選擇好的規(guī)格,和 SSLSocket 可用的配置取中交集,過濾掉那些不安全的低版本的 TLS 和弱密碼套件和 SSLSocket 不支持的配置。

這個階段后,SSLSocket 中的一些不安全的 TLS 版本和弱密碼套件就被過濾了,將會使用 OkHttp 配置規(guī)范中認為的安全版本和強密碼套件開始正式的握手過程。

5.2.2. TLS 連接規(guī)格回退

最開始會嘗試現(xiàn)代的 TLS 規(guī)格,如果不支持的話,會有回退策略(Fallback Strategy),回退到非現(xiàn)代但安全的 TLS 規(guī)格

回退策略由 RealConnection 和 ConnectSpecSelector 一起配合提供。

比如它會先選擇最新的 ConnectionSpec.MODEN_TLS,不支持的話,再更換為 ConnectionSpec.COMPATIABLE_TLS,最后選擇 ConnectionSpec.CLEARTEXT。

策略很簡單,就是連接失敗的時候,更換下一套規(guī)范重新進行連接。

5.2.3. TLS 擴展配置

Android 平臺,最終在 AndroidPlatform.configureTlsExtensions 來完成配置

@Override public void configureTlsExtensions(
    SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
  // Enable SNI and session tickets.
  if (hostname != null) {
    setUseSessionTickets.invokeOptionalWithoutCheckedException(sslSocket, true);
    setHostname.invokeOptionalWithoutCheckedException(sslSocket, hostname);
  }

  // Enable ALPN.
  if (setAlpnProtocols != null && setAlpnProtocols.isSupported(sslSocket)) {
    Object[] parameters = {concatLengthPrefixed(protocols)};
    setAlpnProtocols.invokeWithoutCheckedException(sslSocket, parameters);
  }
}

因為某些手機機型是支持 TLS 擴展的,OkHttp 采用發(fā)射的方式嘗試加載擴展,讓這些機型的擴展配置生效。

如果 ConectionSpec 支持 TLS 的擴展,這里還會配置 SNI,session tickets 和 ALPN。

5.3. 握手

調(diào)用 SSLSocket.startHandShake 開始進行握手:

// Force handshake. This can throw!
sslSocket.startHandshake();
Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());

這里客戶端正式向服務(wù)端發(fā)出數(shù)據(jù)包,內(nèi)容為可選擇的密碼和請求證書。服務(wù)端會返回相應的密碼套件,tls 版本,節(jié)點證書,本地證書等等,然后封裝在 Handshake 類中

主要內(nèi)容有:

  • CipherSuite, 密碼套件。
  • TlsVersion, TLS 版本。
  • Certificate[] peerCertificates, 站點的證書。
  • Certificate[] localCertificates, 本地的證書。一些安全級別更高的應用,會使用雙向的證書認證。

該過程中,SSLSocket 內(nèi)部會對服務(wù)端返回的 Certificate 進行判斷,是否是可信任的 CA 發(fā)布的。如果不是的話,會拋出異常

5.4. 驗證

到了這一步,服務(wù)端返回的證書已經(jīng)被系統(tǒng)所信任,也就是頒發(fā)的機構(gòu) CA 在系統(tǒng)的可信任 CA 列表中了。但是為了更加安全,還會進行以下兩種驗證。

5.4.1. 站點身份驗證

使用 HostnameVerifier 來驗證 host 是否合法,如果不合法會拋出 SSLPeerUnverifiedException

默認的實現(xiàn)是 OkHostnameVerifier.verify :

  public boolean verify(String host, SSLSession session) {
    try {
      Certificate[] certificates = session.getPeerCertificates();
      return verify(host, (X509Certificate) certificates[0]);
    } catch (SSLException e) {
      return false;
    }
  }

具體的驗證策略比較簡單,主要是檢查證書里的 IP 和 hostname 是否是我們的目標地址

5.4.2. 證書鎖定(Certificate Pinner)

到了該階段,證書已經(jīng)被信任,是屬于平臺的可信任證書授權(quán)機構(gòu)(CA)的。但是這個會受到證書頒發(fā)機構(gòu)的攻擊,比如 2011 DigiNotar 的攻擊。

所以,還可以使用 CertificatePinner 來鎖定,哪些證書和 CA 是可信任的。

缺點,限制了服務(wù)端更新 TLS 證書的能力,所以證書鎖定一定要經(jīng)過服務(wù)端管理員的同意。

5.5. 完成

成功創(chuàng)建,保存這些信息:

  • Socket,安全的連接。
  • Handshake,握手信息。
  • Protocol,使用的 HTTP 協(xié)議。

后面和服務(wù)端的交互,都會被 TLS 過程中協(xié)商好的對稱密鑰進行加密。

6. 應用實例

6.1. 信任所有證書

  • 跳過系統(tǒng)檢驗,不再使用系統(tǒng)默認的 SSLSocketFactory
  • 自定義 TrustManager,信任所有證書
X509TrustManager trustManager = new X509TrustManager() {
    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
    }
};

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{trustManager}, null);
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

OkHttpClient client = new OkHttpClient.Builder()
                        .sslSocketFactory(sslSocketFactory, trustManager)
                        .build();

Request request = new Request.Builder()
                    .url("https://kyfw.12306.cn/otn/")                    
                    .build();

Call call = client.newCall(request);
Response response = call.execute();

Logger.d("response " + response.code());

response.close();

6.2. 信任自簽名證書

還是以 12306 來進行測試,先從官網(wǎng)上下載證書 srca.cer

  • 將自簽名證書,比如 12306 的 srca.cer,保存到 assets
  • 讀取自簽名證書集合,保存到 KeyStore 中
  • 使用 KeyStore 構(gòu)建 X509TrustManager
  • 使用 X509TrustManager 初始化 SSLContext
  • 使用 SSLContext 創(chuàng)建 SSLSocketFactory
// 獲取自簽名證書集合,由證書工廠管理
InputStream inputStream = HttpsActivity.this.getAssets().open("srca.cer");
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Collection<? extends java.security.cert.Certificate> certificates = certificateFactory.generateCertificates(inputStream);
if (certificates.isEmpty()) {
    throw new IllegalArgumentException("expected non-empty set of trusted certificates");
}

// 將證書保存到 KeyStore 中
char[] password = "password".toCharArray();
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, password);
int index = 0;
for (Certificate certificate : certificates) {
    String certificateAlias = String.valueOf(index++);
    keyStore.setCertificateEntry(certificateAlias, certificate);
}

// 使用包含自簽名證書的 KeyStore 構(gòu)建一個 X509TrustManager
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, password);
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);

TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
    throw new IllegalStateException("Unexpected default trust managers:"
        + Arrays.toString(trustManagers));
}

// 使用 X509TrustManager 初始化 SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{trustManagers[0]}, null);
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

OkHttpClient client = new OkHttpClient.Builder()
                        .sslSocketFactory(sslSocketFactory, (X509TrustManager) trustManagers[0])
                        .build();

Request request = new Request.Builder()
                    .url("https://kyfw.12306.cn/otn/")
                    .build();

Call call = client.newCall(request);
Response response = call.execute();

Logger.d("response " + response.code());

response.close();

6.3. 自定義TLS連接規(guī)格

比如使用三個安全級別很高的密碼套件,并且限制 TLS 版本為 1_2

ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)  
    .tlsVersions(TlsVersion.TLS_1_2)
    .cipherSuites(
          CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
          CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
          CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256)
    .build();

OkHttpClient client = new OkHttpClient.Builder() 
    .connectionSpecs(Collections.singletonList(spec))
    .build();

該連接規(guī)格的配置是否能夠生效,還需要和 SSLSocket 的支持情況取交集,SSLSocket 不支持也就用不了

所以這三個密碼套件只能在 Android 5.0 以上的機子生效了

6.4. 使用證書鎖定

比如鎖定了指定 publicobject.com 的證書。

pin 的取值為,先對證書公鑰信息使用 SHA-256 或者 SHA-1 取哈希,然后進行 Base64 編碼,再加上 sha256 或者 sha1 的前綴。

這樣 publicobject.com 只能使用指定公鑰的證書了,安全性進一步提高,但靈活性降低:

CertificatePinner certificatePinner = new CertificatePinner.Builder()
    .add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
    .add("publicobject.com", "sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=")
    .add("publicobject.com", "sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=")
    .add("publicobject.com", "sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=")
    .build();

OkHttpClient client = new OkHttpClient.Builder()
    .certificatePinner(certificatePinner)
    .build();

7. 資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容