diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py index 04264919..51d9c395 100644 --- a/src/onelogin/saml2/response.py +++ b/src/onelogin/saml2/response.py @@ -591,6 +591,21 @@ 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() + self.__prepare_keyinfo(encrypted_id) + 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({ @@ -846,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 @@ -869,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.getparent()) + decrypted = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug=debug, inplace=True) xml.replace(encrypted_assertion_nodes[0], decrypted) return xml 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