Mercurial > prosody-modules
comparison mod_client_certs/mod_client_certs.lua @ 695:f6be46f15b74
mod_client_certs: Checking in the latest version I have with Zash's changes.
| author | Thijs Alkemade <thijsalkemade@gmail.com> |
|---|---|
| date | Tue, 05 Jun 2012 18:02:28 +0200 |
| parents | |
| children | c3337f62a538 |
comparison
equal
deleted
inserted
replaced
| 694:02fcb102b9aa | 695:f6be46f15b74 |
|---|---|
| 1 -- XEP-0257: Client Certificates Management implementation for Prosody | |
| 2 -- Copyright (C) 2012 Thijs Alkemade | |
| 3 -- | |
| 4 -- This file is MIT/X11 licensed. | |
| 5 | |
| 6 local st = require "util.stanza"; | |
| 7 local jid_bare = require "util.jid".bare; | |
| 8 local xmlns_saslcert = "urn:xmpp:saslcert:0"; | |
| 9 local xmlns_pubkey = "urn:xmpp:tmp:pubkey"; | |
| 10 local dm_load = require "util.datamanager".load; | |
| 11 local dm_store = require "util.datamanager".store; | |
| 12 local dm_table = "client_certs"; | |
| 13 local x509 = require "ssl.x509"; | |
| 14 local id_on_xmppAddr = "1.3.6.1.5.5.7.8.5"; | |
| 15 local digest_algo = "sha1"; | |
| 16 | |
| 17 local function enable_cert(username, cert, info) | |
| 18 local certs = dm_load(username, module.host, dm_table) or {}; | |
| 19 local all_certs = dm_load(nil, module.host, dm_table) or {}; | |
| 20 | |
| 21 info.pem = cert:pem(); | |
| 22 local digest = cert:digest(digest_algo); | |
| 23 info.digest = digest; | |
| 24 certs[info.id] = info; | |
| 25 all_certs[digest] = username; | |
| 26 -- Or, have it be keyed by the entire PEM representation | |
| 27 | |
| 28 dm_store(username, module.host, dm_table, certs); | |
| 29 dm_store(nil, module.host, dm_table, all_certs); | |
| 30 return true | |
| 31 end | |
| 32 | |
| 33 local function disable_cert(username, name) | |
| 34 local certs = dm_load(username, module.host, dm_table) or {}; | |
| 35 local all_certs = dm_load(nil, module.host, dm_table) or {}; | |
| 36 | |
| 37 local info = certs[name]; | |
| 38 local cert; | |
| 39 if info then | |
| 40 certs[name] = nil; | |
| 41 cert = x509.cert_from_pem(info.pem); | |
| 42 all_certs[cert:digest(digest_algo)] = nil; | |
| 43 else | |
| 44 return nil, "item-not-found" | |
| 45 end | |
| 46 | |
| 47 dm_store(username, module.host, dm_table, certs); | |
| 48 dm_store(nil, module.host, dm_table, all_certs); | |
| 49 return cert; -- So we can compare it with stuff | |
| 50 end | |
| 51 | |
| 52 module:hook("iq/self/"..xmlns_saslcert..":items", function(event) | |
| 53 local origin, stanza = event.origin, event.stanza; | |
| 54 if stanza.attr.type == "get" then | |
| 55 module:log("debug", "%s requested items", origin.full_jid); | |
| 56 | |
| 57 local reply = st.reply(stanza):tag("items", { xmlns = xmlns_saslcert }); | |
| 58 local certs = dm_load(origin.username, module.host, dm_table) or {}; | |
| 59 | |
| 60 for digest,info in pairs(certs) do | |
| 61 reply:tag("item", { id = info.id }) | |
| 62 :tag("name"):text(info.name):up() | |
| 63 :tag("keyinfo", { xmlns = xmlns_pubkey }):tag("name"):text(info["key_name"]):up() | |
| 64 :tag("x509cert"):text(info.x509cert) | |
| 65 :up(); | |
| 66 end | |
| 67 | |
| 68 origin.send(reply); | |
| 69 return true | |
| 70 end | |
| 71 end); | |
| 72 | |
| 73 module:hook("iq/self/"..xmlns_saslcert..":append", function(event) | |
| 74 local origin, stanza = event.origin, event.stanza; | |
| 75 if stanza.attr.type == "set" then | |
| 76 | |
| 77 local append = stanza:get_child("append", xmlns_saslcert); | |
| 78 local name = append:get_child_text("name", xmlns_saslcert); | |
| 79 local key_info = append:get_child("keyinfo", xmlns_pubkey); | |
| 80 | |
| 81 if not key_info or not name then | |
| 82 origin.send(st.error_reply(stanza, "cancel", "bad-request", "Missing fields.")); -- cancel? not modify? | |
| 83 return true | |
| 84 end | |
| 85 | |
| 86 local id = key_info:get_child_text("name", xmlns_pubkey); | |
| 87 local x509cert = key_info:get_child_text("x509cert", xmlns_pubkey); | |
| 88 | |
| 89 if not id or not x509cert then | |
| 90 origin.send(st.error_reply(stanza, "cancel", "bad-request", "No certificate found.")); | |
| 91 return true | |
| 92 end | |
| 93 | |
| 94 local can_manage = key_info:get_child("no-cert-management", xmlns_saslcert) ~= nil; | |
| 95 local x509cert = key_info:get_child_text("x509cert"); | |
| 96 | |
| 97 local cert = x509.cert_from_pem( | |
| 98 "-----BEGIN CERTIFICATE-----\n" | |
| 99 .. x509cert .. | |
| 100 "\n-----END CERTIFICATE-----\n"); | |
| 101 | |
| 102 | |
| 103 if not cert then | |
| 104 origin.send(st.error_reply(stanza, "modify", "not-acceptable", "Could not parse X.509 certificate")); | |
| 105 return true; | |
| 106 end | |
| 107 | |
| 108 -- Check the certificate. Is it not expired? Does it include id-on-xmppAddr? | |
| 109 | |
| 110 --[[ the method expired doesn't exist in luasec .. yet? | |
| 111 if cert:expired() then | |
| 112 module:log("debug", "This certificate is already expired."); | |
| 113 origin.send(st.error_reply(stanza, "cancel", "bad-request", "This certificate is expired.")); | |
| 114 return true | |
| 115 end | |
| 116 --]] | |
| 117 | |
| 118 if not cert:valid_at(os.time()) then | |
| 119 module:log("debug", "This certificate is not valid at this moment."); | |
| 120 end | |
| 121 | |
| 122 local valid_id_on_xmppAddrs; | |
| 123 local require_id_on_xmppAddr = false; | |
| 124 if require_id_on_xmppAddr then | |
| 125 --local info = {}; | |
| 126 valid_id_on_xmppAddrs = {}; | |
| 127 for _,v in ipairs(cert:subject()) do | |
| 128 --info[#info+1] = (v.name or v.oid) ..":" .. v.value; | |
| 129 if v.oid == id_on_xmppAddr then | |
| 130 if jid_bare(v.value) == jid_bare(origin.full_jid) then | |
| 131 module:log("debug", "The certificate contains a id-on-xmppAddr key, and it is valid."); | |
| 132 valid_id_on_xmppAddrs[#valid_id_on_xmppAddrs+1] = v.value; | |
| 133 -- Is there a point in having >1 ids? Reject?! | |
| 134 else | |
| 135 module:log("debug", "The certificate contains a id-on-xmppAddr key, but it is for %s.", v.value); | |
| 136 -- Reject? | |
| 137 end | |
| 138 end | |
| 139 end | |
| 140 | |
| 141 if #valid_id_on_xmppAddrs == 0 then | |
| 142 origin.send(st.error_reply(stanza, "cancel", "bad-request", "This certificate is has no valid id-on-xmppAddr field.")); | |
| 143 return true -- REJECT?! | |
| 144 end | |
| 145 end | |
| 146 | |
| 147 enable_cert(origin.username, cert, { | |
| 148 id = id, | |
| 149 name = name, | |
| 150 x509cert = x509cert, | |
| 151 no_cert_management = can_manage, | |
| 152 jids = valid_id_on_xmppAddrs, | |
| 153 }); | |
| 154 | |
| 155 module:log("debug", "%s added certificate named %s", origin.full_jid, name); | |
| 156 | |
| 157 origin.send(st.reply(stanza)); | |
| 158 | |
| 159 return true | |
| 160 end | |
| 161 end); | |
| 162 | |
| 163 | |
| 164 local function handle_disable(event) | |
| 165 local origin, stanza = event.origin, event.stanza; | |
| 166 if stanza.attr.type == "set" then | |
| 167 local disable = stanza.tags[1]; | |
| 168 module:log("debug", "%s disabled a certificate", origin.full_jid); | |
| 169 | |
| 170 if disable.name == "revoke" then | |
| 171 module:log("debug", "%s revoked a certificate! Should disconnect all clients that used it", origin.full_jid); | |
| 172 -- TODO hosts.sessions[user].sessions.each{close if uses this cert} | |
| 173 end | |
| 174 local item = disable:get_child("item"); | |
| 175 local name = item and item.attr.id; | |
| 176 | |
| 177 if not name then | |
| 178 origin.send(st.error_reply(stanza, "cancel", "bad-request", "No key specified.")); | |
| 179 return true | |
| 180 end | |
| 181 | |
| 182 disable_cert(origin.username, name); | |
| 183 | |
| 184 origin.send(st.reply(stanza)); | |
| 185 | |
| 186 return true | |
| 187 end | |
| 188 end | |
| 189 | |
| 190 module:hook("iq/self/"..xmlns_saslcert..":disable", handle_disable); | |
| 191 module:hook("iq/self/"..xmlns_saslcert..":revoke", handle_disable); | |
| 192 | |
| 193 -- Here comes the SASL EXTERNAL stuff | |
| 194 | |
| 195 local now = os.time; | |
| 196 module:hook("stream-features", function(event) | |
| 197 local session, features = event.origin, event.features; | |
| 198 if session.secure and session.type == "c2s_unauthed" then | |
| 199 local cert = session.conn:socket():getpeercertificate(); | |
| 200 if not cert then | |
| 201 module:log("error", "No Client Certificate"); | |
| 202 return | |
| 203 end | |
| 204 module:log("info", "Client Certificate: %s", cert:digest(digest_algo)); | |
| 205 local all_certs = dm_load(nil, module.host, dm_table) or {}; | |
| 206 local digest = cert:digest(digest_algo); | |
| 207 local username = all_certs[digest]; | |
| 208 if not cert:valid_at(now()) then | |
| 209 module:log("debug", "Client has an expired certificate", cert:digest(digest_algo)); | |
| 210 return | |
| 211 end | |
| 212 if username then | |
| 213 local certs = dm_load(username, module.host, dm_table) or {}; | |
| 214 local pem = cert:pem(); | |
| 215 for name,info in pairs(certs) do | |
| 216 if info.digest == digest and info.pem == pem then | |
| 217 session.external_auth_cert, session.external_auth_user = pem, username; | |
| 218 module:log("debug", "Stream features:\n%s", tostring(features)); | |
| 219 local mechs = features:get_child("mechanisms", "urn:ietf:params:xml:ns:xmpp-sasl"); | |
| 220 if mechs then | |
| 221 mechs:tag("mechanism"):text("EXTERNAL"); | |
| 222 end | |
| 223 end | |
| 224 end | |
| 225 end | |
| 226 end | |
| 227 end, -1); | |
| 228 | |
| 229 local sm_make_authenticated = require "core.sessionmanager".make_authenticated; | |
| 230 | |
| 231 module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event) | |
| 232 local session, stanza = event.origin, event.stanza; | |
| 233 if session.type == "c2s_unauthed" and event.stanza.attr.mechanism == "EXTERNAL" then | |
| 234 if session.secure then | |
| 235 local cert = session.conn:socket():getpeercertificate(); | |
| 236 if cert:pem() == session.external_auth_cert then | |
| 237 sm_make_authenticated(session, session.external_auth_user); | |
| 238 module:fire_event("authentication-success", { session = session }); | |
| 239 session.external_auth, session.external_auth_user = nil, nil; | |
| 240 session.send(st.stanza("success", { xmlns="urn:ietf:params:xml:ns:xmpp-sasl"})); | |
| 241 session:reset_stream(); | |
| 242 else | |
| 243 module:fire_event("authentication-failure", { session = session, condition = "not-authorized" }); | |
| 244 session.send(st.stanza("failure", { xmlns="urn:ietf:params:xml:ns:xmpp-sasl"}):tag"not-authorized"); | |
| 245 end | |
| 246 else | |
| 247 session.send(st.stanza("failure", { xmlns="urn:ietf:params:xml:ns:xmpp-sasl"}):tag"encryption-required"); | |
| 248 end | |
| 249 return true; | |
| 250 end | |
| 251 end, 1); | |
| 252 |
