Support installing with custom SSL certs, without a CA

Design: http://freeipa.org/page/V3/CA-less_install
https://fedorahosted.org/freeipa/ticket/3363
This commit is contained in:
Petr Viktorin
2013-03-14 13:58:27 +01:00
committed by Martin Kosek
parent a03aba5704
commit 03a2c66eda
7 changed files with 217 additions and 31 deletions

View File

@@ -536,6 +536,9 @@ def main():
fd.write("ra_plugin=dogtag\n") fd.write("ra_plugin=dogtag\n")
fd.write("dogtag_version=%s\n" % fd.write("dogtag_version=%s\n" %
dogtag.install_constants.DOGTAG_VERSION) dogtag.install_constants.DOGTAG_VERSION)
else:
fd.write("enable_ra=False\n")
fd.write("ra_plugin=none\n")
fd.write("mode=production\n") fd.write("mode=production\n")
fd.close() fd.close()
finally: finally:
@@ -560,9 +563,7 @@ def main():
sstore.backup_state("install", "group_exists", group_exists) sstore.backup_state("install", "group_exists", group_exists)
#Automatically disable pkinit w/ dogtag until that is supported #Automatically disable pkinit w/ dogtag until that is supported
#[certs.ipa_self_signed() must be called only after api.finalize()] options.setup_pkinit = False
if not ipautil.file_exists(config.dir + "/pkinitcert.p12") and not certs.ipa_self_signed():
options.setup_pkinit = False
# Install CA cert so that we can do SSL connections with ldap # Install CA cert so that we can do SSL connections with ldap
install_ca_cert(config) install_ca_cert(config)

View File

@@ -38,6 +38,7 @@ import pickle
import random import random
import tempfile import tempfile
import nss.error import nss.error
import base64
from optparse import OptionGroup, OptionValueError, SUPPRESS_HELP from optparse import OptionGroup, OptionValueError, SUPPRESS_HELP
from ipaserver.install import dsinstance from ipaserver.install import dsinstance
@@ -60,7 +61,7 @@ from ipapython import sysrestore
from ipapython.ipautil import * from ipapython.ipautil import *
from ipapython import ipautil from ipapython import ipautil
from ipapython import dogtag from ipapython import dogtag
from ipalib import api, errors, util from ipalib import api, errors, util, x509
from ipapython.config import IPAOptionParser from ipapython.config import IPAOptionParser
from ipalib.x509 import load_certificate_from_file, load_certificate_chain_from_file from ipalib.x509 import load_certificate_from_file, load_certificate_chain_from_file
from ipalib.util import validate_domain_name from ipalib.util import validate_domain_name
@@ -185,6 +186,8 @@ def parse_options():
help="The password of the Apache Server PKCS#12 file") help="The password of the Apache Server PKCS#12 file")
cert_group.add_option("--pkinit_pin", dest="pkinit_pin", cert_group.add_option("--pkinit_pin", dest="pkinit_pin",
help="The password of the Kerberos KDC PKCS#12 file") 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, cert_group.add_option("--subject", action="callback", callback=subject_callback,
type="string", type="string",
help="The certificate subject base (default O=<realm-name>)") help="The certificate subject base (default O=<realm-name>)")
@@ -280,7 +283,14 @@ def parse_options():
if cnt > 0 and cnt < 4: if cnt > 0 and cnt < 4:
parser.error("All PKCS#12 options are required if any are used.") 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( parser.error(
"PKCS#12 options cannot be used with the external CA options.") "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") parser.error("You cannot specify --external_cert_file together with --external-ca")
if options.external_ca_file: if options.external_ca_file:
parser.error("You cannot specify --external_ca_file together with --external-ca") 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 if ((options.external_cert_file and not options.external_ca_file) or
(not options.external_cert_file and options.external_ca_file)): (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.update_entry(dn, mod)
conn.disconnect() conn.disconnect()
def main(): def main():
global ds global ds
global uninstalling global uninstalling
@@ -821,6 +834,13 @@ def main():
else: else:
domain_name = options.domain_name 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() domain_name = domain_name.lower()
ip = get_server_ip_address(host_name, fstore, options.unattended, options) ip = get_server_ip_address(host_name, fstore, options.unattended, options)
@@ -921,6 +941,7 @@ def main():
dogtag.install_constants.DOGTAG_VERSION) dogtag.install_constants.DOGTAG_VERSION)
else: else:
fd.write("enable_ra=False\n") fd.write("enable_ra=False\n")
fd.write("ra_plugin=none\n")
fd.write("mode=production\n") fd.write("mode=production\n")
fd.close() fd.close()
@@ -955,8 +976,6 @@ def main():
root_logger.critical("failed to add DS group: %s" % e) root_logger.critical("failed to add DS group: %s" % e)
# Create a directory server instance # Create a directory server instance
ds = dsinstance.DsInstance(fstore=fstore)
if external != 2: if external != 2:
# Configure ntpd # Configure ntpd
if options.conf_ntp: if options.conf_ntp:
@@ -966,17 +985,22 @@ def main():
ntp.create_instance() ntp.create_instance()
if options.dirsrv_pkcs12: if options.dirsrv_pkcs12:
ds = dsinstance.DsInstance(fstore=fstore,
cert_nickname=dirsrv_cert_name)
ds.create_instance(realm_name, host_name, domain_name, ds.create_instance(realm_name, host_name, domain_name,
dm_password, dirsrv_pkcs12_info, dm_password, dirsrv_pkcs12_info,
idstart=options.idstart, idmax=options.idmax,
subject_base=options.subject, subject_base=options.subject,
hbac_allow=not options.hbac_allow) hbac_allow=not options.hbac_allow)
else: else:
ds = dsinstance.DsInstance(fstore=fstore)
ds.create_instance(realm_name, host_name, domain_name, ds.create_instance(realm_name, host_name, domain_name,
dm_password, dm_password,
idstart=options.idstart, idmax=options.idmax, idstart=options.idstart, idmax=options.idmax,
subject_base=options.subject, subject_base=options.subject,
hbac_allow=not options.hbac_allow) hbac_allow=not options.hbac_allow)
else: else:
ds = dsinstance.DsInstance(fstore=fstore)
ds.init_info( ds.init_info(
realm_name, host_name, domain_name, dm_password, realm_name, host_name, domain_name, dm_password,
False, options.subject, 1101, 1100, None) False, options.subject, 1101, 1100, None)
@@ -1031,8 +1055,8 @@ def main():
ds.enable_ssl() ds.enable_ssl()
ds.restart() ds.restart()
# We need to ldap_enable the CA now that DS is up and running
if setup_ca: 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, ca.ldap_enable('CA', host_name, dm_password,
ipautil.realm_to_suffix(realm_name)) ipautil.realm_to_suffix(realm_name))
if not dogtag.install_constants.SHARED_DB: if not dogtag.install_constants.SHARED_DB:
@@ -1047,8 +1071,29 @@ def main():
ca.enable_client_auth_to_db() ca.enable_client_auth_to_db()
ca.restart() ca.restart()
# Upload the CA cert to the directory # Upload the CA cert to the directory
ds.upload_ca_cert() 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) krb = krbinstance.KrbInstance(fstore)
if options.pkinit_pkcs12: if options.pkinit_pkcs12:
@@ -1178,8 +1223,6 @@ def main():
else: else:
print "In order for Firefox autoconfiguration to work you will need to" 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 "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): if ipautil.file_exists(ANSWER_CACHE):
os.remove(ANSWER_CACHE) os.remove(ANSWER_CACHE)

