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