1<?php 2 3namespace Firebase\JWT; 4 5use DomainException; 6use InvalidArgumentException; 7use 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 */ 21class JWK 22{ 23 /** 24 * Parse a set of JWK keys 25 * 26 * @param array<mixed> $jwks The JSON Web Key Set as an associative array 27 * 28 * @return array<string, Key> An associative array of key IDs (kid) to Key objects 29 * 30 * @throws InvalidArgumentException Provided JWK Set is empty 31 * @throws UnexpectedValueException Provided JWK Set was invalid 32 * @throws DomainException OpenSSL failure 33 * 34 * @uses parseKey 35 */ 36 public static function parseKeySet(array $jwks): array 37 { 38 $keys = []; 39 40 if (!isset($jwks['keys'])) { 41 throw new UnexpectedValueException('"keys" member must exist in the JWK Set'); 42 } 43 44 if (empty($jwks['keys'])) { 45 throw new InvalidArgumentException('JWK Set did not contain any keys'); 46 } 47 48 foreach ($jwks['keys'] as $k => $v) { 49 $kid = isset($v['kid']) ? $v['kid'] : $k; 50 if ($key = self::parseKey($v)) { 51 $keys[(string) $kid] = $key; 52 } 53 } 54 55 if (0 === \count($keys)) { 56 throw new UnexpectedValueException('No supported algorithms found in JWK Set'); 57 } 58 59 return $keys; 60 } 61 62 /** 63 * Parse a JWK key 64 * 65 * @param array<mixed> $jwk An individual JWK 66 * 67 * @return Key The key object for the JWK 68 * 69 * @throws InvalidArgumentException Provided JWK is empty 70 * @throws UnexpectedValueException Provided JWK was invalid 71 * @throws DomainException OpenSSL failure 72 * 73 * @uses createPemFromModulusAndExponent 74 */ 75 public static function parseKey(array $jwk): ?Key 76 { 77 if (empty($jwk)) { 78 throw new InvalidArgumentException('JWK must not be empty'); 79 } 80 81 if (!isset($jwk['kty'])) { 82 throw new UnexpectedValueException('JWK must contain a "kty" parameter'); 83 } 84 85 if (!isset($jwk['alg'])) { 86 // The "alg" parameter is optional in a KTY, but is required for parsing in 87 // this library. Add it manually to your JWK array if it doesn't already exist. 88 // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 89 throw new UnexpectedValueException('JWK must contain an "alg" parameter'); 90 } 91 92 switch ($jwk['kty']) { 93 case 'RSA': 94 if (!empty($jwk['d'])) { 95 throw new UnexpectedValueException('RSA private keys are not supported'); 96 } 97 if (!isset($jwk['n']) || !isset($jwk['e'])) { 98 throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"'); 99 } 100 101 $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']); 102 $publicKey = \openssl_pkey_get_public($pem); 103 if (false === $publicKey) { 104 throw new DomainException( 105 'OpenSSL error: ' . \openssl_error_string() 106 ); 107 } 108 return new Key($publicKey, $jwk['alg']); 109 default: 110 // Currently only RSA is supported 111 break; 112 } 113 114 return null; 115 } 116 117 /** 118 * Create a public key represented in PEM format from RSA modulus and exponent information 119 * 120 * @param string $n The RSA modulus encoded in Base64 121 * @param string $e The RSA exponent encoded in Base64 122 * 123 * @return string The RSA public key represented in PEM format 124 * 125 * @uses encodeLength 126 */ 127 private static function createPemFromModulusAndExponent( 128 string $n, 129 string $e 130 ): string { 131 $mod = JWT::urlsafeB64Decode($n); 132 $exp = JWT::urlsafeB64Decode($e); 133 134 $modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod); 135 $publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp); 136 137 $rsaPublicKey = \pack( 138 'Ca*a*a*', 139 48, 140 self::encodeLength(\strlen($modulus) + \strlen($publicExponent)), 141 $modulus, 142 $publicExponent 143 ); 144 145 // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. 146 $rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA 147 $rsaPublicKey = \chr(0) . $rsaPublicKey; 148 $rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey; 149 150 $rsaPublicKey = \pack( 151 'Ca*a*', 152 48, 153 self::encodeLength(\strlen($rsaOID . $rsaPublicKey)), 154 $rsaOID . $rsaPublicKey 155 ); 156 157 $rsaPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" . 158 \chunk_split(\base64_encode($rsaPublicKey), 64) . 159 '-----END PUBLIC KEY-----'; 160 161 return $rsaPublicKey; 162 } 163 164 /** 165 * DER-encode the length 166 * 167 * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See 168 * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. 169 * 170 * @param int $length 171 * @return string 172 */ 173 private static function encodeLength(int $length): string 174 { 175 if ($length <= 0x7F) { 176 return \chr($length); 177 } 178 179 $temp = \ltrim(\pack('N', $length), \chr(0)); 180 181 return \pack('Ca*', 0x80 | \strlen($temp), $temp); 182 } 183} 184