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

@@ -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

View File

@@ -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)

View File

@@ -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:

View File

@@ -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

View File

@@ -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)