From 3aa0731fc660ea3d111a44926ab5dea71dc510e7 Mon Sep 17 00:00:00 2001 From: Jan Cholasta Date: Sep 30 2014 06:50:47 +0000 Subject: External CA installer options usability fixes The --external_cert_file and --external_ca_file options of ipa-server-install and ipa-ca-install have been replaced by --external-cert-file option which accepts multiple files. The files are accepted in PEM and DER certificate and PKCS#7 certificate chain formats. https://fedorahosted.org/freeipa/ticket/4480 Reviewed-By: Petr Viktorin --- diff --git a/install/tools/ipa-ca-install b/install/tools/ipa-ca-install index 475794b..e54af2f 100755 --- a/install/tools/ipa-ca-install +++ b/install/tools/ipa-ca-install @@ -26,9 +26,10 @@ from ipapython import ipautil from ipaserver.install import installutils from ipaserver.install import certs -from ipaserver.install.installutils import ( - ReplicaConfig, private_ccache, create_replica_config, - validate_external_cert) +from ipaserver.install.installutils import (HostnameLocalhost, ReplicaConfig, + expand_replica_info, read_replica_info, get_host_name, BadHostError, + private_ccache, read_replica_info_dogtag_port, load_external_cert, + create_replica_config, validate_external_cert) from ipaserver.install import dsinstance, cainstance, bindinstance from ipaserver.install.replication import replica_conn_check from ipapython import version @@ -65,10 +66,9 @@ def parse_options(): default=False, help="unattended installation never prompts the user") parser.add_option("--external-ca", dest="external_ca", action="store_true", default=False, help="Generate a CSR to be signed by an external CA") - parser.add_option("--external_cert_file", dest="external_cert_file", - help="PEM file containing a certificate signed by the external CA") - parser.add_option("--external_ca_file", dest="external_ca_file", - help="PEM file containing the external CA chain") + parser.add_option("--external-cert-file", dest="external_cert_files", + action="append", metavar="FILE", + help="File containing the IPA CA certificate and the external CA certificate chain") options, args = parser.parse_args() safe_options = parser.get_safe_opts(options) @@ -83,12 +83,9 @@ def parse_options(): filename = None if options.external_ca: - if options.external_cert_file: - parser.error("You cannot specify --external_cert_file " + if options.external_cert_files: + parser.error("You cannot specify --external-cert-file " "together with --external-ca") - if options.external_ca_file: - parser.error("You cannot specify --external_ca_file together " - "with --external-ca") return safe_options, options, filename @@ -242,23 +239,19 @@ def install_master(safe_options, options): if options.external_ca: if cainstance.is_step_one_done(): print ("CA is already installed.\nRun the installer with " - "--external-cert-file and --external-ca-file.") + "--external-cert-file.") sys.exit(1) - elif options.external_cert_file: + elif options.external_cert_files: if not cainstance.is_step_one_done(): print ("CA is not installed yet. To install with an external CA " "is a two-stage process.\nFirst run the installer with " "--external-ca.") sys.exit(1) - try: - validate_external_cert(options.external_cert_file, - options.external_ca_file, subject_base) - except ValueError, e: - print e - sys.exit(1) + external_cert_file, external_ca_file = load_external_cert( + options.external_cert_files, subject_base) - if options.external_cert_file: + if options.external_cert_files: external = 2 elif options.external_ca: external = 1 @@ -308,8 +301,8 @@ def install_master(safe_options, options): else: ca.configure_instance(host_name, domain_name, dm_password, dm_password, - cert_file=options.external_cert_file, - cert_chain_file=options.external_ca_file, + cert_file=external_cert_file.name, + cert_chain_file=external_ca_file.name, subject_base=subject_base) ca.stop(ca.dogtag_constants.PKI_INSTANCE_NAME) diff --git a/install/tools/ipa-server-install b/install/tools/ipa-server-install index e73a098..6988b10 100755 --- a/install/tools/ipa-server-install +++ b/install/tools/ipa-server-install @@ -38,7 +38,7 @@ import nss.error import base64 import pwd import textwrap -from optparse import OptionGroup, OptionValueError +from optparse import OptionGroup, OptionValueError, SUPPRESS_HELP try: from ipaserver.install import adtrustinstance @@ -204,10 +204,15 @@ def parse_options(): cert_group = OptionGroup(parser, "certificate system options") cert_group.add_option("", "--external-ca", dest="external_ca", action="store_true", default=False, help="Generate a CSR for the IPA CA certificate to be signed by an external CA") - cert_group.add_option("", "--external_cert_file", dest="external_cert_file", - help="File containing the IPA CA certificate signed by the external CA in PEM format") - cert_group.add_option("", "--external_ca_file", dest="external_ca_file", - help="File containing the external CA certificate chain in PEM format") + cert_group.add_option("--external-cert-file", dest="external_cert_files", + action="append", metavar="FILE", + help="File containing the IPA CA certificate and the external CA certificate chain") + cert_group.add_option("--external_cert_file", dest="external_cert_files", + action="append", + help=SUPPRESS_HELP) + cert_group.add_option("--external_ca_file", dest="external_cert_files", + action="append", + help=SUPPRESS_HELP) cert_group.add_option("--no-pkinit", dest="setup_pkinit", action="store_false", default=True, help="disables pkinit setup steps") cert_group.add_option("--dirsrv_pkcs12", dest="dirsrv_pkcs12", @@ -321,25 +326,19 @@ def parse_options(): if options.pkinit_pkcs12 and options.pkinit_pin is None: parser.error("You must specify --pkinit_pin with --pkinit_pkcs12") - if (options.external_cert_file or options.external_ca_file) and options.dirsrv_pkcs12: - parser.error( - "PKCS#12 options cannot be used with the external CA options.") + if options.external_cert_files and options.dirsrv_pkcs12: + parser.error("Service certificate file options cannot be used with " + "the external CA options.") if options.external_ca: - if options.external_cert_file: - parser.error("You cannot specify --external_cert_file together with --external-ca") - if options.external_ca_file: - parser.error("You cannot specify --external_ca_file together with --external-ca") + if options.external_cert_files: + parser.error("You cannot specify --external-cert-file " + "together with --external-ca") if options.dirsrv_pkcs12: parser.error("You cannot specify PKCS#12 options together with --external-ca") - if ((options.external_cert_file and not options.external_ca_file) or - (not options.external_cert_file and options.external_ca_file)): - parser.error("if either external CA option is used, both are required.") - - if (options.external_ca_file and not os.path.isabs(options.external_ca_file)): - parser.error("--external-ca-file must use an absolute path") - if (options.external_cert_file and not os.path.isabs(options.external_cert_file)): + if (options.external_cert_files and + any(not os.path.isabs(path) for path in options.external_cert_files)): parser.error("--external-cert-file must use an absolute path") if options.idmax == 0: @@ -393,11 +392,10 @@ def read_cache(dm_password): shutil.rmtree(top_dir) # These are the only ones that may be overridden - for opt in ('external_ca_file', 'external_cert_file'): - try: - del optdict[opt] - except KeyError: - pass + try: + del optdict['external_cert_files'] + except KeyError: + pass return optdict @@ -636,7 +634,7 @@ def main(): else: standard_logging_setup(paths.IPASERVER_INSTALL_LOG, debug=options.debug) print "\nThe log file for this installation can be found in /var/log/ipaserver-install.log" - if not options.external_ca and not options.external_cert_file and is_ipa_configured(): + if not options.external_ca and not options.external_cert_files and is_ipa_configured(): installation_cleanup = False sys.exit("IPA server is already configured on this system.\n" + "If you want to reinstall the IPA server, please uninstall " + @@ -729,14 +727,14 @@ def main(): if options.external_ca: if cainstance.is_step_one_done(): print ("CA is already installed.\nRun the installer with " - "--external_cert_file and --external_ca_file.") + "--external-cert-file.") sys.exit(1) if ipautil.file_exists(paths.ROOT_IPA_CSR): print ("CA CSR file %s already exists.\nIn order to continue " "remove the file and run the installer again." % paths.ROOT_IPA_CSR) sys.exit(1) - elif options.external_cert_file: + elif options.external_cert_files: if not cainstance.is_step_one_done(): # This can happen if someone passes external_ca_file without # already having done the first stage of the CA install. @@ -758,13 +756,9 @@ def main(): except Exception, e: sys.exit("Cannot process the cache file: %s" % str(e)) - if options.external_cert_file: - try: - validate_external_cert(options.external_cert_file, - options.external_ca_file, options.subject) - except ValueError, e: - print e - sys.exit(1) + if options.external_cert_files: + external_cert_file, external_ca_file = load_external_cert( + options.external_cert_files, options.subject) # We only set up the CA if the PKCS#12 options are not given. if options.dirsrv_pkcs12: @@ -779,7 +773,7 @@ def main(): # Figure out what external CA step we're in. See cainstance.py for more # info on the 3 states. - if options.external_cert_file: + if options.external_cert_files: external = 2 elif options.external_ca: external = 1 @@ -1119,8 +1113,8 @@ def main(): # stage 2 of external CA installation ca.configure_instance(host_name, domain_name, dm_password, dm_password, - cert_file=options.external_cert_file, - cert_chain_file=options.external_ca_file, + cert_file=external_cert_file.name, + cert_chain_file=external_ca_file.name, subject_base=options.subject, ca_signing_algorithm=options.ca_signing_algorithm) diff --git a/install/tools/man/ipa-ca-install.1 b/install/tools/man/ipa-ca-install.1 index 2e0b079..8f7201c 100644 --- a/install/tools/man/ipa-ca-install.1 +++ b/install/tools/man/ipa-ca-install.1 @@ -37,6 +37,9 @@ Directory Manager (existing master) password \fB\-w\fR \fIADMIN_PASSWORD\fR, \fB\-\-admin\-password\fR=\fIADMIN_PASSWORD\fR Admin user Kerberos password used for connection check .TP +\fB\-\-external\-cert\-file\fR=\fIFILE\fR +File containing the IPA CA certificate and the external CA certificate chain. The file is accepted in PEM and DER certificate and PKCS#7 certificate chain formats. This option may be used multiple times. +.TP \fB\-\-no\-host\-dns\fR Do not use DNS for hostname lookup during installation .TP diff --git a/install/tools/man/ipa-cacert-manage.1 b/install/tools/man/ipa-cacert-manage.1 index 3006be7..1f37788 100644 --- a/install/tools/man/ipa-cacert-manage.1 +++ b/install/tools/man/ipa-cacert-manage.1 @@ -56,10 +56,7 @@ Sign the renewed certificate by itself. Sign the renewed certificate by external CA. .TP \fB\-\-external\-cert\-file\fR=\fIFILE\fR -PEM file containing a certificate signed by the external CA. Must be given with \-\-external\-ca\-file. -.TP -\fB\-\-external\-ca\-file\fR=\fIFILE\fR -PEM file containing the external CA chain. +File containing the IPA CA certificate and the external CA certificate chain. The file is accepted in PEM and DER certificate and PKCS#7 certificate chain formats. This option may be used multiple times. .TP \fB\-n\fR \fINICKNAME\fR, \fB\-\-nickname\fR=\fINICKNAME\fR Nickname for the certificate. diff --git a/install/tools/man/ipa-server-install.1 b/install/tools/man/ipa-server-install.1 index ecea26d..92d9ec8 100644 --- a/install/tools/man/ipa-server-install.1 +++ b/install/tools/man/ipa-server-install.1 @@ -87,15 +87,8 @@ An unattended installation that will never prompt for user input \fB\-\-external\-ca\fR Generate a CSR for the IPA CA certificate to be signed by an external CA. .TP -\fB\-\-external_cert_file\fR=\fIFILE\fR -File containing the IPA CA certificate signed by the external CA in PEM format. Must be given with \-\-external_ca_file. -.TP -\fB\-\-external_ca_file\fR=\fIFILE\fR -File containing the external CA certificate chain in PEM format. Must be given with \-\-external_cert_file. - -If the CA certificate chain is in PKCS#7 format you can convert it to PEM using: - - openssl pkcs7 -in PKCS7_FILE -print_certs -out PEM_FILE +\fB\-\-external\-cert\-file\fR=\fIFILE\fR +File containing the IPA CA certificate and the external CA certificate chain. The file is accepted in PEM and DER certificate and PKCS#7 certificate chain formats. This option may be used multiple times. .TP \fB\-\-no\-pkinit\fR Disables pkinit setup steps diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py index c26046c..26c6037 100644 --- a/ipaserver/install/cainstance.py +++ b/ipaserver/install/cainstance.py @@ -585,7 +585,7 @@ class CAInstance(DogtagInstance): if self.external == 1: print "The next step is to get %s signed by your CA and re-run %s as:" % (self.csr_file, sys.argv[0]) - print "%s --external_cert_file=/path/to/signed_certificate --external_ca_file=/path/to/external_ca_certificate" % sys.argv[0] + print "%s --external-cert-file=/path/to/signed_certificate --external-cert-file=/path/to/external_ca_certificate" % sys.argv[0] sys.exit(0) else: shutil.move(paths.CA_BACKUP_KEYS_P12, @@ -726,7 +726,7 @@ class CAInstance(DogtagInstance): if self.external == 1: print "The next step is to get %s signed by your CA and re-run %s as:" % (self.csr_file, sys.argv[0]) - print "%s --external_cert_file=/path/to/signed_certificate --external_ca_file=/path/to/external_ca_certificate" % sys.argv[0] + print "%s --external-cert-file=/path/to/signed_certificate --external-cert-file=/path/to/external_ca_certificate" % sys.argv[0] sys.exit(0) # pkisilent makes a copy of the CA PKCS#12 file for us but gives diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py index c8e1a8d..395023f 100644 --- a/ipaserver/install/installutils.py +++ b/ipaserver/install/installutils.py @@ -942,52 +942,77 @@ def check_entropy(): except ValueError as e: root_logger.debug("Invalid value in %s %s", paths.ENTROPY_AVAIL, e) -def validate_external_cert(cert_file, ca_file, subject_base): - extcert = None - try: - extcert = x509.load_certificate_from_file(cert_file) - certsubject = DN(str(extcert.subject)) - certissuer = DN(str(extcert.issuer)) - except IOError, e: - raise ValueError("Can't load the PEM certificate: %s." % e) - except (TypeError, NSPRError): - raise ValueError( - "'%s' is not a valid PEM-encoded certificate." % cert_file) - finally: - del extcert +def load_external_cert(files, subject_base): + """ + Load and verify external CA certificate chain from multiple files. - wantsubject = DN(('CN', 'Certificate Authority'), subject_base) - if certsubject != wantsubject: - raise ValueError( - "Subject of the external certificate is not correct (got %s, " - "expected %s)." % (certsubject, wantsubject)) + The files are accepted in PEM and DER certificate and PKCS#7 certificate + chain formats. - extchain = None - try: - extchain = x509.load_certificate_list_from_file(ca_file) - certdict = dict((DN(str(cert.subject)), DN(str(cert.issuer))) - for cert in extchain) - except IOError, e: - raise ValueError("Can't load the external CA chain: %s." % e) - except (TypeError, NSPRError): - raise ValueError( - "'%s' is not a valid PEM-encoded certificate chain." % ca_file) - finally: - del extchain - - if certissuer not in certdict: - raise ValueError( - "The external certificate is not signed by the external CA " - "(unknown issuer %s)." % certissuer) + :param files: Names of files to import + :param subject_base: Subject name base for IPA certificates + :returns: Temporary file with the IPA CA certificate and temporary file + with the external CA certificate chain + """ + with certs.NSSDatabase() as nssdb: + db_password = ipautil.ipa_generate_password() + db_pwdfile = ipautil.write_tmp_file(db_password) + nssdb.create_db(db_pwdfile.name) - while certsubject != certissuer: - certsubject = certissuer try: - certissuer = certdict[certsubject] - except KeyError: - raise ValueError( - "The external CA chain is incomplete (%s is missing from the " - "chain)." % certsubject) + nssdb.import_files(files, db_pwdfile.name) + except RuntimeError as e: + raise ScriptError(str(e)) + + ca_subject = DN(('CN', 'Certificate Authority'), subject_base) + ca_nickname = None + cache = {} + for nickname, trust_flags in nssdb.list_certs(): + cert = nssdb.get_cert(nickname, pem=True) + + nss_cert = x509.load_certificate(cert) + subject = DN(str(nss_cert.subject)) + issuer = DN(str(nss_cert.issuer)) + del nss_cert + + cache[nickname] = (cert, subject, issuer) + if subject == ca_subject: + ca_nickname = nickname + nssdb.trust_root_cert(nickname) + + if ca_nickname is None: + raise ScriptError( + "IPA CA certificate not found in %s" % (", ".join(files))) + + trust_chain = reversed(nssdb.get_trust_chain(ca_nickname)) + ca_cert_chain = [] + for nickname in trust_chain: + cert, subject, issuer = cache[nickname] + ca_cert_chain.append(cert) + if subject == issuer: + break + else: + raise ScriptError( + "CA certificate chain in %s is incomplete" % + (", ".join(files))) + + for nickname in trust_chain: + try: + nssdb.verify_ca_cert_validity(nickname) + except ValueError, e: + raise ScriptError( + "CA certificate %s in %s is not valid: %s" % + (subject, ", ".join(files), e)) + + cert_file = tempfile.NamedTemporaryFile() + cert_file.write(ca_cert_chain[0] + '\n') + cert_file.flush() + + ca_file = tempfile.NamedTemporaryFile() + ca_file.write('\n'.join(ca_cert_chain[1:]) + '\n') + ca_file.flush() + + return cert_file, ca_file def create_system_user(name, group, homedir, shell): diff --git a/ipaserver/install/ipa_cacert_manage.py b/ipaserver/install/ipa_cacert_manage.py index c681261..6a7fd05 100644 --- a/ipaserver/install/ipa_cacert_manage.py +++ b/ipaserver/install/ipa_cacert_manage.py @@ -60,11 +60,10 @@ class CACertManage(admintool.AdminTool): action='store_false', help="Sign the renewed certificate by external CA") renew_group.add_option( - "--external-cert-file", dest='external_cert_file', - help="PEM file containing a certificate signed by the external CA") - renew_group.add_option( - "--external-ca-file", dest='external_ca_file', - help="PEM file containing the external CA chain") + "--external-cert-file", dest="external_cert_files", + action="append", metavar="FILE", + help="File containing the IPA CA certificate and the external CA " + "certificate chain") parser.add_option_group(renew_group) install_group = OptionGroup(parser, "Install options") @@ -90,10 +89,7 @@ class CACertManage(admintool.AdminTool): options = self.options if command == 'renew': - if options.external_cert_file and not options.external_ca_file: - parser.error("--external-ca-file not specified") - elif not options.external_cert_file and options.external_ca_file: - parser.error("--external-cert-file not specified") + pass elif command == 'install': if len(self.args) < 2: parser.error("certificate file name not provided") @@ -107,7 +103,7 @@ class CACertManage(admintool.AdminTool): api.bootstrap(in_server=True) api.finalize() - if ((command == 'renew' and options.external_cert_file) or + if ((command == 'renew' and options.external_cert_files) or command == 'install'): self.conn = self.ldap_connect() else: @@ -166,7 +162,7 @@ class CACertManage(admintool.AdminTool): cert = db.get_cert_from_db(self.cert_nickname, pem=False) options = self.options - if options.external_cert_file: + if options.external_cert_files: return self.renew_external_step_2(ca, cert) if options.self_signed is not None: @@ -200,31 +196,25 @@ class CACertManage(admintool.AdminTool): "ipa-cacert-manage as:" % paths.IPA_CA_CSR) print("ipa-cacert-manage renew " "--external-cert-file=/path/to/signed_certificate " - "--external-ca-file=/path/to/external_ca_certificate") + "--external-cert-file=/path/to/external_ca_certificate") def renew_external_step_2(self, ca, old_cert): print "Importing the renewed CA certificate, please wait" options = self.options - cert_filename = options.external_cert_file - ca_filename = options.external_ca_file + cert_file, ca_file = installutils.load_external_cert( + options.external_cert_files, x509.subject_base()) nss_cert = None nss.nss_init(ca.dogtag_constants.ALIAS_DIR) try: - try: - installutils.validate_external_cert( - cert_filename, ca_filename, x509.subject_base()) - except ValueError, e: - raise admintool.ScriptError(e) - nss_cert = x509.load_certificate(old_cert, x509.DER) subject = nss_cert.subject #pylint: disable=E1101 pkinfo = nss_cert.subject_public_key_info.format() #pylint: enable=E1101 - nss_cert = x509.load_certificate_from_file(cert_filename) + nss_cert = x509.load_certificate_from_file(cert_file.name) if not nss_cert.is_ca_cert(): raise admintool.ScriptError("Not a CA certificate") if nss_cert.subject != subject: @@ -249,7 +239,7 @@ class CACertManage(admintool.AdminTool): raise admintool.ScriptError( "Not compatible with the current CA certificate: %s", e) - ca_certs = x509.load_certificate_list_from_file(ca_filename) + ca_certs = x509.load_certificate_list_from_file(ca_file.name) for ca_cert in ca_certs: tmpdb.add_cert(ca_cert.der_data, str(ca_cert.subject), 'C,,') del ca_certs diff --git a/ipatests/test_integration/test_external_ca.py b/ipatests/test_integration/test_external_ca.py index 747990c..fbffdf1 100644 --- a/ipatests/test_integration/test_external_ca.py +++ b/ipatests/test_integration/test_external_ca.py @@ -97,8 +97,8 @@ class TestExternalCA(IntegrationTest): 'ipa-server-install', '-a', self.master.config.admin_password, '-p', self.master.config.dirman_password, - '--external_cert_file', external_cert_file, - '--external_ca_file', external_ca_file + '--external-cert-file', external_cert_file, + '--external-cert-file', external_ca_file ]) # Make sure IPA server is working properly