Skip to content

Commit

Permalink
WIP Trust Mark validation
Browse files Browse the repository at this point in the history
  • Loading branch information
cicnavi committed Jan 28, 2025
1 parent 735f188 commit aba66cb
Show file tree
Hide file tree
Showing 11 changed files with 363 additions and 2 deletions.
24 changes: 24 additions & 0 deletions src/Claims/GenericClaim.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\OpenID\Claims;

class GenericClaim
{
public function __construct(
protected readonly string $name,
protected readonly mixed $value,
) {
}

public function getName(): string
{
return $this->name;
}

public function getValue(): mixed
{
return $this->value;
}
}
1 change: 1 addition & 0 deletions src/Codebooks/JwtTypesEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ enum JwtTypesEnum: string
case EntityStatementJwt = 'entity-statement+jwt';
case JwkSetJwt = 'jwk-set+jwt';
case TrustMarkJwt = 'trust-mark+jwt';
case TrustMarkDelegationJwt = 'trust-mark-delegation+jwt';
}
9 changes: 9 additions & 0 deletions src/Exceptions/TrustMarkDelegationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\OpenID\Exceptions;

class TrustMarkDelegationException extends JwsException
{
}
27 changes: 27 additions & 0 deletions src/Factories/ClaimFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\OpenID\Factories;

use SimpleSAML\OpenID\Claims\GenericClaim;
use SimpleSAML\OpenID\Helpers;

class ClaimFactory
{
public function __construct(
protected readonly Helpers $helpers,
) {
}

/**
* @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException
*/
public function buildGeneric(mixed $key, mixed $value): GenericClaim
{
return new GenericClaim(
$this->helpers->type()->ensureString($key, 'ClaimKey'),
$value,
);
}
}
9 changes: 9 additions & 0 deletions src/Federation/EntityStatement/TrustMarkOwnerBag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\OpenID\Federation\EntityStatement;

class TrustMarkOwnerBag
{
}
13 changes: 13 additions & 0 deletions src/Federation/EntityStatement/TrustMarkOwnerClaim.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\OpenID\Federation\EntityStatement;

class TrustMarkOwnerClaim
{
public function __construct(
protected readonly string $subject,
) {
}
}
23 changes: 23 additions & 0 deletions src/Federation/Factories/TrustMarkDelegationFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\OpenID\Federation\Factories;

use SimpleSAML\OpenID\Federation\TrustMarkDelegation;
use SimpleSAML\OpenID\Jws\Factories\ParsedJwsFactory;

