Failed Conditions
Push — JWKSet ( 3580c0...03e3f0 )
by Florent
02:43
created

JWKFactory::createFromX5C()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 1
eloc 4
nc 1
nop 2
1
<?php
2
3
/*
4
 * The MIT License (MIT)
5
 *
6
 * Copyright (c) 2014-2016 Spomky-Labs
7
 *
8
 * This software may be modified and distributed under the terms
9
 * of the MIT license.  See the LICENSE file for details.
10
 */
11
12
namespace Jose\Factory;
13
14
use Assert\Assertion;
15
use Base64Url\Base64Url;
16
use Jose\KeyConverter\KeyConverter;
17
use Jose\KeyConverter\RSAKey;
18
use Jose\Object\JWK;
19
use Jose\Object\JWKSet;
20
use Jose\Object\JWKSetInterface;
21
use Jose\Object\JWKSets;
22
use Jose\Object\PublicJWKSet;
23
use Jose\Object\RotatableJWK;
24
use Jose\Object\RotatableJWKSet;
25
use Jose\Object\StorableJWK;
26
use Jose\Object\StorableJWKSet;
27
use Mdanter\Ecc\Curves\CurveFactory;
28
use Mdanter\Ecc\Curves\NistCurve;
29
use Mdanter\Ecc\EccFactory;
30
use Psr\Cache\CacheItemPoolInterface;
31
32
final class JWKFactory implements JWKFactoryInterface
33
{
34
    /**
35
     * {@inheritdoc}
36
     */
37
    public static function createPublicKeySet(JWKSetInterface $jwkset)
38
    {
39
        return new PublicJWKSet($jwkset);
40
    }
41
42
    /**
43
     * {@inheritdoc}
44
     */
45
    public static function createKeySets(array $jwksets)
46
    {
47
        return new JWKSets($jwksets);
48
    }
49
50
    /**
51
     * {@inheritdoc}
52
     */
53
    public static function createStorableKey($filename, array $parameters)
54
    {
55
        return new StorableJWK($filename, $parameters);
56
    }
57
58
    /**
59
     * {@inheritdoc}
60
     */
61
    public static function createRotatableKey($filename, array $parameters, $ttl)
62
    {
63
        return new RotatableJWK($filename, $parameters, $ttl);
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    public static function createRotatableKeySet($filename, array $parameters, $nb_keys, $ttl)
70
    {
71
        return new RotatableJWKSet($filename, $parameters, $nb_keys, $ttl);
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    public static function createStorableKeySet($filename, array $parameters, $nb_keys)
78
    {
79
        return new StorableJWKSet($filename, $parameters, $nb_keys);
80
    }
81
82
    /**
83
     * {@inheritdoc}
84
     */
85
    public static function createKey(array $config)
86
    {
87
        Assertion::keyExists($config, 'kty', 'The key "kty" must be set');
88
        $supported_types = ['RSA' => 'RSA', 'OKP' => 'OKP', 'EC' => 'EC', 'oct' => 'Oct', 'none' => 'None'];
89
        $kty = $config['kty'];
90
        Assertion::keyExists($supported_types, $kty, sprintf('The key type "%s" is not supported. Please use one of %s', $kty, json_encode(array_keys($supported_types))));
91
        $method = sprintf('create%sKey', $supported_types[$kty]);
92
93
        return self::$method($config);
94
    }
95
96
    /**
97
     * {@inheritdoc}
98
     */
99
    public static function createRSAKey(array $values)
100
    {
101
        Assertion::keyExists($values, 'size', 'The key size is not set.');
102
        $size = $values['size'];
103
        unset($values['size']);
104
105
        Assertion::true(0 === $size % 8, 'Invalid key size.');
106
        Assertion::greaterOrEqualThan($size, 384, 'Key length is too short. It needs to be at least 384 bits.');
107
108
        $key = openssl_pkey_new([
109
            'private_key_bits' => $size,
110
            'private_key_type' => OPENSSL_KEYTYPE_RSA,
111
        ]);
112
        openssl_pkey_export($key, $out);
113
        $rsa = new RSAKey($out);
114
        $values = array_merge(
115
            $values,
116
            $rsa->toArray()
117
        );
118
119
        return new JWK($values);
120
    }
121
122
    /**
123
     * {@inheritdoc}
124
     */
125
    public static function createECKey(array $values)
126
    {
127
        Assertion::keyExists($values, 'crv', 'The curve is not set.');
128
        $curve = $values['crv'];
129
        $curve_name = self::getNistName($curve);
130
        $generator = CurveFactory::getGeneratorByName($curve_name);
131
        $private_key = $generator->createPrivateKey();
132
133
        $values = array_merge(
134
            $values,
135
            [
136
                'kty' => 'EC',
137
                'crv' => $curve,
138
                'x'   => self::encodeValue($private_key->getPublicKey()->getPoint()->getX()),
139
                'y'   => self::encodeValue($private_key->getPublicKey()->getPoint()->getY()),
140
                'd'   => self::encodeValue($private_key->getSecret()),
141
            ]
142
        );
143
144
        return new JWK($values);
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150
    public static function createOctKey(array $values)
151
    {
152
        Assertion::keyExists($values, 'size', 'The key size is not set.');
153
        $size = $values['size'];
154
        unset($values['size']);
155
        Assertion::true(0 === $size % 8, 'Invalid key size.');
156
        $values = array_merge(
157
            $values,
158
            [
159
                'kty' => 'oct',
160
                'k'   => Base64Url::encode(random_bytes($size / 8)),
161
            ]
162
        );
163
164
        return new JWK($values);
165
    }
166
167
    /**
168
     * {@inheritdoc}
169
     */
170
    public static function createOKPKey(array $values)
171
    {
172
        Assertion::keyExists($values, 'crv', 'The curve is not set.');
173
        $curve = $values['crv'];
174
        switch ($curve) {
175
            case 'X25519':
176
                Assertion::true(function_exists('curve25519_public'), sprintf('Unsupported "%s" curve', $curve));
177
                $d = random_bytes(32);
178
                $x = curve25519_public($d);
179
                break;
180
            case 'Ed25519':
181
                Assertion::true(function_exists('ed25519_publickey'), sprintf('Unsupported "%s" curve', $curve));
182
                $d = random_bytes(32);
183
                $x = ed25519_publickey($d);
184
                break;
185
            default:
186
                throw new \InvalidArgumentException(sprintf('Unsupported "%s" curve', $curve));
187
        }
188
189
        $values = array_merge(
190
            $values,
191
            [
192
                'kty' => 'OKP',
193
                'crv' => $curve,
194
                'x'   => Base64Url::encode($x),
195
                'd'   => Base64Url::encode($d),
196
            ]
197
        );
198
199
        return new JWK($values);
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     */
205
    public static function createNoneKey(array $values)
206
    {
207
        $values = array_merge(
208
            $values,
209
            [
210
                'kty' => 'none',
211
                'alg' => 'none',
212
                'use' => 'sig',
213
            ]
214
        );
215
216
        return new JWK($values);
217
    }
218
219
    /**
220
     * @param string $value
221
     *
222
     * @return string
223
     */
224
    private static function encodeValue($value)
225
    {
226
        $value = gmp_strval($value);
227
228
        return Base64Url::encode(self::convertDecToBin($value));
229
    }
230
231
    /**
232
     * @param string $value
233
     *
234
     * @return string
235
     */
236
    private static function convertDecToBin($value)
237
    {
238
        $adapter = EccFactory::getAdapter();
239
240
        return hex2bin($adapter->decHex($value));
241
    }
242
243
    /**
244
     * @param string $curve
245
     *
246
     * @throws \InvalidArgumentException
247
     *
248
     * @return string
249
     */
250
    private static function getNistName($curve)
251
    {
252
        switch ($curve) {
253
            case 'P-256':
254
                return NistCurve::NAME_P256;
255
            case 'P-384':
256
                return NistCurve::NAME_P384;
257
            case 'P-521':
258
                return NistCurve::NAME_P521;
259
            default:
260
                throw new \InvalidArgumentException(sprintf('The curve "%s" is not supported.', $curve));
261
        }
262
    }
263
264
    /**
265
     * {@inheritdoc}
266
     */
267
    public static function createFromValues(array $values)
268
    {
269
        if (array_key_exists('keys', $values) && is_array($values['keys'])) {
270
            return new JWKSet($values);
271
        }
272
273
        return new JWK($values);
274
    }
275
276
    /**
277
     * {@inheritdoc}
278
     */
279
    public static function createFromCertificateFile($file, array $additional_values = [])
280
    {
281
        $values = KeyConverter::loadKeyFromCertificateFile($file);
282
        $values = array_merge($values, $additional_values);
283
284
        return new JWK($values);
285
    }
286
287
    /**
288
     * {@inheritdoc}
289
     */
290
    public static function createFromCertificate($certificate, array $additional_values = [])
291
    {
292
        $values = KeyConverter::loadKeyFromCertificate($certificate);
293
        $values = array_merge($values, $additional_values);
294
295
        return new JWK($values);
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     */
301
    public static function createFromX509Resource($res, array $additional_values = [])
302
    {
303
        $values = KeyConverter::loadKeyFromX509Resource($res);
304
        $values = array_merge($values, $additional_values);
305
306
        return new JWK($values);
307
    }
308
309
    /**
310
     * {@inheritdoc}
311
     */
312
    public static function createFromKeyFile($file, $password = null, array $additional_values = [])
313
    {
314
        $values = KeyConverter::loadFromKeyFile($file, $password);
315
        $values = array_merge($values, $additional_values);
316
317
        return new JWK($values);
318
    }
319
320
    /**
321
     * {@inheritdoc}
322
     */
323
    public static function createFromKey($key, $password = null, array $additional_values = [])
324
    {
325
        $values = KeyConverter::loadFromKey($key, $password);
326
        $values = array_merge($values, $additional_values);
327
328
        return new JWK($values);
329
    }
330
331
    /**
332
     * {@inheritdoc}
333
     */
334
    public static function createFromJKU($jku, $allow_unsecured_connection = false, CacheItemPoolInterface $cache = null, $ttl = 300)
335
    {
336
        $content = self::getContent($jku, $allow_unsecured_connection, $cache, $ttl);
337
338
        Assertion::keyExists($content, 'keys', 'Invalid content.');
339
340
        return new JWKSet($content);
341
    }
342
343
    /**
344
     * {@inheritdoc}
345
     */
346
    public static function createFromX5U($x5u, $allow_unsecured_connection = false, CacheItemPoolInterface $cache = null, $ttl = 300)
347
    {
348
        $content = self::getContent($x5u, $allow_unsecured_connection, $cache, $ttl);
349
350
        $jwkset = new JWKSet();
351
        foreach ($content as $kid => $cert) {
352
            $jwk = KeyConverter::loadKeyFromCertificate($cert);
353
            Assertion::notEmpty($jwk, 'Invalid content.');
354
            if (is_string($kid)) {
355
                $jwk['kid'] = $kid;
356
            }
357
            $jwkset->addKey(new JWK($jwk));
358
        }
359
360
        return $jwkset;
361
    }
362
363
    /**
364
     * @param string                                 $url
365
     * @param bool                                   $allow_unsecured_connection
366
     * @param \Psr\Cache\CacheItemPoolInterface|null $cache
367
     * @param int                                    $ttl
368
     *
369
     * @return array
370
     */
371
    private static function getContent($url, $allow_unsecured_connection, CacheItemPoolInterface $cache = null, $ttl = 300)
372
    {
373
        $cache_key = sprintf('JWKFactory-Content-%s', hash('sha512', $url));
374
        if (null !== $cache) {
375
            $item = $cache->getItem($cache_key);
376
            if (!$item->isHit()) {
377
                $content = self::downloadContent($url, $allow_unsecured_connection);
378
                $item->set($content);
379
                $item->expiresAfter($ttl);
380
                $cache->save($item);
381
382
                return $content;
383
            } else {
384
                return $item->get();
385
            }
386
        }
387
388
        return self::downloadContent($url, $allow_unsecured_connection);
389
    }
390
391
    /**
392
     * {@inheritdoc}
393
     */
394
    public static function createFromX5C(array $x5c, array $additional_values = [])
395
    {
396
        $values = KeyConverter::loadFromX5C($x5c);
397
        $values = array_merge($values, $additional_values);
398
399
        return new JWK($values);
400
    }
401
402
    /**
403
     * {@inheritdoc}
404
     */
405
    public static function createFromKeySet(JWKSetInterface $jwk_set, $key_index)
406
    {
407
        Assertion::integer($key_index);
408
409
        return $jwk_set->getKey($key_index);
410
    }
411
412
    /**
413
     * @param string $url
414
     * @param bool   $allow_unsecured_connection
415
     *
416
     * @throws \InvalidArgumentException
417
     *
418
     * @return array
419
     */
420
    private static function downloadContent($url, $allow_unsecured_connection)
421
    {
422
        // The URL must be a valid URL and scheme must be https
423
        Assertion::false(
424
            false === filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED | FILTER_FLAG_HOST_REQUIRED),
425
            'Invalid URL.'
426
        );
427
        Assertion::false(
428
            false === $allow_unsecured_connection && 'https://' !== mb_substr($url, 0, 8, '8bit'),
429
            'Unsecured connection.'
430
        );
431
432
        $params = [
433
            CURLOPT_RETURNTRANSFER => true,
434
            CURLOPT_URL            => $url,
435
        ];
436
        if (false === $allow_unsecured_connection) {
437
            $params[CURLOPT_SSL_VERIFYPEER] = true;
438
            $params[CURLOPT_SSL_VERIFYHOST] = 2;
439
        }
440
441
        $ch = curl_init();
442
        curl_setopt_array($ch, $params);
443
        $content = curl_exec($ch);
444
        $content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
445
        Assertion::eq(1, preg_match('/^application\/json([\s|;].*)?$/', $content_type), sprintf('Content type is not "application/json". It is "%s".', $content_type));
446
        curl_close($ch);
447
448
        Assertion::notEmpty($content, 'Unable to get content.');
449
        $content = json_decode($content, true);
450
        Assertion::isArray($content, 'Invalid content.');
451
452
        return $content;
453
    }
454
}
455