view mod_s2s_auth_posh/mod_s2s_auth_posh.lua @ 5173:460f78654864

mod_muc_rtbl: also filter messages This was a bit tricky because we don't want to run the JIDs through SHA256 on each message. Took a while to come up with this simple plan of just caching the SHA256 of the JIDs on the occupants. This will leave some dirt in the occupants after unloading the module, but that should be ok; once they cycle the room, the hashes will be gone. This is direly needed, otherwise, there is a tight race between the moderation activities and the actors joining the room.
author Jonas Schäfer <jonas@wielicki.name>
date Tue, 21 Feb 2023 21:37:27 +0100
parents 58a112bd9792
children
line wrap: on
line source

-- Copyright (C) 2013 - 2014 Tobias Markmann
-- This file is MIT/X11 licensed.
--
-- Implements authentication via POSH (PKIX over Secure HTTP)
-- http://tools.ietf.org/html/draft-miller-posh-03
--
module:set_global();
local json = require "util.json";

local base64 = require "util.encodings".base64;
local pem2der = require "util.x509".pem2der;
local hashes = require "util.hashes";
local build_url = require "socket.url".build;
local async = require "util.async";
local http = require "net.http";

local cache = require "util.cache".new(100);

local hash_order = { "sha-512", "sha-384", "sha-256", "sha-224", "sha-1" };
local hash_funcs = { hashes.sha512, hashes.sha384, hashes.sha256, hashes.sha224, hashes.sha1 };

local function posh_lookup(host_session, resume)
	-- do nothing if posh info already exists
	if host_session.posh ~= nil then return end

	local target_host = false;
	if host_session.direction == "incoming" then
		target_host = host_session.from_host;
	elseif host_session.direction == "outgoing" then
		target_host = host_session.to_host;
	end

	local cached = cache:get(target_host);
	if cached then
		if os.time() > cached.expires then
			cache:set(target_host, nil);
		else
			host_session.posh = { jwk = cached };
			return false;
		end
	end
	local log = host_session.log or module._log;

	log("debug", "Session direction: %s", tostring(host_session.direction));

	local url = build_url { scheme = "https", host = target_host, path = "/.well-known/posh/xmpp-server.json" };

	log("debug", "Request POSH information for %s", tostring(target_host));
	local redirect_followed = false;
	local function cb (response, code)
		if code ~= 200 then
			log("debug", "No or invalid POSH response received");
			resume();
			return;
		end
		log("debug", "Received POSH response");
		local jwk = json.decode(response);
		if not jwk or type(jwk) ~= "table" then
			log("error", "POSH response is not valid JSON!\n%s", tostring(response));
			resume();
			return;
		end
		if type(jwk.url) == "string" then
			if redirect_followed then
				redirect_followed = true;
				http.request(jwk.url, nil, cb);
			else
				log("error", "POSH had invalid redirect:\n%s", tostring(response));
				resume();
				return;
			end
		end

		host_session.posh = { orig = response };
		jwk.expires = os.time() + tonumber(jwk.expires) or 3600;
		host_session.posh.jwk = jwk;
		cache:set(target_host, jwk);
		resume();
	end
	http.request(url, nil, cb);
	return true;
end

-- Do POSH authentication
module:hook("s2s-check-certificate", function (event)
	local session, cert = event.session, event.cert;
	local log = session.log or module._log;
	if session.cert_identity_status == "valid" then
		log("debug", "Not trying POSH because certificate is already valid");
		return;
	end

	log("info", "Trying POSH authentication.");
	local wait, done = async.waiter();
	if posh_lookup(session, done) then
		wait();
	end
	local posh = session.posh;
	local jwk = posh and posh.jwk;
	local fingerprints = jwk and jwk.fingerprints;

	if type(fingerprints) ~= "table" then
		log("debug", "No POSH authentication data available");
		return;
	end

	local cert_der = pem2der(cert:pem());
	local cert_hashes = {};
	for i = 1, #hash_order do
		cert_hashes[i] = base64.encode(hash_funcs[i](cert_der));
	end
	for i = 1, #fingerprints do
		local fp = fingerprints[i];
		for j = 1, #hash_order do
			local hash = fp[hash_order[j]];
			if cert_hashes[j] == hash then
				session.cert_chain_status = "valid";
				session.cert_identity_status = "valid";
				log("debug", "POSH authentication succeeded!");
				return true;
			elseif hash then
				-- Don't try weaker hashes
				break;
			end
		end
	end

	log("debug", "POSH authentication failed!");
end);

function module.command(arg)
	if not arg[1] then
		print("Usage: mod_s2s_auth_posh /path/to/cert.pem")
		return 1;
	end
	local jwkset = { fingerprints = { }; expires = 86400; }

	for i, cert_file in ipairs(arg) do
		local cert, err = io.open(cert_file);
		if not cert then
			io.stderr:write(err, "\n");
			return 1;
		end
		local cert_pem = cert:read("*a");
		local cert_der, typ = pem2der(cert_pem);
		if typ == "CERTIFICATE" then
			jwkset.fingerprints[i] = { ["sha-256"] = base64.encode(hashes.sha256(cert_der)); };
		elseif typ then
			io.stderr:write(cert_file, " contained a ", typ:lower(), ", was expecting a certificate\n");
			return 1;
		else
			io.stderr:write(cert_file, " did not contain a certificate in PEM format\n");
			return 1;
		end
	end
	print(json.encode(jwkset));
	return 0;
end