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