| 1 |
<?php |
| 2 |
|
| 3 |
namespace Dudlewebs\WPMCS\GCP\Firebase\JWT; |
| 4 |
|
| 5 |
use DomainException; |
| 6 |
use InvalidArgumentException; |
| 7 |
use UnexpectedValueException; |
| 8 |
/** |
| 9 |
* JSON Web Key implementation, based on this spec: |
| 10 |
* https://tools.ietf.org/html/draft-ietf-jose-json-web-key-41 |
| 11 |
* |
| 12 |
* PHP version 5 |
| 13 |
* |
| 14 |
* @category Authentication |
| 15 |
* @package Authentication_JWT |
| 16 |
* @author Bui Sy Nguyen <nguyenbs@gmail.com> |
| 17 |
* @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD |
| 18 |
* @link https://github.com/firebase/php-jwt |
| 19 |
*/ |
| 20 |
class JWK |
| 21 |
{ |
| 22 |
private const OID = '1.2.840.10045.2.1'; |
| 23 |
private const ASN1_OBJECT_IDENTIFIER = 0x6; |
| 24 |
private const ASN1_SEQUENCE = 0x10; |
| 25 |
// also defined in JWT |
| 26 |
private const ASN1_BIT_STRING = 0x3; |
| 27 |
private const EC_CURVES = [ |
| 28 |
'P-256' => '1.2.840.10045.3.1.7', |
| 29 |
// Len: 64 |
| 30 |
'secp256k1' => '1.3.132.0.10', |
| 31 |
// Len: 64 |
| 32 |
'P-384' => '1.3.132.0.34', |
| 33 |
]; |
| 34 |
// For keys with "kty" equal to "OKP" (Octet Key Pair), the "crv" parameter must contain the key subtype. |
| 35 |
// This library supports the following subtypes: |
| 36 |
private const OKP_SUBTYPES = ['Ed25519' => \true]; |
| 37 |
/** |
| 38 |
* Parse a set of JWK keys |
| 39 |
* |
| 40 |
* @param array<mixed> $jwks The JSON Web Key Set as an associative array |
| 41 |
* @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the |
| 42 |
* JSON Web Key Set |
| 43 |
* |
| 44 |
* @return array<string, Key> An associative array of key IDs (kid) to Key objects |
| 45 |
* |
| 46 |
* @throws InvalidArgumentException Provided JWK Set is empty |
| 47 |
* @throws UnexpectedValueException Provided JWK Set was invalid |
| 48 |
* @throws DomainException OpenSSL failure |
| 49 |
* |
| 50 |
* @uses parseKey |
| 51 |
*/ |
| 52 |
public static function parseKeySet(#[\SensitiveParameter] array $jwks, ?string $defaultAlg = null) : array |
| 53 |
{ |
| 54 |
$keys = []; |
| 55 |
if (!isset($jwks['keys'])) { |
| 56 |
throw new UnexpectedValueException('"keys" member must exist in the JWK Set'); |
| 57 |
} |
| 58 |
if (empty($jwks['keys'])) { |
| 59 |
throw new InvalidArgumentException('JWK Set did not contain any keys'); |
| 60 |
} |
| 61 |
foreach ($jwks['keys'] as $k => $v) { |
| 62 |
$kid = isset($v['kid']) ? $v['kid'] : $k; |
| 63 |
if ($key = self::parseKey($v, $defaultAlg)) { |
| 64 |
$keys[(string) $kid] = $key; |
| 65 |
} |
| 66 |
} |
| 67 |
if (0 === \count($keys)) { |
| 68 |
throw new UnexpectedValueException('No supported algorithms found in JWK Set'); |
| 69 |
} |
| 70 |
return $keys; |
| 71 |
} |
| 72 |
/** |
| 73 |
* Parse a JWK key |
| 74 |
* |
| 75 |
* @param array<mixed> $jwk An individual JWK |
| 76 |
* @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the |
| 77 |
* JSON Web Key Set |
| 78 |
* |
| 79 |
* @return Key The key object for the JWK |
| 80 |
* |
| 81 |
* @throws InvalidArgumentException Provided JWK is empty |
| 82 |
* @throws UnexpectedValueException Provided JWK was invalid |
| 83 |
* @throws DomainException OpenSSL failure |
| 84 |
* |
| 85 |
* @uses createPemFromModulusAndExponent |
| 86 |
*/ |
| 87 |
public static function parseKey(#[\SensitiveParameter] array $jwk, ?string $defaultAlg = null) : ?Key |
| 88 |
{ |
| 89 |
if (empty($jwk)) { |
| 90 |
throw new InvalidArgumentException('JWK must not be empty'); |
| 91 |
} |
| 92 |
if (!isset($jwk['kty'])) { |
| 93 |
throw new UnexpectedValueException('JWK must contain a "kty" parameter'); |
| 94 |
} |
| 95 |
if (!isset($jwk['alg'])) { |
| 96 |
if (\is_null($defaultAlg)) { |
| 97 |
// The "alg" parameter is optional in a KTY, but an algorithm is required |
| 98 |
// for parsing in this library. Use the $defaultAlg parameter when parsing the |
| 99 |
// key set in order to prevent this error. |
| 100 |
// @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 |
| 101 |
throw new UnexpectedValueException('JWK must contain an "alg" parameter'); |
| 102 |
} |
| 103 |
$jwk['alg'] = $defaultAlg; |
| 104 |
} |
| 105 |
switch ($jwk['kty']) { |
| 106 |
case 'RSA': |
| 107 |
if (!empty($jwk['d'])) { |
| 108 |
throw new UnexpectedValueException('RSA private keys are not supported'); |
| 109 |
} |
| 110 |
if (!isset($jwk['n']) || !isset($jwk['e'])) { |
| 111 |
throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"'); |
| 112 |
} |
| 113 |
$pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']); |
| 114 |
$publicKey = \openssl_pkey_get_public($pem); |
| 115 |
if (\false === $publicKey) { |
| 116 |
throw new DomainException('OpenSSL error: ' . \openssl_error_string()); |
| 117 |
} |
| 118 |
return new Key($publicKey, $jwk['alg']); |
| 119 |
case 'EC': |
| 120 |
if (isset($jwk['d'])) { |
| 121 |
// The key is actually a private key |
| 122 |
throw new UnexpectedValueException('Key data must be for a public key'); |
| 123 |
} |
| 124 |
if (empty($jwk['crv'])) { |
| 125 |
throw new UnexpectedValueException('crv not set'); |
| 126 |
} |
| 127 |
if (!isset(self::EC_CURVES[$jwk['crv']])) { |
| 128 |
throw new DomainException('Unrecognised or unsupported EC curve'); |
| 129 |
} |
| 130 |
if (empty($jwk['x']) || empty($jwk['y'])) { |
| 131 |
throw new UnexpectedValueException('x and y not set'); |
| 132 |
} |
| 133 |
$publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']); |
| 134 |
return new Key($publicKey, $jwk['alg']); |
| 135 |
case 'OKP': |
| 136 |
if (isset($jwk['d'])) { |
| 137 |
// The key is actually a private key |
| 138 |
throw new UnexpectedValueException('Key data must be for a public key'); |
| 139 |
} |
| 140 |
if (!isset($jwk['crv'])) { |
| 141 |
throw new UnexpectedValueException('crv not set'); |
| 142 |
} |
| 143 |
if (empty(self::OKP_SUBTYPES[$jwk['crv']])) { |
| 144 |
throw new DomainException('Unrecognised or unsupported OKP key subtype'); |
| 145 |
} |
| 146 |
if (empty($jwk['x'])) { |
| 147 |
throw new UnexpectedValueException('x not set'); |
| 148 |
} |
| 149 |
// This library works internally with EdDSA keys (Ed25519) encoded in standard base64. |
| 150 |
$publicKey = JWT::convertBase64urlToBase64($jwk['x']); |
| 151 |
return new Key($publicKey, $jwk['alg']); |
| 152 |
case 'oct': |
| 153 |
if (!isset($jwk['k'])) { |
| 154 |
throw new UnexpectedValueException('k not set'); |
| 155 |
} |
| 156 |
return new Key(JWT::urlsafeB64Decode($jwk['k']), $jwk['alg']); |
| 157 |
default: |
| 158 |
break; |
| 159 |
} |
| 160 |
return null; |
| 161 |
} |
| 162 |
/** |
| 163 |
* Converts the EC JWK values to pem format. |
| 164 |
* |
| 165 |
* @param string $crv The EC curve (only P-256 & P-384 is supported) |
| 166 |
* @param string $x The EC x-coordinate |
| 167 |
* @param string $y The EC y-coordinate |
| 168 |
* |
| 169 |
* @return string |
| 170 |
*/ |
| 171 |
private static function createPemFromCrvAndXYCoordinates(string $crv, string $x, string $y) : string |
| 172 |
{ |
| 173 |
$pem = self::encodeDER(self::ASN1_SEQUENCE, self::encodeDER(self::ASN1_SEQUENCE, self::encodeDER(self::ASN1_OBJECT_IDENTIFIER, self::encodeOID(self::OID)) . self::encodeDER(self::ASN1_OBJECT_IDENTIFIER, self::encodeOID(self::EC_CURVES[$crv]))) . self::encodeDER(self::ASN1_BIT_STRING, \chr(0x0) . \chr(0x4) . JWT::urlsafeB64Decode($x) . JWT::urlsafeB64Decode($y))); |
| 174 |
return \sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", \wordwrap(\base64_encode($pem), 64, "\n", \true)); |
| 175 |
} |
| 176 |
/** |
| 177 |
* Create a public key represented in PEM format from RSA modulus and exponent information |
| 178 |
* |
| 179 |
* @param string $n The RSA modulus encoded in Base64 |
| 180 |
* @param string $e The RSA exponent encoded in Base64 |
| 181 |
* |
| 182 |
* @return string The RSA public key represented in PEM format |
| 183 |
* |
| 184 |
* @uses encodeLength |
| 185 |
*/ |
| 186 |
private static function createPemFromModulusAndExponent(string $n, string $e) : string |
| 187 |
{ |
| 188 |
$mod = JWT::urlsafeB64Decode($n); |
| 189 |
$exp = JWT::urlsafeB64Decode($e); |
| 190 |
$modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod); |
| 191 |
$publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp); |
| 192 |
$rsaPublicKey = \pack('Ca*a*a*', 48, self::encodeLength(\strlen($modulus) + \strlen($publicExponent)), $modulus, $publicExponent); |
| 193 |
// sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. |
| 194 |
$rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); |
| 195 |
// hex version of MA0GCSqGSIb3DQEBAQUA |
| 196 |
$rsaPublicKey = \chr(0) . $rsaPublicKey; |
| 197 |
$rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey; |
| 198 |
$rsaPublicKey = \pack('Ca*a*', 48, self::encodeLength(\strlen($rsaOID . $rsaPublicKey)), $rsaOID . $rsaPublicKey); |
| 199 |
return "-----BEGIN PUBLIC KEY-----\r\n" . \chunk_split(\base64_encode($rsaPublicKey), 64) . '-----END PUBLIC KEY-----'; |
| 200 |
} |
| 201 |
/** |
| 202 |
* DER-encode the length |
| 203 |
* |
| 204 |
* DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See |
| 205 |
* {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. |
| 206 |
* |
| 207 |
* @param int $length |
| 208 |
* @return string |
| 209 |
*/ |
| 210 |
private static function encodeLength(int $length) : string |
| 211 |
{ |
| 212 |
if ($length <= 0x7f) { |
| 213 |
return \chr($length); |
| 214 |
} |
| 215 |
$temp = \ltrim(\pack('N', $length), \chr(0)); |
| 216 |
return \pack('Ca*', 0x80 | \strlen($temp), $temp); |
| 217 |
} |
| 218 |
/** |
| 219 |
* Encodes a value into a DER object. |
| 220 |
* Also defined in Firebase\JWT\JWT |
| 221 |
* |
| 222 |
* @param int $type DER tag |
| 223 |
* @param string $value the value to encode |
| 224 |
* @return string the encoded object |
| 225 |
*/ |
| 226 |
private static function encodeDER(int $type, string $value) : string |
| 227 |
{ |
| 228 |
$tag_header = 0; |
| 229 |
if ($type === self::ASN1_SEQUENCE) { |
| 230 |
$tag_header |= 0x20; |
| 231 |
} |
| 232 |
// Type |
| 233 |
$der = \chr($tag_header | $type); |
| 234 |
// Length |
| 235 |
$der .= \chr(\strlen($value)); |
| 236 |
return $der . $value; |
| 237 |
} |
| 238 |
/** |
| 239 |
* Encodes a string into a DER-encoded OID. |
| 240 |
* |
| 241 |
* @param string $oid the OID string |
| 242 |
* @return string the binary DER-encoded OID |
| 243 |
*/ |
| 244 |
private static function encodeOID(string $oid) : string |
| 245 |
{ |
| 246 |
$octets = \explode('.', $oid); |
| 247 |
// Get the first octet |
| 248 |
$first = (int) \array_shift($octets); |
| 249 |
$second = (int) \array_shift($octets); |
| 250 |
$oid = \chr($first * 40 + $second); |
| 251 |
// Iterate over subsequent octets |
| 252 |
foreach ($octets as $octet) { |
| 253 |
if ($octet == 0) { |
| 254 |
$oid .= \chr(0x0); |
| 255 |
continue; |
| 256 |
} |
| 257 |
$bin = ''; |
| 258 |
while ($octet) { |
| 259 |
$bin .= \chr(0x80 | $octet & 0x7f); |
| 260 |
$octet >>= 7; |
| 261 |
} |
| 262 |
$bin[0] = $bin[0] & \chr(0x7f); |
| 263 |
// Convert to big endian if necessary |
| 264 |
if (\pack('V', 65534) == \pack('L', 65534)) { |
| 265 |
$oid .= \strrev($bin); |
| 266 |
} else { |
| 267 |
$oid .= $bin; |
| 268 |
} |
| 269 |
} |
| 270 |
return $oid; |
| 271 |
} |
| 272 |
} |
| 273 |
|