Hi,

the attached patches fix various bugs and shortcomings in the CA management and renewal code. Related tickets: <https://fedorahosted.org/freeipa/ticket/4416>, <https://fedorahosted.org/freeipa/ticket/4460>.


(Patch 319 was originally posted at <http://www.redhat.com/archives/freeipa-devel/2014-September/msg00132.html>.)

Honza

--
Jan Cholasta
>From 366b3e5d3fcdf4d003c1ff9ee02543edcd5d96c8 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Thu, 18 Sep 2014 16:28:59 +0200
Subject: [PATCH 01/13] Introduce NSS database /etc/ipa/nssdb

---
 freeipa.spec.in                           |  17 ++++
 ipa-client/ipa-install/ipa-client-install | 159 ++++++++++++++++++------------
 ipa-client/ipaclient/ipa_certupdate.py    |   9 ++
 ipalib/rpc.py                             |   2 +-
 ipaplatform/base/paths.py                 |   2 +-
 ipapython/certdb.py                       |  28 ++++++
 6 files changed, 154 insertions(+), 63 deletions(-)

diff --git a/freeipa.spec.in b/freeipa.spec.in
index 29ad077..a81c193 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -412,6 +412,7 @@ mkdir -p %{buildroot}/%{_localstatedir}/lib/ipa/backup
 mkdir -p %{buildroot}%{_sysconfdir}/ipa/
 /bin/touch %{buildroot}%{_sysconfdir}/ipa/default.conf
 /bin/touch %{buildroot}%{_sysconfdir}/ipa/ca.crt
+mkdir -p %{buildroot}%{_sysconfdir}/ipa/nssdb
 mkdir -p %{buildroot}/%{_localstatedir}/lib/ipa-client/sysrestore
 
 %if ! %{ONLY_CLIENT}
@@ -526,6 +527,17 @@ if [ $1 -gt 1 ] ; then
             /bin/systemctl condrestart ntpd.service 2>&1 || :
         fi
     fi
+
+    if [ ! -f '/etc/ipa/nssdb/cert8.db' -a $restore -ge 2 ]; then
+        python2 -c 'from ipapython.certdb import create_ipa_nssdb; create_ipa_nssdb()' >/dev/null 2>&1
+        tempfile=$(mktemp)
+        if certutil -L -d /etc/pki/nssdb -n 'IPA CA' -a >"$tempfile" 2>/dev/null; then
+            certutil -A -d /etc/ipa/nssdb -f /etc/ipa/nssdb/pwdfile.txt -n 'IPA CA' -t CT,C,C -a -i "$tempfile" >/dev/null
+        elif certutil -L -d /etc/pki/nssdb -n 'External CA cert' -a >"$tempfile" 2>/dev/null; then
+            certutil -A -d /etc/ipa/nssdb -f /etc/ipa/nssdb/pwdfile.txt -n 'External CA cert' -t C,, -a -i "$tempfile" >/dev/null
+        fi
+        rm -f "$tempfile"
+    fi
 fi
 
 %triggerin -n freeipa-client -- openssh-server
@@ -785,6 +797,11 @@ fi
 %dir %attr(0755,root,root) %{_sysconfdir}/ipa/
 %ghost %attr(0644,root,apache) %config(noreplace) %{_sysconfdir}/ipa/default.conf
 %ghost %attr(0644,root,apache) %config(noreplace) %{_sysconfdir}/ipa/ca.crt
+%dir %attr(0755,root,root) %{_sysconfdir}/ipa/nssdb
+%ghost %config(noreplace) %{_sysconfdir}/ipa/nssdb/cert8.db
+%ghost %config(noreplace) %{_sysconfdir}/ipa/nssdb/key3.db
+%ghost %config(noreplace) %{_sysconfdir}/ipa/nssdb/secmod.db
+%ghost %config(noreplace) %{_sysconfdir}/ipa/nssdb/pwdfile.txt
 
 %if ! %{ONLY_CLIENT}
 %files tests -f tests-python.list
diff --git a/ipa-client/ipa-install/ipa-client-install b/ipa-client/ipa-install/ipa-client-install
index b3da28d..82e4b18 100755
--- a/ipa-client/ipa-install/ipa-client-install
+++ b/ipa-client/ipa-install/ipa-client-install
@@ -46,7 +46,7 @@ try:
     from ipaplatform import services
     from ipaplatform.paths import paths
     from ipapython import ipautil, sysrestore, version, certmonger, ipaldap
-    from ipapython import kernel_keyring
+    from ipapython import kernel_keyring, certdb
     from ipapython.config import IPAOptionParser
     from ipalib import api, errors
     from ipalib import x509, certstore
@@ -550,6 +550,15 @@ def uninstall(options, env):
             cmonger.service_name, str(e))
 
     # Remove our host cert and CA cert
+    for filename in (os.path.join(paths.IPA_NSSDB_DIR, 'cert8.db'),
+                     os.path.join(paths.IPA_NSSDB_DIR, 'key3.db'),
+                     os.path.join(paths.IPA_NSSDB_DIR, 'secmod.db'),
+                     os.path.join(paths.IPA_NSSDB_DIR, 'pwdfile.txt')):
+        try:
+            os.remove(filename)
+        except OSError, e:
+            root_logger.error("Failed to remove %s: %s", filename, e)
+
     purge_ipa_certs({client_nss_nickname, 'IPA CA', 'External CA cert'})
 
     try:
@@ -2523,62 +2532,71 @@ def install(options, env, fstore, statestore):
     except ValueError:
         pass
 
-    # Add CA certs to a temporary NSS database
+    tmp_nss_dir = tempfile.mkdtemp()
     try:
-        os.mkdir(paths.IPA_NSSDB_DIR)
-        pwd_file = ipautil.write_tmp_file(ipautil.ipa_generate_password())
-        run([paths.CERTUTIL, '-N',
-             '-d', paths.IPA_NSSDB_DIR,
-             '-f', pwd_file.name])
-
-        ca_certs = x509.load_certificate_list_from_file(CACERT)
-        ca_certs = [cert.der_data for cert in ca_certs]
-        for i, cert in enumerate(ca_certs):
-            run([paths.CERTUTIL, '-A',
-                 '-d', paths.IPA_NSSDB_DIR,
-                 '-n', 'CA certificate %d' % (i + 1),
-                 '-t', 'C,,'],
-                 stdin=cert)
-    except CalledProcessError, e:
-        root_logger.info("Failed to add CA to temporary NSS database.")
-        return CLIENT_INSTALL_ERROR
+        # Add CA certs to a temporary NSS database
+        try:
+            pwd_file = ipautil.write_tmp_file(ipautil.ipa_generate_password())
+            run([paths.CERTUTIL, '-N',
+                 '-d', tmp_nss_dir,
+                 '-f', pwd_file.name])
 
-    # Now, let's try to connect to the server's XML-RPC interface
-    connected = False
-    try:
-        api.Backend.rpcclient.connect(nss_dir=paths.IPA_NSSDB_DIR)
-        connected = True
-        root_logger.debug('Try RPC connection')
-        api.Backend.rpcclient.forward('ping')
-    except errors.KerberosError, e:
-        if connected:
-            api.Backend.rpcclient.disconnect()
-        root_logger.info('Cannot connect to the server due to ' +
-            'Kerberos error: %s. Trying with delegate=True', str(e))
+            ca_certs = x509.load_certificate_list_from_file(CACERT)
+            ca_certs = [cert.der_data for cert in ca_certs]
+            for i, cert in enumerate(ca_certs):
+                run([paths.CERTUTIL, '-A',
+                     '-d', tmp_nss_dir,
+                     '-n', 'CA certificate %d' % (i + 1),
+                     '-t', 'C,,'],
+                     stdin=cert)
+        except CalledProcessError, e:
+            root_logger.info("Failed to add CA to temporary NSS database.")
+            return CLIENT_INSTALL_ERROR
+
+        # Now, let's try to connect to the server's XML-RPC interface
+        connected = False
         try:
-            api.Backend.rpcclient.connect(delegate=True, nss_dir=paths.IPA_NSSDB_DIR)
+            api.Backend.rpcclient.connect(nss_dir=tmp_nss_dir)
+            connected = True
             root_logger.debug('Try RPC connection')
             api.Backend.rpcclient.forward('ping')
+        except errors.KerberosError, e:
+            if connected:
+                api.Backend.rpcclient.disconnect()
+            root_logger.info('Cannot connect to the server due to ' +
+                'Kerberos error: %s. Trying with delegate=True', str(e))
+            try:
+                api.Backend.rpcclient.connect(delegate=True,
+                                              nss_dir=tmp_nss_dir)
+                root_logger.debug('Try RPC connection')
+                api.Backend.rpcclient.forward('ping')
 
-            root_logger.info('Connection with delegate=True successful')
+                root_logger.info('Connection with delegate=True successful')
 
-            # The remote server is not capable of Kerberos S4U2Proxy delegation
-            # This features is implemented in IPA server version 2.2 and higher
-            root_logger.warning("Target IPA server has a lower version than " +
-                "the enrolled client")
-            root_logger.warning("Some capabilities including the ipa " +
-                "command capability may not be available")
-        except errors.PublicError, e2:
-            root_logger.warning(
-                'Second connect with delegate=True also failed: %s', str(e2))
+                # The remote server is not capable of Kerberos S4U2Proxy
+                # delegation. This features is implemented in IPA server
+                # version 2.2 and higher
+                root_logger.warning(
+                    "Target IPA server has a lower version than the enrolled "
+                    "client")
+                root_logger.warning(
+                    "Some capabilities including the ipa command capability "
+                    "may not be available")
+            except errors.PublicError, e2:
+                root_logger.warning(
+                    'Second connect with delegate=True also failed: %s',
+                    str(e2))
+                root_logger.error(
+                    "Cannot connect to the IPA server XML-RPC interface: %s",
+                    str(e2))
+                return CLIENT_INSTALL_ERROR
+        except errors.PublicError, e:
             root_logger.error(
-                "Cannot connect to the IPA server XML-RPC interface: %s",
-                str(e2))
+                'Cannot connect to the server due to generic error: %s',
+                str(e))
             return CLIENT_INSTALL_ERROR
