aboutsummaryrefslogtreecommitdiffstats
path: root/core/certmanager.lua
diff options
context:
space:
mode:
Diffstat (limited to 'core/certmanager.lua')
-rw-r--r--core/certmanager.lua226
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;
};