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