CRL management overhaul

* Allow revoking by serial number or cert. Implements #9869
* Allow revoking multiple entries at a time. Implements #3258
* Declutter the main CRL list screen
* Move the create control to the bottom under the list
* Various other efficiency/style improvements
This commit is contained in:
jim-p 2019-11-01 16:14:46 -04:00
parent 8d4663c138
commit 63fb68d713
2 changed files with 285 additions and 97 deletions

View File

@ -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;
}
}
}

View File

@ -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() {
<?php
function build_method_list() {
function build_method_list($importonly = false) {
global $_POST, $crl_methods;
$list = array();
foreach ($crl_methods as $method => $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 {
?>
<table class="table table-striped table-hover table-condensed">
<table class="table table-striped table-hover table-condensed sortable-theme-bootstrap" data-sortable>
<thead>
<tr>
<th><?=gettext("Serial")?></th>
<th><?=gettext("Certificate Name")?></th>
<th><?=gettext("Revocation Reason")?></th>
<th><?=gettext("Revoked At")?></th>
@ -475,18 +512,16 @@ if ($act == "new" || $act == gettext("Save") || $input_errors) {
<tbody>
<?php
foreach ($crl['cert'] as $i => $cert):
$name = htmlspecialchars($cert['descr']);
?>
$name = empty($cert['descr']) ? gettext('Revoked by Serial') : htmlspecialchars($cert['descr']);
$serial = crl_get_entry_serial($cert);
if (empty($serial)) {
$serial = gettext("Invalid");
} ?>
<tr>
<td class="listlr">
<?=$name; ?>
</td>
<td class="listlr">
<?=$openssl_crl_status[$cert["reason"]]; ?>
</td>
<td class="listlr">
<?=date("D M j G:i:s T Y", $cert["revoke_time"]); ?>
</td>
<td><?=htmlspecialchars($serial);?></td>
<td><?=$name; ?></td>
<td><?=$openssl_crl_status[$cert['reason']]; ?></td>
<td><?=date("D M j G:i:s T Y", $cert['revoke_time']); ?></td>
<td class="list">
<a href="system_crlmanager.php?act=delcert&amp;id=<?=$crl['refid']; ?>&amp;certref=<?=$cert['refid']; ?>" usepost>
<i class="fa fa-trash" title="<?=gettext("Delete this certificate from the CRL")?>" alt="<?=gettext("Delete this certificate from the CRL")?>"></i>
@ -516,31 +551,39 @@ if ($act == "new" || $act == gettext("Save") || $input_errors) {
print_info_box(gettext("No certificates found for this CA."), 'danger');
} else {
$section = new Form_Section('Choose a Certificate to Revoke');
$group = new Form_Group(null);
$group->add(new Form_Select(
'certref',
null,
$pconfig['certref'],
build_cacert_list()
))->setWidth(4)->setHelp('Certificate');
$group->add(new Form_Select(
$section->addInput(new Form_Select(
'crlreason',
null,
'Reason',
-1,
$openssl_crl_status
))->setHelp('Reason');
))->setHelp('Select the reason for which the certificates are being revoked.');
$group->add(new Form_Button(
$cacert_list = build_cacert_list();
$section->addInput(new Form_Select(
'certref',
'Revoke Certificates',
$pconfig['certref'],
$cacert_list,
true
))->addClass('multiselect')
->setHelp('Hold down CTRL (PC)/COMMAND (Mac) key to select multiple items.');
$section->addInput(new Form_Input(
'revokeserial',
'Revoke by Serial',
'text',
$pconfig['revokeserial']
))->setHelp('List of certificate serial numbers to revoke (separated by spaces)');
$form->addGlobal(new Form_Button(
'submit',
'Add',
null,
'fa-plus'
))->addClass('btn-success btn-sm');
$section->add($group);
$form->addGlobal(new Form_Input(
'id',
null,
@ -570,11 +613,12 @@ if ($act == "new" || $act == gettext("Save") || $input_errors) {
?>
<div class="panel panel-default">
<div class="panel-heading"><h2 class="panel-title"><?=gettext("Additional Certificate Revocation Lists")?></h2></div>
<div class="panel-heading"><h2 class="panel-title"><?=gettext("Certificate Revocation Lists")?></h2></div>
<div class="panel-body table-responsive">
<table class="table table-striped table-hover table-condensed table-rowdblclickedit">
<thead>
<tr>
<th><?=gettext("CA")?></th>
<th><?=gettext("Name")?></th>
<th><?=gettext("Internal")?></th>
<th><?=gettext("Certificates")?></th>
@ -596,39 +640,7 @@ if ($act == "new" || $act == gettext("Save") || $input_errors) {
$i = 0;
foreach ($a_ca as $ca):
$name = htmlspecialchars($ca['descr']);
if ($ca['prv']) {
$cainternal = "YES";
} else {
$cainternal = "NO";
}
?>
<tr>
<td colspan="4">
<?=$name?>
</td>
<td>
<?php
if ($cainternal == "YES"):
?>
<a href="system_crlmanager.php?act=new&amp;caref=<?=$ca['refid']; ?>" class="btn btn-xs btn-success">
<i class="fa fa-plus icon-embed-btn"></i>
<?=gettext("Add or Import CRL")?>
</a>
<?php
else:
?>
<a href="system_crlmanager.php?act=new&amp;caref=<?=$ca['refid']; ?>&amp;importonly=yes" class="btn btn-xs btn-success">
<i class="fa fa-plus icon-embed-btn"></i>
<?=gettext("Add or Import CRL")?>
</a>
<?php
endif;
?>
</td>
</tr>
<?php
$caname = htmlspecialchars($ca['descr']);
if (is_array($ca_crl_map[$ca['refid']])):
foreach ($ca_crl_map[$ca['refid']] as $crl):
$tmpcrl = lookup_crl($crl);
@ -639,6 +651,7 @@ if ($act == "new" || $act == gettext("Save") || $input_errors) {
$inuse = crl_in_use($tmpcrl['refid']);
?>
<tr>
<td><?=$caname?></td>
<td><?=$tmpcrl['descr']; ?></td>
<td><i class="fa fa-<?=($internal) ? "check" : "times"; ?>"></i></td>
<td><?=($internal) ? count($tmpcrl['cert']) : "Unknown (imported)"; ?></td>
@ -675,9 +688,33 @@ if ($act == "new" || $act == gettext("Save") || $input_errors) {
</div>
</div>
<?php
$form = new Form(false);
$section = new Form_Section('Create or Import a New Certificate Revocation List');
$group = new Form_Group(null);
$group->add(new Form_Select(
'caref',
'Certificate Authority',
null,
build_ca_list()
))->setHelp('Select a Certificate Authority for the new CRL');
$group->add(new Form_Button(
'submit',
'Add',
null,
'fa-plus'
))->addClass('btn-success btn-sm');
$section->add($group);
$form->addGlobal(new Form_Input(
'act',
null,
'hidden',
'new'
));
$form->add($section);
print($form);
}
?>
<script type="text/javascript">
@ -701,6 +738,7 @@ events.push(function() {
hideClass('internal', ($('#method').val() == 'existing'));
hideClass('existing', ($('#method').val() == 'internal'));
$('.multiselect').attr("size","<?= max(3, min(15, count($cacert_list))) ?>");
});
//]]>
</script>