Skip to content

Commit 84aa522

Browse files
committed
Be able to register more than 1 Identity Provider x509cert, linked with an specific use (signing or encryption).
1 parent aee91b8 commit 84aa522

File tree

12 files changed

+427
-129
lines changed

12 files changed

+427
-129
lines changed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,22 @@ $settings = array (
389389
*/
390390
// 'certFingerprint' => '',
391391
// 'certFingerprintAlgorithm' => 'sha1',
392+
393+
/* In some scenarios the IdP uses different certificates for
394+
* signing/encryption, or is under key rollover phase and
395+
* more than one certificate is published on IdP metadata.
396+
* In order to handle that the toolkit offers that parameter.
397+
* (when used, 'x509cert' and 'certFingerprint' values are
398+
* ignored).
399+
*/
400+
// 'x509certMulti' => array(
401+
// 'signing' => array(
402+
// 0 => '<cert1-string>',
403+
// ),
404+
// 'encryption' => array(
405+
// 0 => '<cert2-string>',
406+
// )
407+
// ),
392408
),
393409
);
394410
```
@@ -1095,6 +1111,26 @@ You should be able to workaround this by configuring your server so that it is a
10951111
Or by using the method described on the previous section.
10961112

10971113

1114+
### SP Key rollover ###
1115+
1116+
If you plan to update the SP x509cert and privateKey you can define the new x509cert as $settings['sp']['x509certNew'] and it will be
1117+
published on the SP metadata so Identity Providers can read them and get ready for rollover.
1118+
1119+
1120+
### IdP with multiple certificates ###
1121+
1122+
In some scenarios the IdP uses different certificates for
1123+
signing/encryption, or is under key rollover phase and more than one certificate is published on IdP metadata.
1124+
1125+
In order to handle that the toolkit offers the $settings['idp']['x509certMulti'] parameter.
1126+
1127+
When that parameter is used, 'x509cert' and 'certFingerprint' values will be ignored by the toolkit.
1128+
1129+
The 'x509certMulti' is an array with 2 keys:
1130+
- 'signing'. An array of certs that will be used to validate IdP signature
1131+
- 'encryption' An array with one unique cert that will be used to encrypt data to be sent to the IdP
1132+
1133+
10981134
### Replay attacks ###
10991135

11001136
In order to avoid replay attacks, you can store the ID of the SAML messages already processed, to avoid processing them twice. Since the Messages expires and will be invalidated due that fact, you don't need to store those IDs longer than the time frame that you currently accepting.

lib/Saml2/LogoutRequest.php

Lines changed: 9 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,13 @@ public function __construct(OneLogin_Saml2_Settings $settings, $request = null,
6161

6262
$cert = null;
6363
if (isset($security['nameIdEncrypted']) && $security['nameIdEncrypted']) {
64-
$cert = $idpData['x509cert'];
64+
$existsMultiX509Enc = isset($idpData['x509certMulti']) && isset($idpData['x509certMulti']['encryption']) && !empty($idpData['x509certMulti']['encryption']);
65+
66+
if ($existsMultiX509Enc) {
67+
$cert = $idpData['x509certMulti']['encryption'][0];
68+
} else {
69+
$cert = $idpData['x509cert'];
70+
}
6571
}
6672

6773
if (!empty($nameId)) {
@@ -357,49 +363,8 @@ public function isValid($retrieveParametersFromServer = false)
357363
}
358364

359365
if (isset($_GET['Signature'])) {
360-
if (!isset($_GET['SigAlg'])) {
361-
$signAlg = XMLSecurityKey::RSA_SHA1;
362-
} else {
363-
$signAlg = $_GET['SigAlg'];
364-
}
365-
366-
if ($retrieveParametersFromServer) {
367-
$signedQuery = 'SAMLRequest='.OneLogin_Saml2_Utils::extractOriginalQueryParam('SAMLRequest');
368-
if (isset($_GET['RelayState'])) {
369-
$signedQuery .= '&RelayState='.OneLogin_Saml2_Utils::extractOriginalQueryParam('RelayState');
370-
}
371-
$signedQuery .= '&SigAlg='.OneLogin_Saml2_Utils::extractOriginalQueryParam('SigAlg');
372-
} else {
373-
$signedQuery = 'SAMLRequest='.urlencode($_GET['SAMLRequest']);
374-
if (isset($_GET['RelayState'])) {
375-
$signedQuery .= '&RelayState='.urlencode($_GET['RelayState']);
376-
}
377-
$signedQuery .= '&SigAlg='.urlencode($signAlg);
378-
}
379-
380-
if (!isset($idpData['x509cert']) || empty($idpData['x509cert'])) {
381-
throw new OneLogin_Saml2_Error(
382-
"In order to validate the sign on the Logout Request, the x509cert of the IdP is required",
383-
OneLogin_Saml2_Error::CERT_NOT_FOUND
384-
);
385-
}
386-
$cert = $idpData['x509cert'];
387-
388-
$objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type' => 'public'));
389-
$objKey->loadKey($cert, false, true);
390-
391-
if ($signAlg != XMLSecurityKey::RSA_SHA1) {
392-
try {
393-
$objKey = OneLogin_Saml2_Utils::castKey($objKey, $signAlg, 'public');
394-
} catch (Exception $e) {
395-
throw new OneLogin_Saml2_ValidationError(
396-
"Invalid signAlg in the recieved Logout Request",
397-
OneLogin_Saml2_ValidationError::INVALID_SIGNATURE
398-
);
399-
}
400-
}
401-
402-
if ($objKey->verifySignature($signedQuery, base64_decode($_GET['Signature'])) !== 1) {
366+
$signatureValid = OneLogin_Saml2_Utils::validateBinarySign("SAMLRequest", $_GET, $idpData, $retrieveParametersFromServer);
367+
if (!$signatureValid) {
403368
throw new OneLogin_Saml2_ValidationError(
404369
"Signature validation failed. Logout Request rejected",
405370
OneLogin_Saml2_ValidationError::INVALID_SIGNATURE

lib/Saml2/LogoutResponse.php

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -175,49 +175,8 @@ public function isValid($requestId = null, $retrieveParametersFromServer = false
175175
}
176176

177177
if (isset($_GET['Signature'])) {
178-
if (!isset($_GET['SigAlg'])) {
179-
$signAlg = XMLSecurityKey::RSA_SHA1;
180-
} else {
181-
$signAlg = $_GET['SigAlg'];
182-
}
183-
184-
if ($retrieveParametersFromServer) {
185-
$signedQuery = 'SAMLResponse='.OneLogin_Saml2_Utils::extractOriginalQueryParam('SAMLResponse');
186-
if (isset($_GET['RelayState'])) {
187-
$signedQuery .= '&RelayState='.OneLogin_Saml2_Utils::extractOriginalQueryParam('RelayState');
188-
}
189-
$signedQuery .= '&SigAlg='.OneLogin_Saml2_Utils::extractOriginalQueryParam('SigAlg');
190-
} else {
191-
$signedQuery = 'SAMLResponse='.urlencode($_GET['SAMLResponse']);
192-
if (isset($_GET['RelayState'])) {
193-
$signedQuery .= '&RelayState='.urlencode($_GET['RelayState']);
194-
}
195-
$signedQuery .= '&SigAlg='.urlencode($signAlg);
196-
}
197-
198-
if (!isset($idpData['x509cert']) || empty($idpData['x509cert'])) {
199-
throw new OneLogin_Saml2_Error(
200-
"In order to validate the sign on the Logout Response, the x509cert of the IdP is required",
201-
OneLogin_Saml2_Error::CERT_NOT_FOUND
202-
);
203-
}
204-
$cert = $idpData['x509cert'];
205-
206-
$objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type' => 'public'));
207-
$objKey->loadKey($cert, false, true);
208-
209-
if ($signAlg != XMLSecurityKey::RSA_SHA1) {
210-
try {
211-
$objKey = OneLogin_Saml2_Utils::castKey($objKey, $signAlg, 'public');
212-
} catch (Exception $e) {
213-
throw new OneLogin_Saml2_ValidationError(
214-
"Invalid signAlg in the recieved Logout Response",
215-
OneLogin_Saml2_ValidationError::INVALID_SIGNATURE
216-
);
217-
}
218-
}
219-
220-
if ($objKey->verifySignature($signedQuery, base64_decode($_GET['Signature'])) !== 1) {
178+
$signatureValid = OneLogin_Saml2_Utils::validateBinarySign("SAMLResponse", $_GET, $idpData, $retrieveParametersFromServer);
179+
if (!$signatureValid) {
221180
throw new OneLogin_Saml2_ValidationError(
222181
"Signature validation failed. Logout Response rejected",
223182
OneLogin_Saml2_ValidationError::INVALID_SIGNATURE

lib/Saml2/Response.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -373,8 +373,15 @@ public function isValid($requestId = null)
373373
$fingerprint = $idpData['certFingerprint'];
374374
$fingerprintalg = $idpData['certFingerprintAlgorithm'];
375375

376+
$multiCerts = null;
377+
$existsMultiX509Sign = isset($idpData['x509certMulti']) && isset($idpData['x509certMulti']['signing']) && !empty($idpData['x509certMulti']['signing']);
378+
379+
if ($existsMultiX509Sign) {
380+
$multiCerts = $idpData['x509certMulti']['signing'];
381+
}
382+
376383
# If find a Signature on the Response, validates it checking the original response
377-
if ($hasSignedResponse && !OneLogin_Saml2_Utils::validateSign($this->document, $cert, $fingerprint, $fingerprintalg, OneLogin_Saml2_Utils::RESPONSE_SIGNATURE_XPATH)) {
384+
if ($hasSignedResponse && !OneLogin_Saml2_Utils::validateSign($this->document, $cert, $fingerprint, $fingerprintalg, OneLogin_Saml2_Utils::RESPONSE_SIGNATURE_XPATH, $multiCerts)) {
378385
throw new OneLogin_Saml2_ValidationError(
379386
"Signature validation failed. SAML Response rejected",
380387
OneLogin_Saml2_ValidationError::INVALID_SIGNATURE
@@ -383,7 +390,7 @@ public function isValid($requestId = null)
383390

384391
# If find a Signature on the Assertion (decrypted assertion if was encrypted)
385392
$documentToCheckAssertion = $this->encrypted ? $this->decryptedDocument : $this->document;
386-
if ($hasSignedAssertion && !OneLogin_Saml2_Utils::validateSign($documentToCheckAssertion, $cert, $fingerprint, $fingerprintalg, OneLogin_Saml2_Utils::ASSERTION_SIGNATURE_XPATH)) {
393+
if ($hasSignedAssertion && !OneLogin_Saml2_Utils::validateSign($documentToCheckAssertion, $cert, $fingerprint, $fingerprintalg, OneLogin_Saml2_Utils::ASSERTION_SIGNATURE_XPATH, $multiCerts)) {
387394
throw new OneLogin_Saml2_ValidationError(
388395
"Signature validation failed. SAML Response rejected",
389396
OneLogin_Saml2_ValidationError::INVALID_SIGNATURE
@@ -1004,7 +1011,7 @@ protected function _decryptAssertion($dom)
10041011
OneLogin_Saml2_Error::PRIVATE_KEY_NOT_FOUND
10051012
);
10061013
}
1007-
1014+
10081015
$objenc = new XMLSecEnc();
10091016
$encData = $objenc->locateEncryptedData($dom);
10101017
if (!$encData) {
@@ -1013,7 +1020,7 @@ protected function _decryptAssertion($dom)
10131020
OneLogin_Saml2_ValidationError::MISSING_ENCRYPTED_ELEMENT
10141021
);
10151022
}
1016-
1023+
10171024
$objenc->setNode($encData);
10181025
$objenc->type = $encData->getAttribute("Type");
10191026
if (!$objKey = $objenc->locateKey()) {

lib/Saml2/Settings.php

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class OneLogin_Saml2_Settings
1717
/**
1818
* @var string
1919
*/
20-
private $_baseurl;
20+
private $_baseurl;
2121

2222
/**
2323
* Strict. If active, PHP Toolkit will reject unsigned or unencrypted messages
@@ -143,6 +143,8 @@ public function __construct($settings = null, $spValidationOnly = false)
143143
$this->formatIdPCert();
144144
$this->formatSPCert();
145145
$this->formatSPKey();
146+
$this->formatSPCertNew();
147+
$this->formatIdPCertMulti();
146148
}
147149

148150
/**
@@ -465,14 +467,12 @@ public function checkCompressionSettings($settings)
465467
if (isset($settings['compress'])) {
466468
if (!is_array($settings['compress'])) {
467469
$errors[] = "invalid_syntax";
468-
} else if (
469-
isset($settings['compress']['requests'])
470+
} else if (isset($settings['compress']['requests'])
470471
&& $settings['compress']['requests'] !== true
471472
&& $settings['compress']['requests'] !== false
472473
) {
473474
$errors[] = "'compress'=>'requests' values must be true or false.";
474-
} else if (
475-
isset($settings['compress']['responses'])
475+
} else if (isset($settings['compress']['responses'])
476476
&& $settings['compress']['responses'] !== true
477477
&& $settings['compress']['responses'] !== false
478478
) {
@@ -528,15 +528,16 @@ public function checkIdPSettings($settings)
528528
$security = $settings['security'];
529529

530530
$existsX509 = isset($idp['x509cert']) && !empty($idp['x509cert']);
531+
$existsMultiX509Sign = isset($idp['x509certMulti']) && isset($idp['x509certMulti']['signing']) && !empty($idp['x509certMulti']['signing']);
532+
$existsMultiX509Enc = isset($idp['x509certMulti']) && isset($idp['x509certMulti']['encryption']) && !empty($idp['x509certMulti']['encryption']);
533+
531534
$existsFingerprint = isset($idp['certFingerprint']) && !empty($idp['certFingerprint']);
532-
if (((isset($security['wantAssertionsSigned']) && $security['wantAssertionsSigned'] == true)
533-
|| (isset($security['wantMessagesSigned']) && $security['wantMessagesSigned'] == true))
534-
&& !($existsX509 || $existsFingerprint)
535+
if (!($existsX509 || $existsFingerprint || $existsMultiX509Sign)
535536
) {
536537
$errors[] = 'idp_cert_or_fingerprint_not_found_and_required';
537538
}
538539
if ((isset($security['nameIdEncrypted']) && $security['nameIdEncrypted'] == true)
539-
&& !($existsX509)
540+
&& !($existsX509 || $existsMultiX509Enc)
540541
) {
541542
$errors[] = 'idp_cert_not_found_and_required';
542543
}
@@ -934,6 +935,25 @@ public function formatIdPCert()
934935
}
935936
}
936937

938+
/**
939+
* Formats the Multple IdP certs.
940+
*/
941+
public function formatIdPCertMulti()
942+
{
943+
if (isset($this->_idp['x509certMulti'])) {
944+
if (isset($this->_idp['x509certMulti']['signing'])) {
945+
for ($i=0; $i < count($this->_idp['x509certMulti']['signing']); $i++) {
946+
$this->_idp['x509certMulti']['signing'][$i] = OneLogin_Saml2_Utils::formatCert($this->_idp['x509certMulti']['signing'][$i]);
947+
}
948+
}
949+
if (isset($this->_idp['x509certMulti']['encryption'])) {
950+
for ($i=0; $i < count($this->_idp['x509certMulti']['encryption']); $i++) {
951+
$this->_idp['x509certMulti']['encryption'][$i] = OneLogin_Saml2_Utils::formatCert($this->_idp['x509certMulti']['encryption'][$i]);
952+
}
953+
}
954+
}
955+
}
956+
937957
/**
938958
* Formats the SP cert.
939959
*/
@@ -944,6 +964,16 @@ public function formatSPCert()
944964
}
945965
}
946966

967+
/**
968+
* Formats the SP cert.
969+
*/
970+
public function formatSPCertNew()
971+
{
972+
if (isset($this->_sp['x509certNew'])) {
973+
$this->_sp['x509certNew'] = OneLogin_Saml2_Utils::formatCert($this->_sp['x509certNew']);
974+
}
975+
}
976+
947977
/**
948978
* Formats the SP private key.
949979
*/
@@ -1023,7 +1053,7 @@ public function getBaseURL()
10231053
*/
10241054
public function setIdPCert($cert)
10251055
{
1026-
$this->_idp['x509cert'] = $cert;
1027-
$this->formatIdPCert();
1056+
$this->_idp['x509cert'] = $cert;
1057+
$this->formatIdPCert();
10281058
}
10291059
}

0 commit comments

Comments
 (0)