-    except errors.PublicError, e:
-        root_logger.error(
-            'Cannot connect to the server due to generic error: %s', str(e))
-        return CLIENT_INSTALL_ERROR
+    finally:
+        shutil.rmtree(tmp_nss_dir)
 
     # Use the RPC directly so older servers are supported
     result = api.Backend.rpcclient.forward(
@@ -2590,14 +2608,38 @@ def install(options, env, fstore, statestore):
     if not remote_env['enable_ra']:
         disable_ra()
 
+    # Create IPA NSS database
+    try:
+        certdb.create_ipa_nssdb()
+    except ipautil.CalledProcessError, e:
+        root_logger.error("Failed to create IPA NSS database: %s", e)
+        return CLIENT_INSTALL_ERROR
+
     # Get CA certificates from the certificate store
     ca_certs = get_certs_from_ldap(cli_server[0], cli_basedn, cli_realm,
                                    remote_env['enable_ra'])
+    ca_certs_nss = [(c, n, certstore.key_policy_to_trust_flags(t, True, u))
+                    for (c, n, t, u) in ca_certs]
 
-    # Add the CA to the platform-dependant systemwide CA store
+    # Add the CA certificates to the IPA NSS database
+    root_logger.debug("Adding CA certificates to the IPA NSS database.")
+    for cert, nickname, trust_flags in ca_certs_nss:
+        try:
+            run([paths.CERTUTIL,
+                 "-A",
+                 "-d", paths.IPA_NSSDB_DIR,
+                 "-n", nickname,
+                 "-t", trust_flags],
+                stdin=cert)
+        except CalledProcessError, e:
+            root_logger.error(
+                "Failed to add %s to the IPA NSS database.", nickname)
+            return CLIENT_INSTALL_ERROR
+
+    # Add the CA certificates to the platform-dependant systemwide CA store
     tasks.insert_ca_certs_into_systemwide_ca_store(ca_certs)
 
-    # Add the CA to the default NSS database and trust it
+    # Add the CA certificates to the default NSS database
     if not purge_ipa_certs():
         root_logger.info(
             "Failed to remove old IPA certificates from the default NSS "
@@ -2610,12 +2652,10 @@ def install(options, env, fstore, statestore):
         root_logger.error("Failed to open /etc/pki/nssdb/ipa.txt: %s", e)
         return CLIENT_INSTALL_ERROR
 
-    for cert, nickname, trusted, ext_key_usage in ca_certs:
+    root_logger.debug(
+        "Attempting to add CA certificates to the default NSS database.")
+    for cert, nickname, trust_flags in ca_certs_nss:
         try:
-            root_logger.debug("Attempting to add CA directly to the "
-                              "default NSS database.")
-            trust_flags = certstore.key_policy_to_trust_flags(
-                trusted, True, ext_key_usage)
             run([paths.CERTUTIL,
                  "-A",
                  "-d", paths.NSS_DB_DIR,
@@ -2623,12 +2663,13 @@ def install(options, env, fstore, statestore):
                  "-t", trust_flags],
                 stdin=cert)
         except CalledProcessError, e:
-            root_logger.info("Failed to add CA to the default NSS database.")
+            root_logger.error(
+                "Failed to add %s to the default NSS database.", nickname)
             list_file.close()
             return CLIENT_INSTALL_ERROR
         else:
-            root_logger.info('Added the CA to the default NSS database.')
             list_file.write(nickname + '\n')
+    root_logger.info("Added CA certificates to the default NSS database.")
 
     list_file.close()
 
@@ -2854,7 +2895,3 @@ finally:
         os.remove(CCACHE_FILE)
     except Exception:
         pass
-    try:
-        shutil.rmtree(paths.IPA_NSSDB_DIR)
-    except Exception:
-        pass
diff --git a/ipa-client/ipaclient/ipa_certupdate.py b/ipa-client/ipaclient/ipa_certupdate.py
index 8e7fe04..57dbf20 100644
--- a/ipa-client/ipaclient/ipa_certupdate.py
+++ b/ipa-client/ipaclient/ipa_certupdate.py
@@ -70,6 +70,15 @@ class CertUpdate(admintool.AdminTool):
 
     def update_client(self, certs):
         self.update_file(paths.IPA_CA_CRT, certs)
+        self.update_db(paths.IPA_NSSDB_DIR, certs)
+
+        for nickname in ('IPA CA', 'External CA cert'):
+            try:
+                ipautil.run([paths.CERTUTIL, '-D',
+                             '-d', paths.NSS_DB_DIR,
+                             '-n', nickname])
+            except ipautil.CalledProcessError, e:
+                pass
 
         self.update_db(paths.NSS_DB_DIR, certs)
 
diff --git a/ipalib/rpc.py b/ipalib/rpc.py
index 1bfc4c3..7b966bd 100644
--- a/ipalib/rpc.py
+++ b/ipalib/rpc.py
@@ -483,7 +483,7 @@ class SSLTransport(LanguageAwareTransport):
             if self._connection and host == self._connection[0]:
                 return self._connection[1]
 
-        dbdir = getattr(context, 'nss_dir', paths.NSS_DB_DIR)
+        dbdir = getattr(context, 'nss_dir', paths.IPA_NSSDB_DIR)
         no_init = self.__nss_initialized(dbdir)
         if sys.version_info < (2, 7):
             conn = NSSHTTPS(host, 443, dbdir=dbdir, no_init=no_init)
diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py
index 6f2a29e..1493918 100644
--- a/ipaplatform/base/paths.py
+++ b/ipaplatform/base/paths.py
@@ -63,7 +63,7 @@ class BasePathNamespace(object):
     IPA_DNS_UPDATE_TXT = "/etc/ipa/.dns_update.txt"
     IPA_CA_CRT = "/etc/ipa/ca.crt"
     IPA_DEFAULT_CONF = "/etc/ipa/default.conf"
-    IPA_NSSDB_DIR = "/etc/ipa/.nssdb"
+    IPA_NSSDB_DIR = "/etc/ipa/nssdb"
     KRB5_CONF = "/etc/krb5.conf"
     KRB5_KEYTAB = "/etc/krb5.keytab"
     LDAP_CONF = "/etc/ldap.conf"
diff --git a/ipapython/certdb.py b/ipapython/certdb.py
index a858313..426c809 100644
--- a/ipapython/certdb.py
+++ b/ipapython/certdb.py
@@ -17,6 +17,34 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 
+import os
+
+from ipaplatform.paths import paths
+from ipapython import ipautil
+
 CA_NICKNAME_FMT = "%s IPA CA"
+
+
 def get_ca_nickname(realm, format=CA_NICKNAME_FMT):
     return format % realm
+
+
+def create_ipa_nssdb():
+    pwdfile = os.path.join(paths.IPA_NSSDB_DIR, 'pwdfile.txt')
+
+    ipautil.backup_file(pwdfile)
+    ipautil.backup_file(os.path.join(paths.IPA_NSSDB_DIR, 'cert8.db'))
+    ipautil.backup_file(os.path.join(paths.IPA_NSSDB_DIR, 'key3.db'))
+    ipautil.backup_file(os.path.join(paths.IPA_NSSDB_DIR, 'secmod.db'))
+
+    with open(pwdfile, 'w') as f:
+        f.write(ipautil.ipa_generate_password(pwd_len=40))
+    os.chmod(pwdfile, 0600)
+
+    ipautil.run([paths.CERTUTIL,
+         "-N",
+         "-d", paths.IPA_NSSDB_DIR,
+         "-f", pwdfile])
+    os.chmod(os.path.join(paths.IPA_NSSDB_DIR, 'cert8.db'), 0644)
+    os.chmod(os.path.join(paths.IPA_NSSDB_DIR, 'key3.db'), 0644)
+    os.chmod(os.path.join(paths.IPA_NSSDB_DIR, 'secmod.db'), 0644)
-- 
1.9.3

>From 2ea4763ea43db715359c27c97c12dcfaed78ffda Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Thu, 18 Sep 2014 11:19:46 +0200
Subject: [PATCH 02/13] Move NSSDatabase from ipaserver.certs to
 ipapython.certdb

https://fedorahosted.org/freeipa/ticket/4416
---
 ipapython/certdb.py        | 256 +++++++++++++++++++++++++++++++++++++++++++++
 ipaserver/install/certs.py | 254 +-------------------------------------------
 2 files changed, 257 insertions(+), 253 deletions(-)

diff --git a/ipapython/certdb.py b/ipapython/certdb.py
index 426c809..9c1dfc4 100644
--- a/ipapython/certdb.py
+++ b/ipapython/certdb.py
@@ -18,8 +18,14 @@
 #
 
 import os
+import re
+import tempfile
+import shutil
+from nss import nss
+from nss.error import NSPRError
 
 from ipaplatform.paths import paths
+from ipapython.ipa_log_manager import root_logger
 from ipapython import ipautil
 
 CA_NICKNAME_FMT = "%s IPA CA"
@@ -48,3 +54,253 @@ def create_ipa_nssdb():
     os.chmod(os.path.join(paths.IPA_NSSDB_DIR, 'cert8.db'), 0644)
     os.chmod(os.path.join(paths.IPA_NSSDB_DIR, 'key3.db'), 0644)
     os.chmod(os.path.join(paths.IPA_NSSDB_DIR, 'secmod.db'), 0644)
+
+
+def find_cert_from_txt(cert, start=0):
+    """
+    Given a cert blob (str) which may or may not contian leading and
+    trailing text, pull out just the certificate part. This will return
+    the FIRST cert in a stream of data.
+
+    Returns a tuple (certificate, last position in cert)
+    """
+    s = cert.find('-----BEGIN CERTIFICATE-----', start)
+    e = cert.find('-----END CERTIFICATE-----', s)
+    if e > 0:
+        e = e + 25
+
+    if s < 0 or e < 0:
+        raise RuntimeError("Unable to find certificate")
+
+    cert = cert[s:e]
+    return (cert, e)
+
+
+class NSSDatabase(object):
+    """A general-purpose wrapper around a NSS cert database
+
+    For permanent NSS databases, pass the cert DB directory to __init__
+
+    For temporary databases, do not pass nssdir, and call close() when done
+    to remove the DB. Alternatively, a NSSDatabase can be used as a
+    context manager that calls close() automatically.
+    """
+    # Traditionally, we used CertDB for our NSS DB operations, but that class
+    # got too tied to IPA server details, killing reusability.
+    # BaseCertDB is a class that knows nothing about IPA.
+    # Generic NSS DB code should be moved here.
+    def __init__(self, nssdir=None):
+        if nssdir is None:
+            self.secdir = tempfile.mkdtemp()
+            self._is_temporary = True
+        else:
+            self.secdir = nssdir
+            self._is_temporary = False
+
+    def close(self):
+        if self._is_temporary:
+            shutil.rmtree(self.secdir)
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, tb):
+        self.close()
+
+    def run_certutil(self, args, stdin=None):
+        new_args = [paths.CERTUTIL, "-d", self.secdir]
+        new_args = new_args + args
+        return ipautil.run(new_args, stdin)
+
+    def create_db(self, password_filename):
+        """Create cert DB
+
+        :param password_filename: Name of file containing the database password
+        """
+        self.run_certutil(["-N", "-f", password_filename])
+
+    def list_certs(self):
+        """Return nicknames and cert flags for all certs in the database
+
+        :return: List of (name, trust_flags) tuples
+        """
+        certs, stderr, returncode = self.run_certutil(["-L"])
+        certs = certs.splitlines()
+
+        # FIXME, this relies on NSS never changing the formatting of certutil
+        certlist = []
+        for cert in certs:
+            match = re.match(r'^(.+?)\s+(\w*,\w*,\w*)\s*$', cert)
+            if match:
+                certlist.append(match.groups())
+
+        return tuple(certlist)
+
+    def find_server_certs(self):
+        """Return nicknames and cert flags for server certs in the database
+
+        Server certs have an "u" character in the trust flags.
+
+        :return: List of (name, trust_flags) tuples
+        """
+        server_certs = []
+        for name, flags in self.list_certs():
+            if 'u' in flags:
+                server_certs.append((name, flags))
+
+        return server_certs
+
+    def get_trust_chain(self, nickname):
+        """Return names of certs in a given cert's trust chain
+
+        :param nickname: Name of the cert
+        :return: List of certificate names
+        """
+        root_nicknames = []
+        chain, stderr, returncode = self.run_certutil([
+            "-O", "-n", nickname])
+        chain = chain.splitlines()
+
+        for c in chain:
+            m = re.match('\s*"(.*)" \[.*', c)
+            if m:
+                root_nicknames.append(m.groups()[0])
+
+        return root_nicknames
+
+    def import_pkcs12(self, pkcs12_filename, db_password_filename,
+                      pkcs12_passwd=None):
+        args = [paths.PK12UTIL, "-d", self.secdir,
+                "-i", pkcs12_filename,
+                "-k", db_password_filename, '-v']
+        if pkcs12_passwd is not None:
+            pkcs12_passwd = pkcs12_passwd + '\n'
+            args = args + ["-w", paths.DEV_STDIN]
+        try:
+            ipautil.run(args, stdin=pkcs12_passwd)
+        except ipautil.CalledProcessError, e:
+            if e.returncode == 17:
+                raise RuntimeError("incorrect password for pkcs#12 file %s" %
+                                   pkcs12_filename)
+            elif e.returncode == 10:
+                raise RuntimeError("Failed to open %s" % pkcs12_filename)
+            else:
+                raise RuntimeError("unknown error import pkcs#12 file %s" %
+                                   pkcs12_filename)
+
+    def trust_root_cert(self, root_nickname, trust_flags=None):
+        if root_nickname[:7] == "Builtin":
+            root_logger.debug(
+                "No need to add trust for built-in root CAs, skipping %s" %
+                root_nickname)
+        else:
+            if trust_flags is None:
+                trust_flags = 'C,,'
+            try:
+                self.run_certutil(["-M", "-n", root_nickname,
+                                   "-t", trust_flags])
+            except ipautil.CalledProcessError, e:
+                raise RuntimeError(
+                    "Setting trust on %s failed" % root_nickname)
+
+    def get_cert(self, nickname, pem=False):
+        args = ['-L', '-n', nickname]
+        if pem:
+            args.append('-a')
+        else:
+            args.append('-r')
+        try:
+            cert, err, returncode = self.run_certutil(args)
+        except ipautil.CalledProcessError:
+            raise RuntimeError("Failed to get %s" % nickname)
+        return cert
+
+    def export_pem_cert(self, nickname, location):
+        """Export the given cert to PEM file in the given location"""
+        cert = self.get_cert(nickname)
+        with open(location, "w+") as fd:
+            fd.write(cert)
+        os.chmod(location, 0444)
+
+    def import_pem_cert(self, nickname, flags, location):
+        """Import a cert form the given PEM file.
+
+        The file must contain exactly one certificate.
+        """
+        try:
+            with open(location) as fd:
+                certs = fd.read()
+        except IOError as e:
+            raise RuntimeError(
+                "Failed to open %s: %s" % (location, e.strerror)
+            )
+
+        cert, st = find_cert_from_txt(certs)
+        self.add_cert(cert, nickname, flags, pem=True)
+
+        try:
+            find_cert_from_txt(certs, st)
+        except RuntimeError:
+            pass
+        else:
+            raise ValueError('%s contains more than one certificate' %
+                             location)
+
+    def add_cert(self, cert, nick, flags, pem=False):
+        args = ["-A", "-n", nick, "-t", flags]
+        if pem:
+            args.append("-a")
+        self.run_certutil(args, stdin=cert)
+
+    def delete_cert(self, nick):
+        self.run_certutil(["-D", "-n", nick])
+
+    def verify_server_cert_validity(self, nickname, hostname):
+        """Verify a certificate is valid for a SSL server with given hostname
+
+        Raises a ValueError if the certificate is invalid.
+        """
+        certdb = cert = None
+        if nss.nss_is_initialized():
+            nss.nss_shutdown()
+        nss.nss_init(self.secdir)
+        try:
+            certdb = nss.get_default_certdb()
+            cert = nss.find_cert_from_nickname(nickname)
+            intended_usage = nss.certificateUsageSSLServer
+            try:
+                approved_usage = cert.verify_now(certdb, True, intended_usage)
+            except NSPRError, e:
+                if e.errno != -8102:
+                    raise ValueError(e.strerror)
+                approved_usage = 0
+            if not approved_usage & intended_usage:
+                raise ValueError('invalid for a SSL server')
+            if not cert.verify_hostname(hostname):
+                raise ValueError('invalid for server %s' % hostname)
+        finally:
+            del certdb, cert
+            nss.nss_shutdown()
+
+        return None
+
+    def verify_ca_cert_validity(self, nickname):
+        certdb = cert = None
+        if nss.nss_is_initialized():
+            nss.nss_shutdown()
+        nss.nss_init(self.secdir)
+        try:
+            certdb = nss.get_default_certdb()
+            cert = nss.find_cert_from_nickname(nickname)
+            intended_usage = nss.certificateUsageSSLCA
+            try:
+                approved_usage = cert.verify_now(certdb, True, intended_usage)
+            except NSPRError, e:
+                if e.errno != -8102:    # SEC_ERROR_INADEQUATE_KEY_USAGE
+                    raise ValueError(e.strerror)
+                approved_usage = 0
+            if approved_usage & intended_usage != intended_usage:
+                raise ValueError('invalid for a CA')
+        finally:
+            del certdb, cert
+            nss.nss_shutdown()
diff --git a/ipaserver/install/certs.py b/ipaserver/install/certs.py
index 4d508cd..c2ab59e 100644
--- a/ipaserver/install/certs.py
+++ b/ipaserver/install/certs.py
@@ -19,7 +19,6 @@
 
 import os
 import stat
-import re
 import sys
 import tempfile
 import shutil
@@ -28,15 +27,12 @@ import pwd
 import base64
 from hashlib import sha1
 
-from nss import nss
-from nss.error import NSPRError
-
 from ipapython.ipa_log_manager import root_logger
 from ipapython import dogtag
 from ipapython import sysrestore
 from ipapython import ipautil
 from ipapython import certmonger
-from ipapython.certdb import get_ca_nickname
+from ipapython.certdb import get_ca_nickname, find_cert_from_txt, NSSDatabase
 from ipapython.dn import DN
 from ipalib import pkcs10, x509, api
 from ipalib.errors import CertificateOperationError
@@ -48,24 +44,6 @@ from ipaplatform.paths import paths
 # where apache can reach
 NSS_DIR = paths.HTTPD_ALIAS_DIR
 
-def find_cert_from_txt(cert, start=0):
-    """
-    Given a cert blob (str) which may or may not contian leading and
-    trailing text, pull out just the certificate part. This will return
-    the FIRST cert in a stream of data.
-
-    Returns a tuple (certificate, last position in cert)
-    """
-    s = cert.find('-----BEGIN CERTIFICATE-----', start)
-    e = cert.find('-----END CERTIFICATE-----', s)
-    if e > 0: e = e + 25
-
-    if s < 0 or e < 0:
-        raise RuntimeError("Unable to find certificate")
-
-    cert = cert[s:e]
-    return (cert, e)
-
 def get_cert_nickname(cert):
     """
     Using the subject from cert come up with a nickname suitable
@@ -83,236 +61,6 @@ def get_cert_nickname(cert):
     return (str(dn[0]), dn)
 
 
-class NSSDatabase(object):
-    """A general-purpose wrapper around a NSS cert database
-
-    For permanent NSS databases, pass the cert DB directory to __init__
-
-    For temporary databases, do not pass nssdir, and call close() when done
-    to remove the DB. Alternatively, a NSSDatabase can be used as a
-    context manager that calls close() automatically.
-    """
-    # Traditionally, we used CertDB for our NSS DB operations, but that class
-    # got too tied to IPA server details, killing reusability.
-    # BaseCertDB is a class that knows nothing about IPA.
-    # Generic NSS DB code should be moved here.
-    def __init__(self, nssdir=None):
-        if nssdir is None:
-            self.secdir = tempfile.mkdtemp()
-            self._is_temporary = True
-        else:
-            self.secdir = nssdir
-            self._is_temporary = False
-
-    def close(self):
-        if self._is_temporary:
-            shutil.rmtree(self.secdir)
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, type, value, tb):
-        self.close()
-
-    def run_certutil(self, args, stdin=None):
-        new_args = [paths.CERTUTIL, "-d", self.secdir]
-        new_args = new_args + args
-        return ipautil.run(new_args, stdin)
-
-    def create_db(self, password_filename):
-        """Create cert DB
-
-        :param password_filename: Name of file containing the database password
-        """
-        self.run_certutil(["-N", "-f", password_filename])
-
-    def list_certs(self):
-        """Return nicknames and cert flags for all certs in the database
-
-        :return: List of (name, trust_flags) tuples
-        """
-        certs, stderr, returncode = self.run_certutil(["-L"])
-        certs = certs.splitlines()
-
-        # FIXME, this relies on NSS never changing the formatting of certutil
-        certlist = []
-        for cert in certs:
-            match = re.match(r'^(.+?)\s+(\w*,\w*,\w*)\s*$', cert)
-            if match:
-                certlist.append(match.groups())
-
-        return tuple(certlist)
-
-    def find_server_certs(self):
-        """Return nicknames and cert flags for server certs in the database
-
-        Server certs have an "u" character in the trust flags.
-
-        :return: List of (name, trust_flags) tuples
-        """
-        server_certs = []
-        for name, flags in self.list_certs():
-            if 'u' in flags:
-                server_certs.append((name, flags))
-
-        return server_certs
-
-    def get_trust_chain(self, nickname):
-        """Return names of certs in a given cert's trust chain
-
-        :param nickname: Name of the cert
-        :return: List of certificate names
-        """
-        root_nicknames = []
-        chain, stderr, returncode = self.run_certutil([
-            "-O", "-n", nickname])
-        chain = chain.splitlines()
-
-        for c in chain:
-            m = re.match('\s*"(.*)" \[.*', c)
-            if m:
-                root_nicknames.append(m.groups()[0])
-
-        return root_nicknames
-
-    def import_pkcs12(self, pkcs12_filename, db_password_filename,
-                      pkcs12_passwd=None):
-        args = [paths.PK12UTIL, "-d", self.secdir,
-                "-i", pkcs12_filename,
-                "-k", db_password_filename, '-v']
-        if pkcs12_passwd is not None:
-            pkcs12_passwd = pkcs12_passwd + '\n'
-            args = args + ["-w", paths.DEV_STDIN]
-        try:
-            ipautil.run(args, stdin=pkcs12_passwd)
-        except ipautil.CalledProcessError, e:
-            if e.returncode == 17:
-                raise RuntimeError("incorrect password for pkcs#12 file %s" %
-                    pkcs12_filename)
-            elif e.returncode == 10:
-                raise RuntimeError("Failed to open %s" % pkcs12_filename)
-            else:
-                raise RuntimeError("unknown error import pkcs#12 file %s" %
-                    pkcs12_filename)
-
-    def trust_root_cert(self, root_nickname, trust_flags=None):
-        if root_nickname[:7] == "Builtin":
-            root_logger.debug(
-                "No need to add trust for built-in root CAs, skipping %s" %
-                root_nickname)
-        else:
-            if trust_flags is None:
-                trust_flags = 'C,,'
-            try:
-                self.run_certutil(["-M", "-n", root_nickname,
-                                   "-t", trust_flags])
-            except ipautil.CalledProcessError, e:
-                raise RuntimeError(
-                    "Setting trust on %s failed" % root_nickname)
-
-    def get_cert(self, nickname, pem=False):
-        args = ['-L', '-n', nickname]
-        if pem:
-            args.append('-a')
-        else:
-            args.append('-r')
-        try:
-            cert, err, returncode = self.run_certutil(args)
-        except ipautil.CalledProcessError:
-            raise RuntimeError("Failed to get %s" % nickname)
-        return cert
-
-    def export_pem_cert(self, nickname, location):
-        """Export the given cert to PEM file in the given location"""
-        cert = self.get_cert(nickname)
-        with open(location, "w+") as fd:
-            fd.write(cert)
-        os.chmod(location, 0444)
-
-    def import_pem_cert(self, nickname, flags, location):
-        """Import a cert form the given PEM file.
-
-        The file must contain exactly one certificate.
-        """
-        try:
-            with open(location) as fd:
-                certs = fd.read()
-        except IOError as e:
-            raise RuntimeError(
-                "Failed to open %s: %s" % (location, e.strerror)
-            )
-
-        cert, st = find_cert_from_txt(certs)
-        self.add_cert(cert, nickname, flags, pem=True)
-
-        try:
-            find_cert_from_txt(certs, st)
-        except RuntimeError:
-            pass
-        else:
-            raise ValueError('%s contains more than one certificate' %
-                             location)
-
-    def add_cert(self, cert, nick, flags, pem=False):
-        args = ["-A", "-n", nick, "-t", flags]
-        if pem:
-            args.append("-a")
-        self.run_certutil(args, stdin=cert)
-
-    def delete_cert(self, nick):
-        self.run_certutil(["-D", "-n", nick])
-
-    def verify_server_cert_validity(self, nickname, hostname):
-        """Verify a certificate is valid for a SSL server with given hostname
-
-        Raises a ValueError if the certificate is invalid.
-        """
-        certdb = cert = None
-        if nss.nss_is_initialized():
-            nss.nss_shutdown()
-        nss.nss_init(self.secdir)
-        try:
-            certdb = nss.get_default_certdb()
-            cert = nss.find_cert_from_nickname(nickname)
-            intended_usage = nss.certificateUsageSSLServer
-            try:
-                approved_usage = cert.verify_now(certdb, True, intended_usage)
-            except NSPRError, e:
-                if e.errno != -8102:
-                    raise ValueError(e.strerror)
-                approved_usage = 0
-            if not approved_usage & intended_usage:
-                raise ValueError('invalid for a SSL server')
-            if not cert.verify_hostname(hostname):
-                raise ValueError('invalid for server %s' % hostname)
-        finally:
-            del certdb, cert
-            nss.nss_shutdown()
-
-        return None
-
-    def verify_ca_cert_validity(self, nickname):
-        certdb = cert = None
-        if nss.nss_is_initialized():
-            nss.nss_shutdown()
-        nss.nss_init(self.secdir)
-        try:
-            certdb = nss.get_default_certdb()
-            cert = nss.find_cert_from_nickname(nickname)
-            intended_usage = nss.certificateUsageSSLCA
-            try:
-                approved_usage = cert.verify_now(certdb, True, intended_usage)
-            except NSPRError, e:
-                if e.errno != -8102:    # SEC_ERROR_INADEQUATE_KEY_USAGE
-                    raise ValueError(e.strerror)
-                approved_usage = 0
-            if approved_usage & intended_usage != intended_usage:
-                raise ValueError('invalid for a CA')
-        finally:
-            del certdb, cert
-            nss.nss_shutdown()
-
-
 class CertDB(object):
     """An IPA-server-specific wrapper around NSS
 
-- 
1.9.3

>From efb446d960f18ec443025bd16ff99ca2edd4a36b Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Thu, 18 Sep 2014 11:42:14 +0200
Subject: [PATCH 03/13] Add NSSDatabase.has_nickname for checking nickname
 presence in a NSS DB

---
 ipapython/certdb.py | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/ipapython/certdb.py b/ipapython/certdb.py
index 9c1dfc4..fc09919 100644
--- a/ipapython/certdb.py
+++ b/ipapython/certdb.py
@@ -215,6 +215,14 @@ class NSSDatabase(object):
             raise RuntimeError("Failed to get %s" % nickname)
         return cert
 
+    def has_nickname(self, nickname):
+        try:
+            self.get_cert(nickname)
+        except RuntimeError:
+            return False
+        else:
+            return True
+
     def export_pem_cert(self, nickname, location):
         """Export the given cert to PEM file in the given location"""
         cert = self.get_cert(nickname)
-- 
1.9.3

>From bf9414edde0d117d8e1a1cc8c9233f3a486f421d Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Thu, 18 Sep 2014 12:00:15 +0200
Subject: [PATCH 04/13] Use NSSDatabase instead of direct certutil calls in
 client code

---
 ipa-client/ipa-install/ipa-client-install | 50 ++++++++-----------------------
 ipa-client/ipaclient/ipa_certupdate.py    | 14 ++++-----
 ipapython/certdb.py                       | 20 ++++++-------
 3 files changed, 26 insertions(+), 58 deletions(-)

diff --git a/ipa-client/ipa-install/ipa-client-install b/ipa-client/ipa-install/ipa-client-install
index 82e4b18..85f7c8d 100755
--- a/ipa-client/ipa-install/ipa-client-install
+++ b/ipa-client/ipa-install/ipa-client-install
@@ -226,14 +226,6 @@ def logging_setup(options):
 def log_service_error(name, action, error):
     root_logger.error("%s failed to %s: %s", name, action, str(error))
 
-def nickname_exists(nickname):
-        (sout, serr, returncode) = run([paths.CERTUTIL, "-L", "-d", paths.NSS_DB_DIR, "-n", nickname], raiseonerr=False)
-
-        if returncode == 0:
-            return True
-        else:
-            return False
-
 def purge_ipa_certs(additional=[]):
     filename = paths.NSSDB_IPA_TXT
     if file_exists(filename):
@@ -258,12 +250,11 @@ def purge_ipa_certs(additional=[]):
         if nickname:
             nicknames.add(nickname)
 
+    sys_db = certdb.NSSDatabase(paths.NSS_DB_DIR)
     for nickname in nicknames:
-        while nickname_exists(nickname):
+        while sys_db.has_nickname(nickname):
             try:
-                run([paths.CERTUTIL, "-D",
-                     "-d", paths.NSS_DB_DIR,
-                     "-n", nickname])
+                sys_db.delete_cert(nickname)
             except Exception, e:
                 root_logger.error(
                     "Failed to remove %s from /etc/pki/nssdb: %s", nickname, e)
@@ -2532,23 +2523,16 @@ def install(options, env, fstore, statestore):
     except ValueError:
         pass
 
-    tmp_nss_dir = tempfile.mkdtemp()
-    try:
+    with certdb.NSSDatabase() as tmp_db:
         # Add CA certs to a temporary NSS database
         try:
             pwd_file = ipautil.write_tmp_file(ipautil.ipa_generate_password())
-            run([paths.CERTUTIL, '-N',
-                 '-d', tmp_nss_dir,
-                 '-f', pwd_file.name])
+            tmp_db.create_db(pwd_file.name)
 
             ca_certs = x509.load_certificate_list_from_file(CACERT)
             ca_certs = [cert.der_data for cert in ca_certs]
             for i, cert in enumerate(ca_certs):
-                run([paths.CERTUTIL, '-A',
-                     '-d', tmp_nss_dir,
-                     '-n', 'CA certificate %d' % (i + 1),
-                     '-t', 'C,,'],
-                     stdin=cert)
+                tmp_db.add_cert(cert, 'CA certificate %d' % (i + 1), 'C,,')
         except CalledProcessError, e:
             root_logger.info("Failed to add CA to temporary NSS database.")
             return CLIENT_INSTALL_ERROR
@@ -2556,7 +2540,7 @@ def install(options, env, fstore, statestore):
         # Now, let's try to connect to the server's XML-RPC interface
         connected = False
         try:
-            api.Backend.rpcclient.connect(nss_dir=tmp_nss_dir)
+            api.Backend.rpcclient.connect(nss_dir=tmp_db.secdir)
             connected = True
             root_logger.debug('Try RPC connection')
             api.Backend.rpcclient.forward('ping')
@@ -2567,7 +2551,7 @@ def install(options, env, fstore, statestore):
                 'Kerberos error: %s. Trying with delegate=True', str(e))
             try:
                 api.Backend.rpcclient.connect(delegate=True,
-                                              nss_dir=tmp_nss_dir)
+                                              nss_dir=tmp_db.secdir)
                 root_logger.debug('Try RPC connection')
                 api.Backend.rpcclient.forward('ping')
 
