529 lines
12 KiB
Lua
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,
|
|
}
|