View File

@@ -29,6 +29,8 @@ import base64
from hashlib import sha1 from hashlib import sha1
from ConfigParser import RawConfigParser, MissingSectionHeaderError from ConfigParser import RawConfigParser, MissingSectionHeaderError
from nss import nss
from ipapython import dogtag from ipapython import dogtag
from ipapython import sysrestore from ipapython import sysrestore
from ipapython import ipautil from ipapython import ipautil
@@ -293,9 +295,11 @@ class NSSDatabase(object):
ipautil.run(args) ipautil.run(args)
except ipautil.CalledProcessError, e: except ipautil.CalledProcessError, e:
if e.returncode == 17: if e.returncode == 17:
raise RuntimeError("incorrect password for pkcs#12 file") raise RuntimeError("incorrect password for pkcs#12 file %s" %
pkcs12_filename)
else: 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): def find_root_cert_from_pkcs12(self, pkcs12_fname, passwd_fname=None):
"""Given a PKCS#12 file, try to find any certificates that do """Given a PKCS#12 file, try to find any certificates that do
@@ -355,6 +359,53 @@ class NSSDatabase(object):
fd.write(cert) fd.write(cert)
os.chmod(location, 0444) 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): class CertDB(object):
"""An IPA-server-specific wrapper around NSS """An IPA-server-specific wrapper around NSS
@@ -610,10 +661,7 @@ class CertDB(object):
nick = get_ca_nickname(self.realm) nick = get_ca_nickname(self.realm)
else: else:
nick = str(subject_dn) nick = str(subject_dn)
self.run_certutil(["-A", "-n", nick, self.nssdb.add_single_pem_cert(nick, "CT,,C", cert)
"-t", "CT,,C",
"-a"],
stdin=cert)
except RuntimeError: except RuntimeError:
break break

View File

