diff --git a/.ci-tools/phpstan-baseline.neon b/.ci-tools/phpstan-baseline.neon index f4e341440..d1c856899 100644 --- a/.ci-tools/phpstan-baseline.neon +++ b/.ci-tools/phpstan-baseline.neon @@ -3778,6 +3778,16 @@ parameters: count: 1 path: ../src/Bundle/Resources/config/Algorithms/signature_eddsa.php + - + rawMessage: ''' + Access to constant on deprecated class Jose\Experimental\Signature\ES256K: + since 4.3.0, will be removed in 5.0.0. "ES256K" is a standard algorithm (RFC 8812) and moved to the + library: use Jose\Component\Signature\Algorithm\ES256K instead. + ''' + identifier: classConstant.deprecatedClass + count: 1 + path: ../src/Bundle/Resources/config/Algorithms/signature_experimental.php + - rawMessage: Access to constant on internal class Jose\Component\Core\Util\Ecc\NistCurve. identifier: classConstant.internalClass diff --git a/src/Bundle/Resources/config/Algorithms/signature_ecdsa.php b/src/Bundle/Resources/config/Algorithms/signature_ecdsa.php index fdbac9008..c28d3c12e 100644 --- a/src/Bundle/Resources/config/Algorithms/signature_ecdsa.php +++ b/src/Bundle/Resources/config/Algorithms/signature_ecdsa.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Jose\Component\Signature\Algorithm\ES256; +use Jose\Component\Signature\Algorithm\ES256K; use Jose\Component\Signature\Algorithm\ES384; use Jose\Component\Signature\Algorithm\ES512; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; @@ -28,4 +29,9 @@ ->tag('jose.algorithm', [ 'alias' => 'ES512', ]); + + $container->set(ES256K::class) + ->tag('jose.algorithm', [ + 'alias' => 'ES256K', + ]); }; diff --git a/src/Bundle/Resources/config/Algorithms/signature_experimental.php b/src/Bundle/Resources/config/Algorithms/signature_experimental.php index e29575caa..58c5d6e5a 100644 --- a/src/Bundle/Resources/config/Algorithms/signature_experimental.php +++ b/src/Bundle/Resources/config/Algorithms/signature_experimental.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Jose\Component\Signature\Algorithm\ES256K as StandardES256K; use Jose\Experimental\Signature\Blake2b; use Jose\Experimental\Signature\ES256K; use Jose\Experimental\Signature\HS1; @@ -40,9 +41,11 @@ ]); $container->set(ES256K::class) - ->tag('jose.algorithm', [ - 'alias' => 'ES256K', - ]); + ->deprecate( + 'web-token/jwt-framework', + '4.3.0', + 'The "%service_id%" service is deprecated: the "ES256K" algorithm moved to the library and is registered as "' . StandardES256K::class . '", under the same "ES256K" alias.' + ); $container->set(Blake2b::class) ->tag('jose.algorithm', [ diff --git a/src/Bundle/Resources/config/analyzers.php b/src/Bundle/Resources/config/analyzers.php index af927c0ee..867ccec55 100644 --- a/src/Bundle/Resources/config/analyzers.php +++ b/src/Bundle/Resources/config/analyzers.php @@ -5,6 +5,7 @@ use Jose\Component\Core\Util\Ecc\NistCurve; use Jose\Component\KeyManagement\Analyzer\AlgorithmAnalyzer; use Jose\Component\KeyManagement\Analyzer\ES256KeyAnalyzer; +use Jose\Component\KeyManagement\Analyzer\ES256KKeyAnalyzer; use Jose\Component\KeyManagement\Analyzer\ES384KeyAnalyzer; use Jose\Component\KeyManagement\Analyzer\ES512KeyAnalyzer; use Jose\Component\KeyManagement\Analyzer\HS256KeyAnalyzer; @@ -51,6 +52,7 @@ if (class_exists(NistCurve::class)) { $container->set(ES256KeyAnalyzer::class); + $container->set(ES256KKeyAnalyzer::class); $container->set(ES384KeyAnalyzer::class); $container->set(ES512KeyAnalyzer::class); } diff --git a/src/Experimental/Signature/ES256K.php b/src/Experimental/Signature/ES256K.php index f15cf8660..062ba42e6 100644 --- a/src/Experimental/Signature/ES256K.php +++ b/src/Experimental/Signature/ES256K.php @@ -4,26 +4,24 @@ namespace Jose\Experimental\Signature; -use Jose\Component\Signature\Algorithm\ECDSA; -use Override; +use Jose\Component\Signature\Algorithm\ES256K as StandardES256K; +use function trigger_deprecation; -final readonly class ES256K extends ECDSA +/** + * @deprecated since 4.3.0, will be removed in 5.0.0. "ES256K" is a standard algorithm (RFC 8812) and moved to the + * library: use Jose\Component\Signature\Algorithm\ES256K instead. + */ +final readonly class ES256K extends StandardES256K { - #[Override] - public function name(): string + public function __construct() { - return 'ES256K'; - } - - #[Override] - protected function getHashAlgorithm(): string - { - return 'sha256'; - } - - #[Override] - protected function getSignaturePartLength(): int - { - return 64; + parent::__construct(); + trigger_deprecation( + 'web-token/jwt-framework', + '4.3.0', + 'The class "%s" is deprecated and will be removed in 5.0.0. The "ES256K" algorithm is a standard one and moved to the library: use "%s" instead.', + self::class, + StandardES256K::class + ); } } diff --git a/src/Library/Core/Util/Ecc/KoblitzCurve.php b/src/Library/Core/Util/Ecc/KoblitzCurve.php new file mode 100644 index 000000000..3ce9da98f --- /dev/null +++ b/src/Library/Core/Util/Ecc/KoblitzCurve.php @@ -0,0 +1,29 @@ +getContainer(); + + /** @var AlgorithmManagerFactory $factory */ + $factory = $container->get(AlgorithmManagerFactory::class); + $algorithm = $factory->create(['ES256K'])->get('ES256K'); + static::assertSame(StandardES256K::class, $algorithm::class); + + $consumer = $container->get(DeprecatedES256KConsumer::class); + static::assertInstanceOf(DeprecatedES256KConsumer::class, $consumer); + static::assertInstanceOf(ES256K::class, $consumer->algorithm); + static::assertSame('ES256K', $consumer->algorithm->name()); + } + /** * Two of them reported the name of another one, so they replaced it in the manager and were unreachable. */ @@ -87,7 +112,6 @@ public static function aliases(): iterable yield 'RS1' => ['RS1']; yield 'HS1' => ['HS1']; yield 'HS256/64' => ['HS256/64']; - yield 'ES256K' => ['ES256K']; yield 'BLAKE2B' => ['BLAKE2B']; yield 'A128CTR' => ['A128CTR']; yield 'A192CTR' => ['A192CTR']; diff --git a/tests/Bundle/JoseFramework/TestBundle/Resources/config/services.php b/tests/Bundle/JoseFramework/TestBundle/Resources/config/services.php index 54dfe7231..7e04aa59a 100644 --- a/tests/Bundle/JoseFramework/TestBundle/Resources/config/services.php +++ b/tests/Bundle/JoseFramework/TestBundle/Resources/config/services.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Jose\Tests\Bundle\JoseFramework\TestBundle\Checker\CustomChecker; +use Jose\Tests\Bundle\JoseFramework\TestBundle\Service\DeprecatedES256KConsumer; use Jose\Tests\Bundle\JoseFramework\TestBundle\Service\NestedTokenServiceConsumer; use Psr\Clock\ClockInterface; use Symfony\Component\Clock\NativeClock; @@ -30,4 +31,8 @@ $container->set(NestedTokenServiceConsumer::class) ->public() ; + + $container->set(DeprecatedES256KConsumer::class) + ->public() + ; }; diff --git a/tests/Bundle/JoseFramework/TestBundle/Service/DeprecatedES256KConsumer.php b/tests/Bundle/JoseFramework/TestBundle/Service/DeprecatedES256KConsumer.php new file mode 100644 index 000000000..23ee6affb --- /dev/null +++ b/tests/Bundle/JoseFramework/TestBundle/Service/DeprecatedES256KConsumer.php @@ -0,0 +1,18 @@ +getKey(); + $algorithm = new ES256K(); + $data = 'Hello'; + + static::assertTrue($algorithm->verify($key, $data, hex2bin( + '9c75b9d171d9690a37f2474d4bfab5c234911cb150950ea5cbfc9aedda5ec360725cc47978de95b4efb2a3ed617c7b36b1cd0a26b536662a79d0f3ae873a7924' + ))); + } + + #[Test] + public function es256KSignAndVerify(): void + { + $key = $this->getKey(); + $algorithm = new ES256K(); + $data = 'Hello'; + + static::assertSame('ES256K', $algorithm->name()); + + $signature = $algorithm->sign($key, $data); + + static::assertTrue($algorithm->verify($key, $data, $signature)); + } + + /** + * @return iterable + */ + public static function serializers(): iterable + { + yield 'compact' => [new CompactSerializer()]; + yield 'flattened' => [new JSONFlattenedSerializer()]; + yield 'general' => [new JSONGeneralSerializer()]; + } + + #[Test] + #[DataProvider('serializers')] + public function aTokenIsSignedAndVerifiedThroughEverySerializer(JWSSerializer $serializer): void + { + $key = (new JWKFactory())->ec('secp256k1', [ + 'alg' => 'ES256K', + ]); + $manager = new AlgorithmManager([new ES256K()]); + + $jws = (new JWSBuilder($manager)) + ->withPayload('{"iss":"me"}') + ->addSignature($key, [ + 'alg' => 'ES256K', + ]) + ->build(); + $loaded = $serializer->unserialize($serializer->serialize($jws, 0)); + + static::assertTrue((new JWSVerifier($manager))->verify($loaded, $key->toPublic(), 0)->isVerified()); + static::assertSame('ES256K', $loaded->getSignature(0)->getProtectedHeaderParameter('alg')); + } + + #[Test] + #[IgnoreDeprecations] + public function theExperimentalClassIsADeprecatedSubclassOfTheLibraryOne(): void + { + $algorithm = new ExperimentalES256K(); + + static::assertInstanceOf(ES256K::class, $algorithm); + static::assertSame('ES256K', $algorithm->name()); + static::assertTrue($algorithm->verify($this->getKey(), 'Hello', (new ES256K())->sign($this->getKey(), 'Hello'))); + } + + #[Test] + public function theSecp256k1GeneratorIsOnTheCurve(): void + { + $curve = KoblitzCurve::secp256k1(); + $generator = $curve->getGenerator(); + + static::assertTrue($curve->contains($generator->getX(), $generator->getY())); + static::assertSame(256, $curve->getSize()); + } + + #[Test] + public function theKeyAnalyzerAcceptsTheKeyAndRejectsAPointOffTheCurve(): void + { + $bag = new MessageBag(); + (new ES256KKeyAnalyzer())->analyze($this->getKey(), $bag); + static::assertCount(0, $bag); + + $values = $this->getKey() + ->all(); + $values['y'] = $values['x']; + $bag = new MessageBag(); + (new ES256KKeyAnalyzer())->analyze(new JWK($values), $bag); + static::assertCount(1, $bag); + static::assertSame('Invalid key. The point is not on the curve.', $bag->all()[0]->getMessage()); + } + + private function getKey(): JWK + { + return new JWK([ + 'kty' => 'EC', + 'crv' => 'secp256k1', + 'd' => Base64UrlSafe::encodeUnpadded( + hex2bin('D1592A94BBB9B5D94CDC425FC7DA80B6A47863AE973A9D581FD9D8F29690B659') + ), + 'x' => Base64UrlSafe::encodeUnpadded( + hex2bin('4B4DF318DE05BB8F3A115BF337F9BCBC55CA14B917B46BCB557D3C9A158D4BE0') + ), + 'y' => Base64UrlSafe::encodeUnpadded( + hex2bin('627EB75731A8BBEBC7D9A3C57EC4D7DA2CBA6D2A28E7F45134921861FE1CF5D9') + ), + ]); + } +} diff --git a/tests/SignatureAlgorithm/Experimental/P256KSignatureTest.php b/tests/SignatureAlgorithm/Experimental/P256KSignatureTest.php deleted file mode 100644 index 4964d81e7..000000000 --- a/tests/SignatureAlgorithm/Experimental/P256KSignatureTest.php +++ /dev/null @@ -1,60 +0,0 @@ -getKey(); - $algorithm = new ES256K(); - $data = 'Hello'; - - static::assertTrue($algorithm->verify($key, $data, hex2bin( - '9c75b9d171d9690a37f2474d4bfab5c234911cb150950ea5cbfc9aedda5ec360725cc47978de95b4efb2a3ed617c7b36b1cd0a26b536662a79d0f3ae873a7924' - ))); - } - - #[Test] - public function es256KSignAndVerify(): void - { - $key = $this->getKey(); - $algorithm = new ES256K(); - $data = 'Hello'; - - static::assertSame('ES256K', $algorithm->name()); - - $signature = $algorithm->sign($key, $data); - - static::assertTrue($algorithm->verify($key, $data, $signature)); - } - - private function getKey(): JWK - { - return new JWK([ - 'kty' => 'EC', - 'crv' => 'secp256k1', - 'd' => Base64UrlSafe::encodeUnpadded( - hex2bin('D1592A94BBB9B5D94CDC425FC7DA80B6A47863AE973A9D581FD9D8F29690B659') - ), - 'x' => Base64UrlSafe::encodeUnpadded( - hex2bin('4B4DF318DE05BB8F3A115BF337F9BCBC55CA14B917B46BCB557D3C9A158D4BE0') - ), - 'y' => Base64UrlSafe::encodeUnpadded( - hex2bin('627EB75731A8BBEBC7D9A3C57EC4D7DA2CBA6D2A28E7F45134921861FE1CF5D9') - ), - ]); - } -}