JWK.php 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. <?php
  2. namespace Firebase\JWT;
  3. use DomainException;
  4. use InvalidArgumentException;
  5. use UnexpectedValueException;
  6. /**
  7. * JSON Web Key implementation, based on this spec:
  8. * https://tools.ietf.org/html/draft-ietf-jose-json-web-key-41
  9. *
  10. * PHP version 5
  11. *
  12. * @category Authentication
  13. * @package Authentication_JWT
  14. * @author Bui Sy Nguyen <nguyenbs@gmail.com>
  15. * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
  16. * @link https://github.com/firebase/php-jwt
  17. */
  18. class JWK
  19. {
  20. /**
  21. * Parse a set of JWK keys
  22. *
  23. * @param array $jwks The JSON Web Key Set as an associative array
  24. *
  25. * @return array An associative array that represents the set of keys
  26. *
  27. * @throws InvalidArgumentException Provided JWK Set is empty
  28. * @throws UnexpectedValueException Provided JWK Set was invalid
  29. * @throws DomainException OpenSSL failure
  30. *
  31. * @uses parseKey
  32. */
  33. public static function parseKeySet(array $jwks)
  34. {
  35. $keys = array();
  36. if (!isset($jwks['keys'])) {
  37. throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
  38. }
  39. if (empty($jwks['keys'])) {
  40. throw new InvalidArgumentException('JWK Set did not contain any keys');
  41. }
  42. foreach ($jwks['keys'] as $k => $v) {
  43. $kid = isset($v['kid']) ? $v['kid'] : $k;
  44. if ($key = self::parseKey($v)) {
  45. $keys[$kid] = $key;
  46. }
  47. }
  48. if (0 === \count($keys)) {
  49. throw new UnexpectedValueException('No supported algorithms found in JWK Set');
  50. }
  51. return $keys;
  52. }
  53. /**
  54. * Parse a JWK key
  55. *
  56. * @param array $jwk An individual JWK
  57. *
  58. * @return resource|array An associative array that represents the key
  59. *
  60. * @throws InvalidArgumentException Provided JWK is empty
  61. * @throws UnexpectedValueException Provided JWK was invalid
  62. * @throws DomainException OpenSSL failure
  63. *
  64. * @uses createPemFromModulusAndExponent
  65. */
  66. public static function parseKey(array $jwk)
  67. {
  68. if (empty($jwk)) {
  69. throw new InvalidArgumentException('JWK must not be empty');
  70. }
  71. if (!isset($jwk['kty'])) {
  72. throw new UnexpectedValueException('JWK must contain a "kty" parameter');
  73. }
  74. switch ($jwk['kty']) {
  75. case 'RSA':
  76. if (!empty($jwk['d'])) {
  77. throw new UnexpectedValueException('RSA private keys are not supported');
  78. }
  79. if (!isset($jwk['n']) || !isset($jwk['e'])) {
  80. throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"');
  81. }
  82. $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']);
  83. $publicKey = \openssl_pkey_get_public($pem);
  84. if (false === $publicKey) {
  85. throw new DomainException(
  86. 'OpenSSL error: ' . \openssl_error_string()
  87. );
  88. }
  89. return $publicKey;
  90. default:
  91. // Currently only RSA is supported
  92. break;
  93. }
  94. }
  95. /**
  96. * Create a public key represented in PEM format from RSA modulus and exponent information
  97. *
  98. * @param string $n The RSA modulus encoded in Base64
  99. * @param string $e The RSA exponent encoded in Base64
  100. *
  101. * @return string The RSA public key represented in PEM format
  102. *
  103. * @uses encodeLength
  104. */
  105. private static function createPemFromModulusAndExponent($n, $e)
  106. {
  107. $modulus = JWT::urlsafeB64Decode($n);
  108. $publicExponent = JWT::urlsafeB64Decode($e);
  109. $components = array(
  110. 'modulus' => \pack('Ca*a*', 2, self::encodeLength(\strlen($modulus)), $modulus),
  111. 'publicExponent' => \pack('Ca*a*', 2, self::encodeLength(\strlen($publicExponent)), $publicExponent)
  112. );
  113. $rsaPublicKey = \pack(
  114. 'Ca*a*a*',
  115. 48,
  116. self::encodeLength(\strlen($components['modulus']) + \strlen($components['publicExponent'])),
  117. $components['modulus'],
  118. $components['publicExponent']
  119. );
  120. // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption.
  121. $rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA
  122. $rsaPublicKey = \chr(0) . $rsaPublicKey;
  123. $rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey;
  124. $rsaPublicKey = \pack(
  125. 'Ca*a*',
  126. 48,
  127. self::encodeLength(\strlen($rsaOID . $rsaPublicKey)),
  128. $rsaOID . $rsaPublicKey
  129. );
  130. $rsaPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" .
  131. \chunk_split(\base64_encode($rsaPublicKey), 64) .
  132. '-----END PUBLIC KEY-----';
  133. return $rsaPublicKey;
  134. }
  135. /**
  136. * DER-encode the length
  137. *
  138. * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See
  139. * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information.
  140. *
  141. * @param int $length
  142. * @return string
  143. */
  144. private static function encodeLength($length)
  145. {
  146. if ($length <= 0x7F) {
  147. return \chr($length);
  148. }
  149. $temp = \ltrim(\pack('N', $length), \chr(0));
  150. return \pack('Ca*', 0x80 | \strlen($temp), $temp);
  151. }
  152. }