这篇的全体逻辑和上一篇Http2.0的共同,也是先介绍Https,再看在OkHttp里是怎样完结的。

Https

Https是Http协议加上下一层的SSL/TSL协议组成的,TSL是SSL的后继版别,差别很小,能够理解为一个东西。进行Https衔接时,会先进行TSL的握手,完结证书认证操作,发生对称加密的公钥、加密套件等参数。之后就能够运用这个公钥进行对称加密了。
Https的加密办法一起运用了非对称加密和对称加密:

  1. 运用反向的非对称加密对证书进行签名
  2. 在查看经过的证书公钥基础上,运用非对称加密发生对称加密的公钥
  3. 运用发生的公钥,运用对称加密交互传输的数据。

上面便是Https作业的大致流程,下面具体介绍下加密的常识和握手。

加密常识

关于加密这种技能,很早很早之前就有了。没有加密的数据,称为明文,经过加密的叫做密文。Http默许都是明文传输,这种办法很简略被监听或许修正。加密的终究意图,是确保机密性、完整性、可用性。
暗码是一套加密算法,运用核算机之前都是运用机械式后者暗码本进行操作。在运用核算机之后,加密的安全程度愈来越高,可是被解密也愈来愈简略。

秘钥

假如光有暗码和原始数据,那么破解会简略很多,由于只需知道了暗码的加密办法,反向操作即可拿到明文数据。所以为了添加难度,添加了秘钥。
现在暗码就需求两个参数进行核算了,秘钥+明文+暗码==密文。只拿到暗码和密文,是不能获取原始明文,还需求秘钥。破解的难度就更大了。
用秘钥加密的技能,又由于加密和解密秘钥的状况分两种。

对称加密

加密和解密的秘钥是相同的,这种加密办法被称为对称加密,运用的秘钥被称为公钥。
对称加密的速度很快,可是服务器需求把自己的公钥传到客户端,客户端运用这个公钥对数据进行加密,服务器运用相同的公钥进行解密。
可是这种办法没有办法避免中间人攻击,假如中间人篡改了传输的公钥,运用自己的公钥代替他,那么接能够截取发送方的数据,运用自己的公钥进行解密。

非对称加密

加密和解密的秘钥是不同的,这种加密办法被称为非对称加密,进行加密的是公共的公钥,而进行解密的叫做私钥。这样只要私钥的拥有者才能够解密数据。
反向的非对称加密是数字签名,也便是运用私钥进行加密,运用公共的公钥的进行解密,这样就能够鉴定发送者的身份。也便是只要发送者才有私钥。
非对称加密的缺陷是速度很慢,相同也没有办法避免中间人攻击,中间人截获了服务器的公钥。并用自己的公钥代替,这样也能够获取发送者的数据。

Https的计划

由于对称和非对称加密都有自己的问题,都是由于公钥的传递无法确保安全性,中间人能够经过替换成自己的公钥,完结截取的作业。
Https运用了混合的办法,一起运用了两种办法,运用非对称加密发生对称加密的公钥,再经过对称加密进行处理。首要对称加密比较快速相关于非对称加密。所以仍是运用对称加密比较好,那么对称加密的缺陷时怎样确保公钥能够安全的交流呢。
这儿能够运用非对称加密传输这段公钥。这样这段公钥就能够被安全的传输。由于只要服务器的私钥才能够进行解密。非对称加密有什么问题呢,便是不能判别收到的公钥是否便是真正的公钥,是不是被篡改或许替换。怎样确保受到的公钥便是合法的公钥呢?
那就需求一个组织来给这个公钥背书,能够经过它确保这个公钥是合法的,而承载公钥的载体便是证书。客户端经过证书进行验证,完结公钥的获取。之后就能够经过这个公钥完结非对称加密传输。洽谈对称加密所用的公钥。
Https的计划大体便是这样。 以上便是Https的基础常识。下面剖析下TSL的握手细节。

TSL握手

TSL的握手首要的意图要洽谈加密的算法、对称加密的公钥、TSL/SSL版别。
首要经过衔接到服务器的443端口,经过TCP衔接,这段是明文传输,用于交流上面所说的参数。树立完结衔接后就能够开端进行握手操作了。

OKHttp源码分析(九)Https安全处理

