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