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