From dc46e195b0226cf406b71a32b35531388286efdb Mon Sep 17 00:00:00 2001 From: Kelvin Mo Date: Mon, 14 Feb 2022 20:57:44 +1100 Subject: [PATCH 1/3] Fix for PHP 8.1 compatibility In PHP 8.1, openssl_pkey_export() for EC keys uses a PKCS#8 wrapper instead of just a RFC 5915 private key. ECKey has to be updated to accept this new format. --- src/SimpleJWT/Keys/ECKey.php | 62 ++++++++++++++++++++++++++++--- src/SimpleJWT/Keys/KeyFactory.php | 2 +- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/SimpleJWT/Keys/ECKey.php b/src/SimpleJWT/Keys/ECKey.php index 193bfdf..13aed91 100644 --- a/src/SimpleJWT/Keys/ECKey.php +++ b/src/SimpleJWT/Keys/ECKey.php @@ -46,7 +46,8 @@ class ECKey extends Key { const KTY = 'EC'; - const PEM_PRIVATE = '/-----BEGIN EC PRIVATE KEY-----([^-:]+)-----END EC PRIVATE KEY-----/'; + const PEM_RFC5915_PRIVATE = '/-----BEGIN EC PRIVATE KEY-----([^-:]+)-----END EC PRIVATE KEY-----/'; + const PEM_PKCS8_PRIVATE = '/-----BEGIN PRIVATE KEY-----([^-:]+)-----END PRIVATE KEY-----/'; // used by PHP 8.1 const EC_OID = '1.2.840.10045.2.1'; const P256_OID = '1.2.840.10045.3.1.7'; @@ -149,23 +150,22 @@ public function __construct($data, $format, $password = null, $alg = 'PBES2-HS25 $jwk['crv'] = $curve; $jwk['x'] = Util::base64url_encode($x); $jwk['y'] = Util::base64url_encode($y); - } elseif (preg_match(self::PEM_PRIVATE, $data, $matches)) { + } elseif (preg_match(self::PEM_RFC5915_PRIVATE, $data, $matches)) { /** @var string|bool $der */ $der = base64_decode($matches[1]); - if ($der === FALSE) throw new KeyException('Cannot read PEM key'); $offset += ASN1::readDER($der, $offset, $data); // SEQUENCE $offset += ASN1::readDER($der, $offset, $version); // INTEGER - if (ord($version) != 1) throw new KeyException('Invalid private key version'); + if (ord($version) != 1) throw new KeyException('Invalid private key version: ' . ord($version)); $offset += ASN1::readDER($der, $offset, $d); // OCTET STRING [d] $offset += ASN1::readDER($der, $offset, $data); // SEQUENCE[0] $offset += ASN1::readDER($der, $offset, $curve_oid); // OBJECT IDENTIFIER - parameters $curve_oid = ASN1::decodeOID($curve_oid); - $curve = $this->getCurveNameFromOID($curve_oid); + $curve = self::getCurveNameFromOID($curve_oid); if ($curve == null) throw new KeyException('Unrecognised EC parameter: ' . $curve_oid); $len = self::$curves[$curve]['len']; @@ -184,6 +184,56 @@ public function __construct($data, $format, $password = null, $alg = 'PBES2-HS25 $jwk['d'] = Util::base64url_encode($d); $jwk['x'] = Util::base64url_encode($x); $jwk['y'] = Util::base64url_encode($y); + } elseif (preg_match(self::PEM_PKCS8_PRIVATE, $data, $matches)) { + /** @var string|bool $der */ + $der = base64_decode($matches[1]); + if ($der === FALSE) throw new KeyException('Cannot read PEM key'); + + $offset += ASN1::readDER($der, $offset, $data); // SEQUENCE + $offset += ASN1::readDER($der, $offset, $version); // INTEGER + + if (ord($version) != 0) throw new KeyException('Invalid private key version: ' . ord($version)); + + $offset += ASN1::readDER($der, $offset, $data); // SEQUENCE + $offset += ASN1::readDER($der, $offset, $key_oid); // OBJECT IDENTIFIER - id-ecPublicKey + + if (ASN1::decodeOID($key_oid) != self::EC_OID) throw new KeyException('Invalid key type: ' . ASN1::decodeOID($key_oid)); + + $offset += ASN1::readDER($der, $offset, $curve_oid); // OBJECT IDENTIFIER - parameters + $curve_oid = ASN1::decodeOID($curve_oid); + $curve = self::getCurveNameFromOID($curve_oid); + if ($curve == null) throw new KeyException('Unrecognised EC parameter: ' . $curve_oid); + + $len = self::$curves[$curve]['len']; + + $offset += ASN1::readDER($der, $offset, $private_key); // OCTET STRING [privateKey] + + // Parse the octet string + $offset = 0; + $offset += ASN1::readDER($private_key, $offset, $data); // SEQUENCE + $offset += ASN1::readDER($private_key, $offset, $version); // INTEGER + + if (ord($version) != 1) throw new KeyException('Invalid private key version: ' . ord($version)); + + $offset += ASN1::readDER($private_key, $offset, $d); // OCTET STRING [d] + + $offset += ASN1::readDER($private_key, $offset, $data); // SEQUENCE[0] + $offset += ASN1::readDER($private_key, $offset, $point); // BIT STRING - ECPoint + + if (strlen($point) != $len + 1) throw new KeyException('Incorrect private key length: ' . strlen($point)); + + if (ord($point[0]) != 0x04) throw new KeyException('Invalid private key'); // W + + $x = substr($point, 1, $len / 2); + $y = substr($point, 1 + $len / 2); + + $jwk['kty'] = self::KTY; + $jwk['crv'] = $curve; + $jwk['d'] = Util::base64url_encode($d); + $jwk['x'] = Util::base64url_encode($x); + $jwk['y'] = Util::base64url_encode($y); + } else { + throw new KeyException('Unrecognised key format'); } parent::__construct($jwk); @@ -296,7 +346,7 @@ protected function getThumbnailMembers() { return ['crv', 'kty', 'x', 'y']; } - private function getCurveNameFromOID(string $curve_oid): ?string { + private static function getCurveNameFromOID(string $curve_oid): ?string { foreach (self::$curves as $crv => $params) { if ($params['oid'] == $curve_oid) return $crv; } diff --git a/src/SimpleJWT/Keys/KeyFactory.php b/src/SimpleJWT/Keys/KeyFactory.php index 5dec69a..090211a 100644 --- a/src/SimpleJWT/Keys/KeyFactory.php +++ b/src/SimpleJWT/Keys/KeyFactory.php @@ -64,7 +64,7 @@ class KeyFactory { /** @var array $pem_map */ static $pem_map = [ RSAKey::PEM_PRIVATE => 'SimpleJWT\Keys\RSAKey', - ECKey::PEM_PRIVATE => 'SimpleJWT\Keys\ECKey' + ECKey::PEM_RFC5915_PRIVATE => 'SimpleJWT\Keys\ECKey' ]; /** @var array $oid_map */ From 3ea247fa17fddcfe4ee7c39dd8841eb6e38e9ab8 Mon Sep 17 00:00:00 2001 From: Kelvin Mo Date: Mon, 14 Feb 2022 22:03:13 +1100 Subject: [PATCH 2/3] Throw exception if PEM format not supported --- src/SimpleJWT/Keys/KeyFactory.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/SimpleJWT/Keys/KeyFactory.php b/src/SimpleJWT/Keys/KeyFactory.php index 090211a..1978019 100644 --- a/src/SimpleJWT/Keys/KeyFactory.php +++ b/src/SimpleJWT/Keys/KeyFactory.php @@ -166,6 +166,9 @@ static public function create($data, $format = null, $password = null, $alg = 'P return new $cls($data, 'pem'); } } + + // TODO - it's probably PKCS#8, which uses BEGIN PRIVATE KEY + throw new KeyException('PEM key format not supported'); } } From d7ad1f60c6c1c9e9c603f632cf206c59b4ea7049 Mon Sep 17 00:00:00 2001 From: Kelvin Mo Date: Mon, 14 Feb 2022 22:04:51 +1100 Subject: [PATCH 3/3] Update Changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7ff463..1d0b2d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. +## Unreleased + +- Fixed: Compatibility with PHP 8.1 when using ECDH (#58) + ## 0.6.1 - Changed: JWT::deserialise() no longer takes a `$format` parameter (which