local ngx_ssl = require "ngx.ssl" local pl_utils = require "pl.utils" local mlcache = require "kong.resty.mlcache" local new_tab = require "table.new" local constants = require "kong.constants" local plugin_servers = require "kong.runloop.plugin_servers" local openssl_x509_store = require "resty.openssl.x509.store" local openssl_x509 = require "resty.openssl.x509" local ngx_log = ngx.log local ERR = ngx.ERR local DEBUG = ngx.DEBUG local re_sub = ngx.re.sub local find = string.find local server_name = ngx_ssl.server_name local clear_certs = ngx_ssl.clear_certs local parse_pem_cert = ngx_ssl.parse_pem_cert local parse_pem_priv_key = ngx_ssl.parse_pem_priv_key local set_cert = ngx_ssl.set_cert local set_priv_key = ngx_ssl.set_priv_key local tb_concat = table.concat local tb_sort = table.sort local tb_insert = table.insert local kong = kong local type = type local error = error local assert = assert local tostring = tostring local ipairs = ipairs local ngx_md5 = ngx.md5 local ngx_exit = ngx.exit local ngx_ERROR = ngx.ERROR local default_cert_and_key local DEFAULT_SNI = "*" local CA_KEY = { id = "", } local function log(lvl, ...) ngx_log(lvl, "[ssl] ", ...) end local function parse_cert(cert, parsed) if cert == nil then return nil, nil, parsed end if type(cert) == "cdata" then return cert, nil, parsed end local err cert, err = parse_pem_cert(cert) if not cert then return nil, "could not parse PEM certificate: " .. err end return cert, nil, true end local function parse_key(key, parsed) if key == nil then return nil, nil, parsed end if type(key) == "cdata" then return key, nil, parsed end local err key, err = parse_pem_priv_key(key) if not key then return nil, "could not parse PEM private key: " .. err end return key, nil, true end local function parse_key_and_cert(row) if row == false then return default_cert_and_key end -- parse cert and priv key for later usage by ngx.ssl local err, parsed local key, key_alt local cert, cert_alt cert, err, parsed = parse_cert(row.cert) if err then return nil, err end key, err, parsed = parse_key(row.key, parsed) if err then return nil, err end cert_alt, err, parsed = parse_cert(row.cert_alt, parsed) if err then return nil, err end if cert_alt then key_alt, err, parsed = parse_key(row.key_alt, parsed) if err then return nil, err end end if parsed then return { cert = cert, key = key, cert_alt = cert_alt, key_alt = key_alt, ["$refs"] = row["$refs"], } end return row end local function produce_wild_snis(sni) if type(sni) ~= "string" then error("sni must be a string", 2) end local wild_prefix_sni local wild_suffix_sni local wild_idx = find(sni, "*", nil, true) if wild_idx == 1 then wild_prefix_sni = sni elseif not wild_idx then -- *.example.com lookup local wild_sni, n, err = re_sub(sni, [[([^.]+)(\.[^.]+\.\S+)]], "*$2", "ajo") if err then log(ERR, "could not create SNI wildcard for SNI lookup: ", err) elseif n > 0 then wild_prefix_sni = wild_sni end end if wild_idx == #sni then wild_suffix_sni = sni elseif not wild_idx then -- example.* lookup local wild_sni, n, err = re_sub(sni, [[([^.]+\.)([^.]+)$]], "$1*", "jo") if err then log(ERR, "could not create SNI wildcard for SNI lookup: ", err) elseif n > 0 then wild_suffix_sni = wild_sni end end return wild_prefix_sni, wild_suffix_sni end local function fetch_sni(sni, i) local row, err = kong.db.snis:select_by_name(sni) if err then return nil, "failed to fetch '" .. sni .. "' SNI: " .. err, i end if not row then return false, nil, i end return row, nil, i end local function fetch_certificate(pk, sni_name, ws_id) local certificate, err = kong.db.certificates:select(pk, { workspace = ws_id, }) if err then if sni_name then return nil, "failed to fetch certificate for '" .. sni_name .. "' SNI: " .. err end return nil, "failed to fetch certificate " .. pk.id end if not certificate then if sni_name then return nil, "no SSL certificate configured for sni: " .. sni_name end return nil, "certificate " .. pk.id .. " not found" end return certificate end local get_certificate_opts = { l1_serializer = parse_key_and_cert, } local get_ca_store_opts = { l1_serializer = function(cas) local trust_store, err = openssl_x509_store.new() if err then return nil, err end for _, ca in ipairs(cas) do local x509, err = openssl_x509.new(ca.cert, "PEM") if err then return nil, err end local _, err = trust_store:add(x509) if err then return nil, err end end return trust_store end, } local function init() local conf = kong.configuration if conf.ssl_cert[1] then default_cert_and_key = parse_key_and_cert { cert = assert(pl_utils.readfile(conf.ssl_cert[1])), key = assert(pl_utils.readfile(conf.ssl_cert_key[1])), } end end local function get_certificate(pk, sni_name, ws_id) local cache_key = kong.db.certificates:cache_key(pk) local certificate, err, hit_level = kong.core_cache:get(cache_key, get_certificate_opts, fetch_certificate, pk, sni_name, ws_id) if certificate and hit_level ~= 3 and certificate["$refs"] then certificate, err = parse_key_and_cert(kong.vault.update(certificate)) end return certificate, err end local function find_certificate(sni) if not sni then log(DEBUG, "no SNI provided by client, serving default SSL certificate") return default_cert_and_key end local sni_wild_pref, sni_wild_suf = produce_wild_snis(sni) local bulk = mlcache.new_bulk(4) bulk:add("snis:" .. sni, nil, fetch_sni, sni) if sni_wild_pref then bulk:add("snis:" .. sni_wild_pref, nil, fetch_sni, sni_wild_pref) end if sni_wild_suf then bulk:add("snis:" .. sni_wild_suf, nil, fetch_sni, sni_wild_suf) end bulk:add("snis:" .. DEFAULT_SNI, nil, fetch_sni, DEFAULT_SNI) local res, err = kong.core_cache:get_bulk(bulk) if err then return nil, err end for _, new_sni, err in mlcache.each_bulk_res(res) do if new_sni then return get_certificate(new_sni.certificate, new_sni.name) end if err then -- we choose to not call typedefs.wildcard_host.custom_validator(sni) -- in the front to reduce the cost in normal flow. -- these error messages are from validate_wildcard_host() local patterns = { "must not be an IP", "must not have a port", "invalid value: ", "only one wildcard must be specified", "wildcard must be leftmost or rightmost character", } local idx for _, pat in ipairs(patterns) do idx = err:find(pat, nil, true) if idx then break end end if idx then kong.log.debug("invalid SNI '", sni, "', ", err:sub(idx), ", serving default SSL certificate") else log(ERR, "failed to fetch SNI: ", err) end end end return default_cert_and_key end local function execute() local sn, err = server_name() if err then log(ERR, "could not retrieve SNI: ", err) return ngx_exit(ngx_ERROR) end local cert_and_key, err = find_certificate(sn) if err then log(ERR, err) return ngx_exit(ngx_ERROR) end if cert_and_key == default_cert_and_key then -- use (already set) fallback certificate return end -- set the certificate for this connection local ok, err = clear_certs() if not ok then log(ERR, "could not clear existing (default) certificates: ", err) return ngx_exit(ngx_ERROR) end ok, err = set_cert(cert_and_key.cert) if not ok then log(ERR, "could not set configured certificate: ", err) return ngx_exit(ngx_ERROR) end ok, err = set_priv_key(cert_and_key.key) if not ok then log(ERR, "could not set configured private key: ", err) return ngx_exit(ngx_ERROR) end if cert_and_key.cert_alt and cert_and_key.key_alt then ok, err = set_cert(cert_and_key.cert_alt) if not ok then log(ERR, "could not set alternate configured certificate: ", err) return ngx_exit(ngx_ERROR) end ok, err = set_priv_key(cert_and_key.key_alt) if not ok then log(ERR, "could not set alternate configured private key: ", err) return ngx_exit(ngx_ERROR) end end end local function ca_ids_cache_key(ca_ids) tb_sort(ca_ids) return "ca_stores:" .. ngx_md5(tb_concat(ca_ids, ':')) end local function fetch_ca_certificates(ca_ids) local cas = new_tab(#ca_ids, 0) for i, ca_id in ipairs(ca_ids) do CA_KEY.id = ca_id local obj, err = kong.db.ca_certificates:select(CA_KEY) if not obj then if err then return nil, err end return nil, "CA Certificate '" .. tostring(ca_id) .. "' does not exist" end cas[i] = obj end return cas end local function get_ca_certificate_store(ca_ids) return kong.core_cache:get(ca_ids_cache_key(ca_ids), get_ca_store_opts, fetch_ca_certificates, ca_ids) end local function get_ca_certificate_store_for_plugin(ca_ids) return kong.cache:get(ca_ids_cache_key(ca_ids), get_ca_store_opts, fetch_ca_certificates, ca_ids) end -- here we assume the field name is always `ca_certificates` local get_ca_certificate_reference_entities do local function is_entity_referencing_ca_certificates(name) local entity_schema = require("kong.db.schema.entities." .. name) for _, field in ipairs(entity_schema.fields) do if field.ca_certificates then return true end end return false end -- ordinary entities that reference ca certificates -- For example: services local CA_CERT_REFERENCE_ENTITIES get_ca_certificate_reference_entities = function() if not CA_CERT_REFERENCE_ENTITIES then CA_CERT_REFERENCE_ENTITIES = {} for _, entity_name in ipairs(constants.CORE_ENTITIES) do local res = is_entity_referencing_ca_certificates(entity_name) if res then tb_insert(CA_CERT_REFERENCE_ENTITIES, entity_name) end end end return CA_CERT_REFERENCE_ENTITIES end end -- here we assume the field name is always `ca_certificates` local get_ca_certificate_reference_plugins do local load_module_if_exists = require "kong.tools.module".load_module_if_exists local function is_plugin_referencing_ca_certificates(name) local plugin_schema = "kong.plugins." .. name .. ".schema" local ok, schema = load_module_if_exists(plugin_schema) if not ok then ok, schema = plugin_servers.load_schema(name) end if not ok then return nil, "no configuration schema found for plugin: " .. name end for _, field in ipairs(schema.fields) do if field.config then for _, field in ipairs(field.config.fields) do if field.ca_certificates then return true end end end end return false end -- loaded plugins that reference ca certificates -- For example: mtls-auth local CA_CERT_REFERENCE_PLUGINS get_ca_certificate_reference_plugins = function() if not CA_CERT_REFERENCE_PLUGINS then CA_CERT_REFERENCE_PLUGINS = {} local loaded_plugins = kong.configuration.loaded_plugins for name, v in pairs(loaded_plugins) do local res, err = is_plugin_referencing_ca_certificates(name) if err then return nil, err end if res then CA_CERT_REFERENCE_PLUGINS[name] = true end end end return CA_CERT_REFERENCE_PLUGINS end end return { init = init, find_certificate = find_certificate, produce_wild_snis = produce_wild_snis, execute = execute, get_certificate = get_certificate, get_ca_certificate_store = get_ca_certificate_store, get_ca_certificate_store_for_plugin = get_ca_certificate_store_for_plugin, ca_ids_cache_key = ca_ids_cache_key, get_ca_certificate_reference_entities = get_ca_certificate_reference_entities, get_ca_certificate_reference_plugins = get_ca_certificate_reference_plugins, }