# # Copyright (C) 2020 FreeIPA Contributors see COPYING for license # """ Module provides tests for ipa-cert-fix CLI. """ from cryptography.hazmat.backends import default_backend from cryptography import x509 from datetime import datetime, date import pytest import time import logging from ipaplatform.paths import paths from ipapython.ipaldap import realm_to_serverid from ipatests.pytest_ipa.integration import tasks from ipatests.test_integration.base import IntegrationTest from ipatests.test_integration.test_caless import CALessBase, ipa_certs_cleanup from ipatests.test_integration.test_cert import get_certmonger_fs_id logger = logging.getLogger(__name__) def server_install_teardown(func): def wrapped(*args): master = args[0].master try: func(*args) finally: ipa_certs_cleanup(master) return wrapped def check_status(host, cert_count, state, timeout=600): """Helper method to check that if all the certs are in given state :param host: the host :param cert_count: no of cert to look for :param state: state to check for :param timeout: max time in seconds to wait for the state """ for _i in range(0, timeout, 10): result = host.run_command(['getcert', 'list']) count = result.stdout_text.count(f"status: {state}") logger.info("cert count in %s state : %s", state, count) if int(count) == cert_count: break time.sleep(10) else: raise RuntimeError("request timed out") return count def needs_resubmit(host, req_id): """Helper method to identify if cert request needs to be resubmitted :param host: the host :param req_id: request id to perform operation for Returns True if resubmit needed else False """ # check if cert is in monitoring state tasks.wait_for_certmonger_status( host, ('MONITORING'), req_id, timeout=600 ) # check if cert is valid and not expired cmd = host.run_command( 'getcert list -i {} | grep expires'.format(req_id) ) cert_expiry = cmd.stdout_text.split(' ') cert_expiry = datetime.strptime(cert_expiry[1], '%Y-%m-%d').date() if cert_expiry > date.today(): return False else: return True def get_cert_expiry(host, nssdb_path, cert_nick): """Method to get cert expiry date of given certificate :param host: the host :param nssdb_path: nssdb path of certificate :param cert_nick: certificate nick name for extracting cert from nssdb """ # get initial expiry date to compare later with renewed cert host.run_command([ 'certutil', '-L', '-a', '-d', nssdb_path, '-n', cert_nick, '-o', '/root/cert.pem' ]) data = host.get_file_contents('/root/cert.pem') cert = x509.load_pem_x509_certificate(data, backend=default_backend()) return cert.not_valid_after @pytest.fixture def expire_cert_critical(): """ Fixture to expire the certs by moving the system date using date -s command and revert it back """ hosts = dict() def _expire_cert_critical(host, setup_kra=False): hosts['host'] = host # Do not install NTP as the test plays with the date tasks.install_master(host, setup_dns=False, extra_args=['--no-ntp']) if setup_kra: tasks.install_kra(host) # move date to expire certs tasks.move_date(host, 'stop', '+3Years+1day') yield _expire_cert_critical host = hosts.pop('host') tasks.uninstall_master(host) tasks.move_date(host, 'start', '-3Years-1day') class TestIpaCertFix(IntegrationTest): @classmethod def uninstall(cls, mh): # Uninstall method is empty as the uninstallation is done in # the fixture pass @pytest.fixture def expire_ca_cert(self): tasks.install_master(self.master, setup_dns=False, extra_args=['--no-ntp']) tasks.move_date(self.master, 'stop', '+20Years+1day') yield tasks.uninstall_master(self.master) tasks.move_date(self.master, 'start', '-20Years-1day') def test_missing_csr(self, expire_cert_critical): """ Test that ipa-cert-fix succeeds when CSR is missing from CS.cfg Test case for https://pagure.io/freeipa/issue/8618 Scenario: - move the date so that ServerCert cert-pki-ca is expired - remove the ca.sslserver.certreq directive from CS.cfg - call getcert resubmit in order to create the CSR in certmonger file - use ipa-cert-fix, no issue should be seen """ expire_cert_critical(self.master) # pki must be stopped in order to edit CS.cfg self.master.run_command(['ipactl', 'stop']) self.master.run_command(['sed', '-i', r'/ca\.sslserver\.certreq=/d', paths.CA_CS_CFG_PATH]) # dirsrv needs to be up in order to run ipa-cert-fix self.master.run_command(['ipactl', 'start', '--ignore-service-failures']) # It's the call to getcert resubmit that creates the CSR in certmonger. # In normal operations it would be launched automatically when the # expiration date is near but in the test we force the CSR creation. self.master.run_command(['getcert', 'resubmit', '-n', 'Server-Cert cert-pki-ca', '-d', paths.PKI_TOMCAT_ALIAS_DIR]) # Wait a few secs time.sleep(3) # Now the real test, call ipa-cert-fix and ensure it doesn't # complain about missing sslserver.crt result = self.master.run_command(['ipa-cert-fix', '-v'], stdin_text='yes\n', raiseonerr=False) msg = ("No such file or directory: " "'/etc/pki/pki-tomcat/certs/sslserver.crt'") assert msg not in result.stderr_text # Because of BZ 1897120, pki-cert-fix fails on pki-core 10.10.0 # https://bugzilla.redhat.com/show_bug.cgi?id=1897120 if (tasks.get_pki_version(self.master) != tasks.parse_version('10.10.0')): assert result.returncode == 0 # get the number of certs track by certmonger cmd = self.master.run_command(['getcert', 'list']) certs = cmd.stdout_text.count('Request ID') timeout = 600 renewed = 0 start = time.time() # wait up to 10 min for all certs to renew while time.time() - start < timeout: cmd = self.master.run_command(['getcert', 'list']) renewed = cmd.stdout_text.count('status: MONITORING') if renewed == certs: break time.sleep(100) else: # timeout raise AssertionError('Timeout: Failed to renew all the certs') def test_renew_expired_cert_on_master(self, expire_cert_critical): """Test if ipa-cert-fix renews expired certs Test moves system date to expire certs. Then calls ipa-cert-fix to renew them. This certs include subsystem, audit-signing, OCSP signing, Dogtag HTTPS, IPA RA agent, LDAP and KDC certs. related: https://pagure.io/freeipa/issue/7885 """ expire_cert_critical(self.master) # wait for cert expiry check_status(self.master, 8, "CA_UNREACHABLE") self.master.run_command(['ipa-cert-fix', '-v'], stdin_text='yes\n') check_status(self.master, 9, "MONITORING") # second iteration of ipa-cert-fix result = self.master.run_command( ['ipa-cert-fix', '-v'], stdin_text='yes\n' ) assert "Nothing to do" in result.stdout_text check_status(self.master, 9, "MONITORING") def test_ipa_cert_fix_non_ipa(self): """Test ipa-cert-fix doesn't work on non ipa system ipa-cert-fix tool should not work on non ipa system. related: https://pagure.io/freeipa/issue/7885 """ result = self.master.run_command(['ipa-cert-fix', '-v'], stdin_text='yes\n', raiseonerr=False) assert result.returncode == 2 def test_missing_startup(self, expire_cert_critical): """ Test ipa-cert-fix fails/warns when startup directive is missing This test checks that if 'selftests.container.order.startup' directive is missing from CS.cfg, ipa-cert-fix fails and throw proper error message. It also checks that underlying command 'pki-server cert-fix' should fail to renew the cert. related: https://pagure.io/freeipa/issue/8721 With https://github.com/dogtagpki/pki/pull/3466, it changed to display a warning than failing. This test also checks that if 'selftests.container.order.startup' directive is missing from CS.cfg, ipa-cert-fix dsplay proper warning (depending on pki version) related: https://pagure.io/freeipa/issue/8890 """ expire_cert_critical(self.master) # pki must be stopped in order to edit CS.cfg self.master.run_command(['ipactl', 'stop']) self.master.run_command([ 'sed', '-i', r'/selftests\.container\.order\.startup/d', paths.CA_CS_CFG_PATH ]) # dirsrv needs to be up in order to run ipa-cert-fix self.master.run_command(['ipactl', 'start', '--ignore-service-failures']) result = self.master.run_command(['ipa-cert-fix', '-v'], stdin_text='yes\n', raiseonerr=False) err_msg1 = "ERROR: 'selftests.container.order.startup'" # check that pki-server cert-fix command fails err_msg2 = ("ERROR: CalledProcessError(Command " "['pki-server', 'cert-fix'") warn_msg = "WARNING: No selftests configured in" if (tasks.get_pki_version(self.master) < tasks.parse_version('10.11.0')): assert (err_msg1 in result.stderr_text and err_msg2 in result.stderr_text) else: assert warn_msg in result.stderr_text def test_expired_CA_cert(self, expire_ca_cert): """Test to check ipa-cert-fix when CA certificate is expired In order to fix expired certs using ipa-cert-fix, CA cert should be valid. If CA cert expired, ipa-cert-fix won't work. related: https://pagure.io/freeipa/issue/8721 """ result = self.master.run_command(['ipa-cert-fix', '-v'], stdin_text='yes\n', raiseonerr=False) # check that pki-server cert-fix command fails err_msg = ("ERROR: CalledProcessError(Command " "['pki-server', 'cert-fix'") assert err_msg in result.stderr_text class TestIpaCertFixThirdParty(CALessBase): """ Test that ipa-cert-fix works with an installation with custom certs. """ @classmethod def install(cls, mh): cls.nickname = 'ca1/server' super(TestIpaCertFixThirdParty, cls).install(mh) tasks.install_master(cls.master, setup_dns=True) @server_install_teardown def test_third_party_certs(self): self.create_pkcs12(self.nickname, password=self.cert_password, filename='server.p12') self.prepare_cacert('ca1') # We have a chain length of one. If this is extended then the # additional cert names will need to be calculated. nick_chain = self.nickname.split('/') ca_cert = '%s.crt' % nick_chain[0] # Add the CA to the IPA store self.copy_cert(self.master, ca_cert) self.master.run_command(['ipa-cacert-manage', 'install', ca_cert]) # Apply the new cert chain otherwise ipa-server-certinstall will fail self.master.run_command(['ipa-certupdate']) # Install the updated certs and restart the world self.copy_cert(self.master, 'server.p12') args = ['ipa-server-certinstall', '-p', self.master.config.dirman_password, '--pin', self.master.config.admin_password, '-d', 'server.p12'] self.master.run_command(args) self.master.run_command(['ipactl', 'restart']) # Run ipa-cert-fix. This is basically a no-op but tests that # the DS nickname is used and not a hardcoded value. result = self.master.run_command(['ipa-cert-fix', '-v'],) assert self.nickname in result.stderr_text class TestCertFixKRA(IntegrationTest): @classmethod def uninstall(cls, mh): # Uninstall method is empty as the uninstallation is done in # the fixture pass def test_renew_expired_cert_with_kra(self, expire_cert_critical): """Test if ipa-cert-fix renews expired certs with kra installed This test check if ipa-cert-fix renews certs with kra certificate installed. related: https://pagure.io/freeipa/issue/7885 """ expire_cert_critical(self.master, setup_kra=True) # check if all subsystem cert expired check_status(self.master, 11, "CA_UNREACHABLE") self.master.run_command(['ipa-cert-fix', '-v'], stdin_text='yes\n') check_status(self.master, 12, "MONITORING") class TestCertFixReplica(IntegrationTest): num_replicas = 1 @classmethod def install(cls, mh): tasks.install_master( mh.master, setup_dns=False, extra_args=['--no-ntp'] ) tasks.install_replica( mh.master, mh.replicas[0], setup_dns=False, extra_args=['--no-ntp'] ) @pytest.fixture def expire_certs(self): # move system date to expire certs for host in self.master, self.replicas[0]: tasks.move_date(host, 'stop', '+3years+1days') yield # move date back on replica and master for host in self.master, self.replicas[0]: tasks.move_date(host, 'start', '-3years-1days') def test_renew_expired_cert_replica(self, expire_certs): """Test renewal of certificates on replica with ipa-cert-fix This is to check that ipa-cert-fix renews the certificates on replica related: https://pagure.io/freeipa/issue/7885 """ # wait for cert expiry check_status(self.master, 8, "CA_UNREACHABLE") self.master.run_command(['ipa-cert-fix', '-v'], stdin_text='yes\n') check_status(self.master, 9, "MONITORING") # replica operations # 'Server-Cert cert-pki-ca' cert will be in CA_UNREACHABLE state cmd = self.replicas[0].run_command( ['getcert', 'list', '-d', paths.PKI_TOMCAT_ALIAS_DIR, '-n', 'Server-Cert cert-pki-ca'] ) req_id = get_certmonger_fs_id(cmd.stdout_text) tasks.wait_for_certmonger_status( self.replicas[0], ('CA_UNREACHABLE'), req_id, timeout=600 ) # get initial expiry date to compare later with renewed cert initial_expiry = get_cert_expiry( self.replicas[0], paths.PKI_TOMCAT_ALIAS_DIR, 'Server-Cert cert-pki-ca' ) # check that HTTP,LDAP,PKINIT are renewed and in MONITORING state instance = realm_to_serverid(self.master.domain.realm) dirsrv_cert = paths.ETC_DIRSRV_SLAPD_INSTANCE_TEMPLATE % instance for cert in (paths.KDC_CERT, paths.HTTPD_CERT_FILE): cmd = self.replicas[0].run_command( ['getcert', 'list', '-f', cert] ) req_id = get_certmonger_fs_id(cmd.stdout_text) tasks.wait_for_certmonger_status( self.replicas[0], ('MONITORING'), req_id, timeout=600 ) cmd = self.replicas[0].run_command( ['getcert', 'list', '-d', dirsrv_cert] ) req_id = get_certmonger_fs_id(cmd.stdout_text) tasks.wait_for_certmonger_status( self.replicas[0], ('MONITORING'), req_id, timeout=600 ) # check if replication working fine testuser = 'testuser1' password = 'Secret@123' stdin = (f"{self.master.config.admin_password}\n" f"{self.master.config.admin_password}\n" f"{self.master.config.admin_password}\n") self.master.run_command(['kinit', 'admin'], stdin_text=stdin) tasks.user_add(self.master, testuser, password=password) self.replicas[0].run_command(['kinit', 'admin'], stdin_text=stdin) self.replicas[0].run_command(['ipa', 'user-show', testuser]) # renew shared certificates by resubmitting to certmonger cmd = self.replicas[0].run_command( ['getcert', 'list', '-f', paths.RA_AGENT_PEM] ) req_id = get_certmonger_fs_id(cmd.stdout_text) if needs_resubmit(self.replicas[0], req_id): self.replicas[0].run_command( ['getcert', 'resubmit', '-i', req_id] ) tasks.wait_for_certmonger_status( self.replicas[0], ('MONITORING'), req_id, timeout=600 ) for cert_nick in ('auditSigningCert cert-pki-ca', 'ocspSigningCert cert-pki-ca', 'subsystemCert cert-pki-ca'): cmd = self.replicas[0].run_command( ['getcert', 'list', '-d', paths.PKI_TOMCAT_ALIAS_DIR, '-n', cert_nick] ) req_id = get_certmonger_fs_id(cmd.stdout_text) if needs_resubmit(self.replicas[0], req_id): self.replicas[0].run_command( ['getcert', 'resubmit', '-i', req_id] ) tasks.wait_for_certmonger_status( self.replicas[0], ('MONITORING'), req_id, timeout=600 ) self.replicas[0].run_command( ['ipa-cert-fix', '-v'], stdin_text='yes\n' ) check_status(self.replicas[0], 9, "MONITORING") # Sometimes certmonger takes time to update the cert status # So check in nssdb instead of relying on getcert command renewed_expiry = get_cert_expiry( self.replicas[0], paths.PKI_TOMCAT_ALIAS_DIR, 'Server-Cert cert-pki-ca' ) assert renewed_expiry > initial_expiry