From d7b7eb73bc017c636f2bcf3b14ade11c3691cc0b Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Mon, 13 Nov 2023 13:41:43 +0100 Subject: [PATCH] Ceremony steps (#513) Ceremony steps --- Makefile | 14 +- RELEASES.md | 8 +- bin/phpunit | 21 + composer.json | 1 + deptrac.yaml | 4 - package.json | 2 +- phpstan-baseline.neon | 111 ++--- phpunit.xml.dist | 4 +- src/stimulus/assets/package.json | 4 +- .../Controller/AssertionControllerFactory.php | 9 +- .../AssertionResponseController.php | 4 +- .../AttestationControllerFactory.php | 18 +- .../AttestationResponseController.php | 4 +- ...CeremonyStepManagerFactoryCompilerPass.php | 118 +++++ .../src/DependencyInjection/Configuration.php | 12 + .../Factory/Security/WebauthnFactory.php | 28 +- .../Security/WebauthnServicesFactory.php | 58 +++ .../DependencyInjection/WebauthnExtension.php | 66 ++- .../src/Doctrine/Type/AAGUIDDataType.php | 7 +- ...ertionResponseValidationSucceededEvent.php | 2 +- .../Resources/config/android_safetynet.php | 8 +- src/symfony/src/Resources/config/security.php | 40 +- src/symfony/src/Resources/config/services.php | 42 +- .../Authenticator/WebauthnAuthenticator.php | 10 +- ...uthenticatorAssertionResponseValidator.php | 9 +- ...henticatorAttestationResponseValidator.php | 9 +- src/symfony/src/WebauthnBundle.php | 6 + ...idSafetyNetAttestationStatementSupport.php | 16 +- src/webauthn/src/AttestedCredentialData.php | 14 +- .../src/AuthenticatorAssertionResponse.php | 19 +- ...uthenticatorAssertionResponseValidator.php | 345 +++++--------- ...henticatorAttestationResponseValidator.php | 427 +++++------------- .../src/CeremonyStep/CeremonyStep.php | 22 + .../src/CeremonyStep/CeremonyStepManager.php | 40 ++ .../CeremonyStepManagerFactory.php | 159 +++++++ .../src/CeremonyStep/CheckAlgorithm.php | 76 ++++ .../CheckAllowedCredentialList.php | 38 ++ .../CheckAttestationFormatIsKnownAndValid.php | 48 ++ .../CheckBackupBitsAreConsistent.php | 31 ++ .../src/CeremonyStep/CheckChallenge.php | 31 ++ .../CheckClientDataCollectorType.php | 41 ++ .../src/CeremonyStep/CheckCounter.php | 36 ++ .../src/CeremonyStep/CheckCredentialId.php | 28 ++ .../src/CeremonyStep/CheckExtensions.php | 37 ++ .../CheckHasAttestedCredentialData.php | 32 ++ .../CeremonyStep/CheckMetadataStatement.php | 190 ++++++++ src/webauthn/src/CeremonyStep/CheckOrigin.php | 111 +++++ .../CheckRelyingPartyIdIdHash.php | 63 +++ .../src/CeremonyStep/CheckSignature.php | 90 ++++ .../src/CeremonyStep/CheckTopOrigin.php | 41 ++ .../src/CeremonyStep/CheckUserHandle.php | 37 ++ .../CeremonyStep/CheckUserVerification.php | 33 ++ .../src/CeremonyStep/CheckUserWasPresent.php | 26 ++ .../CeremonyStep/HostTopOriginValidator.php | 22 + .../src/CeremonyStep/TopOriginValidator.php | 10 + .../ClientDataCollector.php | 24 + .../ClientDataCollectorManager.php | 43 ++ .../WebauthnAuthenticationCollector.php | 34 ++ src/webauthn/src/CollectedClientData.php | 3 + .../src/Counter/ThrowExceptionIfInvalid.php | 3 +- .../AuthenticatorResponseDenormalizer.php | 2 +- ...enticatorResponseVerificationException.php | 2 +- .../Exception/InvalidUserHandleException.php | 15 + .../PublicKeyCredentialCreationOptions.php | 6 - .../src/PublicKeyCredentialLoader.php | 26 +- .../src/PublicKeyCredentialSource.php | 29 +- tests/library/AbstractTestCase.php | 118 +++-- .../AndroidKeyAttestationStatementTest.php | 22 - ...droidSafetyNetAttestationStatementTest.php | 36 +- .../AppleAttestationStatementTest.php | 4 +- tests/library/Functional/AssertionTest.php | 6 +- ...testationStatementWithTokenBindingTest.php | 4 +- tests/library/Functional/AttestationTest.php | 4 +- .../PackedAttestationStatementTest.php | 14 +- .../PublicKeyCreationCeremonyTest.php | 13 +- tests/library/Functional/W10Test.php | 32 +- .../TPMAttestationStatementSupportTest.php | 30 +- .../Unit/PublicKeyCredentialSourceTest.php | 8 +- tests/metadataStatements/solo.json | 50 ++ tests/metadataStatements/windows-hello.json | 84 ++++ tests/symfony/config/config.yml | 20 +- ...yNetApiKeyVerificationCompilerPassTest.php | 8 +- 82 files changed, 2368 insertions(+), 954 deletions(-) create mode 100644 bin/phpunit create mode 100644 src/symfony/src/DependencyInjection/Compiler/CeremonyStepManagerFactoryCompilerPass.php create mode 100644 src/webauthn/src/CeremonyStep/CeremonyStep.php create mode 100644 src/webauthn/src/CeremonyStep/CeremonyStepManager.php create mode 100644 src/webauthn/src/CeremonyStep/CeremonyStepManagerFactory.php create mode 100644 src/webauthn/src/CeremonyStep/CheckAlgorithm.php create mode 100644 src/webauthn/src/CeremonyStep/CheckAllowedCredentialList.php create mode 100644 src/webauthn/src/CeremonyStep/CheckAttestationFormatIsKnownAndValid.php create mode 100644 src/webauthn/src/CeremonyStep/CheckBackupBitsAreConsistent.php create mode 100644 src/webauthn/src/CeremonyStep/CheckChallenge.php create mode 100644 src/webauthn/src/CeremonyStep/CheckClientDataCollectorType.php create mode 100644 src/webauthn/src/CeremonyStep/CheckCounter.php create mode 100644 src/webauthn/src/CeremonyStep/CheckCredentialId.php create mode 100644 src/webauthn/src/CeremonyStep/CheckExtensions.php create mode 100644 src/webauthn/src/CeremonyStep/CheckHasAttestedCredentialData.php create mode 100644 src/webauthn/src/CeremonyStep/CheckMetadataStatement.php create mode 100644 src/webauthn/src/CeremonyStep/CheckOrigin.php create mode 100644 src/webauthn/src/CeremonyStep/CheckRelyingPartyIdIdHash.php create mode 100644 src/webauthn/src/CeremonyStep/CheckSignature.php create mode 100644 src/webauthn/src/CeremonyStep/CheckTopOrigin.php create mode 100644 src/webauthn/src/CeremonyStep/CheckUserHandle.php create mode 100644 src/webauthn/src/CeremonyStep/CheckUserVerification.php create mode 100644 src/webauthn/src/CeremonyStep/CheckUserWasPresent.php create mode 100644 src/webauthn/src/CeremonyStep/HostTopOriginValidator.php create mode 100644 src/webauthn/src/CeremonyStep/TopOriginValidator.php create mode 100644 src/webauthn/src/ClientDataCollector/ClientDataCollector.php create mode 100644 src/webauthn/src/ClientDataCollector/ClientDataCollectorManager.php create mode 100644 src/webauthn/src/ClientDataCollector/WebauthnAuthenticationCollector.php create mode 100644 src/webauthn/src/Exception/InvalidUserHandleException.php delete mode 100644 tests/library/Functional/AndroidKeyAttestationStatementTest.php create mode 100644 tests/metadataStatements/solo.json create mode 100644 tests/metadataStatements/windows-hello.json diff --git a/Makefile b/Makefile index d8af8167..e5f4cabd 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ mu: vendor ## Mutation tests .PHONY: tests tests: vendor ## Run all tests - vendor/bin/phpunit --color + bin/phpunit --color yarn test .PHONY: cc @@ -15,18 +15,6 @@ cc: vendor ## Show test coverage rates (HTML) cs: vendor ## Fix all files using defined ECS rules vendor/bin/ecs check --fix -.PHONY: tu -tu: vendor ## Run only unit tests - vendor/bin/phpunit --color --group Unit - -.PHONY: ti -ti: vendor ## Run only integration tests - vendor/bin/phpunit --color --group Integration - -.PHONY: tf -tf: vendor ## Run only functional tests - vendor/bin/phpunit --color --group Functional - .PHONY: st st: vendor ## Run static analyse XDEBUG_MODE=off vendor/bin/phpstan analyse diff --git a/RELEASES.md b/RELEASES.md index e5b5135d..0ffdcb03 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -13,10 +13,8 @@ Such releases will be considered as "pre-releases". ## Minor Release Support Matrix | Version | Supported | -|---------|--------------------| +|---------| ------------------ | +| 5.0.x | :white_check_mark: | | 4.8.x | :white_check_mark: | | 4.7.x | :white_check_mark: | -| 4.6.x | :white_check_mark: | -| <4.6.x | :x: | -| 3.3.x | :x: | -| < 3.3.x | :x: | +| <4.7.x | :x: | diff --git a/bin/phpunit b/bin/phpunit new file mode 100644 index 00000000..a534c6e2 --- /dev/null +++ b/bin/phpunit @@ -0,0 +1,21 @@ +#!/usr/bin/env php +run($GLOBALS['argv'])); +} else { + if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) { + echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n"; + exit(1); + } + + require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php'; +} diff --git a/composer.json b/composer.json index 7b101dc8..c761119a 100644 --- a/composer.json +++ b/composer.json @@ -121,6 +121,7 @@ "symfony/filesystem": "^6.1", "symfony/finder": "^6.1", "symfony/monolog-bundle": "^3.8", + "symfony/phpunit-bridge": "^6.3", "symfony/var-dumper": "^6.1", "symfony/yaml": "^6.1", "symplify/easy-coding-standard": "^12.0", diff --git a/deptrac.yaml b/deptrac.yaml index 3cedbfbf..68b46154 100644 --- a/deptrac.yaml +++ b/deptrac.yaml @@ -42,7 +42,3 @@ parameters: - 'Vendors' - 'Webauthn' - 'MetadataService' - skip_violations: - Webauthn\Bundle\Service\AuthenticatorAssertionResponseValidator: - - Webauthn\Util\CoseSignatureFixer - - Webauthn\U2FPublicKey diff --git a/package.json b/package.json index 70574945..51c6ce96 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "babel-jest": "^29.0", "clean-css-cli": "^5.6.2", "eslint": "^8.1.0", - "eslint-config-prettier": "^8.0.0", + "eslint-config-prettier": "^9.0.0", "eslint-plugin-jest": "^27.0.0", "jest": "^29.0.0", "jest-environment-jsdom": "^29.0", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ded658d8..7d07aff6 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -847,6 +847,16 @@ parameters: count: 1 path: src/symfony/src/DependencyInjection/Factory/Security/WebauthnFactory.php + - + message: "#^Parameter \\#3 \\$securedRpIds of method Webauthn\\\\Bundle\\\\DependencyInjection\\\\Factory\\\\Security\\\\WebauthnServicesFactory\\:\\:createAuthenticatorAssertionResponseValidator\\(\\) expects array\\, mixed given\\.$#" + count: 1 + path: src/symfony/src/DependencyInjection/Factory/Security/WebauthnFactory.php + + - + message: "#^Parameter \\#3 \\$securedRpIds of method Webauthn\\\\Bundle\\\\DependencyInjection\\\\Factory\\\\Security\\\\WebauthnServicesFactory\\:\\:createAuthenticatorAttestationResponseValidator\\(\\) expects array\\, mixed given\\.$#" + count: 1 + path: src/symfony/src/DependencyInjection/Factory/Security/WebauthnFactory.php + - message: "#^Parameter \\#4 \\$method of method Webauthn\\\\Bundle\\\\DependencyInjection\\\\Factory\\\\Security\\\\WebauthnFactory\\:\\:createResponseControllerAndRoute\\(\\) expects string, mixed given\\.$#" count: 2 @@ -917,11 +927,6 @@ parameters: count: 1 path: src/symfony/src/DependencyInjection/Factory/Security/WebauthnFactory.php - - - message: "#^Parameter \\#8 \\$securedRpIds of method Webauthn\\\\Bundle\\\\DependencyInjection\\\\Factory\\\\Security\\\\WebauthnFactory\\:\\:createAuthenticatorService\\(\\) expects array\\, mixed given\\.$#" - count: 1 - path: src/symfony/src/DependencyInjection/Factory/Security/WebauthnFactory.php - - message: "#^Parameter \\#9 \\$failureHandlerId of method Webauthn\\\\Bundle\\\\DependencyInjection\\\\Factory\\\\Security\\\\WebauthnFactory\\:\\:createAssertionRequestControllerAndRoute\\(\\) expects string, mixed given\\.$#" count: 1 @@ -984,7 +989,7 @@ parameters: - message: "#^Cannot access offset 'secured_rp_ids' on mixed\\.$#" - count: 3 + count: 2 path: src/symfony/src/DependencyInjection/WebauthnExtension.php - @@ -1054,7 +1059,7 @@ parameters: path: src/symfony/src/Doctrine/Type/AAGUIDDataType.php - - message: "#^Method Webauthn\\\\Bundle\\\\Doctrine\\\\Type\\\\AAGUIDDataType\\:\\:convertToPHPValue\\(\\) should return Symfony\\\\Component\\\\Uid\\\\AbstractUid\\|null but returns mixed\\.$#" + message: "#^Method Webauthn\\\\Bundle\\\\Doctrine\\\\Type\\\\AAGUIDDataType\\:\\:convertToPHPValue\\(\\) should return Symfony\\\\Component\\\\Uid\\\\Uuid\\|null but returns mixed\\.$#" count: 1 path: src/symfony/src/Doctrine/Type/AAGUIDDataType.php @@ -1159,14 +1164,6 @@ parameters: count: 1 path: src/symfony/src/Resources/config/services.php - - - message: """ - #^Fetching class constant class of deprecated class Webauthn\\\\TokenBinding\\\\TokenBindingHandler\\: - Since 4\\.3\\.0 and will be removed in 5\\.0\\.0$# - """ - count: 2 - path: src/symfony/src/Resources/config/services.php - - message: """ #^Fetching class constant class of deprecated class Webauthn\\\\TokenBinding\\\\TokenBindingNotSupportedHandler\\: @@ -1457,27 +1454,6 @@ parameters: count: 1 path: src/symfony/src/Security/WebauthnFirewallConfig.php - - - message: "#^Parameter \\#1 \\$credentialId of class Webauthn\\\\Bundle\\\\Event\\\\AuthenticatorAssertionResponseValidationSucceededEvent constructor expects string, string\\|null given\\.$#" - count: 1 - path: src/symfony/src/Service/AuthenticatorAssertionResponseValidator.php - - - - message: """ - #^Parameter \\$tokenBindingHandler of method Webauthn\\\\Bundle\\\\Service\\\\AuthenticatorAssertionResponseValidator\\:\\:__construct\\(\\) has typehint with deprecated interface Webauthn\\\\TokenBinding\\\\TokenBindingHandler\\: - Since 4\\.3\\.0 and will be removed in 5\\.0\\.0$# - """ - count: 1 - path: src/symfony/src/Service/AuthenticatorAssertionResponseValidator.php - - - - message: """ - #^Parameter \\$tokenBindingHandler of method Webauthn\\\\Bundle\\\\Service\\\\AuthenticatorAttestationResponseValidator\\:\\:__construct\\(\\) has typehint with deprecated interface Webauthn\\\\TokenBinding\\\\TokenBindingHandler\\: - Since 4\\.3\\.0 and will be removed in 5\\.0\\.0$# - """ - count: 1 - path: src/symfony/src/Service/AuthenticatorAttestationResponseValidator.php - - message: """ #^Call to method create\\(\\) of deprecated class Webauthn\\\\AuthenticationExtensions\\\\AuthenticationExtensions\\: @@ -2063,19 +2039,6 @@ parameters: count: 1 path: src/webauthn/src/AuthenticationExtensions/AuthenticationExtensionsClientOutputsLoader.php - - - message: """ - #^Call to deprecated method getTokenBinding\\(\\) of class Webauthn\\\\CollectedClientData\\: - Since 4\\.3\\.0 and will be removed in 5\\.0\\.0$# - """ - count: 2 - path: src/webauthn/src/AuthenticatorAssertionResponseValidator.php - - - - message: "#^Parameter \\#1 \\$data of static method Cose\\\\Key\\\\Key\\:\\:create\\(\\) expects array\\, array given\\.$#" - count: 1 - path: src/webauthn/src/AuthenticatorAssertionResponseValidator.php - - message: "#^Parameter \\$publicKeyCredentialSourceRepository of method Webauthn\\\\AuthenticatorAssertionResponseValidator\\:\\:__construct\\(\\) has typehint with deprecated interface Webauthn\\\\PublicKeyCredentialSourceRepository\\.$#" count: 1 @@ -2102,30 +2065,6 @@ parameters: count: 1 path: src/webauthn/src/AuthenticatorAssertionResponseValidator.php - - - message: """ - #^Call to deprecated method getTokenBinding\\(\\) of class Webauthn\\\\CollectedClientData\\: - Since 4\\.3\\.0 and will be removed in 5\\.0\\.0$# - """ - count: 2 - path: src/webauthn/src/AuthenticatorAttestationResponseValidator.php - - - - message: """ - #^Fetching deprecated class constant ATTESTATION_ECDAA of class Webauthn\\\\MetadataService\\\\Statement\\\\MetadataStatement\\: - since 4\\.2\\.0 and will be removed in 5\\.0\\.0\\. The ECDAA Trust Anchor does no longer exist in Webauthn specification\\.$# - """ - count: 1 - path: src/webauthn/src/AuthenticatorAttestationResponseValidator.php - - - - message: """ - #^Fetching deprecated class constant TYPE_ECDAA of class Webauthn\\\\AttestationStatement\\\\AttestationStatement\\: - since 4\\.2\\.0 and will be removed in 5\\.0\\.0\\. The ECDAA Trust Anchor does no longer exist in Webauthn specification\\.$# - """ - count: 1 - path: src/webauthn/src/AuthenticatorAttestationResponseValidator.php - - message: "#^Parameter \\$publicKeyCredentialSourceRepository of method Webauthn\\\\AuthenticatorAttestationResponseValidator\\:\\:__construct\\(\\) has typehint with deprecated interface Webauthn\\\\PublicKeyCredentialSourceRepository\\.$#" count: 1 @@ -2195,6 +2134,22 @@ parameters: count: 1 path: src/webauthn/src/AuthenticatorSelectionCriteria.php + - + message: """ + #^Fetching deprecated class constant ATTESTATION_ECDAA of class Webauthn\\\\MetadataService\\\\Statement\\\\MetadataStatement\\: + since 4\\.2\\.0 and will be removed in 5\\.0\\.0\\. The ECDAA Trust Anchor does no longer exist in Webauthn specification\\.$# + """ + count: 1 + path: src/webauthn/src/CeremonyStep/CheckMetadataStatement.php + + - + message: """ + #^Fetching deprecated class constant TYPE_ECDAA of class Webauthn\\\\AttestationStatement\\\\AttestationStatement\\: + since 4\\.2\\.0 and will be removed in 5\\.0\\.0\\. The ECDAA Trust Anchor does no longer exist in Webauthn specification\\.$# + """ + count: 1 + path: src/webauthn/src/CeremonyStep/CheckMetadataStatement.php + - message: """ #^Access to deprecated property \\$tokenBinding of class Webauthn\\\\CollectedClientData\\: @@ -2635,6 +2590,16 @@ parameters: count: 1 path: src/webauthn/src/PublicKeyCredentialSource.php + - + message: "#^Parameter \\#11 \\$backupEligible of static method Webauthn\\\\PublicKeyCredentialSource\\:\\:create\\(\\) expects bool\\|null, mixed given\\.$#" + count: 1 + path: src/webauthn/src/PublicKeyCredentialSource.php + + - + message: "#^Parameter \\#12 \\$backupStatus of static method Webauthn\\\\PublicKeyCredentialSource\\:\\:create\\(\\) expects bool\\|null, mixed given\\.$#" + count: 1 + path: src/webauthn/src/PublicKeyCredentialSource.php + - message: "#^Parameter \\#2 \\$type of static method Webauthn\\\\PublicKeyCredentialSource\\:\\:create\\(\\) expects string, mixed given\\.$#" count: 1 diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1e76257c..ddc6030b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -5,7 +5,7 @@ bootstrap="tests/bootstrap.php" colors="true" cacheDirectory=".phpunit.cache" - displayDetailsOnTestsThatTriggerWarnings="true" + displayDetailsOnTestsThatTriggerDeprecations="true" > @@ -20,7 +20,7 @@ - + diff --git a/src/stimulus/assets/package.json b/src/stimulus/assets/package.json index 5977cbc4..a118d147 100644 --- a/src/stimulus/assets/package.json +++ b/src/stimulus/assets/package.json @@ -22,10 +22,10 @@ }, "peerDependencies": { "@hotwired/stimulus": "^3.0.0", - "@simplewebauthn/browser": "^7.0.0" + "@simplewebauthn/browser": "^8.0.0" }, "devDependencies": { "@hotwired/stimulus": "^3.0.0", - "@simplewebauthn/browser": "^7.0.0" + "@simplewebauthn/browser": "^8.0.0" } } diff --git a/src/symfony/src/Controller/AssertionControllerFactory.php b/src/symfony/src/Controller/AssertionControllerFactory.php index 647a21d3..2a2c9a91 100644 --- a/src/symfony/src/Controller/AssertionControllerFactory.php +++ b/src/symfony/src/Controller/AssertionControllerFactory.php @@ -32,7 +32,7 @@ public function __construct( private readonly ValidatorInterface $validator, private readonly PublicKeyCredentialRequestOptionsFactory $publicKeyCredentialRequestOptionsFactory, private readonly PublicKeyCredentialLoader $publicKeyCredentialLoader, - private readonly AuthenticatorAssertionResponseValidator $attestationResponseValidator, + private readonly AuthenticatorAssertionResponseValidator $authenticatorAssertionResponseValidator, private readonly PublicKeyCredentialUserEntityRepositoryInterface $publicKeyCredentialUserEntityRepository, private readonly PublicKeyCredentialSourceRepository|PublicKeyCredentialSourceRepositoryInterface $publicKeyCredentialSourceRepository ) { @@ -101,17 +101,18 @@ public function createAssertionResponseController( } /** - * @param string[] $securedRelyingPartyIds + * @param null|string[] $securedRelyingPartyIds */ public function createResponseController( OptionsStorage $optionStorage, SuccessHandler $successHandler, FailureHandler|AuthenticationFailureHandlerInterface $failureHandler, - array $securedRelyingPartyIds + null|array $securedRelyingPartyIds = null, + null|AuthenticatorAssertionResponseValidator $authenticatorAssertionResponseValidator = null, ): AssertionResponseController { return new AssertionResponseController( $this->publicKeyCredentialLoader, - $this->attestationResponseValidator, + $authenticatorAssertionResponseValidator ?? $this->authenticatorAssertionResponseValidator, $this->logger, $optionStorage, $successHandler, diff --git a/src/symfony/src/Controller/AssertionResponseController.php b/src/symfony/src/Controller/AssertionResponseController.php index 0e99f270..62d93bf6 100644 --- a/src/symfony/src/Controller/AssertionResponseController.php +++ b/src/symfony/src/Controller/AssertionResponseController.php @@ -24,7 +24,7 @@ final class AssertionResponseController { /** - * @param string[] $securedRelyingPartyIds + * @param null|string[] $securedRelyingPartyIds */ public function __construct( private readonly PublicKeyCredentialLoader $publicKeyCredentialLoader, @@ -33,7 +33,7 @@ public function __construct( private readonly OptionsStorage $optionsStorage, private readonly SuccessHandler $successHandler, private readonly FailureHandler|AuthenticationFailureHandlerInterface $failureHandler, - private readonly array $securedRelyingPartyIds, + private readonly null|array $securedRelyingPartyIds = null, private readonly ?PublicKeyCredentialSourceRepositoryInterface $publicKeyCredentialSourceRepository = null ) { } diff --git a/src/symfony/src/Controller/AttestationControllerFactory.php b/src/symfony/src/Controller/AttestationControllerFactory.php index 22f22b9b..77ebf709 100644 --- a/src/symfony/src/Controller/AttestationControllerFactory.php +++ b/src/symfony/src/Controller/AttestationControllerFactory.php @@ -76,36 +76,30 @@ public function createRequestController( } /** - * @param string[] $securedRelyingPartyIds * @deprecated since 4.5.0 and will be removed in 5.0.0. Please use createResponseController instead. * @infection-ignore-all */ public function createAttestationResponseController( OptionsStorage $optionStorage, SuccessHandler $successHandler, - FailureHandler|AuthenticationFailureHandlerInterface $failureHandler, - array $securedRelyingPartyIds + FailureHandler|AuthenticationFailureHandlerInterface $failureHandler ): AttestationResponseController { - return $this->createResponseController( - $optionStorage, - $successHandler, - $failureHandler, - $securedRelyingPartyIds - ); + return $this->createResponseController($optionStorage, $successHandler, $failureHandler); } /** - * @param string[] $securedRelyingPartyIds + * @param null|string[] $securedRelyingPartyIds */ public function createResponseController( OptionsStorage $optionStorage, SuccessHandler $successHandler, FailureHandler|AuthenticationFailureHandlerInterface $failureHandler, - array $securedRelyingPartyIds + null|array $securedRelyingPartyIds = null, + null|AuthenticatorAttestationResponseValidator $attestationResponseValidator = null, ): AttestationResponseController { return new AttestationResponseController( $this->publicKeyCredentialLoader, - $this->attestationResponseValidator, + $attestationResponseValidator ?? $this->attestationResponseValidator, $this->publicKeyCredentialSourceRepository, $optionStorage, $successHandler, diff --git a/src/symfony/src/Controller/AttestationResponseController.php b/src/symfony/src/Controller/AttestationResponseController.php index ca4f4973..785faa49 100644 --- a/src/symfony/src/Controller/AttestationResponseController.php +++ b/src/symfony/src/Controller/AttestationResponseController.php @@ -27,7 +27,7 @@ final class AttestationResponseController { /** - * @param string[] $securedRelyingPartyIds + * @param null|string[] $securedRelyingPartyIds */ public function __construct( private readonly PublicKeyCredentialLoader $publicKeyCredentialLoader, @@ -36,7 +36,7 @@ public function __construct( private readonly OptionsStorage $optionStorage, private readonly SuccessHandler $successHandler, private readonly FailureHandler|AuthenticationFailureHandlerInterface $failureHandler, - private readonly array $securedRelyingPartyIds, + private readonly null|array $securedRelyingPartyIds = null, ) { if (! $this->credentialSourceRepository instanceof PublicKeyCredentialSourceRepositoryInterface) { trigger_deprecation( diff --git a/src/symfony/src/DependencyInjection/Compiler/CeremonyStepManagerFactoryCompilerPass.php b/src/symfony/src/DependencyInjection/Compiler/CeremonyStepManagerFactoryCompilerPass.php new file mode 100644 index 00000000..9088e595 --- /dev/null +++ b/src/symfony/src/DependencyInjection/Compiler/CeremonyStepManagerFactoryCompilerPass.php @@ -0,0 +1,118 @@ +hasDefinition(CeremonyStepManagerFactory::class)) { + return; + } + $definition = $container->getDefinition(CeremonyStepManagerFactory::class); + $this->setAttestationStatementSupportManager($container, $definition); + $this->setExtensionOutputCheckerHandler($container, $definition); + $this->enableMetadataStatementSupport($container, $definition); + $this->enableCertificateChainValidator($container, $definition); + $this->setAlgorithmManager($container, $definition); + $this->enableTopOriginValidator($container, $definition); + $this->setSecuredRelyingPartyId($container, $definition); + } + + private function setAttestationStatementSupportManager(ContainerBuilder $container, Definition $definition): void + { + if (! $container->hasDefinition(AttestationStatementSupportManager::class)) { + return; + } + + $definition->addMethodCall( + 'setAttestationStatementSupportManager', + [new Reference(AttestationStatementSupportManager::class)] + ); + } + + private function setExtensionOutputCheckerHandler(ContainerBuilder $container, Definition $definition): void + { + if (! $container->hasDefinition(ExtensionOutputCheckerHandler::class)) { + return; + } + + $definition->addMethodCall( + 'setExtensionOutputCheckerHandler', + [new Reference(ExtensionOutputCheckerHandler::class)] + ); + } + + private function enableMetadataStatementSupport(ContainerBuilder $container, Definition $definition): void + { + if ( + ! $container->hasAlias(MetadataStatementRepository::class) || + ! $container->hasAlias(StatusReportRepository::class) || + ! $container->hasAlias(CertificateChainValidator::class) + ) { + return; + } + + $definition->addMethodCall('enableMetadataStatementSupport', [ + new Reference(MetadataStatementRepository::class), + new Reference(StatusReportRepository::class), + new Reference(CertificateChainValidator::class), + ]); + } + + private function enableCertificateChainValidator(ContainerBuilder $container, Definition $definition): void + { + if (! $container->hasDefinition(CertificateChainValidator::class)) { + return; + } + + $definition->addMethodCall('enableCertificateChainValidator', [ + new Reference(CertificateChainValidator::class), + ]); + } + + private function enableTopOriginValidator(ContainerBuilder $container, Definition $definition): void + { + if (! $container->hasDefinition(TopOriginValidator::class) && ! $container->hasAlias( + TopOriginValidator::class + )) { + return; + } + + $definition->addMethodCall('enableTopOriginValidator', [new Reference(TopOriginValidator::class)]); + } + + private function setAlgorithmManager(ContainerBuilder $container, Definition $definition): void + { + if (! $container->hasDefinition('webauthn.cose.algorithm.manager')) { + return; + } + + $definition->addMethodCall('setAlgorithmManager', [new Reference('webauthn.cose.algorithm.manager')]); + } + + private function setSecuredRelyingPartyId(ContainerBuilder $container, Definition $definition): void + { + if (! $container->hasParameter('webauthn.secured_relying_party_ids')) { + return; + } + + $definition->addMethodCall('setSecuredRelyingPartyId', [ + $container->getParameter('webauthn.secured_relying_party_ids'), + ]); + } +} diff --git a/src/symfony/src/DependencyInjection/Configuration.php b/src/symfony/src/DependencyInjection/Configuration.php index 6265c553..555eaebc 100644 --- a/src/symfony/src/DependencyInjection/Configuration.php +++ b/src/symfony/src/DependencyInjection/Configuration.php @@ -98,6 +98,14 @@ public function getConfigTreeBuilder(): TreeBuilder ->setDeprecated('web-auth/webauthn-symfony-bundle', '4.3.0') ->info('This handler will check the token binding header from the request. By default, it is ignored.') ->end() + ->arrayNode('secured_rp_ids') + ->treatFalseLike(null) + ->treatTrueLike(null) + ->treatNullLike(null) + ->useAttributeAsKey('name') + ->scalarPrototype() + ->end() + ->end() ->scalarNode('counter_checker') ->defaultValue(ThrowExceptionIfInvalid::class) ->cannotBeEmpty() @@ -105,6 +113,10 @@ public function getConfigTreeBuilder(): TreeBuilder 'This service will check if the counter is valid. By default it throws an exception (recommended).' ) ->end() + ->scalarNode('top_origin_validator') + ->defaultNull() + ->info('For cross origin (e.g. iframe), this service will be in charge of verifying the top origin.') + ->end() ->end(); $this->addCreationProfilesConfig($rootNode); diff --git a/src/symfony/src/DependencyInjection/Factory/Security/WebauthnFactory.php b/src/symfony/src/DependencyInjection/Factory/Security/WebauthnFactory.php index 22c2f284..b98851c6 100644 --- a/src/symfony/src/DependencyInjection/Factory/Security/WebauthnFactory.php +++ b/src/symfony/src/DependencyInjection/Factory/Security/WebauthnFactory.php @@ -74,6 +74,12 @@ final class WebauthnFactory implements FirewallListenerFactoryInterface, Authent public const FIREWALL_CONFIG_ID_PREFIX = 'security.firewall_config.webauthn.'; + public const AUTHENTICATOR_ATTESTATION_RESPONSE_VALIDATOR_ID_PREFIX = 'security.authenticator_attestation_response_validator.webauthn.'; + + public const AUTHENTICATOR_ASSERTION_RESPONSE_VALIDATOR_ID_PREFIX = 'security.authenticator_assertion_response_validator.webauthn.'; + + public const CEREMONY_STEP_MANAGER_ID_PREFIX = 'security.ceremony_step_manager.webauthn.'; + public const FIREWALL_CONFIG_DEFINITION_ID = 'webauthn.security.firewall_config'; /** @@ -226,6 +232,16 @@ public function createAuthenticator( string $userProviderId ): string|array { $firewallConfigId = $this->servicesFactory->createWebauthnFirewallConfig($container, $firewallName, $config); + $authenticatorAssertionResponseValidatorId = $this->servicesFactory->createAuthenticatorAssertionResponseValidator( + $container, + $firewallName, + $config['secured_rp_ids'] + ); + $authenticatorAttestationResponseValidatorId = $this->servicesFactory->createAuthenticatorAttestationResponseValidator( + $container, + $firewallName, + $config['secured_rp_ids'] + ); $this->createAssertionControllersAndRoutes($container, $firewallName, $config); $this->createAttestationControllersAndRoutes($container, $firewallName, $config); @@ -238,7 +254,8 @@ public function createAuthenticator( $config['failure_handler'], $firewallConfigId, $config['options_storage'], - $config['secured_rp_ids'] + $authenticatorAssertionResponseValidatorId, + $authenticatorAttestationResponseValidatorId ); } @@ -252,9 +269,6 @@ public function createListeners(ContainerBuilder $container, string $firewallNam return []; } - /** - * @param string[] $securedRpIds - */ private function createAuthenticatorService( ContainerBuilder $container, string $firewallName, @@ -263,7 +277,8 @@ private function createAuthenticatorService( string $failureHandlerId, string $firewallConfigId, string $optionsStorageId, - array $securedRpIds + string $authenticatorAssertionResponseValidatorId, + string $authenticatorAttestationResponseValidatorId ): string { $authenticatorId = self::AUTHENTICATOR_ID_PREFIX . $firewallName; $container @@ -273,7 +288,8 @@ private function createAuthenticatorService( ->replaceArgument(2, new Reference($successHandlerId)) ->replaceArgument(3, new Reference($failureHandlerId)) ->replaceArgument(4, new Reference($optionsStorageId)) - ->replaceArgument(5, $securedRpIds) + ->replaceArgument(8, new Reference($authenticatorAssertionResponseValidatorId)) + ->replaceArgument(9, new Reference($authenticatorAttestationResponseValidatorId)) ->addMethodCall('setLogger', [new Reference('webauthn.logger')]); return $authenticatorId; diff --git a/src/symfony/src/DependencyInjection/Factory/Security/WebauthnServicesFactory.php b/src/symfony/src/DependencyInjection/Factory/Security/WebauthnServicesFactory.php index b2ab1f03..4cb017d3 100644 --- a/src/symfony/src/DependencyInjection/Factory/Security/WebauthnServicesFactory.php +++ b/src/symfony/src/DependencyInjection/Factory/Security/WebauthnServicesFactory.php @@ -6,6 +6,12 @@ use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; +use Webauthn\AuthenticatorAssertionResponseValidator; +use Webauthn\AuthenticatorAttestationResponseValidator; +use Webauthn\CeremonyStep\CeremonyStepManager; +use Webauthn\CeremonyStep\CeremonyStepManagerFactory; /** * @internal Helper class for WebauthnFactory only @@ -28,4 +34,56 @@ public function createWebauthnFirewallConfig( return $firewallConfigId; } + + /** + * @param string[] $securedRpIds + */ + public function createAuthenticatorAssertionResponseValidator( + ContainerBuilder $container, + string $firewallName, + array $securedRpIds + ): string { + $ceremonyStepManagerId = WebauthnFactory::CEREMONY_STEP_MANAGER_ID_PREFIX . 'request.' . $firewallName; + $container + ->setDefinition($ceremonyStepManagerId, new Definition(CeremonyStepManager::class)) + ->setFactory([new Reference(CeremonyStepManagerFactory::class), 'requestCeremony']) + ->setArguments([$securedRpIds]) + ; + + $authenticatorAssertionResponseValidatorId = WebauthnFactory::AUTHENTICATOR_ASSERTION_RESPONSE_VALIDATOR_ID_PREFIX . $firewallName; + $container + ->setDefinition( + $authenticatorAssertionResponseValidatorId, + new Definition(AuthenticatorAssertionResponseValidator::class) + ) + ->setArguments([null, null, null, null, null, new Reference($ceremonyStepManagerId)]); + + return $authenticatorAssertionResponseValidatorId; + } + + /** + * @param string[] $securedRpIds + */ + public function createAuthenticatorAttestationResponseValidator( + ContainerBuilder $container, + string $firewallName, + array $securedRpIds + ): string { + $ceremonyStepManagerId = WebauthnFactory::CEREMONY_STEP_MANAGER_ID_PREFIX . 'creation.' . $firewallName; + $container + ->setDefinition($ceremonyStepManagerId, new Definition(CeremonyStepManager::class)) + ->setFactory([new Reference(CeremonyStepManagerFactory::class), 'creationCeremony']) + ->setArguments([$securedRpIds]) + ; + + $authenticatorAttestationResponseValidatorId = WebauthnFactory::AUTHENTICATOR_ATTESTATION_RESPONSE_VALIDATOR_ID_PREFIX . $firewallName; + $container + ->setDefinition( + $authenticatorAttestationResponseValidatorId, + new Definition(AuthenticatorAttestationResponseValidator::class) + ) + ->setArguments([null, null, null, null, null, new Reference($ceremonyStepManagerId)]); + + return $authenticatorAttestationResponseValidatorId; + } } diff --git a/src/symfony/src/DependencyInjection/WebauthnExtension.php b/src/symfony/src/DependencyInjection/WebauthnExtension.php index 4f6ebb98..4dc1b7c9 100644 --- a/src/symfony/src/DependencyInjection/WebauthnExtension.php +++ b/src/symfony/src/DependencyInjection/WebauthnExtension.php @@ -19,6 +19,8 @@ use Symfony\Component\Validator\Validator\ValidatorInterface; use Webauthn\AttestationStatement\AttestationStatementSupport; use Webauthn\AuthenticationExtensions\ExtensionOutputChecker; +use Webauthn\AuthenticatorAssertionResponseValidator; +use Webauthn\AuthenticatorAttestationResponseValidator; use Webauthn\Bundle\Controller\AssertionControllerFactory; use Webauthn\Bundle\Controller\AssertionRequestController; use Webauthn\Bundle\Controller\AssertionResponseController; @@ -39,6 +41,9 @@ use Webauthn\Bundle\Repository\PublicKeyCredentialUserEntityRepositoryInterface; use Webauthn\Bundle\Service\PublicKeyCredentialCreationOptionsFactory; use Webauthn\Bundle\Service\PublicKeyCredentialRequestOptionsFactory; +use Webauthn\CeremonyStep\CeremonyStepManager; +use Webauthn\CeremonyStep\CeremonyStepManagerFactory; +use Webauthn\CeremonyStep\TopOriginValidator; use Webauthn\Counter\CounterChecker; use Webauthn\MetadataService\CanLogData; use Webauthn\MetadataService\CertificateChain\CertificateChainValidator; @@ -83,11 +88,15 @@ public function load(array $configs, ContainerBuilder $container): void $container->registerForAutoconfiguration(CanLogData::class)->addTag(LoggerSetterCompilerPass::TAG); $container->registerForAutoconfiguration(Algorithm::class)->addTag(CoseAlgorithmCompilerPass::TAG); + $container->setParameter('webauthn.secured_relying_party_ids', $config['secured_rp_ids']); $container->setAlias('webauthn.event_dispatcher', $config['event_dispatcher']); $container->setAlias('webauthn.clock', $config['clock']); if ($config['request_factory'] !== null) { $container->setAlias('webauthn.request_factory', $config['request_factory']); } + if ($config['top_origin_validator'] !== null) { + $container->setAlias(TopOriginValidator::class, $config['top_origin_validator']); + } $container->setAlias('webauthn.http_client', $config['http_client']); $container->setAlias('webauthn.logger', $config['logger']); @@ -212,6 +221,31 @@ private function loadCreationControllersSupport(ContainerBuilder $container, arr ->addTag('controller.service_arguments'); $container->setDefinition($attestationRequestControllerId, $attestationRequestController); + $creationCeremonyStepManagerId = sprintf( + 'webauthn.controller.creation.response.ceremony_step_manager.%s', + $name + ); + $container + ->setDefinition($creationCeremonyStepManagerId, new Definition(CeremonyStepManager::class)) + ->setFactory([new Reference(CeremonyStepManagerFactory::class), 'creationCeremony']) + ->setArguments([$creationConfig['secured_rp_ids']]) + ; + + $attestationResponseValidatorId = sprintf( + 'webauthn.controller.creation.response.attestation_validator.%s', + $name + ); + $attestationResponseValidator = new Definition(AuthenticatorAttestationResponseValidator::class); + $attestationResponseValidator->setArguments([ + null, + null, + null, + null, + null, + new Reference($creationCeremonyStepManagerId), + ]); + $container->setDefinition($attestationResponseValidatorId, $attestationResponseValidator); + $attestationResponseControllerId = sprintf('webauthn.controller.creation.response.%s', $name); $attestationResponseController = new Definition(AttestationResponseController::class); $attestationResponseController->setFactory( @@ -221,7 +255,8 @@ private function loadCreationControllersSupport(ContainerBuilder $container, arr new Reference($creationConfig['options_storage']), new Reference($creationConfig['success_handler']), new Reference($creationConfig['failure_handler']), - $creationConfig['secured_rp_ids'], + null, + new Reference($attestationResponseValidatorId), ]); $attestationResponseController->addTag(DynamicRouteCompilerPass::TAG, [ 'method' => $creationConfig['result_method'], @@ -263,7 +298,6 @@ private function loadRequestControllersSupport(ContainerBuilder $container, arra new Reference($requestConfig['options_storage']), new Reference($requestConfig['options_handler']), new Reference($requestConfig['failure_handler']), - $requestConfig['secured_rp_ids'], ]) ->addTag(DynamicRouteCompilerPass::TAG, [ 'method' => $requestConfig['options_method'], @@ -273,6 +307,31 @@ private function loadRequestControllersSupport(ContainerBuilder $container, arra ->addTag('controller.service_arguments'); $container->setDefinition($assertionRequestControllerId, $assertionRequestController); + $requestCeremonyStepManagerId = sprintf( + 'webauthn.controller.request.response.ceremony_step_manager.%s', + $name + ); + $container + ->setDefinition($requestCeremonyStepManagerId, new Definition(CeremonyStepManager::class)) + ->setFactory([new Reference(CeremonyStepManagerFactory::class), 'requestCeremony']) + ->setArguments([$requestConfig['secured_rp_ids']]) + ; + + $assertionResponseValidatorId = sprintf( + 'webauthn.controller.request.response.assertion_validator.%s', + $name + ); + $assertionResponseValidator = new Definition(AuthenticatorAssertionResponseValidator::class); + $assertionResponseValidator->setArguments([ + null, + null, + null, + null, + null, + new Reference($requestCeremonyStepManagerId), + ]); + $container->setDefinition($assertionResponseValidatorId, $assertionResponseValidator); + $assertionResponseControllerId = sprintf('webauthn.controller.request.response.%s', $name); $assertionResponseController = new Definition(AssertionResponseController::class); $assertionResponseController->setFactory( @@ -282,7 +341,8 @@ private function loadRequestControllersSupport(ContainerBuilder $container, arra new Reference($requestConfig['options_storage']), new Reference($requestConfig['success_handler']), new Reference($requestConfig['failure_handler']), - $requestConfig['secured_rp_ids'], + null, + new Reference($assertionResponseValidatorId), ]); $assertionResponseController->addTag(DynamicRouteCompilerPass::TAG, [ 'method' => $requestConfig['result_method'], diff --git a/src/symfony/src/Doctrine/Type/AAGUIDDataType.php b/src/symfony/src/Doctrine/Type/AAGUIDDataType.php index aea9db0d..58436046 100644 --- a/src/symfony/src/Doctrine/Type/AAGUIDDataType.php +++ b/src/symfony/src/Doctrine/Type/AAGUIDDataType.php @@ -6,23 +6,22 @@ use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\Type; -use Symfony\Component\Uid\AbstractUid; use Symfony\Component\Uid\Uuid; final class AAGUIDDataType extends Type { public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string { - if (! $value instanceof AbstractUid) { + if (! $value instanceof Uuid) { return $value; } return $value->__toString(); } - public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?AbstractUid + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?Uuid { - if ($value instanceof AbstractUid || mb_strlen((string) $value, '8bit') !== 36) { + if ($value instanceof Uuid || mb_strlen((string) $value, '8bit') !== 36) { return $value; } diff --git a/src/symfony/src/Event/AuthenticatorAssertionResponseValidationSucceededEvent.php b/src/symfony/src/Event/AuthenticatorAssertionResponseValidationSucceededEvent.php index 00f1facc..b70b33c0 100644 --- a/src/symfony/src/Event/AuthenticatorAssertionResponseValidationSucceededEvent.php +++ b/src/symfony/src/Event/AuthenticatorAssertionResponseValidationSucceededEvent.php @@ -13,7 +13,7 @@ class AuthenticatorAssertionResponseValidationSucceededEvent extends BaseAuthenticatorAssertionResponseValidationSucceededEvent { public function __construct( - string $credentialId, + null|string $credentialId, AuthenticatorAssertionResponse $authenticatorAssertionResponse, PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, ServerRequestInterface|string $request, diff --git a/src/symfony/src/Resources/config/android_safetynet.php b/src/symfony/src/Resources/config/android_safetynet.php index 121dee41..442b9b21 100644 --- a/src/symfony/src/Resources/config/android_safetynet.php +++ b/src/symfony/src/Resources/config/android_safetynet.php @@ -4,8 +4,11 @@ use Jose\Component\KeyManagement\JWKFactory; use Jose\Component\Signature\Algorithm\RS256; +use Psr\Clock\ClockInterface; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Webauthn\AttestationStatement\AndroidSafetyNetAttestationStatementSupport; +use function Symfony\Component\DependencyInjection\Loader\Configurator\param; +use function Symfony\Component\DependencyInjection\Loader\Configurator\service; return static function (ContainerConfigurator $container): void { $container = $container->services() @@ -16,7 +19,8 @@ if (class_exists(JWKFactory::class) && class_exists(RS256::class)) { $container ->set(AndroidSafetyNetAttestationStatementSupport::class) - ->call('setMaxAge', ['%webauthn.android_safetynet.max_age%']) - ->call('setLeeway', ['%webauthn.android_safetynet.leeway%']); + ->args([service(ClockInterface::class)->nullOnInvalid()]) + ->call('setMaxAge', [param('webauthn.android_safetynet.max_age')]) + ->call('setLeeway', [param('webauthn.android_safetynet.leeway')]); } }; diff --git a/src/symfony/src/Resources/config/security.php b/src/symfony/src/Resources/config/security.php index 25bef2fc..3272d3af 100644 --- a/src/symfony/src/Resources/config/security.php +++ b/src/symfony/src/Resources/config/security.php @@ -7,8 +7,6 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; -use Webauthn\AuthenticatorAssertionResponseValidator; -use Webauthn\AuthenticatorAttestationResponseValidator; use Webauthn\Bundle\DependencyInjection\Factory\Security\WebauthnFactory; use Webauthn\Bundle\Repository\PublicKeyCredentialSourceRepositoryInterface; use Webauthn\Bundle\Repository\PublicKeyCredentialUserEntityRepositoryInterface; @@ -41,20 +39,30 @@ $container->set(CacheStorage::class)->args([service(CacheItemPoolInterface::class)]); $container->set(DefaultCreationOptionsHandler::class); $container->set(DefaultRequestOptionsHandler::class); - $container->set(WebauthnFactory::AUTHENTICATOR_DEFINITION_ID, WebauthnAuthenticator::class)->abstract()->args( - [abstract_arg('Firewall config'), abstract_arg('User provider'), abstract_arg('Success handler'), abstract_arg( - 'Failure handler' - ), abstract_arg('Options Storage'), abstract_arg('Secured Relying Party IDs'), service( - PublicKeyCredentialSourceRepositoryInterface::class - ), service( - PublicKeyCredentialUserEntityRepositoryInterface::class - ), service(PublicKeyCredentialLoader::class), service( - AuthenticatorAssertionResponseValidator::class - ), service(AuthenticatorAttestationResponseValidator::class), ] - ); - $container->set(WebauthnFactory::FIREWALL_CONFIG_DEFINITION_ID, WebauthnFirewallConfig::class)->abstract() - ->args([[], // Firewall settings - abstract_arg('Firewall name'), service('security.http_utils'), ]); + $container + ->set(WebauthnFactory::AUTHENTICATOR_DEFINITION_ID, WebauthnAuthenticator::class) + ->abstract() + ->args([ + abstract_arg('Firewall config'), + abstract_arg('User provider'), + abstract_arg('Success handler'), + abstract_arg('Failure handler'), + abstract_arg('Options Storage'), + service(PublicKeyCredentialSourceRepositoryInterface::class), + service(PublicKeyCredentialUserEntityRepositoryInterface::class), + service(PublicKeyCredentialLoader::class), + abstract_arg('Authenticator Assertion Response Validator'), + abstract_arg( + 'Authenticator Attestation Response Validator' + ), //service(AuthenticatorAttestationResponseValidator::class) + ]); + $container + ->set(WebauthnFactory::FIREWALL_CONFIG_DEFINITION_ID, WebauthnFirewallConfig::class) + ->abstract() + ->args([ + [], // Firewall settings + abstract_arg('Firewall name'), service('security.http_utils'), + ]); $container->set(CurrentUserEntityGuesser::class)->args( [service(TokenStorageInterface::class), service(PublicKeyCredentialUserEntityRepositoryInterface::class)] ); diff --git a/src/symfony/src/Resources/config/services.php b/src/symfony/src/Resources/config/services.php index 6068c8c7..b4b38922 100644 --- a/src/symfony/src/Resources/config/services.php +++ b/src/symfony/src/Resources/config/services.php @@ -26,6 +26,8 @@ use Webauthn\Bundle\Service\DefaultSuccessHandler; use Webauthn\Bundle\Service\PublicKeyCredentialCreationOptionsFactory; use Webauthn\Bundle\Service\PublicKeyCredentialRequestOptionsFactory; +use Webauthn\CeremonyStep\CeremonyStepManager; +use Webauthn\CeremonyStep\CeremonyStepManagerFactory; use Webauthn\Counter\ThrowExceptionIfInvalid; use Webauthn\Denormalizer\AttestationStatementDenormalizer; use Webauthn\Denormalizer\AuthenticationExtensionsDenormalizer; @@ -45,8 +47,8 @@ use Webauthn\PublicKeyCredentialSourceRepository; use Webauthn\TokenBinding\IgnoreTokenBindingHandler; use Webauthn\TokenBinding\SecTokenBindingHandler; -use Webauthn\TokenBinding\TokenBindingHandler; use Webauthn\TokenBinding\TokenBindingNotSupportedHandler; +use function Symfony\Component\DependencyInjection\Loader\Configurator\param; use function Symfony\Component\DependencyInjection\Loader\Configurator\service; return static function (ContainerConfigurator $container): void { @@ -66,36 +68,44 @@ ->factory([SystemClock::class, 'fromSystemTimezone']) ; + $container + ->set(CeremonyStepManagerFactory::class) + ; + + $container + ->set('webauthn.ceremony_step_manager.creation') + ->class(CeremonyStepManager::class) + ->factory([service(CeremonyStepManagerFactory::class), 'creationCeremony']) + ->args([param('webauthn.secured_relying_party_ids')]) + ; + + $container + ->set('webauthn.ceremony_step_manager.request') + ->class(CeremonyStepManager::class) + ->factory([service(CeremonyStepManagerFactory::class), 'requestCeremony']) + ->args([param('webauthn.secured_relying_party_ids')]) + ; + $container ->set(AuthenticatorAttestationResponseValidator::class) - ->args([ - service(AttestationStatementSupportManager::class), - null, - service(TokenBindingHandler::class)->nullOnInvalid(), - service(ExtensionOutputCheckerHandler::class), - ]) + ->args([null, null, null, null, null, service('webauthn.ceremony_step_manager.creation')]) ->public(); $container ->set(AuthenticatorAssertionResponseValidator::class) ->class(AuthenticatorAssertionResponseValidator::class) - ->args([ - null, - service(TokenBindingHandler::class)->nullOnInvalid(), - service(ExtensionOutputCheckerHandler::class), - service('webauthn.cose.algorithm.manager'), - ]) + ->args([null, null, null, null, null, service('webauthn.ceremony_step_manager.request')]) ->public(); $container ->set(PublicKeyCredentialLoader::class) - ->args([service(AttestationObjectLoader::class), service('webauthn-serializer')]) + ->args([null, service('webauthn-serializer')]) ->public(); $container ->set(PublicKeyCredentialCreationOptionsFactory::class) - ->args(['%webauthn.creation_profiles%']) + ->args([param('webauthn.creation_profiles')]) ->public(); $container ->set(PublicKeyCredentialRequestOptionsFactory::class) - ->args(['%webauthn.request_profiles%']) + ->args([param('webauthn.request_profiles')]) ->public(); $container diff --git a/src/symfony/src/Security/Http/Authenticator/WebauthnAuthenticator.php b/src/symfony/src/Security/Http/Authenticator/WebauthnAuthenticator.php index 176d322a..db7b8922 100644 --- a/src/symfony/src/Security/Http/Authenticator/WebauthnAuthenticator.php +++ b/src/symfony/src/Security/Http/Authenticator/WebauthnAuthenticator.php @@ -47,16 +47,12 @@ final class WebauthnAuthenticator implements AuthenticatorInterface, Interactive { private LoggerInterface $logger; - /** - * @param string[] $securedRelyingPartyIds - */ public function __construct( private readonly WebauthnFirewallConfig $firewallConfig, private readonly UserProviderInterface $userProvider, private readonly AuthenticationSuccessHandlerInterface $successHandler, private readonly AuthenticationFailureHandlerInterface $failureHandler, private readonly OptionsStorage $optionsStorage, - private readonly array $securedRelyingPartyIds, private readonly PublicKeyCredentialSourceRepository|PublicKeyCredentialSourceRepositoryInterface $publicKeyCredentialSourceRepository, private readonly PublicKeyCredentialUserEntityRepositoryInterface $credentialUserEntityRepository, private readonly PublicKeyCredentialLoader $publicKeyCredentialLoader, @@ -206,8 +202,7 @@ private function processWithAssertion(Request $request): Passport $response, $publicKeyCredentialRequestOptions, $request->getHost(), - $userEntity?->id, - $this->securedRelyingPartyIds + $userEntity?->id ); if ($this->publicKeyCredentialSourceRepository instanceof CanSaveCredentialSource) { $this->publicKeyCredentialSourceRepository->saveCredentialSource($publicKeyCredentialSource); @@ -268,8 +263,7 @@ private function processWithAttestation(Request $request): Passport $credentialSource = $this->attestationResponseValidator->check( $response, $publicKeyCredentialCreationOptions, - $request->getHost(), - $this->securedRelyingPartyIds + $request->getHost() ); if ($this->credentialUserEntityRepository->findOneByUsername($userEntity->name) !== null) { throw InvalidDataException::create($userEntity, 'The username already exists'); diff --git a/src/symfony/src/Service/AuthenticatorAssertionResponseValidator.php b/src/symfony/src/Service/AuthenticatorAssertionResponseValidator.php index 8a2a3d6d..a74297c0 100644 --- a/src/symfony/src/Service/AuthenticatorAssertionResponseValidator.php +++ b/src/symfony/src/Service/AuthenticatorAssertionResponseValidator.php @@ -13,17 +13,22 @@ use Webauthn\AuthenticatorAssertionResponseValidator as BaseAuthenticatorAssertionResponseValidator; use Webauthn\Bundle\Event\AuthenticatorAssertionResponseValidationFailedEvent; use Webauthn\Bundle\Event\AuthenticatorAssertionResponseValidationSucceededEvent; +use Webauthn\CeremonyStep\CeremonyStepManager; use Webauthn\PublicKeyCredentialRequestOptions; use Webauthn\PublicKeyCredentialSource; use Webauthn\TokenBinding\TokenBindingHandler; +/** + * @deprecated since 4.3.0. The class is deprecated and will be removed in 5.0.0. Please use "Webauthn\AuthenticatorAssertionResponseValidator" instead. + */ final class AuthenticatorAssertionResponseValidator extends BaseAuthenticatorAssertionResponseValidator { public function __construct( ?TokenBindingHandler $tokenBindingHandler, ExtensionOutputCheckerHandler $extensionOutputCheckerHandler, ?Manager $algorithmManager, - ?EventDispatcherInterface $eventDispatcher + ?EventDispatcherInterface $eventDispatcher, + null|CeremonyStepManager $ceremonyStepManager = null ) { trigger_deprecation( 'web-auth/webauthn-symfony-bundle', @@ -34,7 +39,7 @@ public function __construct( BaseAuthenticatorAssertionResponseValidator::class ) ); - parent::__construct(null, $tokenBindingHandler, $extensionOutputCheckerHandler, $algorithmManager, $eventDispatcher); + parent::__construct(null, $tokenBindingHandler, $extensionOutputCheckerHandler, $algorithmManager, $eventDispatcher, $ceremonyStepManager); } protected function createAuthenticatorAssertionResponseValidationSucceededEvent( diff --git a/src/symfony/src/Service/AuthenticatorAttestationResponseValidator.php b/src/symfony/src/Service/AuthenticatorAttestationResponseValidator.php index 1e73d059..866cca68 100644 --- a/src/symfony/src/Service/AuthenticatorAttestationResponseValidator.php +++ b/src/symfony/src/Service/AuthenticatorAttestationResponseValidator.php @@ -12,18 +12,23 @@ use Webauthn\AuthenticatorAttestationResponse; use Webauthn\AuthenticatorAttestationResponseValidator as BaseAuthenticatorAttestationResponseValidator; use Webauthn\Bundle\Event\AuthenticatorAttestationResponseValidationSucceededEvent; +use Webauthn\CeremonyStep\CeremonyStepManager; use Webauthn\Event\AuthenticatorAttestationResponseValidationFailedEvent; use Webauthn\PublicKeyCredentialCreationOptions; use Webauthn\PublicKeyCredentialSource; use Webauthn\TokenBinding\TokenBindingHandler; +/** + * @deprecated since 4.3.0. The class is deprecated and will be removed in 5.0.0. Please use "Webauthn\BaseAuthenticatorAttestationResponseValidator" instead. + */ final class AuthenticatorAttestationResponseValidator extends BaseAuthenticatorAttestationResponseValidator { public function __construct( AttestationStatementSupportManager $attestationStatementSupportManager, ?TokenBindingHandler $tokenBindingHandler, ExtensionOutputCheckerHandler $extensionOutputCheckerHandler, - ?EventDispatcherInterface $eventDispatcher + ?EventDispatcherInterface $eventDispatcher, + null|CeremonyStepManager $ceremonyStepManager = null ) { trigger_deprecation( 'web-auth/webauthn-symfony-bundle', @@ -34,7 +39,7 @@ public function __construct( BaseAuthenticatorAttestationResponseValidator::class ) ); - parent::__construct($attestationStatementSupportManager, null, $tokenBindingHandler, $extensionOutputCheckerHandler, $eventDispatcher); + parent::__construct($attestationStatementSupportManager, null, $tokenBindingHandler, $extensionOutputCheckerHandler, $eventDispatcher, $ceremonyStepManager); } protected function createAuthenticatorAttestationResponseValidationSucceededEvent( diff --git a/src/symfony/src/WebauthnBundle.php b/src/symfony/src/WebauthnBundle.php index f6597227..4f09f279 100644 --- a/src/symfony/src/WebauthnBundle.php +++ b/src/symfony/src/WebauthnBundle.php @@ -12,6 +12,7 @@ use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\HttpKernel\Bundle\Bundle; use Webauthn\Bundle\DependencyInjection\Compiler\AttestationStatementSupportCompilerPass; +use Webauthn\Bundle\DependencyInjection\Compiler\CeremonyStepManagerFactoryCompilerPass; use Webauthn\Bundle\DependencyInjection\Compiler\CoseAlgorithmCompilerPass; use Webauthn\Bundle\DependencyInjection\Compiler\CounterCheckerSetterCompilerPass; use Webauthn\Bundle\DependencyInjection\Compiler\DynamicRouteCompilerPass; @@ -35,6 +36,11 @@ public function getContainerExtension(): ?ExtensionInterface public function build(ContainerBuilder $container): void { parent::build($container); + $container->addCompilerPass( + new CeremonyStepManagerFactoryCompilerPass(), + PassConfig::TYPE_BEFORE_OPTIMIZATION, + 0 + ); $container->addCompilerPass( new EventDispatcherSetterCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, diff --git a/src/webauthn/src/AttestationStatement/AndroidSafetyNetAttestationStatementSupport.php b/src/webauthn/src/AttestationStatement/AndroidSafetyNetAttestationStatementSupport.php index 0605d501..b9ba6a8a 100644 --- a/src/webauthn/src/AttestationStatement/AndroidSafetyNetAttestationStatementSupport.php +++ b/src/webauthn/src/AttestationStatement/AndroidSafetyNetAttestationStatementSupport.php @@ -20,6 +20,7 @@ use Jose\Component\Signature\JWS; use Jose\Component\Signature\JWSVerifier; use Jose\Component\Signature\Serializer\CompactSerializer; +use Psr\Clock\ClockInterface; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; @@ -60,8 +61,16 @@ final class AndroidSafetyNetAttestationStatementSupport implements AttestationSt private EventDispatcherInterface $dispatcher; - public function __construct() - { + public function __construct( + private readonly null|ClockInterface $clock = null + ) { + if ($this->clock === null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.8.0', + 'The parameter "$clock" will be required in 5.0.0. Please set a clock instance.' + ); + } if (! class_exists(RS256::class)) { throw UnsupportedFeatureException::create( 'The algorithm RS256 is missing. Did you forget to install the package web-token/jwt-signature-algorithm-rsa?' @@ -243,7 +252,8 @@ private function validatePayload( is_int($payload['timestampMs']) || throw AttestationStatementVerificationException::create( 'Invalid attestation object. Timestamp shall be an integer.' ); - $currentTime = time() * 1000; + + $currentTime = ($this->clock?->now()->getTimestamp() ?? time()) * 1000; $payload['timestampMs'] <= $currentTime + $this->leeway || throw AttestationStatementVerificationException::create( sprintf( 'Invalid attestation object. Issued in the future. Current time: %d. Response time: %d', diff --git a/src/webauthn/src/AttestedCredentialData.php b/src/webauthn/src/AttestedCredentialData.php index 2a1585ad..a0d589d2 100644 --- a/src/webauthn/src/AttestedCredentialData.php +++ b/src/webauthn/src/AttestedCredentialData.php @@ -6,7 +6,6 @@ use JsonSerializable; use ParagonIE\ConstantTime\Base64; -use Symfony\Component\Uid\AbstractUid; use Symfony\Component\Uid\Uuid; use Webauthn\Exception\InvalidDataException; use function array_key_exists; @@ -18,17 +17,14 @@ class AttestedCredentialData implements JsonSerializable { public function __construct( - public AbstractUid $aaguid, + public Uuid $aaguid, public readonly string $credentialId, public readonly ?string $credentialPublicKey ) { } - public static function create( - AbstractUid $aaguid, - string $credentialId, - ?string $credentialPublicKey = null - ): self { + public static function create(Uuid $aaguid, string $credentialId, ?string $credentialPublicKey = null): self + { return new self($aaguid, $credentialId, $credentialPublicKey); } @@ -36,7 +32,7 @@ public static function create( * @deprecated since 4.7.0. Please use the property directly. * @infection-ignore-all */ - public function getAaguid(): AbstractUid + public function getAaguid(): Uuid { return $this->aaguid; } @@ -45,7 +41,7 @@ public function getAaguid(): AbstractUid * @deprecated since 4.7.0. Please use the property directly. * @infection-ignore-all */ - public function setAaguid(AbstractUid $aaguid): void + public function setAaguid(Uuid $aaguid): void { $this->aaguid = $aaguid; } diff --git a/src/webauthn/src/AuthenticatorAssertionResponse.php b/src/webauthn/src/AuthenticatorAssertionResponse.php index 79de3a57..8b105a42 100644 --- a/src/webauthn/src/AuthenticatorAssertionResponse.php +++ b/src/webauthn/src/AuthenticatorAssertionResponse.php @@ -4,6 +4,8 @@ namespace Webauthn; +use Webauthn\AttestationStatement\AttestationObject; + /** * @see https://www.w3.org/TR/webauthn/#authenticatorassertionresponse */ @@ -13,7 +15,8 @@ public function __construct( CollectedClientData $clientDataJSON, public readonly AuthenticatorData $authenticatorData, public readonly string $signature, - public readonly ?string $userHandle + public readonly ?string $userHandle, + public readonly null|AttestationObject $attestationObject = null, ) { parent::__construct($clientDataJSON); } @@ -22,18 +25,10 @@ public static function create( CollectedClientData $clientDataJSON, AuthenticatorData $authenticatorData, string $signature, - ?string $userHandle = null + ?string $userHandle = null, + null|AttestationObject $attestationObject = null, ): self { - return new self($clientDataJSON, $authenticatorData, $signature, $userHandle); - } - - /** - * @deprecated since 4.7.0. Please use the property directly. - * @infection-ignore-all - */ - public function getAuthenticatorData(): AuthenticatorData - { - return $this->authenticatorData; + return new self($clientDataJSON, $authenticatorData, $signature, $userHandle, $attestationObject); } /** diff --git a/src/webauthn/src/AuthenticatorAssertionResponseValidator.php b/src/webauthn/src/AuthenticatorAssertionResponseValidator.php index c1972a17..96a065c0 100644 --- a/src/webauthn/src/AuthenticatorAssertionResponseValidator.php +++ b/src/webauthn/src/AuthenticatorAssertionResponseValidator.php @@ -4,20 +4,16 @@ namespace Webauthn; -use CBOR\Decoder; -use CBOR\Normalizable; use Cose\Algorithm\Manager; -use Cose\Algorithm\Signature\Signature; -use Cose\Key\Key; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Throwable; -use Webauthn\AuthenticationExtensions\AuthenticationExtensions; use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler; +use Webauthn\CeremonyStep\CeremonyStepManager; +use Webauthn\CeremonyStep\CeremonyStepManagerFactory; use Webauthn\Counter\CounterChecker; -use Webauthn\Counter\ThrowExceptionIfInvalid; use Webauthn\Event\AuthenticatorAssertionResponseValidationFailedEvent; use Webauthn\Event\AuthenticatorAssertionResponseValidationSucceededEvent; use Webauthn\Exception\AuthenticatorResponseVerificationException; @@ -25,65 +21,85 @@ use Webauthn\MetadataService\Event\CanDispatchEvents; use Webauthn\MetadataService\Event\NullEventDispatcher; use Webauthn\TokenBinding\TokenBindingHandler; -use Webauthn\Util\CoseSignatureFixer; -use function count; -use function in_array; -use function is_array; use function is_string; -use function parse_url; class AuthenticatorAssertionResponseValidator implements CanLogData, CanDispatchEvents { - private readonly Decoder $decoder; - - private CounterChecker $counterChecker; - private LoggerInterface $logger; + private readonly CeremonyStepManagerFactory $ceremonyStepManagerFactory; + private EventDispatcherInterface $eventDispatcher; public function __construct( - private readonly null|PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository, - private readonly null|TokenBindingHandler $tokenBindingHandler, - private readonly ExtensionOutputCheckerHandler $extensionOutputCheckerHandler, - private readonly null|Manager $algorithmManager, - null|EventDispatcherInterface $eventDispatcher = null, + private readonly null|PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository = null, + private readonly null|TokenBindingHandler $tokenBindingHandler = null, + null|ExtensionOutputCheckerHandler $extensionOutputCheckerHandler = null, + null|Manager $algorithmManager = null, + null|EventDispatcherInterface $eventDispatcher = null, + private null|CeremonyStepManager $ceremonyStepManager = null ) { + if ($this->publicKeyCredentialSourceRepository !== null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.6.0', + 'The parameter "$publicKeyCredentialSourceRepository" is deprecated since 4.6.0 and will be removed in 5.0.0. Please set "null" instead.' + ); + } if ($this->tokenBindingHandler !== null) { trigger_deprecation( - 'web-auth/webauthn-symfony-bundle', + 'web-auth/webauthn-lib', '4.3.0', 'The parameter "$tokenBindingHandler" is deprecated since 4.3.0 and will be removed in 5.0.0. Please set "null" instead.' ); } - if ($this->publicKeyCredentialSourceRepository !== null) { + if ($extensionOutputCheckerHandler !== null) { trigger_deprecation( - 'web-auth/webauthn-symfony-bundle', - '4.6.0', - 'The parameter "$publicKeyCredentialSourceRepository" is deprecated since 4.6.0 and will be removed in 5.0.0. Please set "null" instead.' + 'web-auth/webauthn-lib', + '4.8.0', + 'The parameter "$extensionOutputCheckerHandler" is deprecated since 4.8.0 and will be removed in 5.0.0. Please set "null" instead and inject a CheckExtensions object into the CeremonyStepManager.' ); } - if ($eventDispatcher === null) { - $this->eventDispatcher = new NullEventDispatcher(); - } else { - $this->eventDispatcher = $eventDispatcher; + if ($algorithmManager !== null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.8.0', + 'The parameter "$algorithmManager" is deprecated since 4.8.0 and will be removed in 5.0.0. Please set "null" instead and inject a CheckSignature object into the CeremonyStepManager.' + ); + } + $this->eventDispatcher = $eventDispatcher ?? new NullEventDispatcher(); + if ($eventDispatcher !== null) { trigger_deprecation( 'web-auth/webauthn-lib', '4.5.0', 'The parameter "$eventDispatcher" is deprecated since 4.5.0 will be removed in 5.0.0. Please use `setEventDispatcher` instead.' ); } - $this->decoder = Decoder::create(); - $this->counterChecker = new ThrowExceptionIfInvalid(); + if ($this->ceremonyStepManager === null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.8.0', + 'The parameter "$ceremonyStepManager" will mandatory in 5.0.0. Please set a CeremonyStepManager object instead and set null for $algorithmManager and $extensionOutputCheckerHandler.' + ); + } $this->logger = new NullLogger(); + + $this->ceremonyStepManagerFactory = new CeremonyStepManagerFactory(); + if ($extensionOutputCheckerHandler !== null) { + $this->ceremonyStepManagerFactory->setExtensionOutputCheckerHandler($extensionOutputCheckerHandler); + } + if ($algorithmManager !== null) { + $this->ceremonyStepManagerFactory->setAlgorithmManager($algorithmManager); + } } public static function create( - null|PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository, - null|TokenBindingHandler $tokenBindingHandler, - ExtensionOutputCheckerHandler $extensionOutputCheckerHandler, - null|Manager $algorithmManager, - null|EventDispatcherInterface $eventDispatcher = null + null|PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository = null, + null|TokenBindingHandler $tokenBindingHandler = null, + null|ExtensionOutputCheckerHandler $extensionOutputCheckerHandler = null, + null|Manager $algorithmManager = null, + null|EventDispatcherInterface $eventDispatcher = null, + null|CeremonyStepManager $ceremonyStepManager = null ): self { return new self( $publicKeyCredentialSourceRepository, @@ -91,6 +107,7 @@ public static function create( $extensionOutputCheckerHandler, $algorithmManager, $eventDispatcher, + $ceremonyStepManager ); } @@ -105,7 +122,7 @@ public function check( PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, ServerRequestInterface|string $request, ?string $userHandle, - array $securedRelyingPartyId = [] + null|array $securedRelyingPartyId = null ): PublicKeyCredentialSource { if ($request instanceof ServerRequestInterface) { trigger_deprecation( @@ -124,165 +141,70 @@ public function check( '4.6.0', sprintf( 'Passing a string as first to the method `check` of the class "%s" is deprecated since 4.6.0. Please inject a %s object instead.', - ServerRequestInterface::class, + self::class, PublicKeyCredentialSource::class ) ); } + if ($securedRelyingPartyId !== null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.8.0', + sprintf( + 'Passing a list or secured relying party IDs to the method `check` of the class "%s" is deprecated since 4.8.0 and will be removed in 5.0.0. Please inject a CheckOrigin into the CeremonyStepManager instead.', + self::class + ) + ); + } + + if ($credentialId instanceof PublicKeyCredentialSource) { + $publicKeyCredentialSource = $credentialId; + } else { + $this->publicKeyCredentialSourceRepository instanceof PublicKeyCredentialSourceRepository || throw AuthenticatorResponseVerificationException::create( + 'Please pass the Public Key Credential Source to the method "check".' + ); + $publicKeyCredentialSource = $this->publicKeyCredentialSourceRepository->findOneByCredentialId( + $credentialId + ); + } + $publicKeyCredentialSource !== null || throw AuthenticatorResponseVerificationException::create( + 'The credential ID is invalid.' + ); + $host = is_string($request) ? $request : $request->getUri() + ->getHost(); + + if ($this->ceremonyStepManager === null) { + $this->ceremonyStepManager = $this->ceremonyStepManagerFactory->requestCeremony($securedRelyingPartyId); + } + try { $this->logger->info('Checking the authenticator assertion response', [ 'credentialId' => $credentialId, 'authenticatorAssertionResponse' => $authenticatorAssertionResponse, 'publicKeyCredentialRequestOptions' => $publicKeyCredentialRequestOptions, - 'host' => is_string($request) ? $request : $request->getUri() - ->getHost(), + 'host' => $host, 'userHandle' => $userHandle, ]); - $publicKeyCredentialSource = null; - if ($credentialId instanceof PublicKeyCredentialSource) { - $publicKeyCredentialSource = $credentialId; - } else { - $this->publicKeyCredentialSourceRepository instanceof PublicKeyCredentialSourceRepository || throw AuthenticatorResponseVerificationException::create( - 'The parameter "$publicKeyCredentialSourceRepository" is required.' - ); - $publicKeyCredentialSource = $this->publicKeyCredentialSourceRepository->findOneByCredentialId( - $credentialId - ); - } - $publicKeyCredentialSource !== null || throw AuthenticatorResponseVerificationException::create( - 'The credential ID is invalid.' - ); - if (count($publicKeyCredentialRequestOptions->allowCredentials) !== 0) { - $this->isCredentialIdAllowed( - $publicKeyCredentialSource->publicKeyCredentialId, - $publicKeyCredentialRequestOptions->allowCredentials - ) || throw AuthenticatorResponseVerificationException::create('The credential ID is not allowed.'); - } - $attestedCredentialData = $publicKeyCredentialSource->getAttestedCredentialData(); - $credentialUserHandle = $publicKeyCredentialSource->userHandle; - $responseUserHandle = $authenticatorAssertionResponse->userHandle; - if ($userHandle !== null) { //If the user was identified before the authentication ceremony was initiated, - $credentialUserHandle === $userHandle || throw AuthenticatorResponseVerificationException::create( - 'Invalid user handle' - ); - if ($responseUserHandle !== null && $responseUserHandle !== '') { - $credentialUserHandle === $responseUserHandle || throw AuthenticatorResponseVerificationException::create( - 'Invalid user handle' - ); - } - } else { - ($responseUserHandle !== '' && $credentialUserHandle === $responseUserHandle) || throw AuthenticatorResponseVerificationException::create( - 'Invalid user handle' - ); - } - $credentialPublicKey = $attestedCredentialData->credentialPublicKey; - $credentialPublicKey !== null || throw AuthenticatorResponseVerificationException::create( - 'No public key available.' - ); - $isU2F = U2FPublicKey::isU2FKey($credentialPublicKey); - if ($isU2F === true) { - $credentialPublicKey = U2FPublicKey::convertToCoseKey($credentialPublicKey); - } - $stream = new StringStream($credentialPublicKey); - $credentialPublicKeyStream = $this->decoder->decode($stream); - $stream->isEOF() || throw AuthenticatorResponseVerificationException::create( - 'Invalid key. Presence of extra bytes.' - ); - $stream->close(); - $C = $authenticatorAssertionResponse->clientDataJSON; - $C->type === 'webauthn.get' || throw AuthenticatorResponseVerificationException::create( - 'The client data type is not "webauthn.get".' - ); - hash_equals( - $publicKeyCredentialRequestOptions->challenge, - $C->challenge - ) || throw AuthenticatorResponseVerificationException::create('Invalid challenge.'); - $rpId = $publicKeyCredentialRequestOptions->rpId ?? (is_string($request) ? $request : $request->getUri() - ->getHost()); - $facetId = $this->getFacetId( - $rpId, - $publicKeyCredentialRequestOptions->extensions, - $authenticatorAssertionResponse->authenticatorData - ->extensions - ); - $parsedRelyingPartyId = parse_url($C->origin); - is_array($parsedRelyingPartyId) || throw AuthenticatorResponseVerificationException::create( - 'Invalid origin' - ); - if (! in_array($facetId, $securedRelyingPartyId, true)) { - $scheme = $parsedRelyingPartyId['scheme'] ?? ''; - $scheme === 'https' || throw AuthenticatorResponseVerificationException::create( - 'Invalid scheme. HTTPS required.' - ); - } - $clientDataRpId = $parsedRelyingPartyId['host'] ?? ''; - $clientDataRpId !== '' || throw AuthenticatorResponseVerificationException::create('Invalid origin rpId.'); - $rpIdLength = mb_strlen($facetId); - mb_substr( - '.' . $clientDataRpId, - -($rpIdLength + 1) - ) === '.' . $facetId || throw AuthenticatorResponseVerificationException::create('rpId mismatch.'); - if (! is_string($request) && $C->getTokenBinding() !== null) { - $this->tokenBindingHandler?->check($C->getTokenBinding(), $request); - } - $rpIdHash = hash('sha256', $isU2F ? $C->origin : $facetId, true); - hash_equals( - $rpIdHash, - $authenticatorAssertionResponse->authenticatorData - ->rpIdHash - ) || throw AuthenticatorResponseVerificationException::create('rpId hash mismatch.'); - if ($publicKeyCredentialRequestOptions->userVerification === AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED) { - $authenticatorAssertionResponse->authenticatorData - ->isUserPresent() || throw AuthenticatorResponseVerificationException::create( - 'User was not present' - ); - $authenticatorAssertionResponse->authenticatorData - ->isUserVerified() || throw AuthenticatorResponseVerificationException::create( - 'User authentication required.' - ); - } - $extensionsClientOutputs = $authenticatorAssertionResponse->authenticatorData - ->extensions; - if ($extensionsClientOutputs !== null) { - $this->extensionOutputCheckerHandler->check( - $publicKeyCredentialRequestOptions->extensions, - $extensionsClientOutputs - ); - } - $getClientDataJSONHash = hash( - 'sha256', - $authenticatorAssertionResponse->clientDataJSON - ->rawData, - true - ); - $dataToVerify = $authenticatorAssertionResponse->authenticatorData - ->authData . $getClientDataJSONHash; - $signature = $authenticatorAssertionResponse->signature; - $credentialPublicKeyStream instanceof Normalizable || throw AuthenticatorResponseVerificationException::create( - 'Invalid attestation object. Unexpected object.' - ); - $normalizedData = $credentialPublicKeyStream->normalize(); - is_array($normalizedData) || throw AuthenticatorResponseVerificationException::create( - 'Invalid attestation object. Unexpected object.' - ); - $coseKey = Key::create($normalizedData); - $algorithm = $this->algorithmManager?->get($coseKey->alg()); - $algorithm instanceof Signature || throw AuthenticatorResponseVerificationException::create( - 'Invalid algorithm identifier. Should refer to a signature algorithm' + + $this->ceremonyStepManager->process( + $publicKeyCredentialSource, + $authenticatorAssertionResponse, + $publicKeyCredentialRequestOptions, + $userHandle, + $host ); - $signature = CoseSignatureFixer::fix($signature, $algorithm); - $algorithm->verify( - $dataToVerify, - $coseKey, - $signature - ) || throw AuthenticatorResponseVerificationException::create('Invalid signature.'); - $storedCounter = $publicKeyCredentialSource->counter; - $responseCounter = $authenticatorAssertionResponse->authenticatorData - ->signCount; - if ($responseCounter !== 0 || $storedCounter !== 0) { - $this->counterChecker->check($publicKeyCredentialSource, $responseCounter); + + $publicKeyCredentialSource->counter = $authenticatorAssertionResponse->authenticatorData->signCount; //26.1. + $publicKeyCredentialSource->backupEligible = $authenticatorAssertionResponse->authenticatorData->isBackupEligible(); //26.2. + $publicKeyCredentialSource->backupStatus = $authenticatorAssertionResponse->authenticatorData->isBackedUp(); //26.2. + if ($publicKeyCredentialSource->uvInitialized === false) { + $publicKeyCredentialSource->uvInitialized = $authenticatorAssertionResponse->authenticatorData->isUserVerified(); //26.3. } - $publicKeyCredentialSource->counter = $responseCounter; + /* + * 26.3. + * OPTIONALLY, if response.attestationObject is present, update credentialRecord.attestationObject to the value of response.attestationObject and update credentialRecord.attestationClientDataJSON to the value of response.clientDataJSON. + */ + if (is_string( $credentialId ) && ($this->publicKeyCredentialSourceRepository instanceof PublicKeyCredentialSourceRepository)) { @@ -298,13 +220,14 @@ public function check( null, $authenticatorAssertionResponse, $publicKeyCredentialRequestOptions, - $request, + $host, $userHandle, $publicKeyCredentialSource ) ); + // 27. return $publicKeyCredentialSource; - } catch (Throwable $throwable) { + } catch (AuthenticatorResponseVerificationException $throwable) { $this->logger->error('An error occurred', [ 'exception' => $throwable, ]); @@ -313,7 +236,7 @@ public function check( $credentialId, $authenticatorAssertionResponse, $publicKeyCredentialRequestOptions, - $request, + $host, $userHandle, $throwable ) @@ -332,9 +255,12 @@ public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): v $this->eventDispatcher = $eventDispatcher; } + /** + * @deprecated since 4.8.0 and will be removed in 5.0.0. Please inject a CheckCounter object into a CeremonyStepManager instead. + */ public function setCounterChecker(CounterChecker $counterChecker): self { - $this->counterChecker = $counterChecker; + $this->ceremonyStepManagerFactory->setCounterChecker($counterChecker); return $this; } @@ -342,11 +268,11 @@ protected function createAuthenticatorAssertionResponseValidationSucceededEvent( null|string $credentialId, AuthenticatorAssertionResponse $authenticatorAssertionResponse, PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, - ServerRequestInterface|string $request, + ServerRequestInterface|string $host, ?string $userHandle, PublicKeyCredentialSource $publicKeyCredentialSource ): AuthenticatorAssertionResponseValidationSucceededEvent { - if ($request instanceof ServerRequestInterface) { + if ($host instanceof ServerRequestInterface) { trigger_deprecation( 'web-auth/webauthn-lib', '4.5.0', @@ -361,7 +287,7 @@ protected function createAuthenticatorAssertionResponseValidationSucceededEvent( $credentialId, $authenticatorAssertionResponse, $publicKeyCredentialRequestOptions, - $request, + $host, $userHandle, $publicKeyCredentialSource ); @@ -371,11 +297,11 @@ protected function createAuthenticatorAssertionResponseValidationFailedEvent( string|PublicKeyCredentialSource $credentialId, AuthenticatorAssertionResponse $authenticatorAssertionResponse, PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, - ServerRequestInterface|string $request, + ServerRequestInterface|string $host, ?string $userHandle, Throwable $throwable ): AuthenticatorAssertionResponseValidationFailedEvent { - if ($request instanceof ServerRequestInterface) { + if ($host instanceof ServerRequestInterface) { trigger_deprecation( 'web-auth/webauthn-lib', '4.5.0', @@ -390,42 +316,9 @@ protected function createAuthenticatorAssertionResponseValidationFailedEvent( $credentialId, $authenticatorAssertionResponse, $publicKeyCredentialRequestOptions, - $request, + $host, $userHandle, $throwable ); } - - /** - * @param array $allowedCredentials - */ - private function isCredentialIdAllowed(string $credentialId, array $allowedCredentials): bool - { - foreach ($allowedCredentials as $allowedCredential) { - if (hash_equals($allowedCredential->id, $credentialId)) { - return true; - } - } - return false; - } - - private function getFacetId( - string $rpId, - AuthenticationExtensions $authenticationExtensionsClientInputs, - null|AuthenticationExtensions $authenticationExtensionsClientOutputs - ): string { - if ($authenticationExtensionsClientOutputs === null || ! $authenticationExtensionsClientInputs->has( - 'appid' - ) || ! $authenticationExtensionsClientOutputs->has('appid')) { - return $rpId; - } - $appId = $authenticationExtensionsClientInputs->get('appid') - ->value; - $wasUsed = $authenticationExtensionsClientOutputs->get('appid') - ->value; - if (! is_string($appId) || $wasUsed !== true) { - return $rpId; - } - return $appId; - } } diff --git a/src/webauthn/src/AuthenticatorAttestationResponseValidator.php b/src/webauthn/src/AuthenticatorAttestationResponseValidator.php index 0d14f50c..34310b5d 100644 --- a/src/webauthn/src/AuthenticatorAttestationResponseValidator.php +++ b/src/webauthn/src/AuthenticatorAttestationResponseValidator.php @@ -8,33 +8,22 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; -use Symfony\Component\Uid\Uuid; use Throwable; -use Webauthn\AttestationStatement\AttestationObject; -use Webauthn\AttestationStatement\AttestationStatement; use Webauthn\AttestationStatement\AttestationStatementSupportManager; -use Webauthn\AuthenticationExtensions\AuthenticationExtensions; use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler; +use Webauthn\CeremonyStep\CeremonyStepManager; +use Webauthn\CeremonyStep\CeremonyStepManagerFactory; use Webauthn\Event\AuthenticatorAttestationResponseValidationFailedEvent; use Webauthn\Event\AuthenticatorAttestationResponseValidationSucceededEvent; use Webauthn\Exception\AuthenticatorResponseVerificationException; use Webauthn\MetadataService\CanLogData; use Webauthn\MetadataService\CertificateChain\CertificateChainValidator; -use Webauthn\MetadataService\CertificateChain\CertificateToolbox; use Webauthn\MetadataService\Event\CanDispatchEvents; use Webauthn\MetadataService\Event\NullEventDispatcher; use Webauthn\MetadataService\MetadataStatementRepository; -use Webauthn\MetadataService\Statement\MetadataStatement; use Webauthn\MetadataService\StatusReportRepository; use Webauthn\TokenBinding\TokenBindingHandler; -use Webauthn\TrustPath\CertificateTrustPath; -use Webauthn\TrustPath\EmptyTrustPath; -use function array_key_exists; -use function count; -use function in_array; -use function is_array; use function is_string; -use function parse_url; class AuthenticatorAttestationResponseValidator implements CanLogData, CanDispatchEvents { @@ -42,52 +31,79 @@ class AuthenticatorAttestationResponseValidator implements CanLogData, CanDispat private EventDispatcherInterface $eventDispatcher; - private ?MetadataStatementRepository $metadataStatementRepository = null; - - private ?StatusReportRepository $statusReportRepository = null; - - private ?CertificateChainValidator $certificateChainValidator = null; + private readonly CeremonyStepManagerFactory $ceremonyStepManagerFactory; public function __construct( - private readonly AttestationStatementSupportManager $attestationStatementSupportManager, - private readonly null|PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository, - private readonly null|TokenBindingHandler $tokenBindingHandler, - private readonly ExtensionOutputCheckerHandler $extensionOutputCheckerHandler, + null|AttestationStatementSupportManager $attestationStatementSupportManager = null, + private readonly null|PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository = null, + private readonly null|TokenBindingHandler $tokenBindingHandler = null, + null|ExtensionOutputCheckerHandler $extensionOutputCheckerHandler = null, null|EventDispatcherInterface $eventDispatcher = null, + private null|CeremonyStepManager $ceremonyStepManager = null ) { + if ($this->publicKeyCredentialSourceRepository !== null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.6.0', + 'The parameter "$publicKeyCredentialSourceRepository" is deprecated since 4.6.0 and will be removed in 5.0.0. Please set "null" instead.' + ); + } if ($this->tokenBindingHandler !== null) { trigger_deprecation( - 'web-auth/webauthn-symfony-bundle', + 'web-auth/webauthn-lib', '4.3.0', 'The parameter "$tokenBindingHandler" is deprecated since 4.3.0 and will be removed in 5.0.0. Please set "null" instead.' ); } - if ($this->publicKeyCredentialSourceRepository !== null) { + if ($extensionOutputCheckerHandler !== null) { trigger_deprecation( - 'web-auth/webauthn-symfony-bundle', - '4.6.0', - 'The parameter "$publicKeyCredentialSourceRepository" is deprecated since 4.6.0 and will be removed in 5.0.0. Please set "null" instead.' + 'web-auth/webauthn-lib', + '4.8.0', + 'The parameter "$extensionOutputCheckerHandler" is deprecated since 4.8.0 and will be removed in 5.0.0. Please set "null" instead and inject a CheckExtensions object into the CeremonyStepManager.' ); } - if ($eventDispatcher === null) { - $this->eventDispatcher = new NullEventDispatcher(); - } else { - $this->eventDispatcher = $eventDispatcher; + $this->eventDispatcher = $eventDispatcher ?? new NullEventDispatcher(); + if ($eventDispatcher !== null) { trigger_deprecation( - 'web-auth/webauthn-symfony-bundle', + 'web-auth/webauthn-lib', '4.5.0', 'The parameter "$eventDispatcher" is deprecated since 4.5.0 will be removed in 5.0.0. Please use `setEventDispatcher` instead.' ); } + if ($this->ceremonyStepManager === null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.8.0', + 'The parameter "$ceremonyStepManager" will mandatory in 5.0.0. Please set a CeremonyStepManager object instead and set null for $attestationStatementSupportManager and $extensionOutputCheckerHandler.' + ); + } $this->logger = new NullLogger(); + $this->ceremonyStepManagerFactory = new CeremonyStepManagerFactory(); + if ($attestationStatementSupportManager !== null) { + $this->ceremonyStepManagerFactory->setAttestationStatementSupportManager( + $attestationStatementSupportManager + ); + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.8.0', + 'The parameter "$attestationStatementSupportManager" is deprecated since 4.8.0 will be removed in 5.0.0. Please set a CheckAttestationFormatIsKnownAndValid object into CeremonyStepManager object instead.' + ); + } + if ($extensionOutputCheckerHandler !== null) { + $this->ceremonyStepManagerFactory->setExtensionOutputCheckerHandler($extensionOutputCheckerHandler); + } } + /** + * @private Will become private in 5.0.0 + */ public static function create( - AttestationStatementSupportManager $attestationStatementSupportManager, - null|PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository, - null|TokenBindingHandler $tokenBindingHandler, - ExtensionOutputCheckerHandler $extensionOutputCheckerHandler, - null|EventDispatcherInterface $eventDispatcher = null + null|AttestationStatementSupportManager $attestationStatementSupportManager = null, + null|PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository = null, + null|TokenBindingHandler $tokenBindingHandler = null, + null|ExtensionOutputCheckerHandler $extensionOutputCheckerHandler = null, + null|EventDispatcherInterface $eventDispatcher = null, + null|CeremonyStepManager $ceremonyStepManager = null, ): self { return new self( $attestationStatementSupportManager, @@ -95,6 +111,7 @@ public static function create( $tokenBindingHandler, $extensionOutputCheckerHandler, $eventDispatcher, + $ceremonyStepManager ); } @@ -108,19 +125,28 @@ public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): v $this->eventDispatcher = $eventDispatcher; } - public function setCertificateChainValidator(): self + /** + * @deprecated since 4.8.0 and will be removed in 5.0.0. Please use the CheckMetadataStatement object from the CeremonyStepManager instead. + */ + public function setCertificateChainValidator(CertificateChainValidator $certificateChainValidator): self { + $this->ceremonyStepManagerFactory->enableCertificateChainValidator($certificateChainValidator); return $this; } + /** + * @deprecated since 4.8.0 and will be removed in 5.0.0. Please use the CheckMetadataStatement object from the CeremonyStepManager instead. + */ public function enableMetadataStatementSupport( MetadataStatementRepository $metadataStatementRepository, StatusReportRepository $statusReportRepository, CertificateChainValidator $certificateChainValidator ): self { - $this->metadataStatementRepository = $metadataStatementRepository; - $this->certificateChainValidator = $certificateChainValidator; - $this->statusReportRepository = $statusReportRepository; + $this->ceremonyStepManagerFactory->enableMetadataStatementSupport( + $metadataStatementRepository, + $statusReportRepository, + $certificateChainValidator + ); return $this; } @@ -133,7 +159,7 @@ public function check( AuthenticatorAttestationResponse $authenticatorAttestationResponse, PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, ServerRequestInterface|string $request, - array $securedRelyingPartyId = [] + null|array $securedRelyingPartyId = null, ): PublicKeyCredentialSource { if ($request instanceof ServerRequestInterface) { trigger_deprecation( @@ -146,119 +172,48 @@ public function check( ) ); } + if ($securedRelyingPartyId !== null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.8.0', + sprintf( + 'Passing a list or secured relying party IDs to the method `check` of the class "%s" is deprecated since 4.8.0 and will be removed in 5.0.0. Please inject the list instead.', + self::class + ) + ); + } + $host = is_string($request) ? $request : $request->getUri() + ->getHost(); try { $this->logger->info('Checking the authenticator attestation response', [ 'authenticatorAttestationResponse' => $authenticatorAttestationResponse, 'publicKeyCredentialCreationOptions' => $publicKeyCredentialCreationOptions, - 'host' => is_string($request) ? $request : $request->getUri() - ->getHost(), + 'host' => $host, ]); - //Nothing to do - $C = $authenticatorAttestationResponse->clientDataJSON; - $C->type === 'webauthn.create' || throw AuthenticatorResponseVerificationException::create( - 'The client data type is not "webauthn.create".' - ); - hash_equals( - $publicKeyCredentialCreationOptions->challenge, - $C->challenge - ) || throw AuthenticatorResponseVerificationException::create('Invalid challenge.'); - $rpId = $publicKeyCredentialCreationOptions->rp - ->id ?? (is_string($request) ? $request : $request->getUri()->getHost()); - $facetId = $this->getFacetId( - $rpId, - $publicKeyCredentialCreationOptions->extensions, - $authenticatorAttestationResponse->attestationObject - ->authData - ->extensions - ); - $parsedRelyingPartyId = parse_url($C->origin); - is_array($parsedRelyingPartyId) || throw AuthenticatorResponseVerificationException::create( - sprintf('The origin URI "%s" is not valid', $C->origin) - ); - array_key_exists( - 'scheme', - $parsedRelyingPartyId - ) || throw AuthenticatorResponseVerificationException::create('Invalid origin rpId.'); - $clientDataRpId = $parsedRelyingPartyId['host'] ?? ''; - $clientDataRpId !== '' || throw AuthenticatorResponseVerificationException::create('Invalid origin rpId.'); - $rpIdLength = mb_strlen($facetId); - mb_substr( - '.' . $clientDataRpId, - -($rpIdLength + 1) - ) === '.' . $facetId || throw AuthenticatorResponseVerificationException::create('rpId mismatch.'); - if (! in_array($facetId, $securedRelyingPartyId, true)) { - $scheme = $parsedRelyingPartyId['scheme']; - $scheme === 'https' || throw AuthenticatorResponseVerificationException::create( - 'Invalid scheme. HTTPS required.' - ); - } - if (! is_string($request) && $C->getTokenBinding() !== null) { - $this->tokenBindingHandler?->check($C->getTokenBinding(), $request); - } - $clientDataJSONHash = hash('sha256', $authenticatorAttestationResponse->clientDataJSON ->rawData, true); - $attestationObject = $authenticatorAttestationResponse->attestationObject; - $rpIdHash = hash('sha256', $facetId, true); - hash_equals( - $rpIdHash, - $attestationObject->authData - ->rpIdHash - ) || throw AuthenticatorResponseVerificationException::create('rpId hash mismatch.'); - if ($publicKeyCredentialCreationOptions->authenticatorSelection?->userVerification === AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED) { - $attestationObject->authData - ->isUserPresent() || throw AuthenticatorResponseVerificationException::create( - 'User was not present' - ); - $attestationObject->authData - ->isUserVerified() || throw AuthenticatorResponseVerificationException::create( - 'User authentication required.' - ); - } - $extensionsClientOutputs = $attestationObject->authData - ->extensions; - if ($extensionsClientOutputs !== null) { - $this->extensionOutputCheckerHandler->check( - $publicKeyCredentialCreationOptions->extensions, - $extensionsClientOutputs - ); - } - $this->checkMetadataStatement($publicKeyCredentialCreationOptions, $attestationObject); - $fmt = $attestationObject->attStmt - ->fmt; - $this->attestationStatementSupportManager->has( - $fmt - ) || throw AuthenticatorResponseVerificationException::create( - 'Unsupported attestation statement format.' - ); - $attestationStatementSupport = $this->attestationStatementSupportManager->get($fmt); - $attestationStatementSupport->isValid( - $clientDataJSONHash, - $attestationObject->attStmt, - $attestationObject->authData - ) || throw AuthenticatorResponseVerificationException::create('Invalid attestation statement.'); - $attestationObject->authData - ->hasAttestedCredentialData() || throw AuthenticatorResponseVerificationException::create( - 'There is no attested credential data.' - ); - $attestedCredentialData = $attestationObject->authData - ->attestedCredentialData; - $attestedCredentialData !== null || throw AuthenticatorResponseVerificationException::create( - 'There is no attested credential data.' - ); - $credentialId = $attestedCredentialData->credentialId; - if ($this->publicKeyCredentialSourceRepository !== null) { - $this->publicKeyCredentialSourceRepository->findOneByCredentialId( - $credentialId - ) === null || throw AuthenticatorResponseVerificationException::create( - 'The credential ID already exists.' + if ($this->ceremonyStepManager === null) { + $this->ceremonyStepManager = $this->ceremonyStepManagerFactory->creationCeremony( + $securedRelyingPartyId ); } + $publicKeyCredentialSource = $this->createPublicKeyCredentialSource( - $credentialId, - $attestedCredentialData, - $attestationObject, + $authenticatorAttestationResponse, + $publicKeyCredentialCreationOptions + ); + + $this->ceremonyStepManager->process( + $publicKeyCredentialSource, + $authenticatorAttestationResponse, + $publicKeyCredentialCreationOptions, $publicKeyCredentialCreationOptions->user->id, - $authenticatorAttestationResponse->transports + $host ); + + $publicKeyCredentialSource->counter = $authenticatorAttestationResponse->attestationObject->authData->signCount; + $publicKeyCredentialSource->backupEligible = $authenticatorAttestationResponse->attestationObject->authData->isBackupEligible(); + $publicKeyCredentialSource->backupStatus = $authenticatorAttestationResponse->attestationObject->authData->isBackedUp(); + $publicKeyCredentialSource->uvInitialized = $authenticatorAttestationResponse->attestationObject->authData->isUserVerified(); + $this->logger->info('The attestation is valid'); $this->logger->debug('Public Key Credential Source', [ 'publicKeyCredentialSource' => $publicKeyCredentialSource, @@ -267,7 +222,7 @@ public function check( $this->createAuthenticatorAttestationResponseValidationSucceededEvent( $authenticatorAttestationResponse, $publicKeyCredentialCreationOptions, - $request, + $host, $publicKeyCredentialSource ) ); @@ -280,7 +235,7 @@ public function check( $this->createAuthenticatorAttestationResponseValidationFailedEvent( $authenticatorAttestationResponse, $publicKeyCredentialCreationOptions, - $request, + $host, $throwable ) ); @@ -291,10 +246,10 @@ public function check( protected function createAuthenticatorAttestationResponseValidationSucceededEvent( AuthenticatorAttestationResponse $authenticatorAttestationResponse, PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, - ServerRequestInterface|string $request, + ServerRequestInterface|string $host, PublicKeyCredentialSource $publicKeyCredentialSource ): AuthenticatorAttestationResponseValidationSucceededEvent { - if ($request instanceof ServerRequestInterface) { + if ($host instanceof ServerRequestInterface) { trigger_deprecation( 'web-auth/webauthn-lib', '4.5.0', @@ -308,7 +263,7 @@ protected function createAuthenticatorAttestationResponseValidationSucceededEven return new AuthenticatorAttestationResponseValidationSucceededEvent( $authenticatorAttestationResponse, $publicKeyCredentialCreationOptions, - $request, + $host, $publicKeyCredentialSource ); } @@ -316,10 +271,10 @@ protected function createAuthenticatorAttestationResponseValidationSucceededEven protected function createAuthenticatorAttestationResponseValidationFailedEvent( AuthenticatorAttestationResponse $authenticatorAttestationResponse, PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, - ServerRequestInterface|string $request, + ServerRequestInterface|string $host, Throwable $throwable ): AuthenticatorAttestationResponseValidationFailedEvent { - if ($request instanceof ServerRequestInterface) { + if ($host instanceof ServerRequestInterface) { trigger_deprecation( 'web-auth/webauthn-lib', '4.5.0', @@ -333,152 +288,28 @@ protected function createAuthenticatorAttestationResponseValidationFailedEvent( return new AuthenticatorAttestationResponseValidationFailedEvent( $authenticatorAttestationResponse, $publicKeyCredentialCreationOptions, - $request, + $host, $throwable ); } - private function checkCertificateChain( - AttestationStatement $attestationStatement, - ?MetadataStatement $metadataStatement - ): void { - $trustPath = $attestationStatement->trustPath; - if (! $trustPath instanceof CertificateTrustPath) { - return; - } - $authenticatorCertificates = $trustPath->certificates; - if ($metadataStatement === null) { - $this->certificateChainValidator?->check($authenticatorCertificates, []); - return; - } - $trustedCertificates = CertificateToolbox::fixPEMStructures( - $metadataStatement->attestationRootCertificates - ); - $this->certificateChainValidator?->check($authenticatorCertificates, $trustedCertificates); - } - - private function checkMetadataStatement( + private function createPublicKeyCredentialSource( + AuthenticatorAttestationResponse $authenticatorAttestationResponse, PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, - AttestationObject $attestationObject - ): void { - $attestationStatement = $attestationObject->attStmt; - $attestedCredentialData = $attestationObject->authData - ->attestedCredentialData; + ): PublicKeyCredentialSource { + $attestationObject = $authenticatorAttestationResponse->attestationObject; + $attestedCredentialData = $attestationObject->authData->attestedCredentialData; $attestedCredentialData !== null || throw AuthenticatorResponseVerificationException::create( - 'No attested credential data found' - ); - $aaguid = $attestedCredentialData->aaguid - ->__toString(); - if ($publicKeyCredentialCreationOptions->attestation === null || $publicKeyCredentialCreationOptions->attestation === PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE) { - $this->logger->debug('No attestation is asked.'); - //No attestation is asked. We shall ensure that the data is anonymous. - if ($aaguid === '00000000-0000-0000-0000-000000000000' && in_array( - $attestationStatement->type, - [AttestationStatement::TYPE_NONE, AttestationStatement::TYPE_SELF], - true - )) { - $this->logger->debug('The Attestation Statement is anonymous.'); - $this->checkCertificateChain($attestationStatement, null); - return; - } - $this->logger->debug('Anonymization required. AAGUID and Attestation Statement changed.', [ - 'aaguid' => $aaguid, - 'AttestationStatement' => $attestationStatement, - ]); - $attestedCredentialData->aaguid = Uuid::fromString('00000000-0000-0000-0000-000000000000'); - $attestationObject->attStmt = AttestationStatement::createNone('none', [], EmptyTrustPath::create()); - return; - } - // If no Attestation Statement has been returned or if null AAGUID (=00000000-0000-0000-0000-000000000000) - // => nothing to check - if ($attestationStatement->type === AttestationStatement::TYPE_NONE) { - $this->logger->debug('No attestation returned.'); - //No attestation is returned. We shall ensure that the AAGUID is a null one. - if ($aaguid !== '00000000-0000-0000-0000-000000000000') { - $this->logger->debug('Anonymization required. AAGUID and Attestation Statement changed.', [ - 'aaguid' => $aaguid, - 'AttestationStatement' => $attestationStatement, - ]); - $attestedCredentialData->aaguid = Uuid::fromString('00000000-0000-0000-0000-000000000000'); - return; - } - return; - } - if ($aaguid === '00000000-0000-0000-0000-000000000000') { - //No need to continue if the AAGUID is null. - // This could be the case e.g. with AnonCA type - return; - } - //The MDS Repository is mandatory here - $this->metadataStatementRepository !== null || throw AuthenticatorResponseVerificationException::create( - 'The Metadata Statement Repository is mandatory when requesting attestation objects.' - ); - $metadataStatement = $this->metadataStatementRepository->findOneByAAGUID($aaguid); - // At this point, the Metadata Statement is mandatory - $metadataStatement !== null || throw AuthenticatorResponseVerificationException::create( - sprintf('The Metadata Statement for the AAGUID "%s" is missing', $aaguid) + 'Not attested credential data' ); - // We check the last status report - $this->checkStatusReport($aaguid); - // We check the certificate chain (if any) - $this->checkCertificateChain($attestationStatement, $metadataStatement); - // Check Attestation Type is allowed - if (count($metadataStatement->attestationTypes) !== 0) { - $type = $this->getAttestationType($attestationStatement); - in_array( - $type, - $metadataStatement->attestationTypes, - true - ) || throw AuthenticatorResponseVerificationException::create( - sprintf( - 'Invalid attestation statement. The attestation type "%s" is not allowed for this authenticator.', - $type - ) - ); - } - } - - private function getAttestationType(AttestationStatement $attestationStatement): string - { - return match ($attestationStatement->type) { - AttestationStatement::TYPE_BASIC => MetadataStatement::ATTESTATION_BASIC_FULL, - AttestationStatement::TYPE_SELF => MetadataStatement::ATTESTATION_BASIC_SURROGATE, - AttestationStatement::TYPE_ATTCA => MetadataStatement::ATTESTATION_ATTCA, - AttestationStatement::TYPE_ECDAA => MetadataStatement::ATTESTATION_ECDAA, - AttestationStatement::TYPE_ANONCA => MetadataStatement::ATTESTATION_ANONCA, - default => throw AuthenticatorResponseVerificationException::create('Invalid attestation type'), - }; - } - - private function checkStatusReport(string $aaguid): void - { - $statusReports = $this->statusReportRepository === null ? [] : $this->statusReportRepository->findStatusReportsByAAGUID( - $aaguid - ); - if (count($statusReports) !== 0) { - $lastStatusReport = end($statusReports); - if ($lastStatusReport->isCompromised()) { - throw AuthenticatorResponseVerificationException::create( - 'The authenticator is compromised and cannot be used' - ); - } - } - } - - /** - * @param string[] $transports - */ - private function createPublicKeyCredentialSource( - string $credentialId, - AttestedCredentialData $attestedCredentialData, - AttestationObject $attestationObject, - string $userHandle, - array $transports - ): PublicKeyCredentialSource { + $credentialId = $attestedCredentialData->credentialId; $credentialPublicKey = $attestedCredentialData->credentialPublicKey; $credentialPublicKey !== null || throw AuthenticatorResponseVerificationException::create( 'Not credential public key available in the attested credential data' ); + $userHandle = $publicKeyCredentialCreationOptions->user->id; + $transports = $authenticatorAttestationResponse->transports; + return PublicKeyCredentialSource::create( $credentialId, PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, @@ -491,27 +322,7 @@ private function createPublicKeyCredentialSource( $credentialPublicKey, $userHandle, $attestationObject->authData - ->signCount + ->signCount, ); } - - private function getFacetId( - string $rpId, - AuthenticationExtensions $authenticationExtensionsClientInputs, - null|AuthenticationExtensions $authenticationExtensionsClientOutputs - ): string { - if ($authenticationExtensionsClientOutputs === null || ! $authenticationExtensionsClientInputs->has( - 'appid' - ) || ! $authenticationExtensionsClientOutputs->has('appid')) { - return $rpId; - } - $appId = $authenticationExtensionsClientInputs->get('appid') - ->value; - $wasUsed = $authenticationExtensionsClientOutputs->get('appid') - ->value; - if (! is_string($appId) || $wasUsed !== true) { - return $rpId; - } - return $appId; - } } diff --git a/src/webauthn/src/CeremonyStep/CeremonyStep.php b/src/webauthn/src/CeremonyStep/CeremonyStep.php new file mode 100644 index 00000000..d015e00a --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CeremonyStep.php @@ -0,0 +1,22 @@ +steps as $step) { + $step->process( + $publicKeyCredentialSource, + $authenticatorResponse, + $publicKeyCredentialOptions, + $userHandle, + $host + ); + } + } +} diff --git a/src/webauthn/src/CeremonyStep/CeremonyStepManagerFactory.php b/src/webauthn/src/CeremonyStep/CeremonyStepManagerFactory.php new file mode 100644 index 00000000..5c412f61 --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CeremonyStepManagerFactory.php @@ -0,0 +1,159 @@ +counterChecker = new ThrowExceptionIfInvalid(); + $this->algorithmManager = Manager::create()->add(ES256::create(), RS256::create()); + $this->attestationStatementSupportManager = new AttestationStatementSupportManager([ + new NoneAttestationStatementSupport(), + ]); + $this->extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler(); + } + + public function setCounterChecker(CounterChecker $counterChecker): void + { + $this->counterChecker = $counterChecker; + } + + /** + * @param string[] $securedRelyingPartyId + */ + public function setSecuredRelyingPartyId(array $securedRelyingPartyId): void + { + $this->securedRelyingPartyId = $securedRelyingPartyId; + } + + public function setExtensionOutputCheckerHandler(ExtensionOutputCheckerHandler $extensionOutputCheckerHandler): void + { + $this->extensionOutputCheckerHandler = $extensionOutputCheckerHandler; + } + + public function setAttestationStatementSupportManager( + AttestationStatementSupportManager $attestationStatementSupportManager + ): void { + $this->attestationStatementSupportManager = $attestationStatementSupportManager; + } + + public function setAlgorithmManager(Manager $algorithmManager): void + { + $this->algorithmManager = $algorithmManager; + } + + public function enableMetadataStatementSupport( + MetadataStatementRepository $metadataStatementRepository, + StatusReportRepository $statusReportRepository, + CertificateChainValidator $certificateChainValidator + ): void { + $this->metadataStatementRepository = $metadataStatementRepository; + $this->statusReportRepository = $statusReportRepository; + $this->certificateChainValidator = $certificateChainValidator; + } + + public function enableCertificateChainValidator(CertificateChainValidator $certificateChainValidator): void + { + $this->certificateChainValidator = $certificateChainValidator; + } + + public function enableTopOriginValidator(TopOriginValidator $topOriginValidator): void + { + $this->topOriginValidator = $topOriginValidator; + } + + /** + * @param null|string[] $securedRelyingPartyId + */ + public function creationCeremony(null|array $securedRelyingPartyId = null): CeremonyStepManager + { + $metadataStatementChecker = new CheckMetadataStatement(); + if ($this->certificateChainValidator !== null) { + $metadataStatementChecker->enableCertificateChainValidator($this->certificateChainValidator); + } + if ($this->metadataStatementRepository !== null && $this->statusReportRepository !== null && $this->certificateChainValidator !== null) { + $metadataStatementChecker->enableMetadataStatementSupport( + $this->metadataStatementRepository, + $this->statusReportRepository, + $this->certificateChainValidator, + ); + } + + /* @see https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential */ + return new CeremonyStepManager([ + new CheckClientDataCollectorType(), + new CheckChallenge(), + new CheckOrigin($this->securedRelyingPartyId ?? $securedRelyingPartyId ?? []), + new CheckTopOrigin($this->topOriginValidator), + new CheckRelyingPartyIdIdHash(), + new CheckUserWasPresent(), + new CheckUserVerification(), + new CheckBackupBitsAreConsistent(), + new CheckAlgorithm(), + new CheckExtensions($this->extensionOutputCheckerHandler), + new CheckAttestationFormatIsKnownAndValid($this->attestationStatementSupportManager), + new CheckHasAttestedCredentialData(), + $metadataStatementChecker, + new CheckCredentialId(), + ]); + } + + /** + * @param null|string[] $securedRelyingPartyId + */ + public function requestCeremony(null|array $securedRelyingPartyId = null): CeremonyStepManager + { + /* @see https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion */ + return new CeremonyStepManager([ + new CheckAllowedCredentialList(), + new CheckUserHandle(), + new CheckClientDataCollectorType(), + new CheckChallenge(), + new CheckOrigin($this->securedRelyingPartyId ?? $securedRelyingPartyId ?? []), + new CheckTopOrigin(null), + new CheckRelyingPartyIdIdHash(), + new CheckUserWasPresent(), + new CheckUserVerification(), + new CheckBackupBitsAreConsistent(), + new CheckExtensions($this->extensionOutputCheckerHandler), + new CheckSignature($this->algorithmManager), + new CheckCounter($this->counterChecker), + ]); + } +} diff --git a/src/webauthn/src/CeremonyStep/CheckAlgorithm.php b/src/webauthn/src/CeremonyStep/CheckAlgorithm.php new file mode 100644 index 00000000..9b5e39b2 --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CheckAlgorithm.php @@ -0,0 +1,76 @@ +getAttestedCredentialData() +->credentialPublicKey; + $credentialPublicKey !== null || throw AuthenticatorResponseVerificationException::create( + 'No public key available.' + ); + $algorithms = array_map( + fn ($pubKeyCredParam) => $pubKeyCredParam->alg, + $publicKeyCredentialOptions->pubKeyCredParams + ); + if (count($algorithms) === 0) { + $algorithms = [Algorithms::COSE_ALGORITHM_ES256, Algorithms::COSE_ALGORITHM_RS256]; + } + $coseKey = $this->getCoseKey($credentialPublicKey); + in_array($coseKey->alg(), $algorithms, true) || throw AuthenticatorResponseVerificationException::create( + sprintf('Invalid algorithm. Expected one of %s but got %d', implode(', ', $algorithms), $coseKey->alg()) + ); + } + + private function getCoseKey(string $credentialPublicKey): Key + { + $isU2F = U2FPublicKey::isU2FKey($credentialPublicKey); + if ($isU2F === true) { + $credentialPublicKey = U2FPublicKey::convertToCoseKey($credentialPublicKey); + } + $stream = new StringStream($credentialPublicKey); + $credentialPublicKeyStream = Decoder::create()->decode($stream); + $stream->isEOF() || throw AuthenticatorResponseVerificationException::create( + 'Invalid key. Presence of extra bytes.' + ); + $stream->close(); + $credentialPublicKeyStream instanceof Normalizable || throw AuthenticatorResponseVerificationException::create( + 'Invalid attestation object. Unexpected object.' + ); + $normalizedData = $credentialPublicKeyStream->normalize(); + is_array($normalizedData) || throw AuthenticatorResponseVerificationException::create( + 'Invalid attestation object. Unexpected object.' + ); + /** @var array $normalizedData */ + + return Key::create($normalizedData); + } +} diff --git a/src/webauthn/src/CeremonyStep/CheckAllowedCredentialList.php b/src/webauthn/src/CeremonyStep/CheckAllowedCredentialList.php new file mode 100644 index 00000000..bd51f61e --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CheckAllowedCredentialList.php @@ -0,0 +1,38 @@ +allowCredentials) === 0) { + return; + } + + foreach ($publicKeyCredentialOptions->allowCredentials as $allowedCredential) { + if (hash_equals($allowedCredential->id, $publicKeyCredentialSource->publicKeyCredentialId)) { + return; + } + } + throw AuthenticatorResponseVerificationException::create('The credential ID is not allowed.'); + } +} diff --git a/src/webauthn/src/CeremonyStep/CheckAttestationFormatIsKnownAndValid.php b/src/webauthn/src/CeremonyStep/CheckAttestationFormatIsKnownAndValid.php new file mode 100644 index 00000000..147ffcee --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CheckAttestationFormatIsKnownAndValid.php @@ -0,0 +1,48 @@ +attestationObject; + if ($attestationObject === null) { + return; + } + + $fmt = $attestationObject->attStmt + ->fmt; + $this->attestationStatementSupportManager->has( + $fmt + ) || throw AuthenticatorResponseVerificationException::create('Unsupported attestation statement format.'); + + $attestationStatementSupport = $this->attestationStatementSupportManager->get($fmt); + $clientDataJSONHash = hash('sha256', $authenticatorResponse->clientDataJSON ->rawData, true); + $attestationStatementSupport->isValid( + $clientDataJSONHash, + $attestationObject->attStmt, + $attestationObject->authData + ) || throw AuthenticatorResponseVerificationException::create('Invalid attestation statement.'); + } +} diff --git a/src/webauthn/src/CeremonyStep/CheckBackupBitsAreConsistent.php b/src/webauthn/src/CeremonyStep/CheckBackupBitsAreConsistent.php new file mode 100644 index 00000000..dffa239c --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CheckBackupBitsAreConsistent.php @@ -0,0 +1,31 @@ +authenticatorData : $authenticatorResponse->attestationObject->authData; + if ($authData->isBackupEligible()) { + return; + } + $authData->isBackedUp() !== true || throw AuthenticatorResponseVerificationException::create( + 'Backup up bit is set but the backup is not eligible.' + ); + } +} diff --git a/src/webauthn/src/CeremonyStep/CheckChallenge.php b/src/webauthn/src/CeremonyStep/CheckChallenge.php new file mode 100644 index 00000000..a1c05970 --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CheckChallenge.php @@ -0,0 +1,31 @@ +challenge !== '' || throw AuthenticatorResponseVerificationException::create( + 'Invalid challenge.' + ); + hash_equals( + $publicKeyCredentialOptions->challenge, + $authenticatorResponse->clientDataJSON->challenge + ) || throw AuthenticatorResponseVerificationException::create('Invalid challenge.'); + } +} diff --git a/src/webauthn/src/CeremonyStep/CheckClientDataCollectorType.php b/src/webauthn/src/CeremonyStep/CheckClientDataCollectorType.php new file mode 100644 index 00000000..645cb904 --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CheckClientDataCollectorType.php @@ -0,0 +1,41 @@ +clientDataCollectorManager = $clientDataCollectorManager ?? new ClientDataCollectorManager([ + new WebauthnAuthenticationCollector(), + ]); + } + + public function process( + PublicKeyCredentialSource $publicKeyCredentialSource, + AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse, + PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions, + ?string $userHandle, + string $host + ): void { + $this->clientDataCollectorManager->collect( + $authenticatorResponse->clientDataJSON, + $publicKeyCredentialOptions, + $authenticatorResponse, + $host + ); + } +} diff --git a/src/webauthn/src/CeremonyStep/CheckCounter.php b/src/webauthn/src/CeremonyStep/CheckCounter.php new file mode 100644 index 00000000..dcf84536 --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CheckCounter.php @@ -0,0 +1,36 @@ +authenticatorData : $authenticatorResponse->attestationObject->authData; + $storedCounter = $publicKeyCredentialSource->counter; + $responseCounter = $authData->signCount; + if ($responseCounter !== 0 || $storedCounter !== 0) { + $this->counterChecker->check($publicKeyCredentialSource, $responseCounter); + } + $publicKeyCredentialSource->counter = $responseCounter; + } +} diff --git a/src/webauthn/src/CeremonyStep/CheckCredentialId.php b/src/webauthn/src/CeremonyStep/CheckCredentialId.php new file mode 100644 index 00000000..aeb20205 --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CheckCredentialId.php @@ -0,0 +1,28 @@ +publicKeyCredentialId; + mb_strlen($credentialId) <= 1023 || throw new AuthenticatorResponseVerificationException( + 'Credential ID too long.' + ); + } +} diff --git a/src/webauthn/src/CeremonyStep/CheckExtensions.php b/src/webauthn/src/CeremonyStep/CheckExtensions.php new file mode 100644 index 00000000..2ece81c7 --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CheckExtensions.php @@ -0,0 +1,37 @@ +authenticatorData : $authenticatorResponse->attestationObject->authData; + $extensionsClientOutputs = $authData->extensions; + if ($extensionsClientOutputs !== null) { + $this->extensionOutputCheckerHandler->check( + $publicKeyCredentialOptions->extensions, + $extensionsClientOutputs + ); + } + } +} diff --git a/src/webauthn/src/CeremonyStep/CheckHasAttestedCredentialData.php b/src/webauthn/src/CeremonyStep/CheckHasAttestedCredentialData.php new file mode 100644 index 00000000..cf1cd23d --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CheckHasAttestedCredentialData.php @@ -0,0 +1,32 @@ +authenticatorData : $authenticatorResponse->attestationObject->authData; + $authData + ->hasAttestedCredentialData() || throw AuthenticatorResponseVerificationException::create( + 'There is no attested credential data.' + ); + $authData->attestedCredentialData !== null || throw AuthenticatorResponseVerificationException::create( + 'There is no attested credential data.' + ); + } +} diff --git a/src/webauthn/src/CeremonyStep/CheckMetadataStatement.php b/src/webauthn/src/CeremonyStep/CheckMetadataStatement.php new file mode 100644 index 00000000..ce482a4a --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CheckMetadataStatement.php @@ -0,0 +1,190 @@ +logger = new NullLogger(); + } + + public function enableMetadataStatementSupport( + MetadataStatementRepository $metadataStatementRepository, + StatusReportRepository $statusReportRepository, + CertificateChainValidator $certificateChainValidator + ): void { + $this->metadataStatementRepository = $metadataStatementRepository; + $this->statusReportRepository = $statusReportRepository; + $this->certificateChainValidator = $certificateChainValidator; + } + + public function enableCertificateChainValidator(CertificateChainValidator $certificateChainValidator): void + { + $this->certificateChainValidator = $certificateChainValidator; + } + + public function setLogger(LoggerInterface $logger): void + { + $this->logger = $logger; + } + + public function process( + PublicKeyCredentialSource $publicKeyCredentialSource, + AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse, + PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions, + ?string $userHandle, + string $host + ): void { + if ( + ! $publicKeyCredentialOptions instanceof PublicKeyCredentialCreationOptions + || ! $authenticatorResponse instanceof AuthenticatorAttestationResponse + ) { + return; + } + + $attestationStatement = $authenticatorResponse->attestationObject->attStmt; + $attestedCredentialData = $authenticatorResponse->attestationObject->authData + ->attestedCredentialData; + $attestedCredentialData !== null || throw AuthenticatorResponseVerificationException::create( + 'No attested credential data found' + ); + $aaguid = $attestedCredentialData->aaguid + ->__toString(); + if ($publicKeyCredentialOptions->attestation === null || $publicKeyCredentialOptions->attestation === PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE) { + $this->logger->debug('No attestation is asked.'); + if ($aaguid === '00000000-0000-0000-0000-000000000000' && in_array( + $attestationStatement->type, + [AttestationStatement::TYPE_NONE, AttestationStatement::TYPE_SELF], + true + )) { + $this->logger->debug('The Attestation Statement is anonymous.'); + $this->checkCertificateChain($attestationStatement, null); + return; + } + return; + } + // If no Attestation Statement has been returned or if null AAGUID (=00000000-0000-0000-0000-000000000000) + // => nothing to check + if ($attestationStatement->type === AttestationStatement::TYPE_NONE) { + $this->logger->debug('No attestation returned.'); + //No attestation is returned. We shall ensure that the AAGUID is a null one. + //if ($aaguid !== '00000000-0000-0000-0000-000000000000') { + //$this->logger->debug('Anonymization required. AAGUID and Attestation Statement changed.', [ + // 'aaguid' => $aaguid, + // 'AttestationStatement' => $attestationStatement, + //]); + //$attestedCredentialData->aaguid = Uuid::fromString('00000000-0000-0000-0000-000000000000'); + // return; + //} + return; + } + if ($aaguid === '00000000-0000-0000-0000-000000000000') { + //No need to continue if the AAGUID is null. + // This could be the case e.g. with AnonCA type + return; + } + //The MDS Repository is mandatory here + $this->metadataStatementRepository !== null || throw AuthenticatorResponseVerificationException::create( + 'The Metadata Statement Repository is mandatory when requesting attestation objects.' + ); + $metadataStatement = $this->metadataStatementRepository->findOneByAAGUID($aaguid); + // At this point, the Metadata Statement is mandatory + $metadataStatement !== null || throw AuthenticatorResponseVerificationException::create( + sprintf('The Metadata Statement for the AAGUID "%s" is missing', $aaguid) + ); + // We check the last status report + $this->checkStatusReport($aaguid); + // We check the certificate chain (if any) + $this->checkCertificateChain($attestationStatement, $metadataStatement); + // Check Attestation Type is allowed + if (count($metadataStatement->attestationTypes) !== 0) { + $type = $this->getAttestationType($attestationStatement); + in_array( + $type, + $metadataStatement->attestationTypes, + true + ) || throw AuthenticatorResponseVerificationException::create( + sprintf( + 'Invalid attestation statement. The attestation type "%s" is not allowed for this authenticator.', + $type + ) + ); + } + } + + private function getAttestationType(AttestationStatement $attestationStatement): string + { + return match ($attestationStatement->type) { + AttestationStatement::TYPE_BASIC => MetadataStatement::ATTESTATION_BASIC_FULL, + AttestationStatement::TYPE_SELF => MetadataStatement::ATTESTATION_BASIC_SURROGATE, + AttestationStatement::TYPE_ATTCA => MetadataStatement::ATTESTATION_ATTCA, + AttestationStatement::TYPE_ECDAA => MetadataStatement::ATTESTATION_ECDAA, + AttestationStatement::TYPE_ANONCA => MetadataStatement::ATTESTATION_ANONCA, + default => throw AuthenticatorResponseVerificationException::create('Invalid attestation type'), + }; + } + + private function checkStatusReport(string $aaguid): void + { + $statusReports = $this->statusReportRepository === null ? [] : $this->statusReportRepository->findStatusReportsByAAGUID( + $aaguid + ); + if (count($statusReports) !== 0) { + $lastStatusReport = end($statusReports); + if ($lastStatusReport->isCompromised()) { + throw AuthenticatorResponseVerificationException::create( + 'The authenticator is compromised and cannot be used' + ); + } + } + } + + private function checkCertificateChain( + AttestationStatement $attestationStatement, + ?MetadataStatement $metadataStatement + ): void { + $trustPath = $attestationStatement->trustPath; + if (! $trustPath instanceof CertificateTrustPath) { + return; + } + $authenticatorCertificates = $trustPath->certificates; + if ($metadataStatement === null) { + $this->certificateChainValidator?->check($authenticatorCertificates, []); + return; + } + $trustedCertificates = CertificateToolbox::fixPEMStructures( + $metadataStatement->attestationRootCertificates + ); + $this->certificateChainValidator?->check($authenticatorCertificates, $trustedCertificates); + } +} diff --git a/src/webauthn/src/CeremonyStep/CheckOrigin.php b/src/webauthn/src/CeremonyStep/CheckOrigin.php new file mode 100644 index 00000000..cbc5cd41 --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CheckOrigin.php @@ -0,0 +1,111 @@ +authenticatorData : $authenticatorResponse->attestationObject->authData; + $C = $authenticatorResponse->clientDataJSON; + $rpId = $publicKeyCredentialOptions->rpId ?? $host; + $facetId = $this->getFacetId($rpId, $publicKeyCredentialOptions->extensions, $authData->extensions); + $parsedRelyingPartyId = parse_url($C->origin); + is_array($parsedRelyingPartyId) || throw AuthenticatorResponseVerificationException::create( + 'Invalid origin' + ); + if (! in_array($facetId, $this->securedRelyingPartyId, true)) { + $scheme = $parsedRelyingPartyId['scheme'] ?? ''; + $scheme === 'https' || throw AuthenticatorResponseVerificationException::create( + 'Invalid scheme. HTTPS required.' + ); + } + $clientDataRpId = $parsedRelyingPartyId['host'] ?? ''; + $clientDataRpId !== '' || throw AuthenticatorResponseVerificationException::create('Invalid origin rpId.'); + $rpIdLength = mb_strlen($facetId); + + mb_substr( + '.' . $clientDataRpId, + -($rpIdLength + 1) + ) === '.' . $facetId || throw AuthenticatorResponseVerificationException::create('rpId mismatch.'); + } + + private function getFacetId( + string $rpId, + AuthenticationExtensions $authenticationExtensionsClientInputs, + null|AuthenticationExtensions $authenticationExtensionsClientOutputs + ): string { + if ($authenticationExtensionsClientOutputs === null || ! $authenticationExtensionsClientInputs->has( + 'appid' + ) || ! $authenticationExtensionsClientOutputs->has('appid')) { + return $rpId; + } + $appId = $authenticationExtensionsClientInputs->get('appid') + ->value; + $wasUsed = $authenticationExtensionsClientOutputs->get('appid') + ->value; + if (! is_string($appId) || $wasUsed !== true) { + return $rpId; + } + return $appId; + } +} + +/* +$rpId = $publicKeyCredentialCreationOptions->rp + ->id ?? (is_string($request) ? $request : $request->getUri()->getHost()); +$facetId = $this->getFacetId( + $rpId, + $publicKeyCredentialCreationOptions->extensions, + $authenticatorAttestationResponse->attestationObject + ->authData + ->extensions +); +$parsedRelyingPartyId = parse_url($C->origin); +is_array($parsedRelyingPartyId) || throw AuthenticatorResponseVerificationException::create( + sprintf('The origin URI "%s" is not valid', $C->origin) +); +array_key_exists( + 'scheme', + $parsedRelyingPartyId +) || throw AuthenticatorResponseVerificationException::create('Invalid origin rpId.'); +$clientDataRpId = $parsedRelyingPartyId['host'] ?? ''; +$clientDataRpId !== '' || throw AuthenticatorResponseVerificationException::create('Invalid origin rpId.'); +$rpIdLength = mb_strlen($facetId); +mb_substr( + '.' . $clientDataRpId, + -($rpIdLength + 1) +) === '.' . $facetId || throw AuthenticatorResponseVerificationException::create('rpId mismatch.'); +if (! in_array($facetId, $securedRelyingPartyId, true)) { + $scheme = $parsedRelyingPartyId['scheme']; + $scheme === 'https' || throw AuthenticatorResponseVerificationException::create( + 'Invalid scheme. HTTPS required.' + ); +} + */ diff --git a/src/webauthn/src/CeremonyStep/CheckRelyingPartyIdIdHash.php b/src/webauthn/src/CeremonyStep/CheckRelyingPartyIdIdHash.php new file mode 100644 index 00000000..185c639a --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CheckRelyingPartyIdIdHash.php @@ -0,0 +1,63 @@ +authenticatorData : $authenticatorResponse->attestationObject->authData; + $C = $authenticatorResponse->clientDataJSON; + $attestedCredentialData = $publicKeyCredentialSource->getAttestedCredentialData(); + $credentialPublicKey = $attestedCredentialData->credentialPublicKey; + $credentialPublicKey !== null || throw AuthenticatorResponseVerificationException::create( + 'No public key available.' + ); + $isU2F = U2FPublicKey::isU2FKey($credentialPublicKey); + $rpId = $publicKeyCredentialOptions->rpId ?? $host; + $facetId = $this->getFacetId($rpId, $publicKeyCredentialOptions->extensions, $authData ->extensions); + $rpIdHash = hash('sha256', $isU2F ? $C->origin : $facetId, true); + hash_equals( + $rpIdHash, + $authData + ->rpIdHash + ) || throw AuthenticatorResponseVerificationException::create('rpId hash mismatch.'); + } + + private function getFacetId( + string $rpId, + AuthenticationExtensions $authenticationExtensionsClientInputs, + null|AuthenticationExtensions $authenticationExtensionsClientOutputs + ): string { + if ($authenticationExtensionsClientOutputs === null || ! $authenticationExtensionsClientInputs->has( + 'appid' + ) || ! $authenticationExtensionsClientOutputs->has('appid')) { + return $rpId; + } + $appId = $authenticationExtensionsClientInputs->get('appid') + ->value; + $wasUsed = $authenticationExtensionsClientOutputs->get('appid') + ->value; + if (! is_string($appId) || $wasUsed !== true) { + return $rpId; + } + return $appId; + } +} diff --git a/src/webauthn/src/CeremonyStep/CheckSignature.php b/src/webauthn/src/CeremonyStep/CheckSignature.php new file mode 100644 index 00000000..a89fef32 --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CheckSignature.php @@ -0,0 +1,90 @@ +algorithmManager = $algorithmManager ?? Manager::create()->add(ES256::create(), RS256::create()); + } + + public function process( + PublicKeyCredentialSource $publicKeyCredentialSource, + AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse, + PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions, + ?string $userHandle, + string $host + ): void { + if (! $authenticatorResponse instanceof AuthenticatorAssertionResponse) { + return; + } + $credentialPublicKey = $publicKeyCredentialSource->getAttestedCredentialData() +->credentialPublicKey; + $credentialPublicKey !== null || throw AuthenticatorResponseVerificationException::create( + 'No public key available.' + ); + $coseKey = $this->getCoseKey($credentialPublicKey); + + $getClientDataJSONHash = hash('sha256', $authenticatorResponse->clientDataJSON->rawData, true); + $dataToVerify = $authenticatorResponse->authenticatorData->authData . $getClientDataJSONHash; + $signature = $authenticatorResponse->signature; + $algorithm = $this->algorithmManager->get($coseKey->alg()); + $algorithm instanceof Signature || throw AuthenticatorResponseVerificationException::create( + 'Invalid algorithm identifier. Should refer to a signature algorithm' + ); + $signature = CoseSignatureFixer::fix($signature, $algorithm); + $algorithm->verify( + $dataToVerify, + $coseKey, + $signature + ) || throw AuthenticatorResponseVerificationException::create('Invalid signature.'); + } + + private function getCoseKey(string $credentialPublicKey): Key + { + $isU2F = U2FPublicKey::isU2FKey($credentialPublicKey); + if ($isU2F === true) { + $credentialPublicKey = U2FPublicKey::convertToCoseKey($credentialPublicKey); + } + $stream = new StringStream($credentialPublicKey); + $credentialPublicKeyStream = Decoder::create()->decode($stream); + $stream->isEOF() || throw AuthenticatorResponseVerificationException::create( + 'Invalid key. Presence of extra bytes.' + ); + $stream->close(); + $credentialPublicKeyStream instanceof Normalizable || throw AuthenticatorResponseVerificationException::create( + 'Invalid attestation object. Unexpected object.' + ); + $normalizedData = $credentialPublicKeyStream->normalize(); + is_array($normalizedData) || throw AuthenticatorResponseVerificationException::create( + 'Invalid attestation object. Unexpected object.' + ); + /** @var array $normalizedData */ + + return Key::create($normalizedData); + } +} diff --git a/src/webauthn/src/CeremonyStep/CheckTopOrigin.php b/src/webauthn/src/CeremonyStep/CheckTopOrigin.php new file mode 100644 index 00000000..bb9a5554 --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CheckTopOrigin.php @@ -0,0 +1,41 @@ +clientDataJSON->topOrigin; + if ($topOrigin === null) { + return; + } + if ($authenticatorResponse->clientDataJSON->crossOrigin !== true) { + throw AuthenticatorResponseVerificationException::create('The response is not cross-origin.'); + } + if ($this->topOriginValidator === null) { + (new HostTopOriginValidator($host))->validate($topOrigin); + } else { + $this->topOriginValidator->validate($topOrigin); + } + } +} diff --git a/src/webauthn/src/CeremonyStep/CheckUserHandle.php b/src/webauthn/src/CeremonyStep/CheckUserHandle.php new file mode 100644 index 00000000..27585754 --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CheckUserHandle.php @@ -0,0 +1,37 @@ +userHandle; + $responseUserHandle = $authenticatorResponse->userHandle; + if ($userHandle !== null) { //If the user was identified before the authentication ceremony was initiated, + $credentialUserHandle === $userHandle || throw InvalidUserHandleException::create(); + if ($responseUserHandle !== null && $responseUserHandle !== '') { + $credentialUserHandle === $responseUserHandle || throw InvalidUserHandleException::create(); + } + } else { + ($responseUserHandle !== '' && $credentialUserHandle === $responseUserHandle) || throw InvalidUserHandleException::create(); + } + } +} diff --git a/src/webauthn/src/CeremonyStep/CheckUserVerification.php b/src/webauthn/src/CeremonyStep/CheckUserVerification.php new file mode 100644 index 00000000..392eca8b --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CheckUserVerification.php @@ -0,0 +1,33 @@ +userVerification : $publicKeyCredentialOptions->authenticatorSelection?->userVerification; + if ($userVerification !== AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED) { + return; + } + $authData = $authenticatorResponse instanceof AuthenticatorAssertionResponse ? $authenticatorResponse->authenticatorData : $authenticatorResponse->attestationObject->authData; + $authData->isUserVerified() || throw AuthenticatorResponseVerificationException::create( + 'User authentication required.' + ); + } +} diff --git a/src/webauthn/src/CeremonyStep/CheckUserWasPresent.php b/src/webauthn/src/CeremonyStep/CheckUserWasPresent.php new file mode 100644 index 00000000..32fd7fff --- /dev/null +++ b/src/webauthn/src/CeremonyStep/CheckUserWasPresent.php @@ -0,0 +1,26 @@ +authenticatorData : $authenticatorResponse->attestationObject->authData; + $authData->isUserPresent() || throw AuthenticatorResponseVerificationException::create('User was not present'); + } +} diff --git a/src/webauthn/src/CeremonyStep/HostTopOriginValidator.php b/src/webauthn/src/CeremonyStep/HostTopOriginValidator.php new file mode 100644 index 00000000..dfccbccc --- /dev/null +++ b/src/webauthn/src/CeremonyStep/HostTopOriginValidator.php @@ -0,0 +1,22 @@ +host || throw AuthenticatorResponseVerificationException::create( + 'The top origin does not correspond to the host.' + ); + } +} diff --git a/src/webauthn/src/CeremonyStep/TopOriginValidator.php b/src/webauthn/src/CeremonyStep/TopOriginValidator.php new file mode 100644 index 00000000..d189a611 --- /dev/null +++ b/src/webauthn/src/CeremonyStep/TopOriginValidator.php @@ -0,0 +1,10 @@ +clientDataCollectors as $clientDataCollector) { + if (in_array($collectedClientData->type, $clientDataCollector->supportedTypes(), true)) { + $clientDataCollector->verifyCollectedClientData( + $collectedClientData, + $publicKeyCredentialOptions, + $authenticatorResponse, + $host + ); + return; + } + } + + throw AuthenticatorResponseVerificationException::create('No client data collector found.'); + } +} diff --git a/src/webauthn/src/ClientDataCollector/WebauthnAuthenticationCollector.php b/src/webauthn/src/ClientDataCollector/WebauthnAuthenticationCollector.php new file mode 100644 index 00000000..1a316e25 --- /dev/null +++ b/src/webauthn/src/ClientDataCollector/WebauthnAuthenticationCollector.php @@ -0,0 +1,34 @@ +type, + $this->supportedTypes(), + true + ) || throw AuthenticatorResponseVerificationException::create( + sprintf('The client data type is not "%s" supported.', implode('", "', $this->supportedTypes())) + ); + } +} diff --git a/src/webauthn/src/CollectedClientData.php b/src/webauthn/src/CollectedClientData.php index f82b7cdd..3731207b 100644 --- a/src/webauthn/src/CollectedClientData.php +++ b/src/webauthn/src/CollectedClientData.php @@ -25,6 +25,8 @@ class CollectedClientData public readonly string $origin; + public readonly null|string $topOrigin; + public readonly bool $crossOrigin; /** @@ -67,6 +69,7 @@ public function __construct( ); $this->origin = $origin; + $this->topOrigin = $data['topOrigin'] ?? null; $this->crossOrigin = $data['crossOrigin'] ?? false; $tokenBinding = $data['tokenBinding'] ?? null; diff --git a/src/webauthn/src/Counter/ThrowExceptionIfInvalid.php b/src/webauthn/src/Counter/ThrowExceptionIfInvalid.php index 2a28d7ec..bf411d2a 100644 --- a/src/webauthn/src/Counter/ThrowExceptionIfInvalid.php +++ b/src/webauthn/src/Counter/ThrowExceptionIfInvalid.php @@ -6,7 +6,6 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; -use Throwable; use Webauthn\Exception\CounterException; use Webauthn\MetadataService\CanLogData; use Webauthn\PublicKeyCredentialSource; @@ -31,7 +30,7 @@ public function check(PublicKeyCredentialSource $publicKeyCredentialSource, int $publicKeyCredentialSource->counter, 'Invalid counter.' ); - } catch (Throwable $throwable) { + } catch (CounterException $throwable) { $this->logger->error('The counter is invalid', [ 'current' => $currentCounter, 'new' => $publicKeyCredentialSource->counter, diff --git a/src/webauthn/src/Denormalizer/AuthenticatorResponseDenormalizer.php b/src/webauthn/src/Denormalizer/AuthenticatorResponseDenormalizer.php index 35d2e43f..77ca9b26 100644 --- a/src/webauthn/src/Denormalizer/AuthenticatorResponseDenormalizer.php +++ b/src/webauthn/src/Denormalizer/AuthenticatorResponseDenormalizer.php @@ -27,7 +27,7 @@ public function denormalize(mixed $data, string $type, string $format = null, ar } switch (true) { - case array_key_exists('attestationObject', $data): + case ! array_key_exists('authenticatorData', $data) && ! array_key_exists('signature', $data): $context[self::ALREADY_CALLED] = true; return $this->denormalizer->denormalize( $data, diff --git a/src/webauthn/src/Exception/AuthenticatorResponseVerificationException.php b/src/webauthn/src/Exception/AuthenticatorResponseVerificationException.php index b7a37ad4..08916a79 100644 --- a/src/webauthn/src/Exception/AuthenticatorResponseVerificationException.php +++ b/src/webauthn/src/Exception/AuthenticatorResponseVerificationException.php @@ -6,7 +6,7 @@ use Throwable; -final class AuthenticatorResponseVerificationException extends WebauthnException +class AuthenticatorResponseVerificationException extends WebauthnException { public static function create(string $message, ?Throwable $previous = null): self { diff --git a/src/webauthn/src/Exception/InvalidUserHandleException.php b/src/webauthn/src/Exception/InvalidUserHandleException.php new file mode 100644 index 00000000..19337188 --- /dev/null +++ b/src/webauthn/src/Exception/InvalidUserHandleException.php @@ -0,0 +1,15 @@ + $data, ]); try { - if ($this->serializer === null) { - $json = json_decode($data, true, flags: JSON_THROW_ON_ERROR); - - return $this->loadArray($json); + if ($this->serializer !== null) { + return $this->serializer->deserialize($data, PublicKeyCredential::class, 'json'); } + $json = json_decode($data, true, flags: JSON_THROW_ON_ERROR); - return $this->serializer->deserialize($data, PublicKeyCredential::class, 'json'); + return $this->loadArray($json); } catch (Throwable $throwable) { $this->logger->error('An error occurred', [ 'exception' => $throwable, @@ -155,20 +154,11 @@ private function createResponse(array $response): AuthenticatorResponse $response, 'Invalid data. The parameter "transports" is invalid' ); + if ($this->serializer !== null) { + return $this->serializer->deserialize($response, AuthenticatorResponse::class, 'json'); + } switch (true) { - case array_key_exists('attestationObject', $response): - is_string($response['attestationObject']) || throw InvalidDataException::create( - $response, - 'Invalid data. The parameter "attestationObject" is invalid' - ); - if ($this->serializer !== null) { - return $this->serializer->deserialize( - $response, - AuthenticatorAttestationResponse::class, - 'json' - ); - } - + case ! array_key_exists('authenticatorData', $response) && ! array_key_exists('signature', $response): $attestationObject = $this->attestationObjectLoader->load($response['attestationObject']); return AuthenticatorAttestationResponse::create(CollectedClientData::createFormJson( diff --git a/src/webauthn/src/PublicKeyCredentialSource.php b/src/webauthn/src/PublicKeyCredentialSource.php index 7253a172..815a01c4 100644 --- a/src/webauthn/src/PublicKeyCredentialSource.php +++ b/src/webauthn/src/PublicKeyCredentialSource.php @@ -6,7 +6,6 @@ use JsonSerializable; use ParagonIE\ConstantTime\Base64UrlSafe; -use Symfony\Component\Uid\AbstractUid; use Symfony\Component\Uid\Uuid; use Throwable; use Webauthn\Exception\InvalidDataException; @@ -30,11 +29,14 @@ public function __construct( public array $transports, public string $attestationType, public TrustPath $trustPath, - public AbstractUid $aaguid, + public Uuid $aaguid, public string $credentialPublicKey, public string $userHandle, public int $counter, - public ?array $otherUI = null + public ?array $otherUI = null, + public ?bool $backupEligible = null, + public ?bool $backupStatus = null, + public ?bool $uvInitialized = null, ) { } @@ -48,11 +50,14 @@ public static function create( array $transports, string $attestationType, TrustPath $trustPath, - AbstractUid $aaguid, + Uuid $aaguid, string $credentialPublicKey, string $userHandle, int $counter, - ?array $otherUI = null + ?array $otherUI = null, + ?bool $backupEligible = null, + ?bool $backupStatus = null, + ?bool $uvInitialized = null, ): self { return new self( $publicKeyCredentialId, @@ -64,7 +69,10 @@ public static function create( $credentialPublicKey, $userHandle, $counter, - $otherUI + $otherUI, + $backupEligible, + $backupStatus, + $uvInitialized ); } @@ -128,7 +136,7 @@ public function getTransports(): array * @deprecated since 4.7.0. Please use the property directly. * @infection-ignore-all */ - public function getAaguid(): AbstractUid + public function getAaguid(): Uuid { return $this->aaguid; } @@ -225,7 +233,9 @@ public static function createFromArray(array $data): self Base64UrlSafe::decodeNoPadding($data['credentialPublicKey']), Base64UrlSafe::decodeNoPadding($data['userHandle']), $data['counter'], - $data['otherUI'] ?? null + $data['otherUI'] ?? null, + $data['backupEligible'] ?? null, + $data['backupStatus'] ?? null, ); } catch (Throwable $throwable) { throw InvalidDataException::create($data, 'Unable to load the data', $throwable); @@ -248,6 +258,9 @@ public function jsonSerialize(): array 'userHandle' => Base64UrlSafe::encodeUnpadded($this->userHandle), 'counter' => $this->counter, 'otherUI' => $this->otherUI, + 'backupEligible' => $this->backupEligible, + 'backupStatus' => $this->backupStatus, + 'uvInitialized' => $this->uvInitialized, ]; } } diff --git a/tests/library/AbstractTestCase.php b/tests/library/AbstractTestCase.php index 0180be2c..199d9ca5 100644 --- a/tests/library/AbstractTestCase.php +++ b/tests/library/AbstractTestCase.php @@ -13,18 +13,13 @@ use Cose\Algorithm\Signature\RSA\RS256; use Cose\Algorithm\Signature\RSA\RS384; use Cose\Algorithm\Signature\RSA\RS512; -use Http\Mock\Client; -use Nyholm\Psr7\Factory\Psr17Factory; -use Nyholm\Psr7\Response; use PHPUnit\Framework\TestCase; -use Psr\Http\Client\ClientInterface; use Symfony\Component\Finder\Finder; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\Serializer\SerializerInterface; use Webauthn\AttestationStatement\AndroidKeyAttestationStatementSupport; use Webauthn\AttestationStatement\AndroidSafetyNetAttestationStatementSupport; use Webauthn\AttestationStatement\AppleAttestationStatementSupport; -use Webauthn\AttestationStatement\AttestationObjectLoader; use Webauthn\AttestationStatement\AttestationStatementSupportManager; use Webauthn\AttestationStatement\FidoU2FAttestationStatementSupport; use Webauthn\AttestationStatement\NoneAttestationStatementSupport; @@ -33,6 +28,7 @@ use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler; use Webauthn\AuthenticatorAssertionResponseValidator; use Webauthn\AuthenticatorAttestationResponseValidator; +use Webauthn\CeremonyStep\CeremonyStepManagerFactory; use Webauthn\Denormalizer\WebauthnSerializerFactory; use Webauthn\MetadataService\CertificateChain\CertificateChainValidator; use Webauthn\MetadataService\CertificateChain\PhpCertificateChainValidator; @@ -52,6 +48,8 @@ abstract class AbstractTestCase extends TestCase protected MockClock $clock; + protected MockHttpClient $client; + private ?PublicKeyCredentialLoader $publicKeyCredentialLoader = null; private ?AuthenticatorAttestationResponseValidator $authenticatorAttestationResponseValidator = null; @@ -60,8 +58,6 @@ abstract class AbstractTestCase extends TestCase private ?Manager $algorithmManager = null; - private ?AttestationObjectLoader $attestationObjectLoader = null; - private ?MetadataStatementRepository $metadataStatementRepository = null; private ?PhpCertificateChainValidator $certificateChainValidator = null; @@ -70,11 +66,14 @@ abstract class AbstractTestCase extends TestCase private null|SerializerInterface $webauthnSerializer = null; + private null|CeremonyStepManagerFactory $ceremonyStepManagerFactory = null; + protected function setUp(): void { parent::setUp(); $this->clock = new MockClock(); + $this->client = new MockHttpClient(); } protected function getPublicKeyCredentialLoader(): PublicKeyCredentialLoader @@ -86,34 +85,46 @@ protected function getPublicKeyCredentialLoader(): PublicKeyCredentialLoader return $this->publicKeyCredentialLoader; } - protected function getAuthenticatorAttestationResponseValidator( - ?ClientInterface $client = null - ): AuthenticatorAttestationResponseValidator { + protected function getAuthenticatorAttestationResponseValidator(): AuthenticatorAttestationResponseValidator + { if ($this->authenticatorAttestationResponseValidator === null) { - $this->authenticatorAttestationResponseValidator = new AuthenticatorAttestationResponseValidator( - $this->getAttestationStatementSupportManager($client), - null, - null, - new ExtensionOutputCheckerHandler() + $this->authenticatorAttestationResponseValidator = AuthenticatorAttestationResponseValidator::create( + ceremonyStepManager: $this->getCeremonyStepManagerFactory() + ->creationCeremony() ); - $this->authenticatorAttestationResponseValidator->enableMetadataStatementSupport( - $this->getMetadataStatementRepository($client), + } + + return $this->authenticatorAttestationResponseValidator; + } + + protected function getCeremonyStepManagerFactory(): CeremonyStepManagerFactory + { + if ($this->ceremonyStepManagerFactory === null) { + $this->ceremonyStepManagerFactory = new CeremonyStepManagerFactory(); + $this->ceremonyStepManagerFactory->enableMetadataStatementSupport( + $this->getMetadataStatementRepository(), $this->getStatusReportRepository(), $this->getCertificateChainValidator(), ); + $this->ceremonyStepManagerFactory->setAlgorithmManager($this->getAlgorithmManager()); + $this->ceremonyStepManagerFactory->setAttestationStatementSupportManager( + $this->getAttestationStatementSupportManager() + ); + $this->ceremonyStepManagerFactory->setExtensionOutputCheckerHandler( + ExtensionOutputCheckerHandler::create() + ); + $this->ceremonyStepManagerFactory->setSecuredRelyingPartyId(['localhost']); } - return $this->authenticatorAttestationResponseValidator; + return $this->ceremonyStepManagerFactory; } protected function getAuthenticatorAssertionResponseValidator(): AuthenticatorAssertionResponseValidator { if ($this->authenticatorAssertionResponseValidator === null) { - $this->authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator( - null, - null, - new ExtensionOutputCheckerHandler(), - $this->getAlgorithmManager(), + $this->authenticatorAssertionResponseValidator = AuthenticatorAssertionResponseValidator::create( + ceremonyStepManager: $this->getCeremonyStepManagerFactory() + ->requestCeremony() ); } @@ -145,27 +156,25 @@ protected function getResponsesMap(): array protected function getSerializer(): SerializerInterface { if ($this->webauthnSerializer === null) { - $this->webauthnSerializer = (new WebauthnSerializerFactory($this->getAttestationStatementSupportManager( - null - )))->create(); + $this->webauthnSerializer = (new WebauthnSerializerFactory( + $this->getAttestationStatementSupportManager() + ))->create(); } return $this->webauthnSerializer; } - private function getAttestationStatementSupportManager( - ?ClientInterface $client - ): AttestationStatementSupportManager { - if ($client === null) { - $client = new Client(new Psr17Factory()); - } + private function getAttestationStatementSupportManager(): AttestationStatementSupportManager + { $attestationStatementSupportManager = new AttestationStatementSupportManager(); $attestationStatementSupportManager->add(new NoneAttestationStatementSupport()); $attestationStatementSupportManager->add(new AppleAttestationStatementSupport()); $attestationStatementSupportManager->add(AndroidKeyAttestationStatementSupport::create()); - $androidSafetyNetAttestationStatementSupport = new AndroidSafetyNetAttestationStatementSupport(); + $androidSafetyNetAttestationStatementSupport = new AndroidSafetyNetAttestationStatementSupport( + $this->clock + ); $androidSafetyNetAttestationStatementSupport - ->enableApiVerification($client, 'api_key', new Psr17Factory()) + ->enableApiVerification($this->client, 'api_key') ->setLeeway(0) ->setMaxAge(99_999_999_999); $attestationStatementSupportManager->add($androidSafetyNetAttestationStatementSupport); @@ -195,29 +204,20 @@ private function getAlgorithmManager(): Manager return $this->algorithmManager; } - private function getMetadataStatementRepository(?ClientInterface $client): MetadataStatementRepositoryInterface + private function getMetadataStatementRepository(): MetadataStatementRepositoryInterface { - if ($client === null) { - $client = new Client(new Psr17Factory()); - } if ($this->metadataStatementRepository === null) { $metadataService = ChainedMetadataServices::create(); foreach ($this->getSingleStatements() as $filename) { $metadataService->addServices(LocalResourceMetadataService::create($filename)); } - foreach ($this->getDistantStatements() as $filename) { - $response = new Response(200, [], trim(file_get_contents($filename))); - //$client = new Client(new Psr17Factory()); - $client->addResponse($response); - - $metadataService->addServices( - new JsonMetadataService( - [ - '{"legalHeader": "https://fidoalliance.org/metadata/metadata-statement-legal-header/", "aaguid": "b93fd961-f2e6-462f-b122-82002247de78", "description": "Android Authenticator with SafetyNet Attestation", "authenticatorVersion": 1, "protocolFamily": "fido2", "schema": 3, "upv": [{"major": 1, "minor": 0}], "authenticationAlgorithms": ["secp256r1_ecdsa_sha256_raw"], "publicKeyAlgAndEncodings": ["cose"], "attestationTypes": ["basic_full"], "userVerificationDetails": [[{"userVerificationMethod": "faceprint_internal"}], [{"userVerificationMethod": "fingerprint_internal"}], [{"userVerificationMethod": "passcode_internal"}], [{"userVerificationMethod": "pattern_internal"}]], "keyProtection": ["hardware", "tee"], "isKeyRestricted": false, "matcherProtection": ["tee"], "attachmentHint": ["internal"], "tcDisplay": [], "attestationRootCertificates": ["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"], "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAB7klEQVR4AaWPP2sUQRiHn5mdvexd/plEcvlDCi1E/EMabUWI9jaKWPoV/A7BQhAbG7t8CCUIKQQLuwhCUBsLBSUmGkLudm9n5nWHzMAego3P8Oy9s8vvfd+jzctPz2Ya+Zdbu48mG0ma8Eh8/bF3yWGGwPvV81d7+9/2lpy3Mrty7jswPPz8Yb20lQJ2iain2w9ok02aLURWstxuiHgknnrEK3GERg9poZ7s3CUxl/dvVfrntmRag9BuICJgrXfHnRvAWyJaDxXB+ezCWqX3t6e6i/ri/E1AkdBoLi/cZrL5pqeHb2yvu9RIUKfiWH95IVmmV6eucK1/j8JMIwRo6jNcX77P2vQ6ZEZ7OXreSFA93rnD3Mx6r7YfTxQKGkN4WP8eW7+bz4Z3eHEE9FFZAJXuliXVyUEfif9ZHINW+BQ5fSc+3oTjztTZRkx4LEhtfh1avBMSIkBrA+JvOAohm1AFgJGRpbOoXS/X1KXgHZE4X1Ssxpt18iYImGJiRFWWKCXkBdiR4L0QUEKamIKxhoQZm6fAdMDVjT7cQwBEYh3DSsl4A+trQTwJbUCsT5P+CodTZtYDmNJYcrEDQSChIMsVzoVQ2kLFMCCQFW4AoDbfbRDI7fIi5aAL41jtVNiQiPUjmUBOgAMCm683/ss/TaVXtx4qKMoAAAAASUVORK5CYII=", "authenticatorGetInfo": {"versions": ["FIDO_2_0"], "aaguid": "b93fd961f2e6462fb12282002247de78", "options": {"plat": true, "rk": true, "uv": true}, "transports": ["internal"], "algorithms": [{"type": "public-key", "alg": -7}]}}', - ] - ) - ); - } + $metadataService->addServices( + new JsonMetadataService( + [ + '{"legalHeader": "https://fidoalliance.org/metadata/metadata-statement-legal-header/", "aaguid": "b93fd961-f2e6-462f-b122-82002247de78", "description": "Android Authenticator with SafetyNet Attestation", "authenticatorVersion": 1, "protocolFamily": "fido2", "schema": 3, "upv": [{"major": 1, "minor": 0}], "authenticationAlgorithms": ["secp256r1_ecdsa_sha256_raw"], "publicKeyAlgAndEncodings": ["cose"], "attestationTypes": ["basic_full"], "userVerificationDetails": [[{"userVerificationMethod": "faceprint_internal"}], [{"userVerificationMethod": "fingerprint_internal"}], [{"userVerificationMethod": "passcode_internal"}], [{"userVerificationMethod": "pattern_internal"}]], "keyProtection": ["hardware", "tee"], "isKeyRestricted": false, "matcherProtection": ["tee"], "attachmentHint": ["internal"], "tcDisplay": [], "attestationRootCertificates": ["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"], "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAB7klEQVR4AaWPP2sUQRiHn5mdvexd/plEcvlDCi1E/EMabUWI9jaKWPoV/A7BQhAbG7t8CCUIKQQLuwhCUBsLBSUmGkLudm9n5nWHzMAego3P8Oy9s8vvfd+jzctPz2Ya+Zdbu48mG0ma8Eh8/bF3yWGGwPvV81d7+9/2lpy3Mrty7jswPPz8Yb20lQJ2iain2w9ok02aLURWstxuiHgknnrEK3GERg9poZ7s3CUxl/dvVfrntmRag9BuICJgrXfHnRvAWyJaDxXB+ezCWqX3t6e6i/ri/E1AkdBoLi/cZrL5pqeHb2yvu9RIUKfiWH95IVmmV6eucK1/j8JMIwRo6jNcX77P2vQ6ZEZ7OXreSFA93rnD3Mx6r7YfTxQKGkN4WP8eW7+bz4Z3eHEE9FFZAJXuliXVyUEfif9ZHINW+BQ5fSc+3oTjztTZRkx4LEhtfh1avBMSIkBrA+JvOAohm1AFgJGRpbOoXS/X1KXgHZE4X1Ssxpt18iYImGJiRFWWKCXkBdiR4L0QUEKamIKxhoQZm6fAdMDVjT7cQwBEYh3DSsl4A+trQTwJbUCsT5P+CodTZtYDmNJYcrEDQSChIMsVzoVQ2kLFMCCQFW4AoDbfbRDI7fIi5aAL41jtVNiQiPUjmUBOgAMCm683/ss/TaVXtx4qKMoAAAAASUVORK5CYII=", "authenticatorGetInfo": {"versions": ["FIDO_2_0"], "aaguid": "b93fd961f2e6462fb12282002247de78", "options": {"plat": true, "rk": true, "uv": true}, "transports": ["internal"], "algorithms": [{"type": "public-key", "alg": -7}]}}', + ] + ) + ); $this->metadataStatementRepository = new MetadataStatementRepository($metadataService); } @@ -236,24 +236,10 @@ private function getSingleStatements(): iterable } } - private function getDistantStatements(): iterable - { - $finder = new Finder(); - $finder->files() - ->in(__DIR__ . '/../metadataServices'); - - foreach ($finder->files() as $file) { - yield $file->getRealPath(); - } - } - private function getCertificateChainValidator(): CertificateChainValidator { if ($this->certificateChainValidator === null) { - $this->certificateChainValidator = PhpCertificateChainValidator::create( - new MockHttpClient(), - $this->clock - ); + $this->certificateChainValidator = PhpCertificateChainValidator::create($this->client, $this->clock); } return $this->certificateChainValidator; diff --git a/tests/library/Functional/AndroidKeyAttestationStatementTest.php b/tests/library/Functional/AndroidKeyAttestationStatementTest.php deleted file mode 100644 index 3460ad9d..00000000 --- a/tests/library/Functional/AndroidKeyAttestationStatementTest.php +++ /dev/null @@ -1,22 +0,0 @@ -expectException(CertificateChainException::class); - $this->expectExceptionMessage('Unable to validate the certificate chain.'); + // Given $publicKeyCredentialCreationOptions = PublicKeyCredentialCreationOptions::create( PublicKeyCredentialRpEntity::create('My Application'), PublicKeyCredentialUserEntity::create( @@ -45,16 +42,23 @@ public function anExpiredAndroidSafetyNetAttestationCannotBeVerified(): void ->load( '{"id":"Ac8zKrpVWv9UCwxY1FyMqkESz2lV4CNwTk2-Hp19LgKbvh5uQ2_i6AMbTbTz1zcNapCEeiLJPlAAVM4L7AIow6I","type":"public-key","rawId":"Ac8zKrpVWv9UCwxY1FyMqkESz2lV4CNwTk2+Hp19LgKbvh5uQ2/i6AMbTbTz1zcNapCEeiLJPlAAVM4L7AIow6I=","response":{"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoia21uczQzQ1dWc3diTW92cktQa2dkMWxFcGM2TFpkZmswVVFfbnVaYnAwMGpXNUM2MVBFVzFkTmFwdFowR2tySUs5V1J0YUFYV2tuZElFRUJnTklDUnciLCJvcmlnaW4iOiJodHRwczpcL1wvd2ViYXV0aG4ubW9yc2VsbGkuZnIiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uYW5kcm9pZC5jaHJvbWUifQ","attestationObject":"o2NmbXRxYW5kcm9pZC1zYWZldHluZXRnYXR0U3RtdKJjdmVyaDE0Nzk5MDM3aHJlc3BvbnNlWRS9ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbmcxWXlJNld5Sk5TVWxHYTJwRFEwSkljV2RCZDBsQ1FXZEpVVkpZY205T01GcFBaRkpyUWtGQlFVRkJRVkIxYm5wQlRrSm5hM0ZvYTJsSE9YY3dRa0ZSYzBaQlJFSkRUVkZ6ZDBOUldVUldVVkZIUlhkS1ZsVjZSV1ZOUW5kSFFURlZSVU5vVFZaU01qbDJXako0YkVsR1VubGtXRTR3U1VaT2JHTnVXbkJaTWxaNlRWSk5kMFZSV1VSV1VWRkVSWGR3U0ZaR1RXZFJNRVZuVFZVNGVFMUNORmhFVkVVMFRWUkJlRTFFUVROTlZHc3dUbFp2V0VSVVJUVk5WRUYzVDFSQk0wMVVhekJPVm05M1lrUkZURTFCYTBkQk1WVkZRbWhOUTFaV1RYaEZla0ZTUW1kT1ZrSkJaMVJEYTA1b1lrZHNiV0l6U25WaFYwVjRSbXBCVlVKblRsWkNRV05VUkZVeGRtUlhOVEJaVjJ4MVNVWmFjRnBZWTNoRmVrRlNRbWRPVmtKQmIxUkRhMlIyWWpKa2MxcFRRazFVUlUxNFIzcEJXa0puVGxaQ1FVMVVSVzFHTUdSSFZucGtRelZvWW0xU2VXSXliR3RNYlU1MllsUkRRMEZUU1hkRVVWbEtTMjlhU1doMlkwNUJVVVZDUWxGQlJHZG5SVkJCUkVORFFWRnZRMmRuUlVKQlRtcFlhM293WlVzeFUwVTBiU3N2UnpWM1QyOHJXRWRUUlVOeWNXUnVPRGh6UTNCU04yWnpNVFJtU3pCU2FETmFRMWxhVEVaSWNVSnJOa0Z0V2xaM01rczVSa2N3VHpseVVsQmxVVVJKVmxKNVJUTXdVWFZ1VXpsMVowaEROR1ZuT1c5MmRrOXRLMUZrV2pKd09UTllhSHAxYmxGRmFGVlhXRU40UVVSSlJVZEtTek5UTW1GQlpucGxPVGxRVEZNeU9XaE1ZMUYxV1ZoSVJHRkROMDlhY1U1dWIzTnBUMGRwWm5NNGRqRnFhVFpJTDNob2JIUkRXbVV5YkVvck4wZDFkSHBsZUV0d2VIWndSUzkwV2xObVlsazVNRFZ4VTJ4Q2FEbG1jR293TVRWamFtNVJSbXRWYzBGVmQyMUxWa0ZWZFdWVmVqUjBTMk5HU3pSd1pYWk9UR0Y0UlVGc0swOXJhV3hOZEVsWlJHRmpSRFZ1Wld3MGVFcHBlWE0wTVROb1lXZHhWekJYYUdnMVJsQXpPV2hIYXpsRkwwSjNVVlJxWVhwVGVFZGtkbGd3YlRaNFJsbG9hQzh5VmsxNVdtcFVORXQ2VUVwRlEwRjNSVUZCWVU5RFFXeG5kMmRuU2xWTlFUUkhRVEZWWkVSM1JVSXZkMUZGUVhkSlJtOUVRVlJDWjA1V1NGTlZSVVJFUVV0Q1oyZHlRbWRGUmtKUlkwUkJWRUZOUW1kT1ZraFNUVUpCWmpoRlFXcEJRVTFDTUVkQk1WVmtSR2RSVjBKQ1VYRkNVWGRIVjI5S1FtRXhiMVJMY1hWd2J6UlhObmhVTm1veVJFRm1RbWRPVmtoVFRVVkhSRUZYWjBKVFdUQm1hSFZGVDNaUWJTdDRaMjU0YVZGSE5rUnlabEZ1T1V0NlFtdENaMmR5UW1kRlJrSlJZMEpCVVZKWlRVWlpkMHAzV1VsTGQxbENRbEZWU0UxQlIwZEhNbWd3WkVoQk5reDVPWFpaTTA1M1RHNUNjbUZUTlc1aU1qbHVUREprTUdONlJuWk5WRUZ5UW1kbmNrSm5SVVpDVVdOM1FXOVpabUZJVWpCalJHOTJURE5DY21GVE5XNWlNamx1VERKa2VtTnFTWFpTTVZKVVRWVTRlRXh0VG5sa1JFRmtRbWRPVmtoU1JVVkdha0ZWWjJoS2FHUklVbXhqTTFGMVdWYzFhMk50T1hCYVF6VnFZakl3ZDBsUldVUldVakJuUWtKdmQwZEVRVWxDWjFwdVoxRjNRa0ZuU1hkRVFWbExTM2RaUWtKQlNGZGxVVWxHUVhwQmRrSm5UbFpJVWpoRlMwUkJiVTFEVTJkSmNVRm5hR2cxYjJSSVVuZFBhVGgyV1ROS2MweHVRbkpoVXpWdVlqSTVia3d3WkZWVmVrWlFUVk0xYW1OdGQzZG5aMFZGUW1kdmNrSm5SVVZCWkZvMVFXZFJRMEpKU0RGQ1NVaDVRVkJCUVdSM1EydDFVVzFSZEVKb1dVWkpaVGRGTmt4TldqTkJTMUJFVjFsQ1VHdGlNemRxYW1RNE1FOTVRVE5qUlVGQlFVRlhXbVJFTTFCTVFVRkJSVUYzUWtsTlJWbERTVkZEVTFwRFYyVk1Tblp6YVZaWE5rTm5LMmRxTHpsM1dWUktVbnAxTkVocGNXVTBaVmswWXk5dGVYcHFaMGxvUVV4VFlta3ZWR2g2WTNweGRHbHFNMlJyTTNaaVRHTkpWek5NYkRKQ01HODNOVWRSWkdoTmFXZGlRbWRCU0ZWQlZtaFJSMjFwTDFoM2RYcFVPV1ZIT1ZKTVNTdDRNRm95ZFdKNVdrVldla0UzTlZOWlZtUmhTakJPTUVGQlFVWnRXRkU1ZWpWQlFVRkNRVTFCVW1wQ1JVRnBRbU5EZDBFNWFqZE9WRWRZVURJM09IbzBhSEl2ZFVOSWFVRkdUSGx2UTNFeVN6QXJlVXhTZDBwVlltZEpaMlk0WjBocWRuQjNNbTFDTVVWVGFuRXlUMll6UVRCQlJVRjNRMnR1UTJGRlMwWlZlVm8zWmk5UmRFbDNSRkZaU2t0dldrbG9kbU5PUVZGRlRFSlJRVVJuWjBWQ1FVazVibFJtVWt0SlYyZDBiRmRzTTNkQ1REVTFSVlJXTm10aGVuTndhRmN4ZVVGak5VUjFiVFpZVHpReGExcDZkMG8yTVhkS2JXUlNVbFF2VlhORFNYa3hTMFYwTW1Nd1JXcG5iRzVLUTBZeVpXRjNZMFZYYkV4UldUSllVRXg1Um1wclYxRk9ZbE5vUWpGcE5GY3lUbEpIZWxCb2RETnRNV0kwT1doaWMzUjFXRTAyZEZnMVEzbEZTRzVVYURoQ2IyMDBMMWRzUm1sb2VtaG5iamd4Ukd4a2IyZDZMMHN5VlhkTk5sTTJRMEl2VTBWNGEybFdabllyZW1KS01ISnFkbWM1TkVGc1pHcFZabFYzYTBrNVZrNU5ha1ZRTldVNGVXUkNNMjlNYkRabmJIQkRaVVkxWkdkbVUxZzBWVGw0TXpWdmFpOUpTV1F6VlVVdlpGQndZaTl4WjBkMmMydG1aR1Y2ZEcxVmRHVXZTMU50Y21sM1kyZFZWMWRsV0daVVlra3plbk5wYTNkYVltdHdiVkpaUzIxcVVHMW9kalJ5YkdsNlIwTkhkRGhRYmpod2NUaE5Na3RFWmk5UU0ydFdiM1F6WlRFNFVUMGlMQ0pOU1VsRlUycERRMEY2UzJkQmQwbENRV2RKVGtGbFR6QnRjVWRPYVhGdFFrcFhiRkYxUkVGT1FtZHJjV2hyYVVjNWR6QkNRVkZ6UmtGRVFrMU5VMEYzU0dkWlJGWlJVVXhGZUdSSVlrYzVhVmxYZUZSaFYyUjFTVVpLZG1JelVXZFJNRVZuVEZOQ1UwMXFSVlJOUWtWSFFURlZSVU5vVFV0U01uaDJXVzFHYzFVeWJHNWlha1ZVVFVKRlIwRXhWVVZCZUUxTFVqSjRkbGx0Um5OVk1teHVZbXBCWlVaM01IaE9la0V5VFZSVmQwMUVRWGRPUkVwaFJuY3dlVTFVUlhsTlZGVjNUVVJCZDA1RVNtRk5SVWw0UTNwQlNrSm5UbFpDUVZsVVFXeFdWRTFTTkhkSVFWbEVWbEZSUzBWNFZraGlNamx1WWtkVloxWklTakZqTTFGblZUSldlV1J0YkdwYVdFMTRSWHBCVWtKblRsWkNRVTFVUTJ0a1ZWVjVRa1JSVTBGNFZIcEZkMmRuUldsTlFUQkhRMU54UjFOSllqTkVVVVZDUVZGVlFVRTBTVUpFZDBGM1oyZEZTMEZ2U1VKQlVVUlJSMDA1UmpGSmRrNHdOWHByVVU4NUszUk9NWEJKVW5aS2VucDVUMVJJVnpWRWVrVmFhRVF5WlZCRGJuWlZRVEJSYXpJNFJtZEpRMlpMY1VNNVJXdHpRelJVTW1aWFFsbHJMMnBEWmtNelVqTldXazFrVXk5a1RqUmFTME5GVUZwU2NrRjZSSE5wUzFWRWVsSnliVUpDU2pWM2RXUm5lbTVrU1UxWlkweGxMMUpIUjBac05YbFBSRWxMWjJwRmRpOVRTa2d2VlV3clpFVmhiSFJPTVRGQ2JYTkxLMlZSYlUxR0t5dEJZM2hIVG1oeU5UbHhUUzg1YVd3M01Va3laRTQ0UmtkbVkyUmtkM1ZoWldvMFlsaG9jREJNWTFGQ1ltcDRUV05KTjBwUU1HRk5NMVEwU1N0RWMyRjRiVXRHYzJKcWVtRlVUa001ZFhwd1JteG5UMGxuTjNKU01qVjRiM2x1VlhoMk9IWk9iV3R4TjNwa1VFZElXR3Q0VjFrM2IwYzVhaXRLYTFKNVFrRkNhemRZY2twbWIzVmpRbHBGY1VaS1NsTlFhemRZUVRCTVMxY3dXVE42Tlc5Nk1rUXdZekYwU2t0M1NFRm5UVUpCUVVkcVoyZEZlazFKU1VKTWVrRlBRbWRPVmtoUk9FSkJaamhGUWtGTlEwRlpXWGRJVVZsRVZsSXdiRUpDV1hkR1FWbEpTM2RaUWtKUlZVaEJkMFZIUTBOelIwRlJWVVpDZDAxRFRVSkpSMEV4VldSRmQwVkNMM2RSU1UxQldVSkJaamhEUVZGQmQwaFJXVVJXVWpCUFFrSlpSVVpLYWxJclJ6UlJOamdyWWpkSFEyWkhTa0ZpYjA5ME9VTm1NSEpOUWpoSFFURlZaRWwzVVZsTlFtRkJSa3AyYVVJeFpHNUlRamRCWVdkaVpWZGlVMkZNWkM5alIxbFpkVTFFVlVkRFEzTkhRVkZWUmtKM1JVSkNRMnQzU25wQmJFSm5aM0pDWjBWR1FsRmpkMEZaV1ZwaFNGSXdZMFJ2ZGt3eU9XcGpNMEYxWTBkMGNFeHRaSFppTW1OMldqTk9lVTFxUVhsQ1owNVdTRkk0UlV0NlFYQk5RMlZuU21GQmFtaHBSbTlrU0ZKM1QyazRkbGt6U25OTWJrSnlZVk0xYm1JeU9XNU1NbVI2WTJwSmRsb3pUbmxOYVRWcVkyMTNkMUIzV1VSV1VqQm5Ra1JuZDA1cVFUQkNaMXB1WjFGM1FrRm5TWGRMYWtGdlFtZG5ja0puUlVaQ1VXTkRRVkpaWTJGSVVqQmpTRTAyVEhrNWQyRXlhM1ZhTWpsMlduazVlVnBZUW5aak1td3dZak5LTlV4NlFVNUNaMnR4YUd0cFJ6bDNNRUpCVVhOR1FVRlBRMEZSUlVGSGIwRXJUbTV1TnpoNU5uQlNhbVE1V0d4UlYwNWhOMGhVWjJsYUwzSXpVazVIYTIxVmJWbElVRkZ4TmxOamRHazVVRVZoYW5aM1VsUXlhVmRVU0ZGeU1ESm1aWE54VDNGQ1dUSkZWRlYzWjFwUksyeHNkRzlPUm5ab2MwODVkSFpDUTA5SllYcHdjM2RYUXpsaFNqbDRhblUwZEZkRVVVZzRUbFpWTmxsYVdpOVlkR1ZFVTBkVk9WbDZTbkZRYWxrNGNUTk5SSGh5ZW0xeFpYQkNRMlkxYnpodGR5OTNTalJoTWtjMmVIcFZjalpHWWpaVU9FMWpSRTh5TWxCTVVrdzJkVE5OTkZSNmN6TkJNazB4YWpaaWVXdEtXV2s0ZDFkSlVtUkJka3RNVjFwMUwyRjRRbFppZWxsdGNXMTNhMjAxZWt4VFJGYzFia2xCU21KRlRFTlJRMXAzVFVnMU5uUXlSSFp4YjJaNGN6WkNRbU5EUmtsYVZWTndlSFUyZURaMFpEQldOMU4yU2tORGIzTnBjbE50U1dGMGFpODVaRk5UVmtSUmFXSmxkRGh4THpkVlN6UjJORnBWVGpnd1lYUnVXbm94ZVdjOVBTSmRmUS5leUp1YjI1alpTSTZJbVoxUlZsb0szaFhVRkEzZUhCNVVUZzVhbGh3Y0ZGT05tMWlNV2RYWnpNM1JsQnZOM05VU2pFeFVFMDlJaXdpZEdsdFpYTjBZVzF3VFhNaU9qRTFORGcwT0RneU5UazRNamtzSW1Gd2ExQmhZMnRoWjJWT1lXMWxJam9pWTI5dExtZHZiMmRzWlM1aGJtUnliMmxrTG1kdGN5SXNJbUZ3YTBScFoyVnpkRk5vWVRJMU5pSTZJa0YyV0hGcE1FSnRiVXRKYm1KSVlqTXlaalI2VldoMmVqUmxjR3BwU25RM2EwdE5SMmhUZDNjeFJGVTlJaXdpWTNSelVISnZabWxzWlUxaGRHTm9JanAwY25WbExDSmhjR3REWlhKMGFXWnBZMkYwWlVScFoyVnpkRk5vWVRJMU5pSTZXeUk0VURGelZ6QkZVRXBqYzJ4M04xVjZVbk5wV0V3Mk5IY3JUelV3UldRclVrSkpRM1JoZVRGbk1qUk5QU0pkTENKaVlYTnBZMGx1ZEdWbmNtbDBlU0k2ZEhKMVpYMC5DQldQQ1FNaDBIdjhSTllZc05HTGVuci16RVEyY3o2Q25xalZhblZKOXV1b0d5WFpkc19mRTkwbFRjN0tpYVFMNExWSDl1TnNLWjdyN0xZSzRHTHhHekNqWklwZFlFZUIwdWxaWEN1bDdaVFI2MzZmODBWZmxkZ0dJdDRocWJ6S3dsd0EwNEZJN3ZpbDZjbkNJRHQ4SHVyTzVwRnJIdDVhUkpVcUxnOWhPT3VOaDVYS1JQS29aVTZyQlg5eVhxUmFtbl9SbWd6SkEwRGpqcXNaM3BlYUVvX2g5T0hJUHV3Q3FXZUdlZk5lRmoxVnBnaENpdW1lMXpPb2lwSmt3Tkx3dHdJamNDZ0VqYmc1OEF6ZHBPY01fLUtKYXBUeFJlYk9ZclM3dExTUlZfb2xjZG9PWGUtZ0ctVktCeTRUclJkdE9zNUdydTBqdlNyUGMwZXh6OHV2MkFoYXV0aERhdGFYxcrUbtuZYVMj5mIkvf6KvF1ZzC0gYwKd4+myQgSJCUO2RQAAAAC5P9lh8uZGL7EiggAiR954AEEBzzMqulVa/1QLDFjUXIyqQRLPaVXgI3BOTb4enX0uApu+Hm5Db+LoAxtNtPPXNw1qkIR6Isk+UABUzgvsAijDoqUBAgMmIAEhWCACJyweJ5aGUeFWycOhX/jCeAcTVjAxnbZnJmxj+aLWtyJYIAOY6jc/2y5iT60VYTtZaeBvsQIwgU/XR9Fax7xtatkY"}}' ); + $this->clock->set((new DateTimeImmutable())->setTimestamp(1_548_489_000)); + static::assertInstanceOf(AuthenticatorAttestationResponse::class, $publicKeyCredential->response); - $client = new Client(new Psr17Factory()); - $httpResponse = new Response(); - $httpResponse = $httpResponse->withHeader('content-type', 'application/json'); - $httpResponse->getBody() - ->write('{"isValidSignature":true}'); - $httpResponse->getBody() - ->rewind(); - $client->setDefaultResponse($httpResponse); - $this->getAuthenticatorAttestationResponseValidator($client) + $this->client->setResponseFactory(new MockResponse( + '{"isValidSignature":true}', + [ + 'headers' => [ + 'content-type' => 'application/json', + ], + ] + )); + + // When + $pkSource = $this->getAuthenticatorAttestationResponseValidator() ->check($publicKeyCredential->response, $publicKeyCredentialCreationOptions, 'webauthn.morselli.fr'); + + // Then + static::assertSame('b93fd961-f2e6-462f-b122-82002247de78', $pkSource->aaguid->toRfc4122()); } } diff --git a/tests/library/Functional/AppleAttestationStatementTest.php b/tests/library/Functional/AppleAttestationStatementTest.php index b2e06032..13146278 100644 --- a/tests/library/Functional/AppleAttestationStatementTest.php +++ b/tests/library/Functional/AppleAttestationStatementTest.php @@ -49,7 +49,7 @@ public function anAppleAttestationCanBeVerified(): void static::assertInstanceOf(AuthenticatorAttestationResponse::class, $publicKeyCredential->response); $this->getAuthenticatorAttestationResponseValidator() ->check($publicKeyCredential->response, $publicKeyCredentialCreationOptions, 'dev.dontneeda.pw'); - $publicKeyCredentialDescriptor = $publicKeyCredential->getPublicKeyCredentialDescriptor(['usb']); + $publicKeyCredentialDescriptor = $publicKeyCredential->getPublicKeyCredentialDescriptor(); static::assertSame( base64_decode('J4lAqPXhefDrUD7oh5LQMbBH5TE', true), Base64UrlSafe::decode($publicKeyCredential->id) @@ -59,7 +59,7 @@ public function anAppleAttestationCanBeVerified(): void PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, $publicKeyCredentialDescriptor->type ); - static::assertSame(['usb'], $publicKeyCredentialDescriptor->transports); + static::assertSame([], $publicKeyCredentialDescriptor->transports); /** @var AuthenticatorData $authenticatorData */ $authenticatorData = $publicKeyCredential->response ->attestationObject diff --git a/tests/library/Functional/AssertionTest.php b/tests/library/Functional/AssertionTest.php index 67512146..89f9ae2f 100644 --- a/tests/library/Functional/AssertionTest.php +++ b/tests/library/Functional/AssertionTest.php @@ -175,7 +175,11 @@ public function aPreviouslyFixedKeyCanBeVerified(): void ); static::assertInstanceOf(AuthenticatorAttestationResponse::class, $publicKeyCredential->response); $source = $this->getAuthenticatorAttestationResponseValidator() - ->check($publicKeyCredential->response, $publicKeyCredentialCreationOptions, 'localhost'); + ->check( + $publicKeyCredential->response, + $publicKeyCredentialCreationOptions, + 'tuleap-web.tuleap-aio-dev.docker' + ); $publicKeyCredentialRequestOptions = $this->getSerializer() ->deserialize( diff --git a/tests/library/Functional/AttestationStatementWithTokenBindingTest.php b/tests/library/Functional/AttestationStatementWithTokenBindingTest.php index bc6e8e79..ae66f957 100644 --- a/tests/library/Functional/AttestationStatementWithTokenBindingTest.php +++ b/tests/library/Functional/AttestationStatementWithTokenBindingTest.php @@ -46,7 +46,7 @@ public function anAttestationWithTokenBindingCanBeVerified(): void static::assertInstanceOf(AuthenticatorAttestationResponse::class, $publicKeyCredential->response); $this->getAuthenticatorAttestationResponseValidator() ->check($publicKeyCredential->response, $publicKeyCredentialCreationOptions, 'webauthn.morselli.fr'); - $publicKeyCredentialDescriptor = $publicKeyCredential->getPublicKeyCredentialDescriptor(['usb']); + $publicKeyCredentialDescriptor = $publicKeyCredential->getPublicKeyCredentialDescriptor(); static::assertSame( base64_decode( '+uZVS9+4JgjAYI49YhdzTgHmbn638+ZNSvC0UtHkWTVS+CtTjnaSbqtzdzijByOAvEAsh+TaQJAr43FRj+dYag==', @@ -65,7 +65,7 @@ public function anAttestationWithTokenBindingCanBeVerified(): void PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, $publicKeyCredentialDescriptor->type ); - static::assertSame(['usb'], $publicKeyCredentialDescriptor->transports); + static::assertSame([], $publicKeyCredentialDescriptor->transports); /** @var AuthenticatorData $authenticatorData */ $authenticatorData = $publicKeyCredential->response ->attestationObject diff --git a/tests/library/Functional/AttestationTest.php b/tests/library/Functional/AttestationTest.php index 4d423e59..a6a18863 100644 --- a/tests/library/Functional/AttestationTest.php +++ b/tests/library/Functional/AttestationTest.php @@ -53,9 +53,7 @@ public function anAttestationSignedWithEcDSA521ShouldBeVerified(): void // Then static::assertInstanceOf(AuthenticatorAttestationResponse::class, $publicKeyCredential->response); static::assertSame(['usb'], $publicKeyCredentialSource->transports); - $publicKeyCredentialDescriptor = $publicKeyCredential->getPublicKeyCredentialDescriptor( - $publicKeyCredentialSource->transports - ); + $publicKeyCredentialDescriptor = $publicKeyCredential->getPublicKeyCredentialDescriptor(); static::assertSame( hex2bin('4787c0563f68b2055564bef21dfb4f7953a68e89b7c70e192caec3b7ff26cce3'), Base64UrlSafe::decode($publicKeyCredential->id) diff --git a/tests/library/Functional/PackedAttestationStatementTest.php b/tests/library/Functional/PackedAttestationStatementTest.php index 5bc86d95..a175f31b 100644 --- a/tests/library/Functional/PackedAttestationStatementTest.php +++ b/tests/library/Functional/PackedAttestationStatementTest.php @@ -43,7 +43,7 @@ public function aPackedAttestationCanBeVerified(): void static::assertInstanceOf(AuthenticatorAttestationResponse::class, $publicKeyCredential->response); $this->getAuthenticatorAttestationResponseValidator() ->check($publicKeyCredential->response, $publicKeyCredentialCreationOptions, 'localhost'); - $publicKeyCredentialDescriptor = $publicKeyCredential->getPublicKeyCredentialDescriptor(['usb']); + $publicKeyCredentialDescriptor = $publicKeyCredential->getPublicKeyCredentialDescriptor(); static::assertSame( base64_decode( 'xYw3gEj0LVL83JXz7oKL14XQjh9W1NMFrTALWI+lqXl7ndKW+n8JFYsBCuKbZA3zRAUxAZDHG/tXHsAi6TbO0Q==', @@ -62,7 +62,7 @@ public function aPackedAttestationCanBeVerified(): void PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, $publicKeyCredentialDescriptor->type ); - static::assertSame(['usb'], $publicKeyCredentialDescriptor->transports); + static::assertSame([], $publicKeyCredentialDescriptor->transports); /** @var AuthenticatorData $authenticatorData */ $authenticatorData = $publicKeyCredential->response->attestationObject->authData; static::assertSame( @@ -103,7 +103,7 @@ public function aPackedAttestationWithSelfStatementCanBeVerified(): void $publicKeyCredentialCreationOptions, 'spomky-webauthn.herokuapp.com' ); - $publicKeyCredentialDescriptor = $publicKeyCredential->getPublicKeyCredentialDescriptor(['usb']); + $publicKeyCredentialDescriptor = $publicKeyCredential->getPublicKeyCredentialDescriptor(); static::assertSame( base64_decode( 'AFkzwaxVuCUz4qFPaNAgnYgoZKKTtvGIAaIASAbnlHGy8UktdI/jN0CetpIkiw9++R0AF9a6OJnHD+G4aIWur+Pxj+sI9xDE+AVeQKve', @@ -122,7 +122,7 @@ public function aPackedAttestationWithSelfStatementCanBeVerified(): void PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, $publicKeyCredentialDescriptor->type ); - static::assertSame(['usb'], $publicKeyCredentialDescriptor->transports); + static::assertSame([], $publicKeyCredentialDescriptor->transports); /** @var AuthenticatorData $authenticatorData */ $authenticatorData = $publicKeyCredential->response ->attestationObject @@ -159,9 +159,9 @@ public function p2(): void ->check( $publicKeyCredential->response, $publicKeyCredentialCreationOptions, - 'spomky-webauthn.herokuapp.com' + 'webauthn.spomky-labs.com' ); - $publicKeyCredentialDescriptor = $publicKeyCredential->getPublicKeyCredentialDescriptor(['usb']); + $publicKeyCredentialDescriptor = $publicKeyCredential->getPublicKeyCredentialDescriptor(); static::assertSame( base64_decode('RSRHHrZblfX23SKbu09qBzVp8Y1W1c9GI1EtHZ9gDzY=', true), Base64UrlSafe::decode($publicKeyCredential->id) @@ -174,7 +174,7 @@ public function p2(): void PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, $publicKeyCredentialDescriptor->type ); - static::assertSame(['usb'], $publicKeyCredentialDescriptor->transports); + static::assertSame([], $publicKeyCredentialDescriptor->transports); /** @var AuthenticatorData $authenticatorData */ $authenticatorData = $publicKeyCredential->response ->attestationObject diff --git a/tests/library/Functional/PublicKeyCreationCeremonyTest.php b/tests/library/Functional/PublicKeyCreationCeremonyTest.php index 84a7f001..44e4b903 100644 --- a/tests/library/Functional/PublicKeyCreationCeremonyTest.php +++ b/tests/library/Functional/PublicKeyCreationCeremonyTest.php @@ -17,15 +17,20 @@ final class PublicKeyCreationCeremonyTest extends AbstractTestCase { #[Test] #[DataProvider('getPublicKeyCredentialCreationOptions')] - public function theCeremonySucceeded(string $options, string $response, string $keyId, string $type): void - { + public function theCeremonySucceeded( + string $options, + string $response, + string $keyId, + string $type, + string $host + ): void { $publicKeyCredentialCreationOptions = $this->getSerializer() ->deserialize($options, PublicKeyCredentialCreationOptions::class, 'json'); $publicKeyCredential = $this->getPublicKeyCredentialLoader() ->load($response); static::assertInstanceOf(AuthenticatorAttestationResponse::class, $publicKeyCredential->response); $source = $this->getAuthenticatorAttestationResponseValidator() - ->check($publicKeyCredential->response, $publicKeyCredentialCreationOptions, 'localhost'); + ->check($publicKeyCredential->response, $publicKeyCredentialCreationOptions, $host); static::assertSame(hex2bin($keyId), $source->publicKeyCredentialId); static::assertSame($type, $source->attestationType); @@ -38,12 +43,14 @@ public static function getPublicKeyCredentialCreationOptions(): iterable '{"id":"-cGSjQwC4UBTsh2Mw6guep2uTdLXOExla3QJrVpByOkEWJaOljo54PWOazmHtxBuV5DeysX7qjohoGYK2YibdA","type":"public-key","rawId":"+cGSjQwC4UBTsh2Mw6guep2uTdLXOExla3QJrVpByOkEWJaOljo54PWOazmHtxBuV5DeysX7qjohoGYK2YibdA==","response":{"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJ4R1EyaDJiSzJ0Ym45NmVOdXRib0c1M0Z2VVQzMElWXzJUaG9lS1B1Nzc4Iiwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdXRobi5zcG9ta3ktbGFicy5jb20iLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0","attestationObject":"o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIhALotmA9bjE8DC5afT4C6QJHwB2TDCgh+/DSpIuxt1Z2dAiBzVRmktx9Ur1sjxZJvjhAnzZCRDicD/h2dyd8a+MkVGWN4NWOBWQLCMIICvjCCAaagAwIBAgIEdIb9wjANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbzELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMTk1NTAwMzg0MjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJVd8633JH0xde/9nMTzGk6HjrrhgQlWYVD7OIsuX2Unv1dAmqWBpQ0KxS8YRFwKE1SKE1PIpOWacE5SO8BN6+2jbDBqMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS4xMBMGCysGAQQBguUcAgEBBAQDAgUgMCEGCysGAQQBguUcAQEEBBIEEPigEfOMCk0VgAYXER+e3H0wDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAMVxIgOaaUn44Zom9af0KqG9J655OhUVBVW+q0As6AIod3AH5bHb2aDYakeIyyBCnnGMHTJtuekbrHbXYXERIn4aKdkPSKlyGLsA/A+WEi+OAfXrNVfjhrh7iE6xzq0sg4/vVJoywe4eAJx0fS+Dl3axzTTpYl71Nc7p/NX6iCMmdik0pAuYJegBcTckE3AoYEg4K99AM/JaaKIblsbFh8+3LxnemeNf7UwOczaGGvjS6UzGVI0Odf9lKcPIwYhuTxM5CaNMXTZQ7xq4/yTfC3kPWtE4hFT34UJJflZBiLrxG4OsYxkHw/n5vKgmpspB3GfYuYTWhkDKiE8CYtyg87mhhdXRoRGF0YVjEzJVnqxWDxWv1dKGpGXUmkUO/MGMFSRae4FA9zhbiR3VBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQPnBko0MAuFAU7IdjMOoLnqdrk3S1zhMZWt0Ca1aQcjpBFiWjpY6OeD1jms5h7cQbleQ3srF+6o6IaBmCtmIm3SlAQIDJiABIVggZ9kAaP2QIzTF401zK9+GnJ9t5P5nZMd+7Uq2dj9zrDciWCAYPJnCkmc15U8txqQB+CdSKUhpVrhITkmBPycz6nzp8g=="}}', 'f9c1928d0c02e14053b21d8cc3a82e7a9dae4dd2d7384c656b7409ad5a41c8e90458968e963a39e0f58e6b3987b7106e5790decac5fbaa3a21a0660ad9889b74', 'basic', + 'spomky-labs.com', ]; yield 'wrongEdDSAlgorithmIsFixed' => [ '{"rp": {"name": "Tuleap", "id": "tuleap-web.tuleap-aio-dev.docker"}, "user": {"name": "admin", "id": "MTAx", "displayName": "Site Administrator"}, "challenge": "oq1vpg74u-TmqW3Dv2LwU_jH00NQf65OqpMhrvr7yPY", "pubKeyCredParams": [{"type": "public-key", "alg": -8}, {"type": "public-key", "alg": -7}, {"type": "public-key", "alg": -257}], "attestation": "none"}', '{"clientExtensionResults": {}, "id": "ma2Y7hbtrzJtoDR4N2PkazhnrO6_58gZ8mO8epx-6aCnR9Jtio8Ge1w0_msV7HniYmLIH9yxOW8Yu_9ze_y8oj-MehAozj1jFTsjlQUEc_dxdzG5uFJTn6_RnzhulEWCcZZwcvlNTYne99MpWAD31c-4IuEr-eRRV1DWSANcax0", "rawId": "ma2Y7hbtrzJtoDR4N2PkazhnrO6_58gZ8mO8epx-6aCnR9Jtio8Ge1w0_msV7HniYmLIH9yxOW8Yu_9ze_y8oj-MehAozj1jFTsjlQUEc_dxdzG5uFJTn6_RnzhulEWCcZZwcvlNTYne99MpWAD31c-4IuEr-eRRV1DWSANcax0", "response": {"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVkBCRawLfvD1MyjfrwvZRZlmxIhDbnhAYq58TqWkGOOpv2oRQAAAAIvwFefgRNH6rEWu1qNuSAqAICZrZjuFu2vMm2gNHg3Y-RrOGes7r_nyBnyY7x6nH7poKdH0m2KjwZ7XDT-axXseeJiYsgf3LE5bxi7_3N7_LyiP4x6ECjOPWMVOyOVBQRz93F3Mbm4UlOfr9GfOG6URYJxlnBy-U1Nid730ylYAPfVz7gi4Sv55FFXUNZIA1xrHaMBY09LUAMnIGdFZDI1NTE5IZggCBjXGDcYzBgpGFwYlBgcGJYYTxjdGOYY8BjyGL4YPxg7GEgYfBh_GCIYKxhgChgmGIQYkhhQGH0Y1hjoGIk", "clientDataJSON": "eyJjaGFsbGVuZ2UiOiJvcTF2cGc3NHUtVG1xVzNEdjJMd1VfakgwME5RZjY1T3FwTWhydnI3eVBZIiwib3JpZ2luIjoiaHR0cHM6Ly90dWxlYXAtd2ViLnR1bGVhcC1haW8tZGV2LmRvY2tlciIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ"}, "type": "public-key"}', '99ad98ee16edaf326da034783763e46b3867aceebfe7c819f263bc7a9c7ee9a0a747d26d8a8f067b5c34fe6b15ec79e26262c81fdcb1396f18bbff737bfcbca23f8c7a1028ce3d63153b2395050473f7717731b9b852539fafd19f386e94458271967072f94d4d89def7d3295800f7d5cfb822e12bf9e4515750d648035c6b1d', 'none', + 'tuleap-web.tuleap-aio-dev.docker', ]; } } diff --git a/tests/library/Functional/W10Test.php b/tests/library/Functional/W10Test.php index 0ac79881..ecb0f8d4 100644 --- a/tests/library/Functional/W10Test.php +++ b/tests/library/Functional/W10Test.php @@ -16,6 +16,7 @@ use Webauthn\PublicKeyCredentialDescriptor; use Webauthn\PublicKeyCredentialRequestOptions; use Webauthn\Tests\AbstractTestCase; +use Webauthn\TrustPath\CertificateTrustPath; use Webauthn\TrustPath\EmptyTrustPath; /** @@ -31,7 +32,10 @@ public function anAttestationCanBeVerified( string $credentialId, string $host, string $rpIdHash, - int $signCount + int $signCount, + string $aaguid, + string $attestationType, + string $trustPath, ): void { $publicKeyCredentialCreationOptions = $this->getSerializer() ->deserialize($publicKeyCredentialCreationOptionsData, PublicKeyCredentialCreationOptions::class, 'json'); @@ -40,14 +44,14 @@ public function anAttestationCanBeVerified( static::assertInstanceOf(AuthenticatorAttestationResponse::class, $publicKeyCredential->response); $publicKeyCredentialSource = $this->getAuthenticatorAttestationResponseValidator() ->check($publicKeyCredential->response, $publicKeyCredentialCreationOptions, $host); - $publicKeyCredentialDescriptor = $publicKeyCredential->getPublicKeyCredentialDescriptor(['usb']); + $publicKeyCredentialDescriptor = $publicKeyCredential->getPublicKeyCredentialDescriptor(); static::assertSame($credentialId, Base64UrlSafe::decode($publicKeyCredential->id)); static::assertSame($credentialId, $publicKeyCredentialDescriptor->id); static::assertSame( PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, $publicKeyCredentialDescriptor->type ); - static::assertSame(['usb'], $publicKeyCredentialDescriptor->transports); + static::assertSame([], $publicKeyCredentialDescriptor->transports); /** @var AuthenticatorData $authenticatorData */ $authenticatorData = $publicKeyCredential->response ->attestationObject @@ -62,13 +66,9 @@ public function anAttestationCanBeVerified( static::assertInstanceOf(AttestedCredentialData::class, $authenticatorData->attestedCredentialData); static::assertFalse($authenticatorData->hasExtensions()); if ($publicKeyCredentialCreationOptions->attestation === null || $publicKeyCredentialCreationOptions->attestation === PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE) { - static::assertSame( - '00000000-0000-0000-0000-000000000000', - $publicKeyCredentialSource->aaguid - ->__toString() - ); - static::assertSame('none', $publicKeyCredentialSource->attestationType); - static::assertInstanceOf(EmptyTrustPath::class, $publicKeyCredentialSource->trustPath); + static::assertSame($aaguid, $publicKeyCredentialSource->aaguid ->__toString()); + static::assertSame($attestationType, $publicKeyCredentialSource->attestationType); + static::assertInstanceOf($trustPath, $publicKeyCredentialSource->trustPath); } } @@ -80,7 +80,11 @@ public static function getAttestationCanBeVerifiedData(): iterable true ), 'webauthn.spomky-labs.com', hex2bin( '9604ea82824e98a4ada14b4462d0d73a8ec469130da91b19307459229f74a359' - ), 0, + ), + 0, + '00000000-0000-0000-0000-000000000000', + 'none', + EmptyTrustPath::class, ]; yield [ '{"rp":{"name":"Webauthn Demo","id":"webauthn.spomky-labs.com"},"pubKeyCredParams":[{"type":"public-key","alg":-8},{"type":"public-key","alg":-7},{"type":"public-key","alg":-46},{"type":"public-key","alg":-35},{"type":"public-key","alg":-36},{"type":"public-key","alg":-257},{"type":"public-key","alg":-258},{"type":"public-key","alg":-259},{"type":"public-key","alg":-37},{"type":"public-key","alg":-38},{"type":"public-key","alg":-39}],"challenge":"8zaIzbt6jRK-dgL-QbWeuo2jkIeRC4OB89z7ZbKbucY","attestation":"none","user":{"name":"11","id":"N2Q3ZTQ2ZTktMzI5Yy00YzE0LWI5MWYtMDYyMWYyOTIyYWQ4","displayName":"ee1"},"authenticatorSelection":{"requireResidentKey":false,"userVerification":"preferred"},"timeout":60000}', @@ -91,6 +95,9 @@ public static function getAttestationCanBeVerifiedData(): iterable 'webauthn.spomky-labs.com', hex2bin('9604ea82824e98a4ada14b4462d0d73a8ec469130da91b19307459229f74a359'), 390, + 'f8a011f3-8c0a-4d15-8006-17111f9edc7d', + 'basic', + CertificateTrustPath::class, ]; yield [ '{"rp":{"name":"Webauthn Demo","id":"webauthn.spomky-labs.com"},"pubKeyCredParams":[{"type":"public-key","alg":-8},{"type":"public-key","alg":-7},{"type":"public-key","alg":-46},{"type":"public-key","alg":-35},{"type":"public-key","alg":-36},{"type":"public-key","alg":-257},{"type":"public-key","alg":-258},{"type":"public-key","alg":-259},{"type":"public-key","alg":-37},{"type":"public-key","alg":-38},{"type":"public-key","alg":-39}],"challenge":"33Hr5HpypBGbGvb2KNbyXft2z12eKUXPP9nYubuQwe0","attestation":"none","user":{"name":"55","id":"ZDZhOGNhMTAtNDhhZC00YmY1LTkyYWItZmYzOTlmNDZjY2Ew","displayName":"555"},"authenticatorSelection":{"requireResidentKey":false,"userVerification":"preferred"},"timeout":60000}', @@ -101,6 +108,9 @@ public static function getAttestationCanBeVerifiedData(): iterable 'webauthn.spomky-labs.com', hex2bin('9604ea82824e98a4ada14b4462d0d73a8ec469130da91b19307459229f74a359'), 133, + 'fa2b99dc-9e39-4257-8f92-4a30d23c4118', + 'basic', + CertificateTrustPath::class, ]; } diff --git a/tests/library/Unit/AttestationStatement/TPMAttestationStatementSupportTest.php b/tests/library/Unit/AttestationStatement/TPMAttestationStatementSupportTest.php index 006d3529..dee3168c 100644 --- a/tests/library/Unit/AttestationStatement/TPMAttestationStatementSupportTest.php +++ b/tests/library/Unit/AttestationStatement/TPMAttestationStatementSupportTest.php @@ -6,36 +6,31 @@ use Lcobucci\Clock\SystemClock; use PHPUnit\Framework\Attributes\Test; -use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpClient\MockHttpClient; -use Webauthn\AttestationStatement\AttestationObjectLoader; use Webauthn\AttestationStatement\AttestationStatementSupportManager; use Webauthn\AttestationStatement\NoneAttestationStatementSupport; use Webauthn\AttestationStatement\TPMAttestationStatementSupport; -use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler; use Webauthn\AuthenticatorAttestationResponseValidator; -use Webauthn\MetadataService\CertificateChain\PhpCertificateChainValidator; +use Webauthn\Denormalizer\WebauthnSerializerFactory; use Webauthn\PublicKeyCredentialCreationOptions; use Webauthn\PublicKeyCredentialLoader; use Webauthn\PublicKeyCredentialRpEntity; use Webauthn\PublicKeyCredentialUserEntity; -use Webauthn\Tests\Unit\DummyMetadataStatementRepository; +use Webauthn\Tests\AbstractTestCase; /** * @internal */ -final class TPMAttestationStatementSupportTest extends TestCase +final class TPMAttestationStatementSupportTest extends AbstractTestCase { #[Test] public function theAttestationStatementIsAValidECC(): void { //Given - $metadataStatementRepository = new DummyMetadataStatementRepository(); $attnManager = AttestationStatementSupportManager::create(); $attnManager->add(TPMAttestationStatementSupport::create(SystemClock::fromSystemTimezone())); $attnManager->add(NoneAttestationStatementSupport::create()); - $attnLoader = AttestationObjectLoader::create($attnManager); - $pkLoader = PublicKeyCredentialLoader::create($attnLoader); + $serializer = (new WebauthnSerializerFactory($attnManager))->create(); + $pkLoader = PublicKeyCredentialLoader::create(null, $serializer); $data = '{"id":"BoLAd0jIDI0ztrH1N45XQ_0w_N5ndt3hpNixQi3J2No", "rawId":"BoLAd0jIDI0ztrH1N45XQ_0w_N5ndt3hpNixQi3J2No", "response":{"attestationObject":"o2NmbXRjdHBtZ2F0dFN0bXSmY2FsZzn__mNzaWdZAQAzaz3HmrpCUlkEV2iv-TF2_y0MD7MVc0rLyuD_Ah3X9vx3G21WgeI89PyyvEYw3yEUUdO7sn6YxubMfuePpuSawYKAeSbw3O4LkMDC2fqZmlLyTfoC8L1_8vExv6mWPN7H5U6E_K7IZ38H3mO736ie-mDyoXxalj4WkA9zjKXJM5t7GhHQAqtDaX4HmM47pFH25atgQnoLdB0MTzh6jgYjIiDrMSOqhrQYskiaX_LFfKTiWfviwMOYcMA8FkRPc05LKvPTxp-bx_ghHrd_gIAUA3MjfElVYCVfveMnI61ZwARnf0cTrFp7vfga85YeAXaLOu29JifjodW6DsjL_dnXY3ZlcmMyLjBjeDVjglkFtTCCBbEwggOZoAMCAQICEAaSyUKea0mgpfZbwvZ7byMwDQYJKoZIhvcNAQELBQAwQTE_MD0GA1UEAxM2RVVTLU5UQy1LRVlJRC0yM0Y0RTIyQUQzQkUzNzRBNDQ5NzcyOTU0QUEyODNBRUQ3NTI1NzJFMB4XDTIxMTEyNTIxMzA1NFoXDTI3MDYwMzE3NTE0N1owADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANwiGFmQdIOYto4qGegANWT-LdSr5T5_tj7E_aKtLSNP8bqc6eP11VvCi9ZFnbjiFxi1NdY2GAbUDb3zr1PnZpOcwvn1gh704PLtkZYFkwvFRvm5bIvtsuqYgn71MCup1GCTeJ3EcylidbVpmwX5s9XK5vyRsMpQ1TxPwxPq32toIBcQ3pgZyb9Ic_m1IfWE_hC_XlwZzqfFnFL7XszCGwJmziFjML9VeBrdv0dkrDWMv1sNI1PDDm_JQ8iZwZ83At3qsgnmwN4zudOMUPRMJBNeiVBj9GjW7tV9tSG2Oa_F_JUo0b1Gr_y08PSMhAckj6ZaR8_EBppoty9CbTm65nsCAwEAAaOCAeQwggHgMA4GA1UdDwEB_wQEAwIHgDAMBgNVHRMBAf8EAjAAMG0GA1UdIAEB_wRjMGEwXwYJKwYBBAGCNxUfMFIwUAYIKwYBBQUHAgIwRB5CAFQAQwBQAEEAIAAgAFQAcgB1AHMAdABlAGQAIAAgAFAAbABhAHQAZgBvAHIAbQAgACAASQBkAGUAbgB0AGkAdAB5MBAGA1UdJQQJMAcGBWeBBQgDMEoGA1UdEQEB_wRAMD6kPDA6MTgwDgYFZ4EFAgMMBWlkOjcyMBAGBWeBBQICDAdOUENUNzV4MBQGBWeBBQIBDAtpZDo0RTU0NDMwMDAfBgNVHSMEGDAWgBTTjd-fy_wwa14b1TQrBpJk2U7fpTAdBgNVHQ4EFgQUeq9wlX_04m4THgx-yMSO7QwViv8wgbIGCCsGAQUFBwEBBIGlMIGiMIGfBggrBgEFBQcwAoaBkmh0dHA6Ly9hemNzcHJvZGV1c2Fpa3B1Ymxpc2guYmxvYi5jb3JlLndpbmRvd3MubmV0L2V1cy1udGMta2V5aWQtMjNmNGUyMmFkM2JlMzc0YTQ0OTc3Mjk1NGFhMjgzYWVkNzUyNTcyZS8xMzY0YTJkMy1hZTU0LTQ3YjktODdmMy0zMjA1NDE5NDc0MGUuY2VyMA0GCSqGSIb3DQEBCwUAA4ICAQCiPgQwqysYPQpMiRDpxbsx24d1xVX_kiUwwcQJE3mSYvwe4tnaQSHjlfB3OkpDMjotxFl33oUMxxScjSrgp_1o6rdkiO6QvPMgsqDMX4w-dmWn00akwNbMasTxg39Ceqtocw4i-R9AlNwndpe3QUIt8xkQ5dhlcIF8lc1dXmgz4mkMAtOi3VgaNvHTsRF9pLbTczJss608X8b4gHqM4t7lfIcRB8DvSyfXc7T3k21-4_3jvAb2HRoCCAyv8_XXn1UwkWTrXMLUSiE1p5Sl8ba8I_86Hsemsc0aflwRZrrY2pC3aaA3QbbfAyskiaFPw-ZibY9p0_QVq1XhAKa-dDd70mWvTGKQdrqfZI_SC5zccvDAm6aefAfnYBY2fV92ZFriihA2ULcJaESz3X3JkiK4eO1k0T2uf9-rL4lUEADibwpnsZOBeNWBsztvXDmcZGR_MSoRIQygKMw2U7AproqBPDRDFwhS5yc9UHvD6dMZ3PLx4i_eo-BLr-QJ2HARoyK8KuV0xLEq3XyjWdfZDbAueUVgtic14wK9jiSbhycRT2WV3-QU8KPm5_QCt_eBPwY81a-q84jm2ue_ok8-LYrmWpvihqRhFhK9MLVS96QaHeeuDehYNDWsSIVCr9jB-lchueZ-kZqwyl_4pPMrM7wLXBOR-bV5_pAPv3u_RvQmhVkG7zCCBuswggTToAMCAQICEzMAAAQHrjuoB9SvW8wAAAAABAcwDQYJKoZIhvcNAQELBQAwgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDAeFw0yMTA2MDMxNzUxNDdaFw0yNzA2MDMxNzUxNDdaMEExPzA9BgNVBAMTNkVVUy1OVEMtS0VZSUQtMjNGNEUyMkFEM0JFMzc0QTQ0OTc3Mjk1NEFBMjgzQUVENzUyNTcyRTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMkPU9X8JhPBwDxmFm84D31b8xN5NQz0XR8Nji_-Z8v3WtC4lSdEwJUwqvZkj5OQ3wPA_6haONcCHzqTZhyz1aheOPhXmEeWFWjEiJFj07crEZb9wM4rM1fdcf3vCQNSSDlogC5AM-tITx31hm0YffIrzM3n70fNBBfvlw8t-yhZVOavj7l29gKsyvkR0IadruvLVWWVeH9rueHVrOwlU4wUJpjD41d4U87M3FgUGK2YacQxT0BPHzaOCTE9YhylG5fA_eCF7Q1SxAe347uIaS6I3GhAootzJy9XYeFp_uhc1Yp2hMh5wdeRkm15WKb7tE9T4vwHp0VCQEkUQn1ClN_s7PpfKNFp-DB9ez0Fh7tqag6AssrKE6LgOjfWDWUcgzgIiFLvv9Gx797IZj8LDazK1iGSqI2D8zmmxnGG47MevfY8q2udJW1G4nOcjw49x6XZHmnT3VpVKcTDbI9bEsyc2R9vngftF9FgnEVdyt-QRqE0UqEXJmjLhcxBMeyFZJd_bEAutSBpWugPk10IPFRkXppsuHMZFHJVP96IWwVmm6Q4mX018K996XDubAGblbhvPzJ9NFL_e7xM2ev3rAalz2CzSLYs48EXym7dqGTnP7F9DaF2O0IHT0GQ951wFVoGmA-IYsTMVsdlhVaImCuHgahu1W94H6BvtDkGGku7AgMBAAGjggGOMIIBijAOBgNVHQ8BAf8EBAMCAoQwGwYDVR0lBBQwEgYJKwYBBAGCNxUkBgVngQUIAzAWBgNVHSAEDzANMAsGCSsGAQQBgjcVHzASBgNVHRMBAf8ECDAGAQH_AgEAMB0GA1UdDgQWBBTTjd-fy_wwa14b1TQrBpJk2U7fpTAfBgNVHSMEGDAWgBR6jArOL0hiF-KU0a5VwVLscXSkVjBwBgNVHR8EaTBnMGWgY6Bhhl9odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUUE0lMjBSb290JTIwQ2VydGlmaWNhdGUlMjBBdXRob3JpdHklMjAyMDE0LmNybDB9BggrBgEFBQcBAQRxMG8wbQYIKwYBBQUHMAKGYWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVFBNJTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAxNC5jcnQwDQYJKoZIhvcNAQELBQADggIBAIQJqhFB71eZzZMq0w866QXDKlHcGyIa_IkTK4p5ejIdIA7FJ8neeVToAKUt9ULEb1Od2ir1y5Qx5Zp_edf4F8aikn-yw61hNB3FQ4iSV49eqEMe2Fx6OMBmHRWGtUjAlf5g_N2Qc6rHela2d69nQbpSF3Nq7AESguXxnoqZ-4CGUW0jC_b93sTd5fESHs_iwFX-zWKCwCXerqCuI3PqYWOlbCnftYhsI1CD638wJxw4YFXdSmOrF8dDnd6tlH_0qCZrBX-k4N-8QgK1-BDYIxmvUBnpLFDDitB2dP6YIglY0VcjkPd3BDmodHknG4GQeAvJKHpqF91Y3K1rOWvn4JqzHFvL3JgXgL7LbC_h9EF50HeHayPCToTS8Pmg_4dfUaCwNlxPvu9GvjrDKDNNEV5T73iWMV_GQbVsx6JULAljCthYLo-55mONDcr1x7kakXlQT-yIdIQ57Ix8eHz_qkJkvWxbw8vOgrXhkLK0jGAvW_YSkTV7G9_TYDJ--8IjPPHC1bexKq72-L7KetwH6LbWHGeYkJnaZ1zqeN4USxyJn8K4uhwnjSeK2sZ942zn5EnZnjd85yfdkPLcQY8xtYiWNjc_PprTrjhLyMO71VdMkTDiTTtDha37qywNISPV7vBv8YDiDjX8ElsWbTHTC0XgBp0h-RkjaRKI5C4eTUebZ3B1YkFyZWFYdgAjAAsABAByACCd_8vzbDg65pn7mGjcbcuJ1xU4hL4oA5IsEkFYv60irgAQABAAAwAQACCweOEk52r8mnJ6y9bsGcM3V4dL1LWt8I67Jjx5mcrFuAAgjwd_jaCEEOAJLV97kX3VgbxzopPYMC4NqEFjD0m55PpoY2VydEluZm9Yof9UQ0eAFwAiAAvgBLotxyAAbygBG4efe84V0SVYnO6xLrYaC1oyLgTt3QAUjcjAdORvuzxCfLBU7KNxPFSPE84AAAAUHn9jxccO2yRJARoXARNN0IPNWxnEACIACxfcHNQuRgb_05OKyBrS_1kY5IYxOl67gTlqkHd4g6slACIAC7tcXSHNTw8ANLeZd3PKooKsgrMIlGD47aunn05BcquwaGF1dGhEYXRhWKRqubvw35oW-R27M7uxMvr50Xx4LEgmxuxw7O5Y2X71KkUAAAAACJhwWMrcS4G24TDeUNy-lgAgBoLAd0jIDI0ztrH1N45XQ_0w_N5ndt3hpNixQi3J2NqlAQIDJiABIVggsHjhJOdq_JpyesvW7BnDN1eHS9S1rfCOuyY8eZnKxbgiWCCPB3-NoIQQ4AktX3uRfdWBvHOik9gwLg2oQWMPSbnk-g", "clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiRTJZZWJNbUc5OTkyWGlhbHBGTDFsa1BwdE9JQlBlS3NwaE5rdDFKY2JLayIsIm9yaWdpbiI6Imh0dHBzOi8vd2ViYXV0aG4uZmlyc3R5ZWFyLmlkLmF1IiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUgaHR0cHM6Ly9nb28uZ2wveWFiUGV4In0"}, "type":"public-key", "extensions":{"appid":null, "cred_blob":null, "cred_props":{"rk":true}}}'; $options = PublicKeyCredentialCreationOptions::create( PublicKeyCredentialRpEntity::create('https://webauthn.firstyear.id.au'), @@ -43,19 +38,14 @@ public function theAttestationStatementIsAValidECC(): void base64_decode('E2YebMmG9992XialpFL1lkPptOIBPeKsphNkt1JcbKk', true), attestation: PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT ); - $validator = AuthenticatorAttestationResponseValidator::create( - $attnManager, - null, - null, - ExtensionOutputCheckerHandler::create(), - )->enableMetadataStatementSupport( - $metadataStatementRepository, - $metadataStatementRepository, - PhpCertificateChainValidator::create(new MockHttpClient(), SystemClock::fromSystemTimezone()) - ); + $ceremonyStepManager = $this->getCeremonyStepManagerFactory() + ->creationCeremony(); + + $validator = AuthenticatorAttestationResponseValidator::create(ceremonyStepManager: $ceremonyStepManager); //When $response = $pkLoader->load($data); $source = $validator->check($response->response, $options, 'webauthn.firstyear.id.au'); + //Then static::assertSame('08987058-cadc-4b81-b6e1-30de50dcbe96', $source->aaguid->toRfc4122()); } diff --git a/tests/library/Unit/PublicKeyCredentialSourceTest.php b/tests/library/Unit/PublicKeyCredentialSourceTest.php index 4a990e00..d8955aca 100644 --- a/tests/library/Unit/PublicKeyCredentialSourceTest.php +++ b/tests/library/Unit/PublicKeyCredentialSourceTest.php @@ -39,11 +39,15 @@ public function objectSerialization(): void Uuid::fromString('02ffd35d-7f0c-46b5-9eae-851ee4807b25'), 'publicKey', 'userHandle', - 123_456_789 + 123_456_789, + null, + true, + true, + false ); static::assertSame( - '{"publicKeyCredentialId":"cHVibGljS2V5Q3JlZGVudGlhbElk","type":"type","transports":["transport1","transport2"],"attestationType":"attestationType","trustPath":{"type":"Webauthn\\\\TrustPath\\\\EmptyTrustPath"},"aaguid":"02ffd35d-7f0c-46b5-9eae-851ee4807b25","credentialPublicKey":"cHVibGljS2V5","userHandle":"dXNlckhhbmRsZQ","counter":123456789,"otherUI":null}', + '{"publicKeyCredentialId":"cHVibGljS2V5Q3JlZGVudGlhbElk","type":"type","transports":["transport1","transport2"],"attestationType":"attestationType","trustPath":{"type":"Webauthn\\\\TrustPath\\\\EmptyTrustPath"},"aaguid":"02ffd35d-7f0c-46b5-9eae-851ee4807b25","credentialPublicKey":"cHVibGljS2V5","userHandle":"dXNlckhhbmRsZQ","counter":123456789,"otherUI":null,"backupEligible":true,"backupStatus":true,"uvInitialized":false}', json_encode($source, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ); } diff --git a/tests/metadataStatements/solo.json b/tests/metadataStatements/solo.json new file mode 100644 index 00000000..9707eb6b --- /dev/null +++ b/tests/metadataStatements/solo.json @@ -0,0 +1,50 @@ +{ + "description": "Solo Secp256R1 FIDO2 CTAP2 Authenticator", + "aaguid": "8876631b-d4a0-427f-5773-0ec71c9e0279", + "alternativeDescriptions": { + }, + "protocolFamily": "fido2", + "authenticatorVersion": 2, + "upv": [ + { + "major": 1, + "minor": 0 + } + ], + "schema": 3, + "authenticationAlgorithms": [ + "1" + ], + "publicKeyAlgAndEncodings": [ + "260" + ], + "attestationTypes": [ + "15879", + "15880" + ], + "userVerificationDetails": [ + [ + { + "userVerificationMethod": "presence_internal" + } + ] + ], + "keyProtection": [ + "hardware" + ], + "matcherProtection": [ + "4" + ], + "cryptoStrength": 128, + "attachmentHint": [ + "2" + ], + "isSecondFactorOnly": false, + "tcDisplay": [ + "0" + ], + "attestationRootCertificates": [ + "MIIB9DCCAZoCCQDER2OSj/S+jDAKBggqhkjOPQQDAjCBgDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1hcnlsYW5kMRIwEAYDVQQKDAlTb2xvIEtleXMxEDAOBgNVBAsMB1Jvb3QgQ0ExFTATBgNVBAMMDHNvbG9rZXlzLmNvbTEhMB8GCSqGSIb3DQEJARYSaGVsbG9Ac29sb2tleXMuY29tMCAXDTE4MTExMTEyNTE0MloYDzIwNjgxMDI5MTI1MTQyWjCBgDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1hcnlsYW5kMRIwEAYDVQQKDAlTb2xvIEtleXMxEDAOBgNVBAsMB1Jvb3QgQ0ExFTATBgNVBAMMDHNvbG9rZXlzLmNvbTEhMB8GCSqGSIb3DQEJARYSaGVsbG9Ac29sb2tleXMuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWHAN0CCJVZdMs0oktZ5m93uxmB1iyq8ELRLtqVFLSOiHQEab56qRTB/QzrpGAY++Y2mw+vRuQMNhBiU0KzwjBjAKBggqhkjOPQQDAgNIADBFAiEAz9SlrAXIlEu87vra54rICPs+4b0qhp3PdzcTg7rvnP0CIGjxzlteQQx+jQGd7rwSZuE5RWUPVygYhUstQO9zNUOs" + ], + "icon": "data:image/png;base64,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" +} diff --git a/tests/metadataStatements/windows-hello.json b/tests/metadataStatements/windows-hello.json new file mode 100644 index 00000000..3859f91c --- /dev/null +++ b/tests/metadataStatements/windows-hello.json @@ -0,0 +1,84 @@ +{ + "legalHeader": "https://fidoalliance.org/metadata/metadata-statement-legal-header/", + "aaguid": "08987058-cadc-4b81-b6e1-30de50dcbe96", + "description": "Windows Hello Hardware Authenticator", + "authenticatorVersion": 1, + "protocolFamily": "fido2", + "schema": 3, + "upv": [ + { + "major": 1, + "minor": 0 + } + ], + "authenticationAlgorithms": [ + "rsassa_pkcsv15_sha256_raw" + ], + "publicKeyAlgAndEncodings": [ + "cose" + ], + "attestationTypes": [ + "attca" + ], + "userVerificationDetails": [ + [ + { + "userVerificationMethod": "eyeprint_internal" + } + ], + [ + { + "userVerificationMethod": "passcode_internal" + } + ], + [ + { + "userVerificationMethod": "fingerprint_internal" + } + ], + [ + { + "userVerificationMethod": "faceprint_internal" + } + ] + ], + "keyProtection": [ + "hardware" + ], + "isKeyRestricted": false, + "matcherProtection": [ + "software" + ], + "attachmentHint": [ + "internal" + ], + "tcDisplay": [], + "attestationRootCertificates": [ + "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" + ], + "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACkUlEQVR42uyai3GDMAyGQyegGzACnaCMkBHoBhkhnSAj0A2SDaAT0E6QbEA3cOXW6XEpBtnImMv9utOllxjF/qKHLTdRSm0gdnkAAgACIAACIAACIAACIAgAARAAARAAARAAARBEAFCSJINKkpLuSTtSZbQz76W25zhKkpFWPbtaz6Q75vPuoluuPmqxlZK2yi76s9RznjlpN2K7CrFWaUAHNS0HT0Atw3YpDSjxbdoPuaziG3uk579cvIdeWsbQD7L7NAYoWpKmLy8chueO5reB7KKKrQnQJdDYn9AJZHc5QBT7enINY2hjxrqItsvJWSdxFxKuYlOlWJmE6zPPcsJuN7WFiF7me5DOAws4OyZyG6TOsr/KQziDaJm/mcy2V1V0+T0JeXxqqlrWC9mGGy3O6wwFaI0SdR+EMg9AEAACIAByqViZb+/prgFdN6qb306j3lTWs0BJ76Qjw0ktO+3ad60PQhMrfM9YwqK7lUPe4j+/OR40cDaqJeJ+xo80JsWih1WTBAcb8ysKrb+TfowQKy3v55wbBkk49FJbQusqr4snadL9hEtXC3nO1G1HG6UfxIj5oDnJlHPOVVAerWGmvYQxwc70hiTh7Bidy3/3ZFE6isxf8epNhUCl4n5ftYqWKzMP3IIquaFnquXO0sZ1yn/RWq69SuK6GdPXORfSz4HPnk1bNXO0+UZze5HqKIodNYwnHVVcOUivNcStxj4CGFYhWAWgXgmuF4JzdMhn6wDUm1DpmFyVY7IvQqeTRdod2v2F8lNn/gcpW+rUsOi9mAmFwlSo3Pw9JQ3p+8bhgnAMkPM613BxOBQqc2FEB4SmPQSAAAiAAAiAAAiAAAiAIAAEQAAEQAAEQPco3wIMADOXgFhOTghuAAAAAElFTkSuQmCC", + "authenticatorGetInfo": { + "versions": [ + "FIDO_2_0" + ], + "aaguid": "08987058cadc4b81b6e130de50dcbe96", + "options": { + "plat": true, + "rk": true, + "up": true + }, + "maxCredentialCountInList": 100, + "maxCredentialIdLength": 32, + "transports": [ + "internal" + ], + "algorithms": [ + { + "type": "public-key", + "alg": -257 + } + ], + "minPINLength": 4, + "firmwareVersion": 19042 + } +} diff --git a/tests/symfony/config/config.yml b/tests/symfony/config/config.yml index 4869a14c..df44027b 100644 --- a/tests/symfony/config/config.yml +++ b/tests/symfony/config/config.yml @@ -85,24 +85,6 @@ services: arguments: - '@Webauthn\MetadataService\Service\ChainedMetadataServices' - # Register nyholm/psr7 services for autowiring with PSR-17 (HTTP factories) - Psr\Http\Message\RequestFactoryInterface: '@nyholm.psr7.psr17_factory' - Psr\Http\Message\ResponseFactoryInterface: '@nyholm.psr7.psr17_factory' - Psr\Http\Message\ServerRequestFactoryInterface: '@nyholm.psr7.psr17_factory' - Psr\Http\Message\StreamFactoryInterface: '@nyholm.psr7.psr17_factory' - Psr\Http\Message\UploadedFileFactoryInterface: '@nyholm.psr7.psr17_factory' - Psr\Http\Message\UriFactoryInterface: '@nyholm.psr7.psr17_factory' - - # Register nyholm/psr7 services for autowiring with HTTPlug factories - Http\Message\MessageFactory: '@nyholm.psr7.psr17_factory' - Http\Message\RequestFactory: '@nyholm.psr7.psr17_factory' - Http\Message\ResponseFactory: '@nyholm.psr7.psr17_factory' - Http\Message\StreamFactory: '@nyholm.psr7.psr17_factory' - Http\Message\UriFactory: '@nyholm.psr7.psr17_factory' - - nyholm.psr7.psr17_factory: - class: Nyholm\Psr7\Factory\Psr17Factory - doctrine: dbal: @@ -157,6 +139,8 @@ webauthn: #success_handler: #failure_handler: #option_handler: + secured_rp_ids: + - 'localhost' creation_profiles: default: rp: diff --git a/tests/symfony/functional/CompilerPass/EnforcedSafetyNetApiKeyVerificationCompilerPassTest.php b/tests/symfony/functional/CompilerPass/EnforcedSafetyNetApiKeyVerificationCompilerPassTest.php index 11a64fcb..018bf23f 100644 --- a/tests/symfony/functional/CompilerPass/EnforcedSafetyNetApiKeyVerificationCompilerPassTest.php +++ b/tests/symfony/functional/CompilerPass/EnforcedSafetyNetApiKeyVerificationCompilerPassTest.php @@ -28,8 +28,6 @@ public function androidSafetyNetApiVerificationIsEnabledWhenAllServicesAndParame $this->container->setAlias('webauthn.android_safetynet.http_client', 'http_client'); $this->setParameter('webauthn.android_safetynet.api_key', 'api_key'); - $this->setDefinition('request_factory', new Definition()); - $this->container->setAlias('webauthn.android_safetynet.request_factory', 'request_factory'); //When $this->compile(); @@ -38,11 +36,7 @@ public function androidSafetyNetApiVerificationIsEnabledWhenAllServicesAndParame $this->assertContainerBuilderHasServiceDefinitionWithMethodCall( AndroidSafetyNetAttestationStatementSupport::class, 'enableApiVerification', - [ - new Reference('webauthn.android_safetynet.http_client'), - 'api_key', - new Reference('webauthn.android_safetynet.request_factory'), - ] + [new Reference('webauthn.android_safetynet.http_client'), 'api_key', null] ); }