最近在开发CDN在线加速功能,各个CDN厂商都支持了oscp stapling 功能,所以我们的产品必须也要实现它。实现它的好处就是:可以省掉浏览器和CA机构的服务器校验证书的时间,这样可以提高浏览器的响应速度。
对于一个可信任的 CA 机构颁发的有效证书,在证书到期之前,只要 CA 没有把其吊销,那么这个证书就是有效可信任的。有时,由于某些特殊原因(比如私钥泄漏,证书信息有误,CA 有漏洞被黑客利用,颁发了其他域名的证书等等),需要吊销某些证书。那浏览器或者客户端如何知道当前使用的证书已经被吊销了呢,通常有两种方式:CRL(Certificate Revocation List,证书吊销列表)和 OCSP(Online Certificate Status Protocol,在线证书状态协议
CRL的方式:CA服务器会维护已吊销的证书列表,浏览器在建立SSL之前先去CA服务器查询下自己所访问的域名证书是否已经被吊销,这种方式的缺点也很明显,比如:这个列表会越来越大,查询时间也会变长。我们这里不过细讨论这种方式。
OCSP的方式:这种方式就是通过OCSP协议去CA服务器查询证书是否被吊销,显然OCSP存在隐私和性能问题。比如:浏览器直接去请求第三方CA(Certificate Authority, 数字证书认证机构),会暴露网站的访客(Let’s Encrypt会知道哪些用户在访问Fundebug);此外,浏览器进行OCSP查询会降低HTTPS性能(访问Fundebug会变慢)。
为了解决OCSP存在的2个问题,就有了OCSP stapling。由网站服务器去进行OCSP查询,缓存查询结果,然后在与浏览器进行TLS连接时返回给浏览器,这样浏览器就不需要再去查询了。这样解决了隐私和性能问题。
我们CDN的边缘节点(跟用户最接近的一层)定时去CA网站进行OCSP查询,并把结果缓存在本地,直到过期时再去更新一下。这样,用户来访问时,在TLS握手阶段,我们CDN直接在server hello 中就把OCSP结果下发给用户浏览器了,用户浏览器就不需要再去访问外面的CA服务器了。
-- Description:动态加载ssl证书
-- Copyright (C) by CRGT, shaoshuli, 2020.05.05
local ssl = require "ngx.ssl"
local ocsp = require "ngx.ocsp"
local http = require "resty.http"
local redis = require "redis_utils"
local cjson = require "cjson"
-- 清除之前设置的证书和私钥
local ok, err = ssl.clear_certs()
if not ok then
ngx.log(ngx.ERR, "Failed to clear existing (fallback) certificates.")
return ngx.exit(ngx.ERROR)
end
--[[获取客户端的SNI name, 如果得到的结果为nil,说明客户端没有设置SNI,
这时应该从raw_server_addr method()函数获取serv IP, 进而从dns反向解析出server name.
本功能还待完善。]]
local req_host, err = ssl.server_name()
ngx.log(ngx.INFO, "req_host:", req_host)
if not req_host then
ngx.log(ngx.ERR, "Failed to get server name, exit.")
--return ngx.exit(ngx.ERROR)
end
--取出share dict 实例
local shared_data = ngx.shared.shared_data
--从redis 中获取ssl公钥和私钥
local function get_my_pem_cert_data()
local domain_conf_json = nil
--shared_data:delete(req_host)
local domain_conf_str, flags = shared_data:get(req_host)
--KEY 过期或者不存在,会返回nil
if domain_conf_str == nil then
--ngx.log(ngx.INFO, "Not found key in share dict, start to set key: ", req_host)
local redis_host = "127.0.0.1"
local port = 6379
local db_num = 1
local red_handle = redis.connect_redis(redis_host, port, db_num, nil)
domain_conf_json = redis.get_meta_from_redis(red_handle, req_host)
--ngx.log(ngx.INFO, "domain_conf_json: ", domain_conf_json["domain_name"])
--保存一份到共享内存
local domain_conf_str = cjson.encode(domain_conf_json)
local ok,err = shared_data:safe_set(req_host, domain_conf_str, 120)
else
--已经存在key 了,直接用共享内存的数据
domain_conf_json = cjson.decode(domain_conf_str)
--ngx.log(ngx.INFO, "Found key in share dict:", req_host)
end
--返回ssl证书信息json
local ssl_https_data = domain_conf_json["ssl_https_data"]
return ssl_https_data
end
-- 获取证书内容, 从redis 读取
local ssl_https_data = get_my_pem_cert_data()
if not ssl_https_data then
ngx.log(ngx.ERR, "Failed to get PEM ssl_https: ", err)
return
end
local der_cert_chain = nil
local der_priv_key = nil
--证书格式有pem 和der两种,针对pem 的证书,我们把它转换成der,方便后续做oscp stapling。
if ssl_https_data['type'] == 'pem' then
der_cert_chain, err = ssl.cert_pem_to_der(ssl_https_data['cert_data'])
if not der_cert_chain then
ngx.log(ngx.ERR, "pem cert transe to der cert chain faild.")
return
end
der_priv_key, err = ssl.priv_key_pem_to_der(ssl_https_data['pkey_data'])
if not der_priv_key then
ngx.log(ngx.ERR, "Pem key transe to der private key faild.")
return
end
else
der_cert_chain = ssl_https_data['cert_data']
der_priv_key = ssl_https_data['pkey_data']
end
local ok, err = ssl.set_der_cert(der_cert_chain)
if not ok then
ngx.log(ngx.ERR, "Set der cert faild.")
return
end
local ok, err = ssl.set_der_priv_key(der_priv_key)
if not ok then
ngx.log(ngx.ERR, "set der private key faild.")
return
end
---OSCP更新。共享内存如果不存在或者已过期,则从云端更新oscp response,并update到share dict
local req_host_oscp_res = nil
--oscp key
local req_host_oscp = req_host..'_oscp'
req_host_oscp_res = shared_data:get(req_host_oscp)
if req_host_oscp_res == nil then
local ocsp_url, err = ocsp.get_ocsp_responder_from_der_chain(der_cert_chain)
if not ocsp_url then
ngx.log(ngx.ERR, "failed to get OCSP responder: ", err)
return ngx.exit(ngx.ERROR)
end
ngx.log(ngx.INFO, "oscp url:", ocsp_url)
local ocsp_req, err = ocsp.create_ocsp_request(der_cert_chain)
if not ocsp_req then
ngx.log(ngx.ERR, "failed to create OCSP request: ", err)
return ngx.exit(ngx.ERROR)
end
local httpc = http.new()
httpc:set_timeout(10000)
local res, req_err = httpc:request_uri(ocsp_url, {
method = "POST",
body = ocsp_req,
headers = {
["Content-Type"] = "application/ocsp-request",
}
})
-- 校验 CA 的返回结果
if not res then
ngx.log(ngx.ERR, "OCSP responder query failed: ", err)
return ngx.exit(ngx.ERROR)
end
local http_status = res.status
if http_status ~= 200 then
ngx.log(ngx.ERR, "OCSP responder returns bad HTTP status code ",
http_status)
return ngx.exit(ngx.ERROR)
end
req_host_oscp_res = res.body
--ngx.log(ngx.INFO, 'req_host_oscp_res:', req_host_oscp_res)
--设置ocsp 到 share dict
local ok,err = shared_data:safe_set(req_host_oscp, req_host_oscp_res, 600)
end
--if req_host_oscp_res and #req_host_oscp_res > 0 then
if req_host_oscp_res then
local ok, err = ocsp.validate_ocsp_response(req_host_oscp_res, der_cert_chain)
if not ok then
ngx.log(ngx.ERR, "failed to validate OCSP response: ", err)
return ngx.exit(ngx.ERROR)
end
-- 设置当前 SSL 连接的 OCSP stapling
ok, err = ocsp.set_ocsp_status_resp(req_host_oscp_res)
if not ok then
ngx.log(ngx.ERR, "failed to set ocsp status resp: ", err)
return ngx.exit(ngx.ERROR)
end
end
我们把实现OCSP stapling 和未实现的结果进行下对比:
1、已经实现OCSP stapling的截图如下。从截图我们可以看出,server Hello的时候,服务器就把certifcat Status 下发给用户了。
2、未实现OCSP stapling的截图如下: