diff options
Diffstat (limited to 'ipaserver')
-rw-r--r-- | ipaserver/install/cainstance.py | 561 | ||||
-rw-r--r-- | ipaserver/install/dogtaginstance.py | 399 | ||||
-rw-r--r-- | ipaserver/install/dsinstance.py | 87 | ||||
-rw-r--r-- | ipaserver/install/installutils.py | 107 | ||||
-rw-r--r-- | ipaserver/install/ipa_backup.py | 2 | ||||
-rw-r--r-- | ipaserver/install/ipa_kra_install.py | 243 | ||||
-rw-r--r-- | ipaserver/install/ipa_replica_prepare.py | 1 | ||||
-rw-r--r-- | ipaserver/install/krainstance.py | 346 | ||||
-rw-r--r-- | ipaserver/plugins/dogtag.py | 294 |
9 files changed, 1593 insertions, 447 deletions
diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py index 2a8ecc00c..e8bb7d701 100644 --- a/ipaserver/install/cainstance.py +++ b/ipaserver/install/cainstance.py @@ -19,53 +19,51 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # -import pwd +import array +import base64 +import binascii +import ConfigParser +import dbus +import httplib +import ldap import os -import sys +import pwd import re +import shutil +import stat +import subprocess +import sys +import syslog import time -import ldap -import base64 -import array import tempfile -import binascii -import shutil -import httplib import urllib import xml.dom.minidom -import stat -import syslog -import ConfigParser -import dbus -from ipapython import dogtag -from ipapython.certdb import get_ca_nickname -from ipapython import certmonger from ipalib import api from ipalib import pkcs10, x509 from ipalib import errors -from ipapython.dn import DN -import subprocess -import traceback +from ipaplatform import services +from ipaplatform.paths import paths +from ipaplatform.tasks import tasks + +from ipapython import dogtag +from ipapython import certmonger from ipapython import ipautil from ipapython import ipaldap -from ipaplatform.tasks import tasks -from ipaserver.install import service -from ipaserver.install import installutils -from ipaserver.install import dsinstance +from ipapython.certdb import get_ca_nickname +from ipapython.dn import DN +from ipapython.ipa_log_manager import log_mgr,\ + standard_logging_setup, root_logger + from ipaserver.install import certs -from ipaserver.install.installutils import stopped_service +from ipaserver.install import dsinstance +from ipaserver.install import installutils +from ipaserver.install import service +from ipaserver.install.dogtaginstance import DogtagInstance +from ipaserver.install.dogtaginstance import PKI_USER, DEFAULT_DSPORT from ipaserver.plugins import ldap2 -from ipapython.ipa_log_manager import * -from ipaplatform import services -from ipaplatform.paths import paths - -HTTPD_CONFD = paths.HTTPD_CONF_D_DIR -DEFAULT_DSPORT = dogtag.install_constants.DS_PORT -PKI_USER = "pkiuser" -PKI_DS_USER = dogtag.install_constants.DS_USER # When IPA is installed with DNS support, this CNAME should hold all IPA # replicas with CA configured @@ -88,22 +86,6 @@ RootDNPwd= $PASSWORD ConfigFile = /usr/share/pki/ca/conf/database.ldif """ -def check_inst(): - """ - Validate that the appropriate dogtag/RHCS packages have been installed. - """ - - # Check for a couple of binaries we need - if not os.path.exists(dogtag.install_constants.SPAWN_BINARY): - return False - if not os.path.exists(dogtag.install_constants.DESTROY_BINARY): - return False - - # This is the template tomcat file for a CA - if not os.path.exists(paths.PKI_CONF_SERVER_XML): - return False - - return True def get_preop_pin(instance_root, instance_name): # Only used for Dogtag 9 @@ -113,24 +95,27 @@ def get_preop_pin(instance_root, instance_name): # read the config file and get the preop pin try: - f=open(filename) + f = open(filename) except IOError, e: root_logger.error("Cannot open configuration file." + str(e)) raise e data = f.read() data = data.split('\n') - pattern = re.compile("preop.pin=(.*)" ) + pattern = re.compile("preop.pin=(.*)") for line in data: match = re.search(pattern, line) - if (match): - preop_pin=match.group(1) + if match: + preop_pin = match.group(1) break if preop_pin is None: - raise RuntimeError("Unable to find preop.pin in %s. Is your CA already configured?" % filename) + raise RuntimeError( + "Unable to find preop.pin in %s. Is your CA already configured?" % + filename) return preop_pin + def import_pkcs12(input_file, input_passwd, cert_database, cert_passwd): ipautil.run([paths.PK12UTIL, "-d", cert_database, @@ -138,21 +123,23 @@ def import_pkcs12(input_file, input_passwd, cert_database, "-k", cert_passwd, "-w", input_passwd]) + def get_value(s): """ Parse out a name/value pair from a Javascript variable. """ try: - expr = s.split('=',1) + expr = s.split('=', 1) value = expr[1] value = value.replace('\"', '') - value = value.replace(';','') - value = value.replace('\\n','\n') - value = value.replace('\\r','\r') + value = value.replace(';', '') + value = value.replace('\\n', '\n') + value = value.replace('\\r', '\r') return value except IndexError: return None + def find_substring(data, value): """ Scan through a list looking for a string that starts with value. @@ -161,6 +148,7 @@ def find_substring(data, value): if d.startswith(value): return get_value(d) + def get_defList(data): """ Return a dictionary of defList name/value pairs. @@ -196,6 +184,7 @@ def get_defList(data): return defdict + def get_outputList(data): """ Return a dictionary of outputList name/value pairs. @@ -221,6 +210,7 @@ def get_outputList(data): return outputdict + def get_crl_files(path=None): """ Traverse dogtag's CRL files in default CRL publish directory or in chosen @@ -240,8 +230,8 @@ def get_crl_files(path=None): def is_step_one_done(): - '''Read CS.cfg and determine if step one of an external CA install is done - ''' + """Read CS.cfg and determine if step one of an external CA install is done + """ path = dogtag.install_constants.CS_CFG_PATH if not os.path.exists(path): return False @@ -251,6 +241,14 @@ def is_step_one_done(): return False +def is_ca_installed_locally(): + """Check if CA is installed locally by checking for existence of CS.cfg + :return:True/False + """ + path = dogtag.install_constants.CS_CFG_PATH + return os.path.exists(path) + + class CADSInstance(service.Service): """Certificate Authority DS instance @@ -258,7 +256,8 @@ class CADSInstance(service.Service): Thus this class only does uninstallation. """ def __init__(self, host_name=None, realm_name=None, domain_name=None, dm_password=None, dogtag_constants=None): - service.Service.__init__(self, "pkids", + service.Service.__init__( + self, "pkids", service_desc="directory server for the CA", dm_password=dm_password, ldapi=False, @@ -284,7 +283,7 @@ class CADSInstance(service.Service): serverid = self.restore_state("serverid") # Just eat this state if it exists - running = self.restore_state("running") + self.restore_state("running") if not enabled is None and not enabled: services.knownservices.dirsrv.disable() @@ -297,41 +296,14 @@ class CADSInstance(service.Service): dsdb.untrack_server_cert("Server-Cert") dsinstance.erase_ds_instance_data(serverid) - user_exists = self.restore_state("user_exists") + self.restore_state("user_exists") # At one time we removed this user on uninstall. That can potentially - # orphan files, or worse, if another useradd runs in the intermim, + # orphan files, or worse, if another useradd runs in the interim, # cause files to have a new owner. -def stop_tracking_certificates(dogtag_constants): - """Stop tracking our certificates. Called on uninstall. - """ - cmonger = services.knownservices.certmonger - services.knownservices.messagebus.start() - cmonger.start() - - for nickname in ['Server-Cert cert-pki-ca', - 'auditSigningCert cert-pki-ca', - 'ocspSigningCert cert-pki-ca', - 'subsystemCert cert-pki-ca', - 'caSigningCert cert-pki-ca']: - try: - certmonger.stop_tracking( - dogtag_constants.ALIAS_DIR, nickname=nickname) - except (ipautil.CalledProcessError, RuntimeError), e: - root_logger.error( - "certmonger failed to stop tracking certificate: %s" % str(e)) - - try: - certmonger.stop_tracking(paths.HTTPD_ALIAS_DIR, nickname='ipaCert') - except (ipautil.CalledProcessError, RuntimeError), e: - root_logger.error( - "certmonger failed to stop tracking certificate: %s" % str(e)) - cmonger.stop() - - -class CAInstance(service.Service): +class CAInstance(DogtagInstance): """ When using a dogtag CA the DS database contains just the server cert for DS. The mod_nss database will contain the RA agent @@ -353,19 +325,15 @@ class CAInstance(service.Service): if dogtag_constants is None: dogtag_constants = dogtag.configured_constants() - service.Service.__init__(self, - '%sd' % dogtag_constants.PKI_INSTANCE_NAME, - service_desc="certificate server", - dm_password=dm_password, - ldapi=ldapi) - - self.dogtag_constants = dogtag_constants - self.realm = realm - self.admin_password = None - self.fqdn = host_name - self.domain = None - self.pkcs12_info = None - self.clone = False + super(CAInstance, self).__init__( + realm=realm, + subsystem="CA", + service_desc="certificate server", + dogtag_constants=dogtag_constants, + host_name=host_name, + dm_password=dm_password, + ldapi=ldapi + ) # for external CAs self.external = 0 @@ -374,36 +342,17 @@ class CAInstance(service.Service): self.cert_chain_file = None self.create_ra_agent_db = True - # The same database is used for mod_nss because the NSS context - # will already have been initialized by Apache by the time - # mod_python wants to do things. - self.canickname = None - if realm: - self.canickname = get_ca_nickname(realm) - self.basedn = DN(('o', 'ipaca')) - self.ca_agent_db = tempfile.mkdtemp(prefix = "tmp-") + self.canickname = get_ca_nickname(realm) self.ra_agent_db = ra_db - self.ra_agent_pwd = None - if ra_db: - self.ra_agent_pwd = ra_db + "/pwdfile.txt" - self.ds_port = DEFAULT_DSPORT - self.security_domain_name = "IPA" - self.server_root = dogtag_constants.SERVER_ROOT + self.ra_agent_pwd = self.ra_agent_db + "/pwdfile.txt" self.ra_cert = None self.requestId = None - - def __del__(self): - shutil.rmtree(self.ca_agent_db, ignore_errors=True) - - def is_installed(self): - """ - Installing with an external CA is a two-step process. This - is used to determine if the first step has been done. - - Returns True/False - """ - return os.path.exists(os.path.join( - self.server_root, self.dogtag_constants.PKI_INSTANCE_NAME)) + self.tracking_reqs = (('Server-Cert cert-pki-ca', None), + ('auditSigningCert cert-pki-ca', None), + ('ocspSigningCert cert-pki-ca', None), + ('subsystemCert cert-pki-ca', None), + ('caSigningCert cert-pki-ca', 'ipaCACertRenewal')) + self.log = log_mgr.get_logger(self) def configure_instance(self, host_name, domain, dm_password, admin_password, ds_port=DEFAULT_DSPORT, @@ -440,12 +389,12 @@ class CAInstance(service.Service): # Determine if we are installing as an externally-signed CA and # what stage we're in. if csr_file is not None: - self.csr_file=csr_file - self.external=1 + self.csr_file = csr_file + self.external = 1 elif cert_file is not None: - self.cert_file=cert_file - self.cert_chain_file=cert_chain_file - self.external=2 + self.cert_file = cert_file + self.cert_chain_file = cert_chain_file + self.external = 2 self.step("creating certificate server user", self.__create_ca_user) if self.dogtag_constants.DOGTAG_VERSION >= 10: @@ -454,11 +403,11 @@ class CAInstance(service.Service): if not ipautil.dir_exists(paths.VAR_LIB_PKI_CA_DIR): self.step("creating pki-ca instance", self.create_instance) self.step("configuring certificate server instance", self.__configure_instance) - self.step("stopping certificate server instance to update CS.cfg", self.__stop) + self.step("stopping certificate server instance to update CS.cfg", self.stop_instance) self.step("disabling nonces", self.__disable_nonce) self.step("set up CRL publishing", self.__enable_crl_publish) self.step("enable PKIX certificate path discovery and validation", self.enable_pkix) - self.step("starting certificate server instance", self.__start) + self.step("starting certificate server instance", self.start_instance) # Step 1 of external is getting a CSR so we don't need to do these # steps until we get a cert back from the external CA. if self.external != 1: @@ -474,33 +423,28 @@ class CAInstance(service.Service): self.step("enabling Subject Alternative Name", self.enable_subject_alternative_name) self.step("enabling CRL and OCSP extensions for certificates", self.__set_crl_ocsp_extensions) self.step("setting audit signing renewal to 2 years", self.set_audit_renewal) - self.step("configuring certificate server to start on boot", self.__enable) + self.step("configuring certificate server to start on boot", + self.enable) if not self.clone: - self.step("restarting certificate server", self.__restart_instance) + self.step("restarting certificate server", self.restart_instance) self.step("requesting RA certificate from CA", self.__request_ra_certificate) self.step("issuing RA agent certificate", self.__issue_ra_cert) self.step("adding RA agent as a trusted user", self.__configure_ra) self.step("configure certmonger for renewals", self.configure_certmonger_renewal) - self.step("configure certificate renewals", self.configure_renewal) + self.step("configure certificate renewals", self.configure_cert_renewal) if not self.clone: self.step("configure RA certificate renewal", self.configure_agent_renewal) self.step("configure Server-Cert certificate renewal", self.track_servercert) - self.step("Configure HTTP to proxy connections", self.__http_proxy) + self.step("Configure HTTP to proxy connections", + self.http_proxy) self.start_creation(runtime=210) - def __stop(self): - self.stop() - - def __start(self): - self.start() - - def __spawn_instance(self): """ Create and configure a new CA instance using pkispawn. - pkispawn requires a configuration file with IPA-specific - parameters. + Creates the config file with IPA specific parameters + and passes it to the base class to call pkispawn """ # Create an empty and secured file @@ -522,7 +466,7 @@ class CAInstance(service.Service): config.set("CA", "pki_backup_password", self.admin_password) # Client security database - config.set("CA", "pki_client_database_dir", self.ca_agent_db) + config.set("CA", "pki_client_database_dir", self.agent_db) config.set("CA", "pki_client_database_password", self.admin_password) config.set("CA", "pki_client_database_purge", "False") config.set("CA", "pki_client_pkcs12_password", self.admin_password) @@ -535,7 +479,7 @@ class CAInstance(service.Service): config.set("CA", "pki_admin_nickname", "ipa-ca-agent") config.set("CA", "pki_admin_subject_dn", str(DN(('cn', 'ipa-ca-agent'), self.subject_base))) - config.set("CA", "pki_client_admin_cert_p12", paths.CA_AGENT_P12) + config.set("CA", "pki_client_admin_cert_p12", paths.DOGTAG_AGENT_P12) # Directory server config.set("CA", "pki_ds_ldap_port", str(self.ds_port)) @@ -562,7 +506,7 @@ class CAInstance(service.Service): config.set("CA", "pki_audit_signing_nickname", "auditSigningCert cert-pki-ca") config.set("CA", "pki_ca_signing_nickname", "caSigningCert cert-pki-ca") - if (self.clone): + if self.clone: cafile = self.pkcs12_info[0] shutil.copy(cafile, paths.TMP_CA_P12) pent = pwd.getpwnam(PKI_USER) @@ -610,22 +554,9 @@ class CAInstance(service.Service): with open(cfg_file, "wb") as f: config.write(f) - # Define the things we don't want logged - nolog = (self.admin_password, self.dm_password,) - - args = [paths.PKISPAWN, "-s", "CA", "-f", cfg_file ] - - with open(cfg_file) as f: - root_logger.debug( - 'Contents of pkispawn configuration file (%s):\n%s' % - (cfg_file, ipautil.nolog_replace(f.read(), nolog))) - self.backup_state('installed', True) try: - ipautil.run(args, nolog=nolog) - except ipautil.CalledProcessError, e: - root_logger.critical("failed to configure ca instance %s" % e) - raise RuntimeError('Configuration of CA failed') + DogtagInstance.spawn_instance(self, cfg_file) finally: os.remove(cfg_file) @@ -634,10 +565,10 @@ class CAInstance(service.Service): print "%s --external_cert_file=/path/to/signed_certificate --external_ca_file=/path/to/external_ca_certificate" % sys.argv[0] sys.exit(0) else: - shutil.move(paths.CA_BACKUP_KEYS_P12, \ + shutil.move(paths.CA_BACKUP_KEYS_P12, paths.CACERT_P12) - root_logger.debug("completed creating ca instance") + self.log.debug("completed creating ca instance") def create_instance(self): """ @@ -668,29 +599,21 @@ class CAInstance(service.Service): self.backup_state('installed', True) ipautil.run(args, env={'PKI_HOSTNAME':self.fqdn}) - def __enable(self): - self.backup_state("enabled", self.is_enabled()) - # We do not let the system start IPA components on its own, - # Instead we reply on the IPA init script to start only enabled - # components as found in our LDAP configuration tree - # We need to install DS before we can actually ldap_enable a service. - # so actual enablement is delayed. - def __create_ca_user(self): try: pwd.getpwnam(PKI_USER) - root_logger.debug("ca user %s exists" % PKI_USER) + self.log.debug("ca user %s exists", PKI_USER) except KeyError: - root_logger.debug("adding ca user %s" % PKI_USER) + self.log.debug("adding ca user %s", PKI_USER) args = [paths.USERADD, "-c", "CA System User", "-d", paths.VAR_LIB, "-s", paths.NOLOGIN, "-M", "-r", PKI_USER] try: ipautil.run(args) - root_logger.debug("done adding user") + self.log.debug("done adding user") except ipautil.CalledProcessError, e: - root_logger.critical("failed to add user %s" % e) + self.log.critical("failed to add user %s", e) def __configure_instance(self): # Only used for Dogtag 9 @@ -701,7 +624,7 @@ class CAInstance(service.Service): args = [paths.PERL, paths.PKISILENT, "ConfigureCA", "-cs_hostname", self.fqdn, "-cs_port", str(self.dogtag_constants.ADMIN_SECURE_PORT), - "-client_certdb_dir", self.ca_agent_db, + "-client_certdb_dir", self.agent_db, "-client_certdb_pwd", self.admin_password, "-preop_pin" , preop_pin, "-domain_name", self.security_domain_name, @@ -746,7 +669,7 @@ class CAInstance(service.Service): else: args.append("-external") args.append("false") - if (self.clone): + if self.clone: """sd = security domain --> all CS systems get registered to a security domain. This is set to the hostname and port of the master CA. @@ -785,7 +708,7 @@ class CAInstance(service.Service): ipautil.run(args, env={'PKI_HOSTNAME':self.fqdn}, nolog=nolog) except ipautil.CalledProcessError, e: - root_logger.critical("failed to configure ca instance %s" % e) + self.log.critical("failed to configure ca instance %s", e) raise RuntimeError('Configuration of CA failed') if self.external == 1: @@ -798,15 +721,7 @@ class CAInstance(service.Service): if ipautil.file_exists(paths.ROOT_TMP_CA_P12): shutil.move(paths.ROOT_TMP_CA_P12, paths.CACERT_P12) - root_logger.debug("completed creating ca instance") - - def __restart_instance(self): - try: - self.restart(self.dogtag_constants.PKI_INSTANCE_NAME) - except Exception: - # TODO: roll back here? - root_logger.debug(traceback.format_exc()) - root_logger.critical("Failed to restart the certificate server. See the installation log for details.") + self.log.debug("completed creating ca instance") def __disable_nonce(self): # Turn off Nonces @@ -830,9 +745,9 @@ class CAInstance(service.Service): os.write(admin_fd, self.admin_password) os.close(admin_fd) - # Look thru the cert chain to get all the certs we need to add + # Look through the cert chain to get all the certs we need to add # trust for - p = subprocess.Popen([paths.CERTUTIL, "-d", self.ca_agent_db, + p = subprocess.Popen([paths.CERTUTIL, "-d", self.agent_db, "-O", "-n", "ipa-ca-agent"], stdout=subprocess.PIPE) chain = p.stdout.read() @@ -851,7 +766,7 @@ class CAInstance(service.Service): self.__run_certutil( ['-M', '-t', 'CT,C,C', '-n', nick], - database=self.ca_agent_db, pwd_file=self.admin_password) + database=self.agent_db, pwd_file=self.admin_password) finally: os.remove(admin_name) @@ -867,12 +782,13 @@ class CAInstance(service.Service): '-v', '-n', 'ipa-ca-agent', '-p', self.admin_password, - '-d', self.ca_agent_db, + '-d', self.agent_db, '-r', '/ca/agent/ca/profileReview?requestId=%s' % self.requestId, '%s' % ipautil.format_netloc( self.fqdn, self.dogtag_constants.AGENT_SECURE_PORT), ] - (stdout, stderr, returncode) = ipautil.run(args, nolog=(self.admin_password,)) + (stdout, _stderr, _returncode) = ipautil.run( + args, nolog=(self.admin_password,)) data = stdout.split(self.dogtag_constants.RACERT_LINE_SEP) params = get_defList(data) @@ -888,13 +804,14 @@ class CAInstance(service.Service): '-v', '-n', 'ipa-ca-agent', '-p', self.admin_password, - '-d', self.ca_agent_db, + '-d', self.agent_db, '-e', params, '-r', '/ca/agent/ca/profileProcess', '%s' % ipautil.format_netloc( self.fqdn, self.dogtag_constants.AGENT_SECURE_PORT), ] - (stdout, stderr, returncode) = ipautil.run(args, nolog=(self.admin_password,)) + (stdout, _stderr, _returncode) = ipautil.run( + args, nolog=(self.admin_password,)) data = stdout.split(self.dogtag_constants.RACERT_LINE_SEP) outputList = get_outputList(data) @@ -973,7 +890,7 @@ class CAInstance(service.Service): conn.unbind() - def __run_certutil(self, args, database=None, pwd_file=None,stdin=None): + def __run_certutil(self, args, database=None, pwd_file=None, stdin=None): if not database: database = self.ra_agent_db if not pwd_file: @@ -999,7 +916,7 @@ class CAInstance(service.Service): os.close(f) os.chmod(self.ra_agent_pwd, stat.S_IRUSR) - (stdout, stderr, returncode) = self.__run_certutil(["-N"]) + self.__run_certutil(["-N"]) def __get_ca_chain(self): try: @@ -1016,8 +933,8 @@ class CAInstance(service.Service): try: ipautil.run([paths.PK12UTIL, "-n", "ipa-ca-agent", - "-o", paths.CA_AGENT_P12, - "-d", self.ca_agent_db, + "-o", paths.DOGTAG_AGENT_P12, + "-d", self.agent_db, "-k", pwd_name, "-w", pwd_name]) finally: @@ -1035,14 +952,15 @@ class CAInstance(service.Service): # makes openssl throw up. data = base64.b64decode(chain) - (certlist, stderr, returncode) = ipautil.run([paths.OPENSSL, + (certlist, _stderr, _returncode) = ipautil.run( + [paths.OPENSSL, "pkcs7", "-inform", "DER", "-print_certs", ], stdin=data) - # Ok, now we have all the certificates in certs, walk thru it + # Ok, now we have all the certificates in certs, walk through it # and pull out each certificate and add it to our database st = 1 @@ -1057,7 +975,7 @@ class CAInstance(service.Service): (chain_fd, chain_name) = tempfile.mkstemp() os.write(chain_fd, certlist[st:en+25]) os.close(chain_fd) - (rdn, subject_dn) = certs.get_cert_nickname(certlist[st:en+25]) + (_rdn, subject_dn) = certs.get_cert_nickname(certlist[st:en+25]) if subject_dn == ca_dn: nick = get_ca_nickname(self.realm) trust_flags = 'CT,C,C' @@ -1070,7 +988,7 @@ class CAInstance(service.Service): ) finally: os.remove(chain_name) - subid = subid + 1 + subid += 1 def __request_ra_certificate(self): # Create a noise file for generating our private key @@ -1081,7 +999,10 @@ class CAInstance(service.Service): # Generate our CSR. The result gets put into stdout try: - (stdout, stderr, returncode) = self.__run_certutil(["-R", "-k", "rsa", "-g", "2048", "-s", str(DN(('CN', 'IPA RA'), self.subject_base)), "-z", noise_name, "-a"]) + (stdout, _stderr, _returncode) = self.__run_certutil( + ["-R", "-k", "rsa", "-g", "2048", "-s", + str(DN(('CN', 'IPA RA'), self.subject_base)), + "-z", noise_name, "-a"]) finally: os.remove(noise_name) @@ -1298,75 +1219,32 @@ class CAInstance(service.Service): 'OU=pki-ipa, O=IPA', str(self.subject_base)): print "Updating subject_base in CA template failed" - def enable_client_auth_to_db(self): - """ - Enable client auth connection to the internal db. - """ - caconfig = dogtag.install_constants.CS_CFG_PATH - - with stopped_service(self.dogtag_constants.SERVICE_NAME, - instance_name=self.dogtag_constants.PKI_INSTANCE_NAME): - - # Enable file publishing, disable LDAP - installutils.set_directive(caconfig, - 'authz.instance.DirAclAuthz.ldap.ldapauth.authtype', - 'SslClientAuth', quotes=False, separator='=') - installutils.set_directive(caconfig, - 'authz.instance.DirAclAuthz.ldap.ldapauth.bindDN', - 'uid=pkidbuser,ou=people,o=ipa-ca', quotes=False, separator='=') - installutils.set_directive(caconfig, - 'authz.instance.DirAclAuthz.ldap.ldapauth.clientCertNickname', - 'subsystemCert cert-pki-ca', quotes=False, separator='=') - installutils.set_directive(caconfig, - 'authz.instance.DirAclAuthz.ldap.ldapconn.port', - str(dogtag.install_constants.DS_SECURE_PORT), - quotes=False, separator='=') - installutils.set_directive(caconfig, - 'authz.instance.DirAclAuthz.ldap.ldapconn.secureConn', - 'true', quotes=False, separator='=') - - installutils.set_directive(caconfig, 'internaldb.ldapauth.authtype', - 'SslClientAuth', quotes=False, separator='=') - installutils.set_directive(caconfig, 'internaldb.ldapauth.bindDN', - 'uid=pkidbuser,ou=people,o=ipa-ca', quotes=False, separator='=') - installutils.set_directive(caconfig, - 'internaldb.ldapauth.clientCertNickname', - 'subsystemCert cert-pki-ca', quotes=False, separator='=') - installutils.set_directive(caconfig, 'internaldb.ldapconn.port', - str(dogtag.install_constants.DS_SECURE_PORT), - quotes=False, separator='=') - installutils.set_directive(caconfig, - 'internaldb.ldapconn.secureConn', 'true', quotes=False, - separator='=') - def uninstall(self): - if self.is_configured(): - self.print_msg("Unconfiguring CA") - enabled = self.restore_state("enabled") if not enabled is None and not enabled: self.disable() - # Just eat this state if it exists - installed = self.restore_state("installed") - try: - if self.dogtag_constants.DOGTAG_VERSION >= 10: - ipautil.run([paths.PKIDESTROY, "-i", - self.dogtag_constants.PKI_INSTANCE_NAME, - "-s", "CA"]) - else: + if self.dogtag_constants.DOGTAG_VERSION >= 10: + DogtagInstance.uninstall(self) + else: + if self.is_configured(): + self.print_msg("Unconfiguring CA") + + try: ipautil.run([paths.PKIREMOVE, - "-pki_instance_root=/var/lib", + "-pki_instance_root=%s" % paths.VAR_LIB, "-pki_instance_name=%s" % self.dogtag_constants.PKI_INSTANCE_NAME, "--force"]) - except ipautil.CalledProcessError, e: - root_logger.critical("failed to uninstall CA instance %s" % e) + except ipautil.CalledProcessError, e: + self.log.critical("failed to uninstall CA instance %s", e) + + self.restore_state("installed") # At one time we removed this user on uninstall. That can potentially - # orphan files, or worse, if another useradd runs in the intermim, + # orphan files, or worse, if another useradd runs in the interim, # cause files to have a new owner. - user_exists = self.restore_state("user_exists") + self.restore_state("user_exists") services.knownservices.messagebus.start() cmonger = services.knownservices.certmonger @@ -1383,43 +1261,33 @@ class CAInstance(service.Service): cmonger.stop() # remove CRL files - root_logger.info("Remove old CRL files") + self.log.info("Remove old CRL files") try: for f in get_crl_files(): - root_logger.debug("Remove %s", f) + self.log.debug("Remove %s", f) installutils.remove_file(f) except OSError, e: - root_logger.warning("Error while removing old CRL files: %s" % e) + self.log.warning("Error while removing old CRL files: %s", e) # remove CRL directory - root_logger.info("Remove CRL directory") + self.log.info("Remove CRL directory") if os.path.exists(self.dogtag_constants.CRL_PUBLISH_PATH): try: shutil.rmtree(self.dogtag_constants.CRL_PUBLISH_PATH) except OSError, e: - root_logger.warning("Error while removing CRL publish " - "directory: %s" % e) + self.log.warning("Error while removing CRL publish " + "directory: %s", e) def publish_ca_cert(self, location): args = ["-L", "-n", self.canickname, "-a"] - (cert, err, returncode) = self.__run_certutil(args) + (cert, _err, _returncode) = self.__run_certutil(args) fd = open(location, "w+") fd.write(cert) fd.close() os.chmod(location, 0444) - def __http_proxy(self): - template_filename = ipautil.SHARE_DIR + "ipa-pki-proxy.conf" - sub_dict = dict( - DOGTAG_PORT=self.dogtag_constants.AJP_PORT, - CLONE='' if self.clone else '#', - FQDN=self.fqdn, - ) - template = ipautil.template_file(template_filename, sub_dict) - with open(HTTPD_CONFD + "ipa-pki-proxy.conf", "w") as fd: - fd.write(template) - - def configure_certmonger_renewal(self): + @staticmethod + def configure_certmonger_renewal(): """ Create a new CA type for certmonger that will retrieve updated certificates from the dogtag master server. @@ -1450,41 +1318,30 @@ class CAInstance(service.Service): pre_command=None, post_command='renew_ra_cert') except (ipautil.CalledProcessError, RuntimeError), e: - root_logger.error( - "certmonger failed to start tracking certificate: %s" % e) + self.log.error( + "certmonger failed to start tracking certificate: %s", e) def __get_ca_pin(self): try: - return certmonger.get_pin('internal', + return certmonger.get_pin( + 'internal', dogtag_constants=self.dogtag_constants) except IOError, e: raise RuntimeError( 'Unable to determine PIN for CA instance: %s' % e) - def configure_renewal(self): + def configure_cert_renewal(self): + """ + Configure system certificates for renewal. + """ reqs = ( ('auditSigningCert cert-pki-ca', None), ('ocspSigningCert cert-pki-ca', None), ('subsystemCert cert-pki-ca', None), ('caSigningCert cert-pki-ca', 'ipaCACertRenewal'), ) - pin = self.__get_ca_pin() - # Server-Cert cert-pki-ca is renewed per-server - for nickname, profile in reqs: - try: - certmonger.dogtag_start_tracking( - ca='dogtag-ipa-ca-renew-agent', - nickname=nickname, - pin=pin, - pinfile=None, - secdir=self.dogtag_constants.ALIAS_DIR, - pre_command='stop_pkicad', - post_command='renew_ca_cert "%s"' % nickname, - profile=profile) - except (ipautil.CalledProcessError, RuntimeError), e: - root_logger.error( - "certmonger failed to start tracking certificate: %s" % e) + DogtagInstance.configure_renewal(self, reqs) def track_servercert(self): """ @@ -1503,8 +1360,22 @@ class CAInstance(service.Service): pre_command=None, post_command=None) except (ipautil.CalledProcessError, RuntimeError), e: + self.log.error( + "certmonger failed to start tracking certificate: %s", e) + + @staticmethod + def stop_tracking_agent_certificate(dogtag_constants): + """Stop tracking agent certificate. Called on uninstall. + """ + cmonger = services.knownservices.certmonger + services.knownservices.messagebus.start() + cmonger.start() + try: + certmonger.stop_tracking(paths.HTTPD_ALIAS_DIR, nickname='ipaCert') + except (ipautil.CalledProcessError, RuntimeError), e: root_logger.error( - "certmonger failed to start tracking certificate: %s" % e) + "certmonger failed to stop tracking certificate: %s", e) + cmonger.stop() def enable_subject_key_identifier(self): """ @@ -1517,7 +1388,7 @@ class CAInstance(service.Service): # this is the default setting from pki-ca/pki-tomcat. Don't touch it # if a user has manually modified it. if setlist == '1,2,3,4,5,6,7,8' or setlist == '1,2,3,4,5,6,7,8,9': - setlist = setlist + ',10' + setlist += ',10' installutils.set_directive( self.dogtag_constants.IPA_SERVICE_PROFILE, 'policyset.serverCertSet.list', @@ -1565,7 +1436,7 @@ class CAInstance(service.Service): # this is the default setting from pki-ca/pki-tomcat. Don't touch it # if a user has manually modified it. if setlist == '1,2,3,4,5,6,7,8,10' or setlist == '1,2,3,4,5,6,7,8,9,10': - setlist = setlist + ',11' + setlist += ',11' installutils.set_directive( self.dogtag_constants.IPA_SERVICE_PROFILE, 'policyset.serverCertSet.list', @@ -1608,13 +1479,14 @@ class CAInstance(service.Service): """ # Check the default validity period of the audit signing cert # and set it to 2 years if it is 6 months. - range = installutils.get_directive( + cert_range = installutils.get_directive( '%s/caSignedLogCert.cfg' % self.dogtag_constants.SERVICE_PROFILE_DIR, 'policyset.caLogSigningSet.2.default.params.range', separator='=' ) - root_logger.debug('caSignedLogCert.cfg profile validity range is %s' % range) - if range == "180": + self.log.debug( + 'caSignedLogCert.cfg profile validity range is %s', cert_range) + if cert_range == "180": installutils.set_directive( '%s/caSignedLogCert.cfg' % self.dogtag_constants.SERVICE_PROFILE_DIR, 'policyset.caLogSigningSet.2.default.params.range', @@ -1629,7 +1501,8 @@ class CAInstance(service.Service): quotes=False, separator='=' ) - root_logger.debug('updated caSignedLogCert.cfg profile validity range to 720') + self.log.debug( + 'updated caSignedLogCert.cfg profile validity range to 720') return True return False @@ -1642,9 +1515,9 @@ class CAInstance(service.Service): dn = DN(('cn', 'CA'), ('cn', fqdn), ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn) - filter = '(ipaConfigString=caRenewalMaster)' + renewal_filter = '(ipaConfigString=caRenewalMaster)' try: - self.admin_conn.get_entries(base_dn=dn, filter=filter, + self.admin_conn.get_entries(base_dn=dn, filter=renewal_filter, attrs_list=[]) except errors.NotFound: return False @@ -1684,6 +1557,31 @@ class CAInstance(service.Service): self.admin_conn.update_entry(master_entry) + @staticmethod + def update_cert_config(nickname, cert, dogtag_constants=None): + """ + When renewing a CA subsystem certificate the configuration file + needs to get the new certificate as well. + + nickname is one of the known nicknames. + cert is a DER-encoded certificate. + """ + + if dogtag_constants is None: + dogtag_constants = dogtag.configured_constants() + + # The cert directive to update per nickname + directives = {'auditSigningCert cert-pki-ca': 'ca.audit_signing.cert', + 'ocspSigningCert cert-pki-ca': 'ca.ocsp_signing.cert', + 'caSigningCert cert-pki-ca': 'ca.signing.cert', + 'subsystemCert cert-pki-ca': 'ca.subsystem.cert', + 'Server-Cert cert-pki-ca': 'ca.sslserver.cert'} + + DogtagInstance.update_cert_cs_cfg( + nickname, cert, directives, + dogtag.configured_constants().CS_CFG_PATH, + dogtag_constants) + def replica_ca_install_check(config): if not config.setup_ca: return @@ -1769,11 +1667,6 @@ def install_replica_ca(config, postinstall=False): if ca.is_installed(): sys.exit("A CA is already configured on this system.") - pkcs12_info = None - if ipautil.file_exists(config.dir + "/dogtagcert.p12"): - pkcs12_info = (config.dir + "/dogtagcert.p12", - config.dir + "/dirsrv_pin.txt") - ca = CAInstance(config.realm_name, certs.NSS_DIR, dogtag_constants=dogtag.install_constants) if postinstall: @@ -1812,32 +1705,6 @@ def install_replica_ca(config, postinstall=False): return ca -def update_cert_config(nickname, cert, dogtag_constants=None): - """ - When renewing a CA subsystem certificate the configuration file - needs to get the new certificate as well. - - nickname is one of the known nicknames. - cert is a DER-encoded certificate. - """ - - if dogtag_constants is None: - dogtag_constants = dogtag.configured_constants() - - # The cert directive to update per nickname - directives = {'auditSigningCert cert-pki-ca': 'ca.audit_signing.cert', - 'ocspSigningCert cert-pki-ca': 'ca.ocsp_signing.cert', - 'caSigningCert cert-pki-ca': 'ca.signing.cert', - 'subsystemCert cert-pki-ca': 'ca.subsystem.cert', - 'Server-Cert cert-pki-ca': 'ca.sslserver.cert'} - - with stopped_service(dogtag_constants.SERVICE_NAME, - instance_name=dogtag_constants.PKI_INSTANCE_NAME): - - installutils.set_directive(dogtag.configured_constants().CS_CFG_PATH, - directives[nickname], - base64.b64encode(cert), - quotes=False, separator='=') def update_people_entry(dercert): """ @@ -1874,11 +1741,11 @@ def update_people_entry(dercert): conn.connect( bind_dn=DN(('cn', 'directory manager')), bind_pw=dm_password) - filter = conn.make_filter( + db_filter = conn.make_filter( {'description': ';%s;%s' % (issuer, subject)}, exact=False, trailing_wildcard=False) try: - entries = conn.get_entries(base_dn, conn.SCOPE_SUBTREE, filter) + entries = conn.get_entries(base_dn, conn.SCOPE_SUBTREE, db_filter) except errors.NotFound: entries = [] diff --git a/ipaserver/install/dogtaginstance.py b/ipaserver/install/dogtaginstance.py new file mode 100644 index 000000000..c872f3103 --- /dev/null +++ b/ipaserver/install/dogtaginstance.py @@ -0,0 +1,399 @@ +# Authors: Ade Lee <alee@redhat.com> +# +# Copyright (C) 2014 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import base64 +import os +import shutil +import tempfile +import traceback + +from pki.client import PKIConnection +import pki.system + +from ipaplatform import services +from ipaplatform.paths import paths +from ipapython import certmonger +from ipapython import dogtag +from ipapython import ipaldap +from ipapython import ipautil +from ipapython.dn import DN +from ipaserver.install import service +from ipaserver.install import installutils +from ipaserver.install.installutils import stopped_service +from ipapython.ipa_log_manager import log_mgr + +DEFAULT_DSPORT = dogtag.install_constants.DS_PORT + +PKI_USER = "pkiuser" +PKI_DS_USER = dogtag.install_constants.DS_USER + + +def check_inst(subsystem): + """ + Validate that the appropriate dogtag/RHCS packages have been installed. + """ + + # Check for a couple of binaries we need + if not os.path.exists(dogtag.install_constants.SPAWN_BINARY): + return False + if not os.path.exists(dogtag.install_constants.DESTROY_BINARY): + return False + + if not os.path.exists(paths.PKI_CONF_SERVER_XML_TEMPLATE % subsystem): + return False + + return True + + +def get_security_domain(): + """ + Get the security domain from the REST interface on the local Dogtag CA + This function will succeed if the local dogtag CA is up. + """ + connection = PKIConnection() + domain_client = pki.system.SecurityDomainClient(connection) + info = domain_client.get_security_domain_info() + return info + + +def is_installing_replica(sys_type): + """ + We expect only one of each type of Dogtag subsystem in an IPA deployment. + That means that if a subsystem of the specified type has already been + deployed - and therefore appears in the security domain - then we must be + installing a replica. + """ + info = get_security_domain() + try: + sys_list = info.systems[sys_type] + return len(sys_list.hosts) > 0 + except KeyError: + return False + + +class DogtagInstance(service.Service): + """ + This is the base class for a Dogtag 10+ instance, which uses a + shared tomcat instance and DS to host the relevant subsystems. + + It contains functions that will be common to installations of the + CA, KRA, and eventually TKS and TPS. + """ + + def __init__(self, realm, subsystem, service_desc, dogtag_constants=None, + host_name=None, dm_password=None, ldapi=True): + """Initializer""" + + if dogtag_constants is None: + dogtag_constants = dogtag.configured_constants() + + super(DogtagInstance, self).__init__( + '%sd' % dogtag_constants.PKI_INSTANCE_NAME, + service_desc=service_desc, + dm_password=dm_password, + ldapi=ldapi + ) + + self.dogtag_constants = dogtag_constants + self.realm = realm + self.dm_password = None + self.admin_password = None + self.fqdn = host_name + self.domain = None + self.pkcs12_info = None + self.clone = False + + self.basedn = DN(('o', 'ipa%s' % subsystem.lower())) + self.admin_user = DN(('uid', 'admin'), ('ou', 'people'), ('o', 'ipaca')) + self.agent_db = tempfile.mkdtemp(prefix="tmp-") + self.ds_port = DEFAULT_DSPORT + self.server_root = dogtag_constants.SERVER_ROOT + self.subsystem = subsystem + self.security_domain_name = "IPA" + self.tracking_reqs = None + + # replication parameters + self.master_host = None + self.master_replication_port = None + self.subject_base = None + + self.log = log_mgr.get_logger(self) + + def __del__(self): + shutil.rmtree(self.agent_db, ignore_errors=True) + + def is_installed(self): + """ + Determine if subsystem instance has been installed. + + Returns True/False + """ + return os.path.exists(os.path.join( + self.server_root, self.dogtag_constants.PKI_INSTANCE_NAME, + self.subsystem.lower())) + + def spawn_instance(self, cfg_file, nolog_list=None): + """ + Create and configure a new Dogtag instance using pkispawn. + Passes in a configuration file with IPA-specific + parameters. + """ + subsystem = self.subsystem + + # Define the things we don't want logged + if nolog_list is None: + nolog_list = [] + nolog = tuple(nolog_list) + (self.admin_password, self.dm_password) + + args = [paths.PKISPAWN, + "-s", subsystem, + "-f", cfg_file] + + with open(cfg_file) as f: + self.log.debug( + 'Contents of pkispawn configuration file (%s):\n%s', + cfg_file, ipautil.nolog_replace(f.read(), nolog)) + + try: + ipautil.run(args, nolog=nolog) + except ipautil.CalledProcessError, e: + self.log.critical("failed to configure %s instance %s", + subsystem, e) + raise RuntimeError('Configuration of %s failed' % subsystem) + + def enable(self): + self.backup_state("enabled", self.is_enabled()) + # We do not let the system start IPA components on its own, + # Instead we reply on the IPA init script to start only enabled + # components as found in our LDAP configuration tree + # We need to install DS before we can actually ldap_enable a service. + # so actual enablement is delayed. + + def restart_instance(self): + try: + self.restart(self.dogtag_constants.PKI_INSTANCE_NAME) + except Exception: + self.log.debug(traceback.format_exc()) + self.log.critical( + "Failed to restart the Dogtag instance." + "See the installation log for details.") + + def start_instance(self): + try: + self.start(self.dogtag_constants.PKI_INSTANCE_NAME) + except Exception: + self.log.debug(traceback.format_exc()) + self.log.critical( + "Failed to restart the Dogtag instance." + "See the installation log for details.") + + def stop_instance(self): + try: + self.stop(self.dogtag_constants.PKI_INSTANCE_NAME) + except Exception: + self.log.debug(traceback.format_exc()) + self.log.critical( + "Failed to restart the Dogtag instance." + "See the installation log for details.") + + def enable_client_auth_to_db(self, config): + """ + Enable client auth connection to the internal db. + Path to CS.cfg config file passed in. + """ + + with stopped_service( + self.dogtag_constants.SERVICE_NAME, + instance_name=self.dogtag_constants.PKI_INSTANCE_NAME): + installutils.set_directive( + config, + 'authz.instance.DirAclAuthz.ldap.ldapauth.authtype', + 'SslClientAuth', quotes=False, separator='=') + installutils.set_directive( + config, + 'authz.instance.DirAclAuthz.ldap.ldapauth.bindDN', + 'uid=pkidbuser,ou=people,o=ipaca', quotes=False, separator='=') + installutils.set_directive( + config, + 'authz.instance.DirAclAuthz.ldap.ldapauth.clientCertNickname', + 'subsystemCert cert-pki-ca', quotes=False, separator='=') + installutils.set_directive( + config, + 'authz.instance.DirAclAuthz.ldap.ldapconn.port', + str(dogtag.install_constants.DS_SECURE_PORT), + quotes=False, separator='=') + installutils.set_directive( + config, + 'authz.instance.DirAclAuthz.ldap.ldapconn.secureConn', + 'true', quotes=False, separator='=') + + installutils.set_directive( + config, + 'internaldb.ldapauth.authtype', + 'SslClientAuth', quotes=False, separator='=') + + installutils.set_directive( + config, + 'internaldb.ldapauth.bindDN', + 'uid=pkidbuser,ou=people,o=ipaca', quotes=False, separator='=') + installutils.set_directive( + config, + 'internaldb.ldapauth.clientCertNickname', + 'subsystemCert cert-pki-ca', quotes=False, separator='=') + installutils.set_directive( + config, + 'internaldb.ldapconn.port', + str(dogtag.install_constants.DS_SECURE_PORT), + quotes=False, separator='=') + installutils.set_directive( + config, + 'internaldb.ldapconn.secureConn', 'true', quotes=False, + separator='=') + + def uninstall(self): + if self.is_installed(): + self.print_msg("Unconfiguring %s" % self.subsystem) + + try: + ipautil.run([paths.PKIDESTROY, "-i", + self.dogtag_constants.PKI_INSTANCE_NAME, + "-s", self.subsystem]) + except ipautil.CalledProcessError, e: + self.log.critical("failed to uninstall %s instance %s", + self.subsystem, e) + + def http_proxy(self): + """ Update the http proxy file """ + template_filename = ipautil.SHARE_DIR + "ipa-pki-proxy.conf" + sub_dict = dict( + DOGTAG_PORT=self.dogtag_constants.AJP_PORT, + CLONE='' if self.clone else '#', + FQDN=self.fqdn, + ) + template = ipautil.template_file(template_filename, sub_dict) + with open(paths.HTTPD_IPA_PKI_PROXY_CONF, "w") as fd: + fd.write(template) + + def __get_pin(self): + try: + return certmonger.get_pin('internal', + dogtag_constants=self.dogtag_constants) + except IOError, e: + self.log.debug( + 'Unable to determine PIN for the Dogtag instance: %s', e) + raise RuntimeError(e) + + def configure_renewal(self, reqs=None): + """ Configure certmonger to renew system certs + + @param reqs: list of nicknames and profiles + """ + cmonger = services.knownservices.certmonger + cmonger.enable() + services.knownservices.messagebus.start() + cmonger.start() + + pin = self.__get_pin() + + if reqs is None: + reqs = self.tracking_reqs + + for nickname, profile in reqs: + try: + certmonger.dogtag_start_tracking( + ca='dogtag-ipa-ca-renew-agent', + nickname=nickname, + pin=pin, + pinfile=None, + secdir=self.dogtag_constants.ALIAS_DIR, + pre_command='stop_pkicad', + post_command='renew_ca_cert "%s"' % nickname, + profile=profile) + except (ipautil.CalledProcessError, RuntimeError), e: + self.log.error( + "certmonger failed to start tracking certificate: %s", e) + + def stop_tracking_certificates(self, dogtag_constants, reqs=None): + """Stop tracking our certificates. Called on uninstall. + """ + cmonger = services.knownservices.certmonger + services.knownservices.messagebus.start() + cmonger.start() + + if reqs is None: + reqs = self.tracking_reqs + + for nickname, _profile in reqs: + try: + certmonger.stop_tracking( + dogtag_constants.ALIAS_DIR, nickname=nickname) + except (ipautil.CalledProcessError, RuntimeError), e: + self.log.error( + "certmonger failed to stop tracking certificate: %s", e) + + cmonger.stop() + + @staticmethod + def update_cert_cs_cfg(nickname, cert, directives, cs_cfg, + dogtag_constants=None): + """ + When renewing a Dogtag subsystem certificate the configuration file + needs to get the new certificate as well. + + nickname is one of the known nicknames. + cert is a DER-encoded certificate. + directives is the list of directives to be updated for the subsystem + cs_cfg is the path to the CS.cfg file + """ + + if dogtag_constants is None: + dogtag_constants = dogtag.configured_constants() + + with stopped_service(dogtag_constants.SERVICE_NAME, + instance_name=dogtag_constants.PKI_INSTANCE_NAME): + installutils.set_directive( + cs_cfg, + directives[nickname], + base64.b64encode(cert), + quotes=False, + separator='=') + + def get_admin_cert(self): + """ + Get the certificate for the admin user by checking the ldap entry + for the user. There should be only one certificate per user. + """ + self.log.debug('Trying to find the certificate for the admin user') + conn = None + + try: + conn = ipaldap.IPAdmin(self.fqdn, self.ds_port) + conn.do_simple_bind( + DN(('cn', 'Directory Manager')), + self.dm_password) + + entry_attrs = conn.get_entry(self.admin_user, ['usercertificate']) + admin_cert = entry_attrs.get('usercertificate')[0] + + # TODO(edewata) Add check to warn if there is more than one cert. + finally: + if conn is not None: + conn.unbind() + + return base64.b64encode(admin_cert) diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py index 242e04d99..1719df46d 100644 --- a/ipaserver/install/dsinstance.py +++ b/ipaserver/install/dsinstance.py @@ -25,7 +25,6 @@ import os import re import time import tempfile -import base64 import stat import grp @@ -38,7 +37,9 @@ import ldap from ipaserver.install import ldapupdate from ipaserver.install import replication from ipaserver.install import sysupgrade -from ipalib import errors, certstore +from ipalib import api +from ipalib import certstore +from ipalib import errors from ipaplatform.tasks import tasks from ipalib.constants import CACERT from ipapython.dn import DN @@ -952,3 +953,85 @@ class DsInstance(service.Service): pass self.ldap_disconnect() + + def find_subject_base(self): + """ + Try to find the current value of certificate subject base. + 1) Look in sysupgrade first + 2) If no value is found there, look in DS (start DS if necessary) + 3) Last resort, look in the certmap.conf itself + 4) If all fails, log loudly and return None + + Note that this method can only be executed AFTER the ipa server + is configured, the api is initialized elsewhere and + that a ticket already have been acquired. + """ + root_logger.debug( + 'Trying to find certificate subject base in sysupgrade') + subject_base = sysupgrade.get_upgrade_state( + 'certmap.conf', 'subject_base') + + if subject_base: + root_logger.debug( + 'Found certificate subject base in sysupgrade: %s', + subject_base) + return subject_base + + root_logger.debug( + 'Unable to find certificate subject base in sysupgrade') + root_logger.debug( + 'Trying to find certificate subject base in DS') + + ds_is_running = is_ds_running() + if not ds_is_running: + try: + self.start() + ds_is_running = True + except ipautil.CalledProcessError as e: + root_logger.error('Cannot start DS to find certificate ' + 'subject base: %s', e) + + if ds_is_running: + try: + api.Backend.ldap2.connect(autobind=True) + ret = api.Command['config_show']() + subject_base = str( + ret['result']['ipacertificatesubjectbase'][0]) + root_logger.debug( + 'Found certificate subject base in DS: %s', subject_base) + except errors.PublicError, e: + root_logger.error('Cannot connect to DS to find certificate ' + 'subject base: %s', e) + finally: + try: + api.Backend.ldap2.disconnect() + except Exception: + pass + + if not subject_base: + root_logger.debug('Unable to find certificate subject base in DS') + root_logger.debug('Trying to find certificate subject base in ' + 'certmap.conf') + + certmap_dir = config_dirname( + realm_to_serverid(api.env.realm) + ) + try: + with open(os.path.join(certmap_dir, 'certmap.conf')) as f: + for line in f: + if line.startswith('certmap ipaca'): + subject_base = line.strip().split(',')[-1] + root_logger.debug( + 'Found certificate subject base in certmap.conf: ' + '%s', subject_base) + + except IOError as e: + root_logger.error('Cannot open certmap.conf to find certificate ' + 'subject base: %s', e.strerror) + + if subject_base: + return subject_base + + root_logger.debug('Unable to find certificate subject base in ' + 'certmap.conf') + return None diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py index 6ad7106b5..dc98d7a51 100644 --- a/ipaserver/install/installutils.py +++ b/ipaserver/install/installutils.py @@ -35,9 +35,9 @@ from dns.exception import DNSException import ldap from nss.error import NSPRError -from ipapython import ipautil, sysrestore, admintool, dogtag +from ipapython import ipautil, sysrestore, admintool, dogtag, version from ipapython.admintool import ScriptError -from ipapython.ipa_log_manager import * +from ipapython.ipa_log_manager import root_logger from ipalib.util import validate_hostname from ipapython import config from ipalib import errors, x509 @@ -68,7 +68,7 @@ class HostnameLocalhost(HostLookupError): pass class ReplicaConfig: - def __init__(self): + def __init__(self, top_dir=None): self.realm_name = "" self.domain_name = "" self.master_host_name = "" @@ -78,6 +78,7 @@ class ReplicaConfig: self.subject_base = None self.setup_ca = False self.version = 0 + self.top_dir = top_dir subject_base = ipautil.dn_attribute_property('_subject_base') @@ -174,7 +175,7 @@ def verify_fqdn(host_name, no_host_dns=False, local_hostname=True): raise HostReverseLookupError("The host name %s does not match the reverse lookup %s" % (host_name, revname)) verified.add(address) -def record_in_hosts(ip, host_name=None, file=paths.HOSTS): +def record_in_hosts(ip, host_name=None, conf_file=paths.HOSTS): """ Search record in /etc/hosts - static table lookup for hostnames @@ -184,9 +185,9 @@ def record_in_hosts(ip, host_name=None, file=paths.HOSTS): :param ip: IP address :param host_name: Optional hostname to search - :param file: Optional path to the lookup table + :param conf_file: Optional path to the lookup table """ - hosts = open(file, 'r').readlines() + hosts = open(conf_file, 'r').readlines() for line in hosts: line = line.rstrip('\n') fields = line.partition('#')[0].split() @@ -206,13 +207,13 @@ def record_in_hosts(ip, host_name=None, file=paths.HOSTS): return None return (hosts_ip, names) except IndexError: - print "Warning: Erroneous line '%s' in %s" % (line, file) + print "Warning: Erroneous line '%s' in %s" % (line, conf_file) continue return None -def add_record_to_hosts(ip, host_name, file=paths.HOSTS): - hosts_fd = open(file, 'r+') +def add_record_to_hosts(ip, host_name, conf_file=paths.HOSTS): + hosts_fd = open(conf_file, 'r+') hosts_fd.seek(0, 2) hosts_fd.write(ip+'\t'+host_name+' '+host_name.split('.')[0]+'\n') hosts_fd.close() @@ -512,20 +513,20 @@ def expand_replica_info(filename, password): """ top_dir = tempfile.mkdtemp("ipa") tarfile = top_dir+"/files.tar" - dir = top_dir + "/realm_info" + dir_path = top_dir + "/realm_info" ipautil.decrypt_file(filename, tarfile, password, top_dir) ipautil.run(["tar", "xf", tarfile, "-C", top_dir]) os.remove(tarfile) - return top_dir, dir + return top_dir, dir_path -def read_replica_info(dir, rconfig): +def read_replica_info(dir_path, rconfig): """ Read the contents of a replica installation file. rconfig is a ReplicaConfig object """ - filename = dir + "/realm_info" + filename = dir_path + "/realm_info" fd = open(filename) config = SafeConfigParser() config.readfp(fd) @@ -556,6 +557,67 @@ def read_replica_info_dogtag_port(config_dir): return dogtag_master_ds_port +def read_replica_info_kra_enabled(config_dir): + """ + Check the replica info to determine if a KRA has been installed + on the master + """ + default_file = config_dir + "/default.conf" + if not ipautil.file_exists(default_file): + return False + else: + with open(default_file) as fd: + config = SafeConfigParser() + config.readfp(fd) + + enable_kra = bool(config.get("global", "enable_kra")) + return enable_kra + + +def create_replica_config(dirman_password, filename, options): + top_dir = None + try: + top_dir, dir = expand_replica_info(filename, dirman_password) + except Exception, e: + root_logger.error("Failed to decrypt or open the replica file.") + print "ERROR: Failed to decrypt or open the replica file." + print "Verify you entered the correct Directory Manager password." + sys.exit(1) + config = ReplicaConfig(top_dir) + read_replica_info(dir, config) + root_logger.debug( + 'Installing replica file with version %d (0 means no version in prepared file).', + config.version) + if config.version and config.version > version.NUM_VERSION: + root_logger.error( + 'A replica file from a newer release (%d) cannot be installed on an older version (%d)', + config.version, version.NUM_VERSION) + sys.exit(1) + config.dirman_password = dirman_password + try: + host = get_host_name(options.no_host_dns) + except BadHostError, e: + root_logger.error(str(e)) + sys.exit(1) + if config.host_name != host: + try: + print "This replica was created for '%s' but this machine is named '%s'" % (config.host_name, host) + if not ipautil.user_input("This may cause problems. Continue?", False): + root_logger.debug( + "Replica was created for %s but machine is named %s " + "User chose to exit", + config.host_name, host) + sys.exit(0) + config.host_name = host + print "" + except KeyboardInterrupt: + root_logger.debug("Keyboard Interrupt") + sys.exit(0) + config.dir = dir + config.ca_ds_port = read_replica_info_dogtag_port(config.dir) + return config + + def check_server_configuration(): """ Check if IPA server is configured on the system. @@ -572,6 +634,7 @@ def check_server_configuration(): if not server_fstore.has_files(): raise RuntimeError("IPA is not configured on this system.") + def remove_file(filename): """ Remove a file and log any exceptions raised. @@ -582,6 +645,7 @@ def remove_file(filename): except Exception, e: root_logger.error('Error removing %s: %s' % (filename, str(e))) + def rmtree(path): """ Remove a directory structure and log any exceptions raised. @@ -592,6 +656,7 @@ def rmtree(path): except Exception, e: root_logger.error('Error removing %s: %s' % (path, str(e))) + def is_ipa_configured(): """ Using the state and index install files determine if IPA is already @@ -764,7 +829,7 @@ def check_pkcs12(pkcs12_info, ca_file, hostname): raise ScriptError( '%s server certificates found in %s, expecting only one' % (len(server_certs), pkcs12_filename)) - [(server_cert_name, server_cert_trust)] = server_certs + [(server_cert_name, _server_cert_trust)] = server_certs # Check we have the whole cert chain & the CA is in it trust_chain = nssdb.get_trust_chain(server_cert_name) @@ -849,23 +914,23 @@ def stopped_service(service, instance_name=""): root_logger.debug('Starting %s%s.', service, log_instance_name) services.knownservices[service].start(instance_name) + def check_entropy(): - ''' + """ Checks if the system has enough entropy, if not, displays warning message - ''' + """ try: - with open('/proc/sys/kernel/random/entropy_avail', 'r') as efname: + with open(paths.ENTROPY_AVAIL, 'r') as efname: if int(efname.read()) < 200: emsg = 'WARNING: Your system is running out of entropy, ' \ 'you may experience long delays' service.print_msg(emsg) root_logger.debug(emsg) except IOError as e: - root_logger.debug("Could not open /proc/sys/kernel/random/entropy_avail: %s" % \ - e) + root_logger.debug( + "Could not open %s: %s", paths.ENTROPY_AVAIL, e) except ValueError as e: - root_logger.debug("Invalid value in /proc/sys/kernel/random/entropy_avail %s" % \ - 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 diff --git a/ipaserver/install/ipa_backup.py b/ipaserver/install/ipa_backup.py index 8f27e8a60..0830eb0c5 100644 --- a/ipaserver/install/ipa_backup.py +++ b/ipaserver/install/ipa_backup.py @@ -157,7 +157,7 @@ class Backup(admintool.AdminTool): paths.NTP_CONF, paths.SMB_CONF, paths.SAMBA_KEYTAB, - paths.CA_AGENT_P12, + paths.DOGTAG_AGENT_P12, paths.CACERT_P12, paths.KRB5KDC_KDC_CONF, paths.SYSTEMD_IPA_SERVICE, diff --git a/ipaserver/install/ipa_kra_install.py b/ipaserver/install/ipa_kra_install.py new file mode 100644 index 000000000..2c4f2dcaa --- /dev/null +++ b/ipaserver/install/ipa_kra_install.py @@ -0,0 +1,243 @@ +#! /usr/bin/python2 -E +# Authors: Ade Lee <alee@redhat.com> +# +# Copyright (C) 2014 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +from ConfigParser import RawConfigParser +from textwrap import dedent +from ipalib import api +from ipaplatform import services +from ipaplatform.paths import paths +from ipapython import admintool +from ipapython import dogtag +from ipapython import ipautil +from ipaserver.install import cainstance +from ipaserver.install import dogtaginstance +from ipaserver.install import krainstance +from ipaserver.install import dsinstance +from ipaserver.install import installutils +from ipaserver.install import service +from ipaserver.install.installutils import ( + read_replica_info_kra_enabled, create_replica_config) + + +class KRAInstall(admintool.AdminTool): + + command_name = 'ipa-kra-install' + + usage = "%prog [options] [replica_file]" + + description = "Install a master or replica KRA." + + @classmethod + def add_options(cls, parser, debug_option=True): + super(KRAInstall, cls).add_options(parser, debug_option=True) + + parser.add_option( + "-p", "--password", + dest="password", sensitive=True, + help="Directory Manager (existing master) password") + + parser.add_option( + "-U", "--unattended", + dest="unattended", action="store_true", default=False, + help="unattended installation never prompts the user") + + parser.add_option( + "--uninstall", + dest="uninstall", action="store_true", default=False, + help="uninstall an existing installation. The uninstall can " + "be run with --unattended option") + + def validate_options(self, needs_root=True): + super(KRAInstall, self).validate_options(needs_root=True) + + installutils.check_server_configuration() + + if self.options.unattended and self.options.password is None: + self.option_parser.error( + "Directory Manager password must be specified using -p" + " in unattended mode" + ) + + api.bootstrap(in_server=True) + api.finalize() + + def ask_for_options(self): + super(KRAInstall, self).ask_for_options() + + if not self.options.password: + self.options.password = installutils.read_password( + "Directory Manager", confirm=False, + validate=False, retry=False) + if self.options.password is None: + raise admintool.ScriptError( + "Directory Manager password required") + + @classmethod + def get_command_class(cls, options, args): + if options.uninstall: + return KRAUninstaller + else: + return KRAInstaller + + +class KRAUninstaller(KRAInstall): + log_file_name = paths.PKI_KRA_UNINSTALL_LOG + + def validate_options(self, needs_root=True): + super(KRAUninstaller, self).validate_options(needs_root=True) + + if self.args: + self.option_parser.error("Too many parameters provided.") + + if not api.env.enable_kra: + self.option_parser.error( + "Cannot uninstall. There is no KRA installed on this system." + ) + + def run(self): + super(KRAUninstaller, self).run() + dogtag_constants = dogtag.configured_constants() + + # temporarily disable uninstall until Dogtag ticket: + # https://fedorahosted.org/pki/ticket/1113 is fixed + # TODO(alee) remove this once the above ticket is fixed + raise admintool.ScriptError( + "Uninstall is temporarily disabled. To uninstall, please " + "use ipa-server-install --uninstall" + ) + + kra_instance = krainstance.KRAInstance( + api.env.realm, dogtag_constants=dogtag_constants) + kra_instance.stop_tracking_certificates(dogtag_constants) + if kra_instance.is_installed(): + kra_instance.uninstall() + + # Update config file + parser = RawConfigParser() + parser.read(paths.IPA_DEFAULT_CONF) + parser.set('global', 'enable_kra', 'False') + + with open(paths.IPA_DEFAULT_CONF, 'w') as f: + parser.write(f) + + +class KRAInstaller(KRAInstall): + log_file_name = paths.PKI_KRA_INSTALL_LOG + + INSTALLER_START_MESSAGE = ''' + =================================================================== + This program will setup Dogtag KRA for the FreeIPA Server. + + ''' + + FAIL_MESSAGE = ''' + Your system may be partly configured. + Run ipa-kra-install --uninstall to clean up. + ''' + + def validate_options(self, needs_root=True): + super(KRAInstaller, self).validate_options(needs_root=True) + + dogtag_version = int(api.env.dogtag_version) + enable_kra = api.env.enable_kra + + if enable_kra: + self.option_parser.error("KRA is already installed.") + + ca_installed = cainstance.is_ca_installed_locally() + + if ca_installed: + if dogtag_version >= 10: + # correct dogtag version of CA installed + pass + else: + self.option_parser.error( + "Dogtag must be version 10.2 or above to install KRA") + else: + self.option_parser.error( + "Dogtag CA is not installed. Please install the CA first") + + self.installing_replica = dogtaginstance.is_installing_replica("KRA") + if self.installing_replica: + if not self.args: + self.option_parser.error("A replica file is required.") + if len(self.args) > 1: + self.option_parser.error("Too many arguments provided") + + self.replica_file = self.args[0] + if not ipautil.file_exists(self.replica_file): + self.option_parser.error( + "Replica file %s does not exist" % self.replica_file) + else: + if self.args: + self.option_parser.error("Too many parameters provided. " + "No replica file is required.") + + def _run(self): + super(KRAInstaller, self).run() + print dedent(self.INSTALLER_START_MESSAGE) + + subject = dsinstance.DsInstance().find_subject_base() + if not self.installing_replica: + kra = krainstance.KRAInstance( + api.env.realm, + dogtag_constants=dogtag.install_constants) + + kra.configure_instance( + api.env.host, api.env.domain, self.options.password, + self.options.password, subject_base=subject) + else: + replica_config = create_replica_config( + self.options.password, + self.replica_file, + self.options) + + if not read_replica_info_kra_enabled(replica_config.dir): + raise admintool.ScriptError( + "Either KRA is not installed on the master system or " + "your replica file is out of date" + ) + + kra = krainstance.install_replica_kra(replica_config) + service.print_msg("Restarting the directory server") + + ds = dsinstance.DsInstance() + ds.restart() + + kra.enable_client_auth_to_db(kra.dogtag_constants.KRA_CS_CFG_PATH) + + # Restart apache for new proxy config file + services.knownservices.httpd.restart(capture_output=True) + + # Update config file + parser = RawConfigParser() + parser.read(paths.IPA_DEFAULT_CONF) + parser.set('global', 'enable_kra', 'True') + + with open(paths.IPA_DEFAULT_CONF, 'w') as f: + parser.write(f) + + def run(self): + try: + self._run() + except: + self.log.error(dedent(self.FAIL_MESSAGE)) + raise + diff --git a/ipaserver/install/ipa_replica_prepare.py b/ipaserver/install/ipa_replica_prepare.py index 81b54211f..1099046dd 100644 --- a/ipaserver/install/ipa_replica_prepare.py +++ b/ipaserver/install/ipa_replica_prepare.py @@ -371,6 +371,7 @@ class ReplicaPrepare(admintool.AdminTool): cacert_filename = paths.CACERT_PEM if ipautil.file_exists(cacert_filename): self.copy_info_file(cacert_filename, "cacert.pem") + self.copy_info_file(paths.IPA_DEFAULT_CONF, "default.conf") def save_config(self): self.log.info("Finalizing configuration") diff --git a/ipaserver/install/krainstance.py b/ipaserver/install/krainstance.py new file mode 100644 index 000000000..182e8e034 --- /dev/null +++ b/ipaserver/install/krainstance.py @@ -0,0 +1,346 @@ +# Authors: Ade Lee <alee@redhat.com> +# +# Copyright (C) 2014 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import ConfigParser +import os +import pwd +import shutil +import sys +import tempfile + +from ipalib import api +from ipaplatform import services +from ipaplatform.paths import paths +from ipapython import dogtag +from ipapython import ipaldap +from ipapython import ipautil +from ipapython.dn import DN +from ipaserver.install import certs +from ipaserver.install import cainstance +from ipaserver.install import service +from ipaserver.install.dogtaginstance import DogtagInstance +from ipaserver.install.dogtaginstance import DEFAULT_DSPORT, PKI_USER +from ipapython.ipa_log_manager import log_mgr + +# When IPA is installed with DNS support, this CNAME should hold all IPA +# replicas with KRA configured +IPA_KRA_RECORD = "ipa-kra" + + +class KRAInstance(DogtagInstance): + """ + We assume that the CA has already been installed, and we use the + same tomcat instance to host both the CA and KRA. + The mod_nss database will contain the RA agent cert that will be used + to do authenticated requests against dogtag. The RA agent cert will + be the same for both the CA and KRA. + """ + + def __init__(self, realm, dogtag_constants=None): + if dogtag_constants is None: + dogtag_constants = dogtag.configured_constants() + + super(KRAInstance, self).__init__( + realm=realm, + subsystem="KRA", + service_desc="KRA server", + dogtag_constants=dogtag_constants + ) + + self.basedn = DN(('o', 'kra'), ('o', 'ipaca')) + self.tracking_reqs = (('auditSigningCert cert-pki-kra', None), + ('transportCert cert-pki-kra', None), + ('storageCert cert-pki-kra', None)) + self.log = log_mgr.get_logger(self) + + def configure_instance(self, host_name, domain, dm_password, + admin_password, ds_port=DEFAULT_DSPORT, + pkcs12_info=None, master_host=None, + master_replication_port=None, + subject_base=None): + """Create a KRA instance. + + To create a clone, pass in pkcs12_info. + """ + self.fqdn = host_name + self.domain = domain + self.dm_password = dm_password + self.admin_password = admin_password + self.ds_port = ds_port + self.pkcs12_info = pkcs12_info + if self.pkcs12_info is not None: + self.clone = True + self.master_host = master_host + self.master_replication_port = master_replication_port + if subject_base is None: + self.subject_base = DN(('O', self.realm)) + else: + self.subject_base = subject_base + + # Confirm that a KRA does not already exist + if self.is_installed(): + raise RuntimeError( + "KRA already installed.") + # Confirm that a Dogtag 10 CA instance already exists + ca = cainstance.CAInstance( + api.env.realm, certs.NSS_DIR, + dogtag_constants=dogtag.Dogtag10Constants) + if not ca.is_installed(): + raise RuntimeError( + "KRA configuration failed. " + "A Dogtag CA must be installed first") + + self.step("configuring KRA instance", self.__spawn_instance) + if not self.clone: + self.step("add RA user to KRA agent group", + self.__add_ra_user_to_agent_group) + self.step("restarting KRA", self.restart_instance) + self.step("configure certificate renewals", self.configure_renewal) + self.step("Configure HTTP to proxy connections", + self.http_proxy) + + self.start_creation(runtime=126) + + def __spawn_instance(self): + """ + Create and configure a new KRA instance using pkispawn. + Creates a configuration file with IPA-specific + parameters and passes it to the base class to call pkispawn + """ + + # Create an empty and secured file + (cfg_fd, cfg_file) = tempfile.mkstemp() + os.close(cfg_fd) + pent = pwd.getpwnam(PKI_USER) + os.chown(cfg_file, pent.pw_uid, pent.pw_gid) + + # Create KRA configuration + config = ConfigParser.ConfigParser() + config.optionxform = str + config.add_section("KRA") + + # Security Domain Authentication + config.set("KRA", "pki_security_domain_https_port", "443") + config.set("KRA", "pki_security_domain_password", self.admin_password) + config.set("KRA", "pki_security_domain_user", "admin") + + # issuing ca + config.set("KRA", "pki_issuing_ca_uri", "https://%s" % + ipautil.format_netloc(self.fqdn, 443)) + + # Server + config.set("KRA", "pki_enable_proxy", "True") + config.set("KRA", "pki_restart_configured_instance", "False") + config.set("KRA", "pki_backup_keys", "True") + config.set("KRA", "pki_backup_password", self.admin_password) + + # Client security database + config.set("KRA", "pki_client_database_dir", self.agent_db) + config.set("KRA", "pki_client_database_password", self.admin_password) + config.set("KRA", "pki_client_database_purge", "False") + config.set("KRA", "pki_client_pkcs12_password", self.admin_password) + + # Administrator + config.set("KRA", "pki_admin_name", "admin") + config.set("KRA", "pki_admin_uid", "admin") + config.set("KRA", "pki_admin_email", "root@localhost") + config.set("KRA", "pki_admin_password", self.admin_password) + config.set("KRA", "pki_admin_nickname", "ipa-ca-agent") + config.set("KRA", "pki_admin_subject_dn", + str(DN(('cn', 'ipa-ca-agent'), self.subject_base))) + config.set("KRA", "pki_import_admin_cert", "True") + config.set("KRA", "pki_admin_cert_file", paths.ADMIN_CERT_PATH) + config.set("KRA", "pki_client_admin_cert_p12", paths.DOGTAG_AGENT_P12) + + # Directory server + config.set("KRA", "pki_ds_ldap_port", str(self.ds_port)) + config.set("KRA", "pki_ds_password", self.dm_password) + config.set("KRA", "pki_ds_base_dn", self.basedn) + config.set("KRA", "pki_ds_database", "ipaca") + config.set("KRA", "pki_ds_create_new_db", "False") + + # Certificate subject DNs + config.set("KRA", "pki_subsystem_subject_dn", + str(DN(('cn', 'CA Subsystem'), self.subject_base))) + config.set("KRA", "pki_ssl_server_subject_dn", + str(DN(('cn', self.fqdn), self.subject_base))) + config.set("KRA", "pki_audit_signing_subject_dn", + str(DN(('cn', 'KRA Audit'), self.subject_base))) + config.set( + "KRA", "pki_transport_subject_dn", + str(DN(('cn', 'KRA Transport Certificate'), self.subject_base))) + config.set( + "KRA", "pki_storage_subject_dn", + str(DN(('cn', 'KRA Storage Certificate'), self.subject_base))) + + # Certificate nicknames + # Note that both the server certs and subsystem certs reuse + # the ca certs. + config.set("KRA", "pki_subsystem_nickname", + "subsystemCert cert-pki-ca") + config.set("KRA", "pki_ssl_server_nickname", + "Server-Cert cert-pki-ca") + config.set("KRA", "pki_audit_signing_nickname", + "auditSigningCert cert-pki-kra") + config.set("KRA", "pki_transport_nickname", + "transportCert cert-pki-kra") + config.set("KRA", "pki_storage_nickname", + "storageCert cert-pki-kra") + + # Shared db settings + # Needed because CA and KRA share the same database + # We will use the dbuser created for the CA + config.set("KRA", "pki_share_db", "True") + config.set( + "KRA", "pki_share_dbuser_dn", + str(DN(('uid', 'pkidbuser'), ('ou', 'people'), ('o', 'ipaca')))) + + _p12_tmpfile_handle, p12_tmpfile_name = tempfile.mkstemp(dir=paths.TMP) + if self.clone: + krafile = self.pkcs12_info[0] + shutil.copy(krafile, p12_tmpfile_name) + pent = pwd.getpwnam(PKI_USER) + os.chown(p12_tmpfile_name, pent.pw_uid, pent.pw_gid) + + # create admin cert file if it does not exist + cert = DogtagInstance.get_admin_cert(self) + with open(paths.ADMIN_CERT_PATH, "w") as admin_path: + admin_path.write(cert) + + # Security domain registration + config.set("KRA", "pki_security_domain_hostname", self.master_host) + config.set("KRA", "pki_security_domain_https_port", "443") + config.set("KRA", "pki_security_domain_user", "admin") + config.set("KRA", "pki_security_domain_password", + self.admin_password) + + # Clone + config.set("KRA", "pki_clone", "True") + config.set("KRA", "pki_clone_pkcs12_path", p12_tmpfile_name) + config.set("KRA", "pki_clone_pkcs12_password", self.dm_password) + config.set("KRA", "pki_clone_setup_replication", "False") + config.set( + "KRA", "pki_clone_uri", + "https://%s" % ipautil.format_netloc(self.master_host, 443)) + + # Generate configuration file + with open(cfg_file, "wb") as f: + config.write(f) + + try: + DogtagInstance.spawn_instance(self, cfg_file) + finally: + os.remove(p12_tmpfile_name) + os.remove(cfg_file) + + shutil.move(paths.KRA_BACKUP_KEYS_P12, paths.KRACERT_P12) + self.log.debug("completed creating KRA instance") + + def __add_ra_user_to_agent_group(self): + """ + Add RA agent created for CA to KRA agent group. + """ + conn = ipaldap.IPAdmin(self.fqdn, self.ds_port) + conn.do_simple_bind(DN(('cn', 'Directory Manager')), self.dm_password) + + entry_dn = DN(('uid', "ipara"), ('ou', 'People'), ('o', 'ipaca')) + dn = DN(('cn', 'Data Recovery Manager Agents'), ('ou', 'groups'), + self.basedn) + modlist = [(0, 'uniqueMember', '%s' % entry_dn)] + conn.modify_s(dn, modlist) + + conn.unbind() + + @staticmethod + def update_cert_config(nickname, cert, dogtag_constants=None): + """ + When renewing a KRA subsystem certificate the configuration file + needs to get the new certificate as well. + + nickname is one of the known nicknames. + cert is a DER-encoded certificate. + """ + + if dogtag_constants is None: + dogtag_constants = dogtag.configured_constants() + + # The cert directive to update per nickname + directives = { + 'auditSigningCert cert-pki-kra': 'kra.audit_signing.cert', + 'storageCert cert-pki-kra': 'kra.storage.cert', + 'transportCert cert-pki-kra': 'kra.transport.cert', + 'subsystemCert cert-pki-kra': 'kra.subsystem.cert', + 'Server-Cert cert-pki-ca': 'kra.sslserver.cert'} + + DogtagInstance.update_cert_cs_cfg( + nickname, cert, directives, + dogtag.configured_constants().KRA_CS_CFG_PATH, + dogtag_constants) + + +def install_replica_kra(config, postinstall=False): + """ + Install a KRA on a replica. + + There are two modes of doing this controlled: + - While the replica is being installed + - Post-replica installation + + config is a ReplicaConfig object + + Returns a KRA instance + """ + # note that the cacert.p12 file is regenerated during the + # ipa-replica-prepare process and should include all the certs + # for the CA and KRA + krafile = config.dir + "/cacert.p12" + + if not ipautil.file_exists(krafile): + raise RuntimeError( + "Unable to clone KRA." + " cacert.p12 file not found in replica file") + + _kra = KRAInstance(config.realm_name, + dogtag_constants=dogtag.install_constants) + _kra.dm_password = config.dirman_password + _kra.subject_base = config.subject_base + if _kra.is_installed(): + sys.exit("A KRA is already configured on this system.") + + _kra.configure_instance(config.host_name, config.domain_name, + config.dirman_password, config.dirman_password, + pkcs12_info=(krafile,), + master_host=config.master_host_name, + master_replication_port=config.ca_ds_port, + subject_base=config.subject_base) + + # Restart httpd since we changed it's config and added ipa-pki-proxy.conf + if postinstall: + services.knownservices.httpd.restart() + + # The dogtag DS instance needs to be restarted after installation. + # The procedure for this is: stop dogtag, stop DS, start DS, start + # dogtag + + service.print_msg("Restarting the directory and KRA servers") + _kra.stop(dogtag.install_constants.PKI_INSTANCE_NAME) + services.knownservices.dirsrv.restart() + _kra.start(dogtag.install_constants.PKI_INSTANCE_NAME) + + return _kra diff --git a/ipaserver/plugins/dogtag.py b/ipaserver/plugins/dogtag.py index 0b95ece79..0e141a45c 100644 --- a/ipaserver/plugins/dogtag.py +++ b/ipaserver/plugins/dogtag.py @@ -1,10 +1,11 @@ # Authors: +# Ade Lee <alee@redhat.com> # Andrew Wnuk <awnuk@redhat.com> # Jason Gerard DeRose <jderose@redhat.com> # Rob Crittenden <rcritten@@redhat.com> # John Dennis <jdennis@redhat.com> # -# Copyright (C) 2009 Red Hat +# Copyright (C) 2014 Red Hat # see file 'COPYING' for use and warranty information # # This program is free software; you can redistribute it and/or modify @@ -34,7 +35,7 @@ variety of names, the open source version is called "dogtag". CMS consists of a number of servlets which in rough terms can be thought of as RPC commands. A servlet is invoked by making an HTTP request to a specific URL -and passing URL arguments. Normally CMS responds with an HTTP reponse consisting +and passing URL arguments. Normally CMS responds with an HTTP response consisting of HTML to be rendered by a web browser. This HTTP HTML response has both Javascript SCRIPT components and HTML rendering code. One of the Javascript SCRIPT blocks holds the data for the result. The rest of the response is derived @@ -42,13 +43,13 @@ from templates associated with the servlet which may be customized. The templates pull the result data from Javascript variables. One way to get the result data is to parse the HTML looking for the Javascript -varible initializations. Simple string searchs are not a robust method. First of +variable initializations. Simple string searches are not a robust method. First of all one must be sure the string is only found in a Javascript SCRIPT block and not somewhere else in the HTML document. Some of the Javascript variable initializations are rather complex (e.g. lists of structures). It would be hard to correctly parse such complex and diverse Javascript. Existing Javascript parsers are not generally available. Finally, it's important to know the -character encoding for strings. There is a somewhat complex set of precident +character encoding for strings. There is a somewhat complex set of precedent rules for determining the current character encoding from the HTTP header, meta-equiv tags, mime Content-Type and charset attributes on HTML elements. All of this means trying to read the result data from a CMS HTML response is @@ -119,7 +120,7 @@ values. Python also nicely handles type promotion transparently between int and long objects. For example if you multiply two int objects you may get back a long object if necessary. In general Python int and long objects may be freely mixed without the programmer needing to be aware of which type of -intergral object is being operated on. +integral object is being operated on. The leads to the following rule, always parse a string representing an integral value using the int() constructor even if it might have large @@ -229,20 +230,28 @@ as a dict via the 'namespaces' keyword parameter of etree.XPath(). The predicate for the second location step uses the 're:' namespace to find the function name 'match'. The re:match() takes a string to search as its first argument and a regular expression pattern as its second argument. In this example the string -to seach is the node name of the location step because we called the built-in +to search is the node name of the location step because we called the built-in node() function of XPath. The regular expression pattern we've passed says it's a match if the string begins with 'chapter' is followed by any number of digits and nothing else follows. ''' -from lxml import etree -import urllib2 import datetime +from lxml import etree +import tempfile import time +import urllib2 + +from pki.client import PKIConnection +import pki.crypto as cryptoutil +from pki.kra import KRAClient + +from ipalib import Backend from ipapython.dn import DN import ipapython.dogtag from ipapython import ipautil +from ipaserver.install.certs import CertDB # These are general status return values used when # CMSServlet.outputError() is invoked. @@ -260,6 +269,7 @@ CMS_STATUS_REJECTED = 5 CMS_STATUS_ERROR = 6 CMS_STATUS_EXCEPTION = 7 + def cms_request_status_to_string(request_status): ''' :param request_status: The integral request status value @@ -290,7 +300,7 @@ def parse_and_set_boolean_xml(node, response, response_name): ''' :param node: xml node object containing value to parse for boolean result :param response: response dict to set boolean result in - :param response_name: name of the respone value to set + :param response_name: name of the response value to set :except ValueError: Read the value out of a xml text node and interpret it as a boolean value. @@ -646,7 +656,7 @@ def parse_check_request_result_xml(doc): +-------------------------+---------------+-------------------+-----------------+ |requestId |string |request_id |string | +-------------------------+---------------+-------------------+-----------------+ - |staus |string |cert_request_status|unicode [1]_ | + |status |string |cert_request_status|unicode [1]_ | +-------------------------+---------------+-------------------+-----------------+ |createdOn |long, timestamp|created_on |datetime.datetime| +-------------------------+---------------+-------------------+-----------------+ @@ -1199,6 +1209,57 @@ def parse_unrevoke_cert_xml(doc): return response + +def host_has_service(host, ldap2, service='CA'): + """ + :param host: A host which might be a master for a service. + :param ldap2: connection to the local database + :param service: The service for which the host might be a master. + :return: (true, false) + + Check if a specified host is a master for a specified service. + """ + base_dn = DN(('cn', host), ('cn', 'masters'), ('cn', 'ipa'), + ('cn', 'etc'), api.env.basedn) + filter_attrs = { + 'objectClass': 'ipaConfigObject', + 'cn': service, + 'ipaConfigString': 'enabledService', + } + query_filter = ldap2.make_filter(filter_attrs, rules='&') + try: + ent, trunc = ldap2.find_entries(filter=query_filter, base_dn=base_dn) + if len(ent): + return True + except Exception: + pass + return False + + +def select_any_master(ldap2, service='CA'): + """ + :param ldap2: connection to the local database + :param service: The service for which we're looking for a master. + :return: host as str + + Select any host which is a master for a specified service. + """ + base_dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), + api.env.basedn) + filter_attrs = { + 'objectClass': 'ipaConfigObject', + 'cn': service, + 'ipaConfigString': 'enabledService',} + query_filter = ldap2.make_filter(filter_attrs, rules='&') + try: + ent, trunc = ldap2.find_entries(filter=query_filter, base_dn=base_dn) + if len(ent): + entry = random.choice(ent) + return entry.dn[1].value + except Exception: + pass + return None + #------------------------------------------------------------------------------- from ipalib import api, SkipPluginModule @@ -1214,6 +1275,7 @@ from ipapython import dogtag from ipalib import _ from ipaplatform.paths import paths + class ra(rabase.rabase): """ Request Authority backend plugin. @@ -1258,57 +1320,6 @@ class ra(rabase.rabase): self.error('%s.%s(): %s', self.fullname, func_name, err_msg) raise CertificateOperationError(error=err_msg) - def _host_has_service(self, host, service='CA'): - """ - :param host: A host which might be a master for a service. - :param service: The service for which the host might be a master. - :return: (true, false) - - Check if a specified host is a master for a specified service. - """ - ldap2 = self.api.Backend.ldap2 - base_dn = DN(('cn', host), ('cn', 'masters'), ('cn', 'ipa'), - ('cn', 'etc'), api.env.basedn) - filter_attrs = { - 'objectClass': 'ipaConfigObject', - 'cn': service, - 'ipaConfigString': 'enabledService', - } - filter = ldap2.make_filter(filter_attrs, rules='&') - try: - ent, trunc = ldap2.find_entries(filter=filter, base_dn=base_dn) - if len(ent): - return True - except Exception, e: - pass - return False - - def _select_any_master(self, service='CA'): - """ - :param service: The service for which we're looking for a master. - :return: host - as str - - Select any host which is a master for a specified service. - """ - ldap2 = self.api.Backend.ldap2 - base_dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), - api.env.basedn) - filter_attrs = { - 'objectClass': 'ipaConfigObject', - 'cn': service, - 'ipaConfigString': 'enabledService', - } - filter = ldap2.make_filter(filter_attrs, rules='&') - try: - ent, trunc = ldap2.find_entries(filter=filter, base_dn=base_dn) - if len(ent): - entry = random.choice(ent) - return entry.dn[1].value - except Exception, e: - pass - return None - @cachedproperty def ca_host(self): """ @@ -1317,12 +1328,13 @@ class ra(rabase.rabase): Select our CA host. """ - if self._host_has_service(host=api.env.ca_host): + ldap2 = self.api.Backend.ldap2 + if host_has_service(api.env.ca_host, ldap2, "CA"): return api.env.ca_host if api.env.host != api.env.ca_host: - if self._host_has_service(host=api.env.host): + if host_has_service(api.env.host, ldap2, "CA"): return api.env.host - host = self._select_any_master() + host = select_any_master(ldap2) if host: return host else: @@ -1363,7 +1375,8 @@ class ra(rabase.rabase): parser = etree.XMLParser() doc = etree.fromstring(xml_text, parser) result = parse_func(doc) - self.debug("%s() xml_text:\n%s\nparse_result:\n%s" % (parse_func.__name__, xml_text, result)) + self.debug("%s() xml_text:\n%s\n" + "parse_result:\n%s" % (parse_func.__name__, xml_text, result)) return result def check_request_status(self, request_id): @@ -1410,7 +1423,7 @@ class ra(rabase.rabase): xml='true') # Parse and handle errors - if (http_status != 200): + if http_status != 200: self.raise_certificate_operation_error('check_request_status', detail=http_reason_phrase) @@ -1440,10 +1453,10 @@ class ra(rabase.rabase): Retrieve an existing certificate. :param serial_number: Certificate serial number. Must be a string value - because serial numbers may be of any magnitue and + because serial numbers may be of any magnitude and XMLRPC cannot handle integers larger than 64-bit. The string value should be decimal, but may optionally - be prefixed with a hex radix prefix if the integal value + be prefixed with a hex radix prefix if the integral value is represented as hexadecimal. If no radix prefix is supplied the string will be interpreted as decimal. @@ -1496,7 +1509,7 @@ class ra(rabase.rabase): # Parse and handle errors - if (http_status != 200): + if http_status != 200: self.raise_certificate_operation_error('get_certificate', detail=http_reason_phrase) @@ -1563,7 +1576,7 @@ class ra(rabase.rabase): cert_request=csr, xml='true') # Parse and handle errors - if (http_status != 200): + if http_status != 200: self.raise_certificate_operation_error('request_certificate', detail=http_reason_phrase) @@ -1604,10 +1617,10 @@ class ra(rabase.rabase): def revoke_certificate(self, serial_number, revocation_reason=0): """ :param serial_number: Certificate serial number. Must be a string value - because serial numbers may be of any magnitue and + because serial numbers may be of any magnitude and XMLRPC cannot handle integers larger than 64-bit. The string value should be decimal, but may optionally - be prefixed with a hex radix prefix if the integal value + be prefixed with a hex radix prefix if the integral value is represented as hexadecimal. If no radix prefix is supplied the string will be interpreted as decimal. :param revocation_reason: Integer code of revocation reason. @@ -1644,7 +1657,7 @@ class ra(rabase.rabase): xml='true') # Parse and handle errors - if (http_status != 200): + if http_status != 200: self.raise_certificate_operation_error('revoke_certificate', detail=http_reason_phrase) @@ -1668,10 +1681,10 @@ class ra(rabase.rabase): def take_certificate_off_hold(self, serial_number): """ :param serial_number: Certificate serial number. Must be a string value - because serial numbers may be of any magnitue and + because serial numbers may be of any magnitude and XMLRPC cannot handle integers larger than 64-bit. The string value should be decimal, but may optionally - be prefixed with a hex radix prefix if the integal value + be prefixed with a hex radix prefix if the integral value is represented as hexadecimal. If no radix prefix is supplied the string will be interpreted as decimal. @@ -1704,7 +1717,7 @@ class ra(rabase.rabase): xml='true') # Parse and handle errors - if (http_status != 200): + if http_status != 200: self.raise_certificate_operation_error('take_certificate_off_hold', detail=http_reason_phrase) @@ -1866,4 +1879,133 @@ class ra(rabase.rabase): return results + api.register(ra) + + +# ---------------------------------------------------------------------------- +class kra(Backend): + """ + KRA backend plugin (for Vault) + """ + + def __init__(self, kra_port=443): + if api.env.in_tree: + self.sec_dir = os.path.join(api.env.dot_ipa, 'alias') + pwd_file = os.path.join(self.sec_dir, '.pwd') + self.pem_file = os.path.join(self.sec_dir, ".pemfile") + else: + self.sec_dir = paths.HTTPD_ALIAS_DIR + pwd_file = paths.ALIAS_PWDFILE_TXT + self.pem_file = paths.DOGTAG_AGENT_PEM + + self.kra_port = kra_port + self.transport_nick = "IPA KRA Transport Cert" + self.password = "" + with open(pwd_file, "r") as f: + self.password = f.readline().strip() + + self.keyclient = None + super(kra, self).__init__() + + def _create_pem_file(self): + """ Create PEM file used by KRA plugin for authentication. + + This function reads the IPA HTTPD database and extracts the + Dogtag agent certificate and keys into a PKCS#12 temporary file. + The PKCS#12 file is then converted into PEM format so that it + can be used by python-requests to authenticate to the KRA. + + :return: None + """ + (p12_pwd_fd, p12_pwd_fname) = tempfile.mkstemp() + (p12_fd, p12_fname) = tempfile.mkstemp() + + try: + os.write(p12_pwd_fd, self.password) + os.close(p12_pwd_fd) + os.close(p12_fd) + + certdb = CertDB(api.env.realm) + certdb.export_pkcs12(p12_fname, p12_pwd_fname, "ipaCert") + + certdb.install_pem_from_p12(p12_fname, self.password, self.pem_file) + except: + self.debug("Error when creating PEM file for KRA operations") + raise + finally: + os.remove(p12_fname) + os.remove(p12_pwd_fname) + + def _transport_cert_present(self): + """ Check if the client certDB contains the KRA transport certificate + :return: True/False + """ + # certutil -L -d db_dir -n cert_nick + certdb = CertDB(api.env.realm) + return certdb.has_nickname(self.transport_nick) + + def _setup(self): + """ Do initial setup and crypto initialization of the KRA client + + Creates a PEM file containing the KRA agent cert/keys to be used for + authentication to the KRA (if it does not already exist), Sets up a + connection to the KRA and initializes an NSS certificate database to + store the transport certificate, Retrieves the transport certificate + if it is not already present. + """ + #set up pem file if not present + if not os.path.exists(self.pem_file): + self._create_pem_file() + + # set up connection + connection = PKIConnection('https', + self.kra_host, + str(self.kra_port), + 'kra') + connection.set_authentication_cert(self.pem_file) + + crypto = cryptoutil.NSSCryptoProvider(self.sec_dir, self.password) + + #create kraclient + kraclient = KRAClient(connection, crypto) + + # get transport cert if needed + if not self._transport_cert_present(): + transport_cert = kraclient.system_certs.get_transport_cert() + crypto.import_cert(self.transport_nick, transport_cert, "u,u,u") + + crypto.initialize() + + self.keyclient = kraclient.keys + self.keyclient.set_transport_cert(self.transport_nick) + + @cachedproperty + def kra_host(self): + """ + :return: host + as str + + Select our KRA host. + """ + ldap2 = self.api.Backend.ldap2 + if host_has_service(api.env.kra_host, ldap2, "kra"): + return api.env.kra_host + if api.env.host != api.env.kra_host: + if host_has_service(api.env.host, ldap2, "kra"): + return api.env.host + host = select_any_master(ldap2, "kra") + if host: + return host + else: + return api.env.kra_host + + def get_keyclient(self): + """Return a keyclient to perform key archival and retrieval. + :return: pki.key.keyclient + """ + if self.keyclient is None: + self._setup() + return self.keyclient + +api.register(kra) |