本文目标:
- 学习鉴赏TLS协议的设计,透彻理解原理和重点细节
- 跟进一下密码学应用领域的历史和进展
- 整理现代加密通信协议设计的一般思路
本文有门槛,读者需要对现代密码学有清晰而系统的理解,建议花精力补足背景知识再读。本文最后的参考文献里有一些很不错的学习资料。
目录 :
[TOC]
一 . TLS协议的设计目标:
1. 密码学的方法论
密码学和软件开发不同,软件开发是工程,是手艺,造轮子是写代码的一大乐趣。软件开发中常常有各种权衡,一般难有明确的对错,一般还用建筑来比拟软件的结构,设计的优雅被高度重视。
密码学就不一样了。密码学是科学,不是工程,有严格的技术规范,严禁没有经过学术训练者随意创造。要求严谨的理论建模,严密的数学证明。很少有需要权衡的地方,正确就是正确,错误就是错误。又由于密码学过去在军事上的重要价值,各国政府一直投入大量人力物力财力,不断深入强化己方的算法,破解对手的算法,所以密码学就是一种残酷的军备竞赛。
-
密码学有很多的陷阱(下文会介绍几个),设计使用密码学的协议或者软件,是极其容易出错,高风险的专业活动,单纯的码农背景是做不了的。本着不作死就不会死的伟大理念,首先推荐读者尽可能使用 TLS 这种标准化,开源,广泛使用,久经考验,高性能的协议。本文也只是整理一点粗浅的科普常识,读完这篇文章,并不能使读者具有设计足够安全的密码学协议的能力。
-
密码学经过几十年的军备竞赛式发展,已经发展出大量巧妙而狡猾的攻击方法,我们使用的算法,都是在所有已知的攻击方法下都无法攻破的,由于我们大多数码农并没有精力去了解最前沿的攻击方法,所以我们其实并没有能力去评价一个加密算法,更没有能力自己发明算法。所以最好跟着业界的主流技术走,肯定不会有大错。
-
现代密码学近20年进展迅猛,现在搞现代密码学研究的主要都是数学家,在这个领域里面以一个码农的知识背景,已经很难理解最前沿的东西,连正确使用加密算法都是要谨慎谨慎再谨慎的。一个码农,能了解密码学基本概念,跟进密码学的最新应用趋势,并正确配置部署TLS这种协议,就很不错了。
-
密码学算法很难被正确地使用,各种细节非常容易出错。 例如:
- 1.大多数码农都听说过aes,可是大多数都不了解细节,比如:aes应该用哪种模式?应该用哪种padding?IV/nonce应该取多少bit?IV/nonce应该怎么生成? key size应该选多大?key应该怎么生成?应不应该加MAC?MAC算法的选择?MAC和加密应该怎么组合?
- 2.大多数知道RSA的码农分不清 RSASSA-PKCS1-v1_5 ,RSAES-OAEP 和 RSASSA-PSS
- 3.更多错误参见 这个stackoverflow问答,强烈推荐仔细阅读
-
密码学算法很难被正确地实现(代码实现过程中会引入很多漏洞,比如HeartBleed,比如各种随机数生成器的bug,时间侧通道攻击漏洞)
-
不能一知半解,绝对不能在一知半解的情况下就动手设计密码学协议。犹如“盲人骑瞎马,夜班临深池”。
-
不能闭门造车,密码学相关协议和代码一定要开源,采用大集市式的开发,接受peer review,被越多的人review,出漏洞的可能越小(所以应该尽可能使用开源组件)
2. TLS的设计目标
TLS的设计目标是构建一个安全传输层(Transport Layer Security ),在基于连接的传输层(如tcp)之上提供:
- 密码学安全 (1). 保密, message privacy (保密通过加密encryption实现,所有信息都加密传输,第三方无法窃听 ) (2). 完整性, message integrity( 通过MAC校验机制,一旦被篡改,通信双方会立刻发现 ) (3). 认证, mutual authentication (双方认证,双方都可以配备证书,防止身份被冒充 )
- 互操作,通用性 ( 根据公开的rfc,任何符合rfc的软件实现都可以互操作,不受限于任何专利技术)
- 可扩展性 ( 通过扩展机制 tls_ext可以添加功能,有大量的新功能,都是通过扩展添加的)
- 高效率 (通过session cache,恰当部署cache之后,tls的效率很高)
请认准这几个目标,在后文中,会逐一实现。
3. TLS的历史
- 1995: SSL 2.0, 由Netscape提出,这个版本由于设计缺陷,并不安全,很快被发现有严重漏洞,已经废弃。
- 1996: SSL 3.0. 写成RFC,开始流行。目前(2015年)已经不安全,必须禁用。
- 1999: TLS 1.0. 互联网标准化组织ISOC接替NetScape公司,发布了SSL的升级版TLS 1.0版.
- 2006: TLS 1.1. 作为 RFC 4346 发布。主要fix了CBC模式相关的如BEAST攻击等漏洞
- 2008: TLS 1.2. 作为RFC 5246 发布 。增进安全性。目前(2015年)应该主要部署的版本,请确保你使用的是这个版本
- 2015之后: TLS 1.3,还在制订中,支持0-rtt,大幅增进安全性,砍掉了aead之外的加密方式
由于SSL的2个版本都已经退出历史舞台了,所以本文后面只用TLS这个名字。 读者应该明白,一般所说的SSL就是TLS。
二. TLS协议的原理
1. 自顶向下,分层抽象
构建软件的常用方式是分层,把问题域抽象为多层,每一层的概念定义为一组原语,上一层利用下一层的组件构造实现,并被上一层使用,层层叠叠即成软件。 * 例如在编程语言领域中,汇编语言为一层,在汇编上面是C/C++等静态编译语言,C/C++之上是Python/PHP/lua等动态类型脚本语言层,之上常常还会构造领域特定的DSL * 在网络架构中,以太网是一层,其上是ip协议的网络层,ip之上是tcp等传输层,tcp之上是http等应用层
密码学通信协议也是分层构造得到。大致可以这么分层:
-
最底层是基础算法原语的实现,例如: aes , rsa, md5, sha256,ecdsa, ecdh 等(举的例子都是目前的主流选择,下同)
-
其上是选定参数后,符合密码学里标准分类的算法,包括块加密算法,签名算法,非对称加密算法,MAC算法等,例如: aes-128-cbc-pkcs7,rsaes-oaep ,rsassa-pkcs1-v1_5, hmac-sha256,ecdsa-p256,curve25519 等
-
再其上,是把多种标准算法组合而成的半成品组件,例如:对称传输组件例如 aes-128-cbc + hmac-sha256,aes-128-gcm,认证密钥协商算法: rsassa-OAEP + ecdh-secp256r1,数字信封:rsaes-oaep + aes-cbc-128 + hmac-sha256 ,文件密码加密存储组件:pbkdf2+aes-128-cbc-hmac-sha256,密钥扩展算法 PRF-sha256 等
-
再其上,是用各种组件拼装而成的各种成品密码学协议/软件,例如:tls协议,ssh协议,srp协议,gnupg文件格式,iMessage协议,bitcoin协议等等
第1层,一般程序员都有所了解,例如rsa,简直路人皆知; md5 被广泛使用(当然,也有广泛的误用) 第2层,各种莫名其妙的参数,一般很让程序员摸不着头脑,需要深入学习才能理清。 第3层,很多程序员自己造的轮子,往往说白了就是想重复实现第3层的某个组件而已。 第4层,正确地理解,使用,部署这类成熟的开放协议,并不是那么容易。很多的误用来源于不理解,需要密码学背景知识,才能搞懂是什么,为什么,怎么用。
最难的是理论联系实际。面对一个一团乱麻的实际业务问题,最难的是从中抽象分析出其本质密码学问题,然后用密码学概念体系给业务建模。在分析建模过程中,要求必须有严密的,体系化的思考方式。不体系化的思考方式会导致疏漏,或者误用。
第2层中,密码学算法,常见的有下面几类:
- 块加密算法 block cipher: AES, Serpent, 等
- 流加密算法 stream cipher: RC4,ChaCha20 等
- Hash函数 hash funtion:MD5,sha1,sha256,sha512 , ripemd 160,poly1305 等
- 消息验证码函数 message authentication code: HMAC-sha256,AEAD 等
- 密钥交换 key exchange: DH,ECDH,RSA,PFS方式的(DHE,ECDHE)等
- 公钥加密 public-key encryption: RSA,rabin-williams 等
- 数字签名算法 signature algorithm:RSA,DSA,ECDSA (secp256r1 , ed25519) 等
- 密码衍生函数 key derivation function: TLS-12-PRF(SHA-256) , bcrypto,scrypto,pbkdf2 等
- 随机数生成器 random number generators: /dev/urandom 等
每个类别里面的都有几个算法不断竞争,优胜劣汰,近几十年不断有老的算法被攻破被淘汰,新的算法被提出被推广。这一块话题广,水很深,内容多,陷阱也多,后续byron会翻译整理一系列文章,分享一下每一类里面个人收集的资料。 在此推荐一下 开源电子书crypto101,讲的很透彻,而且很易读)
设计一个加密通信协议的过程,就是自顶向下,逐步细化,挑选各类组件,拼装成完整协议的过程
3. TLS CipherSuite
从上述分层的角度看,TLS大致是由3个组件拼成的: – 1.对称加密传输组件,例如aes-128-gcm(这几个例子都是当前2015年最主流的选择); – 2.认证密钥协商组件,例如rsa-ecdhe; – 3.密钥扩展组件,例如TLS-PRF-sha256
这些组件可以再拆分为5类算法,在TLS中,这5类算法组合在一起,称为一个CipherSuite: authentication (认证算法) encryption (加密算法 ) message authentication code (消息认证码算法 简称MAC) key exchange (密钥交换算法) key derivation function (密钥衍生算法)
TLS协议设计之初就考虑到了这每一类算法的演变,所以没有定死算法,而是设计了一个算法协商过程,来允许加入新的算法( 简直是软件可扩展性设计的典范!),协商出的一个算法组合即一个CipherSuite TLS CipherSuite 在 iana 集中注册,每一个CipherSuite分配有 一个2字节的数字用来标识 ,可以在 iana的注册页面 查看
iana注册页面截图:
在浏览器中,就可以查看当前使用了什么 CipherSuite,在地址栏上,点击一个小锁的标志,就可以看到了。
服务器端支持的CipherSuite列表,如果是用的openssl,可以用 openssl ciphers -V | column -t 命令查看,输出如:
例如其中这一行(这个是目前的主流配置):
表示: 名字为ECDHE-RSA-AES128-GCM-SHA256
的CipherSuite ,用于 TLSv1.2版本,使用 ECDHE 做密钥交换,使用RSA做认证,使用AES-128-gcm做加密算法,MAC由于gcm作为一种aead模式并不需要,所以显示为aead,使用SHA256做PRF算法。
可以参考 man 1 ciphers
要注意的是,由于历史兼容原因,tls标准,和openssl的tls实现中,有一些极度不安全的CipherSuite,一定要禁用,比如:
EXP , EXPORT : 一定要禁用。EXPORT表示上世纪美国出口限制弱化过的算法,早已经被攻破,TLS的FREAK 攻击就是利用了这类坑爹的算法。 eNULL, NULL : 一定要禁用。NULL表示不加密!默认是禁用的。 aNULL : 一定要禁用。表示不做认证(authentication) ,也就是说可以随意做中间人攻击。
ADH : 一定要禁用。表示不做认证的 DH 密钥协商。
上面是举个例子,读者不要自己去研究怎么配置,这太容易搞错。 请按照mozilla官方给出的这个权威文档,复制粘贴就好了。
CipherSuite的更多解释,配置方法等,可以参考byron之前写的一篇文章 SSL/TLS CipherSuite 介绍
4. 协议分层
TLS是用来做加密数据传输的,因此它的主体当然是一个对称加密传输组件。为了给这个组件生成双方共享的密钥,因此就需要先搞一个认证密钥协商组件,故,TLS协议自然分为:
- 做对称加密传输的record协议 ,the record protocol
- 做认证密钥协商的handshake协议,the handshake protocol
还有3个很简单的辅助协议:
- changecipher spec 协议,the changecipher spec protocol, 用来通知对端从handshake切换到record协议(有点冗余,在TLS1.3里面已经被删掉了)
- alert协议,the alert protocol, 用来通知各种返回码,
- application data协议, The application data protocol,就是把http,smtp等的数据流传入record层做处理并传输。
这种 认证密钥协商 + 对称加密传输 的结构,是绝大多数加密通信协议的通用结构,在后文的更多协议案例中,我们可以看到该结构一再出现。
这5个协议中: record协议在tcp流上提供分包, 图片来自网络:
其它的: handshake protocol, alert protocol, changeCipherSpec protocol, application data protocol都封装在record protocol的包里,然后在tcp上传输(此处以tcp举例,也有可能是udp,或者随便什么ipc机制等)
下文分别介绍,内容主要是翻译自 RFC5246,RFC5077,RFC4492
5. record 协议
record协议做应用数据的对称加密传输,占据一个TLS连接的绝大多数流量,因此,先看看record协议 图片来自网络:
Record 协议 — 从应用层接受数据,并且做:
- 分片,逆向是重组
- 生成序列号,为每个数据块生成唯一编号,防止被重放或被重排序
- 压缩,可选步骤,使用握手协议协商出的压缩算法做压缩
- 加密,使用握手协议协商出来的key做加密/解密
- 算HMAC,对数据计算HMAC,并且验证收到的数据包的HMAC正确性
- 发给tcp/ip,把数据发送给 TCP/IP 做传输(或其它ipc机制)。
1. SecurityParameters
record层的上述处理,完全依据下面这个SecurityParameters里面的参数进行:
record 层使用上面的SecurityParameters生成下面的6个参数(不是所有的CipherSuite都需要全部6个,如果不需要,那就是空):
当handshake完成,上述6个参数生成完成之后,就可以建立连接状态,连接状态除了上面的SecurityParameters,还有下面几个参数,并且随着数据的发送/接收,更新下面的参数:
-
compression state : 当前压缩算法的状态。
-
cipher state : 加密算法的当前状态,对块加密算法比如aes,包含密码预处理生成的轮密钥(感谢温博士指出) “round key”,还有IV等;对于流加密,包含能让流加密持续进行加解密的状态信息
-
sequence number : 每个连接状态都包含一个sequence number,并且读和写状态有不同的sequence number。当连接开始传输数据时,sequence number必须置为0. sequence number 是uint64类型的,并且不得超过 $ 2^{64}-1$ 。s. Sequence number不得回绕。如果一个TLS实现无法避开回绕一个sequence number,必须进行重协商。sequence number在每个record被发送时都增加1。并且传输的第1个Record必须使用0作为sequence number。
此处有几个问题值得思考:
(1). 为什么MAC key , encryption key, IV 要分别不同?
在密码学中,对称加密算法一般需要encryption key,IV两个参数,MAC算法需要MAC key参数,因此这3个key用于不同的用途。 当然,不是所有的算法都一定会用到这3个参数,例如新的aead型算法,就不需要MAC key。
(2). 为什么client和server要使用不同的key 如果TLS的双方使用相同的key,那么当使用stream cipher加密应用数据的时候,stream cipher的字节流在两个方向是一样的,如果攻击者知道TLS数据流一个方向的部分明文(比如协议里面的固定值),那么对2个方向的密文做一下xor,就能得到另一个方向对应部分的明文了。
还有,当使用 aead 比如 aes-gcm 做加密的时候,aead标准严格要求,绝对不能用相同的 key+nonce 加密不同的明文,故如果TLS双方使用相同的key,又从相同的数字开始给nonce递增,那就不符合规定,会直接导致 aes-gcm 被攻破。
参考: http://crypto.stackexchange.com/questions/2878/separate-read-and-write-keys-in-tls-key-material
2. record层分段
如上图所示,对要发送的数据流,首先分段,分段成如下格式:
-
version字段 : ,定义当前协商出来的TLS协议版本,例如 TLS 1.2 version 是 { 3, 3 }
-
length字段 : 即长度,tls协议规定length必须小于 $2^{14}$,一般我们不希望length过长,因为解密方需要收完整个record,才能解密,length过长会导致解密方需要等待更多的rtt,增大latency,破坏用户体验,参考 Web性能权威指南 TLS那一章。
-
type字段 : ,用来标识当前record是4种协议中的哪一种,
record压缩 : TLS协议定义了可选的压缩,但是,由于压缩导致了 2012 年被爆出CRIME攻击,BREACH攻击,所以在实际部署中,一定要禁用压缩。 http://www.unclekevin.org/?p=640http://www.freebuf.com/articles/web/5636.html
3. record层的密码学保护
record层的密码学保护:
经过处理后的包格式定义如下:
TLS协议设计目标中的 1.保密(encryption) 2.完整性(authentication) ,和防重放就在这里实现。 实现方式有3类:
- Block Cipher (CBC mode of operation) + HMAC:例如 aes-128-cbc+hmac-sha256
- Stream Cipher (RC4) + HMAC
- Authenticated-Encryption using block cipher (GCM/CCM 模式):例如 aes-128-gcm
1.Block Cipher+HMAC 和 2.Stream Cipher + HMAC 的各类算法目前(2015年)都已经爆出各种漏洞(后文解释),目前最可靠的是 3.Authenticated-Encryption 类的算法,主要就是aes-gcm,下一代的TLS v1.3干脆只保留了3.Authenticated-Encryption,把1和2直接禁止了(所以。。。你真的还要继续用aes-cbc吗?)。
GCM模式是AEAD的,所以不需要MAC算法。 GCM模式是AEAD的一种,AEAD 的 作用类似于 Encrypt-then-HMAC ,例如 Sha256 + Salt + AES + IV
此处需要介绍一个陷阱。 在密码学历史上,出现过3种加密和认证的组合方式:
- Encrypt-and-MAC
- MAC-then-Encrypt
- Encrypt-then-MAC
在TLS协议初定的那个年代,人们还没意识到这3种组合方式的安全性有什么差别,所以TLS协议规定使用 2.MAC-then-Encrypt,即先计算MAC,然后把 “明文+MAC” 再加密(块加密或者流加密)的方式,做流加密+MAC,和块加密+MAC。 但是,悲剧的是,近些年,人们发现 MAC-then-Encrypt 这种结构导致了 很容易构造padding Oracle 相关的攻击,例如这在TLS中,间接形成被攻击者利用,这间接导致了 BEAST 攻击 , Lucky 13攻击 (CVE-2013-0169), 和 POODLE 攻击 (CVE-2014-3566).
目前因此,学术界已经一致同意: Encrypt-then-MAC 才是最安全的! tls使用的是 MAC-then-Encrypt 的模式,导致了一些问题。 具体比较,参见: http://cseweb.ucsd.edu/~mihir/papers/oem.pdf https://www.iacr.org/archive/crypto2001/21390309.pdf http://crypto.stackexchange.com/questions/202/should-we-mac-then-encrypt-or-encrypt-then-mac https://news.ycombinator.com/item?id=4779015 http://tozny.com/blog/encrypting-strings-in-android-lets-make-better-mistakes/
鉴于这个陷阱如此险恶,学术界有人就提出了,干脆把Encrypt和MAC直接集成为一个算法,在算法内部解决好安全问题,不再让码农选择,避免众码农再被这个陷阱坑害,这就是AEAD(Authenticated-Encryption With Addtional data)类的算法,GCM模式就是AEAD最重要的一种。
4. record层的密码学保护—MAC
TLS record 层 MAC的计算方法:
其中的seq_num是当前record的 sequence number,每条record都会++, 可以看到把 seq_num,以及record header里面的几个字段也算进来了,这样解决了防重放问题,并且保证record的任何字段都不能被篡改。
算完MAC,格式如下:
然后根据SecurityParameters.cipher_type,选择对应的对称加密算法进行加密,分类解说如下:
5. record层的密码学保护—stream cipher
stream cipher: 算stream cipher,stream cipher的状态在连续的record之间会复用。 stream cipher的主力是RC4,但是目前RC4已经爆出多个漏洞,所以实际中基本不使用流加密没法,详情请见:
https://tools.ietf.org/html/rfc7457#section-2.5
http://www.imperva.com/docs/HII_Attacking_SSL_when_using_RC4.pdf
6. record层的密码学保护— CBC block cipher
CBC模式块加密 TLS目前靠得住的的块加密cipher也不多,基本就是AES(最靠谱,最主流),Camellia,SEED,(3DES,IDEA之类已经显得老旧,DES请禁用),加密完的格式如下:
这个值得说道说道,因为我们码农平常在业界还能看到很多用AES-CBC的地方,其中的几个参数:
IV : : 要求必须用密码学安全的伪随机数生成器(CSPRNG)生成,并且必须是不可预测的,在Linux下,就是用用/dev/urandom,或者用 openssl 库的 RAND_bytes()。
注意:TLS 在 1.1版本之前,没有这个IV字段,前一个record的最后一个block被当成下一个record的IV来用,然后粗大事了,这导致了 BEAST攻击。 所以,TLS1.2改成了这样。 (还在使用CBC的各位,建议关注一下自己的IV字段是怎么生成出来的。如果要用,做好和TLS1.2的做法保持一致)。
其中 SecurityParameters.record_iv_length 一定等于 SecurityParameters.block_size. 例如 AES-256-CBC的 IV 一定是16字节长的,因为AES 128/192/256 的block size都是16字节。
padding : 使用CBC常用的PKCS 7 padding(在block size=16字节这种情况下,和pkcs 5的算法是一回事,Java代码里面就可以这么用这个case里,和pkcs 5的结果是一样的)
padding_length : 就是PKCS 7 padding的最后一个字节
注意2个险恶的陷阱: 1. 实现的代码必须在收到全部明文之后才能传输密文,否则可能会有BEAST攻击 2. 实现上,根据MAC计算的时间,可能进行时间侧通道攻击,因此必须确保—运行时间和padding是否正确无关。
7. record层的密码学保护— AEAD cipher
AEAD 到了我们重点关注的AEAD,AEAD是新兴的主流加密模式,是目前最重要的模式,其中主流的AEAD模式是 aes-gcm-128/aes-gcm-256/chacha20-poly1305
AEAD加密完的格式是:
AEAD ciphers的输入是: key,nonce, 明文,和 “additional data”. key是 client_write_key 或者 the server_write_key. 不需要使用 MAC key.
每一个AEAD算法都要指定不同的nonce构造算法,并指定 GenericAEADCipher.nonce_explicit 的长度. 在TLS 1.2中,规定很多情况下,可以按照rfc5116 section 3.2.1的技术来做。其中record_iv_length是nonce的显式部分的长度,nonce的隐式部分从key_block作为 client_write_iv和 and server_write_iv得出,并且把显式部分放在 GenericAEAEDCipher.nonce_explicit 里.
在TLS 1.3 draft中,做了更改:
- 规定 AEAD算法的 nonce的长度规定为 max(8 bytes, N_MIN),即如果N_MIN比8大,就用N_MIN; 如果比8小,就用8。
- 并且规定 N_MAX小于8字节的AEAD不得用于TLS。
-
规定TLS AEAD中每条record的nonce通过下面的方法构造出来: 64bit的sequence number的右侧填充0,直到长度达到iv_length。然后把填充过的sequence number和静态的 client_write_iv或 server_write_iv (根据发送端选择)做异或(XOR)。异或完成后,得到的 iv_length 的nonce就可以做每条record的nonce用了。
AEAD输入的明文就是 TLSCompressed.fragment (记得上面的介绍吗?AEAD是MAC和encrypt的集成,所以输入数据不需要在算MAC了).
AEAD输入的additional_data 是: