Completed
Push — master ( a88e8f...a2bcd2 )
by Florent
07:43 queued 04:54
created

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