从TLS看安全协议设计(上)

在开始正文前,我想先谈谈为什么要写这篇文章,为什么要对TLS协议进行分析?

打开网站时我们经常能看到左上角的小绿锁,而TLS正是隐藏在后面的原由。对于大部分程序员,多多少少都可以聊一聊TLS的几分样貌,但是能真正的从其整体发散到细节,从理念落地到实践,以更高的角度去理解TLS是很少的。因此这里想要从我个人的角度,去理清TLS,解读这个将密码科学和软件工程良好结合的优秀设计,也希望能够给读者有所帮助。

设计理念

为什么需要TLS?

在不存在SSL/TLS的互联网中,消息通过例如HTTP明文传递,作为一个攻击者可以截获任意的包,那么他就可以获取报文中的信息,并对报文进行修改,伪装成对端进行窃听或者控制传输。

在上文的描述中,我们可以看出明文传输存在的最显著问题:数据没有加密,消息易被篡改,身份能被伪造。这也对应着信息安全中的机密性完整性身份认证

除了这三个最明显的问题,还有其他很多待解决的问题,例如如何确保用户不能抵赖之前发送的消息,即不可否认性;如何在设计时,降低所需的传输时间,提升性能;如何做到协议的通用性可拓展性。这一系列的问题缺乏一个统一的解决方案,很多情况下仅是通过应用层的一些手段去不完善的处理,这些需求也就促成了SSL/TLS的诞生与发展。

围绕着这些需求,从90年代SSL1.0设计之初,一直走到了08年TLS1.2版本正式发布,整个协议才趋于成熟,又伴随着安全研究的升级对抗以及对性能提升的要求,1.3版本也在几年的商讨中于18年诞生。而这几年业界也是逐渐从1.2版本向1.3升级。当然因为历史遗留原因目前主流的还是1.2版本,因此我的分析也将从1.2版本开始,再迈向1.3,为读者浅析TLS的前世今生。

预备知识

在阅读这一部分时,可以不先完全理解其细节,在浏览后面章节必要时,再返回本章进行对照理解。

因为TLS是建立在密码学之上的协议,想要将TLS理解清楚,对密码学应有一定了解,但是具体深入密码学的细节不是本文重点所在,所以我简单归纳了几个需要了解的点和一些特性,实现细节的话请参看所给资料链接或自行搜索。

密码学基本工具

  • 对称加密:加密和解密时使用同一密钥,常用有AES,(DES,RC4等都被证明不安全了),然后还涉及到加密模式,例如流式加密(RC4使用的就是流式),块加密模式(CBC等模式)。加解密效率高,密钥配送管理不便
  • 非对称加密:公私钥体系。常见的有RSA(基于大整数的质因数分解难题)和ECC(基于椭圆曲线上的离散对数难题),以及DH(基于有限域上的离散对数难题),后两者在使用过程中类似一个协商,而RSA更类似一个单方面的加密传输过程。加解密效率低,密钥派发管理方便
  • 单向散列函数:输入消息不同,输出Hash值不同,保证消息的一致性完整性
  • 消息认证码MAC:比单项散列函数多了个共享秘钥,保证消息的一致性完整性,并且通过密钥加解密确保身份认证。但是因为使用的是共享秘钥不能“第三方证明”和“防抵赖”。
  • 数字签名:利用私钥对hash值签名,公钥认证。保证消息的一致性完整性认证,并且能防止抵赖
  • 伪随机数生成器:模拟产生随机数列的算法。

以上就是常用的密码学工具,在接下来的流程中会分别涉及到,如果对相关的工具有困惑可以自行查阅资料。

RSA ECC DH

这里想要对最常用的非对称加密算法做个简单的介绍,因为后面讲解时会利用到其特性。

对于常用的非对称加密算法,可以简单的分为两种类型,一种是基于RSA,一种是类DH(包括原生的DH和基于ECC的DH,即ECDH),简单来看一下两者的使用和区别。

  • RSA

    基于大整数进行质因数分解的困难,具体原理可以看阮一峰的《RSA算法原理》,这里说下使用方法:

    存在一对公私钥:公钥(E,N),私钥(D,N),明文加密和密文解密类似下列的运算

    密文 = 明文^E mod N

    明文 = 密文^D mod N

    使用的过程类似:

    服务端下发公钥给客户端,客户端利用公钥加密一段会话数据后返回给服务端,服务端利用自己的私钥进行解密。

  • DH(ECDH)

    分为两种,一种是基于有限域上的离散对数问题,即DH类(Diffie-Hellman),具体原理自行搜索,这里只做口语化表达:

    已知x和G,求G^x mod p是简单的;反过来已知G^x mod p和G,求x是困难的。

    第二种是基于椭圆曲线上的离散对数问题,即ECDH类,口语化表达类似:

    已知x和G,求xG是简单的(椭圆曲线上的运算,不是简单的数乘);反过来已知xG和G,求x是困难的。

    所以一般我们把x作为私钥,xG(G^x)为公钥

    使用的过程类似:

    服务端拥有私钥a,客户端拥有私钥b,第一次服务端发送公钥aG和参数G,客户端接受到后计算bG,发送给服务端,此时客户端拥有aG和b,服务端拥有bG和a,双方经过计算a(bG)=b(aG)=abG,得出相同的会话数据,但此时中间人只能截获aG,bG和G,无法求出abG,因为想要求出的话记必须通过aG和G(bG和G)计算出a(b),但是从上文的条件可以看出这是困难的,由此这就是DH类的原理。

我们通过以上的简介不难发现一点,两类算法最大的区别就是RSA做加密传输的时候,整个的会话数据是由客户端决定的,而服务端没有参与到生成的过程,更类似于一个加密传输的过程。而DH类客户端和服务端双方都需要参与到生成的过程中,单方面是无法决定会话数据的,也就是说相较于RSA,DH(ECDH)更类似一个密钥协商的过程。

针对上面的特性,也就衍生出下一个需要关注问题——前向安全,这也是为什么TLS1.3会抛弃RSA作为密钥协商的算法。

前向安全

官方定义是:长期使用的主密钥泄漏不会导致过去的会话密钥泄漏。简单点来说就是我的长期持有的私钥泄漏了不会对之前的会话消息造成影响,那么对于上述两种算法,其又有什么不同呢?

显然,对于RSA来说,如果我们一直使用同一对公私钥,当私钥泄漏时,攻击者就可以轻易解密之前的会话消息,也就是说RSA时不具有前向安全的。不可能每次会话都更新RSA公私钥,因为每次更新下发公钥是不现实的,效率太低了,不可接受。

对于DH(ECDH),如果双方每次通话的私钥a,b都是固定的,那么生成的abG也是固定的,如果泄漏,也无法保证前向安全。但是我们可以通过每次会话时都利用随机数发生器生成临时的a和b(这个效率是可接受的),这样每次计算出来的结果都不一样,这就保证了前向安全,这也就是DHE和ECDHE,其中E表示的就是临时的意思。

基于上述拥有的密码学工具,以及相应的设计目的和需求,我们应该怎么设计呢,假如你是一个协议开发者,你如何协调使用好所拥有的能力,完成一个精巧的架构呢?让我们先来看看TLS是如何操作的,由全局到细节,由问题推方法,我们来细细品味。

协议架构

TLS1.2主要分为两个层级,上层的握手层(以及一些辅助协议,例如密钥切换通知,告警协议)和下层的记录层。

首先为什么TLS会设计成分层架构呢?最主要的原因就是公钥加密算法和对称加密算法的优劣性。当使用对称密钥的方式进行加密时,双方持有相同密钥,根据选定算法对信息加密,然后传输给对端进行解密。整个加解密的过程建立在双方持有相同的密钥,但是对于互联网多方交互的环境下,是不可能做到服务端与每个客户端直接持有一个对称密钥的,无论是维护密钥的成本,还是密钥派发时的安全保证,都是不可接受的。

因此为了解决密钥的派发管理问题,一般是采用公钥加密的方式,服务端将公钥派发出去,客户端利用公钥加密信息后返回给服务端,服务端再用自己的私钥进行解密。但是在真正设计时并没有选择完全使用两端采用公钥加密的方式进行加密沟通,因为公钥加解密的速度是非常慢的。

综合以上两点,TLS分为两层,握手层采用公钥加密的方式协商一段数据作为加密素材,传递给记录层做为对称加密的密钥材料,记录层生成对称密钥加密应用数据,既解决了密钥管理的问题,也保证了效率。同时这也有利于解耦各种需求,并化简整个问题的复杂度,将TLS分层模块化,每一部分完成一定的需求,并让之后的升级更新更加方便。

推演思路

了解了TLS最基本的架构后我们按照握手层和记录层的顺序进行推理分析,由需求得出方案,再根据连贯的问题,补充完善之前的方案。

握手层

先从握手从来看,对于握手层,需要完成的最基本需求是利用非对称加密完成“材料”数据的传输,如客户端C向服务端S发送会话请求,服务端同意后下发自己的公钥S_Pub,客户端利用公钥加密数据Data传输给服务端,服务端利用自己的私钥S_Pri解密,得到数据。

但是以上步骤存在一个问题就是假设存在一个可以截取数据的中间人,是无法保证对端身份可信的。例如下图中间人M截获S_Pub,替换为自己的公钥M_Pub,客户端使用M_Pub作为公钥加密后发送数据,中间人能用自己的M_Pri解密并修改数据,再用S_Pub加密后发送给服务端,从而造成中间人攻击。

之所以产生这个问题是因为缺乏身份认证,解决身份认证问题一般是利用消息认证码MAC或数字签名。对于消息认证码需要对称密钥,此时还不存在所以选用数字签名进行认证。假设数字签名的公私钥对位Sign_Pub和Sign_Pri,利用Sign_Pri对S_Pub进行数字签名,客户端利用Sign_Pub对数字签名进行认证(中间人是无法阻止客户端获取到Sign_Pub的)。

但是又如何确保数字签名的公钥是可信的呢?TLS给出的方案就是PKI证书体系,建立可信第三方CA,然后在客户端预装可信CA的公钥(存在一个证书链体系,不是只有一个CA)。服务端通过下发证书,客户端通过预装公钥验证证书签名以及一些其他信息,确认了服务端身份,这样就能解决身份认证的问题并且保证了服务器公钥的完整性。TLS还支持双向认证,即服务端请求验证客户端身份,但一般只需要验证一方就可以解决中间人问题了。

记录层会通过握手层传输过来的数据Data生成用于加密应用数据的对称密钥,在此之前为了保证刚刚握手的完整性,还需要做消息校验并用协商生成的对称密钥加密,作为双方第一个对称加密的包。通过这个消息就能验证协商出的对称密钥是否正确,以及之前握手消息没有被篡改。之后记录层就可以正式加密传输应用数据了。

完成这样一次会话过程后,我们还需要思考一个问题:每次会话都需要重新生成新的密钥,这样是否很浪费,我们是否能复用之前的密钥。由此TLS产生了会话恢复机制,包括使用Session_id和Session_ticket两种手段。

以上就是从我个人理解的角度,观察TLS握手层一步一步的设计缘由,当然这其中隐藏了很多具体的实现细节,比如每一步的密钥是如何生成的,应该如何使用,如何减少私钥泄漏后的影响,为什么会话恢复有两种模式等等很多有意思的问题,在接下的分析中我会挑选一些我认为比较重要的进行解析。

总结下以上步骤,整体思路如下图:

记录层

对于记录层步骤主要为分段,压缩(可选,因为存在安全问题一般不开启),加密和消息认证MAC(存在三种模式,stream ,block,aead),添加消息头。在这一层会利用握手层传输的密钥材料,生成对称密钥,MAC密钥等必备参数。完成对数据的对称加密和生成消息认证码(AEAD模式将两者结合起来了),这就是这一层做的最主要的事情。

实现细节

握手层冷启动

通过上面的推演,我们大致了解了每一层的目的以及大致的步骤,这里我们来看一下具体的实现。

对于第一次冷启动的过程:

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

ClientHello -------->
ServerHello
Certificate*
ServerKeyExchange*
CertificateRequest*
<-------- ServerHelloDone
Certificate*
ClientKeyExchange
CertificateVerify*
[ChangeCipherSpec]
Finished -------->
[ChangeCipherSpec]
<-------- Finished
Application Data <-------> Application Data
*表示可选发送
[]表示独立消息,这里为了防阻塞单独通知密钥协议已切换,有冗余在1.3中删除
  1. 客户端发送ClientHello携带自己支持的加密组件以及随机值,服务端收到后选择支持的加密组件并携带服务端随机值返回一个ServerHello消息。
  2. 之后服务端发送Certifacate消息,包含证书信息,以及当Certificate信息不足时,发送ServerKeyExchange消息包含补充信息。
  3. 因为TLS支持双向认证,如果服务端需要对客户端身份进行认证的话,就会发送CertifacateRequest消息。
  4. 服务端发送ServerHelloDone表明自己已经发送完成。
  5. 当客户端收到CertificateRequest请求后,如果支持认证就发送Certificate消息包含自己的证书。如果没有收到验证请求,就不发送此条消息。
  6. 客户端发送ClientKeyExchange,交换密钥材料。例如使用RSA,就会生成由46字节的随机值和2字节的版本号组成的Pre_master_secret,使用服务端公钥加密后,在本条消息包含发送。
  7. 如果客户端发送了Certificate消息,就需要发送一条CertificateVerify消息来对之前的握手做Hash并用自己的证书私钥进行签名,表明自己确实拥有客户端证书私钥以验证身份,并保证了之前握手消息未被篡改。
  8. 之后会发送ChangeCipherSpec通知对端已经切换密钥了(为了防阻塞不在本消息流中)
  9. 客户端发送Finished消息,通过结合之前握手消息的Hash,Master_secret等生成验证数据,并利用协商出来的Session_key加密传输给对端。Finished消息是第一条使用协商出来的对称密钥加密的消息,通过这条消息验证了密钥的正确性以及之前握手消息未被篡改。
  10. 同样服务端也发送Finished消息,注意在做Hash时的握手消息包含上条客户端发送的Finished消息。
  11. 双方通过握手以及密钥派生计算已经拥有了相同的Session_key,可以进行正式的应用数据加密传输了。

