Skip to content

Commit a509c88

Browse files
committed
Be able to register more than 1 Identity Provider x509cert, linked with an specific use (signing or encryption
1 parent 728d51f commit a509c88

File tree

13 files changed

+282
-96
lines changed

13 files changed

+282
-96
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,22 @@ This is the settings.json file:
308308
*/
309309
// "certFingerprint": "",
310310
// "certFingerprintAlgorithm": "sha1",
311+
312+
/* In some scenarios the IdP uses different certificates for
313+
* signing/encryption, or is under key rollover phase and
314+
* more than one certificate is published on IdP metadata.
315+
* In order to handle that the toolkit offers that parameter.
316+
* (when used, 'x509cert' and 'certFingerprint' values are
317+
* ignored).
318+
*/
319+
// 'x509certMulti': {
320+
// 'signing': [
321+
// '<cert1-string>'
322+
// ],
323+
// 'encryption': [
324+
// '<cert2-string>'
325+
// ]
326+
// }
311327
}
312328
}
313329
```
@@ -788,12 +804,27 @@ else:
788804
print ', '.join(errors)
789805
```
790806

807+
791808
### SP Key rollover ###
792809

793810
If you plan to update the SP x509cert and privateKey you can define the new x509cert as $settings['sp']['x509certNew'] and it will be
794811
published on the SP metadata so Identity Providers can read them and get ready for rollover.
795812

796813

814+
### IdP with multiple certificates ###
815+
816+
In some scenarios the IdP uses different certificates for
817+
signing/encryption, or is under key rollover phase and more than one certificate is published on IdP metadata.
818+
819+
In order to handle that the toolkit offers the $settings['idp']['x509certMulti'] parameter.
820+
821+
When that parameter is used, 'x509cert' and 'certFingerprint' values will be ignored by the toolkit.
822+
823+
The 'x509certMulti' is an array with 2 keys:
824+
- 'signing'. An array of certs that will be used to validate IdP signature
825+
- 'encryption' An array with one unique cert that will be used to encrypt data to be sent to the IdP.
826+
827+
797828
### Main classes and methods ###
798829

799830
Described below are the main classes and methods that can be invoked from the SAML2 library.
@@ -909,6 +940,7 @@ Configuration of the OneLogin Python Toolkit
909940
* ***get_contacts*** Gets contacts data.
910941
* ***get_organization*** Gets organization data.
911942
* ***format_idp_cert*** Formats the IdP cert.
943+
* ***format_idp_cert_multi*** Formats all registered IdP certs.
912944
* ***format_sp_cert*** Formats the SP cert.
913945
* ***format_sp_cert_new*** Formats the SP cert new.
914946
* ***format_sp_key*** Formats the private key.

src/onelogin/saml2/auth.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -534,10 +534,15 @@ def __validate_signature(self, data, saml_type, raise_exceptions=False):
534534
)
535535
return True
536536

537-
x509cert = self.get_settings().get_idp_cert()
537+
idp_data = self.get_settings().get_idp_data()
538538

539-
if not x509cert:
540-
error_msg = "In order to validate the sign on the %s, the x509cert of the IdP is required" % saml_type
539+
exists_x509cert = 'x509cert' in idp_data and idp_data['x509cert']
540+
exists_multix509sign = 'x509certMulti' in idp_data and \
541+
'signing' in idp_data['x509certMulti'] and \
542+
idp_data['x509certMulti']['signing']
543+
544+
if not (exists_x509cert or exists_multix509sign):
545+
error_msg = 'In order to validate the sign on the %s, the x509cert of the IdP is required' % saml_type
541546
self.__errors.append(error_msg)
542547
raise OneLogin_Saml2_Error(
543548
error_msg,
@@ -559,15 +564,29 @@ def __validate_signature(self, data, saml_type, raise_exceptions=False):
559564
lowercase_urlencoding
560565
)
561566

562-
if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query,
563-
OneLogin_Saml2_Utils.b64decode(signature),
564-
x509cert,
565-
sign_alg,
566-
self.__settings.is_debug_active()):
567+
if exists_multix509sign:
568+
for cert in idp_data['x509certMulti']['signing']:
569+
if OneLogin_Saml2_Utils.validate_binary_sign(signed_query,
570+
OneLogin_Saml2_Utils.b64decode(signature),
571+
cert,
572+
sign_alg):
573+
return True
567574
raise OneLogin_Saml2_ValidationError(
568-
'Signature validation failed. %s rejected.' % saml_type,
575+
'Signature validation failed. %s rejected' % saml_type,
569576
OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
570577
)
578+
else:
579+
cert = idp_data['x509cert']
580+
581+
if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query,
582+
OneLogin_Saml2_Utils.b64decode(signature),
583+
cert,
584+
sign_alg,
585+
self.__settings.is_debug_active()):
586+
raise OneLogin_Saml2_ValidationError(
587+
'Signature validation failed. %s rejected' % saml_type,
588+
OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
589+
)
571590
return True
572591
except Exception as e:
573592
self.__error_reason = str(e)

src/onelogin/saml2/logout_request.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,13 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq=
6363

6464
cert = None
6565
if security['nameIdEncrypted']:
66-
cert = idp_data['x509cert']
66+
exists_multix509enc = 'x509certMulti' in idp_data and \
67+
'encryption' in idp_data['x509certMulti'] and \
68+
idp_data['x509certMulti']['encryption']
69+
if exists_multix509enc:
70+
cert = idp_data['x509certMulti']['encryption'][0]
71+
else:
72+
cert = idp_data['x509cert']
6773

6874
if name_id is not None:
6975
if name_id_format is None:

src/onelogin/saml2/settings.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,14 @@ def __init__(self, settings=None, custom_base_path=None, sp_validation_only=Fals
120120
)
121121

122122
self.format_idp_cert()
123+
if 'x509certMulti' in self.__idp:
124+
self.format_idp_cert_multi()
123125
self.format_sp_cert()
124126
if 'x509certNew' in self.__sp:
125127
self.format_sp_cert_new()
126128
self.format_sp_key()
127129

130+
128131
def __load_paths(self, base_path=None):
129132
"""
130133
Set the paths of the different folders
@@ -368,14 +371,21 @@ def check_idp_settings(self, settings):
368371
exists_x509 = bool(idp.get('x509cert'))
369372
exists_fingerprint = bool(idp.get('certFingerprint'))
370373

374+
exists_multix509sign = 'x509certMulti' in idp and \
375+
'signing' in idp['x509certMulti'] and \
376+
idp['x509certMulti']['signing']
377+
exists_multix509enc = 'x509certMulti' in idp and \
378+
'encryption' in idp['x509certMulti'] and \
379+
idp['x509certMulti']['encryption']
380+
371381
want_assert_sign = bool(security.get('wantAssertionsSigned'))
372382
want_mes_signed = bool(security.get('wantMessagesSigned'))
373383
nameid_enc = bool(security.get('nameIdEncrypted'))
374384

375385
if (want_assert_sign or want_mes_signed) and \
376-
not(exists_x509 or exists_fingerprint):
386+
not(exists_x509 or exists_fingerprint or exists_multix509sign):
377387
errors.append('idp_cert_or_fingerprint_not_found_and_required')
378-
if nameid_enc and not exists_x509:
388+
if nameid_enc and not (exists_x509 or exists_multix509enc):
379389
errors.append('idp_cert_not_found_and_required')
380390
return errors
381391

@@ -715,6 +725,19 @@ def format_idp_cert(self):
715725
"""
716726
self.__idp['x509cert'] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509cert'])
717727

728+
def format_idp_cert_multi(self):
729+
"""
730+
Formats the Multple IdP certs.
731+
"""
732+
if 'x509certMulti' in self.__idp:
733+
if 'signing' in self.__idp['x509certMulti']:
734+
for idx in range(len(self.__idp['x509certMulti']['signing'])):
735+
self.__idp['x509certMulti']['signing'][idx] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509certMulti']['signing'][idx])
736+
737+
if 'encryption' in self.__idp['x509certMulti']:
738+
for idx in range(len(self.__idp['x509certMulti']['encryption'])):
739+
self.__idp['x509certMulti']['encryption'][idx] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509certMulti']['encryption'][idx])
740+
718741
def format_sp_cert(self):
719742
"""
720743
Formats the SP cert.

src/onelogin/saml2/utils.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
760760

761761
@staticmethod
762762
@return_false_on_exception
763-
def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False, xpath=None):
763+
def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False, xpath=None, multicerts=None):
764764
"""
765765
Validates a signature (Message or Assertion).
766766
@@ -785,6 +785,9 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
785785
:param xpath: The xpath of the signed element
786786
:type: string
787787
788+
:param multicerts: Multiple public certs
789+
:type: list
790+
788791
:param raise_exceptions: Whether to return false on failure or raise an exception
789792
:type raise_exceptions: Boolean
790793
"""
@@ -805,8 +808,21 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
805808

806809
if len(signature_nodes) == 1:
807810
signature_node = signature_nodes[0]
808-
# Raises expection if invalid
809-
return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True)
811+
812+
if not multicerts:
813+
return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True)
814+
else:
815+
# If multiple certs are provided, I may ignore cert and
816+
# fingerprint provided by the method and just check the
817+
# certs multicerts
818+
fingerprint = fingerprintalg = None
819+
for cert in multicerts:
820+
if OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, False, raise_exceptions=False):
821+
return True
822+
raise OneLogin_Saml2_ValidationError(
823+
'Signature validation failed. SAML Response rejected.',
824+
OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
825+
)
810826
else:
811827
raise OneLogin_Saml2_ValidationError(
812828
'Expected exactly one signature node; got {}.'.format(len(signature_nodes)),

tests/settings/settings8.json

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"strict": false,
3+
"debug": false,
4+
"custom_base_path": "../../../tests/data/customPath/",
5+
"sp": {
6+
"entityId": "http://stuff.com/endpoints/metadata.php",
7+
"assertionConsumerService": {
8+
"url": "http://stuff.com/endpoints/endpoints/acs.php"
9+
},
10+
"singleLogoutService": {
11+
"url": "http://stuff.com/endpoints/endpoints/sls.php"
12+
},
13+
"NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
14+
"privateKey": "MIICXgIBAAKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABAoGAD4/Z4LWVWV6D1qMIp1Gzr0ZmdWTE1SPdZ7Ej8glGnCzPdguCPuzbhGXmIg0VJ5D+02wsqws1zd48JSMXXM8zkYZVwQYIPUsNn5FetQpwxDIMPmhHg+QNBgwOnk8JK2sIjjLPL7qY7Itv7LT7Gvm5qSOkZ33RCgXcgz+okEIQMYkCQQDzbTOyDL0c5WQV6A2k06T/azdhUdGXF9C0+WkWSfNaovmTgRXh1G+jMlr82Snz4p4/STt7P/XtyWzF3pkVgZr3AkEA7nPjXwHlttNEMo6AtxHd47nizK2NUN803ElIUT8P9KSCoERmSXq66PDekGNic4ldpsSvOeYCk8MAYoDBy9kvVwJBAMLgX4xg6lzhv7hR5+pWjTb1rIY6rCHbrPfU264+UZXz9v2BT/VUznLF81WMvStD9xAPHpFS6R0OLghSZhdzhI0CQQDL8Duvfxzrn4b9QlmduV8wLERoT6rEVxKLsPVz316TGrxJvBZLk/cV0SRZE1cZf4ukXSWMfEcJ/0Zt+LdG1CqjAkEAqwLSglJ9Dy3HpgMz4vAAyZWzAxvyA1zW0no9GOLcPQnYaNUN/Fy2SYtETXTb0CQ9X1rt8ffkFP7ya+5TC83aMg==",
15+
"x509cert": "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo"
16+
},
17+
"idp": {
18+
"entityId": "http://idp.example.com/",
19+
"singleSignOnService": {
20+
"url": "http://idp.example.com/SSOService.php"
21+
},
22+
"singleLogoutService": {
23+
"url": "http://idp.example.com/SingleLogoutService.php"
24+
},
25+
"x509cert": "",
26+
"x509certMulti": {
27+
"signing": [
28+
"MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m",
29+
"MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo"
30+
],
31+
"encryption": [
32+
"MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo"
33+
]
34+
}
35+
},
36+
"security": {
37+
"authnRequestsSigned": false,
38+
"wantAssertionsSigned": false,
39+
"signMetadata": false
40+
},
41+
"contactPerson": {
42+
"technical": {
43+
"givenName": "technical_name",
44+
"emailAddress": "technical@example.com"
45+
},
46+
"support": {
47+
"givenName": "support_name",
48+
"emailAddress": "support@example.com"
49+
}
50+
},
51+
"organization": {
52+
"en-US": {
53+
"name": "sp_test",
54+
"displayname": "SP test",
55+
"url": "http://sp.example.com"
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)