浅析 OkHttp 的 TLS 连接过程

4,620 阅读10分钟
原文链接: www.jianshu.com

1 概述

TLS 是进行 HTTPS 连接的重要环节,通过了 TLS 层进行协商,后续的 HTTP 请求就可以使用协商好的对称密钥进行加密

SSL 是 Netscape 开发的专门用来保护 Web 通讯,目前版本为 3.0。TLS 是 IETF 制定的新协议,建立在 SSL 3.0 之上。所以 TLS 1.0 可以认为是 SSL 3.1

TLS(Transport Layer Security Protocol) 协议分为两部分

  • TLS 记录协议
  • TLS 握手协议

2 基础

2.1 加密

2.1.1 对称密钥加密

编码和解码使用同一个密钥,e = d

加密算法有

  • DES
  • Triple-DES
  • RC2
  • RC4(在 OkHttp 2.3 已经下降支持)

位数越多,枚举攻击花费的时间越长

痛点:发送者和接收者建立对话前,需要一个共享密钥

2.1.2 非对称密钥加密

两个密钥,一个加密,一个解密。私钥持有,公钥公开

  • RSA

破解私钥的难度相当于对极大数进行因式分解

RSA 加密系统中,D 和 E 会相互抵消

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

所以具体哪个是私钥,哪个是公钥是由用户选择的

2.2 数字签名

加了密的校验和

  • 证明是原作者,只有原作者可以私钥来进行加密
  • 证明没有篡改,中途篡改校验和就不再匹配

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

2.3 数字证书

受信任组织担保的用户或公司的信息,没有统一的标准

服务端大部分使用 x509 v3 派生证书,主要信息有

字段 举例
证书序列号 12:34:56:78
证书过期时间 Wed,Sep 17,2017
站点组织名 Lynch
站点DNS主机名 lynch-lee.me
站点公钥 xxxx
证书颁发者 RSA Data Security
数字签名 xxxx

服务端把证书(内含服务端的公钥)发给客户端,客户端使用颁布证书的机构的公钥来解密,检查数字签名,取出公钥。取出服务端的公钥,将后面请求用的对称密钥 X 传递给服务端,后面就用该密钥进行加密传输信息

3 TLS 原理

HTTPS 是在 HTTP 和 TCP 之间加了一层 TLS,这个 TLS 协商了一个对称密钥来进行 HTTP 加密


img_https.png

同时,SSL/TLS 不仅仅可以用在 HTTP,也可以用在 FTP,Telnet 等应用层协议上

SSL/TLS 实际上混合使用了对称和非对称密钥,主要分成这几步

  • 使用非对称密钥建立安全的通道
    • 客户端请求 Https 连接,发送可用的 TLS 版本和可用的密码套件
    • 服务端返回证书,密码套件和 TLS 版本
  • 用安全的通道产生并发送临时的随机对称密钥
    • 生成随机对称密钥,使用证书中的服务端公钥加密,发送给服务端
    • 服务端使用私钥解密获取对称密钥
  • 使用对称密钥加密信息,进行交互

简单的过程如下


TLS 握手

详细如下


SSL Messages

4 主要的类和接口

4.1 JDK

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

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

核心类的关系图


核心类

4.2 OkHttp

  • RealConnection
  • ConnectionSpecSelector
  • ConnectionSpec
  • CipherSuite
  • CertificatePinner

5 源码分析

连接的所有实现,在 RealConnection 中。如果没有从 ConnectionPool 复用,创建新的连接过程如下

  private void buildConnection(int connectTimeout, int readTimeout, int writeTimeout,
      ConnectionSpecSelector connectionSpecSelector) throws IOException {
    connectSocket(connectTimeout, readTimeout);
    establishProtocol(readTimeout, writeTimeout, connectionSpecSelector);
  }
  • connectSocket

    三次握手,创建 TCP 连接

  • establishProtocal

    在 TCP 连接的基础上,开始根据不同版本的协议,来完成连接过程。主要有 HTTP/1.1,HTTP/2 和 SPDY 协议。如果是 HTTPS 类型的,则开始 TLS 建联

  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;
    }
    ... 
  }

只关注 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 创建安全 Socket

这里的安全 Socket 就是 SSLSocket,是握手成功后的 TCP Socket 进行的封装

使用 SSLSocketFactory 创建。会配置

如果 SSLSocketFactory 没有自定义配置的话,会使用 OkHttp 的默认创建。比如在 OkHttpClient 中有这样的代码来构造默认的 SSLSocketFactory

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

systemDefaultSslSocketFactory 方法使用 SSLContext 来构造 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.
    }
  }

这样就是用了系统默认的 X509TrustManager

该 SSLSocketFactory 为系统 SDK 提供,包括它生产的 SSLSocket,所以和系统平台版本强相关,底层为 OpenSSL 库。对 TLS 版本的支持情况不一样,接口也有所不同

SSLSocket 配置信息有两大类

  • 支持的 TLS 协议
  • 支持的密码套件(CipherSuite)

