From 35b69da08e5d58ac4fb2bf8e089399c285bf22b9 Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Wed, 28 Feb 2024 18:34:57 +0100 Subject: [PATCH] System: Trust: Certificates - work in progress for https://github.com/opnsense/core/issues/7248 --- .../OPNsense/Trust/forms/dialogCert.xml | 22 ++- .../mvc/app/library/OPNsense/Trust/Store.php | 155 ++++++++++++++++++ .../mvc/app/models/OPNsense/Trust/Cert.xml | 38 +++-- .../Trust/FieldTypes/CertificatesField.php | 52 +----- 4 files changed, 207 insertions(+), 60 deletions(-) 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 419d20a98..d4decdfd9 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Trust/forms/dialogCert.xml +++ b/src/opnsense/mvc/app/controllers/OPNsense/Trust/forms/dialogCert.xml @@ -6,12 +6,17 @@ header - + - cert.descr - - text + cert.cert_type + + dropdown + + + cert.private_key_location + + dropdown cert.key @@ -28,6 +33,15 @@ dropdown + + header + + + + cert.descr + + text + cert.country diff --git a/src/opnsense/mvc/app/library/OPNsense/Trust/Store.php b/src/opnsense/mvc/app/library/OPNsense/Trust/Store.php index 67028549a..73a52a6fa 100644 --- a/src/opnsense/mvc/app/library/OPNsense/Trust/Store.php +++ b/src/opnsense/mvc/app/library/OPNsense/Trust/Store.php @@ -111,6 +111,161 @@ class Store return false; } + /** + * Create a new 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 $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 createCert( + $keylen_curve, + $lifetime, + $dn, + $digest_alg, + $caref = null, + $x509_extensions = 'usr_cert', + $extns = [] + ) { + $result = []; + $ca = null; + $ca_res_crt = null; + $old_err_level = error_reporting(0); /* prevent openssl error from going to stderr/stdout */ + if ($caref !== null) { + $ca = self::getCA($caref); + if ($ca == null || empty((string)$ca->prv)) { + $result = ['error' => 'missing CA key']; + } + $ca_res_crt = openssl_x509_read(base64_decode($ca->crt)); + $ca_res_key = openssl_pkey_get_private(array(0 => base64_decode($ca->prv), 1 => "")); + if (!$ca_res_key) { + $result = ['error' => 'invalid CA']; + } + } + if (!empty($result)) { + error_reporting($old_err_level); + return $result; + } + + // 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; + } + + // generate a new key pair + $res_key = openssl_pkey_new($args); + if ($res_key !== false) { + $res_csr = openssl_csr_new($dn, $res_key, $args); + if ($res_csr !== false) { + // self sign the certificate + $res_crt = openssl_csr_sign( + $res_csr, + $ca_res_crt, + $ca_res_key ?? $res_key, + $lifetime, + $args, + $ca_serial + ); + if (openssl_pkey_export($res_key, $str_key) && openssl_x509_export($res_crt, $str_crt)) { + $result = ['caref' => $caref, 'crt' => $str_crt, 'prv' => $str_key]; + if ($ca !== null) { + $ca->serial = (int)$ca->serial + 1; + } + } + } + } + /* something went wrong, return openssl error */ + if (empty($result)){ + $result['error'] = ''; + while ($ssl_err = openssl_error_string()) { + $result['error'] .= " " . $ssl_err; + } + } + + // remove tempfile (template) + @unlink($config_filename); + error_reporting($old_err_level); + return $result; + } + + /** + * Extract certificate info into easy to use flattened chunks + * @return array|bool + */ + public static function parseX509($cert) + { + $issue_map = [ + 'L' => 'city', + 'ST' => 'state', + 'O' => 'organization', + 'C' => 'country', + 'emailAddress' => 'email', + 'CN' => 'commonname' + ]; + $altname_map = [ + 'IP Address' => 'altnames_ip', + 'DNS' => 'altnames_dns', + 'email' => 'altnames_email', + 'URI' => 'altnames_uri', + ]; + + $crt = @openssl_x509_parse($cert); + if ($crt !== null) { + $result = []; + // valid from/to and name of this cert + $result['valid_from'] = $crt['validFrom_time_t']; + $result['valid_to'] = $crt['validTo_time_t']; + $result['name'] = $crt['name']; + foreach ($issue_map as $key => $target) { + if (!empty($crt['issuer'][$key])) { + $result[$target] = $crt['issuer'][$key]; + } + } + // OCSP URI + if (!empty($crt['extensions']) && !empty($crt['extensions']['authorityInfoAccess'])) { + foreach (explode("\n", $crt['extensions']['authorityInfoAccess']) as $line) { + if (str_starts_with($line, 'OCSP - URI')) { + $result['ocsp_uri'] = explode(":", $line, 2)[1]; + } + } + } + // Altnames + if (!empty($crt['extensions']) && !empty($crt['extensions']['subjectAltName'])) { + $altnames = []; + foreach (explode(',', trim($crt['extensions']['subjectAltName'])) as $altname) { + $parts = explode(':', trim($altname), 2); + $target = $altname_map[$parts[0]]; + if (isset($altnames[$target])) { + $altnames[$target] = []; + } + $altnames[$target][] = $parts[1]; + } + foreach ($altnames as $key => $values) { + $result[$target] = implode('\n', $values); + } + } + return $result; + } + return false; + } /** * Extract certificate chain diff --git a/src/opnsense/mvc/app/models/OPNsense/Trust/Cert.xml b/src/opnsense/mvc/app/models/OPNsense/Trust/Cert.xml index 2992a1742..41a008052 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Trust/Cert.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Trust/Cert.xml @@ -25,17 +25,17 @@ Y - RSA-2048 + 2048 - RSA-512 - RSA-1024 - RSA-2048 - RSA-3072 - RSA-4096 - RSA-8192 - Elliptic Curve prime256v1 - Elliptic Curve secp384r1 - Elliptic Curve secp521r1 + RSA-512 + RSA-1024 + RSA-2048 + RSA-3072 + RSA-4096 + RSA-8192 + Elliptic Curve prime256v1 + Elliptic Curve secp384r1 + Elliptic Curve secp521r1 @@ -49,6 +49,24 @@ SHA512 + + Y + usr_cert + + Client Certificate + Server Certificate + Combined Client/Server Certificate + Certificate Authority + + + + Y + usr_cert + + Save on this firewall + Download and do not save + + /^[^\x00-\x08\x0b\x0c\x0e-\x1f\n]*$/ diff --git a/src/opnsense/mvc/app/models/OPNsense/Trust/FieldTypes/CertificatesField.php b/src/opnsense/mvc/app/models/OPNsense/Trust/FieldTypes/CertificatesField.php index d9f4f0f61..80bd8f463 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Trust/FieldTypes/CertificatesField.php +++ b/src/opnsense/mvc/app/models/OPNsense/Trust/FieldTypes/CertificatesField.php @@ -31,59 +31,19 @@ namespace OPNsense\Trust\FieldTypes; use OPNsense\Base\FieldTypes\ArrayField; use OPNsense\Base\FieldTypes\TextField; + class CertificatesField extends ArrayField { protected function actionPostLoadingEvent() { - $issue_map = [ - 'L' => 'city', - 'ST' => 'state', - 'O' => 'organization', - 'C' => 'country', - 'emailAddress' => 'email', - 'CN' => 'commonname' - ]; - $altname_map = [ - 'IP Address' => 'altnames_ip', - 'DNS' => 'altnames_dns', - 'email' => 'altnames_email', - 'URI' => 'altnames_uri', - ]; foreach ($this->internalChildnodes as $node) { $cert_data = base64_decode($node->crt); if (!empty($cert_data)) { - $crt = @openssl_x509_parse($cert_data); - if ($crt !== null) { - // valid from/to and name of this cert - $node->valid_from = $crt['validFrom_time_t']; - $node->valid_to = $crt['validTo_time_t']; - $node->name = $crt['name']; - foreach ($issue_map as $key => $target) { - if (!empty($crt['issuer'][$key])) { - $node->$target = $crt['issuer'][$key]; - } - } - // OCSP URI - if (!empty($crt['extensions']) && !empty($crt['extensions']['authorityInfoAccess'])) { - foreach (explode("\n", $crt['extensions']['authorityInfoAccess']) as $line) { - if (str_starts_with($line, 'OCSP - URI')) { - $node->ocsp_uri = explode(":", $line, 2)[1]; - } - } - } - // Altnames - if (!empty($crt['extensions']) && !empty($crt['extensions']['subjectAltName'])) { - $altnames = []; - foreach (explode(',', trim($crt['extensions']['subjectAltName'])) as $altname) { - $parts = explode(':', trim($altname), 2); - $target = $altname_map[$parts[0]]; - if (isset($altnames[$target])) { - $altnames[$target] = []; - } - $altnames[$target][] = $parts[1]; - } - foreach ($altnames as $key => $values) { - $node->$target = implode('\n', $values); + $payload = \OPNsense\Trust\Store::parseX509($cert_data); + if ($payload !== false) { + foreach ($payload as $key => $value) { + if (isset($node->$key)) { + $node->$key = $value; } } }