CDN通过openresty库实现ocsp stapling,有效提升客户端回源效率

背景

        最近在开发CDN在线加速功能,各个CDN厂商都支持了oscp stapling 功能,所以我们的产品必须也要实现它。实现它的好处就是:可以省掉浏览器和CA机构的服务器校验证书的时间,这样可以提高浏览器的响应速度。

一、什么是ocsp stapling

       对于一个可信任的 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连接时返回给浏览器,这样浏览器就不需要再去查询了。这样解决了隐私和性能问题。
 

二、通过openresty 实现ocsp stapling

1、原理介绍

      我们CDN的边缘节点(跟用户最接近的一层)定时去CA网站进行OCSP查询,并把结果缓存在本地,直到过期时再去更新一下。这样,用户来访问时,在TLS握手阶段,我们CDN直接在server hello 中就把OCSP结果下发给用户浏览器了,用户浏览器就不需要再去访问外面的CA服务器了。

2、献上代码

-- 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的截图如下:

你可能感兴趣的:(openresy知识,CDN(内容分发网络))