OkHttp 不包括自己的 SSL/TLS 库,所以 SSLSocket 使用 Android 提供的标准 SSLSocket

5.2 配置

经过上面创建过程后,SSLSocket 已经有了一些操作系统提供的默认配置。但不完全安全,OkHttp 会有自己的连接规格,来过滤掉过时的 TLS 版本和弱密码套件

OkHttp 内置了三套规格,

  • ConnectionSepc.MODEN_TLS

    现代的 TLS 配置

  • ConnectionSpec.COMPATIABLE_TLS

    不是现代的,但安全 TLS 配置

  • ConnectionSpec.CLEARTEXT

    不安全的 TLS 配置

这三套规格跟着版本走,例如,在OkHttp 2.2,下降支持响应POODLE攻击的SSL 3.0。而在OkHttp 2.3 下降的支持RC4

所以与桌面Web浏览器,保持最新的OkHttp是保持安全的最好办法

OkHttp 还会通过反射的方式,来对 SSLSocket 的 TLS 的扩展功能进行配置

  • SNI 和 Session tickets
  • ALPN

OkHttp 会先使用现代的规格进行连接,如果失败会采用回退策略选择下一个

5.2.1 TLS 连接规格选择

该步骤选择适合客户端的 TLS连接规格。一个很大的作用,就是尽可能地使用高版本的 TLS,和最新的密码套件,来提供最安全的连接

连接规格都封装在 ConnectionSpec 中,主要内容就是 TLS 版本和密码套件

连接规格选择的策略由 ConnectSpecSelector 进行,默认使用 OkHttp 的三套规格

最后会调用 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 方法中,会对选择好的规格,和 SSLSocket 可用的配置取中交集,过滤掉那些不安全的低版本的 TLS 和弱密码套件和 SSLSocket 不支持的配置

这个阶段后,SSLSocket 中的一些不安全的 TLS 版本和弱密码套件就被过滤了,将会使用 OkHttp 配置规范中认为的安全版本和强密码套件开始正式的握手过程

5.2.2 TLS 连接规格回退

最开始会尝试现代的 TLS 规格,如果不支持的话,会有回退策略(Fallback Strategy),回退到非现代但安全的 TLS 规格

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

比如它会先选择最新的 ConnectionSpec.MODEN_TLS,不支持的话,再更换为 ConnectionSpec.COMPATIABLE_TLS,最后选择 ConnectionSpec.CLEARTEXT

策略很简单,就是连接失败的时候,更换下一套规范重新进行连接

5.2.3 TLS 扩展配置

Android 平台,最终在 AndroidPlatform 来完成配置

@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 采用发射的方式,让这些机型的扩展配置生效

如果 ConectionSpec 支持 TLS 的扩展,这里会配置 SNI,session tickets 和 ALPN

5.3 握手

调用 SSLSocket 的 startHandShake 开始进行握手

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

这里客户端正式向服务端发出数据包,内容为可选择的密码和请求证书。服务端会返回相应的密码套件,tls 版本,节点证书,本地证书等等,然后封装在 Handshake 类中

主要内容有

  • CipherSuite 密码套件
  • TlsVersion TLS 版本
  • Certificate[] peerCertificates 站点的证书
  • Certificate[] localCertificates 本地的证书。一些安全级别更高的应用,会使用双向的证书认证

该过程中,SSLSocket 内部会对服务端返回的 Certificate 进行判断,是否是可信任的 CA 发布的。如果不是的话,会抛出异常

5.4 验证

到了这一步,服务端返回的证书已经被系统所信任,也就是颁发的机构 CA 在系统的可信任 CA 列表中了。但是为了更加安全,还会进行以下两种验证

5.4.1 站点身份验证

使用 HostnameVerifier 来验证 host 是否合法,如果不合法会抛出 SSLPeerUnverifiedException

默认的实现是 OkHostnameVerifier

  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)

到了该阶段,证书已经被信任,是属于平台的可信任证书授权机构(CA)的。但是这个会受到证书颁发机构的攻击,比如 2011 DigiNotar 的攻击

所以,还可以使用 CertificatePinner 来锁定,哪些证书和 CA 是可信任的

缺点,限制了服务端更新 TLS 证书的能力,所以证书锁定一定要经过服务端管理员的同意

5.5 完成

成功创建,保存

  • Socket,安全的连接
  • Handshake,握手信息
  • Protocol,使用的 HTTP 协议

后面和服务端的交互,都会被 TLS 过程中协商好的对称密钥进行加密

6 应用实例

6.1 信任所有证书

  • 跳过系统检验,不再使用系统默认的 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 来进行测试,先从官网上下载证书 srca.cer

  • 将自签名证书,比如 12306 的 srca.cer,保存到 assets
  • 读取自签名证书集合,保存到 KeyStore 中
  • 使用 KeyStore 构建 X509TrustManager
  • 使用 X509TrustManager 初始化 SSLContext
  • 使用 SSLContext 创建 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 构建一个 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连接规格

比如使用三个安全级别很高的密码套件,并且限制 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();

该连接规格的配置是否能够生效,还需要和 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();

资料