From 5af45595bb113cd39a40ae647dae252befe0aadf Mon Sep 17 00:00:00 2001 From: dane-fichter Date: Wed, 8 Feb 2017 17:29:53 -0800 Subject: [PATCH] Add image signing scenario This change adds the first scenario test to the Barbican Tempest plugin. This scenatio tests Nova and Glance's image signature verification functionality. Depends-On: Ifdf8b426c21e4b3a51f97cbc3d95eb842eb04515 Change-Id: Id9629ecbbc75e19eec81f60daec7b0a085bcdc12 --- barbican_tempest_plugin/clients.py | 14 +- .../tests/scenario/barbican_manager.py | 158 ++++++++++++++++++ .../tests/scenario/test_image_signing.py | 83 +++++++++ tools/pre_test_hook.sh | 9 + tox.ini | 1 - 5 files changed, 261 insertions(+), 4 deletions(-) create mode 100644 barbican_tempest_plugin/tests/scenario/barbican_manager.py create mode 100644 barbican_tempest_plugin/tests/scenario/test_image_signing.py diff --git a/barbican_tempest_plugin/clients.py b/barbican_tempest_plugin/clients.py index 078f383..9c009ba 100644 --- a/barbican_tempest_plugin/clients.py +++ b/barbican_tempest_plugin/clients.py @@ -12,14 +12,22 @@ # License for the specific language governing permissions and limitations under # the License. +from tempest import clients +from tempest.common import credentials_factory as common_creds from tempest import config -from tempest.lib.services import clients - +from tempest.lib.services import clients as cli CONF = config.CONF +ADMIN_CREDS = common_creds.get_configured_admin_credentials() -class Clients(clients.ServiceClients): + +class Manager(clients.Manager): + def __init__(self, credentials=ADMIN_CREDS): + super(Manager, self).__init__(credentials) + + +class Clients(cli.ServiceClients): """Tempest stable service clients and loaded plugins service clients""" def __init__(self, credentials, service=None): diff --git a/barbican_tempest_plugin/tests/scenario/barbican_manager.py b/barbican_tempest_plugin/tests/scenario/barbican_manager.py new file mode 100644 index 0000000..c279f04 --- /dev/null +++ b/barbican_tempest_plugin/tests/scenario/barbican_manager.py @@ -0,0 +1,158 @@ +# Copyright 2017 Johns Hopkins Applied Physics Lab +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import base64 +from datetime import datetime +from datetime import timedelta +import os + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import serialization +from cryptography import x509 +from cryptography.x509.oid import NameOID + +from oslo_log import log as logging +from tempest import config +from tempest.scenario import manager as mgr + +from barbican_tempest_plugin import clients + +CONF = config.CONF +LOG = logging.getLogger(__name__) + + +class BarbicanScenarioTest(mgr.ScenarioTest): + + credentials = ('primary', ) + manager = clients.Manager() + + def setUp(self): + super(BarbicanScenarioTest, self).setUp() + self.img_file = os.path.join(CONF.scenario.img_dir, + CONF.scenario.img_file) + self.private_key = rsa.generate_private_key(public_exponent=3, + key_size=1024, + backend=default_backend()) + self.signing_certificate = self._create_self_signed_certificate( + self.private_key + ) + self.signing_cert_uuid = self._store_cert( + self.signing_certificate + ) + + @classmethod + def skip_checks(cls): + super(BarbicanScenarioTest, cls).skip_checks() + if not CONF.service_available.barbican: + raise cls.skipException('Barbican is not enabled.') + + @classmethod + def setup_clients(cls): + super(BarbicanScenarioTest, cls).setup_clients() + + os = getattr(cls, 'os_%s' % cls.credentials[0]) + cls.consumer_client = os.secret_v1.ConsumerClient( + service='key-manager' + ) + cls.container_client = os.secret_v1.ContainerClient( + service='key-manager' + ) + cls.order_client = os.secret_v1.OrderClient(service='key-manager') + cls.secret_client = os.secret_v1.SecretClient(service='key-manager') + cls.secret_metadata_client = os.secret_v1.SecretMetadataClient( + service='key-manager' + ) + + def _get_uuid(self, href): + return href.split('/')[-1] + + def _create_self_signed_certificate(self, private_key): + issuer = x509.Name([ + x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"), + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"CA"), + x509.NameAttribute(NameOID.LOCALITY_NAME, u"San Francisco"), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"My Company"), + x509.NameAttribute(NameOID.COMMON_NAME, u"Test Certificate"), + ]) + cert_builder = x509.CertificateBuilder( + issuer_name=issuer, subject_name=issuer, + public_key=private_key.public_key(), + serial_number=x509.random_serial_number(), + not_valid_before=datetime.utcnow(), + not_valid_after=datetime.utcnow() + timedelta(days=10) + ) + cert = cert_builder.sign(private_key, + hashes.SHA256(), + default_backend()) + return cert + + def _store_cert(self, cert): + pem_encoding = cert.public_bytes(encoding=serialization.Encoding.PEM) + cert_b64 = base64.b64encode(pem_encoding) + expire_time = (datetime.utcnow() + timedelta(days=5)) + LOG.debug("Uploading certificate to barbican") + result = self.secret_client.create_secret( + expiration=expire_time.isoformat(), algorithm="rsa", + secret_type="certificate", + payload_content_type="application/octet-stream", + payload_content_encoding="base64", + payload=cert_b64 + ) + LOG.debug("Certificate uploaded to barbican (%s)", result) + return self._get_uuid(result['secret_ref']) + + def _sign_image(self, image_file): + LOG.debug("Creating signature for image data") + signer = self.private_key.signer( + padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), + salt_length=padding.PSS.MAX_LENGTH + ), + hashes.SHA256() + ) + chunk_bytes = 8192 + with open(image_file, 'rb') as f: + chunk = f.read(chunk_bytes) + while len(chunk) > 0: + signer.update(chunk) + chunk = f.read(chunk_bytes) + signature = signer.finalize() + signature_b64 = base64.b64encode(signature) + return signature_b64 + + def sign_and_upload_image(self): + img_signature = self._sign_image(self.img_file) + + img_properties = { + 'img_signature': img_signature, + 'img_signature_certificate_uuid': self.signing_cert_uuid, + 'img_signature_key_type': 'RSA-PSS', + 'img_signature_hash_method': 'SHA-256', + } + + LOG.debug("Uploading image with signature metadata properties") + img_uuid = self._image_create( + 'signed_img', + CONF.scenario.img_container_format, + self.img_file, + disk_format=CONF.scenario.img_disk_format, + properties=img_properties + ) + LOG.debug("Uploaded image %s", img_uuid) + + return img_uuid diff --git a/barbican_tempest_plugin/tests/scenario/test_image_signing.py b/barbican_tempest_plugin/tests/scenario/test_image_signing.py new file mode 100644 index 0000000..9070578 --- /dev/null +++ b/barbican_tempest_plugin/tests/scenario/test_image_signing.py @@ -0,0 +1,83 @@ +# Copyright (c) 2017 Johns Hopkins University Applied Physics Laboratory +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import log as logging +from tempest import config +from tempest import exceptions +from tempest.lib import decorators +from tempest import test + +from barbican_tempest_plugin.tests.scenario import barbican_manager + +CONF = config.CONF +LOG = logging.getLogger(__name__) + + +class ImageSigningTest(barbican_manager.BarbicanScenarioTest): + + @decorators.idempotent_id('4343df3c-5553-40ea-8705-0cce73b297a9') + @test.services('compute', 'image') + def test_signed_image_upload_and_boot(self): + """Test that Nova boots a signed image. + + The test follows these steps: + * Create an asymmetric keypair + * Sign an image file with the private key + * Create a certificate with the public key + * Store the certificate in Barbican + * Store the signed image in Glance + * Boot the signed image + * Confirm the instance changes state to Active + """ + img_uuid = self.sign_and_upload_image() + + LOG.debug("Booting server with signed image %s", img_uuid) + instance = self.create_server(name='signed_img_server', + image_id=img_uuid, + wait_until='ACTIVE') + self.servers_client.delete_server(instance['id']) + + @decorators.idempotent_id('74f022d6-a6ef-4458-96b7-541deadacf99') + @test.services('compute', 'image') + def test_signed_image_upload_boot_failure(self): + """Test that Nova refuses to boot an incorrectly signed image. + + If the create_server call succeeds instead of throwing an + exception, it is likely that signature verification is not + turned on. To turn on signature verification, set + verify_glance_signatures=True in the nova configuration + file under the [glance] section. + + The test follows these steps: + * Create an asymmetric keypair + * Sign an image file with the private key + * Create a certificate with the public key + * Store the certificate in Barbican + * Store the signed image in Glance + * Modify the signature to be incorrect + * Attempt to boot the incorrectly signed image + * Confirm an exception is thrown + """ + img_uuid = self.sign_and_upload_image() + + LOG.debug("Modifying image signature to be incorrect") + metadata = {'img_signature': 'fake_signature'} + self.compute_images_client.update_image_metadata( + img_uuid, metadata + ) + + self.assertRaisesRegex(exceptions.BuildErrorException, + "Signature verification for the image failed", + self.create_server, + image_id=img_uuid) diff --git a/tools/pre_test_hook.sh b/tools/pre_test_hook.sh index 806ca40..2640433 100755 --- a/tools/pre_test_hook.sh +++ b/tools/pre_test_hook.sh @@ -11,7 +11,16 @@ export LOCALCONF_PATH=$DEVSTACK_DIR/local.conf # Here we can set some configurations for local.conf # for example, to pass some config options directly to .conf files +# For image signature verification tests echo -e '[[post-config|$NOVA_CONF]]' >> $LOCALCONF_PATH echo -e '[glance]' >> $LOCALCONF_PATH echo -e 'verify_glance_signatures = True' >> $LOCALCONF_PATH +# Allow dynamically created tempest users to create secrets +# in barbican +echo -e '[[test-config|$TEMPEST_CONFIG]]' >> $LOCALCONF_PATH +echo -e '[auth]' >> $LOCALCONF_PATH +echo -e 'tempest_roles=creator' >> $LOCALCONF_PATH +# Glance v1 doesn't do signature verification on image upload +echo -e '[image-feature-enabled]' >> $LOCALCONF_PATH +echo -e 'api_v1=False' >> $LOCALCONF_PATH diff --git a/tox.ini b/tox.ini index f100b39..55a2b65 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,6 @@ commands = python setup.py test --slowest --testr-args='{posargs}' [testenv:pep8] commands = flake8 {posargs} - check-uuid --package barbican_tempest_plugin [testenv:venv] commands = {posargs}