From 38afcf8a28b97516ba21f8fefa5057ddd34dc75c Mon Sep 17 00:00:00 2001 From: Dinesh Prasanth M K Date: Tue, 11 Jul 2017 21:53:05 -0400 Subject: Temp SSL Certificate Creation - Offline System Certificate Renewal `pki-server subsystem-cert-renew` can be used to generate a temporary SSL cert (Signed by CA) and replace the expired SSL cert in NSS DB. This helps to bring up the PKI server temporarily. The online System Certificate renewal procedure can then be used without backdating the system to update other system certificates. Ticket: https://pagure.io/dogtagpki/issue/2776 Change-Id: I411586e70f80029b76890e24425331d657ac71e9 --- base/common/python/pki/nssdb.py | 231 ++++++++++++++++----- base/server/python/pki/server/cli/subsystem.py | 265 ++++++++++++++++++++++++- 2 files changed, 443 insertions(+), 53 deletions(-) diff --git a/base/common/python/pki/nssdb.py b/base/common/python/pki/nssdb.py index cad89081e..3c0ac0682 100644 --- a/base/common/python/pki/nssdb.py +++ b/base/common/python/pki/nssdb.py @@ -1,5 +1,6 @@ # Authors: # Endi S. Dewata +# Dinesh Prasnath M K # # This program is free software; you can redistribute it and/or modify # it under the terms of the Lesser GNU General Public License as published by @@ -294,7 +295,6 @@ class NSSDatabase(object): exts = [] for generic_ext in generic_exts: - data_file = os.path.join(tmpdir, 'csr-ext-%d' % counter) with open(data_file, 'w') as f: f.write(generic_ext['data']) @@ -339,95 +339,224 @@ class NSSDatabase(object): finally: shutil.rmtree(tmpdir) - def create_self_signed_ca_cert(self, subject_dn, request_file, cert_file, - serial='1', validity=240): - + def create_cert(self, request_file, cert_file, serial, issuer=None, + key_usage_ext=None, basic_constraints_ext=None, + aki_ext=None, ski_ext=None, aia_ext=None, ext_key_usage_ext=None, + validity=None): cmd = [ 'certutil', '-C', - '-x', '-d', self.directory ] + # Check if it's self signed + if issuer: + cmd.extend(['-c', issuer]) + else: + cmd.extend('-x') + if self.token: cmd.extend(['-h', self.token]) cmd.extend([ '-f', self.password_file, - '-c', subject_dn, '-a', '-i', request_file, '-o', cert_file, - '-m', serial, - '-v', str(validity), - '--keyUsage', 'digitalSignature,nonRepudiation,certSigning,crlSigning,critical', - '-2', - '-3', - '--extSKID', - '--extAIA' + '-m', serial ]) - p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) + if validity: + cmd.extend(['-v', str(validity)]) keystroke = '' - # Is this a CA certificate [y/N]? - keystroke += 'y\n' + if aki_ext: + cmd.extend(['-3']) + + # Enter value for the authKeyID extension [y/N] + if 'auth_key_id' in aki_ext: + keystroke += 'y\n' + + # Enter value for the key identifier fields,enter to omit: + keystroke += aki_ext['auth_key_id'] + + keystroke += '\n' + + # Select one of the following general name type: + # TODO: General Name type isn't used as of now for AKI + keystroke += '0\n' + + if 'auth_cert_serial' in aki_ext: + keystroke += aki_ext['auth_cert_serial'] + + keystroke += '\n' + + # Is this a critical extension [y/N]? + if 'critical' in aki_ext and aki_ext['critical']: + keystroke += 'y' + + keystroke += '\n' + + # Key Usage Constraints + if key_usage_ext: + + cmd.extend(['--keyUsage']) + + usages = [] + for usage in key_usage_ext: + if key_usage_ext[usage]: + usages.append(usage) + + cmd.extend([','.join(usages)]) - # Enter the path length constraint, enter to skip [<0 for unlimited path]: - keystroke += '\n' + # Extended key usage + if ext_key_usage_ext: + cmd.extend(['--extKeyUsage']) + usages = [] + for usage in ext_key_usage_ext: + if ext_key_usage_ext[usage]: + usages.append(usage) - # Is this a critical extension [y/N]? - keystroke += 'y\n' + cmd.extend([','.join(usages)]) - # Enter value for the authKeyID extension [y/N]? - keystroke += 'y\n' + # Basic constraints + if basic_constraints_ext: - # TODO: generate SHA1 ID (see APolicyRule.formSHA1KeyId()) - # Enter value for the key identifier fields,enter to omit: - keystroke += '2d:7e:83:37:75:5a:fd:0e:8d:52:a3:70:16:93:36:b8:4a:d6:84:9f\n' + cmd.extend(['-2']) - # Select one of the following general name type: - keystroke += '0\n' + # Is this a CA certificate [y/N]? + if basic_constraints_ext['ca']: + keystroke += 'y' - # Enter value for the authCertSerial field, enter to omit: - keystroke += '\n' + keystroke += '\n' - # Is this a critical extension [y/N]? - keystroke += '\n' + # Enter the path length constraint, enter to skip [<0 for unlimited path]: + if basic_constraints_ext['path_length']: + keystroke += basic_constraints_ext['path_length'] - # TODO: generate SHA1 ID (see APolicyRule.formSHA1KeyId()) - # Adding Subject Key ID extension. - # Enter value for the key identifier fields,enter to omit: - keystroke += '2d:7e:83:37:75:5a:fd:0e:8d:52:a3:70:16:93:36:b8:4a:d6:84:9f\n' + keystroke += '\n' - # Is this a critical extension [y/N]? - keystroke += '\n' + # Is this a critical extension [y/N]? + if basic_constraints_ext['critical']: + keystroke += 'y' - # Enter access method type for Authority Information Access extension: - keystroke += '2\n' + keystroke += '\n' - # Select one of the following general name type: - keystroke += '7\n' + if ski_ext: + cmd.extend(['--extSKID']) - # TODO: replace with actual hostname name and port number - # Enter data: - keystroke += 'http://server.example.com:8080/ca/ocsp\n' + # Adding Subject Key ID extension. + # Enter value for the key identifier fields,enter to omit: + if 'sk_id' in ski_ext: + keystroke += ski_ext['sk_id'] - # Select one of the following general name type: - keystroke += '0\n' + keystroke += '\n' - # Add another location to the Authority Information Access extension [y/N] - keystroke += '\n' + # Is this a critical extension [y/N]? + if 'critical' in ski_ext and ski_ext['critical']: + keystroke += 'y' - # Is this a critical extension [y/N]? - keystroke += '\n' + keystroke += '\n' + + if aia_ext: + cmd.extend(['--extAIA']) + + # To ensure whether this is the first AIA being added + firstentry = True + + # Enter access method type for Authority Information Access extension: + for s in aia_ext: + if not firstentry: + keystroke += 'y\n' + + # 1. CA Issuers + if s == 'ca_issuers': + keystroke += '1' + + # 2. OCSP + if s == 'ocsp': + keystroke += '2' + keystroke += '\n' + for gn in aia_ext[s]['uri']: + # 7. URI + keystroke += '7\n' + # Enter data + keystroke += gn + '\n' + + # Any other number to finish + keystroke += '0\n' + + # One entry is done. + firstentry = False + + # Add another location to the Authority Information Access extension [y/N] + keystroke += '\n' + + # Is this a critical extension [y/N]? + if 'critical' in aia_ext and aia_ext['critical']: + keystroke += 'y' + + keystroke += '\n' + + p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) p.communicate(keystroke) rc = p.wait() + return rc + + def create_self_signed_ca_cert(self, request_file, cert_file, + serial='1', validity=240): + + # --keyUsage + key_usage_ext = { + 'digitalSignature': True, + 'nonRepudiation': True, + 'certSigning': True, + 'crlSigning': True, + 'critical': True + } + + # -2 + basic_constraints_ext = { + 'path_length': None + } + + # FIXME: do not hard-code AKI extension + # -3 + aki_ext = { + 'auth_key_id': '0x2d7e8337755afd0e8d52a370169336b84ad6849f' + } + + # FIXME: do not hard-code SKI extension + # --extSKID + ski_ext = { + 'sk_id': '0x2d7e8337755afd0e8d52a370169336b84ad6849f' + } + + # FIXME: do not hard-code AIA extension + # --extAIA + aia_ext = { + 'ocsp': { + 'uri': ['http://server.example.com:8080/ca/ocsp'] + } + + } + + rc = self.create_cert( + request_file=request_file, + cert_file=cert_file, + serial=serial, + validity=validity, + key_usage_ext=key_usage_ext, + basic_constraints_ext=basic_constraints_ext, + aki_ext=aki_ext, + ski_ext=ski_ext, + aia_ext=aia_ext) + if rc: raise Exception('Failed to generate self-signed CA certificate. RC: %d' % rc) diff --git a/base/server/python/pki/server/cli/subsystem.py b/base/server/python/pki/server/cli/subsystem.py index a9857ba5f..1381a6162 100644 --- a/base/server/python/pki/server/cli/subsystem.py +++ b/base/server/python/pki/server/cli/subsystem.py @@ -1,6 +1,7 @@ # Authors: # Endi S. Dewata # Abhijeet Kasurde +# Dinesh Prasanth M K # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -26,7 +27,10 @@ import getpass import os import subprocess import sys -from tempfile import mkstemp +import random +import tempfile +import shutil +import re import pki.cli import pki.nssdb @@ -370,6 +374,7 @@ class SubsystemCertCLI(pki.cli.CLI): self.add_module(SubsystemCertExportCLI()) self.add_module(SubsystemCertUpdateCLI()) self.add_module(SubsystemCertValidateCLI()) + self.add_module(SubsystemCertRenewCLI()) @staticmethod def print_subsystem_cert(cert, show_all=False): @@ -1006,7 +1011,7 @@ class SubsystemCertValidateCLI(pki.cli.CLI): # get internal token password and store in temporary file passwd = instance.get_token_password() - pwfile_handle, pwfile_path = mkstemp() + pwfile_handle, pwfile_path = tempfile.mkstemp() os.write(pwfile_handle, passwd) os.close(pwfile_handle) @@ -1035,3 +1040,259 @@ class SubsystemCertValidateCLI(pki.cli.CLI): finally: os.unlink(pwfile_path) + + +class SubsystemCertRenewCLI(pki.cli.CLI): + def __init__(self): + super(SubsystemCertRenewCLI, self).__init__( + 'renew', 'Renew subsystem certificate') + + def usage(self): + print('Usage: pki-server subsystem-cert-renew [OPTIONS] ') + print() + print(' -i, --instance Instance ID (default: pki-tomcat).') + print(' -v, --verbose Run in verbose mode.') + print(' --help Show help message.') + print(' --temp Create temporary certificate.') + print(' --serial Provide serial number for temp certificate.') + print() + + def execute(self, argv): + try: + opts, args = getopt.gnu_getopt(argv, 'i:v', [ + 'instance=', + 'verbose', + 'help', 'temp', 'serial=']) + + except getopt.GetoptError as e: + print('ERROR: ' + str(e)) + self.usage() + sys.exit(1) + + instance_name = 'pki-tomcat' + is_permanent_cert = True + serial = None + + for o, a in opts: + if o in ('-i', '--instance'): + instance_name = a + + elif o in ('-v', '--verbose'): + self.set_verbose(True) + + elif o == '--help': + self.usage() + sys.exit() + + elif o == '--temp': + is_permanent_cert = False + + elif o == '--serial': + serial = a + + else: + self.print_message('ERROR: unknown option ' + o) + self.usage() + sys.exit(1) + + if len(args) < 1: + print('ERROR: missing subsystem ID') + self.usage() + sys.exit(1) + + if len(args) < 2: + print('ERROR: missing cert ID') + self.usage() + sys.exit(1) + + subsystem_name = args[0] + cert_id = args[1] + + instance = pki.server.PKIInstance(instance_name) + + if not instance.is_valid(): + print('ERROR: Invalid instance %s.' % instance_name) + sys.exit(1) + + # Load the instance. Default: pki-tomcat + instance.load() + + # Get the subsystem - Eg: ca, kra, tps, tks + subsystem = instance.get_subsystem(subsystem_name) + if not subsystem: + print('ERROR: No %s subsystem in instance ' + '%s.' % (subsystem_name, instance_name)) + sys.exit(1) + + cert = subsystem.get_subsystem_cert(cert_id) + + nssdb = instance.open_nssdb() + tmpdir = tempfile.mkdtemp() + + try: + new_cert_file = os.path.join(tmpdir, cert_id + '.crt') + + # Check if the request is for permanent certificate creation + if is_permanent_cert: + # Serial number for permanent certificate must be auto-generated + if serial: + raise Exception('--serial not allowed for permanent cert') + # Fixme: Get the serial from LDAP DB (Method 3a) + else: + if not serial: + # Fixme: Get the highest serial number from NSS DB and add 1 (Method 2b) + # If admin doesn't provide a serial number, generate one + serial = str(random.randint( + int(subsystem.config.get('dbs.beginSerialNumber', '1')), + int(subsystem.config.get('dbs.endSerialNumber', '10000000')))) + + if cert_id == 'sslserver': + self.renew_ssl_cert(subsystem=subsystem, is_permanent_cert=is_permanent_cert, tmpdir=tmpdir, + new_cert_file=new_cert_file, nssdb=nssdb, serial=serial) + + elif cert_id == 'ca_ocsp_signing': + self.renew_ocsp_cert(is_permanent_cert=is_permanent_cert) + + elif cert_id == 'ca_audit_signing': + self.renew_audit_cert(is_permanent_cert=is_permanent_cert) + + elif cert_id == 'subsystem': + self.renew_subsystem_cert(is_permanent_cert=is_permanent_cert) + + else: + # renewal not yet supported + raise Exception('Renewal for %s not yet supported.' % cert_id) + + # Import cert into NSS db + if self.verbose: + print('Removing old %s certificate from NSS database.' % cert_id) + nssdb.remove_cert(cert['nickname']) + + if self.verbose: + print('Adding new %s certificate into NSS database.' % cert_id) + nssdb.add_cert( + nickname=cert['nickname'], + cert_file=new_cert_file) + + # Update CS.cfg with the new certificate + if self.verbose: + print('Updating CS.cfg') + data = nssdb.get_cert( + nickname=cert['nickname'], + output_format='base64') + cert['data'] = data + subsystem.update_subsystem_cert(cert) + subsystem.save() + + finally: + nssdb.close() + shutil.rmtree(tmpdir) + + @staticmethod + def setup_temp_renewal(subsystem, cert_id, tmpdir): + + csr_file = os.path.join(tmpdir, cert_id + '.csr') + ca_cert_file = os.path.join(tmpdir, 'ca_certificate.crt') + + # Export the CSR for the cert + cert_request = subsystem.get_subsystem_cert(cert_id).get('request', None) + if cert_request is None: + print("ERROR: Unable to find certificate request for %s" % cert_id) + sys.exit(1) + + csr_data = pki.nssdb.convert_csr(cert_request, 'base64', 'pem') + with open(csr_file, 'w') as f: + f.write(csr_data) + + # Extract SKI + # 1. Get the CA certificate + # 2. Then get the SKI from it + ca_signing_cert = subsystem.get_subsystem_cert('signing') + ca_cert_data = ca_signing_cert.get('data', None) + if ca_cert_data is None: + print("ERROR: Unable to find certificate data for CA signing certificate.") + sys.exit(1) + + ca_cert = pki.nssdb.convert_cert(ca_cert_data, 'base64', 'pem') + with open(ca_cert_file, 'w') as f: + f.write(ca_cert) + + ca_cert_retrieve_cmd = [ + 'openssl', + 'x509', + '-in', ca_cert_file, + '-noout', + '-text' + ] + + ca_cert_details = subprocess.check_output(ca_cert_retrieve_cmd) + aki = re.search(r'Subject Key Identifier.*\n.*?(.*?)\n', ca_cert_details).group(1) + + # Add 0x to represent this is a Hex + aki = '0x' + aki.strip().replace(':', '') + + return ca_signing_cert, aki, csr_file + + def renew_ssl_cert(self, subsystem, serial, tmpdir, is_permanent_cert, new_cert_file, nssdb): + if self.verbose: + print('Creating SSL server certificate.') + + if is_permanent_cert: + # TODO: Online renewal + print('SSL cert online renewal not yet supported.') + else: + # Generate temp SSL Certificate signed by CA + + ca_signing_cert, aki, csr_file = self.setup_temp_renewal( + subsystem=subsystem, tmpdir=tmpdir, cert_id='sslserver') + + # --keyUsage + key_usage_ext = { + 'digitalSignature': True, + 'nonRepudiation': True, + 'keyEncipherment': True, + 'dataEncipherment': True, + 'critical': True + } + + # -3 + aki_ext = { + 'auth_key_id': aki + } + + # --extKeyUsage + ext_key_usage_ext = { + 'serverAuth': True + } + + rc = nssdb.create_cert( + issuer=ca_signing_cert['nickname'], + request_file=csr_file, + cert_file=new_cert_file, + serial=serial, + key_usage_ext=key_usage_ext, + aki_ext=aki_ext, + ext_key_usage_ext=ext_key_usage_ext) + if rc: + raise Exception('Failed to generate CA-signed temp SSL certificate. RC: %d' % rc) + + def renew_ocsp_cert(self, is_permanent_cert): + if is_permanent_cert: + # TODO: Online renewal + raise Exception('OCSP cert online renewal not yet supported.') + else: + raise Exception('Temp certificate for OCSP is not supported.') + + def renew_subsystem_cert(self, is_permanent_cert): + if is_permanent_cert: + # TODO: Online renewal + raise Exception('Subsystem cert online renewal not yet supported.') + else: + raise Exception('Temp certificate for subsystem is not supported.') + + def renew_audit_cert(self, is_permanent_cert): + if is_permanent_cert: + # TODO: Online renewal + raise Exception('Audit signing cert online renewal not yet supported.') + else: + raise Exception('Temp certificate for audit signing is not supported.') -- cgit