From f755ba9f9182b4df0cf735d8fada15a0b7e8a29f Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 31 Oct 2025 10:46:42 -0400 Subject: [PATCH] Implement method to sign BMC certificates --- confluent_server/confluent/certutil.py | 2 +- confluent_server/confluent/core.py | 14 +++++++++++ confluent_server/confluent/messages.py | 16 +++++++++++++ .../plugins/hardwaremanagement/redfish.py | 24 +++++++++++++++++++ confluent_server/confluent/util.py | 10 +++++--- 5 files changed, 62 insertions(+), 4 deletions(-) diff --git a/confluent_server/confluent/certutil.py b/confluent_server/confluent/certutil.py index 549c2aeb..a471b2bf 100644 --- a/confluent_server/confluent/certutil.py +++ b/confluent_server/confluent/certutil.py @@ -296,7 +296,7 @@ def create_certificate(keyout=None, certout=None, csrfile=None, subj=None, san=N certout = tlsmateriallocation.get('certs', [None])[0] if not certout: certout = tlsmateriallocation.get('bundles', [None])[0] - if not keyout or not certout: + if (not keyout and not csrfile) or not certout: raise Exception('Unable to locate TLS certificate path automatically') assure_tls_ca() if not subj: diff --git a/confluent_server/confluent/core.py b/confluent_server/confluent/core.py index 78820497..b368efbe 100644 --- a/confluent_server/confluent/core.py +++ b/confluent_server/confluent/core.py @@ -300,6 +300,20 @@ def _init_core(): 'default': 'ipmi', }), }, + 'certificate': { + 'sign': PluginRoute({ + 'pluginattrs': ['hardwaremanagement.method'], + 'default': 'ipmi', + }), + 'generate_csr': PluginRoute({ + 'pluginattrs': ['hardwaremanagement.method'], + 'default': 'ipmi', + }), + 'install': PluginRoute({ + 'pluginattrs': ['hardwaremanagement.method'], + 'default': 'ipmi', + }), + }, 'certificate_authorities': PluginCollection({ 'pluginattrs': ['hardwaremanagement.method'], 'default': 'ipmi', diff --git a/confluent_server/confluent/messages.py b/confluent_server/confluent/messages.py index 50a0242e..5967291a 100644 --- a/confluent_server/confluent/messages.py +++ b/confluent_server/confluent/messages.py @@ -519,6 +519,8 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False, return InputAlertDestination(path, nodes, inputdata, multinode) elif len(path) == 3 and path[:3] == ['configuration', 'management_controller', 'certificate_authorities'] and operation not in ('retrieve', 'delete'): return InputCertificateAuthority(path, nodes, inputdata) + elif len(path) == 4 and path[:4] == ['configuration', 'management_controller', 'certificate', 'sign'] and operation not in ('retrieve', 'delete'): + return InputSigningParameters(path, inputdata, nodes, configmanager) elif path == ['identify'] and operation != 'retrieve': return InputIdentifyMessage(path, nodes, inputdata) elif path == ['events', 'hardware', 'decode']: @@ -956,6 +958,20 @@ class ConfluentInputMessage(ConfluentMessage): def is_valid_key(self, key): return key in self.valid_values +class InputSigningParameters(InputConfigChangeSet): + + def get_days(self, node): + attribs = self.get_attributes(node) + return int(attribs['days']) + + def get_added_san(self, node): + attribs = self.get_attributes(node) + addsans = [] + for subj in attribs.get('added_san', '').split(','): + if subj: + addsans.append(subj.strip()) + return addsans + class InputCertificateAuthority(ConfluentInputMessage): keyname = 'pem' diff --git a/confluent_server/confluent/plugins/hardwaremanagement/redfish.py b/confluent_server/confluent/plugins/hardwaremanagement/redfish.py index 1c7e211a..163ea351 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/redfish.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/redfish.py @@ -20,6 +20,7 @@ import confluent.messages as msg import confluent.util as util import copy import errno +from confluent import certutil import eventlet import eventlet.event import eventlet.green.threading as threading @@ -37,6 +38,7 @@ ipmicommand = eventlet.import_patched('pyghmi.redfish.command') import socket import ssl import traceback +import tempfile if not hasattr(ssl, 'SSLEOFError'): ssl.SSLEOFError = None @@ -532,6 +534,8 @@ class IpmiHandler(object): return self.handle_alerts() elif self.element[1:3] == ['management_controller', 'certificate_authorities']: return self.handle_cert_authorities() + elif self.element[1:3] == ['management_controller', 'certificate']: + return self.handle_certificate() elif self.element[1:3] == ['management_controller', 'users']: return self.handle_users() elif self.element[1:3] == ['management_controller', 'net_interfaces']: @@ -582,6 +586,26 @@ class IpmiHandler(object): self.pyghmi_event_to_confluent(event) self.output.put(msg.EventCollection((event,), name=self.node)) + def handle_certificate(self): + self.element = self.element[3:] + if len(self.element) != 1: + raise Exception('Not implemented') + if self.element[0] == 'sign' and self.op == 'update': + csr = self.ipmicmd.get_bmc_csr() + subj, san = util.get_bmc_subject_san(self.cfm, self.node, self.inputdata.get_added_san(self.node)) + with tempfile.NamedTemporaryFile() as tmpfile: + tmpfile.write(csr.encode()) + tmpfile.flush() + certfile = tempfile.NamedTemporaryFile(delete=False) + certname = certfile.name + certfile.close() + certutil.create_certificate(None, certname, tmpfile.name, subj, san, backdate=False, + days=self.inputdata.get_days(self.node)) + with open(certname, 'rb') as certf: + cert = certf.read() + os.unlink(certname) + self.ipmicmd.install_bmc_certificate(cert) + def handle_cert_authorities(self): if len(self.element) == 3: if self.op == 'read': diff --git a/confluent_server/confluent/util.py b/confluent_server/confluent/util.py index 77fcf696..78cfcdd8 100644 --- a/confluent_server/confluent/util.py +++ b/confluent_server/confluent/util.py @@ -94,13 +94,17 @@ def list_interface_indexes(): return -def get_bmc_subject_san(configmanager, nodename, addip=None): +def get_bmc_subject_san(configmanager, nodename, addnames=()): bmc_san = [] subject = '' ipas = set([]) - if addip: - ipas.add(addip) dnsnames = set([]) + for addname in addnames: + try: + addr = ipaddress.ip_address(addname) + ipas.add(addname) + except Exception: + dnsnames.add(addname) nodecfg = configmanager.get_node_attributes(nodename, ('dns.domain', 'hardwaremanagement.manager', 'hardwaremanagement.manager_tls_name')) bmcaddr = nodecfg.get(nodename, {}).get('hardwaremanagement.manager', {}).get('value', '')