Failed Conditions
Push — master ( 281676...9ce453 )
by Florent
02:59
created

JWKFactory::createECKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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