diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml index 677dafc7b..47cd0538c 100644 --- a/.github/workflows/integrate.yml +++ b/.github/workflows/integrate.yml @@ -187,12 +187,6 @@ jobs: run: | vendor/bin/deptrac analyse --fail-on-uncovered --no-cache - - name: "SonarCloud Scan" - uses: "sonarsource/sonarcloud-github-action@master" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - rector_checkstyle: name: "6️⃣ Rector Checkstyle" needs: diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fad4344a6..1a4ae3f73 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -797,11 +797,21 @@ parameters: count: 1 path: src/symfony/src/CredentialOptionsBuilder/ProfileBasedRequestOptionsBuilder.php + - + message: "#^Parameter \\#1 \\$userEntity of method Webauthn\\\\Bundle\\\\CredentialOptionsBuilder\\\\ProfileBasedRequestOptionsBuilder\\:\\:getCredentials\\(\\) expects Webauthn\\\\PublicKeyCredentialUserEntity, Webauthn\\\\PublicKeyCredentialUserEntity\\|null given\\.$#" + count: 1 + path: src/symfony/src/CredentialOptionsBuilder/ProfileBasedRequestOptionsBuilder.php + - message: "#^Parameter \\$credentialSourceRepository of method Webauthn\\\\Bundle\\\\CredentialOptionsBuilder\\\\ProfileBasedRequestOptionsBuilder\\:\\:__construct\\(\\) has typehint with deprecated interface Webauthn\\\\PublicKeyCredentialSourceRepository\\.$#" count: 1 path: src/symfony/src/CredentialOptionsBuilder/ProfileBasedRequestOptionsBuilder.php + - + message: "#^Should not use function \"dump\", please change the code\\.$#" + count: 1 + path: src/symfony/src/CredentialOptionsBuilder/ProfileBasedRequestOptionsBuilder.php + - message: """ #^Fetching class constant class of deprecated class Webauthn\\\\Bundle\\\\Event\\\\AuthenticatorAssertionResponseValidationFailedEvent\\: @@ -3170,6 +3180,11 @@ parameters: count: 1 path: src/webauthn/src/PublicKeyCredentialSource.php + - + message: "#^Method Webauthn\\\\SimpleFakeCredentialGenerator\\:\\:generate\\(\\) should return array\\ but returns mixed\\.$#" + count: 1 + path: src/webauthn/src/SimpleFakeCredentialGenerator.php + - message: "#^Method Webauthn\\\\StringStream\\:\\:read\\(\\) should return string but returns string\\|false\\.$#" count: 1 diff --git a/src/symfony/src/Controller/AssertionControllerFactory.php b/src/symfony/src/Controller/AssertionControllerFactory.php index fcfdf24c6..7b8f96607 100644 --- a/src/symfony/src/Controller/AssertionControllerFactory.php +++ b/src/symfony/src/Controller/AssertionControllerFactory.php @@ -19,6 +19,7 @@ use Webauthn\Bundle\Security\Handler\SuccessHandler; use Webauthn\Bundle\Security\Storage\OptionsStorage; use Webauthn\Bundle\Service\PublicKeyCredentialRequestOptionsFactory; +use Webauthn\FakeCredentialGenerator; use Webauthn\MetadataService\CanLogData; use Webauthn\PublicKeyCredentialLoader; use Webauthn\PublicKeyCredentialSourceRepository; @@ -34,7 +35,8 @@ public function __construct( private readonly null|PublicKeyCredentialLoader $publicKeyCredentialLoader, private readonly AuthenticatorAssertionResponseValidator $authenticatorAssertionResponseValidator, private readonly PublicKeyCredentialUserEntityRepositoryInterface $publicKeyCredentialUserEntityRepository, - private readonly PublicKeyCredentialSourceRepository|PublicKeyCredentialSourceRepositoryInterface $publicKeyCredentialSourceRepository + private readonly PublicKeyCredentialSourceRepository|PublicKeyCredentialSourceRepositoryInterface $publicKeyCredentialSourceRepository, + private readonly null|FakeCredentialGenerator $fakeCredentialGenerator = null, ) { if ($this->publicKeyCredentialLoader !== null) { trigger_deprecation( @@ -68,6 +70,7 @@ public function createAssertionRequestController( $this->publicKeyCredentialSourceRepository, $this->publicKeyCredentialRequestOptionsFactory, $profile, + $this->fakeCredentialGenerator, ); return $this->createRequestController($optionsBuilder, $optionStorage, $optionsHandler, $failureHandler); @@ -84,7 +87,7 @@ public function createRequestController( $optionStorage, $optionsHandler, $failureHandler, - $this->logger + $this->logger, ); } diff --git a/src/symfony/src/CredentialOptionsBuilder/ProfileBasedRequestOptionsBuilder.php b/src/symfony/src/CredentialOptionsBuilder/ProfileBasedRequestOptionsBuilder.php index 1dc0d45a0..839ae4961 100644 --- a/src/symfony/src/CredentialOptionsBuilder/ProfileBasedRequestOptionsBuilder.php +++ b/src/symfony/src/CredentialOptionsBuilder/ProfileBasedRequestOptionsBuilder.php @@ -15,6 +15,7 @@ use Webauthn\Bundle\Repository\PublicKeyCredentialSourceRepositoryInterface; use Webauthn\Bundle\Repository\PublicKeyCredentialUserEntityRepositoryInterface; use Webauthn\Bundle\Service\PublicKeyCredentialRequestOptionsFactory; +use Webauthn\FakeCredentialGenerator; use Webauthn\PublicKeyCredentialDescriptor; use Webauthn\PublicKeyCredentialRequestOptions; use Webauthn\PublicKeyCredentialSource; @@ -32,6 +33,7 @@ public function __construct( private readonly PublicKeyCredentialSourceRepository|PublicKeyCredentialSourceRepositoryInterface $credentialSourceRepository, private readonly PublicKeyCredentialRequestOptionsFactory $publicKeyCredentialRequestOptionsFactory, private readonly string $profile, + private readonly null|FakeCredentialGenerator $fakeCredentialGenerator = null, ) { if (! $this->credentialSourceRepository instanceof PublicKeyCredentialSourceRepositoryInterface) { trigger_deprecation( @@ -71,7 +73,16 @@ public function getFromRequest( $userEntity = $optionsRequest->username === null ? null : $this->userEntityRepository->findOneByUsername( $optionsRequest->username ); - $allowedCredentials = $userEntity === null ? [] : $this->getCredentials($userEntity); + dump($this->fakeCredentialGenerator?->generate($request, $optionsRequest->username ?? '')); + $allowedCredentials = match (true) { + $userEntity === null && $optionsRequest->username === null, $userEntity === null && $optionsRequest->username !== null && $this->fakeCredentialGenerator === null => [], + $userEntity === null && $optionsRequest->username !== null && $this->fakeCredentialGenerator !== null => $this->fakeCredentialGenerator->generate( + $request, + $optionsRequest->username + ), + default => $this->getCredentials($userEntity), + }; + return $this->publicKeyCredentialRequestOptionsFactory->create( $this->profile, $allowedCredentials, diff --git a/src/symfony/src/DependencyInjection/Configuration.php b/src/symfony/src/DependencyInjection/Configuration.php index 3c233703f..e060a5ba8 100644 --- a/src/symfony/src/DependencyInjection/Configuration.php +++ b/src/symfony/src/DependencyInjection/Configuration.php @@ -21,6 +21,7 @@ use Webauthn\Counter\ThrowExceptionIfInvalid; use Webauthn\MetadataService\CertificateChain\PhpCertificateChainValidator; use Webauthn\PublicKeyCredentialCreationOptions; +use Webauthn\SimpleFakeCredentialGenerator; final class Configuration implements ConfigurationInterface { @@ -62,6 +63,13 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultNull() ->info('Creates PSR-7 HTTP Request and Response instances from Symfony ones.') ->end() + ->scalarNode('fake_credential_generator') + ->defaultValue(SimpleFakeCredentialGenerator::class) + ->cannotBeEmpty() + ->info( + 'A service that implements the FakeCredentialGenerator to generate fake credentials for preventing username enumeration.' + ) + ->end() ->scalarNode('clock') ->defaultValue('webauthn.clock.default') ->info('PSR-20 Clock service.') diff --git a/src/symfony/src/DependencyInjection/Factory/Security/WebauthnFactory.php b/src/symfony/src/DependencyInjection/Factory/Security/WebauthnFactory.php index b98851c69..68eb78bec 100644 --- a/src/symfony/src/DependencyInjection/Factory/Security/WebauthnFactory.php +++ b/src/symfony/src/DependencyInjection/Factory/Security/WebauthnFactory.php @@ -10,6 +10,7 @@ use Symfony\Component\Config\Definition\Builder\ParentNodeDefinitionInterface; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpFoundation\Request; @@ -35,6 +36,7 @@ use Webauthn\Bundle\Service\PublicKeyCredentialCreationOptionsFactory; use Webauthn\Bundle\Service\PublicKeyCredentialRequestOptionsFactory; use Webauthn\Denormalizer\WebauthnSerializerFactory; +use Webauthn\FakeCredentialGenerator; use function array_key_exists; use function assert; @@ -490,7 +492,7 @@ private function getAssertionOptionsBuilderId( new Reference(PublicKeyCredentialSourceRepositoryInterface::class), new Reference(PublicKeyCredentialRequestOptionsFactory::class), $config['profile'], - new Reference(WebauthnSerializerFactory::class), + new Reference(FakeCredentialGenerator::class, ContainerInterface::NULL_ON_INVALID_REFERENCE), ]); $container->setDefinition($optionsBuilderId, $optionsBuilder); diff --git a/src/symfony/src/DependencyInjection/WebauthnExtension.php b/src/symfony/src/DependencyInjection/WebauthnExtension.php index d49f64eeb..7240f9803 100644 --- a/src/symfony/src/DependencyInjection/WebauthnExtension.php +++ b/src/symfony/src/DependencyInjection/WebauthnExtension.php @@ -10,6 +10,7 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\FileLoader; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; @@ -45,6 +46,7 @@ use Webauthn\CeremonyStep\CeremonyStepManagerFactory; use Webauthn\CeremonyStep\TopOriginValidator; use Webauthn\Counter\CounterChecker; +use Webauthn\FakeCredentialGenerator; use Webauthn\MetadataService\CanLogData; use Webauthn\MetadataService\CertificateChain\CertificateChainValidator; use Webauthn\MetadataService\Event\CanDispatchEvents; @@ -100,6 +102,7 @@ public function load(array $configs, ContainerBuilder $container): void $container->setAlias('webauthn.http_client', $config['http_client']); $container->setAlias('webauthn.logger', $config['logger']); + $container->setAlias(FakeCredentialGenerator::class, $config['fake_credential_generator']); $container->setAlias(PublicKeyCredentialSourceRepository::class, $config['credential_repository']); $container->setAlias(PublicKeyCredentialSourceRepositoryInterface::class, $config['credential_repository']); $container->setAlias(PublicKeyCredentialUserEntityRepository::class, $config['user_repository']); @@ -287,6 +290,7 @@ private function loadRequestControllersSupport(ContainerBuilder $container, arra new Reference(PublicKeyCredentialSourceRepositoryInterface::class), new Reference(PublicKeyCredentialRequestOptionsFactory::class), $requestConfig['profile'], + new Reference(FakeCredentialGenerator::class, ContainerInterface::NULL_ON_INVALID_REFERENCE), ]); $container->setDefinition($assertionOptionsBuilderId, $assertionOptionsBuilder); } diff --git a/src/symfony/src/Resources/config/services.php b/src/symfony/src/Resources/config/services.php index ba1398686..45e939412 100644 --- a/src/symfony/src/Resources/config/services.php +++ b/src/symfony/src/Resources/config/services.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Lcobucci\Clock\SystemClock; +use Psr\Cache\CacheItemPoolInterface; use Psr\Http\Message\RequestFactoryInterface; use Psr\Log\NullLogger; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; @@ -42,10 +43,12 @@ use Webauthn\Denormalizer\PublicKeyCredentialSourceDenormalizer; use Webauthn\Denormalizer\PublicKeyCredentialUserEntityDenormalizer; use Webauthn\Denormalizer\WebauthnSerializerFactory; +use Webauthn\FakeCredentialGenerator; use Webauthn\MetadataService\Denormalizer\ExtensionDescriptorDenormalizer; use Webauthn\MetadataService\Denormalizer\MetadataStatementSerializerFactory; use Webauthn\PublicKeyCredentialLoader; use Webauthn\PublicKeyCredentialSourceRepository; +use Webauthn\SimpleFakeCredentialGenerator; use Webauthn\TokenBinding\IgnoreTokenBindingHandler; use Webauthn\TokenBinding\SecTokenBindingHandler; use Webauthn\TokenBinding\TokenBindingNotSupportedHandler; @@ -80,6 +83,11 @@ ->args([param('webauthn.secured_relying_party_ids')]) ; + $container + ->set(SimpleFakeCredentialGenerator::class) + ->args([service(CacheItemPoolInterface::class)->nullOnInvalid()]) + ; + $container ->set('webauthn.ceremony_step_manager.request') ->class(CeremonyStepManager::class) @@ -162,6 +170,7 @@ service(AuthenticatorAssertionResponseValidator::class), service(PublicKeyCredentialUserEntityRepositoryInterface::class), service(PublicKeyCredentialSourceRepository::class)->nullOnInvalid(), + service(FakeCredentialGenerator::class)->nullOnInvalid(), ]); $container diff --git a/src/webauthn/src/FakeCredentialGenerator.php b/src/webauthn/src/FakeCredentialGenerator.php new file mode 100644 index 000000000..1b06995fb --- /dev/null +++ b/src/webauthn/src/FakeCredentialGenerator.php @@ -0,0 +1,15 @@ +cache === null) { + return $this->generateCredentials($username); + } + + $cacheKey = 'fake_credentials_' . hash('xxh128', $username); + $cacheItem = $this->cache->getItem($cacheKey); + if ($cacheItem->isHit()) { + return $cacheItem->get(); + } + + $credentials = $this->generateCredentials($username); + $cacheItem->set($credentials); + $this->cache->save($cacheItem); + + return $credentials; + } + + /** + * @return PublicKeyCredentialDescriptor[] + */ + private function generateCredentials(string $username): array + { + $transports = [ + PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_USB, + PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_NFC, + PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_BLE, + ]; + $credentials = []; + for ($i = 0; $i < random_int(1, 3); $i++) { + $randomTransportKeys = array_rand($transports, random_int(1, count($transports))); + if (is_int($randomTransportKeys)) { + $randomTransportKeys = [$randomTransportKeys]; + } + $randomTransports = array_values(array_intersect_key($transports, array_flip($randomTransportKeys))); + $credentials[] = PublicKeyCredentialDescriptor::create( + PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, + hash('sha256', random_bytes(16) . $username), + $randomTransports + ); + } + + return $credentials; + } +}