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

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