Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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