@@ -36,7 +36,7 @@ import certs
import ldap import ldap
from ipaserver.install import ldapupdate from ipaserver.install import ldapupdate
from ipaserver.install import replication from ipaserver.install import replication
from ipalib import errors from ipalib import errors, api
from ipapython.dn import DN from ipapython.dn import DN
SERVER_ROOT_64 = "/usr/lib64/dirsrv" SERVER_ROOT_64 = "/usr/lib64/dirsrv"
@@ -541,7 +541,10 @@ class DsInstance(service.Service):
# We only handle one server cert # We only handle one server cert
nickname = server_certs[0][0] nickname = server_certs[0][0]
self.dercert = dsdb.get_cert_from_db(nickname, pem=False) 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: else:
nickname = self.nickname nickname = self.nickname
cadb = certs.CertDB(self.realm_name, host_name=self.fqdn, subject_base=self.subject_base) 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 # check for open secure port 636 from now on
self.open_ports.append(636) 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) dirname = config_dirname(self.serverid)
certdb = certs.CertDB(self.realm_name, nssdir=dirname, subject_base=self.subject_base) 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.sub_dict['CADERCERT'] = base64.b64encode(dercert)
self._ldap_mod('upload-cacert.ldif', self.sub_dict) self._ldap_mod('upload-cacert.ldif', self.sub_dict)

View File

@@ -61,7 +61,10 @@ class HTTPInstance(service.Service):
subject_base = ipautil.dn_attribute_property('_subject_base') 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.fqdn = fqdn
self.realm = realm self.realm = realm
self.domain = domain_name 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]) raise RuntimeError("Could not find a suitable server cert in import in %s" % self.pkcs12_info[0])
db.create_password_conf() db.create_password_conf()
# We only handle one server cert # We only handle one server cert
nickname = server_certs[0][0] nickname = server_certs[0][0]
self.dercert = db.get_cert_from_db(nickname, pem=False) 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) self.__set_mod_nss_nickname(nickname)
else: else:

View File

@@ -40,6 +40,7 @@ from ipalib.util import validate_hostname
from ipapython import config from ipapython import config
from ipalib import errors from ipalib import errors
from ipapython.dn import DN from ipapython.dn import DN
from ipaserver.install import certs
# Used to determine install status # Used to determine install status
IPA_MODULES = [ IPA_MODULES = [
@@ -699,3 +700,56 @@ def handle_error(error, log_file_name=None):
message = "Unexpected error" message = "Unexpected error"
message += '\n%s: %s' % (type(error).__name__, error) message += '\n%s: %s' % (type(error).__name__, error)
return message, 1 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

View File

@@ -99,6 +99,9 @@ class ReplicaPrepare(admintool.AdminTool):
self.option_parser.error("You cannot specify a --reverse-zone " self.option_parser.error("You cannot specify a --reverse-zone "
"option together with --no-reverse") "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. # If any of the PKCS#12 options are selected, all are required.
pkcs12_opts = [options.dirsrv_pkcs12, options.dirsrv_pin, pkcs12_opts = [options.dirsrv_pkcs12, options.dirsrv_pin,
options.http_pkcs12, options.http_pin] options.http_pkcs12, options.http_pin]
@@ -127,11 +130,6 @@ class ReplicaPrepare(admintool.AdminTool):
if api.env.host == self.replica_fqdn: if api.env.host == self.replica_fqdn:
raise admintool.ScriptError("You can't create a replica on itself") 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 # FIXME: certs.ipa_self_signed_master return value can be
# True, False, None, with different meanings. # True, False, None, with different meanings.
# So, we need to explicitly compare to False # 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 " raise admintool.ScriptError("A selfsign CA backend can only "
"prepare on the original master") "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( config_dir = dsinstance.config_dirname(
dsinstance.realm_to_serverid(api.env.realm)) dsinstance.realm_to_serverid(api.env.realm))
if not ipautil.dir_exists(config_dir): if not ipautil.dir_exists(config_dir):
raise admintool.ScriptError( raise admintool.ScriptError(
"could not find directory instance: %s" % config_dir) "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): def ask_for_options(self):
options = self.options options = self.options
super(ReplicaPrepare, self).ask_for_options() super(ReplicaPrepare, self).ask_for_options()
@@ -275,7 +291,7 @@ class ReplicaPrepare(admintool.AdminTool):
"Creating SSL certificate for the Directory Server") "Creating SSL certificate for the Directory Server")
self.export_certdb("dscert", passwd_fname) 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( self.log.info(
"Creating SSL certificate for the dogtag Directory Server") "Creating SSL certificate for the dogtag Directory Server")
self.export_certdb("dogtagcert", passwd_fname) self.export_certdb("dogtagcert", passwd_fname)