Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
<?php

namespace Firebase\JWT;

use DomainException;
> use InvalidArgumentException;
use UnexpectedValueException; /** * JSON Web Key implementation, based on this spec: * https://tools.ietf.org/html/draft-ietf-jose-json-web-key-41 * * PHP version 5 * * @category Authentication * @package Authentication_JWT * @author Bui Sy Nguyen <nguyenbs@gmail.com> * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD * @link https://github.com/firebase/php-jwt */ class JWK {
> private const OID = '1.2.840.10045.2.1'; /** > private const ASN1_OBJECT_IDENTIFIER = 0x06; * Parse a set of JWK keys > private const ASN1_SEQUENCE = 0x10; // also defined in JWT * > private const ASN1_BIT_STRING = 0x03; * @param array $jwks The JSON Web Key Set as an associative array > private const EC_CURVES = [ * > 'P-256' => '1.2.840.10045.3.1.7', // Len: 64 * @return array An associative array that represents the set of keys > 'secp256k1' => '1.3.132.0.10', // Len: 64 * > // 'P-384' => '1.3.132.0.34', // Len: 96 (not yet supported) * @throws InvalidArgumentException Provided JWK Set is empty > // 'P-521' => '1.3.132.0.35', // Len: 132 (not supported) * @throws UnexpectedValueException Provided JWK Set was invalid > ]; * @throws DomainException OpenSSL failure >
< * @param array $jwks The JSON Web Key Set as an associative array
> * @param array<mixed> $jwks The JSON Web Key Set as an associative array > * @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the > * JSON Web Key Set
< * @return array An associative array that represents the set of keys
> * @return array<string, Key> An associative array of key IDs (kid) to Key objects
*/
< public static function parseKeySet(array $jwks)
> public static function parseKeySet(array $jwks, string $defaultAlg = null): array
{
< $keys = array();
> $keys = [];
if (!isset($jwks['keys'])) { throw new UnexpectedValueException('"keys" member must exist in the JWK Set'); }
>
if (empty($jwks['keys'])) { throw new InvalidArgumentException('JWK Set did not contain any keys'); } foreach ($jwks['keys'] as $k => $v) { $kid = isset($v['kid']) ? $v['kid'] : $k;
< if ($key = self::parseKey($v)) { < $keys[$kid] = $key;
> if ($key = self::parseKey($v, $defaultAlg)) { > $keys[(string) $kid] = $key;
} } if (0 === \count($keys)) { throw new UnexpectedValueException('No supported algorithms found in JWK Set'); } return $keys; } /** * Parse a JWK key *
< * @param array $jwk An individual JWK
> * @param array<mixed> $jwk An individual JWK > * @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the > * JSON Web Key Set
*
< * @return resource|array An associative array that represents the key
> * @return Key The key object for the JWK
* * @throws InvalidArgumentException Provided JWK is empty * @throws UnexpectedValueException Provided JWK was invalid * @throws DomainException OpenSSL failure * * @uses createPemFromModulusAndExponent */
< private static function parseKey(array $jwk)
> public static function parseKey(array $jwk, string $defaultAlg = null): ?Key
{ if (empty($jwk)) { throw new InvalidArgumentException('JWK must not be empty'); }
>
if (!isset($jwk['kty'])) { throw new UnexpectedValueException('JWK must contain a "kty" parameter'); }
> if (!isset($jwk['alg'])) { switch ($jwk['kty']) { > if (\is_null($defaultAlg)) { case 'RSA': > // The "alg" parameter is optional in a KTY, but an algorithm is required if (\array_key_exists('d', $jwk)) { > // for parsing in this library. Use the $defaultAlg parameter when parsing the throw new UnexpectedValueException('RSA private keys are not supported'); > // key set in order to prevent this error. } > // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 if (!isset($jwk['n']) || !isset($jwk['e'])) { > throw new UnexpectedValueException('JWK must contain an "alg" parameter'); throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"'); > } } > $jwk['alg'] = $defaultAlg; > } $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']); >
< if (\array_key_exists('d', $jwk)) {
> if (!empty($jwk['d'])) {
if (false === $publicKey) { throw new DomainException( 'OpenSSL error: ' . \openssl_error_string() ); }
< return $publicKey;
> return new Key($publicKey, $jwk['alg']); > case 'EC': > if (isset($jwk['d'])) { > // The key is actually a private key > throw new UnexpectedValueException('Key data must be for a public key'); > } > > if (empty($jwk['crv'])) { > throw new UnexpectedValueException('crv not set'); > } > > if (!isset(self::EC_CURVES[$jwk['crv']])) { > throw new DomainException('Unrecognised or unsupported EC curve'); > } > > if (empty($jwk['x']) || empty($jwk['y'])) { > throw new UnexpectedValueException('x and y not set'); > } > > $publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']); > return new Key($publicKey, $jwk['alg']);
default: // Currently only RSA is supported break; }
> } > return null; > } /** > * Create a public key represented in PEM format from RSA modulus and exponent information > /** * > * Converts the EC JWK values to pem format. * @param string $n The RSA modulus encoded in Base64 > * * @param string $e The RSA exponent encoded in Base64 > * @param string $crv The EC curve (only P-256 is supported) * > * @param string $x The EC x-coordinate * @return string The RSA public key represented in PEM format > * @param string $y The EC y-coordinate * > * * @uses encodeLength > * @return string */ > */ private static function createPemFromModulusAndExponent($n, $e) > private static function createPemFromCrvAndXYCoordinates(string $crv, string $x, string $y): string { > { $modulus = JWT::urlsafeB64Decode($n); > $pem = $publicExponent = JWT::urlsafeB64Decode($e); > self::encodeDER( > self::ASN1_SEQUENCE, $components = array( > self::encodeDER( 'modulus' => \pack('Ca*a*', 2, self::encodeLength(\strlen($modulus)), $modulus), > self::ASN1_SEQUENCE, 'publicExponent' => \pack('Ca*a*', 2, self::encodeLength(\strlen($publicExponent)), $publicExponent) > self::encodeDER( ); > self::ASN1_OBJECT_IDENTIFIER, > self::encodeOID(self::OID) $rsaPublicKey = \pack( > ) 'Ca*a*a*', > . self::encodeDER( 48, > self::ASN1_OBJECT_IDENTIFIER, self::encodeLength(\strlen($components['modulus']) + \strlen($components['publicExponent'])), > self::encodeOID(self::EC_CURVES[$crv]) $components['modulus'], > ) $components['publicExponent'] > ) . ); > self::encodeDER( > self::ASN1_BIT_STRING, // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. > \chr(0x00) . \chr(0x04) $rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA > . JWT::urlsafeB64Decode($x) $rsaPublicKey = \chr(0) . $rsaPublicKey; > . JWT::urlsafeB64Decode($y) $rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey; > ) > ); $rsaPublicKey = \pack( > 'Ca*a*', > return sprintf( 48, > "-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", self::encodeLength(\strlen($rsaOID . $rsaPublicKey)), > wordwrap(base64_encode($pem), 64, "\n", true) $rsaOID . $rsaPublicKey > );
< private static function createPemFromModulusAndExponent($n, $e) < { < $modulus = JWT::urlsafeB64Decode($n); < $publicExponent = JWT::urlsafeB64Decode($e);
> private static function createPemFromModulusAndExponent( > string $n, > string $e > ): string { > $mod = JWT::urlsafeB64Decode($n); > $exp = JWT::urlsafeB64Decode($e);
< $components = array( < 'modulus' => \pack('Ca*a*', 2, self::encodeLength(\strlen($modulus)), $modulus), < 'publicExponent' => \pack('Ca*a*', 2, self::encodeLength(\strlen($publicExponent)), $publicExponent) < );
> $modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod); > $publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp);
< self::encodeLength(\strlen($components['modulus']) + \strlen($components['publicExponent'])), < $components['modulus'], < $components['publicExponent']
> self::encodeLength(\strlen($modulus) + \strlen($publicExponent)), > $modulus, > $publicExponent
< $rsaPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" .
> return "-----BEGIN PUBLIC KEY-----\r\n" .
< < return $rsaPublicKey;
* * @param int $length * @return string */
< private static function encodeLength($length)
> private static function encodeLength(int $length): string
{ if ($length <= 0x7F) { return \chr($length); } $temp = \ltrim(\pack('N', $length), \chr(0)); return \pack('Ca*', 0x80 | \strlen($temp), $temp);
> } } > } > /** > * Encodes a value into a DER object. > * Also defined in Firebase\JWT\JWT > * > * @param int $type DER tag > * @param string $value the value to encode > * @return string the encoded object > */ > private static function encodeDER(int $type, string $value): string > { > $tag_header = 0; > if ($type === self::ASN1_SEQUENCE) { > $tag_header |= 0x20; > } > > // Type > $der = \chr($tag_header | $type); > > // Length > $der .= \chr(\strlen($value)); > > return $der . $value; > } > > /** > * Encodes a string into a DER-encoded OID. > * > * @param string $oid the OID string > * @return string the binary DER-encoded OID > */ > private static function encodeOID(string $oid): string > { > $octets = explode('.', $oid); > > // Get the first octet > $first = (int) array_shift($octets); > $second = (int) array_shift($octets); > $oid = \chr($first * 40 + $second); > > // Iterate over subsequent octets > foreach ($octets as $octet) { > if ($octet == 0) { > $oid .= \chr(0x00); > continue; > } > $bin = ''; > > while ($octet) { > $bin .= \chr(0x80 | ($octet & 0x7f)); > $octet >>= 7; > } > $bin[0] = $bin[0] & \chr(0x7f); > > // Convert to big endian if necessary > if (pack('V', 65534) == pack('L', 65534)) { > $oid .= strrev($bin); > } else { > $oid .= $bin; > } > } > > return $oid;