Subversion Repositories php_utils

Rev

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
}