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