Failed Conditions
Push — master ( 28d61e...98c33a )
by Florent
03:29
created

src/Component/KeyManagement/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
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2018 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 Base64Url\Base64Url;
17
use FG\ASN1\ASNObject;
18
use FG\ASN1\ExplicitlyTaggedObject;
19
use FG\ASN1\Universal\BitString;
20
use FG\ASN1\Universal\Integer;
21
use FG\ASN1\Universal\ObjectIdentifier;
22
use FG\ASN1\Universal\OctetString;
23
use FG\ASN1\Universal\Sequence;
24
25
/**
26
 * @internal
27
 */
28
class ECKey
29
{
30
    /**
31
     * @var array
32
     */
33
    private $values = [];
34
35
    /**
36
     * ECKey constructor.
37
     *
38
     * @param array $data
39
     */
40
    private function __construct(array $data)
41
    {
42
        $this->loadJWK($data);
43
    }
44
45
    /**
46
     * @param string $pem
47
     *
48
     * @return ECKey
49
     */
50
    public static function createFromPEM(string $pem): self
51
    {
52
        $data = self::loadPEM($pem);
53
54
        return new self($data);
55
    }
56
57
    /**
58
     * @param string $data
59
     *
60
     * @throws \Exception
61
     *
62
     * @return array
63
     */
64
    private static function loadPEM(string $data): array
65
    {
66
        $data = base64_decode(preg_replace('#-.*-|\r|\n#', '', $data));
67
        $asnObject = ASNObject::fromBinary($data);
68
69
        if (!$asnObject instanceof Sequence) {
70
            throw new \InvalidArgumentException('Unable to load the key.');
71
        }
72
        $children = $asnObject->getChildren();
73
        if (self::isPKCS8($children)) {
74
            $children = self::loadPKCS8($children);
75
        }
76
77
        if (4 === count($children)) {
78
            return self::loadPrivatePEM($children);
79
        } elseif (2 === count($children)) {
80
            return self::loadPublicPEM($children);
81
        }
82
83
        throw new \Exception('Unable to load the key.');
84
    }
85
86
    /**
87
     * @param ASNObject[] $children
88
     *
89
     * @return array
90
     */
91
    private static function loadPKCS8(array $children): array
92
    {
93
        $binary = hex2bin($children[2]->getContent());
94
        $asnObject = ASNObject::fromBinary($binary);
95
        if (!$asnObject instanceof Sequence) {
96
            throw new \InvalidArgumentException('Unable to load the key.');
97
        }
98
99
        return $asnObject->getChildren();
100
    }
101
102
    /**
103
     * @param ASNObject[] $children
104
     *
105
     * @return array
106
     */
107
    private static function loadPublicPEM(array $children): array
108
    {
109
        if (!$children[0] instanceof Sequence) {
110
            throw new \InvalidArgumentException('Unsupported key type.');
111
        }
112
113
        $sub = $children[0]->getChildren();
114
        if (!$sub[0] instanceof ObjectIdentifier) {
115
            throw new \InvalidArgumentException('Unsupported key type.');
116
        }
117
        if ('1.2.840.10045.2.1' !== $sub[0]->getContent()) {
118
            throw new \InvalidArgumentException('Unsupported key type.');
119
        }
120
        if (!$sub[1] instanceof ObjectIdentifier) {
121
            throw new \InvalidArgumentException('Unsupported key type.');
122
        }
123
        if (!$children[1] instanceof BitString) {
124
            throw new \InvalidArgumentException('Unable to load the key.');
125
        }
126
127
        $bits = $children[1]->getContent();
128
        $bits_length = mb_strlen($bits, '8bit');
129
        if ('04' !== mb_substr($bits, 0, 2, '8bit')) {
130
            throw new \InvalidArgumentException('Unsupported key type');
131
        }
132
133
        $values = ['kty' => 'EC'];
134
        $values['crv'] = self::getCurve($sub[1]->getContent());
135
        $values['x'] = Base64Url::encode(hex2bin(mb_substr($bits, 2, ($bits_length - 2) / 2, '8bit')));
136
        $values['y'] = Base64Url::encode(hex2bin(mb_substr($bits, ($bits_length - 2) / 2 + 2, ($bits_length - 2) / 2, '8bit')));
137
138
        return $values;
139
    }
140
141
    /**
142
     * @param string $oid
143
     *
144
     * @return string
145
     */
146
    private static function getCurve(string $oid): string
147
    {
148
        $curves = self::getSupportedCurves();
149
        $curve = array_search($oid, $curves, true);
150
        if (!is_string($curve)) {
151
            throw new \InvalidArgumentException('Unsupported OID.');
152
        }
153
154
        return $curve;
155
    }
156
157
    /**
158
     * @return array
159
     */
160
    private static function getSupportedCurves(): array
161
    {
162
        return [
163
            'P-256' => '1.2.840.10045.3.1.7',
164
            'P-384' => '1.3.132.0.34',
165
            'P-521' => '1.3.132.0.35',
166
        ];
167
    }
168
169
    /**
170
     * @param ASNObject $children
171
     */
172
    private static function verifyVersion(ASNObject $children)
173
    {
174
        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...
175
            throw new \InvalidArgumentException('Unable to load the key.');
176
        }
177
    }
