From 03a2c66eda695ad2d4bfe675fa2902035e6b37f0 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 14 Mar 2013 13:58:27 +0100 Subject: Support installing with custom SSL certs, without a CA Design: http://freeipa.org/page/V3/CA-less_install https://fedorahosted.org/freeipa/ticket/3363 --- install/tools/ipa-replica-install | 7 ++-- install/tools/ipa-server-install | 61 +++++++++++++++++++++++++++----- ipaserver/install/certs.py | 60 +++++++++++++++++++++++++++---- ipaserver/install/dsinstance.py | 28 ++++++++++++--- ipaserver/install/httpinstance.py | 10 ++++-- ipaserver/install/installutils.py | 54 ++++++++++++++++++++++++++++ ipaserver/install/ipa_replica_prepare.py | 28 +++++++++++---- 7 files changed, 217 insertions(+), 31 deletions(-) diff --git a/install/tools/ipa-replica-install b/install/tools/ipa-replica-install index 94d60bec..a0f20e44 100755 --- a/install/tools/ipa-replica-install +++ b/install/tools/ipa-replica-install @@ -536,6 +536,9 @@ def main(): fd.write("ra_plugin=dogtag\n") fd.write("dogtag_version=%s\n" % dogtag.install_constants.DOGTAG_VERSION) + else: + fd.write("enable_ra=False\n") + fd.write("ra_plugin=none\n") fd.write("mode=production\n") fd.close() finally: @@ -560,9 +563,7 @@ def main(): sstore.backup_state("install", "group_exists", group_exists) #Automatically disable pkinit w/ dogtag until that is supported - #[certs.ipa_self_signed() must be called only after api.finalize()] - if not ipautil.file_exists(config.dir + "/pkinitcert.p12") and not certs.ipa_self_signed(): - options.setup_pkinit = False + options.setup_pkinit = False # Install CA cert so that we can do SSL connections with ldap install_ca_cert(config) diff --git a/install/tools/ipa-server-install b/install/tools/ipa-server-install index add03792..5aa5cd73 100755 --- a/install/tools/ipa-server-install +++ b/install/tools/ipa-server-install @@ -38,6 +38,7 @@ import pickle import random import tempfile import nss.error +import base64 from optparse import OptionGroup, OptionValueError, SUPPRESS_HELP from ipaserver.install import dsinstance @@ -60,7 +61,7 @@ from ipapython import sysrestore from ipapython.ipautil import * from ipapython import ipautil from ipapython import dogtag -from ipalib import api, errors, util +from ipalib import api, errors, util, x509 from ipapython.config import IPAOptionParser from ipalib.x509 import load_certificate_from_file, load_certificate_chain_from_file from ipalib.util import validate_domain_name @@ -185,6 +186,8 @@ def parse_options(): help="The password of the Apache Server PKCS#12 file") cert_group.add_option("--pkinit_pin", dest="pkinit_pin", help="The password of the Kerberos KDC PKCS#12 file") + cert_group.add_option("--root-ca-file", dest="root_ca_file", + help="PEM file with root CA certificate(s) to trust") cert_group.add_option("--subject", action="callback", callback=subject_callback, type="string", help="The certificate subject base (default O=)") @@ -280,7 +283,14 @@ def parse_options(): if cnt > 0 and cnt < 4: parser.error("All PKCS#12 options are required if any are used.") - if (options.external_cert_file or options.external_ca_file) and cnt: + if options.dirsrv_pkcs12 and not options.root_ca_file: + parser.error( + "--root-ca-file must be given with the PKCS#12 options.") + if options.dirsrv_pkcs12 and not options.root_ca_file: + parser.error( + "The PKCS#12 options must be given with --root-ca-file.") + + 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.") @@ -289,6 +299,8 @@ def parse_options(): 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.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)): @@ -561,6 +573,7 @@ def set_subject_in_config(realm_name, dm_password, suffix, subject_base): conn.update_entry(dn, mod) conn.disconnect() + def main(): global ds global uninstalling @@ -821,6 +834,13 @@ def main(): else: domain_name = options.domain_name + if options.http_pkcs12: + # Check the given PKCS#12 files + ca_file = options.root_ca_file + check_pkcs12 = installutils.check_pkcs12 + http_cert_name = check_pkcs12(http_pkcs12_info, ca_file, host_name) + dirsrv_cert_name = check_pkcs12(dirsrv_pkcs12_info, ca_file, host_name) + domain_name = domain_name.lower() ip = get_server_ip_address(host_name, fstore, options.unattended, options) @@ -921,6 +941,7 @@ def main(): dogtag.install_constants.DOGTAG_VERSION) else: fd.write("enable_ra=False\n") + fd.write("ra_plugin=none\n") fd.write("mode=production\n") fd.close() @@ -955,8 +976,6 @@ def main(): root_logger.critical("failed to add DS group: %s" % e) # Create a directory server instance - ds = dsinstance.DsInstance(fstore=fstore) - if external != 2: # Configure ntpd if options.conf_ntp: @@ -966,17 +985,22 @@ def main(): ntp.create_instance() if options.dirsrv_pkcs12: + ds = dsinstance.DsInstance(fstore=fstore, + cert_nickname=dirsrv_cert_name) ds.create_instance(realm_name, host_name, domain_name, dm_password, dirsrv_pkcs12_info, + idstart=options.idstart, idmax=options.idmax, subject_base=options.subject, hbac_allow=not options.hbac_allow) else: + ds = dsinstance.DsInstance(fstore=fstore) ds.create_instance(realm_name, host_name, domain_name, dm_password, idstart=options.idstart, idmax=options.idmax, subject_base=options.subject, hbac_allow=not options.hbac_allow) else: + ds = dsinstance.DsInstance(fstore=fstore) ds.init_info( realm_name, host_name, domain_name, dm_password, False, options.subject, 1101, 1100, None) @@ -1031,8 +1055,8 @@ def main(): ds.enable_ssl() ds.restart() - # We need to ldap_enable the CA now that DS is up and running if setup_ca: + # We need to ldap_enable the CA now that DS is up and running ca.ldap_enable('CA', host_name, dm_password, ipautil.realm_to_suffix(realm_name)) if not dogtag.install_constants.SHARED_DB: @@ -1047,8 +1071,29 @@ def main(): ca.enable_client_auth_to_db() ca.restart() - # Upload the CA cert to the directory - ds.upload_ca_cert() + # Upload the CA cert to the directory + ds.upload_ca_cert() + else: + with open(options.root_ca_file) as f: + pem_cert = f.read() + + # Trust the CA cert + root_logger.info( + 'Trusting certificate authority from %s' % options.root_ca_file) + + certs.NSSDatabase('/etc/pki/nssdb').import_pem_cert( + 'External CA cert', 'CT,,', options.root_ca_file) + + # Put a CA cert where other instances expect it + with open('/etc/ipa/ca.crt', 'wb') as f: + f.write(pem_cert) + + # Install the CA cert for the HTTP server + with open('/usr/share/ipa/html/ca.crt', 'wb') as f: + f.write(pem_cert) + + # Upload the CA cert to the directory + ds.upload_ca_dercert(base64.b64decode(x509.strip_header(pem_cert))) krb = krbinstance.KrbInstance(fstore) if options.pkinit_pkcs12: @@ -1178,8 +1223,6 @@ def main(): else: print "In order for Firefox autoconfiguration to work you will need to" print "use a SSL signing certificate. See the IPA documentation for more details." - print "You also need to install a PEM copy of the CA certificate into" - print "/usr/share/ipa/html/ca.crt" if ipautil.file_exists(ANSWER_CACHE): os.remove(ANSWER_CACHE) diff --git a/ipaserver/install/certs.py b/ipaserver/install/certs.py index 6d688b35..81f403df 100644 --- a/ipaserver/install/certs.py +++ b/ipaserver/install/certs.py @@ -29,6 +29,8 @@ import base64 from hashlib import sha1 from ConfigParser import RawConfigParser, MissingSectionHeaderError +from nss import nss + from ipapython import dogtag from ipapython import sysrestore from ipapython import ipautil @@ -293,9 +295,11 @@ class NSSDatabase(object): ipautil.run(args) except ipautil.CalledProcessError, e: if e.returncode == 17: - raise RuntimeError("incorrect password for pkcs#12 file") + raise RuntimeError("incorrect password for pkcs#12 file %s" % + pkcs12_filename) else: - raise RuntimeError("unknown error import pkcs#12 file") + raise RuntimeError("unknown error import pkcs#12 file %s" % + pkcs12_filename) def find_root_cert_from_pkcs12(self, pkcs12_fname, passwd_fname=None): """Given a PKCS#12 file, try to find any certificates that do @@ -355,6 +359,53 @@ class NSSDatabase(object): fd.write(cert) os.chmod(location, 0444) + def import_pem_cert(self, nickname, flags, location): + """Import a cert form the given PEM file. + + The file must contain exactly one certificate. + """ + with open(location) as fd: + certs = fd.read() + + cert, st = find_cert_from_txt(certs) + self.add_single_pem_cert(nickname, flags, cert) + + try: + find_cert_from_txt(certs, st) + except RuntimeError: + pass + else: + raise ValueError('%s contains more than one certificate') + + def add_single_pem_cert(self, nick, flags, cert): + """Import a cert in PEM format""" + self.run_certutil(["-A", "-n", nick, + "-t", flags, + "-a"], + stdin=cert) + + def verify_server_cert_validity(self, nickname, hostname): + """Verify a certificate is valid for a SSL server with given hostname + + Raises a ValueError if the certificate is invalid. + """ + certdb = cert = None + nss.nss_init(self.secdir) + try: + certdb = nss.get_default_certdb() + cert = nss.find_cert_from_nickname(nickname) + intended_usage = nss.certificateUsageSSLServer + approved_usage = cert.verify_now(certdb, True, intended_usage) + if not approved_usage & intended_usage: + raise ValueError('invalid for a SSL server') + if not cert.verify_hostname(hostname): + raise ValueError('invalid for server %s' % hostname) + finally: + del certdb, cert + nss.nss_shutdown() + + return None + class CertDB(object): """An IPA-server-specific wrapper around NSS @@ -610,10 +661,7 @@ class CertDB(object): nick = get_ca_nickname(self.realm) else: nick = str(subject_dn) - self.run_certutil(["-A", "-n", nick, - "-t", "CT,,C", - "-a"], - stdin=cert) + self.nssdb.add_single_pem_cert(nick, "CT,,C", cert) except RuntimeError: break diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py index 5f3041c2..38dc94e4 100644 --- a/ipaserver/install/dsinstance.py +++ b/ipaserver/install/dsinstance.py @@ -36,7 +36,7 @@ import certs import ldap from ipaserver.install import ldapupdate from ipaserver.install import replication -from ipalib import errors +from ipalib import errors, api from ipapython.dn import DN SERVER_ROOT_64 = "/usr/lib64/dirsrv" @@ -541,7 +541,10 @@ class DsInstance(service.Service): # We only handle one server cert nickname = server_certs[0][0] self.dercert = dsdb.get_cert_from_db(nickname, pem=False) - dsdb.track_server_cert(nickname, self.principal, dsdb.passwd_fname, 'restart_dirsrv %s' % self.serverid ) + if api.env.enable_ra: + dsdb.track_server_cert( + nickname, self.principal, dsdb.passwd_fname, + 'restart_dirsrv %s' % self.serverid) else: nickname = self.nickname cadb = certs.CertDB(self.realm_name, host_name=self.fqdn, subject_base=self.subject_base) @@ -592,15 +595,30 @@ class DsInstance(service.Service): # check for open secure port 636 from now on self.open_ports.append(636) - def upload_ca_cert(self): + def export_ca_cert(self, nickname, location): + dirname = config_dirname(self.serverid) + dsdb = certs.NSSDatabase(nssdir=dirname) + dsdb.export_pem_cert(nickname, location) + + def upload_ca_cert(self, cacert_name=None): """ - Upload the CA certificate in DER form in the LDAP directory. + Upload the CA certificate from the NSS database to the LDAP directory. """ dirname = config_dirname(self.serverid) certdb = certs.CertDB(self.realm_name, nssdir=dirname, subject_base=self.subject_base) - dercert = certdb.get_cert_from_db(certdb.cacert_name, pem=False) + if cacert_name is None: + cacert_name = certdb.cacert_name + dercert = certdb.get_cert_from_db(cacert_name, pem=False) + self.upload_ca_dercert(dercert) + + def upload_ca_dercert(self, dercert): + """Upload the CA DER certificate to the LDAP directory + """ + # Note: Don't try to optimize if base64 data is already available. + # We want to re-encode using Python's b64encode to ensure the + # data is normalized (no extra newlines in the ldif) self.sub_dict['CADERCERT'] = base64.b64encode(dercert) self._ldap_mod('upload-cacert.ldif', self.sub_dict) diff --git a/ipaserver/install/httpinstance.py b/ipaserver/install/httpinstance.py index 59782cb6..458112fa 100644 --- a/ipaserver/install/httpinstance.py +++ b/ipaserver/install/httpinstance.py @@ -61,7 +61,10 @@ class HTTPInstance(service.Service): subject_base = ipautil.dn_attribute_property('_subject_base') - def create_instance(self, realm, fqdn, domain_name, dm_password=None, autoconfig=True, pkcs12_info=None, self_signed_ca=False, subject_base=None, auto_redirect=True): + def create_instance(self, realm, fqdn, domain_name, dm_password=None, + autoconfig=True, pkcs12_info=None, + self_signed_ca=False, subject_base=None, + auto_redirect=True): self.fqdn = fqdn self.realm = realm self.domain = domain_name @@ -247,10 +250,13 @@ class HTTPInstance(service.Service): raise RuntimeError("Could not find a suitable server cert in import in %s" % self.pkcs12_info[0]) db.create_password_conf() + # We only handle one server cert nickname = server_certs[0][0] self.dercert = db.get_cert_from_db(nickname, pem=False) - db.track_server_cert(nickname, self.principal, db.passwd_fname, 'restart_httpd') + + if api.env.enable_ra: + db.track_server_cert(nickname, self.principal, db.passwd_fname, 'restart_httpd') self.__set_mod_nss_nickname(nickname) else: diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py index a9728582..600acfee 100644 --- a/ipaserver/install/installutils.py +++ b/ipaserver/install/installutils.py @@ -40,6 +40,7 @@ from ipalib.util import validate_hostname from ipapython import config from ipalib import errors from ipapython.dn import DN +from ipaserver.install import certs # Used to determine install status IPA_MODULES = [ @@ -699,3 +700,56 @@ def handle_error(error, log_file_name=None): message = "Unexpected error" message += '\n%s: %s' % (type(error).__name__, error) return message, 1 + + +def check_pkcs12(pkcs12_info, ca_file, hostname): + """Check the given PKCS#12 with server cert and return the cert nickname + + This is used for files given to --*_pkcs12 to ipa-server-install and + ipa-replica-prepare. + + Return a (server cert name, CA cert names) tuple + """ + pkcs12_filename, pin_filename = pkcs12_info + root_logger.debug('Checking PKCS#12 certificate %s', pkcs12_filename) + db_pwd_file = ipautil.write_tmp_file(ipautil.ipa_generate_password()) + with certs.NSSDatabase() as nssdb: + nssdb.create_db(db_pwd_file.name) + + # Import the CA cert first so it has a known nickname + # (if it's present in the PKCS#12 it won't be overwritten) + ca_cert_name = 'The Root CA' + nssdb.import_pem_cert(ca_cert_name, "CT,C,C", ca_file) + + # Import everything in the PKCS#12 + nssdb.import_pkcs12(pkcs12_filename, db_pwd_file.name, pin_filename) + + # Check we have exactly one server cert (one with a private key) + server_certs = nssdb.find_server_certs() + if not server_certs: + raise ScriptError( + 'no server certificate found in %s' % pkcs12_filename) + if len(server_certs) > 1: + raise ScriptError( + '%s server certificates found in %s, expecting only one' % + (len(server_certs), pkcs12_filename)) + [(server_cert_name, server_cert_trust)] = server_certs + + # Check we have the whole cert chain & the CA is in it + for cert_name in nssdb.get_trust_chain(server_cert_name): + if cert_name == ca_cert_name: + break + else: + raise ScriptError( + '%s is not signed by %s, or the full certificate chain is not ' + 'present in the PKCS#12 file' % (pkcs12_filename, ca_file)) + + # Check server validity + try: + nssdb.verify_server_cert_validity(server_cert_name, hostname) + except ValueError as e: + raise ScriptError( + 'The server certificate in %s is not valid: %s' % + (pkcs12_filename, e)) + + return server_cert_name diff --git a/ipaserver/install/ipa_replica_prepare.py b/ipaserver/install/ipa_replica_prepare.py index 8afa4e8e..e7a92266 100644 --- a/ipaserver/install/ipa_replica_prepare.py +++ b/ipaserver/install/ipa_replica_prepare.py @@ -99,6 +99,9 @@ class ReplicaPrepare(admintool.AdminTool): self.option_parser.error("You cannot specify a --reverse-zone " "option together with --no-reverse") + #Automatically disable pkinit w/ dogtag until that is supported + options.setup_pkinit = False + # If any of the PKCS#12 options are selected, all are required. pkcs12_opts = [options.dirsrv_pkcs12, options.dirsrv_pin, options.http_pkcs12, options.http_pin] @@ -127,11 +130,6 @@ class ReplicaPrepare(admintool.AdminTool): if api.env.host == self.replica_fqdn: raise admintool.ScriptError("You can't create a replica on itself") - #Automatically disable pkinit w/ dogtag until that is supported - #[certs.ipa_self_signed() must be called only after api.finalize()] - if not options.pkinit_pkcs12 and not certs.ipa_self_signed(): - options.setup_pkinit = False - # FIXME: certs.ipa_self_signed_master return value can be # True, False, None, with different meanings. # So, we need to explicitly compare to False @@ -139,12 +137,30 @@ class ReplicaPrepare(admintool.AdminTool): raise admintool.ScriptError("A selfsign CA backend can only " "prepare on the original master") + if not api.env.enable_ra and not options.http_pkcs12: + raise admintool.ScriptError( + "Cannot issue certificates: a CA is not installed. Use the " + "--http_pkcs12, --dirsrv_pkcs12 options to provide custom " + "certificates.") + + if options.http_pkcs12: + # Check the given PKCS#12 files + self.check_pkcs12(options.http_pkcs12, options.http_pin) + self.check_pkcs12(options.dirsrv_pkcs12, options.dirsrv_pin) + config_dir = dsinstance.config_dirname( dsinstance.realm_to_serverid(api.env.realm)) if not ipautil.dir_exists(config_dir): raise admintool.ScriptError( "could not find directory instance: %s" % config_dir) + def check_pkcs12(self, pkcs12_file, pkcs12_pin): + pin_file = ipautil.write_tmp_file(pkcs12_pin) + installutils.check_pkcs12( + pkcs12_info=(pkcs12_file, pin_file.name), + ca_file='/etc/ipa/ca.crt', + hostname=self.replica_fqdn) + def ask_for_options(self): options = self.options super(ReplicaPrepare, self).ask_for_options() @@ -275,7 +291,7 @@ class ReplicaPrepare(admintool.AdminTool): "Creating SSL certificate for the Directory Server") self.export_certdb("dscert", passwd_fname) - if not certs.ipa_self_signed(): + if not options.dirsrv_pkcs12 and not certs.ipa_self_signed(): self.log.info( "Creating SSL certificate for the dogtag Directory Server") self.export_certdb("dogtagcert", passwd_fname) -- cgit