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