Software /
code /
prosody
Comparison
util/sasl.lua @ 1585:edc066730d11
Switch to using a more generic credentials_callback/handler for SASL auth.
Not all authentication mechanisms have the same requirements; it makes sense
to provide them only with the information they require (and for them to
depend on that) so that as many auth mechanisms as possible can be supported
with a variety of credentials-storing schemes. This commit patches that together
author | nick@lupine.me.uk |
---|---|
date | Fri, 24 Jul 2009 01:34:25 +0100 |
parent | 1518:9707dfa80980 |
child | 1656:cf9220a364cd |
child | 2175:3ca8755581a1 |
comparison
equal
deleted
inserted
replaced
1584:ffe8a9296e04 | 1585:edc066730d11 |
---|---|
1 -- sasl.lua v0.4 | 1 -- sasl.lua v0.4 |
2 -- Copyright (C) 2008-2009 Tobias Markmann | 2 -- Copyright (C) 2008-2009 Tobias Markmann |
3 -- | 3 -- |
4 -- All rights reserved. | 4 -- All rights reserved. |
5 -- | 5 -- |
6 -- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | 6 -- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: |
7 -- | 7 -- |
8 -- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. | 8 -- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. |
9 -- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. | 9 -- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. |
10 -- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. | 10 -- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. |
11 -- | 11 -- |
12 -- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 12 -- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
13 | 13 |
14 | 14 |
15 local md5 = require "util.hashes".md5; | 15 local md5 = require "util.hashes".md5; |
16 local log = require "util.logger".init("sasl"); | 16 local log = require "util.logger".init("sasl"); |
28 local error = error | 28 local error = error |
29 local print = print | 29 local print = print |
30 | 30 |
31 module "sasl" | 31 module "sasl" |
32 | 32 |
33 local function new_plain(realm, password_handler) | 33 -- Credentials handler: |
34 local object = { mechanism = "PLAIN", realm = realm, password_handler = password_handler} | 34 -- Arguments: ("PLAIN", user, host, password) |
35 -- Returns: true (success) | false (fail) | nil (user unknown) | |
36 local function new_plain(realm, credentials_handler) | |
37 local object = { mechanism = "PLAIN", realm = realm, credentials_handler = credentials_handler} | |
35 function object.feed(self, message) | 38 function object.feed(self, message) |
36 | |
37 if message == "" or message == nil then return "failure", "malformed-request" end | 39 if message == "" or message == nil then return "failure", "malformed-request" end |
38 local response = message | 40 local response = message |
39 local authorization = s_match(response, "([^&%z]+)") | 41 local authorization = s_match(response, "([^&%z]+)") |
40 local authentication = s_match(response, "%z([^&%z]+)%z") | 42 local authentication = s_match(response, "%z([^&%z]+)%z") |
41 local password = s_match(response, "%z[^&%z]+%z([^&%z]+)") | 43 local password = s_match(response, "%z[^&%z]+%z([^&%z]+)") |
42 | 44 |
43 if authentication == nil or password == nil then return "failure", "malformed-request" end | 45 if authentication == nil or password == nil then return "failure", "malformed-request" end |
44 | 46 self.username = authentication |
45 local password_encoding, correct_password = self.password_handler(authentication, self.realm, self.realm, "PLAIN") | 47 local auth_success = self.credentials_handler("PLAIN", self.username, self.realm, password) |
46 | 48 |
47 if correct_password == nil then return "failure", "not-authorized" | 49 if auth_success then |
48 elseif correct_password == false then return "failure", "account-disabled" end | 50 return "success" |
49 | 51 elseif auth_success == nil then |
50 local claimed_password = "" | 52 return "failure", "account-disabled" |
51 if password_encoding == nil then claimed_password = password | 53 else |
52 else claimed_password = password_encoding(password) end | 54 return "failure", "not-authorized" |
53 | 55 end |
54 self.username = authentication | 56 end |
55 if claimed_password == correct_password then | 57 return object |
56 return "success" | 58 end |
57 else | 59 |
58 return "failure", "not-authorized" | 60 -- credentials_handler: |
59 end | 61 -- Arguments: (mechanism, node, domain, realm, decoder) |
60 end | 62 -- Returns: Password encoding, (plaintext) password |
61 return object | |
62 end | |
63 | |
64 | |
65 -- implementing RFC 2831 | 63 -- implementing RFC 2831 |
66 local function new_digest_md5(realm, password_handler) | 64 local function new_digest_md5(realm, credentials_handler) |
67 --TODO complete support for authzid | 65 --TODO complete support for authzid |
68 | 66 |
69 local function serialize(message) | 67 local function serialize(message) |
70 local data = "" | 68 local data = "" |
71 | 69 |
72 if type(message) ~= "table" then error("serialize needs an argument of type table.") end | 70 if type(message) ~= "table" then error("serialize needs an argument of type table.") end |
73 | 71 |
74 -- testing all possible values | 72 -- testing all possible values |
75 if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end | 73 if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end |
76 if message["qop"] then data = data..[[qop="]]..message.qop..[[",]] end | 74 if message["qop"] then data = data..[[qop="]]..message.qop..[[",]] end |
77 if message["charset"] then data = data..[[charset=]]..message.charset.."," end | 75 if message["charset"] then data = data..[[charset=]]..message.charset.."," end |
78 if message["algorithm"] then data = data..[[algorithm=]]..message.algorithm.."," end | 76 if message["algorithm"] then data = data..[[algorithm=]]..message.algorithm.."," end |
79 if message["realm"] then data = data..[[realm="]]..message.realm..[[",]] end | 77 if message["realm"] then data = data..[[realm="]]..message.realm..[[",]] end |
80 if message["rspauth"] then data = data..[[rspauth=]]..message.rspauth.."," end | 78 if message["rspauth"] then data = data..[[rspauth=]]..message.rspauth.."," end |
81 data = data:gsub(",$", "") | 79 data = data:gsub(",$", "") |
82 return data | 80 return data |
83 end | 81 end |
84 | 82 |
85 local function utf8tolatin1ifpossible(passwd) | 83 local function utf8tolatin1ifpossible(passwd) |
86 local i = 1; | 84 local i = 1; |
87 while i <= #passwd do | 85 while i <= #passwd do |
88 local passwd_i = to_byte(passwd:sub(i, i)); | 86 local passwd_i = to_byte(passwd:sub(i, i)); |
89 if passwd_i > 0x7F then | 87 if passwd_i > 0x7F then |
135 message[k] = v; | 133 message[k] = v; |
136 end | 134 end |
137 return message; | 135 return message; |
138 end | 136 end |
139 | 137 |
140 local object = { mechanism = "DIGEST-MD5", realm = realm, password_handler = password_handler}; | 138 local object = { mechanism = "DIGEST-MD5", realm = realm, credentials_handler = credentials_handler}; |
141 | 139 |
142 object.nonce = generate_uuid(); | 140 object.nonce = generate_uuid(); |
143 object.step = 0; | 141 object.step = 0; |
144 object.nonce_count = {}; | 142 object.nonce_count = {}; |
145 | 143 |
146 function object.feed(self, message) | 144 function object.feed(self, message) |
147 self.step = self.step + 1; | 145 self.step = self.step + 1; |
148 if (self.step == 1) then | 146 if (self.step == 1) then |
149 local challenge = serialize({ nonce = object.nonce, | 147 local challenge = serialize({ nonce = object.nonce, |
150 qop = "auth", | 148 qop = "auth", |
151 charset = "utf-8", | 149 charset = "utf-8", |
152 algorithm = "md5-sess", | 150 algorithm = "md5-sess", |
153 realm = self.realm}); | 151 realm = self.realm}); |
154 return "challenge", challenge; | 152 return "challenge", challenge; |
156 local response = parse(message); | 154 local response = parse(message); |
157 -- check for replay attack | 155 -- check for replay attack |
158 if response["nc"] then | 156 if response["nc"] then |
159 if self.nonce_count[response["nc"]] then return "failure", "not-authorized" end | 157 if self.nonce_count[response["nc"]] then return "failure", "not-authorized" end |
160 end | 158 end |
161 | 159 |
162 -- check for username, it's REQUIRED by RFC 2831 | 160 -- check for username, it's REQUIRED by RFC 2831 |
163 if not response["username"] then | 161 if not response["username"] then |
164 return "failure", "malformed-request"; | 162 return "failure", "malformed-request"; |
165 end | 163 end |
166 self["username"] = response["username"]; | 164 self["username"] = response["username"]; |
167 | 165 |
168 -- check for nonce, ... | 166 -- check for nonce, ... |
169 if not response["nonce"] then | 167 if not response["nonce"] then |
170 return "failure", "malformed-request"; | 168 return "failure", "malformed-request"; |
171 else | 169 else |
172 -- check if it's the right nonce | 170 -- check if it's the right nonce |
173 if response["nonce"] ~= tostring(self.nonce) then return "failure", "malformed-request" end | 171 if response["nonce"] ~= tostring(self.nonce) then return "failure", "malformed-request" end |
174 end | 172 end |
175 | 173 |
176 if not response["cnonce"] then return "failure", "malformed-request", "Missing entry for cnonce in SASL message." end | 174 if not response["cnonce"] then return "failure", "malformed-request", "Missing entry for cnonce in SASL message." end |
177 if not response["qop"] then response["qop"] = "auth" end | 175 if not response["qop"] then response["qop"] = "auth" end |
178 | 176 |
179 if response["realm"] == nil or response["realm"] == "" then | 177 if response["realm"] == nil or response["realm"] == "" then |
180 response["realm"] = ""; | 178 response["realm"] = ""; |
181 elseif response["realm"] ~= self.realm then | 179 elseif response["realm"] ~= self.realm then |
182 return "failure", "not-authorized", "Incorrect realm value"; | 180 return "failure", "not-authorized", "Incorrect realm value"; |
183 end | 181 end |
184 | 182 |
185 local decoder; | 183 local decoder; |
186 if response["charset"] == nil then | 184 if response["charset"] == nil then |
187 decoder = utf8tolatin1ifpossible; | 185 decoder = utf8tolatin1ifpossible; |
188 elseif response["charset"] ~= "utf-8" then | 186 elseif response["charset"] ~= "utf-8" then |
189 return "failure", "incorrect-encoding", "The client's response uses "..response["charset"].." for encoding with isn't supported by sasl.lua. Supported encodings are latin or utf-8."; | 187 return "failure", "incorrect-encoding", "The client's response uses "..response["charset"].." for encoding with isn't supported by sasl.lua. Supported encodings are latin or utf-8."; |
190 end | 188 end |
191 | 189 |
192 local domain = ""; | 190 local domain = ""; |
193 local protocol = ""; | 191 local protocol = ""; |
194 if response["digest-uri"] then | 192 if response["digest-uri"] then |
195 protocol, domain = response["digest-uri"]:match("(%w+)/(.*)$"); | 193 protocol, domain = response["digest-uri"]:match("(%w+)/(.*)$"); |
196 if protocol == nil or domain == nil then return "failure", "malformed-request" end | 194 if protocol == nil or domain == nil then return "failure", "malformed-request" end |
197 else | 195 else |
198 return "failure", "malformed-request", "Missing entry for digest-uri in SASL message." | 196 return "failure", "malformed-request", "Missing entry for digest-uri in SASL message." |
199 end | 197 end |
200 | 198 |
201 --TODO maybe realm support | 199 --TODO maybe realm support |
202 self.username = response["username"]; | 200 self.username = response["username"]; |
203 local password_encoding, Y = self.password_handler(response["username"], to_unicode(domain), response["realm"], "DIGEST-MD5", decoder); | 201 local password_encoding, Y = self.credentials_handler("DIGEST-MD5", response["username"], to_unicode(domain), response["realm"], decoder); |
204 if Y == nil then return "failure", "not-authorized" | 202 if Y == nil then return "failure", "not-authorized" |
205 elseif Y == false then return "failure", "account-disabled" end | 203 elseif Y == false then return "failure", "account-disabled" end |
206 local A1 = ""; | 204 local A1 = ""; |
207 if response.authzid then | 205 if response.authzid then |
208 if response.authzid == self.username.."@"..self.realm then | 206 if response.authzid == self.username.."@"..self.realm then |
214 end | 212 end |
215 else | 213 else |
216 A1 = Y..":"..response["nonce"]..":"..response["cnonce"]; | 214 A1 = Y..":"..response["nonce"]..":"..response["cnonce"]; |
217 end | 215 end |
218 local A2 = "AUTHENTICATE:"..protocol.."/"..domain; | 216 local A2 = "AUTHENTICATE:"..protocol.."/"..domain; |
219 | 217 |
220 local HA1 = md5(A1, true); | 218 local HA1 = md5(A1, true); |
221 local HA2 = md5(A2, true); | 219 local HA2 = md5(A2, true); |
222 | 220 |
223 local KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2; | 221 local KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2; |
224 local response_value = md5(KD, true); | 222 local response_value = md5(KD, true); |
225 | 223 |
226 if response_value == response["response"] then | 224 if response_value == response["response"] then |
227 -- calculate rspauth | 225 -- calculate rspauth |
228 A2 = ":"..protocol.."/"..domain; | 226 A2 = ":"..protocol.."/"..domain; |
229 | 227 |
230 HA1 = md5(A1, true); | 228 HA1 = md5(A1, true); |
231 HA2 = md5(A2, true); | 229 HA2 = md5(A2, true); |
232 | 230 |
233 KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2 | 231 KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2 |
234 local rspauth = md5(KD, true); | 232 local rspauth = md5(KD, true); |
235 self.authenticated = true; | 233 self.authenticated = true; |
236 return "challenge", serialize({rspauth = rspauth}); | 234 return "challenge", serialize({rspauth = rspauth}); |
237 else | 235 else |
238 return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated." | 236 return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated." |
239 end | 237 end |
240 elseif self.step == 3 then | 238 elseif self.step == 3 then |
241 if self.authenticated ~= nil then return "success" | 239 if self.authenticated ~= nil then return "success" |
242 else return "failure", "malformed-request" end | 240 else return "failure", "malformed-request" end |
243 end | 241 end |
244 end | 242 end |
245 return object; | 243 return object; |
246 end | 244 end |
247 | 245 |
248 local function new_anonymous(realm, password_handler) | 246 -- Credentials handler: Can be nil. If specified, should take the mechanism as |
249 local object = { mechanism = "ANONYMOUS", realm = realm, password_handler = password_handler} | 247 -- the only argument, and return true for OK, or false for not-OK (TODO) |
248 local function new_anonymous(realm, credentials_handler) | |
249 local object = { mechanism = "ANONYMOUS", realm = realm, credentials_handler = credentials_handler} | |
250 function object.feed(self, message) | 250 function object.feed(self, message) |
251 return "success" | 251 return "success" |
252 end | 252 end |
253 object["username"] = generate_uuid() | 253 object["username"] = generate_uuid() |
254 return object | 254 return object |
255 end | 255 end |
256 | 256 |
257 | 257 |
258 function new(mechanism, realm, password_handler) | 258 function new(mechanism, realm, credentials_handler) |
259 local object | 259 local object |
260 if mechanism == "PLAIN" then object = new_plain(realm, password_handler) | 260 if mechanism == "PLAIN" then object = new_plain(realm, credentials_handler) |
261 elseif mechanism == "DIGEST-MD5" then object = new_digest_md5(realm, password_handler) | 261 elseif mechanism == "DIGEST-MD5" then object = new_digest_md5(realm, credentials_handler) |
262 elseif mechanism == "ANONYMOUS" then object = new_anonymous(realm, password_handler) | 262 elseif mechanism == "ANONYMOUS" then object = new_anonymous(realm, credentials_handler) |
263 else | 263 else |
264 log("debug", "Unsupported SASL mechanism: "..tostring(mechanism)); | 264 log("debug", "Unsupported SASL mechanism: "..tostring(mechanism)); |
265 return nil | 265 return nil |
266 end | 266 end |
267 return object | 267 return object |