diff --git a/freeipa.spec.in b/freeipa.spec.in index 3b0e4b201..ca8ef4a69 100644 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -597,6 +597,7 @@ Requires: python-pytest-multihost >= 0.5 Requires: python-pytest-sourceorder Requires: ldns-utils Requires: python-sssdconfig +Requires: python2-cryptography Provides: %{alt_name}-tests = %{version} Conflicts: %{alt_name}-tests @@ -630,6 +631,7 @@ Requires: python3-pytest-multihost >= 0.5 Requires: python3-pytest-sourceorder Requires: ldns-utils Requires: python3-sssdconfig +Requires: python3-cryptography %description -n python3-ipatests IPA is an integrated solution to provide centrally managed Identity (users, diff --git a/ipatests/test_xmlrpc/test_caacl_profile_enforcement.py b/ipatests/test_xmlrpc/test_caacl_profile_enforcement.py index a73e84539..a5cc3acd3 100644 --- a/ipatests/test_xmlrpc/test_caacl_profile_enforcement.py +++ b/ipatests/test_xmlrpc/test_caacl_profile_enforcement.py @@ -9,13 +9,22 @@ import tempfile import six +from cryptography import x509 +from cryptography.x509.oid import NameOID +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa + from ipalib import api, errors from ipatests.util import ( - prepare_config, unlock_principal_password, change_principal) + prepare_config, unlock_principal_password, change_principal, + host_keytab) from ipatests.test_xmlrpc.xmlrpc_test import XMLRPC_test from ipatests.test_xmlrpc.tracker.certprofile_plugin import CertprofileTracker from ipatests.test_xmlrpc.tracker.caacl_plugin import CAACLTracker from ipatests.test_xmlrpc.tracker.ca_plugin import CATracker +from ipatests.test_xmlrpc.tracker.host_plugin import HostTracker +from ipatests.test_xmlrpc.tracker.service_plugin import ServiceTracker from ipapython.ipautil import run @@ -29,7 +38,6 @@ SMIME_MOD_CONSTR_PROFILE_TEMPLATE = os.path.join(BASE_DIR, 'data/smime-mod.cfg.t CERT_OPENSSL_CONFIG_TEMPLATE = os.path.join(BASE_DIR, 'data/usercert.conf.tmpl') CERT_RSA_PRIVATE_KEY_PATH = os.path.join(BASE_DIR, 'data/usercert-priv-key.pem') - SMIME_USER_INIT_PW = u'Change123' SMIME_USER_PW = u'Secret123' @@ -354,3 +362,294 @@ class TestCertSignMIMEwithSubCA(XMLRPC_test): with change_principal(smime_user, SMIME_USER_PW): api.Command.cert_request(csr, principal=smime_user_principal, cacn=smime_signing_ca.name) + + +@pytest.fixture(scope='class') +def santest_subca(request): + name = u'default-profile-subca' + subject = u'CN={},O=test'.format(name) + tr = CATracker(name, subject) + return tr.make_fixture(request) + + +@pytest.fixture(scope='class') +def santest_subca_acl(request): + tr = CAACLTracker(u'default_profile_subca') + return tr.make_fixture(request) + + +@pytest.fixture(scope='class') +def santest_host_1(request): + tr = HostTracker(u'santest-host-1') + return tr.make_fixture(request) + + +@pytest.fixture(scope='class') +def santest_host_2(request): + tr = HostTracker(u'santest-host-2') + return tr.make_fixture(request) + + +@pytest.fixture(scope='class') +def santest_service_host_1(request, santest_host_1): + tr = ServiceTracker(u'srv', santest_host_1.name) + return tr.make_fixture(request) + + +@pytest.fixture(scope='class') +def santest_service_host_2(request, santest_host_2): + tr = ServiceTracker(u'srv', santest_host_2.name) + return tr.make_fixture(request) + + +@pytest.fixture +def santest_csr(request, santest_host_1, santest_host_2): + backend = default_backend() + pkey = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=backend + ) + + csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, santest_host_1.fqdn), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, api.env.realm) + ])).add_extension(x509.SubjectAlternativeName([ + x509.DNSName(santest_host_1.name), + x509.DNSName(santest_host_2.name) + ]), False + ).add_extension( + x509.BasicConstraints(ca=False, path_length=None), + True + ).add_extension( + x509.KeyUsage( + digital_signature=True, content_commitment=True, + key_encipherment=True, data_encipherment=False, + key_agreement=False, key_cert_sign=False, + crl_sign=False, encipher_only=False, + decipher_only=False + ), + False + ).sign( + pkey, hashes.SHA256(), backend + ).public_bytes(serialization.Encoding.PEM) + + return unicode(csr) + + +class CAACLEnforcementOnCertBase(XMLRPC_test): + """Base setup class for tests with SAN in CSR + + The class prepares an environment for test cases based + on evaluation of ACLs and fields requested in a CSR. + + The class creates following entries: + + * two host entries + * santest-host-1 + * santest-host-2 + * two service entries + * srv/santest-host-1 + * srv/santest-host-2 + * Sub CA + * default-profile-subca + + This one is created in order not to need + to re-import caIPAServiceCert profile + * CA ACL + * default_profile_subca + + After executing the methods the CA ACL should contain: + + CA ACL: + * santest-host-1 -- host + * srv/santest-host-1 -- service + * default-profile-subca -- CA + * caIPAServiceCert -- profile + """ + + def test_prepare_caacl_hosts(self, santest_subca_acl, + santest_host_1, santest_host_2): + santest_subca_acl.ensure_exists() + santest_host_1.ensure_exists() + santest_host_2.ensure_exists() + santest_subca_acl.add_host(santest_host_1.name) + + def test_prepare_caacl_CA(self, santest_subca_acl, santest_subca): + santest_subca.ensure_exists() + santest_subca_acl.add_ca(santest_subca.name) + + def test_prepare_caacl_profile(self, santest_subca_acl): + santest_subca_acl.add_profile(u'caIPAserviceCert') + + def test_prepare_caacl_services(self, santest_subca_acl, + santest_service_host_1, + santest_service_host_2): + santest_service_host_1.ensure_exists() + santest_service_host_2.ensure_exists() + + santest_subca_acl.add_service(santest_service_host_1.name) + + +@pytest.mark.tier1 +class TestSignCertificateWithInvalidSAN(CAACLEnforcementOnCertBase): + """Sign certificate request witn an invalid SAN entry + + Using the environment prepared by the base class, ask to sign + a certificate request for a service managed by one host only. + The CSR contains another domain name in SAN extension that should + be refused as the host does not have rights to manage the service. + """ + def test_request_cert_with_not_allowed_SAN( + self, santest_subca, santest_host_1, santest_host_2, + santest_service_host_1, santest_csr): + + with host_keytab(santest_host_1.name) as keytab_filename: + with change_principal(santest_host_1.attrs['krbcanonicalname'][0], + keytab=keytab_filename): + with pytest.raises(errors.ACIError): + api.Command.cert_request( + santest_csr, + principal=santest_service_host_1.name, + cacn=santest_subca.name + ) + + +@pytest.mark.tier1 +class TestSignServiceCertManagedByMultipleHosts(CAACLEnforcementOnCertBase): + """ Sign certificate request with multiple subject alternative names + + Using the environment of the base class, modify the service to be managed + by the second host. Then request a certificate for the service with SAN + of the second host in CSR. The certificate should be issued. + """ + def test_make_service_managed_by_each_host(self, + santest_host_1, + santest_service_host_1, + santest_host_2, + santest_service_host_2): + api.Command['service_add_host']( + santest_service_host_1.name, host=[santest_host_2.fqdn] + ) + api.Command['service_add_host']( + santest_service_host_2.name, host=[santest_host_1.fqdn] + ) + + def test_extend_the_ca_acl(self, santest_subca_acl, santest_host_2, + santest_service_host_2): + santest_subca_acl.add_host(santest_host_2.name) + santest_subca_acl.add_service(santest_service_host_2.name) + + def test_request_cert_with_additional_host( + self, santest_subca, santest_host_1, santest_host_2, + santest_service_host_1, santest_csr): + + with host_keytab(santest_host_1.name) as keytab_filename: + with change_principal(santest_host_1.attrs['krbcanonicalname'][0], + keytab=keytab_filename): + api.Command.cert_request( + santest_csr, + principal=santest_service_host_1.name, + cacn=santest_subca.name + ) + + +@pytest.mark.tier1 +class TestSignServiceCertWithoutSANServiceInACL(CAACLEnforcementOnCertBase): + """ Sign certificate request with multiple subject alternative names + + This test case doesn't have the service hosted on a host in SAN + in the CA ACL. The assumption is that the issuance will fail. + """ + def test_make_service_managed_by_each_host(self, + santest_host_1, + santest_service_host_1, + santest_host_2, + santest_service_host_2): + api.Command['service_add_host']( + santest_service_host_1.name, host=[santest_host_2.fqdn] + ) + api.Command['service_add_host']( + santest_service_host_2.name, host=[santest_host_1.fqdn] + ) + + def test_extend_the_ca_acl(self, santest_subca_acl, santest_host_2, + santest_service_host_2): + santest_subca_acl.add_host(santest_host_2.name) + + def test_request_cert_with_additional_host( + self, santest_subca, santest_host_1, santest_host_2, + santest_service_host_1, santest_csr): + + with host_keytab(santest_host_1.name) as keytab_filename: + with change_principal(santest_host_1.attrs['krbcanonicalname'][0], + keytab=keytab_filename): + with pytest.raises(errors.ACIError): + api.Command.cert_request( + santest_csr, + principal=santest_service_host_1.name, + cacn=santest_subca.name + ) + + +@pytest.mark.tier1 +class TestManagedByACIOnCertRequest(CAACLEnforcementOnCertBase): + """Test issuence of a certificate by external host + + The test verifies that the managed by attribute of a service + is enforced on certificate signing. + + The two test cases test the issuance of a service certificate + to a service by a second host. + + In one of them the service is not managed by the principal + requesting the certificate, thus the issuance should fail. + + The second one makes the service managed, thus the certificate + should be issued. + """ + def test_update_the_caacl(self, + santest_subca_acl, + santest_host_2, + santest_service_host_2): + santest_subca_acl.add_host(santest_host_2.name) + santest_subca_acl.add_service(santest_service_host_2.name) + + def test_issuing_service_cert_by_unrelated_host(self, + santest_subca, + santest_host_1, + santest_host_2, + santest_service_host_1, + santest_csr): + + with host_keytab(santest_host_2.name) as keytab_filename: + with change_principal(santest_host_2.attrs['krbcanonicalname'][0], + keytab=keytab_filename): + with pytest.raises(errors.ACIError): + api.Command.cert_request( + santest_csr, + principal=santest_service_host_1.name, + cacn=santest_subca.name + ) + + def test_issuing_service_cert_by_related_host(self, + santest_subca, + santest_host_1, + santest_host_2, + santest_service_host_1, + santest_csr): + # The test case alters the previous state by making + # the service managed by the second host. + # Then it attempts to request the certificate again + api.Command['service_add_host']( + santest_service_host_1.name, host=[santest_host_2.fqdn] + ) + + with host_keytab(santest_host_2.name) as keytab_filename: + with change_principal(santest_host_2.attrs['krbcanonicalname'][0], + keytab=keytab_filename): + api.Command.cert_request( + santest_csr, + principal=santest_service_host_1.name, + cacn=santest_subca.name + )