@@ -2595,8 +2579,6 @@ def install(options, env, fstore, statestore):
                 'Cannot connect to the server due to generic error: %s',
                 str(e))
             return CLIENT_INSTALL_ERROR
-    finally:
-        shutil.rmtree(tmp_nss_dir)
 
     # Use the RPC directly so older servers are supported
     result = api.Backend.rpcclient.forward(
@@ -2623,14 +2605,10 @@ def install(options, env, fstore, statestore):
 
     # Add the CA certificates to the IPA NSS database
     root_logger.debug("Adding CA certificates to the IPA NSS database.")
+    ipa_db = certdb.NSSDatabase(paths.IPA_NSSDB_DIR)
     for cert, nickname, trust_flags in ca_certs_nss:
         try:
-            run([paths.CERTUTIL,
-                 "-A",
-                 "-d", paths.IPA_NSSDB_DIR,
-                 "-n", nickname,
-                 "-t", trust_flags],
-                stdin=cert)
+            ipa_db.add_cert(cert, nickname, trust_flags)
         except CalledProcessError, e:
             root_logger.error(
                 "Failed to add %s to the IPA NSS database.", nickname)
@@ -2654,14 +2632,10 @@ def install(options, env, fstore, statestore):
 
     root_logger.debug(
         "Attempting to add CA certificates to the default NSS database.")
+    sys_db = certdb.NSSDatabase(paths.NSS_DB_DIR)
     for cert, nickname, trust_flags in ca_certs_nss:
         try:
-            run([paths.CERTUTIL,
-                 "-A",
-                 "-d", paths.NSS_DB_DIR,
-                 "-n", nickname,
-                 "-t", trust_flags],
-                stdin=cert)
+            sys_db.add_cert(cert, nickname, trust_flags)
         except CalledProcessError, e:
             root_logger.error(
                 "Failed to add %s to the default NSS database.", nickname)
diff --git a/ipa-client/ipaclient/ipa_certupdate.py b/ipa-client/ipaclient/ipa_certupdate.py
index 57dbf20..f7b0e29 100644
--- a/ipa-client/ipaclient/ipa_certupdate.py
+++ b/ipa-client/ipaclient/ipa_certupdate.py
@@ -22,7 +22,7 @@ import tempfile
 import shutil
 
 from ipapython import (admintool, ipautil, ipaldap, sysrestore, dogtag,
-                       certmonger)
+                       certmonger, certdb)
 from ipaplatform import services
 from ipaplatform.paths import paths
 from ipaplatform.tasks import tasks
