# # Copyright (C) 2017 FreeIPA Contributors see COPYING for license # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from __future__ import absolute_import import os import re import time import tempfile from cryptography import x509 from cryptography.hazmat.backends import default_backend from ipatests.pytest_ipa.integration import tasks from ipatests.test_integration.base import IntegrationTest from ipalib import x509 as ipa_x509 from ipaplatform.paths import paths from ipapython.dn import DN from itertools import chain, repeat from ipatests.create_external_ca import ExternalCA, ISSUER_CN IPA_CA = 'ipa_ca.crt' ROOT_CA = 'root_ca.crt' # string to identify PKI restart in the journal PKI_START_STR = 'Started pki_tomcatd' def check_CA_flag(host, nssdb=paths.PKI_TOMCAT_ALIAS_DIR, cn=ISSUER_CN): """ Check if external CA (by default 'example.test' in our test env) has CA flag in nssdb. """ result = host.run_command(['certutil', '-L', '-d', nssdb]) text = result.stdout_text # match CN in cert nickname and C flag in SSL section of NSS flags table match_CA_flag = re.compile(r'.*{}.*\s+C'.format(cn)) match = re.search(match_CA_flag, text) return match def match_in_journal(host, string, since='today', services=('certmonger',)): """ Returns match object for the particular string. """ # prepend '-u' before every service name service_args = list(chain.from_iterable(list(zip(repeat('-u'), services)))) command_args = ['journalctl', '--since={}'.format(since)] + service_args result = host.run_command(command_args) output = result.stdout_text traceback = re.compile(string) match = re.search(traceback, output) return match def install_server_external_ca_step1(host, extra_args=(), raiseonerr=True): """Step 1 to install the ipa server with external ca""" return tasks.install_master( host, external_ca=True, extra_args=extra_args, raiseonerr=raiseonerr, ) def install_server_external_ca_step2(host, ipa_ca_cert, root_ca_cert, raiseonerr=True): """Step 2 to install the ipa server with external ca""" args = ['ipa-server-install', '-U', '-r', host.domain.realm, '-a', host.config.admin_password, '-p', host.config.dirman_password, '--external-cert-file', ipa_ca_cert, '--external-cert-file', root_ca_cert] cmd = host.run_command(args, raiseonerr=raiseonerr) return cmd def service_control_dirsrv(host, function): """Function to control the dirsrv service i.e start, stop, restart etc""" dashed_domain = host.domain.realm.replace(".", '-') dirsrv_service = "dirsrv@%s.service" % dashed_domain cmd = host.run_command(['systemctl', function, dirsrv_service]) assert cmd.returncode == 0 def check_ipaca_issuerDN(host, expected_dn): result = host.run_command(['ipa', 'ca-show', 'ipa']) assert "Issuer DN: {}".format(expected_dn) in result.stdout_text def check_mscs_extension(ipa_csr, template): csr = x509.load_pem_x509_csr(ipa_csr, default_backend()) extensions = [ ext for ext in csr.extensions if ext.oid.dotted_string == template.ext_oid ] assert extensions assert extensions[0].value.value == template.get_ext_data() class TestExternalCA(IntegrationTest): """ Test of FreeIPA server installation with external CA """ num_replicas = 1 num_clients = 1 def test_external_ca(self): # Step 1 of ipa-server-install. result = install_server_external_ca_step1( self.master, extra_args=['--external-ca-type=ms-cs'] ) assert result.returncode == 0 # check CSR for extension ipa_csr = self.master.get_file_contents(paths.ROOT_IPA_CSR) check_mscs_extension(ipa_csr, ipa_x509.MSCSTemplateV1(u'SubCA')) # Sign CA, transport it to the host and get ipa a root ca paths. root_ca_fname, ipa_ca_fname = tasks.sign_ca_and_transport( self.master, paths.ROOT_IPA_CSR, ROOT_CA, IPA_CA) # Step 2 of ipa-server-install. result = install_server_external_ca_step2( self.master, ipa_ca_fname, root_ca_fname) assert result.returncode == 0 # Make sure IPA server is working properly tasks.kinit_admin(self.master) result = self.master.run_command(['ipa', 'user-show', 'admin']) assert 'User login: admin' in result.stdout_text # check that we can also install replica tasks.install_replica(self.master, self.replicas[0]) # check that nsds5ReplicaReleaseTimeout option was set result = tasks.ldapsearch_dm( self.master, 'cn=mapping tree,cn=config', ['(cn=replica)'], ) # case insensitive match text = result.stdout_text.lower() # see ipaserver.install.replication.REPLICA_FINAL_SETTINGS assert 'nsds5ReplicaReleaseTimeout: 60'.lower() in text assert 'nsDS5ReplicaBindDnGroupCheckInterval: 60'.lower() in text def test_client_installation_with_otp(self): # Test for issue 7526: client installation fails with one-time # password when the master is installed with an externally signed # CA because the whole cert chain is not published in # /usr/share/ipa/html/ca.crt # Create a random password for the client client = self.clients[0] client_pwd = 'Secret123' args = ['ipa', 'host-add', client.hostname, '--ip-address', client.ip, '--no-reverse', '--password', client_pwd] self.master.run_command(args) # Enroll the client with the client_pwd client.run_command( ['ipa-client-install', '--domain', self.master.domain.name, '--server', self.master.hostname, '-w', client_pwd, '-U']) class TestExternalCAConstraints(IntegrationTest): """Test of FreeIPA server installation with external CA and constraints """ num_replicas = 0 num_clients = 1 def test_external_ca_constrained(self): install_server_external_ca_step1(self.master) # name constraints for IPA DNS domain (dot prefix) nameconstraint = x509.NameConstraints( permitted_subtrees=[ x509.DNSName("." + self.master.domain.name), ], excluded_subtrees=None ) root_ca_fname, ipa_ca_fname = tasks.sign_ca_and_transport( self.master, paths.ROOT_IPA_CSR, ROOT_CA, IPA_CA, root_ca_extensions=[nameconstraint], ) install_server_external_ca_step2( self.master, ipa_ca_fname, root_ca_fname ) tasks.kinit_admin(self.master) self.master.run_command(['ipa', 'ping']) def verify_caentry(host, cert): """ Verify the content of cn=DOMAIN IPA CA,cn=certificates,cn=ipa,cn=etc,basedn and make sure that ipaConfigString contains the expected values. Verify the content of cn=cacert,cn=certificates,cn=ipa,cn=etc,basedn and make sure that it contains the expected certificate. """ # Check the LDAP entry ldap = host.ldap_connect() # cn=DOMAIN IPA CA must contain ipaConfigString: ipaCa, compatCA ca_nick = '{} IPA CA'.format(host.domain.realm) entry = ldap.get_entry(DN(('cn', ca_nick), ('cn', 'certificates'), ('cn', 'ipa'), ('cn', 'etc'), host.domain.basedn)) ipaconfigstring = [x.lower() for x in entry.get('ipaconfigstring')] expected = ['compatca', 'ipaca'] assert expected == sorted(ipaconfigstring) # cn=cacert,cn=certificates,cn=etc,basedn must contain the latest # IPA CA entry2 = ldap.get_entry(DN(('cn', 'CACert'), ('cn', 'ipa'), ('cn', 'etc'), host.domain.basedn)) cert_from_ldap = entry2.single_value['cACertificate'] assert cert == cert_from_ldap class TestSelfExternalSelf(IntegrationTest): """ Test self-signed > external CA > self-signed test case. """ def test_install_master(self): result = tasks.install_master(self.master) assert result.returncode == 0 # Check the content of the ldap entries for the CA remote_cacrt = self.master.get_file_contents(paths.IPA_CA_CRT) cacrt = ipa_x509.load_pem_x509_certificate(remote_cacrt) verify_caentry(self.master, cacrt) def test_switch_to_external_ca(self): result = self.master.run_command([paths.IPA_CACERT_MANAGE, 'renew', '--external-ca']) assert result.returncode == 0 # Sign CA, transport it to the host and get ipa a root ca paths. root_ca_fname, ipa_ca_fname = tasks.sign_ca_and_transport( self.master, paths.IPA_CA_CSR, ROOT_CA, IPA_CA) # renew CA with externally signed one result = self.master.run_command([paths.IPA_CACERT_MANAGE, 'renew', '--external-cert-file={}'. format(ipa_ca_fname), '--external-cert-file={}'. format(root_ca_fname)]) assert result.returncode == 0 # update IPA certificate databases result = self.master.run_command([paths.IPA_CERTUPDATE]) assert result.returncode == 0 # Check if external CA have "C" flag after the switch result = check_CA_flag(self.master) assert bool(result), ('External CA does not have "C" flag') # Check that ldap entries for the CA have been updated remote_cacrt = self.master.get_file_contents(ipa_ca_fname) cacrt = ipa_x509.load_pem_x509_certificate(remote_cacrt) verify_caentry(self.master, cacrt) def test_issuerDN_after_renew_to_external(self): """ Check if issuer DN is updated after self-signed > external-ca This test checks if issuer DN is updated properly after CA is renewed from self-signed to external-ca """ check_ipaca_issuerDN(self.master, "CN={}".format(ISSUER_CN)) def test_switch_back_to_self_signed(self): # for journalctl --since switch_time = time.strftime('%Y-%m-%d %H:%M:%S') # switch back to self-signed CA result = self.master.run_command([paths.IPA_CACERT_MANAGE, 'renew', '--self-signed']) assert result.returncode == 0 # Confirm there is no traceback in the journal result = match_in_journal(self.master, since=switch_time, string='Traceback') assert not bool(result), ('"Traceback" keyword found in the journal.' 'Please check further') # Check if pki-tomcatd was started after switching back. result = match_in_journal(self.master, since=switch_time, string=PKI_START_STR) assert bool(result), ('pki_tomcatd not started after switching back to' 'self-signed CA') result = self.master.run_command([paths.IPA_CERTUPDATE]) assert result.returncode == 0 def test_issuerDN_after_renew_to_self_signed(self): """ Check if issuer DN is updated after external-ca > self-signed This test checks if issuer DN is updated properly after CA is renewed back from external-ca to self-signed """ issuer_dn = 'CN=Certificate Authority,O={}'.format( self.master.domain.realm) check_ipaca_issuerDN(self.master, issuer_dn) class TestExternalCAdirsrvStop(IntegrationTest): """When the dirsrv service, which gets started during the first ipa-server-install --external-ca phase, is not running when the second phase is run with --external-cert-file options, the ipa-server-install command fail. This test checks if second phase installs successfully when dirsrv is stoped. related ticket: https://pagure.io/freeipa/issue/6611""" def test_external_ca_dirsrv_stop(self): # Step 1 of ipa-server-install result = install_server_external_ca_step1(self.master) assert result.returncode == 0 # stop dirsrv server. service_control_dirsrv(self.master, 'stop') # Sign CA, transport it to the host and get ipa and root ca paths. root_ca_fname, ipa_ca_fname = tasks.sign_ca_and_transport( self.master, paths.ROOT_IPA_CSR, ROOT_CA, IPA_CA) # Step 2 of ipa-server-install. result = install_server_external_ca_step2( self.master, ipa_ca_fname, root_ca_fname) assert result.returncode == 0 # Make sure IPA server is working properly tasks.kinit_admin(self.master) result = self.master.run_command(['ipa', 'user-show', 'admin']) assert 'User login: admin' in result.stdout_text class TestExternalCAInvalidCert(IntegrationTest): """Manual renew external CA cert with invalid file""" def test_external_ca(self): # Step 1 of ipa-server-install. install_server_external_ca_step1(self.master) # Sign CA, transport it to the host and get ipa a root ca paths. root_ca_fname, ipa_ca_fname = tasks.sign_ca_and_transport( self.master, paths.ROOT_IPA_CSR, ROOT_CA, IPA_CA) # Step 2 of ipa-server-install. install_server_external_ca_step2(self.master, ipa_ca_fname, root_ca_fname) self.master.run_command([paths.IPA_CACERT_MANAGE, 'renew', '--external-ca']) result = self.master.run_command(['grep', '-v', 'CERTIFICATE', ipa_ca_fname]) contents = result.stdout_text BAD_CERT = 'bad_ca.crt' invalid_cert = os.path.join(self.master.config.test_dir, BAD_CERT) self.master.put_file_contents(invalid_cert, contents) # Sign CA, transport it to the host and get ipa a root ca paths. root_ca_fname, ipa_ca_fname = tasks.sign_ca_and_transport( self.master, paths.IPA_CA_CSR, ROOT_CA, IPA_CA) # renew CA with invalid cert cmd = [paths.IPA_CACERT_MANAGE, 'renew', '--external-cert-file', invalid_cert, '--external-cert-file', root_ca_fname] result = self.master.run_command(cmd, raiseonerr=False) assert result.returncode == 1 def test_external_ca_with_too_small_key(self): # reuse the existing deployment and renewal CSR root_ca_fname, ipa_ca_fname = tasks.sign_ca_and_transport( self.master, paths.IPA_CA_CSR, ROOT_CA, IPA_CA, key_size=1024) cmd = [ paths.IPA_CACERT_MANAGE, 'renew', '--external-cert-file', ipa_ca_fname, '--external-cert-file', root_ca_fname, ] result = self.master.run_command(cmd, raiseonerr=False) assert result.returncode == 1 class TestExternalCAInvalidIntermediate(IntegrationTest): """Test case for https://pagure.io/freeipa/issue/7877""" def test_invalid_intermediate(self): install_server_external_ca_step1(self.master) root_ca_fname, ipa_ca_fname = tasks.sign_ca_and_transport( self.master, paths.ROOT_IPA_CSR, ROOT_CA, IPA_CA, root_ca_path_length=0 ) result = install_server_external_ca_step2( self.master, ipa_ca_fname, root_ca_fname, raiseonerr=False ) assert result.returncode > 0 assert "basic contraint pathlen" in result.stderr_text class TestExternalCAInstall(IntegrationTest): """install CA cert manually """ def test_install_master(self): # step 1 install ipa-server tasks.install_master(self.master) def test_install_external_ca(self): # Create root CA external_ca = ExternalCA() # Create root CA root_ca = external_ca.create_ca() root_ca_fname = os.path.join(self.master.config.test_dir, ROOT_CA) # Transport certificates (string > file) to master self.master.put_file_contents(root_ca_fname, root_ca) # Install new cert self.master.run_command([paths.IPA_CACERT_MANAGE, 'install', root_ca_fname]) class TestMultipleExternalCA(IntegrationTest): """Setup externally signed ca1 install ipa-server with externally signed ca1 Setup externally signed ca2 and renew ipa-server with externally signed ca2 and check the difference in certificate """ def test_master_install_ca1(self): install_server_external_ca_step1(self.master) # Sign CA, transport it to the host and get ipa a root ca paths. root_ca_fname1 = tempfile.mkdtemp(suffix='root_ca.crt', dir=paths.TMP) ipa_ca_fname1 = tempfile.mkdtemp(suffix='ipa_ca.crt', dir=paths.TMP) ipa_csr = self.master.get_file_contents(paths.ROOT_IPA_CSR) external_ca = ExternalCA() root_ca = external_ca.create_ca(cn='RootCA1') ipa_ca = external_ca.sign_csr(ipa_csr) self.master.put_file_contents(root_ca_fname1, root_ca) self.master.put_file_contents(ipa_ca_fname1, ipa_ca) # Step 2 of ipa-server-install. install_server_external_ca_step2(self.master, ipa_ca_fname1, root_ca_fname1) cert_nick = "caSigningCert cert-pki-ca" result = self.master.run_command([ 'certutil', '-L', '-d', paths.PKI_TOMCAT_ALIAS_DIR, '-n', cert_nick]) assert "CN=RootCA1" in result.stdout_text def test_master_install_ca2(self): root_ca_fname2 = tempfile.mkdtemp(suffix='root_ca.crt', dir=paths.TMP) ipa_ca_fname2 = tempfile.mkdtemp(suffix='ipa_ca.crt', dir=paths.TMP) self.master.run_command([ paths.IPA_CACERT_MANAGE, 'renew', '--external-ca']) ipa_csr = self.master.get_file_contents(paths.IPA_CA_CSR) external_ca = ExternalCA() root_ca = external_ca.create_ca(cn='RootCA2') ipa_ca = external_ca.sign_csr(ipa_csr) self.master.put_file_contents(root_ca_fname2, root_ca) self.master.put_file_contents(ipa_ca_fname2, ipa_ca) # Step 2 of ipa-server-install. self.master.run_command([paths.IPA_CACERT_MANAGE, 'renew', '--external-cert-file', ipa_ca_fname2, '--external-cert-file', root_ca_fname2]) cert_nick = "caSigningCert cert-pki-ca" result = self.master.run_command([ 'certutil', '-L', '-d', paths.PKI_TOMCAT_ALIAS_DIR, '-n', cert_nick]) assert "CN=RootCA2" in result.stdout_text def _step1_profile(master, s): return install_server_external_ca_step1( master, extra_args=['--external-ca-type=ms-cs', f'--external-ca-profile={s}'], raiseonerr=False, ) def _test_invalid_profile(master, profile): result = _step1_profile(master, profile) assert result.returncode != 0 assert '--external-ca-profile' in result.stderr_text def _test_valid_profile(master, profile_cls, profile): result = _step1_profile(master, profile) assert result.returncode == 0 ipa_csr = master.get_file_contents(paths.ROOT_IPA_CSR) check_mscs_extension(ipa_csr, profile_cls(profile)) class TestExternalCAProfileScenarios(IntegrationTest): """ Test the various --external-ca-profile scenarios. This test is broken into sections, with each section first testing invalid arguments, then a valid argument, and finally uninstalling the half-installed IPA. """ ''' Tranche 1: version 1 templates. Test that --external-ca-profile=Foo gets propagated to the CSR. The default template extension when --external-ca-type=ms-cs, a V1 extension with value "SubCA", already gets tested by the ``TestExternalCA`` class. We only need to do Step 1 of installation, then check the CSR. ''' def test_invalid_v1_template(self): _test_invalid_profile(self.master, 'NotAnOid:1') def test_valid_v1_template(self): _test_valid_profile( self.master, ipa_x509.MSCSTemplateV1, 'TemplateOfAwesome') def test_uninstall_1(self): tasks.uninstall_master(self.master) ''' Tranche 2: V2 templates without minor version. Test that V2 template specifiers without minor version get propagated to CSR. This class also tests all error modes in specifying a V2 template, those being: - no major version specified - too many parts specified (i.e. major, minor, and then some more) - major version is not an int - major version is negative - minor version is not an int - minor version is negative We only need to do Step 1 of installation, then check the CSR. ''' def test_v2_template_too_few_parts(self): _test_invalid_profile(self.master, '1.2.3.4') def test_v2_template_too_many_parts(self): _test_invalid_profile(self.master, '1.2.3.4:100:200:300') def test_v2_template_major_version_not_int(self): _test_invalid_profile(self.master, '1.2.3.4:wat:200') def test_v2_template_major_version_negative(self): _test_invalid_profile(self.master, '1.2.3.4:-1:200') def test_v2_template_minor_version_not_int(self): _test_invalid_profile(self.master, '1.2.3.4:100:wat') def test_v2_template_minor_version_negative(self): _test_invalid_profile(self.master, '1.2.3.4:100:-2') def test_v2_template_valid_major_only(self): _test_valid_profile( self.master, ipa_x509.MSCSTemplateV2, '1.2.3.4:100') def test_uninstall_2(self): tasks.uninstall_master(self.master) ''' Tranche 3: V2 templates with minor version. Test that V2 template specifiers _with_ minor version get propagated to CSR. All error modes of V2 template specifiers were tested in ``TestExternalCAProfileV2Major``. We only need to do Step 1 of installation, then check the CSR. ''' def test_v2_template_valid_major_minor(self): _test_valid_profile( self.master, ipa_x509.MSCSTemplateV2, '1.2.3.4:100:200') # this is the end; no need to uninstall.