diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c54e56..832fa8d 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 @@ -24,6 +26,15 @@ 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 +- RegisterResponse (Replaced by WebAuthn/RegistrationResponse) +- SignRequest +- SignResponse (Replaced by WebAuthn/LoginResponse) +- ClientData (internal) +- ResponseTrait (internal) ## [1.2.0] - 2021-10-26 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 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/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/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/ResponseTrait.php b/src/ResponseTrait.php index ad0f4e4..6584f29 100644 --- a/src/ResponseTrait.php +++ b/src/ResponseTrait.php @@ -5,6 +5,10 @@ use Firehed\U2F\InvalidDataException as IDE; +/** + * @deprecated + * @internal + */ trait ResponseTrait { use KeyHandleTrait; 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; 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; 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, 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 { 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/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 { diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 6b1eb22..5058ab6 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 = [ @@ -151,6 +160,9 @@ public function testSetRegistrationsReturnsSelf(): void ); } + /** + * @deprecated + */ public function testSetRegistrationsEnforcesTypeCheck(): void { $wrong = true; @@ -172,6 +184,9 @@ public function testSetSignRequestsReturnsSelf(): void ); } + /** + * @deprecated + */ public function testSetSignRequestsEnforcesTypeCheck(): void { $wrong = true; @@ -233,11 +248,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, @@ -270,7 +285,7 @@ public function testRegistration(): void public function testRegisterDefaultsToTryingEmptyCAList(): void { - $request = $this->getDefaultRegisterRequest(); + $challenge = $this->getDefaultRegistrationChallenge(); $response = $this->getDefaultRegistrationResponse(); $this->expectException(SecurityException::class); @@ -279,25 +294,34 @@ 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 { - // 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 { - $request = $this->getDefaultRegisterRequest(); + $challenge = $this->getDefaultRegistrationChallenge(); $response = $this->getDefaultRegistrationResponse(); $this->server->setTrustedCAs([ @@ -307,12 +331,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 @@ -323,7 +347,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'); @@ -335,79 +359,38 @@ public function testRegisterWorksWithCAList(): void public function testRegisterThrowsWithChangedApplicationParameter(): void { - $request = $this->getDefaultRegisterRequest(); - - $response = $this->createMock(RegistrationResponseInterface::class); - $response->method('getChallenge') - ->willReturn($request->getChallenge()); - $response->method('getRpIdHash') - ->willReturn(hash('sha256', 'https://some.otherdomain.com', true)); + $challenge = $this->getDefaultRegistrationChallenge(); + $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); + $this->server->validateRegistration($challenge, $response); } - public function testRegisterThrowsWithChangedChallengeParameter(): void + public function testRegisterThrowsWithChangedSignedData(): 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); - } - - public function testRegisterThrowsWithChangedPubkey(): 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 )----------------------------------------------------- @@ -469,25 +452,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' ); } @@ -500,11 +483,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 @@ -512,55 +495,47 @@ 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 { // 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]); } /** @@ -587,16 +562,28 @@ 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]); + } + + 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]); } /** @@ -616,20 +603,21 @@ 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) )---------------------------------------- + /** + * @deprecated + */ public function testRegistrationWithoutCidPubkeyBug14Case1(): void { $registerRequest = new RegisterRequest(); @@ -657,6 +645,9 @@ public function testRegistrationWithoutCidPubkeyBug14Case1(): void $this->assertInstanceOf(Registration::class, $registration); } + /** + * @deprecated + */ public function testRegistrationWithoutCidPubkeyBug14Case2(): void { $registerRequest = new RegisterRequest(); @@ -686,6 +677,9 @@ public function testRegistrationWithoutCidPubkeyBug14Case2(): void // -( Helpers )------------------------------------------------------------ + /** + * @deprecated + */ private function getDefaultRegisterRequest(): RegisterRequest { // This would have come from a session, database, etc. @@ -694,6 +688,11 @@ private function getDefaultRegisterRequest(): RegisterRequest ->setChallenge('PfsWR1Umy2V5Al1Bam2tG0yfPLeJElfwRzzAzkYPgzo'); } + private function getDefaultRegistrationChallenge(): ChallengeProviderInterface + { + return new Challenge('PfsWR1Umy2V5Al1Bam2tG0yfPLeJElfwRzzAzkYPgzo'); + } + /** * @deprecated */ @@ -702,11 +701,21 @@ 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(). - $mock = self::createMock(RegistrationResponseInterface::class); $keyHandleBinary = hex2bin( '6d4a7a7393fa51cf24dbe035f26cacc9868a9385320a099b17062ac0ddc11fc0'. '0cb96b1a8fffe4736b7144c508fc343af81c104ba25e086ee5c1ba71da0c7d6d' @@ -717,35 +726,45 @@ 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', // getDefaultRegistrationChallenge + 'getKeyHandleBinary' => $keyHandleBinary, + 'getPublicKey' => $pk, + 'getRpIdHash' => hash('sha256', 'https://u2f.ericstern.com', true), + 'getSignature' => $signature, + 'getSignedData' => $signedData, + ]; + + $data = array_merge($defaults, $overrides); + + $mock = self::createMock(RegistrationResponseInterface::class); + foreach ($data as $method => $value) { + $mock->method($method)->willReturn($value); + } return $mock; } + /** + * @deprecated + */ private function getDefaultSignRequest(): SignRequest { // This would have come from a session, database, etc @@ -756,47 +775,88 @@ private function getDefaultSignRequest(): SignRequest ; } - private function getDefaultRegistration(): RegistrationInterface + private function getDefaultLoginChallenge(): ChallengeProviderInterface + { + return new Challenge('wt2ze8IskcTO3nIsO2D2hFjE5tVD041NpnYesLpJweg'); + } + + /** + * @param array{ + * counter?: int, + * keyHandle?: string, + * publicKey?: PublicKeyInterface, + * } $overrides + */ + private function getDefaultRegistration(array $overrides = []): RegistrationInterface { + $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); // 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(2) + ->setPublicKey($data['publicKey']) + ->setCounter($data['counter']) ; } - 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; } @@ -842,32 +902,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; - } } 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 {