From f90e5445db69de5177f97048e440400a1393b424 Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Thu, 3 Apr 2025 15:08:44 +0200 Subject: [PATCH] System: Trust: Certificates - offer config directory (/usr/local/etc/ssl/ext_sources/) to store locations for certificates not managed by us, but practical to know about their existence. closes https://github.com/opnsense/core/issues/8279 This is useful for services like OPNWAF and Caddy. This commit only adds the facility and changes the admin page, the widget is left unaltered. --- src/etc/ssl/ext_sources/README | 14 +++++ .../Trust/FieldTypes/CertificatesField.php | 21 +++++++ .../mvc/app/views/OPNsense/Trust/cert.volt | 16 ++++- .../scripts/system/cert_fetch_local.py | 59 +++++++++++++++++++ .../conf/actions.d/actions_system.conf | 6 ++ 5 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 src/etc/ssl/ext_sources/README create mode 100755 src/opnsense/scripts/system/cert_fetch_local.py diff --git a/src/etc/ssl/ext_sources/README b/src/etc/ssl/ext_sources/README new file mode 100644 index 0000000000..a2c9c87fb0 --- /dev/null +++ b/src/etc/ssl/ext_sources/README @@ -0,0 +1,14 @@ +OPNsense certificates used by external applications and registered in the gui for viewing purposes only. + +In this directory you can create files with a .conf extension (e.g. myapp.conf) with the following (sample) config: + +[location] +base=/usr/local/md/domains +pattern=pubcert.pem +description=OPNWAF + + +"base" is the directory to recursively iterate, "pattern" is a regex matcher and each item returned will have a description +attached to it for the frontend. + +Files found can be identified by their id, which is an md5 hash of the contents. 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 39fbf82535..703c48140c 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Trust/FieldTypes/CertificatesField.php +++ b/src/opnsense/mvc/app/models/OPNsense/Trust/FieldTypes/CertificatesField.php @@ -28,6 +28,7 @@ namespace OPNsense\Trust\FieldTypes; +use OPNsense\Core\Backend; use OPNsense\Core\Config; use OPNsense\Base\FieldTypes\ArrayField; use OPNsense\Base\FieldTypes\ContainerField; @@ -105,6 +106,26 @@ class CertificatesField extends ArrayField return $container_node; } + /** + * @return array of externally managed certificates + */ + protected static function getStaticChildren() + { + $result = []; + $ext_data = json_decode((new Backend())->configdRun('system trust ext_sources') ?? '', true); + if (is_array($ext_data)) { + foreach ($ext_data as $data) { + $payload = \OPNsense\Trust\Store::parseX509($data['cert'] ?? ''); + if ($payload !== false && !empty($data['id'])) { + $payload['crt_payload'] = $data['cert']; + $payload['descr'] = $data['descr'] ?? ''; + $result[$data['id']] = $payload; + } + } + } + return $result; + } + protected function actionPostLoadingEvent() { $usernames = []; diff --git a/src/opnsense/mvc/app/views/OPNsense/Trust/cert.volt b/src/opnsense/mvc/app/views/OPNsense/Trust/cert.volt index f3038b5d98..33a84bf5c5 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Trust/cert.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Trust/cert.volt @@ -48,7 +48,9 @@ }, formatters: { in_use: function (column, row) { - if (row.in_use === '1') { + if (!row.uuid.includes('-')) { + return ""; + } else if (row.in_use === '1') { return ""; } else if (row.is_user === '1') { return ""; @@ -148,7 +150,17 @@ } }); grid_cert.on("loaded.rs.jquery.bootgrid", function (e){ - // reload categories before grid load + /* should probably live in the "commands" section to optionally render items */ + grid_cert.find('tr').each(function(){ + let tr = $(this); + if (tr.data('row-id') !== undefined && !tr.data('row-id').includes('-')) { + tr.find('button.command-edit').hide(); + tr.find('button.command-copy').hide(); + tr.find('button.command-delete').hide(); + } + }); + + // reload categories after grid load if ($("#ca_filter > option").length == 0) { ajaxGet('/api/trust/cert/ca_list', {}, function(data, status){ if (data.rows !== undefined) { diff --git a/src/opnsense/scripts/system/cert_fetch_local.py b/src/opnsense/scripts/system/cert_fetch_local.py new file mode 100755 index 0000000000..cadbba714d --- /dev/null +++ b/src/opnsense/scripts/system/cert_fetch_local.py @@ -0,0 +1,59 @@ +#!/usr/local/bin/python3 +""" + Copyright (c) 2025 Ad Schellevis + 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. + + --------------------------------------------------------------------------------------------------- + fetch a pluggable set of locally managed certificates which are not available in the configuration +""" +import glob +import hashlib +import os +import re +import ujson +from configparser import ConfigParser + + +if __name__ == '__main__': + result = [] + for conffile in glob.glob("/usr/local/etc/ssl/ext_sources/*.conf"): + cnf = ConfigParser() + cnf.read(conffile) + if cnf.has_section('location') and cnf.has_option('location', 'base') and cnf.has_option('location', 'pattern'): + if cnf.has_option('location', 'description'): + loc_descr = cnf.get('location', 'description') + else: + loc_descr = os.path.basename(conffile)[0:-5] + match_pattern = re.compile(cnf.get('location', 'pattern')) + for root, dirs, files in os.walk(cnf.get('location', 'base')): + for filename in files: + full_path = "%s/%s" % (root, filename) + if match_pattern.match(filename) and os.path.getsize(full_path) < 1024*1024: + payload = open(full_path).read() + result.append({ + 'cert': payload, + 'descr': loc_descr, + 'id': hashlib.md5(payload.encode()).hexdigest() + }) + print(ujson.dumps(result)) \ No newline at end of file diff --git a/src/opnsense/service/conf/actions.d/actions_system.conf b/src/opnsense/service/conf/actions.d/actions_system.conf index 61638f7a7c..bf062736d9 100644 --- a/src/opnsense/service/conf/actions.d/actions_system.conf +++ b/src/opnsense/service/conf/actions.d/actions_system.conf @@ -196,6 +196,12 @@ command:/usr/local/sbin/pluginctl -c crl type:script message: trigger CRL changed event +[trust.ext_sources] +command:/usr/local/opnsense/scripts/system/cert_fetch_local.py +type:script_output +message: fetch list of externally managed certificates +cache_ttl: 60 + [cpu.stream] command:/usr/local/opnsense/scripts/system/cpu.py parameters:--interval %s