diff options
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | src/saml2/response.py | 4 | ||||
-rw-r--r-- | src/saml2/saml.py | 5 | ||||
-rw-r--r-- | tests/saml2_data.py | 30 | ||||
-rw-r--r-- | tests/saml_hok.xml | 45 | ||||
-rw-r--r-- | tests/test_02_saml.py | 50 | ||||
-rw-r--r-- | tests/test_93_hok.py | 53 |
7 files changed, 172 insertions, 18 deletions
@@ -114,6 +114,9 @@ venv.bak/ # Rope project settings .ropeproject +# Visual Studio Code files +.vscode/ + # mkdocs documentation /site diff --git a/src/saml2/response.py b/src/saml2/response.py index 2660e738..c16be47f 100644 --- a/src/saml2/response.py +++ b/src/saml2/response.py @@ -722,11 +722,11 @@ class AuthnResponse(StatusResponse): return True def _holder_of_key_confirmed(self, data): - if not data: + if not data or not data.key_info: return False has_keyinfo = False - for element in extension_elements_to_elements(data, + for element in extension_elements_to_elements(data.key_info, [samlp, saml, xenc, ds]): if isinstance(element, ds.KeyInfo): has_keyinfo = True diff --git a/src/saml2/saml.py b/src/saml2/saml.py index bdb1ec60..0d6728e5 100644 --- a/src/saml2/saml.py +++ b/src/saml2/saml.py @@ -482,8 +482,12 @@ class SubjectConfirmationDataType_(SamlBase): c_any = {"namespace": "##any", "processContents": "lax", "minOccurs": "0", "maxOccurs": "unbounded"} c_any_attribute = {"namespace": "##other", "processContents": "lax"} + c_children['{http://www.w3.org/2000/09/xmldsig#}KeyInfo'] = ('key_info', + [ds.KeyInfo]) + c_cardinality['key_info'] = {"min": 0, "max": 1} def __init__(self, + key_info=None, not_before=None, not_on_or_after=None, recipient=None, @@ -496,6 +500,7 @@ class SubjectConfirmationDataType_(SamlBase): text=text, extension_elements=extension_elements, extension_attributes=extension_attributes) + self.key_info = key_info self.not_before = not_before self.not_on_or_after = not_on_or_after self.recipient = recipient diff --git a/tests/saml2_data.py b/tests/saml2_data.py index fe650e7b..f81f2ce5 100644 --- a/tests/saml2_data.py +++ b/tests/saml2_data.py @@ -123,6 +123,36 @@ TEST_SUBJECT_CONFIRMATION = """<?xml version="1.0" encoding="utf-8"?> </SubjectConfirmation> """ +TEST_HOLDER_OF_KEY_SUBJECT_CONFIRMATION = """<?xml version="1.0" encoding="utf-8"?> +<SubjectConfirmation + Method="urn:oasis:names:tc:SAML:2.0:cm:holder-of-key" + xmlns="urn:oasis:names:tc:SAML:2.0:assertion"> + <SubjectConfirmationData + InResponseTo="responseID" + NotOnOrAfter="2007-09-14T01:05:02Z" + Recipient="recipient"> + <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"> + <X509Data xmlns="http://www.w3.org/2000/09/xmldsig#"> + <X509Certificate xmlns="http://www.w3.org/2000/09/xmldsig#"> +MIICITCCAYoCAQEwDQYJKoZIhvcNAQELBQAwWDELMAkGA1UEBhMCenoxCzAJBgNV +BAgMAnp6MQ0wCwYDVQQHDAR6enp6MQ4wDAYDVQQKDAVaenp6ejEOMAwGA1UECwwF +Wnp6enoxDTALBgNVBAMMBHRlc3QwIBcNMTkwNDEyMTk1MDM0WhgPMzAxODA4MTMx +OTUwMzRaMFgxCzAJBgNVBAYTAnp6MQswCQYDVQQIDAJ6ejENMAsGA1UEBwwEenp6 +ejEOMAwGA1UECgwFWnp6enoxDjAMBgNVBAsMBVp6enp6MQ0wCwYDVQQDDAR0ZXN0 +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHcj80WU/XBsd9FlyQmfjPUdfm +edhCFDd6TEQmZNNqP/UG+VkGa+BXjRIHMfic/WxPTbGhCjv68ci0UDNomUXagFex +LGNpkwa7+CRVtoc/1xgq+ySE6M4nhcCutScoxNvWNn5eSQ66i3U0sTv91MgsXxqE +dTaiZg0BIufEc3dueQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAGUV5B+USHvaRa8k +gCNJSuNpo6ARlv0ekrk8bbdNRBiEUdCMyoGJFfuM9K0zybX6Vr25wai3nvaog294 +Vx/jWjX2g5SDbjItH6VGy6C9GCGf1A07VxFRCfJn5tA9HuJjPKiE+g/BmrV5N4Ce +alzFxPHWYkNOzoRU8qI7OqUai1kL + </X509Certificate> + </X509Data> + </KeyInfo> + </SubjectConfirmationData> +</SubjectConfirmation> +""" + TEST_SUBJECT = """<?xml version="1.0" encoding="utf-8"?> <Subject xmlns="urn:oasis:names:tc:SAML:2.0:assertion"> <NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" diff --git a/tests/saml_hok.xml b/tests/saml_hok.xml new file mode 100644 index 00000000..6aad625a --- /dev/null +++ b/tests/saml_hok.xml @@ -0,0 +1,45 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!-- SAML response with multiple 'holder-of-key' subject confirmations. --> +<ns0:Response xmlns:ns0="urn:oasis:names:tc:SAML:2.0:protocol" + xmlns:ns1="urn:oasis:names:tc:SAML:2.0:assertion" + xmlns:ns2="http://www.w3.org/2000/09/xmldsig#" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Destination="https://sp:443/.auth/saml/login" ID="_df9a1eadc90519252694519504a13dfb8dd67a1bb4" InResponseTo="id-KHlas49TtW2VdC8WN" IssueInstant="2019-05-14T20:35:13Z" Version="2.0"> + <ns1:Issuer>https://idp:8443</ns1:Issuer> + <ns0:Status> + <ns0:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /> + </ns0:Status> + <ns1:Assertion ID="_12d211a5015f71eba8f837d2aa8b95b28bbdc4599b" IssueInstant="2019-05-14T20:35:13Z" Version="2.0"> + <ns1:Issuer>https://idp:8443</ns1:Issuer> + <ns1:Subject> + <ns1:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">57a0a35eefdb29ca8b4ab78d5a118117</ns1:NameID> + <ns1:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:holder-of-key"> + <ns1:SubjectConfirmationData InResponseTo="id-KHlas49TtW2VdC8WN" NotOnOrAfter="2019-05-14T20:36:13Z" Recipient="https://sp:443/.auth/saml/login"> + <ns2:KeyInfo> + <ns2:X509Data> + <ns2:X509Certificate>MIICITCCAYoCAQEwDQYJKoZIhvcNAQELBQAwWDELMAkGA1UEBhMCenoxCzAJBgNVBAgMAnp6MQ0wCwYDVQQHDAR6enp6MQ4wDAYDVQQKDAVaenp6ejEOMAwGA1UECwwFWnp6enoxDTALBgNVBAMMBHRlc3QwIBcNMTkwNDEyMTk1MDM0WhgPMzAxODA4MTMxOTUwMzRaMFgxCzAJBgNVBAYTAnp6MQswCQYDVQQIDAJ6ejENMAsGA1UEBwwEenp6ejEOMAwGA1UECgwFWnp6enoxDjAMBgNVBAsMBVp6enp6MQ0wCwYDVQQDDAR0ZXN0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHcj80WU/XBsd9FlyQmfjPUdfmedhCFDd6TEQmZNNqP/UG+VkGa+BXjRIHMfic/WxPTbGhCjv68ci0UDNomUXagFexLGNpkwa7+CRVtoc/1xgq+ySE6M4nhcCutScoxNvWNn5eSQ66i3U0sTv91MgsXxqEdTaiZg0BIufEc3dueQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAGUV5B+USHvaRa8kgCNJSuNpo6ARlv0ekrk8bbdNRBiEUdCMyoGJFfuM9K0zybX6Vr25wai3nvaog294Vx/jWjX2g5SDbjItH6VGy6C9GCGf1A07VxFRCfJn5tA9HuJjPKiE+g/BmrV5N4CealzFxPHWYkNOzoRU8qI7OqUai1kL</ns2:X509Certificate> + </ns2:X509Data> + </ns2:KeyInfo> + </ns1:SubjectConfirmationData> + </ns1:SubjectConfirmation> + <ns1:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:holder-of-key"> + <ns1:SubjectConfirmationData InResponseTo="id-KHlas49TtW2VdC8WN" NotOnOrAfter="2019-05-14T20:36:13Z" Recipient="https://sp:443/.auth/saml/login"> + <ns2:KeyInfo> + <ns2:X509Data> + <ns2:X509Certificate>MIICITCCAYoCAQEwDQYJKoZIhvcNAQELBQAwWDELMAkGA1UEBhMCenoxCzAJBgNVBAgMAnp6MQ0wCwYDVQQHDAR6enp6MQ4wDAYDVQQKDAVaenp6ejEOMAwGA1UECwwFWnp6enoxDTALBgNVBAMMBHRlc3QwIBcNMTkwNDEyMTk1MDM0WhgPMzAxODA4MTMxOTUwMzRaMFgxCzAJBgNVBAYTAnp6MQswCQYDVQQIDAJ6ejENMAsGA1UEBwwEenp6ejEOMAwGA1UECgwFWnp6enoxDjAMBgNVBAsMBVp6enp6MQ0wCwYDVQQDDAR0ZXN0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjW0kJM+4baWKtvO24ZsGXNvNKKkwTMz7OW5Z6BRqhSOq2WA0c5NCpMk6rD8Z2OTFEolPojEjf8dVyd/Ds/hrjFKQv8wQgbdXLN51YTIsgd6h+hBJO+vzhl0PT4aT7M0JKo5ALtS6qk4tsworW2BnwyvsGSAinwfeWt4t/b1J3kwIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAFtj7WArQQBugmh/KQjjlfTQ5A052QeXfgTyO9vv1S6MRIi7qgiaEv49cGXnJv/TWbySkMKObPMUApjg6z8PqcxuShew5FCTkNvwhABFPiyu0fUj3e2FEPHfsBu76jz4ugtmhUqjqhzwFY9ctnWRkkl6J0AjM3LnHOSgjNIclDZG</ns2:X509Certificate> + </ns2:X509Data> + </ns2:KeyInfo> + </ns1:SubjectConfirmationData> + </ns1:SubjectConfirmation> + </ns1:Subject> + <ns1:AuthnStatement AuthnInstant="2019-05-14T20:35:13Z" SessionIndex="1"> + <ns1:AuthnContext> + <ns1:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</ns1:AuthnContextClassRef> + </ns1:AuthnContext> + </ns1:AuthnStatement> + <ns1:AttributeStatement> + <ns1:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> + <ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">testuser</ns1:AttributeValue> + </ns1:Attribute> + </ns1:AttributeStatement> + </ns1:Assertion> +</ns0:Response> diff --git a/tests/test_02_saml.py b/tests/test_02_saml.py index 7ff64885..2cdb5064 100644 --- a/tests/test_02_saml.py +++ b/tests/test_02_saml.py @@ -867,35 +867,53 @@ class TestSubjectConfirmation: self.sc.subject_confirmation_data = saml.subject_confirmation_data_from_string( saml2_data.TEST_SUBJECT_CONFIRMATION_DATA) new_sc = saml.subject_confirmation_from_string(self.sc.to_string()) - assert new_sc.name_id.sp_provided_id == "sp provided id" - assert new_sc.method == saml.SCM_BEARER - assert new_sc.subject_confirmation_data.not_before == \ - "2007-08-31T01:05:02Z" - assert new_sc.subject_confirmation_data.not_on_or_after == \ - "2007-09-14T01:05:02Z" - assert new_sc.subject_confirmation_data.recipient == "recipient" - assert new_sc.subject_confirmation_data.in_response_to == "responseID" - assert new_sc.subject_confirmation_data.address == "127.0.0.1" - - def testUsingTestData(self): - """Test subject_confirmation_from_string() using test data""" + self._assertBearer(new_sc) + def testBearerUsingTestData(self): + """Test subject_confirmation_from_string() using test data for 'bearer' SubjectConfirmation""" sc = saml.subject_confirmation_from_string( saml2_data.TEST_SUBJECT_CONFIRMATION) + assert sc.verify() + self._assertBearer(sc) + + def _assertBearer(self, sc): + """Asserts SubjectConfirmation that has method 'bearer'""" assert sc.name_id.sp_provided_id == "sp provided id" assert sc.method == saml.SCM_BEARER + assert sc.subject_confirmation_data is not None assert sc.subject_confirmation_data.not_before == "2007-08-31T01:05:02Z" assert sc.subject_confirmation_data.not_on_or_after == "2007-09-14T01:05:02Z" assert sc.subject_confirmation_data.recipient == "recipient" assert sc.subject_confirmation_data.in_response_to == "responseID" assert sc.subject_confirmation_data.address == "127.0.0.1" + assert sc.subject_confirmation_data.key_info is None - def testVerify(self): - """Test SubjectConfirmation verify""" - + def testHolderOfKeyUsingTestData(self): + """Test subject_confirmation_from_string() using test data for 'holder-of-key' SubjectConfirmation""" sc = saml.subject_confirmation_from_string( - saml2_data.TEST_SUBJECT_CONFIRMATION) + saml2_data.TEST_HOLDER_OF_KEY_SUBJECT_CONFIRMATION) assert sc.verify() + assert sc.method == saml.SCM_HOLDER_OF_KEY + assert sc.subject_confirmation_data is not None + assert sc.subject_confirmation_data.not_on_or_after == "2007-09-14T01:05:02Z" + assert sc.subject_confirmation_data.recipient == "recipient" + assert sc.subject_confirmation_data.in_response_to == "responseID" + key_info = sc.subject_confirmation_data.key_info + assert len(key_info) == 1 + assert len(key_info[0].x509_data) == 1 + assert key_info[0].x509_data[0].x509_certificate.text.strip() == """ +MIICITCCAYoCAQEwDQYJKoZIhvcNAQELBQAwWDELMAkGA1UEBhMCenoxCzAJBgNV +BAgMAnp6MQ0wCwYDVQQHDAR6enp6MQ4wDAYDVQQKDAVaenp6ejEOMAwGA1UECwwF +Wnp6enoxDTALBgNVBAMMBHRlc3QwIBcNMTkwNDEyMTk1MDM0WhgPMzAxODA4MTMx +OTUwMzRaMFgxCzAJBgNVBAYTAnp6MQswCQYDVQQIDAJ6ejENMAsGA1UEBwwEenp6 +ejEOMAwGA1UECgwFWnp6enoxDjAMBgNVBAsMBVp6enp6MQ0wCwYDVQQDDAR0ZXN0 +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHcj80WU/XBsd9FlyQmfjPUdfm +edhCFDd6TEQmZNNqP/UG+VkGa+BXjRIHMfic/WxPTbGhCjv68ci0UDNomUXagFex +LGNpkwa7+CRVtoc/1xgq+ySE6M4nhcCutScoxNvWNn5eSQ66i3U0sTv91MgsXxqE +dTaiZg0BIufEc3dueQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAGUV5B+USHvaRa8k +gCNJSuNpo6ARlv0ekrk8bbdNRBiEUdCMyoGJFfuM9K0zybX6Vr25wai3nvaog294 +Vx/jWjX2g5SDbjItH6VGy6C9GCGf1A07VxFRCfJn5tA9HuJjPKiE+g/BmrV5N4Ce +alzFxPHWYkNOzoRU8qI7OqUai1kL""".strip() class TestSubject: diff --git a/tests/test_93_hok.py b/tests/test_93_hok.py new file mode 100644 index 00000000..085c930d --- /dev/null +++ b/tests/test_93_hok.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from contextlib import closing +from datetime import datetime +from dateutil import parser +from string import translate, whitespace +from saml2.authn_context import INTERNETPROTOCOLPASSWORD + +from saml2.server import Server +from saml2.response import authn_response +from saml2.config import config_factory + +from pathutils import dotname, full_path + +# Example SAML response iwth 'holder-of-key' subject confirmtaions +# containing DER-base64 copies (without PEM enclosure) of test_1.crt and test_2.crt +HOLDER_OF_KEY_RESPONSE_FILE = full_path("saml_hok.xml") + +TEST_CERT_1 = full_path("test_1.crt") +TEST_CERT_2 = full_path("test_2.crt") + + +class TestHolderOfKeyResponse: + def test_hok_response_is_parsed(self): + """Verifies that response with 'holder-of-key' subject confirmations is parsed successfully.""" + conf = config_factory("idp", dotname("server_conf")) + resp = authn_response(conf, "https://sp:443/.auth/saml/login", asynchop=False, allow_unsolicited=True) + with open(HOLDER_OF_KEY_RESPONSE_FILE, 'r') as fp: + authn_response_xml = fp.read() + resp.loads(authn_response_xml, False) + resp.do_not_verify = True + + resp.parse_assertion() + + assert resp.get_subject() is not None + assert len(resp.assertion.subject.subject_confirmation) == 2 + actual_certs = [sc.subject_confirmation_data.key_info[0].x509_data[0].x509_certificate.text.strip() + for sc in resp.assertion.subject.subject_confirmation] + expected_certs = [self._read_cert_without_pem_enclosure(TEST_CERT_1), + self._read_cert_without_pem_enclosure(TEST_CERT_2)] + assert actual_certs == expected_certs + + def _read_cert_without_pem_enclosure(self, path): + with open(path, 'r') as fp: + lines = fp.readlines() + lines_without_enclosure = lines[1:-1] + return ''.join(lines_without_enclosure).translate(None, whitespace) + + +if __name__ == "__main__": + t = TestHolderOfKeyResponse() + t.setup_class() + t.test_hok_response_is_parsed() |