28

每次配置HTTPS或者SSL时,都需要指定一些cacert,cert,key之类的东西,他们的具体作用是什么呢?为什么配置了他们之后通信就安全了呢?怎么用openssl命令来生成它们呢?程序中应该如何使用这些文件呢?

本篇以TLS 1.2作为参考,只介绍原理,不深入算法的细节

SSL和TLS的关系

SSL(Secure Sockets Layer)和TLS(Transport Layer Security)的关系就像windows XP和windows 7的关系,升级后改了个名字而已。下面这张表格列出了它们的历史:

协议 创建时间 创建者 RFC 注释
SSL1.0 n/a Netscape n/a 由于有很多安全问题,所以网景公司没有将它公之于众
SSL2.0 1995 Netscape n/a 这是第一个被公众所了解的SSL版本
SSL3.0 1996 Netscape rfc6101 由于2.0还是被发现有很多安全问题,Netscape于是设计了3.0,并且IETF将它整理成RFC发布了出来
TLS1.0 1999 IETF rfc2246 TLS 1.0基于SSL 3.0,修改不大,在某些场合也被称之为SSL 3.1,改名主要是为了和Netscape撇清关系,表示一个新时代的来临。类似于饭店换老板了,然后改了个名字,厨师还是原来的
TLS1.1 2006 IETF rfc4346
TLS1.2 2008 IETF rfc5246
TLS1.3 TBD IETF TBD 还在开发过程中,draft

最初的SSL只支持TCP,不过现在已经可以支持UDP了,请参考Datagram Transport Layer Security Version 1.2

HTTPS和TLS的关系

HTTPS=HTTP+TLS,其它的协议也类似,如FTPS=FTP+TLS。

注意:SSH和SSL/TLS是两个不同的协议,SSH并不依赖于SSL/TLS

加密相关的概念

在正式开始介绍TLS之前,先澄清一些跟加密相关的概念:

对称加密

这是我们加密文件常用的方式,加密的时候输入一个密码,解密的时候也用这个密码,加密和解密都用同一个密码,所以叫对称加密。常见的算法有AES、3DES。

非对称加密

非对称加密是一个很神奇的东西,它有两个不一样的密码,一个叫私钥,另一个叫公钥,用其中一个加密的数据只能用另一个密码解开,用自己的都解不了,也就是说用公钥加密的数据只能由私钥解开,反之亦然。

私钥一般自己保存,而公钥是公开的,同等加密强度下,非对称加密算法的速度比不上对称加密算法的速度,所以非对称加密一般用于数字签名和密码(对称加密算法的密码)的交换。常见的算法有RSA、DSA、ECC。

摘要算法

摘要算法不是用来加密的,其输出长度固定,相当于计算数据的指纹,主要用来做数据校验,验证数据的完整性和正确性。常见的算法有CRC、MD5、SHA1、SHA256。

数字签名

数字签名就是“非对称加密+摘要算法”,其目的不是为了加密,而是用来防止他人篡改数据。

其核心思想是:比如A要给B发送数据,A先用摘要算法得到数据的指纹,然后用A的私钥加密指纹,加密后的指纹就是A的签名,B收到数据和A的签名后,也用同样的摘要算法计算指纹,然后用A公开的公钥解密签名,比较两个指纹,如果相同,说明数据没有被篡改,确实是A发过来的数据。假设C想改A发给B的数据来欺骗B,因为篡改数据后指纹会变,要想跟A的签名里面的指纹一致,就得改签名,但由于没有A的私钥,所以改不了,如果C用自己的私钥生成一个新的签名,B收到数据后用A的公钥根本就解不开。

TLS握手过程

TLS主要包含两部分协议,一部分是Record Protocol,描述了数据的格式,另一部分是Handshaking Protocols,描述了握手过程,本篇中只介绍握手过程,不介绍具体的通信数据格式。

握手的目的有两个,一个是保证通信的双方都是自己期待的对方,任何一方都不可能被冒充,另一个是交换加密密码,使得只有通信的双方知道这个密码,而别人不知道。前一个就是我们常说的认证,而后一个就是密码交换。认证是通过证书来达到的,而密码交换是通过证书里面的非对称加密算法(公私钥)来实现的。

先看握手的交互图:

