密码学可以说是所有计算机学科中科学和工程结合的最紧密的学科之一,并且两者都极其重要。TLS协议作为现代互联网数据安全的基石,自1994年由Netscape首次推出以来(当时叫SSL)发展至今已经有快30年历史了,可以说汇集了现代密码学众多的理论研究成果和安全实践经验,学习TLS的设计一方面对了解现代密码学的应用具有很好的参考价值。另外一面,密码学领域内的轮子区别于其他领域内的轮子,作者需要具备严格的密码学学术训练和大量领域工程实践经验,代码本身也需要经过大量的同行审议,才能算作是可用,这个领域不提倡一言不合就自己造轮子,TLS协议这个有着多年发展迭代历史、经过大量研究人员审议,应该我们在绝大部分场景选择安全通信协议时候的不二选择。这篇blog将会深入TLS协议的细节,记录和学习TLS协议的设计和演进。需要说明的是想要深入学习TLS协议是有一定门槛的,读者需要具备一定的密码学背景知识。
随着内容的深入,后面我们可以一一看到TLS协议是怎么达成这些设计目标的。
在介绍TLS的密码学算法套件前先简单捋一下常见的密码学算法和作用,常见的算法主要有以下几种
工作原理是对固定大小的block的明文进行置换,生成同样大小的密文,明文和密文间是一一映射(置换的性质)。常见的有DES(基本不用了)、3DES、AES(比较主流)等等。
一种是原生的流加密算法,工作原理是通过生成一个任意长度的伪随机字节序列作为keystream, 将keystream和明文进行XOR得到密文。常见的有RC4, Salsa20、ChaCha20等。
还有一种就是由一种块加密算法通过某种构造模式进行组合,从而将明文流划分为一个个block,每个block应用block cipher加密,block间往往不是独立的,而是通过某种构造模式联系起来,常见的构造模式有:
块加密和流加密算法都是对称加密算法,即通信双方有着同样的通信密钥,这对于网络通信的涉及到双方而言,要么双方认识,在通信前就在线下提前确定好了通信密钥 (pre-shared key: PSK),要么就需要通过一种密钥交换算法即时协商出一个通信密钥,密钥交换算法工作的假设是在一个不安全的信道上协商出只有通信双方知道的密钥,所谓的不安全就是可能有窃听者或者中间人。
常见的密钥交换算法有:
RSA作为应用很广的非对称加密算法的典型被人们所熟知,它用来做密钥交换的基本原理是Alice生成一个通信对称密钥,并且用Bob的RSA公钥去加密,Bob收到后用自己的RSA私钥去解密,这样除了Alice和Bob,没人能知道通信的对称密钥,看上去很好。这里我吐槽一句,网上有一大堆介绍TLS的过时文章和blog,里面在介绍密钥协商的时候只提RSA这种方式,导致很多人印象中TLS都是以RSA这种方式来协商密钥的。我在过往的技术面试过程中,但凡问到候选人关于TLS密钥协商的内容,都是回答用非对称加密来加密对称加密的密钥,要知道这种做法已经没人在用了,不具备前向安全。
Diffie-Hellman协议是一种通用的密钥交换协议,通信的双方仅通过交换公钥来计算出一个共同的密钥。alice和bob都拥有对方的公钥和自己的私钥,通过这两者双方都能计算出一个共同的密钥。
最简单DH实现是Alice随机生成一个私钥a, 计算A=(G^a) mod P 作为公钥,其中P是很大的质数,G不需要很大,一般就是2或者5。假设另一方Bob生成的私钥是b,那么对应的公钥就是B=(G^b) mod P, 双方交换公钥后Alice计算 B^a mod P == G^(ab) mod P, Bob计算A^b mod P = G^(ab) mod P,于是得到共同的密钥。由于从公钥计算私钥在数学上是个难题(离散对数问题),因此第三方无法仅通过公钥计算得到通信密钥。需要指出的是DH协议本身不能防范MITM攻击,中间人完全可以分别和Alice和Bob进行DH协商出不同的密钥,因此DH需要配合身份认证机制一起才能安全工作,实际的DH交换主要有两种方式
原始DH的密码安全性在数学上是通过离散对数问题来保证的,但是基于离散对数问题的DH做到安全所需要的private key size通常很大。后来数学家们发现可以在一个椭圆曲线上定义一种加法运算(这里省略这个运算的定义),基于这个加法运算和一个零元可以定义出这个椭圆曲线上一个可交换群(省略定义)。数学上认为,给定椭圆曲线上一个点P和任意一个整数n,计算n * P = P + P + … + P = Q是相对容易的,反之对于一个定义在有限域上的椭圆曲线,给定P和Q,计算n是非常困难的。这个难题姑且称为有限域上椭圆曲线的离散对数问题,将这个难题和DH结合就是ECDH、ECDHE。由于这个上椭圆曲线的离散对数问题要比比普通的离散对数问题要困难很多,因此在同等安全性要求下其所需要的private key size要比普通离散对数问题小很多。
没有抽象代数背景的同学估计已经有点懵了,不要怕,这里有篇非常通俗易懂的ECC介绍,这个系列有三篇文章,感兴趣自行查阅。
hash算法基本上每个程序员都会打过交道,在密码学中hash算法是一个非常重要的primitive,通常被用来构造其他密码学算法,比如后面提到的 消息验证码算法、签名算法、key派生算法、随机数生成算法等等都有hash算法的身影。密码学中的hash算法通常指的是具备密码学性质的Cryptographic hash functions,一个Cryptographic hash function需要具备以下几个性质:
常见的密码学hash有MD5,SHA-1, SHA-2家族(SHA-256, SHA-512…)。其中MD5,SHA-1都已经被认为不再密码学安全了,SHA-2目前不仅在计算性能上优于SHA-1,也具备更好的安全性和碰撞抗性,目前被广泛应用。
关于MD5再多说一点,MD5被广大程序员们滥用最多的地方是用户密码存储,具体做法是后台数据库里不直接存储用户的密码,而且存储密码的经过MD5计算后的摘要,校验用户身份的时候计算用户请求中的密码的MD5和后台存储的MD5值进行比对。这样做来防止数据库被端时候泄漏所有用户密码。这样做显然早就不安全了,由于用户密码的entropy太低了,攻击者完全可以预计算好一个hash到密码的映射链表(俗称彩虹表),有了这个彩虹表,破解用户密码就是简单的查表操作。
聪明的程序员们为了让彩虹表攻击失效,考虑为了每个用户hash计算加盐,即hash = MD5(salt, password),数据库里不仅存储hash,也存储了用户salt,这样攻击者拿到数据库后也不能使用预计算好的彩虹表破解用户密码。这招在十几年前可能还算有效,毕竟全局的彩虹表失效了,但是再现代强大的计算能力下,攻击者完全可以做到在合理的时间内动态根据salt值生成per user的彩虹表,照样破解用户密码。
那么正确的存储用户密码的做法是什么呢?现在主流的做法是基于Bcrypt这样的密码hash函数,有着non-trivial的计算复杂度,能够在一定程度上抵御暴力破解。Bcrypt算法内置了生成salt和hash,而且计算复杂度可调,这样随着硬件计算能力的提升,使用者可以相对应的调节算法的计算代价。
在实际应用中,光有加密是不够的,因为密文可能被篡改,可能压根就不是对端发的,此时需要有消息验证机制来识别这些情况,从而丢弃不合法的消息。消息验证码的作用就是为了校验消息的真实性和完整性。MAC算法和对称加解密算法很类似,首先他们工作都需要一个对称密钥,其次他们都由两部分构成。MAC算法的第一部分是MAC生成算法,输入是key和message,输出是一个tag,第二MAC校验算法,输入是key、message和tag,输出是这个tag是否valid(valid表示message是真实且完整的)。HMAC(Hash-based Message Authentication Code)作为一个生成MAC的标准被广泛应用,tls协议使用的mac算法就是hmac标准,从名字也可以知道HMAC基于一个密码学hash函数。
将加密和消息验证码结合起来通常有三种方式:
现在业界已经广泛接受,只有Encrypt, then authenticate才是最安全的选择,其他两种方式都存在已知的攻击方法,以至于密码学家觉得干脆将Encrypt, then authenticate这种模式固化到一个算法里面好了,避免人们做出错误的选择。这也就是AEAD(Authenticated-Encryption With Additional data)模式的motivation。所谓的Additional data通常是指我们要传输的数据不仅仅有消息内容本身,还是一些关于消息内容的metadata,而且这些metadata往往是不能加密的(比如邮件的发送方、接收者信息)我们不仅希望AE算法能够authenticate消息内容本身,还要能够authenticate metadata。AEAD内部在计算MAC时候将metadata通过某种方式和消息密文进行组合才生成MAC,这样对端在校验MAC也就同时验证了metadata以及密文的合法性。AEAD的实现可以基于block ciper进行构造组合,也可以基于native stream ciper。业界目前最广泛应用的AEAD是block ciper进行GCM模式构造。
MAC保证了消息的真实性和完整性,但是MAC本身不能防止攻击者重放攻击,即攻击者保存了此前双端通信的消息和MAC,然后进行重放,接收方收到后能够校验成功、正常解密,所以一个安全的协议还需要别的机制来防止重放,后面我们会看到TLS是怎么做的。
签名算法和MAC算法的作用类似,都是为了验证对端身份,不同的是MAC算法工作需要双方已有一个共同的对称密钥,然而在很多场景下,通信双方事先没有这个共同的密钥,比如在双方进行密钥交换前,怎么验证server端的证书、怎么验证server发的DH公钥都会涉及到签名。
基于非对称密钥的签名算法通和非对称密钥的加密算法工作过程刚好相反,签名算法是key pair的持有方用private key来生成对消息的签名(通常会基于一个Cryptographic hash function来生成消息的签名),对端使用public key来验证消息签名(验证过程通常会是用public key计算消息的签名,然后和对端传来的进行对比)。非对称加密算法则是由对端使用public key来对message进行加密,key pair持有方使用private key对密文进行解密。
常见的签名算法有
密钥派生算法的作用很直观,就是从一个密钥派生多个密钥。比如双方进行DH交换协商出一个密钥后,通常会用一个密钥派生算法派生出多个密钥,用于加解密、生成/验证mac等等。
密钥派生算法主要有两种,一种是针对entropy足够高的输入密钥的,比如DH交换得到的密钥,这种派生算法通常计算代价相对较低,比如主流的HKDF算法,TLS1.2采用的TLS-12-PRF(SHA-256)算法。还有一种则是针对entropy低的密钥,比如用户设置的密码,这类算法则要求派生算法本身需要花费non-trivial的计算资源来派生密钥,否则派生出来的密钥安全性得不到保证,比如PBKDF2,以及前面提到的Bcrypt, Scrypt。
TLS的一个完整的CipherSuite可以认为主要有四个算法组合到一起:
> openssl ciphers -V
0xCC,0xA9 - ECDHE-ECDSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH Au=ECDSA Enc=ChaCha20-Poly1305 Mac=AEAD
0xCC,0xA8 - ECDHE-RSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH Au=RSA Enc=ChaCha20-Poly1305 Mac=AEAD
0xCC,0xAA - DHE-RSA-CHACHA20-POLY1305 TLSv1.2 Kx=DH Au=RSA Enc=ChaCha20-Poly1305 Mac=AEAD
0xC0,0x30 - ECDHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(256) Mac=AEAD
0xC0,0x2C - ECDHE-ECDSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(256) Mac=AEAD
0xC0,0x28 - ECDHE-RSA-AES256-SHA384 TLSv1.2 Kx=ECDH Au=RSA Enc=AES(256) Mac=SHA384
0xC0,0x24 - ECDHE-ECDSA-AES256-SHA384 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AES(256) Mac=SHA384
0x00,0x9F - DHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=DH Au=RSA Enc=AESGCM(256) Mac=AEAD
0x00,0x6B - DHE-RSA-AES256-SHA256 TLSv1.2 Kx=DH Au=RSA Enc=AES(256) Mac=SHA256
0x00,0xC4 - DHE-RSA-CAMELLIA256-SHA256 TLSv1.2 Kx=DH Au=RSA Enc=Camellia(256) Mac=SHA256
0x00,0x9D - AES256-GCM-SHA384 TLSv1.2 Kx=RSA Au=RSA Enc=AESGCM(256) Mac=AEAD
0x00,0x3D - AES256-SHA256 TLSv1.2 Kx=RSA Au=RSA Enc=AES(256) Mac=SHA256
0x00,0xC0 - CAMELLIA256-SHA256 TLSv1.2 Kx=RSA Au=RSA Enc=Camellia(256) Mac=SHA256
0xC0,0x2F - ECDHE-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(128) Mac=AEAD
0xC0,0x2B - ECDHE-ECDSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(128) Mac=AEAD
0xC0,0x27 - ECDHE-RSA-AES128-SHA256 TLSv1.2 Kx=ECDH Au=RSA Enc=AES(128) Mac=SHA256
0xC0,0x23 - ECDHE-ECDSA-AES128-SHA256 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AES(128) Mac=SHA256
0x00,0x9E - DHE-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=DH Au=RSA Enc=AESGCM(128) Mac=AEAD
0x00,0x67 - DHE-RSA-AES128-SHA256 TLSv1.2 Kx=DH Au=RSA Enc=AES(128) Mac=SHA256
0x00,0xBE - DHE-RSA-CAMELLIA128-SHA256 TLSv1.2 Kx=DH Au=RSA Enc=Camellia(128) Mac=SHA256
这里举几个openssl中tls1.2的cipherSuite实现为例,AES256-SHA256
在tls协议文档中全名叫做TLS_RSA_WITH_AES_256_CBC_SHA256
,它的key exchange算法使用的就是RSA,认证签名算法使用的也是RSA(证书中用RSA签名),加密算法中的AES256,并通过CBC模式进行扩展,MAC算法和key派生基于SHA256,这里需要说明下tls中mac算法采用的是HMAC标准,这里说的基于SHA256指的是HMAC中的hash函数使用的是SHA256,另外tls协议自定义一个基于HMAC的PRF函数用于key派生算法,所以它们都基于同一个hash方法。DHE-RSA-AES256-SHA256
全称叫做TLS_DHE_RSA_WITH_AES_256_CBC_SHA256
,密钥交换使用的DHE,签名使用RSA,加密使用AES256-CBC,MAC和key派生基于SHA256。ECDHE-ECDSA-AES256-GCM-SHA384
密钥交换使用ECDHE,签名使用ECDSA,认证加密使用AES256-GCM(GCM是AEAD的一种实现),由于MAC和加密结合到一起,所以Mac值就是AEAD,key派生基于SHA384。
tls协议工作在tcp协议之上,连接连接后,首先需要通过握手过程让通信双端协商出一堆密码学状态,握手过程重要且很多细节,准备放到另外一篇blog里介绍。这里先介绍握手完成后双端的状态和tls协议对应用数据的处理过程。
tls record帧格式很简单,由header和content两部分组成,header里面包括以下几个字段
content部分取决content type,目前tls定义了以下几种content type
tls协议和很多其他有状态协议一样,双端需要通过协商在内存中维护很多一致的状态才能进行工作,这些状态也是tls record协议工作所依赖的环境上下文。状态主要分为两类,1) 通过协商得到的在整个connection生命周期内保持不变的状态,以及2)双方通信的进行,每收发一个record就需要更新的状态。
1) 主要包括:双方协商一致的cipherSuite以及握手完成后得到的SecurityParameters,
struct {
ConnectionEnd entity; //标识是client还是server
PRFAlgorithm prf_algorithm; //用于key派生的算法
BulkCipherAlgorithm bulk_cipher_algorithm; // 对称加密算法(null, rc4, 3des, aes)
CipherType cipher_type; // 密文类型(stream, block, aead)
uint8 enc_key_length; //对称密钥长度
uint8 block_length; //block大小 (如果有的话)
uint8 fixed_iv_length; //固定全局iv长度 (作用是啥)
uint8 record_iv_length; //每个record进行block加密的iv长度 (和block_length相等)
MACAlgorithm mac_algorithm; // mac算法
uint8 mac_length; //mac长度
uint8 mac_key_length; //mac密钥长度
CompressionMethod compression_algorithm; //压缩算法
opaque master_secret[48]; //握手阶段运行key exchange算法得到的master secret,这也是后面所有对称密钥的base
opaque client_random[32]; //client 随机数
opaque server_random[32]; //server 随机数
} SecurityParameters;
双方根据得到的SecurityParameters,通过tls自定义的key派生算法从master_secret派生出下面的一堆密钥和iv (client和server的write mac key、write encryption key、write IV),用于后续的应用数据的加解密。具体的key派生算法工作如下:
tls先定义了一个P_hash函数:
P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) +
HMAC_hash(secret, A(2) + seed) +
HMAC_hash(secret, A(3) + seed) + ...
其中A() 定义为:
A(0) = seed
A(i) = HMAC_hash(secret, A(i-1))
P_hash可以迭代任意次以产生任意长度的的数据。
tls应用P_hash定义了一个PRF函数:
PRF(secret, label, seed) = P_<hash>(secret, label + seed)
tls的key派生就是使用的这个PRF,具体是:
key_block = PRF(SecurityParameters.master_secret,
"key expansion",
SecurityParameters.server_random + SecurityParameters.client_random)
得到key_block后按照如下顺序对进行分割得到key和iv:
client_write_MAC_key[SecurityParameters.mac_key_length]
server_write_MAC_key[SecurityParameters.mac_key_length]
client_write_key[SecurityParameters.enc_key_length]
server_write_key[SecurityParameters.enc_key_length]
client_write_IV[SecurityParameters.fixed_iv_length]
server_write_IV[SecurityParameters.fixed_iv_length]
值得说明的有:
2)主要包括:
MAC(MAC_write_key, seq_num + //注意mac计算需要seq_num的参与
TLSCompressed.type +
TLSCompressed.version +
TLSCompressed.length +
TLSCompressed.fragment);
类似的,seq_num + TLSCompressed.type + TLSCompressed.version + TLSCompressed.length也作为aead加密下的additional_data。
回顾下tls协议的四个设计目标:密码学安全性、互操作性、可扩展性和相对有效性,这篇blog侧重的是分析tls怎么做到密码学安全性的部分内容,主要是给了一些密码学背景知识,然后介绍了下tls record怎么来保证应用层数据的安全。下一篇blog将继续介绍和分析tls协议的重要且复杂的握手过程,双端通过tls握手才能建立一个安全的连接,tls record协议才能工作。