Completed
Push — master ( 65e224...f25ceb )
by Florent
03:47
created

JWKFactory::createKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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