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