4

作者:鞠朕
野狗科技后端工程师
野狗官博:https://blog.wilddog.com/
野狗官网:https://www.wilddog.com/
公众订阅号:wilddogbaas

图片描述

根据CloudFlare公司的测试报告,OCSP Stapling能提升TLS性能达30%。目前主流的web server都已支持OCSP Stapling,如Apache( 2.3.3 及以上),Nginx(1.3.7及以上), IIS(Win2008及以上)。当使用这些web server作为反向代理的时候,只需要简单的进行配置就可以实现OCSP Stapling。然而野狗的websocket服务使用node.js实现,前边并没有反向代理。目前国内几乎还没有人在node.js上实现过OCSP Stapling,我们在研究的过程中踩过一些坑,也积累了一些经验,在此分享给大家。

OCSP Stapling的来龙去脉

我们知道HTTPS站点基于符合PKI(public key infrastructure )的X.509证书证明自己的身份合法性,但因为信息变更,证书私钥泄露等原因,证书可能在过期之前就被撤销。那么访问HTTPS站点的客户端如何判断服务端下发的证书是否已被撤销呢?这个问题的解决之路上先后出现了三个技术方案:CRLs、OCSP、 OCSP Stapling。

CRLs

CRLs(certificate revocation lists)也就是证书撤销列表。签发证书的CA机构发布并维护着CRLs,并对CRLs签名以防止篡改。当浏览器访问一个HTTPS站点时,如果选择用CRLs的方式,那么浏览器会从服务器下发的证书中取得CRLs的URI,下载CRLs,查询其中是否包含待检验证书的序列号。如果包含,就代表此证书已被撤销。其工作原理如下图所示:

图片描述

这种方式存在一些不足:

  • 增加了HTTPS建连时间,因为浏览器要去下载CRLs。

  • CA维护的CRLs文件会越来越大,因为会不断把撤销的证书信息加入。

  • 解析CRLs并查询,如果CRLs文件很大的话,开销很大。

  • 时效性差,CRLs更新周期一般从5天到14天不等。

  • CRLs只支持EV证书,不支持OV和DV证书。

  • 如果由于网络等原因导致客户端无法成功下载CRLs,默认证书未被撤销。

OCSP

由于CRLs的缺点,在线证书状态协议OCSP(Online Certificate Status Protocol, RFC 6960)应运而生。当浏览器尝试访问一个HTTPS站点时,浏览器从证书中提取OCSP server(CA专门用来处理ocsp请求的服务器,也称OCSP Responser)的URI,向该OCSP server发送一个携带证书的序列号的请求,OCSP server则返回带有目标证书状态的响应。证书状态有三种:good、revoked、unknown。浏览器就能获知证书状态并采取后续动作了。另外,OCSP server的响应也会被签名,以防止被篡改。

图片描述

OCSP实现了实时查询,并且需要较小的网络带宽,客户端的解析开销也比CRLs小很多。但是它存在以下问题:

  • 每个客户端都会独立针对证书发送一个OCSP请求,OCSP server 负载大。

  • 侵犯客户端隐私,OCSP server得知用户访问了哪些站点。

  • 只支持EV证书,不支持OV和DV证书。

OCSP Stapling

与OCSP方式中由客户端向OCSP server发起请求不同,OCSP Stapling是由web服务器向OCSP server周期性地查询证书状态,获得一个带有时间戳和签名的OCSP response并缓存它。当有客户端发起连接请求时,web服务器会把这个response在TLS握手过程中发给客户端。由于有签名的存在,web服务器无法篡改,因此客户端就能得知证书是否已被撤销了。

OCSP Stapling把客户端的查询压力转移到自己身上,访问站点的信息不会泄漏给OCSP server,从而隐私得到了保护。同时由于web server会进行response的缓存,从而减轻了OCSP server的压力。

OCSP stapling把客户端的查询压力转移到自己身上,访问站点的信息不会泄漏给OCSP server,从而隐私得到了保护。同时由于web server会进行response的缓存,从而减轻了OCSP server的压力。

图片描述

OCSP Stapling存在的问题是:

  • 一次只能发送一个OCSP response,不支持证书链(注:Multiple Certificate Status Request Extension, RFC 6961 解决了这个问题,一次可以发送多个response)。

  • 不是所有的浏览器都支持。

OCSP Stpling在Node.JS中的实现

我们使用了Node.JS主力开发人员Fedor Indutny贡献的开源项目:ocsp(https://github.com/indutny/ocsp)。使用开源项目中提供的cache.js,编写代码如下:

server.on('OCSPRequest', function(cert, issuer, cb) {
          ocsp.getOCSPURI(cert, function(err, uri) {
               if (err)
               return cb(err);
               
               var req = ocsp.request.generate(cert, issuer);
               var options = {
                   url: uri,
                   ocsp: req.data
               };

               cache.request(req.id, options, cb);
     });
});

要测试OCSP Stapling的效果,我们推荐使用openssl命令行工具:

openssl s_client -connect example.org:443 -status(example.org是待测试域名),输出信息包括下图中信息:

图片描述

我们看到图中包含了OCSP response信息,并且OCSP Response Status是successful。

也可以使用www.ssllabs.com进行评测,结果如下:

图片描述

下面我们用wireshark抓一下数据包,看看ocsp stapling的相关网络交互行为,其中服务端ip为10.18.6.21,客户端ip为:10.18.6.35,OCSP server ip为:182.50.136.239。

图片描述

上图是客户端连接服务端时,服务端与OCSP server之间的数据包往来:服务端向OCSP server发起了OCSP request请求,并收到了OCSP response,response状态为successful。

图片描述

上图是在客户端抓取的与服务端之间的数据包,可以看到服务端通过TLS向客户端发送了证书状态。

如果服务端由于种种原因无法连接到OCSP server呢?我们进行了一个测试,修改服务器上的hosts文件,将OCSP server的域名绑定到本地,这样服务端就不能完成OCSP Stapling了。

图片描述

图片描述

由上面两图所示,wireshark抓不到服务端与OCSP server之间的数据包了,也就是没进行OCSP Stapling,浏览器也连不上服务端了。这是不正确的,服务端连接OCSP server失败无法获得OCSP response时,应该把验证证书状态的工作转移给客户端进行,而不应直接导致连接握手失败。

官方给的文档和案例中并没有处理这个问题,我们这里给出一个解决方案,已通过我们的测试和验证。忽略获取OCSP response的失败,退化为由客户端进行证书撤销状态的验证,修改代码如下:

server.on('OCSPRequest', function(cert, issuer, cb) {
    ocsp.getOCSPURI(cert, function(err, uri) {
         if (err)
         return cb(err);
         var req = ocsp.request.generate(cert, issuer);
         var options = {
             url: uri,
             ocsp: req.data
         }

         cache.request(req.id, options, function(err,response) {
             if (err) {
             /* ignore err */
             cb();
             return;
             }
             cb(null, response);
         });
     });
});

在客户端抓包结果如下:

图片描述

我们看到客户端自己进行了OCSP查询,得到responseStatus为successful,并与服务端成功建立了连接。

我们在测试的过程中发现了另一个问题:服务器端会频繁访问OCSP server,也就是说OCSP response并未正确地使用缓存。我们做了如下的改进:

server.on('OCSPRequest', function(cert, issuer, cb) {

     ocsp.getOCSPURI(cert, function(err, uri) {
          if (err)
          return cb(err);
          var req = ocsp.request.generate(cert, issuer);
               var options = {
                   url: uri,
                   ocsp: req.data
          };
          if (cache.cache.hasOwnProperty(req.id)) {
                return cb(null, cache.cache[req.id].response);
          } else {
                cache.request(req.id, options, function(err,response) {
                     if (err) {
                            /* ignore err */
                            cb();
                            return;
                         }
                         cb(null, response);
                      });
                   }
               });
          });

判断cache中是否包含证书的response,如果有,直接将response返回给客户端;如果缓存周期到了,旧的OCSP response会从缓存中删除,新的客户端请求到来时,就会走cache.request()查询新的OCSP response并缓存。

我们对改进后的代码进行了测试。开源项目中定义缓存更新时间为36小时。为了加快测试速度,我们修改更新时间为2分钟,测试发现缓存未被清除,并且程序抛出异常,如下图所示:

图片描述

定位异常代码进行debug,我们发现异常导致缓存的OCSP response不能被删除,返回给客户端的回应仍是过期的OCSP response。我们对代码做了如下修改:

图片描述

上图是我们在github上提交的Pull requests。经过测试,OCSP Stapling正常工作了。

在开发的过程中,我们遇到了许多问题,多次和原作者Fedor Indutny沟通都得到了迅速的响应和支持。我们也fix了一些问题并提交了patch,希望能为广大node.js开发者略尽绵薄之力。感谢开源社区的力量!


野狗
2.3k 声望116 粉丝

野狗|安全可靠的实时通信云