Skip to content

Commit 80fab33

Browse files
committed
Improve Signature validation process
1 parent 3309593 commit 80fab33

File tree

3 files changed

+65
-22
lines changed

3 files changed

+65
-22
lines changed

src/onelogin/saml2/response.py

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ def is_valid(self, request_data, request_id=None):
8888

8989
signed_elements = self.process_signed_elements()
9090

91+
has_signed_response = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements
92+
has_signed_assertion = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML in signed_elements
93+
9194
if self.__settings.is_strict():
9295
no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd'
9396
res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
@@ -200,32 +203,26 @@ def is_valid(self, request_data, request_id=None):
200203
if not any_subject_confirmation:
201204
raise Exception('A valid SubjectConfirmation was not found on this Response')
202205

203-
if security.get('wantAssertionsSigned', False) and ('{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML) not in signed_elements:
206+
if security.get('wantAssertionsSigned', False) and not has_signed_assertion:
204207
raise Exception('The Assertion of the Response is not signed and the SP require it')
205208

206-
if security.get('wantMessagesSigned', False) and ('{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP) not in signed_elements:
209+
if security.get('wantMessagesSigned', False) and not has_signed_response:
207210
raise Exception('The Message of the Response is not signed and the SP require it')
208211

209-
if len(signed_elements) > 0:
210-
if len(signed_elements) > 2:
211-
raise Exception('Too many Signatures found. SAML Response rejected')
212+
if not signed_elements or (not has_signed_response and not has_signed_assertion):
213+
raise Exception('No Signature found. SAML Response rejected')
214+
else:
212215
cert = idp_data.get('x509cert', None)
213216
fingerprint = idp_data.get('certFingerprint', None)
214217
fingerprintalg = idp_data.get('certFingerprintAlgorithm', None)
215218

216219
# If find a Signature on the Response, validates it checking the original response
217-
if '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements:
218-
document_to_validate = self.document
219-
# Otherwise validates the assertion (decrypted assertion if was encrypted)
220-
else:
221-
if self.encrypted:
222-
document_to_validate = self.decrypted_document
223-
else:
224-
document_to_validate = self.document
225-
if not OneLogin_Saml2_Utils.validate_sign(document_to_validate, cert, fingerprint, fingerprintalg):
220+
if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH):
221+
raise Exception('Signature validation failed. SAML Response rejected')
222+
223+
document_check_assertion = self.decrypted_document if self.encrypted else self.document
224+
if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH):
226225
raise Exception('Signature validation failed. SAML Response rejected')
227-
else:
228-
raise Exception('No Signature found. SAML Response rejected')
229226

230227
return True
231228
except Exception as err:
@@ -291,7 +288,7 @@ def get_issuers(self):
291288
"""
292289
issuers = []
293290

294-
message_issuer_nodes = self.__query('/samlp:Response/saml:Issuer')
291+
message_issuer_nodes = OneLogin_Saml2_Utils.query(self.document, '/samlp:Response/saml:Issuer')
295292
if len(message_issuer_nodes) == 1:
296293
issuers.append(message_issuer_nodes[0].text)
297294
else:
@@ -486,8 +483,41 @@ def process_signed_elements(self):
486483
verified_seis.append(sei)
487484

488485
signed_elements.append(signed_element)
486+
487+
if signed_elements:
488+
if not self.validate_signed_elements(signed_elements):
489+
raise Exception('Found an unexpected Signature Element. SAML Response rejected')
489490
return signed_elements
490491

492+
def validate_signed_elements(self, signed_elements):
493+
"""
494+
Verifies that the document has the expected signed nodes.
495+
"""
496+
if len(signed_elements) > 2:
497+
return False
498+
499+
response_tag = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP
500+
assertion_tag = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML
501+
502+
if (response_tag in signed_elements and signed_elements.count(response_tag) > 1) or \
503+
(assertion_tag in signed_elements and signed_elements.count(assertion_tag) > 1) or \
504+
(response_tag not in signed_elements and assertion_tag not in signed_elements):
505+
return False
506+
507+
# Check that the signed elements found here, are the ones that will be verified
508+
# by OneLogin_Saml2_Utils.validate_sign
509+
if response_tag in signed_elements:
510+
expected_signature_nodes = OneLogin_Saml2_Utils.query(self.document, OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH)
511+
if len(expected_signature_nodes) != 1:
512+
raise Exception('Unexpected number of Response signatures found. SAML Response rejected.')
513+
514+
if assertion_tag in signed_elements:
515+
expected_signature_nodes = self.__query(OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH)
516+
if len(expected_signature_nodes) != 1:
517+
raise Exception('Unexpected number of Assertion signatures found. SAML Response rejected.')
518+
519+
return True
520+
491521
def validate_timestamps(self):
492522
"""
493523
Verifies that the document is valid according to Conditions Element

src/onelogin/saml2/utils.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ class OneLogin_Saml2_Utils(object):
6565
6666
"""
6767

68+
RESPONSE_SIGNATURE_XPATH = '/samlp:Response/ds:Signature'
69+
ASSERTION_SIGNATURE_XPATH = '/samlp:Response/saml:Assertion/ds:Signature'
70+
6871
@staticmethod
6972
def decode_base64_and_inflate(value):
7073
"""
@@ -865,7 +868,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
865868
return newdoc.saveXML(newdoc.firstChild)
866869

867870
@staticmethod
868-
def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False):
871+
def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False, xpath=None):
869872
"""
870873
Validates a signature (Message or Assertion).
871874
@@ -886,6 +889,9 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
886889
887890
:param debug: Activate the xmlsec debug
888891
:type: bool
892+
893+
:param xpath: The xpath of the signed element
894+
:type: string
889895
"""
890896
try:
891897
if xml is None or xml == '':
@@ -918,10 +924,13 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
918924

919925
xmlsec.addIDs(elem, ["ID"])
920926

921-
signature_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:Response/ds:Signature')
927+
if xpath:
928+
signature_nodes = OneLogin_Saml2_Utils.query(elem, xpath)
929+
else:
930+
signature_nodes = OneLogin_Saml2_Utils.query(elem, OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH)
922931

923-
if not len(signature_nodes) > 0:
924-
signature_nodes += OneLogin_Saml2_Utils.query(elem, '/samlp:Response/saml:Assertion/ds:Signature')
932+
if len(signature_nodes) == 0:
933+
signature_nodes = OneLogin_Saml2_Utils.query(elem, OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH)
925934

926935
if len(signature_nodes) == 1:
927936
signature_node = signature_nodes[0]

tests/src/OneLogin/saml2_tests/signed_response_test.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ def testResponseAndAssertionSigned(self):
5050
Tests the getNameId method of the OneLogin_Saml2_Response
5151
Case valid signed response, signed assertion
5252
"""
53-
settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
53+
settings_info = self.loadSettingsJSON()
54+
settings_info['idp']['entityId'] = "https://federate.example.net/saml/saml2/idp/metadata.php"
55+
settings_info['sp']['entityId'] = "hello.com"
56+
57+
settings = OneLogin_Saml2_Settings(settings_info)
5458
message = self.file_contents(join(self.data_path, 'responses', 'simple_saml_php.xml'))
5559
response = OneLogin_Saml2_Response(settings, b64encode(message))
5660

0 commit comments

Comments
 (0)