握手如上图所示,逐条剖析下

  1. 客户端发送 client hello的报文,发送了客户端支撑的协议版别、暗码套件、随机数、紧缩算法等,服务器要在这之中选中一个自己支撑的,假如自己都不支撑,那么就会断开衔接。
  2. 服务器回来 server hello报文,内含选中的版别、暗码套件、随机数、紧缩算法等。并会回来自己的证书。
  3. 客户端收到证书后,查验这个证书,查验分四步:时刻有效性查看、签发的颁布者可信度检测、签名检测、站点称号检测。假如四项检测都经过了,那么就会取出证书中的公钥。
  4. 经过上面发生的随机数,发生了Pre-master secret,该报文运用从证书中解密取得的公钥进行加密(其实便是服务器的公钥),并经过公钥加密传输到服务端。经过这个数经过DH算法核算出MAC报文摘要和对称加密的公钥。 上面的办法就发生了能够进行对称加密的公钥。下面发送的数据就能够经过这个公钥开端对称加密了。

没有用到数字证书? 传输的证书运用了数字证书也便是反向的对称加密,当收到证书,查验经过后,会运用CA的公钥进行检测,也便是CA运用了自己的私钥进行了加密,只要CA知道私钥。
随机数怎样核算的?能够参阅这儿

随机数核算

传输进程中,会触及3个随机数,客户端发生的/服务端发生的/Pre-master secret。 在传输Pre-master secret时,会运用从证书获取的揭露秘钥,只要服务器才能够解密,关于客户端:
当其生成了Pre-master secret之后,会结合本来的A、B随机数,用DH算法核算出一个master secret,紧接着依据这个master secret推导出hash secretsession secret

关于服务端:
当其解密取得了Pre-master secret之后,会结合本来的A、B随机数,用DH算法核算出一个master secret,紧接着依据这个master secret推导出hash secretsession secret

在客户端和服务端的master secret是依据三个随机数推导出来的,它是不会在网络上传输的,只要两边知道,不会有第三者知道。一起,客户端推导出来的session secrethash secret与服务端也是彻底相同的。

那么现在两边假如开端运用对称算法加密来进行通讯,运用哪个作为同享的密钥呢?进程是这样子的:

两边运用对称加密算法进行加密,用hash secret对HTTP报文做一次运算生成一个MAC,附在HTTP报文的后边,然后用session-secret加密一切数据(HTTP+MAC),然后发送。

接收方则先用session-secret解密数据,然后得到HTTP+MAC,再用相同的算法核算出自己的MAC,假如两个MAC持平,证明数据没有被篡改。

OkHttp的规划

OkHttp是支撑主动的Https衔接的,也便是咱们默许拜访一个Https的网站,会主动的完结TSL的握手和加密。可是关于自签名的证书仍是需求咱们进行装备的。

触及的类

  1. ConnectionSpec :衔接的参数装备,包括SSL/TLS的版别、暗码套件等,这个在OkHttpClient#connectionSpecs进行装备,默许是具有SNI和ALPN等扩展功用的现代TLS和clear text即明文传输。SSL握手的前两部便是交流这部分参数的。
  2. CertificateChainCleaner:证书链整理工具,用于省掉无用的证书,过滤出一个列表,最后一个链结是受信赖的证书。
  3. X509TrustManager:此接口的实例办理哪些 X509 证书可用于验证安全套接字的远程端。 决议计划或许依据受信赖的证书颁布组织、证书撤销列表、在线状态查看或其他办法。这个类对应上面证书检测的签发的颁布者可信度检测、签名检测、时刻有效性查看。
  4. HostnameVerifier:验证主机名是否与服务器的身份验证计划匹配。能够依据证书,也能够依据其他办法。这个用于上面说的验证证书的站点称号检测。
  5. X509Certificate:X.509 证书的抽象类。 这供给了一种拜访 X.509 证书一切属性的规范办法。现有的证书都是X509类型的,这时一个规范。
  6. SSLSocketFactory:这个是jdk供给的工具,担任SSLSocketSSLSocket能够调用handShake进行ssl的握手。
  7. CertificatePinner:固定证书装备,用于对握手经过的证书做固定验证,也便是证书有必要满足固定证书的装备。 上面的类不但有jdk还有OkHttp的工具,共同完结了Https的作业。OkHttp大部分运用了jdk重视Https的支撑。

OkHttpClient装备阶段

OkHttpClient作为OkHttp的入口,能够对上面的类进行装备。看下在buidler里是怎样进行装备的。

单独装备SSLSocketFactory

设置用于维护 HTTPS 衔接的套接字工厂。假如未设置,将运用体系默许值。

public Builder sslSocketFactory(SSLSocketFactory sslSocketFactory) {
  if (sslSocketFactory == null) throw new NullPointerException("sslSocketFactory == null");
  this.sslSocketFactory = sslSocketFactory;
  this.certificateChainCleaner = Platform.get().buildCertificateChainCleaner(sslSocketFactory);
  return this;
}

