Rev 22 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
2 | daniel-mar | 1 | <?php |
2 | |||
3 | /* |
||
4 | * X.509 Utilities for PHP |
||
10 | daniel-mar | 5 | * Copyright 2011-2021 Daniel Marschall, ViaThinkSoft |
22 | daniel-mar | 6 | * Version 2021-12-29 |
2 | daniel-mar | 7 | * |
8 | * Licensed under the Apache License, Version 2.0 (the "License"); |
||
9 | * you may not use this file except in compliance with the License. |
||
10 | * You may obtain a copy of the License at |
||
11 | * |
||
12 | * http://www.apache.org/licenses/LICENSE-2.0 |
||
13 | * |
||
14 | * Unless required by applicable law or agreed to in writing, software |
||
15 | * distributed under the License is distributed on an "AS IS" BASIS, |
||
16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||
17 | * See the License for the specific language governing permissions and |
||
18 | * limitations under the License. |
||
19 | */ |
||
20 | |||
21 | # define('OPENSSL_EXEC', 'openssl'); |
||
22 | # define('OPENSSL_EXEC', 'torify openssl'); |
||
23 | define('OPENSSL_EXEC', 'vtor -cr 1 -- openssl'); |
||
24 | |||
25 | # ToDo: For every function 2 modes: certFile, certPEM |
||
26 | |||
27 | function x_509_matching_issuer($cert, $issuer) { |
||
28 | exec(OPENSSL_EXEC.' verify -purpose any -CApath /dev/null -CAfile '.escapeshellarg($issuer).' '.escapeshellarg($cert), $out, $code); |
||
29 | $out = implode("\n", $out); |
||
30 | # Ab 1.0 wird hier ein Errorcode zurückgeliefert |
||
31 | # if ($code != 0) return false; |
||
32 | |||
33 | # TODO |
||
34 | # error 20 at 0 depth lookup:unable to get local issuer certificate |
||
35 | $chain0_ok = strpos($out, "error 2 at 1 depth lookup:unable to get issuer certificate") !== false; |
||
36 | $all_ok = substr($out, -2) == 'OK'; |
||
37 | |||
38 | $ok = $chain0_ok | $all_ok; |
||
39 | |||
40 | return $ok; |
||
41 | } |
||
42 | |||
43 | function x_509_is_crl_file($infile) { # Only PEM files |
||
44 | $cx = file($infile); |
||
45 | return trim($cx[0]) == '-----BEGIN X509 CRL-----'; |
||
46 | } |
||
47 | |||
48 | function x_509_chain($infile, $CApath) { |
||
49 | $chain = array(); |
||
50 | $chain[] = $infile; |
||
51 | |||
52 | while (true) { |
||
53 | $out = array(); |
||
54 | exec(OPENSSL_EXEC.' x509 -issuer_hash -in '.escapeshellarg($infile).' -noout', $out, $code); |
||
55 | if ($code != 0) return false; |
||
56 | $hash = $out[0]; |
||
57 | unset($out); |
||
58 | |||
59 | # $ary = glob($CApath . $hash . '.*'); |
||
60 | # $aryr = glob($CApath . $hash . '.r*'); |
||
61 | |||
62 | $ary = array(); |
||
63 | $aryr = array(); |
||
22 | daniel-mar | 64 | $all_trusted = @glob($CApath . '*.pem'); |
65 | if ($all_trusted) foreach ($all_trusted as &$a) { |
||
2 | daniel-mar | 66 | if (x_509_is_crl_file($a)) { |
67 | $out = array(); |
||
68 | exec(OPENSSL_EXEC.' crl -hash -noout -in '.escapeshellarg($a), $out, $code); |
||
69 | if ($code != 0) return false; |
||
70 | $this_hash = trim($out[0]); |
||
71 | unset($out); |
||
72 | # echo "CRL $a : $this_hash == $hash<br>\n"; |
||
73 | if ($this_hash == $hash) { |
||
74 | $aryr[] = $a; |
||
75 | } |
||
76 | if ($code != 0) return false; |
||
77 | } else { |
||
78 | $out = array(); |
||
79 | exec(OPENSSL_EXEC.' x509 -subject_hash -noout -in '.escapeshellarg($a), $out, $code); |
||
80 | if ($code != 0) return false; |
||
81 | $this_hash = trim($out[0]); |
||
82 | unset($out); |
||
83 | # echo "CERT $a : $this_hash == $hash<br>\n"; |
||
84 | if ($this_hash == $hash) { |
||
85 | $ary[] = $a; |
||
86 | } |
||
87 | } |
||
88 | } |
||
89 | |||
90 | $found = false; |
||
91 | # echo "Searching issuer for $infile... (Hash = $hash)<br>\n"; |
||
92 | foreach ($ary as &$a) { |
||
93 | if (in_array($a, $aryr)) continue; |
||
94 | |||
95 | # echo "Check $a...<br>\n"; |
||
96 | if (x_509_matching_issuer($infile, $a)) { |
||
97 | # echo "Found! New file is $a<br>\n"; |
||
98 | $found = true; |
||
99 | $infile = $a; |
||
100 | |||
101 | if (in_array($a, $chain)) { |
||
102 | # echo "Finished.\n"; |
||
103 | return $chain; |
||
104 | } |
||
105 | |||
106 | $chain[] = $a; |
||
107 | break; |
||
108 | } |
||
109 | } |
||
110 | if (!$found) { |
||
111 | # echo "No issuer found!\n"; |
||
112 | return false; |
||
113 | } |
||
114 | } |
||
115 | } |
||
116 | |||
117 | function x_509_get_ocsp_uris($infile) { |
||
118 | exec(OPENSSL_EXEC.' x509 -ocsp_uri -in '.escapeshellarg($infile).' -noout', $out, $code); |
||
119 | if ($code != 0) return false; |
||
120 | return $out; |
||
121 | } |
||
122 | |||
123 | |||
24 | daniel-mar | 124 | // TODO: Needs caching, otherwise the page is too slow |
2 | daniel-mar | 125 | function x_509_ocsp_check_chain($infile, $CApath) { |
126 | $x = x_509_chain($infile, $CApath); |
||
127 | |||
128 | if ($x === false) { |
||
129 | return 'Error: Could not complete chain!'; |
||
130 | } |
||
131 | |||
132 | # echo 'Chain: '; |
||
133 | # print_r($x); |
||
134 | |||
135 | $found_ocsp = false; |
||
136 | $diag_nonce_err = false; |
||
137 | $diag_verify_err = false; |
||
138 | $diag_revoked = false; |
||
139 | $diag_unknown = false; |
||
140 | |||
141 | foreach ($x as $n => &$y) { |
||
142 | if (isset($x[$n+1])) { |
||
143 | $issuer = $x[$n+1]; |
||
144 | } else { |
||
145 | $issuer = $y; // Root |
||
146 | } |
||
147 | |||
148 | $uris = x_509_get_ocsp_uris($y); |
||
149 | |||
150 | foreach ($uris as &$uri) { |
||
151 | $found_ocsp = true; |
||
152 | |||
153 | $out = array(); |
||
154 | $xx = parse_url($uri); |
||
155 | $host = $xx['host']; |
||
156 | # $cmd = OPENSSL_EXEC." ocsp -issuer ".escapeshellarg($issuer)." -cert ".escapeshellarg($y)." -url ".escapeshellarg($uri)." -CApath ".escapeshellarg($CApath)." -VAfile ".escapeshellarg($issuer)." -nonce -header 'HOST' ".escapeshellarg($host)." -header 'User-Agent' 'Mozilla/5.0 (Windows NT 6.1; rv23.0) Gecko/20100101 Firefox/23.0' 2>&1" /* -text */; |
||
157 | # TODO: trusted.pem nicht hartcoden |
||
158 | $cmd = OPENSSL_EXEC." ocsp -issuer ".escapeshellarg($issuer)." -cert ".escapeshellarg($y)." -url ".escapeshellarg($uri)." -CAfile ".escapeshellarg($CApath.'/../trusted.pem')." -VAfile ".escapeshellarg($issuer)." -nonce -header 'HOST' ".escapeshellarg($host)." -header 'User-Agent' 'Mozilla/5.0 (Windows NT 6.1; rv23.0) Gecko/20100101 Firefox/23.0' 2>&1" /* -text */; |
||
159 | #echo $cmd; |
||
160 | exec($cmd, $out, $code); |
||
161 | if ($code != 0) { |
||
162 | if (($out[0] == 'Error querying OCSP responsder') || |
||
163 | ($out[0] == 'Error querying OCSP responder')) { |
||
164 | # TODO: openssl has a typo 'Error querying OCSP responsder' |
||
165 | # TODO: why does this error occour for comodo CA? |
||
166 | return "Error querying OCSP responder (Code $code)"; |
||
167 | } |
||
168 | # print_r($out); |
||
169 | return 'Error: OpenSSL-Exec failure ('.$code.')!'; |
||
170 | } |
||
171 | |||
172 | $outc = implode("\n", $out); |
||
173 | if (strpos($outc, "Response verify OK") === false) $diag_verify_err = true; |
||
174 | if (strpos($outc, "WARNING: no nonce in response") !== false) $diag_nonce_err = true; |
||
175 | # We are currently not watching for other warnings (ToDo) |
||
176 | |||
177 | if (strpos($outc, "$y: unknown") !== false) { |
||
178 | $diag_unknown = true; |
||
179 | } else if (strpos($outc, "$y: revoked") !== false) { |
||
180 | $diag_revoked = true; |
||
181 | } else if (strpos($outc, "$y: good") === false) { |
||
182 | #echo "C = $outc<br>\n"; |
||
183 | #Ã TODO: |
||
184 | # COMODO sagt |
||
185 | # C = Responder Error: unauthorized |
||
186 | # STARTCOM sagt |
||
187 | # C = Responder Error: malformedrequest |
||
188 | return "Error: Unexpected OCSP state! ($outc)"; |
||
189 | } |
||
190 | |||
191 | # print_r($out); |
||
192 | unset($out); |
||
193 | } |
||
194 | } |
||
195 | |||
196 | # echo "Found OCSP = ".($found_ocsp ? 1 : 0)."\n"; |
||
197 | # echo "Diag Nonce Error = ".($diag_nonce_err ? 1 : 0)."\n"; |
||
198 | # echo "Diag Verify Error = ".($diag_verify_err ? 1 : 0)."\n"; |
||
199 | # echo "Diag Revoked Error = ".($diag_revoked ? 1 : 0)."\n"; |
||
200 | # echo "Diag Unknown Error = ".($diag_unknown ? 1 : 0)."\n"; |
||
201 | |||
202 | if (!$found_ocsp) { |
||
203 | return 'No OCSP responders found in chain.'; |
||
204 | } |
||
205 | |||
206 | if ($diag_verify_err) { |
||
207 | return 'Error: OCSP Verification failure!'; |
||
208 | } |
||
209 | |||
210 | if ($diag_revoked) { |
||
211 | return 'Error: Some certs are revoked!'; |
||
212 | } |
||
213 | |||
214 | if ($diag_unknown) { |
||
215 | return 'Warning: Some certs have unknown state!'; |
||
216 | } |
||
217 | |||
218 | if ($diag_nonce_err) { |
||
219 | return 'OK, but NONCE missing'; |
||
220 | } |
||
221 | |||
222 | return 'OK'; |
||
223 | } |
||
224 | |||
225 | function _opensslVerify($cert, $mode = 0, $crl_mode = 0) { |
||
226 | # mode |
||
227 | # 0 = cert is a file |
||
228 | # 1 = cert is pem string |
||
229 | |||
230 | # crl_mode |
||
231 | # 0 = no crl check |
||
232 | # 1 = 1 crl check |
||
233 | # 2 = all crl check |
||
234 | |||
235 | $params = ''; |
||
236 | if ($crl_mode == 0) { |
||
237 | $params = ''; |
||
238 | } else if ($crl_mode == 1) { |
||
239 | $params = '-crl_check '; |
||
240 | } else if ($crl_mode == 2) { |
||
241 | $params = '-crl_check_all '; |
||
242 | } else { |
||
243 | return false; |
||
244 | } |
||
245 | |||
246 | if ($mode == 0) { |
||
247 | # $cmd = OPENSSL_EXEC.' verify '.$params.' -CApath '.escapeshellarg(__DIR__.'/../ca/trusted/').' '.escapeshellarg($cert); |
||
248 | $cmd = OPENSSL_EXEC.' verify '.$params.' -CAfile '.escapeshellarg(__DIR__.'/../ca/trusted.pem').' '.escapeshellarg($cert); |
||
249 | } else if ($mode == 1) { |
||
250 | # $cmd = 'echo '.escapeshellarg($cert).' | '.OPENSSL_EXEC.' verify '.$params.' -CApath '.escapeshellarg(__DIR__.'/../ca/trusted/'); |
||
251 | $cmd = 'echo '.escapeshellarg($cert).' | '.OPENSSL_EXEC.' verify '.$params.' -CAfile '.escapeshellarg(__DIR__.'/../ca/trusted.pem'); |
||
252 | } else { |
||
253 | return false; |
||
254 | } |
||
255 | $out = array(); |
||
256 | exec($cmd, $out, $code); |
||
257 | |||
258 | if ($code != 0) return false; |
||
259 | |||
260 | return $out; |
||
261 | } |
||
262 | |||
263 | function opensslVerify($cert, $mode = 0) { |
||
264 | # 0 = cert is a file |
||
265 | # 1 = cert is pem string |
||
266 | |||
267 | $out = _opensslVerify($cert, $mode, 0); |
||
268 | if ($out === false) return 'Internal error'; |
||
269 | $outtext = implode("\n", $out); |
||
270 | |||
271 | $out_crl = _opensslVerify($cert, $mode, 2); |
||
272 | if ($out_crl === false) return 'Internal error'; |
||
273 | $outtext_crl = implode("\n", $out_crl); |
||
274 | |||
275 | if (strpos($outtext, "unable to get local issuer certificate") !== false) { |
||
276 | return 'CA unknown'; |
||
277 | } else if (strpos($outtext, "certificate signature failure") !== false) { |
||
278 | return 'Fraudulent!'; |
||
279 | } |
||
280 | |||
281 | $stat_expired = (strpos($outtext, "certificate has expired") !== false); |
||
282 | $stat_revoked = (strpos($outtext_crl, "certificate revoked") !== false); |
||
283 | |||
284 | # (ToDo) We are currently not looking for warnings |
||
285 | # $stat_crl_expired = (strpos($outtext_crl, "CRL has expired") !== false); |
||
286 | |||
287 | if ($stat_expired && $stat_revoked) { |
||
288 | return 'Expired & Revoked'; |
||
289 | } else if ($stat_revoked) { |
||
290 | return 'Revoked'; |
||
291 | } else if ($stat_expired) { |
||
292 | return 'Expired'; |
||
293 | } |
||
294 | |||
295 | if (strpos($out[0], ': OK') !== false) { |
||
296 | return 'Verified'; |
||
297 | } |
||
298 | |||
299 | return 'Unknown error'; |
||
300 | } |
||
301 | |||
302 | function getTextdump($cert, $mode = 0, $format = 0) { |
||
303 | # mode |
||
304 | # 0 = cert is a file |
||
305 | # 1 = cert is pem string |
||
306 | |||
307 | # format |
||
308 | # 0 = normal |
||
309 | # 1 = nameopt |
||
310 | |||
311 | if ($format == 0) { |
||
312 | $params = ''; |
||
313 | } else if ($format == 1) { |
||
314 | $params = ' -nameopt "esc_ctrl, esc_msb, sep_multiline, space_eq, lname"'; |
||
315 | } else { |
||
316 | return false; |
||
317 | } |
||
318 | |||
319 | if ($mode == 0) { |
||
320 | exec(OPENSSL_EXEC.' x509 -noout -text'.$params.' -in '.escapeshellarg($cert), $out, $code); |
||
321 | } else if ($mode == 1) { |
||
322 | exec('echo '.escapeshellarg($cert).' | '.OPENSSL_EXEC.' x509 -noout -text'.$params, $out, $code); |
||
323 | } else { |
||
324 | return false; |
||
325 | } |
||
326 | |||
327 | if ($code != 0) return false; |
||
328 | |||
329 | $text = implode("\n", $out); |
||
330 | |||
331 | $text = str_replace("\n\n", "\n", $text); # TODO: repeat until no \n\n exist anymore |
||
332 | |||
333 | return $text; |
||
334 | } |
||
335 | |||
336 | function getAttributes($cert, $mode = 0, $issuer = false, $longnames = false) { |
||
337 | # mode |
||
338 | # 0 = cert is a file |
||
339 | # 1 = cert is pem string |
||
340 | |||
341 | if ($longnames) { |
||
342 | $params = ' -nameopt "esc_ctrl, esc_msb, sep_multiline, space_eq, lname"'; |
||
343 | } else { |
||
344 | $params = ' -nameopt "esc_ctrl, esc_msb, sep_multiline, space_eq"'; |
||
345 | } |
||
346 | |||
347 | if ($issuer) { |
||
348 | $params .= ' -issuer'; |
||
349 | } else { |
||
350 | $params .= ' -subject'; |
||
351 | } |
||
352 | |||
353 | if ($mode == 0) { |
||
354 | exec(OPENSSL_EXEC.' x509 -noout'.$params.' -in '.escapeshellarg($cert), $out, $code); |
||
355 | } else if ($mode == 1) { |
||
356 | exec('echo '.escapeshellarg($cert).' | '.OPENSSL_EXEC.' x509 -noout'.$params, $out, $code); |
||
357 | } else { |
||
358 | return false; |
||
359 | } |
||
360 | |||
361 | $attributes = array(); |
||
362 | foreach ($out as $n => &$o) { |
||
363 | if ($n == 0) continue; |
||
364 | preg_match("| (.*) = (.*)$|ismU", $o, $m); |
||
365 | if (!isset($attributes[$m[1]])) $attributes[$m[1]] = array(); |
||
366 | $attributes[$m[1]][] = $m[2]; |
||
367 | } |
||
368 | |||
369 | return $attributes; |
||
370 | } |
||
371 | |||
372 | function openssl_get_sig_base64($cert, $mode = 0) { |
||
373 | # mode |
||
374 | # 0 = cert is a file |
||
375 | # 1 = cert is pem string |
||
376 | |||
10 | daniel-mar | 377 | $params = ''; |
378 | |||
2 | daniel-mar | 379 | $out = array(); |
380 | if ($mode == 0) { |
||
381 | exec(OPENSSL_EXEC.' x509 -noout'.$params.' -in '.escapeshellarg($cert), $out, $code); |
||
382 | } else if ($mode == 1) { |
||
383 | exec('echo '.escapeshellarg($cert).' | '.OPENSSL_EXEC.' x509 -noout'.$params, $out, $code); |
||
384 | } else { |
||
385 | return false; |
||
386 | } |
||
387 | $dump = implode("\n", $out); |
||
388 | |||
389 | /* |
||
390 | |||
391 | Signature Algorithm: sha1WithRSAEncryption |
||
392 | 65:f0:6f:f0:1d:66:a4:fe:d1:38:85:6f:5e:06:7b:f3:a7:08: |
||
393 | ... |
||
394 | 1a:13:37 |
||
395 | |||
396 | */ |
||
397 | |||
398 | $regex = "@\n {4}Signature Algorithm: (\S+)\n(( {8}([a-f0-9][a-f0-9]:){18}\n)* {8}([a-f0-9][a-f0-9](:[a-f0-9][a-f0-9]){0,17}\n))@sm"; |
||
399 | preg_match_all($regex, "$dump\n", $m); |
||
400 | if (!isset($m[2][0])) return false; |
||
401 | $x = preg_replace("@[^a-z0-9]@", "", $m[2][0]); |
||
402 | $x = hex2bin($x); |
||
403 | return base64_encode($x); |
||
404 | } |