From 80f94a997ca7e9f708cd8536460d7549d386f912 Mon Sep 17 00:00:00 2001 From: peppelinux Date: Sat, 7 Nov 2020 14:43:29 +0100 Subject: Configurable signing and digest alg --- docs/howto/config.rst | 19 +++++++++++++++++++ src/saml2/client_base.py | 8 +++++++- src/saml2/config.py | 4 ++++ src/saml2/entity.py | 4 ++++ src/saml2/server.py | 9 +++++++-- 5 files changed, 41 insertions(+), 3 deletions(-) diff --git a/docs/howto/config.rst b/docs/howto/config.rst index 88b0f6fa..3e0ec06d 100644 --- a/docs/howto/config.rst +++ b/docs/howto/config.rst @@ -247,6 +247,7 @@ The globally unique identifier of the entity. .. note:: It is recommended that the entityid should point to a real webpage where the metadata for the entity can be found. + key_file ^^^^^^^^ @@ -1013,6 +1014,23 @@ Example:: } } + +signing_algorithm +""""""""""""""""" + +Default algorithm to be used. Example:: + + 'signing_algorithm': saml2.xmldsig.SIG_RSA_SHA256, + + +digest_algorithm +""""""""""""""""" + +Default algorithm to be used. Example:: + + 'digest_algorithm': saml2.xmldsig.DIGEST_SHA256, + + logout_responses_signed """"""""""""""""""""""" @@ -1031,6 +1049,7 @@ Example:: } } + subject_data """""""""""" diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index 889c4359..03ea7bca 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -186,6 +186,10 @@ class Base(Entity): setattr(self, attr, val) + # signing and digest algs + self.signing_algorithm = self.config.getattr('signing_algorithm', "sp") + self.digest_algorithm = self.config.getattr('digest_algorithm', "sp") + if self.entity_type == "sp" and not any( [ self.want_assertions_signed, @@ -234,8 +238,10 @@ class Base(Entity): raise IdpUnspecified("Too many IdPs to choose from: %s" % eids) try: - srvs = self.metadata.single_sign_on_service(list(eids.keys())[0], binding) + srvs = self.metadata.single_sign_on_service(list(eids.keys())[0], + binding) return next(locations(srvs), None) + except IndexError: raise IdpUnspecified("No IdP to send to given the premises") diff --git a/src/saml2/config.py b/src/saml2/config.py index f28d7748..8b865dcb 100644 --- a/src/saml2/config.py +++ b/src/saml2/config.py @@ -76,6 +76,8 @@ COMMON_ARGS = [ "metadata", "ui_info", "name_id_format", + "signing_algorithm", + "digest_algorithm", ] SP_ARGS = [ @@ -225,6 +227,8 @@ class Config(object): self.attribute_profile = [] self.requested_attribute_name_format = NAME_FORMAT_URI self.delete_tmpfiles = True + self.signing_algorithm = None + self.digest_algorithm = None def setattr(self, context, attr, val): if context == "": diff --git a/src/saml2/entity.py b/src/saml2/entity.py index fdea5a74..8e6680b5 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -453,6 +453,10 @@ class Entity(HTTPBase): sign_alg=None, digest_alg=None, ): + # sign adn digest algs + sign_alg = sign_alg or self.signing_algorithm + digest_alg = digest_alg or self.digest_algorithm + if msg.signature is None: msg.signature = pre_signature_part( msg.id, self.sec.my_cert, 1, sign_alg=sign_alg, digest_alg=digest_alg diff --git a/src/saml2/server.py b/src/saml2/server.py index bcdbd2bb..519f6db1 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -524,7 +524,8 @@ class Server(Entity): if not name_id and userid: try: - name_id = self.ident.construct_nameid(userid, policy, sp_entity_id) + name_id = self.ident.construct_nameid(userid, policy, + sp_entity_id) logger.warning("Unspecified NameID format") except Exception: pass @@ -593,7 +594,11 @@ class Server(Entity): args['best_effort'] = kwargs["best_effort"] except KeyError: args['best_effort'] = False - + + # signing and digest algs + self.signing_algorithm = self.config.getattr('signing_algorithm', "idp") + self.digest_algorithm = self.config.getattr('digest_algorithm', "idp") + for param in ['sign_assertion', 'sign_response', 'encrypt_assertion', 'encrypt_assertion_self_contained', 'encrypted_advice_attributes', 'encrypt_cert_advice', -- cgit v1.2.1 From fb86347e5168af27ed5e729829f175ae17f51282 Mon Sep 17 00:00:00 2001 From: Ivan Kanakarakis Date: Sun, 22 Nov 2020 22:26:39 +0200 Subject: Fix formatting Signed-off-by: Ivan Kanakarakis --- src/saml2/client_base.py | 17 +++--- src/saml2/entity.py | 20 ++++--- src/saml2/server.py | 132 ++++++++++++++++++++++++++--------------------- 3 files changed, 93 insertions(+), 76 deletions(-) diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index 03ea7bca..0842453c 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -171,19 +171,17 @@ class Base(Entity): "authn_requests_signed": False, "want_assertions_signed": False, "want_response_signed": True, - "want_assertions_or_response_signed" : False + "want_assertions_or_response_signed": False, } - for attr, val_default in attribute_defaults.items(): val_config = self.config.getattr(attr, "sp") - if val_config is None: - val = val_default - else: - val = val_config - + val = ( + val_config + if val_config is not None + else val_default + ) if val == 'true': val = True - setattr(self, attr, val) # signing and digest algs @@ -238,8 +236,7 @@ class Base(Entity): raise IdpUnspecified("Too many IdPs to choose from: %s" % eids) try: - srvs = self.metadata.single_sign_on_service(list(eids.keys())[0], - binding) + srvs = self.metadata.single_sign_on_service(list(eids.keys())[0], binding) return next(locations(srvs), None) except IndexError: diff --git a/src/saml2/entity.py b/src/saml2/entity.py index 8e6680b5..bff8db8d 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -800,8 +800,9 @@ class Entity(HTTPBase): return response if sign: - return self.sign(response, to_sign=to_sign, sign_alg=sign_alg, - digest_alg=digest_alg) + return self.sign( + response, to_sign=to_sign, sign_alg=sign_alg, digest_alg=digest_alg + ) else: return response @@ -835,8 +836,7 @@ class Entity(HTTPBase): status=status, **kwargs) if sign: - return self.sign(response, mid, sign_alg=sign_alg, - digest_alg=digest_alg) + return self.sign(response, mid, sign_alg=sign_alg, digest_alg=digest_alg) else: return response @@ -1121,9 +1121,15 @@ class Entity(HTTPBase): rinfo = self.response_args(request, bindings) - response = self._status_response(samlp.ManageNameIDResponse, issuer, - status, sign, sign_alg=sign_alg, - digest_alg=digest_alg, **rinfo) + response = self._status_response( + samlp.ManageNameIDResponse, + issuer, + status, + sign, + sign_alg=sign_alg, + digest_alg=digest_alg, + **rinfo, + ) logger.info("Response: %s", response) diff --git a/src/saml2/server.py b/src/saml2/server.py index 519f6db1..68e04e27 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -384,17 +384,32 @@ class Server(Entity): **kwargs) 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_advice=None, encrypt_cert_assertion=None, - authn_statement=None, - encrypt_assertion_self_contained=False, - encrypted_advice_attributes=False, - pefim=False, sign_alg=None, digest_alg=None, - farg=None, session_not_on_or_after=None): + 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_advice=None, + encrypt_cert_assertion=None, + authn_statement=None, + encrypt_assertion_self_contained=False, + encrypted_advice_attributes=False, + pefim=False, + sign_alg=None, + digest_alg=None, + farg=None, + session_not_on_or_after=None, + ): """ Create a response. A layer of indirection. :param in_response_to: The session identifier of the request @@ -524,8 +539,7 @@ class Server(Entity): if not name_id and userid: try: - name_id = self.ident.construct_nameid(userid, policy, - sp_entity_id) + name_id = self.ident.construct_nameid(userid, policy, sp_entity_id) logger.warning("Unspecified NameID format") except Exception: pass @@ -565,56 +579,53 @@ class Server(Entity): if sp_entity_id: kwargs['sp_entity_id'] = sp_entity_id - return self._response(in_response_to, destination, status, issuer, - sign_response, to_sign, sign_alg=sign_alg, - digest_alg=digest_alg, **kwargs) + return self._response( + in_response_to, + destination, + status, + issuer, + sign_response, + to_sign, + sign_alg=sign_alg, + digest_alg=digest_alg, + **kwargs, + ) + + def gather_authn_response_args( + self, sp_entity_id, name_id_policy, userid, **kwargs + ): + # collect args and return them + args = {} - # ------------------------------------------------------------------------ + args["policy"] = kwargs.get( + "release_policy", self.config.getattr("policy", "idp") + ) + args['best_effort'] = kwargs.get("best_effort", False) - def gather_authn_response_args(self, sp_entity_id, name_id_policy, userid, - **kwargs): - param_default = { + param_defaults = { 'sign_assertion': False, 'sign_response': False, 'encrypt_assertion': False, 'encrypt_assertion_self_contained': True, 'encrypted_advice_attributes': False, 'encrypt_cert_advice': None, - 'encrypt_cert_assertion': None + 'encrypt_cert_assertion': None, } - args = {} - - try: - args["policy"] = kwargs["release_policy"] - except KeyError: - args["policy"] = self.config.getattr("policy", "idp") - - try: - args['best_effort'] = kwargs["best_effort"] - except KeyError: - args['best_effort'] = False - # signing and digest algs self.signing_algorithm = self.config.getattr('signing_algorithm', "idp") self.digest_algorithm = self.config.getattr('digest_algorithm', "idp") - - for param in ['sign_assertion', 'sign_response', 'encrypt_assertion', - 'encrypt_assertion_self_contained', - 'encrypted_advice_attributes', 'encrypt_cert_advice', - 'encrypt_cert_assertion']: - try: - _val = kwargs[param] - except KeyError: - _val = None - - if _val is None: - _val = self.config.getattr(param, "idp") - if _val is None: - args[param] = param_default[param] - else: - args[param] = _val + for param, val_default in param_defaults.items(): + val_kw = kwargs.get(param) + val_config = self.config.getattr(param, "idp") + args[param] = ( + val_kw + if val_kw is not None + else val_config + if val_config is not None + else val_default + ) for arg, attr, eca, pefim in [ ('encrypted_advice_attributes', 'verify_encrypt_cert_advice', @@ -698,7 +709,7 @@ class Server(Entity): sign_alg=None, digest_alg=None, session_not_on_or_after=None, - **kwargs + **kwargs, ): """ Constructs an AuthenticationResponse @@ -733,21 +744,24 @@ class Server(Entity): try: args = self.gather_authn_response_args( - sp_entity_id, name_id_policy=name_id_policy, userid=userid, - name_id=name_id, sign_response=sign_response, + sp_entity_id, + name_id_policy=name_id_policy, + userid=userid, + name_id=name_id, + sign_response=sign_response, sign_assertion=sign_assertion, encrypt_cert_advice=encrypt_cert_advice, encrypt_cert_assertion=encrypt_cert_assertion, encrypt_assertion=encrypt_assertion, - encrypt_assertion_self_contained - =encrypt_assertion_self_contained, + encrypt_assertion_self_contained=encrypt_assertion_self_contained, encrypted_advice_attributes=encrypted_advice_attributes, - pefim=pefim, **kwargs) + pefim=pefim, + **kwargs, + ) except IOError as exc: - response = self.create_error_response(in_response_to, - destination, - sp_entity_id, - exc, name_id) + response = self.create_error_response( + in_response_to, destination, sp_entity_id, exc, name_id + ) return ("%s" % response).split("\n") try: -- cgit v1.2.1 From c0410837a5ee8c5c1fe656c501aa640c57000b59 Mon Sep 17 00:00:00 2001 From: Ivan Kanakarakis Date: Sun, 22 Nov 2020 22:26:50 +0200 Subject: WIP works good - set on init use on create_ Signed-off-by: Ivan Kanakarakis --- src/saml2/client.py | 2 ++ src/saml2/client_base.py | 24 +++++++++++++----------- src/saml2/entity.py | 48 ++++++++++++++++++++++++++++++------------------ src/saml2/server.py | 41 ++++++++++++++++++++++++----------------- src/saml2/sigver.py | 6 +----- 5 files changed, 70 insertions(+), 51 deletions(-) diff --git a/src/saml2/client.py b/src/saml2/client.py index 9809150e..f5033ba2 100644 --- a/src/saml2/client.py +++ b/src/saml2/client.py @@ -201,6 +201,8 @@ class Saml2Client(Base): return self.do_logout(name_id, entity_ids, reason, expire, sign, sign_alg=sign_alg, digest_alg=digest_alg) + # XXX calls DefaultSignature + # XXX calls self.sign def do_logout(self, name_id, entity_ids, reason, expire, sign=None, expected_binding=None, sign_alg=None, digest_alg=None, **kwargs): diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index 0842453c..dd3ddfdf 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -56,7 +56,7 @@ from saml2 import BINDING_PAOS from saml2.xmldsig import SIG_ALLOWED_ALG from saml2.xmldsig import DIGEST_ALLOWED_ALG -from saml2.xmldsig import DefaultSignature + logger = logging.getLogger(__name__) @@ -184,10 +184,6 @@ class Base(Entity): val = True setattr(self, attr, val) - # signing and digest algs - self.signing_algorithm = self.config.getattr('signing_algorithm', "sp") - self.digest_algorithm = self.config.getattr('digest_algorithm', "sp") - if self.entity_type == "sp" and not any( [ self.want_assertions_signed, @@ -287,6 +283,7 @@ class Base(Entity): else: return None + # XXX sp create def create_authn_request( self, destination, @@ -451,12 +448,9 @@ class Base(Entity): client_crt = kwargs.get("client_crt") nsprefix = kwargs.get("nsprefix") - # XXX will be used to embed the signature to the xml doc - ie, POST binding - # XXX always called by the SP, no need to check the context - sign = self.authn_requests_signed if sign is None else sign - def_sig = DefaultSignature() - sign_alg = sign_alg or def_sig.get_sign_alg() - digest_alg = digest_alg or def_sig.get_digest_alg() + sign = sign if sign is not None else self.should_sign + sign_alg = sign_alg or self.signing_algorithm + digest_alg = digest_alg or self.digest_algorithm if sign_alg not in [long_name for short_name, long_name in SIG_ALLOWED_ALG]: raise Exception( @@ -506,6 +500,7 @@ class Base(Entity): return msg + # XXX sp create def create_attribute_query(self, destination, name_id=None, attribute=None, message_id=0, consent=None, extensions=None, sign=False, sign_prepare=False, sign_alg=None, @@ -572,6 +567,7 @@ class Base(Entity): # MUST use SOAP for # AssertionIDRequest, SubjectQuery, # AuthnQuery, AttributeQuery, or AuthzDecisionQuery + # XXX sp create def create_authz_decision_query(self, destination, action, evidence=None, resource=None, subject=None, message_id=0, consent=None, extensions=None, @@ -596,6 +592,7 @@ class Base(Entity): subject=subject, sign_alg=sign_alg, digest_alg=digest_alg, **kwargs) + # XXX sp create def create_authz_decision_query_using_assertion(self, destination, assertion, action=None, resource=None, @@ -632,6 +629,7 @@ class Base(Entity): extensions=extensions, sign=sign, nsprefix=nsprefix) @staticmethod + # XXX sp create def create_assertion_id_request(assertion_id_refs, **kwargs): """ @@ -644,6 +642,7 @@ class Base(Entity): else: return 0, assertion_id_refs[0] + # XXX sp create def create_authn_query(self, subject, destination=None, authn_context=None, session_index="", message_id=0, consent=None, extensions=None, sign=False, nsprefix=None, sign_alg=None, @@ -667,6 +666,7 @@ class Base(Entity): nsprefix=nsprefix, sign_alg=sign_alg, digest_alg=digest_alg) + # XXX sp create def create_name_id_mapping_request(self, name_id_policy, name_id=None, base_id=None, encrypted_id=None, destination=None, @@ -828,6 +828,7 @@ class Base(Entity): # ------------------- ECP ------------------------------------------------ + # XXX sp create def create_ecp_authn_request(self, entityid=None, relay_state="", sign=False, **kwargs): """ Makes an authentication request. @@ -932,6 +933,7 @@ class Base(Entity): # ---------------------------------------------------------------------- @staticmethod + # XXX sp create def create_discovery_service_request(url, entity_id, **kwargs): """ Created the HTTP redirect URL needed to send the user to the diff --git a/src/saml2/entity.py b/src/saml2/entity.py index bff8db8d..39bc3dec 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -141,6 +141,24 @@ class Entity(HTTPBase): else: raise SAMLError("Missing configuration") + def_sig = DefaultSignature() + self.signing_algorithm = ( + self.config.getattr('signing_algorithm') + or def_sig.get_sign_alg() + ) + self.digest_algorithm = ( + self.config.getattr('digest_algorithm') + or def_sig.get_digest_alg() + ) + sign_config = ( + self.config.getattr("authn_requests_signed", "sp") + if self.entity_type == "sp" + else self.config.getattr("sign_response", "idp") + if self.entity_type == "idp" + else False + ) + self.should_sign = sign_config + for item in ["cert_file", "key_file", "ca_certs"]: _val = getattr(self.config, item, None) if not _val: @@ -219,20 +237,8 @@ class Entity(HTTPBase): :return: A dictionary """ - # XXX authn_requests_signed (obj property) applies only to an SP - # XXX sign_response (config option) applies to idp/aa - # XXX Looking into sp/idp/aa properties should be done in the same way - # XXX ^this discrepancy should be fixed - sign_config = ( - self.authn_requests_signed - if self.config.context == "sp" - else self.config.getattr("sign_response") - if self.config.context == "idp" - else None - ) - sign = sign_config if sign is None else sign - def_sig = DefaultSignature() - sigalg = sigalg or def_sig.get_sign_alg() + sign = sign if sign is not None else self.should_sign + sigalg = sigalg or self.signing_algorithm # unless if BINDING_HTTP_ARTIFACT if response: @@ -453,10 +459,6 @@ class Entity(HTTPBase): sign_alg=None, digest_alg=None, ): - # sign adn digest algs - sign_alg = sign_alg or self.signing_algorithm - digest_alg = digest_alg or self.digest_algorithm - if msg.signature is None: msg.signature = pre_signature_part( msg.id, self.sec.my_cert, 1, sign_alg=sign_alg, digest_alg=digest_alg @@ -476,6 +478,7 @@ class Entity(HTTPBase): logger.info("REQUEST: %s", msg) return signed_instance_factory(msg, self.sec, to_sign) + # XXX calls self.sign def _message( self, request_cls, @@ -629,6 +632,7 @@ class Entity(HTTPBase): raise exception return response + # XXX calls self.sign def _response(self, in_response_to, consumer_url=None, status=None, issuer=None, sign=False, to_sign=None, sp_entity_id=None, encrypt_assertion=False, @@ -806,6 +810,7 @@ class Entity(HTTPBase): else: return response + # XXX calls self.sign def _status_response(self, response_class, issuer, status, sign=False, sign_alg=None, digest_alg=None, **kwargs): @@ -914,6 +919,7 @@ class Entity(HTTPBase): # ------------------------------------------------------------------------ + # XXX ent create def create_error_response(self, in_response_to, destination, info, sign=False, issuer=None, sign_alg=None, digest_alg=None, **kwargs): @@ -936,6 +942,7 @@ class Entity(HTTPBase): # ------------------------------------------------------------------------ + # XXX ent create def create_logout_request(self, destination, issuer_entity_id, subject_id=None, name_id=None, reason=None, expire=None, message_id=0, @@ -988,6 +995,7 @@ class Entity(HTTPBase): issuer=self._issuer(), sign_alg=sign_alg, digest_alg=digest_alg, **args) + # XXX ent create def create_logout_response(self, request, bindings=None, status=None, sign=False, issuer=None, sign_alg=None, digest_alg=None): @@ -1015,6 +1023,7 @@ class Entity(HTTPBase): return response + # XXX ent create def create_artifact_resolve(self, artifact, destination, sessid, consent=None, extensions=None, sign=False, sign_alg=None, digest_alg=None): @@ -1036,6 +1045,7 @@ class Entity(HTTPBase): consent, extensions, sign, artifact=artifact, sign_alg=sign_alg, digest_alg=digest_alg) + # XXX ent create def create_artifact_response(self, request, artifact, bindings=None, status=None, sign=False, issuer=None, sign_alg=None, digest_alg=None): @@ -1056,6 +1066,7 @@ class Entity(HTTPBase): return response + # XXX ent create def create_manage_name_id_request(self, destination, message_id=0, consent=None, extensions=None, sign=False, name_id=None, new_id=None, @@ -1114,6 +1125,7 @@ class Entity(HTTPBase): return self._parse_request(xmlstr, saml_request.ManageNameIDRequest, "manage_name_id_service", binding) + # XXX ent create def create_manage_name_id_response(self, request, bindings=None, status=None, sign=False, issuer=None, sign_alg=None, digest_alg=None, diff --git a/src/saml2/server.py b/src/saml2/server.py index 68e04e27..1d4b7543 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -77,8 +77,15 @@ def _shelve_compat(name, *args, **kwargs): class Server(Entity): """ A class that does things that IdPs or AAs do """ - def __init__(self, config_file="", config=None, cache=None, stype="idp", - symkey="", msg_cb=None): + def __init__( + self, + config_file="", + config=None, + cache=None, + stype="idp", + symkey="", + msg_cb=None, + ): Entity.__init__(self, stype, config, config_file, msg_cb=msg_cb) self.eptid = None self.init_config(stype) @@ -218,6 +225,7 @@ class Server(Entity): return False # ------------------------------------------------------------------------- + def parse_authn_request(self, enc_request, binding=BINDING_HTTP_REDIRECT): """Parse a Authentication Request @@ -438,7 +446,6 @@ class Server(Entity): :param encrypt_cert_assertion: Certificate to be used for encryption of assertions. :param authn_statement: Authentication statement. - :param sign_assertion: True if assertions should be signed. :param pefim: True if a response according to the PEFIM profile should be created. :param farg: Argument to pass on to the assertion constructor @@ -510,7 +517,7 @@ class Server(Entity): # ------------------------------------------------------------------------ - # noinspection PyUnusedLocal + # XXX idp create def create_attribute_response(self, identity, in_response_to, destination, sp_entity_id, userid="", name_id=None, status=None, issuer=None, @@ -594,15 +601,15 @@ class Server(Entity): def gather_authn_response_args( self, sp_entity_id, name_id_policy, userid, **kwargs ): + kwargs["policy"] = kwargs.get("release_policy") + # collect args and return them args = {} - args["policy"] = kwargs.get( - "release_policy", self.config.getattr("policy", "idp") - ) - args['best_effort'] = kwargs.get("best_effort", False) - + # XXX will be passed to _authn_response param_defaults = { + 'policy': None, + 'best_effort': False, 'sign_assertion': False, 'sign_response': False, 'encrypt_assertion': False, @@ -610,12 +617,8 @@ class Server(Entity): 'encrypted_advice_attributes': False, 'encrypt_cert_advice': None, 'encrypt_cert_assertion': None, + # need to be named sign_alg and digest_alg } - - # signing and digest algs - self.signing_algorithm = self.config.getattr('signing_algorithm', "idp") - self.digest_algorithm = self.config.getattr('digest_algorithm', "idp") - for param, val_default in param_defaults.items(): val_kw = kwargs.get(param) val_config = self.config.getattr(param, "idp") @@ -687,6 +690,7 @@ class Server(Entity): return args + # XXX idp create def create_authn_response( self, identity, @@ -736,7 +740,6 @@ class Server(Entity): assertions in the advice element. :param encrypt_cert_assertion: Certificate to be used for encryption of assertions. - :param sign_assertion: True if assertions should be signed. :param pefim: True if a response according to the PEFIM profile should be created. :return: A response instance @@ -785,6 +788,7 @@ class Server(Entity): return self.create_error_response(in_response_to, destination, sp_entity_id, exc, name_id) + # XXX idp create def create_authn_request_response(self, identity, in_response_to, destination, sp_entity_id, name_id_policy=None, userid=None, @@ -800,7 +804,7 @@ class Server(Entity): authn_decl=authn_decl, session_not_on_or_after=session_not_on_or_after) - # noinspection PyUnusedLocal + # XXX idp create def create_assertion_id_request_response(self, assertion_id, sign=False, sign_alg=None, digest_alg=None, **kwargs): @@ -827,7 +831,8 @@ class Server(Entity): else: return assertion - # noinspection PyUnusedLocal + # XXX calls self.sign => should it call _message (which calls self.sign)? + # XXX idp create def create_name_id_mapping_response(self, name_id=None, encrypted_id=None, in_response_to=None, issuer=None, sign_response=False, @@ -859,6 +864,7 @@ class Server(Entity): logger.info("Message: %s", _resp) return _resp + # XXX idp create def create_authn_query_response(self, subject, session_index=None, requested_context=None, in_response_to=None, issuer=None, sign_response=False, @@ -892,6 +898,7 @@ class Server(Entity): def parse_ecp_authn_request(self): pass + # XXX idp create def create_ecp_authn_request_response(self, acs_url, identity, in_response_to, destination, sp_entity_id, name_id_policy=None, diff --git a/src/saml2/sigver.py b/src/saml2/sigver.py index ee630340..87441807 100644 --- a/src/saml2/sigver.py +++ b/src/saml2/sigver.py @@ -1689,11 +1689,6 @@ class SecurityContext(object): node_id, ) - def sign_assertion_using_xmlsec(self, statement, **kwargs): - """ Deprecated function. See sign_assertion(). """ - return self.sign_statement( - statement, class_name(saml.Assertion()), **kwargs) - def sign_assertion(self, statement, **kwargs): """Sign a SAML assertion. @@ -1755,6 +1750,7 @@ class SecurityContext(object): return statement +# XXX calls DefaultSignature def pre_signature_part(ident, public_key=None, identifier=None, digest_alg=None, sign_alg=None): """ If an assertion is to be signed the signature part has to be preset -- cgit v1.2.1 From a159cc537835b4588544d9ee129fec10bf757124 Mon Sep 17 00:00:00 2001 From: Ivan Kanakarakis Date: Tue, 24 Nov 2020 15:25:28 +0200 Subject: WIP, TODO cleanup, see description * client_base::Base is the base for an SP and manages SP_ARGS * server::Server is the base for an IdP and maanges AA_IDP_ARGS * entity::Entity is the base of SP/IdPs and manages the COMMON_ARGS The signing_algorithm and digest_algorithm are COMMON_ARGS and should be set and managed by entity::Entity. On init they are set as properties of the Entity object. If no configuration has been given, the internal-default is set (through DefaultSignature()). The set sign_alg and digest_alg must be checked against an allow/block-list --- - Signing is done both by SPs (on requests) and IdPs (on responses). - Signing is done both for the Redirect-binding (apply_binding()) and the POST-binding (_message() > sign()). --- * All client_base::Base(SP) (create_*) methods end in Entity::_message() * Almost all server::Server(IdP) (create_*) methods end in Entity::_response() thus: - Entity::_message() must decide the value of "sign" and call Entity::sign() - Entity::_response() must decide the value of "sign" and call Entity::sign() - Entity::_status_response() must decide the value of "sign" and call Entity::sign() - Entity::sign() must decide the value of sign_alg and digest_alg and call sigver::pre_signature_part() --- All calls to Entity::_message() and Entity::_response() (or to their callers) must pass on sign, sign_alg and digest_alg All calls to sigver::pre_signature_part() should happen through the same call-chain and should pass on specific sign_alg and digest_alg params All relevant params should be set to None unless they have been set by the caller. --- client::do_logout should be refactored to use the same call-chain --- These type of checks (and self.lock blocks) should be removed (there are more for sign_assertion) ``` if (sign and self.sec.cert_handler.generate_cert()) or client_crt is not None: ``` ``` if self.sec.cert_handler.generate_cert() ``` --- Signed-off-by: Ivan Kanakarakis --- src/saml2/client_base.py | 384 ++++++++++++++++++++++++++------------------ src/saml2/entity.py | 410 ++++++++++++++++++++++++++++++++++------------- src/saml2/server.py | 292 +++++++++++++++++++++------------ src/saml2/sigver.py | 2 +- 4 files changed, 723 insertions(+), 365 deletions(-) diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index dd3ddfdf..90a4fbd1 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -283,7 +283,7 @@ class Base(Entity): else: return None - # XXX sp create + # XXX DONE sp create > _message def create_authn_request( self, destination, @@ -296,7 +296,7 @@ class Base(Entity): consent=None, extensions=None, sign=None, - sign_prepare=False, + sign_prepare=None, sign_alg=None, digest_alg=None, allow_create=None, @@ -448,64 +448,39 @@ class Base(Entity): client_crt = kwargs.get("client_crt") nsprefix = kwargs.get("nsprefix") - sign = sign if sign is not None else self.should_sign - sign_alg = sign_alg or self.signing_algorithm - digest_alg = digest_alg or self.digest_algorithm - - if sign_alg not in [long_name for short_name, long_name in SIG_ALLOWED_ALG]: - raise Exception( - "Signature algo not in allowed list: {algo}".format(algo=sign_alg) - ) - if digest_alg not in [long_name for short_name, long_name in DIGEST_ALLOWED_ALG]: - raise Exception( - "Digest algo not in allowed list: {algo}".format(algo=digest_alg) - ) - - if (sign and self.sec.cert_handler.generate_cert()) or client_crt is not None: - with self.lock: - self.sec.cert_handler.update_cert(True, client_crt) - if client_crt is not None: - sign_prepare = True - msg = self._message( - AuthnRequest, - destination, - message_id, - consent, - extensions, - sign, - sign_prepare, - sign_alg=sign_alg, - digest_alg=digest_alg, - protocol_binding=binding, - scoping=scoping, - nsprefix=nsprefix, - **args, - ) - else: - msg = self._message( - AuthnRequest, - destination, - message_id, - consent, - extensions, - sign, - sign_prepare, - sign_alg=sign_alg, - digest_alg=digest_alg, - protocol_binding=binding, - scoping=scoping, - nsprefix=nsprefix, - **args, - ) + msg = self._message( + AuthnRequest, + destination, + message_id, + consent, + extensions, + sign, + sign_prepare, + sign_alg=sign_alg, + digest_alg=digest_alg, + protocol_binding=binding, + scoping=scoping, + nsprefix=nsprefix, + **args, + ) return msg - # XXX sp create - def create_attribute_query(self, destination, name_id=None, - attribute=None, message_id=0, consent=None, - extensions=None, sign=False, sign_prepare=False, sign_alg=None, - digest_alg=None, - **kwargs): + # XXX DONE sp create > _message + def create_attribute_query( + self, + destination, + name_id=None, + attribute=None, + message_id=0, + consent=None, + extensions=None, + sign=None, + sign_prepare=None, + sign_alg=None, + digest_alg=None, + **kwargs, + ): """ Constructs an AttributeQuery :param destination: To whom the query should be sent @@ -559,19 +534,40 @@ class Base(Entity): except KeyError: nsprefix = None - return self._message(AttributeQuery, destination, message_id, consent, - extensions, sign, sign_prepare, subject=subject, - attribute=attribute, nsprefix=nsprefix, - sign_alg=sign_alg, digest_alg=digest_alg) + return self._message( + AttributeQuery, + destination, + message_id, + consent, + extensions, + sign, + sign_prepare, + subject=subject, + attribute=attribute, + nsprefix=nsprefix, + sign_alg=sign_alg, + digest_alg=digest_alg, + ) # MUST use SOAP for # AssertionIDRequest, SubjectQuery, # AuthnQuery, AttributeQuery, or AuthzDecisionQuery - # XXX sp create - def create_authz_decision_query(self, destination, action, - evidence=None, resource=None, subject=None, - message_id=0, consent=None, extensions=None, - sign=None, sign_alg=None, digest_alg=None, **kwargs): + # XXX DONE sp create > _message + def create_authz_decision_query( + self, + destination, + action, + evidence=None, + resource=None, + subject=None, + message_id=0, + consent=None, + extensions=None, + sign=None, + sign_alg=None, + digest_alg=None, + **kwargs, + ): """ Creates an authz decision query. :param destination: The IdP endpoint @@ -586,20 +582,38 @@ class Base(Entity): :return: AuthzDecisionQuery instance """ - return self._message(AuthzDecisionQuery, destination, message_id, - consent, extensions, sign, action=action, - evidence=evidence, resource=resource, - subject=subject, sign_alg=sign_alg, - digest_alg=digest_alg, **kwargs) - - # XXX sp create - def create_authz_decision_query_using_assertion(self, destination, - assertion, action=None, - resource=None, - subject=None, message_id=0, - consent=None, - extensions=None, - sign=False, nsprefix=None): + return self._message( + AuthzDecisionQuery, + destination, + message_id, + consent, + extensions, + sign, + action=action, + evidence=evidence, + resource=resource, + subject=subject, + sign_alg=sign_alg, + digest_alg=digest_alg, + **kwargs, + ) + + # XXX DONE sp create > self.create_authz_decision_query (FIXME pass sign/sign_alg/etc) > _message + def create_authz_decision_query_using_assertion( + self, + destination, + assertion, + action=None, + resource=None, + subject=None, + message_id=0, + consent=None, + extensions=None, + sign=None, + sign_ag=None, + digest_alg=None, + nsprefix=None, + ): """ Makes an authz decision query based on a previously received Assertion. @@ -624,12 +638,21 @@ class Base(Entity): _action = None return self.create_authz_decision_query( - destination, _action, saml.Evidence(assertion=assertion), - resource, subject, message_id=message_id, consent=consent, - extensions=extensions, sign=sign, nsprefix=nsprefix) + destination, + _action, + saml.Evidence(assertion=assertion), + resource, + subject, + message_id=message_id, + consent=consent, + extensions=extensions, + sign=sign, + sign_alg=sign_alg, + digest_alg=digest_alg, + nsprefix=nsprefix, + ) @staticmethod - # XXX sp create def create_assertion_id_request(assertion_id_refs, **kwargs): """ @@ -642,11 +665,21 @@ class Base(Entity): else: return 0, assertion_id_refs[0] - # XXX sp create - def create_authn_query(self, subject, destination=None, authn_context=None, - session_index="", message_id=0, consent=None, - extensions=None, sign=False, nsprefix=None, sign_alg=None, - digest_alg=None): + # XXX DONE sp create > _message + def create_authn_query( + self, + subject, + destination=None, + authn_context=None, + session_index="", + message_id=0, + consent=None, + extensions=None, + sign=None, + nsprefix=None, + sign_alg=None, + digest_alg=None, + ): """ :param subject: The subject its all about as a instance @@ -659,20 +692,37 @@ class Base(Entity): :param sign: Whether the request should be signed or not. :return: """ - return self._message(AuthnQuery, destination, message_id, consent, - extensions, sign, subject=subject, - session_index=session_index, - requested_authn_context=authn_context, - nsprefix=nsprefix, sign_alg=sign_alg, - digest_alg=digest_alg) - - # XXX sp create - def create_name_id_mapping_request(self, name_id_policy, - name_id=None, base_id=None, - encrypted_id=None, destination=None, - message_id=0, consent=None, - extensions=None, sign=False, - nsprefix=None, sign_alg=None, digest_alg=None): + return self._message( + AuthnQuery, + destination, + message_id, + consent, + extensions, + sign, + subject=subject, + session_index=session_index, + requested_authn_context=authn_context, + nsprefix=nsprefix, + sign_alg=sign_alg, + digest_alg=digest_alg, + ) + + # XXX DONE sp create > _message + def create_name_id_mapping_request( + self, + name_id_policy, + name_id=None, + base_id=None, + encrypted_id=None, + destination=None, + message_id=0, + consent=None, + extensions=None, + sign=None, + nsprefix=None, + sign_alg=None, + digest_alg=None, + ): """ :param name_id_policy: @@ -692,29 +742,39 @@ class Base(Entity): "At least one of name_id, base_id or encrypted_id must be present." ) - if name_id: - return self._message(NameIDMappingRequest, destination, message_id, - consent, extensions, sign, - name_id_policy=name_id_policy, name_id=name_id, - nsprefix=nsprefix, sign_alg=sign_alg, - digest_alg=digest_alg) - elif base_id: - return self._message(NameIDMappingRequest, destination, message_id, - consent, extensions, sign, - name_id_policy=name_id_policy, base_id=base_id, - nsprefix=nsprefix, sign_alg=sign_alg, - digest_alg=digest_alg) - else: - return self._message(NameIDMappingRequest, destination, message_id, - consent, extensions, sign, - name_id_policy=name_id_policy, - encrypted_id=encrypted_id, nsprefix=nsprefix, - sign_alg=sign_alg, digest_alg=digest_alg) + id_attr = { + "name_id": name_id, + "base_id": ( + base_id + if not name_id + else None + ), + "encrypted_id": ( + encrypted_id + if not name_id and not base_id + else None + ), + } + + return self._message( + NameIDMappingRequest, + destination, + message_id, + consent, + extensions, + sign, + name_id_policy=name_id_policy, + **id_attr, + nsprefix=nsprefix, + sign_alg=sign_alg, + digest_alg=digest_alg, + ) # ======== response handling =========== - def parse_authn_request_response(self, xmlstr, binding, outstanding=None, - outstanding_certs=None, conv_info=None): + def parse_authn_request_response( + self, xmlstr, binding, outstanding=None, outstanding_certs=None, conv_info=None + ): """ Deal with an AuthnResponse :param xmlstr: The reply as a xml string @@ -743,15 +803,14 @@ class Base(Entity): "return_addrs": self.service_urls(binding=binding), "entity_id": self.config.entityid, "attribute_converters": self.config.attribute_converters, - "allow_unknown_attributes": - self.config.allow_unknown_attributes, - 'conv_info': conv_info + "allow_unknown_attributes": self.config.allow_unknown_attributes, + 'conv_info': conv_info, } try: - resp = self._parse_response(xmlstr, AuthnResponse, - "assertion_consumer_service", - binding, **kwargs) + resp = self._parse_response( + xmlstr, AuthnResponse, "assertion_consumer_service", binding, **kwargs + ) except StatusError as err: logger.error("SAML status error: %s", err) raise @@ -762,12 +821,14 @@ class Base(Entity): raise if not isinstance(resp, AuthnResponse): - logger.error("Response type not supported: %s", - saml2.class_name(resp)) + logger.error("Response type not supported: %s", saml2.class_name(resp)) return None - if (resp.assertion and len(resp.response.encrypted_assertion) == 0 and - resp.assertion.subject.name_id): + if ( + resp.assertion + and len(resp.response.encrypted_assertion) == 0 + and resp.assertion.subject.name_id + ): self.users.add_information_about_person(resp.session_info()) logger.info("--- ADDED person info ----") @@ -777,15 +838,15 @@ class Base(Entity): # SubjectQuery, AuthnQuery, RequestedAuthnContext, AttributeQuery, # AuthzDecisionQuery all get Response as response - def parse_authz_decision_query_response(self, response, - binding=BINDING_SOAP): + def parse_authz_decision_query_response(self, response, binding=BINDING_SOAP): """ Verify that the response is OK """ - kwargs = {"entity_id": self.config.entityid, - "attribute_converters": self.config.attribute_converters} + kwargs = { + "entity_id": self.config.entityid, + "attribute_converters": self.config.attribute_converters, + } - return self._parse_response(response, AuthzResponse, "", binding, - **kwargs) + return self._parse_response(response, AuthzResponse, "", binding, **kwargs) def parse_authn_query_response(self, response, binding=BINDING_SOAP): """ Verify that the response is OK @@ -828,9 +889,16 @@ class Base(Entity): # ------------------- ECP ------------------------------------------------ - # XXX sp create - def create_ecp_authn_request(self, entityid=None, relay_state="", - sign=False, **kwargs): + # XXX DONE sp create > create_authn_request (FIXME DONE sign/sign_alg/etc) > _message + def create_ecp_authn_request( + self, + entityid=None, + relay_state="", + sign=None, + sign_alg=None, + digest_alg=None, + **kwargs, + ): """ Makes an authentication request. :param entityid: The entity ID of the IdP to send the request to @@ -847,16 +915,22 @@ class Base(Entity): # must_understand and act according to the standard # - paos_request = paos.Request(must_understand="1", actor=ACTOR, - response_consumer_url=my_url, - service=ECP_SERVICE) + paos_request = paos.Request( + must_understand="1", + actor=ACTOR, + response_consumer_url=my_url, + service=ECP_SERVICE, + ) # ---------------------------------------- # # ---------------------------------------- - relay_state = ecp.RelayState(actor=ACTOR, must_understand="1", - text=relay_state) + relay_state = ecp.RelayState( + actor=ACTOR, + must_understand="1", + text=relay_state, + ) # ---------------------------------------- # @@ -883,18 +957,23 @@ class Base(Entity): "single_sign_on_service", [_binding], entity_id=entityid ) req_id, authn_req = self.create_authn_request( - location, service_url_binding=BINDING_PAOS, **kwargs + location, + service_url_binding=BINDING_PAOS, + sign=sign, + sign_alg=sign_alg, + digest_alg=digest_alg, + **kwargs, ) # ---------------------------------------- # The SOAP envelope # ---------------------------------------- - soap_envelope = make_soap_enveloped_saml_thingy(authn_req, - [paos_request, - relay_state]) + soap_envelope = make_soap_enveloped_saml_thingy( + authn_req, [paos_request, relay_state] + ) - return req_id, "%s" % soap_envelope + return req_id, str(soap_envelope) def parse_ecp_authn_response(self, txt, outstanding=None): rdict = soap.class_instances_from_soap_enveloped_saml_thingies(txt, @@ -933,7 +1012,6 @@ class Base(Entity): # ---------------------------------------------------------------------- @staticmethod - # XXX sp create def create_discovery_service_request(url, entity_id, **kwargs): """ Created the HTTP redirect URL needed to send the user to the diff --git a/src/saml2/entity.py b/src/saml2/entity.py index 39bc3dec..a619882b 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -75,6 +75,8 @@ from saml2.pack import http_redirect_message from saml2.pack import http_form_post_message from saml2.xmldsig import DefaultSignature +from saml2.xmldsig import SIG_ALLOWED_ALG +from saml2.xmldsig import DIGEST_ALLOWED_ALG logger = logging.getLogger(__name__) @@ -213,6 +215,10 @@ class Entity(HTTPBase): return Issuer(text=self.config.entityid, format=NAMEID_FORMAT_ENTITY) + # XXX DONE will actually use sign_alg and digest_alg for the Redirect-Binding + # XXX DONE deepest level - needs to decide the sign_alg (no digest_alg here) + # XXX verify digest_alg is not needed + # XXX deprecate sigalg for sign_alg def apply_binding( self, binding, @@ -237,8 +243,13 @@ class Entity(HTTPBase): :return: A dictionary """ + # XXX sig-allowed should be configurable sign = sign if sign is not None else self.should_sign - sigalg = sigalg or self.signing_algorithm + sign_alg = sigalg or self.signing_algorithm + if sign_alg not in [long_name for short_name, long_name in SIG_ALLOWED_ALG]: + raise Exception( + "Signature algo not in allowed list: {algo}".format(algo=sign_alg) + ) # unless if BINDING_HTTP_ARTIFACT if response: @@ -260,14 +271,14 @@ class Entity(HTTPBase): relay_state=relay_state, typ=typ, sign=sign, - sigalg=sigalg, + sigalg=sign_alg, backend=self.sec.sec_backend, ) info["url"] = str(destination) info["method"] = "GET" elif binding == BINDING_SOAP or binding == BINDING_PAOS: info = self.use_soap( - msg_str, destination, sign=sign, sigalg=sigalg, **kwargs + msg_str, destination, sign=sign, sigalg=sign_alg, **kwargs ) elif binding == BINDING_URI: info = self.use_http_uri(msg_str, typ, destination) @@ -335,8 +346,13 @@ class Entity(HTTPBase): if not message_id: message_id = sid() - return {"id": message_id, "version": VERSION, - "issue_instant": instant(), "issuer": self._issuer()} + margs = { + "id": message_id, + "version": VERSION, + "issue_instant": instant(), + "issuer": self._issuer(), + } + return margs def response_args(self, message, bindings=None, descr_type=""): """ @@ -450,15 +466,30 @@ class Entity(HTTPBase): # -------------------------------------------------------------------------- + # XXX DONE will actually use sign_alg and digest_alg for the POST-Binding + # XXX DONE deepest level - needs to decide the sign_alg and digest_alg value + # XXX calls pre_signature_part def sign( self, msg, mid=None, to_sign=None, - sign_prepare=False, + sign_prepare=None, sign_alg=None, digest_alg=None, ): + # XXX sig/digest-allowed should be configurable + sign_alg = sign_alg or self.signing_algorithm + digest_alg = digest_alg or self.digest_algorithm + if sign_alg not in [long_name for short_name, long_name in SIG_ALLOWED_ALG]: + raise Exception( + "Signature algo not in allowed list: {algo}".format(algo=sign_alg) + ) + if digest_alg not in [long_name for short_name, long_name in DIGEST_ALLOWED_ALG]: + raise Exception( + "Digest algo not in allowed list: {algo}".format(algo=digest_alg) + ) + if msg.signature is None: msg.signature = pre_signature_part( msg.id, self.sec.my_cert, 1, sign_alg=sign_alg, digest_alg=digest_alg @@ -478,7 +509,11 @@ class Entity(HTTPBase): logger.info("REQUEST: %s", msg) return signed_instance_factory(msg, self.sec, to_sign) - # XXX calls self.sign + # XXX DONE will actually use sign the POST-Binding + # XXX DONE deepest level - needs to decide the sign value + # XXX DONE calls self.sign must figure out sign + # XXX ensure both SPs and IdPs go through this + # XXX ensure this works for the POST-Binding def _message( self, request_cls, @@ -486,8 +521,8 @@ class Entity(HTTPBase): message_id=0, consent=None, extensions=None, - sign=False, - sign_prepare=False, + sign=None, + sign_prepare=None, nsprefix=None, sign_alg=None, digest_alg=None, @@ -533,6 +568,8 @@ class Entity(HTTPBase): req = self.msg_cb(req) reqid = req.id + + sign = sign if sign is not None else self.should_sign if sign: signed_req = self.sign( req, @@ -593,8 +630,7 @@ class Entity(HTTPBase): return True return False - def _encrypt_assertion(self, encrypt_cert, sp_entity_id, response, - node_xpath=None): + def _encrypt_assertion(self, encrypt_cert, sp_entity_id, response, node_xpath=None): """ Encryption of assertions. :param encrypt_cert: Certificate to be used for encryption. @@ -632,15 +668,27 @@ class Entity(HTTPBase): raise exception return response - # XXX calls self.sign - def _response(self, in_response_to, consumer_url=None, status=None, - issuer=None, sign=False, to_sign=None, sp_entity_id=None, - encrypt_assertion=False, - encrypt_assertion_self_contained=False, - encrypted_advice_attributes=False, - encrypt_cert_advice=None, encrypt_cert_assertion=None, - sign_assertion=None, pefim=False, sign_alg=None, - digest_alg=None, **kwargs): + # XXX DONE calls self.sign must figure out sign + def _response( + self, + in_response_to, + consumer_url=None, + status=None, + issuer=None, + sign=None, + to_sign=None, + sp_entity_id=None, + encrypt_assertion=False, + encrypt_assertion_self_contained=False, + encrypted_advice_attributes=False, + encrypt_cert_advice=None, + encrypt_cert_assertion=None, + sign_assertion=None, + pefim=False, + sign_alg=None, + digest_alg=None, + **kwargs, + ): """ Create a Response. Encryption: encrypt_assertion must be true for encryption to be @@ -692,7 +740,12 @@ class Entity(HTTPBase): self._add_info(response, **kwargs) - if not sign and to_sign and not encrypt_assertion: + sign = sign if sign is not None else self.should_sign + if ( + not sign + and to_sign + and not encrypt_assertion + ): return signed_instance_factory(response, self.sec, to_sign) has_encrypt_cert = self.has_encrypt_cert_in_metadata(sp_entity_id) @@ -701,23 +754,31 @@ class Entity(HTTPBase): if not has_encrypt_cert and encrypt_cert_assertion is None: encrypt_assertion = False - if encrypt_assertion or ( - encrypted_advice_attributes and - response.assertion.advice is - not None and - len(response.assertion.advice.assertion) == 1): + if ( + encrypt_assertion + or ( + encrypted_advice_attributes + and response.assertion.advice is not None + and len(response.assertion.advice.assertion) == 1 + ) + ): if sign: - response.signature = pre_signature_part(response.id, - self.sec.my_cert, 1, - sign_alg=sign_alg, - digest_alg=digest_alg) + response.signature = pre_signature_part( + response.id, + self.sec.my_cert, + 1, + sign_alg=sign_alg, + digest_alg=digest_alg, + ) sign_class = [(class_name(response), response.id)] else: sign_class = [] - if encrypted_advice_attributes and response.assertion.advice is \ - not None \ - and len(response.assertion.advice.assertion) > 0: + if ( + encrypted_advice_attributes + and response.assertion.advice is not None + and len(response.assertion.advice.assertion) > 0 + ): _assertions = response.assertion if not isinstance(_assertions, list): _assertions = [_assertions] @@ -810,10 +871,17 @@ class Entity(HTTPBase): else: return response - # XXX calls self.sign - def _status_response(self, response_class, issuer, status, sign=False, - sign_alg=None, digest_alg=None, - **kwargs): + # XXX DONE calls self.sign must figure out sign + def _status_response( + self, + response_class, + issuer, + status, + sign=None, + sign_alg=None, + digest_alg=None, + **kwargs, + ): """ Create a StatusResponse. :param response_class: Which subclass of StatusResponse that should be @@ -828,18 +896,21 @@ class Entity(HTTPBase): mid = sid() for key in ["binding"]: - try: - del kwargs[key] - except KeyError: - pass + kwargs.pop(key, None) if not status: status = success_status_factory() - response = response_class(issuer=issuer, id=mid, version=VERSION, - issue_instant=instant(), - status=status, **kwargs) + response = response_class( + issuer=issuer, + id=mid, + version=VERSION, + issue_instant=instant(), + status=status, + **kwargs, + ) + sign = sign if sign is not None else self.should_sign if sign: return self.sign(response, mid, sign_alg=sign_alg, digest_alg=digest_alg) else: @@ -919,10 +990,18 @@ class Entity(HTTPBase): # ------------------------------------------------------------------------ - # XXX ent create - def create_error_response(self, in_response_to, destination, info, - sign=False, issuer=None, sign_alg=None, - digest_alg=None, **kwargs): + # XXX DONE ent create > _response + def create_error_response( + self, + in_response_to, + destination, + info, + sign=None, + issuer=None, + sign_alg=None, + digest_alg=None, + **kwargs, + ): """ Create a error response. :param in_response_to: The identifier of the message this is a response @@ -937,18 +1016,35 @@ class Entity(HTTPBase): """ status = error_status_factory(info) - return self._response(in_response_to, destination, status, issuer, - sign, sign_alg=sign_alg, digest_alg=digest_alg) + return self._response( + in_response_to, + destination, + status, + issuer, + sign, + sign_alg=sign_alg, + digest_alg=digest_alg, + ) # ------------------------------------------------------------------------ - # XXX ent create - def create_logout_request(self, destination, issuer_entity_id, - subject_id=None, name_id=None, - reason=None, expire=None, message_id=0, - consent=None, extensions=None, sign=False, - session_indexes=None, sign_alg=None, - digest_alg=None): + # XXX DONE ent create > _message + def create_logout_request( + self, + destination, + issuer_entity_id, + subject_id=None, + name_id=None, + reason=None, + expire=None, + message_id=0, + consent=None, + extensions=None, + sign=None, + session_indexes=None, + sign_alg=None, + digest_alg=None, + ): """ Constructs a LogoutRequest :param destination: Destination of the request @@ -970,9 +1066,9 @@ class Entity(HTTPBase): if subject_id: if self.entity_type == "idp": - name_id = NameID(text=self.users.get_entityid(subject_id, - issuer_entity_id, - False)) + name_id = NameID( + text=self.users.get_entityid(subject_id, issuer_entity_id, False) + ) else: name_id = NameID(text=subject_id) @@ -989,16 +1085,33 @@ class Entity(HTTPBase): sis.append(SessionIndex(text=si)) args["session_index"] = sis - return self._message(LogoutRequest, destination, message_id, - consent, extensions, sign, name_id=name_id, - reason=reason, not_on_or_after=expire, - issuer=self._issuer(), sign_alg=sign_alg, - digest_alg=digest_alg, **args) + return self._message( + LogoutRequest, + destination, + message_id, + consent, + extensions, + sign, + name_id=name_id, + reason=reason, + not_on_or_after=expire, + issuer=self._issuer(), + sign_alg=sign_alg, + digest_alg=digest_alg, + **args, + ) - # XXX ent create - def create_logout_response(self, request, bindings=None, status=None, - sign=False, issuer=None, sign_alg=None, - digest_alg=None): + # XXX DONE ent create > _status_response + def create_logout_response( + self, + request, + bindings=None, + status=None, + sign=None, + issuer=None, + sign_alg=None, + digest_alg=None, + ): """ Create a LogoutResponse. :param request: The request this is a response to @@ -1015,18 +1128,32 @@ class Entity(HTTPBase): if not issuer: issuer = self._issuer() - response = self._status_response(samlp.LogoutResponse, issuer, status, - sign, sign_alg=sign_alg, - digest_alg=digest_alg, **rinfo) + response = self._status_response( + samlp.LogoutResponse, + issuer, + status, + sign, + sign_alg=sign_alg, + digest_alg=digest_alg, + **rinfo, + ) logger.info("Response: %s", response) return response - # XXX ent create - def create_artifact_resolve(self, artifact, destination, sessid, - consent=None, extensions=None, sign=False, - sign_alg=None, digest_alg=None): + # XXX DONE ent create > _message + def create_artifact_resolve( + self, + artifact, + destination, + sessid, + consent=None, + extensions=None, + sign=None, + sign_alg=None, + digest_alg=None, + ): """ Create a ArtifactResolve request @@ -1041,23 +1168,45 @@ class Entity(HTTPBase): artifact = Artifact(text=artifact) - return self._message(ArtifactResolve, destination, sessid, - consent, extensions, sign, artifact=artifact, - sign_alg=sign_alg, digest_alg=digest_alg) + return self._message( + ArtifactResolve, + destination, + sessid, + consent, + extensions, + sign, + artifact=artifact, + sign_alg=sign_alg, + digest_alg=digest_alg, + ) - # XXX ent create - def create_artifact_response(self, request, artifact, bindings=None, - status=None, sign=False, issuer=None, - sign_alg=None, digest_alg=None): + # XXX DONE ent create > _status_response + def create_artifact_response( + self, + request, + artifact, + bindings=None, + status=None, + sign=None, + issuer=None, + sign_alg=None, + digest_alg=None, + ): """ Create an ArtifactResponse :return: """ rinfo = self.response_args(request, bindings) - response = self._status_response(ArtifactResponse, issuer, status, - sign=sign, sign_alg=sign_alg, - digest_alg=digest_alg, **rinfo) + response = self._status_response( + ArtifactResponse, + issuer, + status, + sign=sign, + sign_alg=sign_alg, + digest_alg=digest_alg, + **rinfo, + ) msg = element_to_extension_element(self.artifact[artifact]) response.extension_elements = [msg] @@ -1066,13 +1215,22 @@ class Entity(HTTPBase): return response - # XXX ent create - def create_manage_name_id_request(self, destination, message_id=0, - consent=None, extensions=None, sign=False, - name_id=None, new_id=None, - encrypted_id=None, new_encrypted_id=None, - terminate=None, sign_alg=None, - digest_alg=None): + # XXX DONE ent create > _message + def create_manage_name_id_request( + self, + destination, + message_id=0, + consent=None, + extensions=None, + sign=None, + name_id=None, + new_id=None, + encrypted_id=None, + new_encrypted_id=None, + terminate=None, + sign_alg=None, + digest_alg=None, + ): """ :param destination: @@ -1108,9 +1266,16 @@ class Entity(HTTPBase): "One of NewID, NewEncryptedNameID or Terminate has to be " "provided") - return self._message(ManageNameIDRequest, destination, consent=consent, - extensions=extensions, sign=sign, - sign_alg=sign_alg, digest_alg=digest_alg, **kwargs) + return self._message( + ManageNameIDRequest, + destination, + consent=consent, + extensions=extensions, + sign=sign, + sign_alg=sign_alg, + digest_alg=digest_alg, + **kwargs, + ) def parse_manage_name_id_request(self, xmlstr, binding=BINDING_SOAP): """ Deal with a LogoutRequest @@ -1125,11 +1290,18 @@ class Entity(HTTPBase): return self._parse_request(xmlstr, saml_request.ManageNameIDRequest, "manage_name_id_service", binding) - # XXX ent create - def create_manage_name_id_response(self, request, bindings=None, - status=None, sign=False, issuer=None, - sign_alg=None, digest_alg=None, - **kwargs): + # XXX DONE ent create > _status_response + def create_manage_name_id_response( + self, + request, + bindings=None, + status=None, + sign=None, + issuer=None, + sign_alg=None, + digest_alg=None, + **kwargs, + ): rinfo = self.response_args(request, bindings) @@ -1147,16 +1319,26 @@ class Entity(HTTPBase): return response - def parse_manage_name_id_request_response(self, string, - binding=BINDING_SOAP): - return self._parse_response(string, saml_response.ManageNameIDResponse, - "manage_name_id_service", binding, - asynchop=False) + def parse_manage_name_id_request_response(self, string, binding=BINDING_SOAP): + return self._parse_response( + string, + saml_response.ManageNameIDResponse, + "manage_name_id_service", + binding, + asynchop=False, + ) # ------------------------------------------------------------------------ - def _parse_response(self, xmlstr, response_cls, service, binding, - outstanding_certs=None, **kwargs): + def _parse_response( + self, + xmlstr, + response_cls, + service, + binding, + outstanding_certs=None, + **kwargs, + ): """ Deal with a Response :param xmlstr: The response as a xml string @@ -1357,7 +1539,15 @@ class Entity(HTTPBase): return destination - def artifact2message(self, artifact, descriptor, sign=False): + # XXX DONE uses sign but not a create_* + def artifact2message( + self, + artifact, + descriptor, + sign=None, + sign_alg=None, + digest_alg=None, + ): """ :param artifact: The Base64 encoded SAML artifact as sent over the net @@ -1377,6 +1567,8 @@ class Entity(HTTPBase): destination, _sid, sign=sign, + sign_alg=sign_alg, + digest_alg=digest_alg, ) return self.send_using_soap(msg, destination) diff --git a/src/saml2/server.py b/src/saml2/server.py index 1d4b7543..04604b09 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -392,6 +392,7 @@ class Server(Entity): **kwargs) return assertion + # XXX > _response def _authn_response( self, in_response_to, @@ -403,8 +404,8 @@ class Server(Entity): authn=None, issuer=None, policy=None, - sign_assertion=False, - sign_response=False, + sign_assertion=None, + sign_response=None, best_effort=False, encrypt_assertion=False, encrypt_cert_advice=None, @@ -493,10 +494,9 @@ class Server(Entity): to_sign = [] if not encrypt_assertion: if sign_assertion: - assertion.signature = pre_signature_part(assertion.id, - self.sec.my_cert, 2, - sign_alg=sign_alg, - digest_alg=digest_alg) + assertion.signature = pre_signature_part( + assertion.id, self.sec.my_cert, 2, sign_alg=sign_alg, digest_alg=digest_alg + ) to_sign.append((class_name(assertion), assertion.id)) args["assertion"] = assertion @@ -505,25 +505,47 @@ class Server(Entity): self.session_db.store_assertion(assertion, to_sign) return self._response( - in_response_to, consumer_url, status, issuer, sign_response, - to_sign, sp_entity_id=sp_entity_id, + in_response_to, + consumer_url, + status, + issuer, + sign_response, + to_sign, + sp_entity_id=sp_entity_id, encrypt_assertion=encrypt_assertion, encrypt_cert_advice=encrypt_cert_advice, encrypt_cert_assertion=encrypt_cert_assertion, encrypt_assertion_self_contained=encrypt_assertion_self_contained, encrypted_advice_attributes=encrypted_advice_attributes, sign_assertion=sign_assertion, - pefim=pefim, sign_alg=sign_alg, digest_alg=digest_alg, **args) + pefim=pefim, + sign_alg=sign_alg, + digest_alg=digest_alg, + **args, + ) # ------------------------------------------------------------------------ - # XXX idp create - def create_attribute_response(self, identity, in_response_to, destination, - sp_entity_id, userid="", name_id=None, - status=None, issuer=None, - sign_assertion=False, sign_response=False, - attributes=None, sign_alg=None, - digest_alg=None, farg=None, **kwargs): + # XXX calls pre_signature_part without ensuring sign_alg/digest_alg + # XXX DONE idp create > _response + def create_attribute_response( + self, + identity, + in_response_to, + destination, + sp_entity_id, + userid="", + name_id=None, + status=None, + issuer=None, + sign_assertion=None, + sign_response=None, + attributes=None, + sign_alg=None, + digest_alg=None, + farg=None, + **kwargs, + ): """ Create an attribute assertion response. :param identity: A dictionary with attributes and values that are @@ -573,10 +595,10 @@ class Server(Entity): farg=farg['assertion']) if sign_assertion: - assertion.signature = pre_signature_part(assertion.id, - self.sec.my_cert, 1, - sign_alg=sign_alg, - digest_alg=digest_alg) + # XXX calls pre_signature_part without ensuring sign_alg/digest_alg + assertion.signature = pre_signature_part( + assertion.id, self.sec.my_cert, 1, sign_alg=sign_alg, digest_alg=digest_alg + ) # Just the assertion or the response and the assertion ? to_sign = [(class_name(assertion), assertion.id)] kwargs['sign_assertion'] = True @@ -690,7 +712,7 @@ class Server(Entity): return args - # XXX idp create + # XXX DONE idp create > _authn_response > _response def create_authn_response( self, identity, @@ -769,52 +791,67 @@ class Server(Entity): try: _authn = authn - if (sign_assertion or sign_response) and \ - self.sec.cert_handler.generate_cert(): - with self.lock: - self.sec.cert_handler.update_cert(True) - return self._authn_response( - in_response_to, destination, sp_entity_id, identity, - authn=_authn, issuer=issuer, pefim=pefim, - sign_alg=sign_alg, digest_alg=digest_alg, - session_not_on_or_after=session_not_on_or_after, **args) return self._authn_response( - in_response_to, destination, sp_entity_id, identity, - authn=_authn, issuer=issuer, pefim=pefim, sign_alg=sign_alg, + in_response_to, + destination, + sp_entity_id, + identity, + authn=_authn, + issuer=issuer, + pefim=pefim, + sign_alg=sign_alg, digest_alg=digest_alg, - session_not_on_or_after=session_not_on_or_after, **args) - + session_not_on_or_after=session_not_on_or_after, + **args, + ) except MissingValue as exc: - return self.create_error_response(in_response_to, destination, - sp_entity_id, exc, name_id) - - # XXX idp create - def create_authn_request_response(self, identity, in_response_to, - destination, sp_entity_id, - name_id_policy=None, userid=None, - name_id=None, authn=None, authn_decl=None, - issuer=None, sign_response=False, - sign_assertion=False, - session_not_on_or_after=None, **kwargs): - - return self.create_authn_response(identity, in_response_to, destination, - sp_entity_id, name_id_policy, userid, - name_id, authn, issuer, - sign_response, sign_assertion, - authn_decl=authn_decl, - session_not_on_or_after=session_not_on_or_after) - - # XXX idp create - def create_assertion_id_request_response(self, assertion_id, sign=False, - sign_alg=None, - digest_alg=None, **kwargs): - """ + return self.create_error_response( + in_response_to, destination, sp_entity_id, exc, name_id + ) - :param assertion_id: - :param sign: - :return: - """ + # XXX DONE idp create > create_authn_response > _authn_response > _response + def create_authn_request_response( + self, + identity, + in_response_to, + destination, + sp_entity_id, + name_id_policy=None, + userid=None, + name_id=None, + authn=None, + authn_decl=None, + issuer=None, + sign_response=None, + sign_assertion=None, + session_not_on_or_after=None, + sign_alg=None, + digest_alg=None, + **kwargs, + ): + return self.create_authn_response( + identity, + in_response_to, + destination, + sp_entity_id, + name_id_policy, + userid, + name_id, + authn, + issuer, + sign_response, + sign_assertion, + authn_decl=authn_decl, + session_not_on_or_after=session_not_on_or_after, + sign_alg=sign_alg, + digest_alg=digest_alg, + ) + # XXX calls pre_signature_part without ensuring sign_alg/digest_alg + # XXX DONE idp create > [...] + def create_assertion_id_request_response( + self, assertion_id, sign=None, sign_alg=None, digest_alg=None, **kwargs + ): try: (assertion, to_sign) = self.session_db.get_assertion(assertion_id) except KeyError: @@ -822,22 +859,33 @@ class Server(Entity): if to_sign: if assertion.signature is None: - assertion.signature = pre_signature_part(assertion.id, - self.sec.my_cert, 1, - sign_alg=sign_alg, - digest_alg=digest_alg) - + # XXX calls pre_signature_part without ensuring sign_alg/digest_alg + assertion.signature = pre_signature_part( + assertion.id, + self.sec.my_cert, + 1, + sign_alg=sign_alg, + digest_alg=digest_alg, + ) return signed_instance_factory(assertion, self.sec, to_sign) else: return assertion + # XXX calls self.sign without ensuring sign # XXX calls self.sign => should it call _message (which calls self.sign)? - # XXX idp create - def create_name_id_mapping_response(self, name_id=None, encrypted_id=None, - in_response_to=None, - issuer=None, sign_response=False, - status=None, sign_alg=None, - digest_alg=None, **kwargs): + # XXX idp create > NameIDMappingResponse & sign? + def create_name_id_mapping_response( + self, + name_id=None, + encrypted_id=None, + in_response_to=None, + issuer=None, + sign_response=None, + status=None, + sign_alg=None, + digest_alg=None, + **kwargs, + ): """ protocol for mapping a principal's name identifier into a different name identifier for the same principal. @@ -855,8 +903,9 @@ class Server(Entity): ms_args = self.message_args() - _resp = NameIDMappingResponse(name_id, encrypted_id, - in_response_to=in_response_to, **ms_args) + _resp = NameIDMappingResponse( + name_id, encrypted_id, in_response_to=in_response_to, **ms_args + ) if sign_response: return self.sign(_resp, sign_alg=sign_alg, digest_alg=digest_alg) @@ -864,12 +913,20 @@ class Server(Entity): logger.info("Message: %s", _resp) return _resp - # XXX idp create - def create_authn_query_response(self, subject, session_index=None, - requested_context=None, in_response_to=None, - issuer=None, sign_response=False, - status=None, sign_alg=None, digest_alg=None, - **kwargs): + # XXX DONE idp create > _response + def create_authn_query_response( + self, + subject, + session_index=None, + requested_context=None, + in_response_to=None, + issuer=None, + sign_response=None, + status=None, + sign_alg=None, + digest_alg=None, + **kwargs, + ): """ A successful will contain one or more assertions containing authentication statements. @@ -878,33 +935,54 @@ class Server(Entity): """ margs = self.message_args() - asserts = [] - for statement in self.session_db.get_authn_statements( - subject.name_id, session_index, requested_context): - asserts.append(saml.Assertion(authn_statement=statement, - subject=subject, **margs)) + asserts = [ + saml.Assertion(authn_statement=statement, subject=subject, **margs) + for statement in self.session_db.get_authn_statements( + subject.name_id, session_index, requested_context + ) + ] if asserts: args = {"assertion": asserts} else: args = {} - return self._response(in_response_to, "", status, issuer, - sign_response, to_sign=[], sign_alg=sign_alg, - digest_alg=digest_alg, **args) + return self._response( + in_response_to, + "", + status, + issuer, + sign_response, + to_sign=[], + sign_alg=sign_alg, + digest_alg=digest_alg, + **args, + ) # --------- def parse_ecp_authn_request(self): pass - # XXX idp create - def create_ecp_authn_request_response(self, acs_url, identity, - in_response_to, destination, - sp_entity_id, name_id_policy=None, - userid=None, name_id=None, authn=None, - issuer=None, sign_response=False, - sign_assertion=False, **kwargs): + # XXX DONE idp create > create_authn_response > _authn_response > _response + def create_ecp_authn_request_response( + self, + acs_url, + identity, + in_response_to, + destination, + sp_entity_id, + name_id_policy=None, + userid=None, + name_id=None, + authn=None, + issuer=None, + sign_response=None, + sign_assertion=None, + sign_alg=None, + digest_alg=None, + **kwargs, + ): # ---------------------------------------- # Date: Tue, 24 Nov 2020 21:26:45 +0200 Subject: WIP callers Signed-off-by: Ivan Kanakarakis --- src/saml2/ecp.py | 37 +++++++++++++++++++++++++------------ src/saml2/s2repoze/plugins/sp.py | 8 +++++--- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/saml2/ecp.py b/src/saml2/ecp.py index 56448e9c..8db0afad 100644 --- a/src/saml2/ecp.py +++ b/src/saml2/ecp.py @@ -40,7 +40,9 @@ def ecp_capable(headers): #noinspection PyUnusedLocal -def ecp_auth_request(cls, entityid=None, relay_state="", sign=False): +def ecp_auth_request( + cls, entityid=None, relay_state="", sign=None, sign_alg=None, digest_alg=None +): """ Makes an authentication request. :param entityid: The entity ID of the IdP to send the request to @@ -59,9 +61,12 @@ def ecp_auth_request(cls, entityid=None, relay_state="", sign=False): # must_understand and actor according to the standard # - paos_request = paos.Request(must_understand="1", actor=ACTOR, - response_consumer_url=my_url, - service=SERVICE) + paos_request = paos.Request( + must_understand="1", + actor=ACTOR, + response_consumer_url=my_url, + service=SERVICE, + ) eelist.append(element_to_extension_element(paos_request)) @@ -73,7 +78,13 @@ def ecp_auth_request(cls, entityid=None, relay_state="", sign=False): location = cls._sso_location(entityid, binding=BINDING_SOAP) req_id, authn_req = cls.create_authn_request( - location, binding=BINDING_PAOS, service_url_binding=BINDING_PAOS) + location, + binding=BINDING_PAOS, + service_url_binding=BINDING_PAOS, + sign=sign, + sign_alg=sign_alg, + digest_alg=digest_alg, + ) body = soapenv.Body() body.extension_elements = [element_to_extension_element(authn_req)] @@ -96,7 +107,8 @@ def ecp_auth_request(cls, entityid=None, relay_state="", sign=False): must_understand="1", provider_name=None, issuer=saml.Issuer(text=authn_req.issuer.text), - idp_list=idp_list) + idp_list=idp_list, + ) eelist.append(element_to_extension_element(ecp_request)) @@ -104,8 +116,7 @@ def ecp_auth_request(cls, entityid=None, relay_state="", sign=False): # # ---------------------------------------- - relay_state = ecp.RelayState(actor=ACTOR, must_understand="1", - text=relay_state) + relay_state = ecp.RelayState(actor=ACTOR, must_understand="1", text=relay_state) eelist.append(element_to_extension_element(relay_state)) @@ -118,20 +129,22 @@ def ecp_auth_request(cls, entityid=None, relay_state="", sign=False): soap_envelope = soapenv.Envelope(header=header, body=body) - return req_id, "%s" % soap_envelope + return req_id, str(soap_envelope) def handle_ecp_authn_response(cls, soap_message, outstanding=None): rdict = soap.class_instances_from_soap_enveloped_saml_thingies( - soap_message, [paos, ecp, samlp]) + soap_message, [paos, ecp, samlp] + ) _relay_state = None for item in rdict["header"]: if item.c_tag == "RelayState" and item.c_namespace == ecp.NAMESPACE: _relay_state = item - response = authn_response(cls.config, cls.service_urls(), outstanding, - allow_unsolicited=True) + response = authn_response( + cls.config, cls.service_urls(), outstanding, allow_unsolicited=True + ) response.loads("%s" % rdict["body"], False, soap_message) response.verify() diff --git a/src/saml2/s2repoze/plugins/sp.py b/src/saml2/s2repoze/plugins/sp.py index a840ab37..3dec8d14 100644 --- a/src/saml2/s2repoze/plugins/sp.py +++ b/src/saml2/s2repoze/plugins/sp.py @@ -271,7 +271,6 @@ class SAML2Plugin(object): #### IChallenger #### # noinspection PyUnusedLocal def challenge(self, environ, _status, _app_headers, _forget_headers): - _cli = self.saml_client if "REMOTE_USER" in environ: @@ -346,7 +345,7 @@ class SAML2Plugin(object): ) if _cli.authn_requests_signed: - _sid = saml2.s_utils.sid() + _sid = sid() req_id, msg_str = _cli.create_authn_request( dest, vorg=vorg_name, @@ -357,7 +356,10 @@ class SAML2Plugin(object): _sid = req_id else: req_id, req = _cli.create_authn_request( - dest, vorg=vorg_name, sign=False, extensions=extensions + dest, + vorg=vorg_name, + sign=False, + extensions=extensions, ) msg_str = "%s" % req _sid = req_id -- cgit v1.2.1 From 017092fc5c7988c36e1855f941508f9e92fa2a53 Mon Sep 17 00:00:00 2001 From: Ivan Kanakarakis Date: Wed, 25 Nov 2020 00:40:20 +0200 Subject: WIP fix ensure callers Signed-off-by: Ivan Kanakarakis --- src/saml2/client.py | 179 +++++++++++++++++++++++++++++++++------------------- src/saml2/server.py | 50 ++++++++++++--- 2 files changed, 156 insertions(+), 73 deletions(-) diff --git a/src/saml2/client.py b/src/saml2/client.py index f5033ba2..3aaefcd1 100644 --- a/src/saml2/client.py +++ b/src/saml2/client.py @@ -14,8 +14,6 @@ from saml2 import BINDING_HTTP_REDIRECT from saml2 import BINDING_HTTP_POST from saml2 import BINDING_SOAP -from saml2.xmldsig import DefaultSignature - from saml2.ident import decode, code from saml2.httpbase import HTTPError from saml2.s_utils import sid @@ -50,6 +48,7 @@ class Saml2Client(Base): consent=None, extensions=None, sign=None, sigalg=None, + digest_alg=None, response_binding=saml2.BINDING_HTTP_POST, **kwargs, ): @@ -81,6 +80,7 @@ class Saml2Client(Base): extensions=extensions, sign=sign, sigalg=sigalg, + digest_alg=digest_alg, response_binding=response_binding, **kwargs, ) @@ -107,6 +107,7 @@ class Saml2Client(Base): sign=None, response_binding=saml2.BINDING_HTTP_POST, sigalg=None, + digest_alg=None, **kwargs, ): """ Makes all necessary preparations for an authentication request @@ -140,8 +141,8 @@ class Saml2Client(Base): # XXX ^through self.create_authn_request(...) # XXX - sign_redirect will add the signature to the query params # XXX ^through self.apply_binding(...) - sign_post = (binding == BINDING_HTTP_POST and sign) - sign_redirect = (binding == BINDING_HTTP_REDIRECT and sign) + sign_post = False if binding == BINDING_HTTP_REDIRECT else sign + sign_redirect = False if binding == BINDING_HTTP_POST and sign else sign reqid, request = self.create_authn_request( destination, @@ -153,6 +154,7 @@ class Saml2Client(Base): extensions=extensions, sign=sign_post, sign_alg=sigalg, + digest_alg=digest_alg, **kwargs, ) @@ -172,8 +174,15 @@ class Saml2Client(Base): else: raise SignOnError("No supported bindings available for authentication") - def global_logout(self, name_id, reason="", expire=None, sign=None, - sign_alg=None, digest_alg=None): + def global_logout( + self, + name_id, + reason="", + expire=None, + sign=None, + sign_alg=None, + digest_alg=None, + ): """ More or less a layer of indirection :-/ Bootstrapping the whole thing by finding all the IdPs that should be notified. @@ -198,14 +207,28 @@ class Saml2Client(Base): # find out which IdPs/AAs I should notify entity_ids = self.users.issuers_of_info(name_id) - return self.do_logout(name_id, entity_ids, reason, expire, sign, - sign_alg=sign_alg, digest_alg=digest_alg) - - # XXX calls DefaultSignature - # XXX calls self.sign - def do_logout(self, name_id, entity_ids, reason, expire, sign=None, - expected_binding=None, sign_alg=None, digest_alg=None, - **kwargs): + return self.do_logout( + name_id, + entity_ids, + reason, + expire, + sign, + sign_alg=sign_alg, + digest_alg=digest_alg, + ) + + def do_logout( + self, + name_id, + entity_ids, + reason, + expire, + sign=None, + expected_binding=None, + sign_alg=None, + digest_alg=None, + **kwargs, + ): """ :param name_id: Identifier of the Subject (a NameID instance) @@ -234,6 +257,7 @@ class Saml2Client(Base): for binding in [BINDING_SOAP, BINDING_HTTP_POST, BINDING_HTTP_REDIRECT]: if expected_binding and binding != expected_binding: continue + try: srvs = self.metadata.single_logout_service( entity_id, binding, "idpsso" @@ -247,6 +271,7 @@ class Saml2Client(Base): destination = next(locations(srvs), None) logger.info("destination to provider: %s", destination) + try: session_info = self.users.get_info_from( name_id, entity_id, False @@ -254,37 +279,29 @@ class Saml2Client(Base): session_indexes = [session_info['session_index']] except KeyError: session_indexes = None + + sign_post = False if binding == BINDING_HTTP_REDIRECT else sign + sign_redirect = False if binding == BINDING_HTTP_POST and sign else sign + req_id, request = self.create_logout_request( - destination, entity_id, name_id=name_id, reason=reason, - expire=expire, session_indexes=session_indexes) - - sign = sign if sign is not None else self.logout_requests_signed - def_sig = DefaultSignature() - sign_alg = def_sig.get_sign_alg() if sign_alg is None else sign_alg - digest_alg = ( - def_sig.get_digest_alg() - if digest_alg is None - else digest_alg + destination, + entity_id, + name_id=name_id, + reason=reason, + expire=expire, + session_indexes=session_indexes, + sign=sign_post, + sign_alg=sign_alg, + digest_alg=digest_alg, ) - if sign: - if binding == BINDING_HTTP_REDIRECT: - srequest = str(request) - else: - srequest = self.sign( - request, sign_alg=sign_alg, digest_alg=digest_alg - ) - else: - srequest = str(request) - relay_state = self._relay_state(req_id) - http_info = self.apply_binding( binding, - srequest, + str(request), destination, relay_state, - sign=sign, + sign=sign_redirect, sigalg=sign_alg, ) @@ -357,11 +374,15 @@ class Saml2Client(Base): status["entity_ids"].remove(issuer) if "sign_alg" in status: sign_alg = status["sign_alg"] - return self.do_logout(decode(status["name_id"]), - status["entity_ids"], - status["reason"], status["not_on_or_after"], - status["sign"], sign_alg=sign_alg, - digest_alg=digest_alg) + return self.do_logout( + decode(status["name_id"]), + status["entity_ids"], + status["reason"], + status["not_on_or_after"], + status["sign"], + sign_alg=sign_alg, + digest_alg=digest_alg, + ) def _use_soap(self, destination, query_type, **kwargs): _create_func = getattr(self, "create_%s" % query_type) @@ -473,6 +494,7 @@ class Saml2Client(Base): binding=BINDING_SOAP, nsprefix=None, sign_alg=None, + digest_alg=None, ): """ Does a attribute request to an attribute authority, this is by default done over SOAP. @@ -502,10 +524,9 @@ class Saml2Client(Base): response_args = {} if not binding: - binding, destination = self.pick_binding("attribute_service", - None, - "attribute_authority", - entity_id=entityid) + binding, destination = self.pick_binding( + "attribute_service", None, "attribute_authority", entity_id=entityid + ) else: srvs = self.metadata.attribute_service(entityid, binding) if srvs is []: @@ -514,37 +535,62 @@ class Saml2Client(Base): destination = next(locations(srvs), None) if binding == BINDING_SOAP: - return self._use_soap(destination, "attribute_query", - consent=consent, extensions=extensions, - sign=sign, subject_id=subject_id, - attribute=attribute, - sp_name_qualifier=sp_name_qualifier, - name_qualifier=name_qualifier, - format=nameid_format, - response_args=response_args) + return self._use_soap( + destination, + "attribute_query", + consent=consent, + extensions=extensions, + sign=sign, + sign_alg=sign_alg, + digest_alg=digest_alg, + subject_id=subject_id, + attribute=attribute, + sp_name_qualifier=sp_name_qualifier, + name_qualifier=name_qualifier, + format=nameid_format, + response_args=response_args, + ) elif binding == BINDING_HTTP_POST: mid = sid() - query = self.create_attribute_query(destination, subject_id, - attribute, mid, consent, - extensions, sign, nsprefix) - self.state[query.id] = {"entity_id": entityid, - "operation": "AttributeQuery", - "subject_id": subject_id, - "sign": sign} + query = self.create_attribute_query( + destination, + name_id=subject_id, + attribute=attribute, + message_id=mid, + consent=consent, + extensions=extensions, + sign=sign, + sign_alg=sign_alg, + digest_alg=digest_alg, + nsprefix=nsprefix, + ) + self.state[query.id] = { + "entity_id": entityid, + "operation": "AttributeQuery", + "subject_id": subject_id, + "sign": sign, + } relay_state = self._relay_state(query.id) return self.apply_binding( binding, str(query), destination, relay_state, - sign=sign, + sign=False, sigalg=sign_alg, ) else: raise SAMLError("Unsupported binding") def handle_logout_request( - self, request, name_id, binding, sign=None, sign_alg=None, relay_state="" + self, + request, + name_id, + binding, + sign=None, + sign_alg=None, + digest_alg=None, + relay_state="", ): """ Deal with a LogoutRequest @@ -594,7 +640,12 @@ class Saml2Client(Base): sign = self.logout_responses_signed response = self.create_logout_response( - _req.message, response_bindings, status, sign, sign_alg=sign_alg + _req.message, + bindings=response_bindings, + status=status, + sign=sign, + sign_alg=sign_alg, + digest_alg=digest_alg, ) rinfo = self.response_args(_req.message, response_bindings) diff --git a/src/saml2/server.py b/src/saml2/server.py index 04604b09..afb4e35a 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -328,10 +328,25 @@ class Server(Entity): consumer_url]) return farg - def setup_assertion(self, authn, sp_entity_id, in_response_to, consumer_url, - name_id, policy, _issuer, authn_statement, identity, - best_effort, sign_response, farg=None, - session_not_on_or_after=None, **kwargs): + def setup_assertion( + self, + authn, + sp_entity_id, + in_response_to, + consumer_url, + name_id, + policy, + _issuer, + authn_statement, + identity, + best_effort, + sign_response, + farg=None, + session_not_on_or_after=None, + sign_alg=None, + digest_alg=None, + **kwargs, + ): """ Construct and return the Assertion @@ -360,8 +375,15 @@ class Server(Entity): ast.apply_policy(sp_entity_id, policy) except MissingValue as exc: if not best_effort: - return self.create_error_response(in_response_to, consumer_url, - exc, sign_response) + response = self.create_error_response( + in_response_to, + destination=consumer_url, + info=exc, + sign=sign_response, + sign_alg=sign_alg, + digest_alg=digest_alg, + ) + return str(response).split("\n") farg = self.update_farg(in_response_to, consumer_url, farg) @@ -785,9 +807,14 @@ class Server(Entity): ) except IOError as exc: response = self.create_error_response( - in_response_to, destination, sp_entity_id, exc, name_id + in_response_to, + destination=destination, + info=exc, + sign=sign_response, + sign_alg=sign_alg, + digest_alg=digest_alg, ) - return ("%s" % response).split("\n") + return str(response).split("\n") try: _authn = authn @@ -806,7 +833,12 @@ class Server(Entity): ) except MissingValue as exc: return self.create_error_response( - in_response_to, destination, sp_entity_id, exc, name_id + in_response_to, + destination=destination, + info=exc, + sign=sign_response, + sign_alg=sign_alg, + digest_alg=digest_alg, ) # XXX DONE idp create > create_authn_response > _authn_response > _response -- cgit v1.2.1 From 68d3989ed5c34d94178881e600cc4c3224965aec Mon Sep 17 00:00:00 2001 From: Ivan Kanakarakis Date: Mon, 7 Dec 2020 15:43:03 +0200 Subject: Remove unneeded response_factory helper Signed-off-by: Ivan Kanakarakis --- src/saml2/entity.py | 9 ++++----- src/saml2/sigver.py | 17 ----------------- tests/test_50_server.py | 15 +++++++++++++-- tests/test_51_client.py | 39 ++++++++++++++++++++++++++------------- 4 files changed, 43 insertions(+), 37 deletions(-) diff --git a/src/saml2/entity.py b/src/saml2/entity.py index a619882b..5320ab6a 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -62,7 +62,6 @@ from saml2 import class_name from saml2.config import config_factory from saml2.httpbase import HTTPBase from saml2.sigver import security_context -from saml2.sigver import response_factory from saml2.sigver import SigverError from saml2.sigver import SignatureError from saml2.sigver import make_temp @@ -730,10 +729,10 @@ class Entity(HTTPBase): _issuer = self._issuer(issuer) - response = response_factory(issuer=_issuer, - in_response_to=in_response_to, - status=status, sign_alg=sign_alg, - digest_alg=digest_alg) + response = samlp.Response(id=sid(), version=VERSION, issue_instant=instant()) + response.issuer = _issuer + response.in_response_to = in_response_to + response.status = status if consumer_url: response.destination = consumer_url diff --git a/src/saml2/sigver.py b/src/saml2/sigver.py index b8e924c2..d4ba8712 100644 --- a/src/saml2/sigver.py +++ b/src/saml2/sigver.py @@ -1878,23 +1878,6 @@ def pre_encrypt_assertion(response): return response -def response_factory(sign=False, encrypt=False, sign_alg=None, digest_alg=None, - **kwargs): - response = samlp.Response(id=sid(), version=VERSION, - issue_instant=instant()) - - if sign: - response.signature = pre_signature_part( - kwargs['id'], sign_alg=sign_alg, digest_alg=digest_alg) - if encrypt: - pass - - for key, val in kwargs.items(): - setattr(response, key, val) - - return response - - if __name__ == '__main__': import argparse diff --git a/tests/test_50_server.py b/tests/test_50_server.py index 7ee82499..dfc24eee 100644 --- a/tests/test_50_server.py +++ b/tests/test_50_server.py @@ -23,12 +23,15 @@ from saml2 import extension_elements_to_elements from saml2 import s_utils from saml2 import sigver from saml2 import time_util +from saml2 import VERSION from saml2.s_utils import OtherError from saml2.s_utils import do_attribute_statement from saml2.s_utils import factory +from saml2.s_utils import sid from saml2.soap import make_soap_enveloped_saml_thingy from saml2 import BINDING_HTTP_POST from saml2 import BINDING_HTTP_REDIRECT +from saml2.time_util import instant from pytest import raises from pathutils import full_path @@ -44,6 +47,14 @@ AUTHN = { } +def response_factory(**kwargs): + response = samlp.Response(id=sid(), version=VERSION, issue_instant=instant()) + + for key, val in kwargs.items(): + setattr(response, key, val) + + return response + def _eq(l1, l2): return set(l1) == set(l2) @@ -179,7 +190,7 @@ class TestServer1(): assert subject.name_id.format == saml.NAMEID_FORMAT_TRANSIENT def test_response(self): - response = sigver.response_factory( + response = response_factory( in_response_to="_012345", destination="https:#www.example.com", status=s_utils.success_status_factory(), @@ -1239,7 +1250,7 @@ class TestServer1NonAsciiAva(): assert subject.name_id.format == saml.NAMEID_FORMAT_TRANSIENT def test_response(self): - response = sigver.response_factory( + response = response_factory( in_response_to="_012345", destination="https:#www.example.com", status=s_utils.success_status_factory(), diff --git a/tests/test_51_client.py b/tests/test_51_client.py index a20cf941..c82917cd 100644 --- a/tests/test_51_client.py +++ b/tests/test_51_client.py @@ -21,6 +21,7 @@ from saml2 import saml from saml2 import samlp from saml2 import sigver from saml2 import s_utils +from saml2 import VERSION from saml2.assertion import Assertion from saml2.extension.requested_attributes import RequestedAttributes from saml2.extension.requested_attributes import RequestedAttribute @@ -40,7 +41,10 @@ from saml2.sigver import verify_redirect_signature from saml2.sigver import SignatureError, SigverError from saml2.s_utils import do_attribute_statement from saml2.s_utils import factory -from saml2.time_util import in_a_while, a_while_ago +from saml2.s_utils import sid +from saml2.time_util import in_a_while +from saml2.time_util import a_while_ago +from saml2.time_util import instant from defusedxml.common import EntitiesForbidden @@ -53,6 +57,14 @@ AUTHN = { "authn_auth": "http://www.example.com/login" } +def response_factory(**kwargs): + response = samlp.Response(id=sid(), version=VERSION, issue_instant=instant()) + + for key, val in kwargs.items(): + setattr(response, key, val) + + return response + def generate_cert(): sn = uuid.uuid4().urn cert_info = { @@ -943,7 +955,7 @@ class TestClient: # Create an Assertion instance from the signed assertion _ass = saml.assertion_from_string(sigass) - response = sigver.response_factory( + response = response_factory( in_response_to="_012345", destination="https:#www.example.com", status=s_utils.success_status_factory(), @@ -951,10 +963,11 @@ class TestClient: assertion=_ass ) - enctext = _sec.crypto.encrypt_assertion(response, - self.client.sec.encryption_keypairs[ - 0]["cert_file"], - pre_encryption_part()) + enctext = _sec.crypto.encrypt_assertion( + response, + self.client.sec.encryption_keypairs[0]["cert_file"], + pre_encryption_part(), + ) seresp = samlp.response_from_string(enctext) @@ -1023,7 +1036,7 @@ class TestClient: node_id=assertion.id) sigass = rm_xmltag(sigass) - response = sigver.response_factory( + response = response_factory( in_response_to="_012345", destination="http://lingon.catalogix.se:8087/", status=s_utils.success_status_factory(), @@ -1116,7 +1129,7 @@ class TestClient: assertion.advice.encrypted_assertion[0].add_extension_element( a_assertion) - response = sigver.response_factory( + response = response_factory( in_response_to="_012345", destination="http://lingon.catalogix.se:8087/", status=s_utils.success_status_factory(), @@ -1267,7 +1280,7 @@ class TestClient: assertion_2.signature = sigver.pre_signature_part(assertion_2.id, _sec.my_cert, 1) - response = sigver.response_factory( + response = response_factory( in_response_to="_012345", destination="http://lingon.catalogix.se:8087/", status=s_utils.success_status_factory(), @@ -2560,7 +2573,7 @@ class TestClientNonAsciiAva: # Create an Assertion instance from the signed assertion _ass = saml.assertion_from_string(sigass) - response = sigver.response_factory( + response = response_factory( in_response_to="_012345", destination="https:#www.example.com", status=s_utils.success_status_factory(), @@ -2640,7 +2653,7 @@ class TestClientNonAsciiAva: node_id=assertion.id) sigass = rm_xmltag(sigass) - response = sigver.response_factory( + response = response_factory( in_response_to="_012345", destination="http://lingon.catalogix.se:8087/", status=s_utils.success_status_factory(), @@ -2733,7 +2746,7 @@ class TestClientNonAsciiAva: assertion.advice.encrypted_assertion[0].add_extension_element( a_assertion) - response = sigver.response_factory( + response = response_factory( in_response_to="_012345", destination="http://lingon.catalogix.se:8087/", status=s_utils.success_status_factory(), @@ -2885,7 +2898,7 @@ class TestClientNonAsciiAva: assertion_2.signature = sigver.pre_signature_part(assertion_2.id, _sec.my_cert, 1) - response = sigver.response_factory( + response = response_factory( in_response_to="_012345", destination="http://lingon.catalogix.se:8087/", status=s_utils.success_status_factory(), -- cgit v1.2.1 From c0c412e2f17edb9abdd89ee8ef103c85d23d285c Mon Sep 17 00:00:00 2001 From: Ivan Kanakarakis Date: Mon, 7 Dec 2020 22:46:05 +0200 Subject: Difference between sigver.signed_instance_factory and Entity.sign Signed-off-by: Ivan Kanakarakis --- src/saml2/entity.py | 7 ++++++- src/saml2/sigver.py | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/saml2/entity.py b/src/saml2/entity.py index 5320ab6a..3b6c109f 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -467,7 +467,12 @@ class Entity(HTTPBase): # XXX DONE will actually use sign_alg and digest_alg for the POST-Binding # XXX DONE deepest level - needs to decide the sign_alg and digest_alg value - # XXX calls pre_signature_part + # XXX a controler for signed_instance_factory + # XXX syncs pre_signature_part and signed_instance_factory + # XXX makes sure pre_signature_part is called before signed_instance_factory + # XXX calls pre_signature_part - must have sign_alg & digest_alg + # XXX calls signed_instance_factory - after pre_signature_part + # XXX !!expects a msg object!! def sign( self, msg, diff --git a/src/saml2/sigver.py b/src/saml2/sigver.py index d4ba8712..65d4f39c 100644 --- a/src/saml2/sigver.py +++ b/src/saml2/sigver.py @@ -301,6 +301,12 @@ def _instance(klass, ava, seccont, base64encode=False, elements_to_sign=None): return instance +# XXX will actually sign the nodes +# XXX assumes pre_signature_part has already been called +# XXX calls sign without specifying sign_alg/digest_alg +# XXX this is fine as the algs are embeded in the document +# XXX as setup by pre_signature_part +# XXX !!expects instance string!! def signed_instance_factory(instance, seccont, elements_to_sign=None): """ -- cgit v1.2.1 From ac59e8a3763892379d2bb48ad9f8061096a51456 Mon Sep 17 00:00:00 2001 From: Ivan Kanakarakis Date: Mon, 7 Dec 2020 23:11:30 +0200 Subject: Do no prepare assertion signature in create_attribute_response but in _response Signed-off-by: Ivan Kanakarakis --- src/saml2/server.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/saml2/server.py b/src/saml2/server.py index afb4e35a..d23418ff 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -548,7 +548,6 @@ class Server(Entity): # ------------------------------------------------------------------------ - # XXX calls pre_signature_part without ensuring sign_alg/digest_alg # XXX DONE idp create > _response def create_attribute_response( self, @@ -616,20 +615,6 @@ class Server(Entity): issuer=_issuer, name_id=name_id, farg=farg['assertion']) - if sign_assertion: - # XXX calls pre_signature_part without ensuring sign_alg/digest_alg - assertion.signature = pre_signature_part( - assertion.id, self.sec.my_cert, 1, sign_alg=sign_alg, digest_alg=digest_alg - ) - # Just the assertion or the response and the assertion ? - to_sign = [(class_name(assertion), assertion.id)] - kwargs['sign_assertion'] = True - - kwargs["assertion"] = assertion - - if sp_entity_id: - kwargs['sp_entity_id'] = sp_entity_id - return self._response( in_response_to, destination, @@ -637,8 +622,11 @@ class Server(Entity): issuer, sign_response, to_sign, + sign_assertion=sign_assertion, sign_alg=sign_alg, digest_alg=digest_alg, + assertion=assertion, + sp_entity_id=sp_entity_id, **kwargs, ) -- cgit v1.2.1 From 28784008982f884a5472cbefdf9866fcd6031e48 Mon Sep 17 00:00:00 2001 From: Ivan Kanakarakis Date: Mon, 7 Dec 2020 23:13:04 +0200 Subject: Formatting and restructure Signed-off-by: Ivan Kanakarakis --- src/saml2/entity.py | 10 ++++++---- src/saml2/server.py | 1 + src/saml2/sigver.py | 40 ++++++++++++++++++++++++++-------------- 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/saml2/entity.py b/src/saml2/entity.py index 3b6c109f..88c2606b 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -516,8 +516,8 @@ class Entity(HTTPBase): # XXX DONE will actually use sign the POST-Binding # XXX DONE deepest level - needs to decide the sign value # XXX DONE calls self.sign must figure out sign - # XXX ensure both SPs and IdPs go through this - # XXX ensure this works for the POST-Binding + # XXX DONE ensure both SPs and IdPs go through this + # XXX DONE ensure this works for the POST-Binding def _message( self, request_cls, @@ -673,6 +673,8 @@ class Entity(HTTPBase): return response # XXX DONE calls self.sign must figure out sign + # XXX calls signed_instance_factory - must have called pre_signature_part + # XXX calls pre_signature_part - must figure out sign_alg/digest_alg def _response( self, in_response_to, @@ -746,8 +748,8 @@ class Entity(HTTPBase): sign = sign if sign is not None else self.should_sign if ( - not sign - and to_sign + to_sign + and not sign and not encrypt_assertion ): return signed_instance_factory(response, self.sec, to_sign) diff --git a/src/saml2/server.py b/src/saml2/server.py index d23418ff..808ec679 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -414,6 +414,7 @@ class Server(Entity): **kwargs) return assertion + # XXX calls pre_signature_part # XXX > _response def _authn_response( self, diff --git a/src/saml2/sigver.py b/src/saml2/sigver.py index 65d4f39c..52324eb4 100644 --- a/src/saml2/sigver.py +++ b/src/saml2/sigver.py @@ -315,17 +315,20 @@ def signed_instance_factory(instance, seccont, elements_to_sign=None): :param elements_to_sign: Which parts if any that should be signed :return: A class instance if not signed otherwise a string """ - if elements_to_sign: - signed_xml = instance - if not isinstance(instance, six.string_types): - signed_xml = instance.to_string() - for (node_name, nodeid) in elements_to_sign: - signed_xml = seccont.sign_statement( - signed_xml, node_name=node_name, node_id=nodeid) - return signed_xml - else: + if not elements_to_sign: return instance + signed_xml = instance + if not isinstance(instance, six.string_types): + signed_xml = instance.to_string() + + for (node_name, nodeid) in elements_to_sign: + signed_xml = seccont.sign_statement( + signed_xml, node_name=node_name, node_id=nodeid + ) + + return signed_xml + def make_temp(content, suffix="", decode=True, delete_tmpfiles=True): """ @@ -1740,10 +1743,11 @@ class SecurityContext(object): if not item.signature: item.signature = pre_signature_part( - sid, - self.cert_file, - sign_alg=sign_alg, - digest_alg=digest_alg) + ident=sid, + public_key=self.cert_file, + sign_alg=sign_alg, + digest_alg=digest_alg, + ) statement = self.sign_statement( statement, @@ -1757,7 +1761,13 @@ class SecurityContext(object): # XXX FIXME calls DefaultSignature - remove to unveil chain of calls without proper args -def pre_signature_part(ident, public_key=None, identifier=None, digest_alg=None, sign_alg=None): +def pre_signature_part( + ident, + public_key=None, + identifier=None, + digest_alg=None, + sign_alg=None, +): """ If an assertion is to be signed the signature part has to be preset with which algorithms to be used, this function returns such a @@ -1770,10 +1780,12 @@ def pre_signature_part(ident, public_key=None, identifier=None, digest_alg=None, :return: A preset signature part """ + # XXX if not digest_alg: digest_alg = ds.DefaultSignature().get_digest_alg() if not sign_alg: sign_alg = ds.DefaultSignature().get_sign_alg() + signature_method = ds.SignatureMethod(algorithm=sign_alg) canonicalization_method = ds.CanonicalizationMethod( algorithm=ds.ALG_EXC_C14N) -- cgit v1.2.1 From ff9cbcea5568e26d9b7d0adac7e26cd3b5065a4e Mon Sep 17 00:00:00 2001 From: Ivan Kanakarakis Date: Mon, 7 Dec 2020 23:13:31 +0200 Subject: Notes on _response Signed-off-by: Ivan Kanakarakis --- src/saml2/entity.py | 133 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 90 insertions(+), 43 deletions(-) diff --git a/src/saml2/entity.py b/src/saml2/entity.py index 88c2606b..f228f0fa 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -760,14 +760,19 @@ class Entity(HTTPBase): if not has_encrypt_cert and encrypt_cert_assertion is None: encrypt_assertion = False + # XXX if encrypt_assertion or encrypted_advice_attributes + # XXX once in, response becomes a str and uses signed_instance_factory if ( + # XXX goto part-C encrypt_assertion or ( + # XXX goto part-B encrypted_advice_attributes and response.assertion.advice is not None and len(response.assertion.advice.assertion) == 1 ) ): + # XXX part-A (common) prepare sign response if sign: response.signature = pre_signature_part( response.id, @@ -780,6 +785,7 @@ class Entity(HTTPBase): else: sign_class = [] + # XXX part-B if encrypted_advice_attributes if ( encrypted_advice_attributes and response.assertion.advice is not None @@ -788,94 +794,135 @@ class Entity(HTTPBase): _assertions = response.assertion if not isinstance(_assertions, list): _assertions = [_assertions] + for _assertion in _assertions: _assertion.advice.encrypted_assertion = [] - _assertion.advice.encrypted_assertion.append( - EncryptedAssertion()) - _advice_assertions = copy.deepcopy( - _assertion.advice.assertion) + _assertion.advice.encrypted_assertion.append(EncryptedAssertion()) + _advice_assertions = copy.deepcopy(_assertion.advice.assertion) _assertion.advice.assertion = [] + if not isinstance(_advice_assertions, list): _advice_assertions = [_advice_assertions] + for tmp_assertion in _advice_assertions: to_sign_advice = [] + # XXX prepare sign assertion if sign_assertion and not pefim: tmp_assertion.signature = pre_signature_part( - tmp_assertion.id, self.sec.my_cert, 1, - sign_alg=sign_alg, digest_alg=digest_alg) + tmp_assertion.id, + self.sec.my_cert, + 1, + sign_alg=sign_alg, + digest_alg=digest_alg, + ) to_sign_advice.append( - (class_name(tmp_assertion), tmp_assertion.id)) + (class_name(tmp_assertion), tmp_assertion.id), + ) + # XXX prepare encrypt assertion # tmp_assertion = response.assertion.advice.assertion[0] - _assertion.advice.encrypted_assertion[ - 0].add_extension_element(tmp_assertion) + _assertion.advice.encrypted_assertion[0].add_extension_element( + tmp_assertion + ) if encrypt_assertion_self_contained: - advice_tag = \ - response.assertion.advice._to_element_tree().tag + 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) + 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"]]) - + [ + "/*[local-name()=\"%s\"]" % v + for v in [ + "Response", + "Assertion", + "Advice", + "EncryptedAssertion", + "Assertion" + ] + ] + ) + + # XXX sign assertion if to_sign_advice: - response = signed_instance_factory(response, - self.sec, - to_sign_advice) + response = signed_instance_factory( + response, self.sec, to_sign_advice + ) + + # XXX encrypt assertion response = self._encrypt_assertion( - encrypt_cert_advice, sp_entity_id, response, - node_xpath=node_xpath) + encrypt_cert_advice, + sp_entity_id, + response, + node_xpath=node_xpath, + ) response = response_from_string(response) + # XXX part-C if encrypt_assertion if encrypt_assertion: to_sign_assertion = [] - if sign_assertion is not None and sign_assertion: + + # XXX prepare sign assertion + if sign_assertion: _assertions = response.assertion + if not isinstance(_assertions, list): _assertions = [_assertions] + for _assertion in _assertions: _assertion.signature = pre_signature_part( - _assertion.id, self.sec.my_cert, 1, - sign_alg=sign_alg, digest_alg=digest_alg) + _assertion.id, + self.sec.my_cert, + 1, + sign_alg=sign_alg, + digest_alg=digest_alg, + ) to_sign_assertion.append( - (class_name(_assertion), _assertion.id)) + (class_name(_assertion), _assertion.id), + ) + + # XXX prepare encrypt assertion if encrypt_assertion_self_contained: try: - assertion_tag = response.assertion._to_element_tree( - - ).tag + assertion_tag = response.assertion._to_element_tree().tag except: - assertion_tag = response.assertion[ - 0]._to_element_tree().tag + assertion_tag = response.assertion[0]._to_element_tree().tag response = pre_encrypt_assertion(response) - response = \ - response.get_xml_string_with_self_contained_assertion_within_encrypted_assertion( - assertion_tag) + response = response.get_xml_string_with_self_contained_assertion_within_encrypted_assertion( + assertion_tag + ) else: response = pre_encrypt_assertion(response) + + # XXX sign assertion if to_sign_assertion: - response = signed_instance_factory(response, self.sec, - to_sign_assertion) - response = self._encrypt_assertion(encrypt_cert_assertion, - sp_entity_id, response) + response = signed_instance_factory( + response, self.sec, to_sign_assertion + ) + + # XXX encrypt assertion + response = self._encrypt_assertion( + encrypt_cert_assertion, sp_entity_id, response + ) else: + # XXX sign other parts! (defiend by to_sign) if to_sign: - response = signed_instance_factory(response, self.sec, - to_sign) + response = signed_instance_factory(response, self.sec, to_sign) + + # XXX part-D (common) sign response + # XXX handle response having been signed/encrypted => str if sign: return signed_instance_factory(response, self.sec, sign_class) else: return response + # XXX sign response if sign: return self.sign( response, to_sign=to_sign, sign_alg=sign_alg, digest_alg=digest_alg ) - else: - return response + + return response # XXX DONE calls self.sign must figure out sign def _status_response( -- cgit v1.2.1 From dbebbd4434a96e83a30be42221a9f2e2897a1cda Mon Sep 17 00:00:00 2001 From: Ivan Kanakarakis Date: Tue, 8 Dec 2020 00:03:53 +0200 Subject: Resolve sign_alg and digest_alg wherever pre_signature_part is called Signed-off-by: Ivan Kanakarakis --- src/saml2/entity.py | 4 ++++ src/saml2/server.py | 30 +++++++++++++++++++++--------- tests/test_52_default_sign_alg.py | 26 +++++++------------------- 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/src/saml2/entity.py b/src/saml2/entity.py index f228f0fa..7fd18da9 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -772,6 +772,10 @@ class Entity(HTTPBase): and len(response.assertion.advice.assertion) == 1 ) ): + # XXX sig/digest-allowed should be configurable + sign_alg = sign_alg or self.signing_algorithm + digest_alg = digest_alg or self.digest_algorithm + # XXX part-A (common) prepare sign response if sign: response.signature = pre_signature_part( diff --git a/src/saml2/server.py b/src/saml2/server.py index 808ec679..9e34cce2 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -414,8 +414,8 @@ class Server(Entity): **kwargs) return assertion - # XXX calls pre_signature_part - # XXX > _response + # XXX DONE calls pre_signature_part + # XXX calls _response def _authn_response( self, in_response_to, @@ -479,7 +479,6 @@ class Server(Entity): if farg is None: assertion_args = {} - args = {} # if identity: _issuer = self._issuer(issuer) @@ -517,13 +516,21 @@ class Server(Entity): to_sign = [] if not encrypt_assertion: if sign_assertion: + # XXX self.signing_algorithm self.digest_algorithm defined by entity + # XXX this should be handled through entity.py + # XXX sig/digest-allowed should be configurable + sign_alg = sign_alg or self.signing_algorithm + digest_alg = digest_alg or self.digest_algorithm + assertion.signature = pre_signature_part( - assertion.id, self.sec.my_cert, 2, sign_alg=sign_alg, digest_alg=digest_alg + assertion.id, + self.sec.my_cert, + 2, + sign_alg=sign_alg, + digest_alg=digest_alg, ) to_sign.append((class_name(assertion), assertion.id)) - args["assertion"] = assertion - if (self.support_AssertionIDRequest() or self.support_AuthnQuery()): self.session_db.store_assertion(assertion, to_sign) @@ -544,7 +551,7 @@ class Server(Entity): pefim=pefim, sign_alg=sign_alg, digest_alg=digest_alg, - **args, + assertion=assertion, ) # ------------------------------------------------------------------------ @@ -868,7 +875,7 @@ class Server(Entity): digest_alg=digest_alg, ) - # XXX calls pre_signature_part without ensuring sign_alg/digest_alg + # XXX DONE calls pre_signature_part # XXX DONE idp create > [...] def create_assertion_id_request_response( self, assertion_id, sign=None, sign_alg=None, digest_alg=None, **kwargs @@ -880,7 +887,12 @@ class Server(Entity): if to_sign: if assertion.signature is None: - # XXX calls pre_signature_part without ensuring sign_alg/digest_alg + # XXX self.signing_algorithm self.digest_algorithm defined by entity + # XXX this should be handled through entity.py + # XXX sig/digest-allowed should be configurable + sign_alg = sign_alg or self.signing_algorithm + digest_alg = digest_alg or self.digest_algorithm + assertion.signature = pre_signature_part( assertion.id, self.sec.my_cert, diff --git a/tests/test_52_default_sign_alg.py b/tests/test_52_default_sign_alg.py index 274ee858..fee4ee21 100644 --- a/tests/test_52_default_sign_alg.py +++ b/tests/test_52_default_sign_alg.py @@ -42,17 +42,8 @@ def get_ava(assertion): class TestSignedResponse(): - def setup_class(self): self.server = Server("idp_conf") - sign_alg = Mock() - sign_alg.return_value = ds.SIG_RSA_SHA512 - digest_alg = Mock() - digest_alg.return_value = ds.DIGEST_SHA512 - self.restet_default = ds.DefaultSignature - ds.DefaultSignature = MagicMock() - ds.DefaultSignature().get_sign_alg = sign_alg - ds.DefaultSignature().get_digest_alg = digest_alg conf = config.SPConfig() conf.load_file("server_conf") self.client = client.Saml2Client(conf) @@ -62,7 +53,6 @@ class TestSignedResponse(): "mail": ["derek@nyy.mlb.com"], "title": "The man"} def teardown_class(self): - ds.DefaultSignature = self.restet_default self.server.close() def verify_assertion(self, assertion): @@ -76,7 +66,6 @@ class TestSignedResponse(): 'surName': ['Jeter'], 'title': ['The man']} def test_signed_response(self): - print(ds.DefaultSignature().get_digest_alg()) name_id = self.server.ident.transient_nameid( "urn:mace:example.com:saml:roland:sp", "id12") @@ -96,11 +85,10 @@ class TestSignedResponse(): assert signed_resp sresponse = response_from_string(signed_resp) - assert ds.SIG_RSA_SHA512 in str(sresponse), "Not correctly signed!" - assert ds.DIGEST_SHA512 in str(sresponse), "Not correctly signed!" + assert ds.SIG_RSA_SHA1 in str(sresponse), "Not correctly signed!" + assert ds.DIGEST_SHA1 in str(sresponse), "Not correctly signed!" def test_signed_response_1(self): - signed_resp = self.server.create_authn_response( self.ava, "id12", # in_response_to @@ -112,15 +100,15 @@ class TestSignedResponse(): ) sresponse = response_from_string(signed_resp) - assert ds.SIG_RSA_SHA512 in str(sresponse), "Not correctly signed!" - assert ds.DIGEST_SHA512 in str(sresponse), "Not correctly signed!" + assert ds.SIG_RSA_SHA1 in str(sresponse), "Not correctly signed!" + assert ds.DIGEST_SHA1 in str(sresponse), "Not correctly signed!" 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) assert valid - assert ds.SIG_RSA_SHA512 in str(sresponse.assertion[0]), "Not correctly signed!" - assert ds.DIGEST_SHA512 in str(sresponse.assertion[0]), "Not correctly signed!" + assert ds.SIG_RSA_SHA1 in str(sresponse.assertion[0]), "Not correctly signed!" + assert ds.DIGEST_SHA1 in str(sresponse.assertion[0]), "Not correctly signed!" valid = self.server.sec.verify_signature(signed_resp, self.server.config.cert_file, node_name='urn:oasis:names:tc:SAML:2.0:assertion:Assertion', @@ -130,7 +118,6 @@ class TestSignedResponse(): self.verify_assertion(sresponse.assertion) def test_signed_response_2(self): - signed_resp = self.server.create_authn_response( self.ava, "id12", # in_response_to @@ -161,6 +148,7 @@ class TestSignedResponse(): self.verify_assertion(sresponse.assertion) + if __name__ == "__main__": ts = TestSignedResponse() ts.setup_class() -- cgit v1.2.1 From b8117998bf6fb1f516b6220fc4462efab17e7667 Mon Sep 17 00:00:00 2001 From: Ivan Kanakarakis Date: Tue, 8 Dec 2020 00:26:41 +0200 Subject: Calculate entity configurations and then choose Signed-off-by: Ivan Kanakarakis --- src/saml2/entity.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/saml2/entity.py b/src/saml2/entity.py index 7fd18da9..71e9ecfb 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -151,13 +151,12 @@ class Entity(HTTPBase): self.config.getattr('digest_algorithm') or def_sig.get_digest_alg() ) - sign_config = ( - self.config.getattr("authn_requests_signed", "sp") - if self.entity_type == "sp" - else self.config.getattr("sign_response", "idp") - if self.entity_type == "idp" - else False - ) + + sign_config_per_entity_type = { + 'sp': self.config.getattr("authn_requests_signed", "sp"), + 'idp': self.config.getattr("sign_response", "idp"), + } + sign_config = sign_config_per_entity_type.get(self.entity_type, False) self.should_sign = sign_config for item in ["cert_file", "key_file", "ca_certs"]: -- cgit v1.2.1 From eeaf5e0ba6a20a41e212233f3350ada10b35d767 Mon Sep 17 00:00:00 2001 From: Ivan Kanakarakis Date: Tue, 8 Dec 2020 14:33:16 +0200 Subject: Update documentation Signed-off-by: Ivan Kanakarakis --- docs/howto/config.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/howto/config.rst b/docs/howto/config.rst index 3e0ec06d..0fa2401a 100644 --- a/docs/howto/config.rst +++ b/docs/howto/config.rst @@ -1020,7 +1020,7 @@ signing_algorithm Default algorithm to be used. Example:: - 'signing_algorithm': saml2.xmldsig.SIG_RSA_SHA256, + 'signing_algorithm': "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" digest_algorithm @@ -1028,7 +1028,7 @@ digest_algorithm Default algorithm to be used. Example:: - 'digest_algorithm': saml2.xmldsig.DIGEST_SHA256, + 'digest_algorithm': "http://www.w3.org/2001/04/xmlenc#sha512" logout_responses_signed -- cgit v1.2.1