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)