File

util/debug.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 12975:d10957394a3c
line wrap: on
line source

-- Variables ending with these names will not
-- have their values printed ('password' includes
-- 'new_password', etc.)
--
-- luacheck: ignore 122/debug

local censored_names = {
	password = true;
	passwd = true;
	pass = true;
	pwd = true;
};
local optimal_line_length = 65;

local termcolours = require "prosody.util.termcolours";
local getstring = termcolours.getstring;
local styles;
do
	local _ = termcolours.getstyle;
	styles = {
		boundary_padding = _("bright");
		filename         = _("bright", "blue");
		level_num        = _("green");
		funcname         = _("yellow");
		location         = _("yellow");
	};
end

local function get_locals_table(thread, level)
	local locals = {};
	for local_num = 1, math.huge do
		local name, value;
		if thread then
			name, value = debug.getlocal(thread, level, local_num);
		else
			name, value = debug.getlocal(level+1, local_num);
		end
		if not name then break; end
		table.insert(locals, { name = name, value = value });
	end
	return locals;
end

local function get_upvalues_table(func)
	local upvalues = {};
	if func then
		for upvalue_num = 1, math.huge do
			local name, value = debug.getupvalue(func, upvalue_num);
			if not name then break; end
			if name == "" then name = ("[%d]"):format(upvalue_num); end
			table.insert(upvalues, { name = name, value = value });
		end
	end
	return upvalues;
end

local function string_from_var_table(var_table, max_line_len, indent_str)
	local var_string = {};
	local col_pos = 0;
	max_line_len = max_line_len or math.huge;
	indent_str = "\n"..(indent_str or "");
	for _, var in ipairs(var_table) do
		local name, value = var.name, var.value;
		if name:sub(1,1) ~= "(" then
			if type(value) == "string" then
				if censored_names[name:match("%a+$")] then
					value = "<hidden>";
				else
					value = ("%q"):format(value);
				end
			else
				value = tostring(value);
			end
			if #value > max_line_len then
				value = value:sub(1, max_line_len-3).."…";
			end
			local str = ("%s = %s"):format(name, tostring(value));
			col_pos = col_pos + #str;
			if col_pos > max_line_len then
				table.insert(var_string, indent_str);
				col_pos = 0;
			end
			table.insert(var_string, str);
		end
	end
	if #var_string == 0 then
		return nil;
	else
		return "{ "..table.concat(var_string, ", "):gsub(indent_str..", ", indent_str).." }";
	end
end

local function get_traceback_table(thread, start_level)
	local levels = {};
	for level = start_level, math.huge do
		local info;
		if thread then
			info = debug.getinfo(thread, level);
		else
			info = debug.getinfo(level+1);
		end
		if not info then break; end

		levels[(level-start_level)+1] = {
			level = level;
			info = info;
			locals = get_locals_table(thread, level+1);
			upvalues = get_upvalues_table(info.func);
		};
	end
	return levels;
end

local function build_source_boundary_marker(last_source_desc)
	local padding = string.rep("-", math.floor(((optimal_line_length - 6) - #last_source_desc)/2));
	return getstring(styles.boundary_padding, "v"..padding).." "..
		getstring(styles.filename, last_source_desc).." "..
		getstring(styles.boundary_padding, padding..(#last_source_desc%2==0 and "-v" or "v "));
end

local function _traceback(thread, message, level)

	-- Lua manual says: debug.traceback ([thread,] [message [, level]])
	-- I fathom this to mean one of:
	-- ()
	-- (thread)
	-- (message, level)
	-- (thread, message, level)

	if thread == nil then -- Defaults
		thread, message, level = coroutine.running(), message, level;
	elseif type(thread) == "string" then
		thread, message, level = coroutine.running(), thread, message;
	elseif type(thread) ~= "thread" then
		return nil; -- debug.traceback() does this
	end

	level = level or 0;

	message = message and (message.."\n") or "";

	-- +3 counts for this function, and the pcall() and wrapper above us, the +1... I don't know.
	local levels = get_traceback_table(thread, level+(thread == nil and 4 or 0));

	local last_source_desc;

	local lines = {};
	for nlevel, current_level in ipairs(levels) do
		local info = current_level.info;
		local line;
		local func_type = info.namewhat.." ";
		local source_desc = (info.short_src == "[C]" and "C code") or info.short_src or "Unknown";
		if func_type == " " then func_type = ""; end;
		if info.short_src == "[C]" then
			line = "[ C ] "..func_type.."C function "..getstring(styles.location, (info.name and ("%q"):format(info.name) or "(unknown name)"));
		elseif info.what == "main" then
			line = "[Lua] "..getstring(styles.location, info.short_src.." line "..info.currentline);
		else
			local name = info.name or " ";
			if name ~= " " then
				name = ("%q"):format(name);
			end
			if func_type == "global " or func_type == "local " then
				func_type = func_type.."function ";
			end
			line = "[Lua] "..getstring(styles.location, info.short_src.." line "..
				info.currentline).." in "..func_type..getstring(styles.funcname, name)..
				" (defined on line "..info.linedefined..")";
		end
		if source_desc ~= last_source_desc then -- Venturing into a new source, add marker for previous
			last_source_desc = source_desc;
			table.insert(lines, "\t "..build_source_boundary_marker(last_source_desc));
		end
		nlevel = nlevel-1;
		table.insert(lines, "\t"..(nlevel==0 and ">" or " ")..getstring(styles.level_num, "("..nlevel..") ")..line);
		local npadding = (" "):rep(#tostring(nlevel));
		if current_level.locals then
			local locals_str = string_from_var_table(current_level.locals, optimal_line_length, "\t            "..npadding);
			if locals_str then
				table.insert(lines, "\t    "..npadding.."Locals: "..locals_str);
			end
		end
		local upvalues_str = string_from_var_table(current_level.upvalues, optimal_line_length, "\t            "..npadding);
		if upvalues_str then
			table.insert(lines, "\t    "..npadding.."Upvals: "..upvalues_str);
		end
	end

--	table.insert(lines, "\t "..build_source_boundary_marker(last_source_desc));

	return message.."stack traceback:\n"..table.concat(lines, "\n");
end

local function traceback(...)
	local ok, ret = pcall(_traceback, ...);
	if not ok then
		return "Error in error handling: "..ret;
	end
	return ret;
end

local function use()
	debug.traceback = traceback;
end

return {
	get_locals_table = get_locals_table;
	get_upvalues_table = get_upvalues_table;
	string_from_var_table = string_from_var_table;
	get_traceback_table = get_traceback_table;
	traceback = traceback;
	use = use;
};