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