用Nginx搭建HTTPS服务器

摘要

本文主要是在测试https服务时对搭建https服务器的一些实践总结。TLS协议的介绍部分来源于对RFC5246的理解,不当之处,还请指出。后面包含一段对https的性能分析的工程实践。


1 TLS协议简介
1.1 简介

TSL是传输层安全协议(Transport Secure Protocol)的简称,它的前身是SSL(Secure Sockets Layer)。TLS的设计目标是为互联网通信提供安全保证,防止数据被窃听,篡改。TLS协议由记录层(TLS Record Layer)和握手层(TLS Handshake Layer)组成,记录层处于协议的最低层,为TLS协议提供安全可靠的连接。TLS在协议栈中处于HTTP和TCP协议之间,但是TLS协议是独立于应用层协议的,也就是说高层的应用层协议,如HTTP,FTP等都可以创建在TLS之上,TLS的下层可以是任何可靠传输层协议,如TCP,SPX等。

1.2 TLS的版本演进
发布时间 版本 RFC地址
N/A SSL1.0
1995 SSL2.0 rfc6176
1996 SSL3.0
1999 TLS1.0
2006 TLS1.1
2008 TLS1.2 rfc5246

TLS目前已发布的最新版本是TLS1.2,它在RFC5246中定义,其前身SSL1.0,SSL2.0和SSL3.0。SSL由于安全漏洞,已逐渐在被淘汰;TLS1.0和SSL3.0差异很小,几乎等同,渐渐也不再使用,所以现在的话,只要注意TLS1.1以上的版本。

1.3 TLS握手过程

TLS分为记录层协议和握手层协议:
记录层协议(TLS Record Protocol)
记录层协议负责将要发出的消息进行分片,压缩,打上消息认证码(MAC)并最终交到传输层;或将接收到的数据进行解压,解密,数据校验,重新 组装后传到上一层。

握手层协议(TLS Handshake Protocol)
握手层协议处于记录层协议之上,握手层协议的作用是在真正的应用数据传输之前,使客户端和服务器互相进行身份认证,协商加密算法以及生成加密密钥。握手层用于协商出记录层的参数。这些参数将会被记录层用于双方的身份认证,实例化协商的安全参数,错误报告。

TLS在协议栈中的位置:

TLS的协议栈位置

TLS的握手过程

  1. 客户端发送ClientHello消息,包含可选密码组件,一个客户端生成的随机数等,服务端接收后回一个ServerHello消息给客户端,确定密码方案,并生成一个服务器随机数,发给客户端。
    这一步的数据传输在TCP上进行,数据都是公开的,中间人攻击者可以很轻松的得到这些数据。
  2. 服务器发送证书,服务器的身份确认成功后;服务器可能会请求一个客户端证书,要求客户端进行身份认证,客户端给服务器发出证书,验明身份。这一步不是必须的。
  3. 客户端向服务器发出摘要认证,确认接收的消息的完整性,保证没有被第三方篡改。
  4. 双方交换必需的加密参数。其中包括一个预主密钥,客户端会用服务器证书中的公钥将这个密钥加密传给服务器,服务器使用私钥进行解密。
  5. 客户端生成一个预主密钥,并使用服务器证书中的公钥加密,服务器解密后生成一个主密钥。双方使用这个主密钥(master secret)对应用层数据进行对称加密(如AES256)。 主密钥是客户端和服务器共享的。
  6. 最后,把这些准备好的的安全参数交给记录层协议。

下图转自维基百科TLS词条 https://zh.wikipedia.org/wiki/%E5%82%B3%E8%BC%B8%E5%B1%A4%E5%AE%89%E5%85%A8%E5%8D%94%E8%AD%B0

SSL_handshake_with_two_way_authentication_with_certificates.png

