├── .github ├── CONTRIBUTING.md ├── FUNDING.yml └── PULL_REQUEST_TEMPLATE.md ├── Analyzer ├── AlgorithmAnalyzer.php ├── ES256KeyAnalyzer.php ├── ES384KeyAnalyzer.php ├── ES512KeyAnalyzer.php ├── ESKeyAnalyzer.php ├── HS256KeyAnalyzer.php ├── HS384KeyAnalyzer.php ├── HS512KeyAnalyzer.php ├── HSKeyAnalyzer.php ├── KeyAnalyzer.php ├── KeyAnalyzerManager.php ├── KeyIdentifierAnalyzer.php ├── KeysetAnalyzer.php ├── KeysetAnalyzerManager.php ├── Message.php ├── MessageBag.php ├── MixedKeyTypes.php ├── MixedPublicAndPrivateKeys.php ├── NoneAnalyzer.php ├── OctAnalyzer.php ├── RsaAnalyzer.php ├── UsageAnalyzer.php └── ZxcvbnKeyAnalyzer.php ├── JKUFactory.php ├── JWKFactory.php ├── KeyConverter ├── ECKey.php ├── KeyConverter.php └── RSAKey.php ├── LICENSE ├── README.md ├── UrlKeySetFactory.php ├── X5UFactory.php └── composer.json /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY. 4 | Please do not submit any Pull Requests here. It will be automatically closed. 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: FlorentMorselli 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please do not submit any Pull Requests here. It will be automatically closed. 2 | 3 | You should submit it here: https://github.com/web-token/jwt-framework/pulls 4 | -------------------------------------------------------------------------------- /Analyzer/AlgorithmAnalyzer.php: -------------------------------------------------------------------------------- 1 | has('alg')) { 14 | $bag->add(Message::medium('The parameter "alg" should be added.')); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Analyzer/ES256KeyAnalyzer.php: -------------------------------------------------------------------------------- 1 | get('kty') !== 'EC') { 27 | return; 28 | } 29 | if (! $jwk->has('crv')) { 30 | $bag->add(Message::high('Invalid key. The components "crv" is missing.')); 31 | 32 | return; 33 | } 34 | if ($jwk->get('crv') !== $this->getCurveName()) { 35 | return; 36 | } 37 | $x = $jwk->get('x'); 38 | if (! is_string($x)) { 39 | $bag->add(Message::high('Invalid key. The components "x" shall be a string.')); 40 | 41 | return; 42 | } 43 | $x = Base64UrlSafe::decode($x); 44 | $xLength = 8 * mb_strlen($x, '8bit'); 45 | $y = $jwk->get('y'); 46 | if (! is_string($y)) { 47 | $bag->add(Message::high('Invalid key. The components "y" shall be a string.')); 48 | 49 | return; 50 | } 51 | $y = Base64UrlSafe::decode($y); 52 | $yLength = 8 * mb_strlen($y, '8bit'); 53 | if ($yLength !== $xLength || $yLength !== $this->getKeySize()) { 54 | $bag->add( 55 | Message::high(sprintf( 56 | 'Invalid key. The components "x" and "y" size shall be %d bits.', 57 | $this->getKeySize() 58 | )) 59 | ); 60 | } 61 | $xBI = BigInteger::fromBase(bin2hex($x), 16); 62 | $yBI = BigInteger::fromBase(bin2hex($y), 16); 63 | if (! $this->getCurve()->contains($xBI, $yBI)) { 64 | $bag->add(Message::high('Invalid key. The point is not on the curve.')); 65 | } 66 | } 67 | 68 | abstract protected function getAlgorithmName(): string; 69 | 70 | abstract protected function getCurveName(): string; 71 | 72 | abstract protected function getCurve(): Curve; 73 | 74 | abstract protected function getKeySize(): int; 75 | } 76 | -------------------------------------------------------------------------------- /Analyzer/HS256KeyAnalyzer.php: -------------------------------------------------------------------------------- 1 | get('kty') !== 'oct') { 16 | return; 17 | } 18 | if (! $jwk->has('alg') || $jwk->get('alg') !== $this->getAlgorithmName()) { 19 | return; 20 | } 21 | $k = $jwk->get('k'); 22 | if (! is_string($k)) { 23 | $bag->add(Message::high('The key is not valid')); 24 | 25 | return; 26 | } 27 | $k = Base64UrlSafe::decode($k); 28 | $kLength = 8 * mb_strlen($k, '8bit'); 29 | if ($kLength < $this->getMinimumKeySize()) { 30 | $bag->add( 31 | Message::high(sprintf( 32 | 'HS512 algorithm requires at least %d bits key length.', 33 | $this->getMinimumKeySize() 34 | )) 35 | ); 36 | } 37 | } 38 | 39 | abstract protected function getAlgorithmName(): string; 40 | 41 | abstract protected function getMinimumKeySize(): int; 42 | } 43 | -------------------------------------------------------------------------------- /Analyzer/KeyAnalyzer.php: -------------------------------------------------------------------------------- 1 | analyzers[] = $analyzer; 22 | } 23 | 24 | /** 25 | * This method will analyze the JWK object using all analyzers. It returns a message bag that may contains messages. 26 | */ 27 | public function analyze(JWK $jwk): MessageBag 28 | { 29 | $bag = new MessageBag(); 30 | foreach ($this->analyzers as $analyzer) { 31 | $analyzer->analyze($jwk, $bag); 32 | } 33 | 34 | return $bag; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Analyzer/KeyIdentifierAnalyzer.php: -------------------------------------------------------------------------------- 1 | has('kid')) { 14 | $bag->add(Message::medium('The parameter "kid" should be added.')); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Analyzer/KeysetAnalyzer.php: -------------------------------------------------------------------------------- 1 | analyzers[] = $analyzer; 22 | } 23 | 24 | /** 25 | * This method will analyze the JWKSet object using all analyzers. It returns a message bag that may contains 26 | * messages. 27 | */ 28 | public function analyze(JWKSet $jwkset): MessageBag 29 | { 30 | $bag = new MessageBag(); 31 | foreach ($this->analyzers as $analyzer) { 32 | $analyzer->analyze($jwkset, $bag); 33 | } 34 | 35 | return $bag; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Analyzer/Message.php: -------------------------------------------------------------------------------- 1 | message; 53 | } 54 | 55 | /** 56 | * Returns the severity of the message. 57 | */ 58 | public function getSeverity(): string 59 | { 60 | return $this->severity; 61 | } 62 | 63 | public function jsonSerialize(): array 64 | { 65 | return [ 66 | 'message' => $this->message, 67 | 'severity' => $this->severity, 68 | ]; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Analyzer/MessageBag.php: -------------------------------------------------------------------------------- 1 | messages[] = $message; 27 | } 28 | 29 | /** 30 | * Returns all messages. 31 | * 32 | * @return Message[] 33 | */ 34 | public function all(): array 35 | { 36 | return $this->messages; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function jsonSerialize(): array 43 | { 44 | return array_values($this->messages); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function count(): int 51 | { 52 | return count($this->messages); 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function getIterator(): Traversable 59 | { 60 | return new ArrayIterator($this->messages); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Analyzer/MixedKeyTypes.php: -------------------------------------------------------------------------------- 1 | count() === 0) { 14 | return; 15 | } 16 | 17 | $hasSymmetricKeys = false; 18 | $hasAsymmetricKeys = false; 19 | 20 | foreach ($jwkset as $jwk) { 21 | switch ($jwk->get('kty')) { 22 | case 'oct': 23 | $hasSymmetricKeys = true; 24 | 25 | break; 26 | 27 | case 'OKP': 28 | case 'RSA': 29 | case 'EC': 30 | $hasAsymmetricKeys = true; 31 | 32 | break; 33 | } 34 | } 35 | 36 | if ($hasAsymmetricKeys && $hasSymmetricKeys) { 37 | $bag->add(Message::medium('This key set mixes symmetric and assymetric keys.')); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Analyzer/MixedPublicAndPrivateKeys.php: -------------------------------------------------------------------------------- 1 | count() === 0) { 14 | return; 15 | } 16 | 17 | $hasPublicKeys = false; 18 | $hasPrivateKeys = false; 19 | 20 | foreach ($jwkset as $jwk) { 21 | switch ($jwk->get('kty')) { 22 | case 'OKP': 23 | case 'RSA': 24 | case 'EC': 25 | if ($jwk->has('d')) { 26 | $hasPrivateKeys = true; 27 | } else { 28 | $hasPublicKeys = true; 29 | } 30 | 31 | break; 32 | } 33 | } 34 | 35 | if ($hasPrivateKeys && $hasPublicKeys) { 36 | $bag->add(Message::high('This key set mixes public and private keys.')); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Analyzer/NoneAnalyzer.php: -------------------------------------------------------------------------------- 1 | get('kty') !== 'none') { 14 | return; 15 | } 16 | 17 | $bag->add( 18 | Message::high( 19 | 'This key is a meant to be used with the algorithm "none". This algorithm is not secured and should be used with care.' 20 | ) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Analyzer/OctAnalyzer.php: -------------------------------------------------------------------------------- 1 | get('kty') !== 'oct') { 16 | return; 17 | } 18 | $k = $jwk->get('k'); 19 | if (! is_string($k)) { 20 | $bag->add(Message::high('The key is not valid')); 21 | 22 | return; 23 | } 24 | $k = Base64UrlSafe::decode($k); 25 | $kLength = 8 * mb_strlen($k, '8bit'); 26 | if ($kLength < 128) { 27 | $bag->add(Message::high('The key length is less than 128 bits.')); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Analyzer/RsaAnalyzer.php: -------------------------------------------------------------------------------- 1 | get('kty') !== 'RSA') { 18 | return; 19 | } 20 | 21 | $this->checkExponent($jwk, $bag); 22 | $this->checkModulus($jwk, $bag); 23 | } 24 | 25 | private function checkExponent(JWK $jwk, MessageBag $bag): void 26 | { 27 | $e = $jwk->get('e'); 28 | if (! is_string($e)) { 29 | $bag->add(Message::high('The exponent is not valid.')); 30 | 31 | return; 32 | } 33 | $exponent = unpack('l', str_pad(Base64UrlSafe::decode($e), 4, "\0")); 34 | if (! is_array($exponent) || ! isset($exponent[1])) { 35 | throw new InvalidArgumentException('Unable to get the private key'); 36 | } 37 | if ($exponent[1] < 65537) { 38 | $bag->add(Message::high('The exponent is too low. It should be at least 65537.')); 39 | } 40 | } 41 | 42 | private function checkModulus(JWK $jwk, MessageBag $bag): void 43 | { 44 | $n = $jwk->get('n'); 45 | if (! is_string($n)) { 46 | $bag->add(Message::high('The modulus is not valid.')); 47 | 48 | return; 49 | } 50 | $n = 8 * mb_strlen(Base64UrlSafe::decode($n), '8bit'); 51 | if ($n < 2048) { 52 | $bag->add(Message::high('The key length is less than 2048 bits.')); 53 | } 54 | if ($jwk->has('d') && (! $jwk->has('p') || ! $jwk->has('q') || ! $jwk->has('dp') || ! $jwk->has( 55 | 'dq' 56 | ) || ! $jwk->has('qi'))) { 57 | $bag->add( 58 | Message::medium( 59 | 'The key is a private RSA key, but Chinese Remainder Theorem primes are missing. These primes are not mandatory, but signatures and decryption processes are faster when available.' 60 | ) 61 | ); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Analyzer/UsageAnalyzer.php: -------------------------------------------------------------------------------- 1 | has('use')) { 15 | $bag->add(Message::medium('The parameter "use" should be added.')); 16 | } elseif (! in_array($jwk->get('use'), ['sig', 'enc'], true)) { 17 | $bag->add( 18 | Message::high(sprintf( 19 | 'The parameter "use" has an unsupported value "%s". Please use "sig" (signature) or "enc" (encryption).', 20 | $jwk->get('use') 21 | )) 22 | ); 23 | } 24 | if ($jwk->has('key_ops') && ! in_array( 25 | $jwk->get('key_ops'), 26 | ['sign', 'verify', 'encrypt', 'decrypt', 'wrapKey', 'unwrapKey'], 27 | true 28 | )) { 29 | $bag->add( 30 | Message::high(sprintf( 31 | 'The parameter "key_ops" has an unsupported value "%s". Please use one of the following values: %s.', 32 | $jwk->get('key_ops'), 33 | implode(', ', ['verify', 'sign', 'encryp', 'decrypt', 'wrapKey', 'unwrapKey']) 34 | )) 35 | ); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Analyzer/ZxcvbnKeyAnalyzer.php: -------------------------------------------------------------------------------- 1 | get('kty') !== 'oct') { 18 | return; 19 | } 20 | $k = $jwk->get('k'); 21 | if (! is_string($k)) { 22 | $bag->add(Message::high('The key is not valid')); 23 | 24 | return; 25 | } 26 | $k = Base64UrlSafe::decode($k); 27 | if (! class_exists(Zxcvbn::class)) { 28 | return; 29 | } 30 | $zxcvbn = new Zxcvbn(); 31 | try { 32 | $strength = $zxcvbn->passwordStrength($k); 33 | switch (true) { 34 | case $strength['score'] < 3: 35 | $bag->add( 36 | Message::high( 37 | 'The octet string is weak and easily guessable. Please change your key as soon as possible.' 38 | ) 39 | ); 40 | 41 | break; 42 | 43 | case $strength['score'] === 3: 44 | $bag->add(Message::medium('The octet string is safe, but a longer key is preferable.')); 45 | 46 | break; 47 | 48 | default: 49 | break; 50 | } 51 | } catch (Throwable) { 52 | $bag->add(Message::medium('The test of the weakness cannot be performed.')); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /JKUFactory.php: -------------------------------------------------------------------------------- 1 | getContent($url, $header); 20 | $data = JsonConverter::decode($content); 21 | if (! is_array($data)) { 22 | throw new RuntimeException('Invalid content.'); 23 | } 24 | 25 | return JWKSet::createFromKeyData($data); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /JWKFactory.php: -------------------------------------------------------------------------------- 1 | $size, 43 | 'private_key_type' => OPENSSL_KEYTYPE_RSA, 44 | ]); 45 | if ($key === false) { 46 | throw new InvalidArgumentException('Unable to create the key'); 47 | } 48 | $details = openssl_pkey_get_details($key); 49 | if (! is_array($details)) { 50 | throw new InvalidArgumentException('Unable to create the key'); 51 | } 52 | $rsa = RSAKey::createFromKeyDetails($details['rsa']); 53 | $values = array_merge($values, $rsa->toArray()); 54 | 55 | return new JWK($values); 56 | } 57 | 58 | /** 59 | * Creates a EC key with the given curve and additional values. 60 | * 61 | * @param string $curve The curve 62 | * @param array $values values to configure the key 63 | */ 64 | public static function createECKey(string $curve, array $values = []): JWK 65 | { 66 | return ECKey::createECKey($curve, $values); 67 | } 68 | 69 | /** 70 | * Creates a octet key with the given key size and additional values. 71 | * 72 | * @param int $size The key size in bits 73 | * @param array $values values to configure the key 74 | */ 75 | public static function createOctKey(int $size, array $values = []): JWK 76 | { 77 | if ($size % 8 !== 0) { 78 | throw new InvalidArgumentException('Invalid key size.'); 79 | } 80 | $values = array_merge( 81 | $values, 82 | [ 83 | 'kty' => 'oct', 84 | 'k' => Base64UrlSafe::encodeUnpadded(random_bytes($size / 8)), 85 | ] 86 | ); 87 | 88 | return new JWK($values); 89 | } 90 | 91 | /** 92 | * Creates a OKP key with the given curve and additional values. 93 | * 94 | * @param string $curve The curve 95 | * @param array $values values to configure the key 96 | */ 97 | public static function createOKPKey(string $curve, array $values = []): JWK 98 | { 99 | if (! extension_loaded('sodium')) { 100 | throw new RuntimeException('The extension "sodium" is not available. Please install it to use this method'); 101 | } 102 | 103 | switch ($curve) { 104 | case 'X25519': 105 | $keyPair = sodium_crypto_box_keypair(); 106 | $secret = sodium_crypto_box_secretkey($keyPair); 107 | $x = sodium_crypto_box_publickey($keyPair); 108 | 109 | break; 110 | 111 | case 'Ed25519': 112 | $keyPair = sodium_crypto_sign_keypair(); 113 | $secret = sodium_crypto_sign_secretkey($keyPair); 114 | $x = sodium_crypto_sign_publickey($keyPair); 115 | 116 | break; 117 | 118 | default: 119 | throw new InvalidArgumentException(sprintf('Unsupported "%s" curve', $curve)); 120 | } 121 | $secretLength = mb_strlen($secret, '8bit'); 122 | $d = mb_substr($secret, 0, -$secretLength / 2, '8bit'); 123 | 124 | $values = array_merge( 125 | $values, 126 | [ 127 | 'kty' => 'OKP', 128 | 'crv' => $curve, 129 | 'd' => Base64UrlSafe::encodeUnpadded($d), 130 | 'x' => Base64UrlSafe::encodeUnpadded($x), 131 | ] 132 | ); 133 | 134 | return new JWK($values); 135 | } 136 | 137 | /** 138 | * Creates a none key with the given additional values. Please note that this key type is not pat of any 139 | * specification. It is used to prevent the use of the "none" algorithm with other key types. 140 | * 141 | * @param array $values values to configure the key 142 | */ 143 | public static function createNoneKey(array $values = []): JWK 144 | { 145 | $values = array_merge($values, [ 146 | 'kty' => 'none', 147 | 'alg' => 'none', 148 | 'use' => 'sig', 149 | ]); 150 | 151 | return new JWK($values); 152 | } 153 | 154 | /** 155 | * Creates a key from a Json string. 156 | */ 157 | public static function createFromJsonObject(string $value): JWK|JWKSet 158 | { 159 | $json = json_decode($value, true, 512, JSON_THROW_ON_ERROR); 160 | if (! is_array($json)) { 161 | throw new InvalidArgumentException('Invalid key or key set.'); 162 | } 163 | 164 | return self::createFromValues($json); 165 | } 166 | 167 | /** 168 | * Creates a key or key set from the given input. 169 | */ 170 | public static function createFromValues(array $values): JWK|JWKSet 171 | { 172 | if (array_key_exists('keys', $values) && is_array($values['keys'])) { 173 | return JWKSet::createFromKeyData($values); 174 | } 175 | 176 | return new JWK($values); 177 | } 178 | 179 | /** 180 | * This method create a JWK object using a shared secret. 181 | */ 182 | public static function createFromSecret(string $secret, array $additional_values = []): JWK 183 | { 184 | $values = array_merge( 185 | $additional_values, 186 | [ 187 | 'kty' => 'oct', 188 | 'k' => Base64UrlSafe::encodeUnpadded($secret), 189 | ] 190 | ); 191 | 192 | return new JWK($values); 193 | } 194 | 195 | /** 196 | * This method will try to load a X.509 certificate and convert it into a public key. 197 | */ 198 | public static function createFromCertificateFile(string $file, array $additional_values = []): JWK 199 | { 200 | $values = KeyConverter::loadKeyFromCertificateFile($file); 201 | $values = array_merge($values, $additional_values); 202 | 203 | return new JWK($values); 204 | } 205 | 206 | /** 207 | * Extract a keyfrom a key set identified by the given index . 208 | */ 209 | public static function createFromKeySet(JWKSet $jwkset, int|string $index): JWK 210 | { 211 | return $jwkset->get($index); 212 | } 213 | 214 | /** 215 | * This method will try to load a PKCS#12 file and convert it into a public key. 216 | */ 217 | public static function createFromPKCS12CertificateFile( 218 | string $file, 219 | string $secret = '', 220 | array $additional_values = [] 221 | ): JWK { 222 | try { 223 | $content = file_get_contents($file); 224 | if (! is_string($content)) { 225 | throw new RuntimeException('Unable to read the file.'); 226 | } 227 | openssl_pkcs12_read($content, $certs, $secret); 228 | } catch (Throwable $throwable) { 229 | throw new RuntimeException('Unable to load the certificates.', $throwable->getCode(), $throwable); 230 | } 231 | if (! is_array($certs) || ! array_key_exists('pkey', $certs)) { 232 | throw new RuntimeException('Unable to load the certificates.'); 233 | } 234 | 235 | return self::createFromKey($certs['pkey'], null, $additional_values); 236 | } 237 | 238 | /** 239 | * This method will try to convert a X.509 certificate into a public key. 240 | */ 241 | public static function createFromCertificate(string $certificate, array $additional_values = []): JWK 242 | { 243 | $values = KeyConverter::loadKeyFromCertificate($certificate); 244 | $values = array_merge($values, $additional_values); 245 | 246 | return new JWK($values); 247 | } 248 | 249 | /** 250 | * This method will try to convert a X.509 certificate resource into a public key. 251 | */ 252 | public static function createFromX509Resource(OpenSSLCertificate $res, array $additional_values = []): JWK 253 | { 254 | $values = KeyConverter::loadKeyFromX509Resource($res); 255 | $values = array_merge($values, $additional_values); 256 | 257 | return new JWK($values); 258 | } 259 | 260 | /** 261 | * This method will try to load and convert a key file into a JWK object. If the key is encrypted, the password must 262 | * be set. 263 | */ 264 | public static function createFromKeyFile(string $file, ?string $password = null, array $additional_values = []): JWK 265 | { 266 | $values = KeyConverter::loadFromKeyFile($file, $password); 267 | $values = array_merge($values, $additional_values); 268 | 269 | return new JWK($values); 270 | } 271 | 272 | /** 273 | * This method will try to load and convert a key into a JWK object. If the key is encrypted, the password must be 274 | * set. 275 | */ 276 | public static function createFromKey(string $key, ?string $password = null, array $additional_values = []): JWK 277 | { 278 | $values = KeyConverter::loadFromKey($key, $password); 279 | $values = array_merge($values, $additional_values); 280 | 281 | return new JWK($values); 282 | } 283 | 284 | /** 285 | * This method will try to load and convert a X.509 certificate chain into a public key. 286 | * 287 | * Be careful! The certificate chain is loaded, but it is NOT VERIFIED by any mean! It is mandatory to verify the 288 | * root CA or intermediate CA are trusted. If not done, it may lead to potential security issues. 289 | */ 290 | public static function createFromX5C(array $x5c, array $additional_values = []): JWK 291 | { 292 | $values = KeyConverter::loadFromX5C($x5c); 293 | $values = array_merge($values, $additional_values); 294 | 295 | return new JWK($values); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /KeyConverter/ECKey.php: -------------------------------------------------------------------------------- 1 | loadJWK($data); 31 | } 32 | 33 | public static function createFromPEM(string $pem): self 34 | { 35 | $data = self::loadPEM($pem); 36 | 37 | return new self($data); 38 | } 39 | 40 | public static function toPublic(self $private): self 41 | { 42 | $data = $private->toArray(); 43 | if (array_key_exists('d', $data)) { 44 | unset($data['d']); 45 | } 46 | 47 | return new self($data); 48 | } 49 | 50 | /** 51 | * @return array 52 | */ 53 | public function toArray() 54 | { 55 | return $this->values; 56 | } 57 | 58 | private static function loadPEM(string $data): array 59 | { 60 | $data = base64_decode(preg_replace('#-.*-|\r|\n#', '', $data) ?? '', true); 61 | $asnObject = ASNObject::fromBinary($data); 62 | if (! $asnObject instanceof Sequence) { 63 | throw new InvalidArgumentException('Unable to load the key.'); 64 | } 65 | $children = $asnObject->getChildren(); 66 | if (self::isPKCS8($children)) { 67 | $children = self::loadPKCS8($children); 68 | } 69 | 70 | if (count($children) === 4) { 71 | return self::loadPrivatePEM($children); 72 | } 73 | if (count($children) === 2) { 74 | return self::loadPublicPEM($children); 75 | } 76 | 77 | throw new InvalidArgumentException('Unable to load the key.'); 78 | } 79 | 80 | /** 81 | * @param ASNObject[] $children 82 | */ 83 | private static function loadPKCS8(array $children): array 84 | { 85 | $data = $children[2]->getContent(); 86 | if (! is_string($data)) { 87 | throw new InvalidArgumentException('Unable to load the key.'); 88 | } 89 | $binary = hex2bin($data); 90 | $asnObject = ASNObject::fromBinary($binary); 91 | if (! $asnObject instanceof Sequence) { 92 | throw new InvalidArgumentException('Unable to load the key.'); 93 | } 94 | 95 | return $asnObject->getChildren(); 96 | } 97 | 98 | private static function loadPublicPEM(array $children): array 99 | { 100 | if (! $children[0] instanceof Sequence) { 101 | throw new InvalidArgumentException('Unsupported key type.'); 102 | } 103 | 104 | $sub = $children[0]->getChildren(); 105 | if (! $sub[0] instanceof ObjectIdentifier) { 106 | throw new InvalidArgumentException('Unsupported key type.'); 107 | } 108 | if ($sub[0]->getContent() !== '1.2.840.10045.2.1') { 109 | throw new InvalidArgumentException('Unsupported key type.'); 110 | } 111 | if (! $sub[1] instanceof ObjectIdentifier) { 112 | throw new InvalidArgumentException('Unsupported key type.'); 113 | } 114 | if (! $children[1] instanceof BitString) { 115 | throw new InvalidArgumentException('Unable to load the key.'); 116 | } 117 | 118 | $bits = $children[1]->getContent(); 119 | if (! is_string($bits)) { 120 | throw new InvalidArgumentException('Unsupported key type'); 121 | } 122 | $bits_length = mb_strlen($bits, '8bit'); 123 | if (mb_strpos($bits, '04', 0, '8bit') !== 0) { 124 | throw new InvalidArgumentException('Unsupported key type'); 125 | } 126 | 127 | $values = [ 128 | 'kty' => 'EC', 129 | ]; 130 | $oid = $sub[1]->getContent(); 131 | if (! is_string($oid)) { 132 | throw new InvalidArgumentException('Unsupported key type'); 133 | } 134 | $values['crv'] = self::getCurve($oid); 135 | 136 | $xBin = hex2bin(mb_substr($bits, 2, ($bits_length - 2) / 2, '8bit')); 137 | $yBin = hex2bin(mb_substr($bits, (int) (($bits_length - 2) / 2 + 2), ($bits_length - 2) / 2, '8bit')); 138 | if (! is_string($xBin) || ! is_string($yBin)) { 139 | throw new InvalidArgumentException('Unable to load the key.'); 140 | } 141 | 142 | $values['x'] = Base64UrlSafe::encodeUnpadded($xBin); 143 | $values['y'] = Base64UrlSafe::encodeUnpadded($yBin); 144 | 145 | return $values; 146 | } 147 | 148 | private static function getCurve(string $oid): string 149 | { 150 | $curves = self::getSupportedCurves(); 151 | $curve = array_search($oid, $curves, true); 152 | if (! is_string($curve)) { 153 | throw new InvalidArgumentException('Unsupported OID.'); 154 | } 155 | 156 | return $curve; 157 | } 158 | 159 | private static function getSupportedCurves(): array 160 | { 161 | return [ 162 | 'P-256' => '1.2.840.10045.3.1.7', 163 | 'P-384' => '1.3.132.0.34', 164 | 'P-521' => '1.3.132.0.35', 165 | ]; 166 | } 167 | 168 | private static function verifyVersion(ASNObject $children): void 169 | { 170 | if (! $children instanceof Integer || $children->getContent() !== '1') { 171 | throw new InvalidArgumentException('Unable to load the key.'); 172 | } 173 | } 174 | 175 | private static function getXAndY(ASNObject $children, string &$x, string &$y): void 176 | { 177 | if (! $children instanceof ExplicitlyTaggedObject || ! is_array($children->getContent())) { 178 | throw new InvalidArgumentException('Unable to load the key.'); 179 | } 180 | if (! $children->getContent()[0] instanceof BitString) { 181 | throw new InvalidArgumentException('Unable to load the key.'); 182 | } 183 | 184 | $bits = $children->getContent()[0] 185 | ->getContent() 186 | ; 187 | if (! is_string($bits)) { 188 | throw new InvalidArgumentException('Unsupported key type'); 189 | } 190 | $bits_length = mb_strlen($bits, '8bit'); 191 | 192 | if (mb_strpos($bits, '04', 0, '8bit') !== 0) { 193 | throw new InvalidArgumentException('Unsupported key type'); 194 | } 195 | 196 | $x = mb_substr($bits, 2, (int) (($bits_length - 2) / 2), '8bit'); 197 | $y = mb_substr($bits, (int) (($bits_length - 2) / 2 + 2), (int) (($bits_length - 2) / 2), '8bit'); 198 | } 199 | 200 | private static function getD(ASNObject $children): string 201 | { 202 | if (! $children instanceof OctetString) { 203 | throw new InvalidArgumentException('Unable to load the key.'); 204 | } 205 | $data = $children->getContent(); 206 | if (! is_string($data)) { 207 | throw new InvalidArgumentException('Unable to load the key.'); 208 | } 209 | 210 | return $data; 211 | } 212 | 213 | private static function loadPrivatePEM(array $children): array 214 | { 215 | self::verifyVersion($children[0]); 216 | $x = ''; 217 | $y = ''; 218 | $d = self::getD($children[1]); 219 | self::getXAndY($children[3], $x, $y); 220 | 221 | if (! $children[2] instanceof ExplicitlyTaggedObject || ! is_array($children[2]->getContent())) { 222 | throw new InvalidArgumentException('Unable to load the key.'); 223 | } 224 | if (! $children[2]->getContent()[0] instanceof ObjectIdentifier) { 225 | throw new InvalidArgumentException('Unable to load the key.'); 226 | } 227 | 228 | $curve = $children[2]->getContent()[0]->getContent(); 229 | $dBin = hex2bin($d); 230 | $xBin = hex2bin($x); 231 | $yBin = hex2bin($y); 232 | if (! is_string($curve) || ! is_string($dBin) || ! is_string($xBin) || ! is_string($yBin)) { 233 | throw new InvalidArgumentException('Unable to load the key.'); 234 | } 235 | 236 | $values = [ 237 | 'kty' => 'EC', 238 | ]; 239 | $values['crv'] = self::getCurve($curve); 240 | $values['d'] = Base64UrlSafe::encodeUnpadded($dBin); 241 | $values['x'] = Base64UrlSafe::encodeUnpadded($xBin); 242 | $values['y'] = Base64UrlSafe::encodeUnpadded($yBin); 243 | 244 | return $values; 245 | } 246 | 247 | /** 248 | * @param ASNObject[] $children 249 | */ 250 | private static function isPKCS8(array $children): bool 251 | { 252 | if (count($children) !== 3) { 253 | return false; 254 | } 255 | 256 | $classes = [ 257 | 0 => Integer::class, 258 | 1 => Sequence::class, 259 | 2 => OctetString::class, 260 | ]; 261 | foreach ($classes as $k => $class) { 262 | if (! $children[$k] instanceof $class) { 263 | return false; 264 | } 265 | } 266 | 267 | return true; 268 | } 269 | 270 | private function loadJWK(array $jwk): void 271 | { 272 | $keys = [ 273 | 'kty' => 'The key parameter "kty" is missing.', 274 | 'crv' => 'Curve parameter is missing', 275 | 'x' => 'Point parameters are missing.', 276 | 'y' => 'Point parameters are missing.', 277 | ]; 278 | foreach ($keys as $k => $v) { 279 | if (! array_key_exists($k, $jwk)) { 280 | throw new InvalidArgumentException($v); 281 | } 282 | } 283 | 284 | if ($jwk['kty'] !== 'EC') { 285 | throw new InvalidArgumentException('JWK is not an Elliptic Curve key.'); 286 | } 287 | $this->values = $jwk; 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /KeyConverter/KeyConverter.php: -------------------------------------------------------------------------------- 1 | $cert) { 131 | $x5c[$id] = '-----BEGIN CERTIFICATE-----' . PHP_EOL . chunk_split( 132 | $cert, 133 | 64, 134 | PHP_EOL 135 | ) . '-----END CERTIFICATE-----'; 136 | $x509 = openssl_x509_read($x5c[$id]); 137 | if ($x509 === false) { 138 | throw new InvalidArgumentException('Unable to load the certificate chain'); 139 | } 140 | $parsed = openssl_x509_parse($x509); 141 | if ($parsed === false) { 142 | throw new InvalidArgumentException('Unable to load the certificate chain'); 143 | } 144 | } 145 | 146 | return self::loadKeyFromCertificate(reset($x5c)); 147 | } 148 | 149 | private static function loadKeyFromDER(string $der, ?string $password = null): array 150 | { 151 | $pem = self::convertDerToPem($der); 152 | 153 | return self::loadKeyFromPEM($pem, $password); 154 | } 155 | 156 | private static function loadKeyFromPEM(string $pem, ?string $password = null): array 157 | { 158 | if (preg_match('#DEK-Info: (.+),(.+)#', $pem, $matches) === 1) { 159 | $pem = self::decodePem($pem, $matches, $password); 160 | } 161 | 162 | if (! extension_loaded('openssl')) { 163 | throw new RuntimeException('Please install the OpenSSL extension'); 164 | } 165 | 166 | if (preg_match('#BEGIN ENCRYPTED PRIVATE KEY(.+)(.+)#', $pem) === 1) { 167 | $decrypted = openssl_pkey_get_private($pem, $password); 168 | if ($decrypted === false) { 169 | throw new InvalidArgumentException('Unable to decrypt the key.'); 170 | } 171 | openssl_pkey_export($decrypted, $pem); 172 | } 173 | 174 | self::sanitizePEM($pem); 175 | $res = openssl_pkey_get_private($pem); 176 | if ($res === false) { 177 | $res = openssl_pkey_get_public($pem); 178 | } 179 | if ($res === false) { 180 | throw new InvalidArgumentException('Unable to load the key.'); 181 | } 182 | 183 | $details = openssl_pkey_get_details($res); 184 | if (! is_array($details) || ! array_key_exists('type', $details)) { 185 | throw new InvalidArgumentException('Unable to get details of the key'); 186 | } 187 | 188 | switch ($details['type']) { 189 | case OPENSSL_KEYTYPE_EC: 190 | $ec_key = ECKey::createFromPEM($pem); 191 | 192 | return $ec_key->toArray(); 193 | 194 | case OPENSSL_KEYTYPE_RSA: 195 | $rsa_key = RSAKey::createFromPEM($pem); 196 | 197 | return $rsa_key->toArray(); 198 | 199 | default: 200 | throw new InvalidArgumentException('Unsupported key type'); 201 | } 202 | } 203 | 204 | /** 205 | * This method modifies the PEM to get 64 char lines and fix bug with old OpenSSL versions. 206 | */ 207 | private static function sanitizePEM(string &$pem): void 208 | { 209 | preg_match_all('#(-.*-)#', $pem, $matches, PREG_PATTERN_ORDER); 210 | $ciphertext = preg_replace('#-.*-|\r|\n| #', '', $pem); 211 | 212 | $pem = $matches[0][0] . PHP_EOL; 213 | $pem .= chunk_split($ciphertext ?? '', 64, PHP_EOL); 214 | $pem .= $matches[0][1] . PHP_EOL; 215 | } 216 | 217 | /** 218 | * @param string[] $matches 219 | */ 220 | private static function decodePem(string $pem, array $matches, ?string $password = null): string 221 | { 222 | if ($password === null) { 223 | throw new InvalidArgumentException('Password required for encrypted keys.'); 224 | } 225 | 226 | $iv = pack('H*', trim($matches[2])); 227 | $iv_sub = mb_substr($iv, 0, 8, '8bit'); 228 | $symkey = pack('H*', md5($password . $iv_sub)); 229 | $symkey .= pack('H*', md5($symkey . $password . $iv_sub)); 230 | $key = preg_replace('#^(?:Proc-Type|DEK-Info): .*#m', '', $pem); 231 | $ciphertext = base64_decode(preg_replace('#-.*-|\r|\n#', '', $key ?? '') ?? '', true); 232 | if (! is_string($ciphertext)) { 233 | throw new InvalidArgumentException('Unable to encode the data.'); 234 | } 235 | 236 | $decoded = openssl_decrypt($ciphertext, mb_strtolower($matches[1]), $symkey, OPENSSL_RAW_DATA, $iv); 237 | if ($decoded === false) { 238 | throw new RuntimeException('Unable to decrypt the key'); 239 | } 240 | $number = preg_match_all('#-{5}.*-{5}#', $pem, $result); 241 | if ($number !== 2) { 242 | throw new InvalidArgumentException('Unable to load the key'); 243 | } 244 | 245 | $pem = $result[0][0] . PHP_EOL; 246 | $pem .= chunk_split(base64_encode($decoded), 64); 247 | 248 | return $pem . ($result[0][1] . PHP_EOL); 249 | } 250 | 251 | private static function convertDerToPem(string $der_data): string 252 | { 253 | $pem = chunk_split(base64_encode($der_data), 64, PHP_EOL); 254 | 255 | return '-----BEGIN CERTIFICATE-----' . PHP_EOL . $pem . '-----END CERTIFICATE-----' . PHP_EOL; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /KeyConverter/RSAKey.php: -------------------------------------------------------------------------------- 1 | loadJWK($data); 27 | } 28 | 29 | public static function createFromKeyDetails(array $details): self 30 | { 31 | $values = [ 32 | 'kty' => 'RSA', 33 | ]; 34 | $keys = [ 35 | 'n' => 'n', 36 | 'e' => 'e', 37 | 'd' => 'd', 38 | 'p' => 'p', 39 | 'q' => 'q', 40 | 'dp' => 'dmp1', 41 | 'dq' => 'dmq1', 42 | 'qi' => 'iqmp', 43 | ]; 44 | foreach ($details as $key => $value) { 45 | if (in_array($key, $keys, true)) { 46 | $value = Base64UrlSafe::encodeUnpadded($value); 47 | $values[array_search($key, $keys, true)] = $value; 48 | } 49 | } 50 | 51 | return new self($values); 52 | } 53 | 54 | public static function createFromPEM(string $pem): self 55 | { 56 | if (! extension_loaded('openssl')) { 57 | throw new RuntimeException('Please install the OpenSSL extension'); 58 | } 59 | $res = openssl_pkey_get_private($pem); 60 | if ($res === false) { 61 | $res = openssl_pkey_get_public($pem); 62 | } 63 | if ($res === false) { 64 | throw new InvalidArgumentException('Unable to load the key.'); 65 | } 66 | 67 | $details = openssl_pkey_get_details($res); 68 | if (! is_array($details) || ! isset($details['rsa'])) { 69 | throw new InvalidArgumentException('Unable to load the key.'); 70 | } 71 | 72 | return self::createFromKeyDetails($details['rsa']); 73 | } 74 | 75 | public static function createFromJWK(JWK $jwk): self 76 | { 77 | return new self($jwk->all()); 78 | } 79 | 80 | public function isPublic(): bool 81 | { 82 | return ! array_key_exists('d', $this->values); 83 | } 84 | 85 | public static function toPublic(self $private): self 86 | { 87 | $data = $private->toArray(); 88 | $keys = ['p', 'd', 'q', 'dp', 'dq', 'qi']; 89 | foreach ($keys as $key) { 90 | if (array_key_exists($key, $data)) { 91 | unset($data[$key]); 92 | } 93 | } 94 | 95 | return new self($data); 96 | } 97 | 98 | public function toArray(): array 99 | { 100 | return $this->values; 101 | } 102 | 103 | public function toJwk(): JWK 104 | { 105 | return new JWK($this->values); 106 | } 107 | 108 | /** 109 | * This method will try to add Chinese Remainder Theorem (CRT) parameters. With those primes, the decryption process 110 | * is really fast. 111 | */ 112 | public function optimize(): void 113 | { 114 | if (array_key_exists('d', $this->values)) { 115 | $this->populateCRT(); 116 | } 117 | } 118 | 119 | private function loadJWK(array $jwk): void 120 | { 121 | if (! array_key_exists('kty', $jwk)) { 122 | throw new InvalidArgumentException('The key parameter "kty" is missing.'); 123 | } 124 | if ($jwk['kty'] !== 'RSA') { 125 | throw new InvalidArgumentException('The JWK is not a RSA key.'); 126 | } 127 | 128 | $this->values = $jwk; 129 | } 130 | 131 | /** 132 | * This method adds Chinese Remainder Theorem (CRT) parameters if primes 'p' and 'q' are available. If 'p' and 'q' 133 | * are missing, they are computed and added to the key data. 134 | */ 135 | private function populateCRT(): void 136 | { 137 | if (! array_key_exists('p', $this->values) && ! array_key_exists('q', $this->values)) { 138 | $d = BigInteger::createFromBinaryString(Base64UrlSafe::decode($this->values['d'])); 139 | $e = BigInteger::createFromBinaryString(Base64UrlSafe::decode($this->values['e'])); 140 | $n = BigInteger::createFromBinaryString(Base64UrlSafe::decode($this->values['n'])); 141 | 142 | [$p, $q] = $this->findPrimeFactors($d, $e, $n); 143 | $this->values['p'] = Base64UrlSafe::encodeUnpadded($p->toBytes()); 144 | $this->values['q'] = Base64UrlSafe::encodeUnpadded($q->toBytes()); 145 | } 146 | 147 | if (array_key_exists('dp', $this->values) && array_key_exists('dq', $this->values) && array_key_exists( 148 | 'qi', 149 | $this->values 150 | )) { 151 | return; 152 | } 153 | 154 | $one = BigInteger::createFromDecimal(1); 155 | $d = BigInteger::createFromBinaryString(Base64UrlSafe::decode($this->values['d'])); 156 | $p = BigInteger::createFromBinaryString(Base64UrlSafe::decode($this->values['p'])); 157 | $q = BigInteger::createFromBinaryString(Base64UrlSafe::decode($this->values['q'])); 158 | 159 | $this->values['dp'] = Base64UrlSafe::encodeUnpadded($d->mod($p->subtract($one))->toBytes()); 160 | $this->values['dq'] = Base64UrlSafe::encodeUnpadded($d->mod($q->subtract($one))->toBytes()); 161 | $this->values['qi'] = Base64UrlSafe::encodeUnpadded($q->modInverse($p)->toBytes()); 162 | } 163 | 164 | /** 165 | * @return BigInteger[] 166 | */ 167 | private function findPrimeFactors(BigInteger $d, BigInteger $e, BigInteger $n): array 168 | { 169 | $zero = BigInteger::createFromDecimal(0); 170 | $one = BigInteger::createFromDecimal(1); 171 | $two = BigInteger::createFromDecimal(2); 172 | 173 | $k = $d->multiply($e) 174 | ->subtract($one) 175 | ; 176 | 177 | if ($k->isEven()) { 178 | $r = $k; 179 | $t = $zero; 180 | 181 | do { 182 | $r = $r->divide($two); 183 | $t = $t->add($one); 184 | } while ($r->isEven()); 185 | 186 | $found = false; 187 | $y = null; 188 | 189 | for ($i = 1; $i <= 100; ++$i) { 190 | $g = BigInteger::random($n->subtract($one)); 191 | $y = $g->modPow($r, $n); 192 | 193 | if ($y->equals($one) || $y->equals($n->subtract($one))) { 194 | continue; 195 | } 196 | 197 | for ($j = $one; $j->lowerThan($t->subtract($one)); $j = $j->add($one)) { 198 | $x = $y->modPow($two, $n); 199 | 200 | if ($x->equals($one)) { 201 | $found = true; 202 | 203 | break; 204 | } 205 | 206 | if ($x->equals($n->subtract($one))) { 207 | continue; 208 | } 209 | 210 | $y = $x; 211 | } 212 | 213 | $x = $y->modPow($two, $n); 214 | if ($x->equals($one)) { 215 | $found = true; 216 | 217 | break; 218 | } 219 | } 220 | if ($y === null) { 221 | throw new InvalidArgumentException('Unable to find prime factors.'); 222 | } 223 | if ($found === true) { 224 | $p = $y->subtract($one) 225 | ->gcd($n) 226 | ; 227 | $q = $n->divide($p); 228 | 229 | return [$p, $q]; 230 | } 231 | } 232 | 233 | throw new InvalidArgumentException('Unable to find prime factors.'); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2019 Spomky-Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PHP JWT Key Management Component 2 | ================================ 3 | 4 | This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY. 5 | 6 | **Please do not submit any Pull Request here.** 7 | You should go to [the main repository](https://github.com/web-token/jwt-framework) instead. 8 | 9 | # Documentation 10 | 11 | The official documentation is available as https://web-token.spomky-labs.com/ 12 | 13 | # Licence 14 | 15 | This software is release under [MIT licence](LICENSE). 16 | -------------------------------------------------------------------------------- /UrlKeySetFactory.php: -------------------------------------------------------------------------------- 1 | requestFactory->createRequest('GET', $url); 22 | foreach ($header as $k => $v) { 23 | $request = $request->withHeader($k, $v); 24 | } 25 | $response = $this->client->sendRequest($request); 26 | 27 | if ($response->getStatusCode() >= 400) { 28 | throw new RuntimeException('Unable to get the key set.', $response->getStatusCode()); 29 | } 30 | 31 | return $response->getBody() 32 | ->getContents() 33 | ; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /X5UFactory.php: -------------------------------------------------------------------------------- 1 | getContent($url, $header); 24 | $data = JsonConverter::decode($content); 25 | if (! is_array($data)) { 26 | throw new RuntimeException('Invalid content.'); 27 | } 28 | 29 | $keys = []; 30 | foreach ($data as $kid => $cert) { 31 | if (mb_strpos($cert, '-----BEGIN CERTIFICATE-----') === false) { 32 | $cert = '-----BEGIN CERTIFICATE-----' . PHP_EOL . $cert . PHP_EOL . '-----END CERTIFICATE-----'; 33 | } 34 | $jwk = KeyConverter::loadKeyFromCertificate($cert); 35 | if (is_string($kid)) { 36 | $jwk['kid'] = $kid; 37 | $keys[$kid] = new JWK($jwk); 38 | } else { 39 | $keys[] = new JWK($jwk); 40 | } 41 | } 42 | 43 | return new JWKSet($keys); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-token/jwt-key-mgmt", 3 | "description": "Key Management component of the JWT Framework.", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": ["JWS", "JWT", "JWE", "JWA", "JWK", "JWKSet", "Jot", "Jose", "RFC7515", "RFC7516", "RFC7517", "RFC7518", "RFC7519", "RFC7520", "Bundle", "Symfony"], 7 | "homepage": "https://github.com/web-token", 8 | "authors": [ 9 | { 10 | "name": "Florent Morselli", 11 | "homepage": "https://github.com/Spomky" 12 | },{ 13 | "name": "All contributors", 14 | "homepage": "https://github.com/web-token/jwt-key-mgmt/contributors" 15 | } 16 | ], 17 | "autoload": { 18 | "psr-4": { 19 | "Jose\\Component\\KeyManagement\\": "" 20 | } 21 | }, 22 | "require": { 23 | "php": ">=8.1", 24 | "ext-openssl": "*", 25 | "psr/http-factory": "^1.0", 26 | "psr/http-client": "^1.0", 27 | "web-token/jwt-core": "^3.0" 28 | }, 29 | "suggest": { 30 | "ext-sodium": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys", 31 | "web-token/jwt-util-ecc": "To use EC key analyzers.", 32 | "php-http/message-factory": "To enable JKU/X5U support.", 33 | "php-http/httplug": "To enable JKU/X5U support." 34 | } 35 | } 36 | --------------------------------------------------------------------------------