From d81101a2f472a04b95a96e4848c920c9d0baa8cc Mon Sep 17 00:00:00 2001 From: Evgenii Morozov Date: Thu, 25 Sep 2025 08:12:14 +0200 Subject: [PATCH 1/2] implement simple ASN1 decoding --- vicephp/Virtue-JWT/src/Encoding/ASN1.php | 62 ++++++++++++++++--- .../Virtue-JWT/tests/Encoding/ASN1Test.php | 35 +++++++++++ 2 files changed, 89 insertions(+), 8 deletions(-) diff --git a/vicephp/Virtue-JWT/src/Encoding/ASN1.php b/vicephp/Virtue-JWT/src/Encoding/ASN1.php index 37fc3be..8244204 100644 --- a/vicephp/Virtue-JWT/src/Encoding/ASN1.php +++ b/vicephp/Virtue-JWT/src/Encoding/ASN1.php @@ -4,13 +4,12 @@ class ASN1 { - private const INTEGER = 2; - private const BIT_STRING = 3; - private const OCTET_STRING = 4; - private const OBJECT_IDENTIFIER = 6; - private const NULL = 5; - private const SEQUENCE = 48; - + public const INTEGER = 2; + public const BIT_STRING = 3; + public const OCTET_STRING = 4; + public const OBJECT_IDENTIFIER = 6; + public const NULL = 5; + public const SEQUENCE = 48; /** @var int */ private $type; @@ -18,10 +17,14 @@ class ASN1 /** @var string */ private $bytes; - private function __construct(int $type, string $bytes) + /** @var string */ + private $rest; + + private function __construct(int $type, string $bytes, string $rest = "") { $this->type = $type; $this->bytes = $bytes; + $this->rest = $rest; } public function encode(): string @@ -100,4 +103,47 @@ public static function null(): self { return new self(self::NULL, ""); } + + public static function decode(string $string): self + { + $offset = 0; + assert(strlen($string) > 1); + $type = ord($string[$offset++]); + + $length = ord($string[$offset++]); + if ($length & 0x80) { + $temp = $length & ~0x80; + $result = \unpack("N", str_pad(substr($string, $offset, $temp), 4, "\00", STR_PAD_LEFT)); + assert($result !== false); + assert(count($result) == 1); + $length = array_shift($result); + assert(is_int($length)); + $offset += $temp; + } + + $bytes = substr($string, $offset, $length); + $rest = substr($string, $offset + $length); + + return new self($type, $bytes, $rest); + } + + public function bytes(): string + { + return $this->bytes; + } + + public function type(): int + { + return $this->type; + } + + public function rest(): string + { + return $this->rest; + } + + public function length(): int + { + return strlen($this->bytes); + } } diff --git a/vicephp/Virtue-JWT/tests/Encoding/ASN1Test.php b/vicephp/Virtue-JWT/tests/Encoding/ASN1Test.php index 3719222..d1ddddb 100644 --- a/vicephp/Virtue-JWT/tests/Encoding/ASN1Test.php +++ b/vicephp/Virtue-JWT/tests/Encoding/ASN1Test.php @@ -48,4 +48,39 @@ public function testOctetString(): void $asn1 = ASN1::octstr("Hello World"); $this->assertEquals("BAtIZWxsbyBXb3JsZA==", $asn1->__toString()); } + + public function testDecode(): void + { + $longString = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" . + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" . + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" . + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" . + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" . + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" . + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" . + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + $asn1 = ASN1::seq( + ASN1::int(\strrev(\pack('s', 1337))), + ASN1::bitstr($longString), + ); + + $block = ASN1::decode($asn1->encode()); + $this->assertEquals(ASN1::SEQUENCE, $block->type()); + $this->assertEquals(521, $block->length()); + $this->assertEmpty($block->rest()); + + $block = ASN1::decode($block->bytes()); + $this->assertEquals(ASN1::INTEGER, $block->type()); + $this->assertEquals(2, $block->length()); + $this->assertEquals(\strrev(\pack('s', 1337)), $block->bytes()); + $this->assertNotEmpty($block->rest()); + + $block = ASN1::decode($block->rest()); + $this->assertEquals(ASN1::BIT_STRING, $block->type()); + $this->assertEquals(strlen($longString) + 1, $block->length()); + $this->assertEquals("\00" . $longString, $block->bytes()); + $this->assertEmpty($block->rest()); + + } + } From f4bab720d950e86ae1bb9893181253ac0bdd7efc Mon Sep 17 00:00:00 2001 From: Evgenii Morozov Date: Thu, 25 Sep 2025 08:59:39 +0200 Subject: [PATCH 2/2] correctly pack JOSE signatures for ECDSA --- .../src/JWT/Algorithms/OpenSSLSign.php | 23 ++++++++++++++++++- .../src/JWT/Algorithms/OpenSSLVerify.php | 12 ++++++++++ .../tests/JWT/Algorithms/OpenSSLTest.php | 1 - 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/vicephp/Virtue-JWT/src/JWT/Algorithms/OpenSSLSign.php b/vicephp/Virtue-JWT/src/JWT/Algorithms/OpenSSLSign.php index 3994e16..0c0648a 100644 --- a/vicephp/Virtue-JWT/src/JWT/Algorithms/OpenSSLSign.php +++ b/vicephp/Virtue-JWT/src/JWT/Algorithms/OpenSSLSign.php @@ -2,6 +2,7 @@ namespace Virtue\JWT\Algorithms; +use Virtue\Encoding\ASN1; use Virtue\JWK\AsymmetricKey; use Virtue\JWK\Key\OpenSSL\Exportable; use Virtue\JWT\Algorithm; @@ -58,6 +59,27 @@ public function sign(string $msg): string $signature = ''; $success = \openssl_sign($msg, $signature, $private, $this->supported[$this->name]); + + Assert::string($signature); + $ecPadding = [ + 'ES256' => 32, + 'ES384' => 48, + 'ES512' => 66, + ]; + if (array_key_exists($this->private->alg(), $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->private->alg()], "\00", STR_PAD_LEFT); + + $block = ASN1::decode($block->rest()); + assert($block->type() == ASN1::INTEGER); + $y = str_pad(ltrim($block->bytes(), "\00"), $ecPadding[$this->private->alg()], "\00", STR_PAD_LEFT); + $signature = $x . $y; + } + //TODO remove together with the support of PHP versions < 8.0 if (version_compare(PHP_VERSION, '8.0.0') < 0) { \openssl_pkey_free($private); @@ -65,7 +87,6 @@ public function sign(string $msg): string if (!$success) { throw new SignFailed('OpenSSL error: ' . \openssl_error_string()); } else { - Assert::string($signature); return $signature; } } diff --git a/vicephp/Virtue-JWT/src/JWT/Algorithms/OpenSSLVerify.php b/vicephp/Virtue-JWT/src/JWT/Algorithms/OpenSSLVerify.php index fc82834..3080eca 100644 --- a/vicephp/Virtue-JWT/src/JWT/Algorithms/OpenSSLVerify.php +++ b/vicephp/Virtue-JWT/src/JWT/Algorithms/OpenSSLVerify.php @@ -2,6 +2,7 @@ namespace Virtue\JWT\Algorithms; +use Virtue\Encoding\ASN1; use Virtue\JWK\AsymmetricKey; use Virtue\JWK\Key\EdDSA; use Virtue\JWT\Algorithm; @@ -63,6 +64,17 @@ public function verify(Token $token): void if (!$public = \openssl_pkey_get_public($this->public->asPem())) { throw new VerificationFailed('Key is invalid.', VerificationFailed::ON_SIGNATURE); } + $ecPadding = [ + 'ES256' => 32, + 'ES384' => 48, + 'ES512' => 66, + ]; + if (array_key_exists($alg, $ecPadding)) { + $x = substr($sig, 0, $ecPadding[$alg]); + $y = substr($sig, $ecPadding[$alg]); + $sig = ASN1::seq(ASN1::uint(ltrim($x, "\00")), ASN1::uint(ltrim($y, "\00"))); + $sig = $sig->encode(); + } // returns 1 on success, 0 on failure, -1 on error. $success = \openssl_verify($msg, $sig, $public, $this->supported[$alg]); diff --git a/vicephp/Virtue-JWT/tests/JWT/Algorithms/OpenSSLTest.php b/vicephp/Virtue-JWT/tests/JWT/Algorithms/OpenSSLTest.php index 66d07be..4e3e539 100644 --- a/vicephp/Virtue-JWT/tests/JWT/Algorithms/OpenSSLTest.php +++ b/vicephp/Virtue-JWT/tests/JWT/Algorithms/OpenSSLTest.php @@ -34,7 +34,6 @@ public function testSignRSA(string $alg): void $private = new OpenSSL\PrivateKey($alg, $private); $details = \openssl_pkey_get_details($key); - /* var_dump($details['key']); */ $this->assertNotFalse($details); Assert::isMap($details['rsa']); Assert::string($details['rsa']['n']);