diff --git a/src/etc/inc/certs.inc b/src/etc/inc/certs.inc index 1f8f5eeb21..43a0e425d9 100644 --- a/src/etc/inc/certs.inc +++ b/src/etc/inc/certs.inc @@ -1012,10 +1012,18 @@ function crl_update(& $crl) { if (is_array($crl['cert']) && (count($crl['cert']) > 0)) { foreach ($crl['cert'] as $cert) { + /* Determine the serial number to revoke */ + if (isset($cert['serial'])) { + $serial = $cert['serial']; + } elseif (isset($cert['crt'])) { + $serial = cert_get_serial($cert['crt'], true); + } else { + continue; + } $crlconf['revoked'][] = array( - 'serial' => cert_get_serial($cert["crt"], true), - 'rev_date' => $cert["revoke_time"], - 'reason' => ($cert["reason"] == -1) ? null : (int) $cert["reason"], + 'serial' => $serial, + 'rev_date' => $cert['revoke_time'], + 'reason' => ($cert['reason'] == -1) ? null : (int) $cert['reason'], ); } } @@ -1035,9 +1043,21 @@ function cert_revoke($cert, & $crl, $reason = OCSP_REVOKED_STATUS_UNSPECIFIED) { if (!is_crl_internal($crl)) { return false; } - $cert["reason"] = $reason; - $cert["revoke_time"] = time(); - $crl["cert"][] = $cert; + + if (!is_array($cert)) { + /* If passed a not an array but a serial string, set it up as an + * array with the serial number defined */ + $rcert = array(); + $rcert['serial'] = $cert; + } else { + /* If passed a certificate entry, read out the serial and store + * it separately. */ + $rcert = $cert; + $rcert['serial'] = cert_get_serial($cert['crt']); + } + $rcert['reason'] = $reason; + $rcert['revoke_time'] = time(); + $crl['cert'][] = $rcert; crl_update($crl); return true; } @@ -1047,8 +1067,14 @@ function cert_unrevoke($cert, & $crl) { if (!is_crl_internal($crl)) { return false; } + + $serial = crl_get_entry_serial($cert); + foreach ($crl['cert'] as $id => $rcert) { - if (($rcert['refid'] == $cert['refid']) || ($rcert['descr'] == $cert['descr'])) { + /* Check for a match by refid, name, or serial number */ + if (($rcert['refid'] == $cert['refid']) || + ($rcert['descr'] == $cert['descr']) || + (crl_get_entry_serial($rcert) == $serial)) { unset($crl['cert'][$id]); if (count($crl['cert']) == 0) { // Protect against accidentally switching the type to imported, for older CRLs @@ -1082,6 +1108,131 @@ function cert_compare($cert1, $cert2) { return false; } +/****f* certs/crl_get_entry_serial + * NAME + * crl_get_entry_serial - Take a CRL entry and determine the associated serial + * INPUTS + * $entry: CRL certificate list entry to inspect, or serial string + * RESULT + * The requested serial string, if present, or null if it cannot be determined. + ******/ + +function crl_get_entry_serial($entry) { + /* Check the passed entry several ways to determine the serial */ + if (isset($entry['serial']) && (strlen($entry['serial']) > 0)) { + /* Entry is an array with a viable 'serial' element */ + return $entry['serial']; + } elseif (isset($entry['crt'])) { + /* Entry is an array with certificate text which can be used to + * determine the serial */ + return cert_get_serial($entry['crt'], true); + } elseif (cert_validate_serial($entry) != null) { + /* Entry is a valid serial string */ + return $entry; + } + /* Unable to find or determine a serial number */ + return null; +} + +/****f* certs/cert_validate_serial + * NAME + * cert_validate_serial - Validate a given string to test if it can be used as + * a certificate serial. + * INPUTS + * $serial : Serial number string to test + * $returnvalue: Whether to return the parsed value or true/false + * RESULT + * If $returnvalue is true, then the parsed ASN.1 integer value string for + * $serial or null if invalid + * If $returnvalue is false, then true/false based on whether or not $serial + * is valid. + ******/ + +function cert_validate_serial($serial, $returnvalue = false) { + require_once('ASN1.php'); + require_once('ASN1_INT.php'); + /* The ASN.1 parsing function will throw an exception if the value is + * invalid, so take advantage of that to catch other error as well. */ + try { + /* If the serial is not a string, then do not bother with + * further tests. */ + if (!is_string($serial)) { + throw new Exception('Not a string'); + } + /* Process a hex string */ + if ((substr($serial, 0, 2) == '0x')) { + /* If the string is hex, then it must contain only + * valid hex digits */ + if (!ctype_xdigit(substr($serial, 2))) { + throw new Exception('Not a valid hex string'); + } + /* Convert to decimal */ + $serial = base_convert($serial, 16, 10); + } + /* Attempt to create an ASN.1 integer, if it fails, an exception will be thrown */ + $asn1serial = new \Ukrbublik\openssl_x509_crl\ASN1_INT( $serial ); + return ($returnvalue) ? $asn1serial->content : true; + } catch (Exception $ex) { + /* No mattter what the error is, return null or false depending + * on what was requested. */ + return ($returnvalue) ? null : false; + } +} + +/****f* certs/crl_contains_cert + * NAME + * crl_contains_cert - Check if a certificate is present in a CRL + * INPUTS + * $crl : CRL to check + * $cert: Certificate to test + * RESULT + * true if the CRL contains the certificate, false otherwise + ******/ + +function crl_contains_cert($crl, $cert) { + global $config; + if (!is_array($config['crl']) || + !is_array($crl['cert'])) { + return false; + } + + /* Find the issuer of this CRL */ + $ca = lookup_ca($crl['caref']); + $crlissuer = is_array($cert) ? cert_get_issuer($ca['crt']) : null; + $serial = crl_get_entry_serial($cert); + + /* Skip issuer match when sarching by serial instead of certificate */ + $issuer = is_array($cert) ? cert_get_issuer($cert['crt']) : null; + + /* If the requested certificate was not issued by the + * same CA as the CRL, then do not bother checking this + * CRL. */ + if ($issuer != $crlissuer) { + return false; + } + + /* Check CRL entries to see if the certificate serial is revoked */ + foreach ($crl['cert'] as $rcert) { + if (crl_get_entry_serial($rcert) == $serial) { + return true; + } + } + + /* Certificate was not found in the CRL */ + return false; +} + +/****f* certs/is_cert_revoked + * NAME + * is_cert_revoked - Test if a given certificate or serial is revoked + * INPUTS + * $cert : Certificate entry or serial number to test + * $crlref: CRL to check for revoked entries, or empty to check all CRLs + * RESULT + * true if the requested entry is revoked + * false if the requested entry is not revoked + ******/ + function is_cert_revoked($cert, $crlref = "") { global $config; if (!is_array($config['crl'])) { @@ -1090,23 +1241,22 @@ function is_cert_revoked($cert, $crlref = "") { if (!empty($crlref)) { $crl = lookup_crl($crlref); - if (!is_array($crl['cert'])) { - return false; - } - foreach ($crl['cert'] as $rcert) { - if (cert_compare($rcert, $cert)) { - return true; - } - } + return crl_contains_cert($crl, $cert); } else { + if (!is_array($cert)) { + /* If passed a serial, then it cannot be definitively + * matched in this way since we do not know the CA + * associated with the bare serial. */ + return null; + } + + /* Check every CRL in the configuration for a match */ foreach ($config['crl'] as $crl) { if (!is_array($crl['cert'])) { continue; } - foreach ($crl['cert'] as $rcert) { - if (cert_compare($rcert, $cert)) { - return true; - } + if (crl_contains_cert($crl, $cert)) { + return true; } } } diff --git a/src/usr/local/www/system_crlmanager.php b/src/usr/local/www/system_crlmanager.php index 33a0fd1fef..504fba0be6 100644 --- a/src/usr/local/www/system_crlmanager.php +++ b/src/usr/local/www/system_crlmanager.php @@ -102,6 +102,11 @@ if ($act == "new") { $pconfig['caref'] = $_REQUEST['caref']; $pconfig['lifetime'] = $default_lifetime; $pconfig['serial'] = "0"; + $crlca =& lookup_ca($pconfig['caref']); + if (!$crlca) { + $input_errors[] = gettext('Invalid CA'); + unset($act); + } } if ($act == "exp") { @@ -120,36 +125,62 @@ if ($act == "exp") { if ($act == "addcert") { unset($input_errors); $pconfig = $_REQUEST; + $revoke_list = array(); - if (!$pconfig['crlref'] || !$pconfig['certref']) { + if (!$pconfig['crlref'] || (!$pconfig['certref'] && !$pconfig['revokeserial'])) { pfSenseHeader("system_crlmanager.php"); exit; } // certref, crlref $crl =& lookup_crl($pconfig['crlref']); - $cert = lookup_cert($pconfig['certref']); - - if (!$crl['caref'] || !$cert['caref']) { - $input_errors[] = gettext("Both the Certificate and CRL must be specified."); + if (!is_array($pconfig['certref'])) { + $pconfig['certref'] = array(); } - if ($crl['caref'] != $cert['caref']) { - $input_errors[] = gettext("CA mismatch between the Certificate and CRL. Unable to Revoke."); + if (empty($pconfig['certref']) && empty($pconfig['revokeserial'])) { + $input_errors[] = gettext("Select one or more certificates or enter a serial number to revoke."); } if (!is_crl_internal($crl)) { $input_errors[] = gettext("Cannot revoke certificates for an imported/external CRL."); } + foreach ($pconfig['certref'] as $rcert) { + $cert = lookup_cert($rcert); + if ($crl['caref'] == $cert['caref']) { + $revoke_list[] = $cert; + } else { + $input_errors[] = gettext("CA mismatch between the Certificate and CRL. Unable to Revoke."); + } + } + + foreach (explode(' ', $pconfig['revokeserial']) as $serial) { + if (empty($serial)) { + continue; + } + $vserial = cert_validate_serial($serial, true); + if ($vserial != null) { + $revoke_list[] = $vserial; + } else { + $input_errors[] = gettext("Invalid serial in list (Must be ASN.1 integer compatible decimal or hex string)."); + } + } + if (!$input_errors) { $reason = (empty($pconfig['crlreason'])) ? 0 : $pconfig['crlreason']; - cert_revoke($cert, $crl, $reason); + + foreach ($revoke_list as $cert) { + cert_revoke($cert, $crl, $reason); + } + // refresh IPsec and OpenVPN CRLs openvpn_refresh_crls(); vpn_ipsec_configure(); - write_config("Revoked cert {$cert['descr']} in CRL {$crl['descr']}."); + write_config("Revoked certificate(s) in CRL {$crl['descr']}."); pfSenseHeader("system_crlmanager.php"); exit; + } else { + $act = 'edit'; } } @@ -285,13 +316,13 @@ function method_change() { $desc) { - if (($_POST['importonly'] == "yes") && ($method != "existing")) { + if ($importonly && ($method != "existing")) { continue; } @@ -339,17 +370,22 @@ $tab_array[] = array(gettext("Certificates"), false, "system_certmanager.php"); $tab_array[] = array(gettext("Certificate Revocation"), true, "system_crlmanager.php"); display_top_tabs($tab_array); -if ($act == "new" || $act == gettext("Save") || $input_errors) { +if ($act == "new" || $act == gettext("Save")) { $form = new Form(); $section = new Form_Section('Create new Revocation List'); + $section->addInput(new Form_StaticText( + 'Certificate Authority', + $crlca['descr'], + )); + if (!isset($id)) { $section->addInput(new Form_Select( 'method', '*Method', $pconfig['method'], - build_method_list() + build_method_list((!isset($crlca['prv']) || empty($crlca['prv']))) )); } @@ -360,11 +396,11 @@ if ($act == "new" || $act == gettext("Save") || $input_errors) { $pconfig['descr'] )); - $section->addInput(new Form_Select( + $form->addGlobal(new Form_Input( 'caref', - '*Certificate Authority', - $pconfig['caref'], - build_ca_list() + null, + 'hidden', + $pconfig['caref'] )); $form->add($section); @@ -463,9 +499,10 @@ if ($act == "new" || $act == gettext("Save") || $input_errors) { print_info_box(gettext("No certificates found for this CRL."), 'danger'); } else { ?> -