Subversion Repositories php_utils

Rev

Rev 24 | Blame | Compare with Previous | Last modification | View Log | RSS feed

  1. <?php
  2.  
  3. /*
  4.  * X.509 Utilities for PHP
  5.  * Copyright 2011-2021 Daniel Marschall, ViaThinkSoft
  6.  * Version 2021-12-29
  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();
  64.                 $all_trusted = @glob($CApath . '*.pem');
  65.                 if ($all_trusted) foreach ($all_trusted as &$a) {
  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.  
  124. // TODO: Needs caching, otherwise the page is too slow
  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.  
  377.         $params = '';
  378.  
  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. }
  405.