Comparison

util/sasl.lua @ 280:516f4c901991

Rewrote SASL Digest-MD5 responce generating code, fixed some realm related issue and tested it successfully with Psi. Thanks to dwd, remko and jake.
author Tobias Markmann <tm@ayena.de>
date Wed, 12 Nov 2008 21:38:46 +0100
parent 278:770a78cd38d7
child 285:372d0891e8fd
comparison
equal deleted inserted replaced
279:76bad8b64018 280:516f4c901991
6 local tostring = tostring; 6 local tostring = tostring;
7 local st = require "util.stanza"; 7 local st = require "util.stanza";
8 local generate_uuid = require "util.uuid".generate; 8 local generate_uuid = require "util.uuid".generate;
9 local s_match = string.match; 9 local s_match = string.match;
10 local gmatch = string.gmatch 10 local gmatch = string.gmatch
11 local string = string
11 local math = require "math" 12 local math = require "math"
12 local type = type 13 local type = type
13 local error = error 14 local error = error
14 local print = print 15 local print = print
15 16
30 if self.onAuth(authentication, password) == true then 31 if self.onAuth(authentication, password) == true then
31 self.onWrite(st.stanza("success", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"})) 32 self.onWrite(st.stanza("success", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}))
32 self.onSuccess(authentication) 33 self.onSuccess(authentication)
33 else 34 else
34 self.onWrite(st.stanza("failure", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):tag("temporary-auth-failure")); 35 self.onWrite(st.stanza("failure", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):tag("temporary-auth-failure"));
36 self.onFail("Wrong password.")
35 end 37 end
36 end 38 end
37 return object 39 return object
38 end 40 end
39 41
40
41 --[[
42 SERVER:
43 nonce="3145176401",qop="auth",charset=utf-8,algorithm=md5-sess
44
45 CLIENT: username="tobiasfar",nonce="3145176401",cnonce="pJiW7hzeZLvOSAf7gBzwTzLWe4obYOVDlnNESzQCzGg=",nc=00000001,digest-uri="xmpp/jabber.org",qop=auth,response=99a93ba75235136e6403c3a2ba37089d,charset=utf-8
46
47 username="tobias",nonce="4406697386",cnonce="wUnT7vYrOB0V8D/lKd5bhpaNCk+hLJwc8T4CBCqp7WM=",nc=00000001,digest-uri="xmpp/luaetta.ath.cx",qop=auth,response=d202b8a1bdf8204816fb23c5f87b6b63,charset=utf-8
48
49 SERVER:
50 rspauth=ab66d28c260e97da577ce3aac46a8991
51 ]]--
52 local function new_digest_md5(onAuth, onSuccess, onFail, onWrite) 42 local function new_digest_md5(onAuth, onSuccess, onFail, onWrite)
53 local function H(s) 43 --TODO maybe support for authzid
54 return md5.sum(s)
55 end
56
57 local function KD(k, s)
58 return H(k..":"..s)
59 end
60
61 local function HEX(n)
62 return md5.sumhexa(n)
63 end
64
65 local function HMAC(k, s)
66 return crypto.hmac.digest("md5", s, k, true)
67 end
68 44
69 local function serialize(message) 45 local function serialize(message)
70 local data = "" 46 local data = ""
71 47
72 if type(message) ~= "table" then error("serialize needs an argument of type table.") end 48 if type(message) ~= "table" then error("serialize needs an argument of type table.") end
74 -- testing all possible values 50 -- testing all possible values
75 if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end 51 if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end
76 if message["qop"] then data = data..[[qop="]]..message.qop..[[",]] end 52 if message["qop"] then data = data..[[qop="]]..message.qop..[[",]] end
77 if message["charset"] then data = data..[[charset=]]..message.charset.."," end 53 if message["charset"] then data = data..[[charset=]]..message.charset.."," end
78 if message["algorithm"] then data = data..[[algorithm=]]..message.algorithm.."," end 54 if message["algorithm"] then data = data..[[algorithm=]]..message.algorithm.."," end
79 if message["rspauth"] then data = data..[[rspauth=]]..message.algorith.."," end 55 if message["realm"] then data = data..[[realm="]]..message.realm..[[",]] end
56 if message["rspauth"] then data = data..[[rspauth=]]..message.rspauth.."," end
80 data = data:gsub(",$", "") 57 data = data:gsub(",$", "")
81 return data 58 return data
82 end 59 end
83 60
84 local function parse(data) 61 local function parse(data)
85 message = {} 62 message = {}
86 for k, v in gmatch(data, [[([%w%-]+)="?([%w%-%/%.]+)"?,?]]) do 63 log("debug", "parse-message: "..data)
64 for k, v in gmatch(data, [[([%w%-]+)="?([%w%-%/%.%+=]+)"?,?]]) do
87 message[k] = v 65 message[k] = v
66 log("debug", " "..k.." = "..v)
88 end 67 end
89 return message 68 return message
90 end 69 end
91 70
92 local object = { mechanism = "DIGEST-MD5", onAuth = onAuth, onSuccess = onSuccess, onFail = onFail, 71 local object = { mechanism = "DIGEST-MD5", onAuth = onAuth, onSuccess = onSuccess, onFail = onFail,
93 onWrite = onWrite } 72 onWrite = onWrite }
94 73
95 --TODO: something better than math.random would be nice, maybe OpenSSL's random number generator 74 --TODO: something better than math.random would be nice, maybe OpenSSL's random number generator
96 object.nonce = math.random(0, 9) 75 object.nonce = generate_uuid()
97 for i = 1, 9 do object.nonce = object.nonce..math.random(0, 9) end 76 log("debug", "SASL nonce: "..object.nonce)
98 object.step = 1 77 object.step = 1
99 object.nonce_count = {} 78 object.nonce_count = {}
100 local challenge = base64.encode(serialize({ nonce = object.nonce, 79 local challenge = base64.encode(serialize({ nonce = object.nonce,
101 qop = "auth", 80 qop = "auth",
102 charset = "utf-8", 81 charset = "utf-8",
103 algorithm = "md5-sess"} )); 82 algorithm = "md5-sess"} ));
104 object.onWrite(st.stanza("challenge", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):text(challenge)) 83 object.onWrite(st.stanza("challenge", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):text(challenge))
105 object.feed = function(self, stanza) 84 object.feed = function(self, stanza)
85 log("debug", "SASL step: "..self.step)
106 if stanza.name ~= "response" and stanza.name ~= "auth" then self.onFail("invalid-stanza-tag") end 86 if stanza.name ~= "response" and stanza.name ~= "auth" then self.onFail("invalid-stanza-tag") end
107 if stanza.attr.xmlns ~= "urn:ietf:params:xml:ns:xmpp-sasl" then self.onFail("invalid-stanza-namespace") end 87 if stanza.attr.xmlns ~= "urn:ietf:params:xml:ns:xmpp-sasl" then self.onFail("invalid-stanza-namespace") end
108 if stanza.name == "auth" then return end 88 if stanza.name == "auth" then return end
109 self.step = self.step + 1 89 self.step = self.step + 1
110 if (self.step == 2) then 90 if (self.step == 2) then
111 local response = parse(base64.decode(stanza[1])) 91 local response = parse(base64.decode(stanza[1]))
112 -- check for replay attack 92 -- check for replay attack
113 if response["nonce-count"] then 93 if response["nc"] then
114 if self.nonce_count[response["nonce-count"]] then self.onFail("not-authorized") end 94 if self.nonce_count[response["nc"]] then self.onFail("not-authorized") end
115 end 95 end
116 96
117 -- check for username, it's REQUIRED by RFC 2831 97 -- check for username, it's REQUIRED by RFC 2831
118 if not response["username"] then 98 if not response["username"] then
119 self.onFail("malformed-request") 99 self.onFail("malformed-request")
120 end 100 end
101 self["username"] = response["username"]
121 102
122 -- check for nonce, ... 103 -- check for nonce, ...
123 if not response["nonce"] then 104 if not response["nonce"] then
124 self.onFail("malformed-request") 105 self.onFail("malformed-request")
125 else 106 else
126 -- check if it's the right nonce 107 -- check if it's the right nonce
127 if response["nonce"] ~= self.nonce then self.onFail("malformed-request") end 108 if response["nonce"] ~= tostring(self.nonce) then self.onFail("malformed-request") end
128 end 109 end
129 110
130 if not response["cnonce"] then self.onFail("malformed-request") end 111 if not response["cnonce"] then self.onFail("malformed-request") end
131 if not response["qop"] then response["qop"] = "auth" end 112 if not response["qop"] then response["qop"] = "auth" end
132 113
133 local hostname = "" 114 if response["realm"] == nil then response["realm"] = "" end
115
116 local domain = ""
134 local protocol = "" 117 local protocol = ""
135 if response["digest-uri"] then 118 if response["digest-uri"] then
136 protocol, hostname = response["digest-uri"]:match("(%w+)/(.*)$") 119 protocol, domain = response["digest-uri"]:match("(%w+)/(.*)$")
137 else 120 else
138 error("No digest-uri") 121 error("No digest-uri")
139 end 122 end
140 123
141 -- compare response_value with own calculation 124 -- compare response_value with own calculation
142 --local A1 = usermanager.get_md5(response["username"], hostname)..":"..response["nonce"]..response["cnonce"] 125 --local A1 = usermanager.get_md5(response["username"], hostname)..":"..response["nonce"]..response["cnonce"]
143 local A1 = H("tobias:luaetta.ath.cx:tobias")..":"..response["nonce"]..response["cnonce"]
144 local A2 = "AUTHENTICATE:"..response["digest-uri"]
145 126
146 local response_value = HEX(KD(HEX(H(A1)), response["nonce"]..":"..response["nonce-count"]..":"..response["cnonce-value"]..":"..response["qop"]..":"..HEX(H(A2)))) 127 --FIXME actual username and password here :P
128 local X = "tobias:"..response["realm"]..":tobias"
129 local Y = md5.sum(X)
130 local A1 = Y..":"..response["nonce"]..":"..response["cnonce"]--:authzid
131 local A2 = "AUTHENTICATE:"..protocol.."/"..domain
132
133 local HA1 = md5.sumhexa(A1)
134 local HA2 = md5.sumhexa(A2)
135
136 local KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2
137 local response_value = md5.sumhexa(KD)
147 138
148 log("debug", "response_value: "..response_value); 139 log("debug", "response_value: "..response_value);
149 140 log("debug", "response: "..response["response"]);
150 if response["qop"] == "auth" then 141 if response_value == response["response"] then
151 142 -- calculate rspauth
143 A2 = ":"..protocol.."/"..domain
144
145 HA1 = md5.sumhexa(A1)
146 HA2 = md5.sumhexa(A2)
147
148 KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2
149 local rspauth = md5.sumhexa(KD)
150
151 self.onWrite(st.stanza("challenge", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):text(base64.encode(serialize({rspauth = rspauth}))))
152 else 152 else
153 153 self.onWrite(st.stanza("response", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}))
154 self.onFail()
155 end
156 elseif self.step == 3 then
157 if stanza.name == "response" then
158 self.onWrite(st.stanza("success", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}))
159 self.onSuccess(self.username)
160 else
161 self.onFail("Third step isn't a response stanza.")
154 end 162 end
155
156 --local response_value = HEX(KD(HEX(H(A1)), response["nonce"]..":"..response["nonce-count"]..":"..response["cnonce-value"]..":"..response["qop"]..":"..HEX(H(A2))))
157
158 end 163 end
159 --[[
160 local authorization = s_match(response, "([^&%z]+)")
161 local authentication = s_match(response, "%z([^&%z]+)%z")
162 local password = s_match(response, "%z[^&%z]+%z([^&%z]+)")
163 if self.onAuth(authentication, password) == true then
164 self.onWrite(st.stanza("success", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}))
165 self.onSuccess(authentication)
166 else
167 self.onWrite(st.stanza("failure", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):tag("temporary-auth-failure"));
168 end]]--
169 end 164 end
170 return object 165 return object
171 end 166 end
172 167
173 function new(mechanism, onAuth, onSuccess, onFail, onWrite) 168 function new(mechanism, onAuth, onSuccess, onFail, onWrite)