会话恢复过程
客户端服务器出于性能的考虑,可能会恢复前面的会话,或复制一个当前存在的会话,而不是重新协商密码参数,建立新的会话(4.2部分使用tcpdump跟踪发现,这个过程确实是耗时占比很大)。

  1. 客户端发出一个ClientHello消息,这个消息包含要恢复的会话的Session ID。
  2. 服务接到这个SessionID后,检查它的缓存是否存在该会话ID。如果找到了,服务器将使用该会话状态重建连接,并发一个ServerHello给客户端,其中包含一个和ClientHello消息一模一样的SessionID。
  3. 此时,协议规定,必须要双方发出ChangeCipherSpec消息,并接着发出Finished消息确认握手完成。然后客户端服务器就可以传输应用层数据了。
  4. 如果服务器没有在缓存中找到Session ID,那么新的SessionID将会建立,然后又会执行一次完全握手。

2 使用nginx搭建https服务器
2.1 环境准备

先搭建nginx,nginx的搭建在官网中可以找到 http://nginx.org/en/docs/install.html
注意如果是用源码编译,有的较老版本构建编译时需要带上参数--with-http_ssl_module,最新的nginx版本是默认有这个编译参数的。

配置

       server {
             listen 443 ssl;
             server_name somehost;
             ssl_certificate $somehost.crt;
             ssl_certificate_key $somehost.key;
             ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
             ssl_ciphers HIGH:!aNULL:!MD5;
             ...
       }

** ssl_certificate** : 存放服务器证书的地址,主要用于数字证书的身份认证。所有连到服务器的客户端都能拿到这个证书;
*** ssl_certificate_key*** : 是私钥,只能被服务器知道。

ssl_protocols, ssl_ciphers选项是可选的。

2.2 详细操作步骤

下面尝试在本地搭建一个简单的https服务器,在443端口监听。

步骤如下

  1. 搭建一个nginx服务器;
  2. 使用openssl生成服务器私钥,客户端私钥,服务器公钥,客户端公钥;
  3. 生成CA根证书,为服务器证书和客户端证书(可选)提供签名服务;
  4. 生成服务器证书和客户端证书,并请求CA的签名;
  5. 在操作系统上信任我们伪造的服务器证书,这里没有为客户端生成证书;
  6. 随便写一个web服务,提供一个url返回一段静态报文,让nginx为它做一下代理,然后启动这个web服务器;
  7. 测试。

以下是这些步骤的操作命令

****生成服务端私钥****:
$ openssl genrsa -out server.key 2048
****生成服务端公钥****:
$ openssl rsa -in server.key -pubout -out server.pem
****生成客户端私钥****:
$ openssl genrsa -out client.key 2048
****生成客户端公钥****:
$ openssl rsa -in client.key -pubout -out client.pem
****生成CA证书****:

 $ openssl genrsa -out ca.key 2048
 $ openssl req -new -key ca.key -out ca.csr
 $ openssl x509 -req -in ca.csr -signkey ca.key -out  ca.crt

生成客户端服务端证书
服务端:
$ openssl req -new -key server.key -out server.csr
$ openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.crt
客户端:
$ openssl req -new -key client.key -out client.csr
向CA申请签名:
$ openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in client.csr -out client.crt

证书和私钥生成完毕后,开始配置nginx(http部分从略);

      #HTTPS server
      server {
              listen       443 ssl;
              server_name  localhost;
              ssl_certificate      ../ssl/certs/server.crt;  #放置服务器证书的目录
              ssl_certificate_key  ../ssl/private/server.key;  #放置服务器私钥的目录
              ssl_session_cache    shared:SSL:1m;
              ssl_session_timeout  5m;
              ssl_ciphers  HIGH:!aNULL:!MD5;
              ssl_prefer_server_ciphers  on;
              location / {
                     proxy_pass   http://127.0.0.1:8887;
                     proxy_set_header Host $host;
                     proxy_set_header X-Real-IP $remote_addr;
              }
       }

配置完后,保存,在nginx根目录下运行命令:
$ ./sbin/nginx -t -c conf/https_server.conf
nginx会测试一下配置文件 conf/https_server.conf
如果输出下面的东西,表示配置没什么格式问题了:

启动nginx:
$ ./sbin/nginx -c conf/https_server.conf
这里我们搭建一个http服务器,在端口8887上监听,使用nginx代理,在浏览器中访问:
https://localhost/hello
返回:

       {
              "user_id":1, "name":"Tom"
       }   
