0b494d
From dc90e83e77f63384f072ca31d79705e26dc5d656 Mon Sep 17 00:00:00 2001
0b494d
From: Fraser Tweedale <ftweedal@redhat.com>
0b494d
Date: Thu, 30 May 2019 20:57:10 +1000
0b494d
Subject: [PATCH] Handle missing LWCA certificate or chain
0b494d
0b494d
If lightweight CA key replication has not completed, requests for
0b494d
the certificate or chain will return 404**.  This can occur in
0b494d
normal operation, and should be a temporary condition.  Detect this
0b494d
case and handle it by simply omitting the 'certificate' and/or
0b494d
'certificate_out' fields in the response, and add a warning message
0b494d
to the response.
0b494d
0b494d
Also update the client-side plugin that handles the
0b494d
--certificate-out option.  Because the CLI will automatically print
0b494d
the warning message, if the expected field is missing from the
0b494d
response, just ignore it and continue processing.
0b494d
0b494d
** after the Dogtag NullPointerException gets fixed!
0b494d
0b494d
Part of: https://pagure.io/freeipa/issue/7964
0b494d
0b494d
Reviewed-By: Christian Heimes <cheimes@redhat.com>
0b494d
Reviewed-By: Fraser Tweedale <ftweedal@redhat.com>
0b494d
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
0b494d
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
0b494d
---
0b494d
 ipaclient/plugins/ca.py | 19 +++++++++++---
0b494d
 ipalib/messages.py      |  9 +++++++
0b494d
 ipaserver/plugins/ca.py | 57 +++++++++++++++++++++++++++++++----------
0b494d
 3 files changed, 68 insertions(+), 17 deletions(-)
0b494d
0b494d
diff --git a/ipaclient/plugins/ca.py b/ipaclient/plugins/ca.py
0b494d
index f0e7d5ced0d3d9318e34aba84cbc37cf42b9410d..ab47ae85df398e1dc40191691a26639eb3772493 100644
0b494d
--- a/ipaclient/plugins/ca.py
0b494d
+++ b/ipaclient/plugins/ca.py
0b494d
@@ -33,13 +33,24 @@ class WithCertOutArgs(MethodOverride):
0b494d
                                              error=str(e))
0b494d
 
0b494d
         result = super(WithCertOutArgs, self).forward(*keys, **options)
0b494d
+
0b494d
         if filename:
0b494d
+            # if result certificate / certificate_chain not present in result,
0b494d
+            # it means Dogtag did not provide it (probably due to LWCA key
0b494d
+            # replication lag or failure.  The server transmits a warning
0b494d
+            # message in this case, which the client automatically prints.
0b494d
+            # So in this section we just ignore it and move on.
0b494d
+            certs = None
0b494d
             if options.get('chain', False):
0b494d
-                certs = result['result']['certificate_chain']
0b494d
+                if 'certificate_chain' in result['result']:
0b494d
+                    certs = result['result']['certificate_chain']
0b494d
             else:
0b494d
-                certs = [base64.b64decode(result['result']['certificate'])]
0b494d
-            certs = (x509.load_der_x509_certificate(cert) for cert in certs)
0b494d
-            x509.write_certificate_list(certs, filename)
0b494d
+                if 'certificate' in result['result']:
0b494d
+                    certs = [base64.b64decode(result['result']['certificate'])]
0b494d
+            if certs:
0b494d
+                x509.write_certificate_list(
0b494d
+                    (x509.load_der_x509_certificate(cert) for cert in certs),
0b494d
+                    filename)
0b494d
 
0b494d
         return result
0b494d
 
0b494d
diff --git a/ipalib/messages.py b/ipalib/messages.py
0b494d
index 9e2c990d6db8ee41daf3fba6085eed8355dccbe7..646662795648b5a44a5ce25b7610982d5500cfac 100644
0b494d
--- a/ipalib/messages.py
0b494d
+++ b/ipalib/messages.py
0b494d
@@ -487,6 +487,15 @@ class FailedToAddHostDNSRecords(PublicMessage):
0b494d
                "%(reason)s")
0b494d
 
0b494d
 
0b494d
+class LightweightCACertificateNotAvailable(PublicMessage):
0b494d
+    """
0b494d
+    **13031** Certificate is not available
0b494d
+    """
0b494d
+    errno = 13031
0b494d
+    type = "error"
0b494d
+    format = _("The certificate for %(ca)s is not available on this server.")
0b494d
+
0b494d
+
0b494d
 def iter_messages(variables, base):
0b494d
     """Return a tuple with all subclasses
0b494d
     """
0b494d
diff --git a/ipaserver/plugins/ca.py b/ipaserver/plugins/ca.py
0b494d
index 88e7ec2a9f50a3c4f90947c8e3d38e327627a878..c8f1630c65d55ee9e820ea50ef34e08f92c66f4a 100644
0b494d
--- a/ipaserver/plugins/ca.py
0b494d
+++ b/ipaserver/plugins/ca.py
0b494d
@@ -6,7 +6,7 @@ import base64
0b494d
 
