KeyConverter::decodePem()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 28
rs 9.472
c 0
b 0
f 0
cc 4
nc 4
nop 3
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 InvalidArgumentException;
18
use RuntimeException;
19
use Throwable;
20
21
/**
22
 * @internal
23
 */
24
class KeyConverter
25
{
26
    /**
27
     * @throws InvalidArgumentException if the certificate file cannot be read
28
     */
29
    public static function loadKeyFromCertificateFile(string $file): array
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
        if (!\is_string($content)) {
36
            throw new InvalidArgumentException(sprintf('File "%s" cannot be read.', $file));
37
        }
38
39
        return self::loadKeyFromCertificate($content);
40
    }
41
42
    /**
43
     * @throws InvalidArgumentException if the OpenSSL extension is not available
44
     * @throws InvalidArgumentException if the certificate is invalid or cannot be loaded
45
     */
46
    public static function loadKeyFromCertificate(string $certificate): array
47
    {
48
        if (!\extension_loaded('openssl')) {
49
            throw new RuntimeException('Please install the OpenSSL extension');
50
        }
51
52
        try {
53
            $res = openssl_x509_read($certificate);
54
            if (false === $res) {
55
                throw new InvalidArgumentException('Unable to load the certificate.');
56
            }
57
        } catch (Throwable $e) {
58
            $certificate = self::convertDerToPem($certificate);
59
            $res = openssl_x509_read($certificate);
60
        }
61
        if (false === $res) {
62
            throw new InvalidArgumentException('Unable to load the certificate.');
63
        }
64
65
        $values = self::loadKeyFromX509Resource($res);
66
        openssl_x509_free($res);
67
68
        return $values;
69
    }
70
71
    /**
72
     * @param resource $res
73
     *
74
     * @throws InvalidArgumentException if the OpenSSL extension is not available
75
     * @throws InvalidArgumentException if the certificate is invalid or cannot be loaded
76
     */
77
    public static function loadKeyFromX509Resource($res): array
78
    {
79
        if (!\extension_loaded('openssl')) {
80
            throw new RuntimeException('Please install the OpenSSL extension');
81
        }
82
        $key = openssl_get_publickey($res);
83
        if (false === $key) {
84
            throw new InvalidArgumentException('Unable to load the certificate.');
85
        }
86
        $details = openssl_pkey_get_details($key);
87
        if (!\is_array($details)) {
88
            throw new InvalidArgumentException('Unable to load the certificate');
89
        }
90
        if (isset($details['key'])) {
91
            $values = self::loadKeyFromPEM($details['key']);
92
            openssl_x509_export($res, $out);
93
            $x5c = preg_replace('#-.*-#', '', $out);
94
            $x5c = preg_replace('~\R~', PHP_EOL, $x5c);
95
            $x5c = trim($x5c);
96
97
            $values['x5c'] = [$x5c];
98
            $values['x5t'] = Base64Url::encode(openssl_x509_fingerprint($res, 'sha1', true));
99
            $values['x5t#256'] = Base64Url::encode(openssl_x509_fingerprint($res, 'sha256', true));
100
101
            return $values;
102
        }
103
104
        throw new InvalidArgumentException('Unable to load the certificate');
105
    }
106
107
    public static function loadFromKeyFile(string $file, ?string $password = null): array
108
    {
109
        $content = file_get_contents($file);
110
111
        return self::loadFromKey($content, $password);
112
    }
113
114
    public static function loadFromKey(string $key, ?string $password = null): array
115
    {
116
        try {
117
            return self::loadKeyFromDER($key, $password);
118
        } catch (Throwable $e) {
119
            return self::loadKeyFromPEM($key, $password);
120
        }
121
    }
122
123
    /**
124
     * Be careful! The certificate chain is loaded, but it is NOT VERIFIED by any mean!
125
     * It is mandatory to verify the root CA or intermediate  CA are trusted.
126
     * If not done, it may lead to potential security issues.
127
     *
128
     * @throws InvalidArgumentException if the certificate chain is empty
129
     * @throws InvalidArgumentException if the OpenSSL extension is not available
130
     */
131
    public static function loadFromX5C(array $x5c): array
132
    {
133
        if (0 === \count($x5c)) {
134
            throw new InvalidArgumentException('The certificate chain is empty');
135
        }
136
        foreach ($x5c as $id => $cert) {
137
            $x5c[$id] = '-----BEGIN CERTIFICATE-----'.PHP_EOL.chunk_split($cert, 64, PHP_EOL).'-----END CERTIFICATE-----';
138
            $x509 = openssl_x509_read($x5c[$id]);
139
            if (false === $x509) {
140
                throw new InvalidArgumentException('Unable to load the certificate chain');
141
            }
142
            $parsed = openssl_x509_parse($x509);
143
144
            openssl_x509_free($x509);
145
            if (false === $parsed) {
146
                throw new InvalidArgumentException('Unable to load the certificate chain');
147
            }
148
        }
149
150
        return self::loadKeyFromCertificate(reset($x5c));
151
    }
