Software /
code /
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 |
child | 697: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 |