diff mod_client_certs/mod_client_certs.lua @ 713:88ef66a65b13

mod_client_certs: Add Ad-Hoc commands for certificate management
author Florian Zeitz <florob@babelmonkeys.de>
date Tue, 12 Jun 2012 19:27:02 +0200
parents 227d48f927ff
children 17ba2c59d661
line wrap: on
line diff
--- a/mod_client_certs/mod_client_certs.lua	Tue Jun 12 14:00:57 2012 +0200
+++ b/mod_client_certs/mod_client_certs.lua	Tue Jun 12 19:27:02 2012 +0200
@@ -17,34 +17,6 @@
 local digest_algo = "sha1";
 local base64 = require "util.encodings".base64;
 
-local function enable_cert(username, cert, info)
-	local certs = dm_load(username, module.host, dm_table) or {};
-
-	info.pem = cert:pem();
-	local digest = cert:digest(digest_algo);
-	info.digest = digest;
-	certs[info.id] = info;
-
-	dm_store(username, module.host, dm_table, certs);
-	return true
-end
-
-local function disable_cert(username, name)
-	local certs = dm_load(username, module.host, dm_table) or {};
-
-	local info = certs[name];
-	local cert;
-	if info then
-		certs[name] = nil;
-		cert = x509.cert_from_pem(info.pem);
-	else
-		return nil, "item-not-found"
-	end
-
-	dm_store(username, module.host, dm_table, certs);
-	return cert; -- So we can compare it with stuff
-end
-
 local function get_id_on_xmpp_addrs(cert)
 	local id_on_xmppAddrs = {};
 	for k,ext in pairs(cert:extensions()) do