一起装备SSLSocketFactory和X509TrustManager

能够经过sslSocketFactory办法,装备上面的两个参数,正常状况下,咱们不需求装备,只需求选用体系默许的装备即可。

public Builder sslSocketFactory(
    SSLSocketFactory sslSocketFactory, X509TrustManager trustManager) {
  if (sslSocketFactory == null) throw new NullPointerException("sslSocketFactory == null");
  if (trustManager == null) throw new NullPointerException("trustManager == null");
  this.sslSocketFactory = sslSocketFactory;
  this.certificateChainCleaner = CertificateChainCleaner.get(trustManager);
  return this;
}

装备HostnameVerifier

能够经过hostnameVerifier()装备hostnameVerifier,以达到咱们检测证书的站点称号。

public Builder hostnameVerifier(HostnameVerifier hostnameVerifier) {
  if (hostnameVerifier == null) throw new NullPointerException("hostnameVerifier == null");
  this.hostnameVerifier = hostnameVerifier;
  return this;
}

HostnameVerifier是一个接口,咱们只需调用它的verify办法就能够完结校验,这个操作发生在Https握手完结后,体系供给了AbstractVerifier骨架类进行装备。默许是OkHostnameVerifier

装备CertificatePinner

设置固定证书,咱们能够创立一个CertificatePinner,CertificatePinner是一个完结好的类,咱们只需传入主机称号和证书的SHA-256或许SHA-1 hashes即可。握手守信的证书有必要经过装备的固定证书。假如不满足,就会抛出异常,中止链接。

public Builder certificatePinner(CertificatePinner certificatePinner) {
  if (certificatePinner == null) throw new NullPointerException("certificatePinner == null");
  this.certificatePinner = certificatePinner;
  return this;
}

OkHttpClient参数处理阶段

上面咱们能够经过builder装备参数,那么参数是怎么进行处理的呢。咱们装备不装备一个参数又有什么不同呢?

boolean isTLS = false;
for (ConnectionSpec spec : connectionSpecs) {
  isTLS = isTLS || spec.isTls();
}
if (builder.sslSocketFactory != null || !isTLS) {
  // 自定义或不运用Https
  this.sslSocketFactory = builder.sslSocketFactory;
  this.certificateChainCleaner = builder.certificateChainCleaner;
} else {
  // 默许装备
  X509TrustManager trustManager = Util.platformTrustManager();
  this.sslSocketFactory = newSslSocketFactory(trustManager);
  this.certificateChainCleaner = CertificateChainCleaner.get(trustManager);
}
if (sslSocketFactory != null) {
  Platform.get().configureSslSocketFactory(sslSocketFactory);
}

参数的处理代码如上所示,先获取链接的装备是否是TSL,除了明文衔接外,都是运用TSL的。假如咱们装备了自己的sslSocketFactory或许不是TSL衔接(没有装备sslSocketFactory),那么都会运用builde人内部的sslSocketFactory。也便是说装备了,就运用装备的,没有装备,假如当时不支撑TSL,那么sslSocketFactory就为空。
假如没有装备并且是TSL衔接的话,这儿就会运用默许的装备。这儿的逻辑是先获取X509TrustManager,再经过X509TrustManager获取SslSocketFactory,经过SslSocketFactory再获取CertificateChainCleaner。全体的依靠联系便是这样。后边装备自定义证书时,也会运用这个依靠链。 依次看下每个进程:

X509TrustManager trustManager = Util.platformTrustManager();

public static X509TrustManager platformTrustManager() {
  try {
    TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
        TrustManagerFactory.getDefaultAlgorithm());
    trustManagerFactory.init((KeyStore) null);
    TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
    if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
      throw new IllegalStateException("Unexpected default trust managers:"
          + Arrays.toString(trustManagers));
    }
    return (X509TrustManager) trustManagers[0];
  } catch (GeneralSecurityException e) {
    throw assertionError("No System TLS", e); // The system has no TLS. Just give up.
  }
}

经过TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm());获取默许的TrustManager工厂,调用init办法,这个办法传入秘钥库,并运用证书颁布组织和相关信赖资料的来历初始化此工厂。通常运用传入的KeyStore作为做出信赖决议计划的基础。咱们自签名的证书也会经过这个办法进行装备。
后边只饿极取出trustManagerFactory.getTrustManagers(),拿数组第一个作为终究的X509TrustManager。

this.sslSocketFactory = newSslSocketFactory(trustManager);

