URL: https://github.com/freeipa/freeipa/pull/73 Author: apophys Title: #73: Tests for certificates with SAN Action: synchronized
To pull the PR as Git branch: git remote add ghfreeipa https://github.com/freeipa/freeipa git fetch ghfreeipa pull/73/head:pr73 git checkout pr73
From 7ef1437d1edca904ef6528ca3b9571e35351b8ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Kub=C3=ADk?= <mku...@redhat.com> Date: Mon, 12 Sep 2016 14:52:05 +0200 Subject: [PATCH 1/3] ipatests: provide context manager for keytab usage in RPC tests https://fedorahosted.org/freeipa/ticket/6366 --- ipatests/util.py | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/ipatests/util.py b/ipatests/util.py index 0b50f85..aed5cc5 100644 --- a/ipatests/util.py +++ b/ipatests/util.py @@ -40,7 +40,9 @@ from ipalib.plugable import Plugin from ipalib.request import context from ipapython.dn import DN -from ipapython.ipautil import private_ccache, kinit_password, run +from ipapython.ipautil import ( + private_ccache, kinit_password, kinit_keytab, run +) from ipaplatform.paths import paths if six.PY3: @@ -693,8 +695,28 @@ def unlock_principal_password(user, oldpw, newpw): @contextmanager -def change_principal(user, password, client=None, path=None, - canonicalize=False, enterprise=False): +def change_principal(principal, password=None, client=None, path=None, + canonicalize=False, enterprise=False, keytab=None): + """Temporarily change the kerberos principal + + Most of the test cases run with the admin ipa user which is granted + all access and exceptions from rules on some occasions. + + When the test needs to test for an application of some kind + of a restriction it needs to authenticate as a different principal + with required set of rights to the operation. + + The context manager changes the principal identity in two ways: + + * using password + * using keytab + + If the context manager is to be used with a keytab, the keytab + option must be its absolute path. + + The context manager can be used to authenticate with enterprise + principals and aliases when given respective options. + """ if path: ccache_name = path @@ -709,8 +731,12 @@ def change_principal(user, password, client=None, path=None, try: with private_ccache(ccache_name): - kinit_password(user, password, ccache_name, - canonicalize=canonicalize, enterprise=enterprise) + if keytab: + kinit_keytab(principal, keytab, ccache_name) + else: + kinit_password(principal, password, ccache_name, + canonicalize=canonicalize, + enterprise=enterprise) client.Backend.rpcclient.connect() try: @@ -720,6 +746,42 @@ def change_principal(user, password, client=None, path=None, finally: client.Backend.rpcclient.connect() + +@contextmanager +def get_entity_keytab(principal, options=None): + """Requests a keytab for an entity + + The keytab will generate new keys if not specified + otherwise in the options. + To retrieve existing keytab, use the -r option + """ + keytab_filename = os.path.join('/tmp', str(uuid.uuid4())) + + try: + cmd = [paths.IPA_GETKEYTAB, '-p', principal, '-k', keytab_filename] + + if options: + cmd.extend(options) + run(cmd) + + yield keytab_filename + finally: + os.remove(keytab_filename) + + +@contextmanager +def host_keytab(hostname, options=None): + """Retrieves keytab for a particular host + + After leaving the context manager, the keytab file is + deleted. + """ + principal = u'host/{}'.format(hostname) + + with get_entity_keytab(principal, options) as keytab: + yield keytab + + def get_group_dn(cn): return DN(('cn', cn), api.env.container_group, api.env.basedn) From 0b39203678b709da375740f9e78349f3903c8035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Kub=C3=ADk?= <mku...@redhat.com> Date: Mon, 12 Sep 2016 14:53:48 +0200 Subject: [PATCH 2/3] ipatests: Fix name property on a service tracker https://fedorahosted.org/freeipa/ticket/6366 --- ipatests/test_xmlrpc/tracker/service_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipatests/test_xmlrpc/tracker/service_plugin.py b/ipatests/test_xmlrpc/tracker/service_plugin.py index a0bb884..0a90115 100644 --- a/ipatests/test_xmlrpc/tracker/service_plugin.py +++ b/ipatests/test_xmlrpc/tracker/service_plugin.py @@ -52,7 +52,7 @@ class ServiceTracker(KerberosAliasMixin, Tracker): def __init__(self, name, host_fqdn, options=None): super(ServiceTracker, self).__init__(default_version=None) - self._name = "{0}/{1}@{2}".format(name, host_fqdn, api.env.realm) + self._name = u"{0}/{1}@{2}".format(name, host_fqdn, api.env.realm) self.dn = DN( ('krbprincipalname', self.name), api.env.container_service, api.env.basedn) From f7580414dbe85706e6fe3474416e79e14c5427b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Kub=C3=ADk?= <mku...@redhat.com> Date: Mon, 12 Sep 2016 14:54:40 +0200 Subject: [PATCH 3/3] ipatests: Implement tests with CSRs requesting SAN The patch implements several test cases testing the enforcement of CA ACLs on certificate requests with subject alternative names. https://fedorahosted.org/freeipa/ticket/6366 --- freeipa.spec.in | 2 + .../test_xmlrpc/test_caacl_profile_enforcement.py | 303 ++++++++++++++++++++- 2 files changed, 303 insertions(+), 2 deletions(-) diff --git a/freeipa.spec.in b/freeipa.spec.in index 3b0e4b2..ca8ef4a 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 a73e845..a5cc3ac 100644 --- a/ipatests/test_xmlrpc/test_caacl_profile_enforcement.py +++ b/ipatests/test_xmlrpc/test_caacl_profile_enforcement.py @@ -9,13 +9,22 @@ 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 @@ 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 @@ def test_sign_smime_csr_fallback_to_default_cert_profile( 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 + )
-- Manage your subscription for the Freeipa-devel mailing list: https://www.redhat.com/mailman/listinfo/freeipa-devel Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code