ECKey::getOpensslCurveName()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.4555
c 0
b 0
f 0
cc 5
nc 5
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2019 Spomky-Labs
9
 *
10
 * This software may be modified and distributed under the terms
11
 * of the MIT license.  See the LICENSE file for details.
12
 */
13
14
namespace Jose\Component\Core\Util;
15
16
use Base64Url\Base64Url;
17
use InvalidArgumentException;
18
use Jose\Component\Core\JWK;
19
use RuntimeException;
20
21
/**
22
 * @internal
23
 */
24
class ECKey
25
{
26
    public static function convertToPEM(JWK $jwk): string
27
    {
28
        if ($jwk->has('d')) {
29
            return self::convertPrivateKeyToPEM($jwk);
30
        }
31
32
        return self::convertPublicKeyToPEM($jwk);
33
    }
34
35
    /**
36
     * @throws InvalidArgumentException if the curve is not supported
37
     */
38
    public static function convertPublicKeyToPEM(JWK $jwk): string
39
    {
40
        switch ($jwk->get('crv')) {
41
            case 'P-256':
42
                $der = self::p256PublicKey();
43
44
                break;
45
            case 'secp256k1':
46
                $der = self::p256KPublicKey();
47
48
                break;
49
            case 'P-384':
50
                $der = self::p384PublicKey();
51
52
                break;
53
            case 'P-521':
54
                $der = self::p521PublicKey();
55
56
                break;
57
            default:
58
                throw new InvalidArgumentException('Unsupported curve.');
59
        }
60
        $der .= self::getKey($jwk);
61
        $pem = '-----BEGIN PUBLIC KEY-----'.PHP_EOL;
62
        $pem .= chunk_split(base64_encode($der), 64, PHP_EOL);
63
        $pem .= '-----END PUBLIC KEY-----'.PHP_EOL;
64
65
        return $pem;
66
    }
67
68
    /**
69
     * @throws InvalidArgumentException if the curve is not supported
70
     */
71
    public static function convertPrivateKeyToPEM(JWK $jwk): string
72
    {
73
        switch ($jwk->get('crv')) {
74
            case 'P-256':
75
                $der = self::p256PrivateKey($jwk);
76
77
                break;
78
            case 'secp256k1':
79
                $der = self::p256KPrivateKey($jwk);
80
81
                break;
82
            case 'P-384':
83
                $der = self::p384PrivateKey($jwk);
84
85
                break;
86
            case 'P-521':
87
                $der = self::p521PrivateKey($jwk);
88
89
                break;
90
            default:
91
                throw new InvalidArgumentException('Unsupported curve.');
92
        }
93
        $der .= self::getKey($jwk);
94
        $pem = '-----BEGIN EC PRIVATE KEY-----'.PHP_EOL;
95
        $pem .= chunk_split(base64_encode($der), 64, PHP_EOL);
96
        $pem .= '-----END EC PRIVATE KEY-----'.PHP_EOL;
97
98
        return $pem;
99
    }
100
101
    /**
102
     * Creates a EC key with the given curve and additional values.
103
     *
104
     * @param string $curve  The curve
105
     * @param array  $values values to configure the key
106
     */
107
    public static function createECKey(string $curve, array $values = []): JWK
108
    {
109
        $jwk = self::createECKeyUsingOpenSSL($curve);
110
        $values = array_merge($values, $jwk);
111
112
        return new JWK($values);
113
    }
114
115
    /**
116
     * @throws InvalidArgumentException if the curve is not supported
117
     */
118
    private static function getNistCurveSize(string $curve): int
119
    {
120
        switch ($curve) {
121
            case 'P-256':
122
            case 'secp256k1':
123
                return 256;
124
            case 'P-384':
125
                return 384;
126
            case 'P-521':
127
                return 521;
128
            default:
129
                throw new InvalidArgumentException(sprintf('The curve "%s" is not supported.', $curve));
130
        }
131
    }
132
133
    /**
134
     * @throws RuntimeException if the extension OpenSSL is not available
135
     * @throws RuntimeException if the key cannot be created
136
     */
137
    private static function createECKeyUsingOpenSSL(string $curve): array
138
    {
139
        if (!\extension_loaded('openssl')) {
140
            throw new RuntimeException('Please install the OpenSSL extension');
141
        }
142
        $key = openssl_pkey_new([
143
            'curve_name' => self::getOpensslCurveName($curve),
144
            'private_key_type' => OPENSSL_KEYTYPE_EC,
145
        ]);
146
        if (false === $key) {
147
            throw new RuntimeException('Unable to create the key');
148
        }
149
        $result = openssl_pkey_export($key, $out);
150
        if (false === $result) {
151
            throw new RuntimeException('Unable to create the key');
152
        }
153
        $res = openssl_pkey_get_private($out);
154
        if (false === $res) {
155
            throw new RuntimeException('Unable to create the key');
156
        }
157
        $details = openssl_pkey_get_details($res);
158
        $nistCurveSize = self::getNistCurveSize($curve);
159
160
        return [
161
            'kty' => 'EC',
162
            'crv' => $curve,
163
            'd' => Base64Url::encode(str_pad($details['ec']['d'], (int) ceil($nistCurveSize / 8), "\0", STR_PAD_LEFT)),
164
            'x' => Base64Url::encode(str_pad($details['ec']['x'], (int) ceil($nistCurveSize / 8), "\0", STR_PAD_LEFT)),
165
            'y' => Base64Url::encode(str_pad($details['ec']['y'], (int) ceil($nistCurveSize / 8), "\0", STR_PAD_LEFT)),
166
        ];
167
    }
168
169
    /**
170
     * @throws InvalidArgumentException if the curve is not supported
171
     */
172
    private static function getOpensslCurveName(string $curve): string
173
    {
174
        switch ($curve) {
175
            case 'P-256':
176
                return 'prime256v1';
177
            case 'secp256k1':
178
                return 'secp256k1';
179
            case 'P-384':
180
                return 'secp384r1';
181
            case 'P-521':
182
                return 'secp521r1';
183
            default:
184
                throw new InvalidArgumentException(sprintf('The curve "%s" is not supported.', $curve));
185
        }
186
    }
187
188
    private static function p256PublicKey(): string
189
    {
190
        return pack(
191
            'H*',
192
            '3059' // SEQUENCE, length 89
193
                .'3013' // SEQUENCE, length 19
194
                    .'0607' // OID, length 7
195
                        .'2a8648ce3d0201' // 1.2.840.10045.2.1 = EC Public Key
196
                    .'0608' // OID, length 8
197
                        .'2a8648ce3d030107' // 1.2.840.10045.3.1.7 = P-256 Curve
198
                .'0342' // BIT STRING, length 66
199
                    .'00' // prepend with NUL - pubkey will follow
200
        );
201
    }
202
203
    private static function p256KPublicKey(): string
204
    {
205
        return pack(
206
            'H*',
207
            '3056' // SEQUENCE, length 86
208
                .'3010' // SEQUENCE, length 16
209
                    .'0607' // OID, length 7
210
                        .'2a8648ce3d0201' // 1.2.840.10045.2.1 = EC Public Key
211
                    .'0605' // OID, length 8
212
                        .'2B8104000A' // 1.3.132.0.10 secp256k1
213
                .'0342' // BIT STRING, length 66
214
                    .'00' // prepend with NUL - pubkey will follow
215
        );
216
    }
217
218
    private static function p384PublicKey(): string
219
    {
220
        return pack(
221
            'H*',
222
            '3076' // SEQUENCE, length 118
223
                .'3010' // SEQUENCE, length 16
224
                    .'0607' // OID, length 7
225
                        .'2a8648ce3d0201' // 1.2.840.10045.2.1 = EC Public Key
226
                    .'0605' // OID, length 5
227
                        .'2b81040022' // 1.3.132.0.34 = P-384 Curve
228
                .'0362' // BIT STRING, length 98
229
                    .'00' // prepend with NUL - pubkey will follow
230
        );
231
    }
232
233
    private static function p521PublicKey(): string
234
    {
235
        return pack(
236
            'H*',
237
            '30819b' // SEQUENCE, length 154
238
                .'3010' // SEQUENCE, length 16
239
                    .'0607' // OID, length 7
240
                        .'2a8648ce3d0201' // 1.2.840.10045.2.1 = EC Public Key
241
                    .'0605' // OID, length 5
242
                        .'2b81040023' // 1.3.132.0.35 = P-521 Curve
243
                .'038186' // BIT STRING, length 134
244
                    .'00' // prepend with NUL - pubkey will follow
245
        );
246
    }
247
248
    private static function p256PrivateKey(JWK $jwk): string
249
    {
250
        $d = unpack('H*', str_pad(Base64Url::decode($jwk->get('d')), 32, "\0", STR_PAD_LEFT))[1];
251
252
        return pack(
253
            'H*',
254
            '3077' // SEQUENCE, length 87+length($d)=32
255
                .'020101' // INTEGER, 1
256
                .'0420'   // OCTET STRING, length($d) = 32
257
                    .$d
258
                .'a00a' // TAGGED OBJECT #0, length 10
259
                    .'0608' // OID, length 8
260
                        .'2a8648ce3d030107' // 1.3.132.0.34 = P-256 Curve
261
                .'a144' //  TAGGED OBJECT #1, length 68
262
                    .'0342' // BIT STRING, length 66
263
                    .'00' // prepend with NUL - pubkey will follow
264
        );
265
    }
266
267
    private static function p256KPrivateKey(JWK $jwk): string
268
    {
269
        $d = unpack('H*', str_pad(Base64Url::decode($jwk->get('d')), 32, "\0", STR_PAD_LEFT))[1];
270
271
        return pack(
272
            'H*',
273
            '3074' // SEQUENCE, length 84+length($d)=32
274
                .'020101' // INTEGER, 1
275
                .'0420'   // OCTET STRING, length($d) = 32
276
                    .$d
277
                .'a007' // TAGGED OBJECT #0, length 7
278
                    .'0605' // OID, length 5
279
                        .'2b8104000a' //  1.3.132.0.10 secp256k1
280
                .'a144' //  TAGGED OBJECT #1, length 68
281
                    .'0342' // BIT STRING, length 66
282
                    .'00' // prepend with NUL - pubkey will follow
283
        );
284
    }
285
286
    private static function p384PrivateKey(JWK $jwk): string
287
    {
288
        $d = unpack('H*', str_pad(Base64Url::decode($jwk->get('d')), 48, "\0", STR_PAD_LEFT))[1];
289
290
        return pack(
291
            'H*',
292
            '3081a4' // SEQUENCE, length 116 + length($d)=48
293
                .'020101' // INTEGER, 1
294
                .'0430'   // OCTET STRING, length($d) = 30
295
                    .$d
296
                .'a007' // TAGGED OBJECT #0, length 7
297
                    .'0605' // OID, length 5
298
                        .'2b81040022' // 1.3.132.0.34 = P-384 Curve
299
                .'a164' //  TAGGED OBJECT #1, length 100
300
                    .'0362' // BIT STRING, length 98
301
                    .'00' // prepend with NUL - pubkey will follow
302
        );
303
    }
304
305
    private static function p521PrivateKey(JWK $jwk): string
306
    {
307
        $d = unpack('H*', str_pad(Base64Url::decode($jwk->get('d')), 66, "\0", STR_PAD_LEFT))[1];
308
309
        return pack(
310
            'H*',
311
            '3081dc' // SEQUENCE, length 154 + length($d)=66
312
                .'020101' // INTEGER, 1
313
                .'0442'   // OCTET STRING, length(d) = 66
314
                    .$d
315
                .'a007' // TAGGED OBJECT #0, length 7
316
                    .'0605' // OID, length 5
317
                        .'2b81040023' // 1.3.132.0.35 = P-521 Curve
318
                .'a18189' //  TAGGED OBJECT #1, length 137
319
                    .'038186' // BIT STRING, length 134
320
                    .'00' // prepend with NUL - pubkey will follow
321
        );
322
    }
323
324
    private static function getKey(JWK $jwk): string
325
    {
326
        $nistCurveSize = self::getNistCurveSize($jwk->get('crv'));
327
        $length = (int) ceil($nistCurveSize / 8);
328
329
        return
330
            "\04"
331
            .str_pad(Base64Url::decode($jwk->get('x')), $length, "\0", STR_PAD_LEFT)
332
            .str_pad(Base64Url::decode($jwk->get('y')), $length, "\0", STR_PAD_LEFT);
333
    }
334
}
335