@@ -72,11 +72,10 @@ class CertUpdate(admintool.AdminTool):
         self.update_file(paths.IPA_CA_CRT, certs)
         self.update_db(paths.IPA_NSSDB_DIR, certs)
 
+        sys_db = certdb.NSSDatabase(paths.NSS_DB_DIR)
         for nickname in ('IPA CA', 'External CA cert'):
             try:
-                ipautil.run([paths.CERTUTIL, '-D',
-                             '-d', paths.NSS_DB_DIR,
-                             '-n', nickname])
+                sys_db.delete_cert(nickname)
             except ipautil.CalledProcessError, e:
                 pass
 
@@ -165,15 +164,12 @@ class CertUpdate(admintool.AdminTool):
             self.log.error("failed to update %s: %s", filename, e)
 
     def update_db(self, path, certs):
+        db = certdb.NSSDatabase(path)
         for cert, nickname, trusted, eku in certs:
             trust_flags = certstore.key_policy_to_trust_flags(
                 trusted, True, eku)
             try:
-                ipautil.run([paths.CERTUTIL, '-A',
-                             '-d', path,
-                             '-n', nickname,
-                             '-t', trust_flags],
-                             stdin=cert)
+                db.add_cert(cert, nickname, trust_flags)
             except ipautil.CalledProcessError, e:
                 self.log.error(
                     "failed to update %s in %s: %s", nickname, path, e)
diff --git a/ipapython/certdb.py b/ipapython/certdb.py
index fc09919..e6c042e 100644
--- a/ipapython/certdb.py
+++ b/ipapython/certdb.py
@@ -36,24 +36,22 @@ def get_ca_nickname(realm, format=CA_NICKNAME_FMT):
 
 
 def create_ipa_nssdb():
-    pwdfile = os.path.join(paths.IPA_NSSDB_DIR, 'pwdfile.txt')
+    db = NSSDatabase(paths.IPA_NSSDB_DIR)
+    pwdfile = os.path.join(db.secdir, 'pwdfile.txt')
 
     ipautil.backup_file(pwdfile)
-    ipautil.backup_file(os.path.join(paths.IPA_NSSDB_DIR, 'cert8.db'))
-    ipautil.backup_file(os.path.join(paths.IPA_NSSDB_DIR, 'key3.db'))
-    ipautil.backup_file(os.path.join(paths.IPA_NSSDB_DIR, 'secmod.db'))
+    ipautil.backup_file(os.path.join(db.secdir, 'cert8.db'))
+    ipautil.backup_file(os.path.join(db.secdir, 'key3.db'))
+    ipautil.backup_file(os.path.join(db.secdir, 'secmod.db'))
 
     with open(pwdfile, 'w') as f:
         f.write(ipautil.ipa_generate_password(pwd_len=40))
     os.chmod(pwdfile, 0600)
 
-    ipautil.run([paths.CERTUTIL,
-         "-N",
-         "-d", paths.IPA_NSSDB_DIR,
-         "-f", pwdfile])
-    os.chmod(os.path.join(paths.IPA_NSSDB_DIR, 'cert8.db'), 0644)
-    os.chmod(os.path.join(paths.IPA_NSSDB_DIR, 'key3.db'), 0644)
-    os.chmod(os.path.join(paths.IPA_NSSDB_DIR, 'secmod.db'), 0644)
+    db.create_db(pwdfile)
+    os.chmod(os.path.join(db.secdir, 'cert8.db'), 0644)
+    os.chmod(os.path.join(db.secdir, 'key3.db'), 0644)
+    os.chmod(os.path.join(db.secdir, 'secmod.db'), 0644)
 
 
 def find_cert_from_txt(cert, start=0):
-- 
1.9.3

>From 3c9913a4e0ba4d0a4da60c7ebbe6497c63e8fa4e Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Mon, 22 Sep 2014 11:13:15 +0200
Subject: [PATCH 05/13] Use /etc/ipa/nssdb to get nicknames of IPA certs
 installed in /etc/pki/nssdb

