Blame SOURCES/0009-Add-checks-to-detect-mismatch-of-certificates.patch

8cfcc8
From f762f8f5e2f9b6d66d786b426d4d2fe40c994192 Mon Sep 17 00:00:00 2001
8cfcc8
From: Antonio Torres <antorres@redhat.com>
8cfcc8
Date: Fri, 23 Apr 2021 17:42:21 +0200
8cfcc8
Subject: [PATCH] Add checks to detect mismatch of certificates
8cfcc8
8cfcc8
Add checks to detect mismatch of certificates between LDAP
8cfcc8
and NSS databases. Check for existance of entries as well as
8cfcc8
ensure the certificates match between the different databases.
8cfcc8
8cfcc8
Related: https://bugzilla.redhat.com/show_bug.cgi?id=1886770
8cfcc8
Signed-off-by: Antonio Torres <antorres@redhat.com>
8cfcc8
---
8cfcc8
 README.md                       |  24 ++++
8cfcc8
 src/ipahealthcheck/ipa/certs.py | 219 ++++++++++++++++++++++++++++++++
8cfcc8
 2 files changed, 243 insertions(+)
8cfcc8
8cfcc8
diff --git a/README.md b/README.md
8cfcc8
index 0f3ed6a..11e0e88 100644
8cfcc8
--- a/README.md
8cfcc8
+++ b/README.md
8cfcc8
@@ -507,6 +507,30 @@ The trust for certificates stored in NSS databases is compared against a known g
8cfcc8
       }
8cfcc8
     }
8cfcc8
 
8cfcc8
+### IPACertMatchCheck
8cfcc8
+Ensure CA certificate entries in LDAP and NSS databases match.
8cfcc8
+
8cfcc8
+    {
8cfcc8
+      "source": "ipahealthcheck.ipa.certs",
8cfcc8
+      "check": "IPACertMatchCheck",
8cfcc8
+      "result": "ERROR",
8cfcc8
+      "kw": {
8cfcc8
+        "msg": "CA Certificate from /etc/ipa/nssdb does not match /etc/ipa/ca.crt"
8cfcc8
+      }
8cfcc8
+    }
8cfcc8
+
8cfcc8
+### IPADogtagCertsMatchCheck
8cfcc8
+Check if Dogtag certificates present in both NSS DB and LDAP match.
8cfcc8
+
8cfcc8
+    {
8cfcc8
+      "source": "ipahealthcheck.ipa.certs",
8cfcc8
+      "check": "IPADogtagCertsMatchCheck",
8cfcc8
+      "result": "ERROR",
8cfcc8
+      "kw": {
8cfcc8
+        "msg": "'subsystemCert cert-pki-ca' certificate in NSS DB does not match entry in LDAP"
8cfcc8
+      }
8cfcc8
+    }
8cfcc8
+
8cfcc8
 ### IPANSSChainValidation
8cfcc8
 Validate the certificate chain of the NSS certificates. This executes: certutil -V -u V -e -d [dbdir] -n [nickname].
8cfcc8
 
8cfcc8
diff --git a/src/ipahealthcheck/ipa/certs.py b/src/ipahealthcheck/ipa/certs.py
8cfcc8
index c668093..82435f3 100644
8cfcc8
--- a/src/ipahealthcheck/ipa/certs.py
8cfcc8
+++ b/src/ipahealthcheck/ipa/certs.py
8cfcc8
@@ -29,6 +29,7 @@ from ipaserver.plugins import ldap2
8cfcc8
 from ipapython import certdb
8cfcc8
 from ipapython import ipautil
8cfcc8
 from ipapython.dn import DN
8cfcc8
+from ipapython.ipaldap import realm_to_serverid
8cfcc8
 
8cfcc8
 logger = logging.getLogger()
8cfcc8
 DAY = 60 * 60 * 24
8cfcc8
@@ -587,6 +588,224 @@ class IPACertNSSTrust(IPAPlugin):
8cfcc8
                     'verifying trust')
8cfcc8
 
8cfcc8
 
