Software / code / prosody
File
util/prosodyctl.lua @ 13801:a5d5fefb8b68 13.0
mod_tls: Enable Prosody's certificate checking for incoming s2s connections (fixes #1916) (thanks Damian, Zash)
Various options in Prosody allow control over the behaviour of the certificate
verification process For example, some deployments choose to allow falling
back to traditional "dialback" authentication (XEP-0220), while others verify
via DANE, hard-coded fingerprints, or other custom plugins.
Implementing this flexibility requires us to override OpenSSL's default
certificate verification, to allow Prosody to verify the certificate itself,
apply custom policies and make decisions based on the outcome.
To enable our custom logic, we have to suppress OpenSSL's default behaviour of
aborting the connection with a TLS alert message. With LuaSec, this can be
achieved by using the verifyext "lsec_continue" flag.
We also need to use the lsec_ignore_purpose flag, because XMPP s2s uses server
certificates as "client" certificates (for mutual TLS verification in outgoing
s2s connections).
Commit 99d2100d2918 moved these settings out of the defaults and into mod_s2s,
because we only really need these changes for s2s, and they should be opt-in,
rather than automatically applied to all TLS services we offer.
That commit was incomplete, because it only added the flags for incoming
direct TLS connections. StartTLS connections are handled by mod_tls, which was
not applying the lsec_* flags. It previously worked because they were already
in the defaults.
This resulted in incoming s2s connections with "invalid" certificates being
aborted early by OpenSSL, even if settings such as `s2s_secure_auth = false`
or DANE were present in the config.
Outgoing s2s connections inherit verify "none" from the defaults, which means
OpenSSL will receive the cert but will not terminate the connection when it is
deemed invalid. This means we don't need lsec_continue there, and we also
don't need lsec_ignore_purpose (because the remote peer is a "server").
Wondering why we can't just use verify "none" for incoming s2s? It's because
in that mode, OpenSSL won't request a certificate from the peer for incoming
connections. Setting verify "peer" is how you ask OpenSSL to request a
certificate from the client, but also what triggers its built-in verification.
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Tue, 01 Apr 2025 17:26:56 +0100 |
| parent | 13647:2b3d49936518 |
line wrap: on
line source
-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local config = require "prosody.core.configmanager"; local encodings = require "prosody.util.encodings"; local stringprep = encodings.stringprep; local storagemanager = require "prosody.core.storagemanager"; local usermanager = require "prosody.core.usermanager"; local interpolation = require "prosody.util.interpolation"; local signal = require "prosody.util.signal"; local set = require "prosody.util.set"; local path = require"prosody.util.paths"; local lfs = require "lfs"; local type = type; local have_socket_unix, socket_unix = pcall(require, "socket.unix"); have_socket_unix = have_socket_unix and type(socket_unix) == "table"; -- was a function in older LuaSocket local nodeprep, nameprep = stringprep.nodeprep, stringprep.nameprep; local io, os = io, os; local print = print; local tonumber = tonumber; local _G = _G; local prosody = prosody; local error_messages = setmetatable({ ["invalid-username"] = "The given username is invalid in a Jabber ID"; ["invalid-hostname"] = "The given hostname is invalid"; ["no-password"] = "No password was supplied"; ["no-such-user"] = "The given user does not exist on the server"; ["no-such-host"] = "The given hostname does not exist in the config"; ["unable-to-save-data"] = "Unable to store, perhaps you don't have permission?"; ["no-pidfile"] = "There is no 'pidfile' option in the configuration file, see https://prosody.im/doc/prosodyctl#pidfile for help"; ["invalid-pidfile"] = "The 'pidfile' option in the configuration file is not a string, see https://prosody.im/doc/prosodyctl#pidfile for help"; ["pidfile-not-locked"] = "Stale pidfile found. Prosody is probably not running."; ["no-posix"] = "The mod_posix module is not enabled in the Prosody config file, see https://prosody.im/doc/prosodyctl for more info"; ["no-such-method"] = "This module has no commands"; ["not-running"] = "Prosody is not running"; }, { __index = function (_,k) return "Error: "..(tostring(k):gsub("%-", " "):gsub("^.", string.upper)); end }); -- UI helpers local show_message = require "prosody.util.human.io".printf; local function show_usage(usage, desc) print("Usage: ".._G.arg[0].." "..usage); if desc then print(" "..desc); end end local function show_module_configuration_help(mod_name) print("Done.") print("If you installed a prosody plugin, don't forget to add its name under the 'modules_enabled' section inside your configuration file.") print("Depending on the module, there might be further configuration steps required.") print("") print("More info about: ") print(" modules_enabled: https://prosody.im/doc/modules_enabled") print(" "..mod_name..": https://modules.prosody.im/"..mod_name..".html") end -- Server control local function adduser(params) local user, host, password = nodeprep(params.user, true), nameprep(params.host), params.password; if not user then return false, "invalid-username"; elseif not host then return false, "invalid-hostname"; end local host_session = prosody.hosts[host]; if not host_session then return false, "no-such-host"; end storagemanager.initialize_host(host); local provider = host_session.users; if not(provider) or provider.name == "null" then usermanager.initialize_host(host); end local ok, errmsg = usermanager.create_user(user, password, host); if not ok then return false, errmsg or "creating-user-failed"; end return true; end local function user_exists(params) local user, host = nodeprep(params.user), nameprep(params.host); storagemanager.initialize_host(host); local provider = prosody.hosts[host].users; if not(provider) or provider.name == "null" then usermanager.initialize_host(host); end return usermanager.user_exists(user, host); end local function passwd(params) if not user_exists(params) then return false, "no-such-user"; end return adduser(params); end local function deluser(params) if not user_exists(params) then return false, "no-such-user"; end local user, host = nodeprep(params.user), nameprep(params.host); return usermanager.delete_user(user, host); end local function getpid() local pidfile = config.get("*", "pidfile"); if not pidfile then return false, "no-pidfile"; end if type(pidfile) ~= "string" then return false, "invalid-pidfile"; end pidfile = config.resolve_relative_path(prosody.paths.data, pidfile); local modules_disabled = set.new(config.get("*", "modules_disabled")); if prosody.platform ~= "posix" or modules_disabled:contains("posix") then return false, "no-posix"; end local file, err = io.open(pidfile, "r+"); if not file then return false, "pidfile-read-failed", err; end -- Check for a lock on the file local locked, err = lfs.lock(file, "w"); -- luacheck: ignore 211/err if locked then -- Prosody keeps the pidfile locked while it is running. -- We successfully locked the file, which means Prosody is not -- running and the pidfile is stale (somehow it was not -- cleaned up). We'll abort here, to avoid sending signals to -- a non-Prosody PID. file:close(); return false, "pidfile-not-locked"; end local pid = tonumber(file:read("*a")); file:close(); if not pid then return false, "invalid-pid"; end return true, pid; end local function isrunning() local ok, pid, err = getpid(); -- luacheck: ignore 211/err if not ok then if pid == "pidfile-read-failed" or pid == "pidfile-not-locked" then -- Report as not running, since we can't open the pidfile -- (it probably doesn't exist) return true, false; end return ok, pid; end return true, signal.kill(pid, 0) == 0; end local function start(source_dir, lua) lua = lua and lua .. " " or ""; local ok, ret = isrunning(); if not ok then return ok, ret; end if ret then return false, "already-running"; end local notify_socket; if have_socket_unix then local notify_path = path.join(prosody.paths.data, "notify.sock"); os.remove(notify_path); lua = string.format("NOTIFY_SOCKET=%q %s", notify_path, lua); notify_socket = socket_unix.dgram(); local ok = notify_socket:setsockname(notify_path); if not ok then return false, "notify-failed"; end end if not source_dir then os.execute(lua .. "./prosody -D"); else os.execute(lua .. source_dir.."/../../bin/prosody -D"); end if notify_socket then for i = 1, 5 do notify_socket:settimeout(i); if notify_socket:receivefrom() == "READY=1" then return true; end end return false, "not-ready"; end return true; end local function stop() local ok, ret = isrunning(); if not ok then return ok, ret; end if not ret then return false, "not-running"; end local ok, pid = getpid() if not ok then return false, pid; end signal.kill(pid, signal.SIGTERM); return true; end local function reload() local ok, ret = isrunning(); if not ok then return ok, ret; end if not ret then return false, "not-running"; end local ok, pid = getpid() if not ok then return false, pid; end signal.kill(pid, signal.SIGHUP); return true; end local render_cli = interpolation.new("%b{}", function (s) return "'"..s:gsub("'","'\\''").."'" end) local function call_luarocks(operation, mod, server) local dir = prosody.paths.installer; local ok, _, code = os.execute(render_cli("luarocks --lua-version={luav} {op} --tree={dir} {server&--server={server}} {mod?}", { dir = dir; op = operation; mod = mod; server = server; luav = _VERSION:match("5%.%d"); })); return ok and code; end return { show_message = show_message; show_warning = show_message; show_usage = show_usage; show_module_configuration_help = show_module_configuration_help; adduser = adduser; user_exists = user_exists; passwd = passwd; deluser = deluser; getpid = getpid; isrunning = isrunning; start = start; stop = stop; reload = reload; call_luarocks = call_luarocks; error_messages = error_messages; };