Skip to content

Commit 5d82767

Browse files
author
Alex Bublichenko
committed
Parse assertions with Holder-of-Key profile
Problem: Holder-of-Key assertions are used to achieve higher levels of federation security, compared to bearer assertions, by having Relying Party challenge subscriber to prove possession of the key specified in the assertion that represents subscriber in addition to verifying the assertion itself signed by Identity Provider. More information about it can be found in https://pages.nist.gov/800-63-3/sp800-63c.html This library fails to parase SAML respones containing assertions with Holder-of-Key profile, for example: ``` <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> ``` fails to be parsed with the following error: ``` ERROR saml2.response:response.py:836 get subject Traceback (most recent call last): File "/home/abublich/repos/abliqo-pysaml2/venv/local/lib/python2.7/site-packages/pysaml2-4.7.0-py2.7.egg/saml2/response.py", line 828, in _assertion self.get_subject() File "/home/abublich/repos/abliqo-pysaml2/venv/local/lib/python2.7/site-packages/pysaml2-4.7.0-py2.7.egg/saml2/response.py", line 753, in get_subject if not self._holder_of_key_confirmed(_data): File "/home/abublich/repos/abliqo-pysaml2/venv/local/lib/python2.7/site-packages/pysaml2-4.7.0-py2.7.egg/saml2/response.py", line 730, in _holder_of_key_confirmed [samlp, saml, xenc, ds]): File "/home/abublich/repos/abliqo-pysaml2/venv/local/lib/python2.7/site-packages/pysaml2-4.7.0-py2.7.egg/saml2/__init__.py", line 1004, in extension_elements_to_elements for extension_element in extension_elements: TypeError: 'SubjectConfirmationData' object is not iterable ``` The root cause is two-fold: 1. The type SubjectConfirmationDataType_ does not declare KeyInfo as child element. 2. The bug in function _holder_of_key_confirmed: it should check KeyInfo child element of SubjectConfirmationData instead of SubjectConfirmationData itself. Solution: Fixed the root cause and added new unit tests that verify successful parsing of Holder-of-Key assertions.
1 parent 6acaf87 commit 5d82767

File tree

7 files changed

+172
-18
lines changed

7 files changed

+172
-18
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ venv.bak/
114114
# Rope project settings
115115
.ropeproject
116116

117+
# Visual Studio Code files
118+
.vscode/
119+
117120
# mkdocs documentation
118121
/site
119122

src/saml2/response.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -722,11 +722,11 @@ def _bearer_confirmed(self, data):
722722
return True
723723

724724
def _holder_of_key_confirmed(self, data):
725-
if not data:
725+
if not data or not data.key_info:
726726
return False
727727