0b494d
 import six
0b494d
 
0b494d
-from ipalib import api, errors, output, Bytes, DNParam, Flag, Str
0b494d
+from ipalib import api, errors, messages, output, Bytes, DNParam, Flag, Str
0b494d
 from ipalib.constants import IPA_CA_CN
0b494d
 from ipalib.plugable import Registry
0b494d
 from ipapython.dn import ATTR_NAME_BY_OID
0b494d
@@ -163,28 +163,53 @@ class ca(LDAPObject):
0b494d
 
0b494d
 
0b494d
 def set_certificate_attrs(entry, options, want_cert=True):
0b494d
+    """
0b494d
+    Set certificate attributes into the entry.  Depending on
0b494d
+    options, this may contact Dogtag to retrieve certificate or
0b494d
+    chain.  If the retrieval fails with 404 (which can occur under
0b494d
+    normal operation due to lightweight CA key replication delay),
0b494d
+    return a message object that should be set in the response.
0b494d
+
0b494d
+    """
0b494d
     try:
0b494d
         ca_id = entry['ipacaid'][0]
0b494d
     except KeyError:
0b494d
-        return
0b494d
+        return None
0b494d
     full = options.get('all', False)
0b494d
     want_chain = options.get('chain', False)
0b494d
 
0b494d
     want_data = want_cert or want_chain or full
0b494d
     if not want_data:
0b494d
-        return
0b494d
+        return None
0b494d
+
0b494d
+    msg = None
0b494d
 
0b494d
     with api.Backend.ra_lightweight_ca as ca_api:
0b494d
         if want_cert or full:
0b494d
-            der = ca_api.read_ca_cert(ca_id)
0b494d
-            entry['certificate'] = base64.b64encode(der).decode('ascii')
0b494d
+            try:
0b494d
+                der = ca_api.read_ca_cert(ca_id)
0b494d
+                entry['certificate'] = base64.b64encode(der).decode('ascii')
0b494d
+            except errors.HTTPRequestError as e:
0b494d
+                if e.status == 404:  # pylint: disable=no-member
0b494d
+                    msg = messages.LightweightCACertificateNotAvailable(
0b494d
+                        ca=entry['cn'][0])
0b494d
+                else:
0b494d
+                    raise e
0b494d
 
0b494d
         if want_chain or full:
0b494d
-            pkcs7_der = ca_api.read_ca_chain(ca_id)
0b494d
-            certs = x509.pkcs7_to_certs(pkcs7_der, x509.DER)
0b494d
-            ders = [cert.public_bytes(x509.Encoding.DER) for cert in certs]
0b494d
-            entry['certificate_chain'] = ders
0b494d
-
0b494d
+            try:
0b494d
+                pkcs7_der = ca_api.read_ca_chain(ca_id)
0b494d
+                certs = x509.pkcs7_to_certs(pkcs7_der, x509.DER)
0b494d
+                ders = [cert.public_bytes(x509.Encoding.DER) for cert in certs]
0b494d
+                entry['certificate_chain'] = ders
0b494d
+            except errors.HTTPRequestError as e:
0b494d
+                if e.status == 404:  # pylint: disable=no-member
0b494d
+                    msg = messages.LightweightCACertificateNotAvailable(
0b494d
+                        ca=entry['cn'][0])
0b494d
+                else:
0b494d
+                    raise e
0b494d
+
0b494d
+    return msg
0b494d
 
0b494d
 @register()
0b494d
 class ca_find(LDAPSearch):
0b494d
@@ -198,7 +223,9 @@ class ca_find(LDAPSearch):
0b494d
         result = super(ca_find, self).execute(*keys, **options)
0b494d
         if not options.get('pkey_only', False):
0b494d
             for entry in result['result']:
0b494d
-                set_certificate_attrs(entry, options, want_cert=False)
0b494d
+                msg = set_certificate_attrs(entry, options, want_cert=False)
0b494d
+                if msg:
0b494d
+                    self.add_message(msg)
0b494d
         return result
0b494d
 
0b494d
 
0b494d
@@ -220,7 +247,9 @@ class ca_show(LDAPRetrieve):
0b494d
     def execute(self, *keys, **options):
0b494d
         ca_enabled_check(self.api)
0b494d
         result = super(ca_show, self).execute(*keys, **options)
0b494d
-        set_certificate_attrs(result['result'], options)
0b494d
+        msg = set_certificate_attrs(result['result'], options)
0b494d
+        if msg:
0b494d
+            self.add_message(msg)
0b494d
         return result
0b494d
 
0b494d
 
0b494d
@@ -284,7 +313,9 @@ class ca_add(LDAPCreate):
0b494d
         return dn
0b494d
 
0b494d
     def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
0b494d
-        set_certificate_attrs(entry_attrs, options)
0b494d
+        msg = set_certificate_attrs(entry_attrs, options)
0b494d
+        if msg:
0b494d
+            self.add_message(msg)
0b494d
         return dn
0b494d
 
0b494d
 
0b494d
-- 
0b494d
2.20.1
0b494d