From af57ca4fd3cef1efa57c26ad17db988012551197 Mon Sep 17 00:00:00 2001 From: Ievgenii Skliarenko Date: Fri, 30 Jan 2026 10:20:18 +0100 Subject: [PATCH 1/2] Add support for DER-to-ECDSA signature formatting in `AwsKmsSign` - Decode ASN.1 DER-encoded signatures into raw R and S components for ECDSA algorithms. - Implement padding logic for `ES256`, `ES384`, and `ES512` to ensure fixed-length signatures. - Add comprehensive unit tests to validate the changes, including edge cases for padding and negative integers. --- .../src/JWT/Algorithms/AwsKmsSign.php | 28 ++++- .../tests/JWT/Algorithms/AwsKmsSignTest.php | 113 ++++++++++++++++++ 2 files changed, 138 insertions(+), 3 deletions(-) diff --git a/vicephp/Virtue-JWT/src/JWT/Algorithms/AwsKmsSign.php b/vicephp/Virtue-JWT/src/JWT/Algorithms/AwsKmsSign.php index b938d84..264d8ad 100644 --- a/vicephp/Virtue-JWT/src/JWT/Algorithms/AwsKmsSign.php +++ b/vicephp/Virtue-JWT/src/JWT/Algorithms/AwsKmsSign.php @@ -3,6 +3,7 @@ namespace Virtue\JWT\Algorithms; use Virtue\Aws\KmsClient; +use Virtue\Encoding\ASN1; use Virtue\JWT\Algorithm; use Virtue\JWT\SignFailed; use Virtue\JWT\SignsToken; @@ -45,12 +46,33 @@ public function sign(string $msg): string } $result = $this->client->sign([ - 'Message' => $msg, - 'MessageType' => 'RAW', + 'Message' => $msg, + 'MessageType' => 'RAW', 'SigningAlgorithm' => $this->signingAlgorithms[$this->name] ]); Assert::string($result['Signature'], 'Issue when signing the message'); - return $result['Signature']; + $signature = $result['Signature']; + + $ecPadding = [ + 'ES256' => 32, + 'ES384' => 48, + 'ES512' => 66, + ]; + if (array_key_exists($this->name, $ecPadding)) { + $block = ASN1::decode($signature); + assert($block->type() == ASN1::SEQUENCE); + + $block = ASN1::decode($block->bytes()); + assert($block->type() == ASN1::INTEGER); + $x = str_pad(ltrim($block->bytes(), "\00"), $ecPadding[$this->name], "\00", STR_PAD_LEFT); + + $block = ASN1::decode($block->rest()); + assert($block->type() == ASN1::INTEGER); + $y = str_pad(ltrim($block->bytes(), "\00"), $ecPadding[$this->name], "\00", STR_PAD_LEFT); + $signature = $x . $y; + } + + return $signature; } } diff --git a/vicephp/Virtue-JWT/tests/JWT/Algorithms/AwsKmsSignTest.php b/vicephp/Virtue-JWT/tests/JWT/Algorithms/AwsKmsSignTest.php index 0c71194..8b98251 100644 --- a/vicephp/Virtue-JWT/tests/JWT/Algorithms/AwsKmsSignTest.php +++ b/vicephp/Virtue-JWT/tests/JWT/Algorithms/AwsKmsSignTest.php @@ -8,6 +8,7 @@ use Mockery as M; use PHPUnit\Framework\TestCase; use Virtue\Aws\KmsClient; +use Virtue\Encoding\ASN1; use Virtue\JWT\SignFailed; class AwsKmsSignTest extends TestCase @@ -57,4 +58,116 @@ public function testSingMessage(): void $this->assertEquals('', $signature); } + + public function testSignMessageES256(): void + { + $message = 'message'; + $r = str_repeat("\x01", 32); + $s = str_repeat("\x02", 32); + $derSignature = ASN1::seq(ASN1::uint($r), ASN1::uint($s))->encode(); + + $handler = new MockHandler(); + $handler->append(function (CommandInterface $cmd) use ($derSignature) { + $this->assertEquals('ECDSA_SHA_256', $cmd->offsetGet('SigningAlgorithm')); + return new Result(['Signature' => $derSignature]); + }); + + $client = new KmsClient('key/alias', [ + 'version' => 'latest', + 'region' => 'eu-west-1', + 'handler' => $handler, + 'credentials' => ['key' => '', 'secret' => ''] + ]); + + $signer = new AwsKmsSign('ES256', $client); + $signature = $signer->sign($message); + + $this->assertEquals($r . $s, $signature); + $this->assertEquals(64, strlen($signature)); + } + + public function testSignMessageES384WithPadding(): void + { + $message = 'message'; + // ES384 expects 48 bytes for each component + $r = str_repeat("\x01", 30); // Needs padding + $s = str_repeat("\x02", 48); + $derSignature = ASN1::seq(ASN1::uint($r), ASN1::uint($s))->encode(); + + $handler = new MockHandler(); + $handler->append(function (CommandInterface $cmd) use ($derSignature) { + $this->assertEquals('ECDSA_SHA_384', $cmd->offsetGet('SigningAlgorithm')); + return new Result(['Signature' => $derSignature]); + }); + + $client = new KmsClient('key/alias', [ + 'version' => 'latest', + 'region' => 'eu-west-1', + 'handler' => $handler, + 'credentials' => ['key' => '', 'secret' => ''] + ]); + + $signer = new AwsKmsSign('ES384', $client); + $signature = $signer->sign($message); + + $expectedR = str_pad($r, 48, "\x00", STR_PAD_LEFT); + $this->assertEquals($expectedR . $s, $signature); + $this->assertEquals(96, strlen($signature)); + } + + public function testSignMessageES512(): void + { + $message = 'message'; + // ES512 expects 66 bytes for each component + $r = str_repeat("\x01", 66); + $s = str_repeat("\x02", 66); + $derSignature = ASN1::seq(ASN1::uint($r), ASN1::uint($s))->encode(); + + $handler = new MockHandler(); + $handler->append(function (CommandInterface $cmd) use ($derSignature) { + $this->assertEquals('ECDSA_SHA_512', $cmd->offsetGet('SigningAlgorithm')); + return new Result(['Signature' => $derSignature]); + }); + + $client = new KmsClient('key/alias', [ + 'version' => 'latest', + 'region' => 'eu-west-1', + 'handler' => $handler, + 'credentials' => ['key' => '', 'secret' => ''] + ]); + + $signer = new AwsKmsSign('ES512', $client); + $signature = $signer->sign($message); + + $this->assertEquals($r . $s, $signature); + $this->assertEquals(132, strlen($signature)); + } + + public function testSignMessageES256WithNegativeIntegers(): void + { + $message = 'message'; + // 0x80 is 128, which is negative in 8-bit signed integer + $r = "\x80" . str_repeat("\x01", 31); + $s = "\x80" . str_repeat("\x02", 31); + // ASN1::uint should add a leading \00 if the high bit is set + $derSignature = ASN1::seq(ASN1::uint($r), ASN1::uint($s))->encode(); + + $handler = new MockHandler(); + $handler->append(function (CommandInterface $cmd) use ($derSignature) { + return new Result(['Signature' => $derSignature]); + }); + + $client = new KmsClient('key/alias', [ + 'version' => 'latest', + 'region' => 'eu-west-1', + 'handler' => $handler, + 'credentials' => ['key' => '', 'secret' => ''] + ]); + + $signer = new AwsKmsSign('ES256', $client); + $signature = $signer->sign($message); + + $this->assertEquals($r . $s, $signature); + $this->assertEquals(64, strlen($signature)); + } } From dbdc49a14dcdb5bb643ad1b17570c16c22e0845e Mon Sep 17 00:00:00 2001 From: Eugen Skliarenko Date: Fri, 30 Jan 2026 10:24:01 +0100 Subject: [PATCH 2/2] Update vicephp/Virtue-JWT/src/JWT/Algorithms/AwsKmsSign.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- vicephp/Virtue-JWT/src/JWT/Algorithms/AwsKmsSign.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vicephp/Virtue-JWT/src/JWT/Algorithms/AwsKmsSign.php b/vicephp/Virtue-JWT/src/JWT/Algorithms/AwsKmsSign.php index 264d8ad..0fbbb18 100644 --- a/vicephp/Virtue-JWT/src/JWT/Algorithms/AwsKmsSign.php +++ b/vicephp/Virtue-JWT/src/JWT/Algorithms/AwsKmsSign.php @@ -65,12 +65,12 @@ public function sign(string $msg): string $block = ASN1::decode($block->bytes()); assert($block->type() == ASN1::INTEGER); - $x = str_pad(ltrim($block->bytes(), "\00"), $ecPadding[$this->name], "\00", STR_PAD_LEFT); + $r = str_pad(ltrim($block->bytes(), "\00"), $ecPadding[$this->name], "\00", STR_PAD_LEFT); $block = ASN1::decode($block->rest()); assert($block->type() == ASN1::INTEGER); - $y = str_pad(ltrim($block->bytes(), "\00"), $ecPadding[$this->name], "\00", STR_PAD_LEFT); - $signature = $x . $y; + $s = str_pad(ltrim($block->bytes(), "\00"), $ecPadding[$this->name], "\00", STR_PAD_LEFT); + $signature = $r . $s; } return $signature;