Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 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      /**
  24       * Parse a set of JWK keys
  25       *
  26       * @param array $jwks The JSON Web Key Set as an associative array
  27       *
  28       * @return array<string, Key> An associative array of key IDs (kid) to Key objects
  29       *
  30       * @throws InvalidArgumentException     Provided JWK Set is empty
  31       * @throws UnexpectedValueException     Provided JWK Set was invalid
  32       * @throws DomainException              OpenSSL failure
  33       *
  34       * @uses parseKey
  35       */
  36      public static function parseKeySet(array $jwks)
  37      {
  38          $keys = array();
  39  
  40          if (!isset($jwks['keys'])) {
  41              throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
  42          }
  43          if (empty($jwks['keys'])) {
  44              throw new InvalidArgumentException('JWK Set did not contain any keys');
  45          }
  46  
  47          foreach ($jwks['keys'] as $k => $v) {
  48              $kid = isset($v['kid']) ? $v['kid'] : $k;
  49              if ($key = self::parseKey($v)) {
  50                  $keys[$kid] = $key;
  51              }
  52          }
  53  
  54          if (0 === \count($keys)) {
  55              throw new UnexpectedValueException('No supported algorithms found in JWK Set');
  56          }
  57  
  58          return $keys;
  59      }
  60  
  61      /**
  62       * Parse a JWK key
  63       *
  64       * @param array $jwk An individual JWK
  65       *
  66       * @return Key The key object for the JWK
  67       *
  68       * @throws InvalidArgumentException     Provided JWK is empty
  69       * @throws UnexpectedValueException     Provided JWK was invalid
  70       * @throws DomainException              OpenSSL failure
  71       *
  72       * @uses createPemFromModulusAndExponent
  73       */
  74      public static function parseKey(array $jwk)
  75      {
  76          if (empty($jwk)) {
  77              throw new InvalidArgumentException('JWK must not be empty');
  78          }
  79          if (!isset($jwk['kty'])) {
  80              throw new UnexpectedValueException('JWK must contain a "kty" parameter');
  81          }
  82          if (!isset($jwk['alg'])) {
  83              // The "alg" parameter is optional in a KTY, but is required for parsing in
  84              // this library. Add it manually to your JWK array if it doesn't already exist.
  85              // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4
  86              throw new UnexpectedValueException('JWK must contain an "alg" parameter');
  87          }
  88  
  89          switch ($jwk['kty']) {
  90              case 'RSA':
  91                  if (!empty($jwk['d'])) {
  92                      throw new UnexpectedValueException('RSA private keys are not supported');
  93                  }
  94                  if (!isset($jwk['n']) || !isset($jwk['e'])) {
  95                      throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"');
  96                  }
  97  
  98                  $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']);
  99                  $publicKey = \openssl_pkey_get_public($pem);
 100                  if (false === $publicKey) {
 101                      throw new DomainException(
 102                          'OpenSSL error: ' . \openssl_error_string()
 103                      );
 104                  }
 105                  return new Key($publicKey, $jwk['alg']);
 106              default:
 107                  // Currently only RSA is supported
 108                  break;
 109          }
 110      }
 111  
 112      /**
 113       * Create a public key represented in PEM format from RSA modulus and exponent information
 114       *
 115       * @param string $n The RSA modulus encoded in Base64
 116       * @param string $e The RSA exponent encoded in Base64
 117       *
 118       * @return string The RSA public key represented in PEM format
 119       *
 120       * @uses encodeLength
 121       */
 122      private static function createPemFromModulusAndExponent($n, $e)
 123      {
 124          $modulus = JWT::urlsafeB64Decode($n);
 125          $publicExponent = JWT::urlsafeB64Decode($e);
 126  
 127          $components = array(
 128              'modulus' => \pack('Ca*a*', 2, self::encodeLength(\strlen($modulus)), $modulus),
 129              'publicExponent' => \pack('Ca*a*', 2, self::encodeLength(\strlen($publicExponent)), $publicExponent)
 130          );
 131  
 132          $rsaPublicKey = \pack(
 133              'Ca*a*a*',
 134              48,
 135              self::encodeLength(\strlen($components['modulus']) + \strlen($components['publicExponent'])),
 136              $components['modulus'],
 137              $components['publicExponent']
 138          );
 139  
 140          // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption.
 141          $rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA
 142          $rsaPublicKey = \chr(0) . $rsaPublicKey;
 143          $rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey;
 144  
 145          $rsaPublicKey = \pack(
 146              'Ca*a*',
 147              48,
 148              self::encodeLength(\strlen($rsaOID . $rsaPublicKey)),
 149              $rsaOID . $rsaPublicKey
 150          );
 151  
 152          $rsaPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" .
 153              \chunk_split(\base64_encode($rsaPublicKey), 64) .
 154              '-----END PUBLIC KEY-----';
 155  
 156          return $rsaPublicKey;
 157      }
 158  
 159      /**
 160       * DER-encode the length
 161       *
 162       * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4.  See
 163       * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information.
 164       *
 165       * @param int $length
 166       * @return string
 167       */
 168      private static function encodeLength($length)
 169      {
 170          if ($length <= 0x7F) {
 171              return \chr($length);
 172          }
 173  
 174          $temp = \ltrim(\pack('N', $length), \chr(0));
 175  
 176          return \pack('Ca*', 0x80 | \strlen($temp), $temp);
 177      }
 178  }