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

Reply via email to