Diff

mod_client_certs/mod_client_certs.lua @ 695:f6be46f15b74

mod_client_certs: Checking in the latest version I have with Zash's changes.
author Thijs Alkemade <thijsalkemade@gmail.com>
date Tue, 05 Jun 2012 18:02:28 +0200
child 697:c3337f62a538
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_client_certs/mod_client_certs.lua	Tue Jun 05 18:02:28 2012 +0200
@@ -0,0 +1,252 @@
+-- XEP-0257: Client Certificates Management implementation for Prosody
+-- Copyright (C) 2012 Thijs Alkemade
+--
+-- This file is MIT/X11 licensed.
+
+local st = require "util.stanza";
+local jid_bare = require "util.jid".bare;
+local xmlns_saslcert = "urn:xmpp:saslcert:0";
+local xmlns_pubkey = "urn:xmpp:tmp:pubkey";
+local dm_load = require "util.datamanager".load;
+local dm_store = require "util.datamanager".store;
+local dm_table = "client_certs";
+local x509 = require "ssl.x509";
+local id_on_xmppAddr = "1.3.6.1.5.5.7.8.5";
+local digest_algo = "sha1";
+
+local function enable_cert(username, cert, info)
+	local certs = dm_load(username, module.host, dm_table) or {};
+	local all_certs = dm_load(nil, module.host, dm_table) or {};
+
+	info.pem = cert:pem();
+	local digest = cert:digest(digest_algo);
+	info.digest = digest;
+	certs[info.id] = info;
+	all_certs[digest] = username;
+	-- Or, have it be keyed by the entire PEM representation
+
+	dm_store(username, module.host, dm_table, certs);
+	dm_store(nil, module.host, dm_table, all_certs);
+	return true
+end
+
+local function disable_cert(username, name)
+	local certs = dm_load(username, module.host, dm_table) or {};
+	local all_certs = dm_load(nil, module.host, dm_table) or {};
+
+	local info = certs[name];
+	local cert;
+	if info then
+		certs[name] = nil;
+		cert = x509.cert_from_pem(info.pem);
+		all_certs[cert:digest(digest_algo)] = nil;
+	else
+		return nil, "item-not-found"
+	end
+
+	dm_store(username, module.host, dm_table, certs);
+	dm_store(nil, module.host, dm_table, all_certs);
+	return cert; -- So we can compare it with stuff
+end
+
+module:hook("iq/self/"..xmlns_saslcert..":items", function(event)
+	local origin, stanza = event.origin, event.stanza;
+	if stanza.attr.type == "get" then
+		module:log("debug", "%s requested items", origin.full_jid);
+
+		local reply = st.reply(stanza):tag("items", { xmlns = xmlns_saslcert });
+		local certs = dm_load(origin.username, module.host, dm_table) or {};
+
+		for digest,info in pairs(certs) do
+			reply:tag("item", { id = info.id })
+				:tag("name"):text(info.name):up()
+				:tag("keyinfo", { xmlns = xmlns_pubkey }):tag("name"):text(info["key_name"]):up()
+				:tag("x509cert"):text(info.x509cert)
+			:up();
+		end
+
+		origin.send(reply);
+		return true
+	end
+end);
+
+module:hook("iq/self/"..xmlns_saslcert..":append", function(event)
+	local origin, stanza = event.origin, event.stanza;
+	if stanza.attr.type == "set" then
+
+		local append = stanza:get_child("append", xmlns_saslcert);
+		local name = append:get_child_text("name", xmlns_saslcert);
+		local key_info = append:get_child("keyinfo", xmlns_pubkey);
+
+		if not key_info or not name then
+			origin.send(st.error_reply(stanza, "cancel", "bad-request", "Missing fields.")); -- cancel? not modify?
+			return true
+		end
+		
+		local id = key_info:get_child_text("name", xmlns_pubkey);
+		local x509cert = key_info:get_child_text("x509cert", xmlns_pubkey);
+
+		if not id or not x509cert then
+			origin.send(st.error_reply(stanza, "cancel", "bad-request", "No certificate found."));
+			return true
+		end
+
+		local can_manage = key_info:get_child("no-cert-management", xmlns_saslcert) ~= nil;
+		local x509cert = key_info:get_child_text("x509cert");
+
+		local cert = x509.cert_from_pem(
+		"-----BEGIN CERTIFICATE-----\n"
+		.. x509cert ..
+		"\n-----END CERTIFICATE-----\n");
+
+
+		if not cert then
+			origin.send(st.error_reply(stanza, "modify", "not-acceptable", "Could not parse X.509 certificate"));
+			return true;
+		end
+
+		-- Check the certificate. Is it not expired? Does it include id-on-xmppAddr?
+
+		--[[ the method expired doesn't exist in luasec .. yet?
+		if cert:expired() then
+			module:log("debug", "This certificate is already expired.");
+			origin.send(st.error_reply(stanza, "cancel", "bad-request", "This certificate is expired."));
+			return true
+		end
+		--]]
+
+		if not cert:valid_at(os.time()) then
+			module:log("debug", "This certificate is not valid at this moment.");
+		end
+
+		local valid_id_on_xmppAddrs;
+		local require_id_on_xmppAddr = false;
+		if require_id_on_xmppAddr then
+			--local info = {};
+			valid_id_on_xmppAddrs = {};
+			for _,v in ipairs(cert:subject()) do
+				--info[#info+1] = (v.name or v.oid) ..":" .. v.value;
+				if v.oid == id_on_xmppAddr then
+					if jid_bare(v.value) == jid_bare(origin.full_jid) then
+						module:log("debug", "The certificate contains a id-on-xmppAddr key, and it is valid.");
+						valid_id_on_xmppAddrs[#valid_id_on_xmppAddrs+1] = v.value;
+						-- Is there a point in having >1 ids? Reject?!
+					else
+						module:log("debug", "The certificate contains a id-on-xmppAddr key, but it is for %s.", v.value);
+						-- Reject?
+					end
+				end
+			end
+
+			if #valid_id_on_xmppAddrs == 0 then
+				origin.send(st.error_reply(stanza, "cancel", "bad-request", "This certificate is has no valid id-on-xmppAddr field."));
+				return true -- REJECT?!
+			end
+		end
+
+		enable_cert(origin.username, cert, {
+			id = id,
+			name = name,
+			x509cert = x509cert,
+			no_cert_management = can_manage,
+			jids = valid_id_on_xmppAddrs,
+		});
+
+		module:log("debug", "%s added certificate named %s", origin.full_jid, name);
+
+		origin.send(st.reply(stanza));
+
+		return true
+	end
+end);
+
+
+local function handle_disable(event)
+	local origin, stanza = event.origin, event.stanza;
+	if stanza.attr.type == "set" then
+		local disable = stanza.tags[1];
+		module:log("debug", "%s disabled a certificate", origin.full_jid);
+
+		if disable.name == "revoke" then
+			module:log("debug", "%s revoked a certificate! Should disconnect all clients that used it", origin.full_jid);
+			-- TODO hosts.sessions[user].sessions.each{close if uses this cert}
+		end
+		local item = disable:get_child("item");
+		local name = item and item.attr.id;
+
+		if not name then
+			origin.send(st.error_reply(stanza, "cancel", "bad-request", "No key specified."));
+			return true
+		end
+
+		disable_cert(origin.username, name);
+
+		origin.send(st.reply(stanza));
+
+		return true
+	end
+end
+
+module:hook("iq/self/"..xmlns_saslcert..":disable", handle_disable);
+module:hook("iq/self/"..xmlns_saslcert..":revoke", handle_disable);
+
+-- Here comes the SASL EXTERNAL stuff
+
+local now = os.time;
+module:hook("stream-features", function(event)
+	local session, features = event.origin, event.features;
+	if session.secure and session.type == "c2s_unauthed" then
+		local cert = session.conn:socket():getpeercertificate();
+		if not cert then
+			module:log("error", "No Client Certificate");
+			return
+		end
+		module:log("info", "Client Certificate: %s", cert:digest(digest_algo));
+		local all_certs = dm_load(nil, module.host, dm_table) or {};
+		local digest = cert:digest(digest_algo);
+		local username = all_certs[digest];
+		if not cert:valid_at(now()) then
+			module:log("debug", "Client has an expired certificate", cert:digest(digest_algo));
+			return
+		end
+		if username then
+			local certs = dm_load(username, module.host, dm_table) or {};
+			local pem = cert:pem();
+			for name,info in pairs(certs) do
+				if info.digest == digest and info.pem == pem then
+					session.external_auth_cert, session.external_auth_user = pem, username;
+					module:log("debug", "Stream features:\n%s", tostring(features));
+					local mechs = features:get_child("mechanisms", "urn:ietf:params:xml:ns:xmpp-sasl");
+					if mechs then
+						mechs:tag("mechanism"):text("EXTERNAL");
+					end
+				end
+			end
+		end
+	end
+end, -1);
+
+local sm_make_authenticated = require "core.sessionmanager".make_authenticated;
+
+module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event)
+	local session, stanza = event.origin, event.stanza;
+	if session.type == "c2s_unauthed" and event.stanza.attr.mechanism == "EXTERNAL" then
+		if session.secure then
+			local cert = session.conn:socket():getpeercertificate();
+			if cert:pem() == session.external_auth_cert then
+				sm_make_authenticated(session, session.external_auth_user);
+				module:fire_event("authentication-success", { session = session });
+				session.external_auth, session.external_auth_user = nil, nil;
+				session.send(st.stanza("success", { xmlns="urn:ietf:params:xml:ns:xmpp-sasl"}));
+				session:reset_stream();
+			else
+				module:fire_event("authentication-failure", { session = session, condition = "not-authorized" });
+				session.send(st.stanza("failure", { xmlns="urn:ietf:params:xml:ns:xmpp-sasl"}):tag"not-authorized");
+			end
+		else
+			session.send(st.stanza("failure", { xmlns="urn:ietf:params:xml:ns:xmpp-sasl"}):tag"encryption-required");
+		end
+		return true;
+	end
+end, 1);
+