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);