Sindbad~EG File Manager

Current Path : /opt/nginxhttpd_/etc/openresty_config/lua/lib/
Upload File :
Current File : //opt/nginxhttpd_/etc/openresty_config/lua/lib/o2switch_ocsp.lua

--[[
    This file contains a module with some functions that we can used to get and staple the OCSP Response for a certificate
--]]

local _M = {}

local ocsp = require("ngx.ocsp")
local httpc = require("resty.http")
local extension = require("resty.openssl.x509.extension")
local o2debug = require "lib/o2switch_debug"
local o2redis = require "lib/o2switch_redis"
local o2config = require "lib/o2switch_config"

local time = os.time
local tonumber = tonumber
local string = string
local pcall = pcall
local type = type

local defaultTtl = 60*60*12 -- The default cache TTL we'll use if we cant extract the nextUpdate from the OCSP Response
local errorTtl = 1800 -- Cache TTL for Hard error, on which we'll probably not recover.
local tmpErrorTtl = 10 -- Cache TTL Tmp error, on which it'll probably work if we test again later

--- Validate the OCSP Response
-- @param ocsp_resp The DER OCSP Response
-- @param crt the DER Certificate
-- @return bool, errMsg|nil
local function validateOcsp(ocsp_resp, crt)
    if ocsp_resp and type(ocsp_resp) == 'string' and #ocsp_resp > 0 then
        local ok, err = ocsp.validate_ocsp_response(ocsp_resp, crt)
        if not ok then
            --o2debug.debug("validateOcsp() Failed to validate OCSP response: " .. (err or "no error message"))
            return false, "Failed to validate OCSP response: " .. (err or "no error message")
        end
    else
        --o2debug.debug("validateOcsp() OCSP response seems empty.")
        return false, "OCSP response seems empty"
    end
    --o2debug.debug("validateOcsp() OCSP response validated ! OK.")
    return true, nil
end

--- Calculate the appropriate cache TTL based on the OCSP Response. It does this by trying to extract the nextUpdate field from the DER OSCP response.
-- @param ocsp_resp The DER OCSP Response
-- @return ttl|nil, errMsg|nil
local function calculateCacheTtlFromOcspResponse(ocsp_resp)
    -- @See https://github.com/openssl/openssl/blob/master/include/openssl/obj_mac.h#L2002
    local ext, err =  extension.from_der(ocsp_resp, 365)
    if err then
        --o2debug.debug("calculateCacheTtlFromOcspResponse() Error received while trying to extension.from_der(): " .. (err or 'no message'))
        return defaultTtl, nil
    end

    local txt, err = ext:tostring()
    if err then
        --o2debug.debug("calculateCacheTtlFromOcspResponse() Error received while trying to ext:tostring: " .. (err or 'no message'))
        return defaultTtl, nil
    end

    local last
    for w in  string.gmatch (txt,  "%d%d%d%d%d%d%d%d%d%d%d%d%d%d") do
        last = w
    end

    if last == nil then
        --o2debug.debug("calculateCacheTtlFromOcspResponse() Cant extract the Nextupdate from the CRT")
        return defaultTtl, nil
    end

    local ok, nextUpdateTime = pcall(time, {
        year = string.sub(last, 1, 4),
        month = string.sub(last, 5, 6),
        day = string.sub(last, 7, 8),
        hour = string.sub(last, 9, 10),
        -- minute = string.sub(last, 11, 12),
        minute = 00,
        second = 00,
    })
    if not ok then
        --o2debug.debug("calculateCacheTtlFromOcspResponse() Error received when trying to use the time function. Probably a badly extracted nextUpdate field.")
        return defaultTtl, nil
    end

    local ok, delta = pcall(function(nextUpdateTime, currentTime)
        return tonumber(nextUpdateTime - currentTime)
    end, nextUpdateTime, ngx.time())

    if not ok or delta == nil then
        --o2debug.debug("calculateCacheTtlFromOcspResponse() Error while trying to calculate the OCSP Delta (NextUpdate - CurrentTime)")
        return defaultTtl, nil
    end

    -- If the delta is negative, it probably mean that the Nginx time was not updated for a long time.
    if delta < 0 then
        --o2debug.debug("calculateCacheTtlFromOcspResponse() OCSP Delta  (NextUpdate - CurrentTime). Updating Nginx time to try one more time.")
        ngx.update_time()
        local ok, delta = pcall(function(nextUpdateTime, currentTime)
            return tonumber(nextUpdateTime - currentTime)
        end, nextUpdateTime, ngx.time())
        if not ok or delta == nil then
            --o2debug.debug("calculateCacheTtlFromOcspResponse() Error while trying to calculate the OCSP Delta (NextUpdate - CurrentTime). Nginx time already updated.")
            return defaultTtl, nil
        end
    end

    if delta < 0 then
        --o2debug.debug("calculateCacheTtlFromOcspResponse() OCSP Delta (NextUpdate - CurrentTime) still < 0. Nginx time already updated. Response expired ? Wrong time ?")
        return nil, "Delta still negative after updating Nginx time. Response expired ? Wrong time ?"
    end

    --o2debug.debug("calculateCacheTtlFromOcspResponse() OCSP Delta (NextUpdate - CurrentTime) found. " .. delta)
    return delta, nil
