GUI improvements for ECDSA certificate handling

* Make central functions to check and test ECDSA compatibility. Issue #9843
* Filter incompatible certificates from being offered for the GUI or Captive Portal. Implements #9897
* Do the same for IPsec, which implements #4991
* Add a check for key type when generating ipsec.secrets to allow ECDSA certs to work in IPsec for issue #4991

Note that as of this moment, the following curves are known to be compatible:
HTTPS (GUI, Captive Portal): prime256v1, secp384r1
IPsec: prime256v1, secp384r1, secp521r1

Results may vary in other areas which are not yet well-tested, and in packages.
This commit is contained in:
jim-p 2019-11-14 15:59:39 -05:00
parent c3cda38e0a
commit cffcf9bfaa
7 changed files with 167 additions and 62 deletions

View File

@ -2275,4 +2275,144 @@ function ca_setup_trust_store() {
}
}
/****f* certs/cert_get_pkey_curve
* NAME
* cert_get_pkey_curve - Get the ECDSA curve of a private key
* INPUTS
* $pkey : The private key to check
* $decode: true: base64 decode the string before use, false to use as-is.
* RESULT
* false if the private key is not ECDSA or the private key is not present.
* Otherwise, the name of the ECDSA curve used for the private key.
******/
function cert_get_pkey_curve($pkey, $decode = true) {
if ($decode) {
$pkey = base64_decode($pkey);
}
/* Attempt to read the private key, and if successful, its details. */
$res_key = openssl_pkey_get_private($pkey);
if ($res_key) {
$key_details = openssl_pkey_get_details($res_key);
/* If this is an EC key, and the curve name is not empty, return
* that curve name. */
if (($key_details['type'] == OPENSSL_KEYTYPE_EC) &&
(!empty($key_details['ec']['curve_name']))) {
return $key_details['ec']['curve_name'];
}
}
/* Either the private key could not be read, or this is not an EC certificate. */
return false;
}
/* Array containing ECDSA curve names allowed in certain contexts. For instance,
* HTTPS servers and web browsers only support specific curves in TLSv1.3. */
global $cert_curve_compatible;
$cert_curve_compatible = array(
'HTTPS' => array('prime256v1', 'secp384r1'), /* Per TLSv1.3 spec and Mozilla compatibility list */
'IPsec' => array('prime256v1', 'secp384r1', 'secp521r1') /* Per strongSwan docs/issues */
);
/****f* certs/cert_build_curve_list
* NAME
* cert_build_curve_list - Build an option list of ECDSA curves with notations
* about known compatible uses.
* INPUTS
* None
* RESULT
* Returns an option list of OpenSSL EC names with added notes. This can be
* used directly in form option selection lists.
******/
function cert_build_curve_list() {
global $cert_curve_compatible;
/* Get the default list of curve names */
$openssl_ecnames = openssl_get_curve_names();
/* Turn this into a hashed array where key==value */
$curvelist = array_combine($openssl_ecnames, $openssl_ecnames);
/* Check all known compatible curves and note matches */
foreach ($cert_curve_compatible as $consumer => $validcurves) {
/* $consumer will be a name like HTTPS or IPsec
* $validcurves will be an array of curves compatible with the consumer */
foreach ($validcurves as $vc) {
/* If the valid curve is present in the curve list, add
* a note with the consumer name to the value visible to
* the user. */
if (array_key_exists($vc, $curvelist)) {
$curvelist[$vc] .= " [{$consumer}]";
}
}
}
return $curvelist;
}
/****f* certs/cert_check_pkey_compatibility
* NAME
* cert_check_pkey_compatibility - Check a private key to see if it can be
* used in a specific compatible context.
* INPUTS
* $pkey : The private key to check
* $consumer: The consumer name used to validate the curve. See the contents
* of $cert_curve_compatible for details.
* RESULT
* true if the private key may be used in requested area, or if there are no
* restrictions.
* false if the private key cannot be used.
******/
function cert_check_pkey_compatibility($pkey, $consumer) {
global $cert_curve_compatible;
/* Read the curve name from the key */
$curve = cert_get_pkey_curve($pkey);
/* Return true if any of the following conditions are met:
* * This is not an EC key
* * The private key cannot be read
* * There are no restrictions
* * The requested curve is compatible */
return (($curve === false) ||
!array_key_exists($consumer, $cert_curve_compatible) ||
in_array($curve, $cert_curve_compatible[$consumer]));
}
/****f* certs/cert_build_list
* NAME
* cert_build_list - Build an option list of cert or CA entries, checked
* against a specific consumer name.
* INPUTS
* $type : 'ca' for certificate authority entries, 'cert' for certificates.
* $consumer: The consumer name used to filter certificates out of the result.
* See the contents of $cert_curve_compatible for details.
* RESULT
* Returns an option list of entries with incompatible entries removed. This
* can be used directly in form option selection lists.
* NOTES
* This can be expanded in the future to allow for other types of restrictions.
******/
function cert_build_list($type = 'cert', $consumer = '') {
global $config;
/* Ensure that $type is valid */
if (!in_array($type, array('ca', 'cert'))) {
return array();
}
/* Initialize arrays */
init_config_arr(array($type));
$list = array();
/* Create a hashed array with the certificate refid as the key and
* descriptive name as the value. Exclude incompatible certificates. */
foreach ($config[$type] as $cert) {
if (cert_check_pkey_compatibility($cert['prv'], $consumer)) {
$list[$cert['refid']] = $cert['descr'];
}
}
return $list;
}
?>

View File

@ -29,6 +29,7 @@
require_once("ipsec.inc");
require_once("filter.inc");
require_once("auth.inc");
require_once("certs.inc");
function vpn_update_daemon_loglevel($category, $level) {
global $ipsec_log_cats, $ipsec_log_sevs;
@ -691,7 +692,9 @@ EOD;
@chmod($ph1certfile, 0600);
/* XXX" Traffic selectors? */
$pskconf .= " : RSA {$ph1keyfile}\n";
$pskconf .= " : ";
$pskconf .= (cert_get_pkey_curve($cert['prv']) === false) ? "RSA" : "ECDSA";
$pskconf .= " {$ph1keyfile}\n";
} else {
list ($myid_type, $myid_data) = ipsec_find_id($ph1ent, 'local');
list ($peerid_type, $peerid_data) = ipsec_find_id($ph1ent, 'peer', $rgmap);

View File

@ -469,18 +469,6 @@ if ($_POST['save']) {
}
}
function build_cert_list() {
global $a_cert;
$list = array();
foreach ($a_cert as $cert) {
$list[$cert['refid']] = $cert['descr'];
}
return($list);
}
function build_authserver_list() {
$authlist = auth_get_authserver_list();
@ -1106,8 +1094,8 @@ $section->addInput(new Form_Select(
'certref',
'*SSL Certificate',
$pconfig['certref'],
build_cert_list()
))->setHelp('If no certificates are defined, one may be defined here: %1$sSystem &gt; Cert. Manager%2$s', '<a href="system_certmanager.php">', '</a>');
cert_build_list('cert', 'HTTPS')
))->setHelp('Certificates known to be incompatible with use for HTTPS are not included in this list. If no certificates are defined, one may be defined here: %1$sSystem &gt; Cert. Manager%2$s', '<a href="system_certmanager.php">', '</a>');
$section->addInput(new Form_Checkbox(
'nohttpsforwards',

View File

@ -428,17 +428,12 @@ if (!$certs_available) {
$section->add($group);
$values = array();
foreach ($a_cert as $cert) {
$values[ $cert['refid'] ] = $cert['descr'];
}
$section->addInput($input = new Form_Select(
'ssl-certref',
'SSL Certificate',
$pconfig['ssl-certref'],
$values
));
cert_build_list('cert', 'HTTPS')
))->setHelp('Certificates known to be incompatible with use for HTTPS are not included in this list.');
$section->addInput(new Form_Input(
'webguiport',

View File

@ -44,7 +44,7 @@ global $openssl_digest_algs;
global $cert_strict_values;
$max_lifetime = cert_get_max_lifetime();
$default_lifetime = min(3650, $max_lifetime);
$openssl_ecnames = openssl_get_curve_names();
$openssl_ecnames = cert_build_curve_list();
$class = "success";
init_config_arr(array('ca'));
@ -211,7 +211,7 @@ if ($_POST['save']) {
if (!in_array($_POST["keylen"], $ca_keylens)) {
array_push($input_errors, gettext("Please select a valid Key Length."));
}
if (!in_array($_POST["ecname"], $openssl_ecnames)) {
if (!in_array($_POST["ecname"], array_keys($openssl_ecnames))) {
array_push($input_errors, gettext("Please select a valid Elliptic Curve Name."));
}
if (!in_array($_POST["digest_alg"], $openssl_digest_algs)) {
@ -682,8 +682,8 @@ $group->add(new Form_Select(
'ecname',
null,
$pconfig['ecname'],
array_combine($openssl_ecnames, $openssl_ecnames)
));
$openssl_ecnames
))->setHelp('Curves may not be compatible with all uses. Known compatible curve uses are denoted in brackets.');
$section->add($group);
$section->addInput(new Form_Select(

View File

@ -51,7 +51,7 @@ global $openssl_digest_algs;
global $cert_strict_values;
$max_lifetime = cert_get_max_lifetime();
$default_lifetime = min(3650, $max_lifetime);
$openssl_ecnames = openssl_get_curve_names();
$openssl_ecnames = cert_build_curve_list();
$class = "success";
if (isset($_REQUEST['userid']) && is_numericint($_REQUEST['userid'])) {
@ -350,7 +350,7 @@ if ($_POST['save'] == gettext("Save")) {
if (isset($_POST["keylen"]) && !in_array($_POST["keylen"], $cert_keylens)) {
$input_errors[] = gettext("Please select a valid Key Length.");
}
if (isset($_POST["ecname"]) && !in_array($_POST["ecname"], $openssl_ecnames)) {
if (isset($_POST["ecname"]) && !in_array($_POST["ecname"], array_keys($openssl_ecnames))) {
$input_errors[] = gettext("Please select a valid Elliptic Curve Name.");
}
if (!in_array($_POST["digest_alg"], $openssl_digest_algs)) {
@ -364,7 +364,7 @@ if ($_POST['save'] == gettext("Save")) {
if (isset($_POST["csr_keylen"]) && !in_array($_POST["csr_keylen"], $cert_keylens)) {
$input_errors[] = gettext("Please select a valid Key Length.");
}
if (isset($_POST["csr_ecname"]) && !in_array($_POST["csr_ecname"], $openssl_ecnames)) {
if (isset($_POST["csr_ecname"]) && !in_array($_POST["csr_ecname"], array_keys($openssl_ecnames))) {
$input_errors[] = gettext("Please select a valid Elliptic Curve Name.");
}
if (!in_array($_POST["csr_digest_alg"], $openssl_digest_algs)) {
@ -842,8 +842,8 @@ if (in_array($act, array('new', 'edit')) || (($_POST['save'] == gettext("Save"))
'ecname',
null,
$pconfig['ecname'],
array_combine($openssl_ecnames, $openssl_ecnames)
));
$openssl_ecnames
))->setHelp('Curves may not be compatible with all uses. Known compatible curve uses are denoted in brackets.');
$section->add($group);
$section->addInput(new Form_Select(
@ -946,7 +946,7 @@ if (in_array($act, array('new', 'edit')) || (($_POST['save'] == gettext("Save"))
'csr_ecname',
null,
$pconfig['csr_ecname'],
array_combine($openssl_ecnames, $openssl_ecnames)
$openssl_ecnames
));
$section->add($group);

View File

@ -451,6 +451,13 @@ if ($_POST['save']) {
$input_errors[] = gettext("Cannot disable a Phase 1 with a child Phase 2 while the interface is assigned. Remove the interface assignment before disabling this P2.");
}
if (!empty($pconfig['certref'])) {
$errchkcert =& lookup_cert($pconfig['certref']);
if (is_array($errchkcert) && !cert_check_pkey_compatibility($errchkcert['prv'], 'IPsec')) {
$input_errors[] = gettext("The selected ECDSA certificate does not use a curve compatible with IKEv2");
}
}
if (!$input_errors) {
$ph1ent['ikeid'] = $pconfig['ikeid'];
$ph1ent['iketype'] = $pconfig['iketype'];
@ -623,34 +630,6 @@ function build_peerid_list() {
return($list);
}
function build_cert_list() {
global $config;
$list = array();
if (is_array($config['cert'])) {
foreach ($config['cert'] as $cert) {
$list[$cert['refid']] = $cert['descr'];
}
}
return($list);
}
function build_ca_list() {
global $config;
$list = array();
if (is_array($config['ca'])) {
foreach ($config['ca'] as $ca) {
$list[$ca['refid']] = $ca['descr'];
}
}
return($list);
}
function build_eal_list() {
global $p1_ealgos;
@ -806,14 +785,14 @@ $section->addInput(new Form_Select(
'certref',
'*My Certificate',
$pconfig['certref'],
build_cert_list()
cert_build_list('cert', 'IPsec')
))->setHelp('Select a certificate previously configured in the Certificate Manager.');
$section->addInput(new Form_Select(
'caref',
'*Peer Certificate Authority',
$pconfig['caref'],
build_ca_list()
cert_build_list('ca', 'IPsec')
))->setHelp('Select a certificate authority previously configured in the Certificate Manager.');
$form->add($section);