TLS1.3 概述—protocol

tls1.3
TLS1.3的最终版本,在2018年8月发布,它包含着很多不同以往版本的改进,相对于之前版本安全性以及性能具有极大的提高,同时它也具备了更多的扩展和握手模式,那么从实现完整TLS1.3结构的角度去学习TLS,我们应该从哪些方面入手呢?

什么是TLS


TLS代表传输层安全性,并且是SSL(安全套接字层)的后继者。TLS提供了Web浏览器和服务器之间的安全通信。连接本身是安全的,因为使用对称密码术对传输的数据进行加密。密钥是为每个连接唯一生成的,并且基于在会话开始时协商的共享机密(也称为TLS握手)。HTTP + TLS = HTTPS

TLS历史

history

TLS1.2


TLS1.2握手原理

tls1.2
握手的过程主要包括两部分:

  • 参数协商
    客户端向服务器发送client Hello消息,里面包含client所支持的参数(密码套件等等),还包含一些有用的参数(version、random、sessionId等等),server会从中选取自己支持的密码套件、版本,通过server Hello发送给client,其中也包含一个随机数还有一些其他字段。
  • 密钥交换
    之后server会通过Server Key Exchange消息向client发送自己用于协商的公钥,用于协商的算法是:ECDHE或DHE,同样client通过 Client Key Exchange 发送自己的公钥,这样双方都具有了彼此的公钥,用它们生成临时私钥。client在收到server的Certificate消息之后会验证server的身份,验证通过才会发送Client Key Exchange,其中还包含着pre-Masterkey,是对client生成的随机数加密得来,最后使用三个随机数生成Masterkey用于会话密钥。

观察图片我们可以看出,TLS1.2整个握手过程需要2个RTT时间,而且每次握手都用到了非对称加密算法签名或者解密的操作,比较耗时和耗 CPU,每次都要传输证书,证书比较大会消耗带宽。

  • RTT
    Round-Trip Time,往返时延,在计算机网络中它也是一个重知要的性能指标,它表示从发送端发道送数据开始,到发送端收到来自接收端的确认版(接收端收到数据后便立即发送确认),总共经历的时延。

TLS1.2会话恢复

  • SessionID
    将协商好的会话参数缓存在客户端与服务器中,client下次握手时会带上上次握手的SessionID,server对其查询,若存在直接复用。
  • SessionTicket
    server将协商好的会话参数和密钥加密发送给客户端,client下次握手会将这个SessionTicket带上,如果server解密成功就复用上次的会话参数和密钥。

在TLS1.3中没有了SessionID这种会话恢复模式,但是在client Hello中还会存在该字段,主要是为了兼容版本。并且在SessionTicket模式中,添加了Ticket age,指的是会话是存在时间限制的,如果超过了该时间,那么也就不能进行会话恢复。

sessionid
我们可以看到,使用SessionID恢复会话的时候,需要花费1个RTT的时间,在TLS1.3中一定情况下恢复会话只需要花费0个RTT!

在握手的过程中很多数据都会临时计算,如果我们把这些数据提前计算出来,然后存入扩展当中,这样就可以减少握手的时间为1个RTT,TLS1.3实现的主要思想就是这样的。

TLS1.3


TLS1.3握手

更快的访问速度
TLS1.2handshake
这是一张TLS1.2的握手过程图片,前面也分析过,它需要2个RTT的时间才能完成整个握手过程,下面我们看一下TLS1.3的握手过程图:

tls1.3extension
我们会发现,其中存在一些以前版本从来没有出现过的extension,比如:key_share、signature_algorithms等等,这只是一部分,还包含很多扩展,我会一一细说。正因为这些扩展才使得TLS1.3的握手速度大大提高。

注:

  • +:上一消息的扩展消息
  • *:可选发送
  • {}:用握手层流密钥加密
  • []:用流密钥加密

client Hello

当client第一次连接server的时候,它需要向server发送client Hello 消息。

