Skip to content

Commit 45b8918

Browse files
authored
Merge pull request SAML-Toolkits#206 from onelogin/key_rollover_mngmt
Improve Key Rollover management
2 parents 304a43d + 2b7550e commit 45b8918

23 files changed

+1551
-149
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
/demo-old/settings.php
77
/certs/sp.key
88
/certs/sp.crt
9+
/certs/sp_new.crt
910
/certs/metadata.key
1011
/certs/metadata.crt
1112
/tests/build

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ Installation
8686
* `mcrypt`. Install that library and its php driver if you gonna handle
8787
encrypted data (`nameID`, `assertions`).
8888
* `gettext`. Install that library and its php driver. It handles translations.
89+
* `curl`. Install that library and its php driver if you plan to use the IdP Metadata parser.
8990

9091
Since [PHP 5.3 is officially unsupported](http://php.net/eol.php) we recommend you to use a newer PHP version.
9192

@@ -183,6 +184,8 @@ Sometimes we could need a signature on the metadata published by the SP, in
183184
this case we could use the x.509 cert previously mentioned or use a new x.509
184185
cert: `metadata.crt` and `metadata.key`.
185186

187+
Use `sp_new.crt` if you are in a key rollover process and you want to
188+
publish that x509certificate on Service Provider metadata.
186189

187190
#### `extlib/` ####
188191

@@ -337,6 +340,14 @@ $settings = array (
337340
'x509cert' => '',
338341
'privateKey' => '',
339342

343+
/*
344+
* Key rollover
345+
* If you plan to update the SP x509cert and privateKey
346+
* you can define here the new x509cert and it will be
347+
* published on the SP metadata so Identity Providers can
348+
* read them and get ready for rollover.
349+
*/
350+
// 'x509certNew' => '',
340351
),
341352

342353
// Identity Provider Data that we want connected with our SP.
@@ -379,6 +390,22 @@ $settings = array (
379390
*/
380391
// 'certFingerprint' => '',
381392
// 'certFingerprintAlgorithm' => 'sha1',
393+
394+
/* In some scenarios the IdP uses different certificates for
395+
* signing/encryption, or is under key rollover phase and
396+
* more than one certificate is published on IdP metadata.
397+
* In order to handle that the toolkit offers that parameter.
398+
* (when used, 'x509cert' and 'certFingerprint' values are
399+
* ignored).
400+
*/
401+
// 'x509certMulti' => array(
402+
// 'signing' => array(
403+
// 0 => '<cert1-string>',
404+
// ),
405+
// 'encryption' => array(
406+
// 0 => '<cert2-string>',
407+
// )
408+
// ),
382409
),
383410
);
384411
```
@@ -1085,6 +1112,26 @@ You should be able to workaround this by configuring your server so that it is a
10851112
Or by using the method described on the previous section.
10861113

10871114

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

10901137
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.
@@ -1250,6 +1297,7 @@ Configuration of the OneLogin PHP Toolkit
12501297
* `checkSPCerts` - Checks if the x509 certs of the SP exists and are valid.
12511298
* `getSPkey` - Returns the x509 private key of the SP.
12521299
* `getSPcert` - Returns the x509 public cert of the SP.
1300+
* `getSPcertNew` - Returns the future x509 public cert of the SP.
12531301
* `getIdPData` - Gets the IdP data.
12541302
* `getSPData`Gets the SP data.
12551303
* `getSecurityData` - Gets security data.
@@ -1259,6 +1307,7 @@ Configuration of the OneLogin PHP Toolkit
12591307
* `validateMetadata` - Validates an XML SP Metadata.
12601308
* `formatIdPCert` - Formats the IdP cert.
12611309
* `formatSPCert` - Formats the SP cert.
1310+
* `formatSPCertNew` - Formats the SP cert new.
12621311
* `formatSPKey` - Formats the SP private key.
12631312
* `getErrors` - Returns an array with the errors, the array is empty when
12641313
the settings is ok.
@@ -1317,6 +1366,16 @@ Auxiliary class that contains several methods
13171366
(Message or Assertion).
13181367
* `validateSign` - Validates a signature (Message or Assertion).
13191368

1369+
##### OneLogin_Saml2_IdPMetadataParser - `IdPMetadataParser.php` #####
1370+
1371+
Auxiliary class that contains several methods to retrieve and process IdP metadata
1372+
1373+
* `parseRemoteXML` - Get IdP Metadata Info from URL.
1374+
* `parseFileXML` - Get IdP Metadata Info from File.
1375+
* `parseXML` - Get IdP Metadata Info from XML.
1376+
* `injectIntoSettings` - Inject metadata info into php-saml settings array.
1377+
1378+
13201379
For more info, look at the source code; each method is documented and details
13211380
about what it does and how to use it are provided. Make sure to also check the doc folder where
13221381
HTML documentation about the classes and methods is provided for SAML and

certs/README

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Onelogin PHP Toolkit expects certs for the SP stored at:
44

55
* sp.key Private Key
66
* sp.crt Public cert
7+
* sp_new.crt Future Public cert
78

89
Also you can use other cert to sign the metadata of the SP using the:
910

lib/Saml2/IdPMetadataParser.php

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
<?php
2+
3+
/**
4+
* IdP Metadata Parser of OneLogin PHP Toolkit
5+
*
6+
*/
7+
8+
class OneLogin_Saml2_IdPMetadataParser
9+
{
10+
/**
11+
* Get IdP Metadata Info from URL
12+
*
13+
* @param string $url URL where the IdP metadata is published
14+
* @param string $entityId Entity Id of the desired IdP, if no
15+
* entity Id is provided and the XML
16+
* metadata contains more than one
17+
* IDPSSODescriptor, the first is returned
18+
* @param string $desiredNameIdFormat If available on IdP metadata, use that nameIdFormat
19+
*
20+
* @return array metadata info in php-saml settings format
21+
*/
22+
public static function parseRemoteXML($url, $entityId = null, $desiredNameIdFormat = null)
23+
{
24+
$metadataInfo = array();
25+
26+
try {
27+
$ch = curl_init($url);
28+
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
29+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
30+
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
31+
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
32+
curl_setopt($ch, CURLOPT_FAILONERROR, 1);
33+
34+
$xml = curl_exec($ch);
35+
if ($xml !== false) {
36+
$metadataInfo = self::parseXML($xml, $entityId);
37+
} else {
38+
throw new Exception(curl_error($ch), curl_errno($ch));
39+
}
40+
} catch (Exception $e) {
41+
}
42+
return $metadataInfo;
43+
}
44+
45+
/**
46+
* Get IdP Metadata Info from File
47+
*
48+
* @param string $filepath File path
49+
* @param string $entityId Entity Id of the desired IdP, if no
50+
* entity Id is provided and the XML
51+
* metadata contains more than one
52+
* IDPSSODescriptor, the first is returned
53+
* @param string $desiredNameIdFormat If available on IdP metadata, use that nameIdFormat
54+
*
55+
* @return array metadata info in php-saml settings format
56+
*/
57+
public static function parseFileXML($filepath, $entityId = null, $desiredNameIdFormat = null)
58+
{
59+
$metadataInfo = array();
60+
61+
try {
62+
if (file_exists($filepath)) {
63+
$data = file_get_contents($filepath);
64+
$metadataInfo = self::parseXML($data, $entityId);
65+
}
66+
} catch (Exception $e) {
67+
}
68+
return $metadataInfo;
69+
}
70+
71+
/**
72+
* Get IdP Metadata Info from URL
73+
*
74+
* @param string $xml XML that contains IdP metadata
75+
* @param string $entityId Entity Id of the desired IdP, if no
76+
* entity Id is provided and the XML
77+
* metadata contains more than one
78+
* IDPSSODescriptor, the first is returned
79+
* @param string $desiredNameIdFormat If available on IdP metadata, use that nameIdFormat
80+
*
81+
* @return array metadata info in php-saml settings format
82+
*/
83+
public static function parseXML($xml, $entityId = null, $desiredNameIdFormat = null)
84+
{
85+
$metadataInfo = array();
86+
87+
$dom = new DOMDocument();
88+
$dom->preserveWhiteSpace = false;
89+
$dom->formatOutput = true;
90+
try {
91+
$dom = OneLogin_Saml2_Utils::loadXML($dom, $xml);
92+
if (!$dom) {
93+
throw new Exception('Error parsing metadata');
94+
}
95+
96+
$customIdPStr = '';
97+
if (!empty($entityId)) {
98+
$customIdPStr = '[@entityID="' . $entityId . '"]';
99+
}
100+
$idpDescryptorXPath = '//md:EntityDescriptor' . $customIdPStr . '/md:IDPSSODescriptor';
101+
102+
$idpDescriptorNodes = OneLogin_Saml2_Utils::query($dom, $idpDescryptorXPath);
103+
104+
if (isset($idpDescriptorNodes) && $idpDescriptorNodes->length > 0) {
105+
$metadataInfo['idp'] = array();
106+
107+
$idpDescriptor = $idpDescriptorNodes->item(0);
108+
109+
if (empty($entityId) && $idpDescriptor->parentNode->hasAttribute('entityID')) {
110+
$entityId = $idpDescriptor->parentNode->getAttribute('entityID');
111+
}
112+
113+
if (!empty($entityId)) {
114+
$metadataInfo['idp']['entityId'] = $entityId;
115+
}
116+
117+
$ssoNodes = OneLogin_Saml2_Utils::query($dom, './md:SingleSignOnService[@Binding="'.OneLogin_Saml2_Constants::BINDING_HTTP_REDIRECT.'"]', $idpDescriptor);
118+
if ($ssoNodes->length < 1) {
119+
$ssoNodes = OneLogin_Saml2_Utils::query($dom, './md:SingleSignOnService', $idpDescriptor);
120+
}
121+
if ($ssoNodes->length > 0) {
122+
$metadataInfo['idp']['singleSignOnService'] = array(
123+
'url' => $ssoNodes->item(0)->getAttribute('Location'),
124+
'binding' => $ssoNodes->item(0)->getAttribute('Binding')
125+
);
126+
}
127+
128+
$sloNodes = OneLogin_Saml2_Utils::query($dom, './md:SingleSignOnService[@Binding="'.OneLogin_Saml2_Constants::BINDING_HTTP_REDIRECT.'"]', $idpDescriptor);
129+
if ($sloNodes->length < 1) {
130+
$sloNodes = OneLogin_Saml2_Utils::query($dom, './md:SingleSignOnService', $idpDescriptor);
131+
}
132+
if ($sloNodes->length > 0) {
133+
$metadataInfo['idp']['singleLogoutService'] = array(
134+
'url' => $sloNodes->item(0)->getAttribute('Location'),
135+
'binding' => $sloNodes->item(0)->getAttribute('Binding')
136+
);
137+
}
138+
139+
$keyDescriptorCertSigningNodes = OneLogin_Saml2_Utils::query($dom, './md:KeyDescriptor[not(contains(@use, "encryption"))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate', $idpDescriptor);
140+
141+
$keyDescriptorCertEncryptionNodes = OneLogin_Saml2_Utils::query($dom, './md:KeyDescriptor[not(contains(@use, "signing"))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate', $idpDescriptor);
142+
143+
if (!empty($keyDescriptorCertSigningNodes) || !empty($keyDescriptorCertEncryptionNodes)) {
144+
$metadataInfo['idp']['x509certMulti'] = array();
145+
if (!empty($keyDescriptorCertSigningNodes)) {
146+
$idpInfo['x509certMulti']['signing'] = array();
147+
foreach ($keyDescriptorCertSigningNodes as $keyDescriptorCertSigningNode) {
148+
$metadataInfo['idp']['x509certMulti']['signing'][] = OneLogin_Saml2_Utils::formatCert($keyDescriptorCertSigningNode->nodeValue, false);
149+
}
150+
}
151+
if (!empty($keyDescriptorCertEncryptionNodes)) {
152+
$idpInfo['x509certMulti']['encryption'] = array();
153+
foreach ($keyDescriptorCertEncryptionNodes as $keyDescriptorCertEncryptionNode) {
154+
$metadataInfo['idp']['x509certMulti']['encryption'][] = OneLogin_Saml2_Utils::formatCert($keyDescriptorCertEncryptionNode->nodeValue, false);
155+
}
156+
}
157+
158+
$idpCertdata = $metadataInfo['idp']['x509certMulti'];
159+
if (count($idpCertdata) == 1 || ((isset($idpCertdata['signing']) && count($idpCertdata['signing']) == 1) && isset($idpCertdata['encryption']) && count($idpCertdata['encryption']) == 1 && strcmp($idpCertdata['signing'][0], $idpCertdata['encryption'][0]) == 0)) {
160+
if (isset($metadataInfo['idp']['x509certMulti']['signing'][0])) {
161+
$metadataInfo['idp']['x509cert'] = $metadataInfo['idp']['x509certMulti']['signing'][0];
162+
} else {
163+
$metadataInfo['idp']['x509cert'] = $metadataInfo['idp']['x509certMulti']['encryption'][0];
164+
}
165+
unset($metadataInfo['idp']['x509certMulti']);
166+
}
167+
}
168+
169+
$nameIdFormatNodes = OneLogin_Saml2_Utils::query($dom, './md:NameIDFormat', $idpDescriptor);
170+
if ($nameIdFormatNodes->length > 0) {
171+
$metadataInfo['sp']['NameIDFormat'] = $nameIdFormatNodes->item(0)->nodeValue;
172+
if (!empty($desiredNameIdFormat)) {
173+
foreach ($nameIdFormatNodes as $nameIdFormatNode) {
174+
if (strcmp($nameIdFormatNode->nodeValue, $desiredNameIdFormat) == 0) {
175+
$metadataInfo['sp']['NameIDFormat'] = $nameIdFormatNode->nodeValue;
176+
break;
177+
}
178+
}
179+
}
180+
}
181+
}
182+
} catch (Exception $e) {
183+
throw new Exception('Error parsing metadata. '.$e->getMessage());
184+
}
185+
186+
return $metadataInfo;
187+
}
188+
189+
/**
190+
* Inject metadata info into php-saml settings array
191+
*
192+
* @param string $settings php-saml settings array
193+
* @param string $metadataInfo array metadata info
194+
*
195+
* @return array settings
196+
*/
197+
public static function injectIntoSettings($settings, $metadataInfo)
198+
{
199+
if (isset($metadataInfo['idp']) && isset($settings['idp'])) {
200+
if (isset($metadataInfo['idp']['x509certMulti']) && !empty($metadataInfo['idp']['x509certMulti']) && isset($settings['idp']['x509cert'])) {
201+
unset($settings['idp']['x509cert']);
202+
}
203+
204+
if (isset($metadataInfo['idp']['x509cert']) && !empty($metadataInfo['idp']['x509cert']) && isset($settings['idp']['x509certMulti'])) {
205+
unset($settings['idp']['x509certMulti']);
206+
}
207+
}
208+
209+
return array_replace_recursive($settings, $metadataInfo);
210+
}
211+
}

0 commit comments

Comments
 (0)