From 4df48bc0e43bbfc3cc4afa6a66ef6abd8991eca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20Ho=CC=88rberg?= Date: Tue, 13 May 2014 12:01:43 +0200 Subject: [PATCH 1/8] Minor fix Not checking the configuration before verifying the signature. --- src/saml2/response.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/saml2/response.py b/src/saml2/response.py index 9194a47..f8e3e61 100644 --- a/src/saml2/response.py +++ b/src/saml2/response.py @@ -759,8 +759,9 @@ class AuthnResponse(StatusResponse): logger.debug("signed") try: - self.sec.check_signature(assertion, class_name(assertion), - self.xmlstr) + if self.require_signature: + self.sec.check_signature(assertion, class_name(assertion), + self.xmlstr) except Exception as exc: logger.error("correctly_signed_response: %s" % exc) raise From 291bf156d610728ebb3e4ff0cfd5356fba88f4d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20Ho=CC=88rberg?= Date: Tue, 3 Jun 2014 12:59:47 +0200 Subject: [PATCH 2/8] =?UTF-8?q?Fix=20f=C3=B6r=20assertion=20signature.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/saml2/response.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/saml2/response.py b/src/saml2/response.py index 75f7ede..e587c82 100644 --- a/src/saml2/response.py +++ b/src/saml2/response.py @@ -757,14 +757,13 @@ class AuthnResponse(StatusResponse): raise SignatureError("Signature missing for assertion") else: logger.debug("signed") - - try: - if self.require_signature: - self.sec.check_signature(assertion, class_name(assertion), - self.xmlstr) - except Exception as exc: - logger.error("correctly_signed_response: %s" % exc) - raise + try: + if self.require_signature: + self.sec.check_signature(assertion, class_name(assertion), + self.xmlstr) + except Exception as exc: + logger.error("correctly_signed_response: %s" % exc) + raise self.assertion = assertion logger.debug("assertion context: %s" % (self.context,)) From 6e0acc8997b8290ee7a03fbb3aa5a19ce1158e03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20Ho=CC=88rberg?= Date: Mon, 16 Mar 2015 08:38:49 +0100 Subject: [PATCH 3/8] Updated pysaml2 to support PEFIM. Added encrypted assertions with self contained namespaces. Added possibility to add two assertions. One encrypted with all the attributes and a second one that is not encrypted and contains nameid and authentication statement. --- example/sp-repoze/who.ini | 2 +- src/s2repoze/plugins/sp.py | 2 +- src/saml2/__init__.py | 88 +++++++++++++++++++++ src/saml2/client_base.py | 9 +-- src/saml2/entity.py | 12 ++- src/saml2/entity_category/at_egov_pvp2.py | 2 +- src/saml2/response.py | 2 +- src/saml2/server.py | 95 +++++++++++++++-------- 8 files changed, 169 insertions(+), 43 deletions(-) diff --git a/example/sp-repoze/who.ini b/example/sp-repoze/who.ini index 1ed329f..b116b99 100644 --- a/example/sp-repoze/who.ini +++ b/example/sp-repoze/who.ini @@ -17,7 +17,7 @@ saml_conf = sp_conf remember_name = auth_tkt sid_store = outstanding idp_query_param = IdPEntityId -discovery = http://130.239.201.5/role/idp.ds +#discovery = http://130.239.201.5/role/idp.ds [general] request_classifier = s2repoze.plugins.challenge_decider:my_request_classifier diff --git a/src/s2repoze/plugins/sp.py b/src/s2repoze/plugins/sp.py index a80dece..8c93b2a 100644 --- a/src/s2repoze/plugins/sp.py +++ b/src/s2repoze/plugins/sp.py @@ -337,7 +337,7 @@ class SAML2Plugin(object): element_to_extension_element(spcertenc)]) if _cli.authn_requests_signed: - _sid = saml2.s_utils.sid(_cli.seed) + _sid = saml2.s_utils.sid() req_id, msg_str = _cli.create_authn_request( dest, vorg=vorg_name, sign=_cli.authn_requests_signed, message_id=_sid, extensions=extensions) diff --git a/src/saml2/__init__.py b/src/saml2/__init__.py index db05547..0197d91 100644 --- a/src/saml2/__init__.py +++ b/src/saml2/__init__.py @@ -558,6 +558,94 @@ class SamlBase(ExtensionContainer): except ValueError: pass + def get_ns_map_attribute(self, attributes, uri_set): + for attribute in attributes: + if attribute[0] == "{": + uri, tag = attribute[1:].split("}") + uri_set.add(uri) + return uri_set + + def tag_get_uri(self, elem): + if elem.tag[0] == "{": + uri, tag = elem.tag[1:].split("}") + return uri + return None + def get_ns_map(self, elements, uri_set): + + for elem in elements: + uri_set = self.get_ns_map_attribute(elem.attrib, uri_set) + uri_set = self.get_ns_map(elem._children, uri_set) + uri = self.tag_get_uri(elem) + if uri is not None: + uri_set.add(uri) + return uri_set + + def get_prefix_map(self, elements): + uri_set = self.get_ns_map(elements, set()) + prefix_map = {} + for uri in sorted(uri_set): + prefix_map["encas%d" % len(prefix_map)] = uri + return prefix_map + + def get_xml_string_with_self_contained__assertion_within_encrypted_assertion(self, assertion_tag): + prefix_map = self.get_prefix_map([self.encrypted_assertion._to_element_tree().find(assertion_tag)]) + + tree = self._to_element_tree() + + self.set_prefixes(tree.find(self.encrypted_assertion._to_element_tree().tag).find(assertion_tag), prefix_map) + + return ElementTree.tostring(tree, encoding="UTF-8") + + + def set_prefixes(self, elem, prefix_map): + + # check if this is a tree wrapper + if not ElementTree.iselement(elem): + elem = elem.getroot() + + # build uri map and add to root element + uri_map = {} + for prefix, uri in prefix_map.items(): + uri_map[uri] = prefix + elem.set("xmlns:" + prefix, uri) + + # fixup all elements in the tree + memo = {} + for elem in elem.getiterator(): + self.fixup_element_prefixes(elem, uri_map, memo) + + + def fixup_element_prefixes(self, elem, uri_map, memo): + def fixup(name): + try: + return memo[name] + except KeyError: + if name[0] != "{": + return + uri, tag = name[1:].split("}") + if uri in uri_map: + new_name = uri_map[uri] + ":" + tag + memo[name] = new_name + return new_name + # fix element name + name = fixup(elem.tag) + if name: + elem.tag = name + # fix attribute names + for key, value in elem.items(): + name = fixup(key) + if name: + elem.set(name, value) + del elem.attrib[key] + + def to_string_force_namespace(self, nspair): + + elem = self._to_element_tree() + + self.set_prefixes(elem, nspair) + + return ElementTree.tostring(elem, encoding="UTF-8") + def to_string(self, nspair=None): """Converts the Saml object to a string containing XML. diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index a0e5e10..330e533 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -26,7 +26,7 @@ from saml2.soap import make_soap_enveloped_saml_thingy from urlparse import parse_qs -from saml2.s_utils import signature, UnravelError +from saml2.s_utils import signature, UnravelError, exception_trace from saml2.s_utils import do_attributes from saml2 import samlp, BINDING_SOAP, SAMLError @@ -580,13 +580,12 @@ class Base(Entity): logger.error("XML parse error: %s" % err) raise - #logger.debug(">> %s", resp) - if resp is None: return None elif isinstance(resp, AuthnResponse): - self.users.add_information_about_person(resp.session_info()) - logger.info("--- ADDED person info ----") + if resp.assertion is not None and len(resp.response.encrypted_assertion) == 0: + self.users.add_information_about_person(resp.session_info()) + logger.info("--- ADDED person info ----") pass else: logger.error("Response type not supported: %s" % ( diff --git a/src/saml2/entity.py b/src/saml2/entity.py index 8779b19..2f743df 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -63,6 +63,7 @@ from saml2.sigver import CryptoBackendXmlSec1 from saml2.sigver import make_temp from saml2.sigver import pre_encryption_part from saml2.sigver import pre_signature_part +from saml2.sigver import pre_encrypt_assertion from saml2.sigver import signed_instance_factory from saml2.virtual_org import VirtualOrg @@ -438,7 +439,7 @@ class Entity(HTTPBase): request_cls """ if not message_id: - message_id = sid(self.seed) + message_id = sid() for key, val in self.message_args(message_id).items(): if key not in kwargs: @@ -503,7 +504,7 @@ class Entity(HTTPBase): def _response(self, in_response_to, consumer_url=None, status=None, issuer=None, sign=False, to_sign=None, - encrypt_assertion=False, encrypt_cert=None, **kwargs): + encrypt_assertion=False, encrypt_assertion_self_contained=False, encrypt_cert=None, **kwargs): """ Create a Response. :param in_response_to: The session identifier of the request @@ -537,7 +538,13 @@ class Entity(HTTPBase): if sign: response.signature = pre_signature_part(response.id, self.sec.my_cert, 1) + sign_class = [(class_name(response), response.id)] cbxs = CryptoBackendXmlSec1(self.config.xmlsec_binary) + if encrypt_assertion_self_contained: + assertion_tag = response.assertion._to_element_tree().tag + response = pre_encrypt_assertion(response) + response = response.get_xml_string_with_self_contained__assertion_within_encrypted_assertion( + assertion_tag) _, cert_file = make_temp("%s" % encrypt_cert, decode=False) response = cbxs.encrypt_assertion(response, cert_file, pre_encryption_part()) @@ -547,7 +554,6 @@ class Entity(HTTPBase): signed_instance_factory(response, self.sec, to_sign) else: # default is to sign the whole response if anything - sign_class = [(class_name(response), response.id)] return signed_instance_factory(response, self.sec, sign_class) else: diff --git a/src/saml2/entity_category/at_egov_pvp2.py b/src/saml2/entity_category/at_egov_pvp2.py index ca1a23b..4a041c2 100644 --- a/src/saml2/entity_category/at_egov_pvp2.py +++ b/src/saml2/entity_category/at_egov_pvp2.py @@ -3,7 +3,7 @@ __author__ = 'rhoerbe' #2013-09-05 EGOVTOKEN = ["PVP-VERSION", - "PVP-PRINCIPALNAME", + "PVP-PRINCIPAL-NAME", "PVP-GIVENNAME", "PVP-BIRTHDATE", "PVP-USERID", diff --git a/src/saml2/response.py b/src/saml2/response.py index 4d0da44..0365881 100644 --- a/src/saml2/response.py +++ b/src/saml2/response.py @@ -820,7 +820,7 @@ class AuthnResponse(StatusResponse): raise Exception("No assertion part") res = [] - if self.response.encrypted_assertion: + if self.response.encrypted_assertion and key_file is not None and len(key_file) > 0: logger.debug("***Encrypted assertion/-s***") decr_text = self.sec.decrypt(self.xmlstr, key_file) resp = samlp.response_from_string(decr_text) diff --git a/src/saml2/server.py b/src/saml2/server.py index 9ae6bff..1737510 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -280,35 +280,8 @@ class Server(Entity): # ------------------------------------------------------------------------ - def _authn_response(self, in_response_to, consumer_url, - sp_entity_id, identity=None, name_id=None, - status=None, authn=None, issuer=None, policy=None, - sign_assertion=False, sign_response=False, - best_effort=False, encrypt_assertion=False, - encrypt_cert=None, authn_statement=None): - """ Create a response. A layer of indirection. - - :param in_response_to: The session identifier of the request - :param consumer_url: The URL which should receive the response - :param sp_entity_id: The entity identifier of the SP - :param identity: A dictionary with attributes and values that are - expected to be the bases for the assertion in the response. - :param name_id: The identifier of the subject - :param status: The status of the response - :param authn: A dictionary containing information about the - authn context. - :param issuer: The issuer of the response - :param sign_assertion: Whether the assertion should be signed or not - :param sign_response: Whether the response should be signed or not - :param best_effort: Even if not the SPs demands can be met send a - response. - :return: A response instance - """ - - to_sign = [] - args = {} - #if identity: - _issuer = self._issuer(issuer) + def setup_assertion(self, authn, sp_entity_id, in_response_to, consumer_url, name_id, policy, _issuer, + authn_statement, identity, best_effort, sign_response): ast = Assertion(identity) ast.acs = self.config.getattr("attribute_converters", "idp") if policy is None: @@ -342,6 +315,56 @@ class Server(Entity): consumer_url, name_id, self.config.attribute_converters, policy, issuer=_issuer) + return assertion + + def _authn_response(self, in_response_to, consumer_url, + sp_entity_id, identity=None, name_id=None, + status=None, authn=None, issuer=None, policy=None, + sign_assertion=False, sign_response=False, + best_effort=False, encrypt_assertion=False, + encrypt_cert=None, authn_statement=None, + encrypt_assertion_self_contained=False, show_nameid=False): + """ Create a response. A layer of indirection. + + :param in_response_to: The session identifier of the request + :param consumer_url: The URL which should receive the response + :param sp_entity_id: The entity identifier of the SP + :param identity: A dictionary with attributes and values that are + expected to be the bases for the assertion in the response. + :param name_id: The identifier of the subject + :param status: The status of the response + :param authn: A dictionary containing information about the + authn context. + :param issuer: The issuer of the response + :param sign_assertion: Whether the assertion should be signed or not + :param sign_response: Whether the response should be signed or not + :param best_effort: Even if not the SPs demands can be met send a + response. + :return: A response instance + """ + + to_sign = [] + args = {} + #if identity: + _issuer = self._issuer(issuer) + + if encrypt_assertion and show_nameid: + tmp_name_id = name_id + name_id = None + name_id = None + tmp_authn = authn + authn = None + tmp_authn_statement = authn_statement + authn_statement = None + + assertion = self.setup_assertion(authn, sp_entity_id, in_response_to, consumer_url, name_id, policy, + _issuer, authn_statement, identity, best_effort, sign_response) + assertion_only_nameid = None + + if encrypt_assertion and show_nameid: + assertion_only_nameid = self.setup_assertion(tmp_authn, sp_entity_id, in_response_to, consumer_url, + tmp_name_id, policy, _issuer, tmp_authn_statement, [], True, + sign_response) if sign_assertion is not None and sign_assertion: assertion.signature = pre_signature_part(assertion.id, @@ -358,12 +381,16 @@ class Server(Entity): args["assertion"] = assertion - if self.support_AssertionIDRequest() or self.support_AuthnQuery(): + if assertion_only_nameid is not None: + args["assertion_only_nameid"] = assertion_only_nameid + + if (self.support_AssertionIDRequest() or self.support_AuthnQuery()) and assertion_only_nameid is None: self.session_db.store_assertion(assertion, to_sign) return self._response(in_response_to, consumer_url, status, issuer, sign_response, to_sign, encrypt_assertion=encrypt_assertion, - encrypt_cert=encrypt_cert, **args) + encrypt_cert=encrypt_cert, + encrypt_assertion_self_contained=encrypt_assertion_self_contained, **args) # ------------------------------------------------------------------------ @@ -438,6 +465,8 @@ class Server(Entity): name_id=None, authn=None, issuer=None, sign_response=None, sign_assertion=None, encrypt_cert=None, encrypt_assertion=None, + encrypt_assertion_self_contained=False, + show_nameid=False, **kwargs): """ Constructs an AuthenticationResponse @@ -549,6 +578,8 @@ class Server(Entity): sign_response=sign_response, best_effort=best_effort, encrypt_assertion=encrypt_assertion, + encrypt_assertion_self_contained=encrypt_assertion_self_contained, + show_nameid=show_nameid, encrypt_cert=encrypt_cert) return self._authn_response(in_response_to, # in_response_to destination, # consumer_url @@ -562,6 +593,8 @@ class Server(Entity): sign_response=sign_response, best_effort=best_effort, encrypt_assertion=encrypt_assertion, + encrypt_assertion_self_contained=encrypt_assertion_self_contained, + show_nameid=show_nameid, encrypt_cert=encrypt_cert) except MissingValue, exc: From 15119f04dc49eea0bc91c7d43224843ab9b25899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20Ho=CC=88rberg?= Date: Mon, 16 Mar 2015 16:44:00 +0100 Subject: [PATCH 4/8] Updated pysaml2 to support PEFIM. Added encrypted assertions with self contained namespaces in the advice element. --- src/saml2/__init__.py | 14 +++++++++- src/saml2/assertion.py | 41 ++++++++++++++++------------ src/saml2/client_base.py | 4 ++- src/saml2/entity.py | 31 +++++++++++++++++---- src/saml2/response.py | 55 +++++++++++++++++++++++++++---------- src/saml2/server.py | 59 ++++++++++++++++++++++------------------ 6 files changed, 138 insertions(+), 66 deletions(-) diff --git a/src/saml2/__init__.py b/src/saml2/__init__.py index 0197d91..925480d 100644 --- a/src/saml2/__init__.py +++ b/src/saml2/__init__.py @@ -587,7 +587,19 @@ class SamlBase(ExtensionContainer): prefix_map["encas%d" % len(prefix_map)] = uri return prefix_map - def get_xml_string_with_self_contained__assertion_within_encrypted_assertion(self, assertion_tag): + def get_xml_string_with_self_contained_assertion_within_advice_encrypted_assertion(self, assertion_tag, advice_tag): + for tmp_encrypted_assertion in self.assertion.advice.encrypted_assertion: + prefix_map = self.get_prefix_map([tmp_encrypted_assertion._to_element_tree(). + find(assertion_tag)]) + + tree = self._to_element_tree() + + self.set_prefixes(tree.find(assertion_tag).find(advice_tag).find(tmp_encrypted_assertion._to_element_tree() + .tag).find(assertion_tag), prefix_map) + + return ElementTree.tostring(tree, encoding="UTF-8") + + def get_xml_string_with_self_contained_assertion_within_encrypted_assertion(self, assertion_tag): prefix_map = self.get_prefix_map([self.encrypted_assertion._to_element_tree().find(assertion_tag)]) tree = self._to_element_tree() diff --git a/src/saml2/assertion.py b/src/saml2/assertion.py index 0e66eb2..9a35e11 100644 --- a/src/saml2/assertion.py +++ b/src/saml2/assertion.py @@ -666,7 +666,7 @@ class Assertion(dict): name_id, attrconvs, policy, issuer, authn_class=None, authn_auth=None, authn_decl=None, encrypt=None, sec_context=None, authn_decl_ref=None, authn_instant="", - subject_locality="", authn_statem=None): + subject_locality="", authn_statem=None, add_subject=True): """ Construct the Assertion :param sp_entity_id: The entityid of the SP @@ -722,22 +722,29 @@ class Assertion(dict): else: _authn_statement = None - _ass = assertion_factory( - issuer=issuer, - conditions=conds, - subject=factory( - saml.Subject, - name_id=name_id, - subject_confirmation=[factory( - saml.SubjectConfirmation, - method=saml.SCM_BEARER, - subject_confirmation_data=factory( - saml.SubjectConfirmationData, - in_response_to=in_response_to, - recipient=consumer_url, - not_on_or_after=policy.not_on_or_after(sp_entity_id)))] - ), - ) + if not add_subject: + _ass = assertion_factory( + issuer=issuer, + conditions=conds, + subject=None + ) + else: + _ass = assertion_factory( + issuer=issuer, + conditions=conds, + subject=factory( + saml.Subject, + name_id=name_id, + subject_confirmation=[factory( + saml.SubjectConfirmation, + method=saml.SCM_BEARER, + subject_confirmation_data=factory( + saml.SubjectConfirmationData, + in_response_to=in_response_to, + recipient=consumer_url, + not_on_or_after=policy.not_on_or_after(sp_entity_id)))] + ), + ) if _authn_statement: _ass.authn_statement = [_authn_statement] diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index 330e533..10694bd 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -545,6 +545,8 @@ class Base(Entity): :param outstanding: A dictionary with session IDs as keys and the original web request from the user before redirection as values. + :param only_identity_in_encrypted_assertion: Must exist an assertion that is not encrypted that contains all + other information like subject and authentication statement. :return: An response.AuthnResponse or None """ @@ -565,7 +567,7 @@ class Base(Entity): "entity_id": self.config.entityid, "attribute_converters": self.config.attribute_converters, "allow_unknown_attributes": - self.config.allow_unknown_attributes, + self.config.allow_unknown_attributes } try: resp = self._parse_response(xmlstr, AuthnResponse, diff --git a/src/saml2/entity.py b/src/saml2/entity.py index 2f743df..d9e26e3 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -23,7 +23,7 @@ from saml2 import soap from saml2 import element_to_extension_element from saml2 import extension_elements_to_elements -from saml2.saml import NameID +from saml2.saml import NameID, EncryptedAssertion from saml2.saml import Issuer from saml2.saml import NAMEID_FORMAT_ENTITY from saml2.response import LogoutResponse @@ -504,7 +504,8 @@ class Entity(HTTPBase): def _response(self, in_response_to, consumer_url=None, status=None, issuer=None, sign=False, to_sign=None, - encrypt_assertion=False, encrypt_assertion_self_contained=False, encrypt_cert=None, **kwargs): + encrypt_assertion=False, encrypt_assertion_self_contained=False, encrypted_advice_attributes=False, + encrypt_cert=None, **kwargs): """ Create a Response. :param in_response_to: The session identifier of the request @@ -540,14 +541,31 @@ class Entity(HTTPBase): self.sec.my_cert, 1) sign_class = [(class_name(response), response.id)] cbxs = CryptoBackendXmlSec1(self.config.xmlsec_binary) - if encrypt_assertion_self_contained: + xnode_path = None + if encrypted_advice_attributes and encrypt_assertion_self_contained and \ + response.assertion.advice is not None and len(response.assertion.advice.assertion) == 1: + tmp_assertion = response.assertion.advice.assertion[0] + advice_tag = response.assertion.advice._to_element_tree().tag + assertion_tag = tmp_assertion._to_element_tree().tag + response.assertion.advice.encrypted_assertion = [] + response.assertion.advice.encrypted_assertion.append(EncryptedAssertion()) + if isinstance(tmp_assertion, list): + response.assertion.advice.encrypted_assertion[0].add_extension_elements(tmp_assertion) + else: + response.assertion.advice.encrypted_assertion[0].add_extension_element(tmp_assertion) + response.assertion.advice.assertion = [] + response = response.get_xml_string_with_self_contained_assertion_within_advice_encrypted_assertion( + assertion_tag, advice_tag) + node_xpath = ''.join(["/*[local-name()=\"%s\"]" % v for v in + ["Response", "Assertion", "Advice", "EncryptedAssertion", "Assertion"]]) + elif encrypt_assertion_self_contained: assertion_tag = response.assertion._to_element_tree().tag response = pre_encrypt_assertion(response) - response = response.get_xml_string_with_self_contained__assertion_within_encrypted_assertion( + response = response.get_xml_string_with_self_contained_assertion_within_encrypted_assertion( assertion_tag) _, cert_file = make_temp("%s" % encrypt_cert, decode=False) response = cbxs.encrypt_assertion(response, cert_file, - pre_encryption_part()) + pre_encryption_part(), node_xpath=node_xpath) # template(response.assertion.id)) if sign: if to_sign: @@ -949,6 +967,9 @@ class Entity(HTTPBase): response.in_response_to]["key"], decode=False) else: key_file = "" + only_identity_in_encrypted_assertion = False + if "only_identity_in_encrypted_assertion" in kwargs: + only_identity_in_encrypted_assertion = kwargs["only_identity_in_encrypted_assertion"] response = response.verify(key_file) if not response: diff --git a/src/saml2/response.py b/src/saml2/response.py index 0365881..51b14a4 100644 --- a/src/saml2/response.py +++ b/src/saml2/response.py @@ -622,24 +622,32 @@ class AuthnResponse(StatusResponse): attrlist = enc_attr.extensions_as_elements("Attribute", saml) attribute_statement.attribute.extend(attrlist) + def read_attribute_statement(self, attr_statem): + logger.debug("Attribute Statement: %s" % (attr_statem,)) + for aconv in self.attribute_converters: + logger.debug("Converts name format: %s" % (aconv.name_format,)) + + self.decrypt_attributes(attr_statem) + return to_local(self.attribute_converters, attr_statem, + self.allow_unknown_attributes) + def get_identity(self): """ The assertion can contain zero or one attributeStatements """ - if not self.assertion.attribute_statement: - logger.error("Missing Attribute Statement") - ava = {} - else: + ava = {} + if self.assertion.advice: + if self.assertion.advice.assertion: + for tmp_assertion in self.assertion.advice.assertion: + if tmp_assertion.attribute_statement: + assert len(tmp_assertion.attribute_statement) == 1 + ava.update(self.read_attribute_statement(tmp_assertion.attribute_statement[0])) + if self.assertion.attribute_statement: assert len(self.assertion.attribute_statement) == 1 _attr_statem = self.assertion.attribute_statement[0] - - logger.debug("Attribute Statement: %s" % (_attr_statem,)) - for aconv in self.attribute_converters: - logger.debug("Converts name format: %s" % (aconv.name_format,)) - - self.decrypt_attributes(_attr_statem) - ava = to_local(self.attribute_converters, _attr_statem, - self.allow_unknown_attributes) + ava.update(self.read_attribute_statement(_attr_statem)) + if not ava == 1: + logger.error("Missing Attribute Statement") return ava def _bearer_confirmed(self, data): @@ -820,17 +828,35 @@ class AuthnResponse(StatusResponse): raise Exception("No assertion part") res = [] - if self.response.encrypted_assertion and key_file is not None and len(key_file) > 0: + has_encrypted_assertions = self.response.encrypted_assertion + if not has_encrypted_assertions and self.response.assertion: + for tmp_assertion in self.response.assertion: + if tmp_assertion.advice: + if tmp_assertion.advice.encrypted_assertion: + has_encrypted_assertions = True + break + + if has_encrypted_assertions and key_file is not None and len(key_file) > 0: logger.debug("***Encrypted assertion/-s***") decr_text = self.sec.decrypt(self.xmlstr, key_file) resp = samlp.response_from_string(decr_text) res = self.decrypt_assertions(resp.encrypted_assertion, decr_text) + if resp.assertion: + for tmp_ass in resp.assertion: + if tmp_ass.advice and tmp_ass.advice.encrypted_assertion: + advice_res = self.decrypt_assertions(tmp_ass.advice.encrypted_assertion, decr_text) + if tmp_ass.advice.assertion: + tmp_ass.advice.assertion.extend(advice_res) + else: + tmp_ass.advice.assertion = advice_res + tmp_ass.advice.encrypted_assertion = [] + self.response.assertion = resp.assertion if self.response.assertion: self.response.assertion.extend(res) else: self.response.assertion = res - self.response.encrypted_assertion = [] self.xmlstr = decr_text + self.response.encrypted_assertion = [] if self.response.assertion: logger.debug("***Unencrypted assertion***") @@ -840,7 +866,6 @@ class AuthnResponse(StatusResponse): else: self.assertions.append(assertion) self.assertion = self.assertions[0] - return True def verify(self, key_file=""): diff --git a/src/saml2/server.py b/src/saml2/server.py index 1737510..818e394 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -281,7 +281,7 @@ class Server(Entity): # ------------------------------------------------------------------------ def setup_assertion(self, authn, sp_entity_id, in_response_to, consumer_url, name_id, policy, _issuer, - authn_statement, identity, best_effort, sign_response): + authn_statement, identity, best_effort, sign_response, add_subject=True): ast = Assertion(identity) ast.acs = self.config.getattr("attribute_converters", "idp") if policy is None: @@ -302,19 +302,19 @@ class Server(Entity): assertion = ast.construct(sp_entity_id, in_response_to, consumer_url, name_id, self.config.attribute_converters, - policy, issuer=_issuer, + policy, issuer=_issuer, add_subject=add_subject, **authn_args) elif authn_statement: # Got a complete AuthnStatement assertion = ast.construct(sp_entity_id, in_response_to, consumer_url, name_id, self.config.attribute_converters, policy, issuer=_issuer, - authn_statem=authn_statement) + authn_statem=authn_statement, add_subject=add_subject) else: assertion = ast.construct(sp_entity_id, in_response_to, consumer_url, name_id, self.config.attribute_converters, - policy, issuer=_issuer) + policy, issuer=_issuer, add_subject=add_subject) return assertion def _authn_response(self, in_response_to, consumer_url, @@ -323,7 +323,7 @@ class Server(Entity): sign_assertion=False, sign_response=False, best_effort=False, encrypt_assertion=False, encrypt_cert=None, authn_statement=None, - encrypt_assertion_self_contained=False, show_nameid=False): + encrypt_assertion_self_contained=False, encrypted_advice_attributes=False): """ Create a response. A layer of indirection. :param in_response_to: The session identifier of the request @@ -348,22 +348,29 @@ class Server(Entity): #if identity: _issuer = self._issuer(issuer) - if encrypt_assertion and show_nameid: - tmp_name_id = name_id - name_id = None - name_id = None - tmp_authn = authn - authn = None - tmp_authn_statement = authn_statement - authn_statement = None + #if encrypt_assertion and show_nameid: + # tmp_name_id = name_id + # name_id = None + # name_id = None + # tmp_authn = authn + # authn = None + # tmp_authn_statement = authn_statement + # authn_statement = None - assertion = self.setup_assertion(authn, sp_entity_id, in_response_to, consumer_url, name_id, policy, - _issuer, authn_statement, identity, best_effort, sign_response) - assertion_only_nameid = None + if encrypt_assertion and encrypted_advice_attributes: + assertion_attributes = self.setup_assertion(None, None, None, None, None, policy, + None, None, identity, best_effort, sign_response, False) + assertion = self.setup_assertion(authn, sp_entity_id, in_response_to, consumer_url, + name_id, policy, _issuer, authn_statement, [], True, + sign_response) + assertion.advice = saml.Advice() - if encrypt_assertion and show_nameid: - assertion_only_nameid = self.setup_assertion(tmp_authn, sp_entity_id, in_response_to, consumer_url, - tmp_name_id, policy, _issuer, tmp_authn_statement, [], True, + #assertion.advice.assertion_id_ref.append(saml.AssertionIDRef()) + #assertion.advice.assertion_uri_ref.append(saml.AssertionURIRef()) + assertion.advice.assertion.append(assertion_attributes) + else: + assertion = self.setup_assertion(authn, sp_entity_id, in_response_to, consumer_url, + name_id, policy, _issuer, authn_statement, identity, True, sign_response) if sign_assertion is not None and sign_assertion: @@ -381,16 +388,14 @@ class Server(Entity): args["assertion"] = assertion - if assertion_only_nameid is not None: - args["assertion_only_nameid"] = assertion_only_nameid - - if (self.support_AssertionIDRequest() or self.support_AuthnQuery()) and assertion_only_nameid is None: + if (self.support_AssertionIDRequest() or self.support_AuthnQuery()): self.session_db.store_assertion(assertion, to_sign) return self._response(in_response_to, consumer_url, status, issuer, sign_response, to_sign, encrypt_assertion=encrypt_assertion, encrypt_cert=encrypt_cert, - encrypt_assertion_self_contained=encrypt_assertion_self_contained, **args) + encrypt_assertion_self_contained=encrypt_assertion_self_contained, + encrypted_advice_attributes=encrypted_advice_attributes, **args) # ------------------------------------------------------------------------ @@ -466,7 +471,7 @@ class Server(Entity): sign_response=None, sign_assertion=None, encrypt_cert=None, encrypt_assertion=None, encrypt_assertion_self_contained=False, - show_nameid=False, + encrypted_advice_attributes=False, **kwargs): """ Constructs an AuthenticationResponse @@ -579,7 +584,7 @@ class Server(Entity): best_effort=best_effort, encrypt_assertion=encrypt_assertion, encrypt_assertion_self_contained=encrypt_assertion_self_contained, - show_nameid=show_nameid, + encrypted_advice_attributes=encrypted_advice_attributes, encrypt_cert=encrypt_cert) return self._authn_response(in_response_to, # in_response_to destination, # consumer_url @@ -594,7 +599,7 @@ class Server(Entity): best_effort=best_effort, encrypt_assertion=encrypt_assertion, encrypt_assertion_self_contained=encrypt_assertion_self_contained, - show_nameid=show_nameid, + encrypted_advice_attributes=encrypted_advice_attributes, encrypt_cert=encrypt_cert) except MissingValue, exc: From c4c5c224d47c3673e02092f48c63d7dc49ca9375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20Ho=CC=88rberg?= Date: Tue, 17 Mar 2015 11:23:58 +0100 Subject: [PATCH 5/8] Updated pysaml2 to support PEFIM. Added a decrypt flag so a proxy can choose not to decrypt an encrypted assertion. Fix so an signature on a response is always validated. Moved back to original solution. The only use case where the signature should be validated is if the proxy is transparent and the signature is designated for the Service Provider. This use case is no longer valid and if it is to be used a new flag must be created, like never_validate_signature. The default value of never_validate_signature is False. --- src/saml2/client_base.py | 5 +++-- src/saml2/entity.py | 5 ++++- src/saml2/response.py | 16 +++++++--------- tests/test_33_identifier.py | 4 ++++ 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index 10694bd..de43387 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -537,7 +537,7 @@ class Base(Entity): # ======== response handling =========== def parse_authn_request_response(self, xmlstr, binding, outstanding=None, - outstanding_certs=None): + outstanding_certs=None, decrypt=True): """ Deal with an AuthnResponse :param xmlstr: The reply as a xml string @@ -567,7 +567,8 @@ class Base(Entity): "entity_id": self.config.entityid, "attribute_converters": self.config.attribute_converters, "allow_unknown_attributes": - self.config.allow_unknown_attributes + self.config.allow_unknown_attributes, + "decrypt": decrypt } try: resp = self._parse_response(xmlstr, AuthnResponse, diff --git a/src/saml2/entity.py b/src/saml2/entity.py index e64554e..15174eb 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -973,7 +973,10 @@ class Entity(HTTPBase): only_identity_in_encrypted_assertion = False if "only_identity_in_encrypted_assertion" in kwargs: only_identity_in_encrypted_assertion = kwargs["only_identity_in_encrypted_assertion"] - response = response.verify(key_file) + decrypt = True + if "decrypt" in kwargs: + decrypt = kwargs["decrypt"] + response = response.verify(key_file, decrypt=decrypt) if not response: return None diff --git a/src/saml2/response.py b/src/saml2/response.py index 51b14a4..6f36819 100644 --- a/src/saml2/response.py +++ b/src/saml2/response.py @@ -395,7 +395,7 @@ class StatusResponse(object): def loads(self, xmldata, decode=True, origxml=None): return self._loads(xmldata, decode, origxml) - def verify(self, key_file=""): + def verify(self, key_file="", decrypt=True): try: return self._verify() except AssertionError: @@ -759,9 +759,7 @@ class AuthnResponse(StatusResponse): logger.debug("signed") if not verified: try: - if self.require_signature: - self.sec.check_signature(assertion, class_name(assertion), - self.xmlstr) + self.sec.check_signature(assertion, class_name(assertion),self.xmlstr) except Exception as exc: logger.error("correctly_signed_response: %s" % exc) raise @@ -816,7 +814,7 @@ class AuthnResponse(StatusResponse): res.append(assertion) return res - def parse_assertion(self, key_file=""): + def parse_assertion(self, key_file="", decrypt=True): if self.context == "AuthnQuery": # can contain one or more assertions pass @@ -836,7 +834,7 @@ class AuthnResponse(StatusResponse): has_encrypted_assertions = True break - if has_encrypted_assertions and key_file is not None and len(key_file) > 0: + if has_encrypted_assertions and decrypt: logger.debug("***Encrypted assertion/-s***") decr_text = self.sec.decrypt(self.xmlstr, key_file) resp = samlp.response_from_string(decr_text) @@ -868,7 +866,7 @@ class AuthnResponse(StatusResponse): self.assertion = self.assertions[0] return True - def verify(self, key_file=""): + def verify(self, key_file="", decrypt=True): """ Verify that the assertion is syntactically correct and the signature is correct if present. :param key_file: If not the default key file should be used this is it. @@ -886,7 +884,7 @@ class AuthnResponse(StatusResponse): if not isinstance(self.response, samlp.Response): return self - if self.parse_assertion(key_file): + if self.parse_assertion(key_file, decrypt=decrypt): return self else: logger.error("Could not parse the assertion") @@ -1114,7 +1112,7 @@ class AssertionIDResponse(object): return self._postamble() - def verify(self, key_file=""): + def verify(self, key_file="", decrypt=True): try: valid_instance(self.response) except NotValid as exc: diff --git a/tests/test_33_identifier.py b/tests/test_33_identifier.py index 14036b5..6db5a41 100644 --- a/tests/test_33_identifier.py +++ b/tests/test_33_identifier.py @@ -55,6 +55,10 @@ NAME_ID_POLICY_2 = """ class TestIdentifier(): def setup_class(self): + try: + os.remove("subject.db.db") + except: + pass self.id = IdentDB("subject.db", "example.com", "example") def test_persistent_1(self): From 73dfdc9603859dad9a3c7fe261ba53cc20a081c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20Ho=CC=88rberg?= Date: Wed, 18 Mar 2015 08:57:21 +0100 Subject: [PATCH 6/8] Added tests for encryption and signing of the authentication response. Added tests to decrypt authentication responses in the client. --- src/saml2/entity.py | 35 +-- src/saml2/response.py | 2 +- src/saml2/server.py | 8 +- tests/root_cert/localhost.ca.crt | 15 ++ tests/root_cert/localhost.ca.key | 15 ++ tests/test_50_server.py | 406 ++++++++++++++++++++++++++++++- tests/test_51_client.py | 84 ++++++- 7 files changed, 546 insertions(+), 19 deletions(-) create mode 100644 tests/root_cert/localhost.ca.crt create mode 100644 tests/root_cert/localhost.ca.key diff --git a/src/saml2/entity.py b/src/saml2/entity.py index 15174eb..26c2fe5 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -506,6 +506,13 @@ class Entity(HTTPBase): encrypt_assertion=False, encrypt_assertion_self_contained=False, encrypted_advice_attributes=False, encrypt_cert=None, **kwargs): """ Create a Response. + Encryption: + encrypt_assertion must be true for encryption to be performed. If encrypted_advice_attributes also is + true, then will the function try to encrypt the assertion in the the advice element of the main + assertion. Only one assertion element is allowed in the advice element, if multiple assertions exists + in the advice element the main assertion will be encrypted instead, since it's no point to encrypt + If encrypted_advice_attributes is + false the main assertion will be encrypted. Since the same key :param in_response_to: The session identifier of the request :param consumer_url: The URL which should receive the response @@ -535,17 +542,15 @@ class Entity(HTTPBase): return signed_instance_factory(response, self.sec, to_sign) if encrypt_assertion: + node_xpath = None if sign: response.signature = pre_signature_part(response.id, self.sec.my_cert, 1) sign_class = [(class_name(response), response.id)] cbxs = CryptoBackendXmlSec1(self.config.xmlsec_binary) - xnode_path = None - if encrypted_advice_attributes and encrypt_assertion_self_contained and \ - response.assertion.advice is not None and len(response.assertion.advice.assertion) == 1: + if encrypted_advice_attributes and response.assertion.advice is not None \ + and len(response.assertion.advice.assertion) == 1: tmp_assertion = response.assertion.advice.assertion[0] - advice_tag = response.assertion.advice._to_element_tree().tag - assertion_tag = tmp_assertion._to_element_tree().tag response.assertion.advice.encrypted_assertion = [] response.assertion.advice.encrypted_assertion.append(EncryptedAssertion()) if isinstance(tmp_assertion, list): @@ -553,26 +558,28 @@ class Entity(HTTPBase): else: response.assertion.advice.encrypted_assertion[0].add_extension_element(tmp_assertion) response.assertion.advice.assertion = [] - response = response.get_xml_string_with_self_contained_assertion_within_advice_encrypted_assertion( - assertion_tag, advice_tag) + if encrypt_assertion_self_contained: + advice_tag = response.assertion.advice._to_element_tree().tag + assertion_tag = tmp_assertion._to_element_tree().tag + response = response.get_xml_string_with_self_contained_assertion_within_advice_encrypted_assertion( + assertion_tag, advice_tag) node_xpath = ''.join(["/*[local-name()=\"%s\"]" % v for v in - ["Response", "Assertion", "Advice", "EncryptedAssertion", "Assertion"]]) + ["Response", "Assertion", "Advice", "EncryptedAssertion", "Assertion"]]) elif encrypt_assertion_self_contained: assertion_tag = response.assertion._to_element_tree().tag response = pre_encrypt_assertion(response) response = response.get_xml_string_with_self_contained_assertion_within_encrypted_assertion( assertion_tag) + else: + response = pre_encrypt_assertion(response) + if to_sign: + response = signed_instance_factory(response, self.sec, to_sign) _, cert_file = make_temp("%s" % encrypt_cert, decode=False) response = cbxs.encrypt_assertion(response, cert_file, pre_encryption_part(), node_xpath=node_xpath) # template(response.assertion.id)) if sign: - if to_sign: - signed_instance_factory(response, self.sec, to_sign) - else: - # default is to sign the whole response if anything - return signed_instance_factory(response, self.sec, - sign_class) + return signed_instance_factory(response, self.sec, sign_class) else: return response diff --git a/src/saml2/response.py b/src/saml2/response.py index 6f36819..bddc032 100644 --- a/src/saml2/response.py +++ b/src/saml2/response.py @@ -646,7 +646,7 @@ class AuthnResponse(StatusResponse): assert len(self.assertion.attribute_statement) == 1 _attr_statem = self.assertion.attribute_statement[0] ava.update(self.read_attribute_statement(_attr_statem)) - if not ava == 1: + if not ava: logger.error("Missing Attribute Statement") return ava diff --git a/src/saml2/server.py b/src/saml2/server.py index 818e394..24cc392 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -373,11 +373,17 @@ class Server(Entity): name_id, policy, _issuer, authn_statement, identity, True, sign_response) + to_sign = [] if sign_assertion is not None and sign_assertion: + if assertion.advice and assertion.advice.assertion: + for tmp_assertion in assertion.advice.assertion: + tmp_assertion.signature = pre_signature_part(tmp_assertion.id, self.sec.my_cert, 1) + to_sign.append((class_name(tmp_assertion), tmp_assertion.id)) assertion.signature = pre_signature_part(assertion.id, self.sec.my_cert, 1) # Just the assertion or the response and the assertion ? - to_sign = [(class_name(assertion), assertion.id)] + to_sign.append((class_name(assertion), assertion.id)) + # Store which assertion that has been sent to which SP about which # subject. diff --git a/tests/root_cert/localhost.ca.crt b/tests/root_cert/localhost.ca.crt new file mode 100644 index 0000000..c7faff9 --- /dev/null +++ b/tests/root_cert/localhost.ca.crt @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICSTCCAbICAQEwDQYJKoZIhvcNAQELBQAwbTELMAkGA1UEBhMCc2UxCzAJBgNV +BAgTAmFjMQ0wCwYDVQQHEwR1bWVhMRwwGgYDVQQKExNJVFMgVW1lYSBVbml2ZXJz +aXR5MQ0wCwYDVQQLEwRESVJHMRUwEwYDVQQDEwxsb2NhbGhvc3QuY2EwHhcNMTQw +MzE0MDczNDIwWhcNMjQwMzExMDczNDIwWjBtMQswCQYDVQQGEwJzZTELMAkGA1UE +CBMCYWMxDTALBgNVBAcTBHVtZWExHDAaBgNVBAoTE0lUUyBVbWVhIFVuaXZlcnNp +dHkxDTALBgNVBAsTBERJUkcxFTATBgNVBAMTDGxvY2FsaG9zdC5jYTCBnzANBgkq +hkiG9w0BAQEFAAOBjQAwgYkCgYEAzJseOYg+hKGsnGWilv9FrfH2csQ9UZGwgwHz +zR2IquQg/+nONxd4MIwjOnQrLOJuhpu55ZTSpeT901GNDLj4xPQnFrWWyET8NxZg +w7Ilra55iQNaoUWpdi0JQSXI/9CY8t+Y170+7DfBJ6zo4y6+HKaOLNxZy4IbB0SE +aZBGsrUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQAEOQJkD+5fb2mTtxwaZeRyQN9c +If0Xd2E7Z6BstGCUMWa/q4FrNee324kINVsFYg0GBTBwfyYPwR7I70LGjS3gXEzd +RjGx/Z8yvHJyavJj8iRCOflQ0fUlwasFYtrJesPOfM+aJ05Jb8GelUxpKh6BtPlE +IU0FLm//i9ucLQ9zBg== +-----END CERTIFICATE----- diff --git a/tests/root_cert/localhost.ca.key b/tests/root_cert/localhost.ca.key new file mode 100644 index 0000000..a588e7a --- /dev/null +++ b/tests/root_cert/localhost.ca.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDMmx45iD6EoaycZaKW/0Wt8fZyxD1RkbCDAfPNHYiq5CD/6c43 +F3gwjCM6dCss4m6Gm7nllNKl5P3TUY0MuPjE9CcWtZbIRPw3FmDDsiWtrnmJA1qh +Ral2LQlBJcj/0Jjy35jXvT7sN8EnrOjjLr4cpo4s3FnLghsHRIRpkEaytQIDAQAB +AoGAMw6aUjz/bNVzX2u1UPzOhIOWvjjeHFbAt1BraEnwasSWv4W2oeTHZ0XxHIsU +oxS2A/0kPHgQwLkN5ge5rO0TlpAI5X9ZqlJ0SXF5zjJOBtyK6TWoUbwnyzS7lbFC +q9AVrHwMX9uNCboccqzjrzHyNE+4/QT7z2G5AMzjfq+5EwECQQDvOJuUl2pbUUIK +nMCmwkARFEZzZYV2oIBDsagTG8gX7glj5stoYXuez8EnYtNHRDBConyKqruuzqJk +qSKlha7hAkEA2vT4CpAzHSCknwQKXmFwBD5hVBrv+JZSur6XpqEdwXkX2osScAaW +xj3vQEQorJC2CryvUVOTeuFoog0f+6HiVQJBAMs0dMQ2ErxbPBQzr1p4K1/Wrzmb +BVINaKcYJEOHF+Nr6kIYbLTQCeiPZe4E/p/NBomz6MMJ4L/O+xcyrSGZe0ECQQCZ +ejELpnxNpH4AAKML+Ry9vMQYYjFnfGdNAx/l6vWikjEIPYeVAulY2D0GPUCNhXo1 +GIGDbiPodGwVe0G57oVpAkBjckA1LEE1Kzkq5sR9U9t9m+3WSBvMZxLUwJYdVmOY +Y6xKVtJfe9XuQt9tzoWQW8iieWyKOw7yLYCBcjns2iZn +-----END RSA PRIVATE KEY----- diff --git a/tests/test_50_server.py b/tests/test_50_server.py index af2290b..50479ee 100644 --- a/tests/test_50_server.py +++ b/tests/test_50_server.py @@ -1,9 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- import base64 +import os from contextlib import closing from urlparse import parse_qs -from saml2.sigver import pre_encryption_part +import uuid + +from saml2.cert import OpenSSLWrapper +from saml2.sigver import pre_encryption_part, make_temp from saml2.assertion import Policy from saml2.authn_context import INTERNETPROTOCOLPASSWORD from saml2.saml import NameID, NAMEID_FORMAT_TRANSIENT @@ -41,6 +45,40 @@ def _eq(l1, l2): return set(l1) == set(l2) +BASEDIR = os.path.abspath(os.path.dirname(__file__)) + + +def get_ava(assertion): + ava = {} + for statement in assertion.attribute_statement: + for attr in statement.attribute: + value = [] + for tmp_val in attr.attribute_value: + value.append(tmp_val.text) + key = attr.friendly_name + if key is None or len(key) == 0: + key = attr.text + ava[key] = value + return ava + + +def generate_cert(): + sn = uuid.uuid4().urn + cert_info = { + "cn": "localhost", + "country_code": "se", + "state": "ac", + "city": "Umea", + "organization": "ITS", + "organization_unit": "DIRG" + } + osw = OpenSSLWrapper() + ca_cert_str = osw.read_str_from_file("/Users/haho0032/Develop/root_cert/localhost.ca.crt") + ca_key_str = osw.read_str_from_file("/Users/haho0032/Develop/root_cert/localhost.ca.key") + req_cert_str, req_key_str = osw.create_certificate(cert_info, request=True, sn=sn, key_length=2048) + cert_str = osw.create_cert_signed_certificate(ca_cert_str, ca_key_str, req_cert_str) + return cert_str, req_key_str + class TestServer1(): def setup_class(self): self.server = Server("idp_conf") @@ -372,6 +410,371 @@ class TestServer1(): # value. Just that there should be one assert assertion.signature.signature_value.text != "" + + def test_encrypted_signed_response_1(self): + name_id = self.server.ident.transient_nameid( + "urn:mace:example.com:saml:roland:sp", "id12") + ava = {"givenName": ["Derek"], "surName": ["Jeter"], + "mail": ["derek@nyy.mlb.com"], "title": "The man"} + + cert_str, cert_key_str = generate_cert() + + signed_resp = self.server.create_authn_response( + ava, + "id12", # in_response_to + "http://lingon.catalogix.se:8087/", # consumer_url + "urn:mace:example.com:saml:roland:sp", # sp_entity_id + name_id=name_id, + sign_response=True, + sign_assertion=True, + encrypt_assertion=True, + encrypt_assertion_self_contained=True, + encrypted_advice_attributes=True, + encrypt_cert=cert_str, + ) + + sresponse = response_from_string(signed_resp) + + #'urn:oasis:names:tc:SAML:2.0:protocol:AuthnRequest' + + valid = self.server.sec.verify_signature(signed_resp, + self.server.config.cert_file, + node_name='urn:oasis:names:tc:SAML:2.0:protocol:Response', + node_id=sresponse.id, + id_attr="") + assert valid + + _, key_file = make_temp("%s" % cert_key_str, decode=False) + + decr_text = self.server.sec.decrypt(signed_resp, key_file) + + resp = samlp.response_from_string(decr_text) + + #Do not work since the response is changed after the signature is created. + valid = self.server.sec.verify_signature(decr_text, + self.server.config.cert_file, + node_name='urn:oasis:names:tc:SAML:2.0:assertion:Assertion', + node_id=resp.assertion[0].id, + id_attr="") + assert valid + + assert resp.assertion[0].advice.encrypted_assertion[0].extension_elements + + assertion = extension_elements_to_elements(resp.assertion[0].advice.encrypted_assertion[0].extension_elements, + [saml, samlp]) + assert assertion + assert assertion[0].attribute_statement + + ava = ava = get_ava(assertion[0]) + + assert ava ==\ + {'mail': ['derek@nyy.mlb.com'], 'givenname': ['Derek'], 'surname': ['Jeter'], 'title': ['The man']} + + #Should work, but I suspect that xmlsec manipulates the xml to much while encrypting that the signature + #is no longer working. :( + + assert 'EncryptedAssertion> Date: Wed, 18 Mar 2015 09:05:03 +0100 Subject: [PATCH 7/8] Removed comments. --- tests/test_50_server.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tests/test_50_server.py b/tests/test_50_server.py index 50479ee..4f3c414 100644 --- a/tests/test_50_server.py +++ b/tests/test_50_server.py @@ -435,8 +435,6 @@ class TestServer1(): sresponse = response_from_string(signed_resp) - #'urn:oasis:names:tc:SAML:2.0:protocol:AuthnRequest' - valid = self.server.sec.verify_signature(signed_resp, self.server.config.cert_file, node_name='urn:oasis:names:tc:SAML:2.0:protocol:Response', @@ -450,7 +448,6 @@ class TestServer1(): resp = samlp.response_from_string(decr_text) - #Do not work since the response is changed after the signature is created. valid = self.server.sec.verify_signature(decr_text, self.server.config.cert_file, node_name='urn:oasis:names:tc:SAML:2.0:assertion:Assertion', @@ -470,9 +467,6 @@ class TestServer1(): assert ava ==\ {'mail': ['derek@nyy.mlb.com'], 'givenname': ['Derek'], 'surname': ['Jeter'], 'title': ['The man']} - #Should work, but I suspect that xmlsec manipulates the xml to much while encrypting that the signature - #is no longer working. :( - assert 'EncryptedAssertion> Date: Thu, 19 Mar 2015 16:54:11 +0100 Subject: [PATCH 8/8] Added sp entity id for assertion with only attributes in advice element. Without sp entity id the ec filtering was not performed. --- src/saml2/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/saml2/server.py b/src/saml2/server.py index 24cc392..c6d3169 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -358,7 +358,7 @@ class Server(Entity): # authn_statement = None if encrypt_assertion and encrypted_advice_attributes: - assertion_attributes = self.setup_assertion(None, None, None, None, None, policy, + assertion_attributes = self.setup_assertion(None, sp_entity_id, None, None, None, policy, None, None, identity, best_effort, sign_response, False) assertion = self.setup_assertion(authn, sp_entity_id, in_response_to, consumer_url, name_id, policy, _issuer, authn_statement, [], True,