--- 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;