diff options
Diffstat (limited to 'core/certmanager.lua')
-rw-r--r-- | core/certmanager.lua | 226 |
1 files changed, 197 insertions, 29 deletions
diff --git a/core/certmanager.lua b/core/certmanager.lua index d8d07636..7a82c786 100644 --- a/core/certmanager.lua +++ b/core/certmanager.lua @@ -6,34 +6,30 @@ -- COPYING file in the source package for more information. -- -local softreq = require"util.dependencies".softreq; -local ssl = softreq"ssl"; -if not ssl then - return { - create_context = function () - return nil, "LuaSec (required for encryption) was not found"; - end; - reload_ssl_config = function () end; - } -end - +local ssl = require "ssl"; 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_context = ssl.context or require "ssl.context"; 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) @@ -42,7 +38,7 @@ end local luasec_major, luasec_minor = ssl._VERSION:match("^(%d+)%.(%d+)"); local luasec_version = tonumber(luasec_major) * 100 + tonumber(luasec_minor); -local luasec_has = ssl.config or softreq"ssl.config" or { +local luasec_has = ssl.config or { algorithms = { ec = luasec_version >= 5; }; @@ -81,7 +77,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 +91,108 @@ local function find_cert(user_certs, name) log("debug", "No certificate/key found for %s", name); end +local function find_matching_key(cert_path) + return (cert_path:gsub("%.crt$", ".key"):gsub("fullchain", "privkey")); +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 + elseif file:find("%.crt$") or file:find("fullchain") then -- This should catch most fullchain files + local f = io_open(full); + if f then + -- TODO look for chained certificates + local firstline = f:read(); + if firstline == "-----BEGIN CERTIFICATE-----" and lfs.attributes(find_matching_key(full), "mode") == "file" 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_cert_in_index(index, host) + if not host then return nil; end + if not index then return nil; end + local wildcard_host = host:gsub("^[^.]+%.", "*."); + local certs = index[host] or index[wildcard_host]; + if certs then + local cert_filename, services = next(certs); + if services["*"] then + log("debug", "Using cert %q from index for host %q", cert_filename, host); + return { + certificate = cert_filename, + key = find_matching_key(cert_filename), + } + end + end + return nil +end + local function find_host_cert(host) if not host then return nil; end - return find_cert(configmanager.get(host, "certificate"), host) or find_host_cert(host:match("%.(.+)$")); + if not cert_index then + cert_index = index_certs(resolve_path(config_path, global_certificates)); + end + + return find_cert_in_index(cert_index, host) or 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 for service %s port %d", cert_filename, service, port); + 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 +205,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 +214,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", @@ -140,8 +235,74 @@ local core_defaults = { "!3DES", -- 3DES - slow and of questionable security "!aNULL", -- Ciphers that does not authenticate the connection }; + dane = luasec_has.capabilities.dane and configmanager.get("*", "use_dane") and { "no_ee_namechecks" }; } +local mozilla_ssl_configs = { + -- https://wiki.mozilla.org/Security/Server_Side_TLS + -- Version 5.6 as of 2021-12-26 + modern = { + protocol = "tlsv1_3"; + options = { cipher_server_preference = false }; + ciphers = "DEFAULT"; -- TLS 1.3 uses 'ciphersuites' rather than these + curveslist = { "X25519"; "prime256v1"; "secp384r1" }; + ciphersuites = { "TLS_AES_128_GCM_SHA256"; "TLS_AES_256_GCM_SHA384"; "TLS_CHACHA20_POLY1305_SHA256" }; + }; + intermediate = { + protocol = "tlsv1_2+"; + dhparam = nil; -- ffdhe2048.txt + options = { cipher_server_preference = false }; + ciphers = { + "ECDHE-ECDSA-AES128-GCM-SHA256"; + "ECDHE-RSA-AES128-GCM-SHA256"; + "ECDHE-ECDSA-AES256-GCM-SHA384"; + "ECDHE-RSA-AES256-GCM-SHA384"; + "ECDHE-ECDSA-CHACHA20-POLY1305"; + "ECDHE-RSA-CHACHA20-POLY1305"; + "DHE-RSA-AES128-GCM-SHA256"; + "DHE-RSA-AES256-GCM-SHA384"; + }; + curveslist = { "X25519"; "prime256v1"; "secp384r1" }; + ciphersuites = { "TLS_AES_128_GCM_SHA256"; "TLS_AES_256_GCM_SHA384"; "TLS_CHACHA20_POLY1305_SHA256" }; + }; + old = { + protocol = "tlsv1+"; + dhparam = nil; -- openssl dhparam 1024 + options = { cipher_server_preference = true }; + ciphers = { + "ECDHE-ECDSA-AES128-GCM-SHA256"; + "ECDHE-RSA-AES128-GCM-SHA256"; + "ECDHE-ECDSA-AES256-GCM-SHA384"; + "ECDHE-RSA-AES256-GCM-SHA384"; + "ECDHE-ECDSA-CHACHA20-POLY1305"; + "ECDHE-RSA-CHACHA20-POLY1305"; + "DHE-RSA-AES128-GCM-SHA256"; + "DHE-RSA-AES256-GCM-SHA384"; + "DHE-RSA-CHACHA20-POLY1305"; + "ECDHE-ECDSA-AES128-SHA256"; + "ECDHE-RSA-AES128-SHA256"; + "ECDHE-ECDSA-AES128-SHA"; + "ECDHE-RSA-AES128-SHA"; + "ECDHE-ECDSA-AES256-SHA384"; + "ECDHE-RSA-AES256-SHA384"; + "ECDHE-ECDSA-AES256-SHA"; + "ECDHE-RSA-AES256-SHA"; + "DHE-RSA-AES128-SHA256"; + "DHE-RSA-AES256-SHA256"; + "AES128-GCM-SHA256"; + "AES256-GCM-SHA384"; + "AES128-SHA256"; + "AES256-SHA256"; + "AES128-SHA"; + "AES256-SHA"; + "DES-CBC3-SHA"; + }; + curveslist = { "X25519"; "prime256v1"; "secp384r1" }; + ciphersuites = { "TLS_AES_128_GCM_SHA256"; "TLS_AES_256_GCM_SHA384"; "TLS_CHACHA20_POLY1305_SHA256" }; + }; +}; + + if luasec_has.curves then for i = #core_defaults.curveslist, 1, -1 do if not luasec_has.curves[ core_defaults.curveslist[i] ] then @@ -156,20 +317,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({ @@ -177,6 +334,10 @@ local function create_context(host, mode, ...) -- We can't read the password interactively when daemonized password = function() log("error", "Encrypted certificate for %s requires 'ssl' 'password' to be set in config", host); end; }); + local profile = configmanager.get("*", "tls_profile") or "intermediate"; + if profile ~= "legacy" then + cfg:apply(mozilla_ssl_configs[profile]); + end cfg:apply(global_ssl_config); for i = select('#', ...), 1, -1 do @@ -185,8 +346,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 +421,8 @@ local function reload_ssl_config() if luasec_has.options.no_compression then core_defaults.options.no_compression = configmanager.get("*", "ssl_compression") ~= true; end + core_defaults.dane = configmanager.get("*", "use_dane") or false; + cert_index = index_certs(resolve_path(config_path, global_certificates)); end prosody.events.add_handler("config-reloaded", reload_ssl_config); @@ -266,4 +431,7 @@ return { create_context = create_context; reload_ssl_config = reload_ssl_config; find_cert = find_cert; + index_certs = index_certs; + find_host_cert = find_host_cert; + find_cert_in_index = find_cert_in_index; }; |