Skip to content

Commit 4e6bdaa

Browse files
authored
Add Trust Mark validation capabilities (#278)
* Add Trust Mark validation capabilities * Add federation participation limiting capabilities based on Trust Marks
1 parent b3e55c6 commit 4e6bdaa

File tree

15 files changed

+543
-37
lines changed

15 files changed

+543
-37
lines changed

UPGRADE.md

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
# TODO
2-
- upgrade to v9 of oauth2-server https://github.com/thephpleague/oauth2-server/releases/tag/9.0.0
3-
- implement key rollover
4-
- implement token introspection
5-
- implement dynamic client registration
6-
- move request rules to templates (generics) for proper static type handling
2+
73
- remove dependency on laminas/laminas-httphandlerrunner
84
- create a bridge towards SSP utility classes, so they can be easily mocked
95
- move away from SSP database as store; move to DBAL
@@ -51,6 +47,7 @@ and optionally a port (as in all previous module versions).
5147
- authority hints
5248
- federation caching adapter and its arguments
5349
- PKI keys - federation keys used for example to sign federation entity statements
50+
- federation participation limiting based on Trust Marks for RPs
5451
- signer algorithm
5552
- entity statement duration
5653
- organization name

composer.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,10 @@
8080
},
8181
"scripts": {
8282
"pre-commit": [
83+
"vendor/bin/phpcbf",
84+
"vendor/bin/phpcs -p",
8385
"vendor/bin/psalm",
84-
"vendor/bin/phpcs -p"
86+
"vendor/bin/phpunit"
8587
],
8688
"tests": [
8789
"vendor/bin/phpunit --no-coverage"

routing/routes/routes.php

+3
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@
6363
$routes->add(RoutesEnum::AdminTestTrustChainResolution->name, RoutesEnum::AdminTestTrustChainResolution->value)
6464
->controller([TestController::class, 'trustChainResolution'])
6565
->methods([HttpMethodsEnum::GET->value, HttpMethodsEnum::POST->value]);
66+
$routes->add(RoutesEnum::AdminTestTrustMarkValidation->name, RoutesEnum::AdminTestTrustMarkValidation->value)
67+
->controller([TestController::class, 'trustMarkValidation'])
68+
->methods([HttpMethodsEnum::GET->value, HttpMethodsEnum::POST->value]);
6669

6770
/*****************************************************************************************************************
6871
* OpenID Connect

src/Codebooks/RoutesEnum.php

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ enum RoutesEnum: string
2626

2727
// Testing
2828
case AdminTestTrustChainResolution = 'admin/test/trust-chain-resolution';
29+
case AdminTestTrustMarkValidation = 'admin/test/trust-mark-validation';
2930

3031

3132
/*****************************************************************************************************************

src/Controllers/Admin/TestController.php

+71-10
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
class TestController
2121
{
22+
protected readonly Federation $federationWithArrayLogger;
23+
2224
public function __construct(
2325
protected readonly ModuleConfig $moduleConfig,
2426
protected readonly TemplateFactory $templateFactory,
@@ -28,6 +30,14 @@ public function __construct(
2830
protected readonly ArrayLogger $arrayLogger,
2931
) {
3032
$this->authorization->requireAdmin(true);
33+
34+
$this->arrayLogger->setWeight(ArrayLogger::WEIGHT_WARNING);
35+
// Let's create new Federation instance so we can inject our debug logger and go without cache.
36+
$this->federationWithArrayLogger = new Federation(
37+
supportedAlgorithms: $this->federation->supportedAlgorithms(),
38+
cache: null,
39+
logger: $this->arrayLogger,
40+
);
3141
}
3242

3343
/**
@@ -37,14 +47,6 @@ public function __construct(
3747
*/
3848
public function trustChainResolution(Request $request): Response
3949
{
40-
$this->arrayLogger->setWeight(ArrayLogger::WEIGHT_WARNING);
41-
// Let's create new Federation instance so we can inject our debug logger and go without cache.
42-
$federation = new Federation(
43-
supportedAlgorithms: $this->federation->supportedAlgorithms(),
44-
cache: null,
45-
logger: $this->arrayLogger,
46-
);
47-
4850
$leafEntityId = $this->moduleConfig->getIssuer();
4951
$trustChainBag = null;
5052
$resolvedMetadata = [];
@@ -69,7 +71,8 @@ public function trustChainResolution(Request $request): Response
6971
$trustAnchorIds = $this->helpers->str()->convertTextToArray($rawTrustAnchorIds);
7072

7173
try {
72-
$trustChainBag = $federation->trustChainResolver()->for($leafEntityId, $trustAnchorIds);
74+
$trustChainBag = $this->federationWithArrayLogger->trustChainResolver()
75+
->for($leafEntityId, $trustAnchorIds);
7376

7477
foreach ($trustChainBag->getAll() as $index => $trustChain) {
7578
$metadataEntries = [];
@@ -94,7 +97,7 @@ public function trustChainResolution(Request $request): Response
9497

9598
$trustAnchorIds = implode("\n", $trustAnchorIds);
9699
$logMessages = $this->arrayLogger->getEntries();
97-
//dd($this->arrayLogger->getEntries());
100+
98101
return $this->templateFactory->build(
99102
'oidc:tests/trust-chain-resolution.twig',
100103
compact(
@@ -108,4 +111,62 @@ public function trustChainResolution(Request $request): Response
108111
RoutesEnum::AdminTestTrustChainResolution->value,
109112
);
110113
}
114+
115+
public function trustMarkValidation(Request $request): Response
116+
{
117+
$trustMarkId = null;
118+
$leafEntityId = null;
119+
$trustAnchorId = null;
120+
$isFormSubmitted = false;
121+
122+
if ($request->isMethod(Request::METHOD_POST)) {
123+
$isFormSubmitted = true;
124+
125+
!empty($trustMarkId = $request->request->getString('trustMarkId')) ||
126+
throw new OidcException('Empty Trust Mark ID.');
127+
!empty($leafEntityId = $request->request->getString('leafEntityId')) ||
128+
throw new OidcException('Empty leaf entity ID.');
129+
!empty($trustAnchorId = $request->request->getString('trustAnchorId')) ||
130+
throw new OidcException('Empty Trust Anchor ID.');
131+
132+
try {
133+
// We should not try to validate Trust Marks until we have resolved trust chain between leaf and TA.
134+
$trustChain = $this->federation->trustChainResolver()->for(
135+
$leafEntityId,
136+
[$trustAnchorId],
137+
)->getShortest();
138+
139+
try {
140+
$this->federationWithArrayLogger->trustMarkValidator()->doForTrustMarkId(
141+
$trustMarkId,
142+
$trustChain->getResolvedLeaf(),
143+
$trustChain->getResolvedTrustAnchor(),
144+
);
145+
} catch (\Throwable $exception) {
146+
$this->arrayLogger->error('Trust Mark validation error: ' . $exception->getMessage());
147+
}
148+
} catch (TrustChainException $exception) {
149+
$this->arrayLogger->error(sprintf(
150+
'Could not resolve Trust Chain for leaf entity %s under Trust Anchor %s. Error was %s',
151+
$leafEntityId,
152+
$trustAnchorId,
153+
$exception->getMessage(),
154+
));
155+
}
156+
}
157+
158+
$logMessages = $this->arrayLogger->getEntries();
159+
160+
return $this->templateFactory->build(
161+
'oidc:tests/trust-mark-validation.twig',
162+
compact(
163+
'trustMarkId',
164+
'leafEntityId',
165+
'trustAnchorId',
166+
'logMessages',
167+
'isFormSubmitted',
168+
),
169+
RoutesEnum::AdminTestTrustMarkValidation->value,
170+
);
171+
}
111172
}

src/Controllers/Federation/Test.php

+28-7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use SimpleSAML\Module\oidc\Factories\Entities\ClientEntityFactory;
1313
use SimpleSAML\Module\oidc\Services\LoggerService;
1414
use SimpleSAML\Module\oidc\Utils\FederationCache;
15+
use SimpleSAML\Module\oidc\Utils\FederationParticipationValidator;
1516
use SimpleSAML\Module\oidc\Utils\ProtocolCache;
1617
use SimpleSAML\OpenID\Codebooks\EntityTypesEnum;
1718
use SimpleSAML\OpenID\Core;
@@ -37,6 +38,7 @@ public function __construct(
3738
protected Database $database,
3839
protected ClientEntityFactory $clientEntityFactory,
3940
protected CoreFactory $coreFactory,
41+
protected FederationParticipationValidator $federationParticipationValidator,
4042
protected \DateInterval $maxCacheDuration = new \DateInterval('PT30S'),
4143
) {
4244
}
@@ -70,21 +72,40 @@ public function __invoke(): Response
7072
$trustChain = $this->federation
7173
->trustChainResolver()
7274
->for(
73-
// 'https://08-dap.localhost.markoivancic.from.hr/openid/entities/ALeaf/',
75+
'https://08-dap.localhost.markoivancic.from.hr/openid/entities/ALeaf/',
7476
// 'https://trust-anchor.testbed.oidcfed.incubator.geant.org/oidc/rp/',
7577
// 'https://relying-party-php.testbed.oidcfed.incubator.geant.org/',
76-
'https://gorp.testbed.oidcfed.incubator.geant.org',
78+
// 'https://gorp.testbed.oidcfed.incubator.geant.org',
7779
// 'https://maiv1.incubator.geant.org',
7880
[
79-
'https://trust-anchor.testbed.oidcfed.incubator.geant.org/',
81+
// 'https://trust-anchor.testbed.oidcfed.incubator.geant.org/',
8082
'https://08-dap.localhost.markoivancic.from.hr/openid/entities/ABTrustAnchor/',
81-
'https://08-dap.localhost.markoivancic.from.hr/openid/entities/CTrustAnchor/',
83+
// 'https://08-dap.localhost.markoivancic.from.hr/openid/entities/CTrustAnchor/',
8284
],
83-
)->getAll();
84-
dd($trustChain);
85+
)
86+
//->getAll();
87+
->getShortestByTrustAnchorPriority(
88+
'https://08-dap.localhost.markoivancic.from.hr/openid/entities/ABTrustAnchor/',
89+
);
90+
8591
$leaf = $trustChain->getResolvedLeaf();
92+
$trustAnchor = $trustChain->getResolvedTrustAnchor();
93+
94+
$this->federationParticipationValidator->validateForAllOfLimit(
95+
['https://08-dap.localhost.markoivancic.from.hr/openid/entities/ATrustMarkIssuer/trust-mark/member'],
96+
$leaf,
97+
$trustAnchor,
98+
);
99+
86100
dd($leaf->getPayload());
87-
$leafFederationJwks = $leaf->getJwks();
101+
102+
$this->federation->trustMarkValidator()->fromCacheOrDoForTrustMarkId(
103+
'https://08-dap.localhost.markoivancic.from.hr/openid/entities/ATrustMarkIssuer/trust-mark/member',
104+
$leaf,
105+
$trustAnchor,
106+
);
107+
108+
// $leafFederationJwks = $leaf->getJwks();
88109
// dd($leafFederationJwks);
89110
// /** @psalm-suppress PossiblyNullArgument */
90111
$resolvedMetadata = $trustChain->getResolvedMetadata(EntityTypesEnum::OpenIdRelyingParty);

src/Factories/TemplateFactory.php

+7
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,13 @@ protected function includeDefaultMenuItems(): void
134134
Translate::noop('Test Trust Chain Resolution'),
135135
),
136136
);
137+
138+
$this->oidcMenu->addItem(
139+
$this->oidcMenu->buildItem(
140+
$this->moduleConfig->getModuleUrl(RoutesEnum::AdminTestTrustMarkValidation->value),
141+
Translate::noop('Test Trust Mark Validation'),
142+
),
143+
);
137144
}
138145

139146
public function setShowMenu(bool $showMenu): TemplateFactory

src/Server/RequestRules/Rules/ClientIdRule.php

+14-2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ public function __construct(
4848

4949
/**
5050
* @inheritDoc
51+
* @throws \JsonException
52+
* @throws \League\OAuth2\Server\Exception\OAuthServerException
53+
* @throws \Psr\SimpleCache\InvalidArgumentException
54+
* @throws \SimpleSAML\Error\ConfigurationError
55+
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException
56+
* @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException
57+
* @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException
58+
* @throws \SimpleSAML\OpenID\Exceptions\JwksException
59+
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
60+
* @throws \SimpleSAML\OpenID\Exceptions\RequestObjectException
61+
* @throws \SimpleSAML\OpenID\Exceptions\TrustChainException
62+
* @throws \SimpleSAML\OpenID\Exceptions\TrustMarkException
5163
*/
5264
public function checkRule(
5365
ServerRequestInterface $request,
@@ -196,7 +208,7 @@ public function checkRule(
196208
$this->helpers->dateTime()->getFromTimestamp($trustChain->getResolvedExpirationTime()),
197209
$existingClient,
198210
$clientEntityId,
199-
$clientFederationEntity->getJwks(),
211+
$clientFederationEntity->getJwks()->getValue(),
200212
$request,
201213
);
202214

@@ -209,7 +221,7 @@ public function checkRule(
209221
// Check if federation participation is limited by Trust Marks.
210222
if (
211223
$this->moduleConfig->isFederationParticipationLimitedByTrustMarksFor(
212-
$trustChain->getResolvedTrustAnchor()->getIssuer(),
224+
$trustAnchorEntityConfiguration->getIssuer(),
213225
)
214226
) {
215227
$this->federationParticipationValidator->byTrustMarksFor($trustChain);

src/Server/Validators/BearerTokenValidator.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ protected function initJwtConfiguration(): void
7676
InMemory::plainText('empty', 'empty'),
7777
);
7878

79-
/** @psalm-suppress ArgumentTypeCoercion */
79+
/** @psalm-suppress DeprecatedMethod, ArgumentTypeCoercion */
8080
$this->jwtConfiguration->setValidationConstraints(
8181
new StrictValidAt(new SystemClock(new DateTimeZone(date_default_timezone_get()))),
8282
new SignedWith(

src/Services/Container.php

+1
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ public function __construct()
349349
$this->services[JwksResolver::class] = $jwksResolver;
350350
$federationParticipationValidator = new FederationParticipationValidator(
351351
$moduleConfig,
352+
$federation,
352353
$loggerService,
353354
);
354355
$this->services[FederationParticipationValidator::class] = $federationParticipationValidator;

0 commit comments

Comments
 (0)