https和http/2

http://geek.csdn.net/news/detail/188003算法

 

HTTPS协议原理分析

HTTPS协议须要解决的问题

HTTPS做为安全协议而诞生,那么就不得不面对如下两大安全问题:缓存

  • 身份验证安全

    确保通讯双方身份的真实性。直白一些,A但愿与B通讯,A如何确认B的身份不是由C伪造的。服务器

    (由C伪造B的身份与A通讯,称为中间人攻击)网络

  • 通讯加密并发

    通讯的机密性、完整性依赖于算法与密钥,通讯双方是如何选择算法与密钥的。dom

能同时解决以上两个问题,就能确保真实有效的通讯双方采起有效的算法与密钥进行通讯,便完成了协议安全的初衷。源码分析

在介绍HTTPS协议如何解决两大安全问题前,咱们首先了解几个概念。性能

  • 数字证书优化

    数字证书是互联网通讯中标识双方身份信息的数字文件,由CA签发。

  • CA

    CA(certification authority)是数字证书的签发机构。做为权威机构,其审核申请者身份后签发数字证书,这样咱们只须要校验数字证书便可肯定对方的真实身份。

  • HTTPS协议、SSL协议、TLS协议、握手协议的关系

    HTTPS是Hypertext Transfer Protocol over Secure Socket Layer的缩写,即HTTP over SSL,可理解为基于SSL的HTTP协议。HTTPS协议安全是由SSL协议(目前经常使用的,本文基于TLS 1.2进行分析)实现的。

    SSL协议是一种记录协议,扩展性良好,能够很方便的添加子协议,而握手协议即是SSL协议的一个子协议。

    TLS协议是SSL协议的后续版本,本文中涉及的SSL协议默认是TLS协议1.2版本。

HTTPS协议的安全性由SSL协议实现,当前使用的TLS协议1.2版本包含了四个核心子协议:握手协议、密钥配置切换协议、应用数据协议及报警协议。

解决身份验证与通讯加密的核心,即是握手协议,接下来着重介绍握手协议。

握手协议

握手协议的做用即是通讯双方进行身份确认、协商安全链接各参数(加密算法、密钥等),确保双方身份真实而且协商的算法与密钥可以保证通讯安全。

对握手协议的介绍限于客户端对服务端的身份验证,单向身份验证也是目前互联网公司最多见的认证方式。

首先咱们看一下协议交互,如图1所示:

 


图1 握手协议

 

接下来以Wireshark抓取接口的握手协议过程为例,针对每条协议消息分析。

ClientHello消息

ClientHello消息的做用是,将客户端可用于创建加密通道的参数集合,一次性发送给服务端。

消息内容包括:指望协议版本(TLS 1.2)、可供采用的密码套件(Cipher Suites)、客户端随机数(Random)及扩展字段内容(Extension)等信息,如图2所示。

 


图2 ClientHello

 

ServerHello消息

ServerHello消息的做用是,在ClientHello参数集合中选择适合的参数,并将服务端用于创建加密通道的参数发送给客户端。

消息内容包括:采起的协议版本(TLS 1.2)、采用的密码套件(Cipher Suite)、服务端随机数(Random)、用于恢复会话的会话ID(Session ID)及扩展字段等信息,如图3所示。

自此客户端与服务端的协议版本、密码套件已经协商完毕。

这里服务端下发的会话ID可用于后续恢复会话。若客户端在ClientHello中携带了会话ID,而且服务端承认,则双方直接经过原主密钥生成一套新的密钥便可继续通讯。将两个网络往返下降为一个网络往返,提升通道创建的效率。

 


图3 ServerHello

 

Certificate消息

Certificate消息的做用是,将服务端证书的详细信息发送给客户端,供客户端进行服务端身份校验。

消息内容:服务端下发的证书链,如图4所示。

服务端为了保证下发的证书可以被客户端正确识别,就须要将签发此证书的CA证书一同下发,构成证书链,保证客户端能够根据证书链的信息在系统配置中找到根证书,并经过根证书的公钥逐层向下验证证书的合法性。 
如图所示,五八服务器下发了两个证书:本身的证书与签发CA的证书。经过签发CA的证书信息,可以直接找到根证书。

 


图4 Certificate

 

客户端本地校验服务端证书,若校验经过,则客户端对服务端的身份验证便完成了。

Certificate这个阶段解决了两端的身份验证问题。借助CA的力量,经过CA签发证书,将身份验证的工做交给了CA处理。 
只要是咱们承认的CA,签发的证书咱们均承认证书持有者的身份。因为CA的介入,解决了中间人攻击的问题,由于中间人并无服务端的证书可供客户端验证。

ServerKeyExchange消息(可能不发送)

ServerKeyExchange消息的做用是,将须要服务端提供的密钥交换的额外参数,传给客户端。有的算法不须要额外参数,则ServerKeyExchange消息可不发送。

消息内容:用于密钥交换的额外参数,如图5所示。

 


图5 ServerKeyExchange

 

如图5,服务端下发了“EC Diffile-Hellman”密钥交换算法所须要的参数。

ServerHelloDone消息

ServerHelloDone消息的做用是,通知客户端ServerHello阶段的数据均已发送完毕,等待客户端下一步消息。

ClientKeyExchange消息

ClientKeyExchange消息的做用是,将客户端须要为密钥交换提供的数据发送给服务端。

当咱们选用RSA密钥交换算法时,此消息的内容即是经过证书公钥加密的用于生成主密钥的预主密钥。

如图6所示,因为选用的密钥交换算法是“EC Diffie-Hellman”,因此ClientKeyExchange消息发送的是”EC Diffie-Hellman”算法须要的客户端参数。

 


图6 ClientKeyExchange

 

当发送了ClientKeyExchange后,两端均具备了生成主密钥的完整密钥数据与随机数,两端分别根据所选算法计算主密钥便可。

至此,ClientKeyExchange发送后,两端都可生成主密钥,密钥交换问题便解决了。

有的读者可能对随机数的采用有些疑惑,笔者以为随机数的加入是为了提升密钥的随机性。 
因为客户端直接生成的密钥颇有可能不够随机,而经过预主密钥加上两端提供的两个随机数作种子,建立的主密钥能够保证更加贴近真实随机的密钥。

ChangeCipherSpec消息

通过以上六条消息,咱们已经解决了身份认证问题、密码套件选取问题、密钥交换问题。双方也已经经过主密钥生成了实际使用的六个加解密密钥。

ChangeCipherSpec消息的做用,即是声明后续消息均采用密钥加密。在此消息后,咱们在WireShark上便看不到明文信息了。

Finished消息

Finished消息的做用,是对握手阶段全部消息计算摘要,并发送给对方校验,避免通讯过程当中被中间人所篡改。

HTTPS协议总结

自此,HTTPS如何保证通讯安全,经过握手协议的介绍,咱们已经有所了解。

可是,在全面使用HTTPS前,咱们还须要考虑一个众所周知的问题——HTTPS性能。

相对HTTP协议来讲,HTTPS协议创建数据通道的更加耗时,若直接部署到App中,势必下降数据传递的效率,间接影响用户体验。

接下来,介绍HTTPS性能救星——HTTP2协议。

协议新宠-HTTP2

协议介绍

随着互联网的快速发展,HTTP1.x协议获得了迅猛发展,但当App一个页面包含了数十个请求时,HTTP1.x协议的局限性便暴露了出来:

  • 每一个请求与响应须要单独创建链路进行请求(Connection字段可以解决部分问题),浪费资源。
  • 每一个请求与响应都须要添加完整的头信息,应用数据传输效率较低。
  • 默认没有进行加密,数据在传输过程当中容易被监听与篡改。

HTTP2正是为了解决HTTP1.x暴露出来的问题而诞生的。

说到HTTP2不得不提spdy。 
因为HTTP1.x暴露出来的问题,Google设计了全新的名为spdy的新协议。spdy在五层协议栈的TCP层与HTTP层引入了一个新的逻辑层以提升效率。spdy是一个中间层,对TCP层与HTTP层有很好的兼容,不须要修改HTTP层便可改善应用数据传输速度。 
spdy经过多路复用技术,使客户端与服务器只须要保持一条连接便可并发屡次数据交互,提升了通讯效率。 
而HTTP2便士基于spdy的思路开发的。 
经过流与帧概念的引入,继承了spdy的多路复用,并增长了一些实用特性。

HTTP2有什么特性呢?HTTP2的特性不只解决了上述已暴露的问题,还有一些功能使HTTP协议更加好用。

  • 多路复用
  • 压缩头信息
  • 对请求划分优先级
  • 支持服务端Push消息到客户端

此外,HTTP2目前在实际使用中,只用于HTTPS协议场景下,经过握手阶段ClientHello与ServerHello的extension字段协商而来,因此目前HTTP2的使用场景,都是默认安全加密的。

下面介绍HTTP2协议协商以及多路复用与压缩头信息两大特性,实现部分采用okhttp源码(基于parent-3.4.2)进行分析与介绍。

okhttp是目前使用最普遍的支持HTTP2的Android端开源网络库,以okhttp为例介绍HTTP2特性也可方便读者提早了解okhttp,方便后续接入okhttp。

协议协商

HTTP2协议的协商是在握手阶段进行的。

协商的方式是经过握手协议extension扩展字段进行扩展,新增Application Layer Protocol Negotiation字段进行协商。

在握手协议的ClientHello阶段,客户端将所支持的协议列表填入Application Layer Protocol Negotiation字段,供服务端进行挑选。如图7所示:

 


图7 ALPN1

 

服务端收到ClientHello消息后,在客户端所支持的协议列表中选择适当协议做为后续应用层协议。如图8所示:

 


图8 ALPN2

 

这样,两端便完成了HTTP2协议的协商。

在HTTP2未出现时,spdy也是经过扩展字段,扩展出next_protocol_negotiation字段,以NPN协议进行spdy的协商。不过因为NPN协议协商过于复杂,对https协议侵入性较强,在出现ALPN协商协议后,便逐渐被淘汰了。因此,本文协议协商并为对NPN协议协商作介绍。

协议特性之多路复用

http2为了优化http1.x对TCP性能的浪费,提出了多路复用的概念。

多路复用的含义

在HTTP2中,同一域名下的请求,可经过同一条TCP链路进行传输,使多个请求没必要单独创建链路,节省创建链路的开销。

为了达到这个目的,HTTP2提出了流与帧的概念,流表明请求与响应,而请求与响应具体的数据则包装为帧,对链路中传输的数据经过流ID与帧类型进行区分处理。图9即是多路复用的抽象图,每一个块表明一帧,而相同颜色的块则表明是同一个流。

 


图9 http2_stream

 

那么HTTP2的多路复用是如何实现的呢?

因为网络请求的场景不少,咱们选择其中一个路径来介绍:

  1. 客户端与服务端在某个域名的TCP通道已创建
  2. 新建立的客户端请求经过已链接的TCP通道进行请求发送与响应处理
多路复用实现

默认咱们已经添加各参数建立了Request对象r,并经过Request对象建立了Call对象c。并在独立线程中,调用c.execute()方法,进行同步请求操做。

okhttp调用execute方法后,其实是由一系列的interceptor来负责执行的。

interceptor根据添加顺序依此执行,其中咱们关注的是RetryAndFollowUpInterceptor、ConnectInterceptor0、CallServerInterceptor。

1.在RetryAndFollowUpInterceptor中,okhttp为咱们建立了一个StreamAllocation对象,StreamAllocation中含有基于url建立的Address对象。

Address类的url字段与Request类的url字段不一样,Address类的url字段不包括path与query字段,只含有scheme与authority部分,这点在进行Connection复用的equal操做时起了很大做用。

2.在ConnectInterceptor中,StreamAllocation对象的Address与链接池中每一个Connection对象的Address依次进行匹配,匹配成功并知足一些条件的Connection即可复用。基于匹配出的Connection建立Http2xStream,用于后续读写操做。

与链接池中Address匹配主要经过Address的url,url因为只含有scheme与authority因此可用于域名的匹配,这即是okhttp基于域名层面多路复用的基础。 
实际上真正进行流读写操做的是FramedConnection与FramedStream,Connection与Http2xStream是抽象于具体操做的类,以方便上层使用。

3.在CallServerInterceptor中,Http2xStream建立FramedStream用于Request发送,并将FramedStream与对应的StreamID绑定缓存下来,以便Response到来时,可以根据StreamID索引到对应的FramedSteam进行后续操做。

在FramedStream发送完Request后,执行readResponseHeaders方法时进行调用了wait,将当前线程挂起。 
并在FramedConnection读线程收到StreamID消息时,在缓存中查询FramedStream并将对应线程唤醒进行Response解码。

概括下okhttp的多路复用实现思路:

  1. 经过请求的Address与链接池中现有链接Address依次匹配,选出可用的Connection。
  2. 经过Http2xStream建立的FramedStream在发送了请求后,将FramedStream对象与StreamID的映射关系缓存到FramedConnection中。
  3. 收到消息后,FramedConnection解析帧信息,在Map中经过解析的StreamID选出缓存的FramedStream,并唤醒FramedStream进行Response的处理。

在笔者看来,HTTP2即是一个良好兼容http协议格式的自定义协议,经过Stream将数据分发到各请求,经过Frame将请求数据详细细分。

协议特性之压缩头信息

HTTP2为了解决HTTP1.x中头信息过大致使效率低下的问题,提出的解决方案即是压缩头部信息。具体的压缩方式,则引入了HPACK。

HPACK压缩算法是专门为HTTP2头部压缩服务的。为了达到压缩头部信息的目的,HPACK将头部字段缓存为索引,经过索引ID表明头部字段。客户端与服务端维护索引表,通讯过程当中尽量采用索引进行通讯,收到索引后查询索引表,才能解析出真正的头部信息。

HPACK索引表划分为动态索引表与静态索引表,动态索引表是HTTP2协议通讯过程当中两端动态维护的索引表,而静态索引表是硬编码进协议中的索引表。

做为分析HPACK压缩头信息的基础,须要先介绍HPACK对索引以及头部字符串的表示方式。

索引

索引以整型数字表示,因为HPACK须要考虑压缩与编解码问题,因此整型数字结构定义如图10所示:

 


图10 int_strut

 

  • 类别标识

    经过类别标识进行HPACK类别分类,指导后续编解码操做,常见的有1,01,01000000等八个类别。

  • 首字节低位整型

    首字节排除类别标识的剩余位,用于表示低位整型。若数值大于剩余位所能表示的容量,则须要后续字节表示高位整型。

  • 结束标识

    表示此字节是否为整型解析终止字节。

  • 高位整型

    字节余下7bit,用于填充整型高位。

“结束标识+高位整型”字节可能有0个、也有可能有多个,依据数据大小而定。 
譬如,若想表示类别为1,索引为2,则使用10000010便可,不须要额外字节增长高位整型。

头部字符串

头部字符串须要显式声明长度,因此数据首字节由“类型标识+数据长度”组成。如图11所示:

 


图11 string_strut

 

  • 类型标识

    是否选用哈夫曼编码,1为选用,0为不选用,okhttp默认不选用哈夫曼编码。

  • 数据长度

    标识数据长度,采用上面提到的整型表示法表示。

  • 数据内容

    二进制数据。

解码实例

下面综合okhttp源码分析HPACK解码头部字段过程。

对编码部分感兴趣的读者,能够查阅RFC 7541或直接分析OkHttp源码。

当咱们须要解码头部字段时,首先解析头部字段首字节(HPACK头部字段首字节分为8个类别,摘选其中3个类别说明),首字节用于指导当前头部字段的解析规则:

  • 1xxxxxxx

    类别标识为1,表明收到一条K、V均为索引的头部字段。

    K、V值:经过解析HPACK整型获取KV对的索引值,并根据索引值映射对应的头部原字段便可,压缩效率最高。

  • 01xxxxxx

    类别标识为01,表明收到一条K为索引、V为原字段,且须要加入动态索引表的头部字段。

    K值:经过解析HPACK整型获取K值索引值,并经过索引值映射对应的头部原字段。

    V值:经过解析HPACK字符串获取V值原字段。

    获取K、V值后还需插入动态索引表中。

  • 01000000

    01000000表明收到一条K、V均为原字段,且须要加入动态索引表的头部字段。

    K、V值:经过解析HPACK字符串获取K、V原字段,并插入动态索引表中。

还有不加入动态索引表、调整索引表大小等类别,这里就不展开了,感兴趣的能够看okhttp源码实现。

okhttp解析头信息的核心方法实现以下:

void readHeaders() throws IOException { while (!source.exhausted()) { int b = source.readByte() & 0xff; if (b == 0x80) { // 10000000 //类别标识为1,但索引为0 throw new IOException("index == 0"); } else if ((b & 0x80) == 0x80) { // 1NNNNNNN //类别为1,经过readIndexedHeader解析整型index。 int index = readInt(b, PREFIX_7_BITS); //经过index获取完整头部字段 readIndexedHeader(index - 1); } else if (b == 0x40) { // 01000000 //01000000表明KV均为原字段,解析字符串依次获取K值、V值,并插入动态表中 readLiteralHeaderWithIncrementalIndexingNewName(); } else if ((b & 0x40) == 0x40) { // 01NNNNNN //01xxxxxx表明K值为索引,V值为原字符串,依次解析整型index与字符串,并插入动态表中 int index = readInt(b, PREFIX_6_BITS); readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1); } else if ((b & 0x20) == 0x20) { // 001NNNNN //类别为001,含义是更新动态列表容量 maxDynamicTableByteCount = readInt(b, PREFIX_5_BITS); if (maxDynamicTableByteCount < 0 || maxDynamicTableByteCount > headerTableSizeSetting) { throw new IOException("Invalid dynamic table size update " + maxDynamicTableByteCount); } adjustDynamicTableByteCount(); } else if (b == 0x10 || b == 0) { // 000?0000 - Ignore never indexed bit. //这个类别表明KV均为原字符串,依次解析字符串,并不对解析后的KV值插入动态表。 readLiteralHeaderWithoutIndexingNewName(); } else { // 000?NNNN - Ignore never indexed bit. //与上一类别相似,但K值为索引,V值为原字符串 int index = readInt(b, PREFIX_4_BITS); readLiteralHeaderWithoutIndexingIndexedName(index - 1); } } }

压缩效果

K值为“accept-encoding”、V值为“gzip, deflate”的头部字段在HTTP2中可经过索引值15代替,从而达到头部字段压缩的效果。

“accept-charset”头部字段则经过14表明头部K值,而Value值根据HPACK规则编码写入流中。

经过HPACK,一个头部字段变化较少的App,每一个头部字段将会缩减至4字节之内,压缩效果很是明显。

相关文章
相关标签/搜索