Passed
Push — master ( 87f03c...4cefa8 )
by Tim
01:57
created
lib/Aggregator.php 1 patch
Indentation   +714 added lines, -714 removed lines patch added patch discarded remove patch
@@ -23,718 +23,718 @@
 block discarded – undo
23 23
  */
24 24
 class Aggregator
25 25
 {
26
-    /**
27
-     * The list of signature algorithms supported by the aggregator.
28
-     *
29
-     * @var array
30
-     */
31
-    public static $SUPPORTED_SIGNATURE_ALGORITHMS = [
32
-        XMLSecurityKey::RSA_SHA1,
33
-        XMLSecurityKey::RSA_SHA256,
34
-        XMLSecurityKey::RSA_SHA384,
35
-        XMLSecurityKey::RSA_SHA512,
36
-    ];
37
-
38
-    /**
39
-     * The ID of this aggregator.
40
-     *
41
-     * @var string
42
-     */
43
-    protected $id;
44
-
45
-    /**
46
-     * Our log "location".
47
-     *
48
-     * @var string
49
-     */
50
-    protected $logLoc;
51
-
52
-    /**
53
-     * Which cron-tag this should be updated in.
54
-     *
55
-     * @var string|null
56
-     */
57
-    protected $cronTag;
58
-
59
-    /**
60
-     * Absolute path to a cache directory.
61
-     *
62
-     * @var string|null
63
-     */
64
-    protected $cacheDirectory;
65
-
66
-    /**
67
-     * The entity sources.
68
-     *
69
-     * Array of sspmod_aggregator2_EntitySource objects.
70
-     *
71
-     * @var array
72
-     */
73
-    protected $sources = [];
74
-
75
-    /**
76
-     * How long the generated metadata should be valid, as a number of seconds.
77
-     *
78
-     * This is used to set the validUntil attribute on the generated EntityDescriptor.
79
-     *
80
-     * @var int
81
-     */
82
-    protected $validLength;
83
-
84
-    /**
85
-     * Duration we should cache generated metadata.
86
-     *
87
-     * @var int
88
-     */
89
-    protected $cacheGenerated;
90
-
91
-    /**
92
-     * An array of entity IDs to exclude from the aggregate.
93
-     *
94
-     * @var string[]|null
95
-     */
96
-    protected $excluded;
97
-
98
-    /**
99
-     * An indexed array of protocols to filter the aggregate by. keys can be any of:
100
-     *
101
-     * - urn:oasis:names:tc:SAML:1.1:protocol
102
-     * - urn:oasis:names:tc:SAML:2.0:protocol
103
-     *
104
-     * Values will be true if enabled, false otherwise.
105
-     *
106
-     * @var array|null
107
-     */
108
-    protected $protocols;
109
-
110
-    /**
111
-     * An array of roles to filter the aggregate by. Keys can be any of:
112
-     *
113
-     * - SAML2_XML_md_IDPSSODescriptor
114
-     * - SAML2_XML_md_SPSSODescriptor
115
-     * - SAML2_XML_md_AttributeAuthorityDescriptor
116
-     *
117
-     * Values will be true if enabled, false otherwise.
118
-     *
119
-     * @var array|null
120
-     */
121
-    protected $roles;
122
-
123
-    /**
124
-     * The key we should use to sign the metadata.
125
-     *
126
-     * @var string|null
127
-     */
128
-    protected $signKey;
129
-
130
-    /**
131
-     * The password for the private key.
132
-     *
133
-     * @var string|null
134
-     */
135
-    protected $signKeyPass;
136
-
137
-    /**
138
-     * The certificate of the key we sign the metadata with.
139
-     *
140
-     * @var string|null
141
-     */
142
-    protected $signCert;
143
-
144
-    /**
145
-     * The algorithm to use for metadata signing.
146
-     *
147
-     * @var string|null
148
-     */
149
-    protected $signAlg;
150
-
151
-    /**
152
-     * The CA certificate file that should be used to validate https-connections.
153
-     *
154
-     * @var string|null
155
-     */
156
-    protected $sslCAFile;
157
-
158
-    /**
159
-     * The cache ID for our generated metadata.
160
-     *
161
-     * @var string
162
-     */
163
-    protected $cacheId;
164
-
165
-    /**
166
-     * The cache tag for our generated metadata.
167
-     *
168
-     * This tag is used to make sure that a config change
169
-     * invalidates our cached metadata.
170
-     *
171
-     * @var string
172
-     */
173
-    protected $cacheTag;
174
-
175
-    /**
176
-     * The registration information for our generated metadata.
177
-     *
178
-     * @var array
179
-     */
180
-    protected $regInfo;
181
-
182
-
183
-    /**
184
-     * Initialize this aggregator.
185
-     *
186
-     * @param string $id  The id of this aggregator.
187
-     * @param \SimpleSAML\Configuration $config  The configuration for this aggregator.
188
-     */
189
-    protected function __construct($id, Configuration $config)
190
-    {
191
-        assert('is_string($id)');
192
-
193
-        $this->id = $id;
194
-        $this->logLoc = 'aggregator2:'.$this->id.': ';
195
-
196
-        $this->cronTag = $config->getString('cron.tag', null);
197
-
198
-        $this->cacheDirectory = $config->getString('cache.directory', null);
199
-        if ($this->cacheDirectory !== null) {
200
-            $this->cacheDirectory = System::resolvePath($this->cacheDirectory);
201
-        }
202
-
203
-        $this->cacheGenerated = $config->getInteger('cache.generated', null);
204
-        if ($this->cacheGenerated !== null) {
205
-            $this->cacheId = sha1($this->id);
206
-            $this->cacheTag = sha1(serialize($config));
207
-        }
208
-
209
-        // configure entity IDs excluded by default
210
-        $this->excludeEntities($config->getArrayize('exclude', null));
211
-
212
-        // configure filters
213
-        $this->setFilters($config->getArrayize('filter', null));
214
-
215
-        $this->validLength = $config->getInteger('valid.length', 7*24*60*60);
216
-
217
-        $globalConfig = Configuration::getInstance();
218
-        $certDir = $globalConfig->getPathValue('certdir', 'cert/');
219
-
220
-        $signKey = $config->getString('sign.privatekey', null);
221
-        if ($signKey !== null) {
222
-            $signKey = System::resolvePath($signKey, $certDir);
223
-            $this->signKey = @file_get_contents($signKey);
224
-            if ($this->signKey === null) {
225
-                throw new Exception('Unable to load private key from '.var_export($signKey, true));
226
-            }
227
-        }
228
-
229
-        $this->signKeyPass = $config->getString('sign.privatekey_pass', null);
230
-
231
-        $signCert = $config->getString('sign.certificate', null);
232
-        if ($signCert !== null) {
233
-            $signCert = System::resolvePath($signCert, $certDir);
234
-            $this->signCert = @file_get_contents($signCert);
235
-            if ($this->signCert === null) {
236
-                throw new Exception('Unable to load certificate file from '.var_export($signCert, true));
237
-            }
238
-        }
239
-
240
-        $this->signAlg = $config->getString('sign.algorithm', XMLSecurityKey::RSA_SHA1);
241
-        if (!in_array($this->signAlg, self::$SUPPORTED_SIGNATURE_ALGORITHMS)) {
242
-            throw new Exception('Unsupported signature algorithm '.var_export($this->signAlg, true));
243
-        }
244
-
245
-        $this->sslCAFile = $config->getString('ssl.cafile', null);
246
-
247
-        $this->regInfo = $config->getArray('RegistrationInfo', null);
248
-
249
-        $this->initSources($config->getConfigList('sources'));
250
-    }
251
-
252
-
253
-    /**
254
-     * Populate the sources array.
255
-     *
256
-     * This is called from the constructor, and can be overridden in subclasses.
257
-     *
258
-     * @param array $sources  The sources as an array of SimpleSAML_Configuration objects.
259
-     */
260
-    protected function initSources(array $sources)
261
-    {
262
-        foreach ($sources as $source) {
263
-            $this->sources[] = new EntitySource($this, $source);
264
-        }
265
-    }
266
-
267
-
268
-    /**
269
-     * Return an instance of the aggregator with the given id.
270
-     *
271
-     * @param string $id  The id of the aggregator.
272
-     */
273
-    public static function getAggregator($id)
274
-    {
275
-        assert('is_string($id)');
276
-
277
-        $config = Configuration::getConfig('module_aggregator2.php');
278
-        return new Aggregator($id, $config->getConfigItem($id));
279
-    }
280
-
281
-
282
-    /**
283
-     * Retrieve the ID of the aggregator.
284
-     *
285
-     * @return string  The ID of this aggregator.
286
-     */
287
-    public function getId()
288
-    {
289
-        return $this->id;
290
-    }
291
-
292
-
293
-    /**
294
-     * Add an item to the cache.
295
-     *
296
-     * @param string $id  The identifier of this data.
297
-     * @param string $data  The data.
298
-     * @param int $expires  The timestamp the data expires.
299
-     * @param string|null $tag  An extra tag that can be used to verify the validity of the cached data.
300
-     */
301
-    public function addCacheItem($id, $data, $expires, $tag = null)
302
-    {
303
-        assert('is_string($id)');
304
-        assert('is_string($data)');
305
-        assert('is_int($expires)');
306
-        assert('is_null($tag) || is_string($tag)');
307
-
308
-        $cacheFile = $this->cacheDirectory.'/'.$id;
309
-        try {
310
-            System::writeFile($cacheFile, $data);
311
-        } catch (\Exception $e) {
312
-            Logger::warning($this->logLoc.'Unable to write to cache file '.var_export($cacheFile, true));
313
-            return;
314
-        }
315
-
316
-        $expireInfo = (string)$expires;
317
-        if ($tag !== null) {
318
-            $expireInfo .= ':'.$tag;
319
-        }
320
-
321
-        $expireFile = $cacheFile.'.expire';
322
-        try {
323
-            System::writeFile($expireFile, $expireInfo);
324
-        } catch (\Exception $e) {
325
-            Logger::warning($this->logLoc.'Unable to write expiration info to '.var_export($expireFile, true));
326
-        }
327
-    }
328
-
329
-
330
-    /**
331
-     * Check validity of cached data.
332
-     *
333
-     * @param string $id  The identifier of this data.
334
-     * @param string $tag  The tag that was passed to addCacheItem.
335
-     * @return bool  TRUE if the data is valid, FALSE if not.
336
-     */
337
-    public function isCacheValid($id, $tag = null)
338
-    {
339
-        assert('is_string($id)');
340
-        assert('is_null($tag) || is_string($tag)');
341
-
342
-        $cacheFile = $this->cacheDirectory.'/'.$id;
343
-        if (!file_exists($cacheFile)) {
344
-            return false;
345
-        }
346
-
347
-        $expireFile = $cacheFile.'.expire';
348
-        if (!file_exists($expireFile)) {
349
-            return false;
350
-        }
351
-
352
-        $expireData = @file_get_contents($expireFile);
353
-        if ($expireData === false) {
354
-            return false;
355
-        }
356
-
357
-        $expireData = explode(':', $expireData, 2);
358
-
359
-        $expireTime = intval($expireData[0]);
360
-        if ($expireTime <= time()) {
361
-            return false;
362
-        }
363
-
364
-        if (count($expireData) === 1) {
365
-            $expireTag = null;
366
-        } else {
367
-            $expireTag = $expireData[1];
368
-        }
369
-        if ($expireTag !== $tag) {
370
-            return false;
371
-        }
372
-
373
-        return true;
374
-    }
375
-
376
-
377
-    /**
378
-     * Get the cache item.
379
-     *
380
-     * @param string $id  The identifier of this data.
381
-     * @param string $tag  The tag that was passed to addCacheItem.
382
-     * @return string|null  The cache item, or NULL if it isn't cached or if it is expired.
383
-     */
384
-    public function getCacheItem($id, $tag = null)
385
-    {
386
-        assert('is_string($id)');
387
-        assert('is_null($tag) || is_string($tag)');
388
-
389
-        if (!$this->isCacheValid($id, $tag)) {
390
-            return null;
391
-        }
392
-
393
-        $cacheFile = $this->cacheDirectory.'/'.$id;
394
-        return @file_get_contents($cacheFile);
395
-    }
396
-
397
-
398
-    /**
399
-     * Get the cache filename for the specific id.
400
-     *
401
-     * @param string $id  The identifier of the cached data.
402
-     * @return string|null  The filename, or NULL if the cache file doesn't exist.
403
-     */
404
-    public function getCacheFile($id)
405
-    {
406
-        assert('is_string($id)');
407
-
408
-        $cacheFile = $this->cacheDirectory.'/'.$id;
409
-        if (!file_exists($cacheFile)) {
410
-            return null;
411
-        }
412
-
413
-        return $cacheFile;
414
-    }
415
-
416
-
417
-    /**
418
-     * Retrieve the SSL CA file path, if it is set.
419
-     *
420
-     * @return string|null  The SSL CA file path.
421
-     */
422
-    public function getCAFile()
423
-    {
424
-        return $this->sslCAFile;
425
-    }
426
-
427
-
428
-    /**
429
-     * Sign the generated EntitiesDescriptor.
430
-     */
431
-    protected function addSignature(SignedElement $element)
432
-    {
433
-        if ($this->signKey === null) {
434
-            return;
435
-        }
436
-
437
-        $privateKey = new XMLSecurityKey($this->signAlg, ['type' => 'private']);
438
-        if ($this->signKeyPass !== null) {
439
-            $privateKey->passphrase = $this->signKeyPass;
440
-        }
441
-        $privateKey->loadKey($this->signKey, false);
442
-
443
-        $element->setSignatureKey($privateKey);
444
-
445
-        if ($this->signCert !== null) {
446
-            $element->setCertificates([$this->signCert]);
447
-        }
448
-    }
449
-
450
-
451
-    /**
452
-     * Recursively browse the children of an EntitiesDescriptor element looking for EntityDescriptor elements, and
453
-     * return an array containing all of them.
454
-     *
455
-     * @param \SAML2\XML\md\EntitiesDescriptor $entity The source EntitiesDescriptor that holds the entities to extract.
456
-     *
457
-     * @return array An array containing all the EntityDescriptors found.
458
-     */
459
-    private static function extractEntityDescriptors($entity)
460
-    {
461
-        assert('$entity instanceof EntitiesDescriptor');
462
-
463
-        if (!($entity instanceof EntitiesDescriptor)) {
464
-            return [];
465
-        }
466
-
467
-        $results = [];
468
-        foreach ($entity->children as $child) {
469
-            if ($child instanceof EntityDescriptor) {
470
-                $results[] = $child;
471
-                continue;
472
-            }
473
-
474
-            $results = array_merge($results, self::extractEntityDescriptors($child));
475
-        }
476
-        return $results;
477
-    }
478
-
479
-
480
-    /**
481
-     * Retrieve all entities as an EntitiesDescriptor.
482
-     *
483
-     * @return \SAML2\XML\md\EntitiesDescriptor  The entities.
484
-     */
485
-    protected function getEntitiesDescriptor()
486
-    {
487
-        $ret = new EntitiesDescriptor();
488
-        $now = time();
489
-
490
-        // add RegistrationInfo extension if enabled
491
-        if ($this->regInfo !== null) {
492
-            $ri = new RegistrationInfo();
493
-            $ri->registrationInstant = $now;
494
-            foreach ($this->regInfo as $riName => $riValues) {
495
-                switch ($riName) {
496
-                    case 'authority':
497
-                        $ri->registrationAuthority = $riValues;
498
-                        break;
499
-                    case 'instant':
500
-                        $ri->registrationInstant = Utils::xsDateTimeToTimestamp($riValues);
501
-                        break;
502
-                    case 'policies':
503
-                        $ri->RegistrationPolicy = $riValues;
504
-                        break;
505
-                }
506
-            }
507
-            $ret->Extensions[] = $ri;
508
-        }
509
-
510
-        foreach ($this->sources as $source) {
511
-            $m = $source->getMetadata();
512
-            if ($m === NULL) {
513
-                continue;
514
-            }
515
-            if ($m instanceof EntityDescriptor) {
516
-                $ret->children[] = $m;
517
-            } elseif ($m instanceof EntitiesDescriptor) {
518
-                $ret->children = array_merge($ret->children, self::extractEntityDescriptors($m));
519
-            }
520
-        }
521
-
522
-        $ret->children = array_unique($ret->children, SORT_REGULAR);
523
-        $ret->validUntil = $now + $this->validLength;
524
-
525
-        return $ret;
526
-    }
527
-
528
-
529
-    /**
530
-     * Recursively traverse the children of an EntitiesDescriptor, removing those entities listed in the $entities
531
-     * property. Returns the EntitiesDescriptor with the entities filtered out.
532
-     *
533
-     * @param \SAML2\XML\md\EntitiesDescriptor $descriptor The EntitiesDescriptor from where to exclude entities.
534
-     *
535
-     * @return \SAML2\XML\md\EntitiesDescriptor The EntitiesDescriptor with excluded entities filtered out.
536
-     */
537
-    protected function exclude(EntitiesDescriptor $descriptor)
538
-    {
539
-        if (empty($this->excluded)) {
540
-            return $descriptor;
541
-        }
542
-
543
-        $filtered = [];
544
-        foreach ($descriptor->children as $child) {
545
-            if ($child instanceof EntityDescriptor) {
546
-                if (in_array($child->entityID, $this->excluded)) {
547
-                    continue;
548
-                }
549
-                $filtered[] = $child;
550
-            }
551
-
552
-            if ($child instanceof EntitiesDescriptor) {
553
-                $filtered[] = $this->exclude($child);
554
-            }
555
-        }
556
-
557
-        $descriptor->children = $filtered;
558
-        return $descriptor;
559
-    }
560
-
561
-
562
-    /**
563
-     * Recursively traverse the children of an EntitiesDescriptor, keeping only those entities with the roles listed in
564
-     * the $roles property, and support for the protocols listed in the $protocols property. Returns the
565
-     * EntitiesDescriptor containing only those entities.
566
-     *
567
-     * @param \SAML2\XML\md\EntitiesDescriptor $descriptor The EntitiesDescriptor to filter.
568
-     *
569
-     * @return SAML2_XML_md_EntitiesDescriptor The EntitiesDescriptor with only the entities filtered.
570
-     */
571
-    protected function filter(EntitiesDescriptor $descriptor)
572
-    {
573
-        if ($this->roles === null || $this->protocols === null) {
574
-            return $descriptor;
575
-        }
576
-
577
-        $enabled_roles = array_keys($this->roles, true);
578
-        $enabled_protos = array_keys($this->protocols, true);
579
-
580
-        $filtered = [];
581
-        foreach ($descriptor->children as $child) {
582
-            if ($child instanceof EntityDescriptor) {
583
-                foreach ($child->RoleDescriptor as $role) {
584
-                    if (in_array(get_class($role), $enabled_roles)) {
585
-                        // we found a role descriptor that is enabled by our filters, check protocols
586
-                        if (array_intersect($enabled_protos, $role->protocolSupportEnumeration) !== []) {
587
-                            // it supports some protocol we have enabled, add it
588
-                            $filtered[] = $child;
589
-                            break;
590
-                        }
591
-                    }
592
-                }
593
-
594
-            }
595
-
596
-            if ($child instanceof EntitiesDescriptor) {
597
-                $filtered[] = $this->filter($child);
598
-            }
599
-        }
600
-
601
-        $descriptor->children = $filtered;
602
-        return $descriptor;
603
-    }
604
-
605
-
606
-    /**
607
-     * Set this aggregator to exclude a set of entities from the resulting aggregate.
608
-     *
609
-     * @param array|null $entities The entity IDs of the entities to exclude.
610
-     */
611
-    public function excludeEntities($entities)
612
-    {
613
-        assert('is_array($entities) || is_null($entities)');
614
-
615
-        if ($entities === null) {
616
-            return;
617
-        }
618
-        $this->excluded = $entities;
619
-        sort($this->excluded);
620
-        $this->cacheId = sha1($this->cacheId.serialize($this->excluded));
621
-    }
622
-
623
-
624
-    /**
625
-     * Set the internal filters according to one or more options:
626
-     *
627
-     * - 'saml2': all SAML2.0-capable entities.
628
-     * - 'shib13': all SHIB1.3-capable entities.
629
-     * - 'saml20-idp': all SAML2.0-capable identity providers.
630
-     * - 'saml20-sp': all SAML2.0-capable service providers.
631
-     * - 'saml20-aa': all SAML2.0-capable attribute authorities.
632
-     * - 'shib13-idp': all SHIB1.3-capable identity providers.
633
-     * - 'shib13-sp': all SHIB1.3-capable service providers.
634
-     * - 'shib13-aa': all SHIB1.3-capable attribute authorities.
635
-     *
636
-     * @param array|null $set An array of the different roles and protocols to filter by.
637
-     */
638
-    public function setFilters($set)
639
-    {
640
-        assert('is_array($set) || is_null($set)');
641
-
642
-        if ($set === null) {
643
-            return;
644
-        }
645
-
646
-        // configure filters
647
-        $this->protocols = [
648
-            Constants::NS_SAMLP                    => true,
649
-            'urn:oasis:names:tc:SAML:1.1:protocol' => true,
650
-        ];
651
-        $this->roles = [
652
-            'SAML2_XML_md_IDPSSODescriptor'             => true,
653
-            'SAML2_XML_md_SPSSODescriptor'              => true,
654
-            'SAML2_XML_md_AttributeAuthorityDescriptor' => true,
655
-        ];
656
-
657
-        // now translate from the options we have, to specific protocols and roles
658
-
659
-        // check SAML 2.0 protocol
660
-        $options = ['saml2', 'saml20-idp', 'saml20-sp', 'saml20-aa'];
661
-        $this->protocols[Constants::NS_SAMLP] = (array_intersect($set, $options) !== []);
662
-
663
-        // check SHIB 1.3 protocol
664
-        $options = ['shib13', 'shib13-idp', 'shib13-sp', 'shib13-aa'];
665
-        $this->protocols['urn:oasis:names:tc:SAML:1.1:protocol'] = (array_intersect($set, $options) !== []);
666
-
667
-        // check IdP
668
-        $options = ['saml2', 'shib13', 'saml20-idp', 'shib13-idp'];
669
-        $this->roles['SAML2_XML_md_IDPSSODescriptor'] = (array_intersect($set, $options) !== []);
670
-
671
-        // check SP
672
-        $options = ['saml2', 'shib13', 'saml20-sp', 'shib13-sp'];
673
-        $this->roles['SAML2_XML_md_SPSSODescriptor'] = (array_intersect($set, $options) !== []);
674
-
675
-        // check AA
676
-        $options = ['saml2', 'shib13', 'saml20-aa', 'shib13-aa'];
677
-        $this->roles['SAML2_XML_md_AttributeAuthorityDescriptor'] = (array_intersect($set, $options) !== []);
678
-
679
-        $this->cacheId = sha1($this->cacheId.serialize($this->protocols).serialize($this->roles));
680
-    }
681
-
682
-
683
-    /**
684
-     * Retrieve the complete, signed metadata as text.
685
-     *
686
-     * This function will write the new metadata to the cache file, but will not return
687
-     * the cached metadata.
688
-     *
689
-     * @return string  The metadata, as text.
690
-     */
691
-    public function updateCachedMetadata()
692
-    {
693
-        $ed = $this->getEntitiesDescriptor();
694
-        $ed = $this->exclude($ed);
695
-        $ed = $this->filter($ed);
696
-        $this->addSignature($ed);
697
-
698
-        $xml = $ed->toXML();
699
-        $xml = $xml->ownerDocument->saveXML($xml);
700
-
701
-        if ($this->cacheGenerated !== null) {
702
-            Logger::debug($this->logLoc.'Saving generated metadata to cache.');
703
-            $this->addCacheItem($this->cacheId, $xml, time() + $this->cacheGenerated, $this->cacheTag);
704
-        }
705
-
706
-        return $xml;
707
-    }
708
-
709
-
710
-    /**
711
-     * Retrieve the complete, signed metadata as text.
712
-     *
713
-     * @return string  The metadata, as text.
714
-     */
715
-    public function getMetadata()
716
-    {
717
-        if ($this->cacheGenerated !== null) {
718
-            $xml = $this->getCacheItem($this->cacheId, $this->cacheTag);
719
-            if ($xml !== null) {
720
-                Logger::debug($this->logLoc.'Loaded generated metadata from cache.');
721
-                return $xml;
722
-            }
723
-        }
724
-
725
-        return $this->updateCachedMetadata();
726
-    }
727
-
728
-
729
-    /**
730
-     * Update the cached copy of our metadata.
731
-     */
732
-    public function updateCache()
733
-    {
734
-        foreach ($this->sources as $source) {
735
-            $source->updateCache();
736
-        }
737
-
738
-        $this->updateCachedMetadata();
739
-    }
26
+	/**
27
+	 * The list of signature algorithms supported by the aggregator.
28
+	 *
29
+	 * @var array
30
+	 */
31
+	public static $SUPPORTED_SIGNATURE_ALGORITHMS = [
32
+		XMLSecurityKey::RSA_SHA1,
33
+		XMLSecurityKey::RSA_SHA256,
34
+		XMLSecurityKey::RSA_SHA384,
35
+		XMLSecurityKey::RSA_SHA512,
36
+	];
37
+
38
+	/**
39
+	 * The ID of this aggregator.
40
+	 *
41
+	 * @var string
42
+	 */
43
+	protected $id;
44
+
45
+	/**
46
+	 * Our log "location".
47
+	 *
48
+	 * @var string
49
+	 */
50
+	protected $logLoc;
51
+
52
+	/**
53
+	 * Which cron-tag this should be updated in.
54
+	 *
55
+	 * @var string|null
56
+	 */
57
+	protected $cronTag;
58
+
59
+	/**
60
+	 * Absolute path to a cache directory.
61
+	 *
62
+	 * @var string|null
63
+	 */
64
+	protected $cacheDirectory;
65
+
66
+	/**
67
+	 * The entity sources.
68
+	 *
69
+	 * Array of sspmod_aggregator2_EntitySource objects.
70
+	 *
71
+	 * @var array
72
+	 */
73
+	protected $sources = [];
74
+
75
+	/**
76
+	 * How long the generated metadata should be valid, as a number of seconds.
77
+	 *
78
+	 * This is used to set the validUntil attribute on the generated EntityDescriptor.
79
+	 *
80
+	 * @var int
81
+	 */
82
+	protected $validLength;
83
+
84
+	/**
85
+	 * Duration we should cache generated metadata.
86
+	 *
87
+	 * @var int
88
+	 */
89
+	protected $cacheGenerated;
90
+
91
+	/**
92
+	 * An array of entity IDs to exclude from the aggregate.
93
+	 *
94
+	 * @var string[]|null
95
+	 */
96
+	protected $excluded;
97
+
98
+	/**
99
+	 * An indexed array of protocols to filter the aggregate by. keys can be any of:
100
+	 *
101
+	 * - urn:oasis:names:tc:SAML:1.1:protocol
102
+	 * - urn:oasis:names:tc:SAML:2.0:protocol
103
+	 *
104
+	 * Values will be true if enabled, false otherwise.
105
+	 *
106
+	 * @var array|null
107
+	 */
108
+	protected $protocols;
109
+
110
+	/**
111
+	 * An array of roles to filter the aggregate by. Keys can be any of:
112
+	 *
113
+	 * - SAML2_XML_md_IDPSSODescriptor
114
+	 * - SAML2_XML_md_SPSSODescriptor
115
+	 * - SAML2_XML_md_AttributeAuthorityDescriptor
116
+	 *
117
+	 * Values will be true if enabled, false otherwise.
118
+	 *
119
+	 * @var array|null
120
+	 */
121
+	protected $roles;
122
+
123
+	/**
124
+	 * The key we should use to sign the metadata.
125
+	 *
126
+	 * @var string|null
127
+	 */
128
+	protected $signKey;
129
+
130
+	/**
131
+	 * The password for the private key.
132
+	 *
133
+	 * @var string|null
134
+	 */
135
+	protected $signKeyPass;
136
+
137
+	/**
138
+	 * The certificate of the key we sign the metadata with.
139
+	 *
140
+	 * @var string|null
141
+	 */
142
+	protected $signCert;
143
+
144
+	/**
145
+	 * The algorithm to use for metadata signing.
146
+	 *
147
+	 * @var string|null
148
+	 */
149
+	protected $signAlg;
150
+
151
+	/**
152
+	 * The CA certificate file that should be used to validate https-connections.
153
+	 *
154
+	 * @var string|null
155
+	 */
156
+	protected $sslCAFile;
157
+
158
+	/**
159
+	 * The cache ID for our generated metadata.
160
+	 *
161
+	 * @var string
162
+	 */
163
+	protected $cacheId;
164
+
165
+	/**
166
+	 * The cache tag for our generated metadata.
167
+	 *
168
+	 * This tag is used to make sure that a config change
169
+	 * invalidates our cached metadata.
170
+	 *
171
+	 * @var string
172
+	 */
173
+	protected $cacheTag;
174
+
175
+	/**
176
+	 * The registration information for our generated metadata.
177
+	 *
178
+	 * @var array
179
+	 */
180
+	protected $regInfo;
181
+
182
+
183
+	/**
184
+	 * Initialize this aggregator.
185
+	 *
186
+	 * @param string $id  The id of this aggregator.
187
+	 * @param \SimpleSAML\Configuration $config  The configuration for this aggregator.
188
+	 */
189
+	protected function __construct($id, Configuration $config)
190
+	{
191
+		assert('is_string($id)');
192
+
193
+		$this->id = $id;
194
+		$this->logLoc = 'aggregator2:'.$this->id.': ';
195
+
196
+		$this->cronTag = $config->getString('cron.tag', null);
197
+
198
+		$this->cacheDirectory = $config->getString('cache.directory', null);
199
+		if ($this->cacheDirectory !== null) {
200
+			$this->cacheDirectory = System::resolvePath($this->cacheDirectory);
201
+		}
202
+
203
+		$this->cacheGenerated = $config->getInteger('cache.generated', null);
204
+		if ($this->cacheGenerated !== null) {
205
+			$this->cacheId = sha1($this->id);
206
+			$this->cacheTag = sha1(serialize($config));
207
+		}
208
+
209
+		// configure entity IDs excluded by default
210
+		$this->excludeEntities($config->getArrayize('exclude', null));
211
+
212
+		// configure filters
213
+		$this->setFilters($config->getArrayize('filter', null));
214
+
215
+		$this->validLength = $config->getInteger('valid.length', 7*24*60*60);
216
+
217
+		$globalConfig = Configuration::getInstance();
218
+		$certDir = $globalConfig->getPathValue('certdir', 'cert/');
219
+
220
+		$signKey = $config->getString('sign.privatekey', null);
221
+		if ($signKey !== null) {
222
+			$signKey = System::resolvePath($signKey, $certDir);
223
+			$this->signKey = @file_get_contents($signKey);
224
+			if ($this->signKey === null) {
225
+				throw new Exception('Unable to load private key from '.var_export($signKey, true));
226
+			}
227
+		}
228
+
229
+		$this->signKeyPass = $config->getString('sign.privatekey_pass', null);
230
+
231
+		$signCert = $config->getString('sign.certificate', null);
232
+		if ($signCert !== null) {
233
+			$signCert = System::resolvePath($signCert, $certDir);
234
+			$this->signCert = @file_get_contents($signCert);
235
+			if ($this->signCert === null) {
236
+				throw new Exception('Unable to load certificate file from '.var_export($signCert, true));
237
+			}
238
+		}
239
+
240
+		$this->signAlg = $config->getString('sign.algorithm', XMLSecurityKey::RSA_SHA1);
241
+		if (!in_array($this->signAlg, self::$SUPPORTED_SIGNATURE_ALGORITHMS)) {
242
+			throw new Exception('Unsupported signature algorithm '.var_export($this->signAlg, true));
243
+		}
244
+
245
+		$this->sslCAFile = $config->getString('ssl.cafile', null);
246
+
247
+		$this->regInfo = $config->getArray('RegistrationInfo', null);
248
+
249
+		$this->initSources($config->getConfigList('sources'));
250
+	}
251
+
252
+
253
+	/**
254
+	 * Populate the sources array.
255
+	 *
256
+	 * This is called from the constructor, and can be overridden in subclasses.
257
+	 *
258
+	 * @param array $sources  The sources as an array of SimpleSAML_Configuration objects.
259
+	 */
260
+	protected function initSources(array $sources)
261
+	{
262
+		foreach ($sources as $source) {
263
+			$this->sources[] = new EntitySource($this, $source);
264
+		}
265
+	}
266
+
267
+
268
+	/**
269
+	 * Return an instance of the aggregator with the given id.
270
+	 *
271
+	 * @param string $id  The id of the aggregator.
272
+	 */
273
+	public static function getAggregator($id)
274
+	{
275
+		assert('is_string($id)');
276
+
277
+		$config = Configuration::getConfig('module_aggregator2.php');
278
+		return new Aggregator($id, $config->getConfigItem($id));
279
+	}
280
+
281
+
282
+	/**
283
+	 * Retrieve the ID of the aggregator.
284
+	 *
285
+	 * @return string  The ID of this aggregator.
286
+	 */
287
+	public function getId()
288
+	{
289
+		return $this->id;
290
+	}
291
+
292
+
293
+	/**
294
+	 * Add an item to the cache.
295
+	 *
296
+	 * @param string $id  The identifier of this data.
297
+	 * @param string $data  The data.
298
+	 * @param int $expires  The timestamp the data expires.
299
+	 * @param string|null $tag  An extra tag that can be used to verify the validity of the cached data.
300
+	 */
301
+	public function addCacheItem($id, $data, $expires, $tag = null)
302
+	{
303
+		assert('is_string($id)');
304
+		assert('is_string($data)');
305
+		assert('is_int($expires)');
306
+		assert('is_null($tag) || is_string($tag)');
307
+
308
+		$cacheFile = $this->cacheDirectory.'/'.$id;
309
+		try {
310
+			System::writeFile($cacheFile, $data);
311
+		} catch (\Exception $e) {
312
+			Logger::warning($this->logLoc.'Unable to write to cache file '.var_export($cacheFile, true));
313
+			return;
314
+		}
315
+
316
+		$expireInfo = (string)$expires;
317
+		if ($tag !== null) {
318
+			$expireInfo .= ':'.$tag;
319
+		}
320
+
321
+		$expireFile = $cacheFile.'.expire';
322
+		try {
323
+			System::writeFile($expireFile, $expireInfo);
324
+		} catch (\Exception $e) {
325
+			Logger::warning($this->logLoc.'Unable to write expiration info to '.var_export($expireFile, true));
326
+		}
327
+	}
328
+
329
+
330
+	/**
331
+	 * Check validity of cached data.
332
+	 *
333
+	 * @param string $id  The identifier of this data.
334
+	 * @param string $tag  The tag that was passed to addCacheItem.
335
+	 * @return bool  TRUE if the data is valid, FALSE if not.
336
+	 */
337
+	public function isCacheValid($id, $tag = null)
338
+	{
339
+		assert('is_string($id)');
340
+		assert('is_null($tag) || is_string($tag)');
341
+
342
+		$cacheFile = $this->cacheDirectory.'/'.$id;
343
+		if (!file_exists($cacheFile)) {
344
+			return false;
345
+		}
346
+
347
+		$expireFile = $cacheFile.'.expire';
348
+		if (!file_exists($expireFile)) {
349
+			return false;
350
+		}
351
+
352
+		$expireData = @file_get_contents($expireFile);
353
+		if ($expireData === false) {
354
+			return false;
355
+		}
356
+
357
+		$expireData = explode(':', $expireData, 2);
358
+
359
+		$expireTime = intval($expireData[0]);
360
+		if ($expireTime <= time()) {
361
+			return false;
362
+		}
363
+
364
+		if (count($expireData) === 1) {
365
+			$expireTag = null;
366
+		} else {
367
+			$expireTag = $expireData[1];
368
+		}
369
+		if ($expireTag !== $tag) {
370
+			return false;
371
+		}
372
+
373
+		return true;
374
+	}
375
+
376
+
377
+	/**
378
+	 * Get the cache item.
379
+	 *
380
+	 * @param string $id  The identifier of this data.
381
+	 * @param string $tag  The tag that was passed to addCacheItem.
382
+	 * @return string|null  The cache item, or NULL if it isn't cached or if it is expired.
383
+	 */
384
+	public function getCacheItem($id, $tag = null)
385
+	{
386
+		assert('is_string($id)');
387
+		assert('is_null($tag) || is_string($tag)');
388
+
389
+		if (!$this->isCacheValid($id, $tag)) {
390
+			return null;
391
+		}
392
+
393
+		$cacheFile = $this->cacheDirectory.'/'.$id;
394
+		return @file_get_contents($cacheFile);
395
+	}
396
+
397
+
398
+	/**
399
+	 * Get the cache filename for the specific id.
400
+	 *
401
+	 * @param string $id  The identifier of the cached data.
402
+	 * @return string|null  The filename, or NULL if the cache file doesn't exist.
403
+	 */
404
+	public function getCacheFile($id)
405
+	{
406
+		assert('is_string($id)');
407
+
408
+		$cacheFile = $this->cacheDirectory.'/'.$id;
409
+		if (!file_exists($cacheFile)) {
410
+			return null;
411
+		}
412
+
413
+		return $cacheFile;
414
+	}
415
+
416
+
417
+	/**
418
+	 * Retrieve the SSL CA file path, if it is set.
419
+	 *
420
+	 * @return string|null  The SSL CA file path.
421
+	 */
422
+	public function getCAFile()
423
+	{
424
+		return $this->sslCAFile;
425
+	}
426
+
427
+
428
+	/**
429
+	 * Sign the generated EntitiesDescriptor.
430
+	 */
431
+	protected function addSignature(SignedElement $element)
432
+	{
433
+		if ($this->signKey === null) {
434
+			return;
435
+		}
436
+
437
+		$privateKey = new XMLSecurityKey($this->signAlg, ['type' => 'private']);
438
+		if ($this->signKeyPass !== null) {
439
+			$privateKey->passphrase = $this->signKeyPass;
440
+		}
441
+		$privateKey->loadKey($this->signKey, false);
442
+
443
+		$element->setSignatureKey($privateKey);
444
+
445
+		if ($this->signCert !== null) {
446
+			$element->setCertificates([$this->signCert]);
447
+		}
448
+	}
449
+
450
+
451
+	/**
452
+	 * Recursively browse the children of an EntitiesDescriptor element looking for EntityDescriptor elements, and
453
+	 * return an array containing all of them.
454
+	 *
455
+	 * @param \SAML2\XML\md\EntitiesDescriptor $entity The source EntitiesDescriptor that holds the entities to extract.
456
+	 *
457
+	 * @return array An array containing all the EntityDescriptors found.
458
+	 */
459
+	private static function extractEntityDescriptors($entity)
460
+	{
461
+		assert('$entity instanceof EntitiesDescriptor');
462
+
463
+		if (!($entity instanceof EntitiesDescriptor)) {
464
+			return [];
465
+		}
466
+
467
+		$results = [];
468
+		foreach ($entity->children as $child) {
469
+			if ($child instanceof EntityDescriptor) {
470
+				$results[] = $child;
471
+				continue;
472
+			}
473
+
474
+			$results = array_merge($results, self::extractEntityDescriptors($child));
475
+		}
476
+		return $results;
477
+	}
478
+
479
+
480
+	/**
481
+	 * Retrieve all entities as an EntitiesDescriptor.
482
+	 *
483
+	 * @return \SAML2\XML\md\EntitiesDescriptor  The entities.
484
+	 */
485
+	protected function getEntitiesDescriptor()
486
+	{
487
+		$ret = new EntitiesDescriptor();
488
+		$now = time();
489
+
490
+		// add RegistrationInfo extension if enabled
491
+		if ($this->regInfo !== null) {
492
+			$ri = new RegistrationInfo();
493
+			$ri->registrationInstant = $now;
494
+			foreach ($this->regInfo as $riName => $riValues) {
495
+				switch ($riName) {
496
+					case 'authority':
497
+						$ri->registrationAuthority = $riValues;
498
+						break;
499
+					case 'instant':
500
+						$ri->registrationInstant = Utils::xsDateTimeToTimestamp($riValues);
501
+						break;
502
+					case 'policies':
503
+						$ri->RegistrationPolicy = $riValues;
504
+						break;
505
+				}
506
+			}
507
+			$ret->Extensions[] = $ri;
508
+		}
509
+
510
+		foreach ($this->sources as $source) {
511
+			$m = $source->getMetadata();
512
+			if ($m === NULL) {
513
+				continue;
514
+			}
515
+			if ($m instanceof EntityDescriptor) {
516
+				$ret->children[] = $m;
517
+			} elseif ($m instanceof EntitiesDescriptor) {
518
+				$ret->children = array_merge($ret->children, self::extractEntityDescriptors($m));
519
+			}
520
+		}
521
+
522
+		$ret->children = array_unique($ret->children, SORT_REGULAR);
523
+		$ret->validUntil = $now + $this->validLength;
524
+
525
+		return $ret;
526
+	}
527
+
528
+
529
+	/**
530
+	 * Recursively traverse the children of an EntitiesDescriptor, removing those entities listed in the $entities
531
+	 * property. Returns the EntitiesDescriptor with the entities filtered out.
532
+	 *
533
+	 * @param \SAML2\XML\md\EntitiesDescriptor $descriptor The EntitiesDescriptor from where to exclude entities.
534
+	 *
535
+	 * @return \SAML2\XML\md\EntitiesDescriptor The EntitiesDescriptor with excluded entities filtered out.
536
+	 */
537
+	protected function exclude(EntitiesDescriptor $descriptor)
538
+	{
539
+		if (empty($this->excluded)) {
540
+			return $descriptor;
541
+		}
542
+
543
+		$filtered = [];
544
+		foreach ($descriptor->children as $child) {
545
+			if ($child instanceof EntityDescriptor) {
546
+				if (in_array($child->entityID, $this->excluded)) {
547
+					continue;
548
+				}
549
+				$filtered[] = $child;
550
+			}
551
+
552
+			if ($child instanceof EntitiesDescriptor) {
553
+				$filtered[] = $this->exclude($child);
554
+			}
555
+		}
556
+
557
+		$descriptor->children = $filtered;
558
+		return $descriptor;
559
+	}
560
+
561
+
562
+	/**
563
+	 * Recursively traverse the children of an EntitiesDescriptor, keeping only those entities with the roles listed in
564
+	 * the $roles property, and support for the protocols listed in the $protocols property. Returns the
565
+	 * EntitiesDescriptor containing only those entities.
566
+	 *
567
+	 * @param \SAML2\XML\md\EntitiesDescriptor $descriptor The EntitiesDescriptor to filter.
568
+	 *
569
+	 * @return SAML2_XML_md_EntitiesDescriptor The EntitiesDescriptor with only the entities filtered.
570
+	 */
571
+	protected function filter(EntitiesDescriptor $descriptor)
572
+	{
573
+		if ($this->roles === null || $this->protocols === null) {
574
+			return $descriptor;
575
+		}
576
+
577
+		$enabled_roles = array_keys($this->roles, true);
578
+		$enabled_protos = array_keys($this->protocols, true);
579
+
580
+		$filtered = [];
581
+		foreach ($descriptor->children as $child) {
582
+			if ($child instanceof EntityDescriptor) {
583
+				foreach ($child->RoleDescriptor as $role) {
584
+					if (in_array(get_class($role), $enabled_roles)) {
585
+						// we found a role descriptor that is enabled by our filters, check protocols
586
+						if (array_intersect($enabled_protos, $role->protocolSupportEnumeration) !== []) {
587
+							// it supports some protocol we have enabled, add it
588
+							$filtered[] = $child;
589
+							break;
590
+						}
591
+					}
592
+				}
593
+
594
+			}
595
+
596
+			if ($child instanceof EntitiesDescriptor) {
597
+				$filtered[] = $this->filter($child);
598
+			}
599
+		}
600
+
601
+		$descriptor->children = $filtered;
602
+		return $descriptor;
603
+	}
604
+
605
+
606
+	/**
607
+	 * Set this aggregator to exclude a set of entities from the resulting aggregate.
608
+	 *
609
+	 * @param array|null $entities The entity IDs of the entities to exclude.
610
+	 */
611
+	public function excludeEntities($entities)
612
+	{
613
+		assert('is_array($entities) || is_null($entities)');
614
+
615
+		if ($entities === null) {
616
+			return;
617
+		}
618
+		$this->excluded = $entities;
619
+		sort($this->excluded);
620
+		$this->cacheId = sha1($this->cacheId.serialize($this->excluded));
621
+	}
622
+
623
+
624
+	/**
625
+	 * Set the internal filters according to one or more options:
626
+	 *
627
+	 * - 'saml2': all SAML2.0-capable entities.
628
+	 * - 'shib13': all SHIB1.3-capable entities.
629
+	 * - 'saml20-idp': all SAML2.0-capable identity providers.
630
+	 * - 'saml20-sp': all SAML2.0-capable service providers.
631
+	 * - 'saml20-aa': all SAML2.0-capable attribute authorities.
632
+	 * - 'shib13-idp': all SHIB1.3-capable identity providers.
633
+	 * - 'shib13-sp': all SHIB1.3-capable service providers.
634
+	 * - 'shib13-aa': all SHIB1.3-capable attribute authorities.
635
+	 *
636
+	 * @param array|null $set An array of the different roles and protocols to filter by.
637
+	 */
638
+	public function setFilters($set)
639
+	{
640
+		assert('is_array($set) || is_null($set)');
641
+
642
+		if ($set === null) {
643
+			return;
644
+		}
645
+
646
+		// configure filters
647
+		$this->protocols = [
648
+			Constants::NS_SAMLP                    => true,
649
+			'urn:oasis:names:tc:SAML:1.1:protocol' => true,
650
+		];
651
+		$this->roles = [
652
+			'SAML2_XML_md_IDPSSODescriptor'             => true,
653
+			'SAML2_XML_md_SPSSODescriptor'              => true,
654
+			'SAML2_XML_md_AttributeAuthorityDescriptor' => true,
655
+		];
656
+
657
+		// now translate from the options we have, to specific protocols and roles
658
+
659
+		// check SAML 2.0 protocol
660
+		$options = ['saml2', 'saml20-idp', 'saml20-sp', 'saml20-aa'];
661
+		$this->protocols[Constants::NS_SAMLP] = (array_intersect($set, $options) !== []);
662
+
663
+		// check SHIB 1.3 protocol
664
+		$options = ['shib13', 'shib13-idp', 'shib13-sp', 'shib13-aa'];
665
+		$this->protocols['urn:oasis:names:tc:SAML:1.1:protocol'] = (array_intersect($set, $options) !== []);
666
+
667
+		// check IdP
668
+		$options = ['saml2', 'shib13', 'saml20-idp', 'shib13-idp'];
669
+		$this->roles['SAML2_XML_md_IDPSSODescriptor'] = (array_intersect($set, $options) !== []);
670
+
671
+		// check SP
672
+		$options = ['saml2', 'shib13', 'saml20-sp', 'shib13-sp'];
673
+		$this->roles['SAML2_XML_md_SPSSODescriptor'] = (array_intersect($set, $options) !== []);
674
+
675
+		// check AA
676
+		$options = ['saml2', 'shib13', 'saml20-aa', 'shib13-aa'];
677
+		$this->roles['SAML2_XML_md_AttributeAuthorityDescriptor'] = (array_intersect($set, $options) !== []);
678
+
679
+		$this->cacheId = sha1($this->cacheId.serialize($this->protocols).serialize($this->roles));
680
+	}
681
+
682
+
683
+	/**
684
+	 * Retrieve the complete, signed metadata as text.
685
+	 *
686
+	 * This function will write the new metadata to the cache file, but will not return
687
+	 * the cached metadata.
688
+	 *
689
+	 * @return string  The metadata, as text.
690
+	 */
691
+	public function updateCachedMetadata()
692
+	{
693
+		$ed = $this->getEntitiesDescriptor();
694
+		$ed = $this->exclude($ed);
695
+		$ed = $this->filter($ed);
696
+		$this->addSignature($ed);
697
+
698
+		$xml = $ed->toXML();
699
+		$xml = $xml->ownerDocument->saveXML($xml);
700
+
701
+		if ($this->cacheGenerated !== null) {
702
+			Logger::debug($this->logLoc.'Saving generated metadata to cache.');
703
+			$this->addCacheItem($this->cacheId, $xml, time() + $this->cacheGenerated, $this->cacheTag);
704
+		}
705
+
706
+		return $xml;
707
+	}
708
+
709
+
710
+	/**
711
+	 * Retrieve the complete, signed metadata as text.
712
+	 *
713
+	 * @return string  The metadata, as text.
714
+	 */
715
+	public function getMetadata()
716
+	{
717
+		if ($this->cacheGenerated !== null) {
718
+			$xml = $this->getCacheItem($this->cacheId, $this->cacheTag);
719
+			if ($xml !== null) {
720
+				Logger::debug($this->logLoc.'Loaded generated metadata from cache.');
721
+				return $xml;
722
+			}
723
+		}
724
+
725
+		return $this->updateCachedMetadata();
726
+	}
727
+
728
+
729
+	/**
730
+	 * Update the cached copy of our metadata.
731
+	 */
732
+	public function updateCache()
733
+	{
734
+		foreach ($this->sources as $source) {
735
+			$source->updateCache();
736
+		}
737
+
738
+		$this->updateCachedMetadata();
739
+	}
740 740
 }
Please login to merge, or discard this patch.