Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]

   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  }