TLS与OpenSSL
首发于TLS与OpenSSL

TLS的密码学套件

TLS协议有三个作用:验证,防篡改,加密。这三个作用也基本上是密码学相关的三个应用。验证是可以同时支持客户端验证服务端和服务端验证客户端两个需求的,只是在大部分的cs应用场景下,都是客户端验证服务端即可,主要目的是为了防止网站伪造,防钓鱼网站的目的。防篡改的主要密码学方法是哈希算法,各个版本的SSL/TLS握手应用了大量的不同的哈希算法。加密在TLS中有两个主要的体现,一个是握手的过程中的非对称加密用以建立信道,一个信道建立之后的对称加密用于实际的通信。之所以分为两个是因为对称加密在功能上不同完成非对称加密的密钥协商的功能,而非对称加密在性能上达不到对称加密的数据要求。通常一个加密信道建立之后,这个信道都有一个存活的时间,如果存活的时间太长,理论上就会有被破解的风险,因为对称加密的信道相关的参数在协商确定后是基本不变的。所以即使是长连接也需要隔一段时间重新进行TLS握手,这个过程叫做重协商。

但是重协商更多的是一个理论上的功能。我们做工程要时刻的区分理论和实践的区别。通常一个TLS握手之后的连接工程上可以持续的时间远比理论值要长很多,因为不是所有的流量都有必要要防止攻击成本非常高的攻击的。工程是一个折中的过程,可能只有支付等极其敏感的业务需要高严格的安全性参数考虑。所以重协商功能在工程上通常是不启用的,nginx就默认关闭了这个功能(还有其他安全上的考虑)。即使我们需要重协商,在客户端编程的时候,也可以直接的采用重写发起一个连接来完成,业务简单清爽,带来的性能开销也并不会多太多。因为信道过期并不是一个非常频繁的操作,更多的是信道持续不到信道过期的时间。TLS1.3已经取消了重协商的机制。理论和工程的区别在调查技术的时候要时刻的保持清醒,不然很容易变成“书呆子”。

密码学套件

密码学套件是TLS发展了一段时间积累了很多密码学使用的经验之后提出的一整套的解决方案。一个套件中包含了应用于整个握手和传输使用到的所有非对称加密,对称加密和哈希算法,甚至包括证书的类型。最早期的SSL虽然也许要一系列的加密算法,但是这些算法并不是像现在的称为密码学套件(Cipher Suite),而是被称作密码选择(Cipher Choice)。密码学套件是SSLv3开始提出的概念,从此,零散的密码学选择问题变成了一个整体的密码学套件选择的问题。后续的版本在升级的时候会产生新的安全强度更高的密码学套件,同时抛弃比较弱的密码学套件。但是这种密码学套件的方式沿用了下来。TLS1.3的定义和TLS1.2的定义略有不同,这里着重介绍1.2的定义。

一个密码学套件是完成整个TLS握手的关键。在TLS握手的时候ClientHello里面携带了客户端支持的密码学套件列表,ServerHello中携带了Server根据Client提供的密码学套件列表中选择的本地也支持的密码学套件。也就是说选择使用什么密码学套件的选择权在Server的手里(在使用nginx的时候把ssl_prefer_server_ciphers配置项打开),而Server通常可以通过配置文件指定Server要支持的密码学套件的列表和顺序。为此需求,OpenSSL专门定义了一套比较难懂的定义方法,Nginx的密码学套件的配置方法也只是对OpenSSL定义的定义形式的透传。也就是说使用Nginx定义的ssl_prefer_server_ciphers配置项是原封不动的传输给OpenSSL的。使用OpenSSL的工具程序就可以验证分析一个密码学套件字符串:openssl -v cipher 'RC4:HIGH:!aNULL:!MD5' ,就可以看到RC4:HIGH:!aNULL:!MD5 这个密码学套件配置的详细内容。这个命令是专门用于解析OpenSSL的密码学套件的配置的工具,由于字符串格式的配置比较难学习,使用这个命令可以起到有效的帮助。

密码套件分为三大部分:密钥交换算法,数据加密算法,消息验证算法(MAC,message authentication code)。密钥交换算法用于握手过程中建立信道,一般采用非对称加密算法。数据加密算法用于信道建立之后的加密传输数据,一般采用对称加密算法。MAC顾名思义是一种哈希,用于验证消息的完整性,包括整个握手流程的完整性(例如TLS握手的最后一步就是一个对已有的握手消息的全盘哈希计算的过程)。

在OpenSSL的配置文件里写密码学套件的写法和实际在OpenSSL代码里通过解析配置生成的宏套件代码是不同的。OpenSSL的配置文件的写法是一个生成密码学套件序列的文本引擎,而实际的密码学的套件名字却是OpenSSL代码里的宏定义,这个宏定义的名字是和RFC中规定的命名方式是一样的。

TLS_DHE_RSA_WITH_AES_256_CBC_SHA是一个密码学套件的标准名字。这里的TLS代表的是TLS协议,如果未来TLS改名,这个名字可能会变,否则会一直是这个名字。WITH是一个分隔单次,WITH前面的表示的是握手过程所使用的非对称加密方法,WITH后面的表示的是加密信道的对称加密方法和用于数据完整性检查的哈希方法。WITH前面通常有两个单次,第一个单次是约定密钥交换的协议,第二个单次是约定证书的验证算法。要区别这两个域,必须要首先明白,两个节点之间交换信息和证书本身是两个不同的独立的功能。两个功能都需要使用非对称加密算法。交换信息使用的非对称加密算法是第一个单词,证书使用的非对称加密算法是第二个。有的证书套件,例如TLS_RSA_WITH_AES_256_CBC_SHA,WITH单词前面只有一个RSA单词,这时就表示交换算法和证书算法都是使用的RSA,所以只指定一次即可。可选的主要的密钥交换算法包括: RSA, DH, ECDH, ECDHE。可选的主要的证书算法包括:RSA, DSA, ECDSA。两者可以独立选择,并不冲突。AES_256_CBC指的是AES这种对称加密算法的256位算法的CBC模式,AES本身是一类对称加密算法的统称,实际的使用时要指定位数和计算模式,CBC就是一种基于块的计算模式。最后一个SHA就是代码计算一个消息完整性的哈希算法。

对于OpenSSL中对密码学套件的定义,需要从一个设计者的角度来考虑和理解,如果我来设计这个字符串引擎,我该如何的让这一串字符串能代表我要指定的一系列密码学套件?

一个最显然的方法,就是我直接指定我要使用的密码学套件的列表。由于一个密码学套件就有五六个变量,这种制定方法会非常的长,这么长的配置是对配置者非常不友好的,而且并不是所有要使用Nginx的都明白密码学套件是什么,每一种之间的区别在哪。但是这种逐个指定的模式也必须支持,这就是配置引擎的第一个符号冒号,使用冒号来形成列表,逐个的指定密码学套件是一个最显著的需求。同时,由于密码学套件的五六个可变的域,所以使用正则表达式或许可以成为一个选择,但是密码学套件是一个从有效的选项中选择的过程,显然正则匹配又不合适。所以,另外一个思路就产生了,除了可以指定完整的密码学套件的名字,还可以在列表中直接用排除法写明支持某一种哈希,或者排除某一种哈希,这样所有使用这种哈希,或者不使用这种哈希的密码学套件就能够被一次性的选择或者排除。显然选择和排除可以同时出现,排除肯定是比选择有更高的优先级的。这也就是OpenSSL密码学套件定义字符串的感叹号,感叹号表示永久性的排除某一个密码学算法。并且冒号分割的列表中,不但可以指定完整的密码学套件,还可以指定某一种对称加密,或者是哈希算法等单个独立的密码学算法,表示我愿意使用包含这种密码学算法的所有套件。如果给它加个感叹号,就表示我不愿意使用。