2.3 信任证书:

浏览器中输入
https://localhost:443
因为我们的根证书是自己签发的,签发机构是伪造的,不在浏览器的信任列表中。对于这种情况,不同的浏览器对此会有不同的表现,但主流的浏览器都会发出警告。下面提供了一种办法,使浏览器信任证书。注意,这里只是为了测试,正常上网不要忽略浏览器的警告,轻易地信任不明来源的证书。

下面是在Mac下的Google Chrome中信任证书的操作步骤:
. 当浏览器提示连接不安全时,在浏览器的抓包工具中点到如下的界面:

图4

. 点按钮:View Certificate
. 拖动证书的icon,拉到本地桌面上并双击它:

图5:访问https://www.baidu.com得到的服务器证书,仅举例

双击它打开系统的钥匙串,将桌面证书拖拽到系统钥匙串中,
双击打开,选择『始终信任』

图6

图7

注:截图的证书为测试时随意生成的根证书;所有证书信任的办法类似。

重启浏览器后,再访问https://localhost/hello ,会看到这样的结果:

图8
2.4 测试加密效果

现在,我们尝试用抓包工具模拟中间人截取报文,看看能不能截取到报文:

在mac上我们使用charles进行抓包,结果如下,报文已经被加密,看到的内容都是乱码:

图9

尝试用Charles进行一次中间人攻击:伪造证书,在Charles上可以安全一个代理证书,具体操作参考这里:http://www.jianshu.com/p/7a88617ce80b
,然后再访问https://localhost/hello

这时浏览器会又提示我们连接不安全,这是因为TLS握手感知到了客户端的证书来源不明,假如我们点击高级,继续访问,忽略这个警告,那么我们会在抓包工具上看到这样的画面:

图10

报文的内容成功被Charles窃听了,所以,再次验证了一点:如果正常上网浏览网页,操作电子银行等敏感信息时,不要随便忽略浏览器的安全警告。

由于大多数人安全意识薄弱,很习惯地点继续访问不安全连接 ,所以就有了HSTS。

2.5 HSTS是什么

****  HSTS(HTTP Strict Transport Security)****,这是一个由IEEE发布的网络安全策略机制,如果服务器增加了这种策略,那么就意味着要求客户端必须使用https协议和服务器进行对话。
  主流的浏览器都实现了HSTS策略。当TLS握手出现问题时,浏览器不会再访问服务器。
  百度的防火墙代理就实现了这个策略。访问https://www.baidu.com ,在浏览器截取到的服务器响应头中有一个Strict-Transport-Security字段 max-age=172800 。max-age是指HSTS的失效时间,单位是秒。

图11

由于服务器响应头增加了这个字段,浏览器就会履行一些HSTS的职责。假如我们采取抓包工具伪造一个未知证书,再去访问域名,就会受到警告,尝试让浏览器忽略不安全的警告继续访问会被禁止,如下图:

图12

然而HSTS也不是绝对安全的。一方面原因在于Strict-Transport-Security的max-age会过期,浏览器是否强制HSTS策略取决于当前系统时间,它可能伪造;另一方面是因为首次访问网址并不受HSTS保护。可见技术对安全的防护永远是有限的,还需要用户有足够的安全意识进行自我保护。

3 实现https抓包

了解了https的原理之后,知道TLS的握手关键在身份认证这一步。HTTPS的数据传输可以类比成交谈的双方各自亮出身份证,然后用约定的暗号进行交流,这样即时旁边有人窃听,也无法破解经过加密的信息。

测试时,通常会有抓取https包的需求。如果以为https已被加密,我们没办法抓到明文包,那这是不对的。抓包的原理通常是在通信双方插一个中间代理,对原有的客户端和服务器的请求进行来回转发。如果要抓取https包,显然,构造的代理必须有完成TLS握手的能力,所以代理工具一般需要支持ssl。有了TLS的握手能力,我们再设法使通信双方相信代理的身份,就能实现https的抓包了。可以看到,https的数据安全关键点其实就是在数字证书上 ,因此也很容易理解为什么主流的浏览器都会警告用户使用了来路不明的机构颁发的证书。

实现https抓包,具体操作方案就是:用一个支持ssl的代理,伪造一个ca证书,当通信双方在TLS握手时发现有人窃听时,设法让他们相信代理的身份(这个过程就见2.3); 代理身份被信任了,就会发生这个过程: 客户端和代理进行握手,请求数据发到代理上,由代理和服务器做TLS握手,并转传客户端的请求数据;服务器加密报文应答,代理解密报文再用和客户端协商的密码方案加密报文,传给客户端,客户端解密报文。

Charles抓包参看:http://www.jianshu.com/p/7a88617ce80b


4 HTTPS的性能
4.1 HTTPS与HTTP的性能评测对比

我们使用Jmeter来测试http和https的比较。
在远程服务器上搭建一个http服务器,让它返回一个简单的静态网页:

      <!DOCTYPE html>
      <html lang="en">
      <head><meta charset="UTF-8">
            <title>首页测试</title>
      </head><body>
      <p>这是一个测试网页</p>

然后使用nginx进行代理
第一次关闭nginx的ssl安全开关,让服务器作为普通的http服务器,用jmeter发请求;第二次,开启ssl策略,强制进行TLS握手,再用jmeter发请求。

两次的数据比较如下

请求方式 响应时间(ms)
http 164
https 419

100个样本的测试数据
http100个样本的数据:

图13

https100个样本的数据:

图14

注意:
. 使用jmeter发出100个样本时,需要把keep-alive选项勾掉,保证每次请求都进行了TCP握手,而不是复用连接;否则得到的性能数据会更好。
. 发出https请求时,除了在服务端nginx上把ssl的开关打开,还要在jmeter的协议选项中填上https,如图:


图15

可以看到,使用了https,如果每次连接都进行完全握手,https的性能会比http多出250ms左右。也就是说,请求一个动态网页的响应时间假如是200ms,那么https会使性能下降100%。然而,如果使用keep-alive连接复用,性能的对比并没有那么可怕:
使用keep-alive,100个https的请求的结果:

图16

http

图17

除了第一个样本的差距比较大,整个平均响应时间看上去https反而要略优于http。
下面尝试使用tcpdump探查一下TLS握手各个阶段的耗时分布


4.2 TLS握手的性能评测
4.2.1数据包截取

这里使用tcpdump来抓一段https请求的数据包(这里并没有发生TLS完全握手,客户端没有给服务器传证书):

输入命令:
$ sudo tcpdump host $hostname
然后请求一下https的url,会截获到如下的数据:

Line 1 15:19:48.793920 IP client.https.host.63209 > server.httpshost: Flags [S], seq 2411985045, win 65535, options [mss 1460,nop,wscale   5,nop,nop,TS val 669725535 ecr 0,sackOK,eol], length 0
Line 2 15:19:48.875001 IP server.httpshost > client.https.host.63209: Flags [S.], seq 284701943, ack 2411985046, win 14480, options [mss 1460,sackOK,TS val 175414051 ecr 669725535,nop,wscale 8], length 0
Line 3 15:19:48.875074 IP client.https.host.63209 > server.httpshost: Flags [.], ack 1, win 4117, options [nop,nop,TS val 669725616 ecr 175414051], length 0
Line 4 15:19:48.876925 IP client.https.host.63209 > server.httpshost: Flags [P.], seq 1:201, ack 1, win 4117, options [nop,nop,TS val 669725617 ecr 175414051], length 200
Line 5 15:19:48.958843 IP server.httpshost > client.https.host.63209: Flags [.], ack 201, win 61, options [nop,nop,TS val 175414134 ecr 669725617], length 0
Line 6 15:19:48.961690 IP server.httpshost > client.https.host.63209: Flags [.], seq 1:1449, ack 201, win 61, options [nop,nop,TS val 175414135 ecr 669725617], length 1448
Line 7 15:19:48.961696 IP server.httpshost > client.https.host.63209: Flags [.], seq 1449:2897, ack 201, win 61, options [nop,nop,TS val 175414135 ecr 669725617], length 1448
Line 8 15:19:48.961736 IP client.https.host.63209 > server.httpshost: Flags [.], ack 2897, win 4027, options [nop,nop,TS val 669725700 ecr 175414135], length 0
Line 9 15:19:48.961914 IP server.httpshost > client.https.host.63209: Flags [P.], seq 2897:3255, ack 201, win 61, options [nop,nop,TS val 175414135 ecr 669725617], length 358
Line 10 15:19:48.961933 IP client.https.host.63209 > server.httpshost: Flags [.], ack 3255, win 4016, options [nop,nop,TS val 669725700 ecr 175414135], length 0
Line 11 15:19:48.964670 IP client.https.host.63209 > server.httpshost: Flags [P.], seq 201:276, ack 3255, win 4096, options [nop,nop,TS val 669725702 ecr 175414135], length 75
Line 12 15:19:49.045770 IP server.httpshost > client.https.host.63209: Flags [.], ack 276, win 61, options [nop,nop,TS val 175414221 ecr 669725702], length 0
Line 13 15:19:49.045812 IP client.https.host.63209 > server.httpshost: Flags [P.], seq 276:327, ack 3255, win 4096, options [nop,nop,TS val 669725783 ecr 175414221], length 51
Line 14 15:19:49.126510 IP server.httpshost > client.https.host.63209: Flags [.], ack 327, win 61, options [nop,nop,TS val 175414302 ecr 669725783], length 0
Line 15 15:19:49.126517 IP server.httpshost > client.https.host.63209: Flags [P.], seq 3255:3306, ack 327, win 61, options [nop,nop,TS val 175414302 ecr 669725783], length 51
Line 16 15:19:49.126575 IP client.https.host.63209 > server.httpshost: Flags [.], ack 3306, win 4094, options [nop,nop,TS val 669725862 ecr 175414302], length 0
Line 17 15:19:49.127304 IP client.https.host.63209 > server.httpshost: Flags [P.], seq 327:471, ack 3306, win 4096, options [nop,nop,TS val 669725862 ecr 175414302], length 144
Line 18 15:19:49.208120 IP server.httpshost > client.https.host.63209: Flags [.], ack 471, win 65, options [nop,nop,TS val 175414384 ecr 669725862], length 0
Line 19 15:19:49.209352 IP server.httpshost > client.https.host.63209: Flags [P.], seq 3306:3671, ack 471, win 65, options [nop,nop,TS val 175414385 ecr 669725862], length 365
Line 20 15:19:49.209394 IP client.https.host.63209 > server.httpshost: Flags [.], ack 3671, win 4084, options [nop,nop,TS val 669725943 ecr 175414385], length 0
Line 21 15:19:49.210228 IP client.https.host.63209 > server.httpshost: Flags [P.], seq 471:502, ack 3671, win 4096, options [nop,nop,TS val 669725943 ecr 175414385], length 31
Line 22 15:19:49.210263 IP client.https.host.63209 > server.httpshost: Flags [F.], seq 502, ack 3671, win 4096, options [nop,nop,TS val 669725943 ecr 175414385], length 0
Line 23 15:19:49.292232 IP server.httpshost > client.https.host.63209: Flags [.], ack 502, win 65, options [nop,nop,TS val 175414467 ecr 669725943], length 0
Line 24 15:19:49.292239 IP server.httpshost > client.https.host.63209: Flags [F.], seq 3671, ack 502, win 65, options [nop,nop,TS val 175414467 ecr 669725943], length 0
Line 25 15:19:49.292240 IP server.httpshost > client.https.host.63209: Flags [.], ack 503, win 65, options [nop,nop,TS val 175414467 ecr 669725943], length 0
Line 26 15:19:49.292286 IP client.https.host.63209 > server.httpshost: Flags [F.], seq 502, ack 3672, win 4096, options [nop,nop,TS val 669726025 ecr 175414467], length 0
4.2.2抓包数据解析
  • Line1-Line3 是TCP的3路握手,这里耗时从15:19:48.793920到15:19:48.875074 这一次RTT花费了81.154ms
  • Line4-Line5 判断是客户端发ClientHello消息,这里客户端发出了200字节,TLS握手的开始
    *Line7 Line8 Line9 从TCP选项可以看出来,是数据分了三组传送,总字节是1448 * 2 + 358 = 3254个字节,由TLS协议可以判断这是服务器在回应ServerHello以及传递服务器证书;这个过程耗时是89.968 ms;与一次TCP握手耗时相近
  • Line10到Line16 是客户端和服务器协商密钥的过程。这期间,客户端发送了两次数据,服务器发了一次;服务器进行了两次非对称解密运算,一次数字签名校验,一次解密拿到随机预主密钥,总共耗时161.905ms,到Line16 TLS 握手完成
  • Line17行~Line22开始进行应用数据传输,传输时使用预主密钥计算出来的主密钥对应用层数据进行加密,总共耗时82.959ms
  • 最后4行是TCP的关闭过程,可以暂不考虑。
