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

KeyConverter::loadFromX5C()   D

Complexity

Conditions 9
Paths 8

Size

Total Lines 45
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 45
rs 4.909
c 0
b 0
f 0
cc 9
eloc 32
nc 8
nop 1
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
18
/**
19
 * @internal
20
 */
21
class KeyConverter
22
{
23
    /**
24
     * @param string $file
25
     *
26
     * @throws \InvalidArgumentException
27
     *
28
     * @return array
29
     */
30
    public static function loadKeyFromCertificateFile(string $file): array
31
    {
32
        if (!file_exists($file)) {
33
            throw new \InvalidArgumentException(sprintf('File "%s" does not exist.', $file));
34
        }
35
        $content = file_get_contents($file);
36
37
        return self::loadKeyFromCertificate($content);
38
    }
39
40
    /**
41
     * @param string $certificate
42
     *
43
     * @throws \InvalidArgumentException
44
     *
45
     * @return array
46
     */
47
    public static function loadKeyFromCertificate(string $certificate): array
48
    {
49
        try {
50
            $res = openssl_x509_read($certificate);
51
        } catch (\Exception $e) {
52
            $certificate = self::convertDerToPem($certificate);
53
            $res = openssl_x509_read($certificate);
54
        }
55
        if (false === $res) {
56
            throw new \InvalidArgumentException('Unable to load the certificate.');
57
        }
58
59
        $values = self::loadKeyFromX509Resource($res);
60
        openssl_x509_free($res);
61
62
        return $values;
63
    }
64
65
    /**
66
     * @param resource $res
67
     *
68
     * @throws \Exception
69
     *
70
     * @return array
71
     */
72
    public static function loadKeyFromX509Resource($res): array
73
    {
74
        $key = openssl_get_publickey($res);
75
76
        $details = openssl_pkey_get_details($key);
77
        if (isset($details['key'])) {
78
            $values = self::loadKeyFromPEM($details['key']);
79
            openssl_x509_export($res, $out);
80
            $x5c = preg_replace('#-.*-#', '', $out);
81
            $x5c = preg_replace('~\R~', PHP_EOL, $x5c);
82
            $x5c = trim($x5c);
83
            $values['x5c'] = [$x5c];
84
85
            $values['x5t'] = Base64Url::encode(openssl_x509_fingerprint($res, 'sha1', true));
86
            $values['x5t#256'] = Base64Url::encode(openssl_x509_fingerprint($res, 'sha256', true));
87
88
            return $values;
89
        }
90
91
        throw new \InvalidArgumentException('Unable to load the certificate');
92
    }
93
94
    /**
95
     * @param string      $file
96
     * @param null|string $password
97
     *
98
     * @throws \Exception
99
     *
100
     * @return array
101
     */
102
    public static function loadFromKeyFile(string $file, ?string $password = null): array
103
    {
104
        $content = file_get_contents($file);
105
106
        return self::loadFromKey($content, $password);
107
    }
108
109
    /**
110
     * @param string      $key
111
     * @param null|string $password
112
     *
113
     * @throws \Exception
114
     *
115
     * @return array
116
     */
117
    public static function loadFromKey(string $key, ?string $password = null): array
118
    {
119
        try {
120
            return self::loadKeyFromDER($key, $password);
121
        } catch (\Exception $e) {
122
            return self::loadKeyFromPEM($key, $password);
123
        }
124
    }
125
126
    /**
127
     * @param string      $der
128
     * @param null|string $password
129
     *
130
     * @throws \Exception
131
     *
132
     * @return array
133
     */
134
    private static function loadKeyFromDER(string $der, ?string $password = null): array
135
    {
136
        $pem = self::convertDerToPem($der);
137
138
        return self::loadKeyFromPEM($pem, $password);
139
    }
140
141
    /**
142
     * @param string      $pem
143
     * @param null|string $password
144
     *
145
     * @throws \Exception
146
     *
147
     * @return array
148
     */
149
    private static function loadKeyFromPEM(string $pem, ?string $password = null): array
150
    {
151
        if (preg_match('#DEK-Info: (.+),(.+)#', $pem, $matches)) {
152
            $pem = self::decodePem($pem, $matches, $password);
153
        }
154
155
        self::sanitizePEM($pem);
156
157
        $res = openssl_pkey_get_private($pem);
158
        if (false === $res) {
159
            $res = openssl_pkey_get_public($pem);
160
        }
161
        if (false === $res) {
162
            throw new \InvalidArgumentException('Unable to load the key.');
163
        }
164
165
        $details = openssl_pkey_get_details($res);
166
        if (!is_array($details) || !array_key_exists('type', $details)) {
167
            throw new \InvalidArgumentException('Unable to get details of the key');
168
        }
169
170
        switch ($details['type']) {
171
            case OPENSSL_KEYTYPE_EC:
172
                $ec_key = ECKey::createFromPEM($pem);
173
174
                return $ec_key->toArray();
175
            case OPENSSL_KEYTYPE_RSA:
176
                 $rsa_key = RSAKey::createFromPEM($pem);
177
                $rsa_key->optimize();
178
179
                 return $rsa_key->toArray();
180
            default:
181
                throw new \InvalidArgumentException('Unsupported key type');
182
        }
183
    }
184
185
    /**
186
     * This method modifies the PEM to get 64 char lines and fix bug with old OpenSSL versions.
187
     *
188
     * @param string $pem
189
     */
190
    private static function sanitizePEM(string &$pem)
191
    {
192
        preg_match_all('#(-.*-)#', $pem, $matches, PREG_PATTERN_ORDER);
193
        $ciphertext = preg_replace('#-.*-|\r|\n| #', '', $pem);
194
195
        $pem = $matches[0][0].PHP_EOL;
196
        $pem .= chunk_split($ciphertext, 64, PHP_EOL);
197
        $pem .= $matches[0][1].PHP_EOL;
198
    }
199
200
    /**
201
     * @param array $x5c
202
     *
203
     * @return array
204
     */
205
    public static function loadFromX5C(array $x5c): array
206
    {
207
        $certificate = null;
208
        $last_issuer = null;
209
        $last_subject = null;
210
        foreach ($x5c as $cert) {
211
            $current_cert = '-----BEGIN CERTIFICATE-----'.PHP_EOL.$cert.PHP_EOL.'-----END CERTIFICATE-----';
212
            $x509 = openssl_x509_read($current_cert);
213
            if (false === $x509) {
214
                $last_issuer = null;
215
                $last_subject = null;
216
217
                break;
218
            }
219
            $parsed = openssl_x509_parse($x509);
220
221
            openssl_x509_free($x509);
222
            if (false === $parsed) {
223
                $last_issuer = null;
224
                $last_subject = null;
225
226
                break;
227
            }
228
            if (null === $last_subject) {
229
                $last_subject = $parsed['subject'];
230
                $last_issuer = $parsed['issuer'];
231
                $certificate = $current_cert;
232
            } else {
233
                if (json_encode($last_issuer) === json_encode($parsed['subject'])) {
234
                    $last_subject = $parsed['subject'];
235
                    $last_issuer = $parsed['issuer'];
236
                } else {
237
                    $last_issuer = null;
238
                    $last_subject = null;
239
240
                    break;
241
                }
242
            }
243
        }
244
        if (null === $certificate || null !== $last_issuer && json_encode($last_issuer) !== json_encode($last_subject)) {
245
            throw new \InvalidArgumentException('Invalid certificate chain.');
246
        }
247
248
        return self::loadKeyFromCertificate($certificate);
249
    }
250
251
    /**
252
     * @param string      $pem
253
     * @param string[]    $matches
254
     * @param null|string $password
255
     *
256
     * @return string
257
     */
258
    private static function decodePem(string $pem, array $matches, ?string $password = null): string
259
    {
260
        if (null === $password) {
261
            throw new \InvalidArgumentException('Password required for encrypted keys.');
262
        }
263
264
        $iv = pack('H*', trim($matches[2]));
265
        $iv_sub = mb_substr($iv, 0, 8, '8bit');
266
        $symkey = pack('H*', md5($password.$iv_sub));
267
        $symkey .= pack('H*', md5($symkey.$password.$iv_sub));
268
        $key = preg_replace('#^(?:Proc-Type|DEK-Info): .*#m', '', $pem);
269
        $ciphertext = base64_decode(preg_replace('#-.*-|\r|\n#', '', $key));
270
271
        $decoded = openssl_decrypt($ciphertext, strtolower($matches[1]), $symkey, OPENSSL_RAW_DATA, $iv);
272
        if (!is_string($decoded)) {
273
            throw new \InvalidArgumentException('Incorrect password. Key decryption failed.');
274
        }
275
276
        $number = preg_match_all('#-{5}.*-{5}#', $pem, $result);
277
        if (2 !== $number) {
278
            throw new \InvalidArgumentException('Unable to load the key');
279
        }
280
281
        $pem = $result[0][0].PHP_EOL;
282
        $pem .= chunk_split(base64_encode($decoded), 64);
283
        $pem .= $result[0][1].PHP_EOL;
284
285
        return $pem;
286
    }
287
288
    /**
289
     * @param string $der_data
290
     *
291
     * @return string
292
     */
293
    private static function convertDerToPem(string $der_data): string
294
    {
295
        $pem = chunk_split(base64_encode($der_data), 64, PHP_EOL);
296
        $pem = '-----BEGIN CERTIFICATE-----'.PHP_EOL.$pem.'-----END CERTIFICATE-----'.PHP_EOL;
297
298
        return $pem;
299
    }
300
}
301