握手层热启动

分为Session_id和Session_ticket两种。第一种使用Session_id,发送ClientHello时会客户端会携带会话唯一标示,服务端同意后发送ServerHello以及Finished,根据保存的主密钥Master_secrert等信息以及新的随机值重新计算会话密钥来进行应用数据加密。

1
2
3
4
5
6
7
8
9
10
Client                                                Server

ClientHello -------->
ServerHello
[ChangeCipherSpec]
<-------- Finished
[ChangeCipherSpec]
Finished -------->
Application Data <-------> Application Data

但是因为存在分布式Session同步以及存储性能的限制,一般选用下面的Session_ticket手段,这十分类似HTTP中的Cookie和Session机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
第一次:
ClientHello
(empty SessionTicket extension)-------->
ServerHello
(empty SessionTicket extension)
Certificate*
ServerKeyExchange*
CertificateRequest*
<-------- ServerHelloDone
Certificate*
ClientKeyExchange
CertificateVerify*
[ChangeCipherSpec]
Finished -------->
NewSessionTicket
[ChangeCipherSpec]
<-------- Finished
Application Data <-------> Application Data
第二次:
ClientHello
(SessionTicket extension) -------->
ServerHello
(empty SessionTicket extension)
NewSessionTicket
[ChangeCipherSpec]
<-------- Finished
[ChangeCipherSpec]
Finished -------->
Application Data <-------> Application Data

可以看到在第一次中服务端会发送一个NewSessionTicket消息,将会话信息加密并MAC在其中,客户端在第二次中携带ticket,服务端成功解密并验证完整性后,如果同意就会直接省略密钥协商的过程,从而节省了传输时间。

记录层

从握手层得到数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct {
ConnectionEnd entity;
PRFAlgorithm prf_algorithm;
BulkCipherAlgorithm bulk_cipher_algorithm;
CipherType cipher_type;
uint8 enc_key_length;
uint8 block_length;
uint8 fixed_iv_length;
uint8 record_iv_length;
MACAlgorithm mac_algorithm; /*mac 算法*/
uint8 mac_length; /*mac 值的长度*/
uint8 mac_key_length; /*mac 算法密钥的长度*/
CompressionMethod compression_algorithm;
opaque master_secret[48];
opaque client_random[32];
opaque server_random[32];
} SecurityParameters;

利用PRF密钥派生函数生成以下数据用来加密和MAC。同时因为根据密码学的研究,客户端写/服务端读,服务端写/客户端读,这两种流向是不能使用同一个会话密钥的,会产生安全问题,所以生成两组write key。

1
2
3
4
5
6
7
client write MAC key*
server write MAC key*
client write encryption key
server write encryption key
client write IV
server write IV
*表示可选,AEAD模式无需MAC

记录层的流程为数据分段,压缩(可选,因为存在安全问题一般禁用),加密和完整性保护,添加消息头。

我们这里重点关注加密和完整性保护。在密码学中存在三种加密和MAC的组合方式。

  • Encrypt-and-MAC:明文加密和明文MAC拼接在一起。
  • MAC-then-Encrypt:对明文MAC后拼接在明文后,对整个进行加密。
  • Encrypt-then-MAC:对明文加密后在对密文MAC,将MAC内容拼接在密文后。

