first commit

This commit is contained in:
2025-06-17 11:53:18 +02:00
commit 9f0f7ba12b
8804 changed files with 1369176 additions and 0 deletions

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018-2022 Spomky-Labs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService;
use Psr\Log\LoggerInterface;
interface CanLogData
{
public function setLogger(LoggerInterface $logger): void;
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\CertificateChain;
interface CertificateChainValidator
{
/**
* @param string[] $untrustedCertificates
* @param string[] $trustedCertificates
*/
public function check(array $untrustedCertificates, array $trustedCertificates): void;
}

View File

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\CertificateChain;
use function in_array;
use ParagonIE\ConstantTime\Base64;
use const PHP_EOL;
use function preg_replace;
class CertificateToolbox
{
/**
* @param string[] $data
*
* @return string[]
*/
public static function fixPEMStructures(array $data, string $type = 'CERTIFICATE'): array
{
return array_map(static fn ($d): string => self::fixPEMStructure($d, $type), $data);
}
public static function fixPEMStructure(string $data, string $type = 'CERTIFICATE'): string
{
if (str_contains($data, '-----BEGIN')) {
return trim($data);
}
$pem = '-----BEGIN ' . $type . '-----' . PHP_EOL;
$pem .= chunk_split($data, 64, PHP_EOL);
return $pem . ('-----END ' . $type . '-----' . PHP_EOL);
}
public static function convertPEMToDER(string $data): string
{
if (! str_contains($data, '-----BEGIN')) {
return $data;
}
$data = preg_replace('/[\-]{5}.*[\-]{5}[\r\n]*/', '', $data);
$data = preg_replace("/[\r\n]*/", '', $data);
return Base64::decode(trim($data), true);
}
public static function convertDERToPEM(string $data, string $type = 'CERTIFICATE'): string
{
if (str_contains($data, '-----BEGIN')) {
return $data;
}
$der = self::unusedBytesFix($data);
return self::fixPEMStructure(base64_encode($der), $type);
}
/**
* @param string[] $data
*
* @return string[]
*/
public static function convertAllDERToPEM(iterable $data, string $type = 'CERTIFICATE'): array
{
$certificates = [];
foreach ($data as $d) {
$certificates[] = self::convertDERToPEM($d, $type);
}
return $certificates;
}
private static function unusedBytesFix(string $data): string
{
$hash = hash('sha256', $data);
if (in_array($hash, self::getCertificateHashes(), true)) {
$data[mb_strlen($data, '8bit') - 257] = "\0";
}
return $data;
}
/**
* @return string[]
*/
private static function getCertificateHashes(): array
{
return [
'349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8',
'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f',
'1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae',
'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb',
'6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897',
'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511',
];
}
}

View File

@ -0,0 +1,271 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\CertificateChain;
use function count;
use DateTimeZone;
use function in_array;
use Lcobucci\Clock\Clock;
use Lcobucci\Clock\SystemClock;
use function parse_url;
use const PHP_EOL;
use const PHP_URL_SCHEME;
use Psr\Clock\ClockInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
use SpomkyLabs\Pki\CryptoEncoding\PEM;
use SpomkyLabs\Pki\X509\Certificate\Certificate;
use SpomkyLabs\Pki\X509\CertificationPath\CertificationPath;
use SpomkyLabs\Pki\X509\CertificationPath\PathValidation\PathValidationConfig;
use Throwable;
use Webauthn\MetadataService\Event\BeforeCertificateChainValidation;
use Webauthn\MetadataService\Event\CanDispatchEvents;
use Webauthn\MetadataService\Event\CertificateChainValidationFailed;
use Webauthn\MetadataService\Event\CertificateChainValidationSucceeded;
use Webauthn\MetadataService\Event\NullEventDispatcher;
use Webauthn\MetadataService\Exception\CertificateChainException;
use Webauthn\MetadataService\Exception\CertificateRevocationListException;
use Webauthn\MetadataService\Exception\InvalidCertificateException;
/**
* @final
*/
class PhpCertificateChainValidator implements CertificateChainValidator, CanDispatchEvents
{
private const MAX_VALIDATION_LENGTH = 5;
private readonly Clock|ClockInterface $clock;
private EventDispatcherInterface $dispatcher;
public function __construct(
private readonly ClientInterface $client,
private readonly RequestFactoryInterface $requestFactory,
null|Clock|ClockInterface $clock = null,
private readonly bool $allowFailures = true
) {
if ($clock === null) {
trigger_deprecation(
'web-auth/metadata-service',
'4.5.0',
'The parameter "$clock" will become mandatory in 5.0.0. Please set a valid PSR Clock implementation instead of "null".'
);
$clock = new SystemClock(new DateTimeZone('UTC'));
}
$this->clock = $clock;
$this->dispatcher = new NullEventDispatcher();
}
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->dispatcher = $eventDispatcher;
}
/**
* @param string[] $untrustedCertificates
* @param string[] $trustedCertificates
*/
public function check(array $untrustedCertificates, array $trustedCertificates): void
{
foreach ($trustedCertificates as $trustedCertificate) {
$this->dispatcher->dispatch(
BeforeCertificateChainValidation::create($untrustedCertificates, $trustedCertificate)
);
try {
if ($this->validateChain($untrustedCertificates, $trustedCertificate)) {
$this->dispatcher->dispatch(
CertificateChainValidationSucceeded::create($untrustedCertificates, $trustedCertificate)
);
return;
}
} catch (Throwable $exception) {
$this->dispatcher->dispatch(
CertificateChainValidationFailed::create($untrustedCertificates, $trustedCertificate)
);
throw $exception;
}
}
throw CertificateChainException::create($untrustedCertificates, $trustedCertificates);
}
/**
* @param string[] $untrustedCertificates
*/
private function validateChain(array $untrustedCertificates, string $trustedCertificate): bool
{
$untrustedCertificates = array_map(
static fn (string $cert): Certificate => Certificate::fromPEM(PEM::fromString($cert)),
array_reverse($untrustedCertificates)
);
$trustedCertificate = Certificate::fromPEM(PEM::fromString($trustedCertificate));
// The trust path and the authenticator certificate are the same
if (count(
$untrustedCertificates
) === 1 && $untrustedCertificates[0]->toPEM()->string() === $trustedCertificate->toPEM()->string()) {
return true;
}
$uniqueCertificates = array_map(
static fn (Certificate $cert): string => $cert->toPEM()
->string(),
array_merge($untrustedCertificates, [$trustedCertificate])
);
count(array_unique($uniqueCertificates)) === count(
$uniqueCertificates
) || throw CertificateChainException::create(
$untrustedCertificates,
[$trustedCertificate],
'Invalid certificate chain with duplicated certificates.'
);
if (! $this->validateCertificates($trustedCertificate, ...$untrustedCertificates)) {
return false;
}
$certificates = array_merge([$trustedCertificate], $untrustedCertificates);
$numCerts = count($certificates);
for ($i = 1; $i < $numCerts; $i++) {
if ($this->isRevoked($certificates[$i])) {
throw CertificateChainException::create(
$untrustedCertificates,
[$trustedCertificate],
'Unable to validate the certificate chain. Revoked certificate found.'
);
}
}
return true;
}
private function isRevoked(Certificate $subject): bool
{
try {
$csn = $subject->tbsCertificate()
->serialNumber();
} catch (Throwable $e) {
throw InvalidCertificateException::create(
$subject->toPEM()
->string(),
sprintf('Failed to parse certificate: %s', $e->getMessage()),
$e
);
}
try {
$urls = $this->getCrlUrlList($subject);
} catch (Throwable $e) {
if ($this->allowFailures) {
return false;
}
throw InvalidCertificateException::create(
$subject->toPEM()
->string(),
'Failed to get CRL distribution points: ' . $e->getMessage(),
$e
);
}
foreach ($urls as $url) {
try {
$revokedCertificates = $this->retrieveRevokedSerialNumbers($url);
if (in_array($csn, $revokedCertificates, true)) {
return true;
}
} catch (Throwable $e) {
if ($this->allowFailures) {
return false;
}
throw CertificateRevocationListException::create($url, sprintf(
'Failed to retrieve the CRL:' . PHP_EOL . '%s',
$e->getMessage()
), $e);
}
}
return false;
}
private function validateCertificates(Certificate ...$certificates): bool
{
try {
$config = PathValidationConfig::create($this->clock->now(), self::MAX_VALIDATION_LENGTH);
CertificationPath::create(...$certificates)->validate($config);
return true;
} catch (Throwable) {
return false;
}
}
/**
* @return string[]
*/
private function retrieveRevokedSerialNumbers(string $url): array
{
try {
$request = $this->requestFactory->createRequest('GET', $url);
$response = $this->client->sendRequest($request);
if ($response->getStatusCode() !== 200) {
throw CertificateRevocationListException::create($url, 'Failed to download the CRL');
}
$crlData = $response->getBody()
->getContents();
$crl = UnspecifiedType::fromDER($crlData)->asSequence();
count($crl) === 3 || throw CertificateRevocationListException::create($url, 'Invalid CRL.');
$tbsCertList = $crl->at(0)
->asSequence();
count($tbsCertList) >= 6 || throw CertificateRevocationListException::create($url, 'Invalid CRL.');
$list = $tbsCertList->at(5)
->asSequence();
return array_map(static function (UnspecifiedType $r) use ($url): string {
$sequence = $r->asSequence();
count($sequence) >= 1 || throw CertificateRevocationListException::create($url, 'Invalid CRL.');
return $sequence->at(0)
->asInteger()
->number();
}, $list->elements());
} catch (Throwable $e) {
throw CertificateRevocationListException::create($url, 'Failed to download the CRL', $e);
}
}
/**
* @return string[]
*/
private function getCrlUrlList(Certificate $subject): array
{
try {
$urls = [];
$extensions = $subject->tbsCertificate()
->extensions();
if ($extensions->hasCRLDistributionPoints()) {
$crlDists = $extensions->crlDistributionPoints();
foreach ($crlDists->distributionPoints() as $dist) {
$url = $dist->fullName()
->names()
->firstURI();
$scheme = parse_url($url, PHP_URL_SCHEME);
if (! in_array($scheme, ['http', 'https'], true)) {
continue;
}
$urls[] = $url;
}
}
return $urls;
} catch (Throwable $e) {
throw InvalidCertificateException::create(
$subject->toPEM()
->string(),
'Failed to get CRL distribution points from certificate: ' . $e->getMessage(),
$e
);
}
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Event;
final class BeforeCertificateChainValidation implements WebauthnEvent
{
/**
* @param string[] $untrustedCertificates
*/
public function __construct(
public readonly array $untrustedCertificates,
public readonly string $trustedCertificate
) {
}
/**
* @param string[] $untrustedCertificates
*/
public static function create(array $untrustedCertificates, string $trustedCertificate): self
{
return new self($untrustedCertificates, $trustedCertificate);
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Event;
use Psr\EventDispatcher\EventDispatcherInterface;
interface CanDispatchEvents
{
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void;
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Event;
final class CertificateChainValidationFailed implements WebauthnEvent
{
/**
* @param string[] $untrustedCertificates
*/
public function __construct(
public readonly array $untrustedCertificates,
public readonly string $trustedCertificate
) {
}
/**
* @param string[] $untrustedCertificates
*/
public static function create(array $untrustedCertificates, string $trustedCertificate): self
{
return new self($untrustedCertificates, $trustedCertificate);
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Event;
final class CertificateChainValidationSucceeded implements WebauthnEvent
{
/**
* @param string[] $untrustedCertificates
*/
public function __construct(
public readonly array $untrustedCertificates,
public readonly string $trustedCertificate
) {
}
/**
* @param string[] $untrustedCertificates
*/
public static function create(array $untrustedCertificates, string $trustedCertificate): self
{
return new self($untrustedCertificates, $trustedCertificate);
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Event;
use Webauthn\MetadataService\Statement\MetadataStatement;
final class MetadataStatementFound implements WebauthnEvent
{
public function __construct(
public readonly MetadataStatement $metadataStatement
) {
}
public static function create(MetadataStatement $metadataStatement): self
{
return new self($metadataStatement);
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Event;
use Psr\EventDispatcher\EventDispatcherInterface;
final class NullEventDispatcher implements EventDispatcherInterface
{
public function dispatch(object $event): object
{
return $event;
}
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Event;
interface WebauthnEvent
{
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Exception;
use Throwable;
class CertificateChainException extends MetadataServiceException
{
/**
* @param array<string> $untrustedCertificates
* @param array<string> $trustedCertificates
*/
public function __construct(
public readonly array $untrustedCertificates,
public readonly array $trustedCertificates,
string $message,
?Throwable $previous = null
) {
parent::__construct($message, $previous);
}
/**
* @param array<string> $untrustedCertificates
* @param array<string> $trustedCertificates
*/
public static function create(
array $untrustedCertificates,
array $trustedCertificates,
string $message = 'Unable to validate the certificate chain.',
?Throwable $previous = null
): self {
return new self($untrustedCertificates, $trustedCertificates, $message, $previous);
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Exception;
use Throwable;
class CertificateException extends MetadataServiceException
{
public function __construct(
public readonly string $certificate,
string $message,
?Throwable $previous = null
) {
parent::__construct($message, $previous);
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Exception;
use Throwable;
final class CertificateRevocationListException extends MetadataServiceException
{
public function __construct(
public readonly string $url,
string $message,
?Throwable $previous = null
) {
parent::__construct($message, $previous);
}
public static function create(string $url, string $message, ?Throwable $previous = null): self
{
return new self($url, $message, $previous);
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Exception;
use Throwable;
final class ExpiredCertificateException extends CertificateException
{
public static function create(
string $certificate,
string $message = 'Expired certificate',
?Throwable $previous = null
): self {
return new self($certificate, $message, $previous);
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Exception;
use Throwable;
final class InvalidCertificateException extends MetadataServiceException
{
public function __construct(
public readonly string $certificate,
string $message,
?Throwable $previous = null
) {
parent::__construct($message, $previous);
}
public static function create(string $certificate, string $message, ?Throwable $previous = null): self
{
return new self($certificate, $message, $previous);
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Exception;
use Exception;
use Throwable;
class MetadataServiceException extends Exception
{
public function __construct(string $message, ?Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
}
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Exception;
class MetadataStatementException extends MetadataServiceException
{
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Exception;
use Throwable;
final class MetadataStatementLoadingException extends MetadataStatementException
{
public static function create(string $message, ?Throwable $previous = null): self
{
return new self($message, $previous);
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Exception;
use Throwable;
final class MissingMetadataStatementException extends MetadataStatementException
{
public function __construct(
public readonly string $aaguid,
string $message,
?Throwable $previous = null
) {
parent::__construct($message, $previous);
}
public static function create(
string $aaguid,
string $message = 'The Metadata Statement is missing',
?Throwable $previous = null
): self {
return new self($aaguid, $message, $previous);
}
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Exception;
final class RevokedCertificateException extends CertificateException
{
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService;
use Webauthn\MetadataService\Statement\MetadataStatement;
interface MetadataStatementRepository
{
public function findOneByAAGUID(string $aaguid): ?MetadataStatement;
}

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Service;
use Webauthn\MetadataService\Exception\MissingMetadataStatementException;
use Webauthn\MetadataService\Statement\MetadataStatement;
final class ChainedMetadataServices implements MetadataService
{
/**
* @var MetadataService[]
*/
private array $services = [];
public function __construct(MetadataService ...$services)
{
foreach ($services as $service) {
$this->addServices($service);
}
}
public static function create(MetadataService ...$services): self
{
return new self(...$services);
}
public function addServices(MetadataService ...$services): self
{
foreach ($services as $service) {
$this->services[] = $service;
}
return $this;
}
public function list(): iterable
{
foreach ($this->services as $service) {
yield from $service->list();
}
}
public function has(string $aaguid): bool
{
foreach ($this->services as $service) {
if ($service->has($aaguid)) {
return true;
}
}
return false;
}
public function get(string $aaguid): MetadataStatement
{
foreach ($this->services as $service) {
if ($service->has($aaguid)) {
return $service->get($aaguid);
}
}
throw MissingMetadataStatementException::create($aaguid);
}
}

View File

@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Service;
use ParagonIE\ConstantTime\Base64;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use function sprintf;
use Webauthn\MetadataService\Event\CanDispatchEvents;
use Webauthn\MetadataService\Event\MetadataStatementFound;
use Webauthn\MetadataService\Event\NullEventDispatcher;
use Webauthn\MetadataService\Exception\MetadataStatementLoadingException;
use Webauthn\MetadataService\Exception\MissingMetadataStatementException;
use Webauthn\MetadataService\Statement\MetadataStatement;
final class DistantResourceMetadataService implements MetadataService, CanDispatchEvents
{
private ?MetadataStatement $statement = null;
private EventDispatcherInterface $dispatcher;
/**
* @param array<string, string> $additionalHeaderParameters
*/
public function __construct(
private readonly RequestFactoryInterface $requestFactory,
private readonly ClientInterface $httpClient,
private readonly string $uri,
private readonly bool $isBase64Encoded = false,
private readonly array $additionalHeaderParameters = [],
) {
$this->dispatcher = new NullEventDispatcher();
}
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->dispatcher = $eventDispatcher;
}
/**
* @param array<string, mixed> $additionalHeaderParameters
*/
public static function create(
RequestFactoryInterface $requestFactory,
ClientInterface $httpClient,
string $uri,
bool $isBase64Encoded = false,
array $additionalHeaderParameters = []
): self {
return new self($requestFactory, $httpClient, $uri, $isBase64Encoded, $additionalHeaderParameters);
}
public function list(): iterable
{
$this->loadData();
$this->statement !== null || throw MetadataStatementLoadingException::create(
'Unable to load the metadata statement'
);
$aaguid = $this->statement->getAaguid();
if ($aaguid === null) {
yield from [];
} else {
yield from [$aaguid];
}
}
public function has(string $aaguid): bool
{
$this->loadData();
$this->statement !== null || throw MetadataStatementLoadingException::create(
'Unable to load the metadata statement'
);
return $aaguid === $this->statement->getAaguid();
}
public function get(string $aaguid): MetadataStatement
{
$this->loadData();
$this->statement !== null || throw MetadataStatementLoadingException::create(
'Unable to load the metadata statement'
);
if ($aaguid === $this->statement->getAaguid()) {
$this->dispatcher->dispatch(MetadataStatementFound::create($this->statement));
return $this->statement;
}
throw MissingMetadataStatementException::create($aaguid);
}
private function loadData(): void
{
if ($this->statement !== null) {
return;
}
$content = $this->fetch();
if ($this->isBase64Encoded) {
$content = Base64::decode($content, true);
}
$this->statement = MetadataStatement::createFromString($content);
}
private function fetch(): string
{
$request = $this->requestFactory->createRequest('GET', $this->uri);
foreach ($this->additionalHeaderParameters as $k => $v) {
$request = $request->withHeader($k, $v);
}
$response = $this->httpClient->sendRequest($request);
$response->getStatusCode() === 200 || throw MetadataStatementLoadingException::create(sprintf(
'Unable to contact the server. Response code is %d',
$response->getStatusCode()
));
$response->getBody()
->rewind();
$content = $response->getBody()
->getContents();
$content !== '' || throw MetadataStatementLoadingException::create(
'Unable to contact the server. The response has no content'
);
return $content;
}
}

View File

@ -0,0 +1,226 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Service;
use function array_key_exists;
use function is_array;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\Algorithm\ES256;
use Jose\Component\Signature\Algorithm\RS256;
use Jose\Component\Signature\JWSVerifier;
use Jose\Component\Signature\Serializer\CompactSerializer;
use const JSON_THROW_ON_ERROR;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use function sprintf;
use Throwable;
use Webauthn\MetadataService\CertificateChain\CertificateChainValidator;
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
use Webauthn\MetadataService\Event\CanDispatchEvents;
use Webauthn\MetadataService\Event\MetadataStatementFound;
use Webauthn\MetadataService\Event\NullEventDispatcher;
use Webauthn\MetadataService\Exception\MetadataStatementLoadingException;
use Webauthn\MetadataService\Exception\MissingMetadataStatementException;
use Webauthn\MetadataService\Statement\MetadataStatement;
use Webauthn\MetadataService\Statement\StatusReport;
final class FidoAllianceCompliantMetadataService implements MetadataService, CanDispatchEvents
{
private bool $loaded = false;
/**
* @var MetadataStatement[]
*/
private array $statements = [];
/**
* @var array<string, array<int, StatusReport>>
*/
private array $statusReports = [];
private EventDispatcherInterface $dispatcher;
/**
* @param array<string, mixed> $additionalHeaderParameters
*/
public function __construct(
private readonly RequestFactoryInterface $requestFactory,
private readonly ClientInterface $httpClient,
private readonly string $uri,
private readonly array $additionalHeaderParameters = [],
private readonly ?CertificateChainValidator $certificateChainValidator = null,
private readonly ?string $rootCertificateUri = null,
) {
$this->dispatcher = new NullEventDispatcher();
}
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->dispatcher = $eventDispatcher;
}
/**
* @param array<string, mixed> $additionalHeaderParameters
*/
public static function create(
RequestFactoryInterface $requestFactory,
ClientInterface $httpClient,
string $uri,
array $additionalHeaderParameters = [],
?CertificateChainValidator $certificateChainValidator = null,
?string $rootCertificateUri = null,
): self {
return new self(
$requestFactory,
$httpClient,
$uri,
$additionalHeaderParameters,
$certificateChainValidator,
$rootCertificateUri
);
}
/**
* @return string[]
*/
public function list(): iterable
{
$this->loadData();
yield from array_keys($this->statements);
}
public function has(string $aaguid): bool
{
$this->loadData();
return array_key_exists($aaguid, $this->statements);
}
public function get(string $aaguid): MetadataStatement
{
$this->loadData();
array_key_exists($aaguid, $this->statements) || throw MissingMetadataStatementException::create($aaguid);
$mds = $this->statements[$aaguid];
$this->dispatcher->dispatch(MetadataStatementFound::create($mds));
return $mds;
}
/**
* @return StatusReport[]
*/
public function getStatusReports(string $aaguid): iterable
{
$this->loadData();
return $this->statusReports[$aaguid] ?? [];
}
private function loadData(): void
{
if ($this->loaded) {
return;
}
$content = $this->fetch($this->uri, $this->additionalHeaderParameters);
$jwtCertificates = [];
try {
$payload = $this->getJwsPayload($content, $jwtCertificates);
$data = json_decode($payload, true, 512, JSON_THROW_ON_ERROR);
$this->validateCertificates(...$jwtCertificates);
foreach ($data['entries'] as $datum) {
$entry = MetadataBLOBPayloadEntry::createFromArray($datum);
$mds = $entry->getMetadataStatement();
if ($mds !== null && $entry->getAaguid() !== null) {
$this->statements[$entry->getAaguid()] = $mds;
$this->statusReports[$entry->getAaguid()] = $entry->getStatusReports();
}
}
} catch (Throwable) {
}
$this->loaded = true;
}
/**
* @param array<string, mixed> $headerParameters
*/
private function fetch(string $uri, array $headerParameters): string
{
$request = $this->requestFactory->createRequest('GET', $uri);
foreach ($headerParameters as $k => $v) {
$request = $request->withHeader($k, $v);
}
$response = $this->httpClient->sendRequest($request);
$response->getStatusCode() === 200 || throw MetadataStatementLoadingException::create(sprintf(
'Unable to contact the server. Response code is %d',
$response->getStatusCode()
));
$response->getBody()
->rewind();
$content = $response->getBody()
->getContents();
$content !== '' || throw MetadataStatementLoadingException::create(
'Unable to contact the server. The response has no content'
);
return $content;
}
/**
* @param string[] $rootCertificates
*/
private function getJwsPayload(string $token, array &$rootCertificates): string
{
$jws = (new CompactSerializer())->unserialize($token);
$jws->countSignatures() === 1 || throw MetadataStatementLoadingException::create(
'Invalid response from the metadata service. Only one signature shall be present.'
);
$signature = $jws->getSignature(0);
$payload = $jws->getPayload();
$payload !== '' || throw MetadataStatementLoadingException::create(
'Invalid response from the metadata service. The token payload is empty.'
);
$header = $signature->getProtectedHeader();
array_key_exists('alg', $header) || throw MetadataStatementLoadingException::create(
'The "alg" parameter is missing.'
);
array_key_exists('x5c', $header) || throw MetadataStatementLoadingException::create(
'The "x5c" parameter is missing.'
);
is_array($header['x5c']) || throw MetadataStatementLoadingException::create(
'The "x5c" parameter should be an array.'
);
$key = JWKFactory::createFromX5C($header['x5c']);
$rootCertificates = $header['x5c'];
$verifier = new JWSVerifier(new AlgorithmManager([new ES256(), new RS256()]));
$isValid = $verifier->verifyWithKey($jws, $key, 0);
$isValid || throw MetadataStatementLoadingException::create(
'Invalid response from the metadata service. The token signature is invalid.'
);
$payload = $jws->getPayload();
$payload !== null || throw MetadataStatementLoadingException::create(
'Invalid response from the metadata service. The payload is missing.'
);
return $payload;
}
private function validateCertificates(string ...$untrustedCertificates): void
{
if ($this->certificateChainValidator === null || $this->rootCertificateUri === null) {
return;
}
$untrustedCertificates = CertificateToolbox::fixPEMStructures($untrustedCertificates);
$rootCertificate = CertificateToolbox::convertDERToPEM($this->fetch($this->rootCertificateUri, []));
$this->certificateChainValidator->check($untrustedCertificates, [$rootCertificate]);
}
}

View File

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Service;
use const DIRECTORY_SEPARATOR;
use function file_get_contents;
use InvalidArgumentException;
use function is_array;
use RuntimeException;
use function sprintf;
use Webauthn\MetadataService\Statement\MetadataStatement;
final class FolderResourceMetadataService implements MetadataService
{
private readonly string $rootPath;
public function __construct(string $rootPath)
{
$this->rootPath = rtrim($rootPath, DIRECTORY_SEPARATOR);
is_dir($this->rootPath) || throw new InvalidArgumentException('The given parameter is not a valid folder.');
is_readable($this->rootPath) || throw new InvalidArgumentException(
'The given parameter is not a valid folder.'
);
}
public function list(): iterable
{
$files = glob($this->rootPath . DIRECTORY_SEPARATOR . '*');
is_array($files) || throw new RuntimeException('Unable to read files.');
foreach ($files as $file) {
if (is_dir($file) || ! is_readable($file)) {
continue;
}
yield basename($file);
}
}
public function has(string $aaguid): bool
{
$filename = $this->rootPath . DIRECTORY_SEPARATOR . $aaguid;
return is_file($filename) && is_readable($filename);
}
public function get(string $aaguid): MetadataStatement
{
$this->has($aaguid) || throw new InvalidArgumentException(sprintf(
'The MDS with the AAGUID "%s" does not exist.',
$aaguid
));
$filename = $this->rootPath . DIRECTORY_SEPARATOR . $aaguid;
$data = trim(file_get_contents($filename));
$mds = MetadataStatement::createFromString($data);
$mds->getAaguid() !== null || throw new RuntimeException('Invalid Metadata Statement.');
return $mds;
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Service;
use function array_key_exists;
use Psr\EventDispatcher\EventDispatcherInterface;
use Webauthn\MetadataService\Event\CanDispatchEvents;
use Webauthn\MetadataService\Event\MetadataStatementFound;
use Webauthn\MetadataService\Event\NullEventDispatcher;
use Webauthn\MetadataService\Exception\MissingMetadataStatementException;
use Webauthn\MetadataService\Statement\MetadataStatement;
final class InMemoryMetadataService implements MetadataService, CanDispatchEvents
{
/**
* @var MetadataStatement[]
*/
private array $statements = [];
private EventDispatcherInterface $dispatcher;
public function __construct(MetadataStatement ...$statements)
{
foreach ($statements as $statement) {
$this->addStatements($statement);
}
$this->dispatcher = new NullEventDispatcher();
}
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->dispatcher = $eventDispatcher;
}
public static function create(MetadataStatement ...$statements): self
{
return new self(...$statements);
}
public function addStatements(MetadataStatement ...$statements): self
{
foreach ($statements as $statement) {
$aaguid = $statement->getAaguid();
if ($aaguid === null) {
continue;
}
$this->statements[$aaguid] = $statement;
}
return $this;
}
public function list(): iterable
{
yield from array_keys($this->statements);
}
public function has(string $aaguid): bool
{
return array_key_exists($aaguid, $this->statements);
}
public function get(string $aaguid): MetadataStatement
{
array_key_exists($aaguid, $this->statements) || throw MissingMetadataStatementException::create($aaguid);
$mds = $this->statements[$aaguid];
$this->dispatcher->dispatch(MetadataStatementFound::create($mds));
return $mds;
}
}

View File

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Service;
use function file_get_contents;
use ParagonIE\ConstantTime\Base64;
use Psr\EventDispatcher\EventDispatcherInterface;
use Webauthn\MetadataService\Event\CanDispatchEvents;
use Webauthn\MetadataService\Event\MetadataStatementFound;
use Webauthn\MetadataService\Event\NullEventDispatcher;
use Webauthn\MetadataService\Exception\MetadataStatementLoadingException;
use Webauthn\MetadataService\Exception\MissingMetadataStatementException;
use Webauthn\MetadataService\Statement\MetadataStatement;
final class LocalResourceMetadataService implements MetadataService, CanDispatchEvents
{
private ?MetadataStatement $statement = null;
private EventDispatcherInterface $dispatcher;
public function __construct(
private readonly string $filename,
private readonly bool $isBase64Encoded = false,
) {
$this->dispatcher = new NullEventDispatcher();
}
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->dispatcher = $eventDispatcher;
}
public static function create(string $filename, bool $isBase64Encoded = false): self
{
return new self($filename, $isBase64Encoded);
}
public function list(): iterable
{
$this->loadData();
$this->statement !== null || throw MetadataStatementLoadingException::create(
'Unable to load the metadata statement'
);
$aaguid = $this->statement->getAaguid();
if ($aaguid === null) {
yield from [];
} else {
yield from [$aaguid];
}
}
public function has(string $aaguid): bool
{
$this->loadData();
$this->statement !== null || throw MetadataStatementLoadingException::create(
'Unable to load the metadata statement'
);
return $aaguid === $this->statement->getAaguid();
}
public function get(string $aaguid): MetadataStatement
{
$this->loadData();
$this->statement !== null || throw MetadataStatementLoadingException::create(
'Unable to load the metadata statement'
);
if ($aaguid === $this->statement->getAaguid()) {
$this->dispatcher->dispatch(MetadataStatementFound::create($this->statement));
return $this->statement;
}
throw MissingMetadataStatementException::create($aaguid);
}
private function loadData(): void
{
if ($this->statement !== null) {
return;
}
$content = file_get_contents($this->filename);
if ($this->isBase64Encoded) {
$content = Base64::decode($content, true);
}
$this->statement = MetadataStatement::createFromString($content);
}
}

View File

@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Service;
use function array_key_exists;
use function is_array;
use function is_int;
use function is_string;
use JsonSerializable;
use Webauthn\MetadataService\Exception\MetadataStatementLoadingException;
use Webauthn\MetadataService\Utils;
/**
* @final
*/
class MetadataBLOBPayload implements JsonSerializable
{
/**
* @var MetadataBLOBPayloadEntry[]
*/
private array $entries = [];
/**
* @var string[]
*/
private array $rootCertificates = [];
public function __construct(
private readonly int $no,
private readonly string $nextUpdate,
private readonly ?string $legalHeader = null
) {
}
public function addEntry(MetadataBLOBPayloadEntry $entry): self
{
$this->entries[] = $entry;
return $this;
}
public function getLegalHeader(): ?string
{
return $this->legalHeader;
}
public function getNo(): int
{
return $this->no;
}
public function getNextUpdate(): string
{
return $this->nextUpdate;
}
/**
* @return MetadataBLOBPayloadEntry[]
*/
public function getEntries(): array
{
return $this->entries;
}
/**
* @param array<string, mixed> $data
*/
public static function createFromArray(array $data): self
{
$data = Utils::filterNullValues($data);
foreach (['no', 'nextUpdate', 'entries'] as $key) {
array_key_exists($key, $data) || throw MetadataStatementLoadingException::create(sprintf(
'Invalid data. The parameter "%s" is missing',
$key
));
}
is_int($data['no']) || throw MetadataStatementLoadingException::create(
'Invalid data. The parameter "no" shall be an integer'
);
is_string($data['nextUpdate']) || throw MetadataStatementLoadingException::create(
'Invalid data. The parameter "nextUpdate" shall be a string'
);
is_array($data['entries']) || throw MetadataStatementLoadingException::create(
'Invalid data. The parameter "entries" shall be a n array of entries'
);
$object = new self($data['no'], $data['nextUpdate'], $data['legalHeader'] ?? null);
foreach ($data['entries'] as $entry) {
$object->addEntry(MetadataBLOBPayloadEntry::createFromArray($entry));
}
return $object;
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
$data = [
'legalHeader' => $this->legalHeader,
'nextUpdate' => $this->nextUpdate,
'no' => $this->no,
'entries' => array_map(
static fn (MetadataBLOBPayloadEntry $object): array => $object->jsonSerialize(),
$this->entries
),
];
return Utils::filterNullValues($data);
}
/**
* @return string[]
*/
public function getRootCertificates(): array
{
return $this->rootCertificates;
}
/**
* @param string[] $rootCertificates
*/
public function setRootCertificates(array $rootCertificates): self
{
$this->rootCertificates = $rootCertificates;
return $this;
}
}

View File

@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Service;
use function array_key_exists;
use function count;
use function is_array;
use function is_string;
use JsonSerializable;
use Webauthn\MetadataService\Exception\MetadataStatementLoadingException;
use Webauthn\MetadataService\Statement\BiometricStatusReport;
use Webauthn\MetadataService\Statement\MetadataStatement;
use Webauthn\MetadataService\Statement\StatusReport;
use Webauthn\MetadataService\Utils;
/**
* @final
*/
class MetadataBLOBPayloadEntry implements JsonSerializable
{
/**
* @var string[]
*/
private array $attestationCertificateKeyIdentifiers = [];
/**
* @var BiometricStatusReport[]
*/
private array $biometricStatusReports = [];
/**
* @var StatusReport[]
*/
private array $statusReports = [];
/**
* @param string[] $attestationCertificateKeyIdentifiers
*/
public function __construct(
private readonly ?string $aaid,
private readonly ?string $aaguid,
array $attestationCertificateKeyIdentifiers,
private readonly ?MetadataStatement $metadataStatement,
private readonly string $timeOfLastStatusChange,
private readonly ?string $rogueListURL,
private readonly ?string $rogueListHash
) {
if ($aaid !== null && $aaguid !== null) {
throw MetadataStatementLoadingException::create('Authenticators cannot support both AAID and AAGUID');
}
if ($aaid === null && $aaguid === null && count($attestationCertificateKeyIdentifiers) === 0) {
throw MetadataStatementLoadingException::create(
'If neither AAID nor AAGUID are set, the attestation certificate identifier list shall not be empty'
);
}
foreach ($attestationCertificateKeyIdentifiers as $attestationCertificateKeyIdentifier) {
is_string($attestationCertificateKeyIdentifier) || throw MetadataStatementLoadingException::create(
'Invalid attestation certificate identifier. Shall be a list of strings'
);
preg_match(
'/^[0-9a-f]+$/',
$attestationCertificateKeyIdentifier
) === 1 || throw MetadataStatementLoadingException::create(
'Invalid attestation certificate identifier. Shall be a list of strings'
);
}
$this->attestationCertificateKeyIdentifiers = $attestationCertificateKeyIdentifiers;
}
public function getAaid(): ?string
{
return $this->aaid;
}
public function getAaguid(): ?string
{
return $this->aaguid;
}
/**
* @return string[]
*/
public function getAttestationCertificateKeyIdentifiers(): array
{
return $this->attestationCertificateKeyIdentifiers;
}
public function getMetadataStatement(): ?MetadataStatement
{
return $this->metadataStatement;
}
public function addBiometricStatusReports(BiometricStatusReport ...$biometricStatusReports): self
{
foreach ($biometricStatusReports as $biometricStatusReport) {
$this->biometricStatusReports[] = $biometricStatusReport;
}
return $this;
}
/**
* @return BiometricStatusReport[]
*/
public function getBiometricStatusReports(): array
{
return $this->biometricStatusReports;
}
public function addStatusReports(StatusReport ...$statusReports): self
{
foreach ($statusReports as $statusReport) {
$this->statusReports[] = $statusReport;
}
return $this;
}
/**
* @return StatusReport[]
*/
public function getStatusReports(): array
{
return $this->statusReports;
}
public function getTimeOfLastStatusChange(): string
{
return $this->timeOfLastStatusChange;
}
public function getRogueListURL(): string|null
{
return $this->rogueListURL;
}
public function getRogueListHash(): string|null
{
return $this->rogueListHash;
}
/**
* @param array<string, mixed> $data
*/
public static function createFromArray(array $data): self
{
$data = Utils::filterNullValues($data);
array_key_exists('timeOfLastStatusChange', $data) || throw MetadataStatementLoadingException::create(
'Invalid data. The parameter "timeOfLastStatusChange" is missing'
);
array_key_exists('statusReports', $data) || throw MetadataStatementLoadingException::create(
'Invalid data. The parameter "statusReports" is missing'
);
is_array($data['statusReports']) || throw MetadataStatementLoadingException::create(
'Invalid data. The parameter "statusReports" shall be an array of StatusReport objects'
);
$object = new self(
$data['aaid'] ?? null,
$data['aaguid'] ?? null,
$data['attestationCertificateKeyIdentifiers'] ?? [],
isset($data['metadataStatement']) ? MetadataStatement::createFromArray($data['metadataStatement']) : null,
$data['timeOfLastStatusChange'],
$data['rogueListURL'] ?? null,
$data['rogueListHash'] ?? null
);
foreach ($data['statusReports'] as $statusReport) {
$object->addStatusReports(StatusReport::createFromArray($statusReport));
}
if (array_key_exists('biometricStatusReport', $data)) {
foreach ($data['biometricStatusReport'] as $biometricStatusReport) {
$object->addBiometricStatusReports(BiometricStatusReport::createFromArray($biometricStatusReport));
}
}
return $object;
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
$data = [
'aaid' => $this->aaid,
'aaguid' => $this->aaguid,
'attestationCertificateKeyIdentifiers' => $this->attestationCertificateKeyIdentifiers,
'statusReports' => array_map(
static fn (StatusReport $object): array => $object->jsonSerialize(),
$this->statusReports
),
'timeOfLastStatusChange' => $this->timeOfLastStatusChange,
'rogueListURL' => $this->rogueListURL,
'rogueListHash' => $this->rogueListHash,
];
return Utils::filterNullValues($data);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Service;
use Webauthn\MetadataService\Statement\MetadataStatement;
interface MetadataService
{
/**
* @return string[] The list of AAGUID supported by the service
*/
public function list(): iterable;
public function has(string $aaguid): bool;
public function get(string $aaguid): MetadataStatement;
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Service;
use function array_key_exists;
use Psr\EventDispatcher\EventDispatcherInterface;
use Webauthn\MetadataService\Event\CanDispatchEvents;
use Webauthn\MetadataService\Event\MetadataStatementFound;
use Webauthn\MetadataService\Event\NullEventDispatcher;
use Webauthn\MetadataService\Exception\MissingMetadataStatementException;
use Webauthn\MetadataService\Statement\MetadataStatement;
final class StringMetadataService implements MetadataService, CanDispatchEvents
{
/**
* @var MetadataStatement[]
*/
private array $statements = [];
private EventDispatcherInterface $dispatcher;
public function __construct(string ...$statements)
{
foreach ($statements as $statement) {
$this->addStatements(MetadataStatement::createFromString($statement));
}
$this->dispatcher = new NullEventDispatcher();
}
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->dispatcher = $eventDispatcher;
}
public static function create(string ...$statements): self
{
return new self(...$statements);
}
public function addStatements(MetadataStatement ...$statements): self
{
foreach ($statements as $statement) {
$aaguid = $statement->getAaguid();
if ($aaguid === null) {
continue;
}
$this->statements[$aaguid] = $statement;
}
return $this;
}
public function list(): iterable
{
yield from array_keys($this->statements);
}
public function has(string $aaguid): bool
{
return array_key_exists($aaguid, $this->statements);
}
public function get(string $aaguid): MetadataStatement
{
array_key_exists($aaguid, $this->statements) || throw MissingMetadataStatementException::create($aaguid);
$mds = $this->statements[$aaguid];
$this->dispatcher->dispatch(MetadataStatementFound::create($mds));
return $mds;
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Statement;
use JsonSerializable;
use Webauthn\MetadataService\Exception\MetadataStatementLoadingException;
abstract class AbstractDescriptor implements JsonSerializable
{
private readonly ?int $maxRetries;
private readonly ?int $blockSlowdown;
public function __construct(?int $maxRetries = null, ?int $blockSlowdown = null)
{
$maxRetries >= 0 || throw MetadataStatementLoadingException::create(
'Invalid data. The value of "maxRetries" must be a positive integer'
);
$blockSlowdown >= 0 || throw MetadataStatementLoadingException::create(
'Invalid data. The value of "blockSlowdown" must be a positive integer'
);
$this->maxRetries = $maxRetries;
$this->blockSlowdown = $blockSlowdown;
}
public function getMaxRetries(): ?int
{
return $this->maxRetries;
}
public function getBlockSlowdown(): ?int
{
return $this->blockSlowdown;
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Statement;
use JsonSerializable;
/**
* @final
*/
class AlternativeDescriptions implements JsonSerializable
{
/**
* @var array<string, string>
*/
private array $descriptions = [];
/**
* @param array<string, string> $descriptions
*/
public static function create(array $descriptions = []): self
{
$object = new self();
foreach ($descriptions as $k => $v) {
$object->add($k, $v);
}
return $object;
}
/**
* @return array<string, string>
*/
public function all(): array
{
return $this->descriptions;
}
public function add(string $locale, string $description): self
{
$this->descriptions[$locale] = $description;
return $this;
}
/**
* @return array<string, string>
*/
public function jsonSerialize(): array
{
return $this->descriptions;
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Statement;
use JsonSerializable;
/**
* @final
*/
class AuthenticatorGetInfo implements JsonSerializable
{
/**
* @var string[]
*/
private array $info = [];
/**
* @param array<string|int, mixed> $data
*/
public static function create(array $data = []): self
{
$object = new self();
foreach ($data as $k => $v) {
$object->add($k, $v);
}
return $object;
}
public function add(string|int $key, mixed $value): self
{
$this->info[$key] = $value;
return $this;
}
/**
* @return string[]
*/
public function jsonSerialize(): array
{
return $this->info;
}
}

View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Statement;
abstract class AuthenticatorStatus
{
final public const NOT_FIDO_CERTIFIED = 'NOT_FIDO_CERTIFIED';
final public const FIDO_CERTIFIED = 'FIDO_CERTIFIED';
final public const USER_VERIFICATION_BYPASS = 'USER_VERIFICATION_BYPASS';
final public const ATTESTATION_KEY_COMPROMISE = 'ATTESTATION_KEY_COMPROMISE';
final public const USER_KEY_REMOTE_COMPROMISE = 'USER_KEY_REMOTE_COMPROMISE';
final public const USER_KEY_PHYSICAL_COMPROMISE = 'USER_KEY_PHYSICAL_COMPROMISE';
final public const UPDATE_AVAILABLE = 'UPDATE_AVAILABLE';
final public const REVOKED = 'REVOKED';
final public const SELF_ASSERTION_SUBMITTED = 'SELF_ASSERTION_SUBMITTED';
final public const FIDO_CERTIFIED_L1 = 'FIDO_CERTIFIED_L1';
final public const FIDO_CERTIFIED_L1plus = 'FIDO_CERTIFIED_L1plus';
final public const FIDO_CERTIFIED_L2 = 'FIDO_CERTIFIED_L2';
final public const FIDO_CERTIFIED_L2plus = 'FIDO_CERTIFIED_L2plus';
final public const FIDO_CERTIFIED_L3 = 'FIDO_CERTIFIED_L3';
final public const FIDO_CERTIFIED_L3plus = 'FIDO_CERTIFIED_L3plus';
final public const FIDO_CERTIFIED_L4 = 'FIDO_CERTIFIED_L4';
final public const FIDO_CERTIFIED_L5 = 'FIDO_CERTIFIED_L5';
/**
* @return string[]
*/
public static function list(): array
{
return [
self::NOT_FIDO_CERTIFIED,
self::FIDO_CERTIFIED,
self::USER_VERIFICATION_BYPASS,
self::ATTESTATION_KEY_COMPROMISE,
self::USER_KEY_REMOTE_COMPROMISE,
self::USER_KEY_PHYSICAL_COMPROMISE,
self::UPDATE_AVAILABLE,
self::REVOKED,
self::SELF_ASSERTION_SUBMITTED,
self::FIDO_CERTIFIED_L1,
self::FIDO_CERTIFIED_L1plus,
self::FIDO_CERTIFIED_L2,
self::FIDO_CERTIFIED_L2plus,
self::FIDO_CERTIFIED_L3,
self::FIDO_CERTIFIED_L3plus,
self::FIDO_CERTIFIED_L4,
self::FIDO_CERTIFIED_L5,
];
}
}

View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Statement;
use Webauthn\MetadataService\Utils;
/**
* @final
*/
class BiometricAccuracyDescriptor extends AbstractDescriptor
{
public function __construct(
private readonly ?float $selfAttestedFRR,
private readonly ?float $selfAttestedFAR,
private readonly ?float $maxTemplates,
?int $maxRetries = null,
?int $blockSlowdown = null
) {
parent::__construct($maxRetries, $blockSlowdown);
}
public function getSelfAttestedFRR(): ?float
{
return $this->selfAttestedFRR;
}
public function getSelfAttestedFAR(): ?float
{
return $this->selfAttestedFAR;
}
public function getMaxTemplates(): ?float
{
return $this->maxTemplates;
}
/**
* @param array<string, mixed> $data
*/
public static function createFromArray(array $data): self
{
return new self(
$data['selfAttestedFRR'] ?? null,
$data['selfAttestedFAR'] ?? null,
$data['maxTemplates'] ?? null,
$data['maxRetries'] ?? null,
$data['blockSlowdown'] ?? null
);
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
$data = [
'selfAttestedFRR' => $this->selfAttestedFRR,
'selfAttestedFAR' => $this->selfAttestedFAR,
'maxTemplates' => $this->maxTemplates,
'maxRetries' => $this->getMaxRetries(),
'blockSlowdown' => $this->getBlockSlowdown(),
];
return Utils::filterNullValues($data);
}
}

View File

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Statement;
use JsonSerializable;
/**
* @final
*/
class BiometricStatusReport implements JsonSerializable
{
private ?int $certLevel = null;
private ?int $modality = null;
private ?string $effectiveDate = null;
private ?string $certificationDescriptor = null;
private ?string $certificateNumber = null;
private ?string $certificationPolicyVersion = null;
private ?string $certificationRequirementsVersion = null;
public function getCertLevel(): int|null
{
return $this->certLevel;
}
public function getModality(): int|null
{
return $this->modality;
}
public function getEffectiveDate(): ?string
{
return $this->effectiveDate;
}
public function getCertificationDescriptor(): ?string
{
return $this->certificationDescriptor;
}
public function getCertificateNumber(): ?string
{
return $this->certificateNumber;
}
public function getCertificationPolicyVersion(): ?string
{
return $this->certificationPolicyVersion;
}
public function getCertificationRequirementsVersion(): ?string
{
return $this->certificationRequirementsVersion;
}
/**
* @param array<string, mixed> $data
*/
public static function createFromArray(array $data): self
{
$object = new self();
$object->certLevel = $data['certLevel'] ?? null;
$object->modality = $data['modality'] ?? null;
$object->effectiveDate = $data['effectiveDate'] ?? null;
$object->certificationDescriptor = $data['certificationDescriptor'] ?? null;
$object->certificateNumber = $data['certificateNumber'] ?? null;
$object->certificationPolicyVersion = $data['certificationPolicyVersion'] ?? null;
$object->certificationRequirementsVersion = $data['certificationRequirementsVersion'] ?? null;
return $object;
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
$data = [
'certLevel' => $this->certLevel,
'modality' => $this->modality,
'effectiveDate' => $this->effectiveDate,
'certificationDescriptor' => $this->certificationDescriptor,
'certificateNumber' => $this->certificateNumber,
'certificationPolicyVersion' => $this->certificationPolicyVersion,
'certificationRequirementsVersion' => $this->certificationRequirementsVersion,
];
return array_filter($data, static fn ($var): bool => $var !== null);
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Statement;
use function array_key_exists;
use Webauthn\MetadataService\Exception\MetadataStatementLoadingException;
use Webauthn\MetadataService\Utils;
/**
* @final
*/
class CodeAccuracyDescriptor extends AbstractDescriptor
{
private readonly int $base;
private readonly int $minLength;
public function __construct(int $base, int $minLength, ?int $maxRetries = null, ?int $blockSlowdown = null)
{
$base >= 0 || throw MetadataStatementLoadingException::create(
'Invalid data. The value of "base" must be a positive integer'
);
$minLength >= 0 || throw MetadataStatementLoadingException::create(
'Invalid data. The value of "minLength" must be a positive integer'
);
$this->base = $base;
$this->minLength = $minLength;
parent::__construct($maxRetries, $blockSlowdown);
}
public function getBase(): int
{
return $this->base;
}
public function getMinLength(): int
{
return $this->minLength;
}
/**
* @param array<string, mixed> $data
*/
public static function createFromArray(array $data): self
{
array_key_exists('base', $data) || throw MetadataStatementLoadingException::create(
'The parameter "base" is missing'
);
array_key_exists('minLength', $data) || throw MetadataStatementLoadingException::create(
'The parameter "minLength" is missing'
);
return new self(
$data['base'],
$data['minLength'],
$data['maxRetries'] ?? null,
$data['blockSlowdown'] ?? null
);
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
$data = [
'base' => $this->base,
'minLength' => $this->minLength,
'maxRetries' => $this->getMaxRetries(),
'blockSlowdown' => $this->getBlockSlowdown(),
];
return Utils::filterNullValues($data);
}
}

View File

@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Statement;
use function array_key_exists;
use function is_array;
use JsonSerializable;
use Webauthn\MetadataService\Exception\MetadataStatementLoadingException;
use Webauthn\MetadataService\Utils;
/**
* @final
*/
class DisplayPNGCharacteristicsDescriptor implements JsonSerializable
{
private readonly int $width;
private readonly int $height;
private readonly int $bitDepth;
private readonly int $colorType;
private readonly int $compression;
private readonly int $filter;
private readonly int $interlace;
/**
* @var RgbPaletteEntry[]
*/
private array $plte = [];
public function __construct(
int $width,
int $height,
int $bitDepth,
int $colorType,
int $compression,
int $filter,
int $interlace
) {
$width >= 0 || throw MetadataStatementLoadingException::create('Invalid width');
$height >= 0 || throw MetadataStatementLoadingException::create('Invalid height');
($bitDepth >= 0 && $bitDepth <= 254) || throw MetadataStatementLoadingException::create('Invalid bit depth');
($colorType >= 0 && $colorType <= 254) || throw MetadataStatementLoadingException::create(
'Invalid color type'
);
($compression >= 0 && $compression <= 254) || throw MetadataStatementLoadingException::create(
'Invalid compression'
);
($filter >= 0 && $filter <= 254) || throw MetadataStatementLoadingException::create('Invalid filter');
($interlace >= 0 && $interlace <= 254) || throw MetadataStatementLoadingException::create(
'Invalid interlace'
);
$this->width = $width;
$this->height = $height;
$this->bitDepth = $bitDepth;
$this->colorType = $colorType;
$this->compression = $compression;
$this->filter = $filter;
$this->interlace = $interlace;
}
public function addPalettes(RgbPaletteEntry ...$rgbPaletteEntries): self
{
foreach ($rgbPaletteEntries as $rgbPaletteEntry) {
$this->plte[] = $rgbPaletteEntry;
}
return $this;
}
public function getWidth(): int
{
return $this->width;
}
public function getHeight(): int
{
return $this->height;
}
public function getBitDepth(): int
{
return $this->bitDepth;
}
public function getColorType(): int
{
return $this->colorType;
}
public function getCompression(): int
{
return $this->compression;
}
public function getFilter(): int
{
return $this->filter;
}
public function getInterlace(): int
{
return $this->interlace;
}
/**
* @return RgbPaletteEntry[]
*/
public function getPaletteEntries(): array
{
return $this->plte;
}
/**
* @param array<string, mixed> $data
*/
public static function createFromArray(array $data): self
{
$data = Utils::filterNullValues($data);
foreach ([
'width',
'compression',
'height',
'bitDepth',
'colorType',
'compression',
'filter',
'interlace',
] as $key) {
array_key_exists($key, $data) || throw MetadataStatementLoadingException::create(sprintf(
'Invalid data. The key "%s" is missing',
$key
));
}
$object = new self(
$data['width'],
$data['height'],
$data['bitDepth'],
$data['colorType'],
$data['compression'],
$data['filter'],
$data['interlace']
);
if (isset($data['plte'])) {
$plte = $data['plte'];
is_array($plte) || throw MetadataStatementLoadingException::create('Invalid "plte" parameter');
foreach ($plte as $item) {
$object->addPalettes(RgbPaletteEntry::createFromArray($item));
}
}
return $object;
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
$data = [
'width' => $this->width,
'height' => $this->height,
'bitDepth' => $this->bitDepth,
'colorType' => $this->colorType,
'compression' => $this->compression,
'filter' => $this->filter,
'interlace' => $this->interlace,
'plte' => $this->plte,
];
return Utils::filterNullValues($data);
}
}

View File

@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Statement;
use function array_key_exists;
use JsonSerializable;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Webauthn\MetadataService\Exception\MetadataStatementLoadingException;
use Webauthn\MetadataService\Utils;
/**
* @deprecated since 4.2.0 and will be removed in 5.0.0. The ECDAA Trust Anchor does no longer exist in Webauthn specification.
* @final
*/
class EcdaaTrustAnchor implements JsonSerializable
{
public function __construct(
private readonly string $X,
private readonly string $Y,
private readonly string $c,
private readonly string $sx,
private readonly string $sy,
private readonly string $G1Curve
) {
}
public function getX(): string
{
return $this->X;
}
public function getY(): string
{
return $this->Y;
}
public function getC(): string
{
return $this->c;
}
public function getSx(): string
{
return $this->sx;
}
public function getSy(): string
{
return $this->sy;
}
public function getG1Curve(): string
{
return $this->G1Curve;
}
/**
* @param array<string, mixed> $data
*/
public static function createFromArray(array $data): self
{
$data = Utils::filterNullValues($data);
foreach (['X', 'Y', 'c', 'sx', 'sy', 'G1Curve'] as $key) {
array_key_exists($key, $data) || throw MetadataStatementLoadingException::create(sprintf(
'Invalid data. The key "%s" is missing',
$key
));
}
return new self(
Base64UrlSafe::decode($data['X']),
Base64UrlSafe::decode($data['Y']),
Base64UrlSafe::decode($data['c']),
Base64UrlSafe::decode($data['sx']),
Base64UrlSafe::decode($data['sy']),
$data['G1Curve']
);
}
/**
* @return array<string, string>
*/
public function jsonSerialize(): array
{
$data = [
'X' => Base64UrlSafe::encodeUnpadded($this->X),
'Y' => Base64UrlSafe::encodeUnpadded($this->Y),
'c' => Base64UrlSafe::encodeUnpadded($this->c),
'sx' => Base64UrlSafe::encodeUnpadded($this->sx),
'sy' => Base64UrlSafe::encodeUnpadded($this->sy),
'G1Curve' => $this->G1Curve,
];
return Utils::filterNullValues($data);
}
}

View File

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Statement;
use function array_key_exists;
use JsonSerializable;
use Webauthn\MetadataService\Exception\MetadataStatementLoadingException;
use Webauthn\MetadataService\Utils;
/**
* @final
*/
class ExtensionDescriptor implements JsonSerializable
{
private readonly ?int $tag;
public function __construct(
private readonly string $id,
?int $tag,
private readonly ?string $data,
private readonly bool $failIfUnknown
) {
if ($tag !== null) {
$tag >= 0 || throw MetadataStatementLoadingException::create(
'Invalid data. The parameter "tag" shall be a positive integer'
);
}
$this->tag = $tag;
}
public function getId(): string
{
return $this->id;
}
public function getTag(): ?int
{
return $this->tag;
}
public function getData(): ?string
{
return $this->data;
}
public function isFailIfUnknown(): bool
{
return $this->failIfUnknown;
}
/**
* @param array<string, mixed> $data
*/
public static function createFromArray(array $data): self
{
$data = Utils::filterNullValues($data);
array_key_exists('id', $data) || throw MetadataStatementLoadingException::create(
'Invalid data. The parameter "id" is missing'
);
array_key_exists('fail_if_unknown', $data) || throw MetadataStatementLoadingException::create(
'Invalid data. The parameter "fail_if_unknown" is missing'
);
return new self($data['id'], $data['tag'] ?? null, $data['data'] ?? null, $data['fail_if_unknown']);
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
$result = [
'id' => $this->id,
'tag' => $this->tag,
'data' => $this->data,
'fail_if_unknown' => $this->failIfUnknown,
];
return Utils::filterNullValues($result);
}
}

View File

@ -0,0 +1,556 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Statement;
use function array_key_exists;
use function is_array;
use function is_string;
use const JSON_THROW_ON_ERROR;
use JsonSerializable;
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
use Webauthn\MetadataService\Exception\MetadataStatementLoadingException;
use Webauthn\MetadataService\Utils;
/**
* @final
*/
class MetadataStatement implements JsonSerializable
{
final public const KEY_PROTECTION_SOFTWARE = 'software';
final public const KEY_PROTECTION_HARDWARE = 'hardware';
final public const KEY_PROTECTION_TEE = 'tee';
final public const KEY_PROTECTION_SECURE_ELEMENT = 'secure_element';
final public const KEY_PROTECTION_REMOTE_HANDLE = 'remote_handle';
final public const MATCHER_PROTECTION_SOFTWARE = 'software';
final public const MATCHER_PROTECTION_TEE = 'tee';
final public const MATCHER_PROTECTION_ON_CHIP = 'on_chip';
final public const ATTACHMENT_HINT_INTERNAL = 'internal';
final public const ATTACHMENT_HINT_EXTERNAL = 'external';
final public const ATTACHMENT_HINT_WIRED = 'wired';
final public const ATTACHMENT_HINT_WIRELESS = 'wireless';
final public const ATTACHMENT_HINT_NFC = 'nfc';
final public const ATTACHMENT_HINT_BLUETOOTH = 'bluetooth';
final public const ATTACHMENT_HINT_NETWORK = 'network';
final public const ATTACHMENT_HINT_READY = 'ready';
final public const ATTACHMENT_HINT_WIFI_DIRECT = 'wifi_direct';
final public const TRANSACTION_CONFIRMATION_DISPLAY_ANY = 'any';
final public const TRANSACTION_CONFIRMATION_DISPLAY_PRIVILEGED_SOFTWARE = 'privileged_software';
final public const TRANSACTION_CONFIRMATION_DISPLAY_TEE = 'tee';
final public const TRANSACTION_CONFIRMATION_DISPLAY_HARDWARE = 'hardware';
final public const TRANSACTION_CONFIRMATION_DISPLAY_REMOTE = 'remote';
final public const ALG_SIGN_SECP256R1_ECDSA_SHA256_RAW = 'secp256r1_ecdsa_sha256_raw';
final public const ALG_SIGN_SECP256R1_ECDSA_SHA256_DER = 'secp256r1_ecdsa_sha256_der';
final public const ALG_SIGN_RSASSA_PSS_SHA256_RAW = 'rsassa_pss_sha256_raw';
final public const ALG_SIGN_RSASSA_PSS_SHA256_DER = 'rsassa_pss_sha256_der';
final public const ALG_SIGN_SECP256K1_ECDSA_SHA256_RAW = 'secp256k1_ecdsa_sha256_raw';
final public const ALG_SIGN_SECP256K1_ECDSA_SHA256_DER = 'secp256k1_ecdsa_sha256_der';
final public const ALG_SIGN_SM2_SM3_RAW = 'sm2_sm3_raw';
final public const ALG_SIGN_RSA_EMSA_PKCS1_SHA256_RAW = 'rsa_emsa_pkcs1_sha256_raw';
final public const ALG_SIGN_RSA_EMSA_PKCS1_SHA256_DER = 'rsa_emsa_pkcs1_sha256_der';
final public const ALG_SIGN_RSASSA_PSS_SHA384_RAW = 'rsassa_pss_sha384_raw';
final public const ALG_SIGN_RSASSA_PSS_SHA512_RAW = 'rsassa_pss_sha256_raw';
final public const ALG_SIGN_RSASSA_PKCSV15_SHA256_RAW = 'rsassa_pkcsv15_sha256_raw';
final public const ALG_SIGN_RSASSA_PKCSV15_SHA384_RAW = 'rsassa_pkcsv15_sha384_raw';
final public const ALG_SIGN_RSASSA_PKCSV15_SHA512_RAW = 'rsassa_pkcsv15_sha512_raw';
final public const ALG_SIGN_RSASSA_PKCSV15_SHA1_RAW = 'rsassa_pkcsv15_sha1_raw';
final public const ALG_SIGN_SECP384R1_ECDSA_SHA384_RAW = 'secp384r1_ecdsa_sha384_raw';
final public const ALG_SIGN_SECP521R1_ECDSA_SHA512_RAW = 'secp512r1_ecdsa_sha256_raw';
final public const ALG_SIGN_ED25519_EDDSA_SHA256_RAW = 'ed25519_eddsa_sha512_raw';
final public const ALG_KEY_ECC_X962_RAW = 'ecc_x962_raw';
final public const ALG_KEY_ECC_X962_DER = 'ecc_x962_der';
final public const ALG_KEY_RSA_2048_RAW = 'rsa_2048_raw';
final public const ALG_KEY_RSA_2048_DER = 'rsa_2048_der';
final public const ALG_KEY_COSE = 'cose';
final public const ATTESTATION_BASIC_FULL = 'basic_full';
final public const ATTESTATION_BASIC_SURROGATE = 'basic_surrogate';
/**
* @deprecated since 4.2.0 and will be removed in 5.0.0. The ECDAA Trust Anchor does no longer exist in Webauthn specification.
*/
final public const ATTESTATION_ECDAA = 'ecdaa';
final public const ATTESTATION_ATTCA = 'attca';
final public const ATTESTATION_ANONCA = 'anonca';
private ?string $legalHeader = null;
private ?string $aaid = null;
private ?string $aaguid = null;
/**
* @var string[]
*/
private array $attestationCertificateKeyIdentifiers = [];
private AlternativeDescriptions $alternativeDescriptions;
/**
* @var string[]
*/
private array $keyProtection = [];
private ?bool $isKeyRestricted = null;
private ?bool $isFreshUserVerificationRequired = null;
private ?int $cryptoStrength = null;
/**
* @var string[]
*/
private array $attachmentHint = [];
private ?string $tcDisplayContentType = null;
/**
* @var DisplayPNGCharacteristicsDescriptor[]
*/
private array $tcDisplayPNGCharacteristics = [];
/**
* @var EcdaaTrustAnchor[]
*/
private array $ecdaaTrustAnchors = [];
private ?string $icon = null;
/**
* @var ExtensionDescriptor[]
*/
private array $supportedExtensions = [];
private null|AuthenticatorGetInfo $authenticatorGetInfo = null;
/**
* @param Version[] $upv
* @param string[] $authenticationAlgorithms
* @param string[] $publicKeyAlgAndEncodings
* @param string[] $attestationTypes
* @param VerificationMethodANDCombinations[] $userVerificationDetails
* @param string[] $matcherProtection
* @param string[] $tcDisplay
* @param string[] $attestationRootCertificates
*/
public function __construct(
private readonly string $description,
private readonly int $authenticatorVersion,
private readonly string $protocolFamily,
private readonly int $schema,
private readonly array $upv,
private readonly array $authenticationAlgorithms,
private readonly array $publicKeyAlgAndEncodings,
private readonly array $attestationTypes,
private readonly array $userVerificationDetails,
private readonly array $matcherProtection,
private readonly array $tcDisplay,
private readonly array $attestationRootCertificates,
) {
$this->alternativeDescriptions = new AlternativeDescriptions();
$this->authenticatorGetInfo = new AuthenticatorGetInfo();
}
public static function createFromString(string $statement): self
{
$data = json_decode($statement, true, 512, JSON_THROW_ON_ERROR);
return self::createFromArray($data);
}
public function getLegalHeader(): ?string
{
return $this->legalHeader;
}
public function getAaid(): ?string
{
return $this->aaid;
}
public function getAaguid(): ?string
{
return $this->aaguid;
}
public function isKeyRestricted(): ?bool
{
return $this->isKeyRestricted;
}
public function isFreshUserVerificationRequired(): ?bool
{
return $this->isFreshUserVerificationRequired;
}
public function getAuthenticatorGetInfo(): AuthenticatorGetInfo|null
{
return $this->authenticatorGetInfo;
}
/**
* @return string[]
*/
public function getAttestationCertificateKeyIdentifiers(): array
{
return $this->attestationCertificateKeyIdentifiers;
}
public function getDescription(): string
{
return $this->description;
}
public function getAlternativeDescriptions(): AlternativeDescriptions
{
return $this->alternativeDescriptions;
}
public function getAuthenticatorVersion(): int
{
return $this->authenticatorVersion;
}
public function getProtocolFamily(): string
{
return $this->protocolFamily;
}
/**
* @return Version[]
*/
public function getUpv(): array
{
return $this->upv;
}
public function getSchema(): ?int
{
return $this->schema;
}
/**
* @return string[]
*/
public function getAuthenticationAlgorithms(): array
{
return $this->authenticationAlgorithms;
}
/**
* @return string[]
*/
public function getPublicKeyAlgAndEncodings(): array
{
return $this->publicKeyAlgAndEncodings;
}
/**
* @return string[]
*/
public function getAttestationTypes(): array
{
return $this->attestationTypes;
}
/**
* @return VerificationMethodANDCombinations[]
*/
public function getUserVerificationDetails(): array
{
return $this->userVerificationDetails;
}
/**
* @return string[]
*/
public function getKeyProtection(): array
{
return $this->keyProtection;
}
/**
* @return string[]
*/
public function getMatcherProtection(): array
{
return $this->matcherProtection;
}
public function getCryptoStrength(): ?int
{
return $this->cryptoStrength;
}
/**
* @return string[]
*/
public function getAttachmentHint(): array
{
return $this->attachmentHint;
}
/**
* @return string[]
*/
public function getTcDisplay(): array
{
return $this->tcDisplay;
}
public function getTcDisplayContentType(): ?string
{
return $this->tcDisplayContentType;
}
/**
* @return DisplayPNGCharacteristicsDescriptor[]
*/
public function getTcDisplayPNGCharacteristics(): array
{
return $this->tcDisplayPNGCharacteristics;
}
/**
* @return string[]
*/
public function getAttestationRootCertificates(): array
{
return $this->attestationRootCertificates;
}
/**
* @return EcdaaTrustAnchor[]
*
* @deprecated since 4.2.0 and will be removed in 5.0.0. The ECDAA Trust Anchor does no longer exist in Webauthn specification.
*/
public function getEcdaaTrustAnchors(): array
{
return $this->ecdaaTrustAnchors;
}
public function getIcon(): ?string
{
return $this->icon;
}
/**
* @return ExtensionDescriptor[]
*/
public function getSupportedExtensions(): array
{
return $this->supportedExtensions;
}
/**
* @param array<string, mixed> $data
*/
public static function createFromArray(array $data): self
{
$requiredKeys = [
'description',
'authenticatorVersion',
'protocolFamily',
'schema',
'upv',
'authenticationAlgorithms',
'publicKeyAlgAndEncodings',
'attestationTypes',
'userVerificationDetails',
'matcherProtection',
'tcDisplay',
'attestationRootCertificates',
];
foreach ($requiredKeys as $key) {
array_key_exists($key, $data) || throw MetadataStatementLoadingException::create(sprintf(
'Invalid data. The key "%s" is missing',
$key
));
}
$subObjects = [
'authenticationAlgorithms',
'publicKeyAlgAndEncodings',
'attestationTypes',
'matcherProtection',
'tcDisplay',
'attestationRootCertificates',
];
foreach ($subObjects as $subObject) {
is_array($data[$subObject]) || throw MetadataStatementLoadingException::create(sprintf(
'Invalid Metadata Statement. The parameter "%s" shall be a list of strings.',
$subObject
));
foreach ($data[$subObject] as $datum) {
is_string($datum) || throw MetadataStatementLoadingException::create(sprintf(
'Invalid Metadata Statement. The parameter "%s" shall be a list of strings.',
$subObject
));
}
}
$object = new self(
$data['description'],
$data['authenticatorVersion'],
$data['protocolFamily'],
$data['schema'],
array_map(static function ($upv): Version {
is_array($upv) || throw MetadataStatementLoadingException::create('Invalid Metadata Statement');
return Version::createFromArray($upv);
}, $data['upv']),
$data['authenticationAlgorithms'],
$data['publicKeyAlgAndEncodings'],
$data['attestationTypes'],
array_map(static function ($userVerificationDetails): VerificationMethodANDCombinations {
is_array($userVerificationDetails) || throw MetadataStatementLoadingException::create(
'Invalid Metadata Statement'
);
return VerificationMethodANDCombinations::createFromArray($userVerificationDetails);
}, $data['userVerificationDetails']),
$data['matcherProtection'],
$data['tcDisplay'],
CertificateToolbox::fixPEMStructures($data['attestationRootCertificates'])
);
$object->legalHeader = $data['legalHeader'] ?? null;
$object->aaid = $data['aaid'] ?? null;
$object->aaguid = $data['aaguid'] ?? null;
$object->attestationCertificateKeyIdentifiers = $data['attestationCertificateKeyIdentifiers'] ?? [];
$object->alternativeDescriptions = AlternativeDescriptions::create($data['alternativeDescriptions'] ?? []);
$object->authenticatorGetInfo = isset($data['attestationTypes']) ? AuthenticatorGetInfo::create(
$data['attestationTypes']
) : null;
$object->keyProtection = $data['keyProtection'] ?? [];
$object->isKeyRestricted = $data['isKeyRestricted'] ?? null;
$object->isFreshUserVerificationRequired = $data['isFreshUserVerificationRequired'] ?? null;
$object->cryptoStrength = $data['cryptoStrength'] ?? null;
$object->attachmentHint = $data['attachmentHint'] ?? [];
$object->tcDisplayContentType = $data['tcDisplayContentType'] ?? null;
if (isset($data['tcDisplayPNGCharacteristics'])) {
$tcDisplayPNGCharacteristics = $data['tcDisplayPNGCharacteristics'];
is_array($tcDisplayPNGCharacteristics) || throw MetadataStatementLoadingException::create(
'Invalid Metadata Statement'
);
foreach ($tcDisplayPNGCharacteristics as $tcDisplayPNGCharacteristic) {
is_array($tcDisplayPNGCharacteristic) || throw MetadataStatementLoadingException::create(
'Invalid Metadata Statement'
);
$object->tcDisplayPNGCharacteristics[] = DisplayPNGCharacteristicsDescriptor::createFromArray(
$tcDisplayPNGCharacteristic
);
}
}
$object->ecdaaTrustAnchors = $data['ecdaaTrustAnchors'] ?? [];
$object->icon = $data['icon'] ?? null;
if (isset($data['supportedExtensions'])) {
$supportedExtensions = $data['supportedExtensions'];
is_array($supportedExtensions) || throw MetadataStatementLoadingException::create(
'Invalid Metadata Statement'
);
foreach ($supportedExtensions as $supportedExtension) {
is_array($supportedExtension) || throw MetadataStatementLoadingException::create(
'Invalid Metadata Statement'
);
$object->supportedExtensions[] = ExtensionDescriptor::createFromArray($supportedExtension);
}
}
return $object;
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
$data = [
'legalHeader' => $this->legalHeader,
'aaid' => $this->aaid,
'aaguid' => $this->aaguid,
'attestationCertificateKeyIdentifiers' => $this->attestationCertificateKeyIdentifiers,
'description' => $this->description,
'alternativeDescriptions' => $this->alternativeDescriptions,
'authenticatorVersion' => $this->authenticatorVersion,
'protocolFamily' => $this->protocolFamily,
'upv' => $this->upv,
'authenticationAlgorithms' => $this->authenticationAlgorithms,
'publicKeyAlgAndEncodings' => $this->publicKeyAlgAndEncodings,
'attestationTypes' => $this->attestationTypes,
'userVerificationDetails' => $this->userVerificationDetails,
'keyProtection' => $this->keyProtection,
'isKeyRestricted' => $this->isKeyRestricted,
'isFreshUserVerificationRequired' => $this->isFreshUserVerificationRequired,
'matcherProtection' => $this->matcherProtection,
'cryptoStrength' => $this->cryptoStrength,
'attachmentHint' => $this->attachmentHint,
'tcDisplay' => $this->tcDisplay,
'tcDisplayContentType' => $this->tcDisplayContentType,
'tcDisplayPNGCharacteristics' => array_map(
static fn (DisplayPNGCharacteristicsDescriptor $object): array => $object->jsonSerialize(),
$this->tcDisplayPNGCharacteristics
),
'attestationRootCertificates' => CertificateToolbox::fixPEMStructures($this->attestationRootCertificates),
'ecdaaTrustAnchors' => array_map(
static fn (EcdaaTrustAnchor $object): array => $object->jsonSerialize(),
$this->ecdaaTrustAnchors
),
'icon' => $this->icon,
'authenticatorGetInfo' => $this->authenticatorGetInfo,
'supportedExtensions' => array_map(
static fn (ExtensionDescriptor $object): array => $object->jsonSerialize(),
$this->supportedExtensions
),
];
return Utils::filterNullValues($data);
}
}

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Statement;
use function array_key_exists;
use function is_int;
use Webauthn\MetadataService\Exception\MetadataStatementLoadingException;
use Webauthn\MetadataService\Utils;
/**
* @final
*/
class PatternAccuracyDescriptor extends AbstractDescriptor
{
private readonly int $minComplexity;
public function __construct(int $minComplexity, ?int $maxRetries = null, ?int $blockSlowdown = null)
{
$minComplexity >= 0 || throw MetadataStatementLoadingException::create(
'Invalid data. The value of "minComplexity" must be a positive integer'
);
$this->minComplexity = $minComplexity;
parent::__construct($maxRetries, $blockSlowdown);
}
public function getMinComplexity(): int
{
return $this->minComplexity;
}
/**
* @param array<string, mixed> $data
*/
public static function createFromArray(array $data): self
{
$data = Utils::filterNullValues($data);
array_key_exists('minComplexity', $data) || throw MetadataStatementLoadingException::create(
'The key "minComplexity" is missing'
);
foreach (['minComplexity', 'maxRetries', 'blockSlowdown'] as $key) {
if (array_key_exists($key, $data)) {
is_int($data[$key]) || throw MetadataStatementLoadingException::create(
sprintf('Invalid data. The value of "%s" must be a positive integer', $key)
);
}
}
return new self($data['minComplexity'], $data['maxRetries'] ?? null, $data['blockSlowdown'] ?? null);
}
/**
* @return array<string, int|null>
*/
public function jsonSerialize(): array
{
$data = [
'minComplexity' => $this->minComplexity,
'maxRetries' => $this->getMaxRetries(),
'blockSlowdown' => $this->getBlockSlowdown(),
];
return Utils::filterNullValues($data);
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Statement;
use function array_key_exists;
use function is_int;
use JsonSerializable;
use Webauthn\MetadataService\Exception\MetadataStatementLoadingException;
/**
* @final
*/
class RgbPaletteEntry implements JsonSerializable
{
private readonly int $r;
private readonly int $g;
private readonly int $b;
public function __construct(int $r, int $g, int $b)
{
($r >= 0 && $r <= 255) || throw MetadataStatementLoadingException::create('The key "r" is invalid');
($g >= 0 && $g <= 255) || throw MetadataStatementLoadingException::create('The key "g" is invalid');
($b >= 0 && $b <= 255) || throw MetadataStatementLoadingException::create('The key "b" is invalid');
$this->r = $r;
$this->g = $g;
$this->b = $b;
}
public function getR(): int
{
return $this->r;
}
public function getG(): int
{
return $this->g;
}
public function getB(): int
{
return $this->b;
}
/**
* @param array<string, mixed> $data
*/
public static function createFromArray(array $data): self
{
foreach (['r', 'g', 'b'] as $key) {
array_key_exists($key, $data) || throw MetadataStatementLoadingException::create(sprintf(
'The key "%s" is missing',
$key
));
is_int($data[$key]) || throw MetadataStatementLoadingException::create(
sprintf('The key "%s" is invalid', $key)
);
}
return new self($data['r'], $data['g'], $data['b']);
}
/**
* @return array<string, int>
*/
public function jsonSerialize(): array
{
return [
'r' => $this->r,
'g' => $this->g,
'b' => $this->b,
];
}
}

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Statement;
use function array_key_exists;
use function is_string;
use JsonSerializable;
use Webauthn\MetadataService\Exception\MetadataStatementLoadingException;
/**
* @final
*/
class RogueListEntry implements JsonSerializable
{
public function __construct(
private readonly string $sk,
private readonly string $date
) {
}
public function getSk(): string
{
return $this->sk;
}
public function getDate(): ?string
{
return $this->date;
}
/**
* @param array<string, mixed> $data
*/
public static function createFromArray(array $data): self
{
array_key_exists('sk', $data) || throw MetadataStatementLoadingException::create('The key "sk" is missing');
is_string($data['sk']) || throw MetadataStatementLoadingException::create('The key "date" is invalid');
array_key_exists('date', $data) || throw MetadataStatementLoadingException::create(
'The key "date" is missing'
);
is_string($data['date']) || throw MetadataStatementLoadingException::create('The key "date" is invalid');
return new self($data['sk'], $data['date']);
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
return [
'sk' => $this->sk,
'date' => $this->date,
];
}
}

View File

@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Statement;
use function array_key_exists;
use function in_array;
use function is_string;
use JsonSerializable;
use Webauthn\MetadataService\Exception\MetadataStatementLoadingException;
use Webauthn\MetadataService\Utils;
/**
* @final
*/
class StatusReport implements JsonSerializable
{
/**
* @see AuthenticatorStatus
*/
private readonly string $status;
public function __construct(
string $status,
private readonly ?string $effectiveDate,
private readonly ?string $certificate,
private readonly ?string $url,
private readonly ?string $certificationDescriptor,
private readonly ?string $certificateNumber,
private readonly ?string $certificationPolicyVersion,
private readonly ?string $certificationRequirementsVersion
) {
in_array($status, AuthenticatorStatus::list(), true) || throw MetadataStatementLoadingException::create(
'The value of the key "status" is not acceptable'
);
$this->status = $status;
}
public function isCompromised(): bool
{
return in_array($this->status, [
AuthenticatorStatus::ATTESTATION_KEY_COMPROMISE,
AuthenticatorStatus::USER_KEY_PHYSICAL_COMPROMISE,
AuthenticatorStatus::USER_KEY_REMOTE_COMPROMISE,
AuthenticatorStatus::USER_VERIFICATION_BYPASS,
], true);
}
public function getStatus(): string
{
return $this->status;
}
public function getEffectiveDate(): ?string
{
return $this->effectiveDate;
}
public function getCertificate(): ?string
{
return $this->certificate;
}
public function getUrl(): ?string
{
return $this->url;
}
public function getCertificationDescriptor(): ?string
{
return $this->certificationDescriptor;
}
public function getCertificateNumber(): ?string
{
return $this->certificateNumber;
}
public function getCertificationPolicyVersion(): ?string
{
return $this->certificationPolicyVersion;
}
public function getCertificationRequirementsVersion(): ?string
{
return $this->certificationRequirementsVersion;
}
/**
* @param array<string, mixed> $data
*/
public static function createFromArray(array $data): self
{
$data = Utils::filterNullValues($data);
array_key_exists('status', $data) || throw MetadataStatementLoadingException::create(
'The key "status" is missing'
);
foreach ([
'effectiveDate',
'certificate',
'url',
'certificationDescriptor',
'certificateNumber',
'certificationPolicyVersion',
'certificationRequirementsVersion',
] as $key) {
if (isset($data[$key])) {
$value = $data[$key];
$value === null || is_string($value) || throw MetadataStatementLoadingException::create(sprintf(
'The value of the key "%s" is invalid',
$key
));
}
}
return new self(
$data['status'],
$data['effectiveDate'] ?? null,
$data['certificate'] ?? null,
$data['url'] ?? null,
$data['certificationDescriptor'] ?? null,
$data['certificateNumber'] ?? null,
$data['certificationPolicyVersion'] ?? null,
$data['certificationRequirementsVersion'] ?? null
);
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
$data = [
'status' => $this->status,
'effectiveDate' => $this->effectiveDate,
'certificate' => $this->certificate,
'url' => $this->url,
'certificationDescriptor' => $this->certificationDescriptor,
'certificateNumber' => $this->certificateNumber,
'certificationPolicyVersion' => $this->certificationPolicyVersion,
'certificationRequirementsVersion' => $this->certificationRequirementsVersion,
];
return Utils::filterNullValues($data);
}
}

View File

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Statement;
use function is_array;
use JsonSerializable;
use Webauthn\MetadataService\Exception\MetadataStatementLoadingException;
/**
* @final
*/
class VerificationMethodANDCombinations implements JsonSerializable
{
/**
* @var VerificationMethodDescriptor[]
*/
private array $verificationMethods = [];
public function addVerificationMethodDescriptor(VerificationMethodDescriptor $verificationMethodDescriptor): self
{
$this->verificationMethods[] = $verificationMethodDescriptor;
return $this;
}
/**
* @return VerificationMethodDescriptor[]
*/
public function getVerificationMethods(): array
{
return $this->verificationMethods;
}
/**
* @param array<string, mixed> $data
*/
public static function createFromArray(array $data): self
{
$object = new self();
foreach ($data as $datum) {
is_array($datum) || throw MetadataStatementLoadingException::create('Invalid data');
$object->addVerificationMethodDescriptor(VerificationMethodDescriptor::createFromArray($datum));
}
return $object;
}
/**
* @return array<array<mixed>>
*/
public function jsonSerialize(): array
{
return array_map(
static fn (VerificationMethodDescriptor $object): array => $object->jsonSerialize(),
$this->verificationMethods
);
}
}

View File

@ -0,0 +1,186 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Statement;
use function array_key_exists;
use function is_array;
use JsonSerializable;
use Webauthn\MetadataService\Exception\MetadataStatementLoadingException;
use Webauthn\MetadataService\Utils;
/**
* @final
*/
class VerificationMethodDescriptor implements JsonSerializable
{
final public const USER_VERIFY_PRESENCE_INTERNAL = 'presence_internal';
final public const USER_VERIFY_FINGERPRINT_INTERNAL = 'fingerprint_internal';
final public const USER_VERIFY_PASSCODE_INTERNAL = 'passcode_internal';
final public const USER_VERIFY_VOICEPRINT_INTERNAL = 'voiceprint_internal';
final public const USER_VERIFY_FACEPRINT_INTERNAL = 'faceprint_internal';
final public const USER_VERIFY_LOCATION_INTERNAL = 'location_internal';
final public const USER_VERIFY_EYEPRINT_INTERNAL = 'eyeprint_internal';
final public const USER_VERIFY_PATTERN_INTERNAL = 'pattern_internal';
final public const USER_VERIFY_HANDPRINT_INTERNAL = 'handprint_internal';
final public const USER_VERIFY_PASSCODE_EXTERNAL = 'passcode_external';
final public const USER_VERIFY_PATTERN_EXTERNAL = 'pattern_external';
final public const USER_VERIFY_NONE = 'none';
final public const USER_VERIFY_ALL = 'all';
private readonly string $userVerificationMethod;
public function __construct(
string $userVerificationMethod,
private readonly ?CodeAccuracyDescriptor $caDesc = null,
private readonly ?BiometricAccuracyDescriptor $baDesc = null,
private readonly ?PatternAccuracyDescriptor $paDesc = null
) {
$userVerificationMethod >= 0 || throw MetadataStatementLoadingException::create(
'The parameter "userVerificationMethod" is invalid'
);
$this->userVerificationMethod = $userVerificationMethod;
}
public function getUserVerificationMethod(): string
{
return $this->userVerificationMethod;
}
public function userPresence(): bool
{
return $this->userVerificationMethod === self::USER_VERIFY_PRESENCE_INTERNAL;
}
public function fingerprint(): bool
{
return $this->userVerificationMethod === self::USER_VERIFY_FINGERPRINT_INTERNAL;
}
public function passcodeInternal(): bool
{
return $this->userVerificationMethod === self::USER_VERIFY_PASSCODE_INTERNAL;
}
public function voicePrint(): bool
{
return $this->userVerificationMethod === self::USER_VERIFY_VOICEPRINT_INTERNAL;
}
public function facePrint(): bool
{
return $this->userVerificationMethod === self::USER_VERIFY_FACEPRINT_INTERNAL;
}
public function location(): bool
{
return $this->userVerificationMethod === self::USER_VERIFY_LOCATION_INTERNAL;
}
public function eyePrint(): bool
{
return $this->userVerificationMethod === self::USER_VERIFY_EYEPRINT_INTERNAL;
}
public function patternInternal(): bool
{
return $this->userVerificationMethod === self::USER_VERIFY_PATTERN_INTERNAL;
}
public function handprint(): bool
{
return $this->userVerificationMethod === self::USER_VERIFY_HANDPRINT_INTERNAL;
}
public function passcodeExternal(): bool
{
return $this->userVerificationMethod === self::USER_VERIFY_PASSCODE_EXTERNAL;
}
public function patternExternal(): bool
{
return $this->userVerificationMethod === self::USER_VERIFY_PATTERN_EXTERNAL;
}
public function none(): bool
{
return $this->userVerificationMethod === self::USER_VERIFY_NONE;
}
public function all(): bool
{
return $this->userVerificationMethod === self::USER_VERIFY_ALL;
}
public function getCaDesc(): ?CodeAccuracyDescriptor
{
return $this->caDesc;
}
public function getBaDesc(): ?BiometricAccuracyDescriptor
{
return $this->baDesc;
}
public function getPaDesc(): ?PatternAccuracyDescriptor
{
return $this->paDesc;
}
/**
* @param array<string, mixed> $data
*/
public static function createFromArray(array $data): self
{
$data = Utils::filterNullValues($data);
if (isset($data['userVerification']) && ! isset($data['userVerificationMethod'])) {
$data['userVerificationMethod'] = $data['userVerification'];
unset($data['userVerification']);
}
array_key_exists('userVerificationMethod', $data) || throw MetadataStatementLoadingException::create(
'The parameters "userVerificationMethod" is missing'
);
foreach (['caDesc', 'baDesc', 'paDesc'] as $key) {
if (isset($data[$key])) {
is_array($data[$key]) || throw MetadataStatementLoadingException::create(
sprintf('Invalid parameter "%s"', $key)
);
}
}
$caDesc = isset($data['caDesc']) ? CodeAccuracyDescriptor::createFromArray($data['caDesc']) : null;
$baDesc = isset($data['baDesc']) ? BiometricAccuracyDescriptor::createFromArray($data['baDesc']) : null;
$paDesc = isset($data['paDesc']) ? PatternAccuracyDescriptor::createFromArray($data['paDesc']) : null;
return new self($data['userVerificationMethod'], $caDesc, $baDesc, $paDesc);
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
$data = [
'userVerificationMethod' => $this->userVerificationMethod,
'caDesc' => $this->caDesc?->jsonSerialize(),
'baDesc' => $this->baDesc?->jsonSerialize(),
'paDesc' => $this->paDesc?->jsonSerialize(),
];
return Utils::filterNullValues($data);
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService\Statement;
use function array_key_exists;
use function is_int;
use JsonSerializable;
use Webauthn\MetadataService\Exception\MetadataStatementLoadingException;
use Webauthn\MetadataService\Utils;
/**
* @final
*/
class Version implements JsonSerializable
{
private readonly ?int $major;
private readonly ?int $minor;
public function __construct(?int $major, ?int $minor)
{
if ($major === null && $minor === null) {
throw MetadataStatementLoadingException::create('Invalid data. Must contain at least one item');
}
$major >= 0 || throw MetadataStatementLoadingException::create('Invalid argument "major"');
$minor >= 0 || throw MetadataStatementLoadingException::create('Invalid argument "minor"');
$this->major = $major;
$this->minor = $minor;
}
public function getMajor(): ?int
{
return $this->major;
}
public function getMinor(): ?int
{
return $this->minor;
}
/**
* @param array<string, mixed> $data
*/
public static function createFromArray(array $data): self
{
$data = Utils::filterNullValues($data);
foreach (['major', 'minor'] as $key) {
if (array_key_exists($key, $data)) {
is_int($data[$key]) || throw MetadataStatementLoadingException::create(
sprintf('Invalid value for key "%s"', $key)
);
}
}
return new self($data['major'] ?? null, $data['minor'] ?? null);
}
/**
* @return array<string, int|null>
*/
public function jsonSerialize(): array
{
$data = [
'major' => $this->major,
'minor' => $this->minor,
];
return Utils::filterNullValues($data);
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService;
use Webauthn\MetadataService\Statement\StatusReport;
interface StatusReportRepository
{
/**
* @return StatusReport[]
*/
public function findStatusReportsByAAGUID(string $aaguid): array;
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Webauthn\MetadataService;
/**
* @internal
*/
abstract class Utils
{
/**
* @param array<mixed> $data
*
* @return array<mixed>
*/
public static function filterNullValues(array $data): array
{
return array_filter($data, static fn ($var): bool => $var !== null);
}
}