Failed Conditions
Push — v7 ( 477009...5356df )
by Florent
03:33
created

ECKey::toPEM()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 10
nc 12
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2017 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 FG\ASN1\ExplicitlyTaggedObject;
18
use FG\ASN1\Universal\BitString;
19
use FG\ASN1\Universal\Integer;
20
use FG\ASN1\Universal\ObjectIdentifier;
21
use FG\ASN1\Universal\OctetString;
22
use FG\ASN1\Universal\Sequence;
23
use FG\ASN1\Object;
24
use Jose\Component\Core\JWK;
25
26
final class ECKey
27
{
28
    /**
29
     * @var null|Sequence
30
     */
31
    private $sequence = null;
32
33
    /**
34
     * @var bool
35
     */
36
    private $private = false;
37
38
    /**
39
     * @var array
40
     */
41
    private $values = [];
42
43
    /**
44
     * ECKey constructor.
45
     *
46
     * @param JWK $data
47
     */
48
    private function __construct(JWK $data)
49
    {
50
        $this->loadJWK($data->all());
51
        $this->private = isset($this->values['d']);
52
    }
53
54
    /**
55
     * @param JWK $jwk
56
     *
57
     * @return ECKey
58
     */
59
    public static function createFromJWK(JWK $jwk): ECKey
60
    {
61
        return new self($jwk);
62
    }
63
64
    /**
65
     * @param string $pem
66
     *
67
     * @return ECKey
68
     */
69
    public static function createFromPEM(string $pem): ECKey
70
    {
71
        $data = self::loadPEM($pem);
72
73
        return new self(JWK::create($data));
74
    }
75
76
    /**
77
     * @param string $data
78
     *
79
     * @return array
80
     *
81
     * @throws \Exception
82
     */
83
    private static function loadPEM(string $data): array
84
    {
85
        $data = base64_decode(preg_replace('#-.*-|\r|\n#', '', $data));
86
        $asnObject = Object::fromBinary($data);
87
88
        if (!$asnObject instanceof Sequence) {
89
            throw new \InvalidArgumentException('Unable to load the key.');
90
        }
91
        $children = $asnObject->getChildren();
92
        if (self::isPKCS8($children)) {
93
            $children = self::loadPKCS8($children);
94
        }
95
96
        if (4 === count($children)) {
97
            return self::loadPrivatePEM($children);
98
        } elseif (2 === count($children)) {
99
            return self::loadPublicPEM($children);
100
        }
101
102
        throw new \Exception('Unable to load the key.');
103
    }
104
105
    /**
106
     * @param array $children
107
     *
108
     * @return array
109
     */
110
    private static function loadPKCS8(array $children): array
111
    {
112
        $binary = hex2bin($children[2]->getContent());
113
        $asnObject = Object::fromBinary($binary);
114
        if (!$asnObject instanceof Sequence) {
115
            throw new \InvalidArgumentException('Unable to load the key.');
116
        }
117
118
        return $asnObject->getChildren();
119
    }
120
121
    /**
122
     * @param array $children
123
     *
124
     * @return array
125
     */
126
    private static function loadPublicPEM(array $children): array
127
    {
128
        if (!$children[0] instanceof Sequence) {
129
            throw new \InvalidArgumentException('Unsupported key type.');
130
        }
131
132
        $sub = $children[0]->getChildren();
133
        if (!$sub[0] instanceof ObjectIdentifier) {
134
            throw new \InvalidArgumentException('Unsupported key type.');
135
        }
136
        if ('1.2.840.10045.2.1' !== $sub[0]->getContent()) {
137
            throw new \InvalidArgumentException('Unsupported key type.');
138
        }
139
        if (!$sub[1] instanceof ObjectIdentifier) {
140
            throw new \InvalidArgumentException('Unsupported key type.');
141
        }
142
        if (!$children[1] instanceof BitString) {
143
            throw new \InvalidArgumentException('Unable to load the key.');
144
        }
145
146
        $bits = $children[1]->getContent();
147
        $bits_length = mb_strlen($bits, '8bit');
148
        if ('04' !== mb_substr($bits, 0, 2, '8bit')) {
149
            throw new \InvalidArgumentException('Unsupported key type');
150
        }
151
152
        $values = ['kty' => 'EC'];
153
        $values['crv'] = self::getCurve($sub[1]->getContent());
154
        $values['x'] = Base64Url::encode(hex2bin(mb_substr($bits, 2, ($bits_length - 2) / 2, '8bit')));
155
        $values['y'] = Base64Url::encode(hex2bin(mb_substr($bits, ($bits_length - 2) / 2 + 2, ($bits_length - 2) / 2, '8bit')));
156
157
        return $values;
158
    }
159
160
    /**
161
     * @param string $oid
162
     *
163
     * @return string
164
     */
165
    private static function getCurve(string $oid): string
166
    {
167
        $curves = self::getSupportedCurves();
168
        $curve = array_search($oid, $curves, true);
169
        if (!is_string($curve)) {
170
            throw new \InvalidArgumentException('Unsupported OID.');
171
        }
172
173
        return $curve;
174
    }
175
176
    /**
177
     * @return array
178
     */
179
    private static function getSupportedCurves(): array
180
    {
181
        return [
182
            'P-256' => '1.2.840.10045.3.1.7',
183
            'P-384' => '1.3.132.0.34',
184
            'P-521' => '1.3.132.0.35',
185
        ];
186
    }
187
188
    /**
189
     * @param object $children
190
     */
191
    private static function verifyVersion(Object $children)
192
    {
193
        if (!$children instanceof Integer || '1' !== $children->getContent()) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of '1' (string) and $children->getContent() (integer) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
194
            throw new \InvalidArgumentException('Unable to load the key.');
195
        }
196
    }
