diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py index 36bc59d7f..9ab05f7ca 100644 --- a/ipaplatform/base/paths.py +++ b/ipaplatform/base/paths.py @@ -101,8 +101,12 @@ class BasePathNamespace: OPENLDAP_LDAP_CONF = "/etc/openldap/ldap.conf" PAM_LDAP_CONF = "/etc/pam_ldap.conf" PASSWD = "/etc/passwd" + # Trusted CA certificates used to be written out to this file. In newer + # versions of FreeIPA, it has been replaced by IPA_P11_KIT. SYSTEMWIDE_IPA_CA_CRT = "/etc/pki/ca-trust/source/anchors/ipa-ca.crt" IPA_P11_KIT = "/etc/pki/ca-trust/source/ipa.p11-kit" + CA_CERTIFICATES_BUNDLE_PEM = None + CA_CERTIFICATES_DIR = None NSS_DB_DIR = "/etc/pki/nssdb" PKI_TOMCAT = "/etc/pki/pki-tomcat" PKI_TOMCAT_ALIAS_DIR = "/etc/pki/pki-tomcat/alias" diff --git a/ipaplatform/base/tasks.py b/ipaplatform/base/tasks.py index ad4321fd2..2e35dfd42 100644 --- a/ipaplatform/base/tasks.py +++ b/ipaplatform/base/tasks.py @@ -71,6 +71,23 @@ class BaseTaskNamespace: Returns True if the operation succeeded, False otherwise. """ + try: + if self.platform_insert_ca_certs(ca_certs): + return self.reload_systemwide_ca_store() + except Exception: + logger.exception('Could not populate systemwide CA store') + + return False + + def platform_insert_ca_certs(self, ca_certs): + """ + Platform implementations override this method to implement + population of the systemwide CA store. + + Returns True if changes were made to the CA store, False otherwise. + + Raises Exception if something went wrong. + """ raise NotImplementedError() def remove_ca_certs_from_systemwide_ca_store(self): @@ -81,6 +98,25 @@ class BaseTaskNamespace: Returns True if the operation succeeded, False otherwise. """ + try: + if self.platform_remove_ca_certs(): + return self.reload_systemwide_ca_store() + except Exception: + logger.exception( + 'Could not remove certificates from systemwide CA store' + ) + + return False + + def platform_remove_ca_certs(self): + """ + Platform implementations override this method to implement + removal of certificates from the systemwide CA store. + + Returns True if changes were made to the CA store, False otherwise. + + Raises Exception if something went wrong. + """ raise NotImplementedError() def get_svc_list_file(self): diff --git a/ipaplatform/debian/paths.py b/ipaplatform/debian/paths.py index 62757de50..40d71334d 100644 --- a/ipaplatform/debian/paths.py +++ b/ipaplatform/debian/paths.py @@ -43,7 +43,13 @@ class DebianPathNamespace(BasePathNamespace): CHRONY_CONF = "/etc/chrony/chrony.conf" OPENLDAP_LDAP_CONF = "/etc/ldap/ldap.conf" ETC_DEBIAN_VERSION = "/etc/debian_version" - IPA_P11_KIT = "/usr/local/share/ca-certificates/ipa-ca.crt" + # Old versions of freeipa wrote all trusted certificates to a single + # file, which is not supported by ca-certificates. + CA_CERTIFICATES_BUNDLE_PEM = "/usr/local/share/ca-certificates/ipa-ca.crt" + CA_CERTIFICATES_DIR = "/usr/local/share/ca-certificates/ipa-ca" + # Debian's p11-kit does not use ipa.p11-kit, so the file is provided + # for information only. + IPA_P11_KIT = "/usr/local/share/ca-certificates/ipa.p11-kit" ETC_SYSCONFIG_DIR = "/etc/default" SYSCONFIG_AUTOFS = "/etc/default/autofs" SYSCONFIG_DIRSRV = "/etc/default/dirsrv" diff --git a/ipaplatform/debian/tasks.py b/ipaplatform/debian/tasks.py index 31982a0ee..025c5d12d 100644 --- a/ipaplatform/debian/tasks.py +++ b/ipaplatform/debian/tasks.py @@ -8,12 +8,21 @@ This module contains default Debian-specific implementations of system tasks. from __future__ import absolute_import +import logging +import os +import shutil +from pathlib import Path + from ipaplatform.base.tasks import BaseTaskNamespace from ipaplatform.redhat.tasks import RedHatTaskNamespace from ipaplatform.paths import paths from ipapython import directivesetter from ipapython import ipautil +from ipapython.dn import DN + +logger = logging.getLogger(__name__) + class DebianTaskNamespace(RedHatTaskNamespace): @staticmethod @@ -88,4 +97,113 @@ class DebianTaskNamespace(RedHatTaskNamespace): def restore_pkcs11_modules(self, fstore): pass + def platform_insert_ca_certs(self, ca_certs): + # ca-certificates does not use this file, so it doesn't matter if we + # fail to create it. + try: + self.write_p11kit_certs(paths.IPA_P11_KIT, ca_certs), + except Exception: + logger.exception("""\ +Could not create p11-kit anchor trust file. On Debian this file is not +used by ca-certificates and is provided for information only.\ +""") + + return any([ + self.write_ca_certificates_dir( + paths.CA_CERTIFICATES_DIR, ca_certs + ), + self.remove_ca_certificates_bundle( + paths.CA_CERTIFICATES_BUNDLE_PEM + ), + ]) + + def write_ca_certificates_dir(self, directory, ca_certs): + # pylint: disable=ipa-forbidden-import + from ipalib import x509 # FixMe: break import cycle + # pylint: enable=ipa-forbidden-import + + path = Path(directory) + try: + path.mkdir(mode=0o755, exist_ok=True) + except Exception: + logger.error("Could not create %s", path) + raise + + for cert, nickname, trusted, _ext_key_usage in ca_certs: + if not trusted: + continue + + # I'm not handling errors here because they have already + # been checked by the time we get here + subject = DN(cert.subject) + issuer = DN(cert.issuer) + + # Construct the certificate filename using the Subject DN so that + # the user can see which CA a particular file is for, and include + # the serial number to disambiguate clashes where a subordinate CA + # had a new certificate issued. + # + # Strictly speaking, certificates are uniquely idenified by (Issuer + # DN, Serial Number). Do we care about the possibility of a clash + # where a subordinate CA had two certificates issued by different + # CAs who used the same serial number?) + filename = f'{subject.ldap_text()} {cert.serial_number}.crt' + + # pylint: disable=old-division + cert_path = path / filename + # pylint: enable=old-division + try: + f = open(cert_path, 'w') + except Exception: + logger.error("Could not create %s", cert_path) + raise + + with f: + try: + os.fchmod(f.fileno(), 0o644) + except Exception: + logger.error("Could not set mode of %s", cert_path) + raise + + try: + f.write(f"""\ +This file was created by IPA. Do not edit. + +Description: {nickname} +Subject: {subject.ldap_text()} +Issuer: {issuer.ldap_text()} +Serial Number (dec): {cert.serial_number} +Serial Number (hex): {cert.serial_number:#x} + +""") + pem = cert.public_bytes(x509.Encoding.PEM).decode('ascii') + f.write(pem) + except Exception: + logger.error("Could not write to %s", cert_path) + raise + + return True + + def platform_remove_ca_certs(self): + return any([ + self.remove_ca_certificates_dir(paths.CA_CERTIFICATES_DIR), + self.remove_ca_certificates_bundle(paths.IPA_P11_KIT), + self.remove_ca_certificates_bundle( + paths.CA_CERTIFICATES_BUNDLE_PEM + ), + ]) + + def remove_ca_certificates_dir(self, directory): + path = Path(paths.CA_CERTIFICATES_DIR) + if not path.exists(): + return False + + try: + shutil.rmtree(path) + except Exception: + logger.error("Could not remove %s", path) + raise + + return True + tasks = DebianTaskNamespace() diff --git a/ipaplatform/redhat/tasks.py b/ipaplatform/redhat/tasks.py index 0a4f4370d..3e1d42770 100644 --- a/ipaplatform/redhat/tasks.py +++ b/ipaplatform/redhat/tasks.py @@ -28,6 +28,7 @@ from __future__ import print_function, absolute_import import ctypes import logging import os +from pathlib import Path import socket import traceback import errno @@ -296,127 +297,120 @@ class RedHatTaskNamespace(BaseTaskNamespace): logger.info("Systemwide CA database updated.") return True - def insert_ca_certs_into_systemwide_ca_store(self, ca_certs): + def platform_insert_ca_certs(self, ca_certs): + return any([ + self.write_p11kit_certs(paths.IPA_P11_KIT, ca_certs), + self.remove_ca_certificates_bundle( + paths.SYSTEMWIDE_IPA_CA_CRT + ), + ]) + + def write_p11kit_certs(self, filename, ca_certs): # pylint: disable=ipa-forbidden-import from ipalib import x509 # FixMe: break import cycle from ipalib.errors import CertificateError # pylint: enable=ipa-forbidden-import - new_cacert_path = paths.SYSTEMWIDE_IPA_CA_CRT - - if os.path.exists(new_cacert_path): - try: - os.remove(new_cacert_path) - except OSError as e: - logger.error( - "Could not remove %s: %s", new_cacert_path, e) - return False - - new_cacert_path = paths.IPA_P11_KIT - + path = Path(filename) try: - f = open(new_cacert_path, 'w') - os.fchmod(f.fileno(), 0o644) - except IOError as e: - logger.info("Failed to open %s: %s", new_cacert_path, e) - return False + f = open(path, 'w') + except IOError: + logger.error("Failed to open %s", path) + raise - f.write("# This file was created by IPA. Do not edit.\n" - "\n") + with f: + f.write("# This file was created by IPA. Do not edit.\n" + "\n") - has_eku = set() - for cert, nickname, trusted, _ext_key_usage in ca_certs: try: - subject = cert.subject_bytes - issuer = cert.issuer_bytes - serial_number = cert.serial_number_bytes - public_key_info = cert.public_key_info_bytes - except (PyAsn1Error, ValueError, CertificateError) as e: - logger.warning( - "Failed to decode certificate \"%s\": %s", nickname, e) - continue + os.fchmod(f.fileno(), 0o644) + except IOError: + logger.error("Failed to set mode of %s", path) + raise - label = urllib.parse.quote(nickname) - subject = urllib.parse.quote(subject) - issuer = urllib.parse.quote(issuer) - serial_number = urllib.parse.quote(serial_number) - public_key_info = urllib.parse.quote(public_key_info) - - obj = ("[p11-kit-object-v1]\n" - "class: certificate\n" - "certificate-type: x-509\n" - "certificate-category: authority\n" - "label: \"%(label)s\"\n" - "subject: \"%(subject)s\"\n" - "issuer: \"%(issuer)s\"\n" - "serial-number: \"%(serial_number)s\"\n" - "x-public-key-info: \"%(public_key_info)s\"\n" % - dict(label=label, - subject=subject, - issuer=issuer, - serial_number=serial_number, - public_key_info=public_key_info)) - if trusted is True: - obj += "trusted: true\n" - elif trusted is False: - obj += "x-distrusted: true\n" - obj += "{pem}\n\n".format( - pem=cert.public_bytes(x509.Encoding.PEM).decode('ascii')) - f.write(obj) - - if (cert.extended_key_usage is not None and - public_key_info not in has_eku): + has_eku = set() + for cert, nickname, trusted, _ext_key_usage in ca_certs: try: - ext_key_usage = cert.extended_key_usage_bytes - except PyAsn1Error as e: - logger.warning( - "Failed to encode extended key usage for \"%s\": %s", - nickname, e) - continue - value = urllib.parse.quote(ext_key_usage) + subject = cert.subject_bytes + issuer = cert.issuer_bytes + serial_number = cert.serial_number_bytes + public_key_info = cert.public_key_info_bytes + except (PyAsn1Error, ValueError, CertificateError): + logger.error( + "Failed to decode certificate \"%s\"", nickname) + raise + + label = urllib.parse.quote(nickname) + subject = urllib.parse.quote(subject) + issuer = urllib.parse.quote(issuer) + serial_number = urllib.parse.quote(serial_number) + public_key_info = urllib.parse.quote(public_key_info) + obj = ("[p11-kit-object-v1]\n" - "class: x-certificate-extension\n" - "label: \"ExtendedKeyUsage for %(label)s\"\n" - "x-public-key-info: \"%(public_key_info)s\"\n" - "object-id: 2.5.29.37\n" - "value: \"%(value)s\"\n\n" % + "class: certificate\n" + "certificate-type: x-509\n" + "certificate-category: authority\n" + "label: \"%(label)s\"\n" + "subject: \"%(subject)s\"\n" + "issuer: \"%(issuer)s\"\n" + "serial-number: \"%(serial_number)s\"\n" + "x-public-key-info: \"%(public_key_info)s\"\n" % dict(label=label, - public_key_info=public_key_info, - value=value)) + subject=subject, + issuer=issuer, + serial_number=serial_number, + public_key_info=public_key_info)) + if trusted is True: + obj += "trusted: true\n" + elif trusted is False: + obj += "x-distrusted: true\n" + obj += "{pem}\n\n".format( + pem=cert.public_bytes(x509.Encoding.PEM).decode('ascii')) + f.write(obj) - has_eku.add(public_key_info) - f.close() - - # Add the CA to the systemwide CA trust database - if not self.reload_systemwide_ca_store(): - return False + if (cert.extended_key_usage is not None and + public_key_info not in has_eku): + try: + ext_key_usage = cert.extended_key_usage_bytes + except PyAsn1Error: + logger.error( + "Failed to encode extended key usage for \"%s\"", + nickname) + raise + value = urllib.parse.quote(ext_key_usage) + obj = ("[p11-kit-object-v1]\n" + "class: x-certificate-extension\n" + "label: \"ExtendedKeyUsage for %(label)s\"\n" + "x-public-key-info: \"%(public_key_info)s\"\n" + "object-id: 2.5.29.37\n" + "value: \"%(value)s\"\n\n" % + dict(label=label, + public_key_info=public_key_info, + value=value)) + f.write(obj) + has_eku.add(public_key_info) return True - def remove_ca_certs_from_systemwide_ca_store(self): - result = True - update = False + def platform_remove_ca_certs(self): + return any([ + self.remove_ca_certificates_bundle(paths.IPA_P11_KIT), + self.remove_ca_certificates_bundle(paths.SYSTEMWIDE_IPA_CA_CRT), + ]) - # Remove CA cert from systemwide store - for new_cacert_path in (paths.IPA_P11_KIT, - paths.SYSTEMWIDE_IPA_CA_CRT): - if not os.path.exists(new_cacert_path): - continue - try: - os.remove(new_cacert_path) - except OSError as e: - logger.error( - "Could not remove %s: %s", new_cacert_path, e) - result = False - else: - update = True + def remove_ca_certificates_bundle(self, filename): + path = Path(filename) + if not path.is_file(): + return False - if update: - if not self.reload_systemwide_ca_store(): - return False + try: + path.unlink() + except Exception: + logger.error("Could not remove %s", path) + raise - return result + return True def backup_hostname(self, fstore, statestore): filepath = paths.ETC_HOSTNAME