From 6906d77222730fb658600de63b0fa6d156be60a1 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 14:08:26 -0700 Subject: [PATCH 01/21] Deprecate RegisterRequest & SignRequest --- CHANGELOG.md | 5 +++++ src/RegisterRequest.php | 3 +++ src/Server.php | 6 ++++++ src/SignRequest.php | 3 +++ 4 files changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c54e56..c49a47d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Server::setRegisterRequest(RegisterRequest) - Server::setRegistrations(RegistrationInterface[]) - Server::setSignRequests(SignRequest[]) +- Server::generateRegisterRequest() +- Server::generateSignRequest(RegistrationInterface) +- Server::generateSignRequests(RegistrationInterface[]) +- RegisterRequest +- SignRequest ## [1.2.0] - 2021-10-26 diff --git a/src/RegisterRequest.php b/src/RegisterRequest.php index c294fc7..9830409 100644 --- a/src/RegisterRequest.php +++ b/src/RegisterRequest.php @@ -4,6 +4,9 @@ use JsonSerializable; +/** + * @deprecated + */ class RegisterRequest implements JsonSerializable, ChallengeProvider { use AppIdTrait; diff --git a/src/Server.php b/src/Server.php index 57db3d5..f9d2eba 100644 --- a/src/Server.php +++ b/src/Server.php @@ -368,6 +368,8 @@ public function setSignRequests(array $signRequests): self * Creates a new RegisterRequest to be sent to the authenticated user to be * used by the `u2f.register` API. * + * @deprecated + * * @return RegisterRequest */ public function generateRegisterRequest(): RegisterRequest @@ -381,6 +383,8 @@ public function generateRegisterRequest(): RegisterRequest * Creates a new SignRequest for an existing registration for an * authenticating user, used by the `u2f.sign` API. * + * @deprecated + * * @param RegistrationInterface $reg one of the user's existing Registrations * @return SignRequest */ @@ -397,6 +401,8 @@ public function generateSignRequest(RegistrationInterface $reg): SignRequest * ensures that all sign requests share a single challenge, which greatly * simplifies compatibility with WebAuthn * + * @deprecated + * * @param RegistrationInterface[] $registrations * @return SignRequest[] */ diff --git a/src/SignRequest.php b/src/SignRequest.php index 898e2ea..7642142 100644 --- a/src/SignRequest.php +++ b/src/SignRequest.php @@ -4,6 +4,9 @@ use JsonSerializable; +/** + * @deprecated + */ class SignRequest implements JsonSerializable, ChallengeProvider, KeyHandleInterface { use AppIdTrait; From 8f362f8dbae7e48c1dc1573fa9317cefbe2e987b Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 14:11:36 -0700 Subject: [PATCH 02/21] Deprecate U2F response formats --- CHANGELOG.md | 2 ++ src/RegisterResponse.php | 3 +++ src/SignResponse.php | 3 +++ 3 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c49a47d..20bd6f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Server::generateSignRequest(RegistrationInterface) - Server::generateSignRequests(RegistrationInterface[]) - RegisterRequest +- RegisterResponse (Replaced by WebAuthn/RegistrationResponse) - SignRequest +- SignResponse (Replaced by WebAuthn/LoginResponse) ## [1.2.0] - 2021-10-26 diff --git a/src/RegisterResponse.php b/src/RegisterResponse.php index c417008..b331900 100644 --- a/src/RegisterResponse.php +++ b/src/RegisterResponse.php @@ -5,6 +5,9 @@ use Firehed\U2F\InvalidDataException as IDE; +/** + * @deprecated U2F support is being removed. Migrate to WebAuthn flows. + */ class RegisterResponse implements RegistrationResponseInterface { use ResponseTrait; diff --git a/src/SignResponse.php b/src/SignResponse.php index ecf2a04..27ec7d2 100644 --- a/src/SignResponse.php +++ b/src/SignResponse.php @@ -5,6 +5,9 @@ use Firehed\U2F\InvalidDataException as IDE; +/** + * @deprecated U2F support is being removed. Migrate to WebAuthn flows. + */ class SignResponse implements LoginResponseInterface { use ResponseTrait; From 59bf4eff2dc31d70c0c78feda0c6a57cb61497e8 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 14:20:10 -0700 Subject: [PATCH 03/21] Mark tests for deprecated classes as also deprecated --- tests/RegisterRequestTest.php | 1 + tests/RegisterResponseTest.php | 1 + tests/SignRequestTest.php | 1 + tests/SignResponseTest.php | 1 + 4 files changed, 4 insertions(+) diff --git a/tests/RegisterRequestTest.php b/tests/RegisterRequestTest.php index b705e20..757350d 100644 --- a/tests/RegisterRequestTest.php +++ b/tests/RegisterRequestTest.php @@ -5,6 +5,7 @@ /** * @covers Firehed\U2F\RegisterRequest + * @deprecated */ class RegisterRequestTest extends \PHPUnit\Framework\TestCase { diff --git a/tests/RegisterResponseTest.php b/tests/RegisterResponseTest.php index 827bd6c..b948837 100644 --- a/tests/RegisterResponseTest.php +++ b/tests/RegisterResponseTest.php @@ -5,6 +5,7 @@ /** * @covers Firehed\U2F\RegisterResponse + * @deprecated */ class RegisterResponseTest extends \PHPUnit\Framework\TestCase { diff --git a/tests/SignRequestTest.php b/tests/SignRequestTest.php index b0cd4bf..25f3a8f 100644 --- a/tests/SignRequestTest.php +++ b/tests/SignRequestTest.php @@ -5,6 +5,7 @@ /** * @covers Firehed\U2F\SignRequest + * @deprecated */ class SignRequestTest extends \PHPUnit\Framework\TestCase { diff --git a/tests/SignResponseTest.php b/tests/SignResponseTest.php index 6364210..51a4117 100644 --- a/tests/SignResponseTest.php +++ b/tests/SignResponseTest.php @@ -5,6 +5,7 @@ /** * @covers Firehed\U2F\SignResponse + * @deprecated */ class SignResponseTest extends \PHPUnit\Framework\TestCase { From 1507f2bb787c7415a99564cb67ab37df0c9e771c Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 14:25:35 -0700 Subject: [PATCH 04/21] Deprecate tests for old APIs --- tests/ServerTest.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 6b1eb22..6d165b9 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -57,6 +57,9 @@ public function testDisableCAVerificationReturnsSelf(): void ); } + /** + * @deprecated + */ public function testGenerateRegisterRequest(): void { $req = $this->server->generateRegisterRequest(); @@ -76,6 +79,9 @@ public function testGenerateRegisterRequest(): void ); } + /** + * @deprecated + */ public function testGenerateSignRequest(): void { $kh = \random_bytes(16); @@ -104,6 +110,9 @@ public function testGenerateSignRequest(): void ); } + /** + * @deprecated + */ public function testGenerateSignRequests(): void { $registrations = [ From 38434c17c81ea4a859f55129629952a1b74e6cdc Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 14:33:53 -0700 Subject: [PATCH 05/21] Migrate to new tool --- tests/ServerTest.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 6d165b9..534829f 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -242,11 +242,11 @@ public function testLegacyRegistration(): void public function testRegistration(): void { - $request = $this->getDefaultRegisterRequest(); + $challenge = $this->getDefaultRegistrationChallenge(); $response = $this->getDefaultRegistrationResponse(); $registration = $this->server - ->validateRegistration($request, $response); + ->validateRegistration($challenge, $response); $this->assertInstanceOf( RegistrationInterface::class, $registration, @@ -279,7 +279,7 @@ public function testRegistration(): void public function testRegisterDefaultsToTryingEmptyCAList(): void { - $request = $this->getDefaultRegisterRequest(); + $challenge = $this->getDefaultRegistrationChallenge(); $response = $this->getDefaultRegistrationResponse(); $this->expectException(SecurityException::class); @@ -288,7 +288,7 @@ public function testRegisterDefaultsToTryingEmptyCAList(): void // meaning that an exception should be thrown unless either a) // a matching CA is provided or b) verification is explicitly disabled $server = new Server(self::APP_ID); - $server->validateRegistration($request, $response); + $server->validateRegistration($challenge, $response); } public function testRegisterThrowsIfChallengeDoesNotMatch(): void @@ -306,7 +306,7 @@ public function testRegisterThrowsIfChallengeDoesNotMatch(): void public function testRegisterThrowsWithUntrustedDeviceIssuerCertificate(): void { - $request = $this->getDefaultRegisterRequest(); + $challenge = $this->getDefaultRegistrationChallenge(); $response = $this->getDefaultRegistrationResponse(); $this->server->setTrustedCAs([ @@ -316,12 +316,12 @@ public function testRegisterThrowsWithUntrustedDeviceIssuerCertificate(): void ]); $this->expectException(SecurityException::class); $this->expectExceptionCode(SecurityException::NO_TRUSTED_CA); - $this->server->validateRegistration($request, $response); + $this->server->validateRegistration($challenge, $response); } public function testRegisterWorksWithCAList(): void { - $request = $this->getDefaultRegisterRequest(); + $challenge = $this->getDefaultRegistrationChallenge(); $response = $this->getDefaultRegistrationResponse(); // This contains the actual trusted + verified certificates which are // good to use in production. The messages in these tests were @@ -332,7 +332,7 @@ public function testRegisterWorksWithCAList(): void $this->server->setTrustedCAs($CAs); try { - $reg = $this->server->validateRegistration($request, $response); + $reg = $this->server->validateRegistration($challenge, $response); } catch (SecurityException $e) { if ($e->getCode() === SecurityException::NO_TRUSTED_CA) { $this->fail('CA Verification should have succeeded'); From 393f5dd6939f64b63a03abcc5f28d5aea1f8ca86 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 14:42:31 -0700 Subject: [PATCH 06/21] Allow overrides for default registration challenge --- tests/ServerTest.php | 47 ++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 534829f..d0063e7 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -711,7 +711,18 @@ private function getDefaultRegisterResponse(): RegisterResponse return RegisterResponse::fromJson($this->safeReadFile('register_response.json')); } - private function getDefaultRegistrationResponse(): RegistrationResponseInterface + /** + * @param array{ + * getAttestationCertificate?: AttestationCertificateInterface, + * getChallenge?: string, + * getKeyHandleBinary?: string, + * getPublicKey?: PublicKeyInterface, + * getRpIdHash?: string, + * getSignature?: string, + * getSignedData?: string, + * } $overrides + */ + private function getDefaultRegistrationResponse(array $overrides = []): RegistrationResponseInterface { // This data was manually extracted from an actual key exchange. It // does NOT correspond to the values from getDefaultLoginResponse(). @@ -726,32 +737,38 @@ private function getDefaultRegistrationResponse(): RegistrationResponseInterface '43e68d1b03d1f9558d77c5a308163be26ab1b8778692b6282b4c6f023e5bd298'. 'f4028967599eeaec31609df19d34546fc7eba72c23f78bc9d75ac63eebd52d09' )); - $mock->method('getAttestationCertificate') - ->willReturn($this->getDefaultAttestationCertificate()); - $mock->method('getChallenge') - ->willReturn('PfsWR1Umy2V5Al1Bam2tG0yfPLeJElfwRzzAzkYPgzo'); - $mock->method('getKeyHandleBinary') - ->willReturn($keyHandleBinary); - $mock->method('getPublicKey') - ->willReturn($pk); - $mock->method('getRpIdHash') - ->willReturn(hash('sha256', 'https://u2f.ericstern.com', true)); - $mock->method('getSignature')->willReturn(hex2bin( + $signature = hex2bin( '304402207646e5d330cb99cd86fddd67029bdb4c1d128146e4f70a046c5953ab'. '64a40a6a0220683fa0c3bb1f6328f7ace7b00894e7dcd6d735474ac7ea517d3b'. '2b441ebc95e4' - )); + ); + $challengeParamaeterJson = '{"typ":"navigator.id.finishEnrollment","c'. 'hallenge":"PfsWR1Umy2V5Al1Bam2tG0yfPLeJElfwRzzAzkYPgzo","origin"'. ':"https://u2f.ericstern.com","cid_pubkey":""}'; - $mock->method('getSignedData')->willReturn(sprintf( + $signedData = sprintf( '%s%s%s%s%s', chr(0), hash('sha256', 'https://u2f.ericstern.com', true), hash('sha256', $challengeParamaeterJson, true), $keyHandleBinary, $pk->getBinary() - )); + ); + $defaults = [ + 'getAttestationCertificate' => $this->getDefaultAttestationCertificate(), + 'getChallenge' => 'PfsWR1Umy2V5Al1Bam2tG0yfPLeJElfwRzzAzkYPgzo', // defaultregchallenge + 'getKeyHandleBinary' => $keyHandleBinary, + 'getPublicKey' => $pk, + 'getRpIdHash' => hash('sha256', 'https://u2f.ericstern.com', true), + 'getSignature' => $signature, + 'getSignedData' => $signedData, + ]; + + $data = array_merge($defaults, $overrides); + + foreach ($data as $method => $value) { + $mock->method($method)->willReturn($value); + } return $mock; } From 69e75733ef11b33740344315f78130d2785fe755 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 14:49:10 -0700 Subject: [PATCH 07/21] Migrate more logic across --- tests/ServerTest.php | 77 ++++++++++++-------------------------------- 1 file changed, 21 insertions(+), 56 deletions(-) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index d0063e7..402ebcd 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -344,79 +344,39 @@ public function testRegisterWorksWithCAList(): void public function testRegisterThrowsWithChangedApplicationParameter(): void { - $request = $this->getDefaultRegisterRequest(); + $challenge = $this->getDefaultRegistrationChallenge(); - $response = $this->createMock(RegistrationResponseInterface::class); - $response->method('getChallenge') - ->willReturn($request->getChallenge()); - $response->method('getRpIdHash') - ->willReturn(hash('sha256', 'https://some.otherdomain.com', true)); + $response = $this->getDefaultRegistrationResponse([ + 'getRpIdHash' => hash('sha256', 'https://some.otherdomain.com', true), + ]); $this->expectException(SecurityException::class); $this->expectExceptionCode(SecurityException::WRONG_RELYING_PARTY); - $this->server->validateRegistration($request, $response); - } - - public function testRegisterThrowsWithChangedChallengeParameter(): void - { - $request = $this->getDefaultRegisterRequest(); - // Mess up some known-good data: challenge parameter - $data = $this->readJsonFile('register_response.json'); - $cli = fromBase64Web($data['clientData']); - $obj = json_decode($cli, true); - $obj['cid_pubkey'] = 'nonsense'; - $cli = toBase64Web($this->safeEncode($obj)); - $data['clientData'] = $cli; - $response = RegisterResponse::fromJson($this->safeEncode($data)); - - $this->expectException(SecurityException::class); - $this->expectExceptionCode(SecurityException::SIGNATURE_INVALID); - $this->server->validateRegistration($request, $response); - } - - public function testRegisterThrowsWithChangedKeyHandle(): void - { - $request = $this->getDefaultRegisterRequest(); - // Mess up some known-good data: key handle - $data = $this->readJsonFile('register_response.json'); - $reg = $data['registrationData']; - $reg[70] = chr(ord($reg[70]) + 1); // Change a byte in the key handle - $data['registrationData'] = $reg; - $response = RegisterResponse::fromJson($this->safeEncode($data)); - - $this->expectException(SecurityException::class); - $this->expectExceptionCode(SecurityException::SIGNATURE_INVALID); - $this->server->validateRegistration($request, $response); + $this->server->validateRegistration($challenge, $response); } - public function testRegisterThrowsWithChangedPubkey(): void + public function testRegisterThrowsWithChangedSignedData(): void { - $request = $this->getDefaultRegisterRequest(); - // Mess up some known-good data: public key - $data = $this->readJsonFile('register_response.json'); - $reg = $data['registrationData']; - $reg[3] = chr(ord($reg[3]) + 1); // Change a byte in the public key - $data['registrationData'] = $reg; - $response = RegisterResponse::fromJson($this->safeEncode($data)); + $challenge = $this->getDefaultRegistrationChallenge(); + $response = $this->getDefaultRegistrationResponse([ + 'getSignedData' => 'value changed', + ]); $this->expectException(SecurityException::class); $this->expectExceptionCode(SecurityException::SIGNATURE_INVALID); - $this->server->validateRegistration($request, $response); + $this->server->validateRegistration($challenge, $response); } public function testRegisterThrowsWithBadSignature(): void { - $request = $this->getDefaultRegisterRequest(); - // Mess up some known-good data: signature - $data = $this->readJsonFile('register_response.json'); - $reg = $data['registrationData']; - $last = str_rot13(substr($reg, -5)); // rot13 a few chars in signature - $data['registrationData'] = substr($reg, 0, -5).$last; - $response = RegisterResponse::fromJson($this->safeEncode($data)); + $challenge = $this->getDefaultRegistrationChallenge(); + $response = $this->getDefaultRegistrationResponse([ + 'getSignature' => 'value changed', + ]); $this->expectException(SecurityException::class); $this->expectExceptionCode(SecurityException::SIGNATURE_INVALID); - $this->server->validateRegistration($request, $response); + $this->server->validateRegistration($challenge, $response); } // -( Authentication )----------------------------------------------------- @@ -703,6 +663,11 @@ private function getDefaultRegisterRequest(): RegisterRequest ->setChallenge('PfsWR1Umy2V5Al1Bam2tG0yfPLeJElfwRzzAzkYPgzo'); } + private function getDefaultRegistrationChallenge(): ChallengeProviderInterface + { + return new Challenge('PfsWR1Umy2V5Al1Bam2tG0yfPLeJElfwRzzAzkYPgzo'); + } + /** * @deprecated */ From d5ac233b0c64b25ab9f82b7daa07ac5f84d962db Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 15:01:23 -0700 Subject: [PATCH 08/21] More test updates --- tests/ServerTest.php | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 402ebcd..1011fbf 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -293,15 +293,24 @@ public function testRegisterDefaultsToTryingEmptyCAList(): void public function testRegisterThrowsIfChallengeDoesNotMatch(): void { - // This would have come from a session, database, etc. - $request = (new RegisterRequest()) - ->setAppId('https://u2f.ericstern.com') - ->setChallenge('some-other-challenge'); + $challenge = $this->getDefaultRegistrationChallenge(); + $response = $this->getDefaultRegistrationResponse([ + 'getChallenge' => 'some-other-challenge', + ]); + + $this->expectException(SecurityException::class); + $this->expectExceptionCode(SecurityException::CHALLENGE_MISMATCH); + $this->server->validateRegistration($challenge, $response); + } + + public function testRegisterThrowsIfChallengeDoesNotMatchInverse(): void + { + $challenge = new Challenge('some-other-challenge'); $response = $this->getDefaultRegistrationResponse(); $this->expectException(SecurityException::class); $this->expectExceptionCode(SecurityException::CHALLENGE_MISMATCH); - $this->server->validateRegistration($request, $response); + $this->server->validateRegistration($challenge, $response); } public function testRegisterThrowsWithUntrustedDeviceIssuerCertificate(): void @@ -345,7 +354,6 @@ public function testRegisterWorksWithCAList(): void public function testRegisterThrowsWithChangedApplicationParameter(): void { $challenge = $this->getDefaultRegistrationChallenge(); - $response = $this->getDefaultRegistrationResponse([ 'getRpIdHash' => hash('sha256', 'https://some.otherdomain.com', true), ]); @@ -655,6 +663,9 @@ public function testRegistrationWithoutCidPubkeyBug14Case2(): void // -( Helpers )------------------------------------------------------------ + /** + * @deprecated + */ private function getDefaultRegisterRequest(): RegisterRequest { // This would have come from a session, database, etc. @@ -737,6 +748,9 @@ private function getDefaultRegistrationResponse(array $overrides = []): Registra return $mock; } + /** + * @deprecated + */ private function getDefaultSignRequest(): SignRequest { // This would have come from a session, database, etc From d13eb1b70c4a0947e4ec15d2cb34ecb4a9137afd Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 15:22:59 -0700 Subject: [PATCH 09/21] Continue updating out of legacy models --- tests/ServerTest.php | 58 +++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 1011fbf..c8e4b00 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -446,25 +446,25 @@ public function testValidateLogin(): void { // All normal $registration = $this->getDefaultRegistration(); - $request = $this->getDefaultSignRequest(); + $challenge = $this->getDefaultLoginChallenge(); $response = $this->getDefaultLoginResponse(); - $return = $this->server->validateLogin($request, $response, [$registration]); + $updated = $this->server->validateLogin($challenge, $response, [$registration]); $this->assertInstanceOf( RegistrationInterface::class, - $return, - 'A successful authentication should have returned an object '. + $updated, + 'A successful authentication should have registrationed an object '. 'implementing RegistrationInterface' ); $this->assertNotSame( $registration, - $return, + $updated, 'A new object implementing RegistrationInterface should have been '. 'returned' ); $this->assertSame( $response->getCounter(), - $return->getCounter(), + $updated->getCounter(), 'The new registration\'s counter did not match the Response' ); } @@ -477,11 +477,11 @@ public function testValidateLoginThrowsWithObviousReplayAttack(): void { // All normal $registration = $this->getDefaultRegistration(); - $request = $this->getDefaultSignRequest(); + $challenge = $this->getDefaultLoginChallenge(); $response = $this->getDefaultLoginResponse(); - $updatedRegistration = $this->server->validateLogin($request, $response, [$registration]); - // Here is where you would persist $new_registration to update the + $updatedRegistration = $this->server->validateLogin($challenge, $response, [$registration]); + // Here is where you would persist $updatedRegistration to update the // stored counter value. This simulates fetching that updated value and // trying to authenticate with it. Uses a completely new Server // instances to fully simulate a new request. The available sign @@ -489,39 +489,33 @@ public function testValidateLoginThrowsWithObviousReplayAttack(): void // a worst-case scenario. $this->expectException(SecurityException::class); $this->expectExceptionCode(SecurityException::COUNTER_USED); - $this->server->validateLogin($request, $response, [$updatedRegistration]); + $this->server->validateLogin($challenge, $response, [$updatedRegistration]); } public function testValidateLoginThrowsWhenCounterGoesBackwards(): void { // Counter from "DB" bumped, suggesting response was cloned - $registration = (new Registration()) - ->setKeyHandle(fromBase64Web(self::ENCODED_KEY_HANDLE)) - ->setPublicKey($this->getDefaultPublicKey()) - ->setCounter(82) - ; - $request = $this->getDefaultSignRequest(); + $registration = $this->getDefaultRegistration([ + 'counter' => 82, + ]); + $challenge = $this->getDefaultLoginChallenge(); $response = $this->getDefaultLoginResponse(); $this->expectException(SecurityException::class); $this->expectExceptionCode(SecurityException::COUNTER_USED); - $this->server->validateLogin($request, $response, [$registration]); + $this->server->validateLogin($challenge, $response, [$registration]); } public function testValidateLoginThrowsWhenChallengeDoesNotMatch(): void { $registration = $this->getDefaultRegistration(); // Change request challenge - $request = (new SignRequest()) - ->setAppId('https://u2f.ericstern.com') - ->setChallenge('some-other-challenge') - ->setKeyHandle(fromBase64Web(self::ENCODED_KEY_HANDLE)) - ; + $challenge = new Challenge('some-other-challenge'); $response = $this->getDefaultLoginResponse(); $this->expectException(SecurityException::class); $this->expectExceptionCode(SecurityException::CHALLENGE_MISMATCH); - $this->server->validateLogin($request, $response, [$registration]); + $this->server->validateLogin($challenge, $response, [$registration]); } public function testValidateLoginThrowsIfNoRegistrationMatchesKeyHandle(): void @@ -761,14 +755,28 @@ private function getDefaultSignRequest(): SignRequest ; } - private function getDefaultRegistration(): RegistrationInterface + private function getDefaultLoginChallenge(): ChallengeProviderInterface { + return new Challenge('wt2ze8IskcTO3nIsO2D2hFjE5tVD041NpnYesLpJweg'); + } + + /** + * @param array{ + * counter?: int, + * } $overrides + */ + private function getDefaultRegistration(array $overrides = []): RegistrationInterface + { + $defaults = [ + 'counter' => 2, + ]; + $data = array_merge($defaults, $overrides); // From database attached to the authenticating user return (new Registration()) ->setKeyHandle(fromBase64Web(self::ENCODED_KEY_HANDLE)) ->setAttestationCertificate($this->getDefaultAttestationCertificate()) ->setPublicKey($this->getDefaultPublicKey()) - ->setCounter(2) + ->setCounter($data['counter']) ; } From b0034c0e3fbd513142a0ae4ca7d70991cc0e78da Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 15:34:33 -0700 Subject: [PATCH 10/21] More updates --- tests/ServerTest.php | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index c8e4b00..7fc3f35 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -521,17 +521,15 @@ public function testValidateLoginThrowsWhenChallengeDoesNotMatch(): void public function testValidateLoginThrowsIfNoRegistrationMatchesKeyHandle(): void { // Change registration KH - $registration = (new Registration()) - ->setKeyHandle(fromBase64Web('some-other-key-handle')) - ->setPublicKey($this->getDefaultPublicKey()) - ->setCounter(2) - ; - $request = $this->getDefaultSignRequest(); + $registration = $this->getDefaultRegistration([ + 'keyHandle' => 'some-other-key-handle', + ]); + $challenge = $this->getDefaultLoginChallenge(); $response = $this->getDefaultLoginResponse(); $this->expectException(SecurityException::class); $this->expectExceptionCode(SecurityException::KEY_HANDLE_UNRECOGNIZED); - $this->server->validateLogin($request, $response, [$registration]); + $this->server->validateLogin($challenge, $response, [$registration]); } /** @@ -763,17 +761,25 @@ private function getDefaultLoginChallenge(): ChallengeProviderInterface /** * @param array{ * counter?: int, + * keyHandle?: string, * } $overrides */ private function getDefaultRegistration(array $overrides = []): RegistrationInterface { $defaults = [ 'counter' => 2, + 'keyHandle' => fromBase64Web(self::ENCODED_KEY_HANDLE), ]; + /** + * @var array{ + * counter: int, + * keyHandle: string, + * } (phpstan/phpstan#5846) + */ $data = array_merge($defaults, $overrides); // From database attached to the authenticating user return (new Registration()) - ->setKeyHandle(fromBase64Web(self::ENCODED_KEY_HANDLE)) + ->setKeyHandle($data['keyHandle']) ->setAttestationCertificate($this->getDefaultAttestationCertificate()) ->setPublicKey($this->getDefaultPublicKey()) ->setCounter($data['counter']) From 287be4345689c84db8345c3845511302e55eaac4 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 15:57:06 -0700 Subject: [PATCH 11/21] Make default login response configurable --- tests/ServerTest.php | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 7fc3f35..6c4de77 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -694,7 +694,6 @@ private function getDefaultRegistrationResponse(array $overrides = []): Registra { // This data was manually extracted from an actual key exchange. It // does NOT correspond to the values from getDefaultLoginResponse(). - $mock = self::createMock(RegistrationResponseInterface::class); $keyHandleBinary = hex2bin( '6d4a7a7393fa51cf24dbe035f26cacc9868a9385320a099b17062ac0ddc11fc0'. '0cb96b1a8fffe4736b7144c508fc343af81c104ba25e086ee5c1ba71da0c7d6d' @@ -724,7 +723,7 @@ private function getDefaultRegistrationResponse(array $overrides = []): Registra ); $defaults = [ 'getAttestationCertificate' => $this->getDefaultAttestationCertificate(), - 'getChallenge' => 'PfsWR1Umy2V5Al1Bam2tG0yfPLeJElfwRzzAzkYPgzo', // defaultregchallenge + 'getChallenge' => 'PfsWR1Umy2V5Al1Bam2tG0yfPLeJElfwRzzAzkYPgzo', // getDefaultRegistrationChallenge 'getKeyHandleBinary' => $keyHandleBinary, 'getPublicKey' => $pk, 'getRpIdHash' => hash('sha256', 'https://u2f.ericstern.com', true), @@ -734,6 +733,7 @@ private function getDefaultRegistrationResponse(array $overrides = []): Registra $data = array_merge($defaults, $overrides); + $mock = self::createMock(RegistrationResponseInterface::class); foreach ($data as $method => $value) { $mock->method($method)->willReturn($value); } @@ -786,36 +786,52 @@ private function getDefaultRegistration(array $overrides = []): RegistrationInte ; } - private function getDefaultLoginResponse(): LoginResponseInterface + /** + * @param array{ + * getChallenge?: string, + * getCounter?: int, + * getKeyHandleBinary?: string, + * getSignature?: string, + * getSignedData?: string, + * } $overrides + */ + private function getDefaultLoginResponse(array $overrides = []): LoginResponseInterface { // This data was manually extracted from an actual key exchange. It // does NOT correspond to the values from // getDefaultRegistrationResponse(). - $mock = self::createMock(LoginResponseInterface::class); - $mock->method('getChallenge') - ->willReturn('wt2ze8IskcTO3nIsO2D2hFjE5tVD041NpnYesLpJweg'); - $mock->method('getCounter') - ->willReturn(45); - $mock->method('getKeyHandleBinary')->willReturn(hex2bin( + $keyHandleBinary = hex2bin( '2549d54d2b4f9fe576f9b0aed1196f3dbba40691d30f9322d591a094339c374b'. 'c3e39ae74c3d015dd911b7bf21b93c09eed55ac53a927ad1e3af6dad0a39982d' - )); - $mock->method('getSignature')->willReturn(hex2bin( + ); + $signature = hex2bin( '304602210093f2d51bc3d560b0d57657e77057c9d5ff2b27ff5d942e7854883e'. '281117e0f6022100c776c9af98b1ad719d517d57a2801f873d7964863cac2e47'. 'e2a696ee042ca49e' - )); + ); $challengeParamaeterJson = '{"typ":"navigator.id.getAssertion","chall'. 'enge":"wt2ze8IskcTO3nIsO2D2hFjE5tVD041NpnYesLpJweg","origin":"ht'. 'tps://u2f.ericstern.com","cid_pubkey":""}'; - $mock->method('getSignedData')->willReturn(sprintf( + $signedData = sprintf( '%s%s%s%s', hash('sha256', 'https://u2f.ericstern.com', true), chr(1), pack('N', 45), hash('sha256', $challengeParamaeterJson, true) - )); + ); + $defaults = [ + 'getChallenge' => 'wt2ze8IskcTO3nIsO2D2hFjE5tVD041NpnYesLpJweg', // getDefaultLoginChallenge + 'getCounter' => 45, + 'getKeyHandleBinary' => $keyHandleBinary, + 'getSignature' => $signature, + 'getSignedData' => $signedData, + ]; + $data = array_merge($defaults, $overrides); + $mock = self::createMock(LoginResponseInterface::class); + foreach ($data as $method => $result) { + $mock->method($method)->willReturn($result); + } return $mock; } From 0d7c57e292c0dee160b6456714ab9acd5b6331e3 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 15:57:33 -0700 Subject: [PATCH 12/21] Make registration PK configurable --- tests/ServerTest.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 6c4de77..a09036e 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -585,16 +585,14 @@ public function testValidateLoginThrowsIfRequestIsSignedWithWrongKey(): void '9OxeRv2zYiz7SrVa8eb4LbGR9IDUE7gJySiiuQYWt1w=' ); assert($pk !== false); - $registration = (new Registration()) - ->setKeyHandle(fromBase64Web(self::ENCODED_KEY_HANDLE)) - ->setPublicKey(new ECPublicKey($pk)) - ->setCounter(2) - ; - $request = $this->getDefaultSignRequest(); + $registration = $this->getDefaultRegistration([ + 'publicKey' => new ECPublicKey($pk), + ]); + $challenge = $this->getDefaultLoginChallenge(); $response = $this->getDefaultLoginResponse(); $this->expectException(SecurityException::class); $this->expectExceptionCode(SecurityException::SIGNATURE_INVALID); - $this->server->validateLogin($request, $response, [$registration]); + $this->server->validateLogin($challenge, $response, [$registration]); } // -( Alternate formats (see #14) )---------------------------------------- @@ -762,6 +760,7 @@ private function getDefaultLoginChallenge(): ChallengeProviderInterface * @param array{ * counter?: int, * keyHandle?: string, + * publicKey?: PublicKeyInterface, * } $overrides */ private function getDefaultRegistration(array $overrides = []): RegistrationInterface @@ -769,11 +768,13 @@ private function getDefaultRegistration(array $overrides = []): RegistrationInte $defaults = [ 'counter' => 2, 'keyHandle' => fromBase64Web(self::ENCODED_KEY_HANDLE), + 'publicKey' => $this->getDefaultPublicKey(), ]; /** * @var array{ * counter: int, * keyHandle: string, + * publicKey: PublicKeyInterface, * } (phpstan/phpstan#5846) */ $data = array_merge($defaults, $overrides); @@ -781,7 +782,7 @@ private function getDefaultRegistration(array $overrides = []): RegistrationInte return (new Registration()) ->setKeyHandle($data['keyHandle']) ->setAttestationCertificate($this->getDefaultAttestationCertificate()) - ->setPublicKey($this->getDefaultPublicKey()) + ->setPublicKey($data['publicKey']) ->setCounter($data['counter']) ; } From d09d9948697ce4cc4e725ac935abbdf19944316a Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 15:59:30 -0700 Subject: [PATCH 13/21] Test login with incorrect signature --- tests/ServerTest.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index a09036e..58e60e0 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -556,16 +556,15 @@ public function testAuthenticateThrowsIfNoRequestMatchesKeyHandle(): void public function testValidateLoginThrowsIfSignatureIsInvalid(): void { + $challenge = $this->getDefaultLoginChallenge(); + $response = $this->getDefaultLoginResponse([ + 'getSignature' => 'some-other-signature', + ]); $registration = $this->getDefaultRegistration(); - $request = $this->getDefaultSignRequest(); - // Trimming a byte off the signature to cause a mismatch - $data = $this->readJsonFile('sign_response.json'); - $data['signatureData'] = substr($data['signatureData'], 0, -1); - $response = SignResponse::fromJson($this->safeEncode($data)); $this->expectException(SecurityException::class); $this->expectExceptionCode(SecurityException::SIGNATURE_INVALID); - $this->server->validateLogin($request, $response, [$registration]); + $this->server->validateLogin($challenge, $response, [$registration]); } /** From 80874ad8b40d29d3b83613a371f850964ae74ff4 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 16:00:45 -0700 Subject: [PATCH 14/21] Add additional signature mismatch test --- tests/ServerTest.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 58e60e0..4f3cfa7 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -567,6 +567,19 @@ public function testValidateLoginThrowsIfSignatureIsInvalid(): void $this->server->validateLogin($challenge, $response, [$registration]); } + public function testValidateLoginThrowsIfWrongDataIsSigned(): void + { + $challenge = $this->getDefaultLoginChallenge(); + $response = $this->getDefaultLoginResponse([ + 'getSignedData' => 'some other signed data', + ]); + $registration = $this->getDefaultRegistration(); + + $this->expectException(SecurityException::class); + $this->expectExceptionCode(SecurityException::SIGNATURE_INVALID); + $this->server->validateLogin($challenge, $response, [$registration]); + } + /** * Arguably the most important authentication test: ensure that * a perfectly-valid signature is rejected if it's not actually from the From dd24ca339df34d71998db70e9b4f5ab5d6be57e4 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 16:01:30 -0700 Subject: [PATCH 15/21] Deprecate last couple of legacy format related tests --- tests/ServerTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 4f3cfa7..266f8ae 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -609,6 +609,9 @@ public function testValidateLoginThrowsIfRequestIsSignedWithWrongKey(): void // -( Alternate formats (see #14) )---------------------------------------- + /** + * @deprecated + */ public function testRegistrationWithoutCidPubkeyBug14Case1(): void { $registerRequest = new RegisterRequest(); @@ -636,6 +639,9 @@ public function testRegistrationWithoutCidPubkeyBug14Case1(): void $this->assertInstanceOf(Registration::class, $registration); } + /** + * @deprecated + */ public function testRegistrationWithoutCidPubkeyBug14Case2(): void { $registerRequest = new RegisterRequest(); From f527132ced7add3e2cd06390e206ca165d4e2ecf Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 16:02:14 -0700 Subject: [PATCH 16/21] Remove some now-unused helpers --- tests/ServerTest.php | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 266f8ae..cd34ba1 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -896,32 +896,10 @@ public function getDefaultPublicKey(): PublicKeyInterface return new ECPublicKey($pk); } - /** @return mixed[] */ - private function readJsonFile(string $file): array - { - return $this->safeDecode($this->safeReadFile($file)); - } - private function safeReadFile(string $file): string { $body = file_get_contents(__DIR__.'/'.$file); assert($body !== false); return $body; } - - /** @return mixed[] */ - private function safeDecode(string $json): array - { - $data = json_decode($json, true); - assert($data !== false); - return $data; - } - - /** @param mixed[] $data */ - private function safeEncode(array $data): string - { - $json = json_encode($data); - assert($json !== false); - return $json; - } } From d6cecfda0f0344e46df50833fee2b2e025f99419 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 16:23:33 -0700 Subject: [PATCH 17/21] Add other deprecation markers --- tests/ServerTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/ServerTest.php b/tests/ServerTest.php index cd34ba1..5058ab6 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -160,6 +160,9 @@ public function testSetRegistrationsReturnsSelf(): void ); } + /** + * @deprecated + */ public function testSetRegistrationsEnforcesTypeCheck(): void { $wrong = true; @@ -181,6 +184,9 @@ public function testSetSignRequestsReturnsSelf(): void ); } + /** + * @deprecated + */ public function testSetSignRequestsEnforcesTypeCheck(): void { $wrong = true; From 40eae4f0bca574a10ede9ab8aced69e2d3a46b61 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 16:47:29 -0700 Subject: [PATCH 18/21] Mark all traits as internal and deprecate ClientData --- CHANGELOG.md | 3 +++ src/AppIdTrait.php | 3 +++ src/ChallengeTrait.php | 3 +++ src/ClientData.php | 4 ++++ src/KeyHandleTrait.php | 3 +++ src/ResponseTrait.php | 3 +++ src/VersionTrait.php | 3 +++ src/WebAuthn/AuthenticatorData.php | 2 ++ 8 files changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20bd6f9..c658f9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Server's constructor now can take `string $appId` as a parameter +- WebAuthn\AuthenticatorData marked as internal +- All traits marked as internal ### Deprecated - ChallengeProvider @@ -31,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - RegisterResponse (Replaced by WebAuthn/RegistrationResponse) - SignRequest - SignResponse (Replaced by WebAuthn/LoginResponse) +- ClientData (internal) ## [1.2.0] - 2021-10-26 diff --git a/src/AppIdTrait.php b/src/AppIdTrait.php index ee38d01..114c899 100644 --- a/src/AppIdTrait.php +++ b/src/AppIdTrait.php @@ -3,6 +3,9 @@ namespace Firehed\U2F; +/** + * @internal + */ trait AppIdTrait { /** @var string */ diff --git a/src/ChallengeTrait.php b/src/ChallengeTrait.php index 767cb23..f7622c9 100644 --- a/src/ChallengeTrait.php +++ b/src/ChallengeTrait.php @@ -2,6 +2,9 @@ namespace Firehed\U2F; +/** + * @internal + */ trait ChallengeTrait { /** @var string */ diff --git a/src/ClientData.php b/src/ClientData.php index 6034105..be90960 100644 --- a/src/ClientData.php +++ b/src/ClientData.php @@ -5,6 +5,10 @@ use Firehed\U2F\InvalidDataException as IDE; +/** + * @deprecated + * @internal + */ class ClientData { use ChallengeTrait; diff --git a/src/KeyHandleTrait.php b/src/KeyHandleTrait.php index 8ab31b9..5c53e7e 100644 --- a/src/KeyHandleTrait.php +++ b/src/KeyHandleTrait.php @@ -2,6 +2,9 @@ namespace Firehed\U2F; +/** + * @internal + */ trait KeyHandleTrait { /** @var string (binary) */ diff --git a/src/ResponseTrait.php b/src/ResponseTrait.php index ad0f4e4..c4fe0c6 100644 --- a/src/ResponseTrait.php +++ b/src/ResponseTrait.php @@ -5,6 +5,9 @@ use Firehed\U2F\InvalidDataException as IDE; +/** + * @internal + */ trait ResponseTrait { use KeyHandleTrait; diff --git a/src/VersionTrait.php b/src/VersionTrait.php index 6318686..0eecb30 100644 --- a/src/VersionTrait.php +++ b/src/VersionTrait.php @@ -3,6 +3,9 @@ namespace Firehed\U2F; +/** + * @internal + */ trait VersionTrait { /** @var 'U2F_V2' */ diff --git a/src/WebAuthn/AuthenticatorData.php b/src/WebAuthn/AuthenticatorData.php index b497afa..f73e66c 100644 --- a/src/WebAuthn/AuthenticatorData.php +++ b/src/WebAuthn/AuthenticatorData.php @@ -7,6 +7,8 @@ use Firehed\CBOR\Decoder; /** + * @internal + * * @phpstan-type AttestedCredentialData array{ * aaguid: string, * credentialId: string, From 3878e6f876ae8475d740c1ec5eb14978cf71a027 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 16:47:57 -0700 Subject: [PATCH 19/21] Deprecate clientdata test --- tests/ClientDataTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/ClientDataTest.php b/tests/ClientDataTest.php index 40ba082..440e14c 100644 --- a/tests/ClientDataTest.php +++ b/tests/ClientDataTest.php @@ -5,6 +5,7 @@ /** * @covers Firehed\U2F\ClientData + * @deprecated */ class ClientDataTest extends \PHPUnit\Framework\TestCase { From 50e31d42d67acf6e1061889bc492e1527972152d Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 16:49:44 -0700 Subject: [PATCH 20/21] Deprecate ResponseTrait too --- CHANGELOG.md | 1 + src/ResponseTrait.php | 1 + tests/ResponseTraitTest.php | 1 + 3 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c658f9b..832fa8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - SignRequest - SignResponse (Replaced by WebAuthn/LoginResponse) - ClientData (internal) +- ResponseTrait (internal) ## [1.2.0] - 2021-10-26 diff --git a/src/ResponseTrait.php b/src/ResponseTrait.php index c4fe0c6..6584f29 100644 --- a/src/ResponseTrait.php +++ b/src/ResponseTrait.php @@ -6,6 +6,7 @@ use Firehed\U2F\InvalidDataException as IDE; /** + * @deprecated * @internal */ trait ResponseTrait diff --git a/tests/ResponseTraitTest.php b/tests/ResponseTraitTest.php index b1d23f9..4a76c64 100644 --- a/tests/ResponseTraitTest.php +++ b/tests/ResponseTraitTest.php @@ -5,6 +5,7 @@ /** * @covers Firehed\U2F\ResponseTrait + * @deprecated */ class ResponseTraitTest extends \PHPUnit\Framework\TestCase { From b9b8db50d0265202cfcbce84e892158aff99248c Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 27 Oct 2021 16:51:40 -0700 Subject: [PATCH 21/21] Line number in baseline --- phpstan-baseline.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9472b5c..7bd4c93 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -46,7 +46,7 @@ parameters: path: tests/ResponseTraitTest.php - - message: "#^Method class@anonymous/tests/ResponseTraitTest\\.php\\:16\\:\\:parseResponse\\(\\) has parameter \\$response with no value type specified in iterable type array\\.$#" + message: "#^Method class@anonymous/tests/ResponseTraitTest\\.php\\:17\\:\\:parseResponse\\(\\) has parameter \\$response with no value type specified in iterable type array\\.$#" count: 1 path: tests/ResponseTraitTest.php