diff options
-rw-r--r-- | core/certmanager.lua | 34 | ||||
-rw-r--r-- | core/portmanager.lua | 21 | ||||
-rw-r--r-- | net/server_epoll.lua | 35 | ||||
-rw-r--r-- | net/server_event.lua | 28 | ||||
-rw-r--r-- | net/server_select.lua | 18 | ||||
-rw-r--r-- | net/tls_luasec.lua | 90 | ||||
-rw-r--r-- | plugins/mod_admin_shell.lua | 7 | ||||
-rw-r--r-- | plugins/mod_c2s.lua | 6 | ||||
-rw-r--r-- | plugins/mod_s2s.lua | 9 | ||||
-rw-r--r-- | plugins/mod_s2s_auth_certs.lua | 6 | ||||
-rw-r--r-- | plugins/mod_saslauth.lua | 11 | ||||
-rw-r--r-- | util/sslconfig.lua | 54 |
12 files changed, 237 insertions, 82 deletions
diff --git a/core/certmanager.lua b/core/certmanager.lua index c193e824..6013c633 100644 --- a/core/certmanager.lua +++ b/core/certmanager.lua @@ -9,7 +9,6 @@ local ssl = require "ssl"; local configmanager = require "core.configmanager"; local log = require "util.logger".init("certmanager"); -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; @@ -313,10 +312,6 @@ else core_defaults.curveslist = nil; end -local path_options = { -- These we pass through resolve_path() - key = true, certificate = true, cafile = true, capath = true, dhparam = true -} - local function create_context(host, mode, ...) local cfg = new_config(); cfg:apply(core_defaults); @@ -352,34 +347,7 @@ local function create_context(host, mode, ...) 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 - if type(user_ssl_config[option]) == "string" then - user_ssl_config[option] = resolve_path(config_path, user_ssl_config[option]); - else - user_ssl_config[option] = nil; - end - end - - -- LuaSec expects dhparam to be a callback that takes two arguments. - -- We ignore those because it is mostly used for having a separate - -- set of params for EXPORT ciphers, which we don't have by default. - if type(user_ssl_config.dhparam) == "string" then - local f, err = io_open(user_ssl_config.dhparam); - if not f then return nil, "Could not open DH parameters: "..err end - local dhparam = f:read("*a"); - f:close(); - user_ssl_config.dhparam = function() return dhparam; end - end - - local ctx, err = ssl_newcontext(user_ssl_config); - - -- COMPAT Older LuaSec ignores the cipher list from the config, so we have to take care - -- of it ourselves (W/A for #x) - if ctx and user_ssl_config.ciphers then - local success; - success, err = ssl_context.setcipher(ctx, user_ssl_config.ciphers); - if not success then ctx = nil; end - end + local ctx, err = cfg:build(); if not ctx then err = err or "invalid ssl config" diff --git a/core/portmanager.lua b/core/portmanager.lua index 38c74b66..8c7dfddb 100644 --- a/core/portmanager.lua +++ b/core/portmanager.lua @@ -240,21 +240,22 @@ local function add_sni_host(host, service) log("debug", "Gathering certificates for SNI for host %s, %s service", host, service or "default"); for name, interface, port, n, active_service --luacheck: ignore 213 in active_services:iter(service, nil, nil, nil) do - if active_service.server.hosts and active_service.tls_cfg then - local config_prefix = (active_service.config_prefix or name).."_"; - if config_prefix == "_" then config_prefix = ""; end - local prefix_ssl_config = config.get(host, config_prefix.."ssl"); + if active_service.server and active_service.tls_cfg then local alternate_host = name and config.get(host, name.."_host"); if not alternate_host and name == "https" then -- TODO should this be some generic thing? e.g. in the service definition alternate_host = config.get(host, "http_host"); end local autocert = certmanager.find_host_cert(alternate_host or host); - -- luacheck: ignore 211/cfg - local ssl, err, cfg = certmanager.create_context(host, "server", prefix_ssl_config, autocert, active_service.tls_cfg); - if ssl then - active_service.server.hosts[alternate_host or host] = ssl; - else + local manualcert = active_service.tls_cfg; + local certificate = (autocert and autocert.certificate) or manualcert.certificate; + local key = (autocert and autocert.key) or manualcert.key; + local ok, err = active_service.server:sslctx():set_sni_host( + host, + certificate, + key + ); + if not ok then log("error", "Error creating TLS context for SNI host %s: %s", host, err); end end @@ -277,7 +278,7 @@ prosody.events.add_handler("host-deactivated", function (host) for name, interface, port, n, active_service --luacheck: ignore 213 in active_services:iter(nil, nil, nil, nil) do if active_service.tls_cfg then - active_service.server.hosts[host] = nil; + active_service.server:sslctx():remove_sni_host(host) end end end); diff --git a/net/server_epoll.lua b/net/server_epoll.lua index fa275d71..f8bab56c 100644 --- a/net/server_epoll.lua +++ b/net/server_epoll.lua @@ -18,7 +18,6 @@ local traceback = debug.traceback; local logger = require "util.logger"; local log = logger.init("server_epoll"); local socket = require "socket"; -local luasec = require "ssl"; local realtime = require "util.time".now; local monotonic = require "util.time".monotonic; local indexedbheap = require "util.indexedbheap"; @@ -614,6 +613,30 @@ function interface:set_sslctx(sslctx) self._sslctx = sslctx; end +function interface:sslctx() + return self.tls_ctx +end + +function interface:ssl_info() + local sock = self.conn; + return sock.info and sock:info(); +end + +function interface:ssl_peercertificate() + local sock = self.conn; + return sock.getpeercertificate and sock:getpeercertificate(); +end + +function interface:ssl_peerverification() + local sock = self.conn; + return sock.getpeerverification and sock:getpeerverification(); +end + +function interface:ssl_peerfinished() + local sock = self.conn; + return sock.getpeerfinished and sock:getpeerfinished(); +end + function interface:starttls(tls_ctx) if tls_ctx then self.tls_ctx = tls_ctx; end self.starttls = false; @@ -641,11 +664,7 @@ function interface:inittls(tls_ctx, now) self.starttls = false; self:debug("Starting TLS now"); self:updatenames(); -- Can't getpeer/sockname after wrap() - local ok, conn, err = pcall(luasec.wrap, self.conn, self.tls_ctx); - if not ok then - conn, err = ok, conn; - self:debug("Failed to initialize TLS: %s", err); - end + local conn, err = self.tls_ctx:wrap(self.conn); if not conn then self:on("disconnect", err); self:destroy(); @@ -656,8 +675,8 @@ function interface:inittls(tls_ctx, now) if conn.sni then if self.servername then conn:sni(self.servername); - elseif self._server and type(self._server.hosts) == "table" and next(self._server.hosts) ~= nil then - conn:sni(self._server.hosts, true); + elseif next(self.tls_ctx._sni_contexts) ~= nil then + conn:sni(self.tls_ctx._sni_contexts, true); end end if self.extra and self.extra.tlsa and conn.settlsa then diff --git a/net/server_event.lua b/net/server_event.lua index c30181b8..dfd94db4 100644 --- a/net/server_event.lua +++ b/net/server_event.lua @@ -47,7 +47,7 @@ local s_sub = string.sub local coroutine_wrap = coroutine.wrap local coroutine_yield = coroutine.yield -local has_luasec, ssl = pcall ( require , "ssl" ) +local has_luasec = pcall ( require , "ssl" ) local socket = require "socket" local levent = require "luaevent.core" local inet = require "util.net"; @@ -153,7 +153,7 @@ function interface_mt:_start_ssl(call_onconnect) -- old socket will be destroyed _ = self.eventwrite and self.eventwrite:close( ) self.eventread, self.eventwrite = nil, nil local err - self.conn, err = ssl.wrap( self.conn, self._sslctx ) + self.conn, err = self._sslctx:wrap(self.conn) if err then self.fatalerror = err self.conn = nil -- cannot be used anymore @@ -168,8 +168,8 @@ function interface_mt:_start_ssl(call_onconnect) -- old socket will be destroyed if self.conn.sni then if self.servername then self.conn:sni(self.servername); - elseif self._server and type(self._server.hosts) == "table" and next(self._server.hosts) ~= nil then - self.conn:sni(self._server.hosts, true); + elseif next(self._sslctx._sni_contexts) ~= nil then + self.conn:sni(self._sslctx._sni_contexts, true); end end @@ -274,6 +274,26 @@ function interface_mt:pause() return self:_lock(self.nointerface, true, self.nowriting); end +function interface_mt:sslctx() + return self._sslctx +end + +function interface_mt:ssl_info() + return self.conn.info and self.conn:info() +end + +function interface_mt:ssl_peercertificate() + return self.conn.getpeercertificate and self.conn:getpeercertificate() +end + +function interface_mt:ssl_peerverification() + return self.conn.getpeerverification and self.conn:getpeerverification() +end + +function interface_mt:ssl_peerfinished() + return self.conn.getpeerfinished and self.conn:getpeerfinished() +end + function interface_mt:resume() self:_lock(self.nointerface, false, self.nowriting); if self.readcallback and not self.eventread then diff --git a/net/server_select.lua b/net/server_select.lua index eea850ce..51439fca 100644 --- a/net/server_select.lua +++ b/net/server_select.lua @@ -359,6 +359,18 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport handler.sslctx = function ( ) return sslctx end + handler.ssl_info = function( ) + return socket.info and socket:info() + end + handler.ssl_peercertificate = function( ) + return socket.getpeercertificate and socket:getpeercertificate() + end + handler.ssl_peerverification = function( ) + return socket.getpeerverification and socket:getpeerverification() + end + handler.ssl_peerfinished = function( ) + return socket.getpeerfinished and socket:getpeerfinished() + end handler.send = function( _, data, i, j ) return send( socket, data, i, j ) end @@ -652,7 +664,7 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport end out_put( "server.lua: attempting to start tls on " .. tostring( socket ) ) local oldsocket, err = socket - socket, err = ssl_wrap( socket, sslctx ) -- wrap socket + socket, err = sslctx:wrap(socket) -- wrap socket if not socket then out_put( "server.lua: error while starting tls on client: ", tostring(err or "unknown error") ) @@ -662,8 +674,8 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport if socket.sni then if self.servername then socket:sni(self.servername); - elseif self._server and type(self._server.hosts) == "table" and next(self._server.hosts) ~= nil then - socket:sni(self.server().hosts, true); + elseif next(sslctx._sni_contexts) ~= nil then + socket:sni(sslctx._sni_contexts, true); end end diff --git a/net/tls_luasec.lua b/net/tls_luasec.lua new file mode 100644 index 00000000..680b455e --- /dev/null +++ b/net/tls_luasec.lua @@ -0,0 +1,90 @@ +-- Prosody IM +-- Copyright (C) 2021 Prosody folks +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +--[[ +This file provides a shim abstraction over LuaSec, consolidating some code +which was previously spread between net.server backends, portmanager and +certmanager. + +The goal is to provide a more or less well-defined API on top of LuaSec which +abstracts away some of the things which are not needed and simplifies usage of +commonly used things (such as SNI contexts). Eventually, network backends +which do not rely on LuaSocket+LuaSec should be able to provide *this* API +instead of having to mimic LuaSec. +]] +local softreq = require"util.dependencies".softreq; +local ssl = softreq"ssl"; +local ssl_newcontext = ssl.newcontext; +local ssl_context = ssl.context or softreq"ssl.context"; +local io_open = io.open; + +local context_api = {}; +local context_mt = {__index = context_api}; + +function context_api:set_sni_host(host, cert, key) + local ctx, err = self._builder:clone():apply({ + certificate = cert, + key = key, + }):build(); + if not ctx then + return false, err + end + + self._sni_contexts[host] = ctx._inner + + return true, nil +end + +function context_api:remove_sni_host(host) + self._sni_contexts[host] = nil +end + +function context_api:wrap(sock) + local ok, conn, err = pcall(ssl.wrap, sock, self._inner); + if not ok then + return nil, err + end + return conn, nil +end + +local function new_context(cfg, builder) + -- LuaSec expects dhparam to be a callback that takes two arguments. + -- We ignore those because it is mostly used for having a separate + -- set of params for EXPORT ciphers, which we don't have by default. + if type(cfg.dhparam) == "string" then + local f, err = io_open(cfg.dhparam); + if not f then return nil, "Could not open DH parameters: "..err end + local dhparam = f:read("*a"); + f:close(); + cfg.dhparam = function() return dhparam; end + end + + local inner, err = ssl_newcontext(cfg); + if not inner then + return nil, err + end + + -- COMPAT Older LuaSec ignores the cipher list from the config, so we have to take care + -- of it ourselves (W/A for #x) + if inner and cfg.ciphers then + local success; + success, err = ssl_context.setcipher(inner, cfg.ciphers); + if not success then + return nil, err + end + end + + return setmetatable({ + _inner = inner, + _builder = builder, + _sni_contexts = {}, + }, context_mt), nil +end + +return { + new_context = new_context, +}; diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index ae7e3c7c..c60cc75b 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -807,9 +807,7 @@ available_columns = { mapper = function(conn, session) if not session.secure then return "insecure"; end if not conn or not conn:ssl() then return "secure" end - local sock = conn and conn:socket(); - if not sock then return "secure"; end - local tls_info = sock.info and sock:info(); + local tls_info = conn.ssl_info and conn:ssl_info(); return tls_info and tls_info.protocol or "secure"; end; }; @@ -819,8 +817,7 @@ available_columns = { width = 30; key = "conn"; mapper = function(conn) - local sock = conn:socket(); - local info = sock and sock.info and sock:info(); + local info = conn and conn.ssl_info and conn:ssl_info(); if info then return info.cipher end end; }; diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua index c8f54fa7..8c0844ae 100644 --- a/plugins/mod_c2s.lua +++ b/plugins/mod_c2s.lua @@ -117,8 +117,7 @@ function stream_callbacks._streamopened(session, attr) session.secure = true; session.encrypted = true; - local sock = session.conn:socket(); - local info = sock.info and sock:info(); + local info = session.conn:ssl_info(); if type(info) == "table" then (session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher); session.compressed = info.compression; @@ -295,8 +294,7 @@ function listener.onconnect(conn) session.encrypted = true; -- Check if TLS compression is used - local sock = conn:socket(); - local info = sock.info and sock:info(); + local info = conn:ssl_info(); if type(info) == "table" then (session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher); session.compressed = info.compression; diff --git a/plugins/mod_s2s.lua b/plugins/mod_s2s.lua index 2f3815c4..3afb73eb 100644 --- a/plugins/mod_s2s.lua +++ b/plugins/mod_s2s.lua @@ -383,10 +383,10 @@ end --- Helper to check that a session peer's certificate is valid local function check_cert_status(session) local host = session.direction == "outgoing" and session.to_host or session.from_host - local conn = session.conn:socket() + local conn = session.conn local cert - if conn.getpeercertificate then - cert = conn:getpeercertificate() + if conn.ssl_peercertificate then + cert = conn:ssl_peercertificate() end return module:fire_event("s2s-check-certificate", { host = host, session = session, cert = cert }); @@ -398,8 +398,7 @@ local function session_secure(session) session.secure = true; session.encrypted = true; - local sock = session.conn:socket(); - local info = sock.info and sock:info(); + local info = session.conn:ssl_info(); if type(info) == "table" then (session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher); session.compressed = info.compression; diff --git a/plugins/mod_s2s_auth_certs.lua b/plugins/mod_s2s_auth_certs.lua index 992ee934..bde3cb82 100644 --- a/plugins/mod_s2s_auth_certs.lua +++ b/plugins/mod_s2s_auth_certs.lua @@ -9,7 +9,7 @@ local measure_cert_statuses = module:metric("counter", "checked", "", "Certifica module:hook("s2s-check-certificate", function(event) local session, host, cert = event.session, event.host, event.cert; - local conn = session.conn:socket(); + local conn = session.conn; local log = session.log or log; if not cert then @@ -18,8 +18,8 @@ module:hook("s2s-check-certificate", function(event) end local chain_valid, errors; - if conn.getpeerverification then - chain_valid, errors = conn:getpeerverification(); + if conn.ssl_peerverification then + chain_valid, errors = conn:ssl_peerverification(); else chain_valid, errors = false, { { "Chain verification not supported by this version of LuaSec" } }; end diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index ab863aa3..649f9ba6 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -242,7 +242,7 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:abort", function(event) end); local function tls_unique(self) - return self.userdata["tls-unique"]:getpeerfinished(); + return self.userdata["tls-unique"]:ssl_peerfinished(); end local mechanisms_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-sasl' }; @@ -262,18 +262,17 @@ module:hook("stream-features", function(event) -- check whether LuaSec has the nifty binding to the function needed for tls-unique -- FIXME: would be nice to have this check only once and not for every socket if sasl_handler.add_cb_handler then - local socket = origin.conn:socket(); - local info = socket.info and socket:info(); - if info.protocol == "TLSv1.3" then + local info = origin.conn:ssl_info(); + if info and info.protocol == "TLSv1.3" then log("debug", "Channel binding 'tls-unique' undefined in context of TLS 1.3"); - elseif socket.getpeerfinished and socket:getpeerfinished() then + elseif origin.conn.ssl_peerfinished and origin.conn:ssl_peerfinished() then log("debug", "Channel binding 'tls-unique' supported"); sasl_handler:add_cb_handler("tls-unique", tls_unique); else log("debug", "Channel binding 'tls-unique' not supported (by LuaSec?)"); end sasl_handler["userdata"] = { - ["tls-unique"] = socket; + ["tls-unique"] = origin.conn; }; else log("debug", "Channel binding not supported by SASL handler"); diff --git a/util/sslconfig.lua b/util/sslconfig.lua index 6074a1fb..23fb934c 100644 --- a/util/sslconfig.lua +++ b/util/sslconfig.lua @@ -3,9 +3,16 @@ local type = type; local pairs = pairs; local rawset = rawset; +local rawget = rawget; +local error = error; local t_concat = table.concat; local t_insert = table.insert; local setmetatable = setmetatable; +local config_path = prosody.paths.config or "."; +local resolve_path = require"util.paths".resolve_relative_path; + +-- TODO: use net.server directly here +local tls_impl = require"net.tls_luasec"; local _ENV = nil; -- luacheck: std none @@ -34,7 +41,7 @@ function handlers.options(config, field, new) options[value] = true; end end - config[field] = options; + rawset(config, field, options) end handlers.verifyext = handlers.options; @@ -70,6 +77,20 @@ finalisers.curveslist = finalisers.ciphers; -- TLS 1.3 ciphers finalisers.ciphersuites = finalisers.ciphers; +-- Path expansion +function finalisers.key(path) + if type(path) == "string" then + return resolve_path(config_path, path); + else + return nil + end +end +finalisers.certificate = finalisers.key; +finalisers.cafile = finalisers.key; +finalisers.capath = finalisers.key; +-- XXX: copied from core/certmanager.lua, but this seems odd, because it would remove a dhparam function from the config +finalisers.dhparam = finalisers.key; + -- protocol = "x" should enable only that protocol -- protocol = "x+" should enable x and later versions @@ -89,11 +110,14 @@ end -- Merge options from 'new' config into 'config' local function apply(config, new) + -- 0 == cache + rawset(config, 0, nil); if type(new) == "table" then for field, value in pairs(new) do (handlers[field] or rawset)(config, field, value); end end + return config end -- Finalize the config into the form LuaSec expects @@ -107,17 +131,45 @@ local function final(config) return output; end +local function build(config) + local cached = rawget(config, 0); + if cached then + return cached, nil + end + + local ctx, err = tls_impl.new_context(config:final(), config); + if ctx then + rawset(config, 0, ctx); + end + return ctx, err +end + local sslopts_mt = { __index = { apply = apply; final = final; + build = build; }; + __newindex = function() + error("SSL config objects cannot be modified directly. Use :apply()") + end; }; + local function new() return setmetatable({options={}}, sslopts_mt); end +local function clone(config) + local result = new(); + for k, v in pairs(config) do + rawset(result, k, v); + end + return result +end + +sslopts_mt.__index.clone = clone; + return { apply = apply; final = final; |