Failed Conditions
Pull Request — master (#303)
by Florent
03:27 queued 01:31
created

src/KeyConverter/ECKey.php (2 issues)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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