Failed Conditions
Pull Request — master (#126)
by Florent
02:54
created

JWKFactory::createRotatableKeySet()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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