From 7cb95beef7dd79de7901fa0b655ba549570c44ef Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Thu, 7 Mar 2024 19:12:38 +0100 Subject: [PATCH] System: Trust: Authorities - work in progress for https://github.com/opnsense/core/issues/7248 * add boilerplate code (more or less the same as Certificates) --- .../OPNsense/Trust/Api/CaController.php | 196 +++++++++++++++ .../OPNsense/Trust/CaController.php | 45 ++++ .../OPNsense/Trust/forms/dialogCa.xml | 123 ++++++++++ .../mvc/app/models/OPNsense/Trust/Ca.php | 42 ++++ .../mvc/app/models/OPNsense/Trust/Ca.xml | 87 +++++++ .../OPNsense/Trust/FieldTypes/CAsField.php | 119 +++++++++ .../Trust/FieldTypes/CertificatesField.php | 12 +- .../mvc/app/views/OPNsense/Trust/ca.volt | 228 ++++++++++++++++++ 8 files changed, 851 insertions(+), 1 deletion(-) create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/Trust/Api/CaController.php create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/Trust/CaController.php create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/Trust/forms/dialogCa.xml create mode 100644 src/opnsense/mvc/app/models/OPNsense/Trust/Ca.php create mode 100644 src/opnsense/mvc/app/models/OPNsense/Trust/Ca.xml create mode 100644 src/opnsense/mvc/app/models/OPNsense/Trust/FieldTypes/CAsField.php create mode 100644 src/opnsense/mvc/app/views/OPNsense/Trust/ca.volt diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Trust/Api/CaController.php b/src/opnsense/mvc/app/controllers/OPNsense/Trust/Api/CaController.php new file mode 100644 index 000000000..9b47ffb7f --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Trust/Api/CaController.php @@ -0,0 +1,196 @@ +refid)) { + $node->refid = uniqid(); + } + $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': + break; + case 'existing': + 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': + break; + case 'ocsp': + break; + } + if ($error !== false) { + throw new UserException($error, "Certificate error"); + } + } + + public function searchAction() + { + $carefs = $this->request->get('carefs'); + $filter_funct = function ($record) use ($carefs) { + return empty($carefs) || array_intersect(explode(',', $record->caref), $carefs); + }; + return $this->searchBase( + 'ca', + ['refid', 'descr', 'caref', 'name', 'valid_from', 'valid_to'], + null, + $filter_funct + ); + } + + public function getAction($uuid = null) + { + return $this->getBase('ca', 'ca', $uuid); + } + + public function addAction() + { + return $this->addBase('ca', 'ca'); + } + + public function setAction($uuid = null) + { + return $this->setBase('ca', 'ca', $uuid); + } + + public function delAction($uuid) + { + if ($this->request->isPost() && !empty($uuid)) { + $node = $this->getModel()->getNodeByReference('ca.' . $uuid); + if ($node !== null) { + $this->checkAndThrowValueInUse((string)$node->refid, false, false, ['ca']); + } + return $this->delBase('ca', $uuid); + } + return ['status' => 'failed']; + } + + public function toggleAction($uuid, $enabled = null) + { + return $this->toggleBase('ca', $uuid, $enabled); + } + + public function caInfoAction($caref) + { + if ($this->request->isGet()) { + $ca = CertStore::getCACertificate($caref); + if ($ca) { + $payload = CertStore::parseX509($ca['cert']); + if ($payload) { + return $payload; + } + } + } + return []; + } + + public function rawDumpAction($uuid) + { + $payload = $this->getBase('ca', 'ca', $uuid); + if (!empty($payload['ca'])) { + if (!empty($payload['ca']['crt_payload'])) { + return CertStore::dumpX509($payload['ca']['crt_payload']); + } + } + return []; + } + + public function caListAction() + { + $result = []; + if ($this->request->isGet()) { + $result['rows'] = []; + if (isset(Config::getInstance()->object()->ca)) { + foreach (Config::getInstance()->object()->ca as $cert) { + if (isset($cert->refid)) { + $result['rows'][] = [ + 'caref' => (string)$cert->refid, + 'descr' => (string)$cert->descr + ]; + } + } + } + $result['count'] = count($result['rows']); + } + return $result; + } + + /** + * generate file download content + * @param string $uuid certificate reference + * @param string $type one of crt/prv/pkcs12, + * $_POST['password'] my contain an optional password for the pkcs12 format + * @return array + */ + public function generateFileAction($uuid = null, $type = 'crt') + { + $result = ['status' => 'failed']; + if ($this->request->isPost() && !empty($uuid)) { + $node = $this->getModel()->getNodeByReference('ca.' . $uuid); + if ($node === null || empty((string)$node->crt_payload)) { + $result['error'] = gettext('Misssing certificate'); + } elseif ($type == 'crt') { + $result['status'] = 'ok'; + $result['payload'] = (string)$node->crt_payload; + } elseif ($type == 'prv') { + $result['status'] = 'ok'; + $result['payload'] = (string)$node->prv_payload; + } + } + return $result; + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Trust/CaController.php b/src/opnsense/mvc/app/controllers/OPNsense/Trust/CaController.php new file mode 100644 index 000000000..bb358fcb3 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Trust/CaController.php @@ -0,0 +1,45 @@ +view->formDialogEditCert = $this->getForm("dialogCa"); + $this->view->pick('OPNsense/Trust/ca'); + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Trust/forms/dialogCa.xml b/src/opnsense/mvc/app/controllers/OPNsense/Trust/forms/dialogCa.xml new file mode 100644 index 000000000..8b637567e --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Trust/forms/dialogCa.xml @@ -0,0 +1,123 @@ +
+ + ca.action + + dropdown + + + header + + + + + ca.cert_type + + dropdown + + + + ca.private_key_location + + + dropdown + + + ca.key_type + + dropdown + + + ca.digest + + dropdown + + + ca.caref + + + dropdown + + + ca.lifetime + + text + + + header + + + + + ca.descr + + text + + + ca.country + + dropdown + + + ca.state + + text + + + ca.city + + text + + + ca.organization + + text + + + ca.organizationalunit + + text + + + ca.email + + text + + + ca.commonname + + text + + + ca.ocsp_uri + + text + + + + ca.altnames_email + + textbox + + + header + + true + + + + ca.crt_payload + + textbox + + + ca.prv_payload + + textbox + + + ca.csr_payload + + textbox + + +
diff --git a/src/opnsense/mvc/app/models/OPNsense/Trust/Ca.php b/src/opnsense/mvc/app/models/OPNsense/Trust/Ca.php new file mode 100644 index 000000000..f94940c80 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Trust/Ca.php @@ -0,0 +1,42 @@ + + /ca+ + 1.0.0 + + + + + + + + + ca + self-signed + Please select a valid certificate from the list + + + internal + Y + + Import an existing Certificate Authority + Create an internal Certificate Authority + Create an OCSP signing certificate + + + + Y + 2048 + + RSA-512 + RSA-1024 + RSA-2048 + RSA-3072 + RSA-4096 + RSA-8192 + Elliptic Curve prime256v1 + Elliptic Curve secp384r1 + Elliptic Curve secp521r1 + + + + Y + sha256 + + SHA1 + SHA224 + SHA256 + SHA384 + SHA512 + + + + Y + 397 + + + /^[^\x00-\x08\x0b\x0c\x0e-\x1f\n]*$/ + + + /^[^\x00-\x08\x0b\x0c\x0e-\x1f\n]*$/ + + + /^[^\x00-\x08\x0b\x0c\x0e-\x1f\n]*$/ + + + /^[^\x00-\x08\x0b\x0c\x0e-\x1f\n]*$/ + + + /^[^\x00-\x08\x0b\x0c\x0e-\x1f\n]*$/ + NL + Y + + + /^[^\x00-\x08\x0b\x0c\x0e-\x1f\n]*$/ + + + /^[^\x00-\x08\x0b\x0c\x0e-\x1f\n]*$/ + + + + + + + + + + + diff --git a/src/opnsense/mvc/app/models/OPNsense/Trust/FieldTypes/CAsField.php b/src/opnsense/mvc/app/models/OPNsense/Trust/FieldTypes/CAsField.php new file mode 100644 index 000000000..545b20142 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Trust/FieldTypes/CAsField.php @@ -0,0 +1,119 @@ + 'countryName', + 'state' => 'stateOrProvinceName', + 'city' => 'localityName', + 'organization' => 'organizationName', + 'organizationalunit' => 'organizationalUnitName', + 'email' => 'emailAddress', + 'commonname' => 'commonName', + ] as $source => $target + ) { + if (!empty((string)$this->$source)) { + $dn[$target] = (string)$this->$source; + } + } + + return $dn; + } +} + +/** + * Class CAsField + * @package OPNsense\Trust\FieldTypes + */ +class CAsField extends ArrayField +{ + /** + * @inheritDoc + */ + public function newContainerField($ref, $tagname) + { + $container_node = new CaContainerField($ref, $tagname); + $pmodel = $this->getParentModel(); + $container_node->setParentModel($pmodel); + return $container_node; + } + + protected function actionPostLoadingEvent() + { + foreach ($this->internalChildnodes as $node) { + $node->crt_payload = !empty((string)$node->crt) ? (string)base64_decode($node->crt) : ''; + $payload = false; + if (!empty((string)$node->crt_payload)) { + $payload = \OPNsense\Trust\Store::parseX509($node->crt_payload); + } + if ($payload !== false) { + $countries = []; + foreach ($payload as $key => $value) { + if (isset($node->$key)) { + /* prevent injection of invalid countries which trip migrations */ + if ($key == 'country') { + if (empty($countries)) { + $countries = array_keys($node->$key->getNodeData()); + } + if (in_array($value, $countries)) { + $node->$key = $value; + } + } else { + $node->$key = $value; + } + } + } + } + $node->prv_payload = !empty((string)$node->prv) ? (string)base64_decode($node->prv) : ''; + + if (!empty((string)$node->crt_payload)) { + $node->action = 'existing'; + } + } + return parent::actionPostLoadingEvent(); + } +} 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 ec4806b7b..aab2a332b 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Trust/FieldTypes/CertificatesField.php +++ b/src/opnsense/mvc/app/models/OPNsense/Trust/FieldTypes/CertificatesField.php @@ -119,7 +119,17 @@ class CertificatesField extends ArrayField if ($payload !== false) { foreach ($payload as $key => $value) { if (isset($node->$key)) { - $node->$key = $value; + /* prevent injection of invalid countries which trip migrations */ + if ($key == 'country') { + if (empty($countries)) { + $countries = array_keys($node->$key->getNodeData()); + } + if (in_array($value, $countries)) { + $node->$key = $value; + } + } else { + $node->$key = $value; + } } } } diff --git a/src/opnsense/mvc/app/views/OPNsense/Trust/ca.volt b/src/opnsense/mvc/app/views/OPNsense/Trust/ca.volt new file mode 100644 index 000000000..fc6a21fba --- /dev/null +++ b/src/opnsense/mvc/app/views/OPNsense/Trust/ca.volt @@ -0,0 +1,228 @@ +{# + # Copyright (c) 2024 Deciso B.V. + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without modification, + # are permitted provided that the following conditions are met: + # + # 1. Redistributions of source code must retain the above copyright notice, + # this list of conditions and the following disclaimer. + # + # 2. Redistributions in binary form must reproduce the above copyright notice, + # this list of conditions and the following disclaimer in the documentation + # and/or other materials provided with the distribution. + # + # THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + # AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + # POSSIBILITY OF SUCH DAMAGE. + #} + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Description') }}{{ lang._('Issuer') }}{{ lang._('Name') }}{{ lang._('Valid from') }}{{ lang._('Valid to') }}{{ lang._('Commands') }}
+ + +
+
+ + {{ partial("layout_partials/base_dialog",['fields':formDialogEditCert,'id':'DialogCert','label':lang._('Edit Certificate')])}}