diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Trust/Api/CertController.php b/src/opnsense/mvc/app/controllers/OPNsense/Trust/Api/CertController.php
index 716696cd2..4588c3edd 100644
--- a/src/opnsense/mvc/app/controllers/OPNsense/Trust/Api/CertController.php
+++ b/src/opnsense/mvc/app/controllers/OPNsense/Trust/Api/CertController.php
@@ -48,7 +48,91 @@ class CertController extends ApiMutableModelControllerBase
if (empty((string)$node->refid)) {
$node->refid = uniqid();
}
- throw new UserException((string)$node->refid, (string)$node->action);
+ $error = false;
+ if (!empty((string)$node->prv_payload)) {
+ /** private key manually offered */
+ $node->prv = base64_encode((string)$node->prv_payload);
+ }
+ switch ((string)$node->action) {
+ case 'internal':
+ $data = CertStore::createCert(
+ (string)$node->key_type,
+ (string)$node->lifetime,
+ $node->dn(),
+ (string)$node->digest,
+ (string)$node->caref,
+ (string)$node->cert_type,
+ $node->extns()
+ );
+ if (!empty($data['crt']) && !empty($data['prv'])) {
+ $node->crt = base64_encode($data['crt']);
+ if ((string)$node->private_key_location == 'local') {
+ /* return only in volatile storage */
+ $node->prv_payload = $data['prv'];
+ } else {
+ $node->prv= base64_encode($data['prv']);
+ }
+ } else {
+ $error = $data['error'] ?? '';
+ }
+ break;
+ case 'external':
+ $data = CertStore::createCert(
+ (string)$node->key_type,
+ (string)$node->lifetime,
+ $node->dn(),
+ (string)$node->digest,
+ false,
+ (string)$node->cert_type,
+ $node->extns()
+ );
+ if (!empty($data['csr'])) {
+ $node->csr = base64_encode($data['csr']);
+ } else {
+ $error = $data['error'] ?? '';
+ }
+ break;
+ case 'import':
+ if (CertStore::parseX509((string)$node->crt_payload) === false) {
+ $error = gettext('Invalid X509 certificate provided');
+ } else {
+ $node->crt = base64_encode((string)$node->crt_payload);
+ if (
+ !empty(trim((string)$node->prv_payload)) &&
+ openssl_pkey_get_private((string)$node->prv_payload) === false
+ ) {
+ $error = gettext('Invalid private key provided');
+ }
+ }
+ break;
+ case 'import_csr':
+ if (CertStore::parseX509((string)$node->crt_payload) === false) {
+ $error = gettext('Invalid X509 certificate provided');
+ } else {
+ $node->crt = base64_encode((string)$node->crt_payload);
+ }
+ break;
+ case 'reissue':
+ $data = CertStore::reIssueCert(
+ (string)$node->key_type,
+ (string)$node->lifetime,
+ $node->dn(),
+ (string)$node->prv_payload,
+ (string)$node->digest,
+ (string)$node->caref,
+ (string)$node->cert_type,
+ $node->extns()
+ );
+ if (!empty($data['crt'])) {
+ $node->crt = base64_encode($data['crt']);
+ } else {
+ $error = $data['error'] ?? '';
+ }
+ break;
+ }
+ if ($error !== false) {
+ throw new UserException($error, "Certificate error");
+ }
}
public function searchAction()
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Trust/forms/dialogCert.xml b/src/opnsense/mvc/app/controllers/OPNsense/Trust/forms/dialogCert.xml
index c37d946e7..b062791b8 100644
--- a/src/opnsense/mvc/app/controllers/OPNsense/Trust/forms/dialogCert.xml
+++ b/src/opnsense/mvc/app/controllers/OPNsense/Trust/forms/dialogCert.xml
@@ -7,20 +7,22 @@
header
-
+
cert.cert_type
dropdown
+
cert.private_key_location
+
dropdown
- cert.key
+ cert.key_type
dropdown
@@ -35,10 +37,15 @@
dropdown
+
+ cert.lifetime
+
+ text
+
header
-
+
cert.descr
@@ -84,12 +91,13 @@
cert.ocsp_uri
text
+
header
true
-
+
cert.altnames_dns
@@ -131,6 +139,6 @@
cert.csr_payload
textbox
-
+
diff --git a/src/opnsense/mvc/app/library/OPNsense/Trust/Store.php b/src/opnsense/mvc/app/library/OPNsense/Trust/Store.php
index 941aceac8..db610b48c 100644
--- a/src/opnsense/mvc/app/library/OPNsense/Trust/Store.php
+++ b/src/opnsense/mvc/app/library/OPNsense/Trust/Store.php
@@ -184,6 +184,62 @@ class Store
return [];
}
+ /**
+ * create openssl options config including configuration file to use
+ * @param string $keylen_curve rsa key length or elliptic curve name to use
+ * @param string $digest_alg digest algorithm
+ * @param string $x509_extensions openssl section to use
+ * @param array $extns template fragments to replace in openssl.cnf
+ * @return array needed for certificate generation, caller is responsible for removing ['filename'] after use
+ */
+ private static function _createSSLOptions($keylen_curve, $digest_alg, $x509_extensions = 'usr_cert', $extns = [])
+ {
+ // define temp filename to use for openssl.cnf and add extensions values to it
+ $configFilename = tempnam(sys_get_temp_dir(), 'ssl');
+
+ $template = file_get_contents('/usr/local/etc/ssl/opnsense.cnf');
+ foreach (array_keys($extns) as $extnTag) {
+ $template_extn = $extnTag . ' = ' . str_replace(array("\r", "\n"), '', $extns[$extnTag]);
+ // Overwrite the placeholders for this property
+ $template = str_replace('###OPNsense:' . $extnTag . '###', $template_extn, $template);
+ }
+ file_put_contents($configFilename, $template);
+
+ $args = [
+ 'config' => $configFilename,
+ 'x509_extensions' => $x509_extensions,
+ 'digest_alg' => $digest_alg,
+ 'encrypt_key' => false
+ ];
+ if (is_numeric($keylen_curve)) {
+ $args['private_key_type'] = OPENSSL_KEYTYPE_RSA;
+ $args['private_key_bits'] = (int)$keylen_curve;
+ } else {
+ $args['private_key_type'] = OPENSSL_KEYTYPE_EC;
+ $args['curve_name'] = $keylen_curve;
+ }
+
+ return $args;
+ }
+
+ /**
+ * @param array to add 'error' result to when openssl_error_string returns data
+ */
+ private static function _addSSLErrors(&$arr)
+ {
+ $data = '';
+ while ($ssl_err = openssl_error_string()) {
+ $data .= " " . $ssl_err;
+ }
+ if (!empty(trim($data))) {
+ if (!isset($arr['error'])) {
+ $arr['error'] = $data;
+ } else {
+ $arr['error'] .= ("\n" . $data);
+ }
+ }
+ }
+
/**
* Sign a certificate, when signed by a CA, make sure to serialize the config after doing so,
* it's the callers responsibility to update the serial number administration of the supplied CA.
@@ -208,39 +264,56 @@ class Store
$extns = []
) {
$old_err_level = error_reporting(0); /* prevent openssl error from going to stderr/stdout */
- // handle parameters which can only be set via the configuration file
- $config_filename = create_temp_openssl_config($extns);
- $args = [
- 'config' => $config_filename,
- 'x509_extensions' => $x509_extensions,
- 'digest_alg' => $digest_alg,
- 'encrypt_key' => false
- ];
- if (is_numeric($keylen_curve)) {
- $args['private_key_type'] = OPENSSL_KEYTYPE_RSA;
- $args['private_key_bits'] = (int)$keylen_curve;
- } else {
- $args['private_key_type'] = OPENSSL_KEYTYPE_EC;
- $args['curve_name'] = $keylen_curve;
- }
- $tmp = self::_signCert($csr, $caref, $lifetime, $args);
- if (!empty($tmp['crt'])) {
- $result = ['crt' => $tmp['crt']];
- } else {
- $result = $tmp;
- }
+ $args = self::_createSSLOptions($keylen_curve, $digest_alg, $x509_extensions, $extns);
+ $result = self::_signCert($csr, $caref, $lifetime, $args);
- /* something went wrong, return openssl error */
- if (empty($result) || !empty($result['error'])) {
- $result['error'] = $result['error'] ?? '';
- while ($ssl_err = openssl_error_string()) {
- $result['error'] .= " " . $ssl_err;
- }
- }
+ self::_addSSLErrors($result);
// remove tempfile (template)
- @unlink($config_filename);
+ @unlink($args['filename']);
+ error_reporting($old_err_level);
+ return $result;
+ }
+
+ /**
+ * re-issue a certificate, when signed by a CA, make sure to serialize the config after doing so,
+ * it's the callers responsibility to update the serial number administration of the supplied CA.
+ * A call to \OPNsense\Core\Config::getInstance()->save(); would persist the new serial.
+ *
+ * @param string $keylen_curve rsa key length or elliptic curve name to use
+ * @param int $lifetime in number of days
+ * @param array $dn subject to use
+ * @param string $prv private key
+ * @param string $digest_alg digest algorithm
+ * @param string $caref key to certificate authority
+ * @param string $x509_extensions openssl section to use
+ * @param array $extns template fragments to replace in openssl.cnf
+ * @return array containing generated certificate or returned errors
+ */
+ public static function reIssueCert(
+ $keylen_curve,
+ $lifetime,
+ $dn,
+ $prv,
+ $digest_alg,
+ $caref,
+ $x509_extensions = 'usr_cert',
+ $extns = []
+ ) {
+ $old_err_level = error_reporting(0); /* prevent openssl error from going to stderr/stdout */
+
+ $args = self::_createSSLOptions($keylen_curve, $digest_alg, $x509_extensions, $extns);
+ $csr = openssl_csr_new($dn, $prv, $args);
+ if ($csr !== false) {
+ $result = self::_signCert($csr, $caref, $lifetime, $args);
+ } else {
+ $result = [];
+ }
+ self::_addSSLErrors($result);
+
+ // remove tempfile (template)
+ @unlink($args['filename']);
error_reporting($old_err_level);
return $result;
}
@@ -270,22 +343,7 @@ class Store
) {
$result = [];
$old_err_level = error_reporting(0); /* prevent openssl error from going to stderr/stdout */
-
- // handle parameters which can only be set via the configuration file
- $config_filename = create_temp_openssl_config($extns);
- $args = [
- 'config' => $config_filename,
- 'x509_extensions' => $x509_extensions,
- 'digest_alg' => $digest_alg,
- 'encrypt_key' => false
- ];
- if (is_numeric($keylen_curve)) {
- $args['private_key_type'] = OPENSSL_KEYTYPE_RSA;
- $args['private_key_bits'] = (int)$keylen_curve;
- } else {
- $args['private_key_type'] = OPENSSL_KEYTYPE_EC;
- $args['curve_name'] = $keylen_curve;
- }
+ $args = self::_createSSLOptions($keylen_curve, $digest_alg, $x509_extensions, $extns);
// generate a new key pair
$res_key = openssl_pkey_new($args);
@@ -293,7 +351,7 @@ class Store
$res_csr = openssl_csr_new($dn, $res_key, $args);
if ($res_csr !== false) {
if ($caref !== false) {
- $tmp = self::_signCert($res_csr, $caref !== null ? $caref : $res_key, $lifetime, $args);
+ $tmp = self::_signCert($res_csr, !empty($caref) ? $caref : $res_key, $lifetime, $args);
if (!empty($tmp['crt'])) {
$result = ['caref' => $caref, 'crt' => $tmp['crt'], 'prv' => $str_key];
} else {
@@ -307,16 +365,10 @@ class Store
}
}
}
- /* something went wrong, return openssl error */
- if (empty($result) || !empty($result['error'])) {
- $result['error'] = $result['error'] ?? '';
- while ($ssl_err = openssl_error_string()) {
- $result['error'] .= " " . $ssl_err;
- }
- }
+ self::_addSSLErrors($result);
// remove tempfile (template)
- @unlink($config_filename);
+ @unlink($args['filename']);
error_reporting($old_err_level);
return $result;
}
@@ -412,26 +464,4 @@ class Store
}
return implode("\n", $chain);
}
-
- /**
- * Create a temporary config file, to help with calls that require properties that can only be set via the config file.
- *
- * @param $dn
- * @return string The name of the temporary config file.
- */
- public static function createTempOpenSSLconfig($extns = [])
- {
- // define temp filename to use for openssl.cnf and add extensions values to it
- $configFilename = tempnam(sys_get_temp_dir(), 'ssl');
-
- $template = file_get_contents('/usr/local/etc/ssl/opnsense.cnf');
-
- foreach (array_keys($extns) as $extnTag) {
- $template_extn = $extnTag . ' = ' . str_replace(array("\r", "\n"), '', $extns[$extnTag]);
- // Overwrite the placeholders for this property
- $template = str_replace('###OPNsense:' . $extnTag . '###', $template_extn, $template);
- }
- file_put_contents($configFilename, $template);
- return $configFilename;
- }
}
diff --git a/src/opnsense/mvc/app/models/OPNsense/Trust/Cert.xml b/src/opnsense/mvc/app/models/OPNsense/Trust/Cert.xml
index a05c0c618..9afa01945 100644
--- a/src/opnsense/mvc/app/models/OPNsense/Trust/Cert.xml
+++ b/src/opnsense/mvc/app/models/OPNsense/Trust/Cert.xml
@@ -24,7 +24,7 @@
Reissue and replace certificate (does not restart services)
-
+
Y
2048
@@ -38,7 +38,7 @@
Elliptic Curve secp384r1
Elliptic Curve secp521r1
-
+
Y
sha256
@@ -60,9 +60,13 @@
Certificate Authority
+
+ Y
+ 397
+
Y
- usr_cert
+ firewall
Save on this firewall
Download and do not save
@@ -85,7 +89,9 @@
NL
Y
-
+
+ /^[^\x00-\x08\x0b\x0c\x0e-\x1f\n]*$/
+
/^[^\x00-\x08\x0b\x0c\x0e-\x1f\n]*$/
diff --git a/src/opnsense/mvc/app/views/OPNsense/Trust/cert.volt b/src/opnsense/mvc/app/views/OPNsense/Trust/cert.volt
index 3a813da57..783115d66 100644
--- a/src/opnsense/mvc/app/views/OPNsense/Trust/cert.volt
+++ b/src/opnsense/mvc/app/views/OPNsense/Trust/cert.volt
@@ -115,11 +115,15 @@
target.show();
}
});
- /* expand PEM section */
+ /* expand/collapse PEM section */
if (['import', 'import_csr'].includes($(this).val())) {
if ($(".pem_section > table > tbody > tr:eq(0) > td:eq(0)").is(':hidden')) {
$(".pem_section > table > thead").click();
}
+ } else {
+ if (!$(".pem_section > table > tbody > tr:eq(0) > td:eq(0)").is(':hidden')) {
+ $(".pem_section > table > thead").click();
+ }
}
});