1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 """DIGEST-MD5 authentication mechanism for PyXMPP SASL implementation.
18
19 Normative reference:
20 - `RFC 2831 <http://www.ietf.org/rfc/rfc2831.txt>`__
21 """
22
23 __revision__="$Id: digest_md5.py 683 2008-12-05 18:25:45Z jajcus $"
24 __docformat__="restructuredtext en"
25
26 from binascii import b2a_hex
27 import re
28 import logging
29
30 try:
31 import hashlib
32 md5_factory = hashlib.md5
33 except:
34 import md5
35 md5_factory = md5.new
36
37 from pyxmpp.sasl.core import ClientAuthenticator,ServerAuthenticator
38 from pyxmpp.sasl.core import Failure,Response,Challenge,Success,Failure
39
40 from pyxmpp.utils import to_utf8,from_utf8
41
42 quote_re=re.compile(r"(?<!\\)\\(.)")
43
45 """Unquote quoted value from DIGEST-MD5 challenge or response.
46
47 If `s` doesn't start or doesn't end with '"' then return it unchanged,
48 remove the quotes and escape backslashes otherwise.
49
50 :Parameters:
51 - `s`: a quoted string.
52 :Types:
53 - `s`: `str`
54
55 :return: the unquoted string.
56 :returntype: `str`"""
57 if not s.startswith('"') or not s.endswith('"'):
58 return s
59 return quote_re.sub(r"\1",s[1:-1])
60
62 """Prepare a string for quoting for DIGEST-MD5 challenge or response.
63
64 Don't add the quotes, only escape '"' and "\\" with backslashes.
65
66 :Parameters:
67 - `s`: a raw string.
68 :Types:
69 - `s`: `str`
70
71 :return: `s` with '"' and "\\" escaped using "\\".
72 :returntype: `str`"""
73 s=s.replace('\\','\\\\')
74 s=s.replace('"','\\"')
75 return '%s' % (s,)
76
78 """H function of the DIGEST-MD5 algorithm (MD5 sum).
79
80 :Parameters:
81 - `s`: a string.
82 :Types:
83 - `s`: `str`
84
85 :return: MD5 sum of the string.
86 :returntype: `str`"""
87 return md5_factory(s).digest()
88
90 """KD function of the DIGEST-MD5 algorithm.
91
92 :Parameters:
93 - `k`: a string.
94 - `s`: a string.
95 :Types:
96 - `k`: `str`
97 - `s`: `str`
98
99 :return: MD5 sum of the strings joined with ':'.
100 :returntype: `str`"""
101 return _h_value("%s:%s" % (k,s))
102
104 """Compute MD5 sum of username:realm:password.
105
106 :Parameters:
107 - `username`: a username.
108 - `realm`: a realm.
109 - `passwd`: a password.
110 :Types:
111 - `username`: `str`
112 - `realm`: `str`
113 - `passwd`: `str`
114
115 :return: the MD5 sum of the parameters joined with ':'.
116 :returntype: `str`"""
117 if realm is None:
118 realm=""
119 if type(passwd) is unicode:
120 passwd=passwd.encode("utf-8")
121 return _h_value("%s:%s:%s" % (username,realm,passwd))
122
124 """Compute DIGEST-MD5 response value.
125
126 :Parameters:
127 - `urp_hash`: MD5 sum of username:realm:password.
128 - `nonce`: nonce value from a server challenge.
129 - `cnonce`: cnonce value from the client response.
130 - `nonce_count`: nonce count value.
131 - `authzid`: authorization id.
132 - `digest_uri`: digest-uri value.
133 :Types:
134 - `urp_hash`: `str`
135 - `nonce`: `str`
136 - `nonce_count`: `int`
137 - `authzid`: `str`
138 - `digest_uri`: `str`
139
140 :return: the computed response value.
141 :returntype: `str`"""
142 if authzid:
143 a1="%s:%s:%s:%s" % (urp_hash,nonce,cnonce,authzid)
144 else:
145 a1="%s:%s:%s" % (urp_hash,nonce,cnonce)
146 a2="AUTHENTICATE:"+digest_uri
147 return b2a_hex(_kd_value( b2a_hex(_h_value(a1)),"%s:%s:%s:%s:%s" % (
148 nonce,nonce_count,
149 cnonce,"auth",b2a_hex(_h_value(a2)) ) ))
150
152 """Compute DIGEST-MD5 rspauth value.
153
154 :Parameters:
155 - `urp_hash`: MD5 sum of username:realm:password.
156 - `nonce`: nonce value from a server challenge.
157 - `cnonce`: cnonce value from the client response.
158 - `nonce_count`: nonce count value.
159 - `authzid`: authorization id.
160 - `digest_uri`: digest-uri value.
161 :Types:
162 - `urp_hash`: `str`
163 - `nonce`: `str`
164 - `nonce_count`: `int`
165 - `authzid`: `str`
166 - `digest_uri`: `str`
167
168 :return: the computed rspauth value.
169 :returntype: `str`"""
170 if authzid:
171 a1="%s:%s:%s:%s" % (urp_hash,nonce,cnonce,authzid)
172 else:
173 a1="%s:%s:%s" % (urp_hash,nonce,cnonce)
174 a2=":"+digest_uri
175 return b2a_hex(_kd_value( b2a_hex(_h_value(a1)),"%s:%s:%s:%s:%s" % (
176 nonce,nonce_count,
177 cnonce,"auth",b2a_hex(_h_value(a2)) ) ))
178
179 _param_re=re.compile(r'^(?P<var>[^=]+)\=(?P<val>(\"(([^"\\]+)|(\\\")'
180 r'|(\\\\))+\")|([^",]+))(\s*\,\s*(?P<rest>.*))?$')
181
183 """Provides PLAIN SASL authentication for a client.
184
185 :Ivariables:
186 - `password`: current authentication password
187 - `pformat`: current authentication password format
188 - `realm`: current authentication realm
189 """
190
192 """Initialize a `DigestMD5ClientAuthenticator` object.
193
194 :Parameters:
195 - `password_manager`: name of the password manager object providing
196 authentication credentials.
197 :Types:
198 - `password_manager`: `PasswordManager`"""
199 ClientAuthenticator.__init__(self,password_manager)
200 self.username=None
201 self.rspauth_checked=None
202 self.response_auth=None
203 self.authzid=None
204 self.pformat=None
205 self.realm=None
206 self.password=None
207 self.nonce_count=None
208 self.__logger=logging.getLogger("pyxmpp.sasl.DigestMD5ClientAuthenticator")
209
210 - def start(self,username,authzid):
211 """Start the authentication process initializing client state.
212
213 :Parameters:
214 - `username`: username (authentication id).
215 - `authzid`: authorization id.
216 :Types:
217 - `username`: `unicode`
218 - `authzid`: `unicode`
219
220 :return: the (empty) initial response
221 :returntype: `sasl.Response` or `sasl.Failure`"""
222 self.username=from_utf8(username)
223 if authzid:
224 self.authzid=from_utf8(authzid)
225 else:
226 self.authzid=""
227 self.password=None
228 self.pformat=None
229 self.nonce_count=0
230 self.response_auth=None
231 self.rspauth_checked=0
232 self.realm=None
233 return Response()
234
236 """Process a challenge and return the response.
237
238 :Parameters:
239 - `challenge`: the challenge from server.
240 :Types:
241 - `challenge`: `str`
242
243 :return: the response or a failure indicator.
244 :returntype: `sasl.Response` or `sasl.Failure`"""
245 if not challenge:
246 self.__logger.debug("Empty challenge")
247 return Failure("bad-challenge")
248 challenge=challenge.split('\x00')[0]
249 if self.response_auth:
250 return self._final_challenge(challenge)
251 realms=[]
252 nonce=None
253 charset="iso-8859-1"
254 while challenge:
255 m=_param_re.match(challenge)
256 if not m:
257 self.__logger.debug("Challenge syntax error: %r" % (challenge,))
258 return Failure("bad-challenge")
259 challenge=m.group("rest")
260 var=m.group("var")
261 val=m.group("val")
262 self.__logger.debug("%r: %r" % (var,val))
263 if var=="realm":
264 realms.append(_unquote(val))
265 elif var=="nonce":
266 if nonce:
267 self.__logger.debug("Duplicate nonce")
268 return Failure("bad-challenge")
269 nonce=_unquote(val)
270 elif var=="qop":
271 qopl=_unquote(val).split(",")
272 if "auth" not in qopl:
273 self.__logger.debug("auth not supported")
274 return Failure("not-implemented")
275 elif var=="charset":
276 if val!="utf-8":
277 self.__logger.debug("charset given and not utf-8")
278 return Failure("bad-challenge")
279 charset="utf-8"
280 elif var=="algorithm":
281 if val!="md5-sess":
282 self.__logger.debug("algorithm given and not md5-sess")
283 return Failure("bad-challenge")
284 if not nonce:
285 self.__logger.debug("nonce not given")
286 return Failure("bad-challenge")
287 self._get_password()
288 return self._make_response(charset,realms,nonce)
289
291 """Retrieve user's password from the password manager.
292
293 Set `self.password` to the password and `self.pformat`
294 to its format name ('plain' or 'md5:user:realm:pass')."""
295 if self.password is None:
296 self.password,self.pformat=self.password_manager.get_password(
297 self.username,["plain","md5:user:realm:pass"])
298 if not self.password or self.pformat not in ("plain","md5:user:realm:pass"):
299 self.__logger.debug("Couldn't get plain password. Password: %r Format: %r"
300 % (self.password,self.pformat))
301 return Failure("password-unavailable")
302
304 """Make a response for the first challenge from the server.
305
306 :Parameters:
307 - `charset`: charset name from the challenge.
308 - `realms`: realms list from the challenge.
309 - `nonce`: nonce value from the challenge.
310 :Types:
311 - `charset`: `str`
312 - `realms`: `str`
313 - `nonce`: `str`
314
315 :return: the response or a failure indicator.
316 :returntype: `sasl.Response` or `sasl.Failure`"""
317 params=[]
318 realm=self._get_realm(realms,charset)
319 if isinstance(realm,Failure):
320 return realm
321 elif realm:
322 realm=_quote(realm)
323 params.append('realm="%s"' % (realm,))
324
325 try:
326 username=self.username.encode(charset)
327 except UnicodeError:
328 self.__logger.debug("Couldn't encode username to %r" % (charset,))
329 return Failure("incompatible-charset")
330
331 username=_quote(username)
332 params.append('username="%s"' % (username,))
333
334 cnonce=self.password_manager.generate_nonce()
335 cnonce=_quote(cnonce)
336 params.append('cnonce="%s"' % (cnonce,))
337
338 params.append('nonce="%s"' % (_quote(nonce),))
339
340 self.nonce_count+=1
341 nonce_count="%08x" % (self.nonce_count,)
342 params.append('nc=%s' % (nonce_count,))
343
344 params.append('qop=auth')
345
346 serv_type=self.password_manager.get_serv_type().encode("us-ascii")
347 host=self.password_manager.get_serv_host().encode("us-ascii")
348 serv_name=self.password_manager.get_serv_name().encode("us-ascii")
349
350 if serv_name and serv_name != host:
351 digest_uri="%s/%s/%s" % (serv_type,host,serv_name)
352 else:
353 digest_uri="%s/%s" % (serv_type,host)
354
355 digest_uri=_quote(digest_uri)
356 params.append('digest-uri="%s"' % (digest_uri,))
357
358 if self.authzid:
359 try:
360 authzid=self.authzid.encode(charset)
361 except UnicodeError:
362 self.__logger.debug("Couldn't encode authzid to %r" % (charset,))
363 return Failure("incompatible-charset")
364 authzid=_quote(authzid)
365 else:
366 authzid=""
367
368 if self.pformat=="md5:user:realm:pass":
369 urp_hash=self.password
370 else:
371 urp_hash=_make_urp_hash(username,realm,self.password)
372
373 response=_compute_response(urp_hash,nonce,cnonce,nonce_count,
374 authzid,digest_uri)
375 self.response_auth=_compute_response_auth(urp_hash,nonce,cnonce,
376 nonce_count,authzid,digest_uri)
377 params.append('response=%s' % (response,))
378 if authzid:
379 params.append('authzid="%s"' % (authzid,))
380 return Response(",".join(params))
381
383 """Choose a realm from the list specified by the server.
384
385 :Parameters:
386 - `realms`: the realm list.
387 - `charset`: encoding of realms on the list.
388 :Types:
389 - `realms`: `list` of `str`
390 - `charset`: `str`
391
392 :return: the realm chosen or a failure indicator.
393 :returntype: `str` or `Failure`"""
394 if realms:
395 realms=[unicode(r,charset) for r in realms]
396 realm=self.password_manager.choose_realm(realms)
397 else:
398 realm=self.password_manager.choose_realm([])
399 if realm:
400 if type(realm) is unicode:
401 try:
402 realm=realm.encode(charset)
403 except UnicodeError:
404 self.__logger.debug("Couldn't encode realm to %r" % (charset,))
405 return Failure("incompatible-charset")
406 elif charset!="utf-8":
407 try:
408 realm=unicode(realm,"utf-8").encode(charset)
409 except UnicodeError:
410 self.__logger.debug("Couldn't encode realm from utf-8 to %r"
411 % (charset,))
412 return Failure("incompatible-charset")
413 self.realm=realm
414 return realm
415
417 """Process the second challenge from the server and return the response.
418
419 :Parameters:
420 - `challenge`: the challenge from server.
421 :Types:
422 - `challenge`: `str`
423
424 :return: the response or a failure indicator.
425 :returntype: `sasl.Response` or `sasl.Failure`"""
426 if self.rspauth_checked:
427 return Failure("extra-challenge")
428 challenge=challenge.split('\x00')[0]
429 rspauth=None
430 while challenge:
431 m=_param_re.match(challenge)
432 if not m:
433 self.__logger.debug("Challenge syntax error: %r" % (challenge,))
434 return Failure("bad-challenge")
435 challenge=m.group("rest")
436 var=m.group("var")
437 val=m.group("val")
438 self.__logger.debug("%r: %r" % (var,val))
439 if var=="rspauth":
440 rspauth=val
441 if not rspauth:
442 self.__logger.debug("Final challenge without rspauth")
443 return Failure("bad-success")
444 if rspauth==self.response_auth:
445 self.rspauth_checked=1
446 return Response("")
447 else:
448 self.__logger.debug("Wrong rspauth value - peer is cheating?")
449 self.__logger.debug("my rspauth: %r" % (self.response_auth,))
450 return Failure("bad-success")
451
453 """Process success indicator from the server.
454
455 Process any addiitional data passed with the success.
456 Fail if the server was not authenticated.
457
458 :Parameters:
459 - `data`: an optional additional data with success.
460 :Types:
461 - `data`: `str`
462
463 :return: success or failure indicator.
464 :returntype: `sasl.Success` or `sasl.Failure`"""
465 if not self.response_auth:
466 self.__logger.debug("Got success too early")
467 return Failure("bad-success")
468 if self.rspauth_checked:
469 return Success(self.username,self.realm,self.authzid)
470 else:
471 r = self._final_challenge(data)
472 if isinstance(r, Failure):
473 return r
474 if self.rspauth_checked:
475 return Success(self.username,self.realm,self.authzid)
476 else:
477 self.__logger.debug("Something went wrong when processing additional data with success?")
478 return Failure("bad-success")
479
481 """Provides DIGEST-MD5 SASL authentication for a server."""
482
484 """Initialize a `DigestMD5ServerAuthenticator` object.
485
486 :Parameters:
487 - `password_manager`: name of the password manager object providing
488 authentication credential verification.
489 :Types:
490 - `password_manager`: `PasswordManager`"""
491 ServerAuthenticator.__init__(self,password_manager)
492 self.nonce=None
493 self.username=None
494 self.realm=None
495 self.authzid=None
496 self.done=None
497 self.last_nonce_count=None
498 self.__logger=logging.getLogger("pyxmpp.sasl.DigestMD5ServerAuthenticator")
499
500 - def start(self,response):
501 """Start the authentication process.
502
503 :Parameters:
504 - `response`: the initial response from the client (empty for
505 DIGEST-MD5).
506 :Types:
507 - `response`: `str`
508
509 :return: a challenge, a success indicator or a failure indicator.
510 :returntype: `sasl.Challenge`, `sasl.Success` or `sasl.Failure`"""
511 _unused = response
512 self.last_nonce_count=0
513 params=[]
514 realms=self.password_manager.get_realms()
515 if realms:
516 self.realm=_quote(realms[0])
517 for r in realms:
518 r=_quote(r)
519 params.append('realm="%s"' % (r,))
520 else:
521 self.realm=None
522 nonce=_quote(self.password_manager.generate_nonce())
523 self.nonce=nonce
524 params.append('nonce="%s"' % (nonce,))
525 params.append('qop="auth"')
526 params.append('charset=utf-8')
527 params.append('algorithm=md5-sess')
528 self.authzid=None
529 self.done=0
530 return Challenge(",".join(params))
531
533 """Process a client reponse.
534
535 :Parameters:
536 - `response`: the response from the client.
537 :Types:
538 - `response`: `str`
539
540 :return: a challenge, a success indicator or a failure indicator.
541 :returntype: `sasl.Challenge`, `sasl.Success` or `sasl.Failure`"""
542 if self.done:
543 return Success(self.username,self.realm,self.authzid)
544 if not response:
545 return Failure("not-authorized")
546 return self._parse_response(response)
547
549 """Parse a client reponse and pass to further processing.
550
551 :Parameters:
552 - `response`: the response from the client.
553 :Types:
554 - `response`: `str`
555
556 :return: a challenge, a success indicator or a failure indicator.
557 :returntype: `sasl.Challenge`, `sasl.Success` or `sasl.Failure`"""
558 response=response.split('\x00')[0]
559 if self.realm:
560 realm=to_utf8(self.realm)
561 realm=_quote(realm)
562 else:
563 realm=None
564 username=None
565 cnonce=None
566 digest_uri=None
567 response_val=None
568 authzid=None
569 nonce_count=None
570 while response:
571 m=_param_re.match(response)
572 if not m:
573 self.__logger.debug("Response syntax error: %r" % (response,))
574 return Failure("not-authorized")
575 response=m.group("rest")
576 var=m.group("var")
577 val=m.group("val")
578 self.__logger.debug("%r: %r" % (var,val))
579 if var=="realm":
580 realm=val[1:-1]
581 elif var=="cnonce":
582 if cnonce:
583 self.__logger.debug("Duplicate cnonce")
584 return Failure("not-authorized")
585 cnonce=val[1:-1]
586 elif var=="qop":
587 if val!='auth':
588 self.__logger.debug("qop other then 'auth'")
589 return Failure("not-authorized")
590 elif var=="digest-uri":
591 digest_uri=val[1:-1]
592 elif var=="authzid":
593 authzid=val[1:-1]
594 elif var=="username":
595 username=val[1:-1]
596 elif var=="response":
597 response_val=val
598 elif var=="nc":
599 nonce_count=val
600 self.last_nonce_count+=1
601 if int(nonce_count)!=self.last_nonce_count:
602 self.__logger.debug("bad nonce: %r != %r"
603 % (nonce_count,self.last_nonce_count))
604 return Failure("not-authorized")
605 return self._check_params(username,realm,cnonce,digest_uri,
606 response_val,authzid,nonce_count)
607
608 - def _check_params(self,username,realm,cnonce,digest_uri,
609 response_val,authzid,nonce_count):
610 """Check parameters of a client reponse and pass them to further
611 processing.
612
613 :Parameters:
614 - `username`: user name.
615 - `realm`: realm.
616 - `cnonce`: cnonce value.
617 - `digest_uri`: digest-uri value.
618 - `response_val`: response value computed by the client.
619 - `authzid`: authorization id.
620 - `nonce_count`: nonce count value.
621 :Types:
622 - `username`: `str`
623 - `realm`: `str`
624 - `cnonce`: `str`
625 - `digest_uri`: `str`
626 - `response_val`: `str`
627 - `authzid`: `str`
628 - `nonce_count`: `int`
629
630 :return: a challenge, a success indicator or a failure indicator.
631 :returntype: `sasl.Challenge`, `sasl.Success` or `sasl.Failure`"""
632 if not cnonce:
633 self.__logger.debug("Required 'cnonce' parameter not given")
634 return Failure("not-authorized")
635 if not response_val:
636 self.__logger.debug("Required 'response' parameter not given")
637 return Failure("not-authorized")
638 if not username:
639 self.__logger.debug("Required 'username' parameter not given")
640 return Failure("not-authorized")
641 if not digest_uri:
642 self.__logger.debug("Required 'digest_uri' parameter not given")
643 return Failure("not-authorized")
644 if not nonce_count:
645 self.__logger.debug("Required 'nc' parameter not given")
646 return Failure("not-authorized")
647 return self._make_final_challenge(username,realm,cnonce,digest_uri,
648 response_val,authzid,nonce_count)
649
652 """Send the second challenge in reply to the client response.
653
654 :Parameters:
655 - `username`: user name.
656 - `realm`: realm.
657 - `cnonce`: cnonce value.
658 - `digest_uri`: digest-uri value.
659 - `response_val`: response value computed by the client.
660 - `authzid`: authorization id.
661 - `nonce_count`: nonce count value.
662 :Types:
663 - `username`: `str`
664 - `realm`: `str`
665 - `cnonce`: `str`
666 - `digest_uri`: `str`
667 - `response_val`: `str`
668 - `authzid`: `str`
669 - `nonce_count`: `int`
670
671 :return: a challenge, a success indicator or a failure indicator.
672 :returntype: `sasl.Challenge`, `sasl.Success` or `sasl.Failure`"""
673 username_uq=from_utf8(username.replace('\\',''))
674 if authzid:
675 authzid_uq=from_utf8(authzid.replace('\\',''))
676 else:
677 authzid_uq=None
678 if realm:
679 realm_uq=from_utf8(realm.replace('\\',''))
680 else:
681 realm_uq=None
682 digest_uri_uq=digest_uri.replace('\\','')
683 self.username=username_uq
684 self.realm=realm_uq
685 password,pformat=self.password_manager.get_password(
686 username_uq,realm_uq,("plain","md5:user:realm:pass"))
687 if pformat=="md5:user:realm:pass":
688 urp_hash=password
689 elif pformat=="plain":
690 urp_hash=_make_urp_hash(username,realm,password)
691 else:
692 self.__logger.debug("Couldn't get password.")
693 return Failure("not-authorized")
694 valid_response=_compute_response(urp_hash,self.nonce,cnonce,
695 nonce_count,authzid,digest_uri)
696 if response_val!=valid_response:
697 self.__logger.debug("Response mismatch: %r != %r" % (response_val,valid_response))
698 return Failure("not-authorized")
699 s=digest_uri_uq.split("/")
700 if len(s)==3:
701 serv_type,host,serv_name=s
702 elif len(s)==2:
703 serv_type,host=s
704 serv_name=None
705 else:
706 self.__logger.debug("Bad digest_uri: %r" % (digest_uri_uq,))
707 return Failure("not-authorized")
708 info={}
709 info["mechanism"]="DIGEST-MD5"
710 info["username"]=username_uq
711 info["serv-type"]=serv_type
712 info["host"]=host
713 info["serv-name"]=serv_name
714 if self.password_manager.check_authzid(authzid_uq,info):
715 rspauth=_compute_response_auth(urp_hash,self.nonce,
716 cnonce,nonce_count,authzid,digest_uri)
717 self.authzid=authzid
718 self.done=1
719 return Challenge("rspauth="+rspauth)
720 else:
721 self.__logger.debug("Authzid check failed")
722 return Failure("invalid_authzid")
723
724
725