Failed Conditions
Push — APNKeysSupport ( 8a74c9 )
by Florent
03:00
created

src/KeyConverter/ECKey.php (1 issue)

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