clientHello消息的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
  uint16 ProtocolVersion;
      opaque Random[32];

      uint8 CipherSuite[2];    /* Cryptographic suite selector */

      struct {
          ProtocolVersion legacy_version = 0x0303;    /* TLS v1.2 */
          Random random;
          opaque legacy_session_id<0..32>;
          CipherSuite cipher_suites<2..2^16-2>;
          opaque legacy_compression_methods<1..2^8-1>;
          Extension extensions<8..2^16-1>;
      } ClientHello;

简单介绍一下比较重要的几个字段的含义:

  • legacy_version
    在 TLS 以前的版本里,这个字段被用来版本协商和表示 Client 所能支持的 TLS 最高版本号。经验表明,很多 Server 并没有正确的实现版本协商,导致了 “version intolerance” —— Sever 拒绝了一些本来可以支持的 ClientHello 消息,只因为这些消息的版本号高于 Server 能支持的版本号。在TLS1.3中,设置了一个supported_version的扩展来表明client所支持的版本。legacy_version 字段必须设置成 0x0303,这是 TLS 1.2 的版本号。在 TLS 1.3 中的 ClientHello 消息中的 legacy_version 都设置成 0x0303,supported_versions 扩展设置成 0x0304。主要是为了兼容之前的TLS版本。
  • legacy_session_id
    前面我也提到过,TLS1.3中不再使用SessionID进行会话恢复,这一特性已经和预共享密钥PSK合并了,设置这个字段的意义,主要也是为了兼容之前版本,如果 Client 有 TLS 1.3 版本之前的 Server 设置的缓存 Session ID,那么这个字段要填上这个 ID 值。兼容模式下,这个值必须是非空的,所以如果Client不能提供之前版本的值,那么需要重新生成一个32字节的值。

还有cipher_suites、legacy_compression_methods,包含的是Client支持的密码套件和压缩算法,压缩算法TLS1.3也已经不再支持了,这个字段主要还是为了兼容版本,对于每个 ClientHello,该向量必须包含一个设置为 0 的一个字节,它对应着 TLS 之前版本中的 null 压缩方法。

supported_groups

这个扩展表明了 Client 支持的用于密钥交换的命名组。按照优先级从高到低。这个扩展中的 “extension_data” 字段包含一个 “NamedGroupList” 值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 enum {

          /* Elliptic Curve Groups (ECDHE) */
          secp256r1(0x0017), secp384r1(0x0018), secp521r1(0x0019),
          x25519(0x001D), x448(0x001E),

          /* Finite Field Groups (DHE) */
          ffdhe2048(0x0100), ffdhe3072(0x0101), ffdhe4096(0x0102),
          ffdhe6144(0x0103), ffdhe8192(0x0104),

          /* Reserved Code Points */
          ffdhe_private_use(0x01FC..0x01FF),
          ecdhe_private_use(0xFE00..0xFEFF),
          (0xFFFF)
      } NamedGroup;
 struct {
          NamedGroup named_group_list<2..2^16-1>;
      } NamedGroupList;

key_share

这个扩展我觉得是TLS1.3的重大改变,它里面包含了Client对应于supported_groups中参数的公钥集,如果使用了曲线,则会表明所使用的曲线以及对应的公钥。

1
2
3
4
   struct {
          NamedGroup group;
          opaque key_exchange<1..2^16-1>;
      } KeyShareEntry;

  • group:
    要交换的密钥的命名组。
  • key_exchange:
    密钥交换信息。这个字段的内容由特定的组和相应的定义确定。主要包含特定组的公钥等信息。

在 ClientHello 消息中,“key_share” 扩展中的 “extension_data” 包含 KeyShareClientHello 值:

1
2
3
  struct {
          KeyShareEntry client_shares<0..2^16-1>;
      } KeyShareClientHello;
  • client_shares:
    按照 Client 偏好降序顺序提供的 KeyShareEntry 值列表。

在golang中该结构的实现:

1
2
3
4
5
6
7
8
9
10
type KeyShareEntry struct {
    group           NamedGroup
    length          uint16
    keyExchange     []byte
}