private static SSLSocketFactory newSslSocketFactory(X509TrustManager trustManager) {
  try {
    SSLContext sslContext = Platform.get().getSSLContext();
    sslContext.init(null, new TrustManager[] { trustManager }, null);
    return sslContext.getSocketFactory();
  } catch (GeneralSecurityException e) {
    throw assertionError("No System TLS", e); // The system has no TLS. Just give up.
  }
}

获取X509TrustManager后,这儿经过Platform.get().getSSLContext()获取SSLContext。Platform.get()经过反射获取了不同平台的装备工具,这样OKHttp就能够运行在不同的平台上。获取SSLContext后,就能够init办法,对SSLContext进行装备,调用getSocketFactory获取终究的SSLSocketFactory。

this.certificateChainCleaner = CertificateChainCleaner.get(trustManager);

这儿装备了CertificateChainCleaner,获取trustManager中的能够用于验证对等方的证书,之后创立一个BasicCertificateChainCleaner

经过上面的两个装备的过程,就完结了装备阶段,看看在衔接时是怎样运用Https的。

OkHttp衔接Https阶段

Https的握手发生在Http衔接之后,在ConnectInterceptor这个衔接拦截器中。在调用完connectSocket后,就开端进行SSL的握手。由于Https需求默许衔接443 端口,可是Http会衔接80端口,这个逻辑是在哪儿装备的呢。在咱们构建恳求的Request传入的HttpUrl中,有一个port字段便是用于确认端口的。在获取端口时,假如没有进行显式的装备。就会依据defaultPort()进行装备。逻辑也比较简略。所以connectSocket会直接衔接443端口,为下面的SSL握手做了预备。

public static int defaultPort(String scheme) {
  if (scheme.equals("http")) {
    return 80;
  } else if (scheme.equals("https")) {
    return 443;
  } else {
    return -1;
  }
}

进行SSL衔接首要经过connectTls进行。 经过下面的办法进行判别。假如sslSocketFactory不为null,那么就会运用Https进行衔接。

if (route.address().sslSocketFactory() == null)
private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
  Address address = route.address();
  SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
  boolean success = false;
  SSLSocket sslSocket = null;
  try {
    // 创立SSLSocket,是对原始Socke的包装
    sslSocket = (SSLSocket) sslSocketFactory.createSocket(
        rawSocket, address.url().host(), address.url().port(), true /* autoClose */);
    // 装备SSL版别和暗码套件
    ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
    if (connectionSpec.supportsTlsExtensions()) {
      // 装备SSL扩展
      Platform.get().configureTlsExtensions(
          sslSocket, address.url().host(), address.protocols());
    }
    // 进行握手
    sslSocket.startHandshake();
    // 等待握手完结
    SSLSession sslSocketSession = sslSocket.getSession();
    Handshake unverifiedHandshake = Handshake.get(sslSocketSession);
    // 进行证书域名确认
    if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) {
      List<Certificate> peerCertificates = unverifiedHandshake.peerCertificates();
      if (!peerCertificates.isEmpty()) {
        X509Certificate cert = (X509Certificate) 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));
      } else {
        throw new SSLPeerUnverifiedException(
            "Hostname " + address.url().host() + " not verified (no certificates)");
      }
    }
    // 检测固定证书
    address.certificatePinner().check(address.url().host(),
        unverifiedHandshake.peerCertificates());
    // 握手成功,获取Http协议
    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);
    }
  }
}

逻辑比较明晰,逐条剖析下

  1. 经过sslSocketFactory创立SSLSocket。经过SSLSocket能够直接进行执行SSL的握手。
  2. 传入上面讲到的TSL版别和暗码套件
  3. 装备SSL的扩展,这儿假如ALPN的扩展,会写上运用的Http版别,握完手后会取这个装备,并判别是否运用Http2.0版别。
  4. 调用sslSocket.startHandshake(),进行握手。这时一个同步的操作,会堵塞当时线程,直到握手成功,假如中间出了什么问题,那么会直接抛出异常。
  5. 完结握手后会获取Handshake数据,执行到这儿阐明握手已经成功了,服务器的证书已经被信赖了。证实的信息就在Handshake中。
  6. 经过咱们传域名检测完结域名检测,也便是hostnameVerifier类,调用它的verify办法,经过回来的boolean值,进行判别。默许的值时OkHostnameVerifierverify办法完结如下。这儿检测了host和ip的值。假如不共同,或许证书被替换了。
    public boolean verify(String host, X509Certificate certificate) {
      return verifyAsIpAddress(host)
          ? verifyIpAddress(host, certificate)
          : verifyHostname(host, certificate);
    }
    
  7. 经过CertificatePinner固定证书检测,调用check进行检测。假如当时受信的证书不满足固定的装备,那么就不能继续恳求。固定证书的威力很大,假如装备了,那么后续的版别有必要满足这个固定的装备,所以一向要商议好。
  8. 一切的查看经过,握手成功。这时便是获取装备的时分了。比方商议的Http版别和衔接成功的输入输出流,之后的传输,也会经过SSlSocket的输入输出进行装备了。 以上就完结了SSL的握手和装备。