---
 ipa-client/ipa-install/ipa-client-install | 78 +++++++++----------------------
 ipa-client/ipaclient/ipa_certupdate.py    | 60 +++++++++---------------
 ipaplatform/base/paths.py                 |  1 -
 3 files changed, 43 insertions(+), 96 deletions(-)

diff --git a/ipa-client/ipa-install/ipa-client-install b/ipa-client/ipa-install/ipa-client-install
index 85f7c8d..53678d2 100755
--- a/ipa-client/ipa-install/ipa-client-install
+++ b/ipa-client/ipa-install/ipa-client-install
@@ -226,41 +226,6 @@ def logging_setup(options):
 def log_service_error(name, action, error):
     root_logger.error("%s failed to %s: %s", name, action, str(error))
 
-def purge_ipa_certs(additional=[]):
-    filename = paths.NSSDB_IPA_TXT
-    if file_exists(filename):
-        try:
-            with open(filename, 'r') as f:
-                lines = f.readlines()
-        except IOError, e:
-            root_logger.error("Failed to open %s: %s", filename, e)
-            return False
-        finally:
-            try:
-                os.unlink(filename)
-            except OSError, e:
-                root_logger.error("Failed to remove %s: %s", filename, e)
-                return False
-    else:
-        lines = []
-
-    nicknames = set(additional)
-    for line in lines:
-        nickname = line.strip()
-        if nickname:
-            nicknames.add(nickname)
-
-    sys_db = certdb.NSSDatabase(paths.NSS_DB_DIR)
-    for nickname in nicknames:
-        while sys_db.has_nickname(nickname):
-            try:
-                sys_db.delete_cert(nickname)
-            except Exception, e:
-                root_logger.error(
-                    "Failed to remove %s from /etc/pki/nssdb: %s", nickname, e)
-
-    return True
-
 def cert_summary(msg, certs, indent='    '):
     if msg:
         s = '%s\n' % msg
@@ -541,16 +506,32 @@ def uninstall(options, env):
             cmonger.service_name, str(e))
 
     # Remove our host cert and CA cert
-    for filename in (os.path.join(paths.IPA_NSSDB_DIR, 'cert8.db'),
-                     os.path.join(paths.IPA_NSSDB_DIR, 'key3.db'),
-                     os.path.join(paths.IPA_NSSDB_DIR, 'secmod.db'),
-                     os.path.join(paths.IPA_NSSDB_DIR, 'pwdfile.txt')):
+    ipa_db = certdb.NSSDatabase(paths.IPA_NSSDB_DIR)
+    try:
+        ipa_certs = ipa_db.list_certs()
+    except CalledProcessError, e:
+        root_logger.error(
+            "Failed to list certificates in %s: %s", ipa_db.secdir, e)
+        ipa_certs = []
+
+    for filename in (os.path.join(ipa_db.secdir, 'cert8.db'),
+                     os.path.join(ipa_db.secdir, 'key3.db'),
+                     os.path.join(ipa_db.secdir, 'secmod.db'),
+                     os.path.join(ipa_db.secdir, 'pwdfile.txt')):
         try:
             os.remove(filename)
         except OSError, e:
             root_logger.error("Failed to remove %s: %s", filename, e)
 
-    purge_ipa_certs({client_nss_nickname, 'IPA CA', 'External CA cert'})
+    sys_db = certdb.NSSDatabase(paths.NSS_DB_DIR)
+    for nickname, trust_flags in ipa_certs:
+        while sys_db.has_nickname(nickname):
+            try:
+                sys_db.delete_cert(nickname)
+            except Exception, e:
+                root_logger.error("Failed to remove %s from %s: %s",
+                                  nickname, sys_db.secdir, e)
+                break
 
     try:
         cmonger.stop()
@@ -2618,18 +2599,6 @@ def install(options, env, fstore, statestore):
     tasks.insert_ca_certs_into_systemwide_ca_store(ca_certs)
 
     # Add the CA certificates to the default NSS database
-    if not purge_ipa_certs():
-        root_logger.info(
-            "Failed to remove old IPA certificates from the default NSS "
-            "database.")
-        return CLIENT_INSTALL_ERROR
-
-    try:
-        list_file = open(paths.NSSDB_IPA_TXT, 'w')
-    except IOError, e:
-        root_logger.error("Failed to open /etc/pki/nssdb/ipa.txt: %s", e)
-        return CLIENT_INSTALL_ERROR
-
     root_logger.debug(
         "Attempting to add CA certificates to the default NSS database.")
     sys_db = certdb.NSSDatabase(paths.NSS_DB_DIR)
@@ -2639,14 +2608,9 @@ def install(options, env, fstore, statestore):
         except CalledProcessError, e:
             root_logger.error(
                 "Failed to add %s to the default NSS database.", nickname)
-            list_file.close()
             return CLIENT_INSTALL_ERROR
-        else:
-            list_file.write(nickname + '\n')
     root_logger.info("Added CA certificates to the default NSS database.")
 
-    list_file.close()
-
     if not options.on_master:
         client_dns(cli_server[0], hostname, options.dns_updates)
 
diff --git a/ipa-client/ipaclient/ipa_certupdate.py b/ipa-client/ipaclient/ipa_certupdate.py
index f7b0e29..14bb25f 100644
--- a/ipa-client/ipaclient/ipa_certupdate.py
+++ b/ipa-client/ipaclient/ipa_certupdate.py
@@ -70,49 +70,32 @@ class CertUpdate(admintool.AdminTool):
 
     def update_client(self, certs):
         self.update_file(paths.IPA_CA_CRT, certs)
-        self.update_db(paths.IPA_NSSDB_DIR, certs)
 
+        ipa_db = certdb.NSSDatabase(paths.IPA_NSSDB_DIR)
         sys_db = certdb.NSSDatabase(paths.NSS_DB_DIR)
-        for nickname in ('IPA CA', 'External CA cert'):
-            try:
-                sys_db.delete_cert(nickname)
-            except ipautil.CalledProcessError, e:
-                pass
 
-        self.update_db(paths.NSS_DB_DIR, certs)
-
-        new_nicknames = set(c[1] for c in certs)
-        old_nicknames = set()
-        if ipautil.file_exists(paths.NSSDB_IPA_TXT):
-            try:
-                list_file = open(paths.NSSDB_IPA_TXT, 'r')
-            except IOError, e:
-                self.log.error("failed to open %s: %s", paths.NSSDB_IPA_TXT, e)
-            else:
+        # Remove IPA certs from /etc/pki/nssdb
+        for nickname, trust_flags in ipa_db.list_certs():
+            while sys_db.has_nickname(nickname):
                 try:
-                    lines = list_file.readlines()
-                except IOError, e:
-                    self.log.error(
-                        "failed to read %s: %s", paths.NSSDB_IPA_TXT, e)
-                else:
-                    for line in lines:
-                        nickname = line.strip()
-                        if nickname:
-                            old_nicknames.add(nickname)
-                list_file.close()
-        if new_nicknames != old_nicknames:
-            try:
-                list_file = open(paths.NSSDB_IPA_TXT, 'w')
-            except IOError, e:
-                self.log.error("failed to open %s: %s", paths.NSSDB_IPA_TXT, e)
-            else:
+                    sys_db.delete_cert(nickname)
+                except ipautil.CalledProcessError, e:
+                    self.log.error("Failed to remove %s from %s: %s",
+                                   nickname, sys_db.secdir, e)
+                    break
+
+        # Remove old IPA certs from /etc/ipa/nssdb
+        for nickname in ('IPA CA', 'External CA cert'):
+            while ipa_db.has_nickname(nickname):
                 try:
-                    for nickname in new_nicknames:
-                        list_file.write(nickname + '\n')
-                except IOError, e:
-                    self.log.error(
-                        "failed to write %s: %s", paths.NSSDB_IPA_TXT, e)
-                list_file.close()
+                    ipa_db.delete_cert(nickname)
+                except ipautil.CalledProcessError, e:
+                    self.log.error("Failed to remove %s from %s: %s",
+                                   nickname, ipa_db.secdir, e)
+                    break
+
+        self.update_db(ipa_db.secdir, certs)
+        self.update_db(sys_db.secdir, certs)
 
         tasks.remove_ca_certs_from_systemwide_ca_store()
         tasks.insert_ca_certs_into_systemwide_ca_store(certs)
@@ -133,6 +116,7 @@ class CertUpdate(admintool.AdminTool):
         criteria = {
             'cert-database': dogtag_constants.ALIAS_DIR,
             'cert-nickname': nickname,
+            'ca-name': 'dogtag-ipa-ca-renew-agent',
         }
         request_id = certmonger.get_request_id(criteria)
         if request_id is not None:
diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py
index 1493918..5f73d85 100644
--- a/ipaplatform/base/paths.py
+++ b/ipaplatform/base/paths.py
@@ -85,7 +85,6 @@ class BasePathNamespace(object):
     NSSDB_CERT8_DB = "/etc/pki/nssdb/cert8.db"
     NSSDB_KEY3_DB = "/etc/pki/nssdb/key3.db"
     NSSDB_SECMOD_DB = "/etc/pki/nssdb/secmod.db"
-    NSSDB_IPA_TXT = "/etc/pki/nssdb/ipa.txt"
     PKI_TOMCAT = "/etc/pki/pki-tomcat"
     PKI_TOMCAT_ALIAS_DIR = "/etc/pki/pki-tomcat/alias/"
     PKI_TOMCAT_PASSWORD_CONF = "/etc/pki/pki-tomcat/password.conf"
-- 
1.9.3

>From 840b849535bbc74489c54cf1ea709bf8a9bfb7a0 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Thu, 18 Sep 2014 10:52:56 +0200
Subject: [PATCH 06/13] Check if IPA client is configured in ipa-certupdate

https://fedorahosted.org/freeipa/ticket/4460
---
 ipa-client/ipaclient/ipa_certupdate.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/ipa-client/ipaclient/ipa_certupdate.py b/ipa-client/ipaclient/ipa_certupdate.py
index 14bb25f..48f11b4 100644
--- a/ipa-client/ipaclient/ipa_certupdate.py
+++ b/ipa-client/ipaclient/ipa_certupdate.py
@@ -41,6 +41,12 @@ class CertUpdate(admintool.AdminTool):
         super(CertUpdate, self).validate_options(needs_root=True)
 
     def run(self):
+        fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
+        if (not fstore.has_files() and
+            not os.path.exists(paths.IPA_DEFAULT_CONF)):
+            raise admintool.ScriptError(
+                "IPA client is not configured on this system.")
+
         api.bootstrap(context='cli_installer')
         api.finalize()
 
-- 
1.9.3

>From cfd3504f7ac4ed06985b4fd5d437cda0325c7e31 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Thu, 18 Sep 2014 11:15:49 +0200
Subject: [PATCH 07/13] Get server hostname from jsonrpc_uri in ipa-certupdate

---
 ipa-client/ipaclient/ipa_certupdate.py | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/ipa-client/ipaclient/ipa_certupdate.py b/ipa-client/ipaclient/ipa_certupdate.py
index 48f11b4..ff16b9b 100644
--- a/ipa-client/ipaclient/ipa_certupdate.py
+++ b/ipa-client/ipaclient/ipa_certupdate.py
@@ -20,6 +20,7 @@
 import os
 import tempfile
 import shutil
+import urlparse
 
 from ipapython import (admintool, ipautil, ipaldap, sysrestore, dogtag,
                        certmonger, certdb)
@@ -50,10 +51,7 @@ class CertUpdate(admintool.AdminTool):
         api.bootstrap(context='cli_installer')
         api.finalize()
 
-        try:
-            server = api.env.server
-        except AttributeError:
-            server = api.env.host
+        server = urlparse.urlsplit(api.env.jsonrpc_uri).hostname
         ldap = ipaldap.IPAdmin(server)
 
         tmpdir = tempfile.mkdtemp(prefix="tmp-")
-- 
1.9.3

>From e93e4ecb8e64780675f375e33896b8e9de2db82d Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Wed, 17 Sep 2014 15:04:11 +0200
Subject: [PATCH 08/13] Remove ipa-ca.crt from systemwide CA store on client
 uninstall and cert update