4.2.3 TLS性能结果汇总

从4.2.2 的分析可以看到整个过程400多ms的时间分配如下:

过程 耗时/ms 耗时占比%
tcp握手 81.154ms 19.49
传递证书和ClientHello,ServerHello 89.968ms 21.61
协商主密钥和签名校验(2次公钥加密/解密运算) 161.905ms 38.89
应用层数据传输 82.959ms 19.93
其他 0.357ms 0.08
总耗时(Line22时间减去Line1) 416.343ms 19.49

进行过充分优化的https连接主要的耗时基本上集中在应用层数据传输上,这跟我们使用Jmeter评测keep-alive状态得到的数据相吻合。

从表中可见,HTTPS完全握手下性能消耗大多集中在TLS握手期间,约占60%。TLS握手中服务器进行RSA解密不仅会有时间消耗,还会对服务器形成CPU压力。

通过这个分析,可以看出HTTPS的性能优化策略应该会从尝试考虑减少TLS握手频率,复用TLS会话,在分布式服务中用缓存共享会话,或将解密运算转到云端,提升服务器的硬件级别等方面入手。

附1:密码学相关概念:

主密钥(master secret):一个48位的共享密钥,客户端和服务器都会知道。
MAC算法: 消息认证码算法。
对称密钥加密:加密和解密编码使用的密钥相同。对称加密的缺点之一是任何两台主机进行通信都需要维护一个双方都知道的密钥,造成管理负担,一个有N个站点的网络,可能需要维护 N(N-1)个密钥。
流加密:是一种对称加密算法,加密和解密双方使用相同伪随机加密数据流(pseudo-random stream)作为密钥,明文数据每次与密钥数据流顺次对应加密,得到密文数据流。
分组加密:又称分块加密或块密码,是一种对称密钥算法。它将明文分成多个等长的模块(block),使用确定的算法和对称密钥对每组分别加密解密。如AES,DES
公钥加密:非对称加密。加密和解密使用不同的密钥,加密密钥是公开的,解密密钥一般被服务器保存,不对外公布。
数字签名:附在报文中的特殊加密校验码,用来证明报文作者的身份。
数字证书:类似与身份ID,常用的数字证书格式是X.509 v3,证书中包含基本的字段信息:序列号,签名算法ID,证书颁发机构,有效期,对象名称,对象公开密钥信息,发布者的唯一ID,对象的唯一ID,扩展。
PKI: Public Key Infrastructure 公钥基础设施,是一组由硬件、软件、参与者、管理政策与流程组成的基础架构,其目的在于创造、管理、分配、使用、存储以及撤销数字证书。

附2:参考资料

说明: 本文关于TLS协议的描述本于RFC5246,Nginx搭建https本于nginx的官网文档

  1. RFC5246参考网址: https://tools.ietf.org/html/rfc5246
  2. Nginx安装参考网址:http://nginx.org/en/docs/install.html
  3. Nginx搭建https参考网址: http://nginx.org/en/docs/http/configuring_https_servers.html
  4. 信任证书方案本文只是在MAC系统上进行了操作实践,其他操作系统可参考 : http://blog.getpostman.com/2014/01/28/using-self-signed-certificates-with-postman/

附3:使用charles进行https抓包

也是我写的一篇文章:http://www.jianshu.com/p/7a88617ce80b

推荐阅读更多精彩内容