def test_decrypt_xml_with_document_encrypted(self): self.maxDiff = None with cast(BinaryIO, (DATA_DIR / 'saml_response_decrypted.xml').open('rb')) as f: document_decrypted = parse_xml(f.read()) with cast(BinaryIO, (DATA_DIR / 'saml_response_encrypted.xml').open('rb')) as f: document_encrypted = parse_xml(f.read()) expected = dump_xml(document_decrypted).decode('utf-8') self.assertEqual(decrypt_xml(document_encrypted, KEY_FILE), 1) actual = dump_xml(document_encrypted).decode('utf-8') self.assertXMLEqual(expected, actual)
def test_load_xml_attributes_definition_element(self): data = parse_xml(b'<lightRequest><requestedAttributes><attribute>data</attribute>' b'</requestedAttributes></lightRequest>') with self.assertRaisesMessage(ValidationError, "'<lightRequest><requestedAttributes><attribute>': " "'Missing attribute.definition element.'"): LightRequest.load_xml(data) data = parse_xml(b'<lightRequest><requestedAttributes><attribute><foo>data</foo>' b'</attribute></requestedAttributes></lightRequest>') with self.assertRaisesMessage(ValidationError, '\'<lightRequest><requestedAttributes><attribute><foo>\': ' '"Unexpected element \'foo\'"'): LightRequest.load_xml(data)
def test_encrypt_xml_node(self): supported_ciphers = set(XmlBlockCipher) # type: Set[XmlBlockCipher] if LIBXMLSEC_VERSION < (1, 2, 27): # pragma: no cover supported_ciphers -= { XmlBlockCipher.AES128_GCM, XmlBlockCipher.AES192_GCM, XmlBlockCipher.AES256_GCM } for cipher in supported_ciphers: with cast(BinaryIO, (DATA_DIR / 'saml_response_decrypted.xml').open('rb')) as f: document = parse_xml(f.read()) remove_extra_xml_whitespace(document.getroot()) original = dump_xml(document).decode() # Encrypt <Assertion> assertion = document.find(".//{}".format( Q_NAMES['saml2:Assertion'])) encrypt_xml_node(assertion, CERT_FILE, cipher, XmlKeyTransport.RSA_OAEP_MGF1P) # <Assertion> replaced with <EncryptedData> self.assertIsNone( document.find(".//{}".format(Q_NAMES['saml2:Assertion']))) enc_data = document.find(".//{}/{}".format( Q_NAMES['saml2:EncryptedAssertion'], Q_NAMES['xmlenc:EncryptedData'])) self.assertIsNotNone(enc_data) self.assertEqual(enc_data[0].get('Algorithm'), cipher.value) # Verify that the original and decrypted document match. self.assertEqual(decrypt_xml(document, KEY_FILE), 1) decrypted = dump_xml(document).decode() self.assertEqual(original, decrypted)
def test_verify_xml_signatures_fail(self): with cast(TextIO, (DATA_DIR / 'signed_response.xml').open('r')) as f: tree = parse_xml(f.read()) # Fails because of pretty printing self.assertRaises(SecurityError, verify_xml_signatures, tree, CERT_FILE)
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_xml_with_document_encrypted_wrong_key(self): self.maxDiff = None with cast(BinaryIO, (DATA_DIR / 'saml_response_encrypted.xml').open('rb')) as f: document_encrypted = parse_xml(f.read()) self.assertRaises(xmlsec.Error, decrypt_xml, document_encrypted, WRONG_KEY_FILE)
def test_load_xml_wrong_field_element(self): with self.assertRaisesMessage( ValidationError, '\'<xmlUser><name><familyName>\': "Unknown element \'familyName\'."' ): XMLUser.load_xml( parse_xml(XML_USER_DATA.replace('lastName', 'familyName')))
def pop_light_response(self, uid: str) -> Optional[LightResponse]: """Look up a LightResponse by a unique id and then remove it.""" data = self.get_cache(self.response_cache_name).get_and_remove(uid) LOGGER.debug('Got Light Response from cache: id=%r, data=%s', uid, data) return LightResponse().load_xml( parse_xml(data)) if data is not None else None
def test_load_xml_attributes_unexpected_element(self): data = parse_xml(b'<lightRequest><requestedAttributes><myField>data</myField>' b'</requestedAttributes></lightRequest>') with self.assertRaisesMessage(ValidationError, '\'<lightRequest><requestedAttributes><myField>\': ' '"Unexpected element \'myField\'"'): LightRequest.load_xml(data)
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 get_saml_request(self, country_parameter: str, cert_file: Optional[str]) -> SAMLRequest: """ Extract and decrypt a SAML request from POST data. :param country_parameter: A parameter containing citizen country code. :param cert_file: The path of a certificate to verify the signature. :return: A SAML request. """ try: request = SAMLRequest( parse_xml( b64decode( self.request.POST.get('SAMLRequest', '').encode('ascii'))), self.request.POST[country_parameter].upper(), self.request.POST.get('RelayState')) except XMLSyntaxError as e: raise ParseError(str(e)) from None LOGGER.info('[#%r] Received SAML request: id=%r, issuer=%r', self.log_id, request.id, request.issuer) if cert_file: request.verify_request(cert_file) return request
def test_load_xml_with_response_status_failure(self): self.maxDiff = None response = self.create_response(False) with cast(BinaryIO, (DATA_DIR / 'light_response_failure.xml').open('r')) as f: data = f.read() self.assertEqual(LightResponse.load_xml(parse_xml(data)), response)
def test_export_xml_full_sample(self): self.maxDiff = None with cast(BinaryIO, (DATA_DIR / 'light_request.xml').open('rb')) as f: data = f.read() request = LightRequest.load_xml(parse_xml(data)) self.assertEqual(dump_xml(request.export_xml()), data)
def test_load_xml_attribute_values_unexpected_element(self): data = parse_xml(b'<lightRequest><requestedAttributes><attribute><definition>data</definition><foo/>' b'</attribute></requestedAttributes></lightRequest>') with self.assertRaisesMessage(ValidationError, '\'<lightRequest><requestedAttributes><attribute><foo>\': ' '"Unexpected element \'foo\'"'): LightRequest.load_xml(data)
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_verify_xml_signatures_success(self): with cast(TextIO, (DATA_DIR / 'signed_response.xml').open('r')) as f: tree = parse_xml(f.read()) remove_extra_xml_whitespace( tree) # Reverts pretty printing applied after signing verify_xml_signatures(tree, 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_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 post(self, request: HttpRequest) -> HttpResponse: """Handle a HTTP POST request.""" saml_response_xml = b64decode(self.request.POST.get('SAMLResponse', '').encode('ascii')).decode('utf-8') if saml_response_xml: # Verify signatures cert_file = (CONNECTOR_SETTINGS.service_provider['response_signature'] or {}).get('cert_file') if cert_file: response = SAMLResponse(parse_xml(saml_response_xml)) response.verify_response(cert_file) response.verify_assertion(cert_file) # Reformat with pretty printing for display saml_response_xml = dump_xml(parse_xml(saml_response_xml)).decode('utf-8') self.saml_response = saml_response_xml self.relay_state = self.request.POST.get('RelayState') return self.get(request)
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_create_light_request_our_issuer_set(self): saml_request_xml, _saml_request_encoded = self.load_saml_request() view = ServiceProviderRequestView() view.saml_request = SAMLRequest(parse_xml(saml_request_xml), 'ca', 'xyz') light_request = view.create_light_request('test-saml-request-issuer', 'test-light-request-issuer') self.assertEqual(light_request.issuer, 'test-light-request-issuer')
def test_create_light_request_success(self): self.maxDiff = None with cast(TextIO, (DATA_DIR / 'saml_request.xml').open('r')) as f: data = f.read() saml_request = SAMLRequest(parse_xml(data), 'CA', 'relay123') self.assertEqual( saml_request.create_light_request().get_data_as_dict(), LIGHT_REQUEST_DICT)
def test_create_light_request_wrong_issuer(self): saml_request_xml, _saml_request_encoded = self.load_saml_request() view = ServiceProviderRequestView() view.saml_request = SAMLRequest(parse_xml(saml_request_xml), 'ca', 'xyz') with self.assertRaisesMessage(SecurityError, 'Invalid SAML request issuer'): view.create_light_request('wrong-saml-issuer', 'test-light-request-issuer')
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_get_saml_response_invalid_signature(self): with cast(TextIO, (DATA_DIR / 'signed_response_and_assertion.xml').open('r')) as f: tree = parse_xml(f.read()) remove_extra_xml_whitespace(tree) saml_response_encoded = b64encode(dump_xml(tree, pretty_print=False)).decode('ascii') view = IdentityProviderResponseView() view.request = self.factory.post(self.url, {'SAMLResponse': saml_response_encoded}) self.assertRaises(SecurityError, view.get_saml_response, None, WRONG_CERT_FILE)
def load_saml_request(self, signed=False) -> Tuple[str, str]: path = 'saml_request.xml' if not signed else 'saml_request_signed.xml' with cast(BinaryIO, (DATA_DIR / path).open('rb')) as f: saml_request_pretty = f.read() saml_request = parse_xml(saml_request_pretty) remove_extra_xml_whitespace(saml_request) saml_request_encoded = b64encode( dump_xml(saml_request, pretty_print=False)) return saml_request_pretty.decode( 'utf-8'), saml_request_encoded.decode('ascii')
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_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_put_light_response(self): with cast(TextIO, (DATA_DIR / 'light_response.xml').open('r')) as f: data = f.read() response = LightResponse.load_xml(parse_xml(data)) self.storage.put_light_response('abc', response) self.assertEqual(self.client_class_mock.mock_calls, [call(timeout=33)]) self.assertEqual(self.client_mock.mock_calls, [ call.connect(self.HOST, self.PORT), call.get_cache(self.RESPONSE_CACHE_NAME), call.get_cache().put('abc', data) ])
def test_from_light_request_invalid_id(self): self.maxDiff = None with cast(BinaryIO, (DATA_DIR / 'light_request_minimal.xml').open('rb')) as f: request = LightRequest.load_xml(parse_xml(f)) request.id = '0day' with self.assert_validation_error( 'id', "Light request id is not a valid XML id: '0day'"): SAMLRequest.from_light_request( request, 'test/destination', datetime(2017, 12, 11, 14, 12, 5, 148000))