使用了感叹号,问题又出现了。我要给什么东西加感叹号?任何一种密码学算法是一个方面,但是有很多类别是否可以一次性定义?为此OpenSSL定义了一系列的配置变量,只使用这一系列的变量就可以指定特定的密码学算法或者某个常用的密码学算法的集合。

完整的支持的变量列表可以在man文件中找到,例如/docs/man1.0.2/apps/ciphers.html网址中定义的一系列变量。比如DEFAULT变量,这个变量相当于套件序列:ALL:!EXPORT:!LOW:!aNULL:!eNULL:!SSLv2 。这个序列里面又包含了其他变量的使用,例如ALL变量就表示出了eNULL之外的所有密码学套件,而eNULL就表示所有没有加密算法的密码学套件,也就是所有明文传输的密码学套件。EXPORT表示的所有的出口强度的密码学。这里的感叹号就表示了从ALL指定的所有密码套件中排除掉出口强度的密码学。出口强度是一个非常低的强度,出口的密码学强度都是有关部门确定可以直接破解的加密方法(否则为什么要出口?)。美国国内就严格要求了可以出口的密码学的强度,OpenPGP就是因为密码学的强度过大所以以出口高强度密码学在美国被起诉了,不过作者最后把整个代码出版成一本书,并没有出口密码学,而是出口书籍知识,用这种方法完美的绕过了美国的密码学限制法律。LOW表示的所有已经被认为的可以破解的弱强度密码,这部门密码和EXPORT密码是不重合的,但是两部分加起来就几乎是极度不可靠密码套件的全体了。在OpenSSL中都是默认根本不编译启用的。aNULL表示的是所有的不包含认证的密码学套件。类似的变量还有很多,就不一一列举。

在密码学套件的字符串里面,还有一个加号和一个减号两个操作符号,也是用于操作变量的,但是使用的比较少。在man中也有相关的介绍。由于现在浏览器的寡头现象特别严重,每一种浏览器支持的加密套件基本都是固定的通用的一些,所以服务器在选择自己支持什么加密套件的时候,应当参考不同的客户端发来ClientHello时携带的客户端支持的套件集。虽然总体的种类很多,但是一段时间实际通用的却很少。例如在当前TLS1.2的环境下,证书也就是RSA和ECDSA两种选择,握手也基本上只有RSA和ECDHE两种选择,加密算法也一般只会选择AES-GCM(TLS1.2废弃了IDEA和DES相关的加密套件),哈希算法也大部分情况下时SHA256。所以实际的变化并不太多,可能使用一个完整密码学套件的冒号列表就可以完全表达了。指定的各个密码学套件的先后顺序就是服务器选择使用哪个密码学套件的先后顺序。

这是谷歌Chrome浏览器的58.0.3029.110 (64-bit)版本在发送ClientHello消息的时候携带的客户端支持的Cipher Suites列表。这种流行程度,Chrome可以代表大部分的流行度。如果Chrome都不支持的套件,那么这个套件也很快就会被从服务端不支持。无论在哪里,用户才是上帝。

由于对于TLS1.2来说,服务端主要支持的密码学就那么几种,所以一个常见的写法是固定的写:

ssl_ciphers ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:RSA+AES128:!aNULL:!eNULL:!LOW:!ADH:!RC4:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS;

这一行写到nginx的配置里就会原封不动的传递给OpenSSL的内部。我们可以看到,优先选择ECC证书和ECDHE握手的方式,如果客户端不支持或者只有RSA证书,就会选择RSA证书和椭圆曲线的握手算法,而一直没有选择RSA的握手算法。只有在椭圆曲线的握手算法完全不支持之后才会去可能使用RSA来握手,RSA握手在实际的应用相对在减少。ECDHE逐渐成为主流。

编辑于 2018-05-24

文章被以下专栏收录