The file has been obsoleted by ipa.p11-kit, leaving it in place may cause
failures in ipa-server-install.
---
 ipaplatform/base/paths.py   |  1 +
 ipaplatform/fedora/tasks.py | 38 ++++++++++++++++++++++++++++----------
 2 files changed, 29 insertions(+), 10 deletions(-)

diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py
index 5f73d85..baaa109 100644
--- a/ipaplatform/base/paths.py
+++ b/ipaplatform/base/paths.py
@@ -80,6 +80,7 @@ class BasePathNamespace(object):
     PAM_LDAP_CONF = "/etc/pam_ldap.conf"
     PASSWD = "/etc/passwd"
     ETC_PKI_CA_DIR = "/etc/pki-ca"
+    SYSTEMWIDE_CA_STORE = "/etc/pki/ca-trust/source/anchors/"
     IPA_P11_KIT = "/etc/pki/ca-trust/source/ipa.p11-kit"
     NSS_DB_DIR = "/etc/pki/nssdb"
     NSSDB_CERT8_DB = "/etc/pki/nssdb/cert8.db"
diff --git a/ipaplatform/fedora/tasks.py b/ipaplatform/fedora/tasks.py
index 926c0ea..9de6360 100644
--- a/ipaplatform/fedora/tasks.py
+++ b/ipaplatform/fedora/tasks.py
@@ -155,6 +155,16 @@ class FedoraTaskNamespace(BaseTaskNamespace):
         auth_config.execute()
 
     def insert_ca_certs_into_systemwide_ca_store(self, ca_certs):
+        new_cacert_path = os.path.join(paths.SYSTEMWIDE_CA_STORE, 'ipa-ca.crt')
+
+        if os.path.exists(new_cacert_path):
+            try:
+                os.remove(new_cacert_path)
+            except OSError, e:
+                root_logger.error(
+                    "Could not remove %s: %s", new_cacert_path, e)
+                return False
+
         new_cacert_path = paths.IPA_P11_KIT
 
         try:
@@ -247,25 +257,33 @@ class FedoraTaskNamespace(BaseTaskNamespace):
         return False
 
     def remove_ca_certs_from_systemwide_ca_store(self):
-        new_cacert_path = paths.IPA_P11_KIT
+        ipa_ca_crt = os.path.join(paths.SYSTEMWIDE_CA_STORE, 'ipa-ca.crt')
+        update = False
 
         # Remove CA cert from systemwide store
-        if os.path.exists(new_cacert_path):
+        for new_cacert_path in (paths.IPA_P11_KIT, ipa_ca_crt):
+            if not os.path.exists(new_cacert_path):
+                continue
             try:
                 os.remove(new_cacert_path)
-                ipautil.run([paths.UPDATE_CA_TRUST])
             except OSError, e:
-                root_logger.error('Could not remove: %s, %s'
-                                   % (new_cacert_path, str(e)))
-                return False
+                root_logger.error(
+                    "Could not remove %s: %s", new_cacert_path, e)
+            else:
+                update = True
+
+        if update:
+            try:
+                ipautil.run([paths.UPDATE_CA_TRUST])
             except CalledProcessError, e:
-                root_logger.error('Could not update systemwide CA trust '
-                                  'database: %s' % str(e))
+                root_logger.error(
+                    "Could not update systemwide CA trust database: %s", e)
                 return False
             else:
-                root_logger.info('Systemwide CA database updated.')
+                root_logger.info("Systemwide CA database updated.")
+                return True
 
-        return True
+        return False
 
     def backup_and_replace_hostname(self, fstore, statestore, hostname):
         old_hostname = socket.gethostname()
-- 
1.9.3

>From 4a5ebdbfdc2729da5e3c21b15617ec69955f6492 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Tue, 16 Sep 2014 10:34:57 +0200
Subject: [PATCH 09/13] Fix certmonger.wait_for_request

---
 ipapython/certmonger.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/ipapython/certmonger.py b/ipapython/certmonger.py
index f58b6da..6d2b31f 100644
--- a/ipapython/certmonger.py
+++ b/ipapython/certmonger.py
@@ -468,7 +468,8 @@ def check_state(dirs):
 
 def wait_for_request(request_id, timeout=120):
     for i in range(0, timeout, 5):
-        state = get_request_value(request_id, 'state').strip()
+        request = _get_request({'nickname': request_id})
+        state, blocked = request.obj_if.get_status()
         root_logger.debug("certmonger request is in state %r", state)
         if state in ('CA_REJECTED', 'CA_UNREACHABLE', 'CA_UNCONFIGURED',
                      'NEED_GUIDANCE', 'NEED_CA', 'MONITORING'):
-- 
1.9.3

>From 63abb6479c1f89b5996f19f69b3ef6cb1d8aba59 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Tue, 16 Sep 2014 10:38:31 +0200
Subject: [PATCH 10/13] Refactor dogtag-ipa-ca-renew-agent

---
 .../certmonger/dogtag-ipa-ca-renew-agent-submit    | 454 +++++++++++++--------
 1 file changed, 287 insertions(+), 167 deletions(-)

diff --git a/install/certmonger/dogtag-ipa-ca-renew-agent-submit b/install/certmonger/dogtag-ipa-ca-renew-agent-submit
index 4f0b78a..81aaf0a 100755
--- a/install/certmonger/dogtag-ipa-ca-renew-agent-submit
+++ b/install/certmonger/dogtag-ipa-ca-renew-agent-submit
@@ -31,6 +31,7 @@ import tempfile
 import shutil
 import base64
 import contextlib
+import json
 
 from ipapython import ipautil
 from ipapython.dn import DN
@@ -53,6 +54,119 @@ UNCONFIGURED = 4
 WAIT_WITH_DELAY = 5
 OPERATION_NOT_SUPPORTED_BY_HELPER = 6
 
+
+class RequestException(Exception):
+    code = None
+    body = ''
+
+
+class RequestGenericException(RequestException):
+    def __init__(self, code, body):
+        super(RequestGenericException, self).__init__(
+            "Unknown error {0}".format(code))
+        self.code = code
+        self.body = body
+
+
+class RequestWaitException(RequestException):
+    def __init__(self, cookie, message):
+        super(RequestWaitException, self).__init__(message)
+        self.cookie = cookie
+
+    @property
+    def body(self):
+        return '{0}\n'.format(self.cookie)
+
+
+class Error(RequestException):
+    @property
+    def body(self):
+        return '{0}\n'.format(self)
+
+
+class Wait(RequestWaitException):
+    def __init__(self, cookie=None):
+        super(Wait, self).__init__(
+            cookie, "Waiting for the request to complete")
+
+    code = WAIT
+
+
+class Rejected(Error):
+    code = REJECTED
+
+
+class Unreachable(Error):
+    code = UNREACHABLE
+
+
+class Unconfigured(Error):
+    code = UNCONFIGURED
+
+
+class WaitWithDelay(RequestWaitException):
+    code = WAIT_WITH_DELAY
+
+    def __init__(self, delay, cookie=None):
+        super(WaitWithDelay, self).__init__(
+            cookie, "Waiting {0}s for the request to complete".format(delay))
+        self.delay = delay
+
+    @property
+    def body(self):
+        return '{0}\n{1}'.format(
+            self.delay, super(WaitWithDelay, self).body)
+
+
+class OperationNotSupported(Error):
+    code = OPERATION_NOT_SUPPORTED_BY_HELPER
+
+
+def request_handler(fn):
+    def decorated(operation, csr, cookie=None, profile=None, cert=None):
+        if operation == 'POLL':
+            if not cookie:
+                raise Unconfigured("Cookie not provided")
+
+            try:
+                context = json.loads(cookie)
+                if not isinstance(context, dict):
+                    raise TypeError
+            except (TypeError, ValueError):
+                raise Unconfigured("Invalid cookie: {0!r}".format(cookie))
+
+            try:
+                child_cookie = context.get('cookie')
+                if isinstance(child_cookie, unicode):
+                    child_cookie = child_cookie.encode('raw_unicode_escape')
+                elif child_cookie is not None:
+                    raise TypeError
+            except (TypeError, UnicodeEncodeError):
+                raise Unconfigured(
+                    "Invalid child cookie in cookie: {0!r}".format(
+                        child_cookie))
+        else:
+            context = {}
+            child_cookie = None
+
+        if profile is not None:
+            context['profile'] = profile.decode('raw_unicode_escape')
+        else:
+            context['profile'] = None
+
+        try:
+            return fn(context, operation, csr, child_cookie, profile, cert)
+        except RequestWaitException, e:
+            if e.cookie is not None:
+                context['cookie'] = e.cookie.decode('raw_unicode_escape')
+            else:
+                context.pop('cookie', None)
+            e.cookie = json.dumps(context)
+            raise e
+
+    return decorated
+
+
 @contextlib.contextmanager
 def ldap_connect():
     conn = None
@@ -64,10 +178,28 @@ def ldap_connect():
         if conn is not None and conn.isconnected():
             conn.disconnect()
 
-def request_cert():
+
+@request_handler
+def request_cert(context, operation, csr, cookie=None, profile=None,
+                 cert=None):
     """
     Request certificate from IPA CA.
     """
+    os.environ['CERTMONGER_OPERATION'] = operation
+    os.environ['CERTMONGER_CSR'] = csr
+    if cookie is not None:
+        os.environ['CERTMONGER_CA_COOKIE'] = cookie
+    else:
+        os.environ.pop('CERTMONGER_CA_COOKIE', None)
+    if profile is not None:
+        os.environ['CERTMONGER_CA_PROFILE'] = profile
+    else:
+        os.environ.pop('CERTMONGER_CA_PROFILE', None)
+    if cert is not None:
+        os.environ['CERTMONGER_CERTIFICATE'] = cert
+    else:
+        os.environ.pop('CERTMONGER_CERTIFICATE', None)
+
     syslog.syslog(syslog.LOG_NOTICE,
                   "Forwarding request to dogtag-ipa-renew-agent")
 
@@ -79,45 +211,51 @@ def request_cert():
 
     syslog.syslog(syslog.LOG_NOTICE, "dogtag-ipa-renew-agent returned %d" % rc)
 
-    if stdout.endswith('\n'):
-        stdout = stdout[:-1]
-
-    if rc == WAIT_WITH_DELAY:
-        delay, sep, cookie = stdout.partition('\n')
-        return (rc, delay, cookie)
+    body = stdout
+    if body.endswith('\n'):
+        body = body[:-1]
+
+    if rc == ISSUED:
+        return body
+    elif rc == WAIT:
+        raise Wait(body)
+    elif rc == REJECTED:
+        raise Rejected(body)
+    elif rc == UNREACHABLE:
+        raise Unreachable(body)
+    elif rc == UNCONFIGURED:
+        raise Unconfigured(body)
+    elif rc == WAIT_WITH_DELAY:
+        delay, cookie = body.split('\n', 1)
+        raise WaitWithDelay(delay, cookie)
+    elif rc == OPERATION_NOT_SUPPORTED_BY_HELPER:
+        raise OperationNotSupported()
     else:
-        return (rc, stdout)
+        raise RequestGenericException(rc, stdout)
+
 
-def store_cert():
+@request_handler
+def store_cert(context, operation, csr, cookie=None, profile=None, cert=None):
     """
     Store certificate in LDAP.
     """
-    operation = os.environ.get('CERTMONGER_OPERATION')
     if operation == 'SUBMIT':
-        attempts = 0
+        context['counter'] = 0
     elif operation == 'POLL':
-        cookie = os.environ.get('CERTMONGER_CA_COOKIE')
-        if not cookie:
-            return (UNCONFIGURED, "Cookie not provided")
-
-        try:
-            attempts = int(cookie)
-        except ValueError:
-            return (UNCONFIGURED, "Invalid cookie: %r" % cookie)
+        if 'counter' not in context:
+            raise Unconfigured("No counter in cookie")
+        if not isinstance(context['counter'], (int, long)):
+            raise Unconfigured(
+                "Invalid counter in cookie: {0!r}".format(context['counter']))
     else:
-        return (OPERATION_NOT_SUPPORTED_BY_HELPER,)
-
-    csr = os.environ.get('CERTMONGER_CSR')
-    if not csr:
-        return (UNCONFIGURED, "Certificate request not provided")
+        raise OperationNotSupported()
 
     nickname = pkcs10.get_friendlyname(csr)
     if not nickname:
