1#!/usr/bin/env python 2# -*- coding: utf-8 -*- 3# 4import calendar 5import logging 6import six 7 8from saml2.samlp import STATUS_VERSION_MISMATCH 9from saml2.samlp import STATUS_AUTHN_FAILED 10from saml2.samlp import STATUS_INVALID_ATTR_NAME_OR_VALUE 11from saml2.samlp import STATUS_INVALID_NAMEID_POLICY 12from saml2.samlp import STATUS_NO_AUTHN_CONTEXT 13from saml2.samlp import STATUS_NO_AVAILABLE_IDP 14from saml2.samlp import STATUS_NO_PASSIVE 15from saml2.samlp import STATUS_NO_SUPPORTED_IDP 16from saml2.samlp import STATUS_PARTIAL_LOGOUT 17from saml2.samlp import STATUS_PROXY_COUNT_EXCEEDED 18from saml2.samlp import STATUS_REQUEST_DENIED 19from saml2.samlp import STATUS_REQUEST_UNSUPPORTED 20from saml2.samlp import STATUS_REQUEST_VERSION_DEPRECATED 21from saml2.samlp import STATUS_REQUEST_VERSION_TOO_HIGH 22from saml2.samlp import STATUS_REQUEST_VERSION_TOO_LOW 23from saml2.samlp import STATUS_RESOURCE_NOT_RECOGNIZED 24from saml2.samlp import STATUS_TOO_MANY_RESPONSES 25from saml2.samlp import STATUS_UNKNOWN_ATTR_PROFILE 26from saml2.samlp import STATUS_UNKNOWN_PRINCIPAL 27from saml2.samlp import STATUS_UNSUPPORTED_BINDING 28from saml2.samlp import STATUS_RESPONDER 29 30from saml2 import xmldsig as ds 31from saml2 import xmlenc as xenc 32 33from saml2 import samlp 34from saml2 import class_name 35from saml2 import saml 36from saml2 import extension_elements_to_elements 37from saml2 import SAMLError 38from saml2 import time_util 39 40from saml2.s_utils import RequestVersionTooLow 41from saml2.s_utils import RequestVersionTooHigh 42from saml2.saml import attribute_from_string, XSI_TYPE 43from saml2.saml import SCM_BEARER 44from saml2.saml import SCM_HOLDER_OF_KEY 45from saml2.saml import SCM_SENDER_VOUCHES 46from saml2.saml import encrypted_attribute_from_string 47from saml2.sigver import security_context 48from saml2.sigver import DecryptError 49from saml2.sigver import SignatureError 50from saml2.sigver import signed 51from saml2.attribute_converter import to_local 52from saml2.time_util import str_to_time, later_than 53 54from saml2.validate import validate_on_or_after 55from saml2.validate import validate_before 56from saml2.validate import valid_instance 57from saml2.validate import valid_address 58from saml2.validate import NotValid 59 60logger = logging.getLogger(__name__) 61 62 63# --------------------------------------------------------------------------- 64 65 66class IncorrectlySigned(SAMLError): 67 pass 68 69 70class DecryptionFailed(SAMLError): 71 pass 72 73 74class VerificationError(SAMLError): 75 pass 76 77 78class StatusError(SAMLError): 79 pass 80 81 82class UnsolicitedResponse(SAMLError): 83 pass 84 85 86class StatusVersionMismatch(StatusError): 87 pass 88 89 90class StatusAuthnFailed(StatusError): 91 pass 92 93 94class StatusInvalidAttrNameOrValue(StatusError): 95 pass 96 97 98class StatusInvalidNameidPolicy(StatusError): 99 pass 100 101 102class StatusNoAuthnContext(StatusError): 103 pass 104 105 106class StatusNoAvailableIdp(StatusError): 107 pass 108 109 110class StatusNoPassive(StatusError): 111 pass 112 113 114class StatusNoSupportedIdp(StatusError): 115 pass 116 117 118class StatusPartialLogout(StatusError): 119 pass 120 121 122class StatusProxyCountExceeded(StatusError): 123 pass 124 125 126class StatusRequestDenied(StatusError): 127 pass 128 129 130class StatusRequestUnsupported(StatusError): 131 pass 132 133 134class StatusRequestVersionDeprecated(StatusError): 135 pass 136 137 138class StatusRequestVersionTooHigh(StatusError): 139 pass 140 141 142class StatusRequestVersionTooLow(StatusError): 143 pass 144 145 146class StatusResourceNotRecognized(StatusError): 147 pass 148 149 150class StatusTooManyResponses(StatusError): 151 pass 152 153 154class StatusUnknownAttrProfile(StatusError): 155 pass 156 157 158class StatusUnknownPrincipal(StatusError): 159 pass 160 161 162class StatusUnsupportedBinding(StatusError): 163 pass 164 165 166class StatusResponder(StatusError): 167 pass 168 169 170STATUSCODE2EXCEPTION = { 171 STATUS_VERSION_MISMATCH: StatusVersionMismatch, 172 STATUS_AUTHN_FAILED: StatusAuthnFailed, 173 STATUS_INVALID_ATTR_NAME_OR_VALUE: StatusInvalidAttrNameOrValue, 174 STATUS_INVALID_NAMEID_POLICY: StatusInvalidNameidPolicy, 175 STATUS_NO_AUTHN_CONTEXT: StatusNoAuthnContext, 176 STATUS_NO_AVAILABLE_IDP: StatusNoAvailableIdp, 177 STATUS_NO_PASSIVE: StatusNoPassive, 178 STATUS_NO_SUPPORTED_IDP: StatusNoSupportedIdp, 179 STATUS_PARTIAL_LOGOUT: StatusPartialLogout, 180 STATUS_PROXY_COUNT_EXCEEDED: StatusProxyCountExceeded, 181 STATUS_REQUEST_DENIED: StatusRequestDenied, 182 STATUS_REQUEST_UNSUPPORTED: StatusRequestUnsupported, 183 STATUS_REQUEST_VERSION_DEPRECATED: StatusRequestVersionDeprecated, 184 STATUS_REQUEST_VERSION_TOO_HIGH: StatusRequestVersionTooHigh, 185 STATUS_REQUEST_VERSION_TOO_LOW: StatusRequestVersionTooLow, 186 STATUS_RESOURCE_NOT_RECOGNIZED: StatusResourceNotRecognized, 187 STATUS_TOO_MANY_RESPONSES: StatusTooManyResponses, 188 STATUS_UNKNOWN_ATTR_PROFILE: StatusUnknownAttrProfile, 189 STATUS_UNKNOWN_PRINCIPAL: StatusUnknownPrincipal, 190 STATUS_UNSUPPORTED_BINDING: StatusUnsupportedBinding, 191 STATUS_RESPONDER: StatusResponder, 192} 193 194 195# --------------------------------------------------------------------------- 196 197 198def _dummy(_): 199 return None 200 201 202def for_me(conditions, myself): 203 """ Am I among the intended audiences """ 204 205 if not conditions.audience_restriction: # No audience restriction 206 return True 207 208 for restriction in conditions.audience_restriction: 209 if not restriction.audience: 210 continue 211 for audience in restriction.audience: 212 if audience.text.strip() == myself: 213 return True 214 else: 215 # print("Not for me: %s != %s" % (audience.text.strip(), 216 # myself)) 217 pass 218 219 return False 220 221 222def authn_response(conf, return_addrs, outstanding_queries=None, timeslack=0, 223 asynchop=True, allow_unsolicited=False, 224 want_assertions_signed=False, conv_info=None): 225 sec = security_context(conf) 226 if not timeslack: 227 try: 228 timeslack = int(conf.accepted_time_diff) 229 except TypeError: 230 timeslack = 0 231 232 return AuthnResponse(sec, conf.attribute_converters, conf.entityid, 233 return_addrs, outstanding_queries, timeslack, 234 asynchop=asynchop, allow_unsolicited=allow_unsolicited, 235 want_assertions_signed=want_assertions_signed, 236 conv_info=conv_info) 237 238 239# comes in over SOAP so synchronous 240def attribute_response(conf, return_addrs, timeslack=0, asynchop=False, 241 test=False, conv_info=None): 242 sec = security_context(conf) 243 if not timeslack: 244 try: 245 timeslack = int(conf.accepted_time_diff) 246 except TypeError: 247 timeslack = 0 248 249 return AttributeResponse(sec, conf.attribute_converters, conf.entityid, 250 return_addrs, timeslack, asynchop=asynchop, 251 test=test, conv_info=conv_info) 252 253 254class StatusResponse(object): 255 msgtype = "status_response" 256 257 def __init__(self, sec_context, return_addrs=None, timeslack=0, 258 request_id=0, asynchop=True, conv_info=None): 259 self.sec = sec_context 260 self.return_addrs = return_addrs 261 262 self.timeslack = timeslack 263 self.request_id = request_id 264 265 self.xmlstr = "" 266 self.origxml = "" 267 self.name_id = None 268 self.response = None 269 self.not_on_or_after = 0 270 self.in_response_to = None 271 self.signature_check = self.sec.correctly_signed_response 272 self.require_signature = False 273 self.require_response_signature = False 274 self.require_signature_or_response_signature = False 275 self.not_signed = False 276 self.asynchop = asynchop 277 self.do_not_verify = False 278 self.conv_info = conv_info or {} 279 280 def _clear(self): 281 self.xmlstr = "" 282 self.name_id = None 283 self.response = None 284 self.not_on_or_after = 0 285 286 def _postamble(self): 287 if not self.response: 288 logger.error("Response was not correctly signed") 289 if self.xmlstr: 290 logger.info("Response: %s", self.xmlstr) 291 raise IncorrectlySigned() 292 293 logger.debug("response: %s", self.response) 294 295 try: 296 valid_instance(self.response) 297 except NotValid as exc: 298 logger.error("Not valid response: %s", exc.args[0]) 299 self._clear() 300 return self 301 302 self.in_response_to = self.response.in_response_to 303 return self 304 305 def load_instance(self, instance): 306 if signed(instance): 307 # This will check signature on Assertion which is the default 308 try: 309 self.response = self.sec.check_signature(instance) 310 except SignatureError: 311 # The response as a whole might be signed or not 312 self.response = self.sec.check_signature( 313 instance, samlp.NAMESPACE + ":Response") 314 else: 315 self.not_signed = True 316 self.response = instance 317 318 return self._postamble() 319 320 def _loads(self, xmldata, decode=True, origxml=None): 321 322 # own copy 323 if isinstance(xmldata, six.binary_type): 324 self.xmlstr = xmldata[:].decode('utf-8') 325 else: 326 self.xmlstr = xmldata[:] 327 logger.debug("xmlstr: %s", self.xmlstr) 328 if origxml: 329 self.origxml = origxml 330 else: 331 self.origxml = self.xmlstr 332 333 if self.do_not_verify: 334 args = {"do_not_verify": True} 335 else: 336 args = {} 337 338 try: 339 self.response = self.signature_check( 340 xmldata, origdoc=origxml, must=self.require_signature, 341 require_response_signature=self.require_response_signature, 342 **args) 343 344 except TypeError: 345 raise 346 except SignatureError: 347 raise 348 except Exception as excp: 349 logger.exception("EXCEPTION: %s", excp) 350 raise 351 352 # print("<", self.response) 353 354 return self._postamble() 355 356 def status_ok(self): 357 status = self.response.status 358 logger.info("status: %s", status) 359 360 if not status or status.status_code.value == samlp.STATUS_SUCCESS: 361 return True 362 363 err_code = ( 364 status.status_code.status_code.value 365 if status.status_code.status_code 366 else None 367 ) 368 err_msg = ( 369 status.status_message.text 370 if status.status_message 371 else err_code or "Unknown error" 372 ) 373 err_cls = STATUSCODE2EXCEPTION.get(err_code, StatusError) 374 375 msg = "Unsuccessful operation: {status}\n{msg} from {code}".format( 376 status=status, msg=err_msg, code=err_code 377 ) 378 logger.info(msg) 379 raise err_cls(msg) 380 381 def issue_instant_ok(self): 382 """ Check that the response was issued at a reasonable time """ 383 upper = time_util.shift_time(time_util.time_in_a_while(days=1), 384 self.timeslack).timetuple() 385 lower = time_util.shift_time(time_util.time_a_while_ago(days=1), 386 -self.timeslack).timetuple() 387 # print("issue_instant: %s" % self.response.issue_instant) 388 # print("%s < x < %s" % (lower, upper)) 389 issued_at = str_to_time(self.response.issue_instant) 390 return lower < issued_at < upper 391 392 def _verify(self): 393 if self.request_id and self.in_response_to and \ 394 self.in_response_to != self.request_id: 395 logger.error("Not the id I expected: %s != %s", 396 self.in_response_to, self.request_id) 397 return None 398 399 try: 400 assert self.response.version == "2.0" 401 except AssertionError: 402 _ver = float(self.response.version) 403 if _ver < 2.0: 404 raise RequestVersionTooLow() 405 else: 406 raise RequestVersionTooHigh() 407 408 if self.asynchop: 409 if self.response.destination and \ 410 self.response.destination not in self.return_addrs: 411 logger.error("%s not in %s", self.response.destination, 412 self.return_addrs) 413 return None 414 415 assert self.issue_instant_ok() 416 assert self.status_ok() 417 return self 418 419 def loads(self, xmldata, decode=True, origxml=None): 420 return self._loads(xmldata, decode, origxml) 421 422 def verify(self, keys=None): 423 try: 424 return self._verify() 425 except AssertionError: 426 logger.exception("verify") 427 return None 428 429 def update(self, mold): 430 self.xmlstr = mold.xmlstr 431 self.in_response_to = mold.in_response_to 432 self.response = mold.response 433 434 def issuer(self): 435 return self.response.issuer.text.strip() 436 437 438class LogoutResponse(StatusResponse): 439 msgtype = "logout_response" 440 441 def __init__(self, sec_context, return_addrs=None, timeslack=0, 442 asynchop=True, conv_info=None): 443 StatusResponse.__init__(self, sec_context, return_addrs, timeslack, 444 asynchop=asynchop, conv_info=conv_info) 445 self.signature_check = self.sec.correctly_signed_logout_response 446 447 448class NameIDMappingResponse(StatusResponse): 449 msgtype = "name_id_mapping_response" 450 451 def __init__(self, sec_context, return_addrs=None, timeslack=0, 452 request_id=0, asynchop=True, conv_info=None): 453 StatusResponse.__init__(self, sec_context, return_addrs, timeslack, 454 request_id, asynchop, conv_info=conv_info) 455 self.signature_check = self.sec \ 456 .correctly_signed_name_id_mapping_response 457 458 459class ManageNameIDResponse(StatusResponse): 460 msgtype = "manage_name_id_response" 461 462 def __init__(self, sec_context, return_addrs=None, timeslack=0, 463 request_id=0, asynchop=True, conv_info=None): 464 StatusResponse.__init__(self, sec_context, return_addrs, timeslack, 465 request_id, asynchop, conv_info=conv_info) 466 self.signature_check = self.sec.correctly_signed_manage_name_id_response 467 468 469# ---------------------------------------------------------------------------- 470 471class AuthnResponse(StatusResponse): 472 """ This is where all the profile compliance is checked. 473 This one does saml2int compliance. """ 474 msgtype = "authn_response" 475 476 def __init__(self, sec_context, attribute_converters, entity_id, 477 return_addrs=None, outstanding_queries=None, 478 timeslack=0, asynchop=True, allow_unsolicited=False, 479 test=False, allow_unknown_attributes=False, 480 want_assertions_signed=False, 481 want_assertions_or_response_signed=False, 482 want_response_signed=False, 483 conv_info=None, **kwargs): 484 485 StatusResponse.__init__(self, sec_context, return_addrs, timeslack, 486 asynchop=asynchop, conv_info=conv_info) 487 self.entity_id = entity_id 488 self.attribute_converters = attribute_converters 489 if outstanding_queries: 490 self.outstanding_queries = outstanding_queries 491 else: 492 self.outstanding_queries = {} 493 self.context = "AuthnReq" 494 self.came_from = None 495 self.ava = None 496 self.assertion = None 497 self.assertions = [] 498 self.session_not_on_or_after = 0 499 self.allow_unsolicited = allow_unsolicited 500 self.require_signature = want_assertions_signed 501 self.require_signature_or_response_signature = want_assertions_or_response_signed 502 self.require_response_signature = want_response_signed 503 self.test = test 504 self.allow_unknown_attributes = allow_unknown_attributes 505 # 506 try: 507 self.extension_schema = kwargs["extension_schema"] 508 except KeyError: 509 self.extension_schema = {} 510 511 def check_subject_confirmation_in_response_to(self, irp): 512 for assertion in self.response.assertion: 513 for _sc in assertion.subject.subject_confirmation: 514 try: 515 assert _sc.subject_confirmation_data.in_response_to == irp 516 except AssertionError: 517 return False 518 519 return True 520 521 def loads(self, xmldata, decode=True, origxml=None): 522 self._loads(xmldata, decode, origxml) 523 524 if self.asynchop: 525 if self.in_response_to in self.outstanding_queries: 526 self.came_from = self.outstanding_queries[self.in_response_to] 527 # del self.outstanding_queries[self.in_response_to] 528 try: 529 if not self.check_subject_confirmation_in_response_to( 530 self.in_response_to): 531 logger.exception( 532 "Unsolicited response %s" % self.in_response_to) 533 raise UnsolicitedResponse( 534 "Unsolicited response: %s" % self.in_response_to) 535 except AttributeError: 536 pass 537 elif self.allow_unsolicited: 538 # Should check that I haven't seen this before 539 pass 540 else: 541 logger.exception( 542 "Unsolicited response %s" % self.in_response_to) 543 raise UnsolicitedResponse( 544 "Unsolicited response: %s" % self.in_response_to) 545 546 return self 547 548 def clear(self): 549 self._clear() 550 self.came_from = None 551 self.ava = None 552 self.assertion = None 553 554 def authn_statement_ok(self, optional=False): 555 try: 556 # the assertion MUST contain one AuthNStatement 557 assert len(self.assertion.authn_statement) == 1 558 except AssertionError: 559 if optional: 560 return True 561 else: 562 logger.error("No AuthnStatement") 563 raise 564 565 authn_statement = self.assertion.authn_statement[0] 566 if authn_statement.session_not_on_or_after: 567 if validate_on_or_after(authn_statement.session_not_on_or_after, 568 self.timeslack): 569 self.session_not_on_or_after = calendar.timegm( 570 time_util.str_to_time( 571 authn_statement.session_not_on_or_after)) 572 else: 573 return False 574 return True 575 # check authn_statement.session_index 576 577 def condition_ok(self, lax=False): 578 if not self.assertion.conditions: 579 # Conditions is Optional for Assertion, so, if it's absent, then we 580 # assume that its valid 581 return True 582 583 if self.test: 584 lax = True 585 586 conditions = self.assertion.conditions 587 588 logger.debug("conditions: %s", conditions) 589 590 # if no sub-elements or elements are supplied, then the 591 # assertion is considered to be valid. 592 if not conditions.keyswv(): 593 return True 594 595 # if both are present NotBefore must be earlier than NotOnOrAfter 596 if conditions.not_before and conditions.not_on_or_after: 597 if not later_than(conditions.not_on_or_after, 598 conditions.not_before): 599 return False 600 601 try: 602 if conditions.not_on_or_after: 603 self.not_on_or_after = validate_on_or_after( 604 conditions.not_on_or_after, self.timeslack) 605 if conditions.not_before: 606 validate_before(conditions.not_before, self.timeslack) 607 except Exception as excp: 608 logger.error("Exception on conditions: %s", excp) 609 if not lax: 610 raise 611 else: 612 self.not_on_or_after = 0 613 614 if not for_me(conditions, self.entity_id): 615 if not lax: 616 raise Exception("Not for me!!!") 617 618 if conditions.condition: # extra conditions 619 for cond in conditions.condition: 620 try: 621 if cond.extension_attributes[ 622 XSI_TYPE] in self.extension_schema: 623 pass 624 else: 625 raise Exception("Unknown condition") 626 except KeyError: 627 raise Exception("Missing xsi:type specification") 628 629 return True 630 631 def decrypt_attributes(self, attribute_statement): 632 """ 633 Decrypts possible encrypted attributes and adds the decrypts to the 634 list of attributes. 635 636 :param attribute_statement: A SAML.AttributeStatement which might 637 contain both encrypted attributes and attributes. 638 """ 639 # _node_name = [ 640 # "urn:oasis:names:tc:SAML:2.0:assertion:EncryptedData", 641 # "urn:oasis:names:tc:SAML:2.0:assertion:EncryptedAttribute"] 642 643 for encattr in attribute_statement.encrypted_attribute: 644 if not encattr.encrypted_key: 645 _decr = self.sec.decrypt(encattr.encrypted_data) 646 _attr = attribute_from_string(_decr) 647 attribute_statement.attribute.append(_attr) 648 else: 649 _decr = self.sec.decrypt(encattr) 650 enc_attr = encrypted_attribute_from_string(_decr) 651 attrlist = enc_attr.extensions_as_elements("Attribute", saml) 652 attribute_statement.attribute.extend(attrlist) 653 654 def read_attribute_statement(self, attr_statem): 655 logger.debug("Attribute Statement: %s", attr_statem) 656 # for aconv in self.attribute_converters: 657 # logger.debug("Converts name format: %s", aconv.name_format) 658 659 self.decrypt_attributes(attr_statem) 660 return to_local(self.attribute_converters, attr_statem, 661 self.allow_unknown_attributes) 662 663 def get_identity(self): 664 """ The assertion can contain zero or more attributeStatements 665 666 """ 667 ava = {} 668 for _assertion in self.assertions: 669 if _assertion.advice: 670 if _assertion.advice.assertion: 671 for tmp_assertion in _assertion.advice.assertion: 672 if tmp_assertion.attribute_statement: 673 assert len(tmp_assertion.attribute_statement) == 1 674 ava.update(self.read_attribute_statement( 675 tmp_assertion.attribute_statement[0])) 676 if _assertion.attribute_statement: 677 logger.debug("Assertion contains %s attribute statement(s)", 678 (len(self.assertion.attribute_statement))) 679 for _attr_statem in _assertion.attribute_statement: 680 logger.debug("Attribute Statement: %s" % (_attr_statem,)) 681 ava.update(self.read_attribute_statement(_attr_statem)) 682 if not ava: 683 logger.debug("Assertion contains no attribute statements") 684 return ava 685 686 def _bearer_confirmed(self, data): 687 if not data: 688 return False 689 690 if data.address: 691 if not valid_address(data.address): 692 return False 693 # verify that I got it from the correct sender 694 695 # These two will raise exception if untrue 696 validate_on_or_after(data.not_on_or_after, self.timeslack) 697 validate_before(data.not_before, self.timeslack) 698 699 # not_before must be < not_on_or_after 700 if not later_than(data.not_on_or_after, data.not_before): 701 return False 702 703 if self.asynchop and self.came_from is None: 704 if data.in_response_to: 705 if data.in_response_to in self.outstanding_queries: 706 self.came_from = self.outstanding_queries[ 707 data.in_response_to] 708 # del self.outstanding_queries[data.in_response_to] 709 elif self.allow_unsolicited: 710 pass 711 else: 712 # This is where I don't allow unsolicited reponses 713 # Either in_response_to == None or has a value I don't 714 # recognize 715 logger.debug("in response to: '%s'", data.in_response_to) 716 logger.info("outstanding queries: %s", 717 self.outstanding_queries.keys()) 718 raise Exception( 719 "Combination of session id and requestURI I don't " 720 "recall") 721 return True 722 723 def _holder_of_key_confirmed(self, data): 724 if not data or not data.extension_elements: 725 return False 726 727 has_keyinfo = False 728 for element in extension_elements_to_elements(data.extension_elements, 729 [samlp, saml, xenc, ds]): 730 if isinstance(element, ds.KeyInfo): 731 has_keyinfo = True 732 733 return has_keyinfo 734 735 def get_subject(self): 736 """ The assertion must contain a Subject 737 """ 738 assert self.assertion.subject 739 subject = self.assertion.subject 740 subjconf = [] 741 742 if not self.verify_attesting_entity(subject.subject_confirmation): 743 raise VerificationError("No valid attesting address") 744 745 for subject_confirmation in subject.subject_confirmation: 746 _data = subject_confirmation.subject_confirmation_data 747 748 if subject_confirmation.method == SCM_BEARER: 749 if not self._bearer_confirmed(_data): 750 continue 751 elif subject_confirmation.method == SCM_HOLDER_OF_KEY: 752 if not self._holder_of_key_confirmed(_data): 753 continue 754 elif subject_confirmation.method == SCM_SENDER_VOUCHES: 755 pass 756 else: 757 raise ValueError("Unknown subject confirmation method: %s" % ( 758 subject_confirmation.method,)) 759 760 _recip = _data.recipient 761 if not _recip or not self.verify_recipient(_recip): 762 raise VerificationError("No valid recipient") 763 764 subjconf.append(subject_confirmation) 765 766 if not subjconf: 767 raise VerificationError("No valid subject confirmation") 768 769 subject.subject_confirmation = subjconf 770 771 # The subject may contain a name_id 772 773 if subject.name_id: 774 self.name_id = subject.name_id 775 elif subject.encrypted_id: 776 # decrypt encrypted ID 777 _name_id_str = self.sec.decrypt( 778 subject.encrypted_id.encrypted_data.to_string()) 779 _name_id = saml.name_id_from_string(_name_id_str) 780 self.name_id = _name_id 781 782 logger.info("Subject NameID: %s", self.name_id) 783 return self.name_id 784 785 def _assertion(self, assertion, verified=False): 786 """ 787 Check the assertion 788 :param assertion: 789 :return: True/False depending on if the assertion is sane or not 790 """ 791 792 if not hasattr(assertion, 'signature') or not assertion.signature: 793 logger.debug("unsigned") 794 if self.require_signature: 795 raise SignatureError("Signature missing for assertion") 796 else: 797 logger.debug("signed") 798 if not verified and self.do_not_verify is False: 799 try: 800 self.sec.check_signature(assertion, class_name(assertion), 801 self.xmlstr) 802 except Exception as exc: 803 logger.error("correctly_signed_response: %s", exc) 804 raise 805 806 self.assertion = assertion 807 logger.debug("assertion context: %s", self.context) 808 logger.debug("assertion keys: %s", assertion.keyswv()) 809 logger.debug("outstanding_queries: %s", self.outstanding_queries) 810 811 # if self.context == "AuthnReq" or self.context == "AttrQuery": 812 if self.context == "AuthnReq": 813 self.authn_statement_ok() 814 # elif self.context == "AttrQuery": 815 # self.authn_statement_ok(True) 816 817 if not self.condition_ok(): 818 raise VerificationError("Condition not OK") 819 820 logger.debug("--- Getting Identity ---") 821 822 # if self.context == "AuthnReq" or self.context == "AttrQuery": 823 # self.ava = self.get_identity() 824 # logger.debug("--- AVA: %s", self.ava) 825 826 try: 827 self.get_subject() 828 if self.asynchop: 829 if self.allow_unsolicited: 830 pass 831 elif self.came_from is None: 832 raise VerificationError("Came from") 833 return True 834 except Exception: 835 logger.exception("get subject") 836 raise 837 838 def decrypt_assertions(self, encrypted_assertions, decr_txt, issuer=None, 839 verified=False): 840 """ Moves the decrypted assertion from the encrypted assertion to a 841 list. 842 843 :param encrypted_assertions: A list of encrypted assertions. 844 :param decr_txt: The string representation containing the decrypted 845 data. Used when verifying signatures. 846 :param issuer: The issuer of the response. 847 :param verified: If True do not verify signatures, otherwise verify 848 the signature if it exists. 849 :return: A list of decrypted assertions. 850 """ 851 res = [] 852 for encrypted_assertion in encrypted_assertions: 853 if encrypted_assertion.extension_elements: 854 assertions = extension_elements_to_elements( 855 encrypted_assertion.extension_elements, [saml, samlp]) 856 for assertion in assertions: 857 if assertion.signature and not verified: 858 if not self.sec.check_signature( 859 assertion, origdoc=decr_txt, 860 node_name=class_name(assertion), issuer=issuer): 861 logger.error("Failed to verify signature on '%s'", 862 assertion) 863 raise SignatureError() 864 res.append(assertion) 865 return res 866 867 def find_encrypt_data_assertion(self, enc_assertions): 868 """ Verifies if a list of encrypted assertions contains encrypted data. 869 870 :param enc_assertions: A list of encrypted assertions. 871 :return: True encrypted data exists otherwise false. 872 """ 873 for _assertion in enc_assertions: 874 if _assertion.encrypted_data is not None: 875 return True 876 877 def find_encrypt_data_assertion_list(self, _assertions): 878 """ Verifies if a list of assertions contains encrypted data in the 879 advice element. 880 881 :param _assertions: A list of assertions. 882 :return: True encrypted data exists otherwise false. 883 """ 884 for _assertion in _assertions: 885 if _assertion.advice: 886 if _assertion.advice.encrypted_assertion: 887 res = self.find_encrypt_data_assertion( 888 _assertion.advice.encrypted_assertion) 889 if res: 890 return True 891 892 def find_encrypt_data(self, resp): 893 """ Verifies if a saml response contains encrypted assertions with 894 encrypted data. 895 896 :param resp: A saml response. 897 :return: True encrypted data exists otherwise false. 898 """ 899 if resp.encrypted_assertion: 900 res = self.find_encrypt_data_assertion(resp.encrypted_assertion) 901 if res: 902 return True 903 if resp.assertion: 904 for tmp_assertion in resp.assertion: 905 if tmp_assertion.advice: 906 if tmp_assertion.advice.encrypted_assertion: 907 res = self.find_encrypt_data_assertion( 908 tmp_assertion.advice.encrypted_assertion) 909 if res: 910 return True 911 return False 912 913 def parse_assertion(self, keys=None): 914 """ Parse the assertions for a saml response. 915 916 :param keys: A string representing a RSA key or a list of strings 917 containing RSA keys. 918 :return: True if the assertions are parsed otherwise False. 919 """ 920 if self.context == "AuthnQuery": 921 # can contain one or more assertions 922 pass 923 else: 924 # This is a saml2int limitation 925 try: 926 assert ( 927 len(self.response.assertion) == 1 928 or len(self.response.encrypted_assertion) == 1 929 or self.assertion is not None 930 ) 931 except AssertionError: 932 raise Exception("No assertion part") 933 934 if self.response.assertion: 935 logger.debug("***Unencrypted assertion***") 936 for assertion in self.response.assertion: 937 if not self._assertion(assertion, False): 938 return False 939 940 if self.find_encrypt_data(self.response): 941 logger.debug("***Encrypted assertion/-s***") 942 _enc_assertions = [] 943 resp = self.response 944 decr_text = str(self.response) 945 946 decr_text_old = None 947 while self.find_encrypt_data(resp) and decr_text_old != decr_text: 948 decr_text_old = decr_text 949 try: 950 decr_text = self.sec.decrypt_keys(decr_text, keys) 951 except DecryptError as e: 952 continue 953 else: 954 resp = samlp.response_from_string(decr_text) 955 # check and prepare for comparison between str and unicode 956 if type(decr_text_old) != type(decr_text): 957 if isinstance(decr_text_old, six.binary_type): 958 decr_text_old = decr_text_old.decode("utf-8") 959 else: 960 decr_text_old = decr_text_old.encode("utf-8") 961 962 _enc_assertions = self.decrypt_assertions( 963 resp.encrypted_assertion, decr_text 964 ) 965 966 decr_text_old = None 967 while ( 968 self.find_encrypt_data(resp) 969 or self.find_encrypt_data_assertion_list(_enc_assertions) 970 ) and decr_text_old != decr_text: 971 decr_text_old = decr_text 972 try: 973 decr_text = self.sec.decrypt_keys(decr_text, keys) 974 except DecryptError as e: 975 continue 976 else: 977 resp = samlp.response_from_string(decr_text) 978 _enc_assertions = self.decrypt_assertions( 979 resp.encrypted_assertion, decr_text, verified=True 980 ) 981 # check and prepare for comparison between str and unicode 982 if type(decr_text_old) != type(decr_text): 983 if isinstance(decr_text_old, six.binary_type): 984 decr_text_old = decr_text_old.decode("utf-8") 985 else: 986 decr_text_old = decr_text_old.encode("utf-8") 987 988 all_assertions = _enc_assertions 989 if resp.assertion: 990 all_assertions = all_assertions + resp.assertion 991 992 if len(all_assertions) > 0: 993 for tmp_ass in all_assertions: 994 if tmp_ass.advice and tmp_ass.advice.encrypted_assertion: 995 996 advice_res = self.decrypt_assertions( 997 tmp_ass.advice.encrypted_assertion, 998 decr_text, 999 tmp_ass.issuer) 1000 if tmp_ass.advice.assertion: 1001 tmp_ass.advice.assertion.extend(advice_res) 1002 else: 1003 tmp_ass.advice.assertion = advice_res 1004 if len(advice_res) > 0: 1005 tmp_ass.advice.encrypted_assertion = [] 1006 1007 self.response.assertion = resp.assertion 1008 for assertion in _enc_assertions: 1009 if not self._assertion(assertion, True): 1010 return False 1011 else: 1012 self.assertions.append(assertion) 1013 1014 self.xmlstr = decr_text 1015 if len(_enc_assertions) > 0: 1016 self.response.encrypted_assertion = [] 1017 1018 if self.response.assertion: 1019 for assertion in self.response.assertion: 1020 self.assertions.append(assertion) 1021 1022 if self.assertions and len(self.assertions) > 0: 1023 self.assertion = self.assertions[0] 1024 1025 if self.context == "AuthnReq" or self.context == "AttrQuery": 1026 self.ava = self.get_identity() 1027 logger.debug("--- AVA: %s", self.ava) 1028 1029 return True 1030 1031 def verify(self, keys=None): 1032 """ Verify that the assertion is syntactically correct and the 1033 signature is correct if present. 1034 1035 :param keys: If not the default key file should be used then use one 1036 of these. 1037 """ 1038 1039 try: 1040 res = self._verify() 1041 except AssertionError as err: 1042 logger.error("Verification error on the response: %s", err) 1043 raise 1044 else: 1045 if res is None: 1046 return None 1047 1048 if not isinstance(self.response, samlp.Response): 1049 return self 1050 1051 if self.parse_assertion(keys): 1052 return self 1053 else: 1054 logger.error("Could not parse the assertion") 1055 return None 1056 1057 def session_id(self): 1058 """ Returns the SessionID of the response """ 1059 return self.response.in_response_to 1060 1061 def id(self): 1062 """ Return the ID of the response """ 1063 return self.response.id 1064 1065 def authn_info(self): 1066 res = [] 1067 for astat in self.assertion.authn_statement: 1068 context = astat.authn_context 1069 try: 1070 authn_instant = astat.authn_instant 1071 except AttributeError: 1072 authn_instant = "" 1073 if context: 1074 try: 1075 aclass = context.authn_context_class_ref.text 1076 except AttributeError: 1077 aclass = "" 1078 try: 1079 authn_auth = [a.text for a in 1080 context.authenticating_authority] 1081 except AttributeError: 1082 authn_auth = [] 1083 res.append((aclass, authn_auth, authn_instant)) 1084 return res 1085 1086 def authz_decision_info(self): 1087 res = {"permit": [], "deny": [], "indeterminate": []} 1088 for adstat in self.assertion.authz_decision_statement: 1089 # one of 'Permit', 'Deny', 'Indeterminate' 1090 res[adstat.decision.text.lower()] = adstat 1091 return res 1092 1093 def session_info(self): 1094 """ Returns a predefined set of information gleened from the 1095 response. 1096 :returns: Dictionary with information 1097 """ 1098 if self.session_not_on_or_after > 0: 1099 nooa = self.session_not_on_or_after 1100 else: 1101 nooa = self.not_on_or_after 1102 1103 if self.context == "AuthzQuery": 1104 return {"name_id": self.name_id, "came_from": self.came_from, 1105 "issuer": self.issuer(), "not_on_or_after": nooa, 1106 "authz_decision_info": self.authz_decision_info()} 1107 else: 1108 authn_statement = self.assertion.authn_statement[0] 1109 return {"ava": self.ava, "name_id": self.name_id, 1110 "came_from": self.came_from, "issuer": self.issuer(), 1111 "not_on_or_after": nooa, "authn_info": self.authn_info(), 1112 "session_index": authn_statement.session_index} 1113 1114 def __str__(self): 1115 return self.xmlstr 1116 1117 def verify_recipient(self, recipient): 1118 """ 1119 Verify that I'm the recipient of the assertion 1120 1121 :param recipient: A URI specifying the entity or location to which an 1122 attesting entity can present the assertion. 1123 :return: True/False 1124 """ 1125 if not self.conv_info: 1126 return True 1127 1128 _info = self.conv_info 1129 1130 try: 1131 if recipient == _info['entity_id']: 1132 return True 1133 except KeyError: 1134 pass 1135 1136 try: 1137 if recipient in self.return_addrs: 1138 return True 1139 except KeyError: 1140 pass 1141 1142 return False 1143 1144 def verify_attesting_entity(self, subject_confirmation): 1145 """ 1146 At least one address specification has to be correct. 1147 1148 :param subject_confirmation: A SubbjectConfirmation instance 1149 :return: True/False 1150 """ 1151 1152 try: 1153 address = self.conv_info['remote_addr'] 1154 except KeyError: 1155 address = '0.0.0.0' 1156 1157 correct = 0 1158 for subject_conf in subject_confirmation: 1159 if subject_conf.subject_confirmation_data is None: 1160 correct += 1 # In reality undefined 1161 elif subject_conf.subject_confirmation_data.address: 1162 if address == '0.0.0.0': # accept anything 1163 correct += 1 1164 elif subject_conf.subject_confirmation_data.address == address: 1165 correct += 1 1166 else: 1167 correct += 1 1168 1169 if correct: 1170 return True 1171 else: 1172 return False 1173 1174 1175class AuthnQueryResponse(AuthnResponse): 1176 msgtype = "authn_query_response" 1177 1178 def __init__(self, sec_context, attribute_converters, entity_id, 1179 return_addrs=None, timeslack=0, asynchop=False, test=False, 1180 conv_info=None): 1181 AuthnResponse.__init__(self, sec_context, attribute_converters, 1182 entity_id, return_addrs, timeslack=timeslack, 1183 asynchop=asynchop, test=test, 1184 conv_info=conv_info) 1185 self.entity_id = entity_id 1186 self.attribute_converters = attribute_converters 1187 self.assertion = None 1188 self.context = "AuthnQuery" 1189 1190 def condition_ok(self, lax=False): # Should I care about conditions ? 1191 return True 1192 1193 1194class AttributeResponse(AuthnResponse): 1195 msgtype = "attribute_response" 1196 1197 def __init__(self, sec_context, attribute_converters, entity_id, 1198 return_addrs=None, timeslack=0, asynchop=False, test=False, 1199 conv_info=None): 1200 AuthnResponse.__init__(self, sec_context, attribute_converters, 1201 entity_id, return_addrs, timeslack=timeslack, 1202 asynchop=asynchop, test=test, 1203 conv_info=conv_info) 1204 self.entity_id = entity_id 1205 self.attribute_converters = attribute_converters 1206 self.assertion = None 1207 self.context = "AttrQuery" 1208 1209 1210class AuthzResponse(AuthnResponse): 1211 """ A successful response will be in the form of assertions containing 1212 authorization decision statements.""" 1213 msgtype = "authz_decision_response" 1214 1215 def __init__(self, sec_context, attribute_converters, entity_id, 1216 return_addrs=None, timeslack=0, asynchop=False, 1217 conv_info=None): 1218 AuthnResponse.__init__(self, sec_context, attribute_converters, 1219 entity_id, return_addrs, timeslack=timeslack, 1220 asynchop=asynchop, conv_info=conv_info) 1221 self.entity_id = entity_id 1222 self.attribute_converters = attribute_converters 1223 self.assertion = None 1224 self.context = "AuthzQuery" 1225 1226 1227class ArtifactResponse(AuthnResponse): 1228 msgtype = "artifact_response" 1229 1230 def __init__(self, sec_context, attribute_converters, entity_id, 1231 return_addrs=None, timeslack=0, asynchop=False, test=False, 1232 conv_info=None): 1233 AuthnResponse.__init__(self, sec_context, attribute_converters, 1234 entity_id, return_addrs, timeslack=timeslack, 1235 asynchop=asynchop, test=test, 1236 conv_info=conv_info) 1237 self.entity_id = entity_id 1238 self.attribute_converters = attribute_converters 1239 self.assertion = None 1240 self.context = "ArtifactResolve" 1241 1242 1243def response_factory(xmlstr, conf, return_addrs=None, outstanding_queries=None, 1244 timeslack=0, decode=True, request_id=0, origxml=None, 1245 asynchop=True, allow_unsolicited=False, 1246 want_assertions_signed=False, conv_info=None): 1247 sec_context = security_context(conf) 1248 if not timeslack: 1249 try: 1250 timeslack = int(conf.accepted_time_diff) 1251 except TypeError: 1252 timeslack = 0 1253 1254 attribute_converters = conf.attribute_converters 1255 entity_id = conf.entityid 1256 extension_schema = conf.extension_schema 1257 1258 response = StatusResponse(sec_context, return_addrs, timeslack, request_id, 1259 asynchop, conv_info=conv_info) 1260 try: 1261 response.loads(xmlstr, decode, origxml) 1262 if response.response.assertion or response.response.encrypted_assertion: 1263 authnresp = AuthnResponse( 1264 sec_context, attribute_converters, entity_id, return_addrs, 1265 outstanding_queries, timeslack, asynchop, allow_unsolicited, 1266 extension_schema=extension_schema, 1267 want_assertions_signed=want_assertions_signed, 1268 conv_info=conv_info) 1269 authnresp.update(response) 1270 return authnresp 1271 except TypeError: 1272 response.signature_check = sec_context.correctly_signed_logout_response 1273 response.loads(xmlstr, decode, origxml) 1274 logoutresp = LogoutResponse(sec_context, return_addrs, timeslack, 1275 asynchop=asynchop, conv_info=conv_info) 1276 logoutresp.update(response) 1277 return logoutresp 1278 1279 return response 1280 1281 1282# =========================================================================== 1283# A class of it's own 1284 1285 1286class AssertionIDResponse(object): 1287 msgtype = "assertion_id_response" 1288 1289 def __init__(self, sec_context, attribute_converters, timeslack=0, 1290 **kwargs): 1291 1292 self.sec = sec_context 1293 self.timeslack = timeslack 1294 self.xmlstr = "" 1295 self.origxml = "" 1296 self.name_id = "" 1297 self.response = None 1298 self.not_signed = False 1299 self.attribute_converters = attribute_converters 1300 self.assertion = None 1301 self.context = "AssertionIdResponse" 1302 self.signature_check = self.sec.correctly_signed_assertion_id_response 1303 1304 # Because this class is not a subclass of StatusResponse we need 1305 # to add these attributes directly so that the _parse_response() 1306 # method of the Entity class can treat instances of this class 1307 # like all other responses. 1308 self.require_signature = False 1309 self.require_response_signature = False 1310 self.require_signature_or_response_signature = False 1311 1312 def loads(self, xmldata, decode=True, origxml=None): 1313 # own copy 1314 self.xmlstr = xmldata[:] 1315 logger.debug("xmlstr: %s", self.xmlstr) 1316 self.origxml = origxml 1317 1318 try: 1319 self.response = self.signature_check(xmldata, origdoc=origxml) 1320 self.assertion = self.response 1321 except TypeError: 1322 raise 1323 except SignatureError: 1324 raise 1325 except Exception as excp: 1326 logger.exception("EXCEPTION: %s", excp) 1327 raise 1328 1329 # print("<", self.response) 1330 1331 return self._postamble() 1332 1333 def verify(self, keys=None): 1334 try: 1335 valid_instance(self.response) 1336 except NotValid as exc: 1337 logger.error("Not valid response: %s", exc.args[0]) 1338 raise 1339 return self 1340 1341 def _postamble(self): 1342 if not self.response: 1343 logger.error("Response was not correctly signed") 1344 if self.xmlstr: 1345 logger.info("Response: %s", self.xmlstr) 1346 raise IncorrectlySigned() 1347 1348 logger.debug("response: %s", self.response) 1349 1350 return self 1351