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