Hi,

the attached patch fixes <https://fedorahosted.org/freeipa/ticket/4628>. It depends on my patch 333, which is also attached.


(The original patch was posted at <http://www.redhat.com/archives/freeipa-devel/2014-September/msg00454.html>.)

How to test:

  1. install server

  2. run "ipa-certupdate"

3. run "getcert list -d /etc/pki/pki-tomcat/alias -n 'caSigningCert cert-pki-ca'", the request should be in MONITORING state, there should be no ca-error

Honza

--
Jan Cholasta
>From 6ac462b18d0a31d8eb697e949bc909327f41d618 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Tue, 14 Oct 2014 10:30:07 +0200
Subject: [PATCH] Handle profile changes in dogtag-ipa-ca-renew-agent

To update the CA certificate in the Dogtag NSS database, the
"ipa-cacert-manage renew" and "ipa-certupdate" commands temporarily change
the profile of the CA certificate certmonger request, resubmit it and
change the profile back to the original one.

When something goes wrong while resubmitting the request, it needs to be
modified and resubmitted again manually. This might fail with invalid
cookie error, because changing the profile does not change the internal
state of the request.

Detect this in dogtag-ipa-ca-renew-agent and reset the internal state when
profile is changed.

https://fedorahosted.org/freeipa/ticket/4627
---
 .../certmonger/dogtag-ipa-ca-renew-agent-submit    | 87 ++++++++++++++++++++--
 1 file changed, 80 insertions(+), 7 deletions(-)

diff --git a/install/certmonger/dogtag-ipa-ca-renew-agent-submit b/install/certmonger/dogtag-ipa-ca-renew-agent-submit
index 4f0b78a..ca4380c 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
@@ -64,6 +65,78 @@ def ldap_connect():
         if conn is not None and conn.isconnected():
             conn.disconnect()
 
+def call_handler(_handler, *args, **kwargs):
+    """
+    Request handler call wrapper
+
+    Before calling the handler, get the original profile name and cookie from
+    the provided cookie, if there is one. If the profile name does not match
+    the requested profile name, drop the cookie and restart the request.
+
+    After calling the handler, put the requested profile name and cookie
+    returned by the handler in a new cookie and return it.
+    """
+    operation = os.environ['CERTMONGER_OPERATION']
+    if operation == 'POLL':
+        cookie = os.environ.pop('CERTMONGER_CA_COOKIE', None)
+        if cookie is not None:
+            try:
+                context = json.loads(cookie)
+                if not isinstance(context, dict):
+                    raise TypeError
+            except (TypeError, ValueError):
+                return (UNCONFIGURED, "Invalid cookie: %r" % cookie)
+        else:
+            return (UNCONFIGURED, "Cookie not provided")
+
+        if 'profile' in context:
+            profile = context.pop('profile')
+            try:
+                if profile is not None:
+                    if not isinstance(profile, unicode):
+                        raise TypeError
+                    profile = profile.encode('raw_unicode_escape')
+            except (TypeError, UnicodeEncodeError):
+                return (UNCONFIGURED,
+                        "Invalid 'profile' in cookie: %r" % profile)
+        else:
+            return (UNCONFIGURED, "No 'profile' in cookie")
+
+        # If profile has changed between SUBMIT and POLL, restart request
+        if os.environ.get('CERTMONGER_CA_PROFILE') != profile:
+            os.environ['CERTMONGER_OPERATION'] = 'SUBMIT'
+            context = {}
+
+        if 'cookie' in context:
+            cookie = context.pop('cookie')
+            try:
+                if not isinstance(cookie, unicode):
+                    raise TypeError
+                cookie = cookie.encode('raw_unicode_escape')
+            except (TypeError, UnicodeEncodeError):
+                return (UNCONFIGURED,
+                        "Invalid 'cookie' in cookie: %r" % cookie)
+            os.environ['CERTMONGER_CA_COOKIE'] = cookie
+    else:
+        context = {}
+
+    result = _handler(*args, **kwargs)
+
+    if result[0] in (WAIT, WAIT_WITH_DELAY):
+        context['cookie'] = result[-1].decode('raw_unicode_escape')
+
+    profile = os.environ.get('CERTMONGER_CA_PROFILE')
+    if profile is not None:
+        profile = profile.decode('raw_unicode_escape')
+    context['profile'] = profile
+
+    cookie = json.dumps(context)
+    os.environ['CERTMONGER_CA_COOKIE'] = cookie
+    if result[0] in (WAIT, WAIT_WITH_DELAY):
+        result = result[:-1] + (cookie,)
+
+    return result
+
 def request_cert():
     """
     Request certificate from IPA CA.
@@ -144,7 +217,7 @@ def store_cert():
             syslog.syslog(
                 syslog.LOG_ERR,
                 "Updating renewal certificate failed: %s. Sleeping 30s" % e)
-            return (WAIT_WITH_DELAY, 30, attempts)
+            return (WAIT_WITH_DELAY, 30, str(attempts))
         else:
             syslog.syslog(
                 syslog.LOG_ERR,
@@ -179,7 +252,7 @@ def request_and_store_cert():
         else:
             os.environ['CERTMONGER_CA_COOKIE'] = cookie
 
-        result = request_cert()
+        result = call_handler(request_cert)
         if result[0] == WAIT:
             return (result[0], 'request:%s' % result[1])
         elif result[0] == WAIT_WITH_DELAY:
@@ -198,7 +271,7 @@ def request_and_store_cert():
         os.environ['CERTMONGER_CA_COOKIE'] = cookie
     os.environ['CERTMONGER_CERTIFICATE'] = cert
 
-    result = store_cert()
+    result = call_handler(store_cert)
     if result[0] == WAIT:
         return (result[0], 'store:%s:%s' % (cert, result[1]))
     elif result[0] == WAIT_WITH_DELAY:
@@ -258,7 +331,7 @@ def retrieve_cert():
                     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)
+                return (WAIT_WITH_DELAY, 8 * 60 * 60, str(attempts))
 
         cert = base64.b64encode(cert)
         cert = x509.make_pem(cert)
@@ -323,14 +396,14 @@ def renew_ca_cert():
         return (OPERATION_NOT_SUPPORTED_BY_HELPER,)
 
     if state == 'retrieve':
-        result = retrieve_cert()
+        result = call_handler(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()
+        result = call_handler(request_and_store_cert)
 
     if result[0] == WAIT:
         return (result[0], '%s:%s' % (state, result[1]))
@@ -369,7 +442,7 @@ def main():
             else:
                 handler = retrieve_cert
 
-        res = handler()
+        res = call_handler(handler)
         for item in res[1:]:
             print item
         return res[0]
-- 
1.9.3

>From 7a451bd69ab59c566293be4bb5fbbe1095d54397 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Tue, 14 Oct 2014 11:12:55 +0200
Subject: [PATCH] Do not wait for new CA certificate to appear in LDAP in
 ipa-certupdate

If new certificate is not available, reuse the old one, instead of waiting
indefinitely for the new certificate to appear.

https://fedorahosted.org/freeipa/ticket/4628
---
 .../certmonger/dogtag-ipa-ca-renew-agent-submit    | 87 ++++++++++++----------
 ipa-client/ipaclient/ipa_certupdate.py             |  6 +-
 2 files changed, 53 insertions(+), 40 deletions(-)

diff --git a/install/certmonger/dogtag-ipa-ca-renew-agent-submit b/install/certmonger/dogtag-ipa-ca-renew-agent-submit
index ca4380c..9a01eb3 100755
--- a/install/certmonger/dogtag-ipa-ca-renew-agent-submit
+++ b/install/certmonger/dogtag-ipa-ca-renew-agent-submit
@@ -279,25 +279,11 @@ def request_and_store_cert():
     else:
         return result
 
-def retrieve_cert():
+def retrieve_or_reuse_cert():
     """
-    Retrieve new certificate from LDAP.
+    Retrieve certificate from LDAP. If the certificate is not available, reuse
+    the old certificate.
     """
-    operation = os.environ.get('CERTMONGER_OPERATION')
-    if operation == 'SUBMIT':
-        attempts = 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)
-    else:
-        return (OPERATION_NOT_SUPPORTED_BY_HELPER,)
-
     csr = os.environ.get('CERTMONGER_CSR')
     if not csr:
         return (UNCONFIGURED, "Certificate request not provided")
@@ -306,12 +292,9 @@ def retrieve_cert():
     if not nickname:
         return (REJECTED, "No friendly name in the certificate request")
 
-    old_cert = os.environ.get('CERTMONGER_CERTIFICATE')
-    if not old_cert:
+    cert = os.environ.get('CERTMONGER_CERTIFICATE')
+    if not cert:
         return (REJECTED, "New certificate requests not supported")
-    old_cert = x509.normalize_certificate(old_cert)
-
-    syslog.syslog(syslog.LOG_NOTICE, "Updating certificate for %s" % nickname)
 
     with ldap_connect() as conn:
         try:
@@ -320,23 +303,50 @@ def retrieve_cert():
                    ('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 (ISSUED, cert)
+
+def retrieve_cert():
+    """
+    Retrieve new certificate from LDAP.
+    """
+    operation = os.environ.get('CERTMONGER_OPERATION')
+    if operation == 'SUBMIT':
+        attempts = 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)
+    else:
+        return (OPERATION_NOT_SUPPORTED_BY_HELPER,)
 
-        if cert == old_cert:
-            attempts += 1
-            if attempts < 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, str(attempts))
+    old_cert = os.environ.get('CERTMONGER_CERTIFICATE')
+    if old_cert:
+        old_cert = x509.normalize_certificate(old_cert)
 
-        cert = base64.b64encode(cert)
-        cert = x509.make_pem(cert)
+    result = call_handler(retrieve_or_reuse_cert)
+    if result[0] != ISSUED:
+        return result
 
-    return (ISSUED, cert)
+    new_cert = x509.normalize_certificate(result[1])
+    if new_cert == old_cert:
+        attempts += 1
+        if attempts < 4:
+            syslog.syslog(syslog.LOG_INFO, "Updated certificate not available")
+            # No cert available yet, tell certmonger to wait another 8 hours
+            return (WAIT_WITH_DELAY, 8 * 60 * 60, str(attempts))
+
+    return result
 
 def export_csr():
     """
@@ -414,10 +424,11 @@ def renew_ca_cert():
 
 def main():
     handlers = {
-        'ipaStorage':       store_cert,
-        'ipaRetrieval':     retrieve_cert,
-        'ipaCSRExport':     export_csr,
-        'ipaCACertRenewal': renew_ca_cert,
+        'ipaStorage':           store_cert,
+        'ipaRetrievalOrReuse':  retrieve_or_reuse_cert,
+        'ipaRetrieval':         retrieve_cert,
+        'ipaCSRExport':         export_csr,
+        'ipaCACertRenewal':     renew_ca_cert,
     }
 
     api.bootstrap(context='renew')
diff --git a/ipa-client/ipaclient/ipa_certupdate.py b/ipa-client/ipaclient/ipa_certupdate.py
index ff16b9b..f2a7bf1 100644
--- a/ipa-client/ipaclient/ipa_certupdate.py
+++ b/ipa-client/ipaclient/ipa_certupdate.py
@@ -127,14 +127,16 @@ 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='ipaRetrievalOrReuse')
             try:
                 state = certmonger.wait_for_request(request_id, timeout)
             except RuntimeError:
                 raise admintool.ScriptError(
                     "Resubmitting certmonger request '%s' timed out, "
                     "please check the request manually" % request_id)
-            if state != 'MONITORING':
+            ca_error = certmonger.get_request_value(request_id, 'ca-error')
+            if state != 'MONITORING' or ca_error:
                 raise admintool.ScriptError(
                     "Error resubmitting certmonger request '%s', "
                     "please check the request manually" % request_id)
-- 
1.9.3

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

Reply via email to