https://blog.helong.info/blog/2015/09/06/tls-protocol-analysis-and-crypto-protocol-design/?from=timeline&isappinstalled=0
最近发现密码学很有意思,刚好还和工作有点关系,就研究了一下,本文是其中一部分笔记和一些思考。
密码学理论艰深,概念繁多,本人知识水平有限,错误难免,如果您发现错误,请务必指出,非常感谢!
本文禁止转载
本文目标:
- 学习鉴赏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 命令查看,输出如:
例如其中这一行(这个是目前的主流配置):
1
|
|
表示: 名字为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里面的参数进行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
|
record 层使用上面的SecurityParameters生成下面的6个参数(不是所有的CipherSuite都需要全部6个,如果不需要,那就是空):
1
2
3
4
5
6
|
|
当handshake完成,上述6个参数生成完成之后,就可以建立连接状态,连接状态除了上面的SecurityParameters,还有下面几个参数,并且随着数据的发送/接收,更新下面的参数:
-
compression state : 当前压缩算法的状态。
-
cipher state : 加密算法的当前状态。这包含The current state of the encryption algorithm. This will consist of the scheduled过的 key(不知道咋翻译,就是aes加密算法要把密码和iv等参数预处理一下,处理的结果,一般保存在一个上下文里)。对于流加密,包含能让流加密持续进行加解密的状态信息
-
sequence number : 每个连接状态都包含一个sequence number,并且读和写状态有不同的sequence number。当连接开始传输数据时,sequence number必须置为0. sequence number 是uint64类型的,并且不得超过 264?1264?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层分段
如上图所示,对要发送的数据流,首先分段,分段成如下格式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
|
-
version字段 : ,定义当前协商出来的TLS协议版本,例如 TLS 1.2 version 是 { 3, 3 }
-
length字段 : 即长度,tls协议规定length必须小于 214214,一般我们不希望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层的密码学保护:
经过处理后的包格式定义如下:
1
2
3
4
5
6
7
8
9
10
|
|
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的计算方法:
1
2
3
4
5
|
|
其中的seq_num是当前record的 sequence number,每条record都会++, 可以看到把 seq_num,以及record header里面的几个字段也算进来了,这样解决了防重放问题,并且保证record的任何字段都不能被篡改。
算完MAC,格式如下:
1
2
3
4
|
|
然后根据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请禁用),加密完的格式如下:
1
2
3
4
5
6
7
8
9
|
|
这个值得说道说道,因为我们码农平常在业界还能看到很多用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加密完的格式是:
1
2
3
4
5
6
|
|
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 是:
1
2
|
|
“+” 表示字符串拼接。
可以看到,此处类似上面的MAC计算,算入了seq_num来防重放,type,version,length等字段防止这些元数据被篡改。
1
2
|
|
解密+验证完整性:
1
2
3
|
|
如果解密/验证完整性失败,就回复一条 fatal bad_record_mac alert 消息.
aes-gcm的iv长度,nonce长度,nonce构成等,后续再深入探讨。
8. record层的密码学保护— Key扩展
Key 扩展
TLS握手生成的master_secret只有48字节,2组encryption key, MAC key, IV加起来,长度一般都超过48,(例如 AES_256_CBC_SHA256 需要 128字节),所以,TLS里面用1个函数,来把48字节延长到需要的长度,称为PRF:
1
2
3
4
|
|
然后,key_block像下面这样被分割:
1
2
3
4
5
6
|
|
TLS使用HMAC结构,和在CipherSuite中指定的hash函数(安全等级起码是SHA256的水平) 来构造PRF,
首先定义P_hash,把(secret,seed)扩展成无限长的字节流:
1
2
3
|
|
其中”+“表示字符串拼接。 A() 定义为:
1
2
|
|
TLS的 PRF 就是把 P_hash 应用在secret上:
1
|
|
其中 label 是一个协议规定的,固定的 ASCII string.
要注意的是,TLS 1.3里面已经废弃了这种方式,改为使用更靠谱的 HKDF,HKDF 也是 html5的WebCryptoAPI的标准算法之一。
5. handshake 协议
handshake protocol重要而繁琐。
TLS 1.3对握手做了大修改,下面先讲TLS 1.2,讲完再介绍一下分析TLS 1.3.
1.handshake的总体流程
handshake protocol用于产生给record protocol使用的SecurityParameters。 在handshake中:
- 客户端和服务器端协商TLS协议版本号和一个CipherSuite,
- 认证对端的身份(可选,一般如https是客户端认证服务器端的身份),
- 并且使用密钥协商算法生成共享的master secret。
步骤如下:
-
交换Hello消息,协商出算法,交换random值,检查session resumption.
-
交换必要的密码学参数,来允许client和server协商出premaster secret。
-
交换证书和密码学参数,让client和server做认证,证明自己的身份。
-
从premaster secret和交换的random值 ,生成出master secret。
-
把SecerityParameters提供被record层。
-
允许client和server确认对端得出了相同的SecurityParameters,并且握手过程的数据没有被攻击者篡改。
Handshake的结果是在双方建立相同的Session,Session 包含下列字段:
- session identifier session id,用来唯一标识一个session,在session 恢复的时候,也要用到
- peer certificate 对端的 X509v3 格式证书. 如果不需要认证对端的身份,就为空。
- compression method 压缩算法,一般被禁用
- cipher spec CipherSuite,如上文介绍,包含: 用于生成key的pseudorandom function (PRF) , 块加密算法例如AES, MAC算法 (例如 HMAC-SHA256). 还包括一个 mac_length字段,在后文的we握手协议介绍
- master secret 48字节的,client和server共享密钥。
- is resumable 一个标志位,用来标识当前session是否能被恢复。
以上字段,随后被用于生成 record层的SecurityParameters,多个连接可以通过握手协议的session恢复功能来复用同一个session。
握手协议使用 非对称加密/密钥协商/数字签名 3类算法, 因此要求读者对这3类算法概念清晰,能准确区分。 在此澄清一下,: 非对称的算法分为3类: , * 非对称加密,有:RSAES-PKCS1-v1_5,RSAES-OAEP ,Rabin-Williams-OAEP, Rabin-Williams-PKCS1-v1_5等 * 非对称密钥协商,有:DH,DHE,ECDH,ECDHE 等 * 非对称数字签名:RSASSA-PKCS1-v1_5,RSASSA-PSS,ECDSA,DSA,ED25519 等
另外,非对称加密算法,可以当作密钥协商算法来用,所以 RSAES-PKCS1-v1_5,RSAES-OAEP 也可以当作密钥协商算法来用。
插播一段 RSA:
RSA的实际工程应用,要遵循PKCS#1 标准,见 https://www.ietf.org/rfc/rfc3447
其中的 RSAES-PKCS1-v1_5 和 RSASSA-PKCS1-v1_5 是使用RSA算法的两种不同scheme(体制)。 RSAES表示 RSA Encryption schemes,即非对称加密, RSAES有:RSAES-OAEP,RSAES-PKCS1-v1_5两种,其中RSAES-OAEP更新更安全
RSASSA表示 Signature schemes with appendix,即appendix模式(appendix和recovery的区别请参看密码学教材)的非对称数字签名算法。 RSASSA有: RSASSA-PSS, RSASSA-PKCS1-v1_5 两种, 其中RSASSA-PSS更新更安全
RSA还有一个缺陷,就是很容易被时间侧通道攻击,所以现在的RSA实现都要加 blinding ,后文有介绍。
可以看到,RSA是一种很特殊的算法,既可以当非对称加密算法使用,又可以当非对称数字签名使用。这一点很有迷惑性,其实很多用RSA的人都分不清自己用的是RSA的哪种模式。
相比之下,ECC(椭圆曲线)这一块的算法就很清晰,ECDSA只能用作数字签名,ECDH只能用作密钥交换。
分清楚 RSAES-PKCS1-v1_5 和 RSASSA-PKCS1-v1_5 有什么用涅?
PKCS#1规范解释:
A generally good cryptographic practice is to employ a given RSA key pair in only one scheme. This avoids the risk that vulnerability in one scheme may compromise the security of the other, and may be essential to maintain provable security.
FIPS PUB 186-3 美国标准规定:
An RSA key pair used for digital signatures shall only be used for one digital signature scheme (e.g., ANS X9.31, RSASSA-PKCS1 v1.5 or RSASSA-PSS; see Sections 5.4 and 5.5). In addition, an RSA digital signature key pair shall not be used for other purposes (e.g., key establishment).
一对密钥只做一个用途,要么用作非对称加解密,要么用作签名验证,别混着用! 一对密钥只做一个用途,要么用作非对称加解密,要么用作签名验证,别混着用! 一对密钥只做一个用途,要么用作非对称加解密,要么用作签名验证,别混着用!
这个要求,决定了一个协议的 PFS(前向安全性),在斯诺登曝光NSA的“今日捕获,明日破解”政策后,越发重要。
https://news.ycombinator.com/item?id=5942534
http://news.netcraft.com/archives/2013/06/25/ssl-intercepted-today-decrypted-tomorrow.html
https://lwn.net/Articles/572926/
https://www.eff.org/deeplinks/2014/04/why-web-needs-perfect-forward-secrecy
http://www.wired.com/2013/10/lavabit_unsealed
PFS反映到密钥协商过程中,就是:
- 不要使用RSA做密钥协商,一定只用RSA做数字签名。
- 不要把ECDH的公钥固定内置在客户端做密钥协商
后文可以看到这一原则在 TLS 1.3, QUIC,Apple的iMessage等协议中一再贯彻。
非对称RSA/ECC这个话题比较大了,后面有空再写文章吧,读者可以先看一下参考资料,里面有清晰的介绍。
插播结束,继续TLS。
由于设计的时候,就要考虑兼容性,而且实际历史悠久,所以TLS协议90年代曾经使用的一些算法,现在已经被破解了,例如有的被发现漏洞(rc4),有的密钥长度过短(例如曾经美帝有出口限制,限制RSA 在512比特以下,对称加密密钥限制40比特以下,后来2005年限制被取消),但是考虑到兼容,现在的TLS实现中,还是包含了这种已经被破解的老算法的代码。这样,如果攻击者可以干扰握手过程,诱使client和server使用这种已经被破解的算法,就会威胁TLS协议的安全,这被称为“降级攻击”。
为了在握手协议解决降级攻击的问题,TLS协议规定:client发送ClientHello消息,server必须回复ServerHello消息,否则就是fatal error,当成连接失败处理。ClientHello和ServerHello消息用于建立client和server之间的安全增强能力,ClientHello和ServerHello消息建立如下属性:
- Protocol Version
- Session ID
- Cipher Suite
- Compression Method.
另外,产生并交换两个random值 ClientHello.random 和 ServerHello.random
密钥协商使用四条: server的Certificate,ServerKeyExchange,client的Certificate,ClientKeyExchange 。TLS规定以后如果要新增密钥协商方法,可以订制这4条消息的数据格式,并且指定这4条消息的使用方法。密钥协商得出的共享密钥必须足够长,当前定义的密钥协商算法生成的密钥长度必须大于46字节。
在hello消息之后,server会把自己的证书在一条Certificate消息里面发给客户端(如果需要做服务器端认证的话,例如https)。 并且,如果需要的话,server会发送一条ServerKeyExchange消息,(例如如果服务器的证书只用做签名,不用做密钥交换,或者服务器没有证书)。client对server的认证完成后,server可以要求client发送client的证书,如果这是协商出来的CipherSuite允许的。下一步,server会发送ServerHelloDone消息,表示握手的hello消息部分已经结束。然后server会等待一个client的响应。如果server已经发过了CertificateRequest消息,client必须发送Certificate消息。然后发送ClientKeyExchange消息,并且这条消息的内容取决于ClientHello和ServerHello消息协商的算法。如果client发送了有签名能力的证书,就显式发送一个经过数字签名的CertificateVerify消息,来证明自己拥有证书私钥。
然后,client发送一个ChangeCipherSpec消息,并且client拷贝待定的Cipher Spec到当前的Cipher Spec。然后client立即用新算法+新key+新密钥 发送Finished消息。收到后,server发送自己的ChangeCipherSpec消息,作为响应,并且拷贝待定的Cipher Spec到当前的Cipher Spec。此时,握手就完成了,client和server可以开始交换应用层数据(如下图所示)。应用层数据不得在握手完成前发送。
引用一个来自网络的图片:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
|
注:为了帮助解决管道阻塞的问题,ChangeCipherSpec是一个独立的TLS protocol content type,并不是一个握手消息。
TLS的完整握手过程,要进行RSA/ECDH/ECDSA等非对称计算,非对称计算是很慢的。关于非对称的性能: 例如在2015年的服务器cpu: Intel? Xeon? CPU E3-1230 V2 @ 3.30GHz 上, 使用如下命令测试:
1
2
3
4
5
|
|
结果如下表:
算法 | 性能 | 性能 | |
---|---|---|---|
RSA-2048 | 私钥运算 723.7 次/秒 | 公钥运算 23505.8 次/秒 | | |
256 bit ecdsa (nistp256) | 签名 8628.4 次/秒 | 验证 2217.0 次/秒 | | |
256 bit ecdh (nistp256) | ECDH协商 2807.8 次/秒 | | | |
aes-128-cbc | 加密 121531.39 K/秒 | | | |
aes-128-cbc 使用aesni硬件加速 | 加密 683682.13 K/秒 | | |
注:非对称的单位是 次/秒,这是由于非对称一般只用于处理一个block, 对称的单位是 K/秒,因为对称一般用于处理大量数据流,所以单位和流量一样。 可以给非对称的 次/秒 乘以 block size ,就可以和对称做比较了。例如rsa-2048,723.7*2048/8/1024=185.2672 K/秒 , 故 RSA-2048 私钥运算性能 是aes-128-cbc 的 1.5/10001.5/1000。是aesni的 2.6/100002.6/10000。
如上,性能数据惨不忍睹, 简直不能忍!!!
有鉴于此,TLS从设计之初,就采用了万能手段—加cache,有2种cache手段:session id,和session ticket。把握手的结果直接cache起来,绕过握手运算。
当client和server决定恢复一个之前的session,或复用一个已有的session时(可以不用协商一个新的SecurityParameters),消息流程如下:
客户端使用要被恢复的session,发送一个ClientHello,把Session ID包含在其中。server在自己的session cache中,查找客户端发来的Session ID,如果找到,sever把找到的session 状态恢复到当前连接,然后发送一个ServerHello,在ServerHello中把Session ID带回去。然后,client和server都必须ChangeCipherSpec消息,并紧跟着发送Finished消息。这几步完成后,client和server 开始交换应用层数据(如下图所示)。如果server在session cache中没有找到Session ID,那server就生成一个新的session ID在ServerHello里给客户端,并且client和server进行完整的握手。
流程图如下:
1
2
3
4
5
6
7
8
9
10
11
|
|
3. handshake 协议外层结构
从消息格式来看,TLS Handshake Protocol 在 TLS Record Protocol 的上层. 这个协议用于协商一个session的安全参数。 Handshake 消息(例如ClientHello,ServerHello等) 被包装进 TLSPlaintext结构里面,传入TLS record层,根据当前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
|
|
TLS协议规定,handshake 协议的消息必须按照规定的顺序发,收到不按顺序来的消息,当成fatal error处理。也就是说,TLS协议可以当成状态机来建模编码。
下面按照消息发送必须遵循的顺序,逐个解释每一条握手消息。
handshake协议的外层字段,见这个抓包:
4. handshake — ClientHello,ServerHello,HelloRequest
Hello消息有3个:ClientHello, ServerHello,HellloRequest 逐个说明:
4.1 Client Hello
当客户端第一次连接到服务器时,第一条message必须发送ClientHello。 另外,rfc里规定,如果客户端和服务器支持重协商,在客户端收到服务器发来的HelloRequest后,也可以回一条ClientHello,在一条已经建立的连接上开始重协商。(重协商是个很少用到的特性。)
消息结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
|
Random 其中:
gmt_unix_time 是 unix epoch时间戳。
random_bytes 是 28字节的,用密码学安全随机数生成器 生成出来的随机数。
密码学安全的随机数生成,这是个很大的话题,也是一个大陷阱,目前最好的做法就是用 /dev/urandom,或者openssl库的 RAND_bytes()
历史上,恰好就在SSL的random_bytes这个字段,NetScape浏览器早期版本被爆出过随机数生成器漏洞。 被爆菊的随机数生成器使用 pid + 时间戳 来初始化一个seed,并用MD5(seed)得出结果。 见 http://www.cs.berkeley.edu/~daw/papers/ddj-netscape.html, 建议读者检查一下自己的随机数生成器。
client_version : 客户端支持的最高版本号。
random : 客户端生成的random。
ClientHello.session_id 唯一标识一个session,用来做session cache。如果为空,表示不做复用,要求服务器生成新的session。 session_id的来源有:
- 之前的协商好的连接的session_id
- 当前连接的session_id
- 当前也在使用中的另一条连接的session_id
其中第三种允许不做重新握手,就同时建立多条独立的安全连接。这些独立的连接可能顺序创建,也可以同时创建。一个SessionID当握手协商的Finished消息完成后,就合法可用了。存活直到太旧被移除,或者session 关联的某个连接发生fatal error。SessionID的内容由服务器端生成。
注:由于SessionID的传输是不加密,不做MAC保护的,服务器不允许把私密信息发在里面,不能允许伪造的SessionID在服务器造成安全问题。(握手过程中的数据,整体是受Finished消息的保护的)
ClientHello.cipher_suites字段,包含了客户端支持的CipherSuite的列表,按照客户端希望的优先级排序,每个CipherSuite有2个字节,每个CipherSuite由:一个密钥交换算法,一个大量数据加密算法(需要制定key length参数),一个MAC算法,一个PRF 构成。服务器会从客户端发过来的列表中选择一个;如果没有可以接受的选择,就返回一个 handshake failure 的 alert,并关闭连接。如果列表包含服务器不认识,不支持,或者禁用的CipherSuite,服务器必须忽略。 如果SessionID不为空,则cipher_suites里面起码要包含客户端cache的session里面的那个CipherSuite
compression_methods,类似地,ClientHello里面包含压缩算法的列表,按照客户端优先级排序。当然,如前介绍,服务器一般禁用TLS的压缩。
compression_methods 后面可以跟一组扩展(extensions), extensions都是可选的,比较有用的扩展如: SNI, session ticket,ALPN,OCSP 等,后文介绍。
客户端发送了ClientHello后,服务器端必须回复ServerHello消息,回复其他消息都会导致 fatal error 关闭连接。
4.2 Server Hello
当收到客户端发来的ClientHello后,正常处理完后,服务器必须回复ServerHello。
消息结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
|
server_version : 服务器选择 ClientHello.client_version 和 服务器支持的版本号 中的最小的。
random : 服务器生成的random,必须确保和客户端生成的random没有关联。
session_id : 服务器为本连接分配的SessionID。如果ClientHello.session_id不为空,服务器会在自己的本地做查找。
- 如果找到了匹配,并且服务器决定复用找到的session建立连接,服务器应该把ClientHello.session_id同样的 session id填入ServerHello.session_id,这表示恢复了一个session,并且双方会立即发送Finished消息。
- 否则,回复一个和ClientHello.random_id不同的Serverhello.session_id,来标识新session。服务器可以回复一个空的session_id,来告诉客户端这个session不要cache,不能恢复。 如果一个session 被恢复了,那必须恢复成之前协商的session里面的 CipherSuite。要注意的是,并不要求服务器一定要恢复session, 服务器可以不做恢复。
在实践中,session cache在服务器端要求key-value形式的存储,如果tls服务器不止一台的话,就有一个存储怎么共享的问题,要么存储同步到所有TLS服务器的内存里,要么专门搞服务来支持存储,并使用rpc访问, 无论如何,都是很麻烦的事情,相比之下,后文要介绍的session ticket就简单多了,所以一般优先使用session ticket。
cipher_suite : 服务器选定的一个CipherSuite。如果是恢复的session,那就是session里的CipherSuite。
compression_method : 跟上面类似。
extensions : 扩展列表。要注意的是,ServerHello.extensions 必须是 ClientHello.extensions的子集。
4.3 Hello Extensions
The extension 的格式是:
1
2
3
4
5
6
7
8
|
|
其中:
-
“extension_type” 标识是哪一个扩展类型。
-
“extension_data” 一坨二进制的buffer,扩展的数据体,各个扩展自己做解析。
extension_type 只能出现一次,ExtensionType之间不指定顺序。
extensions 可能在新连接创建时被发送,也可能在要求session恢复的时候被发送。所以各个extension都需要规定自己再完整握手和session恢复情况下的行为。 这些情况比较琐碎而微妙,具体案例要具体分析。
4.4 Hello Request
服务器任何时候都可以发送 HelloRequest 消息。
HelloRequest的意思是,客户端应该开始协商过程。客户端应该在方便的时候发送ClientHello。服务器不应该在客户端刚创建好连接后,就发送HelloRequest,此时应该让客户端发送ClientHello。
客户端收到这个消息后,可以直接忽略这条消息。 服务器发现客户端没有响应HelloRequest后,可以发送fatal error alert。
消息结构:
1
|
|
HelloRequest不包含在握手消息的hash计算范围内。
5. handshake — Server Certificate
当服务器确定了CipherSuite后,根据CipherSuite里面的认证算法,如果需要发送证书给客户端,那么就发送 Server Certificate消息给客户端。Server Certificate总是在ServerHello之后立即发送,所以在同一个RTT里。
Server Certificate里面包含了服务器的证书链。
消息结构:
1
2
3
4
5
|
|
certificate_list : 证书列表,发送者的证书必须是第一个,后续的每一个证书都必须是前一个的签署证书。根证书可以省略
证书申请的时候,一般会收到好几个证书,有的需要自己按照这个格式来拼接成证书链。
如果服务器要认证客户端的身份,那么服务器会发送Certificate Request消息,客户端应该也以 这条Server Certificate消息的格式回复。
服务器发送的证书必须:
-
证书类型必须是 X.509v3。除非明确地协商成别的了(比较少见,rfc里提到了例如 OpenPGP格式)。
-
服务器证书的公钥,必须和选择的密钥交换算法配套。
密钥交换+认证算法 | 配套的证书中公钥类型 | |
---|---|---|
RSA / RSA_PSK | RSA 公钥;证书中必须允许私钥用于加密 (即如果使用了X509V3规定的key usage扩展, keyEncipherment比特位必须置位) 这种用法没有前向安全性,因此在 TLS 1.3中被废弃了 | | |
DHE_RSA / ECDHE_RSA | RSA 公钥;证书中必须允许私钥用于签名(即如果使用了X509V3规定的key usage扩展, digitalSignature比特位必须置位),并且允许server key exchange消息将要使用的签名模式(例如 PKCS1_V1.5 ,OAEP等)和hash算法(例如sha1, sha256等) | | |
DHE_DSS | DSA 公钥; 历史遗留产物,从来没有被大规模用过,安全性差,废弃状态。证书必须允许私钥用于签名,必须允许server key exchange消息中使用的hash算法。 | |
DH_DSS / DH_RSA | Diffie-Hellman 公钥; 要求key usage里面的keyAgreement比特位必须置位。 这种用法没有前向安全性,因此在 TLS 1.3中被废弃了 | | |
ECDH_ECDSA / ECDH_RSA | 能做 ECDH 用途的公钥;公钥必须使用 客户端支持的ec曲线和点格式。这种用法没有前向安全性,因此在 TLS 1.3中被废弃了| | |
ECDHE_ECDSA | ECDSA用途的公钥;证书必须运输私钥用作签名,必须允许server key exchange消息里面要用到的hash算法。公钥必须使用客户端支持的ec曲线和点格式。| |
- “server_name” 和 “trusted_ca_keys” 扩展用于引导证书选择。
其中有5种是ECC密钥交换算法:ECDH_ECDSA, ECDHE_ECDSA, ECDH_RSA, ECDHE_RSA, ECDH_anon。 ECC(椭圆曲线)体制相比RSA,由于公钥更小,性能更高,所以在移动互联网环境下越发重要。 以上ECC的5种算法都用ECDH来计算premaster secret, 仅仅是ECDH密钥的生命周期和认证算法不同。 其中只有 ECDHE_ECDSA 和 ECDHE_RSA 是前向安全的。
如果客户端在ClientHello里提供了 “signature_algorithms” 扩展,那么服务器提供的所有证书必须用 “signature_algoritms”中提供的 hash/signature算法对 之一签署。要注意的是,这意味着,一个包含某种签名算法密钥的证书,可能被另一种签名算法签署(例如,一个RSA公钥可能被一个ECDSA公钥签署)。(这在TLS1.2和TLS1.1中是不一样的,TLS1.1要求所有的算法都相同。)注意这也意味着DH_DSS,DH_RSA,ECDH_ECDSA,和ECDH_RSA 密钥交换不限制签署证书的算法。固定DH证书可能使用”signature_algorithms”扩展列表中的 hash/签名算法对 中的某一个签署。名字 DH_DSS, DH_RSA, ECDH_ECDSA, 和 ECDH_RSA 只是历史原因,这几个名字的后半部分中指定的算法,并不会被使用,即DH_DSS中的DSS并不会被使用,DH_RSA中并不会使用RSA做签名,ECDH_ECDSA并不会使用ECDSA算法。。。 如果服务器有多个证书,就必须从中选择一个,一般根据服务器的外网ip地址,SNI中指定的hostname,服务器配置来做选择。如果服务器只有一个证书,那么要确保这一个证书符合这些条件。 要注意的是,存在一些证书使用了TLS目前不支持的 算法组合。例如,使用 RSASSA-PSS签名公钥的证书(即证书的SubjectPublicKeyInfo字段是id-RSASSA-PSS)。由于TLS没有给这些算法定义对应的签名算法,这些证书不能在TLS中使用。 如果一个CipherSuite指定了新的TLS密钥交换算法,也会指定证书格式和要求的密钥编码方法。
6. handshake — Server Key Exchange
服务器会在 server Certificate 消息之后,立即发送 Server Key Exchange消息。 (如果协商出的CipherSuite不需要做认证,即anonymous negotiation,会在ServerHello之后立即发送Server Key Exchange消息)
只有在server Certificate 消息没有足够的信息,不能让客户端完成premaster的密钥交换时,服务器才发送 server Key Exchange, 主要是对前向安全的几种密钥协商算法,列表如下:
- DHE_DSS
- DHE_RSA
- DH_anon
- ECDHE_ECDSA
- ECDHE_RSA
- ECDH_anon
对下面几种密钥交换方法,发送ServerKeyExchange消息是非法的:
- RSA
- DH_DSS
- DH_RSA
- ECDH_ECDSA
- ECDH_RSA
需要注意的是,ECDH和ECDSA公钥的数据结构是一样的。所以,CA在签署一个证书的时候,可能要使用 X.509 v3 的 keyUsage 和 extendedKeyUsage 扩展来限定ECC公钥的使用方式。
ServerKeyExchange传递足够的信息给客户端,来让客户端交换premaster secret。一般要传递的是:一个 Diffie-Hellman 公钥,或者一个其他算法(例如RSA)的公钥。
在TLS实际部署中,我们一般只使用这4种:ECDHE_RSA, DHE_RSA, ECDHE_ECDSA,RSA
其中RSA密钥协商(也可以叫密钥传输)算法,由于没有前向安全性,在TLS 1.3里面已经被废除了。参见: Confirming Consensus on removing RSA key Transport from TLS 1.3
消息格式:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
|