From 30b613ebb2a30ad00d886e6f9fc08716b54daf92 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Fri, 21 Mar 2014 08:59:52 +0100 Subject: [PATCH 01/16] Deal with no subject_confirmation element present. Changed version to 2.0.0, needed by pysaml2 dependent projects. --- setup.py | 2 +- src/saml2/response.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 9cca65d..30a2404 100755 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ if sys.version_info < (2, 7): setup( name='pysaml2', - version='2.0.0beta', + version='2.0.0', description='Python implementation of SAML Version 2 to be used in a WSGI environment', # long_description = read("README"), author='Roland Hedberg', diff --git a/src/saml2/response.py b/src/saml2/response.py index b987130..3c6bfa3 100644 --- a/src/saml2/response.py +++ b/src/saml2/response.py @@ -873,7 +873,9 @@ class AuthnResponse(StatusResponse): correct = 0 for subject_conf in self.assertion.subject.subject_confirmation: - if subject_conf.subject_confirmation_data.address: + if subject_conf.subject_confirmation_data is None: + correct += 1 # In reality undefined + elif subject_conf.subject_confirmation_data.address: if subject_conf.subject_confirmation_data.address == address: correct += 1 else: From c713f54dbcbff3fdcc855d73e54f6aeb53a48474 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Fri, 21 Mar 2014 09:13:30 +0100 Subject: [PATCH 02/16] working on new version. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 30a2404..6d1a544 100755 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ if sys.version_info < (2, 7): setup( name='pysaml2', - version='2.0.0', + version='2.0.1beta', description='Python implementation of SAML Version 2 to be used in a WSGI environment', # long_description = read("README"), author='Roland Hedberg', From 86f0ea0af1f7ce5f50d68d05524407fa0cebd920 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Fri, 21 Mar 2014 12:46:04 +0100 Subject: [PATCH 03/16] Stuff was removed that shouldn't have been. Reinstating it! --- src/saml2/metadata.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/saml2/metadata.py b/src/saml2/metadata.py index afeb701..f2f24e4 100644 --- a/src/saml2/metadata.py +++ b/src/saml2/metadata.py @@ -456,13 +456,13 @@ def do_spsso_descriptor(conf, cert=None): ENDPOINTS["sp"]).items(): setattr(spsso, endpoint, instlist) - # ext = do_endpoints(endps, ENDPOINT_EXT["sp"]) - # if ext: - # if spsso.extensions is None: - # spsso.extensions = md.Extensions() - # for vals in ext.values(): - # for val in vals: - # spsso.extensions.add_extension_element(val) + ext = do_endpoints(endps, ENDPOINT_EXT["sp"]) + if ext: + if spsso.extensions is None: + spsso.extensions = md.Extensions() + for vals in ext.values(): + for val in vals: + spsso.extensions.add_extension_element(val) if cert: encryption_type = conf.encryption_type From c9c01cc57f2669419e291bf847a9bf04fba10c75 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Fri, 21 Mar 2014 12:49:19 +0100 Subject: [PATCH 04/16] PEP-8 clean up . --- src/saml2/response.py | 151 +++++++++++++++++++++++++----------------- 1 file changed, 89 insertions(+), 62 deletions(-) diff --git a/src/saml2/response.py b/src/saml2/response.py index 3c6bfa3..8212584 100644 --- a/src/saml2/response.py +++ b/src/saml2/response.py @@ -204,7 +204,7 @@ def _dummy(_): def for_me(conditions, myself): """ Am I among the intended audiences """ - if not conditions.audience_restriction: # No audience restriction + if not conditions.audience_restriction: # No audience restriction return True for restriction in conditions.audience_restriction: @@ -216,19 +216,20 @@ def for_me(conditions, myself): else: #print "Not for me: %s != %s" % (audience.text.strip(), myself) pass - + return False def authn_response(conf, return_addrs, outstanding_queries=None, timeslack=0, - asynchop=True, allow_unsolicited=False, want_assertions_signed=False): + asynchop=True, allow_unsolicited=False, + want_assertions_signed=False): sec = security_context(conf) if not timeslack: try: timeslack = int(conf.accepted_time_diff) except TypeError: timeslack = 0 - + return AuthnResponse(sec, conf.attribute_converters, conf.entityid, return_addrs, outstanding_queries, timeslack, asynchop=asynchop, allow_unsolicited=allow_unsolicited, @@ -271,13 +272,13 @@ class StatusResponse(object): self.require_response_signature = False self.not_signed = False self.asynchop = asynchop - + def _clear(self): self.xmlstr = "" self.name_id = None self.response = None self.not_on_or_after = 0 - + def _postamble(self): if not self.response: logger.error("Response was not correctly signed") @@ -293,10 +294,10 @@ class StatusResponse(object): logger.error("Not valid response: %s" % exc.args[0]) self._clear() return self - + self.in_response_to = self.response.in_response_to return self - + def load_instance(self, instance): if signed(instance): # This will check signature on Assertion which is the default @@ -309,9 +310,9 @@ class StatusResponse(object): else: self.not_signed = True self.response = instance - + return self._postamble() - + def _loads(self, xmldata, decode=True, origxml=None): # own copy @@ -319,7 +320,8 @@ class StatusResponse(object): logger.debug("xmlstr: %s" % (self.xmlstr,)) try: - self.response = self.signature_check(xmldata, origdoc=origxml, must=self.require_signature, + self.response = self.signature_check(xmldata, origdoc=origxml, + must=self.require_signature, require_response_signature=self.require_response_signature) except TypeError: @@ -329,11 +331,11 @@ class StatusResponse(object): except Exception, excp: #logger.exception("EXCEPTION: %s", excp) raise - + #print "<", self.response - + return self._postamble() - + def status_ok(self): if self.response.status: status = self.response.status @@ -369,7 +371,7 @@ class StatusResponse(object): def _verify(self): if self.request_id and self.in_response_to and \ - self.in_response_to != self.request_id: + self.in_response_to != self.request_id: logger.error("Not the id I expected: %s != %s" % ( self.in_response_to, self.request_id)) return None @@ -385,11 +387,11 @@ class StatusResponse(object): if self.asynchop: if self.response.destination and \ - self.response.destination not in self.return_addrs: + self.response.destination not in self.return_addrs: logger.error("%s not in %s" % (self.response.destination, - self.return_addrs)) + self.return_addrs)) return None - + assert self.issue_instant_ok() assert self.status_ok() return self @@ -408,10 +410,10 @@ class StatusResponse(object): self.xmlstr = mold.xmlstr self.in_response_to = mold.in_response_to self.response = mold.response - + def issuer(self): return self.response.issuer.text.strip() - + class LogoutResponse(StatusResponse): msgtype = "logout_response" @@ -430,7 +432,8 @@ class NameIDMappingResponse(StatusResponse): request_id=0, asynchop=True): StatusResponse.__init__(self, sec_context, return_addrs, timeslack, request_id, asynchop) - self.signature_check = self.sec.correctly_signed_name_id_mapping_response + self.signature_check = self.sec\ + .correctly_signed_name_id_mapping_response class ManageNameIDResponse(StatusResponse): @@ -455,7 +458,8 @@ class AuthnResponse(StatusResponse): return_addrs=None, outstanding_queries=None, timeslack=0, asynchop=True, allow_unsolicited=False, test=False, allow_unknown_attributes=False, - want_assertions_signed=False, want_response_signed=False, **kwargs): + want_assertions_signed=False, want_response_signed=False, + **kwargs): StatusResponse.__init__(self, sec_context, return_addrs, timeslack, asynchop=asynchop) @@ -465,7 +469,7 @@ class AuthnResponse(StatusResponse): self.outstanding_queries = outstanding_queries else: self.outstanding_queries = {} - self.context = "AuthnReq" + self.context = "AuthnReq" self.came_from = "" self.ava = None self.assertion = None @@ -481,19 +485,40 @@ class AuthnResponse(StatusResponse): except KeyError: self.extension_schema = {} + def check_subject_confirmation_in_response_to(self, irp): + for assertion in self.response.assertion: + for _sc in assertion.subject.subject_confirmation: + try: + assert _sc.subject_confirmation_data.in_response_to == irp + except AssertionError: + return False + + return True + def loads(self, xmldata, decode=True, origxml=None): self._loads(xmldata, decode, origxml) - + if self.asynchop: if self.in_response_to in self.outstanding_queries: self.came_from = self.outstanding_queries[self.in_response_to] del self.outstanding_queries[self.in_response_to] + try: + if not self.check_subject_confirmation_in_response_to( + self.in_response_to): + logger.exception( + "Unsolicited response %s" % self.in_response_to) + raise UnsolicitedResponse( + "Unsolicited response: %s" % self.in_response_to) + except AttributeError: + pass elif self.allow_unsolicited: pass else: - logger.exception("Unsolicited response %s" % self.in_response_to) - raise UnsolicitedResponse("Unsolicited response: %s" % self.in_response_to) - + logger.exception( + "Unsolicited response %s" % self.in_response_to) + raise UnsolicitedResponse( + "Unsolicited response: %s" % self.in_response_to) + return self def clear(self): @@ -501,7 +526,7 @@ class AuthnResponse(StatusResponse): self.came_from = "" self.ava = None self.assertion = None - + def authn_statement_ok(self, optional=False): try: # the assertion MUST contain one AuthNStatement @@ -511,7 +536,7 @@ class AuthnResponse(StatusResponse): return True else: raise - + authn_statement = self.assertion.authn_statement[0] if authn_statement.session_not_on_or_after: if validate_on_or_after(authn_statement.session_not_on_or_after, @@ -523,7 +548,7 @@ class AuthnResponse(StatusResponse): return False return True # check authn_statement.session_index - + def condition_ok(self, lax=False): if self.test: lax = True @@ -541,7 +566,8 @@ class AuthnResponse(StatusResponse): # if both are present NotBefore must be earlier than NotOnOrAfter if conditions.not_before and conditions.not_on_or_after: - if not later_than(conditions.not_on_or_after, conditions.not_before): + if not later_than(conditions.not_on_or_after, + conditions.not_before): return False try: @@ -562,10 +588,11 @@ class AuthnResponse(StatusResponse): if not lax: raise Exception("Not for me!!!") - if conditions.condition: # extra conditions + if conditions.condition: # extra conditions for cond in conditions.condition: try: - if cond.extension_attributes[XSI_TYPE] in self.extension_schema: + if cond.extension_attributes[ + XSI_TYPE] in self.extension_schema: pass else: raise Exception("Unknown condition") @@ -582,9 +609,9 @@ class AuthnResponse(StatusResponse): :param attribute_statement: A SAML.AttributeStatement which might contain both encrypted attributes and attributes. """ -# _node_name = [ -# "urn:oasis:names:tc:SAML:2.0:assertion:EncryptedData", -# "urn:oasis:names:tc:SAML:2.0:assertion:EncryptedAttribute"] + # _node_name = [ + # "urn:oasis:names:tc:SAML:2.0:assertion:EncryptedData", + # "urn:oasis:names:tc:SAML:2.0:assertion:EncryptedAttribute"] for encattr in attribute_statement.encrypted_attribute: if not encattr.encrypted_key: @@ -624,7 +651,7 @@ class AuthnResponse(StatusResponse): if data.address: if not valid_address(data.address): return False - # verify that I got it from the correct sender + # verify that I got it from the correct sender # These two will raise exception if untrue validate_on_or_after(data.not_on_or_after, self.timeslack) @@ -650,7 +677,8 @@ class AuthnResponse(StatusResponse): logger.info("outstanding queries: %s" % ( self.outstanding_queries.keys(),)) raise Exception( - "Combination of session id and requestURI I don't recall") + "Combination of session id and requestURI I don't " + "recall") return True def _holder_of_key_confirmed(self, data): @@ -687,12 +715,12 @@ class AuthnResponse(StatusResponse): subject_confirmation.method,)) subjconf.append(subject_confirmation) - + if not subjconf: raise VerificationError("No valid subject confirmation") - + subject.subject_confirmation = subjconf - + # The subject must contain a name_id try: assert subject.name_id @@ -709,19 +737,19 @@ class AuthnResponse(StatusResponse): logger.info("Subject NameID: %s" % self.name_id) return self.name_id - + def _assertion(self, assertion): self.assertion = assertion logger.debug("assertion context: %s" % (self.context,)) logger.debug("assertion keys: %s" % (assertion.keyswv())) logger.debug("outstanding_queries: %s" % (self.outstanding_queries,)) - + #if self.context == "AuthnReq" or self.context == "AttrQuery": if self.context == "AuthnReq": self.authn_statement_ok() -# elif self.context == "AttrQuery": -# self.authn_statement_ok(True) + # elif self.context == "AttrQuery": + # self.authn_statement_ok(True) if not self.condition_ok(): raise VerificationError("Condition not OK") @@ -732,7 +760,7 @@ class AuthnResponse(StatusResponse): self.ava = self.get_identity() logger.debug("--- AVA: %s" % (self.ava,)) - + try: self.get_subject() if self.asynchop: @@ -744,7 +772,7 @@ class AuthnResponse(StatusResponse): except Exception: logger.exception("get subject") raise - + def _encrypted_assertion(self, xmlstr): if xmlstr.encrypted_data: assertion_str = self.sec.decrypt(xmlstr.encrypted_data.to_string()) @@ -765,7 +793,7 @@ class AuthnResponse(StatusResponse): logger.debug("Decrypted Assertion: %s" % assertion) return self._assertion(assertion) - + def parse_assertion(self): if self.context == "AuthnQuery": # can contain one or more assertions @@ -773,10 +801,10 @@ class AuthnResponse(StatusResponse): else: # This is a saml2int limitation try: assert len(self.response.assertion) == 1 or \ - len(self.response.encrypted_assertion) == 1 + len(self.response.encrypted_assertion) == 1 except AssertionError: raise Exception("No assertion part") - + if self.response.assertion: logger.debug("***Unencrypted response***") for assertion in self.response.assertion: @@ -793,7 +821,7 @@ class AuthnResponse(StatusResponse): def verify(self): """ Verify that the assertion is syntactically correct and the signature is correct if present.""" - + try: self._verify() except AssertionError: @@ -807,15 +835,15 @@ class AuthnResponse(StatusResponse): else: logger.error("Could not parse the assertion") return None - + def session_id(self): - """ Returns the SessionID of the response """ + """ Returns the SessionID of the response """ return self.response.in_response_to - + def id(self): """ Return the ID of the response """ return self.response.id - + def authn_info(self): res = [] for astat in self.assertion.authn_statement: @@ -858,7 +886,7 @@ class AuthnResponse(StatusResponse): return {"ava": self.ava, "name_id": self.name_id, "came_from": self.came_from, "issuer": self.issuer(), "not_on_or_after": nooa, "authn_info": self.authn_info()} - + def __str__(self): return "%s" % self.xmlstr @@ -892,7 +920,6 @@ class AuthnQueryResponse(AuthnResponse): def __init__(self, sec_context, attribute_converters, entity_id, return_addrs=None, timeslack=0, asynchop=False, test=False): - AuthnResponse.__init__(self, sec_context, attribute_converters, entity_id, return_addrs, timeslack=timeslack, asynchop=asynchop, test=test) @@ -910,7 +937,6 @@ class AttributeResponse(AuthnResponse): def __init__(self, sec_context, attribute_converters, entity_id, return_addrs=None, timeslack=0, asynchop=False, test=False): - AuthnResponse.__init__(self, sec_context, attribute_converters, entity_id, return_addrs, timeslack=timeslack, asynchop=asynchop, test=test) @@ -941,7 +967,6 @@ class ArtifactResponse(AuthnResponse): def __init__(self, sec_context, attribute_converters, entity_id, return_addrs=None, timeslack=0, asynchop=False, test=False): - AuthnResponse.__init__(self, sec_context, attribute_converters, entity_id, return_addrs, timeslack=timeslack, asynchop=asynchop, test=test) @@ -953,14 +978,15 @@ class ArtifactResponse(AuthnResponse): def response_factory(xmlstr, conf, return_addrs=None, outstanding_queries=None, timeslack=0, decode=True, request_id=0, origxml=None, - asynchop=True, allow_unsolicited=False, want_assertions_signed=False): + asynchop=True, allow_unsolicited=False, + want_assertions_signed=False): sec_context = security_context(conf) if not timeslack: try: timeslack = int(conf.accepted_time_diff) except TypeError: timeslack = 0 - + attribute_converters = conf.attribute_converters entity_id = conf.entityid extension_schema = conf.extension_schema @@ -985,9 +1011,10 @@ def response_factory(xmlstr, conf, return_addrs=None, outstanding_queries=None, asynchop=asynchop) logoutresp.update(response) return logoutresp - + return response + # =========================================================================== # A class of it's own From acacb49ef7a32ef49fb875a87f17d26cb3dc74e2 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Fri, 21 Mar 2014 12:52:21 +0100 Subject: [PATCH 05/16] Rebuilt --- tests/servera.xml | 112 +++++++++++++--------------------------------- 1 file changed, 31 insertions(+), 81 deletions(-) diff --git a/tests/servera.xml b/tests/servera.xml index be9a472..f600be5 100644 --- a/tests/servera.xml +++ b/tests/servera.xml @@ -1,82 +1,32 @@ - - - - - - - - - - MIICsDCCAhmgAwIBAgIJAJrzqSSwmDY9MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV - BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX - aWRnaXRzIFB0eSBMdGQwHhcNMDkxMDA2MTk0OTQxWhcNMDkxMTA1MTk0OTQxWjBF - MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 - ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB - gQDJg2cms7MqjniT8Fi/XkNHZNPbNVQyMUMXE9tXOdqwYCA1cc8vQdzkihscQMXy - 3iPw2cMggBu6gjMTOSOxECkuvX5ZCclKr8pXAJM5cY6gVOaVO2PdTZcvDBKGbiaN - efiEw5hnoZomqZGp8wHNLAUkwtH9vjqqvxyS/vclc6k2ewIDAQABo4GnMIGkMB0G - A1UdDgQWBBRePsKHKYJsiojE78ZWXccK9K4aJTB1BgNVHSMEbjBsgBRePsKHKYJs - iojE78ZWXccK9K4aJaFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt - U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAJrzqSSw - mDY9MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAJSrKOEzHO7TL5cy6 - h3qh+3+JAk8HbGBW+cbX6KBCAw/mzU8flK25vnWwXS3dv2FF3Aod0/S7AWNfKib5 - U/SA9nJaz/mWeF9S0farz9AQFc8/NSzAzaVq7YbM4F6f6N2FRl7GikdXRCed45j6 - mrPzGzk3ECbupFnqyREH3+ZPSdk= - - - - - - - - - - - urn:oasis:names:tc:SAML:2.0:nameid-format:transient - - urn:oasis:names:tc:SAML:2.0:nameid-format:persistent - - - - - - - AB Exempel - AB Exempel - - http://www.example.org - - - - Roland - Hedberg - tech@eample.com - tech@example.org - +46 70 100 0000 - - +http://www.swamid.se/category/sfs-1993-1153http://www.swamid.se/category/hei-serviceMIICsDCCAhmgAwIBAgIJAJrzqSSwmDY9MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMDkxMDA2MTk0OTQxWhcNMDkxMTA1MTk0OTQxWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB +gQDJg2cms7MqjniT8Fi/XkNHZNPbNVQyMUMXE9tXOdqwYCA1cc8vQdzkihscQMXy +3iPw2cMggBu6gjMTOSOxECkuvX5ZCclKr8pXAJM5cY6gVOaVO2PdTZcvDBKGbiaN +efiEw5hnoZomqZGp8wHNLAUkwtH9vjqqvxyS/vclc6k2ewIDAQABo4GnMIGkMB0G +A1UdDgQWBBRePsKHKYJsiojE78ZWXccK9K4aJTB1BgNVHSMEbjBsgBRePsKHKYJs +iojE78ZWXccK9K4aJaFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt +U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAJrzqSSw +mDY9MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAJSrKOEzHO7TL5cy6 +h3qh+3+JAk8HbGBW+cbX6KBCAw/mzU8flK25vnWwXS3dv2FF3Aod0/S7AWNfKib5 +U/SA9nJaz/mWeF9S0farz9AQFc8/NSzAzaVq7YbM4F6f6N2FRl7GikdXRCed45j6 +mrPzGzk3ECbupFnqyREH3+ZPSdk= +MIICsDCCAhmgAwIBAgIJAJrzqSSwmDY9MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMDkxMDA2MTk0OTQxWhcNMDkxMTA1MTk0OTQxWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB +gQDJg2cms7MqjniT8Fi/XkNHZNPbNVQyMUMXE9tXOdqwYCA1cc8vQdzkihscQMXy +3iPw2cMggBu6gjMTOSOxECkuvX5ZCclKr8pXAJM5cY6gVOaVO2PdTZcvDBKGbiaN +efiEw5hnoZomqZGp8wHNLAUkwtH9vjqqvxyS/vclc6k2ewIDAQABo4GnMIGkMB0G +A1UdDgQWBBRePsKHKYJsiojE78ZWXccK9K4aJTB1BgNVHSMEbjBsgBRePsKHKYJs +iojE78ZWXccK9K4aJaFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt +U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAJrzqSSw +mDY9MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAJSrKOEzHO7TL5cy6 +h3qh+3+JAk8HbGBW+cbX6KBCAw/mzU8flK25vnWwXS3dv2FF3Aod0/S7AWNfKib5 +U/SA9nJaz/mWeF9S0farz9AQFc8/NSzAzaVq7YbM4F6f6N2FRl7GikdXRCed45j6 +mrPzGzk3ECbupFnqyREH3+ZPSdk= +urn:oasis:names:tc:SAML:2.0:nameid-format:transienturn:oasis:names:tc:SAML:2.0:nameid-format:persistentAB ExempelAB Exempelhttp://www.example.orgRolandHedbergtech@eample.comtech@example.org+46 70 100 0000 From c52306c11a99285971f2e0a836f0cfce984ed9e2 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Fri, 21 Mar 2014 12:53:17 +0100 Subject: [PATCH 06/16] Handling of specific exception. --- src/saml2/entity.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/saml2/entity.py b/src/saml2/entity.py index 67939e1..f089fb8 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -23,6 +23,7 @@ from saml2.saml import NameID from saml2.saml import Issuer from saml2.saml import NAMEID_FORMAT_ENTITY from saml2.response import LogoutResponse +from saml2.response import UnsolicitedResponse from saml2.time_util import instant from saml2.s_utils import sid from saml2.s_utils import UnravelError @@ -839,11 +840,14 @@ class Entity(HTTPBase): response = response.loads(xmlstr, False, origxml=origxml) except SigverError, err: logger.error("Signature Error: %s" % err) - return None + raise + except UnsolicitedResponse: + logger.error("Unsolicited response") + raise except Exception, err: if "not well-formed" in "%s" % err: logger.error("Not well-formed XML") - return None + raise logger.debug("XMLSTR: %s" % xmlstr) From bbb01cdbc2c88b6ab5211b3ff956693c95bbc5d6 Mon Sep 17 00:00:00 2001 From: rhoerbe Date: Fri, 21 Mar 2014 14:13:56 +0100 Subject: [PATCH 07/16] added support for RFC 1123 date format --- src/saml2/httpbase.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/saml2/httpbase.py b/src/saml2/httpbase.py index 3b6b630..8c99c95 100644 --- a/src/saml2/httpbase.py +++ b/src/saml2/httpbase.py @@ -69,9 +69,18 @@ def _since_epoch(cdate): cdate = cdate[5:] try: - t = time.strptime(cdate, "%d-%b-%Y %H:%M:%S %Z") + t = time.strptime(cdate, "%d-%b-%Y %H:%M:%S %Z") # e.g. 18-Apr-2014 12:30:51 GMT except ValueError: - t = time.strptime(cdate, "%d-%b-%y %H:%M:%S %Z") + try: + t = time.strptime(cdate, "%d-%b-%y %H:%M:%S %Z") # e.g. 18-Apr-14 12:30:51 GMT + except ValueError: + try: + t = time.strptime(cdate, "%d %b %Y %H:%M:%S %Z") # e.g. 18 Apr 2014 12:30:51 GMT + except ValueError: + raise Exception, 'ValueError: Date "{0}" does not match any of '.format(cdate) + \ + '"%d-%b-%Y %H:%M:%S %Z", ' + \ + '"%d-%b-%y %H:%M:%S %Z", ' + \ + '"%d %b %Y %H:%M:%S %Z".' #return int(time.mktime(t)) return calendar.timegm(t) From 3e993a5a456a3c6429d9a79e25bea4276d86d884 Mon Sep 17 00:00:00 2001 From: rhoerbe Date: Fri, 21 Mar 2014 15:07:26 +0100 Subject: [PATCH 08/16] added support for RFC 1123 date format --- src/saml2/httpbase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/saml2/httpbase.py b/src/saml2/httpbase.py index 8c99c95..56d4a7e 100644 --- a/src/saml2/httpbase.py +++ b/src/saml2/httpbase.py @@ -67,7 +67,7 @@ def _since_epoch(cdate): if len(cdate) < 5: return utc_now() - cdate = cdate[5:] + cdate = cdate[5:] # assume short weekday, i.e. do not support obsolete RFC 1036 date format try: t = time.strptime(cdate, "%d-%b-%Y %H:%M:%S %Z") # e.g. 18-Apr-2014 12:30:51 GMT except ValueError: From b6fe85543a64904928761a45dd3365b87236ff72 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Mon, 24 Mar 2014 12:24:38 +0100 Subject: [PATCH 09/16] PEP-8 stuff --- src/saml2/assertion.py | 3 +- src/saml2/cert.py | 186 +++++++++++++++++++++++++++-------------- src/saml2/server.py | 7 +- src/saml2/sigver.py | 5 +- 4 files changed, 131 insertions(+), 70 deletions(-) diff --git a/src/saml2/assertion.py b/src/saml2/assertion.py index a0bccd6..1057ca4 100644 --- a/src/saml2/assertion.py +++ b/src/saml2/assertion.py @@ -541,7 +541,8 @@ class Assertion(dict): def __init__(self, dic=None): dict.__init__(self, dic) - def _authn_context_decl(self, decl, authn_auth=None): + @staticmethod + def _authn_context_decl(decl, authn_auth=None): """ Construct the authn context with a authn context declaration :param decl: The authn context declaration diff --git a/src/saml2/cert.py b/src/saml2/cert.py index 638052e..4cfcd20 100644 --- a/src/saml2/cert.py +++ b/src/saml2/cert.py @@ -9,7 +9,6 @@ from os.path import join from os import remove from Crypto.Util import asn1 - class WrongInput(Exception): pass @@ -23,55 +22,82 @@ class PayloadError(Exception): class OpenSSLWrapper(object): - def __init__(self): pass - def create_certificate(self, cert_info, request=False, valid_from=0, valid_to=315360000, sn=1, key_length=1024, - hash_alg="sha256", write_to_file=False, cert_dir="", cipher_passphrase = None): + def create_certificate(self, cert_info, request=False, valid_from=0, + valid_to=315360000, sn=1, key_length=1024, + hash_alg="sha256", write_to_file=False, cert_dir="", + cipher_passphrase=None): """ - Can create certificate requests, to be signed later by another certificate with the method + Can create certificate requests, to be signed later by another + certificate with the method create_cert_signed_certificate. If request is True. - Can also create self signed root certificates if request is False. This is default behaviour. + Can also create self signed root certificates if request is False. + This is default behaviour. :param cert_info: Contains information about the certificate. Is a dictionary that must contain the keys: - cn = Common name. This part must match the host being authenticated - country_code = Two letter description of the country. + cn = Common name. This part + must match the host being authenticated + country_code = Two letter description + of the country. state = State city = City - organization = Organization, can be a company name. - organization_unit = A unit at the organization, can be a department. + organization = Organization, can be a + company name. + organization_unit = A unit at the + organization, can be a department. Example: cert_info_ca = { "cn": "company.com", "country_code": "se", "state": "AC", "city": "Dorotea", - "organization": "Company", - "organization_unit": "Sales" + "organization": + "Company", + "organization_unit": + "Sales" } - :param request: True if this is a request for certificate, that should be signed. - False if this is a self signed certificate, root certificate. - :param valid_from: When the certificate starts to be valid. Amount of seconds from when the + :param request: True if this is a request for certificate, + that should be signed. + False if this is a self signed certificate, + root certificate. + :param valid_from: When the certificate starts to be valid. + Amount of seconds from when the certificate is generated. - :param valid_to: How long the certificate will be valid from when it is generated. - The value is in seconds. Default is 315360000 seconds, a.k.a 10 years. - :param sn: Serial number for the certificate. Default is 1. - :param key_length: Length of the key to be generated. Defaults to 1024. - :param hash_alg: Hash algorithm to use for the key. Default is sha256. - :param write_to_file: True if you want to write the certificate to a file. The method will then return - a tuple with path to certificate file and path to key file. - False if you want to get the result as strings. The method will then return a tuple - with the certificate string and the key as string. - WILL OVERWRITE ALL EXISTING FILES WITHOUT ASKING! - :param cert_dir: Where to save the files if write_to_file is true. - :param cipher_passphrase A dictionary with cipher and passphrase. Example: - {"cipher": "blowfish", "passphrase": "qwerty"} - :return: string representation of certificate, string representation of private key + :param valid_to: How long the certificate will be valid from + when it is generated. + The value is in seconds. Default is + 315360000 seconds, a.k.a 10 years. + :param sn: Serial number for the certificate. Default + is 1. + :param key_length: Length of the key to be generated. Defaults + to 1024. + :param hash_alg: Hash algorithm to use for the key. Default + is sha256. + :param write_to_file: True if you want to write the certificate + to a file. The method will then return + a tuple with path to certificate file and + path to key file. + False if you want to get the result as + strings. The method will then return a tuple + with the certificate string and the key as + string. + WILL OVERWRITE ALL EXISTING FILES WITHOUT + ASKING! + :param cert_dir: Where to save the files if write_to_file is + true. + :param cipher_passphrase A dictionary with cipher and passphrase. + Example:: + {"cipher": "blowfish", "passphrase": "qwerty"} + + :return: string representation of certificate, + string representation of private key if write_to_file parameter is False otherwise - path to certificate file, path to private key file + path to certificate file, path to private + key file """ cn = cert_info["cn"] @@ -97,7 +123,7 @@ class OpenSSLWrapper(object): k = crypto.PKey() k.generate_key(crypto.TYPE_RSA, key_length) - # create a self-signed cert + # create a self-signed cert cert = crypto.X509() if request: @@ -113,8 +139,8 @@ class OpenSSLWrapper(object): cert.get_subject().CN = cn if not request: cert.set_serial_number(sn) - cert.gmtime_adj_notBefore(valid_from) #Valid before present time - cert.gmtime_adj_notAfter(valid_to) #3 650 days + cert.gmtime_adj_notBefore(valid_from) #Valid before present time + cert.gmtime_adj_notAfter(valid_to) #3 650 days cert.set_issuer(cert.get_subject()) cert.set_pubkey(k) cert.sign(k, hash_alg) @@ -122,13 +148,16 @@ class OpenSSLWrapper(object): filesCreated = False try: if request: - tmp_cert = crypto.dump_certificate_request(crypto.FILETYPE_PEM, cert) + tmp_cert = crypto.dump_certificate_request(crypto.FILETYPE_PEM, + cert) else: tmp_cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) tmp_key = None if cipher_passphrase is not None: - tmp_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, k, cipher_passphrase["cipher"], - cipher_passphrase["passphrase"]) + tmp_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, k, + cipher_passphrase["cipher"], + cipher_passphrase[ + "passphrase"]) else: tmp_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, k) if write_to_file: @@ -172,36 +201,52 @@ class OpenSSLWrapper(object): return base64.b64encode(str(str_data)) - def create_cert_signed_certificate(self, sign_cert_str, sign_key_str, request_cert_str, hash_alg="sha256", - valid_from=0, valid_to=315360000, sn=1, passphrase=None): + def create_cert_signed_certificate(self, sign_cert_str, sign_key_str, + request_cert_str, hash_alg="sha256", + valid_from=0, valid_to=315360000, sn=1, + passphrase=None): """ Will sign a certificate request with a give certificate. - :param sign_cert_str: This certificate will be used to sign with. Must be a string representation of - the certificate. If you only have a file use the method read_str_from_file to + :param sign_cert_str: This certificate will be used to sign with. + Must be a string representation of + the certificate. If you only have a file + use the method read_str_from_file to get a string representation. - :param sign_key_str: This is the key for the ca_cert_str represented as a string. - If you only have a file use the method read_str_from_file to get a string + :param sign_key_str: This is the key for the ca_cert_str + represented as a string. + If you only have a file use the method + read_str_from_file to get a string representation. - :param request_cert_str: This is the prepared certificate to be signed. Must be a string representation of - the requested certificate. If you only have a file use the method read_str_from_file + :param request_cert_str: This is the prepared certificate to be + signed. Must be a string representation of + the requested certificate. If you only have + a file use the method read_str_from_file to get a string representation. - :param hash_alg: Hash algorithm to use for the key. Default is sha256. - :param valid_from: When the certificate starts to be valid. Amount of seconds from when the + :param hash_alg: Hash algorithm to use for the key. Default + is sha256. + :param valid_from: When the certificate starts to be valid. + Amount of seconds from when the certificate is generated. - :param valid_to: How long the certificate will be valid from when it is generated. - The value is in seconds. Default is 315360000 seconds, a.k.a 10 years. - :param sn: Serial number for the certificate. Default is 1. + :param valid_to: How long the certificate will be valid from + when it is generated. + The value is in seconds. Default is + 315360000 seconds, a.k.a 10 years. + :param sn: Serial number for the certificate. Default + is 1. :param passphrase: Password for the private key in sign_key_str. - :return: String representation of the signed certificate. + :return: String representation of the signed + certificate. """ ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, sign_cert_str) ca_key = None if passphrase is not None: - ca_key = crypto.load_privatekey(crypto.FILETYPE_PEM, sign_key_str, passphrase) + ca_key = crypto.load_privatekey(crypto.FILETYPE_PEM, sign_key_str, + passphrase) else: ca_key = crypto.load_privatekey(crypto.FILETYPE_PEM, sign_key_str) - req_cert = crypto.load_certificate_request(crypto.FILETYPE_PEM, request_cert_str) + req_cert = crypto.load_certificate_request(crypto.FILETYPE_PEM, + request_cert_str) cert = crypto.X509() cert.set_subject(req_cert.get_subject()) @@ -217,7 +262,8 @@ class OpenSSLWrapper(object): def verify_chain(self, cert_chain_str_list, cert_str): """ - :param cert_chain_str_list: Must be a list of certificate strings, where the first certificate to be validate + :param cert_chain_str_list: Must be a list of certificate strings, + where the first certificate to be validate is in the beginning and the root certificate is last. :param cert_str: The certificate to be validated. :return: @@ -229,7 +275,8 @@ class OpenSSLWrapper(object): else: cert_str = tmp_cert_str return (True, - "Signed certificate is valid and correctly signed by CA certificate.") + "Signed certificate is valid and correctly signed by CA " + "certificate.") def certificate_not_valid_yet(self, cert): starts_to_be_valid = dateutil.parser.parse(cert.get_notBefore()) @@ -243,18 +290,24 @@ class OpenSSLWrapper(object): """ Verifies if a certificate is valid and signed by a given certificate. - :param signing_cert_str: This certificate will be used to verify the signature. Must be a string representation - of the certificate. If you only have a file use the method read_str_from_file to + :param signing_cert_str: This certificate will be used to verify the + signature. Must be a string representation + of the certificate. If you only have a file + use the method read_str_from_file to get a string representation. - :param cert_str: This certificate will be verified if it is correct. Must be a string representation - of the certificate. If you only have a file use the method read_str_from_file to + :param cert_str: This certificate will be verified if it is + correct. Must be a string representation + of the certificate. If you only have a file + use the method read_str_from_file to get a string representation. :return: Valid, Message - Valid = True if the certificate is valid, otherwise false. + Valid = True if the certificate is valid, + otherwise false. Message = Why the validation failed. """ try: - ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, signing_cert_str) + ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, + signing_cert_str) cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_str) if self.certificate_not_valid_yet(ca_cert): @@ -270,7 +323,8 @@ class OpenSSLWrapper(object): return False, "The signed certificate is not valid yet." if ca_cert.get_subject().CN == cert.get_subject().CN: - return False, "CN may not be equal for CA certificate and the signed certificate." + return False, ("CN may not be equal for CA certificate and the " + "signed certificate.") cert_algorithm = cert.get_signature_algorithm() @@ -279,9 +333,9 @@ class OpenSSLWrapper(object): der_seq = asn1.DerSequence() der_seq.decode(cert_asn1) - cert_certificate=der_seq[0] + cert_certificate = der_seq[0] #cert_signature_algorithm=der_seq[1] - cert_signature=der_seq[2] + cert_signature = der_seq[2] cert_signature_decoded = asn1.DerObject() cert_signature_decoded.decode(cert_signature) @@ -289,12 +343,14 @@ class OpenSSLWrapper(object): signature_payload = cert_signature_decoded.payload if signature_payload[0] != '\x00': - return False, "The certificate should not contain any unused bits." + return (False, + "The certificate should not contain any unused bits.") signature = signature_payload[1:] try: - crypto.verify(ca_cert, signature, cert_certificate, cert_algorithm) + crypto.verify(ca_cert, signature, cert_certificate, + cert_algorithm) return True, "Signed certificate is valid and correctly signed by CA certificate." except crypto.Error, e: return False, "Certificate is incorrectly signed." diff --git a/src/saml2/server.py b/src/saml2/server.py index 89d0fdd..8199f24 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -521,7 +521,6 @@ class Server(Entity): try: _authn = authn - response = None if (sign_assertion or sign_response) and self.sec.cert_handler.generate_cert(): with self.lock: self.sec.cert_handler.update_cert(True) @@ -536,7 +535,8 @@ class Server(Entity): sign_assertion=sign_assertion, sign_response=sign_response, best_effort=best_effort, - encrypt_assertion=encrypt_assertion, encrypt_cert=encrypt_cert) + encrypt_assertion=encrypt_assertion, + encrypt_cert=encrypt_cert) return self._authn_response(in_response_to, # in_response_to destination, # consumer_url sp_entity_id, # sp_entity_id @@ -548,7 +548,8 @@ class Server(Entity): sign_assertion=sign_assertion, sign_response=sign_response, best_effort=best_effort, - encrypt_assertion=encrypt_assertion, encrypt_cert=encrypt_cert) + encrypt_assertion=encrypt_assertion, + encrypt_cert=encrypt_cert) except MissingValue, exc: return self.create_error_response(in_response_to, destination, diff --git a/src/saml2/sigver.py b/src/saml2/sigver.py index 0fa5ce4..57fa591 100644 --- a/src/saml2/sigver.py +++ b/src/saml2/sigver.py @@ -1011,6 +1011,7 @@ def security_context(conf, debug=None): tmp_key_file=conf.tmp_key_file, validate_certificate=conf.validate_certificate) + def encrypt_cert_from_item(item): _encrypt_cert = None try: @@ -1031,6 +1032,7 @@ def encrypt_cert_from_item(item): return None return _encrypt_cert + class CertHandlerExtra(object): def __init__(self): pass @@ -1488,7 +1490,8 @@ class SecurityContext(object): return self.correctly_signed_message(decoded_xml, "assertion", must, origdoc, only_valid_cert) - def correctly_signed_response(self, decoded_xml, must=False, origdoc=None,only_valid_cert=False, + def correctly_signed_response(self, decoded_xml, must=False, origdoc=None, + only_valid_cert=False, require_response_signature=False, **kwargs): """ Check if a instance is correctly signed, if we have metadata for the IdP that sent the info use that, if not use the key that are in From c5aa4a2adfd7eee02ed4a3c45f6d7730eaa8d6b2 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Mon, 24 Mar 2014 12:25:20 +0100 Subject: [PATCH 10/16] Added a method that checks any given return URL against what's registered in metadata. --- src/saml2/discovery.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/saml2/discovery.py b/src/saml2/discovery.py index 160b394..20a7e80 100644 --- a/src/saml2/discovery.py +++ b/src/saml2/discovery.py @@ -62,7 +62,8 @@ class DiscoveryServer(Entity): # ------------------------------------------------------------------------- - def create_discovery_service_response(self, return_url=None, + @staticmethod + def create_discovery_service_response(return_url=None, returnIDParam="entityID", entity_id=None, **kwargs): if return_url is None: @@ -87,3 +88,13 @@ class DiscoveryServer(Entity): return True return False + + def verify_return(self, entity_id, return_url): + for endp in self.metadata.discovery_response(entity_id): + try: + assert return_url.startswith(endp["location"]) + except AssertionError: + pass + else: + return True + return False From eeb4b5d694f4b0cce199a778a8f90f595cd51075 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Thu, 27 Mar 2014 11:12:41 +0100 Subject: [PATCH 11/16] Fixed a problem with filtering assertion by required/optional attributes. --- src/saml2/assertion.py | 42 ++++++++++++++++++----------- src/saml2/attribute_converter.py | 7 +++++ src/saml2/attributemaps/saml_uri.py | 1 + src/saml2/server.py | 1 + tools/update_metadata.sh | 1 + 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/saml2/assertion.py b/src/saml2/assertion.py index 1057ca4..0ed3057 100644 --- a/src/saml2/assertion.py +++ b/src/saml2/assertion.py @@ -24,7 +24,7 @@ import xmlenc from saml2 import saml from saml2.time_util import instant, in_a_while -from saml2.attribute_converter import from_local +from saml2.attribute_converter import from_local, get_local_name from saml2.s_utils import sid, MissingValue from saml2.s_utils import factory from saml2.s_utils import assertion_factory @@ -78,7 +78,7 @@ def _match(attr, ava): return None -def filter_on_attributes(ava, required=None, optional=None): +def filter_on_attributes(ava, required=None, optional=None, acs=None): """ Filter :param ava: An attribute value assertion as a dictionary @@ -98,18 +98,23 @@ def filter_on_attributes(ava, required=None, optional=None): nform = "" for nform in ["friendly_name", "name"]: try: - _fn = _match(attr[nform], ava) + _name = attr[nform] except KeyError: - pass - else: - if _fn: - try: - values = [av["text"] for av in attr["attribute_value"]] - except KeyError: - values = [] - res[_fn] = _filter_values(ava[_fn], values, True) - found = True - break + if nform == "friendly_name": + _name = get_local_name(acs, attr["name"], + attr["name_format"]) + else: + continue + + _fn = _match(_name, ava) + if _fn: + try: + values = [av["text"] for av in attr["attribute_value"]] + except KeyError: + values = [] + res[_fn] = _filter_values(ava[_fn], values, True) + found = True + break if not found: raise MissingValue("Required attribute missing: '%s'" % ( @@ -311,7 +316,8 @@ class Policy(object): self.compile(restrictions) else: self._restrictions = None - + self.acs = [] + def compile(self, restrictions): """ This is only for IdPs or AAs, and it's about limiting what is returned to the SP. @@ -484,7 +490,8 @@ class Policy(object): ava = filter_attribute_value_assertions(ava, _rest) if required or optional: - ava = filter_on_attributes(ava, required, optional) + logger.debug("required: %s, optional: %s" % (required, optional)) + ava = filter_on_attributes(ava, required, optional, self.acs) return ava @@ -540,7 +547,8 @@ class Assertion(dict): def __init__(self, dic=None): dict.__init__(self, dic) - + self.acs = [] + @staticmethod def _authn_context_decl(decl, authn_auth=None): """ @@ -727,6 +735,8 @@ class Assertion(dict): :param metadata: Metadata to use :return: The resulting AVA after the policy is applied """ + + policy.acs = self.acs ava = policy.restrict(self, sp_entity_id, metadata) self.update(ava) return ava \ No newline at end of file diff --git a/src/saml2/attribute_converter.py b/src/saml2/attribute_converter.py index 1e30ff4..49d00bf 100644 --- a/src/saml2/attribute_converter.py +++ b/src/saml2/attribute_converter.py @@ -255,6 +255,13 @@ def to_local_name(acs, attr): return attr.friendly_name +def get_local_name(acs, attr, name_format): + for aconv in acs: + #print ac.format, name_format + if aconv.name_format == name_format: + return aconv._fro[attr] + + def d_to_local_name(acs, attr): """ :param acs: List of AttributeConverter instances diff --git a/src/saml2/attributemaps/saml_uri.py b/src/saml2/attributemaps/saml_uri.py index 877ffc6..9d05b8a 100644 --- a/src/saml2/attributemaps/saml_uri.py +++ b/src/saml2/attributemaps/saml_uri.py @@ -177,6 +177,7 @@ MAP = { 'edupersonaffiliation': EDUPERSON_OID+'1', 'eduPersonPrincipalName': EDUPERSON_OID+'6', 'edupersonprincipalname': EDUPERSON_OID+'6', + 'eppn': EDUPERSON_OID+'6', 'localityName': X500ATTR_OID+'7', 'owner': X500ATTR_OID+'32', 'norEduOrgUnitUniqueNumber': NOREDUPERSON_OID+'2', diff --git a/src/saml2/server.py b/src/saml2/server.py index 8199f24..b9f20ed 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -308,6 +308,7 @@ class Server(Entity): #if identity: _issuer = self._issuer(issuer) ast = Assertion(identity) + ast.acs = self.config.getattr("attribute_converters", "idp") if policy is None: policy = Policy() try: diff --git a/tools/update_metadata.sh b/tools/update_metadata.sh index f09e5cc..aa6a023 100755 --- a/tools/update_metadata.sh +++ b/tools/update_metadata.sh @@ -1,2 +1,3 @@ +#!/bin/sh curl -O -G http://md.swamid.se/md/swamid-2.0.xml mdexport.py -t local -o swamid2.md swamid-2.0.xml From 5fe5c0d990ada195348a3b1495439e38dcd19640 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Fri, 28 Mar 2014 08:11:55 +0100 Subject: [PATCH 12/16] Updated the run script to work. --- example/run.sh | 12 +++++++++--- example/sp-wsgi/{conf.py => conf.py.example} | 2 -- 2 files changed, 9 insertions(+), 5 deletions(-) rename example/sp-wsgi/{conf.py => conf.py.example} (91%) diff --git a/example/run.sh b/example/run.sh index cdf4ce5..21c95d5 100755 --- a/example/run.sh +++ b/example/run.sh @@ -1,12 +1,18 @@ #!/bin/sh -cd sp -../../tools/make_metadata.py sp_conf > sp.xml +cd sp-wsgi +if [ ! -f conf.py ] ; then + cp conf.py.example conf.py +fi +../../tools/make_metadata.py conf > sp.xml cd ../idp2 +if [ ! -f idp_conf.py ] ; then + cp idp_conf.py.example conf.py +fi ../../tools/make_metadata.py idp_conf > idp.xml -cd ../sp +cd ../sp-wsgi ./sp.py sp_conf & cd ../idp2 diff --git a/example/sp-wsgi/conf.py b/example/sp-wsgi/conf.py.example similarity index 91% rename from example/sp-wsgi/conf.py rename to example/sp-wsgi/conf.py.example index b821e6d..14f9062 100644 --- a/example/sp-wsgi/conf.py +++ b/example/sp-wsgi/conf.py.example @@ -35,7 +35,5 @@ CONFIG = { "cert_file": "pki/mycert.pem", "xmlsec_binary": xmlsec_path, "metadata": {"local": ["../idp2/idp.xml"]}, - #"metadata": {"mdfile": ["./swamid2.md"]}, - #"metadata": {"local": ["./swamid-2.0.xml"]}, "name_form": NAME_FORMAT_URI, } From 692aaac27cece54949bd189be1f580c7374d999b Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Fri, 28 Mar 2014 08:21:46 +0100 Subject: [PATCH 13/16] Updated the run script to work. Also changed the name of the script and its working. Updated README. --- example/README | 6 +++++- example/all.sh | 37 +++++++++++++++++++++++++++++++++++++ example/run.sh | 22 ---------------------- 3 files changed, 42 insertions(+), 23 deletions(-) create mode 100755 example/all.sh delete mode 100755 example/run.sh diff --git a/example/README b/example/README index 8085301..2964918 100644 --- a/example/README +++ b/example/README @@ -21,6 +21,10 @@ To make it easy, for me :-), both the IdP and the SP uses the same keys. To run the setup do -./run.sh +./all.sh start and then use your favourit webbrowser to look at "http://localhost:8087/whoami" + +./all stop + +will of course stop your IdP and SP. \ No newline at end of file diff --git a/example/all.sh b/example/all.sh new file mode 100755 index 0000000..b58cdaf --- /dev/null +++ b/example/all.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +startme() { + cd sp-wsgi + if [ ! -f conf.py ] ; then + cp conf.py.example conf.py + fi + ../../tools/make_metadata.py conf > sp.xml + + cd ../idp2 + if [ ! -f idp_conf.py ] ; then + cp idp_conf.py.example conf.py + fi + ../../tools/make_metadata.py idp_conf > idp.xml + + cd ../sp-wsgi + ./sp.py conf & + + cd ../idp2 + ./idp.py idp_conf & + + cd .. +} + +stopme() { + pkill -f "sp.py" + pkill -f "idp.py" +} + +case "$1" in + start) startme ;; + stop) stopme ;; + restart) stopme; startme ;; + *) echo "usage: $0 start|stop|restart" >&2 + exit 1 + ;; +esac \ No newline at end of file diff --git a/example/run.sh b/example/run.sh deleted file mode 100755 index 21c95d5..0000000 --- a/example/run.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/sh - -cd sp-wsgi -if [ ! -f conf.py ] ; then - cp conf.py.example conf.py -fi -../../tools/make_metadata.py conf > sp.xml - -cd ../idp2 -if [ ! -f idp_conf.py ] ; then - cp idp_conf.py.example conf.py -fi -../../tools/make_metadata.py idp_conf > idp.xml - -cd ../sp-wsgi -./sp.py sp_conf & - -cd ../idp2 -./idp.py idp_conf & - -cd .. - From 2db8008701a05823be41d2321aa5e1f59df3b8ca Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Fri, 28 Mar 2014 09:39:36 +0100 Subject: [PATCH 14/16] More user friendly error message. --- src/saml2/assertion.py | 47 +++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/saml2/assertion.py b/src/saml2/assertion.py index 0ed3057..59cd621 100644 --- a/src/saml2/assertion.py +++ b/src/saml2/assertion.py @@ -93,32 +93,33 @@ def filter_on_attributes(ava, required=None, optional=None, acs=None): if required is None: required = [] + nform = "friendly_name" for attr in required: - found = False - nform = "" - for nform in ["friendly_name", "name"]: + try: + _name = attr[nform] + except KeyError: + if nform == "friendly_name": + _name = get_local_name(acs, attr["name"], + attr["name_format"]) + else: + continue + + _fn = _match(_name, ava) + if not _fn: # In the unlikely case that someone has provided us + # with URIs as attribute names + _fn = _match(attr["name"], ava) + + if _fn: try: - _name = attr[nform] + values = [av["text"] for av in attr["attribute_value"]] except KeyError: - if nform == "friendly_name": - _name = get_local_name(acs, attr["name"], - attr["name_format"]) - else: - continue - - _fn = _match(_name, ava) - if _fn: - try: - values = [av["text"] for av in attr["attribute_value"]] - except KeyError: - values = [] - res[_fn] = _filter_values(ava[_fn], values, True) - found = True - break - - if not found: - raise MissingValue("Required attribute missing: '%s'" % ( - attr[nform],)) + values = [] + res[_fn] = _filter_values(ava[_fn], values, True) + continue + else: + desc = "Required attribute missing: '%s' (%s)" % (attr["name"], + _name) + raise MissingValue(desc) if optional is None: optional = [] From cd24eced2107e518a753256bfc51ccdacf227d47 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Fri, 28 Mar 2014 20:13:54 +0100 Subject: [PATCH 15/16] Whatever is defined in the configuration is the default. --- src/saml2/client_base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index 2f57d14..d36b9cf 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -301,6 +301,9 @@ class Base(Entity): except KeyError: pass + if sign is None: + sign = self.authn_requests_signed + if (sign and self.sec.cert_handler.generate_cert()) or \ client_crt is not None: with self.lock: From c1eb135c73a9541d39326fa82a1bed1f69f7c489 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Sun, 30 Mar 2014 21:05:19 +0200 Subject: [PATCH 16/16] A 's' to much. --- src/saml2/client_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index d36b9cf..fdf700b 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -243,7 +243,7 @@ class Base(Entity): try: args["assertion_consumer_service_url"] = kwargs[ "assertion_consumer_service_url"] - del kwargs["assertion_consumer_service_urls"] + del kwargs["assertion_consumer_service_url"] except KeyError: try: args["attribute_consuming_service_index"] = str(kwargs[