197
198
    /**
199
     * @param object      $children
200
     * @param string|null $x
201
     * @param string|null $y
202
     */
203
    private static function getXAndY(Object $children, ?string &$x, ?string &$y)
204
    {
205
        if (!$children instanceof ExplicitlyTaggedObject || !is_array($children->getContent())) {
206
            throw new \InvalidArgumentException('Unable to load the key.');
207
        }
208
        if (!$children->getContent()[0] instanceof BitString) {
209
            throw new \InvalidArgumentException('Unable to load the key.');
210
        }
211
212
        $bits = $children->getContent()[0]->getContent();
213
        $bits_length = mb_strlen($bits, '8bit');
214
215
        if ('04' !== mb_substr($bits, 0, 2, '8bit')) {
216
            throw new \InvalidArgumentException('Unsupported key type');
217
        }
218
219
        $x = mb_substr($bits, 2, ($bits_length - 2) / 2, '8bit');
220
        $y = mb_substr($bits, ($bits_length - 2) / 2 + 2, ($bits_length - 2) / 2, '8bit');
221
    }
222
223
    /**
224
     * @param object $children
225
     *
226
     * @return string
227
     */
228
    private static function getD(Object $children): string
229
    {
230
        if (!$children instanceof OctetString) {
231
            throw new \InvalidArgumentException('Unable to load the key.');
232
        }
233
234
        return $children->getContent();
235
    }
236
237
    /**
238
     * @param array $children
239
     *
240
     * @return array
241
     */
242
    private static function loadPrivatePEM(array $children): array
243
    {
244
        self::verifyVersion($children[0]);
245
        $x = null;
246
        $y = null;
247
        $d = self::getD($children[1]);
248
        self::getXAndY($children[3], $x, $y);
249
250
        if (!$children[2] instanceof ExplicitlyTaggedObject || !is_array($children[2]->getContent())) {
251
            throw new \InvalidArgumentException('Unable to load the key.');
252
        }
253
        if (!$children[2]->getContent()[0] instanceof ObjectIdentifier) {
254
            throw new \InvalidArgumentException('Unable to load the key.');
255
        }
256
257
        $curve = $children[2]->getContent()[0]->getContent();
258
259
        $values = ['kty' => 'EC'];
260
        $values['crv'] = self::getCurve($curve);
261
        $values['d'] = Base64Url::encode(hex2bin($d));
262
        $values['x'] = Base64Url::encode(hex2bin($x));
263
        $values['y'] = Base64Url::encode(hex2bin($y));
264
265
        return $values;
266
    }
267
268
    /**
269
     * @param array $children
270
     *
271
     * @return bool
272
     */
273
    private static function isPKCS8(array $children): bool
274
    {
275
        if (3 !== count($children)) {
276
            return false;
277
        }
278
279
        $classes = [0 => Integer::class, 1 => Sequence::class, 2 => OctetString::class];
280
        foreach ($classes as $k => $class) {
281
            if (!$children[$k] instanceof $class) {
282
                return false;
283
            }
284
        }
285
286
        return true;
287
    }
288
289
    /**
290
     * @param ECKey $private
291
     *
292
     * @return ECKey
293
     */
294
    public static function toPublic(ECKey $private): ECKey
295
    {
296
        $data = $private->toArray();
297
        if (array_key_exists('d', $data)) {
298
            unset($data['d']);
299
        }
300
301
        return new self(JWK::create($data));
302
    }
303
304
    /**
305
     * @return array
306
     */
307
    public function toArray()
308
    {
309
        return $this->values;
310
    }
311
312
    /**
313
     * @param array $jwk
314
     */
315
    private function loadJWK(array $jwk)
316
    {
317
        $keys = [
318
            'kty' => 'The key parameter "kty" is missing.',
319
            'crv' => 'Curve parameter is missing',
320
            'x' => 'Point parameters are missing.',
321
            'y' => 'Point parameters are missing.',
322
        ];
323
        foreach ($keys as $k => $v) {
324
            if (!array_key_exists($k, $jwk)) {
325
                throw new \InvalidArgumentException($v);
326
            }
327
        }
328
329
        if ('EC' !== $jwk['kty']) {
330
            throw new \InvalidArgumentException('JWK is not an Elliptic Curve key.');
331
        }
332
        $this->values = $jwk;
333
    }
334
335
    private function initPublicKey()
336
    {
337
        $this->sequence = new Sequence();
338
        $oid_sequence = new Sequence();
339
        $oid_sequence->addChild(new ObjectIdentifier('1.2.840.10045.2.1'));
340
        $oid_sequence->addChild(new ObjectIdentifier($this->getOID($this->values['crv'])));
341
        $this->sequence->addChild($oid_sequence);
342
343
        $bits = '04';
344
        $bits .= bin2hex(Base64Url::decode($this->values['x']));
345
        $bits .= bin2hex(Base64Url::decode($this->values['y']));
346
        $this->sequence->addChild(new BitString($bits));
347
    }
348
349
    private function initPrivateKey()
350
    {
351
        $this->sequence = new Sequence();
352
        $this->sequence->addChild(new Integer(1));
353
        $this->sequence->addChild(new OctetString(bin2hex(Base64Url::decode($this->values['d']))));
354
355
        $oid = new ObjectIdentifier($this->getOID($this->values['crv']));
356
        $this->sequence->addChild(new ExplicitlyTaggedObject(0, $oid));
357
358
        $bits = '04';
359
        $bits .= bin2hex(Base64Url::decode($this->values['x']));
360
        $bits .= bin2hex(Base64Url::decode($this->values['y']));
361
        $bit = new BitString($bits);
362
        $this->sequence->addChild(new ExplicitlyTaggedObject(1, $bit));
363
    }
364
365
    /**
366
     * @return string
367
     */
368
    public function toPEM(): string
369
    {
370
        if (null === $this->sequence) {
371
            if (array_key_exists('d', $this->values)) {
372
                $this->initPrivateKey();
373
            } else {
374
                $this->initPublicKey();
375
            }
376
        }
377
        $result = '-----BEGIN '.($this->private ? 'EC PRIVATE' : 'PUBLIC').' KEY-----'.PHP_EOL;
378
        $result .= chunk_split(base64_encode($this->sequence->getBinary()), 64, PHP_EOL);
379
        $result .= '-----END '.($this->private ? 'EC PRIVATE' : 'PUBLIC').' KEY-----'.PHP_EOL;
380
381
        return $result;
382
    }
383
384
    /**
385
     * @param $curve
386
     *
387
     * @return string
388
     */
389
    private function getOID(string $curve): string
390
    {
391
        $curves = $this->getSupportedCurves();
392
        if (!array_key_exists($curve, $curves)) {
393
            throw new \InvalidArgumentException('Unsupported curve.');
394
395
        }
396
397
        return $curves[$curve];
398
    }
399
}
400