ECKey::loadPKCS8()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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