CDNはopenrestyライブラリを通じてocsp staplingを実現し、クライアントのソースバック効率を効果的に向上させる


背景
最近CDNオンラインアクセラレーション機能を開発しており、各CDNメーカーはoscp stapling機能をサポートしているので、私たちの製品もそれを実現しなければなりません.これを実現するメリットは、ブラウザとCA機関のサーバが証明書を検証する時間を省くことで、ブラウザの応答速度を向上させることです.
一、ocsp staplingとは
信頼できるCA機関が発行した有効な証明書は、証明書が期限切れになる前にCAが取り消されていない限り、この証明書は有効で信頼できるものです.場合によっては、秘密鍵の漏洩、証明書情報の誤り、CAの脆弱性がハッカーに利用され、他のドメイン名の証明書が発行されたなど、特定の理由で、証明書を取り消す必要があります.では、ブラウザやクライアントは、現在使用されている証明書が取り消されていることをどのように知っていますか.通常、CRL(Certificate Revocation List、証明書取り消しリスト)とOCSP(Online Certificate Status Protocol、オンライン証明書ステータスプロトコル)の2つの方法があります.
CRLの方式:CAサーバーはすでに取り消した証明書のリストを維持することができて、ブラウザはSSLを創立する前に先にCAサーバーに行って自分の訪問したドメイン名の証明書がすでに取り消しられたかどうかを調べて、この方式の欠点も明らかで、例えば:このリストはますます大きくなって、問い合わせの時間も長くなります.ここではこの方法について詳しく議論しません.
OCSPの方式:この方式はOCSPプロトコルを通じてCAサーバーに行って証明書が取り消されたかどうかを照会することであり、OCSPにプライバシーと性能の問題があることは明らかである.例えば、ブラウザはサードパーティCA(Certificate Authority、デジタル証明書認証機関)に直接要求し、ウェブサイトの訪問者(Let’s EncryptがFundebugにアクセスしているユーザーを知っている)を暴露します.また、ブラウザがOCSPクエリーを行うとHTTPSのパフォーマンスが低下します(Fundebugへのアクセスが遅くなります).
OCSPに存在する2つの問題を解決するために,OCSP staplingがある.Webサーバから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が実現されていないスクリーンショットは以下の通りである.