Hi,

the attached patch fixes <https://fedorahosted.org/freeipa/ticket/4629>. It depends on my patches 333 and 334, which are 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. kinit as admin

  3. run "ipa-cacert-manage renew --external-ca", it will produce a CSR

  4. sign the CSR with some external CA to get new IPA CA certificate

5. run "while true; do ldapdelete -H ldap://$HOSTNAME -Y GSSAPI 'cn=caSigningCert cert-pki-ca,cn=ca_renewal,cn=ipa,cn=etc,<suffix>'; done" in background

6. run "ipa-cacert-manage renew --external-cert-file=<path to new IPA CA certificate> --external-cert-file=<path to external CA certificate chain>"

  7. stop the loop from step 5

8. 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

>From 87dba46f643f152b8d1f8a0323c4b46b2c484062 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Tue, 14 Oct 2014 11:26:15 +0200
Subject: [PATCH] Fail if certmonger can't see new CA certificate in LDAP in
 ipa-cacert-manage

This should not normally happen, but if it does, report an error instead of
waiting idefinitely for the certificate to appear.

https://fedorahosted.org/freeipa/ticket/4629
---
 .../certmonger/dogtag-ipa-ca-renew-agent-submit    | 40 +++++++++-------------
 ipaserver/install/ipa_cacert_manage.py             |  3 +-
 2 files changed, 19 insertions(+), 24 deletions(-)

diff --git a/install/certmonger/dogtag-ipa-ca-renew-agent-submit b/install/certmonger/dogtag-ipa-ca-renew-agent-submit
index 9a01eb3..e5ad963 100755
--- a/install/certmonger/dogtag-ipa-ca-renew-agent-submit
+++ b/install/certmonger/dogtag-ipa-ca-renew-agent-submit
@@ -311,25 +311,11 @@ def retrieve_or_reuse_cert():
 
     return (ISSUED, cert)
 
-def retrieve_cert():
+def retrieve_cert_continuous():
     """
-    Retrieve new certificate from LDAP.
+    Retrieve new certificate from LDAP. Repeat every eight hours until the
+    certificate is available.
     """
-    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,)
-
     old_cert = os.environ.get('CERTMONGER_CERTIFICATE')
     if old_cert:
         old_cert = x509.normalize_certificate(old_cert)
@@ -340,11 +326,19 @@ def retrieve_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))
+        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, '')
+
+    return result
+
+def retrieve_cert():
+    """
+    Retrieve new certificate from LDAP.
+    """
+    result = call_handler(retrieve_cert_continuous)
+    if result[0] == WAIT_WITH_DELAY:
+        return (REJECTED, "Updated certificate not available")
 
     return result
 
@@ -451,7 +445,7 @@ def main():
             if ca.is_renewal_master():
                 handler = request_and_store_cert
             else:
-                handler = retrieve_cert
+                handler = retrieve_cert_continuous
 
         res = call_handler(handler)
         for item in res[1:]:
diff --git a/ipaserver/install/ipa_cacert_manage.py b/ipaserver/install/ipa_cacert_manage.py
index a521e39..2a8d95f 100644
--- a/ipaserver/install/ipa_cacert_manage.py
+++ b/ipaserver/install/ipa_cacert_manage.py
@@ -297,7 +297,8 @@ class CACertManage(admintool.AdminTool):
             raise admintool.ScriptError(
                 "Resubmitting certmonger request '%s' timed out, "
                 "please check the request manually" % self.request_id)
-        if state != 'MONITORING':
+        ca_error = certmonger.get_request_value(self.request_id, 'ca-error')
+        if state != 'MONITORING' or ca_error:
             raise admintool.ScriptError(
                 "Error resubmitting certmonger request '%s', "
                 "please check the request manually" % self.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