view mod_s2s_auth_posh/mod_s2s_auth_posh.lua @ 5160:8474a3b80200

mod_firewall: Fix 'is_admin' internal dependency rule #1797 (thanks diane) Looks like the boolean logic was inverted here. Instead, for now, simply check if is_admin is there. It is deprecated in trunk and was briefly removed before being brought back with a 'deprecated' warning as part of the new roles and permissions work. Making this dependency conditioned on the existence of the underlying function should make it work until it actually goes away for real.
author Kim Alvefur <zash@zash.se>
date Fri, 27 Jan 2023 23:06:25 +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