From 02eb59f6504ce9b6ebf4b287607dcf944c114307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Mon, 3 Nov 2025 14:41:37 +0100 Subject: [PATCH 1/2] Introduce TrustMarkIssuers claim --- rector.php | 2 +- src/Codebooks/ClaimsEnum.php | 1 + .../Claims/TrustMarkIssuersClaimBag.php | 64 ++++++++++++++++ .../Claims/TrustMarkIssuersClaimValue.php | 49 +++++++++++++ src/Federation/EntityStatement.php | 21 ++++++ .../Factories/FederationClaimFactory.php | 46 ++++++++++++ .../Factories/TrustChainFactory.php | 2 +- src/Federation/TrustMarkValidator.php | 2 +- src/Jws/ParsedJws.php | 2 +- .../Claims/TrustMarkIssuersClaimBagTest.php | 73 +++++++++++++++++++ .../Claims/TrustMarkIssuersClaimValueTest.php | 62 ++++++++++++++++ tests/src/Federation/EntityStatementTest.php | 16 ++++ .../Factories/FederationClaimFactoryTest.php | 17 +++++ 13 files changed, 353 insertions(+), 4 deletions(-) create mode 100644 src/Federation/Claims/TrustMarkIssuersClaimBag.php create mode 100644 src/Federation/Claims/TrustMarkIssuersClaimValue.php create mode 100644 tests/src/Federation/Claims/TrustMarkIssuersClaimBagTest.php create mode 100644 tests/src/Federation/Claims/TrustMarkIssuersClaimValueTest.php diff --git a/rector.php b/rector.php index 584c8de..2e0a9db 100644 --- a/rector.php +++ b/rector.php @@ -20,7 +20,7 @@ // naming: true, instanceOf: true, earlyReturn: true, - strictBooleans: true, +// strictBooleans: true, // carbon: true, rectorPreset: true, phpunitCodeQuality: true, diff --git a/src/Codebooks/ClaimsEnum.php b/src/Codebooks/ClaimsEnum.php index b47f391..93eb8be 100644 --- a/src/Codebooks/ClaimsEnum.php +++ b/src/Codebooks/ClaimsEnum.php @@ -143,6 +143,7 @@ enum ClaimsEnum: string case Type = 'type'; case TrustChain = 'trust_chain'; case TrustMark = 'trust_mark'; + case TrustMarkIssuers = 'trust_mark_issuers'; case TrustMarkOwners = 'trust_mark_owners'; case TrustMarkType = 'trust_mark_type'; case TrustMarks = 'trust_marks'; diff --git a/src/Federation/Claims/TrustMarkIssuersClaimBag.php b/src/Federation/Claims/TrustMarkIssuersClaimBag.php new file mode 100644 index 0000000..6b8a3c3 --- /dev/null +++ b/src/Federation/Claims/TrustMarkIssuersClaimBag.php @@ -0,0 +1,64 @@ + */ + protected array $trustMarkIssuersClaimValues = []; + + + public function __construct(TrustMarkIssuersClaimValue ...$trustMarkIssuersClaimValues) + { + $this->add(...$trustMarkIssuersClaimValues); + } + + + public function add(TrustMarkIssuersClaimValue ...$trustMarkIssuersClaimValues): void + { + foreach ($trustMarkIssuersClaimValues as $trustMarkIssuersClaimValue) { + $this->trustMarkIssuersClaimValues[$trustMarkIssuersClaimValue->getTrustMarkType()] = + $trustMarkIssuersClaimValue; + } + } + + + public function has(string $trustMarkType): bool + { + return isset($this->trustMarkIssuersClaimValues[$trustMarkType]); + } + + + public function get(string $trustMarkType): ?TrustMarkIssuersClaimValue + { + return $this->trustMarkIssuersClaimValues[$trustMarkType] ?? null; + } + + + /** + * @return \SimpleSAML\OpenID\Federation\Claims\TrustMarkIssuersClaimValue[] + */ + public function getAll(): array + { + return $this->trustMarkIssuersClaimValues; + } + + + /** + * @return array> + */ + public function jsonSerialize(): array + { + return array_combine( + array_keys($this->trustMarkIssuersClaimValues), + array_map( + fn(TrustMarkIssuersClaimValue $tMICValue): array => $tMICValue->getTrustMarkIssuers(), + $this->trustMarkIssuersClaimValues, + ), + ); + } +} diff --git a/src/Federation/Claims/TrustMarkIssuersClaimValue.php b/src/Federation/Claims/TrustMarkIssuersClaimValue.php new file mode 100644 index 0000000..0b51a6c --- /dev/null +++ b/src/Federation/Claims/TrustMarkIssuersClaimValue.php @@ -0,0 +1,49 @@ + $trustMarkIssuers + */ + public function __construct( + protected readonly string $trustMarkType, + protected readonly array $trustMarkIssuers, + ) { + } + + + /** + * @return non-empty-string + */ + public function getTrustMarkType(): string + { + return $this->trustMarkType; + } + + + /** + * @return array + */ + public function getTrustMarkIssuers(): array + { + return $this->trustMarkIssuers; + } + + + /** + * @return array> + */ + public function jsonSerialize(): array + { + return [ + $this->trustMarkType => $this->getTrustMarkIssuers(), + ]; + } +} diff --git a/src/Federation/EntityStatement.php b/src/Federation/EntityStatement.php index 018d7c6..89fa317 100644 --- a/src/Federation/EntityStatement.php +++ b/src/Federation/EntityStatement.php @@ -9,6 +9,7 @@ use SimpleSAML\OpenID\Codebooks\EntityTypesEnum; use SimpleSAML\OpenID\Codebooks\JwtTypesEnum; use SimpleSAML\OpenID\Exceptions\EntityStatementException; +use SimpleSAML\OpenID\Federation\Claims\TrustMarkIssuersClaimBag; use SimpleSAML\OpenID\Federation\Claims\TrustMarkOwnersClaimBag; use SimpleSAML\OpenID\Federation\Claims\TrustMarksClaimBag; use SimpleSAML\OpenID\Jws\ParsedJws; @@ -234,6 +235,25 @@ public function getTrustMarkOwners(): ?TrustMarkOwnersClaimBag } + public function getTrustMarkIssuers(): ?TrustMarkIssuersClaimBag + { + // trust_mark_issuers + // OPTIONAL. A Trust Anchor MAY use this claim to tell which combination of Trust Mark type identifiers and + // issuers are trusted by the federation. It is a JSON object with member names that are Trust Mark type + // identifiers, and each corresponding value being an array of Entity Identifiers that are trusted to + // represent the accreditation authority for Trust Marks with that identifier. + + $claimKey = ClaimsEnum::TrustMarkIssuers->value; + $trustMarkIssuersClaimData = $this->getPayloadClaim($claimKey); + + if (is_null($trustMarkIssuersClaimData)) { + return null; + } + + return $this->claimFactory->forFederation()->buildTrustMarkIssuersClaimBagFrom($trustMarkIssuersClaimData); + } + + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\JwsException @@ -339,6 +359,7 @@ protected function validate(): void $this->getMetadataPolicy(...), $this->getTrustMarks(...), $this->getTrustMarkOwners(...), + $this->getTrustMarkIssuers(...), $this->getFederationFetchEndpoint(...), $this->getFederationTrustMarkEndpoint(...), ); diff --git a/src/Federation/Factories/FederationClaimFactory.php b/src/Federation/Factories/FederationClaimFactory.php index 2a9d1cc..8e9d2b7 100644 --- a/src/Federation/Factories/FederationClaimFactory.php +++ b/src/Federation/Factories/FederationClaimFactory.php @@ -7,6 +7,8 @@ use SimpleSAML\OpenID\Codebooks\ClaimsEnum; use SimpleSAML\OpenID\Exceptions\TrustMarkException; use SimpleSAML\OpenID\Factories\ClaimFactory; +use SimpleSAML\OpenID\Federation\Claims\TrustMarkIssuersClaimBag; +use SimpleSAML\OpenID\Federation\Claims\TrustMarkIssuersClaimValue; use SimpleSAML\OpenID\Federation\Claims\TrustMarkOwnersClaimBag; use SimpleSAML\OpenID\Federation\Claims\TrustMarkOwnersClaimValue; use SimpleSAML\OpenID\Federation\Claims\TrustMarksClaimBag; @@ -152,4 +154,48 @@ public function buildTrustMarkOwnersClaimBag( ): TrustMarkOwnersClaimBag { return new TrustMarkOwnersClaimBag(...$trustMarkOwnersClaimValues); } + + + public function buildTrustMarkIssuersClaimBagFrom(mixed $trustMarkIssuersClaimData): TrustMarkIssuersClaimBag + { + $trustMarkIssuersClaimData = $this->helpers->type()->ensureArrayWithKeysAsNonEmptyStrings( + $trustMarkIssuersClaimData, + ); + + $trustMarkIssuersClaimValues = []; + + foreach ($trustMarkIssuersClaimData as $trustMarkType => $trustMarkIssuersClaim) { + $trustMarkIssuersClaim = $this->helpers->type()->ensureArrayWithValuesAsNonEmptyStrings( + $trustMarkIssuersClaim, + ); + + $trustMarkIssuersClaimValues[] = $this->buildTrustMarkIssuersClaimValue( + $trustMarkType, + $trustMarkIssuersClaim, + ); + } + + return $this->buildTrustMarkIssuerClaimBag(...$trustMarkIssuersClaimValues); + } + + + public function buildTrustMarkIssuersClaimValue( + mixed $trustMarkType, + mixed $trustMarkIssuers, + ): TrustMarkIssuersClaimValue { + $trustMarkType = $this->helpers->type()->ensureNonEmptyString($trustMarkType); + $trustMarkIssuers = $this->helpers->type()->ensureArrayWithValuesAsNonEmptyStrings($trustMarkIssuers); + + return new TrustMarkIssuersClaimValue( + $trustMarkType, + $trustMarkIssuers, + ); + } + + + public function buildTrustMarkIssuerClaimBag( + TrustMarkIssuersClaimValue ...$trustMarkIssuersClaimValues, + ): TrustMarkIssuersClaimBag { + return new TrustMarkIssuersClaimBag(...$trustMarkIssuersClaimValues); + } } diff --git a/src/Federation/Factories/TrustChainFactory.php b/src/Federation/Factories/TrustChainFactory.php index c1238a4..ee26785 100644 --- a/src/Federation/Factories/TrustChainFactory.php +++ b/src/Federation/Factories/TrustChainFactory.php @@ -74,7 +74,7 @@ public function fromStatements(EntityStatement ...$statements): TrustChain public function fromTokens(string ...$tokens): TrustChain { $statements = array_map( - fn(string $token): EntityStatement => $this->entityStatementFactory->fromToken($token), + $this->entityStatementFactory->fromToken(...), $tokens, ); diff --git a/src/Federation/TrustMarkValidator.php b/src/Federation/TrustMarkValidator.php index d8fc9fe..e8262ad 100644 --- a/src/Federation/TrustMarkValidator.php +++ b/src/Federation/TrustMarkValidator.php @@ -422,7 +422,7 @@ public function doForTrustMark( $this->validateSubjectClaim($trustMark, $leafEntityConfiguration); - // If Trust Mark Issuer is the Trust Anchor itself, we don't have to resolve chain, as Trust Anchor is trusted + // If Trust Mark Issuer is the Trust Anchor itself, we don't have to resolve a chain, as Trust Anchor is trusted // out-of-band. Otherwise, we have to resolve trust for Trust Mark Issuer. $trustMarkIssuerEntityConfiguration = $trustMark->getIssuer() === $trustAnchorEntityConfiguration->getIssuer() ? diff --git a/src/Jws/ParsedJws.php b/src/Jws/ParsedJws.php index a3293b3..d5ec0e1 100644 --- a/src/Jws/ParsedJws.php +++ b/src/Jws/ParsedJws.php @@ -125,7 +125,7 @@ public function getPayload(): array } $payloadString = $this->jwsDecorator->jws()->getPayload(); - if ($payloadString === null || $payloadString === '' || $payloadString === '0') { + if (in_array($payloadString, [null, '', '0'], true)) { return $this->payload = []; } diff --git a/tests/src/Federation/Claims/TrustMarkIssuersClaimBagTest.php b/tests/src/Federation/Claims/TrustMarkIssuersClaimBagTest.php new file mode 100644 index 0000000..1a5ed5a --- /dev/null +++ b/tests/src/Federation/Claims/TrustMarkIssuersClaimBagTest.php @@ -0,0 +1,73 @@ +trustMarkIssuerClaimValueMock = $this->createMock(TrustMarkIssuersClaimValue::class); + $this->trustMarkIssuerClaimValueMock->method('getTrustMarkType')->willReturn('trustMarkType'); + $this->trustMarkIssuerClaimValueMock->method('getTrustMarkIssuers') + ->willReturn(['https://issuer1.org', 'https://issuer2.org']); + } + + + protected function sut( + TrustMarkIssuersClaimValue ...$trustMarkIssuerClaimValues, + ): TrustMarkIssuersClaimBag { + return new TrustMarkIssuersClaimBag(...$trustMarkIssuerClaimValues); + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(TrustMarkIssuersClaimBag::class, $this->sut()); + } + + + public function testCanAddAndGet(): void + { + $this->assertEmpty($this->sut()->getAll()); + $sut = $this->sut($this->trustMarkIssuerClaimValueMock); + $this->assertCount(1, $sut->getAll()); + $this->assertTrue($sut->has('trustMarkType')); + $this->assertSame($this->trustMarkIssuerClaimValueMock, $sut->get('trustMarkType')); + + $trustMarkIssuerClaimValueMock2 = $this->createMock(TrustMarkIssuersClaimValue::class); + $trustMarkIssuerClaimValueMock2->method('getTrustMarkType')->willReturn('trustMarkType2'); + $sut->add($trustMarkIssuerClaimValueMock2); + $this->assertCount(2, $sut->getAll()); + $this->assertTrue($sut->has('trustMarkType2')); + $this->assertSame($trustMarkIssuerClaimValueMock2, $sut->get('trustMarkType2')); + } + + + public function testCanJsonSerialize(): void + { + $trustMarkIssuerClaimValueMock2 = $this->createMock(TrustMarkIssuersClaimValue::class); + $trustMarkIssuerClaimValueMock2->method('getTrustMarkType')->willReturn('trustMarkType2'); + $this->trustMarkIssuerClaimValueMock->method('getTrustMarkIssuers')->willReturn([]); + + $sut = $this->sut($this->trustMarkIssuerClaimValueMock, $trustMarkIssuerClaimValueMock2); + $this->assertSame( + [ + 'trustMarkType' => ['https://issuer1.org', 'https://issuer2.org'], + 'trustMarkType2' => [], + ], + $sut->jsonSerialize(), + ); + } +} diff --git a/tests/src/Federation/Claims/TrustMarkIssuersClaimValueTest.php b/tests/src/Federation/Claims/TrustMarkIssuersClaimValueTest.php new file mode 100644 index 0000000..5c7453b --- /dev/null +++ b/tests/src/Federation/Claims/TrustMarkIssuersClaimValueTest.php @@ -0,0 +1,62 @@ + + */ + protected array $trustMarkIssuers; + + + protected function setUp(): void + { + $this->trustMarkType = 'trustMarkType'; + $this->trustMarkIssuers = ['https://issuer1.org', 'https://issuer2.org']; + } + + + protected function sut( + ?string $trustMarkType = null, + ?array $trustMarkIssuers = null, + ): TrustMarkIssuersClaimValue { + $trustMarkType ??= $this->trustMarkType; + $trustMarkIssuers ??= $this->trustMarkIssuers; + + return new TrustMarkIssuersClaimValue($trustMarkType, $trustMarkIssuers); + ; + } + + + public function testCanCreateInstance(): void + { + $this->assertInstanceOf(TrustMarkIssuersClaimValue::class, $this->sut()); + } + + + public function testCanGetProperties(): void + { + $sut = $this->sut(); + $this->assertSame($this->trustMarkType, $sut->getTrustMarkType()); + $this->assertSame($this->trustMarkIssuers, $sut->getTrustMarkIssuers()); + } + + + public function testCanJsonSerialize(): void + { + $this->assertSame( + [$this->trustMarkType => $this->trustMarkIssuers], + $this->sut()->jsonSerialize(), + ); + } +} diff --git a/tests/src/Federation/EntityStatementTest.php b/tests/src/Federation/EntityStatementTest.php index 98df2d7..e5238e1 100644 --- a/tests/src/Federation/EntityStatementTest.php +++ b/tests/src/Federation/EntityStatementTest.php @@ -323,6 +323,22 @@ public function testTrustMarkOwnersIsBuildUsingFactoryOptional(): void } + public function testTrustMarkIssuersIsBuildUsingFactoryOptional(): void + { + $this->validPayload['trust_mark_issuers'] = [ + 'trustMarkType' => ['https://issuer1.org', 'https://issuer2.org'], + ]; + $this->signatureMock->method('getProtectedHeader')->willReturn($this->sampleHeader); + $this->jsonHelperMock->method('decode')->willReturn($this->validPayload); + + $this->federationClaimFactoryMock->expects($this->atLeastOnce()) + ->method('buildTrustMarkIssuersClaimBagFrom') + ->with($this->validPayload['trust_mark_issuers']); + + $this->sut()->getTrustMarkIssuers(); + } + + public function testThrowsOnInvalidTrustMarks(): void { $this->validPayload['trust_marks'] = 'invalid'; diff --git a/tests/src/Federation/Factories/FederationClaimFactoryTest.php b/tests/src/Federation/Factories/FederationClaimFactoryTest.php index 5b38b43..95454d1 100644 --- a/tests/src/Federation/Factories/FederationClaimFactoryTest.php +++ b/tests/src/Federation/Factories/FederationClaimFactoryTest.php @@ -10,6 +10,8 @@ use SimpleSAML\OpenID\Claims\JwksClaim; use SimpleSAML\OpenID\Codebooks\ClaimsEnum; use SimpleSAML\OpenID\Factories\ClaimFactory; +use SimpleSAML\OpenID\Federation\Claims\TrustMarkIssuersClaimBag; +use SimpleSAML\OpenID\Federation\Claims\TrustMarkIssuersClaimValue; use SimpleSAML\OpenID\Federation\Claims\TrustMarkOwnersClaimBag; use SimpleSAML\OpenID\Federation\Claims\TrustMarkOwnersClaimValue; use SimpleSAML\OpenID\Federation\Claims\TrustMarksClaimBag; @@ -26,6 +28,8 @@ #[UsesClass(TrustMarksClaimBag::class)] #[UsesClass(TrustMarkOwnersClaimValue::class)] #[UsesClass(TrustMarkOwnersClaimBag::class)] +#[UsesClass(TrustMarkIssuersClaimBag::class)] +#[UsesClass(TrustMarkIssuersClaimValue::class)] final class FederationClaimFactoryTest extends TestCase { protected Helpers $helpers; @@ -131,4 +135,17 @@ public function testCanBuildTrustMarkOwnersClaimBagFrom(): void $this->sut()->buildTrustMarkOwnersClaimBagFrom($trustMarkOwnersClaimData), ); } + + + public function testCanBuildTrustMarkIssuersClaimBagFrom(): void + { + $trustMarkIssuersClaimData = [ + 'trustMarkType' => ['https://issuer1.org', 'https://issuer2.org'], + ]; + + $this->assertInstanceOf( + TrustMarkIssuersClaimBag::class, + $this->sut()->buildTrustMarkIssuersClaimBagFrom($trustMarkIssuersClaimData), + ); + } } From 6ccfcbff8612b5dbb7e74de3ad48223a5fa1d2ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Ivan=C4=8Di=C4=87?= Date: Mon, 3 Nov 2025 15:37:51 +0100 Subject: [PATCH 2/2] Start validating trust mark issuers advertized by Trust Anchor when validating Trust Marks --- src/Federation/TrustMarkValidator.php | 86 ++++++++++++++ .../src/Federation/TrustMarkValidatorTest.php | 106 ++++++++++++++++++ 2 files changed, 192 insertions(+) diff --git a/src/Federation/TrustMarkValidator.php b/src/Federation/TrustMarkValidator.php index e8262ad..23e29da 100644 --- a/src/Federation/TrustMarkValidator.php +++ b/src/Federation/TrustMarkValidator.php @@ -421,6 +421,7 @@ public function doForTrustMark( ); $this->validateSubjectClaim($trustMark, $leafEntityConfiguration); + $this->validateTrustMarkIssuers($trustMark, $trustAnchorEntityConfiguration); // If Trust Mark Issuer is the Trust Anchor itself, we don't have to resolve a chain, as Trust Anchor is trusted // out-of-band. Otherwise, we have to resolve trust for Trust Mark Issuer. @@ -516,6 +517,91 @@ public function validateSubjectClaim( } + /** + * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException + * @throws \SimpleSAML\OpenID\Exceptions\TrustMarkException + * @throws \SimpleSAML\OpenID\Exceptions\JwsException + */ + public function validateTrustMarkIssuers( + TrustMark $trustMark, + EntityStatement $trustAnchorEntityConfiguration, + ): void { + $this->logger?->debug('Validating Trust Mark Issuers.'); + + if (is_null($trustMarkIssuersClaimBag = $trustAnchorEntityConfiguration->getTrustMarkIssuers())) { + $this->logger?->debug( + sprintf( + 'Trust Anchor %s does not define Trust Mark Issuers. Skipping validation.', + $trustAnchorEntityConfiguration->getIssuer(), + ), + ); + return; + } + + $this->logger?->debug( + sprintf( + 'Trust Anchor %s defines Trust Mark Issuers.', + $trustAnchorEntityConfiguration->getIssuer(), + ), + ['trustMarkIssuers' => $trustMarkIssuersClaimBag->jsonSerialize()], + ); + + $trustMarkIssuersClaimValue = $trustMarkIssuersClaimBag->get($trustMark->getTrustMarkType()); + + if (is_null($trustMarkIssuersClaimValue)) { + $this->logger?->debug( + sprintf( + 'Trust Anchor %s does not define issuers of Trust Mark %s. Skipping validation.', + $trustAnchorEntityConfiguration->getIssuer(), + $trustMark->getTrustMarkType(), + ), + ); + return; + } + + if ($trustMarkIssuersClaimValue->getTrustMarkIssuers() === []) { + $this->logger?->debug( + sprintf( + 'Trust Anchor %s defines any issuers of Trust Mark %s. Skipping validation.', + $trustAnchorEntityConfiguration->getIssuer(), + $trustMark->getTrustMarkType(), + ), + ); + return; + } + + $this->logger?->debug( + sprintf( + 'Trust Anchor %s defines issuers %s of Trust Mark %s.', + $trustAnchorEntityConfiguration->getIssuer(), + implode(', ', $trustMarkIssuersClaimValue->getTrustMarkIssuers()), + $trustMark->getTrustMarkType(), + ), + ); + + if (!in_array($trustMark->getIssuer(), $trustMarkIssuersClaimValue->getTrustMarkIssuers(), true)) { + $error = sprintf( + 'Trust Mark %s is not issued by any of the Trust Mark Issuers %s defined by Trust Anchor %s.', + $trustMark->getTrustMarkType(), + implode(', ', $trustMarkIssuersClaimValue->getTrustMarkIssuers()), + $trustAnchorEntityConfiguration->getIssuer(), + ); + + $this->logger?->error($error); + throw new TrustMarkException($error); + } + + $this->logger?->debug( + sprintf( + 'Trust Mark %s is issued by one of the Trust Mark Issuers %s defined by Trust Anchor %s.', + $trustMark->getTrustMarkType(), + implode(', ', $trustMarkIssuersClaimValue->getTrustMarkIssuers()), + $trustAnchorEntityConfiguration->getIssuer(), + ), + ); + } + + /** * @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException * @throws \SimpleSAML\OpenID\Exceptions\JwsException diff --git a/tests/src/Federation/TrustMarkValidatorTest.php b/tests/src/Federation/TrustMarkValidatorTest.php index 3d76e00..81184f6 100644 --- a/tests/src/Federation/TrustMarkValidatorTest.php +++ b/tests/src/Federation/TrustMarkValidatorTest.php @@ -11,6 +11,8 @@ use SimpleSAML\OpenID\Decorators\CacheDecorator; use SimpleSAML\OpenID\Decorators\DateIntervalDecorator; use SimpleSAML\OpenID\Exceptions\TrustMarkException; +use SimpleSAML\OpenID\Federation\Claims\TrustMarkIssuersClaimBag; +use SimpleSAML\OpenID\Federation\Claims\TrustMarkIssuersClaimValue; use SimpleSAML\OpenID\Federation\Claims\TrustMarkOwnersClaimBag; use SimpleSAML\OpenID\Federation\Claims\TrustMarkOwnersClaimValue; use SimpleSAML\OpenID\Federation\Claims\TrustMarksClaimBag; @@ -54,6 +56,10 @@ final class TrustMarkValidatorTest extends TestCase protected MockObject $trustMarkDelegationMock; + protected MockObject $trustMarkIssuersClaimBagMock; + + protected MockObject $trustMarkIssuersClaimValueMock; + protected function setUp(): void { @@ -78,6 +84,9 @@ protected function setUp(): void $this->trustMarkOwnersClaimValueMock = $this->createMock(TrustMarkOwnersClaimValue::class); $this->trustMarkDelegationMock = $this->createMock(TrustMarkDelegation::class); + + $this->trustMarkIssuersClaimBagMock = $this->createMock(TrustMarkIssuersClaimBag::class); + $this->trustMarkIssuersClaimValueMock = $this->createMock(TrustMarkIssuersClaimValue::class); } @@ -717,4 +726,101 @@ public function testValidateTrustMarkDelegationThrowsForInvalidTrustMarkType(): $this->trustAnchorConfigurationMock, ); } + + + public function testDoForTrustMarkValidatesTrustMarkIssuers(): void + { + $this->trustMarkMock->method('getTrustMarkType')->willReturn('trustMarkType'); + $this->trustMarkMock->method('getSubject')->willReturn('leafEntityId'); + $this->trustAnchorConfigurationMock->expects($this->once())->method('getTrustMarkIssuers'); + + $this->sut()->doForTrustMark( + $this->trustMarkMock, + $this->leafEntityConfigurationMock, + $this->trustAnchorConfigurationMock, + ); + } + + + public function testValidateTrustMarkIssuersPassesIfNoTrustMarkIssuersDefined(): void + { + $this->trustMarkMock->method('getTrustMarkType')->willReturn('trustMarkType'); + $this->trustMarkMock->method('getSubject')->willReturn('leafEntityId'); + $this->trustMarkIssuersClaimBagMock->expects($this->once())->method('get') + ->with('trustMarkType') + ->willReturn(null); + $this->trustAnchorConfigurationMock->expects($this->once())->method('getTrustMarkIssuers') + ->willReturn($this->trustMarkIssuersClaimBagMock); + + $this->trustMarkMock->expects($this->atLeastOnce())->method('getTrustMarkType'); + + $this->sut()->validateTrustMarkIssuers( + $this->trustMarkMock, + $this->trustAnchorConfigurationMock, + ); + } + + + public function testValidateTrustMarkIssuersPassesForEmptyTrustMarkIssuers(): void + { + $this->trustMarkMock->method('getTrustMarkType')->willReturn('trustMarkType'); + $this->trustMarkMock->method('getSubject')->willReturn('leafEntityId'); + $this->trustMarkIssuersClaimValueMock->method('getTrustMarkIssuers')->willReturn([]); + $this->trustMarkIssuersClaimBagMock->expects($this->once())->method('get') + ->with('trustMarkType') + ->willReturn($this->trustMarkIssuersClaimValueMock); + $this->trustAnchorConfigurationMock->expects($this->once())->method('getTrustMarkIssuers') + ->willReturn($this->trustMarkIssuersClaimBagMock); + + $this->trustMarkMock->expects($this->atLeastOnce())->method('getTrustMarkType'); + + $this->sut()->validateTrustMarkIssuers( + $this->trustMarkMock, + $this->trustAnchorConfigurationMock, + ); + } + + + public function testValidateTrustMarkIssuersThrowsForIssuerNotAdvertisedByTrustAnchor(): void + { + $this->trustMarkMock->method('getTrustMarkType')->willReturn('trustMarkType'); + $this->trustMarkMock->method('getSubject')->willReturn('leafEntityId'); + $this->trustMarkIssuersClaimValueMock->method('getTrustMarkIssuers') + ->willReturn(['https://example.com/trust-mark-issuer']); + $this->trustMarkIssuersClaimBagMock->expects($this->once())->method('get') + ->with('trustMarkType') + ->willReturn($this->trustMarkIssuersClaimValueMock); + $this->trustAnchorConfigurationMock->expects($this->once())->method('getTrustMarkIssuers') + ->willReturn($this->trustMarkIssuersClaimBagMock); + + $this->expectException(TrustMarkException::class); + $this->expectExceptionMessage('not issued by any'); + ; + + $this->sut()->validateTrustMarkIssuers( + $this->trustMarkMock, + $this->trustAnchorConfigurationMock, + ); + } + + + public function testValidateTrustMarkIssuersPassesForIssuerAdvertisedByTrustAnchor(): void + { + $this->trustMarkMock->method('getTrustMarkType')->willReturn('trustMarkType'); + $this->trustMarkMock->method('getSubject')->willReturn('leafEntityId'); + $this->trustMarkMock->method('getIssuer')->willReturn('trustMarkIssuerId'); + + $this->trustMarkIssuersClaimValueMock->method('getTrustMarkIssuers') + ->willReturn(['trustMarkIssuerId']); + $this->trustMarkIssuersClaimBagMock->expects($this->once())->method('get') + ->with('trustMarkType') + ->willReturn($this->trustMarkIssuersClaimValueMock); + $this->trustAnchorConfigurationMock->expects($this->once())->method('getTrustMarkIssuers') + ->willReturn($this->trustMarkIssuersClaimBagMock); + + $this->sut()->validateTrustMarkIssuers( + $this->trustMarkMock, + $this->trustAnchorConfigurationMock, + ); + } }