diff --git a/ipalib/plugins/cert.py b/ipalib/plugins/cert.py index 1154e2e30..60161cf1c 100644 --- a/ipalib/plugins/cert.py +++ b/ipalib/plugins/cert.py @@ -417,7 +417,16 @@ class cert_show(VirtualCommand): operation="retrieve certificate" def execute(self, serial_number): - self.check_access() + hostname = None + try: + self.check_access() + except errors.ACIError, acierr: + self.debug("Not granted by ACI to retrieve certificate, looking at principal") + bind_principal = getattr(context, 'principal') + if not bind_principal.startswith('host/'): + raise acierr + hostname = get_host_from_principal(bind_principal) + result=self.Backend.ra.get_certificate(serial_number) cert = x509.load_certificate(result['certificate']) result['subject'] = unicode(cert.subject) @@ -426,6 +435,12 @@ class cert_show(VirtualCommand): result['valid_not_after'] = unicode(cert.valid_not_after_str) result['md5_fingerprint'] = unicode(nss.data_to_hex(nss.md5_digest(cert.der_data), 64)[0]) result['sha1_fingerprint'] = unicode(nss.data_to_hex(nss.sha1_digest(cert.der_data), 64)[0]) + if hostname: + # If we have a hostname we want to verify that the subject + # of the certificate matches it, otherwise raise an error + if hostname != cert.subject.common_name: + raise acierr + return dict(result=result) api.register(cert_show) @@ -457,7 +472,17 @@ class cert_revoke(VirtualCommand): ) def execute(self, serial_number, **kw): - self.check_access() + hostname = None + try: + self.check_access() + except errors.ACIError, acierr: + self.debug("Not granted by ACI to revoke certificate, looking at principal") + try: + # Let cert_show() handle verifying that the subject of the + # cert we're dealing with matches the hostname in the principal + result = api.Command['cert_show'](unicode(serial_number))['result'] + except errors.NotImplementedError: + pass return dict( result=self.Backend.ra.revoke_certificate(serial_number, **kw) ) diff --git a/ipaserver/install/Makefile.am b/ipaserver/install/Makefile.am index 964837cb9..8932eadbb 100644 --- a/ipaserver/install/Makefile.am +++ b/ipaserver/install/Makefile.am @@ -15,6 +15,7 @@ app_PYTHON = \ replication.py \ certs.py \ ldapupdate.py \ + certmonger.py \ $(NULL) EXTRA_DIST = \ diff --git a/ipaserver/install/certmonger.py b/ipaserver/install/certmonger.py new file mode 100644 index 000000000..bb56c2ab3 --- /dev/null +++ b/ipaserver/install/certmonger.py @@ -0,0 +1,152 @@ +# Authors: Rob Crittenden +# +# Copyright (C) 2010 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; version 2 only +# +# 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, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +# Some certmonger functions, mostly around updating the request file. +# This is used so we can add tracking to the Apache and 389-ds +# server certificates created during the IPA server installation. + +import os +import re +import time +from ipapython import ipautil + +REQUEST_DIR='/var/lib/certmonger/requests/' + +def find_request_value(filename, directive): + """ + Return a value from a certmonger request file for the requested directive + + It tries to do this a number of times because sometimes there is a delay + when ipa-getcert returns and the file is fully updated, particularly + when doing a request. Genrerating a CSR is fast but not instantaneous. + """ + tries = 1 + value = None + found = False + while value is None and tries <= 5: + tries=tries + 1 + time.sleep(1) + fp = open(filename, 'r') + lines = fp.readlines() + fp.close() + + for line in lines: + if found: + # A value can span multiple lines. If it does then it has a + # leading space. + if not line.startswith(' '): + # We hit the next directive, return now + return value + else: + value = value + line[1:] + else: + if line.startswith(directive + '='): + found = True + value = line[len(directive)+1:] + + return value + +def get_request_value(request_id, directive): + """ + There is no guarantee that the request_id will match the filename + in the certmonger requests directory, so open each one to find the + request_id. + """ + fileList=os.listdir(REQUEST_DIR) + for file in fileList: + value = find_request_value('%s/%s' % (REQUEST_DIR, file), 'id') + if value is not None and value.rstrip() == request_id: + return find_request_value('%s/%s' % (REQUEST_DIR, file), directive) + + return None + +def add_request_value(request_id, directive, value): + """ + Add a new directive to a certmonger request file. + + The certmonger service MUST be stopped in order for this to work. + """ + fileList=os.listdir(REQUEST_DIR) + for file in fileList: + id = find_request_value('%s/%s' % (REQUEST_DIR, file), 'id') + if id is not None and id.rstrip() == request_id: + current_value = find_request_value('%s/%s' % (REQUEST_DIR, file), directive) + if not current_value: + fp = open('%s/%s' % (REQUEST_DIR, file), 'a') + fp.write('%s=%s\n' % (directive, value)) + fp.close() + + return + +def add_principal(request_id, principal): + """ + In order for a certmonger request to be renwable it needs a principal. + + When an existing certificate is added via start-tracking it won't have + a principal. + """ + return add_request_value(request_id, 'template_principal', principal) + +def add_subject(request_id, subject): + """ + In order for a certmonger request to be renwable it needs the subject + set in the request file. + + When an existing certificate is added via start-tracking it won't have + a subject_template set. + """ + return add_request_value(request_id, 'template_subject', subject) + +def request_cert(nssdb, nickname, subject, principal, passwd_fname=None): + """ + Execute certmonger to request a server certificate + """ + args = ['/usr/bin/ipa-getcert', + 'request', + '-d', nssdb, + '-n', nickname, + '-N', subject, + '-K', principal, + ] + if passwd_fname: + args.append('-p') + args.append(passwd_fname) + (stdout, stderr, returncode) = ipautil.run(args) + # FIXME: should be some error handling around this + m = re.match('New signing request "(\d+)" added', stdout) + request_id = m.group(1) + return request_id + +def stop_tracking(request_id): + """ + Stop tracking the current request. + + This assumes that the certmonger service is running. + """ + args = ['/usr/bin/ipa-getcert', + 'stop-tracking', + '-i', request_id + ] + (stdout, stderr, returncode) = ipautil.run(args) + +if __name__ == '__main__': + request_id = request_cert("/etc/httpd/alias", "Test", "cn=tiger.example.com,O=IPA", "HTTP/tiger.example.com@EXAMPLE.COM") + csr = get_request_value(request_id, 'csr') + print csr + stop_tracking(request_id) diff --git a/ipaserver/install/certs.py b/ipaserver/install/certs.py index cf89c22f0..7f246d11c 100644 --- a/ipaserver/install/certs.py +++ b/ipaserver/install/certs.py @@ -34,6 +34,9 @@ from ipapython import sysrestore from ipapython import ipautil from ipalib import pkcs10 from ConfigParser import RawConfigParser +import service +import certmonger +from ipalib import x509 from nss.error import NSPRError import nss.nss as nss @@ -432,6 +435,51 @@ class CertDB(object): raise RuntimeError("Unable to find serial number") + def track_server_cert(self, nickname, principal, password_file=None): + """ + Tell certmonger to track the given certificate nickname. + """ + service.chkconfig_on("certmonger") + service.start("certmonger") + args = ["/usr/bin/ipa-getcert", "start-tracking", + "-d", self.secdir, + "-n", nickname] + if password_file: + args.append("-p") + args.append(password_file) + try: + (stdout, stderr, returncode) = ipautil.run(args) + except ipautil.CalledProcessError, e: + logging.error("tracking certificate failed: %s" % str(e)) + + service.stop("certmonger") + cert = self.get_cert_from_db(nickname) + subject = str(x509.get_subject(cert)) + m = re.match('New tracking request "(\d+)" added', stdout) + request_id = m.group(1) + + certmonger.add_principal(request_id, principal) + certmonger.add_subject(request_id, subject) + + service.start("certmonger") + + def untrack_server_cert(self, nickname): + """ + Tell certmonger to stop tracking the given certificate nickname. + """ + + # Always start certmonger. We can't untrack something if it isn't + # running + service.start("certmonger") + args = ["/usr/bin/ipa-getcert", "stop-tracking", + "-d", self.secdir, + "-n", nickname] + try: + (stdout, stderr, returncode) = ipautil.run(args) + except ipautil.CalledProcessError, e: + logging.error("untracking certificate failed: %s" % str(e)) + service.stop("certmonger") + def create_server_cert(self, nickname, hostname, other_certdb=None, subject=None): """ other_certdb can mean one of two things, depending on the context. @@ -449,7 +497,7 @@ class CertDB(object): cdb = self if subject is None: subject=self.subject_format % hostname - (out, err) = self.request_cert(subject) + self.request_cert(subject) cdb.issue_server_cert(self.certreq_fname, self.certder_fname) self.add_cert(self.certder_fname, nickname) fd = open(self.certder_fname, "r") @@ -486,7 +534,6 @@ class CertDB(object): args.append("-a") (stdout, stderr, returncode) = self.run_certutil(args) os.remove(self.noise_fname) - return (stdout, stderr) def issue_server_cert(self, certreq_fname, cert_fname): diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py index f91d69b7b..a53348456 100644 --- a/ipaserver/install/dsinstance.py +++ b/ipaserver/install/dsinstance.py @@ -165,7 +165,7 @@ class DsInstance(service.Service): self.sub_dict = None self.domain = domain_name self.serverid = None - self.host_name = None + self.fqdn = None self.pkcs12_info = None self.ds_user = None self.dercert = None @@ -177,19 +177,19 @@ class DsInstance(service.Service): else: self.suffix = None - def create_instance(self, ds_user, realm_name, host_name, domain_name, dm_password, pkcs12_info=None, self_signed_ca=False, uidstart=1100, gidstart=1100, subject_base=None, hbac_allow=True): + def create_instance(self, ds_user, realm_name, fqdn, domain_name, dm_password, pkcs12_info=None, self_signed_ca=False, uidstart=1100, gidstart=1100, subject_base=None, hbac_allow=True): self.ds_user = ds_user self.realm_name = realm_name.upper() self.serverid = realm_to_serverid(self.realm_name) self.suffix = util.realm_to_suffix(self.realm_name) - self.host_name = host_name + self.fqdn = fqdn self.dm_password = dm_password self.domain = domain_name self.pkcs12_info = pkcs12_info self.self_signed_ca = self_signed_ca self.uidstart = uidstart self.gidstart = gidstart - self.principal = "ldap/%s@%s" % (self.host_name, self.realm_name) + self.principal = "ldap/%s@%s" % (self.fqdn, self.realm_name) self.subject_base = subject_base self.__setup_sub_dict() @@ -232,12 +232,12 @@ class DsInstance(service.Service): def __setup_sub_dict(self): server_root = find_server_root() - self.sub_dict = dict(FQHN=self.host_name, SERVERID=self.serverid, + self.sub_dict = dict(FQHN=self.fqdn, SERVERID=self.serverid, PASSWORD=self.dm_password, SUFFIX=self.suffix.lower(), REALM=self.realm_name, USER=self.ds_user, SERVER_ROOT=server_root, DOMAIN=self.domain, TIME=int(time.time()), UIDSTART=self.uidstart, - GIDSTART=self.gidstart, HOST=self.host_name, + GIDSTART=self.gidstart, HOST=self.fqdn, ESCAPED_SUFFIX= escape_dn_chars(self.suffix.lower()), ) @@ -356,7 +356,7 @@ class DsInstance(service.Service): def __config_uidgid_gen_first_master(self): if (self.uidstart == self.gidstart and - has_managed_entries(self.host_name, self.dm_password)): + has_managed_entries(self.fqdn, self.dm_password)): self._ldap_mod("dna-upg.ldif", self.sub_dict) else: self._ldap_mod("dna-posix.ldif", self.sub_dict) @@ -377,7 +377,7 @@ class DsInstance(service.Service): self._ldap_mod("version-conf.ldif") def __user_private_groups(self): - if has_managed_entries(self.host_name, self.dm_password): + if has_managed_entries(self.fqdn, self.dm_password): self._ldap_mod("user_private_groups.ldif", self.sub_dict) def __add_enrollment_module(self): @@ -397,17 +397,19 @@ class DsInstance(service.Service): self.dercert = dsdb.get_cert_from_db(nickname) else: nickname = "Server-Cert" - cadb = certs.CertDB(httpinstance.NSS_DIR, host_name=self.host_name, subject_base=self.subject_base) + cadb = certs.CertDB(httpinstance.NSS_DIR, host_name=self.fqdn, subject_base=self.subject_base) if self.self_signed_ca: cadb.create_self_signed() dsdb.create_from_cacert(cadb.cacert_fname, passwd=None) - self.dercert = dsdb.create_server_cert("Server-Cert", self.host_name, cadb) + self.dercert = dsdb.create_server_cert("Server-Cert", self.fqdn, cadb) + dsdb.track_server_cert("Server-Cert", self.principal, dsdb.passwd_fname) dsdb.create_pin_file() else: # FIXME, need to set this nickname in the RA plugin cadb.export_ca_cert('ipaCert', False) dsdb.create_from_cacert(cadb.cacert_fname, passwd=None) - self.dercert = dsdb.create_server_cert("Server-Cert", self.host_name, cadb) + self.dercert = dsdb.create_server_cert("Server-Cert", self.fqdn, cadb) + dsdb.track_server_cert("Server-Cert", self.principal, dsdb.passwd_fname) dsdb.create_pin_file() conn = ipaldap.IPAdmin("127.0.0.1") @@ -491,6 +493,9 @@ class DsInstance(service.Service): serverid = self.restore_state("serverid") if not serverid is None: + dirname = config_dirname(serverid) + dsdb = certs.CertDB(dirname) + dsdb.untrack_server_cert("Server-Cert") erase_ds_instance_data(serverid) ds_user = self.restore_state("user") diff --git a/ipaserver/install/httpinstance.py b/ipaserver/install/httpinstance.py index 48a908f15..af8fdde18 100644 --- a/ipaserver/install/httpinstance.py +++ b/ipaserver/install/httpinstance.py @@ -120,10 +120,9 @@ class HTTPInstance(service.Service): self.print_msg(selinux_warning) def __create_http_keytab(self): - http_principal = "HTTP/" + self.fqdn + "@" + self.realm - installutils.kadmin_addprinc(http_principal) - installutils.create_keytab("/etc/httpd/conf/ipa.keytab", http_principal) - self.move_service(http_principal) + installutils.kadmin_addprinc(self.principal) + installutils.create_keytab("/etc/httpd/conf/ipa.keytab", self.principal) + self.move_service(self.principal) self.add_cert_to_service() pent = pwd.getpwnam("apache") @@ -186,9 +185,11 @@ class HTTPInstance(service.Service): db.create_from_cacert(ca_db.cacert_fname) db.create_password_conf() self.dercert = db.create_server_cert("Server-Cert", self.fqdn, ca_db) + db.track_server_cert("Server-Cert", self.principal, db.passwd_fname) db.create_signing_cert("Signing-Cert", "Object Signing Cert", ca_db) else: self.dercert = db.create_server_cert("Server-Cert", self.fqdn, ca_db) + db.track_server_cert("Server-Cert", self.principal, db.passwd_fname) db.create_signing_cert("Signing-Cert", "Object Signing Cert", ca_db) db.create_password_conf() @@ -251,6 +252,8 @@ class HTTPInstance(service.Service): if not running is None: self.stop() + db = certs.CertDB(NSS_DIR) + db.untrack_server_cert("Server-Cert") if not enabled is None and not enabled: self.chkconfig_off() diff --git a/ipaserver/install/service.py b/ipaserver/install/service.py index 4958721e7..47489c09c 100644 --- a/ipaserver/install/service.py +++ b/ipaserver/install/service.py @@ -145,12 +145,14 @@ class Service: conn.unbind() return newdn = "krbprincipalname=%s,cn=services,cn=accounts,%s" % (principal, self.suffix) + hostdn = "fqdn=%s,cn=computers,cn=accounts,%s" % (self.fqdn, self.suffix) conn.deleteEntry(dn) entry.dn = newdn classes = entry.getValues("objectclass") classes = classes + ["ipaobject", "ipaservice", "pkiuser"] entry.setValues("objectclass", list(set(classes))) entry.setValue("ipauniqueid", str(uuid.uuid1())) + entry.setValue("managedby", hostdn) conn.addEntry(entry) conn.unbind() return newdn