Files
2026-07-13 12:32:21 +08:00

529 lines
12 KiB
Lua

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,
}