-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTrustChainResolver.php
More file actions
351 lines (310 loc) · 15.1 KB
/
Copy pathTrustChainResolver.php
File metadata and controls
351 lines (310 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
<?php
declare(strict_types=1);
namespace SimpleSAML\OpenID\Federation;
use Psr\Log\LoggerInterface;
use SimpleSAML\OpenID\Decorators\CacheDecorator;
use SimpleSAML\OpenID\Decorators\DateIntervalDecorator;
use SimpleSAML\OpenID\Exceptions\TrustChainException;
use SimpleSAML\OpenID\Federation\Factories\TrustChainBagFactory;
use SimpleSAML\OpenID\Federation\Factories\TrustChainFactory;
use Throwable;
class TrustChainResolver
{
protected int $maxTrustChainDepth;
protected int $maxAuthorityHints;
public function __construct(
protected readonly EntityStatementFetcher $entityStatementFetcher,
protected readonly TrustChainFactory $trustChainFactory,
protected readonly TrustChainBagFactory $trustChainBagFactory,
protected readonly DateIntervalDecorator $maxCacheDurationDecorator,
protected readonly ?CacheDecorator $cacheDecorator = null,
protected readonly ?LoggerInterface $logger = null,
int $maxTrustChainDepth = 10,
int $maxAuthorityHints = 6,
) {
$this->maxTrustChainDepth = min(20, max(1, $maxTrustChainDepth));
$this->maxAuthorityHints = min(12, max(1, $maxAuthorityHints));
}
/**
* Get entity configuration statements chains up to given Trust Anchors.
*
* @param non-empty-string $entityId
* @param non-empty-array<non-empty-string> $trustAnchorIds
* @param \SimpleSAML\OpenID\Federation\EntityStatement[] $populatedChain Recursively populated with configuration
* entity statements for one chain path.
* @param int $depth Recursively defined chain depth.
* @return array<array<non-empty-string,\SimpleSAML\OpenID\Federation\EntityStatement>>
*/
public function getConfigurationChains(
string $entityId,
array $trustAnchorIds,
array $populatedChain = [],
int $depth = 1,
): array {
$populatedChainEntityIds = array_keys($populatedChain);
$debugStartInfo = [
'depth' => $depth,
'entityId' => $entityId,
'trustAnchorIds' => $trustAnchorIds,
'populatedChainEntityIds' => $populatedChainEntityIds,
];
$this->logger?->debug('Start getting configuration chains.', $debugStartInfo);
$configurationChains = [];
try {
$this->validateStart($entityId, $trustAnchorIds);
} catch (Throwable $throwable) {
$this->logger?->error(
'Error validating configuration chain fetch start condition: ' . $throwable->getMessage(),
$debugStartInfo,
);
return $configurationChains;
}
// Check for maximum allowed depth.
if ($depth > $this->getMaxTrustChainDepth()) {
$this->logger?->error(
'Maximum allowed depth reached while getting configuration chains.',
$debugStartInfo,
);
return $configurationChains;
}
// Avoid cycles, and possibility for entities declaring authority over themselves.
if (in_array($entityId, $populatedChainEntityIds)) {
$this->logger?->error(
'Possible loop detected. Duplicate configuration in chain path encountered, disregarding path.',
$debugStartInfo,
);
return $configurationChains;
}
try {
$this->logger?->debug('Fetching entity configuration statement.', $debugStartInfo);
$entityConfig = $this->entityStatementFetcher->fromCacheOrWellKnownEndpoint($entityId);
$this->logger?->debug(
'Fetched entity configuration statement.',
[...$debugStartInfo, 'entityConfigPayload' => $entityConfig->getPayload(),],
);
} catch (Throwable $throwable) {
$this->logger?->error(
'Unable to fetch entity configuration statement, error was: ' . $throwable->getMessage(),
$debugStartInfo,
);
return $configurationChains;
}
if (in_array($entityId, $trustAnchorIds, true)) {
$this->logger?->info(
'Common trust anchor found: ' . $entityId,
$debugStartInfo,
);
/** @var array<non-empty-string, \SimpleSAML\OpenID\Federation\EntityStatement> $fullConfigChain */
$fullConfigChain = array_merge($populatedChain, [$entityId => $entityConfig]);
$configurationChains[] = $fullConfigChain;
$this->logger?->debug(
'Returning configuration chain.',
[...$debugStartInfo, 'returnedConfigChainEntityIds' => array_keys($fullConfigChain),],
);
return $configurationChains;
}
try {
$entityAuthorityHints = $entityConfig->getAuthorityHints();
if ((!is_array($entityAuthorityHints)) || $entityAuthorityHints === []) {
$this->logger?->info('No common trust anchor in this path.', $debugStartInfo);
return $configurationChains;
}
$this->logger?->debug(
'There are more authority hints to process on this path.',
[$debugStartInfo, 'entityAuthorityHints' => $entityAuthorityHints],
);
if (
($entityAuthorityHintsCount = count($entityAuthorityHints)) >
$this->maxAuthorityHints
) {
$message = sprintf(
'Encountered %s entity authority hints, while max %s is allowed, stopping with this path.',
$entityAuthorityHintsCount,
$this->maxAuthorityHints,
);
$this->logger?->error($message, $debugStartInfo);
return $configurationChains;
}
foreach ($entityAuthorityHints as $authorityHint) {
/** @var array<non-empty-string, \SimpleSAML\OpenID\Federation\EntityStatement> $currentPath */
$currentPath = array_merge($populatedChain, [$entityId => $entityConfig]);
$configurationChains = array_merge(
$configurationChains,
$this->getConfigurationChains($authorityHint, $trustAnchorIds, $currentPath, $depth + 1),
);
}
} catch (Throwable $throwable) {
$this->logger?->error(
'Unable to handle entity authority hints, error was: ' . $throwable->getMessage(),
$debugStartInfo,
);
}
return $configurationChains;
}
/**
* Resolve trust chains for given entity and trust anchor IDs.
*
* @param non-empty-string $entityId ID of the leaf (subject) entity for which to resolve the trust chain.
* @param non-empty-array<non-empty-string> $validTrustAnchorIds IDs of the valid trust anchors.
*
* @throws \SimpleSAML\OpenID\Exceptions\TrustChainException
*/
public function for(string $entityId, array $validTrustAnchorIds): TrustChainBag
{
$this->validateStart($entityId, $validTrustAnchorIds);
$debugStartInfo = ['entityId' => $entityId, 'validTrustAnchorIds' => $validTrustAnchorIds];
$this->logger?->debug('Trust chain resolving started.', $debugStartInfo);
$resolvedChains = [];
foreach ($validTrustAnchorIds as $index => $validTrustAnchorId) {
$debugCacheQueryInfo = ['entityId' => $entityId, 'validTrustAnchorId' => $validTrustAnchorId];
$this->logger?->debug('Checking if the trust chain exists in cache.', $debugCacheQueryInfo);
try {
/** @var ?string[] $tokens */
$tokens = $this->cacheDecorator?->get(null, $entityId, $validTrustAnchorId);
if (is_array($tokens)) {
$this->logger?->debug(
'Found trust chain tokens in cache, using it to build trust chain.',
[...$debugCacheQueryInfo, 'tokens' => $tokens],
);
$resolvedChains[] = $this->trustChainFactory->fromTokens(...$tokens);
// Unset it as valid trust anchor ID so that it is not taken into account at regular resolving.
unset($validTrustAnchorIds[$index]);
continue;
}
$this->logger?->debug('Trust chain does not exist in cache.', $debugCacheQueryInfo);
} catch (Throwable $exception) {
$this->logger?->warning(
'Error while trying to get trust chain from cache: ' . $exception->getMessage(),
$debugCacheQueryInfo,
);
}
}
if ($validTrustAnchorIds !== []) {
$debugStandardResolveInfo = ['entityId' => $entityId, 'validTrustAnchorIds' => $validTrustAnchorIds];
$this->logger?->debug(
'Continuing with standard resolving for remaining valid trust anchor IDs.',
['entityId' => $entityId, 'validTrustAnchorIds' => $validTrustAnchorIds],
);
$this->logger?->debug('Start fetching all configuration chains.', $debugStandardResolveInfo);
$configurationChains = $this->getConfigurationChains($entityId, $validTrustAnchorIds);
$this->logger?->debug(
sprintf('Fetched %s configuration chains.', count($configurationChains)),
$debugStandardResolveInfo,
);
foreach ($configurationChains as $configurationChain) {
$debugConfigChainResolveInfo = [
...$debugStandardResolveInfo,
'configurationChainEntityIds' => array_keys($configurationChain),
];
$this->logger?->debug('Start resolving for configuration chain.', $debugConfigChainResolveInfo);
try {
// If we only have one element in the configuration chain, check if we are dealing with the
// Trust Chain for Trust Anchor itself.
if (
(count($configurationChain) === 1) &&
(array_key_first($configurationChain) === $entityId)
) {
// Handle the special Trust Anchor Trust Chain case.
$trustAnchorStatement = current($configurationChain);
$resolvedChains[] = $this->trustChainFactory->forTrustAnchor($trustAnchorStatement);
} else {
// Handle normal Trust Chain resolution.
// Reverse order so we can start from the Trust Anchor.
$configurationChain = array_reverse($configurationChain);
$currenChainElements = [];
$previousEntity = null;
foreach ($configurationChain as $id => $configurationStatement) {
if (array_key_first($configurationChain) === $id) {
// This is Trust Anchor configuration statement, we need to add it.
array_unshift($currenChainElements, $configurationStatement);
} elseif (!is_null($previousEntity)) {
// We have moved on from the first configuration entity in the chain, so we need to
// start populating subordinate statements.
array_unshift(
$currenChainElements,
$this->entityStatementFetcher->fromCacheOrFetchEndpoint($id, $previousEntity),
);
}
// We need to add leaf entity configuration statement as the last item in the trust chain.
if (array_key_last($configurationChain) === $id) {
array_unshift($currenChainElements, $configurationStatement);
}
$previousEntity = $configurationStatement;
}
$resolvedChains[] = $this->trustChainFactory->fromStatements(...$currenChainElements);
}
} catch (Throwable $exception) {
$this->logger?->error(
sprintf(
'Error resolving trust chain from configuration chain, skipping. Error was: %s',
$exception->getMessage(),
),
$debugConfigChainResolveInfo,
);
continue;
}
}
}
if ($resolvedChains === []) {
$message = 'Could not resolve trust chains or no common trust anchors found.';
$this->logger?->error($message, $debugStartInfo);
throw new TrustChainException($message);
}
$this->logger?->debug(
sprintf('Found %s trust chains, building its bag.', count($resolvedChains)),
$debugStartInfo,
);
try {
$trustChainBag = $this->trustChainBagFactory->build($this->cacheTrustChain(array_pop($resolvedChains)));
while ($trustChain = array_pop($resolvedChains)) {
$trustChainBag->add($this->cacheTrustChain($trustChain));
}
} catch (Throwable $throwable) {
$message = 'Error building Trust Chain Bag: ' . $throwable->getMessage();
$this->logger?->error($message, $debugStartInfo);
throw new TrustChainException($message);
}
return $trustChainBag;
}
/**
* @throws \SimpleSAML\OpenID\Exceptions\TrustChainException
* @phpstan-ignore missingType.iterableValue (We validate it here)
*/
protected function validateStart(string $entityId, array $validTrustAnchorIds): void
{
$errors = [];
if ($entityId === '' || $entityId === '0') {
$errors[] = 'Empty entity ID.';
}
if ($validTrustAnchorIds === []) {
$errors[] = 'No valid Trust Anchors provided.';
}
if ($errors !== []) {
$message = 'Validation errors encountered: ' . implode(', ', $errors);
$this->logger?->error($message);
throw new TrustChainException($message);
}
}
public function getMaxTrustChainDepth(): int
{
return $this->maxTrustChainDepth;
}
/**
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
* @throws \SimpleSAML\OpenID\Exceptions\TrustChainException
*/
public function cacheTrustChain(TrustChain $trustChain): TrustChain
{
$this->cacheDecorator?->set(
$trustChain->jsonSerialize(),
$this->maxCacheDurationDecorator->lowestInSecondsComparedToExpirationTime(
$trustChain->getResolvedExpirationTime(),
),
$trustChain->getResolvedLeaf()->getIssuer(),
$trustChain->getResolvedTrustAnchor()->getIssuer(),
);
return $trustChain;
}
}