Sindbad~EG File Manager
--[[
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