From 14bb7739776af79f51fe67cc06eb3b44346f74cd Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Thu, 2 Nov 2023 14:25:48 -0700 Subject: [PATCH] Have the library handle challenge management (#35) The examples so far have all used sessions to manage the active challenges, but not all applications are stateful in this way - namely, most APIs will not be session-based. Instead, this creates a new `ChallengeManagerInterface` that handles this for applications. For now there's a single implementation that's still session-based, though (via #30 which I'm reworking) other implementations will be provided (e.g. a cache pool). The majority of the change here is updating examples and adding tests. Note that this would be a BC break but since the library is still pre-1.0 it's not a concern for practical purposes. --- README.md | 58 +++++++++++++-------- composer-require-checker.json | 6 +++ examples/functions.php | 7 +++ examples/readmeLoginStep1.php | 4 +- examples/readmeLoginStep3.php | 4 +- examples/readmeRegisterStep1.php | 6 +-- examples/readmeRegisterStep3.php | 4 +- phpunit.xml | 2 +- src/ChallengeManagerInterface.php | 33 ++++++++++++ src/CreateResponse.php | 10 +++- src/GetResponse.php | 15 ++++-- src/Responses/AssertionInterface.php | 3 +- src/Responses/AttestationInterface.php | 3 +- src/SessionChallengeManager.php | 43 ++++++++++++++++ tests/ChallengeManagerTestTrait.php | 70 ++++++++++++++++++++++++++ tests/CreateResponseTest.php | 45 +++++++++++------ tests/EndToEndTest.php | 12 ++++- tests/FixedChallengeManager.php | 31 ++++++++++++ tests/GetResponseTest.php | 60 +++++++++++++++------- tests/SessionChallengeManagerTest.php | 26 ++++++++++ tests/bootstrap.php | 4 ++ 21 files changed, 371 insertions(+), 75 deletions(-) create mode 100644 composer-require-checker.json create mode 100644 src/ChallengeManagerInterface.php create mode 100644 src/SessionChallengeManager.php create mode 100644 tests/ChallengeManagerTestTrait.php create mode 100644 tests/FixedChallengeManager.php create mode 100644 tests/SessionChallengeManagerTest.php create mode 100644 tests/bootstrap.php diff --git a/README.md b/README.md index c1b7180..79dcad4 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,15 @@ The protocol is always required; the port must only be present if using a non-st $rp = new \Firehed\WebAuthn\RelyingParty('https://www.example.com'); ``` +Also create a `ChallengeManagerInterface`. +This will store and validate the one-time use challenges that are central to the WebAuthn protocol. +See the [Challenge Management](#challenge-management) section below for more information. + +```php +session_start(); +$challengeManager = new \Firehed\WebAuthn\SessionChallengeManager(); +``` + > [!IMPORTANT] > WebAuthn will only work in a "secure context". > This means that the domain MUST run over `https`, with a sole exception for `localhost`. @@ -49,20 +58,13 @@ $rp = new \Firehed\WebAuthn\RelyingParty('https://www.example.com'); This step takes place either when a user is first registering, or later on to supplement or replace their password. 1) Create an endpoint that will return a new, random Challenge. -This may be stored in a user's session or equivalent; it needs to be kept statefully server-side. Send it to the user as base64. ```php createChallenge(); // Send to user header('Content-type: application/json'); @@ -154,11 +156,9 @@ $data = json_decode($json, true); $parser = new ResponseParser(); $createResponse = $parser->parseCreateResponse($data); -$rp = $valueFromSetup; // e.g. $psr11Container->get(RelyingParty::class); -$challenge = $_SESSION['webauthn_challenge']; - try { - $credential = $createResponse->verify($challenge, $rp); + // $challengeManager and $rp are the values from the setup step + $credential = $createResponse->verify($challengeManager, $rp); } catch (Throwable) { // Verification failed. Send an error to the user? header('HTTP/1.1 403 Unauthorized'); @@ -205,10 +205,7 @@ This assumes the same schema from the previous Registration example. ```php createChallenge(); // Send to user header('Content-type: application/json'); @@ -310,13 +306,11 @@ $data = json_decode($json, true); $parser = new ResponseParser(); $getResponse = $parser->parseGetResponse($data); -$rp = $valueFromSetup; // e.g. $psr11Container->get(RelyingParty::class); -$challenge = $_SESSION['webauthn_challenge']; - $credentialContainer = getCredentialsForUserId($pdo, $_SESSION['authenticating_user_id']); try { - $updatedCredential = $getResponse->verify($challenge, $rp, $credentialContainer); + // $challengeManager and $rp are the values from the setup step + $updatedCredential = $getResponse->verify($challengeManager, $rp, $credentialContainer); } catch (Throwable) { // Verification failed. Send an error to the user? header('HTTP/1.1 403 Unauthorized'); @@ -440,6 +434,26 @@ Those wire formats are covered by semantic versioning and guaranteed to not have Similarly, for data storage, the output of `Codecs\Credential::encode()` are also covered. +### Challenge management + +Challenges are a [cryptographic nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce) that ensure a login attempt works only once. +Their single-use nature is critical to the security of the WebAuthn protocol. + +Your application SHOULD use one of the library-provided `ChallengeManagerInterface` implementations to ensure the correct behavior. + +| Implementation | Usage | +| --- | --- | +| `SessionChallengeManager` | Manages challenges through native PHP [Sessions](https://www.php.net/manual/en/intro.session.php). | + +If one of the provided options is not suitable, you MAY implement the interface yourself or manage challenges manually. +In the event you find this necessary, you SHOULD open an Issue and/or Pull Request for the library that indicates the shortcoming. + +> [!WARNING] +> You MUST validate that the challenge was generated by your server recently and has not already been used. +> **Failing to do so will compromise the security of the protocol!** +> Implementations MUST NOT trust a client-provided value. +> The built-in `ChallengeManagerInterface` implementations will handle this for you. + Challenges generated by your server SHOULD expire after a short amount of time. You MAY use the `ExpiringChallenge` class for convenience (e.g. `$challenge = ExpiringChallenge::withLifetime(60);`), which will throw an exception if the specified expiration window has been exceeded. It is RECOMMENDED that your javascript code uses the `timeout` setting (denoted in milliseconds) and matches the server-side challenge expiration, give or take a few seconds. diff --git a/composer-require-checker.json b/composer-require-checker.json new file mode 100644 index 0000000..81d083c --- /dev/null +++ b/composer-require-checker.json @@ -0,0 +1,6 @@ +{ + "symbol-whitelist": [ + "PHP_SESSION_ACTIVE", + "session_status" + ] +} diff --git a/examples/functions.php b/examples/functions.php index 3a8e3f9..366f5f8 100644 --- a/examples/functions.php +++ b/examples/functions.php @@ -3,9 +3,11 @@ declare(strict_types=1); use Firehed\WebAuthn\{ + ChallengeManagerInterface, Codecs, CredentialContainer, RelyingParty, + SessionChallengeManager, }; /** @@ -28,6 +30,11 @@ function createUser(PDO $pdo, string $username): array return $response; } +function getChallengeManager(): ChallengeManagerInterface +{ + return new SessionChallengeManager(); +} + function getCredentialsForUserId(PDO $pdo, string $userId): CredentialContainer { $stmt = $pdo->prepare('SELECT * FROM user_credentials WHERE user_id = ?'); diff --git a/examples/readmeLoginStep1.php b/examples/readmeLoginStep1.php index 8a36c26..14dfa5d 100644 --- a/examples/readmeLoginStep1.php +++ b/examples/readmeLoginStep1.php @@ -19,8 +19,8 @@ $credentialContainer = getCredentialsForUserId($pdo, $user['id']); -$challenge = ExpiringChallenge::withLifetime(120); -$_SESSION['webauthn_challenge'] = $challenge; +$challengeManager = getChallengeManager(); +$challenge = $challengeManager->createChallenge(); // Send to user header('Content-type: application/json'); diff --git a/examples/readmeLoginStep3.php b/examples/readmeLoginStep3.php index 6dee223..0ad7b75 100644 --- a/examples/readmeLoginStep3.php +++ b/examples/readmeLoginStep3.php @@ -20,12 +20,12 @@ $getResponse = $parser->parseGetResponse($data); $rp = getRelyingParty(); -$challenge = $_SESSION['webauthn_challenge']; $credentialContainer = getCredentialsForUserId($pdo, $_SESSION['authenticating_user_id']); +$challengeManager = getChallengeManager(); try { - $updatedCredential = $getResponse->verify($challenge, $rp, $credentialContainer); + $updatedCredential = $getResponse->verify($challengeManager, $rp, $credentialContainer); } catch (Throwable) { // Verification failed. Send an error to the user? header('HTTP/1.1 403 Unauthorized'); diff --git a/examples/readmeRegisterStep1.php b/examples/readmeRegisterStep1.php index 14761f4..94fcf21 100644 --- a/examples/readmeRegisterStep1.php +++ b/examples/readmeRegisterStep1.php @@ -13,10 +13,8 @@ $_SESSION['user_id'] = $user['id']; // Generate challenge -$challenge = ExpiringChallenge::withLifetime(120); - -// Store server-side; adjust to your app's needs -$_SESSION['webauthn_challenge'] = $challenge; +$challengeManager = getChallengeManager(); +$challenge = $challengeManager->createChallenge(); // Send to user header('Content-type: application/json'); diff --git a/examples/readmeRegisterStep3.php b/examples/readmeRegisterStep3.php index b33f851..b60aa04 100644 --- a/examples/readmeRegisterStep3.php +++ b/examples/readmeRegisterStep3.php @@ -18,10 +18,10 @@ $createResponse = $parser->parseCreateResponse($data); $rp = getRelyingParty(); -$challenge = $_SESSION['webauthn_challenge']; +$challengeManager = getChallengeManager(); try { - $credential = $createResponse->verify($challenge, $rp); + $credential = $createResponse->verify($challengeManager, $rp); } catch (Throwable) { // Verification failed. Send an error to the user? header('HTTP/1.1 403 Unauthorized'); diff --git a/phpunit.xml b/phpunit.xml index 3dff4d4..891f16c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,7 +1,7 @@ useFromClientDataJSON($cdjChallenge); + if ($challenge === null) { + $this->fail('7.1.8', 'C.challenge'); + } + $b64u = Codecs\Base64Url::encode($challenge->getBinary()->unwrap()); - if (!hash_equals($b64u, $C['challenge'])) { + if (!hash_equals($b64u, $cdjChallenge)) { $this->fail('7.1.8', 'C.challenge'); } diff --git a/src/GetResponse.php b/src/GetResponse.php index bf4b20c..635f2d1 100644 --- a/src/GetResponse.php +++ b/src/GetResponse.php @@ -15,12 +15,15 @@ */ class GetResponse implements Responses\AssertionInterface { + private AuthenticatorData $authData; + public function __construct( private BinaryString $credentialId, private BinaryString $rawAuthenticatorData, private BinaryString $clientDataJson, private BinaryString $signature, ) { + $this->authData = AuthenticatorData::parse($this->rawAuthenticatorData); } /** @@ -36,7 +39,7 @@ public function getUsedCredentialId(): BinaryString * @link https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion */ public function verify( - ChallengeInterface $challenge, + ChallengeManagerInterface $challenge, RelyingParty $rp, CredentialContainer | CredentialInterface $credential, UserVerificationRequirement $uv = UserVerificationRequirement::Preferred, @@ -71,7 +74,7 @@ public function verify( // 7.2.8 $cData = $this->clientDataJson->unwrap(); - $authData = AuthenticatorData::parse($this->rawAuthenticatorData); + $authData = $this->authData; $sig = $this->signature->unwrap(); // 7.2.9 @@ -89,8 +92,14 @@ public function verify( } // 7.2.12 + $cdjChallenge = $C['challenge']; + $challenge = $challenge->useFromClientDataJSON($cdjChallenge); + if ($challenge === null) { + $this->fail('7.2.12', 'C.challenge'); + } + $b64u = Codecs\Base64Url::encode($challenge->getBinary()->unwrap()); - if (!hash_equals($b64u, $C['challenge'])) { + if (!hash_equals($b64u, $cdjChallenge)) { $this->fail('7.2.12', 'C.challenge'); } diff --git a/src/Responses/AssertionInterface.php b/src/Responses/AssertionInterface.php index 358c29d..beb87d8 100644 --- a/src/Responses/AssertionInterface.php +++ b/src/Responses/AssertionInterface.php @@ -7,6 +7,7 @@ use Firehed\WebAuthn\{ BinaryString, ChallengeInterface, + ChallengeManagerInterface, CredentialContainer, CredentialInterface, RelyingParty, @@ -30,7 +31,7 @@ public function getUsedCredentialId(): BinaryString; * @api */ public function verify( - ChallengeInterface $challenge, + ChallengeManagerInterface $challenge, RelyingParty $rp, CredentialContainer | CredentialInterface $credential, UserVerificationRequirement $uv = UserVerificationRequirement::Preferred, diff --git a/src/Responses/AttestationInterface.php b/src/Responses/AttestationInterface.php index dbb9460..a3ee042 100644 --- a/src/Responses/AttestationInterface.php +++ b/src/Responses/AttestationInterface.php @@ -6,6 +6,7 @@ use Firehed\WebAuthn\{ ChallengeInterface, + ChallengeManagerInterface, CredentialInterface, RelyingParty, UserVerificationRequirement, @@ -20,7 +21,7 @@ interface AttestationInterface { public function verify( - ChallengeInterface $challenge, + ChallengeManagerInterface $challenge, RelyingParty $rp, UserVerificationRequirement $uv = UserVerificationRequirement::Preferred, ): CredentialInterface; diff --git a/src/SessionChallengeManager.php b/src/SessionChallengeManager.php new file mode 100644 index 0000000..89cff4b --- /dev/null +++ b/src/SessionChallengeManager.php @@ -0,0 +1,43 @@ +getChallengeManager(); + $c1 = $cm->createChallenge(); + $c2 = $cm->createChallenge(); + self::assertNotSame($c1, $c2); + self::assertNotSame($c1->getBase64(), $c2->getBase64()); + } + + public function testMostRecentChallengeCanBeRetrieved(): void + { + $cm = $this->getChallengeManager(); + $c = $cm->createChallenge(); + $cdjValue = Codecs\Base64Url::encode($c->getBinary()->unwrap()); + + $found = $cm->useFromClientDataJSON($cdjValue); + self::assertInstanceOf(ChallengeInterface::class, $found); + self::assertSame($c->getBase64(), $found->getBase64()); + } + + public function testMostRecentChallengeCanBeRetrievedOnlyOnce(): void + { + $cm = $this->getChallengeManager(); + $c = $cm->createChallenge(); + $cdjValue = Codecs\Base64Url::encode($c->getBinary()->unwrap()); + + $found = $cm->useFromClientDataJSON($cdjValue); + $again = $cm->useFromClientDataJSON($cdjValue); + + self::assertInstanceOf(ChallengeInterface::class, $found); + self::assertNull($again); + } + + public function testNoChallengeIsReturnedIfManagerIsEmpty(): void + { + $cm = $this->getChallengeManager(); + + $c = Challenge::random(); + $cdjValue = Codecs\Base64Url::encode($c->getBinary()->unwrap()); + + $found = $cm->useFromClientDataJSON($cdjValue); + + self::assertNull($found); + } + + public function testRetrievalDoesNotCreateChallengeFromUserData(): void + { + $cm = $this->getChallengeManager(); + $c = $cm->createChallenge(); + + $userChallenge = Challenge::random(); + $cdjValue = Codecs\Base64Url::encode($userChallenge->getBinary()->unwrap()); + + $retrieved = $cm->useFromClientDataJSON($cdjValue); + // The implmentation may return the previously-stored value or null, + // but MUST NOT attempt to reconstruct the challenge from the user- + // provided value. + self::assertNotSame($userChallenge->getBase64(), $retrieved?->getBase64()); + } +} diff --git a/tests/CreateResponseTest.php b/tests/CreateResponseTest.php index 19e70a8..c727671 100644 --- a/tests/CreateResponseTest.php +++ b/tests/CreateResponseTest.php @@ -11,7 +11,7 @@ class CreateResponseTest extends \PHPUnit\Framework\TestCase { // These hold the values which would be kept server-side. private RelyingParty $rp; - private Challenge $challenge; + private ChallengeManagerInterface $cm; // These hold the _default_ values from a sample parsed response. private BinaryString $id; @@ -22,13 +22,6 @@ public function setUp(): void { $this->rp = new RelyingParty('http://localhost:8888'); - $this->challenge = new Challenge(BinaryString::fromBytes([ - 40, 96, 197, 186, 42, 202, 51, 237, - 134, 178, 3, 251, 22, 204, 231, 157, - 47, 77, 43, 123, 3, 245, 57, 77, - 20, 74, 166, 166, 240, 37, 141, 188, - ])); - $this->id = BinaryString::fromBytes([ 236, 58, 219, 22, 123, 115, 98, 124, 11, 0, 207, 244, 106, 41, 249, 202, @@ -171,6 +164,13 @@ public function setUp(): void 97, 108, 104, 111, 115, 116, 58, 56, 56, 56, 56, 34, 125, ]); + + $this->cm = new FixedChallengeManager(new Challenge(BinaryString::fromBytes([ + 40, 96, 197, 186, 42, 202, 51, 237, + 134, 178, 3, 251, 22, 204, 231, 157, + 47, 77, 43, 123, 3, 245, 57, 77, + 20, 74, 166, 166, 240, 37, 141, 188, + ]))); } // 7.1.7 @@ -188,7 +188,22 @@ public function testCDJTypeMismatchIsError(): void ); $this->expectRegistrationError('7.1.7'); - $response->verify($this->challenge, $this->rp); + $response->verify($this->cm, $this->rp); + } + + public function testUsedChallengeIsError(): void + { + $response = new CreateResponse( + id: $this->id, + ao: $this->attestationObject, + clientDataJson: $this->clientDataJson, + ); + + $cred = $response->verify($this->cm, $this->rp); + + // Simulate replay. ChallengeManager no longer recognizes this one. + $this->expectRegistrationError('7.1.8'); + $response->verify($this->cm, $this->rp); } // 7.1.8 @@ -206,7 +221,7 @@ public function testCDJChallengeMismatchIsError(): void ); $this->expectRegistrationError('7.1.8'); - $response->verify($this->challenge, $this->rp); + $response->verify($this->cm, $this->rp); } // 7.1.9 @@ -224,7 +239,7 @@ public function testCDJOriginMismatchIsError(): void ); $this->expectRegistrationError('7.1.0'); - $response->verify($this->challenge, $this->rp); + $response->verify($this->cm, $this->rp); } // 7.1.13 @@ -238,7 +253,7 @@ public function testRelyingPartyIdMismatchIsError(): void ); $this->expectRegistrationError('7.1.13'); - $response->verify($this->challenge, $rp); + $response->verify($this->cm, $rp); } // 7.1.14 @@ -258,7 +273,7 @@ public function testUserVerifiedNotPresentWhenRequiredIsError(): void ); $this->expectRegistrationError('7.1.15'); - $response->verify($this->challenge, $this->rp, UserVerificationRequirement::Required); + $response->verify($this->cm, $this->rp, UserVerificationRequirement::Required); } // 7.1.16 @@ -298,7 +313,7 @@ public function testFormatSpecificVerificationOccurs(): void ao: $ao, clientDataJson: $this->clientDataJson, ); - $response->verify($this->challenge, $this->rp); + $response->verify($this->cm, $this->rp); } public function testSuccess(): void @@ -309,7 +324,7 @@ public function testSuccess(): void clientDataJson: $this->clientDataJson, ); - $cred = $response->verify($this->challenge, $this->rp); + $cred = $response->verify($this->cm, $this->rp); self::assertSame(0, $cred->getSignCount()); // Look for a specific id and public key? diff --git a/tests/EndToEndTest.php b/tests/EndToEndTest.php index 77818d6..4503bd8 100644 --- a/tests/EndToEndTest.php +++ b/tests/EndToEndTest.php @@ -30,7 +30,7 @@ public function testRegisterAndLogin(string $directory): void $registerRequest = $this->safeReadJsonFile("$directory/register.json"); $attestation = $parser->parseCreateResponse($registerRequest); - $credential = $attestation->verify($registerChallenge, $this->rp); + $credential = $attestation->verify($this->wrapChallenge($registerChallenge), $this->rp); $loginInfo = $this->safeReadJsonFile("$directory/loginInfo.json"); $loginChallenge = new Challenge( @@ -40,7 +40,7 @@ public function testRegisterAndLogin(string $directory): void $assertion = $parser->parseGetResponse($loginRequest); $updatedCredential = $assertion->verify( - $loginChallenge, + $this->wrapChallenge($loginChallenge), $this->rp, $credential, ); @@ -88,4 +88,12 @@ private function safeReadJsonFile(string $path): array assert(is_array($data)); return $data; } + + private function wrapChallenge(ChallengeInterface $challenge): ChallengeManagerInterface + { + $mock = $this->createMock(ChallengeManagerInterface::class); + $mock->method('useFromClientDataJSON') + ->willReturn($challenge); + return $mock; + } } diff --git a/tests/FixedChallengeManager.php b/tests/FixedChallengeManager.php new file mode 100644 index 0000000..a91a982 --- /dev/null +++ b/tests/FixedChallengeManager.php @@ -0,0 +1,31 @@ + */ + private array $seen = []; + + public function __construct(private ChallengeInterface $challenge) + { + } + + public function createChallenge(): ChallengeInterface + { + throw new BadMethodCallException('Should not be used during testing'); + } + + public function useFromClientDataJSON(string $base64Url): ?ChallengeInterface + { + if (array_key_exists($base64Url, $this->seen)) { + return null; + } + $this->seen[$base64Url] = true; + return $this->challenge; + } +} diff --git a/tests/GetResponseTest.php b/tests/GetResponseTest.php index f1e460b..74d66ad 100644 --- a/tests/GetResponseTest.php +++ b/tests/GetResponseTest.php @@ -10,9 +10,9 @@ class GetResponseTest extends \PHPUnit\Framework\TestCase { // These hold the values which would be kept server-side. - private Challenge $challenge; private CredentialInterface $credential; private RelyingParty $rp; + private ChallengeManagerInterface $cm; // These hold the _default_ values from a sample parsed response. private BinaryString $id; @@ -32,13 +32,6 @@ public function setUp(): void $this->rp = new RelyingParty('http://localhost:8888'); - $this->challenge = new Challenge(BinaryString::fromBytes([ - 145, 94, 61, 93, 225, 209, 17, 150, - 18, 48, 223, 38, 136, 44, 81, 173, - 233, 248, 232, 46, 211, 200, 99, 52, - 142, 111, 103, 233, 244, 188, 26, 108, - ])); - $this->id = BinaryString::fromBytes([ 116, 216, 28, 85, 64, 195, 24, 125, 129, 100, 47, 13, 163, 166, 205, 188, @@ -84,6 +77,13 @@ public function setUp(): void 48, 31, 150, 139, 30, 239, 98, 204, 1, 90, 103, 114, 23, 105, ]); + + $this->cm = new FixedChallengeManager(new Challenge(BinaryString::fromBytes([ + 145, 94, 61, 93, 225, 209, 17, 150, + 18, 48, 223, 38, 136, 44, 81, 173, + 233, 248, 232, 46, 211, 200, 99, 52, + 142, 111, 103, 233, 244, 188, 26, 108, + ]))); } // 7.2.11 @@ -102,7 +102,25 @@ public function testCDJTypeMismatchIsError(): void ); $this->expectVerificationError('7.2.11'); - $response->verify($this->challenge, $this->rp, $this->credential); + $response->verify($this->cm, $this->rp, $this->credential); + } + + // 7.2.12 + public function testUsedChallengeIsError(): void + { + $container = new CredentialContainer([$this->credential]); + + $response = new GetResponse( + credentialId: $this->id, + rawAuthenticatorData: $this->rawAuthenticatorData, + clientDataJson: $this->clientDataJson, + signature: $this->signature, + ); + + $credential = $response->verify($this->cm, $this->rp, $container); + + $this->expectVerificationError('7.2.12'); + $response->verify($this->cm, $this->rp, $container); } // 7.2.12 @@ -120,8 +138,9 @@ public function testCDJChallengeMismatchIsError(): void signature: $this->signature, ); + // Simulate replay. ChallengeManager no longer recognizes this one. $this->expectVerificationError('7.2.12'); - $response->verify($this->challenge, $this->rp, $this->credential); + $response->verify($this->cm, $this->rp, $this->credential); } // 7.2.13 @@ -140,7 +159,7 @@ public function testCDJOriginMismatchIsError(): void ); $this->expectVerificationError('7.2.13'); - $response->verify($this->challenge, $this->rp, $this->credential); + $response->verify($this->cm, $this->rp, $this->credential); } // 7.2.15 @@ -156,7 +175,7 @@ public function testRelyingPartyIdMismatchIsError(): void ); $this->expectVerificationError('7.2.15'); - $response->verify($this->challenge, $rp, $this->credential); + $response->verify($this->cm, $rp, $this->credential); } // 7.2.16 @@ -178,7 +197,12 @@ public function testUserVerifiedNotPresentWhenRequiredIsError(): void ); $this->expectVerificationError('7.2.17'); - $response->verify($this->challenge, $this->rp, $this->credential, UserVerificationRequirement::Required); + $response->verify( + $this->cm, + $this->rp, + $this->credential, + UserVerificationRequirement::Required, + ); } // 7.2.20 @@ -192,7 +216,7 @@ public function testIncorrectSignatureIsError(): void ); $this->expectVerificationError('7.2.20'); - $response->verify($this->challenge, $this->rp, $this->credential); + $response->verify($this->cm, $this->rp, $this->credential); } public function testVerifyReturnsCredentialWithUpdatedCounter(): void @@ -208,7 +232,7 @@ public function testVerifyReturnsCredentialWithUpdatedCounter(): void signature: $this->signature, ); - $updatedCredential = $response->verify($this->challenge, $this->rp, $this->credential); + $updatedCredential = $response->verify($this->cm, $this->rp, $this->credential); self::assertGreaterThan( 0, $updatedCredential->getSignCount(), @@ -243,7 +267,7 @@ public function testCredentialContainerWorks(): void signature: $this->signature, ); - $credential = $response->verify($this->challenge, $this->rp, $container); + $credential = $response->verify($this->cm, $this->rp, $container); self::assertSame($this->credential->getStorageId(), $credential->getStorageId()); } @@ -259,7 +283,7 @@ public function testEmptyCredentialContainerFails(): void ); $this->expectVerificationError('7.2.7'); - $response->verify($this->challenge, $this->rp, $container); + $response->verify($this->cm, $this->rp, $container); } public function testCredentialContainerMissingUsedCredentialFails(): void @@ -276,7 +300,7 @@ public function testCredentialContainerMissingUsedCredentialFails(): void ); $this->expectVerificationError('7.2.7'); - $response->verify($this->challenge, $this->rp, $container); + $response->verify($this->cm, $this->rp, $container); } private function expectVerificationError(string $section): void diff --git a/tests/SessionChallengeManagerTest.php b/tests/SessionChallengeManagerTest.php new file mode 100644 index 0000000..bbc4c2e --- /dev/null +++ b/tests/SessionChallengeManagerTest.php @@ -0,0 +1,26 @@ +