@@ -61,7 +33,81 @@
 	module:log("debug", "Found JIDs: (%d) %s", #id_on_xmppAddrs, table.concat(id_on_xmppAddrs, ", "));
 	return id_on_xmppAddrs;
 end
-	
+
+local function enable_cert(username, cert, info)
+	-- 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.");
+	return nil, "This certificate is expired.";
+	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 = true;
+	if require_id_on_xmppAddr then
+		valid_id_on_xmppAddrs = get_id_on_xmpp_addrs(cert);
+
+		local found = false;
+		for i,k in pairs(valid_id_on_xmppAddrs) do
+			if jid_bare(k) == (username .. "@" .. module.host) then
+				found = true;
+				break;
+			end
+		end
+
+		if not found then
+			return nil, "This certificate is has no valid id-on-xmppAddr field.";
+		end
+	end
+
+	local certs = dm_load(username, module.host, dm_table) or {};
+
+	info.pem = cert:pem();
+	local digest = cert:digest(digest_algo);
+	info.digest = digest;
+	certs[info.id] = info;
+
+	dm_store(username, module.host, dm_table, certs);
+	return true
+end
+
+local function disable_cert(username, name, disconnect)
+	local certs = dm_load(username, module.host, dm_table) or {};
+
+	local info = certs[name];
+
+	if not info then
+		return nil, "item-not-found"
+	end
+
+	certs[name] = nil;
+
+	if disconnect then
+		module:log("debug", "%s revoked a certificate! Disconnecting all clients that used it", username);
+		local sessions = hosts[module.host].sessions[username].sessions;
+		local disabled_cert_pem = info.pem;
+
+		for _, session in pairs(sessions) do
+			if session and session.conn then
+				local cert = session.conn:socket():getpeercertificate();
+
+				if cert and cert:pem() == disabled_cert_pem then
+					module:log("debug", "Found a session that should be closed: %s", tostring(session));
+					session:close{ condition = "not-authorized", text = "This client side certificate has been revoked."};
+				end
+			end
+		end
+	end
+
+	dm_store(username, module.host, dm_table, certs);
+	return info;
+end
 
 module:hook("iq/self/"..xmlns_saslcert..":items", function(event)
 	local origin, stanza = event.origin, event.stanza;
@@ -119,46 +165,18 @@
 			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 = true;
-		if require_id_on_xmppAddr then
-			valid_id_on_xmppAddrs = get_id_on_xmpp_addrs(cert);
-
-			local found = false;
-			for i,k in pairs(valid_id_on_xmppAddrs) do
-				if jid_bare(k) == jid_bare(origin.full_jid) then
-					found = true;
-					break;
-				end
-			end
-
-			if not found 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, {
+		local ok, err = enable_cert(origin.username, cert, {
 			id = id,
 			name = name,
 			x509cert = x509cert,
 			no_cert_management = can_manage,
 		});
 
+		if not ok then
+			origin.send(st.error_reply(stanza, "cancel", "bad-request", err));
+			return true -- REJECT?!
+		end
+
 		module:log("debug", "%s added certificate named %s", origin.full_jid, name);
 
 		origin.send(st.reply(stanza));
@@ -182,24 +200,8 @@
 			return true
 		end
 
-		local disabled_cert = disable_cert(origin.username, name);
-
-		if disabled_cert and disable.name == "revoke" then
-			module:log("debug", "%s revoked a certificate! Disconnecting all clients that used it", origin.full_jid);
-			local sessions = hosts[module.host].sessions[origin.username].sessions;
-			local disabled_cert_pem = disabled_cert:pem();
+		disable_cert(origin.username, name, disable.name == "revoke");
 
-			for _, session in pairs(sessions) do
-				if session and session.conn then
-					local cert = session.conn:socket():getpeercertificate();
-				
-					if cert and cert:pem() == disabled_cert_pem then
-						module:log("debug", "Found a session that should be closed: %s", tostring(session));
-						session:close{ condition = "not-authorized", text = "This client side certificate has been revoked."};
-					end
-				end
-			end
-		end
 		origin.send(st.reply(stanza));
 
 		return true
@@ -209,6 +211,151 @@
 module:hook("iq/self/"..xmlns_saslcert..":disable", handle_disable);
 module:hook("iq/self/"..xmlns_saslcert..":revoke", handle_disable);
 
+-- Ad-hoc command
+local adhoc_new = module:require "adhoc".new;
+local dataforms_new = require "util.dataforms".new;
+
+local function generate_error_message(errors)
+	local errmsg = {};
+	for name, err in pairs(errors) do
+		errmsg[#errmsg + 1] = name .. ": " .. err;
+	end
+	return table.concat(errmsg, "\n");
+end
+
+local choose_subcmd_layout = dataforms_new {
+	title = "Certificate management";
+	instructions = "What action do you want to perform?";
+
+	{ name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/certs#subcmd" };
+	{ name = "subcmd", type = "list-single", label = "Actions", required = true,
+		value = { {label = "Add certificate", value = "add"},
+			  {label = "List certificates", value = "list"},
+			  {label = "Disable certificate", value = "disable"},
+			  {label = "Revoke certificate", value = "revoke"},
+		};
+	};
+};
+
+local add_layout = dataforms_new {
+	title = "Adding a certificate";
+	instructions = "Enter the certificate in PEM format";
+
+	{ name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/certs#add" };
+	{ name = "name", type = "text-single", label = "Name", required = true };
+	{ name = "cert", type = "text-multi", label = "PEM certificate", required = true };
+	{ name = "manage", type = "boolean", label = "Can manage certificates", value = true };
+};
+
+
+local disable_layout_stub = dataforms_new { { name = "cert", type = "list-single", label = "Certificate", required = true } };
+
+
+local function adhoc_handler(self, data, state)
+	if data.action == "cancel" then return { status = "canceled" }; end
+
+	if not state or data.action == "prev" then
+		return { status = "executing", form = choose_subcmd_layout, actions = { "next" } }, {};
+	end
+
+	if not state.subcmd then
+		local fields, errors = choose_subcmd_layout:data(data.form);
+		if errors then
+			return { status = "completed", error = { message = generate_error_message(errors) } };
+		end
+		local subcmd = fields.subcmd
+
+		if subcmd == "add" then
+			return { status = "executing", form = add_layout, actions = { "prev", "next", "complete" } }, { subcmd = "add" };
+		elseif subcmd == "list" then
+			local list_layout = dataforms_new {
+				title = "List of certificates";
+			};
+
+			local certs = dm_load(jid_split(data.from), module.host, dm_table) or {};
+
+			for digest, info in pairs(certs) do
+				list_layout[#list_layout + 1] = { name = info.id, type = "text-multi", label = info.name, value = info.x509cert };
+			end
+
+			return { status = "completed", result = list_layout };
+		else
+			local layout = dataforms_new {
+				{ name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/certs#" .. subcmd };
+				{ name = "cert", type = "list-single", label = "Certificate", required = true };
+			};
+
+			if subcmd == "disable" then
+				layout.title = "Disabling a certificate";
+				layout.instructions = "Select the certificate to disable";
+			elseif subcmd == "revoke" then
+				layout.title = "Revoking a certificate";
+				layout.instructions = "Select the certificate to revoke";
+			end
+
+			local certs = dm_load(jid_split(data.from), module.host, dm_table) or {};
+
+			local values = {};
+			for digest, info in pairs(certs) do
+				values[#values + 1] = { label = info.name, value = info.id };
+			end
+
+			return { status = "executing", form = { layout = layout, values = { cert = values } }, actions = { "prev", "next", "complete" } },
+				{ subcmd = subcmd };
+		end
+	end
+
+	if state.subcmd == "add" then
+		local fields, errors = add_layout:data(data.form);
+		if errors then
+			return { status = "completed", error = { message = generate_error_message(errors) } };
+		end
+
+		local name = fields.name;
+		local x509cert = fields.cert:gsub("^%s*(.-)%s*$", "%1");
+
+		local cert = x509.cert_from_pem(
+		"-----BEGIN CERTIFICATE-----\n"
+		.. x509cert ..
+		"\n-----END CERTIFICATE-----\n");
+
+		if not cert then
+			return { status = "completed", error = { message = "Could not parse X.509 certificate" } };
+		end
+
+		local ok, err = enable_cert(jid_split(data.from), cert, {
+			id = cert:digest(digest_algo),
+			name = name,
+			x509cert = x509cert,
+			no_cert_management = not fields.manage
+		});
+
+		if not ok then
+			return { status = "completed", error = { message = err } };
+		end
+
+		module:log("debug", "%s added certificate named %s", data.from, name);
+
+		return { status = "completed", info = "Successfully added certificate " .. name .. "." };
+	else
+		local fields, errors = disable_layout_stub:data(data.form);
+		if errors then
+			return { status = "completed", error = { message = generate_error_message(errors) } };
+		end
+
+		local info = disable_cert(jid_split(data.from), fields.cert, state.subcmd == "revoke" );
+
+		if state.subcmd == "revoke" then
+			return { status = "completed", info = "Revoked certificate " .. info.name .. "."  };
+		else
+			return { status = "completed", info = "Disabled certificate " .. info.name .. "."  };
+		end
+	end
+end
+
+local cmd_desc = adhoc_new("Manage certificates", "http://prosody.im/protocol/certs", adhoc_handler, "user");
+module:provides("adhoc", cmd_desc);
+
 -- Here comes the SASL EXTERNAL stuff
 
 local now = os.time;