Search moodle.org's
Developer Documentation

See Release Notes

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

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