Mercurial > prosody-modules
comparison mod_client_certs/mod_client_certs.lua @ 709:151743149f07
mod_client_certs: Follow the rules in XEP-0178 about the inclusion of the username when using EXTERNAL, instead of mapping one certificate to one user.
author | Thijs Alkemade <thijsalkemade@gmail.com> |
---|---|
date | Sat, 09 Jun 2012 23:15:44 +0200 |
parents | 3a3293f37139 |
children | 227d48f927ff |
comparison
equal
deleted
inserted
replaced
708:d9a4e2f11b07 | 709:151743149f07 |
---|---|
3 -- | 3 -- |
4 -- This file is MIT/X11 licensed. | 4 -- This file is MIT/X11 licensed. |
5 | 5 |
6 local st = require "util.stanza"; | 6 local st = require "util.stanza"; |
7 local jid_bare = require "util.jid".bare; | 7 local jid_bare = require "util.jid".bare; |
8 local jid_split = require "util.jid".split; | |
8 local xmlns_saslcert = "urn:xmpp:saslcert:0"; | 9 local xmlns_saslcert = "urn:xmpp:saslcert:0"; |
9 local xmlns_pubkey = "urn:xmpp:tmp:pubkey"; | 10 local xmlns_pubkey = "urn:xmpp:tmp:pubkey"; |
10 local dm_load = require "util.datamanager".load; | 11 local dm_load = require "util.datamanager".load; |
11 local dm_store = require "util.datamanager".store; | 12 local dm_store = require "util.datamanager".store; |
12 local dm_table = "client_certs"; | 13 local dm_table = "client_certs"; |
13 local x509 = require "ssl.x509"; | 14 local x509 = require "ssl.x509"; |
14 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"; |
15 local id_ce_subjectAltName = "2.5.29.17"; | 16 local id_ce_subjectAltName = "2.5.29.17"; |
16 local digest_algo = "sha1"; | 17 local digest_algo = "sha1"; |
18 local base64 = require "util.encodings".base64; | |
17 | 19 |
18 local function enable_cert(username, cert, info) | 20 local function enable_cert(username, cert, info) |
19 local certs = dm_load(username, module.host, dm_table) or {}; | 21 local certs = dm_load(username, module.host, dm_table) or {}; |
20 local all_certs = dm_load(nil, module.host, dm_table) or {}; | |
21 | 22 |
22 info.pem = cert:pem(); | 23 info.pem = cert:pem(); |
23 local digest = cert:digest(digest_algo); | 24 local digest = cert:digest(digest_algo); |
24 info.digest = digest; | 25 info.digest = digest; |
25 certs[info.id] = info; | 26 certs[info.id] = info; |
26 all_certs[digest] = username; | |
27 -- Or, have it be keyed by the entire PEM representation | |
28 | 27 |
29 dm_store(username, module.host, dm_table, certs); | 28 dm_store(username, module.host, dm_table, certs); |
30 dm_store(nil, module.host, dm_table, all_certs); | |
31 return true | 29 return true |
32 end | 30 end |
33 | 31 |
34 local function disable_cert(username, name) | 32 local function disable_cert(username, name) |
35 local certs = dm_load(username, module.host, dm_table) or {}; | 33 local certs = dm_load(username, module.host, dm_table) or {}; |
36 local all_certs = dm_load(nil, module.host, dm_table) or {}; | |
37 | 34 |
38 local info = certs[name]; | 35 local info = certs[name]; |
39 local cert; | 36 local cert; |
40 if info then | 37 if info then |
41 certs[name] = nil; | 38 certs[name] = nil; |
42 cert = x509.cert_from_pem(info.pem); | 39 cert = x509.cert_from_pem(info.pem); |
43 all_certs[cert:digest(digest_algo)] = nil; | |
44 else | 40 else |
45 return nil, "item-not-found" | 41 return nil, "item-not-found" |
46 end | 42 end |
47 | 43 |
48 dm_store(username, module.host, dm_table, certs); | 44 dm_store(username, module.host, dm_table, certs); |
49 dm_store(nil, module.host, dm_table, all_certs); | |
50 return cert; -- So we can compare it with stuff | 45 return cert; -- So we can compare it with stuff |
51 end | 46 end |
47 | |
48 local function get_id_on_xmpp_addrs(cert) | |
49 local id_on_xmppAddrs = {}; | |
50 for k,ext in pairs(cert:extensions()) do | |
51 if k == id_ce_subjectAltName then | |
52 for e,extv in pairs(ext) do | |
53 if e == id_on_xmppAddr then | |
54 for i,v in ipairs(extv) do | |
55 id_on_xmppAddrs[#id_on_xmppAddrs+1] = v; | |
56 end | |
57 end | |
58 end | |
59 end | |
60 end | |
61 module:log("debug", "Found JIDs: (%d) %s", #id_on_xmppAddrs, table.concat(id_on_xmppAddrs, ", ")); | |
62 return id_on_xmppAddrs; | |
63 end | |
64 | |
52 | 65 |
53 module:hook("iq/self/"..xmlns_saslcert..":items", function(event) | 66 module:hook("iq/self/"..xmlns_saslcert..":items", function(event) |
54 local origin, stanza = event.origin, event.stanza; | 67 local origin, stanza = event.origin, event.stanza; |
55 if stanza.attr.type == "get" then | 68 if stanza.attr.type == "get" then |
56 module:log("debug", "%s requested items", origin.full_jid); | 69 module:log("debug", "%s requested items", origin.full_jid); |
121 end | 134 end |
122 | 135 |
123 local valid_id_on_xmppAddrs; | 136 local valid_id_on_xmppAddrs; |
124 local require_id_on_xmppAddr = true; | 137 local require_id_on_xmppAddr = true; |
125 if require_id_on_xmppAddr then | 138 if require_id_on_xmppAddr then |
126 valid_id_on_xmppAddrs = {}; | 139 valid_id_on_xmppAddrs = get_id_on_xmpp_addrs(cert); |
127 for k,ext in pairs(cert:extensions()) do | 140 |
128 if k == id_ce_subjectAltName then | 141 local found = false; |
129 for e,extv in pairs(ext) do | 142 for i,k in pairs(valid_id_on_xmppAddrs) do |
130 if e == id_on_xmppAddr then | 143 if jid_bare(k) == jid_bare(origin.full_jid) then |
131 if jid_bare(extv[1]) == jid_bare(origin.full_jid) then | 144 found = true; |
132 module:log("debug", "The certificate contains a id-on-xmppAddr key, and it is valid."); | 145 break; |
133 valid_id_on_xmppAddrs[#valid_id_on_xmppAddrs+1] = extv[1]; | 146 end |
134 -- Is there a point in having >1 ids? Reject?! | 147 end |
135 else | 148 |
136 module:log("debug", "The certificate contains a id-on-xmppAddr key, but it is for %s.", v.value); | 149 if not found then |
137 -- Reject? | |
138 end | |
139 end | |
140 end | |
141 end | |
142 end | |
143 | |
144 if #valid_id_on_xmppAddrs == 0 then | |
145 origin.send(st.error_reply(stanza, "cancel", "bad-request", "This certificate is has no valid id-on-xmppAddr field.")); | 150 origin.send(st.error_reply(stanza, "cancel", "bad-request", "This certificate is has no valid id-on-xmppAddr field.")); |
146 return true -- REJECT?! | 151 return true -- REJECT?! |
147 end | 152 end |
148 end | 153 end |
149 | 154 |
150 enable_cert(origin.username, cert, { | 155 enable_cert(origin.username, cert, { |
151 id = id, | 156 id = id, |
152 name = name, | 157 name = name, |
153 x509cert = x509cert, | 158 x509cert = x509cert, |
154 no_cert_management = can_manage, | 159 no_cert_management = can_manage, |
155 jids = valid_id_on_xmppAddrs, | |
156 }); | 160 }); |
157 | 161 |
158 module:log("debug", "%s added certificate named %s", origin.full_jid, name); | 162 module:log("debug", "%s added certificate named %s", origin.full_jid, name); |
159 | 163 |
160 origin.send(st.reply(stanza)); | 164 origin.send(st.reply(stanza)); |
184 module:log("debug", "%s revoked a certificate! Disconnecting all clients that used it", origin.full_jid); | 188 module:log("debug", "%s revoked a certificate! Disconnecting all clients that used it", origin.full_jid); |
185 local sessions = hosts[module.host].sessions[origin.username].sessions; | 189 local sessions = hosts[module.host].sessions[origin.username].sessions; |
186 local disabled_cert_pem = disabled_cert:pem(); | 190 local disabled_cert_pem = disabled_cert:pem(); |
187 | 191 |
188 for _, session in pairs(sessions) do | 192 for _, session in pairs(sessions) do |
189 local cert = session.external_auth_cert; | 193 if session and session.conn then |
194 local cert = session.conn:socket():getpeercertificate(); | |
190 | 195 |
191 if cert and cert == disabled_cert_pem then | 196 if cert and cert:pem() == disabled_cert_pem then |
192 module:log("debug", "Found a session that should be closed: %s", tostring(session)); | 197 module:log("debug", "Found a session that should be closed: %s", tostring(session)); |
193 session:close{ condition = "not-authorized", text = "This client side certificate has been revoked."}; | 198 session:close{ condition = "not-authorized", text = "This client side certificate has been revoked."}; |
199 end | |
194 end | 200 end |
195 end | 201 end |
196 end | 202 end |
197 origin.send(st.reply(stanza)); | 203 origin.send(st.reply(stanza)); |
198 | 204 |
213 if not cert then | 219 if not cert then |
214 module:log("error", "No Client Certificate"); | 220 module:log("error", "No Client Certificate"); |
215 return | 221 return |
216 end | 222 end |
217 module:log("info", "Client Certificate: %s", cert:digest(digest_algo)); | 223 module:log("info", "Client Certificate: %s", cert:digest(digest_algo)); |
218 local all_certs = dm_load(nil, module.host, dm_table) or {}; | |
219 local digest = cert:digest(digest_algo); | |
220 local username = all_certs[digest]; | |
221 if not cert:valid_at(now()) then | 224 if not cert:valid_at(now()) then |
222 module:log("debug", "Client has an expired certificate", cert:digest(digest_algo)); | 225 module:log("debug", "Client has an expired certificate", cert:digest(digest_algo)); |
223 return | 226 return |
224 end | 227 end |
225 if username then | 228 module:log("debug", "Stream features:\n%s", tostring(features)); |
226 local certs = dm_load(username, module.host, dm_table) or {}; | 229 local mechs = features:get_child("mechanisms", "urn:ietf:params:xml:ns:xmpp-sasl"); |
230 if mechs then | |
231 mechs:tag("mechanism"):text("EXTERNAL"); | |
232 end | |
233 end | |
234 end, -1); | |
235 | |
236 local sm_make_authenticated = require "core.sessionmanager".make_authenticated; | |
237 | |
238 module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event) | |
239 local session, stanza = event.origin, event.stanza; | |
240 if session.type == "c2s_unauthed" and stanza.attr.mechanism == "EXTERNAL" then | |
241 if session.secure then | |
242 local cert = session.conn:socket():getpeercertificate(); | |
243 local username_data = stanza:get_text(); | |
244 local username = nil; | |
245 | |
246 if username_data == "=" then | |
247 -- Check for either an id_on_xmppAddr | |
248 local jids = get_id_on_xmpp_addrs(cert); | |
249 | |
250 if not (#jids == 1) then | |
251 module:log("debug", "Client tried to authenticate as =, but certificate has multiple JIDs."); | |
252 module:fire_event("authentication-failure", { session = session, condition = "not-authorized" }); | |
253 session.send(st.stanza("failure", { xmlns="urn:ietf:params:xml:ns:xmpp-sasl"}):tag"not-authorized"); | |
254 return true; | |
255 end | |
256 | |
257 username = jids[1]; | |
258 else | |
259 -- Check the base64 encoded username | |
260 username = base64.decode(username_data); | |
261 end | |
262 | |
263 local user, host, resource = jid_split(username); | |
264 | |
265 module:log("debug", "Inferred username: %s", user or "nil"); | |
266 | |
267 if (not username) or (not host == module.host) then | |
268 module:log("debug", "No valid username found for %s", tostring(session)); | |
269 module:fire_event("authentication-failure", { session = session, condition = "not-authorized" }); | |
270 session.send(st.stanza("failure", { xmlns="urn:ietf:params:xml:ns:xmpp-sasl"}):tag"not-authorized"); | |
271 return true; | |
272 end | |
273 | |
274 local certs = dm_load(user, module.host, dm_table) or {}; | |
275 local digest = cert:digest(digest_algo); | |
227 local pem = cert:pem(); | 276 local pem = cert:pem(); |
277 | |
228 for name,info in pairs(certs) do | 278 for name,info in pairs(certs) do |
229 if info.digest == digest and info.pem == pem then | 279 if info.digest == digest and info.pem == pem then |
230 session.external_auth_cert, session.external_auth_user = pem, username; | 280 sm_make_authenticated(session, user); |
231 module:log("debug", "Stream features:\n%s", tostring(features)); | 281 module:fire_event("authentication-success", { session = session }); |
232 local mechs = features:get_child("mechanisms", "urn:ietf:params:xml:ns:xmpp-sasl"); | 282 session.send(st.stanza("success", { xmlns="urn:ietf:params:xml:ns:xmpp-sasl"})); |
233 if mechs then | 283 session:reset_stream(); |
234 mechs:tag("mechanism"):text("EXTERNAL"); | 284 return true; |
235 end | 285 end |
236 end | 286 end |
237 end | 287 module:fire_event("authentication-failure", { session = session, condition = "not-authorized" }); |
238 end | 288 session.send(st.stanza("failure", { xmlns="urn:ietf:params:xml:ns:xmpp-sasl"}):tag"not-authorized"); |
239 end | |
240 end, -1); | |
241 | |
242 local sm_make_authenticated = require "core.sessionmanager".make_authenticated; | |
243 | |
244 module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event) | |
245 local session, stanza = event.origin, event.stanza; | |
246 if session.type == "c2s_unauthed" and event.stanza.attr.mechanism == "EXTERNAL" then | |
247 if session.secure then | |
248 local cert = session.conn:socket():getpeercertificate(); | |
249 if cert:pem() == session.external_auth_cert then | |
250 sm_make_authenticated(session, session.external_auth_user); | |
251 module:fire_event("authentication-success", { session = session }); | |
252 session.external_auth, session.external_auth_user = nil, nil; | |
253 session.send(st.stanza("success", { xmlns="urn:ietf:params:xml:ns:xmpp-sasl"})); | |
254 session:reset_stream(); | |
255 else | |
256 module:fire_event("authentication-failure", { session = session, condition = "not-authorized" }); | |
257 session.send(st.stanza("failure", { xmlns="urn:ietf:params:xml:ns:xmpp-sasl"}):tag"not-authorized"); | |
258 end | |
259 else | 289 else |
260 session.send(st.stanza("failure", { xmlns="urn:ietf:params:xml:ns:xmpp-sasl"}):tag"encryption-required"); | 290 session.send(st.stanza("failure", { xmlns="urn:ietf:params:xml:ns:xmpp-sasl"}):tag"encryption-required"); |
261 end | 291 end |
262 return true; | 292 return true; |
263 end | 293 end |