+--------+                                      +--------+
|        |   1. ClientHello                     |        |
|        |------------------------------------->|        |
|        |                                      |        |
|        |   2. ServerHello                     |        |
|        |   3. Certificate                     |        |
|        |   4. ServerKeyExchange (optional)    |        |
|        |   5. CertificateRequest (optional)   |        |
|        |   6. ServerHelloDone                 |        |
|        |<------------------------------------ |        |
| Client |                                      | Server |
|        |   7. Certificate (optional)          |        |
|        |   8. ClientKeyExchange               |        |
|        |   9. CertificateVerify (optional)    |        |
|        |  10. Finished                        |        |
|        |------------------------------------> |        |
|        |                                      |        |
|        |  11. Finished                        |        |
|        |<------------------------------------ |        |
+--------+                                      +--------+

注意: 下面解释过程中用到的具体协议版本、算法和值都是示例,实际中可能不是这些

ClientHello

client->server: 
hello,咱建立个连接呗,我这边的支持的最高版本是TLS1.1,
支持的密码套件(cipher suite)有“TLS_RSA_WITH_AES_128_CBC_SHA”和“TLS_RSA_WITH_AES_256_CBC_SHA256”,
支持的压缩算法有DEFLATE,我这边生成的随机串是abc123456。

这里有几点需要解释一下:

  • 客户端会把自己最喜欢的密码套件放在最前面,这样服务器端就会根据客户端的要求优先选择排在前面的算法套件

  • 密码套件就是一个密码算法三件套,里面包含了一个非对称加密算法,一个对称加密算法,以及一个数据摘要算法。以TLS_RSA_WITH_AES_128_CBC_SHA为例,RSA是非对称加密算法,表示后面用到的证书里面的公钥用的是RSA算法,通信的过程中需要签名的地方也用这个算法,并且密码(key)的交换过程也使用这个算法;AES_128_CBC是对称加密算法,用来加密握手后传输的数据,其密码由RSA负责协商生成;SHA是数据摘要算法,表示后面交换的证书里签名
    用到的摘要算法是sha1,并且后续通信过程中需要用到数据校验的地方也是用的这个算法。在Record Protocol协议中,摘要算法是必须的,即数据包都需要有校验码,而签名是可选的。

  • ClientHello里面还可以包含session id,即表示重用前面session里的一些内容,比如已经协商好的算法套件等,服务器收到session id后会去内存里面找,如果这是一个合法的session id,那么它就可以选择重用前面的session,这样可以省去很多握手的过程。为了简化讨论,这里不介绍session重用的问题。

ServerHello

server收到client的hello消息后,就在自己加载的证书中去找一个和客户支持的算法套件相匹配的证书,并且挑选一个自己也支持的对称加密算法(证书里面只有非对称加密和摘要算法,不包含对称加密算法)。如果出现下面几种情况,握手失败:

  • 客户端支持的TLS版本太低,比如server要求最低版本为1.2,而客户端支持的最高版本是1.1

  • 根据客户端所支持的密码套件,找不到相应要求的证书

  • 无法就支持的对称加密算法达成一致

如果一切都OK,那么服务器端将返回ServerHello:

server->client: 
hello,没问题,我们就使用TLS1.1吧,
算法采用“TLS_RSA_WITH_AES_256_CBC_SHA256”,这个加密强度更高更安全,
压缩就算了,我这边不支持,我这边生成的随机数是654321def。

如果server支持session重用的话,这里还会返回session id

Certificate

服务器在发送完ServerHello之后紧接着发送Certificate消息,里面包含自己的证书。

当然这步在有些情况下可以忽略掉,就是非对称加密算法选择使用dh_anon,当然这是特殊的情况,并且也不安全,所以这里就不展开讨论。

server->client: 这是我的证书(身份证),请过目

ServerKeyExchange(可选)

在前面的ServerHello中,双方已经协商好了密码套件,对于套件里面的非对称加密算法,有些需要更多的信息才能生成一个可靠的密码,而有些不需要,比如RSA,就不需要发送这个消息,客户端自己生成一个准密码(premaster)就可以了,而有些算法,比如DHE_RSA,就需要发送一点特殊的信息给客户端,便于它生成premaster。

premaster可以理解为最终密码的初级版本,有了这个密码之后,稍微再做一下计算就可以得到最终要使用的对称加密的密码

server->client: 这是生成premaster所需要的一些信息,请查收

