From c55149ce226de412acc69d6b9e090eb093864c02 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Sat, 29 Jun 2024 09:35:46 +0200 Subject: [PATCH 1/2] Add FakeCredentialGenerator for fake credentials generation The update introduces a new FakeCredentialGenerator and its simple implementation, SimpleFakeCredentialGenerator, for generating fake credentials. This addition helps prevent username enumeration by providing fake credentials for nonexistent users. Changes have been made across multiple files, including service configuration updates and logic changes in the ProfileBasedRequestOptionsBuilder. --- phpstan-baseline.neon | 15 +++++ .../Controller/AssertionControllerFactory.php | 7 +- .../ProfileBasedRequestOptionsBuilder.php | 13 +++- .../src/DependencyInjection/Configuration.php | 8 +++ .../Factory/Security/WebauthnFactory.php | 4 +- .../DependencyInjection/WebauthnExtension.php | 4 ++ src/symfony/src/Resources/config/services.php | 9 +++ src/webauthn/src/FakeCredentialGenerator.php | 15 +++++ .../src/SimpleFakeCredentialGenerator.php | 67 +++++++++++++++++++ 9 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 src/webauthn/src/FakeCredentialGenerator.php create mode 100644 src/webauthn/src/SimpleFakeCredentialGenerator.php 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; + } +} From 9eee9f7bd0b3a598ecc5ba92a28041e77886cf54 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Sat, 29 Jun 2024 09:39:33 +0200 Subject: [PATCH 2/2] Add FakeCredentialGenerator for fake credentials generation The update introduces a new FakeCredentialGenerator and its simple implementation, SimpleFakeCredentialGenerator, for generating fake credentials. This addition helps prevent username enumeration by providing fake credentials for nonexistent users. Changes have been made across multiple files, including service configuration updates and logic changes in the ProfileBasedRequestOptionsBuilder. --- .github/workflows/integrate.yml | 6 ------ 1 file changed, 6 deletions(-) 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: