BeforeValidException.php
3 years ago
CachedKeySet.php
3 years ago
ExpiredException.php
3 years ago
JWK.php
3 years ago
JWT.php
3 years ago
Key.php
3 years ago
SignatureInvalidException.php
3 years ago
JWK.php
323 lines
| 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 | } |
| 323 |