diff options
Diffstat (limited to 'core/certmanager.lua')
-rw-r--r-- | core/certmanager.lua | 134 |
1 files changed, 119 insertions, 15 deletions
diff --git a/core/certmanager.lua b/core/certmanager.lua index d8d07636..753eb4d5 100644 --- a/core/certmanager.lua +++ b/core/certmanager.lua @@ -20,20 +20,26 @@ end local configmanager = require "core.configmanager"; local log = require "util.logger".init("certmanager"); local ssl_context = ssl.context or softreq"ssl.context"; -local ssl_x509 = ssl.x509 or softreq"ssl.x509"; local ssl_newcontext = ssl.newcontext; local new_config = require"util.sslconfig".new; local stat = require "lfs".attributes; +local x509 = require "util.x509"; +local lfs = require "lfs"; + local tonumber, tostring = tonumber, tostring; local pairs = pairs; local t_remove = table.remove; local type = type; local io_open = io.open; local select = select; +local now = os.time; +local next = next; +local pcall = pcall; local prosody = prosody; -local resolve_path = require"util.paths".resolve_relative_path; +local pathutil = require"util.paths"; +local resolve_path = pathutil.resolve_relative_path; local config_path = prosody.paths.config or "."; local function test_option(option) @@ -81,7 +87,7 @@ local function find_cert(user_certs, name) if crt_path == key_path then if key_path:sub(-4) == ".crt" then key_path = key_path:sub(1, -4) .. "key"; - elseif key_path:sub(-13) == "fullchain.pem" then + elseif key_path:sub(-14) == "/fullchain.pem" then key_path = key_path:sub(1, -14) .. "privkey.pem"; end end @@ -95,12 +101,107 @@ local function find_cert(user_certs, name) log("debug", "No certificate/key found for %s", name); end +local function find_matching_key(cert_path) + -- FIXME we shouldn't need to guess the key filename + if cert_path:sub(-4) == ".crt" then + return cert_path:sub(1, -4) .. "key"; + elseif cert_path:sub(-14) == "/fullchain.pem" then + return cert_path:sub(1, -14) .. "privkey.pem"; + end +end + +local function index_certs(dir, files_by_name, depth_limit) + files_by_name = files_by_name or {}; + depth_limit = depth_limit or 3; + if depth_limit <= 0 then return files_by_name; end + + local ok, iter, v, i = pcall(lfs.dir, dir); + if not ok then + log("error", "Error indexing certificate directory %s: %s", dir, iter); + -- Return an empty index, otherwise this just triggers a nil indexing + -- error, plus this function would get called again. + -- Reloading the config after correcting the problem calls this again so + -- that's what should be done. + return {}, iter; + end + for file in iter, v, i do + local full = pathutil.join(dir, file); + if lfs.attributes(full, "mode") == "directory" then + if file:sub(1,1) ~= "." then + index_certs(full, files_by_name, depth_limit-1); + end + -- TODO support more filename patterns? + elseif full:match("%.crt$") or full:match("/fullchain%.pem$") then + local f = io_open(full); + if f then + -- TODO look for chained certificates + local firstline = f:read(); + if firstline == "-----BEGIN CERTIFICATE-----" then + f:seek("set") + local cert = ssl.loadcertificate(f:read("*a")) + -- TODO if more than one cert is found for a name, the most recently + -- issued one should be used. + -- for now, just filter out expired certs + -- TODO also check if there's a corresponding key + if cert:validat(now()) then + local names = x509.get_identities(cert); + log("debug", "Found certificate %s with identities %q", full, names); + for name, services in pairs(names) do + -- TODO check services + if files_by_name[name] then + files_by_name[name][full] = services; + else + files_by_name[name] = { [full] = services; }; + end + end + end + end + f:close(); + end + end + end + log("debug", "Certificate index: %q", files_by_name); + -- | hostname | filename | service | + return files_by_name; +end + +local cert_index; + local function find_host_cert(host) if not host then return nil; end + if not cert_index then + cert_index = index_certs(resolve_path(config_path, global_certificates)); + end + local certs = cert_index[host]; + if certs then + local cert_filename, services = next(certs); + if services["*"] then + log("debug", "Using cert %q from index", cert_filename); + return { + certificate = cert_filename, + key = find_matching_key(cert_filename), + } + end + end + return find_cert(configmanager.get(host, "certificate"), host) or find_host_cert(host:match("%.(.+)$")); end local function find_service_cert(service, port) + if not cert_index then + cert_index = index_certs(resolve_path(config_path, global_certificates)); + end + for _, certs in pairs(cert_index) do + for cert_filename, services in pairs(certs) do + if services[service] or services["*"] then + log("debug", "Using cert %q from index", cert_filename); + return { + certificate = cert_filename, + key = find_matching_key(cert_filename), + } + end + end + end local cert_config = configmanager.get("*", service.."_certificate"); if type(cert_config) == "table" then cert_config = cert_config[port] or cert_config.default; @@ -113,7 +214,7 @@ local core_defaults = { capath = "/etc/ssl/certs"; depth = 9; protocol = "tlsv1+"; - verify = (ssl_x509 and { "peer", "client_once", }) or "none"; + verify = "none"; options = { cipher_server_preference = luasec_has.options.cipher_server_preference; no_ticket = luasec_has.options.no_ticket; @@ -122,7 +223,10 @@ local core_defaults = { single_ecdh_use = luasec_has.options.single_ecdh_use; no_renegotiation = luasec_has.options.no_renegotiation; }; - verifyext = { "lsec_continue", "lsec_ignore_purpose" }; + verifyext = { + "lsec_continue", -- Continue past certificate verification errors + "lsec_ignore_purpose", -- Validate client certificates as if they were server certificates + }; curve = luasec_has.algorithms.ec and not luasec_has.capabilities.curves_list and "secp384r1"; curveslist = { "X25519", @@ -156,20 +260,16 @@ local path_options = { -- These we pass through resolve_path() key = true, certificate = true, cafile = true, capath = true, dhparam = true } -if luasec_version < 5 and ssl_x509 then - -- COMPAT mw/luasec-hg - for i=1,#core_defaults.verifyext do -- Remove lsec_ prefix - core_defaults.verify[#core_defaults.verify+1] = core_defaults.verifyext[i]:sub(6); - end -end - local function create_context(host, mode, ...) local cfg = new_config(); cfg:apply(core_defaults); local service_name, port = host:match("^(%S+) port (%d+)$"); - if service_name then + -- port 0 is used with client-only things that normally don't need certificates, e.g. https + if service_name and port ~= "0" then + log("debug", "Automatically locating certs for service %s on port %s", service_name, port); cfg:apply(find_service_cert(service_name, tonumber(port))); else + log("debug", "Automatically locating certs for host %s", host); cfg:apply(find_host_cert(host)); end cfg:apply({ @@ -185,8 +285,10 @@ local function create_context(host, mode, ...) local user_ssl_config = cfg:final(); if mode == "server" then - if not user_ssl_config.certificate then return nil, "No certificate present in SSL/TLS configuration for "..host; end - if not user_ssl_config.key then return nil, "No key present in SSL/TLS configuration for "..host; end + if not user_ssl_config.certificate then + log("info", "No certificate present in SSL/TLS configuration for %s. SNI will be required.", host); + end + if user_ssl_config.certificate and not user_ssl_config.key then return nil, "No key present in SSL/TLS configuration for "..host; end end for option in pairs(path_options) do @@ -258,6 +360,7 @@ local function reload_ssl_config() if luasec_has.options.no_compression then core_defaults.options.no_compression = configmanager.get("*", "ssl_compression") ~= true; end + cert_index = index_certs(resolve_path(config_path, global_certificates)); end prosody.events.add_handler("config-reloaded", reload_ssl_config); @@ -266,4 +369,5 @@ return { create_context = create_context; reload_ssl_config = reload_ssl_config; find_cert = find_cert; + find_host_cert = find_host_cert; }; |