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