-- -- Licensed to the Apache Software Foundation (ASF) under one -- or more contributor license agreements. See the NOTICE file -- distributed with this work for additional information -- regarding copyright ownership. The ASF licenses this file -- to you under the Apache License, Version 2.0 (the -- "License"); you may not use this file except in compliance -- with the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, -- software distributed under the License is distributed on an -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -- KIND, either express or implied. See the License for the -- specific language governing permissions and limitations -- under the License. -- local require = require local http = require("resty.http") local ngx = ngx local ngx_ocsp = require("ngx.ocsp") local ngx_ssl = require("ngx.ssl") local radixtree_sni = require("apisix.ssl.router.radixtree_sni") local core = require("apisix.core") local plugin_name = "ocsp-stapling" local ocsp_resp_cache = ngx.shared[plugin_name] local plugin_schema = { type = "object", properties = {}, } local _M = { name = plugin_name, schema = plugin_schema, version = 0.1, priority = -44, } function _M.check_schema(conf) return core.schema.check(plugin_schema, conf) end local function fetch_ocsp_resp(der_cert_chain) core.log.info("fetch ocsp response from remote") local ocsp_url, err = ngx_ocsp.get_ocsp_responder_from_der_chain(der_cert_chain) if not ocsp_url then -- if cert not support ocsp, the report error is nil if not err then err = "cert not contains authority_information_access extension" end return nil, "failed to get ocsp url: " .. err end local ocsp_req, err = ngx_ocsp.create_ocsp_request(der_cert_chain) if not ocsp_req then return nil, "failed to create ocsp request: " .. err end local httpc = http.new() local res, err = httpc:request_uri(ocsp_url, { method = "POST", headers = { ["Content-Type"] = "application/ocsp-request", }, body = ocsp_req }) if not res then return nil, "ocsp responder query failed: " .. err end local http_status = res.status if http_status ~= 200 then return nil, "ocsp responder returns bad http status code: " .. http_status end if res.body and #res.body > 0 then return res.body, nil end return nil, "ocsp responder returns empty body" end local function set_ocsp_resp(full_chain_pem_cert, skip_verify, cache_ttl) local der_cert_chain, err = ngx_ssl.cert_pem_to_der(full_chain_pem_cert) if not der_cert_chain then return false, "failed to convert certificate chain from PEM to DER: ", err end local ocsp_resp = ocsp_resp_cache:get(der_cert_chain) if ocsp_resp == nil then core.log.info("not ocsp resp cache found, fetch from ocsp responder") ocsp_resp, err = fetch_ocsp_resp(der_cert_chain) if ocsp_resp == nil then return false, err end core.log.info("fetch ocsp resp ok, cache it") ocsp_resp_cache:set(der_cert_chain, ocsp_resp, cache_ttl) end if not skip_verify then local ok, err = ngx_ocsp.validate_ocsp_response(ocsp_resp, der_cert_chain) if not ok then return false, "failed to validate ocsp response: " .. err end end -- set the OCSP stapling local ok, err = ngx_ocsp.set_ocsp_status_resp(ocsp_resp) if not ok then return false, "failed to set ocsp status response: " .. err end return true end local original_set_cert_and_key local function set_cert_and_key(sni, value) if value.gm then -- should not run with gm plugin core.log.warn("gm plugin enabled, no need to run ocsp-stapling plugin") return original_set_cert_and_key(sni, value) end if not value.ocsp_stapling then core.log.info("no 'ocsp_stapling' field found, no need to run ocsp-stapling plugin") return original_set_cert_and_key(sni, value) end if not value.ocsp_stapling.enabled then return original_set_cert_and_key(sni, value) end if not ngx.ctx.tls_ext_status_req then core.log.info("no status request required, no need to send ocsp response") return original_set_cert_and_key(sni, value) end local ok, err = radixtree_sni.set_pem_ssl_key(sni, value.cert, value.key) if not ok then return false, err end local fin_pem_cert = value.cert -- multiple certificates support. if value.certs then for i = 1, #value.certs do local cert = value.certs[i] local key = value.keys[i] ok, err = radixtree_sni.set_pem_ssl_key(sni, cert, key) if not ok then return false, err end fin_pem_cert = cert end end local ok, err = set_ocsp_resp(fin_pem_cert, value.ocsp_stapling.skip_verify, value.ocsp_stapling.cache_ttl) if not ok then core.log.error("no ocsp response send: ", err) end return true end function _M.init() if core.schema.ssl.properties.gm ~= nil then core.log.error("ocsp-stapling plugin should not run with gm plugin") end original_set_cert_and_key = radixtree_sni.set_cert_and_key radixtree_sni.set_cert_and_key = set_cert_and_key if core.schema.ssl.properties.ocsp_stapling ~= nil then core.log.error("Field 'ocsp_stapling' is occupied") end core.schema.ssl.properties.ocsp_stapling = { type = "object", properties = { enabled = { type = "boolean", default = false, }, skip_verify = { type = "boolean", default = false, }, cache_ttl = { type = "integer", minimum = 60, default = 3600, }, } } end function _M.destroy() radixtree_sni.set_cert_and_key = original_set_cert_and_key core.schema.ssl.properties.ocsp_stapling = nil ocsp_resp_cache:flush_all() end return _M