type KeyShareClientHello struct {
    length          uint16
    clientShares    []KeyShareEntry
}

如果我们只实现椭圆曲线的话,首先需要选择要使用的椭圆曲线,之后再选取随机数生成公钥,将公钥存入keyExchange字段中,也就是说这个扩展已经将Client用于协商会话密钥的参数提前计算出来,并存储了起来,这与之前版本的形式server先选择参数,然后发给Client,然后Client再计算相比较,极大的节省了时间和握手过程中占用的CPU。

Client 可以提供与其提供的 support groups 一样多数量的 KeyShareEntry 的值。每个值都代表了一组密钥交换参数。例如,Client 可能会为多个椭圆曲线或者多个 FFDHE 组提供 shares。每个 KeyShareEntry 中的 key_exchange 值必须独立生成。Client 不能为相同的 group 提供多个 KeyShareEntry 值。Client 不能为,没有出现在 Client 的 “supported_group” 扩展中列出的 group 提供任何 KeyShareEntry 值。Server 会检查这些规则,如果违反了规则,立即发送 “illegal_parameter” alert 消息中止握手。

当选用PSK密钥协商模式时,即使在supported_groups中不存在支持的算法也不会终止握手,这时候,server会向Client发送和serverhello具有相同结构的消息:HelloRetryRequest,它的目的主要是想让Client作出一些改变以使得握手正常进行,在这种情况下,在 HelloRetryRequest 消息中,“key_share” 扩展中的 “extension_data” 字段包含 KeyShareHelloRetryRequest 值。

1
2
3
    struct {
          NamedGroup selected_group;
      } KeyShareHelloRetryRequest;
  • selected_group
    表明server所选择的NamedGroup中组

Client收到此消息之后也会对其进行验证,selected_group是否在NamedGroup中出现了,selected_group 没有在原始的 ClientHello 中的 “key_share” 中出现过。如果上面 的检查都失败了,那么 Client 必须通过 “illegal_parameter” alert 消息来中止握手。否则,在发送新的 ClientHello 时,Client 必须将原始的 “key_share” 扩展替换为仅包含触发 HelloRetryRequest 的 selected_group 字段中指示的组,这个组中只包含新的 KeyShareEntry。

在 ServerHello 消息中,“key_share” 扩展中的 “extension_data” 字段包含 KeyShareServerHello 值。

1
2
3
  struct {
          KeyShareEntry server_share;
      } KeyShareServerHello;
  • server_share:
    与 Client 共享的位于同一组的单个 KeyShareEntry 值。

我们再来看一下ECDHE的参数,对于 secp256r1,secp384r1 和 secp521r1,内容是以下结构体的序列化值:

1
2
3
4
5
      struct {
          uint8 legacy_form = 4;
          opaque X[coordinate_length];
          opaque Y[coordinate_length];
      } UncompressedPointRepresentation;

对端还要验证对方的公钥以确保为有效的点:

  • 验证公钥不是无穷大点
  • 两个整数x、y中间有正确的间隔
  • x、y是椭圆曲线方程的正确的解

小结

TLS 1.3 中优化握手:

  • client发送clientHello(extension)消息,extension中的support_groups中携带client支持的椭圆曲线的类型,并且在扩展key_share中计算出了相对应的公钥,一起发送给server
  • server收到clientHello后会首先选择相应的椭圆曲线参数计算自身的公钥,从key_share扩展中提取相应的公钥作为密钥协商的参数,计算主密钥,并且把自身计算出的公钥放到serverHello的扩展key_share中,然后发送serverHello等消息给client,Client从key_share中取出公钥计算主密钥。

TLS1.3会话恢复

在本文前面我提到过TLS1.3已经不再使用SessionID进行会话恢复了,现在主要使用的是SessionTicket进行会话恢复,但是又不同于TLS1.2中使用SessionTicket进行会话恢复的过程,也做出了一些改变,或者说是进行了一些更新(PSK)。

tls1.3handshake
会话恢复所花费的时间是1个RTT,这与整个的握手时间是一样的。在TLS1.3中采用的会话恢复机制是PSK它与SessionTicket有些类似,Client通过PSK发送被server加密的会话缓存参数,如果server解密成功就可以直接复用会话,不需要再重新传输证书和协商密钥了。

密钥交换模式

  • PSK-Only
  • (EC)DHE
  • PSK with (EC)DHE(暂时还没出现)

PSK handshake

在使用PSK密钥交换模式时我们首先要了解几个ClientHello的其它扩展:

Pre-Shared Key Exchange Modes

为了使用PSK,client还需要发送Pre-Shared Key Exchange Modes扩展,它的含义是Client 仅支持使用具有这些模式的 PSK,这就限制了在这个 ClientHello 中提供的 PSK 的使用,也限制了 Server 通过 NewSessionTicket 提供的 PSK 的使用。
如果Client提供了 pre_shared_key扩展,那么就必须提供该扩展

1
2
3
4
5
  enum { psk_ke(0), psk_dhe_ke(1), (255) } PskKeyExchangeMode;

      struct {
          PskKeyExchangeMode ke_modes<1..255>;
      } PskKeyExchangeModes;
  • psk_ke:
    仅 PSK 密钥建立。在这种模式下,Server 不能提供 key_share 值
  • psk_dhe_ke:
    PSK 和 (EC)DHE 建立。在这种模式下,Client 和 Server 必须提供 key_share值。

这样的话就可以进行模式选择,并作出相应的改变。未来分配的任何值都必须要能保证传输的协议消息可以明确的标识 Server 选择的模式。目前 Server 选择的值由 ServerHello 中存在的 key_share 表示。

Pre-Shared Key

该扩展是用来协商标识的,该标识是与PSK密钥相关联的给定握手所使用的预共享密钥的标识。或者说是New Session Ticket+binders,由于在TLS1.3中,New Session Ticket可以在握手结束后可能多次发送,所以Pre-Shared Key可能会存储多组对应的值,下面我们具体来了解一下它的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct {
          opaque identity<1..2^16-1>;
          uint32 obfuscated_ticket_age;
      } PskIdentity;

      opaque PskBinderEntry<32..255>;

      struct {
          PskIdentity identities<7..2^16-1>;
          PskBinderEntry binders<33..2^16-1>;
      } OfferedPsks;

      struct {
          select (Handshake.msg_type) {
              case client_hello: OfferedPsks;
              case server_hello: uint16 selected_identity;
          };
      } PreSharedKeyExtension;
  • identity:
    一个预共享密钥的标签。
  • obfuscated_ticket_age:
    SessionTicket的寿命的混淆版本,为了防止一些相关连接的被动观察者。而在TLS1.2中是不存在这样的字段,即不会标识出客户端已存在的时间,server收到后主要靠里面的内容来判断Ticket是否过期,而在TLS1.3中就增加了这样一个字段来表示Ticket的寿命,因为是明文传输所以会被观察者发现,所以给时间加了一些调味品,是New Session Ticket中的ticket_age_add,因为New Session Ticket本身就是被加密的,所以这个ticket_age_add只有通信两端才知道。
  • 混淆的方法:
    用 ticket 时间(毫秒为单位)加上 “ticket_age_add” 字段,最后对 2^32 取模。注意,NewSessionTicket 消息中的 “ticket_lifetime” 字段是秒为单位,但是 “obfuscated_ticket_age” 是毫秒为单位。
  • identities:
    Client 愿意和 Server 协商的 identities 列表,其内容就是NewSessionTicket中的ticket部分。如果和 early_data 一起发送,第一个标识被用来标识 0-RTT 的。有关early_data后面还会说到。
  • selected_identity:
    server选择的标识,是server在自己的 Pre-Shared Key扩展中自己设置的选择的标识,表明正常解析了Client的扩展,其实选择的话就是一个序号。

0-RTT

