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