Https與OkHttp的曖昧關係以及那些不清楚的知識點

滑板上的老砒霜發表於2019-10-27

0.前言

上週出現一個問題,使用集團SDK中的WebView不能開啟某個網頁了,列印Log javax.net.ssl.SSLPeerUnverifiedException:*** not vertified。這個錯誤查了一下午,在最終回家的時候找到了答案,記錄下來,是個借鑑,因為專案是使用的集團的網路庫,程式碼不能洩露,這裡就用okhttp進行替換。

1.什麼是Https

什麼是https,這個概念感覺已經爛大街了,沒什麼想說的,但既然標題都給到這了,情緒也到了,好像不說點又有點感覺不對,簡單說一下

1.1為什麼使用https

先說傳統http有什麼缺陷:

(1)資料明文傳播,內容易被竊取監聽

(2)不驗證通訊方的身份,遭遇偽裝。

(3)無法證明報文完整性,可能被篡改。(比如中間人攻擊,你用的charles就是這樣)

上述及時http的缺陷也是https要解決的問題

(1)利用混合加密方式(非對稱加密用來傳遞祕鑰,對稱加密使用傳遞的祕鑰對資料進 行加密),確保資料內容加密。

(2)通過第三方權威機構頒發的證照(包括伺服器資訊,服務端公鑰,以及通過第三方數字證照頒發機構使用其私鑰對伺服器資訊,伺服器公鑰的hash資訊摘要進行的數字簽名)來保證通訊方的身份以及報文是否被修改。

所以HTTP+加密+完整性保護+認證=HTTPS

1.2 SSL

https的實現其實就是在TCP與HTTP協議層次之間加了一個SSL層。如圖

Https與OkHttp的曖昧關係以及那些不清楚的知識點
(HTTPS 使用 SSL(Secure Socket Layer) 和 TLS(Transport Layer Security)這兩個協議。TSL是以 SSL為原型開發的協議,有時會統一稱該協議為 SSL) SSL協議不僅可以被http使用其他應用層協議也可以使用

2. Android中的SSLScoketFactory、SSLContext、TrustManagr和HostnameVerifier

2.1

根據api文件,先對這幾個翻譯一下

SSLContext:

此類的例項代表SSL協議實現,該實現充當安全套接字工廠或SSLEngines的工廠。此類由一組可選的金鑰和信任管理器以及安全隨機位元組的源初始化。

SSLScoketFactory:

SSLSocket的工廠,SSLSocket繼承自Socket,提供SSL協議和TLS協議的安全套接字通訊。

TrustManager:信任管理器,我們一般使用它的子介面X509TrustManager,管理X509證照,驗證遠端安全套接字(x.509標準規定了證照可以包含什麼資訊,並說明了記錄資訊的方法)

HostnameVerifier:

以下解釋來自阿里程式設計規約: 在實現的HostnameVerifier子類中,需要使用verify函式效驗伺服器主機名的合法性,否則會導致惡意程式利用中間人攻擊繞過主機名效驗。在握手期間,如果URL的主機名和伺服器的標識主機名不匹配,則驗證機制可以回撥此介面實現程式來確定是否應該允許此連線,如果回撥內實現不恰當,預設接受所有域名,則有安全風險。

3. Okhttp與https

眾所周知,Okhttp中真正開啟網路連線和傳送網路資料是在攔截器裡做的,直接看ConnectInterceptor,這是OkHttp中內建的攔截器,用來開啟網路連結。

@Override public Response intercept(Chain chain) throws IOException {
    ......
     HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }
複製程式碼

無關程式碼省略, 首先StreamA了location在呼叫newStream的時候會找到一個健康的Connection,這個就是streamAllocation.connection返回的RealConnection,生成RealConnection物件後會呼叫它的connect方法

public void connect(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
      EventListener eventListener) {
       ....
        connectSocket(connectTimeout, readTimeout, call, eventListener);
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, 
       ....
  }
複製程式碼

留下關鍵程式碼,首先建立一個tcp的socket連線,因為ssl層進行握手是也是通過tcp連線進行的,所以先建立socket連線,然後再看establishProtocol方法

 private void establishProtocol(ConnectionSpecSelector 
 ...
    connectTls(connectionSpecSelector);
...
  }
複製程式碼

這裡會呼叫connectTls,從名字就可以看出這裡是進行SSL握手的地方

  private void connectTls(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();
      // block for session establishment
      SSLSession sslSocketSession = sslSocket.getSession();
      Handshake unverifiedHandshake = Handshake.get(sslSocketSession);

      // Verify that the socket's certificates are acceptable for the target host.
      if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) {
        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);
      }
    }
  }

複製程式碼

通過程式碼可以看到,首先通過SSLSocketFactory建立SSLSocket,這個SSLSocketFactory就是我們在構件OkHttpClient的時候傳進去的,然後會呼叫handshake方法進行握手,然後會回HostnameVerifier進行主機驗證,這個時候如果返回false就會拋錯,最後是對證照的校驗。

3總結

回到最開始的那個問題,集團的WebView庫使用了集團的網路庫,我們的專案也使用了集團的網路庫,在進行網路庫設定時,傳入了HostnameVerifier進行主機校驗,但是這個校驗中沒有包含WebView中訪問的域名,導致出現問題。

關注我的微信公眾號

Https與OkHttp的曖昧關係以及那些不清楚的知識點

相關文章