Skip to content

Commit 3eb11bc

Browse files
committed
Be able to register more than 1 Identity Provider x509cert, linked with an specific use (signing or encryption)
1 parent dc015e0 commit 3eb11bc

File tree

11 files changed

+274
-31
lines changed

11 files changed

+274
-31
lines changed

README.md

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,8 +332,24 @@ This is the settings.json file:
332332
* Notice that if you want to validate any SAML Message sent by the HTTP-Redirect binding, you
333333
* will need to provide the whole x509cert.
334334
*/
335-
// 'certFingerprint' => '',
336-
// 'certFingerprintAlgorithm' => 'sha1',
335+
// 'certFingerprint': '',
336+
// 'certFingerprintAlgorithm': 'sha1',
337+
338+
/* In some scenarios the IdP uses different certificates for
339+
* signing/encryption, or is under key rollover phase and
340+
* more than one certificate is published on IdP metadata.
341+
* In order to handle that the toolkit offers that parameter.
342+
* (when used, 'x509cert' and 'certFingerprint' values are
343+
* ignored).
344+
*/
345+
// 'x509certMulti': {
346+
// 'signing': [
347+
// '<cert1-string>'
348+
// ],
349+
// 'encryption': [
350+
// '<cert2-string>'
351+
// ]
352+
// }
337353
}
338354
}
339355
```
@@ -827,6 +843,25 @@ else:
827843
print ', '.join(errors)
828844
```
829845

846+
### SP Key rollover ###
847+
848+
If you plan to update the SP x509cert and privateKey you can define the new x509cert as $settings['sp']['x509certNew'] and it will be
849+
published on the SP metadata so Identity Providers can read them and get ready for rollover.
850+
851+
852+
### IdP with multiple certificates ###
853+
854+
In some scenarios the IdP uses different certificates for
855+
signing/encryption, or is under key rollover phase and more than one certificate is published on IdP metadata.
856+
857+
In order to handle that the toolkit offers the $settings['idp']['x509certMulti'] parameter.
858+
859+
When that parameter is used, 'x509cert' and 'certFingerprint' values will be ignored by the toolkit.
860+
861+
The 'x509certMulti' is an array with 2 keys:
862+
- 'signing'. An array of certs that will be used to validate IdP signature
863+
- 'encryption' An array with one unique cert that will be used to encrypt data to be sent to the IdP
864+
830865

831866
### Main classes and methods ###
832867

@@ -944,6 +979,7 @@ Configuration of the OneLogin Python Toolkit
944979
* ***get_contacts*** Gets contacts data.
945980
* ***get_organization*** Gets organization data.
946981
* ***format_idp_cert*** Formats the IdP cert.
982+
* ***format_idp_cert_multi*** Formats all registered IdP certs.
947983
* ***format_sp_cert*** Formats the SP cert.
948984
* ***format_sp_cert_new*** Formats the SP cert new.
949985
* ***format_sp_key*** Formats the private key.

src/onelogin/saml2/logout_request.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,13 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq=
6767

6868
cert = None
6969
if 'nameIdEncrypted' in security and security['nameIdEncrypted']:
70-
cert = idp_data['x509cert']
70+
exists_multix509enc = 'x509certMulti' in idp_data and \
71+
'encryption' in idp_data['x509certMulti'] and \
72+
idp_data['x509certMulti']['encryption']
73+
if exists_multix509enc:
74+
cert = idp_data['x509certMulti']['encryption'][0]
75+
else:
76+
cert = idp_data['x509cert']
7177

7278
if name_id is not None:
7379
if name_id_format is not None:
@@ -380,19 +386,32 @@ def is_valid(self, request_data, raise_exceptions=False):
380386
signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding))
381387
signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding))
382388

383-
if 'x509cert' not in idp_data or not idp_data['x509cert']:
389+
exists_x509cert = 'x509cert' in idp_data and idp_data['x509cert']
390+
exists_multix509sign = 'x509certMulti' in idp_data and \
391+
'signing' in idp_data['x509certMulti'] and \
392+
idp_data['x509certMulti']['signing']
393+
394+
if not (exists_x509cert or exists_multix509sign):
384395
raise OneLogin_Saml2_Error(
385396
'In order to validate the sign on the Logout Request, the x509cert of the IdP is required',
386397
OneLogin_Saml2_Error.CERT_NOT_FOUND
387398
)
388-
cert = idp_data['x509cert']
389-
390-
if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
399+
if exists_multix509sign:
400+
for cert in idp_data['x509certMulti']['signing']:
401+
if OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
402+
return True
391403
raise OneLogin_Saml2_ValidationError(
392404
'Signature validation failed. Logout Request rejected',
393405
OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
394406
)
407+
else:
408+
cert = idp_data['x509cert']
395409

410+
if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
411+
raise OneLogin_Saml2_ValidationError(
412+
'Signature validation failed. Logout Request rejected',
413+
OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
414+
)
396415
return True
397416
except Exception as err:
398417
# pylint: disable=R0801sign_alg

src/onelogin/saml2/logout_response.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,18 +146,32 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
146146
signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding))
147147
signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding))
148148

149-
if 'x509cert' not in idp_data or not idp_data['x509cert']:
149+
exists_x509cert = 'x509cert' in idp_data and idp_data['x509cert']
150+
exists_multix509sign = 'x509certMulti' in idp_data and \
151+
'signing' in idp_data['x509certMulti'] and \
152+
idp_data['x509certMulti']['signing']
153+
154+
if not (exists_x509cert or exists_multix509sign):
150155
raise OneLogin_Saml2_Error(
151156
'In order to validate the sign on the Logout Response, the x509cert of the IdP is required',
152157
OneLogin_Saml2_Error.CERT_NOT_FOUND
153158
)
154-
cert = idp_data['x509cert']
155-
156-
if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
159+
if exists_multix509sign:
160+
for cert in idp_data['x509certMulti']['signing']:
161+
if OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
162+
return True
157163
raise OneLogin_Saml2_ValidationError(
158164
'Signature validation failed. Logout Response rejected',
159165
OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
160166
)
167+
else:
168+
cert = idp_data['x509cert']
169+
170+
if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
171+
raise OneLogin_Saml2_ValidationError(
172+
'Signature validation failed. Logout Response rejected',
173+
OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
174+
)
161175

162176
return True
163177
# pylint: disable=R0801

src/onelogin/saml2/response.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,15 +290,19 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
290290
fingerprint = idp_data.get('certFingerprint', None)
291291
fingerprintalg = idp_data.get('certFingerprintAlgorithm', None)
292292

293+
multicerts = None
294+
if 'x509certMulti' in idp_data and 'signing' in idp_data['x509certMulti'] and idp_data['x509certMulti']['signing']:
295+
multicerts = idp_data['x509certMulti']['signing']
296+
293297
# If find a Signature on the Response, validates it checking the original response
294-
if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH, raise_exceptions=False):
298+
if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH, multicerts=multicerts, raise_exceptions=False):
295299
raise OneLogin_Saml2_ValidationError(
296300
'Signature validation failed. SAML Response rejected',
297301
OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
298302
)
299303

300304
document_check_assertion = self.decrypted_document if self.encrypted else self.document
301-
if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH, raise_exceptions=False):
305+
if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH, multicerts=multicerts, raise_exceptions=False):
302306
raise OneLogin_Saml2_ValidationError(
303307
'Signature validation failed. SAML Response rejected',
304308
OneLogin_Saml2_ValidationError.INVALID_SIGNATURE

src/onelogin/saml2/settings.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ def __init__(self, settings=None, custom_base_path=None, sp_validation_only=Fals
114114
if 'x509certNew' in self.__sp:
115115
self.format_sp_cert_new()
116116
self.format_sp_key()
117+
if 'x509certMulti' in self.__idp:
118+
self.format_idp_cert_multi()
117119

118120
def __load_paths(self, base_path=None):
119121
"""
@@ -361,14 +363,21 @@ def check_idp_settings(self, settings):
361363
exists_x509 = bool(idp.get('x509cert'))
362364
exists_fingerprint = bool(idp.get('certFingerprint'))
363365

366+
exists_multix509sign = 'x509certMulti' in idp and \
367+
'signing' in idp['x509certMulti'] and \
368+
idp['x509certMulti']['signing']
369+
exists_multix509enc = 'x509certMulti' in idp and \
370+
'encryption' in idp['x509certMulti'] and \
371+
idp['x509certMulti']['encryption']
372+
364373
want_assert_sign = bool(security.get('wantAssertionsSigned'))
365374
want_mes_signed = bool(security.get('wantMessagesSigned'))
366375
nameid_enc = bool(security.get('nameIdEncrypted'))
367376

368377
if (want_assert_sign or want_mes_signed) and \
369-
not(exists_x509 or exists_fingerprint):
378+
not(exists_x509 or exists_fingerprint or exists_multix509sign):
370379
errors.append('idp_cert_or_fingerprint_not_found_and_required')
371-
if nameid_enc and not exists_x509:
380+
if nameid_enc and not (exists_x509 or exists_multix509enc):
372381
errors.append('idp_cert_not_found_and_required')
373382

374383
return errors
@@ -722,6 +731,19 @@ def format_idp_cert(self):
722731
"""
723732
self.__idp['x509cert'] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509cert'])
724733

734+
def format_idp_cert_multi(self):
735+
"""
736+
Formats the Multple IdP certs.
737+
"""
738+
if 'x509certMulti' in self.__idp:
739+
if 'signing' in self.__idp['x509certMulti']:
740+
for idx in range(len(self.__idp['x509certMulti']['signing'])):
741+
self.__idp['x509certMulti']['signing'][idx] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509certMulti']['signing'][idx])
742+
743+
if 'encryption' in self.__idp['x509certMulti']:
744+
for idx in range(len(self.__idp['x509certMulti']['encryption'])):
745+
self.__idp['x509certMulti']['encryption'][idx] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509certMulti']['encryption'][idx])
746+
725747
def format_sp_cert(self):
726748
"""
727749
Formats the SP cert.

src/onelogin/saml2/utils.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -915,7 +915,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
915915

916916
@staticmethod
917917
@return_false_on_exception
918-
def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False, xpath=None):
918+
def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False, xpath=None, multicerts=None):
919919
"""
920920
Validates a signature (Message or Assertion).
921921
@@ -940,6 +940,9 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
940940
:param xpath: The xpath of the signed element
941941
:type: string
942942
943+
:param multicerts: Multiple public certs
944+
:type: list
945+
943946
:param raise_exceptions: Whether to return false on failure or raise an exception
944947
:type raise_exceptions: Boolean
945948
"""
@@ -986,7 +989,17 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
986989
if len(signature_nodes) == 1:
987990
signature_node = signature_nodes[0]
988991

989-
return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True)
992+
if not multicerts:
993+
return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True)
994+
else:
995+
# If multiple certs are provided, I may ignore cert and
996+
# fingerprint provided by the method and just check the
997+
# certs multicerts
998+
fingerprint = fingerprintalg = None
999+
for cert in multicerts:
1000+
if OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, False, raise_exceptions=False):
1001+
return True
1002+
raise OneLogin_Saml2_ValidationError('Signature validation failed. SAML Response rejected.')
9901003
else:
9911004
raise OneLogin_Saml2_ValidationError('Expected exactly one signature node; got {}.'.format(len(signature_nodes)), OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_SIGNATURES)
9921005

0 commit comments

Comments
 (0)