Mercurial > prosody-modules
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 712:227d48f927ff | 713:88ef66a65b13 |
|---|---|
| 15 local id_on_xmppAddr = "1.3.6.1.5.5.7.8.5"; | 15 local id_on_xmppAddr = "1.3.6.1.5.5.7.8.5"; |
| 16 local id_ce_subjectAltName = "2.5.29.17"; | 16 local id_ce_subjectAltName = "2.5.29.17"; |
| 17 local digest_algo = "sha1"; | 17 local digest_algo = "sha1"; |
| 18 local base64 = require "util.encodings".base64; | 18 local base64 = require "util.encodings".base64; |
| 19 | 19 |
| 20 local function enable_cert(username, cert, info) | |
| 21 local certs = dm_load(username, module.host, dm_table) or {}; | |
| 22 | |
| 23 info.pem = cert:pem(); | |
| 24 local digest = cert:digest(digest_algo); | |
| 25 info.digest = digest; | |
| 26 certs[info.id] = info; | |
| 27 | |
| 28 dm_store(username, module.host, dm_table, certs); | |
| 29 return true | |
| 30 end | |
| 31 | |
| 32 local function disable_cert(username, name) | |
| 33 local certs = dm_load(username, module.host, dm_table) or {}; | |
| 34 | |
| 35 local info = certs[name]; | |
| 36 local cert; | |
| 37 if info then | |
| 38 certs[name] = nil; | |
| 39 cert = x509.cert_from_pem(info.pem); | |
| 40 else | |
| 41 return nil, "item-not-found" | |
| 42 end | |
| 43 | |
| 44 dm_store(username, module.host, dm_table, certs); | |
| 45 return cert; -- So we can compare it with stuff | |
| 46 end | |
| 47 | |
| 48 local function get_id_on_xmpp_addrs(cert) | 20 local function get_id_on_xmpp_addrs(cert) |
| 49 local id_on_xmppAddrs = {}; | 21 local id_on_xmppAddrs = {}; |
| 50 for k,ext in pairs(cert:extensions()) do | 22 for k,ext in pairs(cert:extensions()) do |
| 51 if k == id_ce_subjectAltName then | 23 if k == id_ce_subjectAltName then |
| 52 for e,extv in pairs(ext) do | 24 for e,extv in pairs(ext) do |
| 59 end | 31 end |
| 60 end | 32 end |
| 61 module:log("debug", "Found JIDs: (%d) %s", #id_on_xmppAddrs, table.concat(id_on_xmppAddrs, ", ")); | 33 module:log("debug", "Found JIDs: (%d) %s", #id_on_xmppAddrs, table.concat(id_on_xmppAddrs, ", ")); |
| 62 return id_on_xmppAddrs; | 34 return id_on_xmppAddrs; |
| 63 end | 35 end |
| 64 | 36 |
| 37 local function enable_cert(username, cert, info) | |
| 38 -- Check the certificate. Is it not expired? Does it include id-on-xmppAddr? | |
| 39 | |
| 40 --[[ the method expired doesn't exist in luasec .. yet? | |
| 41 if cert:expired() then | |
| 42 module:log("debug", "This certificate is already expired."); | |
| 43 return nil, "This certificate is expired."; | |
| 44 end | |
| 45 --]] | |
| 46 | |
| 47 if not cert:valid_at(os.time()) then | |
| 48 module:log("debug", "This certificate is not valid at this moment."); | |
| 49 end | |
| 50 | |
| 51 local valid_id_on_xmppAddrs; | |
| 52 local require_id_on_xmppAddr = true; | |
| 53 if require_id_on_xmppAddr then | |
| 54 valid_id_on_xmppAddrs = get_id_on_xmpp_addrs(cert); | |
| 55 | |
| 56 local found = false; | |
| 57 for i,k in pairs(valid_id_on_xmppAddrs) do | |
| 58 if jid_bare(k) == (username .. "@" .. module.host) then | |
| 59 found = true; | |
| 60 break; | |
| 61 end | |
| 62 end | |
| 63 | |
| 64 if not found then | |
| 65 return nil, "This certificate is has no valid id-on-xmppAddr field."; | |
| 66 end | |
| 67 end | |
| 68 | |
| 69 local certs = dm_load(username, module.host, dm_table) or {}; | |
| 70 | |
| 71 info.pem = cert:pem(); | |
| 72 local digest = cert:digest(digest_algo); | |
| 73 info.digest = digest; | |
| 74 certs[info.id] = info; | |
| 75 | |
| 76 dm_store(username, module.host, dm_table, certs); | |
| 77 return true | |
| 78 end | |
| 79 | |
| 80 local function disable_cert(username, name, disconnect) | |
| 81 local certs = dm_load(username, module.host, dm_table) or {}; | |
| 82 | |
| 83 local info = certs[name]; | |
| 84 | |
| 85 if not info then | |
| 86 return nil, "item-not-found" | |
| 87 end | |
| 88 | |
| 89 certs[name] = nil; | |
| 90 | |
| 91 if disconnect then | |
| 92 module:log("debug", "%s revoked a certificate! Disconnecting all clients that used it", username); | |
| 93 local sessions = hosts[module.host].sessions[username].sessions; | |
| 94 local disabled_cert_pem = info.pem; | |
| 95 | |
| 96 for _, session in pairs(sessions) do | |
| 97 if session and session.conn then | |
| 98 local cert = session.conn:socket():getpeercertificate(); | |
| 99 | |
| 100 if cert and cert:pem() == disabled_cert_pem then | |
| 101 module:log("debug", "Found a session that should be closed: %s", tostring(session)); | |
| 102 session:close{ condition = "not-authorized", text = "This client side certificate has been revoked."}; | |
| 103 end | |
| 104 end | |
| 105 end | |
| 106 end | |
| 107 | |
| 108 dm_store(username, module.host, dm_table, certs); | |
| 109 return info; | |
| 110 end | |
| 65 | 111 |
| 66 module:hook("iq/self/"..xmlns_saslcert..":items", function(event) | 112 module:hook("iq/self/"..xmlns_saslcert..":items", function(event) |
| 67 local origin, stanza = event.origin, event.stanza; | 113 local origin, stanza = event.origin, event.stanza; |
| 68 if stanza.attr.type == "get" then | 114 if stanza.attr.type == "get" then |
| 69 module:log("debug", "%s requested items", origin.full_jid); | 115 module:log("debug", "%s requested items", origin.full_jid); |
| 117 if not cert then | 163 if not cert then |
| 118 origin.send(st.error_reply(stanza, "modify", "not-acceptable", "Could not parse X.509 certificate")); | 164 origin.send(st.error_reply(stanza, "modify", "not-acceptable", "Could not parse X.509 certificate")); |
| 119 return true; | 165 return true; |
| 120 end | 166 end |
| 121 | 167 |
| 122 -- Check the certificate. Is it not expired? Does it include id-on-xmppAddr? | 168 local ok, err = enable_cert(origin.username, cert, { |
| 123 | |
| 124 --[[ the method expired doesn't exist in luasec .. yet? | |
| 125 if cert:expired() then | |
| 126 module:log("debug", "This certificate is already expired."); | |
| 127 origin.send(st.error_reply(stanza, "cancel", "bad-request", "This certificate is expired.")); | |
| 128 return true | |
| 129 end | |
| 130 --]] | |
| 131 | |
| 132 if not cert:valid_at(os.time()) then | |
| 133 module:log("debug", "This certificate is not valid at this moment."); | |
| 134 end | |
| 135 | |
| 136 local valid_id_on_xmppAddrs; | |
| 137 local require_id_on_xmppAddr = true; | |
| 138 if require_id_on_xmppAddr then | |
| 139 valid_id_on_xmppAddrs = get_id_on_xmpp_addrs(cert); | |
| 140 | |
| 141 local found = false; | |
| 142 for i,k in pairs(valid_id_on_xmppAddrs) do | |
| 143 if jid_bare(k) == jid_bare(origin.full_jid) then | |
| 144 found = true; | |
| 145 break; | |
| 146 end | |
| 147 end | |
| 148 | |
| 149 if not found then | |
| 150 origin.send(st.error_reply(stanza, "cancel", "bad-request", "This certificate is has no valid id-on-xmppAddr field.")); | |
| 151 return true -- REJECT?! | |
| 152 end | |
| 153 end | |
| 154 | |
| 155 enable_cert(origin.username, cert, { | |
| 156 id = id, | 169 id = id, |
| 157 name = name, | 170 name = name, |
| 158 x509cert = x509cert, | 171 x509cert = x509cert, |
| 159 no_cert_management = can_manage, | 172 no_cert_management = can_manage, |
| 160 }); | 173 }); |
| 161 | 174 |
| 175 if not ok then | |
| 176 origin.send(st.error_reply(stanza, "cancel", "bad-request", err)); | |
| 177 return true -- REJECT?! | |
| 178 end | |
| 179 | |
| 162 module:log("debug", "%s added certificate named %s", origin.full_jid, name); | 180 module:log("debug", "%s added certificate named %s", origin.full_jid, name); |
| 163 | 181 |
| 164 origin.send(st.reply(stanza)); | 182 origin.send(st.reply(stanza)); |
| 165 | 183 |
| 166 return true | 184 return true |
| 180 if not name then | 198 if not name then |
| 181 origin.send(st.error_reply(stanza, "cancel", "bad-request", "No key specified.")); | 199 origin.send(st.error_reply(stanza, "cancel", "bad-request", "No key specified.")); |
| 182 return true | 200 return true |
| 183 end | 201 end |
| 184 | 202 |
| 185 local disabled_cert = disable_cert(origin.username, name); | 203 disable_cert(origin.username, name, disable.name == "revoke"); |
| 186 | 204 |
| 187 if disabled_cert and disable.name == "revoke" then | |
| 188 module:log("debug", "%s revoked a certificate! Disconnecting all clients that used it", origin.full_jid); | |
| 189 local sessions = hosts[module.host].sessions[origin.username].sessions; | |
| 190 local disabled_cert_pem = disabled_cert:pem(); | |
| 191 | |
| 192 for _, session in pairs(sessions) do | |
| 193 if session and session.conn then | |
| 194 local cert = session.conn:socket():getpeercertificate(); | |
| 195 | |
| 196 if cert and cert:pem() == disabled_cert_pem then | |
| 197 module:log("debug", "Found a session that should be closed: %s", tostring(session)); | |
| 198 session:close{ condition = "not-authorized", text = "This client side certificate has been revoked."}; | |
| 199 end | |
| 200 end | |
| 201 end | |
| 202 end | |
| 203 origin.send(st.reply(stanza)); | 205 origin.send(st.reply(stanza)); |
| 204 | 206 |
| 205 return true | 207 return true |
| 206 end | 208 end |
| 207 end | 209 end |
| 208 | 210 |
| 209 module:hook("iq/self/"..xmlns_saslcert..":disable", handle_disable); | 211 module:hook("iq/self/"..xmlns_saslcert..":disable", handle_disable); |
| 210 module:hook("iq/self/"..xmlns_saslcert..":revoke", handle_disable); | 212 module:hook("iq/self/"..xmlns_saslcert..":revoke", handle_disable); |
| 213 | |
| 214 -- Ad-hoc command | |
| 215 local adhoc_new = module:require "adhoc".new; | |
| 216 local dataforms_new = require "util.dataforms".new; | |
| 217 | |
| 218 local function generate_error_message(errors) | |
| 219 local errmsg = {}; | |
| 220 for name, err in pairs(errors) do | |
| 221 errmsg[#errmsg + 1] = name .. ": " .. err; | |
| 222 end | |
| 223 return table.concat(errmsg, "\n"); | |
| 224 end | |
| 225 | |
| 226 local choose_subcmd_layout = dataforms_new { | |
| 227 title = "Certificate management"; | |
| 228 instructions = "What action do you want to perform?"; | |
| 229 | |
| 230 { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/certs#subcmd" }; | |
| 231 { name = "subcmd", type = "list-single", label = "Actions", required = true, | |
| 232 value = { {label = "Add certificate", value = "add"}, | |
| 233 {label = "List certificates", value = "list"}, | |
| 234 {label = "Disable certificate", value = "disable"}, | |
| 235 {label = "Revoke certificate", value = "revoke"}, | |
| 236 }; | |
| 237 }; | |
| 238 }; | |
| 239 | |
| 240 local add_layout = dataforms_new { | |
| 241 title = "Adding a certificate"; | |
| 242 instructions = "Enter the certificate in PEM format"; | |
| 243 | |
| 244 { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/certs#add" }; | |
| 245 { name = "name", type = "text-single", label = "Name", required = true }; | |
| 246 { name = "cert", type = "text-multi", label = "PEM certificate", required = true }; | |
| 247 { name = "manage", type = "boolean", label = "Can manage certificates", value = true }; | |
| 248 }; | |
| 249 | |
| 250 | |
| 251 local disable_layout_stub = dataforms_new { { name = "cert", type = "list-single", label = "Certificate", required = true } }; | |
| 252 | |
| 253 | |
| 254 local function adhoc_handler(self, data, state) | |
| 255 if data.action == "cancel" then return { status = "canceled" }; end | |
| 256 | |
| 257 if not state or data.action == "prev" then | |
| 258 return { status = "executing", form = choose_subcmd_layout, actions = { "next" } }, {}; | |
| 259 end | |
| 260 | |
| 261 if not state.subcmd then | |
| 262 local fields, errors = choose_subcmd_layout:data(data.form); | |
| 263 if errors then | |
| 264 return { status = "completed", error = { message = generate_error_message(errors) } }; | |
| 265 end | |
| 266 local subcmd = fields.subcmd | |
| 267 | |
| 268 if subcmd == "add" then | |
| 269 return { status = "executing", form = add_layout, actions = { "prev", "next", "complete" } }, { subcmd = "add" }; | |
| 270 elseif subcmd == "list" then | |
| 271 local list_layout = dataforms_new { | |
| 272 title = "List of certificates"; | |
| 273 }; | |
| 274 | |
| 275 local certs = dm_load(jid_split(data.from), module.host, dm_table) or {}; | |
| 276 | |
| 277 for digest, info in pairs(certs) do | |
| 278 list_layout[#list_layout + 1] = { name = info.id, type = "text-multi", label = info.name, value = info.x509cert }; | |
| 279 end | |
| 280 | |
| 281 return { status = "completed", result = list_layout }; | |
| 282 else | |
| 283 local layout = dataforms_new { | |
| 284 { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/certs#" .. subcmd }; | |
| 285 { name = "cert", type = "list-single", label = "Certificate", required = true }; | |
| 286 }; | |
| 287 | |
| 288 if subcmd == "disable" then | |
| 289 layout.title = "Disabling a certificate"; | |
| 290 layout.instructions = "Select the certificate to disable"; | |
| 291 elseif subcmd == "revoke" then | |
| 292 layout.title = "Revoking a certificate"; | |
| 293 layout.instructions = "Select the certificate to revoke"; | |
| 294 end | |
| 295 | |
| 296 local certs = dm_load(jid_split(data.from), module.host, dm_table) or {}; | |
| 297 | |
| 298 local values = {}; | |
| 299 for digest, info in pairs(certs) do | |
| 300 values[#values + 1] = { label = info.name, value = info.id }; | |
| 301 end | |
| 302 | |
| 303 return { status = "executing", form = { layout = layout, values = { cert = values } }, actions = { "prev", "next", "complete" } }, | |
| 304 { subcmd = subcmd }; | |
| 305 end | |
| 306 end | |
| 307 | |
| 308 if state.subcmd == "add" then | |
| 309 local fields, errors = add_layout:data(data.form); | |
| 310 if errors then | |
| 311 return { status = "completed", error = { message = generate_error_message(errors) } }; | |
| 312 end | |
| 313 | |
| 314 local name = fields.name; | |
| 315 local x509cert = fields.cert:gsub("^%s*(.-)%s*$", "%1"); | |
| 316 | |
| 317 local cert = x509.cert_from_pem( | |
| 318 "-----BEGIN CERTIFICATE-----\n" | |
| 319 .. x509cert .. | |
| 320 "\n-----END CERTIFICATE-----\n"); | |
| 321 | |
| 322 if not cert then | |
| 323 return { status = "completed", error = { message = "Could not parse X.509 certificate" } }; | |
| 324 end | |
| 325 | |
| 326 local ok, err = enable_cert(jid_split(data.from), cert, { | |
| 327 id = cert:digest(digest_algo), | |
| 328 name = name, | |
| 329 x509cert = x509cert, | |
| 330 no_cert_management = not fields.manage | |
| 331 }); | |
| 332 | |
| 333 if not ok then | |
| 334 return { status = "completed", error = { message = err } }; | |
| 335 end | |
| 336 | |
| 337 module:log("debug", "%s added certificate named %s", data.from, name); | |
| 338 | |
| 339 return { status = "completed", info = "Successfully added certificate " .. name .. "." }; | |
| 340 else | |
| 341 local fields, errors = disable_layout_stub:data(data.form); | |
| 342 if errors then | |
| 343 return { status = "completed", error = { message = generate_error_message(errors) } }; | |
| 344 end | |
| 345 | |
| 346 local info = disable_cert(jid_split(data.from), fields.cert, state.subcmd == "revoke" ); | |
| 347 | |
| 348 if state.subcmd == "revoke" then | |
| 349 return { status = "completed", info = "Revoked certificate " .. info.name .. "." }; | |
| 350 else | |
| 351 return { status = "completed", info = "Disabled certificate " .. info.name .. "." }; | |
| 352 end | |
| 353 end | |
| 354 end | |
| 355 | |
| 356 local cmd_desc = adhoc_new("Manage certificates", "http://prosody.im/protocol/certs", adhoc_handler, "user"); | |
| 357 module:provides("adhoc", cmd_desc); | |
| 211 | 358 |
| 212 -- Here comes the SASL EXTERNAL stuff | 359 -- Here comes the SASL EXTERNAL stuff |
| 213 | 360 |
| 214 local now = os.time; | 361 local now = os.time; |
| 215 module:hook("stream-features", function(event) | 362 module:hook("stream-features", function(event) |
