Completed
Push — master ( fe04e5...e565c7 )
by Florent
02:21
created

KeyConverter::loadKeyFromX509Resource()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 20
rs 9.4285
cc 3
eloc 14
nc 3
nop 1
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 Base64Url\Base64Url;
15
use phpseclib\Crypt\RSA;
16
17
/**
18
 * This class will help you to load an EC key or a RSA key/certificate (private or public) and get values to create a JWK object.
19
 */
20
final class KeyConverter
21
{
22
    /**
23
     * @param string $file
24
     *
25
     * @throws \InvalidArgumentException
26
     *
27
     * @return array
28
     */
29
    public static function loadKeyFromCertificateFile($file)
30
    {
31
        if (!file_exists($file)) {
32
            throw new \InvalidArgumentException(sprintf('File "%s" does not exist.', $file));
33
        }
34
        $content = file_get_contents($file);
35
36
        return self::loadKeyFromCertificate($content);
37
    }
38
39
    /**
40
     * @param string $certificate
41
     *
42
     * @throws \InvalidArgumentException
43
     *
44
     * @return array
45
     */
46
    public static function loadKeyFromCertificate($certificate)
47
    {
48
        try {
49
            $res = openssl_x509_read($certificate);
50
        } catch (\Exception $e) {
51
            $certificate = self::convertDerToPem($certificate);
52
            $res = openssl_x509_read($certificate);
53
        }
54
        if (false === $res) {
55
            throw new \InvalidArgumentException('Unable to load the certificate');
56
        }
57
        $values = self::loadKeyFromX509Resource($res);
58
        openssl_x509_free($res);
59
60
        return $values;
61
    }
62
63
    /**
64
     * @param resource $res
65
     *
66
     * @throws \Exception
67
     *
68
     * @return array
69
     */
70
    public static function loadKeyFromX509Resource($res)
71
    {
72
        $key = openssl_get_publickey($res);
73
74
        $details = openssl_pkey_get_details($key);
75
        if (isset($details['key'])) {
76
            $values = self::loadKeyFromPEM($details['key']);
77
            if (function_exists('openssl_x509_fingerprint')) {
78
                $values['x5t'] = Base64Url::encode(openssl_x509_fingerprint($res, 'sha1', true));
79
                $values['x5t#256'] = Base64Url::encode(openssl_x509_fingerprint($res, 'sha256', true));
80
            } else {
81
                openssl_x509_export($res, $pem);
82
                $values['x5t'] = Base64Url::encode(self::calculateX509Fingerprint($pem, 'sha1', true));
83
                $values['x5t#256'] = Base64Url::encode(self::calculateX509Fingerprint($pem, 'sha256', true));
84
            }
85
86
            return $values;
87
        }
88
        throw new \InvalidArgumentException('Unable to load the certificate');
89
    }
90
91
    /**
92
     * @param string      $file
93
     * @param null|string $password
94
     *
95
     * @throws \Exception
96
     *
97
     * @return array
98
     */
99
    public static function loadFromKeyFile($file, $password = null)
100
    {
101
        $content = file_get_contents($file);
102
103
        return self::loadFromKey($content, $password);
104
    }
105
106
    /**
107
     * @param string      $key
108
     * @param null|string $password
109
     *
110
     * @throws \Exception
111
     *
112
     * @return array
113
     */
114
    public static function loadFromKey($key, $password = null)
115
    {
116
        try {
117
            return self::loadKeyFromDER($key, $password);
118
        } catch (\Exception $e) {
119
            return self::loadKeyFromPEM($key, $password);
120
        }
121
    }
122
123
    /**
124
     * @param string      $der
125
     * @param null|string $password
126
     *
127
     * @throws \Exception
128
     *
129
     * @return array
130
     */
131
    private static function loadKeyFromDER($der, $password = null)
132
    {
133
        $pem = self::convertDerToPem($der);
134
135
        return self::loadKeyFromPEM($pem, $password);
136
    }
137
138
    /**
139
     * @param string      $pem
140
     * @param null|string $password
141
     *
142
     * @throws \Exception
143
     *
144
     * @return array
145
     */
146
    private static function loadKeyFromPEM($pem, $password = null)
147
    {
148
        if (preg_match('#DEK-Info: (.+),(.+)#', $pem, $matches)) {
149
            $pem = self::decodePEM($pem, $matches, $password);
150
        }
151
152
        $res = openssl_pkey_get_private($pem);
153
        if ($res === false) {
154
            $res = openssl_pkey_get_public($pem);
155
        }
156
        if ($res === false) {
157
            throw new \InvalidArgumentException('Unable to load the key');
158
        }
159
160
        $details = openssl_pkey_get_details($res);
161
        if (!is_array($details) || !array_key_exists('type', $details)) {
162
            throw new \Exception('Unable to get details of the key');
163
        }
164
165
        switch ($details['type']) {
166
            case OPENSSL_KEYTYPE_EC:
167
                $ec_key = new ECKey($pem);
168
169
                return $ec_key->toArray();
170
            case OPENSSL_KEYTYPE_RSA:
171
                $temp = [
172
                    'kty' => 'RSA',
173
                ];
174
175
                foreach ([
176
                    'n' => 'n',
177
                    'e' => 'e',
178
                    'd' => 'd',
179
                    'p' => 'p',
180
                    'q' => 'q',
181
                    'dp' => 'dmp1',
182
                    'dq' => 'dmq1',
183
                    'qi' => 'iqmp',
184
                        ] as $A => $B) {
185
                    if (array_key_exists($B, $details['rsa'])) {
186
                        $temp[$A] = Base64Url::encode($details['rsa'][$B]);
187
                    }
188
                }
189
190
                return $temp;
191
                /*
192
                 * The following lines will be used when FGrosse/PHPASN1 v1.4.0 will be available
193
                 * (not available because of current version of mdanter/phpecc.
194
                 * $rsa_key = new RSAKey($pem);
195
                 *
196
                 * return $rsa_key->toArray();
197
                 */
198
            default:
199
                throw new \InvalidArgumentException('Unsupported key type');
200
        }
201
    }
202
203
    /**
204
     * @param array $data
205
     *
206
     * @throws \Exception
207
     *
208
     * @return \phpseclib\Crypt\RSA
209
     */
210
    public static function fromArrayToRSACrypt(array $data)
211
    {
212
        $xml = self::fromArrayToXML($data);
213
        $rsa = new RSA();
214
        $rsa->loadKey($xml);
215
216
        return $rsa;
217
    }
218
219
    /**
220
     * @param array $x5c
221
     *
222
     * @return array
223
     */
224
    public static function loadFromX5C(array $x5c)
225
    {
226
        $certificate = null;
227
        $last_issuer = null;
228
        $last_subject = null;
229
        foreach ($x5c as $cert) {
230
            $current_cert = "-----BEGIN CERTIFICATE-----\n$cert\n-----END CERTIFICATE-----";
231
            $x509 = openssl_x509_read($current_cert);
232
            if (false === $x509) {
233
                $last_issuer = null;
234
                $last_subject = null;
235
                break;
236
            }
237
            $parsed = openssl_x509_parse($x509);
238
239
            openssl_x509_free($x509);
240
            if (false === $parsed) {
241
                $last_issuer = null;
242
                $last_subject = null;
243
                break;
244
            }
245
            if (null === $last_subject) {
246
                $last_subject = $parsed['subject'];
247
                $last_issuer = $parsed['issuer'];
248
                $certificate = $current_cert;
249
            } else {
250
                if (json_encode($last_issuer) === json_encode($parsed['subject'])) {
251
                    $last_subject = $parsed['subject'];
252
                    $last_issuer = $parsed['issuer'];
253
                } else {
254
                    $last_issuer = null;
255
                    $last_subject = null;
256
                    break;
257
                }
258
            }
259
        }
260
        if (null === $last_issuer || json_encode($last_issuer) !== json_encode($last_subject)) {
261
            throw new \InvalidArgumentException('Invalid certificate chain.');
262
        }
263
264
        return self::loadKeyFromCertificate($certificate);
265
    }
266
267
    /**
268
     * @param array $data
269
     *
270
     * @throws \Exception
271
     *
272
     * @return string
273
     */
274
    public static function fromArrayToXML(array $data)
275
    {
276
        $result = "<RSAKeyPair>\n";
277
        foreach ($data as $key => $value) {
278
            $element = self::getElement($key);
279
            $value = strtr($value, '-_', '+/');
280
281
            switch (strlen($value) % 4) {
282
                case 0:
283
                    break; // No pad chars in this case
284
                case 2:
285
                    $value .= '==';
286
                    break; // Two pad chars
287
                case 3:
288
                    $value .= '=';
289
                    break; // One pad char
290
                default:
291
                    throw new \Exception('Invalid data');
292
            }
293
294
            $result .= "\t<$element>$value</$element>\n";
295
        }
296
        $result .= '</RSAKeyPair>';
297
298
        return $result;
299
    }
300
301
    /**
302
     * @param $key
303
     *
304
     * @return string
305
     */
306
    private static function getElement($key)
307
    {
308
        $values = [
309
            'n'  => 'Modulus',
310
            'e'  => 'Exponent',
311
            'p'  => 'P',
312
            'd'  => 'D',
313
            'q'  => 'Q',
314
            'dp' => 'DP',
315
            'dq' => 'DQ',
316
            'qi' => 'InverseQ',
317
        ];
318
        if (array_key_exists($key, $values)) {
319
            return $values[$key];
320
        } else {
321
            throw new \InvalidArgumentException('Unsupported key data');
322
        }
323
    }
324
325
    /**
326
     * @param string      $pem
327
     * @param string[]    $matches
328
     * @param null|string $password
329
     *
330
     * @return string
331
     */
332
    private static function decodePem($pem, array $matches, $password = null)
333
    {
334
        if (null === $password) {
335
            throw new \InvalidArgumentException('Password required for encrypted keys.');
336
        }
337
        $iv = pack('H*', trim($matches[2]));
338
        $symkey = pack('H*', md5($password.substr($iv, 0, 8)));
339
        $symkey .= pack('H*', md5($symkey.$password.substr($iv, 0, 8)));
340
        $key = preg_replace('#^(?:Proc-Type|DEK-Info): .*#m', '', $pem);
341
        $ciphertext = base64_decode(preg_replace('#-.*-|\r|\n#', '', $key));
342
343
        $decoded = openssl_decrypt($ciphertext, strtolower($matches[1]), $symkey, true, $iv);
344
345
        $number = preg_match_all('#-{5}.*-{5}#', $pem, $result);
346
        if (2 !== $number) {
347
            throw new \InvalidArgumentException('Unable to load the key');
348
        }
349
        $pem = $result[0][0].PHP_EOL;
350
        $pem .= chunk_split(base64_encode($decoded), 64);
351
        $pem .= $result[0][1].PHP_EOL;
352
353
        return $pem;
354
    }
355
356
    /**
357
     * @param string $der_data
358
     *
359
     * @return string
360
     */
361
    private static function convertDerToPem($der_data)
362
    {
363
        $pem = chunk_split(base64_encode($der_data), 64, PHP_EOL);
364
        $pem = '-----BEGIN CERTIFICATE-----'.PHP_EOL.$pem.'-----END CERTIFICATE-----'.PHP_EOL;
365
366
        return $pem;
367
    }
368
369
    /**
370
     * @param string $pem
371
     * @param string $algorithm
372
     * @param bool   $binary
373
     *
374
     * @return string
375
     */
376
    private static function calculateX509Fingerprint($pem, $algorithm, $binary = false)
377
    {
378
        $pem = preg_replace('#-.*-|\r|\n#', '', $pem);
379
        $bin = base64_decode($pem);
380
381
        return hash($algorithm, $bin, $binary);
382
    }
383
}
384