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 |