在实践运用中咱们或许需求装备自己的证书,假如彻底运用CA的证书,咱们是不需求装备什么的,运用默许装备即可,可是仍是有些场景需求自己动手装备Https。最常见的情形便是装备自签名的证书,服务器给咱们一个根证书,咱们装备在本地,在握手阶段,服务器给出的证书,会受这个根证书的认证。这样既完结了自签名证书的装备。下面是一些场景和常用的OkHttp的代码装备。

装备自签名证书

信赖一切证书

这是一种非常不安全的装备,这么装备,会导致毫无安全性可言。可是有些场景仍是能够暂时运用的。

static class HttpsTrustAllCertsTrustManager implements 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]; //回来长度为0的数组,相当于return null
  }
  public static SSLSocketFactory createSSLSocketFactory() {
    SSLSocketFactory sSLSocketFactory = null;
    try {
      SSLContext sc = Platform.get().getSSLContext();
      sc.init(null, new TrustManager[]{new HttpsTrustAllCertsTrustManager()},new SecureRandom());
      sSLSocketFactory = sc.getSocketFactory();
    } catch (Exception e) {
    }
    return sSLSocketFactory;
  }
}
static class TrustAllHostnameVerifier implements HostnameVerifier {
  @Override
  public boolean verify(String s, SSLSession sslSession) {
    return true;
  }
}
//构建OkHttpClient
OkHttpClient mClient =
    new OkHttpClient.Builder()
        .sslSocketFactory(HttpsTrustAllCertsTrustManager
            .createSSLSocketFactory(), new HttpsTrustAllCertsTrustManager())
        .hostnameVerifier(new TrustAllHostnameVerifier())
        .build();

上面共装备了两个变量,SslSocketFactory和HostnameVerifier。

  1. 第一个变量依靠X509TrustManager。这个认证中心,咱们需求给一个空完结,这样就会信赖一切的证书,创立的形式和OkHttpClient创立默许的装备套路相同。
  2. 第二个HostnameVerifier,假如咱们不进行装备,会走一个默许的OkHostnameVerifier,假如不设置也会验证域名。所以还需求完结一个自定义的验证期,永久回来true。

这样经过两个两步的设置,就完结了一切证书的装备作业。这种形式能够配合固定证书运用,也便是服务器的证书只能满足固定的规则才能够,也不失是一种战略。

装备自签名证书

关于自签名的证书,一般都是一个根证书,服务器回来的证书,运用这个根证书就能够进行认证。咱们的使命便是装备这个默许的自签名证书进入OkHttp的装备。

try {
  CertificateFactory cf = CertificateFactory.getInstance("X.509");
  //获取证书输入流
  InputStream caInput = null;
  Certificate ca;
  try {
      ca = cf.generateCertificate(caInput);
  } finally {
      caInput.close();
  }
  // 创立KeyStore,穿入证书
  String keyStoreType = KeyStore.getDefaultType();
  KeyStore keyStore = KeyStore.getInstance(keyStoreType);
  keyStore.load(null, null);
  keyStore.setCertificateEntry("ca", ca);
  // 创立TrustManagerFactory,用于生成TrustManager
  TrustManagerFactory tmf =
      TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
  tmf.init(keyStore);
  SSLContext s = SSLContext.getInstance("TLSv1", "AndroidOpenSSL");
  s.init(null, tmf.getTrustManagers(), null);
  return s.getSocketFactory();
} catch (Exception e) {
  e.printStackTrace();
} 

上面信赖一切证书,咱们只是自己完结了一个TrustManager,可是在装备自签名证书的时分,就需求经过TrustManagerFactory获取了。和上面装备的首要差异,也在于TrustManager的创立。

  1. 获取证书的输入流,构建Certificate
  2. 获取KeyStore,经过传入证书Certificate
  3. 创立TrustManagerFactory,并调用init,初始化KeyStore
  4. 经过SSLContext的init办法获取SSLSocketFactory

经过传入的SSLSocketFactory,传入OkHttpClient就能够了。全体逻辑仍是比较简略的。