CertificateRequest(可选)

只有在需要验证客户端的身份的时候才用得着,在大部分情况下,尤其是HTTPS,这一步不需要。比如我们访问银行的网站,我们只要保证那确实是银行的网站就可以了,银行验证我们是通过账号密码,而不是我们的证书。而U盾就是一个验证客户端的例子,银行给你的U盾里面有你的证书,你通过U盾访问银行的时候,银行会验证U盾里面证书是不是你的,这种情况下,你和银行之间进行TLS握手的时候,银行会给你发这个CertificateRequest请求。

server->client: 把你的证书(身份证)也给我看看,我要确认一下你是不是XXX。

ServerHelloDone

server->client: 我要告诉你的就是这么多了,处理完了给我个回话吧。

Certificate(可选)

如果客户端在前面收到了服务器的CertificateRequest请求,那么将会在这里给服务器发送自己的证书,就算自己没有证书,也要发送这个消息告诉服务器端自己没有证书,然后由服务器端来决定是否继续。

client->server: 这是我的证书(身份证),请过目

ClientKeyExchange

客户端验证完服务器端的证书后(怎么验证证书将在后面介绍),就会生成一个premaster,生成的方式跟采用的密码交换算法有关,以TLS_RSA_WITH_AES_128_CBC_SHA为例,其密码交换算法是RSA,于是客户端自己直接生成一个48字节长度的premaster即可,不需要服务器发过来的ServerKeyExchange。

client->server: 
这是计算真正密码要用到的premaster,它是用你证书里的公钥加密了的哦,
记得用你的私钥解密后才能看到哦

CertificateVerify(可选)

如果客户端给服务器发了证书,就需要发送该消息给服务器,主要用于验证证书对应的私钥确实是在客户端手里。

client->server: 
这是一段用我私钥加密的数据,你用我给你的证书里的公钥解密看看,
如果能解开,说明我没骗你,私钥确实是在我手里,
并不是我随便找了一个别人的证书忽悠你

发送的消息里面都带有校验码,所以解密后计算下校验码,能对上说明解密成功

Finished

当前面的过程都没问题后,服务器和客户端都根据得到的信息计算对称加密用的密码,这是RFC里面给出的计算方法:

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

虽然不太了解PRF的细节,但至少客户端和服务器端用的算法和输入都是一样的,所以得到的master密码也是一样的。这里pre_master_secret就是ClientKeyExchange里面客户端发给服务器端的premaster,ClientHello.random和ServerHello.random分别是握手开始时双方发送的hello请求中的随机字符串。

这里加入随机数的原因主要是为了防止重放攻击,保证每次握手后得到的密码都是不一样的

然后双方将自己缓存的握手过程中的数据计算一个校验码,并用对称加密算法和刚算出来的master密码加密,发给对方,这一步有两目的,一个是保证双方算出来的master密码都是一样的,即我这边加密的数据你那边能解开;另一个目的是确保我们两个人的通信过程中的每一步都没有被其他人篡改,因为握手的前半部分都是明文,所以有可能被篡改,只要双方根据各自缓存的握手过程的数据算出来的校验码是一样的,说明中间没人篡改过。

client->server: 这是用我们协商的对称加密算法和密码加密过的握手数据的指纹,看能不能解开,并且和你那边算出来的指纹是一样的
server->client: 这是用我们协商的对称加密算法和密码加密过的握手数据的指纹,你也看看能不能解开,并且和你那边算出来的指纹是一样的

如果双方发送完Finished而对方没有报错,握手就完成了,双发都得到了密码,并且这个密码别人不知道,后续的所有数据传输过程都会用这个密码进行加密,加密算法就是ServerHello里面协商好的对称加密算法。

在上面握手的过程中,一旦有任何一方觉得有问题,都可能随时终止握手过程

握手不成功常见问题

配置好了之后还是连不上,一般会是下面几种问题:

  • 版本不一致,有一方的版本太低,另一方为了安全不同意跟它通信

  • 无法就cipher suite达成一致,有一方支持的加密算法太弱,安全程度不够

  • 证书有问题,没法通过验证

  • 服务器端需要验证客户端的证书,而客户端没有配置

证书相关

开始之前,看看我们常说的那些跟证书相关的概念

基本概念

私钥

私钥就是一个算法名称加上密码串,自己保存,从不给任何人看