178
179
    /**
180
     * @param ASNObject   $children
181
     * @param string|null $x
182
     * @param string|null $y
183
     */
184
    private static function getXAndY(ASNObject $children, ?string &$x, ?string &$y)
185
    {
186
        if (!$children instanceof ExplicitlyTaggedObject || !is_array($children->getContent())) {
187
            throw new \InvalidArgumentException('Unable to load the key.');
188
        }
189
        if (!$children->getContent()[0] instanceof BitString) {
190
            throw new \InvalidArgumentException('Unable to load the key.');
191
        }
192
193
        $bits = $children->getContent()[0]->getContent();
194
        $bits_length = mb_strlen($bits, '8bit');
195
196
        if ('04' !== mb_substr($bits, 0, 2, '8bit')) {
197
            throw new \InvalidArgumentException('Unsupported key type');
198
        }
199
200
        $x = mb_substr($bits, 2, ($bits_length - 2) / 2, '8bit');
201
        $y = mb_substr($bits, ($bits_length - 2) / 2 + 2, ($bits_length - 2) / 2, '8bit');
202
    }
203
204
    /**
205
     * @param ASNObject $children
206
     *
207
     * @return string
208
     */
209
    private static function getD(ASNObject $children): string
210
    {
211
        if (!$children instanceof OctetString) {
212
            throw new \InvalidArgumentException('Unable to load the key.');
213
        }
214
215
        return $children->getContent();
216
    }
217
218
    /**
219
     * @param array $children
220
     *
221
     * @return array
222
     */
223
    private static function loadPrivatePEM(array $children): array
224
    {
225
        self::verifyVersion($children[0]);
226
        $x = null;
227
        $y = null;
228
        $d = self::getD($children[1]);
229
        self::getXAndY($children[3], $x, $y);
230
231
        if (!$children[2] instanceof ExplicitlyTaggedObject || !is_array($children[2]->getContent())) {
232
            throw new \InvalidArgumentException('Unable to load the key.');
233
        }
234
        if (!$children[2]->getContent()[0] instanceof ObjectIdentifier) {
235
            throw new \InvalidArgumentException('Unable to load the key.');
236
        }
237
238
        $curve = $children[2]->getContent()[0]->getContent();
239
240
        $values = ['kty' => 'EC'];
241
        $values['crv'] = self::getCurve($curve);
242
        $values['d'] = Base64Url::encode(hex2bin($d));
243
        $values['x'] = Base64Url::encode(hex2bin($x));
244
        $values['y'] = Base64Url::encode(hex2bin($y));
245
246
        return $values;
247
    }
248
249
    /**
250
     * @param ASNObject[] $children
251
     *
252
     * @return bool
253
     */
254
    private static function isPKCS8(array $children): bool
255
    {
256
        if (3 !== count($children)) {
257
            return false;
258
        }
259
260
        $classes = [0 => Integer::class, 1 => Sequence::class, 2 => OctetString::class];
261
        foreach ($classes as $k => $class) {
262
            if (!$children[$k] instanceof $class) {
263
                return false;
264
            }
265
        }
266
267
        return true;
268
    }
269
270
    /**
271
     * @param ECKey $private
272
     *
273
     * @return ECKey
274
     */
275
    public static function toPublic(self $private): self
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 array
287
     */
288
    public function toArray()
289
    {
290
        return $this->values;
291
    }
292
293
    /**
294
     * @param array $jwk
295
     */
296
    private function loadJWK(array $jwk)
297
    {
298
        $keys = [
299
            'kty' => 'The key parameter "kty" is missing.',
300
            'crv' => 'Curve parameter is missing',
301
            'x'   => 'Point parameters are missing.',
302
            'y'   => 'Point parameters are missing.',
303
        ];
304
        foreach ($keys as $k => $v) {
305
            if (!array_key_exists($k, $jwk)) {
306
                throw new \InvalidArgumentException($v);
307
            }
308
        }
309
310
        if ('EC' !== $jwk['kty']) {
311
            throw new \InvalidArgumentException('JWK is not an Elliptic Curve key.');
312
        }
313
        $this->values = $jwk;
314
    }
315
}
316