前面已经提到过TLS1.3已经将握手时间优化到了1-RTT,对比之前版本的速度已经快了很多,但是TLS1.3最终极的做法是0-RTT,即握手的时间是0-RTT。

Client发送ClientHello消息,除了在PSK模式中提到的那些扩展外,还应该具有一个early_data扩展,同理server发送serverHello中也应该包括该扩展,并表示其愿意接受early_data,client发送完early_data后,发送End_Of_Early_Data报文表示client自己发送完了early_data。

如果 Server 提供了 early_data 扩展,Client 必须验证 Server 的 selected_identity 是否为 0。如果返回任何其他值,Client 必须使用 “illegal_parameter” alert 消息中止握手。下面我们看一下它的结构:

1
2
3
4
5
6
7
8
9
  struct {} Empty;

      struct {
          select (Handshake.msg_type) {
              case new_session_ticket:   uint32 max_early_data_size;
              case client_hello:         Empty;
              case encrypted_extensions: Empty;
          };
      } EarlyDataIndication;

其中的max_early_data_size字段表明,允许Client发送的最大0-RTT的数据量。

发生错误会导致0-RTT降级到1-RTT。

New Session Ticket

Post-Handshake Messages在 Server 接收到 Client 的 Finished 消息以后的任何时刻,它都可以发送 NewSessionTicket 消息。此消息在 ticket 值和从恢复主密钥派生出来的 PSK 之间创建了唯一的关联。

1
2
3
4
5
6
7
   struct {
          uint32 ticket_lifetime;
          uint32 ticket_age_add;
          opaque ticket_nonce<0..255>;
          opaque ticket<1..2^16-1>;
          Extension extensions<0..2^16-2>;
      } NewSessionTicket;
  • ticket_lifetime:
    这个字段表示 ticket 的生存时间,这个时间是以 ticket 发布时间为网络字节顺序的 32 位无符号整数表示以秒为单位的时间。Server 禁止使用任何大于 604800秒(7 天)的值。值为零表示应立即丢弃 ticket。无论 ticket_lifetime 如何,Client 都不得缓存超过 7 天的 ticket,并且可以根据本地策略提前删除 ticket。Server 可以将 ticket 视为有效的时间段短于 ticket_lifetime 中所述的时间段。
  • ticket_age_add:
    安全的生成的随机 32 位值,用于模糊 Client 在 “pre_shared_key” 扩展中包含的 ticket 的时间。Client 的 ticket age 以模 2 ^ 32 的形式添加此值,以计算出 Client 要传输的值。Server 必须为它发出的每个 ticket 生成一个新值。
  • ticket_nonce:
    每一个 ticket 的值,在本次连接中发出的所有的 ticket 中是唯一的。初始值是0,发送一个则++
  • ticket:
    这个值是被用作 PSK 标识的值

TLS1.3一些其他扩展和机制

降级保护机制

主要通过随机数来实现,当协商TLS1.2或更老的版本,为了响应ClientHello在random后8个字节填入特定的随机值,若为TLS1.2则后8个字节的值为:

1
44 4F 57 4E 47 52 44 01

supported_version

主要功能的话前面也有提到,对Client标明所支持的TLS版本,对Server标明正在使用的TLS版本,如果协商TLS之前的版本,这个扩展必须带上,若不存在该扩展,server要协商之前的版本,则中止握手,若存在server将禁止使用ClientHello中的legacy_version作为版本协商的值,只能使用supported_versions中的值。
对于server:

  • 版本
  • 版本>=TLS1.3则必须发送supported_version扩展,还要设置serverHello.legacy_version为0x0303,若扩展存在Client会忽略serverHello.legacy_version,而去读取supported_version的值。
1
2
3
4
5
6
7
8
9
    struct {
          select (Handshake.msg_type) {
              case client_hello:
                   ProtocolVersion versions<2..254>;

              case server_hello: /* and HelloRetryRequest */
                   ProtocolVersion selected_version;
          };
      } SupportedVersions;