TLS中采用的是第二种,但是随着发展发现了很多问题,例如Padding Oracle漏洞等。对于具体的做法又分为块模式+Hmac,流模式+Hmac,以及AEAD模式。块和流模式采用的是MAC then encrypt,之后为了从密码学上直接解决这个问题,密码学专家便提出了将加密和完整性保护融合在一个算法中即AEAD,彻底解决以上问题,因此更推荐使用AEAD模式例如aes-256-gcm。

密钥派生函数与密钥变化

以上就是1.2的基本流程,针对密钥这一块需要单独强调一下。

我们先看下PRF函数,它通过输入的材料可以通过递归的Hash生成无限长的随机输出,我们可以截取任意长度作为我们的产出需求,看一下具体的表现:

1
2
3
4
5
6
7
 P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) +
HMAC_hash(secret, A(2) + seed) +
HMAC_hash(secret, A(3) + seed) + ...

其中:
A(0) = seed
A(i) = HMAC_hash(secret, A(i-1))

因此在TLS中的表现形式就为:

1
PRF(secret, label, seed) = P_<hash>(secret, label + seed)

我们有了这样一个派生密钥的工具后,我们在来看下具体的密钥变化。在整个过程中主要涉及三个密钥:

Premaster_secret -> Master_secret -> Session_key

其中Premaster_secret是在握手交换时完成的,采用不同的密钥协商算法会有不同变化,如下:

  • RSA:客户端生成随机值作为Premaster_secret。
  • 静态DH,ECDH:服务端公钥包含在证书中,客户端看是否包含在证书,不在的话就用ClientKeyExchange中,通过计算得出Premaster_secret。
  • 动态DHE,ECDHE:公钥分别由服务端ServerKeyExchange,客户端ClientKeyExchange消息中,并计算得出Premaster_secret.

之后使用Premaster_seceret通过PRF函数生成48字节的Master_secret

1
2
3
master_secret = PRF(pre_master_secret, "master secret",
ClientHello.random + ServerHello.random)
[0..47];

Master_secret才是真正传递给记录层的,之后记录层利用Master_secret生成Session_key,然后切分为对称密钥,IV,MAC密钥来处理真正的应用数据。

1
2
3
4
key_block = PRF(SecurityParameters.master_secret,
"key expansion",
SecurityParameters.server_random +
SecurityParameters.client_random);

这些变化的理由,我个人思考如下:

  • 为什么不直接使用Premaster_secret生成Session_key呢?

    因为Premaster_secret的格式不同,RSA是48位,ECC类基于具体算法,我们想要得到固定格式的熵源,所以利用PRF函数计算出Master_secret,并且保证secret的随机性不仅仅受单一方影响,比如rsa做秘钥交换时仅仅是客户端发送48字节的值

  • 为什么不直接使用Master_secret加密呢?

    长度不够;利用主密钥生成会话密钥会让攻击者进行计算破解难度更大

更多问题限于篇幅请看我之前的文章:TLS相关FAQ

回顾与展望

说到这里,我们暂停一下我们的脚步,首先来回顾一下TLS1.2是怎么完成它开始的设计目标的。

  • 机密性:非对称和对称加密的结合使用。
  • 身份认证:证书体系的建立。
  • 完整性保护:握手阶段的CertificateVerify,Finished消息做校验,记录层对消息做MAC。
  • 性能:基于Session的会话恢复机制。
  • 通用性:凡是基于RFC文档实现的,理论上上都是可以做到多种设备的兼容,并且采用的证书体系也是全球通用的。
  • 可拓展性:在握手的会话中支持插件机制。

除了以上特性TLS在其他种种方面也是做到基本可用成型,做到了对网络传输安全的保障。当然,这些年来随之密码学的研究深入,以及技术迭代的发展,由此产生了更高的要求,TLS1.2在一些方面已经不能满足业界了,我们简单看下它的主要问题在哪:

  • 不安全的密码组件,模式和配置。例如RC4,MD5等一种算法被破解,块加密,流加密模式的不安全因素。
  • 伴随着斯诺登泄密事件,前向安全性更加被重视。
  • 1.2冷启动至少需要2RTT,热启动需要1RTT,对于很多短链接的场景下,耗时过长是不可接受的。

为了改善这些问题,1.3版本做了巨大的改变,甚至一定程度上放弃了前向兼容来保证架构的重新调整,让我们在一篇文章中看下TLS1.3究竟有何神奇之处。

总结

本来准备一篇文章写完整个TLS1.2和1.3的,但是发现就算省略了很多细节,仅仅是1.2也已经6000多字了,决定还是分上下两篇,方便读者阅读。

参考