728728
has_keyinfo = False
729-
for element in extension_elements_to_elements(data,
729+
for element in extension_elements_to_elements(data.key_info,
730730
[samlp, saml, xenc, ds]):
731731
if isinstance(element, ds.KeyInfo):
732732
has_keyinfo = True

src/saml2/saml.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,8 +482,12 @@ class SubjectConfirmationDataType_(SamlBase):
482482
c_any = {"namespace": "##any", "processContents": "lax", "minOccurs": "0",
483483
"maxOccurs": "unbounded"}
484484
c_any_attribute = {"namespace": "##other", "processContents": "lax"}
485+
c_children['{http://www.w3.org/2000/09/xmldsig#}KeyInfo'] = ('key_info',
486+
[ds.KeyInfo])
487+
c_cardinality['key_info'] = {"min": 0, "max": 1}
485488

486489
def __init__(self,
490+
key_info=None,
487491
not_before=None,
488492
not_on_or_after=None,
489493
recipient=None,
@@ -496,6 +500,7 @@ def __init__(self,
496500
text=text,
497501
extension_elements=extension_elements,
498502
extension_attributes=extension_attributes)
503+
self.key_info = key_info
499504
self.not_before = not_before
500505
self.not_on_or_after = not_on_or_after
501506
self.recipient = recipient

tests/saml2_data.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,36 @@
123123
</SubjectConfirmation>
124124
"""
125125

126+
TEST_HOLDER_OF_KEY_SUBJECT_CONFIRMATION = """<?xml version="1.0" encoding="utf-8"?>
127+
<SubjectConfirmation
128+
Method="urn:oasis:names:tc:SAML:2.0:cm:holder-of-key"
129+
xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
130+
<SubjectConfirmationData
131+
InResponseTo="responseID"
132+
NotOnOrAfter="2007-09-14T01:05:02Z"
133+
Recipient="recipient">
134+
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
135+
<X509Data xmlns="http://www.w3.org/2000/09/xmldsig#">
136+
<X509Certificate xmlns="http://www.w3.org/2000/09/xmldsig#">
137+
MIICITCCAYoCAQEwDQYJKoZIhvcNAQELBQAwWDELMAkGA1UEBhMCenoxCzAJBgNV
138+
BAgMAnp6MQ0wCwYDVQQHDAR6enp6MQ4wDAYDVQQKDAVaenp6ejEOMAwGA1UECwwF
139+
Wnp6enoxDTALBgNVBAMMBHRlc3QwIBcNMTkwNDEyMTk1MDM0WhgPMzAxODA4MTMx
140+
OTUwMzRaMFgxCzAJBgNVBAYTAnp6MQswCQYDVQQIDAJ6ejENMAsGA1UEBwwEenp6
141+
ejEOMAwGA1UECgwFWnp6enoxDjAMBgNVBAsMBVp6enp6MQ0wCwYDVQQDDAR0ZXN0
142+
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHcj80WU/XBsd9FlyQmfjPUdfm
143+
edhCFDd6TEQmZNNqP/UG+VkGa+BXjRIHMfic/WxPTbGhCjv68ci0UDNomUXagFex
144+
LGNpkwa7+CRVtoc/1xgq+ySE6M4nhcCutScoxNvWNn5eSQ66i3U0sTv91MgsXxqE
145+
dTaiZg0BIufEc3dueQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAGUV5B+USHvaRa8k
146+
gCNJSuNpo6ARlv0ekrk8bbdNRBiEUdCMyoGJFfuM9K0zybX6Vr25wai3nvaog294
147+
Vx/jWjX2g5SDbjItH6VGy6C9GCGf1A07VxFRCfJn5tA9HuJjPKiE+g/BmrV5N4Ce
148+
alzFxPHWYkNOzoRU8qI7OqUai1kL
149+
</X509Certificate>
150+
</X509Data>
151+
</KeyInfo>
152+
</SubjectConfirmationData>
153+
</SubjectConfirmation>
154+
"""
155+
126156
TEST_SUBJECT = """<?xml version="1.0" encoding="utf-8"?>
127157
<Subject xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
128158
<NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"

tests/saml_hok.xml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<!-- SAML response with multiple 'holder-of-key' subject confirmations. -->
3+
<ns0:Response xmlns:ns0="urn:oasis:names:tc:SAML:2.0:protocol"
4+
xmlns:ns1="urn:oasis:names:tc:SAML:2.0:assertion"
5+
xmlns:ns2="http://www.w3.org/2000/09/xmldsig#"
6+
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">
7+
<ns1:Issuer>https://idp:8443</ns1:Issuer>
8+
<ns0:Status>
9+
<ns0:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
10+
</ns0:Status>
11+
<ns1:Assertion ID="_12d211a5015f71eba8f837d2aa8b95b28bbdc4599b" IssueInstant="2019-05-14T20:35:13Z" Version="2.0">
12+
<ns1:Issuer>https://idp:8443</ns1:Issuer>
13+
<ns1:Subject>
14+
<ns1:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">57a0a35eefdb29ca8b4ab78d5a118117</ns1:NameID>
15+
<ns1:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:holder-of-key">
16+
<ns1:SubjectConfirmationData InResponseTo="id-KHlas49TtW2VdC8WN" NotOnOrAfter="2019-05-14T20:36:13Z" Recipient="https://sp:443/.auth/saml/login">
17+
<ns2:KeyInfo>
18+
<ns2:X509Data>
19+
<ns2:X509Certificate>MIICITCCAYoCAQEwDQYJKoZIhvcNAQELBQAwWDELMAkGA1UEBhMCenoxCzAJBgNVBAgMAnp6MQ0wCwYDVQQHDAR6enp6MQ4wDAYDVQQKDAVaenp6ejEOMAwGA1UECwwFWnp6enoxDTALBgNVBAMMBHRlc3QwIBcNMTkwNDEyMTk1MDM0WhgPMzAxODA4MTMxOTUwMzRaMFgxCzAJBgNVBAYTAnp6MQswCQYDVQQIDAJ6ejENMAsGA1UEBwwEenp6ejEOMAwGA1UECgwFWnp6enoxDjAMBgNVBAsMBVp6enp6MQ0wCwYDVQQDDAR0ZXN0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHcj80WU/XBsd9FlyQmfjPUdfmedhCFDd6TEQmZNNqP/UG+VkGa+BXjRIHMfic/WxPTbGhCjv68ci0UDNomUXagFexLGNpkwa7+CRVtoc/1xgq+ySE6M4nhcCutScoxNvWNn5eSQ66i3U0sTv91MgsXxqEdTaiZg0BIufEc3dueQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAGUV5B+USHvaRa8kgCNJSuNpo6ARlv0ekrk8bbdNRBiEUdCMyoGJFfuM9K0zybX6Vr25wai3nvaog294Vx/jWjX2g5SDbjItH6VGy6C9GCGf1A07VxFRCfJn5tA9HuJjPKiE+g/BmrV5N4CealzFxPHWYkNOzoRU8qI7OqUai1kL</ns2:X509Certificate>
20+
</ns2:X509Data>
21+
</ns2:KeyInfo>
22+
</ns1:SubjectConfirmationData>
23+
</ns1:SubjectConfirmation>
24+
<ns1:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:holder-of-key">
25+
<ns1:SubjectConfirmationData InResponseTo="id-KHlas49TtW2VdC8WN" NotOnOrAfter="2019-05-14T20:36:13Z" Recipient="https://sp:443/.auth/saml/login">
26+
<ns2:KeyInfo>
27+
<ns2:X509Data>
28+
<ns2:X509Certificate>MIICITCCAYoCAQEwDQYJKoZIhvcNAQELBQAwWDELMAkGA1UEBhMCenoxCzAJBgNVBAgMAnp6MQ0wCwYDVQQHDAR6enp6MQ4wDAYDVQQKDAVaenp6ejEOMAwGA1UECwwFWnp6enoxDTALBgNVBAMMBHRlc3QwIBcNMTkwNDEyMTk1MDM0WhgPMzAxODA4MTMxOTUwMzRaMFgxCzAJBgNVBAYTAnp6MQswCQYDVQQIDAJ6ejENMAsGA1UEBwwEenp6ejEOMAwGA1UECgwFWnp6enoxDjAMBgNVBAsMBVp6enp6MQ0wCwYDVQQDDAR0ZXN0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjW0kJM+4baWKtvO24ZsGXNvNKKkwTMz7OW5Z6BRqhSOq2WA0c5NCpMk6rD8Z2OTFEolPojEjf8dVyd/Ds/hrjFKQv8wQgbdXLN51YTIsgd6h+hBJO+vzhl0PT4aT7M0JKo5ALtS6qk4tsworW2BnwyvsGSAinwfeWt4t/b1J3kwIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAFtj7WArQQBugmh/KQjjlfTQ5A052QeXfgTyO9vv1S6MRIi7qgiaEv49cGXnJv/TWbySkMKObPMUApjg6z8PqcxuShew5FCTkNvwhABFPiyu0fUj3e2FEPHfsBu76jz4ugtmhUqjqhzwFY9ctnWRkkl6J0AjM3LnHOSgjNIclDZG</ns2:X509Certificate>
29+
</ns2:X509Data>
30+
</ns2:KeyInfo>
31+
</ns1:SubjectConfirmationData>
32+
</ns1:SubjectConfirmation>
33+
</ns1:Subject>
34+
<ns1:AuthnStatement AuthnInstant="2019-05-14T20:35:13Z" SessionIndex="1">
35+
<ns1:AuthnContext>
36+
<ns1:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</ns1:AuthnContextClassRef>
37+
</ns1:AuthnContext>
38+
</ns1:AuthnStatement>
39+
<ns1:AttributeStatement>
40+
<ns1:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
41+
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">testuser</ns1:AttributeValue>
42+
</ns1:Attribute>
43+
</ns1:AttributeStatement>
44+
</ns1:Assertion>
45+
</ns0:Response>

tests/test_02_saml.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -867,35 +867,53 @@ def testAccessors(self):
867867
self.sc.subject_confirmation_data = saml.subject_confirmation_data_from_string(
868868
saml2_data.TEST_SUBJECT_CONFIRMATION_DATA)
869869
new_sc = saml.subject_confirmation_from_string(self.sc.to_string())
870-
assert new_sc.name_id.sp_provided_id == "sp provided id"
871-
assert new_sc.method == saml.SCM_BEARER
872-
assert new_sc.subject_confirmation_data.not_before == \
873-
"2007-08-31T01:05:02Z"
874-
assert new_sc.subject_confirmation_data.not_on_or_after == \
875-
"2007-09-14T01:05:02Z"
876-
assert new_sc.subject_confirmation_data.recipient == "recipient"
877-
assert new_sc.subject_confirmation_data.in_response_to == "responseID"
878-
assert new_sc.subject_confirmation_data.address == "127.0.0.1"
879-
880-
def testUsingTestData(self):
881-
"""Test subject_confirmation_from_string() using test data"""
870+
self._assertBearer(new_sc)
882871

872+
def testBearerUsingTestData(self):
873+
"""Test subject_confirmation_from_string() using test data for 'bearer' SubjectConfirmation"""
883874
sc = saml.subject_confirmation_from_string(
884875
saml2_data.TEST_SUBJECT_CONFIRMATION)
876+
assert sc.verify()
877+
self._assertBearer(sc)
878+
879+
def _assertBearer(self, sc):
880+
"""Asserts SubjectConfirmation that has method 'bearer'"""
885881
assert sc.name_id.sp_provided_id == "sp provided id"
886882
assert sc.method == saml.SCM_BEARER
883+
assert sc.subject_confirmation_data is not None
887884
assert sc.subject_confirmation_data.not_before == "2007-08-31T01:05:02Z"
888885
assert sc.subject_confirmation_data.not_on_or_after == "2007-09-14T01:05:02Z"
889886
assert sc.subject_confirmation_data.recipient == "recipient"
890887
assert sc.subject_confirmation_data.in_response_to == "responseID"
891888
assert sc.subject_confirmation_data.address == "127.0.0.1"
889+
assert sc.subject_confirmation_data.key_info is None
892890

893-
def testVerify(self):
894-
"""Test SubjectConfirmation verify"""
895-
891+
def testHolderOfKeyUsingTestData(self):
892+
"""Test subject_confirmation_from_string() using test data for 'holder-of-key' SubjectConfirmation"""
896893
sc = saml.subject_confirmation_from_string(
897-
saml2_data.TEST_SUBJECT_CONFIRMATION)
894+
saml2_data.TEST_HOLDER_OF_KEY_SUBJECT_CONFIRMATION)
898895
assert sc.verify()
896+
assert sc.method == saml.SCM_HOLDER_OF_KEY
897+
assert sc.subject_confirmation_data is not None
898+
assert sc.subject_confirmation_data.not_on_or_after == "2007-09-14T01:05:02Z"
899+
assert sc.subject_confirmation_data.recipient == "recipient"
900+
assert sc.subject_confirmation_data.in_response_to == "responseID"
901+
key_info = sc.subject_confirmation_data.key_info
902+
assert len(key_info) == 1
903+
assert len(key_info[0].x509_data) == 1
904+
assert key_info[0].x509_data[0].x509_certificate.text.strip() == """
905+
MIICITCCAYoCAQEwDQYJKoZIhvcNAQELBQAwWDELMAkGA1UEBhMCenoxCzAJBgNV
906+
BAgMAnp6MQ0wCwYDVQQHDAR6enp6MQ4wDAYDVQQKDAVaenp6ejEOMAwGA1UECwwF
907+
Wnp6enoxDTALBgNVBAMMBHRlc3QwIBcNMTkwNDEyMTk1MDM0WhgPMzAxODA4MTMx
908+
OTUwMzRaMFgxCzAJBgNVBAYTAnp6MQswCQYDVQQIDAJ6ejENMAsGA1UEBwwEenp6
909+
ejEOMAwGA1UECgwFWnp6enoxDjAMBgNVBAsMBVp6enp6MQ0wCwYDVQQDDAR0ZXN0
910+
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHcj80WU/XBsd9FlyQmfjPUdfm
911+
edhCFDd6TEQmZNNqP/UG+VkGa+BXjRIHMfic/WxPTbGhCjv68ci0UDNomUXagFex
912+
LGNpkwa7+CRVtoc/1xgq+ySE6M4nhcCutScoxNvWNn5eSQ66i3U0sTv91MgsXxqE
913+
dTaiZg0BIufEc3dueQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAGUV5B+USHvaRa8k
914+
gCNJSuNpo6ARlv0ekrk8bbdNRBiEUdCMyoGJFfuM9K0zybX6Vr25wai3nvaog294
915+
Vx/jWjX2g5SDbjItH6VGy6C9GCGf1A07VxFRCfJn5tA9HuJjPKiE+g/BmrV5N4Ce
916+
alzFxPHWYkNOzoRU8qI7OqUai1kL""".strip()
899917

900918

901919
class TestSubject:

tests/test_93_hok.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
from contextlib import closing
4+
from datetime import datetime
5+
from dateutil import parser
6+
from string import translate, whitespace
7+
from saml2.authn_context import INTERNETPROTOCOLPASSWORD
8+
9+
from saml2.server import Server
10+
from saml2.response import authn_response
11+
from saml2.config import config_factory
12+
13+
from pathutils import dotname, full_path
14+
15+
# Example SAML response iwth 'holder-of-key' subject confirmtaions
16+
# containing DER-base64 copies (without PEM enclosure) of test_1.crt and test_2.crt
17+
HOLDER_OF_KEY_RESPONSE_FILE = full_path("saml_hok.xml")
18+
19+
TEST_CERT_1 = full_path("test_1.crt")
20+
TEST_CERT_2 = full_path("test_2.crt")
21+
22+
23+
class TestHolderOfKeyResponse:
24+
def test_hok_response_is_parsed(self):
25+
"""Verifies that response with 'holder-of-key' subject confirmations is parsed successfully."""
26+
conf = config_factory("idp", dotname("server_conf"))
27+
resp = authn_response(conf, "https://sp:443/.auth/saml/login", asynchop=False, allow_unsolicited=True)
28+
with open(HOLDER_OF_KEY_RESPONSE_FILE, 'r') as fp:
29+
authn_response_xml = fp.read()
30+
resp.loads(authn_response_xml, False)
31+
resp.do_not_verify = True
32+
33+
resp.parse_assertion()
34+
35+
assert resp.get_subject() is not None
36+
assert len(resp.assertion.subject.subject_confirmation) == 2
37+
actual_certs = [sc.subject_confirmation_data.key_info[0].x509_data[0].x509_certificate.text.strip()
38+
for sc in resp.assertion.subject.subject_confirmation]
39+
expected_certs = [self._read_cert_without_pem_enclosure(TEST_CERT_1),
40+
self._read_cert_without_pem_enclosure(TEST_CERT_2)]
41+
assert actual_certs == expected_certs
42+
43+
def _read_cert_without_pem_enclosure(self, path):
44+
with open(path, 'r') as fp:
45+
lines = fp.readlines()
46+
lines_without_enclosure = lines[1:-1]
47+
return ''.join(lines_without_enclosure).translate(None, whitespace)
48+
49+
50+
if __name__ == "__main__":
51+
t = TestHolderOfKeyResponse()
52+
t.setup_class()
53+
t.test_hok_response_is_parsed()

0 commit comments

Comments
 (0)