def test_create_light_response_no_auth_statement(self): root = Element(Q_NAMES['saml2p:Response'], nsmap=EIDAS_NAMESPACES) SubElement(root, Q_NAMES['saml2:Assertion']) saml = SAMLResponse(ElementTree(root)) response = saml.create_light_response() self.assertIsNone(response.ip_address) self.assertIsNone(response.level_of_assurance)
def test_verify_assertion_nia_not_decrypted(self): with cast(TextIO, (DATA_DIR / 'nia_test_response.xml').open('r')) as f: tree = parse_xml(f.read()) remove_extra_xml_whitespace(tree) response = SAMLResponse(tree) self.assertFalse(response.verify_assertion(NIA_CERT_FILE))
def test_create_light_response_failed_response(self): self.maxDiff = None with cast(BinaryIO, (DATA_DIR / 'saml_response_failed.xml').open('rb')) as f: response = SAMLResponse(parse_xml(f), 'relay123') self.assertEqual(response.create_light_response(), self.create_light_response(False))
def test_verify_response_nia(self): with cast(TextIO, (DATA_DIR / 'nia_test_response.xml').open('r')) as f: tree = parse_xml(f.read()) remove_extra_xml_whitespace(tree) response = SAMLResponse(tree) response.verify_response(NIA_CERT_FILE)
def test_create_light_response_not_encrypted(self): self.maxDiff = None with cast(BinaryIO, (DATA_DIR / 'saml_response.xml').open('rb')) as f: saml_response = SAMLResponse(parse_xml(f), 'relay123') light_response = saml_response.create_light_response() self.assertEqual(light_response, self.create_light_response(True))
def test_encrypt_assertion_no_assertion(self): root = Element(Q_NAMES['saml2p:Response']) response = SAMLResponse((ElementTree(root))) # Nothing to encrypt. self.assertFalse( response.encrypt_assertion(CERT_FILE, XmlBlockCipher.AES256_CBC, XmlKeyTransport.RSA_OAEP_MGF1P))
def test_str(self): self.assertEqual( str(SAMLResponse(ElementTree(Element('root')), 'relay')), "relay_state = 'relay', document = <?xml version='1.0' encoding='utf-8' standalone='yes'?>\n<root/>\n" ) self.assertEqual(str(SAMLResponse(None, None)), 'relay_state = None, document = None')
def test_verify_response_without_assertion(self): with cast(TextIO, (DATA_DIR / 'signed_failed_response.xml').open('r')) as f: tree = parse_xml(f.read()) remove_extra_xml_whitespace(tree) response = SAMLResponse(tree) response.verify_response(CERT_FILE)
def test_sign_response_without_issuer(self): root = Element(Q_NAMES['saml2p:Response']) SubElement(root, Q_NAMES['saml2:Assertion']) response = SAMLResponse(ElementTree(root)) response.sign_response(**SIGNATURE_OPTIONS) self.assertIsNotNone(response.response_signature) self.assertIsNone(response.assertion_signature) self.assertEqual(root.index(response.response_signature), 0)
def test_create_light_response_with_unsupported_sub_status(self): with cast(BinaryIO, (DATA_DIR / 'saml_response_failed_unsupported_sub_status.xml' ).open('rb')) as f: response = SAMLResponse(parse_xml(f), 'relay123') expected = self.create_light_response(False) expected.status.sub_status_code = None self.assertEqual(response.create_light_response(), expected)
def test_sign_assertion_with_issuer(self): root = Element(Q_NAMES['saml2p:Response']) assertion = SubElement(root, Q_NAMES['saml2:Assertion']) SubElement(assertion, Q_NAMES['saml2:Issuer']) response = SAMLResponse(ElementTree(root)) self.assertTrue(response.sign_assertion(**SIGNATURE_OPTIONS)) self.assertIsNone(response.response_signature) self.assertIsNotNone(response.assertion_signature) self.assertEqual(assertion.index(response.assertion_signature), 1)
def test_verify_and_remove_signature_bad_reference(self, signatures_mock): root = Element('root') signature = SubElement(root, 'signature') child = SubElement(root, 'child') signatures_mock.return_value = [SignatureInfo(signature, (child, ))] response = SAMLResponse(ElementTree(root)) with self.assertRaisesMessage( SecurityError, 'Signature does not reference parent element'): response._verify_and_remove_signature(signature, 'cert.pem') self.assertEqual(signatures_mock.mock_calls, [call(root, 'cert.pem')])
def test_verify_and_remove_signature_not_found(self, signatures_mock): root = Element('root') signature = SubElement(root, 'signature') SubElement(root, 'child') signatures_mock.return_value = [SignatureInfo(signature, (root, ))] response = SAMLResponse(ElementTree(root)) with self.assertRaisesMessage(SecurityError, 'Signature not found'): response._verify_and_remove_signature(Element('signature2'), 'cert.pem') self.assertEqual(signatures_mock.mock_calls, [call(root, 'cert.pem')])
def test_create_light_response_with_status_version_mismatch(self): with cast( BinaryIO, (DATA_DIR / 'saml_response_failed_version_mismatch.xml').open('rb')) as f: response = SAMLResponse(parse_xml(f), 'relay123') expected = self.create_light_response(False) expected.status.status_code = StatusCode.REQUESTER expected.status.sub_status_code = SubStatusCode.VERSION_MISMATCH self.assertEqual(response.create_light_response(), expected)
def test_sign_assertion_response_signed(self): root = Element(Q_NAMES['saml2p:Response']) SubElement(root, Q_NAMES['saml2:Assertion']) response_signature = SubElement(root, Q_NAMES['ds:Signature']) response = SAMLResponse(ElementTree(root)) with self.assertRaisesMessage(SecurityError, 'response signature is already present'): response.sign_assertion(**SIGNATURE_OPTIONS) self.assertIs(response.response_signature, response_signature) # Preserved self.assertIsNone(response.assertion_signature)
def test_sign_assertion_decrypted(self): root = Element(Q_NAMES['saml2p:Response']) assertion = SubElement( SubElement(root, Q_NAMES['saml2:EncryptedAssertion']), Q_NAMES['saml2:Assertion']) response_signature = SubElement(root, Q_NAMES['ds:Signature']) assertion_signature = SubElement(assertion, Q_NAMES['ds:Signature']) response = SAMLResponse(ElementTree(root)) self.assertFalse(response.sign_assertion(**SIGNATURE_OPTIONS)) self.assertIs(response.response_signature, response_signature) # Preserved self.assertIs(response.assertion_signature, assertion_signature) # Preserved
def test_create_light_response_decrypted(self): self.maxDiff = None with cast(BinaryIO, (DATA_DIR / 'saml_response_decrypted.xml').open('rb')) as f: response = SAMLResponse(parse_xml(f), 'relay123') light_response = self.create_light_response( True, level_of_assurance=LevelOfAssurance.SUBSTANTIAL, ip_address='217.31.205.1', id='_751e557772344aa59e9e3f35d2c9f6d6', in_response_to_id='e399fb9b-9454-4284-831f-4aa33d83757e', issuer='urn:microsoft:cgg2010:fpsts') self.assertEqual(response.create_light_response(), light_response)
def get_saml_response(self, key_file: Optional[str], cert_file: Optional[str]) -> SAMLResponse: """ Extract and decrypt a SAML response from POST data. :param key_file: An optional path to a key to decrypt the response. :param cert_file: An optional path to a certificate to verify the response. :return: A SAML response. """ raw_response = b64decode( self.request.POST.get('SAMLResponse', '').encode('ascii')).decode('utf-8') LOGGER.debug('Raw SAML Response: %s', raw_response) try: response = SAMLResponse(parse_xml(raw_response), self.request.POST.get('RelayState')) except XMLSyntaxError as e: raise ParseError(str(e)) from None LOGGER.info( '[#%r] Received SAML response: id=%r, issuer=%r, in_response_to_id=%r', self.log_id, response.id, response.issuer, response.in_response_to_id) if cert_file: response.verify_response(cert_file) if key_file: response.decrypt(key_file) if cert_file: response.verify_assertion(cert_file) return response
def test_decrypt(self): self.maxDiff = None with cast(BinaryIO, (DATA_DIR / 'saml_response_decrypted.xml').open('rb')) as f: document_decrypted = f.read() with cast(BinaryIO, (DATA_DIR / 'saml_response_encrypted.xml').open('rb')) as f: document_encrypted = f.read() response = SAMLResponse(parse_xml(document_encrypted)) self.assertEqual(response.decrypt(KEY_FILE), 1) self.assertXMLEqual( dump_xml(response.document).decode('utf-8'), document_decrypted.decode('utf-8'))
def test_create_light_response_with_extra_elements(self): self.maxDiff = None with cast(BinaryIO, (DATA_DIR / 'saml_response.xml').open('rb')) as f: response = SAMLResponse(parse_xml(f), 'relay123') SubElement( response.document.find(".//{}".format( Q_NAMES['saml2p:StatusCode'])), 'something') SubElement( response.document.find(".//{}".format( Q_NAMES['saml2:AuthnStatement'])), 'something') SubElement( response.document.find(".//{}".format( Q_NAMES['saml2:AuthnContext'])), 'something') self.assertEqual(response.create_light_response(), self.create_light_response(True))
def test_assertion_too_many(self): root = Element(Q_NAMES['saml2p:Response']) SubElement(root, Q_NAMES['saml2:Assertion']) SubElement(SubElement(root, Q_NAMES['saml2:EncryptedAssertion']), Q_NAMES['saml2:Assertion']) with self.assertRaisesMessage(ParseError, 'Too many assertion elements'): SAMLResponse(ElementTree(root)).assertion
def test_assertion_exists_decrypted(self): root = Element(Q_NAMES['saml2p:Response']) encrypted_assertion = SubElement(root, Q_NAMES['saml2:EncryptedAssertion']) decrypted_assertion = SubElement(encrypted_assertion, Q_NAMES['saml2:Assertion']) self.assertIs( SAMLResponse(ElementTree(root)).assertion, decrypted_assertion)
def test_assertion_signature_not_exists(self): # Base structure root = Element(Q_NAMES['saml2p:Response']) assertion = SubElement(root, Q_NAMES['saml2:Assertion']) # Place a few signature elements as booby traps SubElement(root, Q_NAMES['ds:Signature']) SubElement(SubElement(assertion, Q_NAMES['saml2:Issuer']), Q_NAMES['ds:Signature']) self.assertIsNone(SAMLResponse(ElementTree(root)).assertion_signature)
def test_encrypt_assertion_without_encrypted_assertion_elm(self): root = Element(Q_NAMES['saml2p:Response']) first_child = SubElement(root, 'FirstChild') assertion = SubElement(root, Q_NAMES['saml2:Assertion']) SubElement(assertion, Q_NAMES['saml2:Issuer']).text = 'CZ.NIC' third_child = SubElement(root, 'ThirdChild') response = SAMLResponse((ElementTree(root))) # Encryption happened. self.assertTrue( response.encrypt_assertion(CERT_FILE, XmlBlockCipher.AES256_CBC, XmlKeyTransport.RSA_OAEP_MGF1P)) # Order of elements kept. self.assertIs(root[0], first_child) self.assertIs(root[2], third_child) # <Assertion> replaced with <EncryptedAssertion>. self.assertEqual(root[1].tag, Q_NAMES['saml2:EncryptedAssertion']) self.assertEqual(root[1][0].tag, Q_NAMES['xmlenc:EncryptedData']) # Make sure we can decrypt the result. self.assertEqual(response.decrypt(KEY_FILE), 1)
def test_create_light_response_correct_id_and_issuer(self): self.maxDiff = None view = IdentityProviderResponseView() view.request = self.factory.post(self.url) with cast(TextIO, (DATA_DIR / 'saml_response.xml').open('r')) as f: view.saml_response = SAMLResponse(parse_xml(f.read()), 'relay123') light_response = view.create_light_response('test-light-response-issuer') self.assertEqual(light_response.id, 'test-saml-response-id') # Preserved self.assertEqual(light_response.in_response_to_id, 'test-saml-request-id') # Preserved self.assertEqual(light_response.issuer, 'test-light-response-issuer') # Replaced
def test_create_light_response_unrecognized_auth_context_class(self): root = Element(Q_NAMES['saml2p:Response'], { 'ID': 'id', 'InResponseTo': 'id0' }, nsmap=EIDAS_NAMESPACES) context_class = SubElement( SubElement( SubElement(SubElement(root, Q_NAMES['saml2:Assertion']), Q_NAMES['saml2:AuthnStatement']), Q_NAMES['saml2:AuthnContext']), Q_NAMES['saml2:AuthnContextClassRef']) context_class.text = 'saml2:AuthnContextClassRef:unrecognized' saml = SAMLResponse(ElementTree(root)) response = saml.create_light_response() self.assertEqual(response.id, 'id') self.assertEqual(response.in_response_to_id, 'id0') self.assertTrue(response.status.failure) self.assertEqual(response.status.status_code, StatusCode.RESPONDER) self.assertIn('saml2:AuthnContextClassRef:unrecognized', response.status.status_message) self.assertIsNone(response.level_of_assurance)
def test_from_light_response(self): self.maxDiff = None saml_response = SAMLResponse.from_light_response( self.create_light_response(True), 'saml-request-issuer', 'test/destination', datetime(2017, 12, 11, 14, 12, 5, 148000), timedelta(minutes=5)) with cast(TextIO, (DATA_DIR / 'saml_response_from_light_response.xml').open('r')) as f2: data = f2.read() self.assertXMLEqual( dump_xml(saml_response.document).decode('utf-8'), data)
def test_create_light_response_auth_context_class_alias_not_used(self): root = Element(Q_NAMES['saml2p:Response'], { 'ID': 'id', 'InResponseTo': 'id0' }, nsmap=EIDAS_NAMESPACES) context_class = SubElement( SubElement( SubElement(SubElement(root, Q_NAMES['saml2:Assertion']), Q_NAMES['saml2:AuthnStatement']), Q_NAMES['saml2:AuthnContext']), Q_NAMES['saml2:AuthnContextClassRef']) context_class.text = LevelOfAssurance.HIGH.value saml = SAMLResponse(ElementTree(root)) response = saml.create_light_response( {context_class.text: LevelOfAssurance.LOW}) self.assertEqual(response.id, 'id') self.assertEqual(response.in_response_to_id, 'id0') self.assertFalse(response.status.failure) self.assertIsNone(response.status.status_code) self.assertEqual(response.level_of_assurance, LevelOfAssurance.HIGH) # Not overridden
def test_assertion_signature_exists_decrypted(self): # Base structure root = Element(Q_NAMES['saml2p:Response']) encrypted_assertion = SubElement(root, Q_NAMES['saml2:EncryptedAssertion']) decrypted_assertion = SubElement(encrypted_assertion, Q_NAMES['saml2:Assertion']) # Place a few signature elements as booby traps SubElement(root, Q_NAMES['ds:Signature']) SubElement(SubElement(decrypted_assertion, Q_NAMES['saml2:Issuer']), Q_NAMES['ds:Signature']) # This one must be found signature = SubElement(decrypted_assertion, Q_NAMES['ds:Signature']) self.assertIs( SAMLResponse(ElementTree(root)).assertion_signature, signature)
def test_response_signature_not_exists(self): # Base structure root = Element(Q_NAMES['saml2p:Response']) assertion = SubElement(root, Q_NAMES['saml2:Assertion']) encrypted_assertion = SubElement(root, Q_NAMES['saml2:EncryptedAssertion']) decrypted_assertion = SubElement(encrypted_assertion, Q_NAMES['saml2:Assertion']) # Place a few signature elements as booby traps SubElement(assertion, Q_NAMES['ds:Signature']) SubElement(decrypted_assertion, Q_NAMES['ds:Signature']) SubElement(SubElement(assertion, Q_NAMES['saml2:Issuer']), Q_NAMES['ds:Signature']) SubElement(SubElement(decrypted_assertion, Q_NAMES['saml2:Issuer']), Q_NAMES['ds:Signature']) # No signature must be found self.assertIsNone(SAMLResponse(ElementTree(root)).response_signature)