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