-        return (REJECTED, "No friendly name in the certificate request")
+        raise Rejected("No friendly name in the certificate request")
 
-    cert = os.environ.get('CERTMONGER_CERTIFICATE')
     if not cert:
-        return (REJECTED, "New certificate requests not supported")
+        raise Rejected("New certificate requests not supported")
 
     dercert = x509.normalize_certificate(cert)
 
@@ -139,104 +277,88 @@ def store_cert():
             except errors.EmptyModlist:
                 pass
     except Exception, e:
-        attempts += 1
-        if attempts < 10:
+        context['counter'] += 1
+        if context['counter'] < 10:
             syslog.syslog(
                 syslog.LOG_ERR,
                 "Updating renewal certificate failed: %s. Sleeping 30s" % e)
-            return (WAIT_WITH_DELAY, 30, attempts)
+            raise WaitWithDelay(30)
         else:
             syslog.syslog(
                 syslog.LOG_ERR,
                 "Giving up. To retry storing the certificate, resubmit the "
                 "request with profile \"ipaStorage\"")
 
-    return (ISSUED, cert)
+    return cert
+
 
-def request_and_store_cert():
+@request_handler
+def request_and_store_cert(context, operation, csr, cookie=None, profile=None,
+                           cert=None):
     """
     Request certificate from IPA CA and store it in LDAP.
     """
-    operation = os.environ.get('CERTMONGER_OPERATION')
     if operation == 'SUBMIT':
-        state = 'request'
-        cookie = None
+        context['state'] = u'request'
     elif operation == 'POLL':
-        cookie = os.environ.get('CERTMONGER_CA_COOKIE')
-        if not cookie:
-            return (UNCONFIGURED, "Cookie not provided")
-
-        state, sep, cookie = cookie.partition(':')
-        if state not in ('request', 'store'):
-            return (UNCONFIGURED,
-                    "Invalid cookie: %r" % os.environ['CERTMONGER_CA_COOKIE'])
-    else:
-        return (OPERATION_NOT_SUPPORTED_BY_HELPER,)
+        if 'state' not in context:
+            raise Unconfigured("No state name in cookie")
+        if context['state'] not in (u'request', u'store'):
+            raise Unconfigured(
+                "Invalid state name in cookie: {0!r}".format(context['state']))
 
-    if state == 'request':
-        if cookie is None:
-            os.environ['CERTMONGER_OPERATION'] = 'SUBMIT'
-        else:
-            os.environ['CERTMONGER_CA_COOKIE'] = cookie
-
-        result = request_cert()
-        if result[0] == WAIT:
-            return (result[0], 'request:%s' % result[1])
-        elif result[0] == WAIT_WITH_DELAY:
-            return (result[0], result[1], 'request:%s' % result[2])
-        elif result[0] != ISSUED:
-            return result
-        else:
-            cert = result[1]
-            cookie = None
+        if context['state'] == u'store':
+            try:
+                cert = context['certificate']
+                if isinstance(cert, unicode):
+                    cert = cert.encode('raw_unicode_escape')
+                else:
+                    raise TypeError
+            except KeyError:
+                raise Unconfigured("No certificate in cookie")
+            except (TypeError, UnicodeEncodeError):
+                raise Unconfigured(
+                    "Invalid certificate in cookie: {0!r}".format(cert))
     else:
-        cert, sep, cookie = cookie.partition(':')
+        raise OperationNotSupported()
 
-    if cookie is None:
-        os.environ['CERTMONGER_OPERATION'] = 'SUBMIT'
-    else:
-        os.environ['CERTMONGER_CA_COOKIE'] = cookie
-    os.environ['CERTMONGER_CERTIFICATE'] = cert
+    if context['state'] == u'request':
+        cert = request_cert(operation, csr, cookie, profile, cert)
+        context['state'] = u'store'
+        context['certificate'] = cert.decode('raw_unicode_escape')
+        operation = 'SUBMIT'
+        cookie = None
 
-    result = store_cert()
-    if result[0] == WAIT:
-        return (result[0], 'store:%s:%s' % (cert, result[1]))
-    elif result[0] == WAIT_WITH_DELAY:
-        return (result[0], result[1], 'store:%s:%s' % (cert, result[2]))
-    else:
-        return result
+    if context['state'] == u'store':
+        cert = store_cert(operation, csr, cookie, profile, cert)
 
-def retrieve_cert():
+    return cert
+
+
+@request_handler
+def retrieve_cert(context, operation, csr, cookie=None, profile=None,
+                  cert=None):
     """
     Retrieve new certificate from LDAP.
     """
-    operation = os.environ.get('CERTMONGER_OPERATION')
     if operation == 'SUBMIT':
-        attempts = 0
+        context['counter'] = 0
     elif operation == 'POLL':
-        cookie = os.environ.get('CERTMONGER_CA_COOKIE')
-        if not cookie:
-            return (UNCONFIGURED, "Cookie not provided")
-
-        try:
-            attempts = int(cookie)
-        except ValueError:
-            return (UNCONFIGURED, "Invalid cookie: %r" % cookie)
+        if 'counter' not in context:
+            raise Unconfigured("No counter in cookie")
+        if not isinstance(context['counter'], (int, long)):
+            raise Unconfigured(
+                "Invalid counter in cookie: {0!r}".format(context['counter']))
     else:
-        return (OPERATION_NOT_SUPPORTED_BY_HELPER,)
-
-    csr = os.environ.get('CERTMONGER_CSR')
-    if not csr:
-        return (UNCONFIGURED, "Certificate request not provided")
+        raise OperationNotSupported()
 
     nickname = pkcs10.get_friendlyname(csr)
     if not nickname:
-        return (REJECTED, "No friendly name in the certificate request")
+        raise Rejected("No friendly name in the certificate request")
 
-    old_cert = os.environ.get('CERTMONGER_CERTIFICATE')
-    if not old_cert:
-        return (REJECTED, "New certificate requests not supported")
-    old_cert = x509.normalize_certificate(old_cert)
+    if not cert:
+        raise Rejected("New certificate requests not supported")
+    old_cert = x509.normalize_certificate(cert)
 
     syslog.syslog(syslog.LOG_NOTICE, "Updating certificate for %s" % nickname)
 
@@ -252,132 +374,130 @@ def retrieve_cert():
             cert = entry.single_value['usercertificate']
 
         if cert == old_cert:
-            attempts += 1
-            if attempts < 4:
+            context['counter'] += 1
+            if context['counter'] < 4:
                 syslog.syslog(
                     syslog.LOG_INFO,
                     "Updated certificate for %s not available" % nickname)
-                # No cert available yet, tell certmonger to wait another 8 hours
-                return (WAIT_WITH_DELAY, 8 * 60 * 60, attempts)
+                # No cert available yet, tell certmonger to wait another 8 hrs
+                raise WaitWithDelay(8 * 60 * 60)
 
         cert = base64.b64encode(cert)
         cert = x509.make_pem(cert)
 
-    return (ISSUED, cert)
+    return cert
+
 
-def export_csr():
+@request_handler
+def export_csr(context, operation, csr, cookie=None, profile=None, cert=None):
     """
     This does not actually renew the cert, it just writes the CSR provided
     by certmonger to /var/lib/ipa/ca.csr and returns the existing cert.
     """
-    operation = os.environ.get('CERTMONGER_OPERATION')
     if operation != 'SUBMIT':
-        return (OPERATION_NOT_SUPPORTED_BY_HELPER,)
+        raise OperationNotSupported()
 
-    csr = os.environ.get('CERTMONGER_CSR')
-    if not csr:
-        return (UNCONFIGURED, "Certificate request not provided")
-
-    cert = os.environ.get('CERTMONGER_CERTIFICATE')
     if not cert:
-        return (REJECTED, "New certificate requests not supported")
+        raise Rejected("New certificate requests not supported")
 
     csr_file = paths.IPA_CA_CSR
     try:
         with open(csr_file, 'wb') as f:
             f.write(csr)
     except Exception, e:
-        return (UNREACHABLE, "Failed to write %s: %s" % (csr_file, e))
+        raise Unreachable("Failed to write {0}: {1}".format(csr_file, e))
 
-    return (ISSUED, cert)
+    return cert
 
-def renew_ca_cert():
+
+@request_handler
+def renew_ca_cert(context, operation, csr, cookie=None, profile=None,
+                  cert=None):
     """
     This is used for automatic CA certificate renewal.
     """
     cert = os.environ.get('CERTMONGER_CERTIFICATE')
     if not cert:
-        return (REJECTED, "New certificate requests not supported")
+        raise Rejected("New certificate requests not supported")
     is_self_signed = x509.is_self_signed(cert)
 
-    operation = os.environ.get('CERTMONGER_OPERATION')
     if operation == 'SUBMIT':
-        state = 'retrieve'
-
+        context['state'] = u'retrieve'
         if is_self_signed:
             ca = cainstance.CAInstance(host_name=api.env.host, ldapi=False)
             if ca.is_renewal_master():
-                state = 'request'
+                context['state'] = u'request'
     elif operation == 'POLL':
-        cookie = os.environ.get('CERTMONGER_CA_COOKIE')
-        if not cookie:
-            return (UNCONFIGURED, "Cookie not provided")
+        if 'state' not in context:
+            raise Unconfigured("No state name in cookie")
+        if context['state'] not in (u'retrieve', u'request'):
+            raise Unconfigured(
+                "Invalid state name in cookie: {0!r}".format(context['state']))
+    else:
+        raise OperationNotSupported()
 
-        state, sep, cookie = cookie.partition(':')
-        if state not in ('retrieve', 'request'):
-            return (UNCONFIGURED,
-                    "Invalid cookie: %r" % os.environ['CERTMONGER_CA_COOKIE'])
+    if context['state'] == u'retrieve':
+        try:
+            cert = retrieve_cert(operation, csr, cookie, profile, cert)
+        except WaitWithDelay:
+            if not is_self_signed:
+                syslog.syslog(syslog.LOG_ALERT,
+                              "IPA CA certificate is about to expire, "
+                              "use ipa-cacert-manage to renew it")
+            raise
+    elif context['state'] == u'request':
+        profile = 'caCACert'
+        cert = request_and_store_cert(operation, csr, cookie, profile, cert)
+
+    return cert
 
-        os.environ['CERTMONGER_CA_COOKIE'] = cookie
-    else:
-        return (OPERATION_NOT_SUPPORTED_BY_HELPER,)
-
-    if state == 'retrieve':
-        result = retrieve_cert()
-        if result[0] == WAIT_WITH_DELAY and not is_self_signed:
-            syslog.syslog(syslog.LOG_ALERT,
-                          "IPA CA certificate is about to expire, "
-                          "use ipa-cacert-manage to renew it")
-    elif state == 'request':
-        os.environ['CERTMONGER_CA_PROFILE'] = 'caCACert'
-        result = request_and_store_cert()
-
-    if result[0] == WAIT:
-        return (result[0], '%s:%s' % (state, result[1]))
-    elif result[0] == WAIT_WITH_DELAY:
-        return (result[0], result[1], '%s:%s' % (state, result[2]))
-    else:
-        return result
 
 def main():
-    handlers = {
-        'ipaStorage':       store_cert,
-        'ipaRetrieval':     retrieve_cert,
-        'ipaCSRExport':     export_csr,
-        'ipaCACertRenewal': renew_ca_cert,
-    }
+    operation = os.environ.get('CERTMONGER_OPERATION') or None
+    if operation not in ('SUBMIT', 'POLL'):
+        raise OperationNotSupported()
+
+    csr = os.environ.get('CERTMONGER_CSR') or None
+    if not csr:
+        raise Unconfigured("Certificate request not provided")
+
+    cookie = os.environ.get('CERTMONGER_CA_COOKIE') or None
+    profile = os.environ.get('CERTMONGER_CA_PROFILE') or None
+    cert = os.environ.get('CERTMONGER_CERTIFICATE') or None
 
     api.bootstrap(context='renew')
     api.finalize()
 
