From 8571e6f0a9843b9880de7b152f00c7a26bd52182 Mon Sep 17 00:00:00 2001 From: Wytze van der Raay Date: Thu, 16 Jun 2011 09:19:58 +0000 Subject: [PATCH] Fix for https://bugs.cacert.org/view.php?id=918 (detection and prevention of weak keys for CAcert-issued certificates) --- includes/account.php | 157 ++++++++++++++-- includes/account_stuff.php | 357 +++++++++++++++++++++++++++++++++++++ 2 files changed, 498 insertions(+), 16 deletions(-) diff --git a/includes/account.php b/includes/account.php index 685b53a..14702b9 100644 --- a/includes/account.php +++ b/includes/account.php @@ -299,6 +299,15 @@ $_SESSION['_config']['rootcert'] = 1; $emails .= "SPKAC = $spkac"; + if (($weakKey = checkWeakKeySPKAC($emails)) !== "") + { + $id = 4; + showheader(_("My CAcert.org Account!")); + echo $weakKey; + showfooter(); + exit; + } + $query = "insert into emailcerts set `CN`='$defaultemail', `keytype`='NS', @@ -330,6 +339,16 @@ } else if($_REQUEST['keytype'] == "MS" || $_REQUEST['keytype'] == "VI") { if($csr == "") $csr = "-----BEGIN CERTIFICATE REQUEST-----\n".clean_csr($_REQUEST['CSR'])."\n-----END CERTIFICATE REQUEST-----\n"; + + if (($weakKey = checkWeakKeyCSR($csr)) !== "") + { + $id = 4; + showheader(_("My CAcert.org Account!")); + echo $weakKey; + showfooter(); + exit; + } + $tmpfname = tempnam("/tmp", "id4CSR"); $fp = fopen($tmpfname, "w"); fputs($fp, $csr); @@ -613,17 +632,23 @@ if($process != "" && $oldid == 10) { $CSR = clean_csr($_REQUEST['CSR']); - $_SESSION['_config']['tmpfname'] = tempnam("/tmp", "id10CSR"); - $fp = fopen($_SESSION['_config']['tmpfname'], "w"); if(strpos($CSR,"---BEGIN")===FALSE) { // In case the CSR is missing the ---BEGIN lines, add them automatically: - fputs($fp,"-----BEGIN CERTIFICATE REQUEST-----\n".$CSR."\n-----END CERTIFICATE REQUEST-----\n"); + $CSR = "-----BEGIN CERTIFICATE REQUEST-----\n".$CSR."\n-----END CERTIFICATE REQUEST-----\n"; } - else + + if (($weakKey = checkWeakKeyCSR($CSR)) !== "") { - fputs($fp, $CSR); + showheader(_("My CAcert.org Account!")); + echo $weakKey; + showfooter(); + exit; } + + $_SESSION['_config']['tmpfname'] = tempnam("/tmp", "id10CSR"); + $fp = fopen($_SESSION['_config']['tmpfname'], "w"); + fputs($fp, $CSR); fclose($fp); $CSR = $_SESSION['_config']['tmpfname']; $_SESSION['_config']['subject'] = trim(`/usr/bin/openssl req -text -noout -in "$CSR"|tr -d "\\0"|grep "Subject:"`); @@ -658,6 +683,23 @@ if($process != "" && $oldid == 11) { + if(!file_exists($_SESSION['_config']['tmpfname'])) + { + showheader(_("My CAcert.org Account!")); + printf(_("Your certificate request has failed to be processed correctly, see %sthe WIKI page%s for reasons and solutions."), "", ""); + showfooter(); + exit; + } + + if (($weakKey = checkWeakKeyCSR(file_get_contents( + $_SESSION['_config']['tmpfname']))) !== "") + { + showheader(_("My CAcert.org Account!")); + echo $weakKey; + showfooter(); + exit; + } + $id = 11; if($_SESSION['_config']['0.CN'] == "" && $_SESSION['_config']['0.subjectAltName'] == "") { @@ -731,13 +773,6 @@ mysql_query("insert into `domlink` set `certid`='$CSRid', `domid`='$dom'"); $CSRname=generatecertpath("csr","server",$CSRid); - if(!file_exists($_SESSION['_config']['tmpfname'])) - { - showheader(_("My CAcert.org Account!")); - printf(_("Your certificate request has failed to be processed correctly, see %sthe WIKI page%s for reasons and solutions."), "", ""); - showfooter(); - exit; - } rename($_SESSION['_config']['tmpfname'], $CSRname); chmod($CSRname,0644); mysql_query("update `domaincerts` set `CSR_name`='$CSRname' where `id`='$CSRid'"); @@ -780,8 +815,17 @@ printf(_("Invalid ID '%s' presented, can't do anything with it.")."
\n", $id); continue; } - mysql_query("update `domaincerts` set `renewed`='1' where `id`='$id'"); + $row = mysql_fetch_assoc($res); + + if (($weakKey = checkWeakKeyX509(file_get_contents( + $row['crt_name']))) !== "") + { + echo $weakKey, "
\n"; + continue; + } + + mysql_query("update `domaincerts` set `renewed`='1' where `id`='$id'"); $query = "insert into `domaincerts` set `domid`='".$row['domid']."', `CN`='".mysql_real_escape_string($row['CN'])."', @@ -946,8 +990,17 @@ printf(_("Invalid ID '%s' presented, can't do anything with it.")."
\n", $id); continue; } - mysql_query("update `emailcerts` set `renewed`='1' where `id`='$id'"); + $row = mysql_fetch_assoc($res); + + if (($weakKey = checkWeakKeyX509(file_get_contents( + $row['crt_name']))) !== "") + { + echo $weakKey, "
\n"; + continue; + } + + mysql_query("update `emailcerts` set `renewed`='1' where `id`='$id'"); $query = "insert into emailcerts set `memid`='".$row['memid']."', `CN`='".mysql_real_escape_string($row['CN'])."', @@ -1378,6 +1431,15 @@ $_SESSION['_config']['rootcert'] = 1; $emails .= "SPKAC = $spkac"; + if (($weakKey = checkWeakKeySPKAC($emails)) !== "") + { + $id = 17; + showheader(_("My CAcert.org Account!")); + echo $weakKey; + showfooter(); + exit; + } + $query = "insert into `orgemailcerts` set `CN`='$defaultemail', `keytype`='NS', @@ -1408,6 +1470,16 @@ mysql_query("update `orgemailcerts` set `csr_name`='$CSRname' where `id`='$emailid'"); } else if($_REQUEST['keytype'] == "MS" || $_REQUEST['keytype']=="VI") { $csr = "-----BEGIN CERTIFICATE REQUEST-----\n".clean_csr($_REQUEST['CSR'])."-----END CERTIFICATE REQUEST-----\n"; + + if (($weakKey = checkWeakKeyCSR($csr)) !== "") + { + $id = 17; + showheader(_("My CAcert.org Account!")); + echo $weakKey; + showfooter(); + exit; + } + $tmpfname = tempnam("/tmp", "id17CSR"); $fp = fopen($tmpfname, "w"); fputs($fp, $csr); @@ -1514,8 +1586,17 @@ printf(_("Invalid ID '%s' presented, can't do anything with it.")."
\n", $id); continue; } - mysql_query("update `orgemailcerts` set `renewed`='1' where `id`='$id'"); + $row = mysql_fetch_assoc($res); + + if (($weakKey = checkWeakKeyX509(file_get_contents( + $row['crt_name']))) !== "") + { + echo $weakKey, "
\n"; + continue; + } + + mysql_query("update `orgemailcerts` set `renewed`='1' where `id`='$id'"); if($row['revoke'] > 0) { printf(_("It would seem '%s' has already been revoked. I'll skip this for now.")."
\n", $row['CN']); @@ -1625,6 +1706,16 @@ if($process != "" && $oldid == 20) { $CSR = clean_csr($_REQUEST['CSR']); + + if (($weakKey = checkWeakKeyCSR($CSR)) !== "") + { + $id = 20; + showheader(_("My CAcert.org Account!")); + echo $weakKey; + showfooter(); + exit; + } + $_SESSION['_config']['tmpfname'] = tempnam("/tmp", "id20CSR"); $fp = fopen($_SESSION['_config']['tmpfname'], "w"); fputs($fp, $CSR); @@ -1674,6 +1765,23 @@ if($process != "" && $oldid == 21) { $id = 21; + + if(!file_exists($_SESSION['_config']['tmpfname'])) + { + showheader(_("My CAcert.org Account!")); + printf(_("Your certificate request has failed to be processed correctly, see %sthe WIKI page%s for reasons and solutions."), "", ""); + showfooter(); + exit; + } + + if (($weakKey = checkWeakKeyCSR(file_get_contents( + $_SESSION['_config']['tmpfname']))) !== "") + { + showheader(_("My CAcert.org Account!")); + echo $weakKey; + showfooter(); + exit; + } if($_SESSION['_config']['0.CN'] == "" && $_SESSION['_config']['0.subjectAltName'] == "") { @@ -1799,8 +1907,17 @@ printf(_("Invalid ID '%s' presented, can't do anything with it.")."
\n", $id); continue; } - mysql_query("update `orgdomaincerts` set `renewed`='1' where `id`='$id'"); + $row = mysql_fetch_assoc($res); + + if (($weakKey = checkWeakKeyX509(file_get_contents( + $row['crt_name']))) !== "") + { + echo $weakKey, "
\n"; + continue; + } + + mysql_query("update `orgdomaincerts` set `renewed`='1' where `id`='$id'"); if($row['revoke'] > 0) { printf(_("It would seem '%s' has already been revoked. I'll skip this for now.")."
\n", $row['CN']); @@ -2497,6 +2614,14 @@ showfooter(); exit; } + + if (($weakKey = checkWeakKeyCSR($CSR)) !== "") + { + showheader(_("My CAcert.org Account!")); + echo $weakKey; + showfooter(); + exit; + } $query = "insert into `domaincerts` set `CN`='".$_SESSION['_config']['0.CN']."', diff --git a/includes/account_stuff.php b/includes/account_stuff.php index fa6757b..7c8980c 100644 --- a/includes/account_stuff.php +++ b/includes/account_stuff.php @@ -284,4 +284,361 @@ function hideall() { support@cacert.org", + $errorId); + } + + /** + * Checks whether the given CSR contains a vulnerable key + * + * @param $csr string + * The CSR to be checked + * @param $encoding string [optional] + * The encoding the CSR is in (for the "-inform" parameter of OpenSSL, + * currently only "PEM" (default) or "DER" allowed) + * @return string containing the reason if the key is considered weak, + * empty string otherwise + */ + function checkWeakKeyCSR($csr, $encoding = "PEM") + { + // non-PEM-encodings may be binary so don't use echo + $descriptorspec = array( + 0 => array("pipe", "r"), // STDIN for child + 1 => array("pipe", "w"), // STDOUT for child + ); + $encoding = escapeshellarg($encoding); + $proc = proc_open("openssl req -inform $encoding -text -noout", + $descriptorspec, $pipes); + + if (is_resource($proc)) + { + fwrite($pipes[0], $csr); + fclose($pipes[0]); + + $csrText = ""; + while (!feof($pipes[1])) + { + $csrText .= fread($pipes[1], 8192); + } + fclose($pipes[1]); + + if (($status = proc_close($proc)) !== 0 || $csrText === "") + { + return _("I didn't receive a valid Certificate Request, hit ". + "the back button and try again."); + } + } else { + return failWithId("checkWeakKeyCSR(): Failed to start OpenSSL"); + } + + + return checkWeakKeyText($csrText); + } + + /** + * Checks whether the given X509 certificate contains a vulnerable key + * + * @param $cert string + * The X509 certificate to be checked + * @param $encoding string [optional] + * The encoding the certificate is in (for the "-inform" parameter of + * OpenSSL, currently only "PEM" (default), "DER" or "NET" allowed) + * @return string containing the reason if the key is considered weak, + * empty string otherwise + */ + function checkWeakKeyX509($cert, $encoding = "PEM") + { + // non-PEM-encodings may be binary so don't use echo + $descriptorspec = array( + 0 => array("pipe", "r"), // STDIN for child + 1 => array("pipe", "w"), // STDOUT for child + ); + $encoding = escapeshellarg($encoding); + $proc = proc_open("openssl x509 -inform $encoding -text -noout", + $descriptorspec, $pipes); + + if (is_resource($proc)) + { + fwrite($pipes[0], $cert); + fclose($pipes[0]); + + $certText = ""; + while (!feof($pipes[1])) + { + $certText .= fread($pipes[1], 8192); + } + fclose($pipes[1]); + + if (($status = proc_close($proc)) !== 0 || $certText === "") + { + return _("I didn't receive a valid Certificate Request, hit ". + "the back button and try again."); + } + } else { + return failWithId("checkWeakKeyCSR(): Failed to start OpenSSL"); + } + + + return checkWeakKeyText($certText); + } + + /** + * Checks whether the given SPKAC contains a vulnerable key + * + * @param $spkac string + * The SPKAC to be checked + * @param $spkacname string [optional] + * The name of the variable that contains the SPKAC. The default is + * "SPKAC" + * @return string containing the reason if the key is considered weak, + * empty string otherwise + */ + function checkWeakKeySPKAC($spkac, $spkacname = "SPKAC") + { + /* Check for the debian OpenSSL vulnerability */ + + $spkac = escapeshellarg($spkac); + $spkacname = escapeshellarg($spkacname); + $spkacText = `echo $spkac | openssl spkac -spkac $spkacname`; + if ($spkacText === null) { + return _("I didn't receive a valid Certificate Request, hit the ". + "back button and try again."); + } + + return checkWeakKeyText($spkacText); + } + + /** + * Checks whether the given text representation of a CSR or a SPKAC contains + * a weak key + * + * @param $text string + * The text representation of a key as output by the + * "openssl -text -noout" commands + * @return string containing the reason if the key is considered weak, + * empty string otherwise + */ + function checkWeakKeyText($text) + { + /* Which public key algorithm? */ + if (!preg_match('/^\s*Public Key Algorithm: ([^\s]+)$/m', $text, + $algorithm)) + { + return failWithId("checkWeakKeyText(): Couldn't extract the ". + "public key algorithm used"); + } else { + $algorithm = $algorithm[1]; + } + + + if ($algorithm === "rsaEncryption") + { + if (!preg_match('/^\s*RSA Public Key: \((\d+) bit\)$/m', $text, + $keysize)) + { + return failWithId("checkWeakKeyText(): Couldn't parse the RSA ". + "key size"); + } else { + $keysize = intval($keysize[1]); + } + + if ($keysize < 1024) + { + return sprintf(_("The keys that you use are very small ". + "and therefore insecure. Please generate stronger ". + "keys. More information about this issue can be ". + "found in %sthe wiki%s"), + "", + ""); + } elseif ($keysize < 2048) { + // not critical but log so we have some statistics about + // affected users + trigger_error("checkWeakKeyText(): Certificate for small ". + "key (< 2048 bit) requested", E_USER_NOTICE); + } + + + $debianVuln = checkDebianVulnerability($text, $keysize); + if ($debianVuln === true) + { + return sprintf(_("The keys you use have very likely been ". + "generated with a vulnerable version of OpenSSL which ". + "was distributed by debian. Please generate new keys. ". + "More information about this issue can be found in ". + "%sthe wiki%s"), + "", + ""); + } elseif ($debianVuln === false) { + // not vulnerable => do nothing + } else { + return failWithId("checkWeakKeyText(): Something went wrong in". + "checkDebianVulnerability()"); + } + + if (!preg_match('/^\s*Exponent: (\d+) \(0x[0-9a-fA-F]+\)$/m', $text, + $exponent)) + { + return failWithId("checkWeakKeyText(): Couldn't parse the RSA ". + "exponent"); + } else { + $exponent = $exponent[1]; // exponent might be very big => + //handle as string using bc*() + + if (bccomp($exponent, "3") === 0) + { + return sprintf(_("The keys you use might be insecure. ". + "Although there is currently no known attack for ". + "reasonable encryption schemes, we're being ". + "cautious and don't allow certificates for such ". + "keys. Please generate stronger keys. More ". + "information about this issue can be found in ". + "%sthe wiki%s"), + "", + ""); + } elseif (!(bccomp($exponent, "65537") >= 0 && + (bccomp($exponent, "100000") === -1 || + // speed things up if way smaller than 2^256 + bccomp($exponent, bcpow("2", "256")) === -1) )) { + // 65537 <= exponent < 2^256 recommended by NIST + // not critical but log so we have some statistics about + // affected users + trigger_error("checkWeakKeyText(): Certificate for ". + "unsuitable exponent '$exponent' requested", + E_USER_NOTICE); + } + } + } + + /* No weakness found */ + return ""; + } + + /** + * Reimplement the functionality of the openssl-vulnkey tool + * + * @param $text string + * The text representation of a key as output by the + * "openssl -text -noout" commands + * @param $keysize int [optional] + * If the key size is already known it can be provided so it doesn't + * have to be parsed again. This also skips the check whether the key + * is an RSA key => use wisely + * @return TRUE if key is vulnerable, FALSE otherwise, NULL in case of error + */ + function checkDebianVulnerability($text, $keysize = 0) + { + $keysize = intval($keysize); + + if ($keysize === 0) + { + /* Which public key algorithm? */ + if (!preg_match('/^\s*Public Key Algorithm: ([^\s]+)$/m', $text, + $algorithm)) + { + trigger_error("checkDebianVulnerability(): Couldn't extract ". + "the public key algorithm used", E_USER_WARNING); + return null; + } else { + $algorithm = $algorithm[1]; + } + + if ($algorithm !== "rsaEncryption") return false; + + /* Extract public key size */ + if (!preg_match('/^\s*RSA Public Key: \((\d+) bit\)$/m', $text, + $keysize)) + { + trigger_error("checkDebianVulnerability(): Couldn't parse the ". + "RSA key size", E_USER_WARNING); + return null; + } else { + $keysize = intval($keysize[1]); + } + } + + // $keysize has been made sure to contain an int + $blacklist = "/usr/share/openssl-blacklist/blacklist.RSA-$keysize"; + if (!(is_file($blacklist) && is_readable($blacklist))) + { + if (in_array($keysize, array(512, 1024, 2048, 4096))) + { + trigger_error("checkDebianVulnerability(): Blacklist for ". + "$keysize bit keys not accessible. Expected at ". + "$blacklist", E_USER_ERROR); + return null; + } + + trigger_error("checkDebianVulnerability(): $blacklist is not ". + "readable. Unsupported key size?", E_USER_WARNING); + return false; + } + + + /* Extract RSA modulus */ + if (!preg_match('/^\s*Modulus \(\d+ bit\):\n'. + '((?:\s*[0-9a-f][0-9a-f]:(?:\n)?)+[0-9a-f][0-9a-f])$/m', + $text, $modulus)) + { + trigger_error("checkDebianVulnerability(): Couldn't extract the ". + "RSA modulus", E_USER_WARNING); + return null; + } else { + $modulus = $modulus[1]; + // strip whitespace and colon leftovers + $modulus = str_replace(array(" ", "\t", "\n", ":"), "", $modulus); + + // when using "openssl xxx -text" first byte was 00 in all my test + // cases but 00 not present in the "openssl xxx -modulus" output + if ($modulus[0] === "0" && $modulus[1] === "0") + { + $modulus = substr($modulus, 2); + } else { + trigger_error("checkDebianVulnerability(): First byte is not ". + "zero", E_USER_NOTICE); + } + + $modulus = strtoupper($modulus); + } + + + /* calculate checksum and look it up in the blacklist */ + $checksum = substr(sha1("Modulus=$modulus\n"), 20); + + // $checksum and $blacklist should be safe, but just to make sure + $checksum = escapeshellarg($checksum); + $blacklist = escapeshellarg($blacklist); + exec("grep $checksum $blacklist", $dummy, $debianVuln); + if ($debianVuln === 0) // grep returned something => it is on the list + { + return true; + } elseif ($debianVuln === 1) { // grep returned nothing + return false; + } else { + trigger_error("checkDebianVulnerability(): Something went wrong ". + "when looking up the key with checksum $checksum in the ". + "blacklist $blacklist", E_USER_ERROR); + return null; + } + + // Should not get here + return null; + } ?>