152
153
    private static function loadKeyFromDER(string $der, ?string $password = null): array
154
    {
155
        $pem = self::convertDerToPem($der);
156
157
        return self::loadKeyFromPEM($pem, $password);
158
    }
159
160
    /**
161
     * @throws InvalidArgumentException if the OpenSSL extension is not available
162
     * @throws InvalidArgumentException if the key cannot be loaded
163
     */
164
    private static function loadKeyFromPEM(string $pem, ?string $password = null): array
165
    {
166
        if (1 === preg_match('#DEK-Info: (.+),(.+)#', $pem, $matches)) {
167
            $pem = self::decodePem($pem, $matches, $password);
168
        }
169
170
        if (!\extension_loaded('openssl')) {
171
            throw new RuntimeException('Please install the OpenSSL extension');
172
        }
173
        self::sanitizePEM($pem);
174
        $res = openssl_pkey_get_private($pem);
175
        if (false === $res) {
176
            $res = openssl_pkey_get_public($pem);
177
        }
178
        if (false === $res) {
179
            throw new InvalidArgumentException('Unable to load the key.');
180
        }
181
182
        $details = openssl_pkey_get_details($res);
183
        if (!\is_array($details) || !\array_key_exists('type', $details)) {
184
            throw new InvalidArgumentException('Unable to get details of the key');
185
        }
186
187
        switch ($details['type']) {
188
            case OPENSSL_KEYTYPE_EC:
189
                $ec_key = ECKey::createFromPEM($pem);
190
191
                return $ec_key->toArray();
192
            case OPENSSL_KEYTYPE_RSA:
193
                 $rsa_key = RSAKey::createFromPEM($pem);
194
                $rsa_key->optimize();
195
196
                 return $rsa_key->toArray();
197
            default:
198
                throw new InvalidArgumentException('Unsupported key type');
199
        }
200
    }
201
202
    /**
203
     * This method modifies the PEM to get 64 char lines and fix bug with old OpenSSL versions.
204
     */
205
    private static function sanitizePEM(string &$pem): void
206
    {
207
        preg_match_all('#(-.*-)#', $pem, $matches, PREG_PATTERN_ORDER);
208
        $ciphertext = preg_replace('#-.*-|\r|\n| #', '', $pem);
209
210
        $pem = $matches[0][0].PHP_EOL;
211
        $pem .= chunk_split($ciphertext, 64, PHP_EOL);
212
        $pem .= $matches[0][1].PHP_EOL;
213
    }
214
215
    /**
216
     * @param string[] $matches
217
     *
218
     * @throws InvalidArgumentException if the password to decrypt the key is not provided
219
     * @throws InvalidArgumentException if the key cannot be loaded
220
     */
221
    private static function decodePem(string $pem, array $matches, ?string $password = null): string
222
    {
223
        if (null === $password) {
224
            throw new InvalidArgumentException('Password required for encrypted keys.');
225
        }
226
227
        $iv = pack('H*', trim($matches[2]));
228
        $iv_sub = mb_substr($iv, 0, 8, '8bit');
229
        $symkey = pack('H*', md5($password.$iv_sub));
230
        $symkey .= pack('H*', md5($symkey.$password.$iv_sub));
231
        $key = preg_replace('#^(?:Proc-Type|DEK-Info): .*#m', '', $pem);
232
        $ciphertext = base64_decode(preg_replace('#-.*-|\r|\n#', '', $key), true);
233
234
        $decoded = openssl_decrypt($ciphertext, mb_strtolower($matches[1]), $symkey, OPENSSL_RAW_DATA, $iv);
235
        if (false === $decoded) {
236
            throw new RuntimeException('Unable to decrypt the key');
237
        }
238
        $number = preg_match_all('#-{5}.*-{5}#', $pem, $result);
239
        if (2 !== $number) {
240
            throw new InvalidArgumentException('Unable to load the key');
241
        }
242
243
        $pem = $result[0][0].PHP_EOL;
244
        $pem .= chunk_split(base64_encode($decoded), 64);
245
        $pem .= $result[0][1].PHP_EOL;
246
247
        return $pem;
248
    }
249
250
    private static function convertDerToPem(string $der_data): string
251
    {
252
        $pem = chunk_split(base64_encode($der_data), 64, PHP_EOL);
253
254
        return '-----BEGIN CERTIFICATE-----'.PHP_EOL.$pem.'-----END CERTIFICATE-----'.PHP_EOL;
255
    }
256
}
257