From 397cfc8d42ae50be6c7c59d953c85685d3ce6274 Mon Sep 17 00:00:00 2001 From: Alexander Schrijver Date: Wed, 13 May 2020 16:45:56 +0200 Subject: [PATCH 1/4] Add the ability to decrypt EncryptedIDs from AttributeStatements. --- src/onelogin/saml2/response.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 04264919..b5525b0f 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -591,6 +591,19 @@ def get_attributes(self): if attr_text: values.append(attr_text) + # Parse encrypted ids + for encrypted_id in attr.iterchildren('{%s}EncryptedID' % OneLogin_Saml2_Constants.NSMAP['saml']): + key = self.__settings.get_sp_key() + encrypted_data = encrypted_id.getchildren()[0] + nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) + values.append({ + 'NameID': { + 'Format': nameid.get('Format'), + 'NameQualifier': nameid.get('NameQualifier'), + 'value': nameid.text + } + }) + # Parse any nested NameID children for nameid in attr.iterchildren('{%s}NameID' % OneLogin_Saml2_Constants.NSMAP['saml']): values.append({ From 17bacb3cca55b2a68078ec5236859bb18fffda41 Mon Sep 17 00:00:00 2001 From: Alexander Schrijver Date: Tue, 26 May 2020 15:29:37 +0200 Subject: [PATCH 2/4] Make sure EncryptedKey is included in the KeyInfo when decrypting EncryptedIDs. --- src/onelogin/saml2/response.py | 65 +++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index b5525b0f..e1f9c1b7 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -594,7 +594,9 @@ def get_attributes(self): # Parse encrypted ids for encrypted_id in attr.iterchildren('{%s}EncryptedID' % OneLogin_Saml2_Constants.NSMAP['saml']): key = self.__settings.get_sp_key() + self.__prepare_keyinfo(encrypted_id) encrypted_data = encrypted_id.getchildren()[0] + nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key) values.append({ 'NameID': { @@ -859,6 +861,39 @@ def __query(self, query, tagid=None): document = self.document return OneLogin_Saml2_XML.query(document, query, None, tagid) + def __prepare_keyinfo(self, node): + """ + Directly include the EncryptedKey in the KeyInfo of the EncryptedData. This + is needed for decrypting with OneLogin_Saml2_Utils.decrypt_element. + """ + encrypted_data_keyinfo = OneLogin_Saml2_XML.query(node, 'xenc:EncryptedData/ds:KeyInfo') + if not encrypted_data_keyinfo: + raise OneLogin_Saml2_ValidationError( + 'No KeyInfo present, invalid Assertion', + OneLogin_Saml2_ValidationError.KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA + ) + encrypted_data_keyinfo = encrypted_data_keyinfo[0] + children = encrypted_data_keyinfo.getchildren() + if not children: + raise OneLogin_Saml2_ValidationError( + 'KeyInfo has no children nodes, invalid Assertion', + OneLogin_Saml2_ValidationError.CHILDREN_NODE_NOT_FOUND_IN_KEYINFO + ) + for child in children: + if 'RetrievalMethod' in child.tag: + if child.attrib['Type'] != 'http://www.w3.org/2001/04/xmlenc#EncryptedKey': + raise OneLogin_Saml2_ValidationError( + 'Unsupported Retrieval Method found', + OneLogin_Saml2_ValidationError.UNSUPPORTED_RETRIEVAL_METHOD + ) + uri = child.attrib['URI'] + if not uri.startswith('#'): + break + uri = uri.split('#')[1] + encrypted_key = OneLogin_Saml2_XML.query(node, './xenc:EncryptedKey[@Id=$tagid]', None, uri) + if encrypted_key: + encrypted_data_keyinfo.append(encrypted_key[0]) + def __decrypt_assertion(self, xml): """ Decrypts the Assertion @@ -882,35 +917,9 @@ def __decrypt_assertion(self, xml): if encrypted_assertion_nodes: encrypted_data_nodes = OneLogin_Saml2_XML.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData') if encrypted_data_nodes: - keyinfo = OneLogin_Saml2_XML.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData/ds:KeyInfo') - if not keyinfo: - raise OneLogin_Saml2_ValidationError( - 'No KeyInfo present, invalid Assertion', - OneLogin_Saml2_ValidationError.KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA - ) - keyinfo = keyinfo[0] - children = keyinfo.getchildren() - if not children: - raise OneLogin_Saml2_ValidationError( - 'KeyInfo has no children nodes, invalid Assertion', - OneLogin_Saml2_ValidationError.CHILDREN_NODE_NOT_FOUND_IN_KEYINFO - ) - for child in children: - if 'RetrievalMethod' in child.tag: - if child.attrib['Type'] != 'http://www.w3.org/2001/04/xmlenc#EncryptedKey': - raise OneLogin_Saml2_ValidationError( - 'Unsupported Retrieval Method found', - OneLogin_Saml2_ValidationError.UNSUPPORTED_RETRIEVAL_METHOD - ) - uri = child.attrib['URI'] - if not uri.startswith('#'): - break - uri = uri.split('#')[1] - encrypted_key = OneLogin_Saml2_XML.query(encrypted_assertion_nodes[0], './xenc:EncryptedKey[@Id=$tagid]', None, uri) - if encrypted_key: - keyinfo.append(encrypted_key[0]) - encrypted_data = encrypted_data_nodes[0] + self.__prepare_keyinfo(encrypted_data) + decrypted = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug=debug, inplace=True) xml.replace(encrypted_assertion_nodes[0], decrypted) return xml From f303bd7c1e9e146256c51c1a4efea922dd32e11b Mon Sep 17 00:00:00 2001 From: Alexander Schrijver Date: Thu, 28 May 2020 15:17:48 +0200 Subject: [PATCH 3/4] When preparing the keyinfo for EncryptedAssertions, I accidentally went 1 level too deep. --- src/onelogin/saml2/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index e1f9c1b7..51d9c395 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -918,7 +918,7 @@ def __decrypt_assertion(self, xml): encrypted_data_nodes = OneLogin_Saml2_XML.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData') if encrypted_data_nodes: encrypted_data = encrypted_data_nodes[0] - self.__prepare_keyinfo(encrypted_data) + self.__prepare_keyinfo(encrypted_data.getparent()) decrypted = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug=debug, inplace=True) xml.replace(encrypted_assertion_nodes[0], decrypted) From 08bcfd05e7a3433319037c61cb5fa10ea9fc167c Mon Sep 17 00:00:00 2001 From: Alexander Schrijver Date: Fri, 8 Jan 2021 17:41:24 +0100 Subject: [PATCH 4/4] Add a test for encrypted ids using the OneLogin_Saml2_Response class. --- .../src/OneLogin/saml2_tests/response_test.py | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py index 5110f6e7..e1f03332 100644 --- a/tests/src/OneLogin/saml2_tests/response_test.py +++ b/tests/src/OneLogin/saml2_tests/response_test.py @@ -3,7 +3,7 @@ # Copyright (c) 2010-2021 OneLogin, Inc. # MIT License -from base64 import b64decode +from base64 import b64decode, b64encode from lxml import etree from datetime import datetime from datetime import timedelta @@ -14,9 +14,11 @@ from xml.dom.minidom import parseString from onelogin.saml2 import compat +from onelogin.saml2.constants import OneLogin_Saml2_Constants from onelogin.saml2.response import OneLogin_Saml2_Response from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils +from onelogin.saml2.xml_utils import OneLogin_Saml2_XML class OneLogin_Saml2_Response_Test(unittest.TestCase): @@ -1861,3 +1863,49 @@ def testGetAssertionNotOnOrAfter(self): response.is_valid(request_data) self.assertIsNone(response.get_error()) self.assertEqual(response.get_assertion_not_on_or_after(), 2671081021) + + def testEncryptedId(self): + """ + Test that decrypting EncryptedID elements works as expected. + """ + settings = OneLogin_Saml2_Settings(self.loadSettingsJSON()) + + base64_content = self.file_contents(join(self.data_path, 'responses', 'valid_unsigned_response.xml.base64')) + xml = b64decode(base64_content) + response_element = OneLogin_Saml2_XML.to_etree(xml) + + # Add an EncryptedID element to the existing response. + encrypted_id = OneLogin_Saml2_Utils.generate_name_id( + "123456782", + sp_nq=None, + nq="urn:etoegang:1.9:EntityConcernedID:RSIN", + sp_format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + cert=settings.get_sp_cert(), + ) + attribute = ( + '' + "" + + encrypted_id + + "" + ) + statement_element = OneLogin_Saml2_XML.query(response_element, '//saml:AttributeStatement') + encrypted_attribute_element = OneLogin_Saml2_XML.to_etree(attribute) + statement_element[0].append(encrypted_attribute_element) + + # Try to parse the Response + response = OneLogin_Saml2_Response( + settings, b64encode(OneLogin_Saml2_XML.to_string(response_element)) + ) + response.is_valid(self.get_request_data()) + attributes = response.get_attributes() + + self.assertEqual( + attributes['urn:etoegang:core:LegalSubjectID'], + [ + {'NameID': { + 'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + 'NameQualifier': 'urn:etoegang:1.9:EntityConcernedID:RSIN', + 'value': '123456782'} + } + ] + ) \ No newline at end of file