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 @@
+
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')])}}