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