1 | <?php |
||
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 |
||
39 | |||
40 | /** |
||
41 | * @param string $certificate |
||
42 | * |
||
43 | * @throws \InvalidArgumentException |
||
44 | * |
||
45 | * @return array |
||
46 | */ |
||
47 | public static function loadKeyFromCertificate(string $certificate): array |
||
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 |
||
300 | } |
||
301 |