From 08117a7bcf0eb893d44f8dc3f2494cb710628676 Mon Sep 17 00:00:00 2001
From: Sam Morris <sam@robots.org.uk>
Date: Tue, 17 Dec 2019 18:41:35 +0000
Subject: [PATCH] [WIP] Debian: write out only one CA certificate per file

ca-certificates populates /etc/ssl/certs with symlinks to its input
files and then runs 'openssl rehash' to create the symlinks that libssl
uses to look up a CA certificate to see if it is trused.

'openssl rehash' ignores any files that contain more than one
certificate: <https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=945274>.

With this change, we write out trusted CA certificates to
/usr/local/share/ipa-ca, one certificate per file.

Fixes: https://pagure.io/freeipa/issue/8106
---
 ipaplatform/debian/paths.py |  3 +-
 ipaplatform/debian/tasks.py | 96 +++++++++++++++++++++++++++++++++++++
 2 files changed, 98 insertions(+), 1 deletion(-)

diff --git a/ipaplatform/debian/paths.py b/ipaplatform/debian/paths.py
index 764b5a2815..d535adfe56 100644
--- a/ipaplatform/debian/paths.py
+++ b/ipaplatform/debian/paths.py
@@ -43,7 +43,8 @@ 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"
+    CA_CERTIFICATES_BUNDLE_PEM = "/usr/local/share/ca-certificates/ipa-ca.crt"
+    CA_CERTIFICATES_DIR = "/usr/local/share/ca-certificates/ipa-ca" # XXX could use /usr/share/ca-certificates/ipa-ca
     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 31982a0ee9..09a5d7483b 100644
--- a/ipaplatform/debian/tasks.py
+++ b/ipaplatform/debian/tasks.py
@@ -8,6 +8,11 @@
 
 from __future__ import absolute_import
 
+import errno
+import logging
+import os
+from pathlib import Path
+
 from ipaplatform.base.tasks import BaseTaskNamespace
 from ipaplatform.redhat.tasks import RedHatTaskNamespace
 from ipaplatform.paths import paths
@@ -15,6 +20,9 @@
 from ipapython import directivesetter
 from ipapython import ipautil
 
+logger = logging.getLogger(__name__)
+
+
 class DebianTaskNamespace(RedHatTaskNamespace):
     @staticmethod
     def restore_pre_ipa_client_configuration(fstore, statestore,
@@ -88,4 +96,92 @@ def configure_pkcs11_modules(self, fstore):
     def restore_pkcs11_modules(self, fstore):
         pass
 
+    def insert_ca_certs_into_systemwide_ca_store(self, ca_certs):
+        result = True
+
+        # pylint: disable=ipa-forbidden-import
+        from ipalib import x509  # FixMe: break import cycle
+        # pylint: enable=ipa-forbidden-import
+
+        # TODO: it would be nice to have paths.IPA_P11_KIT created here, so
+        # that it could still be used by p11-kit in the future if Debian were
+        # to adopt Red Hat's CA certificate management code. If we were to do
+        # that, the code should be split out into a separate method that both
+        # DebianTaskNamespace and RedHatTaskNamespace can call.
+
+        ca_certificates_dir_path = Path(paths.CA_CERTIFICATES_DIR)
+        try:
+            ca_certificates_dir_path.mkdir(mode=0o755, exist_ok=True)
+        except OSError as e:
+            logger.error(
+                'Could not create %s: %s', ca_certificates_dir_path, e)
+            return False
+
+        for i, (cert, nickname, trusted, _ext_key_usage) in enumerate(ca_certs):
+            if not trusted:
+                continue
+
+            try:
+                subject = cert.subject.rfc4514_string()
+                issuer = cert.issuer.rfc4514_string()
+                serial_number = cert.serial_number
+            except (PyAsn1Error, ValueError, CertificateError) as e:
+                logger.warning(
+                    "Failed to decode certificate \"%s\": %s", nickname, e)
+                continue
+
+            ca_path = ca_certificates_dir_path/f'ipa-ca-{i}.crt'
+
+            try:
+                with open(ca_path, 'w') as f:
+                    os.fchmod(f.fileno(), 0o644)
+
+                    f.write(f'''\
+This file was created by IPA. Do not edit.
+
+Subject: {subject}
+Issuer: {issuer}
+Serial Number (dec): {serial_number}
+Serial Number (hex): {serial_number:#x}
+''')
+
+                    f.write(cert.public_bytes(x509.Encoding.PEM).decode('ascii'))
+            except IOError as e:
+                logger.info("Failed to open/write to %s: %s", ca_path, e)
+                return False
+
+        if not self.reload_systemwide_ca_store():
+            result = False
+
+        return result
+
+    def remove_ca_certs_from_systemwide_ca_store(self):
+        result = True
+        update = False
+
+        # Old versions of freeipa wrote all trusted certificates to a single
+        # file, which is not supported by ca-certificates.
+        old_cacert_paths = [paths.CA_CERTIFICATES_BUNDLE_PEM, paths.IPA_P11_KIT]
+
+        ca_certificates_dir_path = Path(paths.CA_CERTIFICATES_DIR)
+        if ca_certificates_dir_path.is_dir():
+            old_cacert_paths.extend(p for p in ca_certificates_dir_path.iterdir() if p.suffix == '.crt' and p.is_file())
+
+        for old_cacert_path in old_cacert_paths:
+            try:
+                os.remove(old_cacert_path)
+            except OSError as e:
+                if e.errno != 2:
+                    logger.error(
+                        "Could not remove %s: %s", old_cacert_path, e)
+                    result = False
+            else:
+                update = True
+
+        if update:
+            if not self.reload_systemwide_ca_store():
+                return False
+
+        return result
+
 tasks = DebianTaskNamespace()