end

--- Fetch the OCSP Response from the Redis cache layer.
-- @param crt DER encode Certificate
-- @param name The domain name (used as a key to store it in Redis)
-- @return bool, ErrMsg|null, oscpResponse|null, ,delta|nil
local function getOcspFromRedis(crt, name)
    local ocsp_resp, err = o2redis.getElmFromRedis('ocsp', name)

    if not ocsp_resp or err then
        --o2debug.debug("getOcspFromRedis() No OCSP in Redis for " .. name .. ' ' .. (err or ''))
        return true, nil, nil, tmpErrorTtl
    end

    
    local ok, err = validateOcsp(ocsp_resp, crt)
    if not ok or err then
        --o2debug.debug("getOcspFromRedis() OCSP retrieved from Redis is not valid " .. name .. ' ' .. (err or ''))
        return false, "OCSP Validation failed", nil, tmpErrorTtl
    end

    local ttl, err = calculateCacheTtlFromOcspResponse(ocsp_resp)
    if not ttl or err then
        --o2debug.debug("getOcspFromRedis() Cant get a TTL for the OCSP retrieve from Redis " .. name)
        return false, "Cant get a TTL for the OCSP store in Redis", nil, tmpErrorTtl
    end
    --o2debug.debug("getOcspFromRedis() OCSP Response retrieved from Redis " .. name)
    return true, nil, ocsp_resp, ttl
end

