Failed Conditions
Push — v7 ( 61ffea...f7e5f1 )
by Florent
04:20
created

ECKey::toDER()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 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\KeyManagement\KeyConverter;
15
16
use Assert\Assertion;
17
use Base64Url\Base64Url;
18
use FG\ASN1\Exception\ParserException;
19
use FG\ASN1\ExplicitlyTaggedObject;
20
use FG\ASN1\Object;
21
use FG\ASN1\Universal\BitString;
22
use FG\ASN1\Universal\Integer;
23
use FG\ASN1\Universal\ObjectIdentifier;
24
use FG\ASN1\Universal\OctetString;
25
use FG\ASN1\Universal\Sequence;
26
use Jose\Component\Core\JWK;
27
28
final class ECKey
29
{
30
    /**
31
     * @var null|Sequence
32
     */
33
    private $sequence = null;
34
35
    /**
36
     * @var bool
37
     */
38
    private $private = false;
39
40
    /**
41
     * @var array
42
     */
43
    private $values = [];
44
45
    /**
46
     * ECKey constructor.
47
     *
48
     * @param $data
49
     */
50
    private function __construct($data)
51
    {
52
        $this->sequence = new Sequence();
53
54
        if ($data instanceof JWK) {
55
            $this->loadJWK($data->all());
56
        } elseif (is_array($data)) {
57
            $this->loadJWK($data);
58
        } elseif (is_string($data)) {
59
            $this->loadPEM($data);
60
        } else {
61
            throw new \InvalidArgumentException('Unsupported input');
62
        }
63
        $this->private = isset($this->values['d']);
64
    }
65
66
    /**
67
     * @param JWK $jwk
68
     *
69
     * @return ECKey
70
     */
71
    public static function createFromJWK(JWK $jwk): ECKey
72
    {
73
        return new self($jwk);
74
    }
75
76
    /**
77
     * @param string $pem
78
     *
79
     * @return ECKey
80
     */
81
    public static function createFromPEM(string $pem): ECKey
82
    {
83
        return new self($pem);
84
    }
85
86
    /**
87
     * @param string $data
88
     *
89
     * @throws \Exception
90
     * @throws ParserException
91
     */
92
    private function loadPEM(string $data)
93
    {
94
        $data = base64_decode(preg_replace('#-.*-|\r|\n#', '', $data));
95
        $asnObject = Object::fromBinary($data);
96
97
        Assertion::isInstanceOf($asnObject, Sequence::class);
98
        $children = $asnObject->getChildren();
0 ignored issues
show
Bug introduced by
The method getChildren does only exist in FG\ASN1\Construct, but not in FG\ASN1\AbstractString a...d FG\ASN1\UnknownObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
99
        if (self::isPKCS8($children)) {
100
            $children = self::loadPKCS8($children);
101
        }
102
103
        if (4 === count($children)) {
104
            $this->loadPrivatePEM($children);
105
106
            return;
107
        } elseif (2 === count($children)) {
108
            $this->loadPublicPEM($children);
109
110
            return;
111
        }
112
        throw new \Exception('Unable to load the key');
113
    }
114
115
    /**
116
     * @param array $children
117
     *
118
     * @return array
119
     */
120
    private function loadPKCS8(array $children): array
121
    {
122
        $binary = hex2bin($children[2]->getContent());
123
        $asnObject = Object::fromBinary($binary);
124
        Assertion::isInstanceOf($asnObject, Sequence::class);
125
126
        return $asnObject->getChildren();
0 ignored issues
show
Bug introduced by
The method getChildren does only exist in FG\ASN1\Construct, but not in FG\ASN1\AbstractString a...d FG\ASN1\UnknownObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
127
    }
128
129
    /**
130
     * @param array $children
131
     *
132
     * @return bool
133
     */
134
    private function isPKCS8(array $children): bool
135
    {
136
        if (3 !== count($children)) {
137
            return false;
138
        }
139
140
        $classes = [0 => Integer::class, 1 => Sequence::class, 2 => OctetString::class];
141
        foreach ($classes as $k => $class) {
142
            if (!$children[$k] instanceof $class) {
143
                return false;
144
            }
145
        }
146
147
        return true;
148
    }
149
150
    /**
151
     * @param array $jwk
152
     */
153
    private function loadJWK(array $jwk)
154
    {
155
        Assertion::true(array_key_exists('kty', $jwk), 'JWK is not an Elliptic Curve key');
156
        Assertion::eq($jwk['kty'], 'EC', 'JWK is not an Elliptic Curve key');
157
        Assertion::true(array_key_exists('crv', $jwk), 'Curve parameter is missing');
158
        Assertion::true(array_key_exists('x', $jwk), 'Point parameters are missing');
159
        Assertion::true(array_key_exists('y', $jwk), 'Point parameters are missing');
160
161
        $this->values = $jwk;
162
        if (array_key_exists('d', $jwk)) {
163
            $this->initPrivateKey();
164
        } else {
165
            $this->initPublicKey();
166
        }
167
    }
168
169
    private function initPublicKey()
170
    {
171
        $oid_sequence = new Sequence();
172
        $oid_sequence->addChild(new ObjectIdentifier('1.2.840.10045.2.1'));
173
        $oid_sequence->addChild(new ObjectIdentifier($this->getOID($this->values['crv'])));
174
        $this->sequence->addChild($oid_sequence);
175
176
        $bits = '04';
177
        $bits .= bin2hex(Base64Url::decode($this->values['x']));
178
        $bits .= bin2hex(Base64Url::decode($this->values['y']));
179
        $this->sequence->addChild(new BitString($bits));
180
    }
181
182
    private function initPrivateKey()
183
    {
184
        $this->sequence->addChild(new Integer(1));
185
        $this->sequence->addChild(new OctetString(bin2hex(Base64Url::decode($this->values['d']))));
186
187
        $oid = new ObjectIdentifier($this->getOID($this->values['crv']));
188
        $this->sequence->addChild(new ExplicitlyTaggedObject(0, $oid));
189
190
        $bits = '04';
191
        $bits .= bin2hex(Base64Url::decode($this->values['x']));
192
        $bits .= bin2hex(Base64Url::decode($this->values['y']));
193
        $bit = new BitString($bits);
194
        $this->sequence->addChild(new ExplicitlyTaggedObject(1, $bit));
195
    }
196
197
    /**
198
     * @param array $children
199
     *
200
     * @throws \Exception
201
     */
202
    private function loadPublicPEM(array $children)
203
    {
204
        Assertion::isInstanceOf($children[0], Sequence::class, 'Unsupported key type');
205
206
        $sub = $children[0]->getChildren();
207
        Assertion::isInstanceOf($sub[0], ObjectIdentifier::class, 'Unsupported key type');
208
        Assertion::eq('1.2.840.10045.2.1', $sub[0]->getContent(), 'Unsupported key type');
209
210
        Assertion::isInstanceOf($sub[1], ObjectIdentifier::class, 'Unsupported key type');
211
        Assertion::isInstanceOf($children[1], BitString::class, 'Unable to load the key');
212
213
        $bits = $children[1]->getContent();
214
        $bits_length = mb_strlen($bits, '8bit');
215
216
        Assertion::eq('04', mb_substr($bits, 0, 2, '8bit'), 'Unsupported key type');
217
218
        $this->values['kty'] = 'EC';
219
        $this->values['crv'] = $this->getCurve($sub[1]->getContent());
220
        $this->values['x'] = Base64Url::encode(hex2bin(mb_substr($bits, 2, ($bits_length - 2) / 2, '8bit')));
221
        $this->values['y'] = Base64Url::encode(hex2bin(mb_substr($bits, ($bits_length - 2) / 2 + 2, ($bits_length - 2) / 2, '8bit')));
222
    }
223
224
    /**
225
     * @param object $children
226
     */
227
    private function verifyVersion(Object $children)
228
    {
229
        Assertion::isInstanceOf($children, Integer::class, 'Unable to load the key');
230
        Assertion::eq(1, $children->getContent(), 'Unable to load the key');
231
    }
232
233
    /**
234
     * @param object      $children
235
     * @param string|null $x
236
     * @param string|null $y
237
     */
238
    private function getXAndY(Object $children, ?string &$x, ?string &$y)
239
    {
240
        Assertion::isInstanceOf($children, ExplicitlyTaggedObject::class, 'Unable to load the key');
241
        Assertion::isArray($children->getContent(), 'Unable to load the key');
242
        Assertion::isInstanceOf($children->getContent()[0], BitString::class, 'Unable to load the key');
243
244
        $bits = $children->getContent()[0]->getContent();
245
        $bits_length = mb_strlen($bits, '8bit');
246
247
        Assertion::eq('04', mb_substr($bits, 0, 2, '8bit'), 'Unsupported key type');
248
249
        $x = mb_substr($bits, 2, ($bits_length - 2) / 2, '8bit');
250
        $y = mb_substr($bits, ($bits_length - 2) / 2 + 2, ($bits_length - 2) / 2, '8bit');
251
    }
252
253
    /**
254
     * @param object $children
255
     *
256
     * @return string
257
     */
258
    private function getD(Object $children): string
259
    {
260
        Assertion::isInstanceOf($children, '\FG\ASN1\Universal\OctetString', 'Unable to load the key');
261
262
        return $children->getContent();
263
    }
264
265
    /**
266
     * @param array $children
267
     */
268
    private function loadPrivatePEM(array $children)
269
    {
270
        $this->verifyVersion($children[0]);
271
        $x = null;
272
        $y = null;
273
        $d = $this->getD($children[1]);
274
        $this->getXAndY($children[3], $x, $y);
275
276
        Assertion::isInstanceOf($children[2], ExplicitlyTaggedObject::class, 'Unable to load the key');
277
        Assertion::isArray($children[2]->getContent(), 'Unable to load the key');
278
        Assertion::isInstanceOf($children[2]->getContent()[0], ObjectIdentifier::class, 'Unable to load the key');
279
280
        $curve = $children[2]->getContent()[0]->getContent();
281
282
        $this->private = true;
283
        $this->values['kty'] = 'EC';
284
        $this->values['crv'] = $this->getCurve($curve);
285
        $this->values['d'] = Base64Url::encode(hex2bin($d));
286
        $this->values['x'] = Base64Url::encode(hex2bin($x));
287
        $this->values['y'] = Base64Url::encode(hex2bin($y));
288
    }
289
290
    /**
291
     * @param ECKey $private
292
     *
293
     * @return ECKey
294
     */
295
    public static function toPublic(ECKey $private): ECKey
296
    {
297
        $data = $private->toArray();
298
        if (array_key_exists('d', $data)) {
299
            unset($data['d']);
300
        }
301
302
        return new self($data);
303
    }
304
305
    /**
306
     * @return array
307
     */
308
    public function toArray()
309
    {
310
        return $this->values;
311
    }
312
313
    /**
314
     * @return string
315
     */
316
    public function toPEM(): string
317
    {
318
        $result = '-----BEGIN '.($this->private ? 'EC PRIVATE' : 'PUBLIC').' KEY-----'.PHP_EOL;
319
        $result .= chunk_split(base64_encode($this->sequence->getBinary()), 64, PHP_EOL);
320
        $result .= '-----END '.($this->private ? 'EC PRIVATE' : 'PUBLIC').' KEY-----'.PHP_EOL;
321
322
        return $result;
323
    }
324
325
    /**
326
     * @param $curve
327
     *
328
     * @return string
329
     */
330
    private function getOID(string $curve): string
331
    {
332
        $curves = $this->getSupportedCurves();
333
        $oid = array_key_exists($curve, $curves) ? $curves[$curve] : null;
334
335
        Assertion::notNull($oid, 'Unsupported curve');
336
337
        return $oid;
338
    }
339
340
    /**
341
     * @param string $oid
342
     *
343
     * @return string
344
     */
345
    private function getCurve(string $oid): string
346
    {
347
        $curves = $this->getSupportedCurves();
348
        $curve = array_search($oid, $curves, true);
349
        Assertion::string($curve, 'Unsupported OID');
350
351
        return $curve;
352
    }
353
354
    /**
355
     * @return array
356
     */
357
    private function getSupportedCurves(): array
358
    {
359
        return [
360
            'P-256' => '1.2.840.10045.3.1.7',
361
            'P-384' => '1.3.132.0.34',
362
            'P-521' => '1.3.132.0.35',
363
        ];
364
    }
365
}
366