Failed Conditions
Pull Request — master (#146)
by Florent
14:41 queued 11:55
created

src/KeyConverter/ECKey.php (1 issue)

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