Completed
Push — master ( 765f89...887aa0 )
by Florent
03:16
created

JWKFactory::createStorableKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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