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()) { |
|
|
|
|
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
|
|
|
|
This error could be the result of:
1. Missing dependencies
PHP Analyzer uses your
composer.json
file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects thecomposer.json
to be in the root folder of your repository.Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the
require
orrequire-dev
section?2. Missing use statement
PHP does not complain about undefined classes in
ìnstanceof
checks. For example, the following PHP code will work perfectly fine:If you have not tested against this specific condition, such errors might go unnoticed.