公钥

公钥也是一个算法名称加上密码串,一般不会单独给别人,而是嵌在证书里面一起给别人

CA

专门用自己的私钥给别人进行签名的单位或者机构

申请(签名)文件

在公钥的基础上加上一些申请人的属性信息,比如我是谁,来自哪里,名字叫什么,证书适用于什么场景等的信息,然后带上进行的签名,发给CA(私下安全的方式发送),带上自己签名的目的是为了防止别人篡改文件。

证书文件

证书由公钥加上描述信息,然后经过私钥签名之后得到,一般都是一个人的私钥给另一个人的公钥签名,如果是自己的私钥给自己的公钥签名,就叫自签名。

签名过程

CA收到申请文件后,会走核实流程,确保申请人确实是证书中描述的申请人,防止别人冒充申请者申请证书,核实通过后,会用CA的私钥对申请文件进行签名,签名后的证书包含申请者的基本信息,CA的基本信息,证书的使用年限,申请人的公钥,签名用到的摘要算法,CA的签名。

签完名之后,证书就可以用了。

证书找谁签名合适

别人认不认你的证书要看上面签的是谁的名,所以签名一定要找权威的人来签,否则别人不认,哪谁是权威的人呢?那就是CA,哪些CA是受人相信的呢?那就要看软件的配置,配置相信谁就相信谁,比如浏览器里面默认配置的那些,只要是那些CA签名的证书,浏览器都会相信,而你自己写的程序,可以由你自己指定信任的CA。

信任一个CA就是说你相信你手上拿到的CA的证书是正确的,这是安全的前提,CA的证书是怎么到你手里的,这个不属于规范的范畴,不管你是U盘拷贝的,还是怎么弄来得,反正你得确保拿到的CA证书没问题,比如浏览器、操作系统等,安装好了之后里面就内置了很多信任的CA的证书。

那么CA的证书又是谁签的名呢?一般CA都是分级的,CA的证书都是由上一级的CA来签名,而最上一级CA的证书是自签名证书。

证书如何验证

下面以浏览器为例,说明证书的验证过程:

  1. 在TLS握手的过程中,浏览器得到了网站的证书

  2. 打开证书,查看是哪个CA签名的这个证书

  3. 在自己信任的CA库中,找相应CA的证书,

  4. 用CA证书里面的公钥解密网站证书上的签名,取出网站证书的校验码(指纹),然后用同样的算法(比如sha256)算出出网站证书的校验码,如果校验码和签名中的校验码对的上,说明这个证书是合法的,且没被人篡改过

  5. 读出里面的CN,对于网站的证书,里面一般包含的是域名

  6. 检查里面的域名和自己访问网站的域名对不对的上,对的上的话,就说明这个证书确实是颁发给这个网站的

  7. 到此为止检查通过

如果浏览器发现证书有问题,一般是证书里面的签名者不是浏览器认为值得信任的CA,浏览器就会给出警告页面,这时候需要谨慎,有可能证书被掉包了。如访问12306网站,由于12306的证书是自己签的名,并且浏览器不认为12306是受信的CA,所以就会给警告,但是一旦你把12306的根证书安装到了你的浏览器中,那么下次就不会警告了,因为你配置了浏览器让它相信12306是一个受信的CA。

证书生成示例

下面以实际的例子来看看怎么生成证书。

生成CA的私钥和证书

#创建一个cert目录,后续操作都在该目录下进行
dev@dev:~$ mkdir cert && cd cert

dev@dev:~/cert$ openssl req -newkey rsa:2048 -nodes -sha256 -keyout ca.key -x509 -days 365 -out ca.crt
......
Common Name (e.g. server FQDN or YOUR name) []:ca.com
......
  • -newkey rsa:2048:生成一个长度为2048的采用RSA算法的私钥

  • -nodes:这个私钥在本地存储的时候不加密(可以通过其它参数来加密私钥,这样存储比较安全)

  • -sha256:生成的证书里面使用sha256作为摘要算法

  • -keyout ca.key: 输出私钥到key.pem

  • -x509:证书文件格式为x509,目前TLS默认只支持这种格式的证书

  • -days 365:证书有效期1年

  • -out ca.crt:生成的证书文件保存到ca.crt

生成的过程中会要求填一些信息,除了Common Name要取一个容易区分的名字之外,其它都可以随便填写,我们在这里将它填为ca.com.

生成私钥和证书签名申请文件

dev@dev:~/cert$ openssl req -newkey rsa:2048 -nodes -sha256 -keyout domain.key -new -out domain.csr
......
Common Name (e.g. server FQDN or YOUR name) []:domain.com
......

#这里将CN设置成domain.com

这里和上面的区别就是这里是-new生成一个证书签名申请文件,而上面用-x509生成一个自签名文件,其它的参数意义都一样。

从这里可以看出,CA的私钥和普通人的私钥没什么区别,唯一的区别就是CA用私钥自签名的证书受别人相信,而普通人的自签名证书别人不信,所以需要CA来给证书签名。

使用CA的私钥对申请文件进行签名

dev@dev:~/cert$ openssl x509 -CA ca.crt -CAkey ca.key -in domain.csr -req -days 365 -out domain.crt -CAcreateserial -sha256

由于需要往生成的证书里写入签名者的信息,所以这里需要ca.crt,因为只有这里有CA的描述信息,ca.key里面只有私钥的信息。

查看证书内容

上面生成的证书文件格式都是pem格式。通过下面这个命令可以看到证书的内容:

dev@dev:~/cert$ openssl x509 -text -noout -in ca.crt
dev@dev:~/cert$ openssl x509 -text -noout -in domain.crt

程序支持TLS需要哪些文件

回到最开始的问题,cacert,cert,key对应于上面的哪些东西呢? cacert就是CA的证书,cert就是程序自己的证书,key就是程序自己的私钥。对于服务器来说,至少需要有自己的私钥和证书,而对于客户端来说,至少需要一个cacert,不然没法验证服务器的证书是否正确。

TLS开发示例

server

服务器采用python开发,只需要指定server的私钥和证书就可以了,代码如下:

import BaseHTTPServer, SimpleHTTPServer
import ssl

httpd = BaseHTTPServer.HTTPServer(('localhost', 443), SimpleHTTPServer.SimpleHTTPRequestHandler)
httpd.socket = ssl.wrap_socket (httpd.socket, keyfile="./domain.key", certfile='./domain.crt', server_side=True)
httpd.serve_forever()

将上面的代码保存为server.py,然后启动服务:

#监听443端口需要root权限
dev@dev:~/cert$ sudo python server.py

client

这里使用大家都熟悉的curl作为客户端来测试:

#直接访问报错,提示证书验证失败,
#那是因为domain.crt是我们自己的CA签名的,curl根本就不认识,更谈不上相信它了
dev@dev:~/cert$ curl https://127.0.0.1
curl: (60) server certificate verification failed. CAfile: /etc/ssl/certs/ca-certificates.crt CRLfile: none
More details here: http://curl.haxx.se/docs/sslcerts.html
......

#参数中显式的指定我们CA的证书,让它成为curl信任的CA,这样curl就认为我们的证书没问题了
#但curl还是报错,说这个证书是发给domain.com的,而不是127.0.0.1
dev@dev:~/cert$ curl --cacert ./ca.crt https://127.0.0.1
curl: (51) SSL: certificate subject name (domain.com) does not match target host name '127.0.0.1'

#往/etc/hosts加上一条记录,设置域名domain.com的ip地址为127.0.0.1
dev@dev:~/cert$ sudo sh -c "echo '127.0.0.1 domain.com' >> /etc/hosts"

#然后通过域名来访问,得到了服务器的正确返回
dev@dev:~/cert$ curl --cacert ./ca.crt  https://domain.com
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"><html>
<title>Directory listing for /</title>
<body>
<h2>Directory listing for /</h2>
<hr>
<ul>
<li><a href="ca.crt">ca.crt</a>
<li><a href="ca.key">ca.key</a>
<li><a href="ca.srl">ca.srl</a>
<li><a href="domain.crt">domain.crt</a>
<li><a href="domain.csr">domain.csr</a>
<li><a href="domain.key">domain.key</a>
<li><a href="server.py">server.py</a>
</ul>
<hr>
</body>
</html>

#测试完成之后记得手动将domain.com从/etc/hosts里面删掉

参考

Transport Layer Security
OpenSSL Essentials: Working with SSL Certificates, Private Keys and CSRs


public0821
3.8k 声望1.7k 粉丝