From 5743bb42f4760ac5ae4c775f1146bb8ee64102f2 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Mon, 2 Aug 2021 20:19:25 -0400 Subject: [PATCH] Consistent encoding of ECDSA public keys, with tests --- src/Keys/AsymmetricPublicKey.php | 35 +++++++++++++++++++++++++++++++- src/Protocol/Version3.php | 6 +++--- tests/Version1Test.php | 13 ++++++++++++ tests/Version2Test.php | 13 ++++++++++++ tests/Version3Test.php | 16 ++++++++++++++- tests/Version4Test.php | 19 +++++++++++++++++ 6 files changed, 97 insertions(+), 5 deletions(-) diff --git a/src/Keys/AsymmetricPublicKey.php b/src/Keys/AsymmetricPublicKey.php index e956d206..c52b3656 100644 --- a/src/Keys/AsymmetricPublicKey.php +++ b/src/Keys/AsymmetricPublicKey.php @@ -14,6 +14,7 @@ ProtocolInterface, Util }; +use FG\ASN1\Exception\ParserException; use ParagonIE\Paseto\Protocol\{ Version1, Version2, @@ -143,9 +144,17 @@ public static function v4(string $keyMaterial): self * @return string * * @throws TypeError + * @throws ParserException */ public function encode(): string { + if (hash_equals($this->protocol::header(), Version3::HEADER)) { + return Base64UrlSafe::encodeUnpadded( + Hex::decode( + Version3::getPublicKeyCompressed($this->key) + ) + ); + } return Base64UrlSafe::encodeUnpadded($this->key); } @@ -161,10 +170,34 @@ public function encode(): string */ public static function fromEncodedString(string $encoded, ProtocolInterface $version = null): self { - $decoded = Base64UrlSafe::decode($encoded); + if (hash_equals($version::header(), Version3::HEADER)) { + $decoded = Version3::getPublicKeyPem( + Hex::encode( + Base64UrlSafe::decode($encoded) + ) + ); + } else { + $decoded = Base64UrlSafe::decode($encoded); + } return new static($decoded, $version); } + /** + * @return string + */ + public function toHexString(): string + { + if (hash_equals($this->protocol::header(), Version3::HEADER)) { + if (Binary::safeStrlen($this->key) === 98) { + return $this->key; + } + if (Binary::safeStrlen($this->key) !== 49) { + return Version3::getPublicKeyCompressed($this->key); + } + } + return Hex::encode($this->key); + } + /** * Get the version of PASETO that this key is intended for. * diff --git a/src/Protocol/Version3.php b/src/Protocol/Version3.php index aa63a272..4fc600a4 100644 --- a/src/Protocol/Version3.php +++ b/src/Protocol/Version3.php @@ -260,7 +260,7 @@ public static function sign( $header = self::HEADER . '.public.'; $easyEcc = new EasyECC(self::CURVE); // PASETO Version 3 - Sign - Step 2 & 3: - $pk = Hex::decode($key->getPublicKey()->raw()); + $pk = Hex::decode($key->getPublicKey()->toHexString()); if (Binary::safeStrlen($pk) !== 49) { throw new PasetoException( 'Invalid public key length: ' . Binary::safeStrlen($pk), @@ -340,7 +340,7 @@ public static function verify( $easyEcc = new EasyECC(self::CURVE); // PASETO Version 3 - Verify - Step 4 & 5: - $pk = Hex::decode($key->raw()); + $pk = Hex::decode($key->toHexString()); if (Binary::safeStrlen($pk) !== 49) { throw new PasetoException( 'Invalid public key length: ' . Binary::safeStrlen($pk), @@ -349,7 +349,7 @@ public static function verify( } $valid = $easyEcc->verify( Util::preAuthEncode($pk, $givenHeader, $message, $footer, $implicit), - PublicKey::fromString($key->raw(), 'P384'), + PublicKey::fromString($key->toHexString(), 'P384'), Hex::encode($signature), true ); diff --git a/tests/Version1Test.php b/tests/Version1Test.php index 79bd5e67..91c0e37f 100644 --- a/tests/Version1Test.php +++ b/tests/Version1Test.php @@ -54,6 +54,19 @@ public function testNonceDerivation() ); } + public function testPublicKeyEncode() + { + $sk = AsymmetricSecretKey::generate(); + $pk = $sk->getPublicKey(); + + $encoded = $pk->encode(); + $decoded = AsymmetricPublicKey::fromEncodedString($encoded, new Version1()); + $this->assertSame( + $pk->raw(), + $decoded->raw() + ); + } + /** * @covers Version1::decrypt() * @covers Version1::encrypt() diff --git a/tests/Version2Test.php b/tests/Version2Test.php index 34b799f7..b2957f62 100644 --- a/tests/Version2Test.php +++ b/tests/Version2Test.php @@ -32,6 +32,19 @@ public function testKeyGen() $this->assertSame(64, Binary::safeStrlen($secret->raw())); } + public function testPublicKeyEncode() + { + $sk = AsymmetricSecretKey::generate(); + $pk = $sk->getPublicKey(); + + $encoded = $pk->encode(); + $decoded = AsymmetricPublicKey::fromEncodedString($encoded, new Version2()); + $this->assertSame( + $pk->raw(), + $decoded->raw() + ); + } + /** * @covers Version2::decrypt() * @covers Version2::encrypt() diff --git a/tests/Version3Test.php b/tests/Version3Test.php index db9023c8..1ba8ad34 100644 --- a/tests/Version3Test.php +++ b/tests/Version3Test.php @@ -4,7 +4,8 @@ use ParagonIE\ConstantTime\Binary; use ParagonIE\Paseto\Exception\InvalidVersionException; use ParagonIE\Paseto\Exception\PasetoException; -use ParagonIE\Paseto\Keys\AsymmetricSecretKey; +use ParagonIE\Paseto\Keys\Version3\AsymmetricPublicKey; +use ParagonIE\Paseto\Keys\Version3\AsymmetricSecretKey; use ParagonIE\Paseto\Keys\Version3\SymmetricKey; use ParagonIE\Paseto\Protocol\{ Version2, @@ -31,6 +32,19 @@ public function testKeyGen() $this->assertGreaterThanOrEqual(48, Binary::safeStrlen($secret->raw())); // PEM encoded } + public function testPublicKeyEncode() + { + $sk = AsymmetricSecretKey::generate(new Version3); + $pk = $sk->getPublicKey(); + + $encoded = $pk->encode(); + $decoded = AsymmetricPublicKey::fromEncodedString($encoded, new Version3()); + $this->assertSame( + $pk->raw(), + $decoded->raw() + ); + } + /** * @covers Version3::decrypt() * @covers Version3::encrypt() diff --git a/tests/Version4Test.php b/tests/Version4Test.php index b536f91a..40c06274 100644 --- a/tests/Version4Test.php +++ b/tests/Version4Test.php @@ -1,9 +1,12 @@ assertGreaterThanOrEqual(48, Binary::safeStrlen($secret->raw())); // PEM encoded } + /** + * @throws ParserException + */ + public function testPublicKeyEncode() + { + $sk = AsymmetricSecretKey::generate(); + $pk = $sk->getPublicKey(); + + $encoded = $pk->encode(); + $decoded = AsymmetricPublicKey::fromEncodedString($encoded, new Version4()); + $this->assertSame( + $pk->raw(), + $decoded->raw() + ); + } + /** * @covers Version4::decrypt() * @covers Version4::encrypt()