Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/Federation/Factories/TrustChainFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,17 @@ public function fromTokens(string ...$tokens): TrustChain

return $this->fromStatements(...$statements);
}

/**
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
* @throws \SimpleSAML\OpenID\Exceptions\TrustChainException
*/
public function forTrustAnchor(EntityStatement $trustAnchorStatement): TrustChain
{
$trustChain = $this->empty();

$trustChain->addForTrustAnchorOnly($trustAnchorStatement);

return $trustChain;
}
}
32 changes: 25 additions & 7 deletions src/Federation/TrustChain.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,11 @@ public function getResolvedLeaf(): EntityStatement
/**
* @throws \SimpleSAML\OpenID\Exceptions\TrustChainException
*/
public function getResolvedImmediateSuperior(): EntityStatement
public function getResolvedImmediateSuperior(): ?EntityStatement
{
$this->validateIsResolved();

($immediateSuperior = $this->entities[1] ?? null) ||
throw new TrustChainException('Empty immediate superior statement encountered.');

return $immediateSuperior;
return $this->entities[1] ?? null;
}

/**
Expand Down Expand Up @@ -208,6 +205,27 @@ public function addTrustAnchor(EntityStatement $entityStatement): void
$this->isResolved = true;
}

/**
* Add a Trust Anchor Entity Configuration to create a single entity statement chain. This accommodates a special
* case for the Trust Chain of the Trust Anchor itself.
*
* @throws \SimpleSAML\OpenID\Exceptions\TrustChainException
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
*
* @internal
*/
public function addForTrustAnchorOnly(EntityStatement $entityStatement): void
{
$this->validateIsNotResolved();
$this->validateIsEmpty();
$this->validateConfigurationStatement($entityStatement);

$this->entities[] = $entityStatement;
$this->updateExpirationTime($entityStatement);

$this->isResolved = true;
}

/**
* @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
Expand Down Expand Up @@ -400,7 +418,7 @@ protected function resolveMetadataFor(EntityTypesEnum $entityTypeEnum): void
// When an Entity participates in a federation or federations with one or more Entity Types, its Entity
// Configuration MUST contain a metadata claim with JSON object values for each of the corresponding
// Entity Type Identifiers, even if the values are the empty JSON object {} (when the Entity Type
// has no associated metadata or Immediate Superiors supply any needed metadata).
// has no associated metadata or Immediate Superiors supply any necessary metadata).
$leafMetadata = $this->getResolvedLeaf()->getMetadata();
if (
(!is_array($leafMetadata)) || // Claim 'metadata' is optional.
Expand All @@ -422,7 +440,7 @@ protected function resolveMetadataFor(EntityTypesEnum $entityTypeEnum): void
// subject's Entity Configuration. If both metadata and metadata_policy
// appear in a Subordinate Statement, then the stated metadata MUST
// be applied before the metadata_policy.
$immediateSuperiorMetadata = $this->getResolvedImmediateSuperior()->getMetadata();
$immediateSuperiorMetadata = $this->getResolvedImmediateSuperior()?->getMetadata();
if (
is_array($immediateSuperiorMetadata) &&
isset($immediateSuperiorMetadata[$entityTypeEnum->value]) &&
Expand Down
58 changes: 35 additions & 23 deletions src/Federation/TrustChainResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,32 +225,44 @@ public function for(string $entityId, array $validTrustAnchorIds): TrustChainBag
];
$this->logger?->debug('Start resolving for configuration chain.', $debugConfigChainResolveInfo);
try {
// 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),
);
// 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;
}

// 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);
}

$resolvedChains[] = $this->trustChainFactory->fromStatements(...$currenChainElements);
} catch (Throwable $exception) {
$this->logger?->error(
sprintf(
Expand Down
14 changes: 14 additions & 0 deletions tests/src/Federation/Factories/TrustChainFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,18 @@ public function testCanBuildFromTokens(): void
$this->sut()->fromTokens('token', 'token2', 'tokent3'),
);
}

public function testCanBuildForTrustAnchor(): void
{
$expirationTime = time() + 60;

$trustAnchor = $this->createMock(EntityStatement::class);
$trustAnchor->method('isConfiguration')->willReturn(true);
$trustAnchor->method('getExpirationTime')->willReturn($expirationTime);

$this->assertInstanceOf(
TrustChain::class,
$this->sut()->forTrustAnchor($trustAnchor),
);
}
}
15 changes: 15 additions & 0 deletions tests/src/Federation/TrustChainResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,21 @@ public function testCanResolveMultipleTrustChains(): void
$this->sut()->for('l', ['i', 't']);
}

public function testCanResolveTrustChainForTrustAnchorOnly(): void
{
$this->entityStatementFetcherMock
->method('fromCacheOrWellKnownEndpoint')
->willReturnCallback(fn(string $entityId) =>
$this->configChainSample[$entityId] ?? throw new \Exception('No entity.'));

$this->trustChainFactoryMock->expects($this->once())->method('forTrustAnchor');

$this->trustChainBagFactoryMock->expects($this->once())->method('build');
$this->cacheDecoratorMock->expects($this->once())->method('set');

$this->sut()->for('t', ['t']);
}

public function testTrustChainResolveChecksCacheFirst(): void
{
$this->cacheDecoratorMock
Expand Down
20 changes: 18 additions & 2 deletions tests/src/Federation/TrustChainTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,22 @@ public function testCanCreateBasicTrustChain(): void
$this->assertNull($sut->getResolvedMetadata(EntityTypesEnum::OpenIdRelyingParty));
}

public function testCanCreateTrustChainForTrustAnchorOnly(): void
{
$sut = $this->sut();
$sut->addForTrustAnchorOnly($this->trustAnchorMock);

$this->assertFalse($sut->isEmpty());
$this->assertCount(1, $sut->getEntities());
$this->assertSame(1, $sut->getResolvedLength());
$this->assertNotEmpty($sut->jsonSerialize());
$this->assertSame($this->expirationTime, $sut->getResolvedExpirationTime());
$this->assertSame($this->trustAnchorMock, $sut->getResolvedLeaf());
$this->assertSame($this->trustAnchorMock, $sut->getResolvedTrustAnchor());
$this->assertNotInstanceOf(EntityStatement::class, $sut->getResolvedImmediateSuperior());
$this->assertNull($sut->getResolvedMetadata(EntityTypesEnum::OpenIdRelyingParty));
}

public function testThrowsForNonConfigurationStatementForLeaf(): void
{
$this->expectException(EntityStatementException::class);
Expand Down Expand Up @@ -267,7 +283,7 @@ public function testCanGetResolvedMetadataIfNoPoliciesAreDefined(): void
$this->assertIsArray($sut->getResolvedMetadata(EntityTypesEnum::OpenIdRelyingParty));
}

public function testThrowsOnAttemtpToAddMultipleLeafs(): void
public function testThrowsOnAttemptToAddMultipleLeafs(): void
{
$this->expectException(TrustChainException::class);
$this->expectExceptionMessage('empty');
Expand All @@ -286,7 +302,7 @@ public function testThrowsOnAttemtpToAddSubodrinateWithoutLeaf(): void
$sut->addSubordinate($this->subordinateMock);
}

public function testThrowsOnAttemtpToAddTrustAnchorWithoutSubordinate(): void
public function testThrowsOnAttemptToAddTrustAnchorWithoutSubordinate(): void
{
$this->expectException(TrustChainException::class);
$this->expectExceptionMessage('at least');
Expand Down
2 changes: 1 addition & 1 deletion tests/src/Jws/JwsDecoratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
#[CoversClass(JwsDecorator::class)]
final class JwsDecoratorTest extends TestCase
{
protected JWS $jwsMock;
protected \PHPUnit\Framework\MockObject\MockObject $jwsMock;

protected function setUp(): void
{
Expand Down