From 94952145b355c1dee646705cf00e44923a683af4 Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Sat, 2 Mar 2024 17:54:16 +0100 Subject: [PATCH] System: Trust: Certificates - work in progress for https://github.com/opnsense/core/issues/7248 Implement certificate actions, further optimize certificate store to limit code duplication. --- .../OPNsense/Trust/Api/CertController.php | 86 ++++++++- .../OPNsense/Trust/forms/dialogCert.xml | 18 +- .../mvc/app/library/OPNsense/Trust/Store.php | 182 ++++++++++-------- .../mvc/app/models/OPNsense/Trust/Cert.xml | 14 +- .../mvc/app/views/OPNsense/Trust/cert.volt | 6 +- 5 files changed, 219 insertions(+), 87 deletions(-) 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(); + } } });