--- Manually fetch a fresh OCSP within Openresty and add it to the Redis cache if it's valid.
-- @param crt DER encode Certificate
-- @param name The domain name (used as a key to store it in Redis)
-- @return oscp_response (or nil if err), , err_message (or nil if ok), delta (cache ttl or negative ttl in case of err)
local function fetchOcspFromCrt(crt, name)
    -- 1. Retrieve the OCSP Responder URL from the CRT
    local ocsp_url, err = ocsp.get_ocsp_responder_from_der_chain(crt)
    if not ocsp_url then
        --o2debug.debug("fetchOcspFromCrt() " .. name or '' .. " Failed to get OCSP Responder: " .. (err or "no error message"))
        return nil, "Failed to get OCSP Responder: " .. (err or "no error message"), errorTtl
    end

    -- 2. Prepare the request to the OCSP Responder from the CRT
    local ocsp_req, err = ocsp.create_ocsp_request(crt)
    if not ocsp_req then
        --o2debug.debug("fetchOcspFromCrt() " .. name or '' .. " Failed to create OCSP request: " .. (err or "no error message"))
        return nil, "Failed to create OCSP request: " .. (err or "no error message"), errorTtl
    end

    -- 3. Make the request to the OCSP responder and retrieve the raw OCSP Response
    local httpClient = httpc.new()
    local res, err = httpClient:request_uri(ocsp_url , {
        method = "POST",
        body = ocsp_req,
        headers = {
            ["Content-Type"] = "application/ocsp-request"
        },
    })

    if not res then
        --o2debug.debug("fetchOcspFromCrt() " .. name or '' .. " OCSP responder query failed: " .. (err or "no error message"))
        return nil, "OCSP responder query failed: " .. (err or "no error message"), tmpErrorTtl
    end

    -- 4. Validate the response from the OCSP Responder. Check the HTTP code first and then check with the Nginx function
    local http_status = res.status
    if http_status ~= 200 then
        --o2debug.debug("fetchOcspFromCrt() " .. name or '' .. " OCSP responder returned a bad HTTP status code" .. http_status)
        return nil, "OCSP responder returned a bad HTTP status code" .. http_status, tmpErrorTtl
    end

    local ocsp_resp = res.body
    local ok, err = validateOcsp(ocsp_resp, crt)
    if not ok then
        --o2debug.debug("fetchOcspFromCrt() " .. name or '' .. " Error on OCSP response validation" .. (err or 'no err msg'))
        return nil, "Error on OCSP response validation" .. http_status, tmpErrorTtl
    end

    -- 5. Parse the OCSP Response to extract the NextUpdate field and determine how long we can cache a response.
    local ttl, err = calculateCacheTtlFromOcspResponse(ocsp_resp)
    if not ttl then
        --o2debug.debug("fetchOcspFromCrt() " .. name or '' .. " Cant calculate the TTL for the OCSP response " .. (err or 'no err msg'))
        return nil, "Cant calculate the TTL for the OCSP response" .. http_status, tmpErrorTtl
    end

    return ocsp_resp, nil, ttl
end

---
-- Bellow, the functions that we expose via the module.
---

--- Get the OCSP response from the certificate CRT (DER format)
-- It will return the OCSP response to use for the stapling, followed by an error message (nil if not), then followed
-- by the Delta (OCSP Response Next Update - now) that can be used to determine for how much time we'll cache the
-- response. The delta can also be used in case of error, to know for a long we'll cache the error to avoid bad looping.
-- @param crt The CRT in the DER format
-- @param name The domain name (or nil)
-- @return oscp_response (or nil if err), err_message (or nil if ok), delta (cache ttl or negative ttl in case of err)
function _M.getOcsp(crt, name)
    -- First, we check if we have an OCSP Response stored in Redis
    local ok, errMsr, ocsp_resp, ttl = getOcspFromRedis(crt, name)
    if ok and not errMsr and ocsp_resp and ttl then
        return ocsp_resp, nil, delta
    end

    if o2config.fetchOcspLocally then
        --o2debug.debug('Cant fetch the OCSP from redis, switching to an alternative method.')
        -- If we don't have a valid OCSP, try to fetch a new one instead
        local ocsp_resp, err, delta = fetchOcspFromCrt(crt, name)
        if ocsp_resp and type(ocsp_resp) == string and not err then
            o2redis.setElmToRedis(name, 'ocsp', ocsp_resp)
        end
        return ocsp_resp, err, delta
    end
    --o2debug.debug('Cant fetch the OCSP from redis and fetchOcspLocally set to false.')
    return nil, nil, errorTtl
end

--- Staple the OCSP Response
-- @return bool, errMsg|nil
function _M.stapleOcspResponse(ocspResp)
    if not ocspResp then
        --o2debug.debug("stapleOcspResponse() ocspResp is empty or equal to nil. Abort.")
        return false, "ocspResp is empty or equal to nil. Abort."
    end
    local ok, err = ocsp.set_ocsp_status_resp(ocspResp)
    if not ok then
        --o2debug.debug("stapleOcspResponse() Error while trying to staple the response:" .. (err or 'no message'))
    end
    return ok, err
end

return _M

Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists