diff --git a/src/etc/inc/certs.inc b/src/etc/inc/certs.inc index fbcada82d2..628753ad61 100644 --- a/src/etc/inc/certs.inc +++ b/src/etc/inc/certs.inc @@ -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; +} + ?> diff --git a/src/etc/inc/vpn.inc b/src/etc/inc/vpn.inc index 51f91cd210..429501f91a 100644 --- a/src/etc/inc/vpn.inc +++ b/src/etc/inc/vpn.inc @@ -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); diff --git a/src/usr/local/www/services_captiveportal.php b/src/usr/local/www/services_captiveportal.php index 358d025187..1cdcf68d66 100644 --- a/src/usr/local/www/services_captiveportal.php +++ b/src/usr/local/www/services_captiveportal.php @@ -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 > Cert. Manager%2$s', '', ''); + 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 > Cert. Manager%2$s', '', ''); $section->addInput(new Form_Checkbox( 'nohttpsforwards', diff --git a/src/usr/local/www/system_advanced_admin.php b/src/usr/local/www/system_advanced_admin.php index c8bc37e7be..55eeee2833 100644 --- a/src/usr/local/www/system_advanced_admin.php +++ b/src/usr/local/www/system_advanced_admin.php @@ -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', diff --git a/src/usr/local/www/system_camanager.php b/src/usr/local/www/system_camanager.php index 82e3a025ab..b4056536e7 100644 --- a/src/usr/local/www/system_camanager.php +++ b/src/usr/local/www/system_camanager.php @@ -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( diff --git a/src/usr/local/www/system_certmanager.php b/src/usr/local/www/system_certmanager.php index 9a4ceee8b7..5ee77f09f7 100644 --- a/src/usr/local/www/system_certmanager.php +++ b/src/usr/local/www/system_certmanager.php @@ -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); diff --git a/src/usr/local/www/vpn_ipsec_phase1.php b/src/usr/local/www/vpn_ipsec_phase1.php index de1455d211..4f7bc5220f 100644 --- a/src/usr/local/www/vpn_ipsec_phase1.php +++ b/src/usr/local/www/vpn_ipsec_phase1.php @@ -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);