-    operation = os.environ.get('CERTMONGER_OPERATION')
-    if operation not in ('SUBMIT', 'POLL'):
-        return OPERATION_NOT_SUPPORTED_BY_HELPER
-
     tmpdir = tempfile.mkdtemp(prefix="tmp-")
     try:
         principal = str('host/%s@%s' % (api.env.host, api.env.realm))
         ipautil.kinit_hostprincipal(paths.KRB5_KEYTAB, tmpdir, principal)
 
-        profile = os.environ.get('CERTMONGER_CA_PROFILE')
-        if profile:
-            handler = handlers.get(profile, request_and_store_cert)
-        else:
+        if not profile:
             ca = cainstance.CAInstance(host_name=api.env.host, ldapi=False)
-            if ca.is_renewal_master():
-                handler = request_and_store_cert
-            else:
-                handler = retrieve_cert
+            if not ca.is_renewal_master():
+                profile = 'ipaRetrieval'
 
-        res = handler()
-        for item in res[1:]:
-            print item
-        return res[0]
+        handler = {
+            'ipaStorage':       store_cert,
+            'ipaRetrieval':     retrieve_cert,
+            'ipaCSRExport':     export_csr,
+            'ipaCACertRenewal': renew_ca_cert,
+        }.get(profile, request_and_store_cert)
+
+        cert = handler(operation, csr, cookie, profile, cert)
     finally:
         shutil.rmtree(tmpdir)
 
+    print cert
+
 try:
-    sys.exit(main())
+    main()
+    sys.exit(ISSUED)
+except RequestException, e:
+    syslog.syslog(syslog.LOG_INFO, str(e))
+    sys.stdout.write(e.body)
+    sys.exit(e.code)
 except Exception, e:
     syslog.syslog(syslog.LOG_ERR, traceback.format_exc())
     print "Internal error"
-- 
1.9.3

>From b4c64fd7f8aa36d04dc2d8a78da54807a4a8fd8e Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Tue, 16 Sep 2014 10:38:48 +0200
Subject: [PATCH 11/13] Restart request in dogtag-ipa-ca-renew-agent if profile
 changes

---
 install/certmonger/dogtag-ipa-ca-renew-agent-submit | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/install/certmonger/dogtag-ipa-ca-renew-agent-submit b/install/certmonger/dogtag-ipa-ca-renew-agent-submit
index 81aaf0a..8690fb8 100755
--- a/install/certmonger/dogtag-ipa-ca-renew-agent-submit
+++ b/install/certmonger/dogtag-ipa-ca-renew-agent-submit
@@ -136,6 +136,20 @@ def request_handler(fn):
                 raise Unconfigured("Invalid cookie: {0!r}".format(cookie))
 
             try:
+                context_profile = context['profile']
+                if isinstance(context_profile, unicode):
+                    context_profile = context_profile.encode(
+                        'raw_unicode_escape')
+                elif context_profile is not None:
+                    raise TypeError
+            except KeyError:
+                raise Unconfigured("No profile name in cookie")
+            except (TypeError, UnicodeEncodeError):
+                raise Unconfigured(
+                    "Invalid profile name {0!r} in cookie".format(
+                        context_profile))
+
+            try:
                 child_cookie = context.get('cookie')
                 if isinstance(child_cookie, unicode):
                     child_cookie = child_cookie.encode('raw_unicode_escape')
@@ -145,6 +159,12 @@ def request_handler(fn):
                 raise Unconfigured(
                     "Invalid child cookie in cookie: {0!r}".format(
                         child_cookie))
+
+            # If profile has changed between SUBMIT and POLL, restart request
+            if context_profile != profile:
+                operation = 'SUBMIT'
+                context = {}
+                child_cookie = None
         else:
             context = {}
             child_cookie = None
-- 
1.9.3

>From c2f5b6194d8708a16bb7de5cbaf45bef35a345b3 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Thu, 18 Sep 2014 10:28:03 +0200
Subject: [PATCH 12/13] Do not wait for new CA certificate to appear in LDAP in
 ipa-certupdate

---
 .../certmonger/dogtag-ipa-ca-renew-agent-submit    | 66 ++++++++++++++--------
 ipa-client/ipaclient/ipa_certupdate.py             |  2 +-
 2 files changed, 43 insertions(+), 25 deletions(-)

diff --git a/install/certmonger/dogtag-ipa-ca-renew-agent-submit b/install/certmonger/dogtag-ipa-ca-renew-agent-submit
index 8690fb8..f6a695b 100755
--- a/install/certmonger/dogtag-ipa-ca-renew-agent-submit
+++ b/install/certmonger/dogtag-ipa-ca-renew-agent-submit
@@ -356,20 +356,13 @@ def request_and_store_cert(context, operation, csr, cookie=None, profile=None,
 
 
 @request_handler
-def retrieve_cert(context, operation, csr, cookie=None, profile=None,
-                  cert=None):
+def try_retrieve_cert(context, operation, csr, cookie=None, profile=None,
+                      cert=None):
     """
-    Retrieve new certificate from LDAP.
+    Retrieve new certificate from LDAP. If new certificate is not available,
+    use the old one.
     """
-    if operation == 'SUBMIT':
-        context['counter'] = 0
-    elif operation == 'POLL':
-        if 'counter' not in context:
-            raise Unconfigured("No counter in cookie")
-        if not isinstance(context['counter'], (int, long)):
-            raise Unconfigured(
-                "Invalid counter in cookie: {0!r}".format(context['counter']))
-    else:
+    if operation != 'SUBMIT':
         raise OperationNotSupported()
 
     nickname = pkcs10.get_friendlyname(csr)
@@ -378,7 +371,6 @@ def retrieve_cert(context, operation, csr, cookie=None, profile=None,
 
     if not cert:
         raise Rejected("New certificate requests not supported")
-    old_cert = x509.normalize_certificate(cert)
 
     syslog.syslog(syslog.LOG_NOTICE, "Updating certificate for %s" % nickname)
 
@@ -389,21 +381,46 @@ def retrieve_cert(context, operation, csr, cookie=None, profile=None,
                    ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn),
                 ['usercertificate'])
         except errors.NotFound:
-            cert = old_cert
+            pass
         else:
             cert = entry.single_value['usercertificate']
+            cert = base64.b64encode(cert)
+            cert = x509.make_pem(cert)
+
+    return cert
+
+
+@request_handler
+def retrieve_cert(context, operation, csr, cookie=None, profile=None,
+                  cert=None):
+    """
+    Retrieve new certificate from LDAP.
+    """
+    if operation == 'SUBMIT':
+        context['counter'] = 0
+    elif operation == 'POLL':
+        if 'counter' not in context:
+            raise Unconfigured("No counter in cookie")
+        if not isinstance(context['counter'], (int, long)):
+            raise Unconfigured(
+                "Invalid counter in cookie: {0!r}".format(context['counter']))
+    else:
+        raise OperationNotSupported()
+
+    if cert:
+        old_cert = x509.normalize_certificate(cert)
+    else:
+        old_cert = None
 
-        if cert == old_cert:
-            context['counter'] += 1
-            if context['counter'] < 4:
-                syslog.syslog(
-                    syslog.LOG_INFO,
-                    "Updated certificate for %s not available" % nickname)
-                # No cert available yet, tell certmonger to wait another 8 hrs
-                raise WaitWithDelay(8 * 60 * 60)
+    cert = try_retrieve_cert('SUBMIT', csr, cookie, profile, cert)
+    new_cert = x509.normalize_certificate(cert)
 
-        cert = base64.b64encode(cert)
-        cert = x509.make_pem(cert)
+    if new_cert == old_cert:
+        context['counter'] += 1
+        if context['counter'] < 4:
+            syslog.syslog(syslog.LOG_INFO, "Updated certificate not available")
+            # No cert available yet, tell certmonger to wait another 8 hours
+            raise WaitWithDelay(8 * 60 * 60)
 
     return cert
 
@@ -500,6 +517,7 @@ def main():
 
         handler = {
             'ipaStorage':       store_cert,
+            'ipaTryRetrieval':  try_retrieve_cert,
             'ipaRetrieval':     retrieve_cert,
             'ipaCSRExport':     export_csr,
             'ipaCACertRenewal': renew_ca_cert,
diff --git a/ipa-client/ipaclient/ipa_certupdate.py b/ipa-client/ipaclient/ipa_certupdate.py
index ff16b9b..cdf9d48 100644
--- a/ipa-client/ipaclient/ipa_certupdate.py
+++ b/ipa-client/ipaclient/ipa_certupdate.py
@@ -127,7 +127,7 @@ class CertUpdate(admintool.AdminTool):
             timeout = api.env.startup_timeout + 60
 
             self.log.debug("resubmitting certmonger request '%s'", request_id)
-            certmonger.resubmit_request(request_id, profile='ipaRetrieval')
+            certmonger.resubmit_request(request_id, profile='ipaTryRetrieval')
             try:
                 state = certmonger.wait_for_request(request_id, timeout)
             except RuntimeError:
-- 
1.9.3

>From e2a83247b4bc5fe6cec37b1f33d764a6641c2461 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Thu, 18 Sep 2014 10:29:26 +0200
Subject: [PATCH 13/13] Fail if certmonger can't see new CA certificate in LDAP
 in ipa-cacert-manage

---
 .../certmonger/dogtag-ipa-ca-renew-agent-submit    | 25 ++++++++++++++++++++++
 ipaserver/install/ipa_cacert_manage.py             |  9 +++++---
 2 files changed, 31 insertions(+), 3 deletions(-)

diff --git a/install/certmonger/dogtag-ipa-ca-renew-agent-submit b/install/certmonger/dogtag-ipa-ca-renew-agent-submit
index f6a695b..32c5cab 100755
--- a/install/certmonger/dogtag-ipa-ca-renew-agent-submit
+++ b/install/certmonger/dogtag-ipa-ca-renew-agent-submit
@@ -391,6 +391,30 @@ def try_retrieve_cert(context, operation, csr, cookie=None, profile=None,
 
 
 @request_handler
+def do_retrieve_cert(context, operation, csr, cookie=None, profile=None,
+                     cert=None):
+    """
+    Retrieve new certificate from LDAP. If new certificate is not available,
+    fail.
+    """
+    if operation != 'SUBMIT':
+        raise OperationNotSupported()
+
+    if cert:
+        old_cert = x509.normalize_certificate(cert)
+    else:
+        old_cert = None
+
+    cert = try_retrieve_cert('SUBMIT', csr, cookie, profile, cert)
+    new_cert = x509.normalize_certificate(cert)
+
+    if new_cert == old_cert:
+        raise Rejected("Updated certificate not available")
+
+    return cert
+
+
+@request_handler
 def retrieve_cert(context, operation, csr, cookie=None, profile=None,
                   cert=None):
     """
@@ -518,6 +542,7 @@ def main():
         handler = {
             'ipaStorage':       store_cert,
             'ipaTryRetrieval':  try_retrieve_cert,
+            'ipaDoRetrieval':   do_retrieve_cert,
             'ipaRetrieval':     retrieve_cert,
             'ipaCSRExport':     export_csr,
             'ipaCACertRenewal': renew_ca_cert,
diff --git a/ipaserver/install/ipa_cacert_manage.py b/ipaserver/install/ipa_cacert_manage.py
index c681261..67103a6e 100644
--- a/ipaserver/install/ipa_cacert_manage.py
+++ b/ipaserver/install/ipa_cacert_manage.py
@@ -153,8 +153,11 @@ class CACertManage(admintool.AdminTool):
             raise admintool.ScriptError("CA is not configured on this system")
 
         nss_dir = ca.dogtag_constants.ALIAS_DIR
-        criteria = {'cert-database': nss_dir,
-                    'cert-nickname': self.cert_nickname}
+        criteria = {
+            'cert-database': nss_dir,
+            'cert-nickname': self.cert_nickname,
+            'ca-name': 'dogtag-ipa-ca-renew-agent',
+        }
         self.request_id = certmonger.get_request_id(criteria)
         if self.request_id is None:
             raise admintool.ScriptError(
@@ -291,7 +294,7 @@ class CACertManage(admintool.AdminTool):
         except errors.NotFound:
             raise admintool.ScriptError("CA renewal master not found")
 
-        self.resubmit_request(ca, 'ipaRetrieval')
+        self.resubmit_request(ca, 'ipaDoRetrieval')
 
         print "CA certificate successfully renewed"
 
-- 
1.9.3

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to