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

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