8cfcc8
+@registry
8cfcc8
+class IPACertMatchCheck(IPAPlugin):
8cfcc8
+    """
8cfcc8
+    Ensure certificates match between LDAP and NSS databases
8cfcc8
+    """
8cfcc8
+
8cfcc8
+    requires = ('dirsrv',)
8cfcc8
+
8cfcc8
+    def get_cert_list_from_db(self, nssdb, nickname):
8cfcc8
+        """
8cfcc8
+        Retrieve all certificates from an NSS database for nickname.
8cfcc8
+        """
8cfcc8
+        try:
8cfcc8
+            args = ["-L", "-n", nickname, "-a"]
8cfcc8
+            result = nssdb.run_certutil(args, capture_output=True)
8cfcc8
+            return x509.load_certificate_list(result.raw_output)
8cfcc8
+        except ipautil.CalledProcessError:
8cfcc8
+            return []
8cfcc8
+
8cfcc8
+    @duration
8cfcc8
+    def check(self):
8cfcc8
+        if not self.ca.is_configured():
8cfcc8
+            logger.debug("No CA configured, skipping certificate match check")
8cfcc8
+            return
8cfcc8
+
8cfcc8
+        # Ensure /etc/ipa/ca.crt matches the NSS DB CA certificates
8cfcc8
+        def match_cacert_and_db(plugin, cacerts, dbpath):
8cfcc8
+            db = certs.CertDB(api.env.realm, dbpath)
8cfcc8
+            nickname = '%s IPA CA' % api.env.realm
8cfcc8
+            try:
8cfcc8
+                dbcacerts = self.get_cert_list_from_db(db, nickname)
8cfcc8
+            except Exception as e:
8cfcc8
+                yield Result(plugin, constants.ERROR,
8cfcc8
+                             error=str(e),
8cfcc8
+                             msg='Unable to load CA cert: {error}')
8cfcc8
+                return False
8cfcc8
+
8cfcc8
+            ok = True
8cfcc8
+            for cert in dbcacerts:
8cfcc8
+                if cert not in cacerts:
8cfcc8
+                    ok = False
8cfcc8
+                    yield Result(plugin, constants.ERROR,
8cfcc8
+                                 nickname=nickname,
8cfcc8
+                                 serial_number=cert.serial_number,
8cfcc8
+                                 dbdir=dbpath,
8cfcc8
+                                 certdir=paths.IPA_CA_CRT,
8cfcc8
+                                 msg=('CA Certificate nickname {nickname} '
8cfcc8
+                                      'with serial number {serial} '
8cfcc8
+                                      'is in {dbdir} but is not in'
8cfcc8
+                                      '%s' % paths.IPA_CA_CRT))
8cfcc8
+            return ok
8cfcc8
+
8cfcc8
+        try:
8cfcc8
+            cacerts = x509.load_certificate_list_from_file(paths.IPA_CA_CRT)
8cfcc8
+        except Exception:
8cfcc8
+            yield Result(self, constants.ERROR,
8cfcc8
+                         path=paths.IPA_CA_CRT,
8cfcc8
+                         msg='Unable to load CA cert file {path}: {error}')
8cfcc8
+            return
8cfcc8
+
8cfcc8
+        # Ensure CA cert entry from LDAP matches /etc/ipa/ca.crt
8cfcc8
+        dn = DN('cn=%s IPA CA' % api.env.realm,
8cfcc8
+                'cn=certificates,cn=ipa,cn=etc',
8cfcc8
+                api.env.basedn)
8cfcc8
+        try:
8cfcc8
+            entry = self.conn.get_entry(dn)
8cfcc8
+        except errors.NotFound:
8cfcc8
+            yield Result(self, constants.ERROR,
8cfcc8
+                         dn=str(dn),
8cfcc8
+                         msg='CA Certificate entry \'{dn}\' '
8cfcc8
+                             'not found in LDAP')
8cfcc8
+            return
8cfcc8
+
8cfcc8
+        cacerts_ok = True
8cfcc8
+        # Are all the certs in LDAP for the IPA CA in /etc/ipa/ca.crt
8cfcc8
+        for cert in entry['CACertificate']:
8cfcc8
+            if cert not in cacerts:
8cfcc8
+                cacerts_ok = False
8cfcc8
+                yield Result(self, constants.ERROR,
8cfcc8
+                             dn=str(dn),
8cfcc8
+                             serial_number=cert.serial_number,
8cfcc8
+                             msg=('CA Certificate serial number {serial} is '
8cfcc8
+                                  'in LDAP \'{dn}\' but is not in '
8cfcc8
+                                  '%s' % paths.IPA_CA_CRT))
8cfcc8
+
8cfcc8
+        # Ensure NSS DBs have matching CA certs for /etc/ipa/ca.crt
8cfcc8
+        serverid = realm_to_serverid(api.env.realm)
8cfcc8
+        dspath = paths.ETC_DIRSRV_SLAPD_INSTANCE_TEMPLATE % serverid
8cfcc8
+
8cfcc8
+        cacertds_ok = yield from match_cacert_and_db(self, cacerts, dspath)
8cfcc8
+        cacertnss_ok = yield from match_cacert_and_db(self, cacerts,
8cfcc8
+                                                      paths.IPA_NSSDB_DIR)
8cfcc8
+        if cacerts_ok:
8cfcc8
+            yield Result(self, constants.SUCCESS,
8cfcc8
+                         key=paths.IPA_CA_CRT)
8cfcc8
+        if cacertds_ok:
8cfcc8
+            yield Result(self, constants.SUCCESS,
8cfcc8
+                         key=dspath)
8cfcc8
+        if cacertnss_ok:
8cfcc8
+            yield Result(self, constants.SUCCESS,
8cfcc8
+                         key=paths.IPA_NSSDB_DIR)
8cfcc8
+
8cfcc8
+
8cfcc8
+@registry
8cfcc8
+class IPADogtagCertsMatchCheck(IPAPlugin):
8cfcc8
+    """
8cfcc8
+    Check if dogtag certs present in both NSS DB and LDAP match
8cfcc8
+    """
8cfcc8
+    requires = ('dirsrv',)
8cfcc8
+
8cfcc8
+    @duration
8cfcc8
+    def check(self):
8cfcc8
+        if not self.ca.is_configured():
8cfcc8
+            logger.debug('CA is not configured, skipping connectivity check')
8cfcc8
+            return
8cfcc8
+
8cfcc8
+        def match_ldap_nss_cert(plugin, ldap, db, cert_dn, attr, cert_nick):
8cfcc8
+            try:
8cfcc8
+                entry = ldap.get_entry(cert_dn)
8cfcc8
+            except errors.NotFound:
8cfcc8
+                yield Result(plugin, constants.ERROR,
8cfcc8
+                             msg='%s entry not found in LDAP' % cert_dn)
8cfcc8
+                return False
8cfcc8
+            try:
8cfcc8
+                nsscert = db.get_cert_from_db(cert_nick)
8cfcc8
+            except Exception as e:
8cfcc8
+                yield Result(plugin, constants.ERROR,
8cfcc8
+                             error=str(e),
8cfcc8
+                             msg=('Unable to load %s certificate:'
8cfcc8
+                                  '{error}' % cert_nick))
8cfcc8
+                return False
8cfcc8
+            cert_matched = any([cert == nsscert for cert in entry[attr]])
8cfcc8
+            if not cert_matched:
8cfcc8
+                yield Result(plugin, constants.ERROR,
8cfcc8
+                             key=cert_nick,
8cfcc8
+                             nickname=cert_nick,
8cfcc8
+                             dbdir=db.secdir,
8cfcc8
+                             msg=('{nickname} certificate in NSS DB {dbdir} '
8cfcc8
+                                  'does not match entry in LDAP'))
8cfcc8
+                return False
8cfcc8
+            return True
8cfcc8
+
8cfcc8
+        def match_ldap_nss_certs_by_subject(plugin, ldap, db, dn,
8cfcc8
+                                            expected_nicks_subjects):
8cfcc8
+            entries = ldap.get_entries(dn)
8cfcc8
+            all_ok = True
8cfcc8
+            for nick, subject in expected_nicks_subjects.items():
8cfcc8
+                cert = db.get_cert_from_db(nick)
8cfcc8
+                ok = any([cert in entry['userCertificate'] and
8cfcc8
+                          subject == entry['subjectName'][0]
8cfcc8
+                          for entry in entries
8cfcc8
+                          if 'userCertificate' in entry])
8cfcc8
+                if not ok:
8cfcc8
+                    all_ok = False
8cfcc8
+                    yield Result(plugin, constants.ERROR,
8cfcc8
+                                 key=nick,
8cfcc8
+                                 nickname=nick,
8cfcc8
+                                 dbdir=db.secdir,
8cfcc8
+                                 msg=('{nickname} certificate in NSS DB '
8cfcc8
+                                      '{dbdir} does not match entry in LDAP'))
8cfcc8
+            return all_ok
8cfcc8
+
8cfcc8
+        db = certs.CertDB(api.env.realm, paths.PKI_TOMCAT_ALIAS_DIR)
8cfcc8
+        dn = DN('uid=pkidbuser,ou=people,o=ipaca')
8cfcc8
+        subsystem_nick = 'subsystemCert cert-pki-ca'
8cfcc8
+        subsystem_ok = yield from match_ldap_nss_cert(self, self.conn,
8cfcc8
+                                                      db, dn,
8cfcc8
+                                                      'userCertificate',
8cfcc8
+                                                      subsystem_nick)
8cfcc8
+        dn = DN('cn=%s IPA CA' % api.env.realm,
8cfcc8
+                'cn=certificates,cn=ipa,cn=etc',
8cfcc8
+                api.env.basedn)
8cfcc8
+        casigning_nick = 'caSigningCert cert-pki-ca'
8cfcc8
+        casigning_ok = yield from match_ldap_nss_cert(self, self.conn,
8cfcc8
+                                                      db, dn, 'CACertificate',
8cfcc8
+                                                      casigning_nick)
8cfcc8
+
8cfcc8
+        expected_nicks_subjects = {
8cfcc8
+            'ocspSigningCert cert-pki-ca':
8cfcc8
+                'CN=OCSP Subsystem,O=%s' % api.env.realm,
8cfcc8
+            'subsystemCert cert-pki-ca':
8cfcc8
+                'CN=CA Subsystem,O=%s' % api.env.realm,
8cfcc8
+            'auditSigningCert cert-pki-ca':
8cfcc8
+                'CN=CA Audit,O=%s' % api.env.realm,
8cfcc8
+            'Server-Cert cert-pki-ca':
8cfcc8
+                'CN=%s,O=%s' % (api.env.host, api.env.realm),
8cfcc8
+        }
8cfcc8
+
8cfcc8
+        kra = krainstance.KRAInstance(api.env.realm)
8cfcc8
+        if kra.is_installed():
8cfcc8
+            kra_expected_nicks_subjects = {
8cfcc8
+                'transportCert cert-pki-kra':
8cfcc8
+                    'CN=KRA Transport Certificate,O=%s' % api.env.realm,
8cfcc8
+                'storageCert cert-pki-kra':
8cfcc8
+                    'CN=KRA Storage Certificate,O=%s' % api.env.realm,
8cfcc8
+                'auditSigningCert cert-pki-kra':
8cfcc8
+                    'CN=KRA Audit,O=%s' % api.env.realm,
8cfcc8
+            }
8cfcc8
+            expected_nicks_subjects.update(kra_expected_nicks_subjects)
8cfcc8
+
8cfcc8
+        ipaca_basedn = DN('ou=certificateRepository,ou=ca,o=ipaca')
8cfcc8
+        ipaca_certs_ok = yield from match_ldap_nss_certs_by_subject(
8cfcc8
+                                    self, self.conn, db,
8cfcc8
+                                    ipaca_basedn,
8cfcc8
+                                    expected_nicks_subjects
8cfcc8
+                                )
8cfcc8
+
8cfcc8
+        if subsystem_ok:
8cfcc8
+            yield Result(self, constants.SUCCESS,
8cfcc8
+                         key=subsystem_nick)
8cfcc8
+        if casigning_ok:
8cfcc8
+            yield Result(self, constants.SUCCESS,
8cfcc8
+                         key=casigning_nick)
8cfcc8
+        if ipaca_certs_ok:
8cfcc8
+            yield Result(self, constants.SUCCESS,
8cfcc8
+                         key=str(ipaca_basedn))
8cfcc8
+
8cfcc8
+
8cfcc8
 @registry
8cfcc8
 class IPANSSChainValidation(IPAPlugin):
8cfcc8
     """Validate the certificate chain of the certs."""
8cfcc8
-- 
8cfcc8
2.26.3
8cfcc8