ILIAS  release_8 Revision v8.24
JWK.php
Go to the documentation of this file.
1<?php
2
3namespace Firebase\JWT;
4
5use DomainException;
6use InvalidArgumentException;
7use UnexpectedValueException;
8
21class 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
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
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
191 private static function createPemFromCrvAndXYCoordinates(string $crv, string $x, string $y): string
192 {
193 $pem =
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)
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
231 private static function createPemFromModulusAndExponent(
232 string $n,
233 string $e
234 ): string {
235 $mod = JWT::urlsafeB64Decode($n);
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
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
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
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}
const OID
Definition: JWK.php:23
static createPemFromCrvAndXYCoordinates(string $crv, string $x, string $y)
Converts the EC JWK values to pem format.
Definition: JWK.php:191
static createPemFromModulusAndExponent(string $n, string $e)
Create a public key represented in PEM format from RSA modulus and exponent information.
Definition: JWK.php:231
static parseKeySet(array $jwks, string $defaultAlg=null)
Parse a set of JWK keys.
Definition: JWK.php:55
const ASN1_OBJECT_IDENTIFIER
Definition: JWK.php:24
static parseKey(array $jwk, string $defaultAlg=null)
Parse a JWK key.
Definition: JWK.php:96
static encodeLength(int $length)
DER-encode the length.
Definition: JWK.php:275
const OKP_SUBTYPES
Definition: JWK.php:36
static encodeDER(int $type, string $value)
Encodes a value into a DER object.
Definition: JWK.php:294
static encodeOID(string $oid)
Encodes a string into a DER-encoded OID.
Definition: JWK.php:316
const ASN1_SEQUENCE
Definition: JWK.php:25
const ASN1_BIT_STRING
Definition: JWK.php:26
const EC_CURVES
Definition: JWK.php:27
static urlsafeB64Decode(string $input)
Decode a string with URL-safe Base64.
Definition: JWT.php:412
$keys
Definition: metadata.php:204
string $kid
Key ID.
Definition: System.php:88
string $key
Consumer key/client ID value.
Definition: System.php:193
$type