From 34804b2d5f70e3f1835ab5368da589053f188ca7 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 1 Apr 2024 12:13:21 -0400 Subject: [PATCH] Provide components for cert management with modern XCC Refresh getcsr and installcert to handle latest firmware. Also add ability to have pre-existing CSR, and trust the SAN on the way through. If this becomes more properly a feature, then would likely impose a SAN on certs, similar to the SSH principals, rather than deferring to the CSR to get it right. --- confluent_server/confluent/certutil.py | 76 +++++++++++++++++++------- misc/getcsr.py | 54 ++++++++++++++---- misc/installcert.py | 15 +++++ 3 files changed, 112 insertions(+), 33 deletions(-) diff --git a/confluent_server/confluent/certutil.py b/confluent_server/confluent/certutil.py index 2e788bad..9a478787 100644 --- a/confluent_server/confluent/certutil.py +++ b/confluent_server/confluent/certutil.py @@ -206,7 +206,7 @@ def create_simple_ca(keyout, certout): finally: os.remove(tmpconfig) -def create_certificate(keyout=None, certout=None): +def create_certificate(keyout=None, certout=None, csrout=None): if not keyout: keyout, certout = get_certificate_paths() if not keyout: @@ -214,9 +214,10 @@ def create_certificate(keyout=None, certout=None): assure_tls_ca() shortname = socket.gethostname().split('.')[0] longname = shortname # socket.getfqdn() - subprocess.check_call( - ['openssl', 'ecparam', '-name', 'secp384r1', '-genkey', '-out', - keyout]) + if not csrout: + subprocess.check_call( + ['openssl', 'ecparam', '-name', 'secp384r1', '-genkey', '-out', + keyout]) san = ['IP:{0}'.format(x) for x in get_ip_addresses()] # It is incorrect to put IP addresses as DNS type. However # there exists non-compliant clients that fail with them as IP @@ -229,21 +230,34 @@ def create_certificate(keyout=None, certout=None): os.close(tmphdl) tmphdl, extconfig = tempfile.mkstemp() os.close(tmphdl) - tmphdl, csrout = tempfile.mkstemp() - os.close(tmphdl) + needcsr = False + if csrout is None: + needcsr = True + tmphdl, csrout = tempfile.mkstemp() + os.close(tmphdl) shutil.copy2(sslcfg, tmpconfig) - serialnum = '0x' + ''.join(['{:02x}'.format(x) for x in bytearray(os.urandom(20))]) try: - with open(tmpconfig, 'a') as cfgfile: - cfgfile.write('\n[SAN]\nsubjectAltName={0}'.format(san)) - with open(extconfig, 'a') as cfgfile: - cfgfile.write('\nbasicConstraints=CA:false\nsubjectAltName={0}'.format(san)) - subprocess.check_call([ - 'openssl', 'req', '-new', '-key', keyout, '-out', csrout, '-subj', - '/CN={0}'.format(longname), - '-extensions', 'SAN', '-config', tmpconfig - ]) + if needcsr: + with open(tmpconfig, 'a') as cfgfile: + cfgfile.write('\n[SAN]\nsubjectAltName={0}'.format(san)) + with open(extconfig, 'a') as cfgfile: + cfgfile.write('\nbasicConstraints=CA:false\nsubjectAltName={0}'.format(san)) + subprocess.check_call([ + 'openssl', 'req', '-new', '-key', keyout, '-out', csrout, '-subj', + '/CN={0}'.format(longname), + '-extensions', 'SAN', '-config', tmpconfig + ]) + else: + # when used manually, allow the csr SAN to stand + # may add explicit subj/SAN argument, in which case we would skip copy + with open(tmpconfig, 'a') as cfgfile: + cfgfile.write('\ncopy_extensions=copy\n') + with open(extconfig, 'a') as cfgfile: + cfgfile.write('\nbasicConstraints=CA:false\n') if os.path.exists('/etc/confluent/tls/cakey.pem'): + # simple style CA in effect, make a random serial number and + # hope for the best, and accept inability to backdate the cert + serialnum = '0x' + ''.join(['{:02x}'.format(x) for x in bytearray(os.urandom(20))]) subprocess.check_call([ 'openssl', 'x509', '-req', '-in', csrout, '-CA', '/etc/confluent/tls/cacert.pem', @@ -252,20 +266,40 @@ def create_certificate(keyout=None, certout=None): '-extfile', extconfig ]) else: + # we moved to a 'proper' CA, mainly for access to backdating + # start of certs for finicky system clocks + # this also provides a harder guarantee of serial uniqueness, but + # not of practical consequence (160 bit random value is as good as + # guaranteed unique) + # downside is certificate generation is serialized + cacfgfile = '/etc/confluent/tls/ca/openssl.cfg' + if needcsr: + tmphdl, tmpcafile = tempfile.mkstemp() + shutil.copy2(cacfgfile, tmpcafile) + os.close(tmphdl) + cacfgfile = tmpcafile + # with realcalock: # if we put it in server, we must lock it subprocess.check_call([ - 'openssl', 'ca', '-config', '/etc/confluent/tls/ca/openssl.cfg', + 'openssl', 'ca', '-config', cacfgfile, '-in', csrout, '-out', certout, '-batch', '-notext', '-startdate', '19700101010101Z', '-enddate', '21000101010101Z', '-extfile', extconfig ]) finally: os.remove(tmpconfig) - os.remove(csrout) - os.remove(extconfig) + if needcsr: + os.remove(csrout) + print(extconfig) # os.remove(extconfig) if __name__ == '__main__': + import sys outdir = os.getcwd() keyout = os.path.join(outdir, 'key.pem') - certout = os.path.join(outdir, 'cert.pem') - create_certificate(keyout, certout) + certout = os.path.join(outdir, sys.argv[2] + 'cert.pem') + csrout = None + try: + csrout = sys.argv[1] + except IndexError: + csrout = None + create_certificate(keyout, certout, csrout) diff --git a/misc/getcsr.py b/misc/getcsr.py index 253bfcd8..6f956b2d 100644 --- a/misc/getcsr.py +++ b/misc/getcsr.py @@ -12,11 +12,40 @@ ap.add_argument('--state', help='State or Province') ap.add_argument('--city', help='City or Locality') ap.add_argument('--org', help='Organization name') ap.add_argument('--name', help='Common/Host Name') +ap.add_argument('outcsr', help='CSR filename to save') args = ap.parse_args() c = cmd.Command(args.xcc, os.environ['XCCUSER'], os.environ['XCCPASS'], verifycallback=lambda x: True) -params = [ + +overview = c._do_web_request('/redfish/v1/') +cs = overview.get('CertificateService', {}).get('@odata.id', None) +if cs: + csinfo = c._do_web_request(cs) + gcsr = csinfo.get('Actions', {}).get('#CertificateService.GenerateCSR', {}).get('target', None) + if gcsr: + #https://n241-bmc/redfish/v1/Managers/1/NetworkProtocol HTTPS + #/redfish/v1/Managers/1/NetworkProtocol/HTTPS/Certificates + #/redfish/v1/CertificateService/CertificateLocations + csrargs = { + 'City': args.city, + 'State': args.state, + 'Organization': args.org, + 'Country': args.country, + 'CommonName': args.name, + 'KeyPairAlgorithm': 'TPM_ALG_ECDH', + 'KeyCurveId': 'TPM_ECC_NIST_P384', + 'CertificateCollection': { '@odata.id': '/redfish/v1/Managers/1/NetworkProtocol/HTTPS/Certificates'} + } + + csrinfo = c._do_web_request(gcsr, csrargs) + if 'CSRString' in csrinfo: + with open(args.outcsr, 'w') as csrout: + csrout.write(csrinfo['CSRString']) + sys.exit(0) + +else: + params = [ '0', # 'serviceType' args.country, args.state, @@ -32,15 +61,16 @@ params = [ '', '', '', -] -wc = c.oem.wc -rsp, status = wc.grab_json_response_with_status('/api/function', {'Sec_GenKeyAndCSR': ','.join(params)}) -rsp, status = wc.grab_json_response_with_status('/api/dataset', {'CSR_Format': '1'}) -rsp, status = wc.grab_json_response_with_status('/api/function', {'Sec_DownloadCSRANDCert': '0,4,0'}) -wc.request('GET', '/download/{0}'.format(rsp['FileName'])) -rsp = wc.getresponse() -csr = rsp.read() -if rsp.getheader('Content-Encoding', None) == 'gzip': - csr = gzip.GzipFile(fileobj=io.BytesIO(csr)).read() -print(csr) + ] + + wc = c.oem.wc + rsp, status = wc.grab_json_response_with_status('/api/function', {'Sec_GenKeyAndCSR': ','.join(params)}) + rsp, status = wc.grab_json_response_with_status('/api/dataset', {'CSR_Format': '1'}) + rsp, status = wc.grab_json_response_with_status('/api/function', {'Sec_DownloadCSRANDCert': '0,4,0'}) + wc.request('GET', '/download/{0}'.format(rsp['FileName'])) + rsp = wc.getresponse() + csr = rsp.read() + if rsp.getheader('Content-Encoding', None) == 'gzip': + csr = gzip.GzipFile(fileobj=io.BytesIO(csr)).read() + print(csr) diff --git a/misc/installcert.py b/misc/installcert.py index 9654bf54..2d53e800 100644 --- a/misc/installcert.py +++ b/misc/installcert.py @@ -8,8 +8,23 @@ ap.add_argument('xcc', help='XCC address') ap.add_argument('cert', help='Certificate in PEM format') args = ap.parse_args() +cert = open(args.cert, 'r').read() c = cmd.Command(args.xcc, os.environ['XCCUSER'], os.environ['XCCPASS'], verifycallback=lambda x: True) +overview = c._do_web_request('/redfish/v1/') +cs = overview.get('CertificateService', {}).get('@odata.id', None) +if cs: + csinfo = c._do_web_request(cs) + gcsr = csinfo.get('Actions', {}).get('#CertificateService.ReplaceCertificate', {}).get('target', None) + if gcsr: + repcertargs = { + 'CertificateUri': { '@odata.id': '/redfish/v1/Managers/1/NetworkProtocol/HTTPS/Certificates/1' }, + 'CertificateType': 'PEM', + 'CertificateString': cert } + print(repr(c._do_web_request(gcsr, repcertargs))) + sys.exit(0) + + #CertificateService.ReplaceCertificate wc = c.oem.wc cert = open(args.cert, 'rb').read() res = wc.grab_json_response_with_status('/api/function', {'Sec_ImportCert': '0,1,0,0,,{0}'.format(cert)})