# HG changeset patch # User Florian Zeitz # Date 1339522022 -7200 # Node ID 88ef66a65b133f8c19982ddf5a0d8e40d720e682 # Parent 227d48f927ffcdd0252dc5b675a594c45769f03b mod_client_certs: Add Ad-Hoc commands for certificate management diff -r 227d48f927ff -r 88ef66a65b13 mod_client_certs/mod_client_certs.lua --- 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;