class TrustMarkDelegationFactory extends ParsedJwsFactory
{
public function fromToken(string $token): TrustMarkDelegation
{
return new TrustMarkDelegation(
$this->jwsParser->parse($token),
$this->jwsVerifierDecorator,
$this->jwksFactory,
$this->jwsSerializerManagerDecorator,
$this->timestampValidationLeeway,
$this->helpers,
);
}
}
1 change: 1 addition & 0 deletions src/Federation/TrustMark.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public function getReference(): ?string
/**
* @return ?non-empty-string
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
* @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException
*/
public function getDelegation(): ?string
{
Expand Down
110 changes: 110 additions & 0 deletions src/Federation/TrustMarkDelegation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\OpenID\Federation;

use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
use SimpleSAML\OpenID\Codebooks\JwtTypesEnum;
use SimpleSAML\OpenID\Exceptions\TrustMarkDelegationException;
use SimpleSAML\OpenID\Jws\ParsedJws;

class TrustMarkDelegation extends ParsedJws
{
/**
* @return non-empty-string
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
* @throws \SimpleSAML\OpenID\Exceptions\TrustMarkDelegationException
*/
public function getIssuer(): string
{
return parent::getIssuer() ?? throw new TrustMarkDelegationException('No Issuer claim found.');
}

/**
* @return non-empty-string
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
* @throws \SimpleSAML\OpenID\Exceptions\TrustMarkDelegationException
*/
public function getSubject(): string
{
return parent::getSubject() ?? throw new TrustMarkDelegationException('No Subject claim found.');
}

/**
* @return non-empty-string
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
* @throws \SimpleSAML\OpenID\Exceptions\TrustMarkDelegationException
*/
public function getIdentifier(): string
{
return parent::getIdentifier() ?? throw new TrustMarkDelegationException('No Identifier claim found.');
}

/**
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
* @throws \SimpleSAML\OpenID\Exceptions\TrustMarkDelegationException
*/
public function getIssuedAt(): int
{
return parent::getIssuedAt() ?? throw new TrustMarkDelegationException('No Issued At claim found.');
}

/**
* @return ?non-empty-string
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
* @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException
*/
public function getReference(): ?string
{
$ref = $this->getPayloadClaim(ClaimsEnum::Ref->value);

return is_null($ref) ?
null :
$this->helpers->type()->ensureNonEmptyString($ref, ClaimsEnum::Ref->value);
}

/**
* @return non-empty-string
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
* @throws \SimpleSAML\OpenID\Exceptions\TrustMarkDelegationException
*/
public function getKeyId(): string
{
return parent::getKeyId() ?? throw new TrustMarkDelegationException('No KeyId header claim found.');
}

/**
* @throws \SimpleSAML\OpenID\Exceptions\TrustMarkDelegationException
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
* @return non-empty-string
*/
public function getType(): string
{
$typ = parent::getType() ?? throw new TrustMarkDelegationException('No Type header claim found.');

if ($typ !== JwtTypesEnum::TrustMarkJwt->value) {
throw new TrustMarkDelegationException('Invalid Type header claim.');
}

return $typ;
}

/**
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
* @throws \SimpleSAML\OpenID\Exceptions\TrustMarkDelegationException
*/
public function validate(): void
{
$this->validateByCallbacks(
$this->getIssuer(...),
$this->getSubject(...),
$this->getIdentifier(...),
$this->getIssuedAt(...),
$this->getExpirationTime(...),
$this->getReference(...),
$this->getKeyId(...),
$this->getType(...),
);
}
}
144 changes: 144 additions & 0 deletions src/Federation/TrustMarkValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,150 @@

namespace SimpleSAML\OpenID\Federation;

use Psr\Log\LoggerInterface;
use SimpleSAML\OpenID\Exceptions\TrustMarkException;
use SimpleSAML\OpenID\Federation\Factories\TrustMarkDelegationFactory;
use Throwable;

class TrustMarkValidator
{
public function __construct(
protected readonly TrustChainResolver $trustChainResolver,
protected readonly TrustMarkDelegationFactory $trustMarkDelegationFactory,
protected readonly ?LoggerInterface $logger = null,
) {
}

/**
* @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
* @throws \SimpleSAML\OpenID\Exceptions\TrustMarkException
* @throws \SimpleSAML\OpenID\Exceptions\TrustChainException
*/
public function for(
TrustMark $trustMark,
EntityStatement $leafEntityConfiguration,
EntityStatement $trustAnchorEntityConfiguration,
): void {
$this->logger?->debug(
'Validating Trust Mark.',
[
'trustMarkPayload' => $trustMark->getPayload(),
'leafEntityConfigurationPayload' => $leafEntityConfiguration->getPayload(),
'trustAnchorEntityConfigurationPayload' => $trustAnchorEntityConfiguration->getPayload(),
],
);

$this->validateSubjectClaim($trustMark, $leafEntityConfiguration);

$trustMarkIssuerTrustChain = $this->validateTrustChainForTrustMarkIssuer(
$trustMark,
$trustAnchorEntityConfiguration,
);

$trustMarkIssuerEntityStatement = $trustMarkIssuerTrustChain->getResolvedLeaf();

$this->validateTrustMarkSignature($trustMark, $trustMarkIssuerEntityStatement);

// TODO mivanci continue Validate delegation
}

/**
* @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
* @throws \SimpleSAML\OpenID\Exceptions\TrustMarkException
*/
public function validateSubjectClaim(
TrustMark $trustMark,
EntityStatement $leafEntityConfiguration,
): void {
$this->logger?->debug('Validating Trust Mark subject.');

if ($trustMark->getSubject() !== $leafEntityConfiguration->getIssuer()) {
$error = sprintf(
'Leaf entity ID %s is different than the subject %s of Trust Mark %s',
$leafEntityConfiguration->getIssuer(),
$trustMark->getSubject(),
$trustMark->getIdentifier(),
);
$this->logger?->error($error);
throw new TrustMarkException($error);
}

$this->logger?->debug(
sprintf(
'Leaf entity ID %s is the subject of the Trust Mark %s.',
$leafEntityConfiguration->getIssuer(),
$trustMark->getIdentifier(),
),
);
}

/**
* @throws \SimpleSAML\OpenID\Exceptions\EntityStatementException
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
* @throws \SimpleSAML\OpenID\Exceptions\TrustMarkException
* @throws \SimpleSAML\OpenID\Exceptions\TrustChainException
*/
public function validateTrustChainForTrustMarkIssuer(
TrustMark $trustMark,
EntityStatement $trustAnchorEntityConfiguration,
): TrustChain {
$this->logger?->debug(
sprintf(
'Validating Trust Mark Issuer %s by resolving its Trust Chain with Trust Anchor %s.',
$trustMark->getIssuer(),
$trustAnchorEntityConfiguration->getIssuer(),
),
);

try {
$trustMarkIssuerChain = $this->trustChainResolver->for(
$trustMark->getIssuer(),
[$trustAnchorEntityConfiguration->getIssuer()],
)->getShortest();
} catch (Throwable $exception) {
$error = sprintf(
'Error resolving Trust Chain for Issuer %s using Trust Anchor %s. Error was: %s',
$trustMark->getIssuer(),
$trustAnchorEntityConfiguration->getIssuer(),
$exception->getMessage(),
);

$this->logger?->error($error);
throw new TrustMarkException($error);
}

$this->logger?->debug(
sprintf(
'Successfully resolved Trust Chain for Issuer %s using Trust Anchor %s',
$trustMark->getIssuer(),
$trustAnchorEntityConfiguration->getIssuer(),
),
['resolvedTrustChainForIssuer' => $trustMarkIssuerChain->jsonSerialize()],
);

return $trustMarkIssuerChain;
}

public function validateTrustMarkSignature(
TrustMark $trustMark,
EntityStatement $trustMarkIssuerEntityStatement,
): void {
$this->logger?->debug('Validating Trust Mark signature.');
try {
$trustMark->verifyWithKeySet($trustMarkIssuerEntityStatement->getJwks());
} catch (Throwable $exception) {
$error = sprintf(
'Trust Mark signature validation failed with error: %s',
$exception->getMessage(),
);
$this->logger?->error(
$error,
['trustMarkIssuerJwks' => $trustMarkIssuerEntityStatement->getJwks()],
);
throw new TrustMarkException($error);
}
$this->logger?->debug('Trust Mark signature validated.');
}
}
Loading

0 comments on commit aba66cb

Please sign in to comment.