Merge "Add image signature verification"

This commit is contained in:
Jenkins 2016-01-22 12:47:51 +00:00 committed by Gerrit Code Review
commit 18a45ae36f
6 changed files with 267 additions and 2 deletions

View File

@ -23,6 +23,7 @@ import random
import sys
import time
import cryptography
import glanceclient
from glanceclient.common import http
import glanceclient.exc
@ -40,6 +41,8 @@ import six.moves.urllib.parse as urlparse
from nova import exception
from nova.i18n import _LE, _LI, _LW
import nova.image.download as image_xfers
from nova import objects
from nova import signature_utils
glance_opts = [
@ -81,6 +84,10 @@ should be fully qualified urls of the form
help='A list of url scheme that can be downloaded directly '
'via the direct_url. Currently supported schemes: '
'[file].'),
cfg.BoolOpt('verify_glance_signatures',
default=False,
help='Require Nova to perform signature verification on '
'each image downloaded from Glance.'),
]
LOG = logging.getLogger(__name__)
@ -366,17 +373,70 @@ class GlanceImageService(object):
except Exception:
_reraise_translated_image_exception(image_id)
# Retrieve properties for verification of Glance image signature
verifier = None
if CONF.glance.verify_glance_signatures:
image_meta_dict = self.show(context, image_id,
include_locations=False)
image_meta = objects.ImageMeta.from_dict(image_meta_dict)
img_signature = image_meta.properties.get('img_signature')
img_sig_hash_method = image_meta.properties.get(
'img_signature_hash_method'
)
img_sig_cert_uuid = image_meta.properties.get(
'img_signature_certificate_uuid'
)
img_sig_key_type = image_meta.properties.get(
'img_signature_key_type'
)
try:
verifier = signature_utils.get_verifier(context,
img_sig_cert_uuid,
img_sig_hash_method,
img_signature,
img_sig_key_type)
except exception.SignatureVerificationError:
with excutils.save_and_reraise_exception():
LOG.error(_LE('Image signature verification failed '
'for image: %s'), image_id)
close_file = False
if data is None and dst_path:
data = open(dst_path, 'wb')
close_file = True
if data is None:
# Perform image signature verification
if verifier:
try:
for chunk in image_chunks:
verifier.update(chunk)
verifier.verify()
LOG.info(_LI('Image signature verification succeeded '
'for image: %s'), image_id)
except cryptography.exceptions.InvalidSignature:
with excutils.save_and_reraise_exception():
LOG.error(_LE('Image signature verification failed '
'for image: %s'), image_id)
return image_chunks
else:
try:
for chunk in image_chunks:
if verifier:
verifier.update(chunk)
data.write(chunk)
if verifier:
verifier.verify()
LOG.info(_LI('Image signature verification succeeded '
'for image %s'), image_id)
except cryptography.exceptions.InvalidSignature:
data.truncate(0)
with excutils.save_and_reraise_exception():
LOG.error(_LE('Image signature verification failed '
'for image: %s'), image_id)
except Exception as ex:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Error writing to %(path)s: %(exception)s"),

View File

@ -251,6 +251,27 @@ class HVType(Enum):
return super(HVType, self).coerce(obj, attr, value)
class ImageSignatureHashType(Enum):
# Represents the possible hash methods used for image signing
def __init__(self):
self.hashes = ('SHA-224', 'SHA-256', 'SHA-384', 'SHA-512')
super(ImageSignatureHashType, self).__init__(
valid_values=self.hashes
)
class ImageSignatureKeyType(Enum):
# Represents the possible keypair types used for image signing
def __init__(self):
self.key_types = (
'DSA', 'ECC_SECT571K1', 'ECC_SECT409K1', 'ECC_SECT571R1',
'ECC_SECT409R1', 'ECC_SECP521R1', 'ECC_SECP384R1', 'RSA-PSS'
)
super(ImageSignatureKeyType, self).__init__(
valid_values=self.key_types
)
class OSType(Enum):
LINUX = "linux"
@ -734,6 +755,14 @@ class HVTypeField(BaseEnumField):
AUTO_TYPE = HVType()
class ImageSignatureHashTypeField(BaseEnumField):
AUTO_TYPE = ImageSignatureHashType()
class ImageSignatureKeyTypeField(BaseEnumField):
AUTO_TYPE = ImageSignatureKeyType()
class OSTypeField(BaseEnumField):
AUTO_TYPE = OSType()

View File

@ -160,7 +160,8 @@ class ImageMetaProps(base.NovaObject):
# Version 1.9: added hw_cpu_thread_policy field
# Version 1.10: added hw_cpu_realtime_mask field
# Version 1.11: Added hw_firmware_type field
VERSION = '1.11'
# Version 1.12: Added properties for image signature verification
VERSION = '1.12'
def obj_make_compatible(self, primitive, target_version):
super(ImageMetaProps, self).obj_make_compatible(primitive,
@ -371,6 +372,19 @@ class ImageMetaProps(base.NovaObject):
# integer value 1
'img_version': fields.IntegerField(),
# base64 of encoding of image signature
'img_signature': fields.StringField(),
# string indicating hash method used to compute image signature
'img_signature_hash_method': fields.ImageSignatureHashTypeField(),
# string indicating Castellan uuid of certificate
# used to compute the image's signature
'img_signature_certificate_uuid': fields.UUIDField(),
# string indicating type of key used to compute image signature
'img_signature_key_type': fields.ImageSignatureKeyTypeField(),
# string of username with admin privileges
'os_admin_user': fields.StringField(),

View File

@ -17,6 +17,7 @@
import datetime
from six.moves import StringIO
import cryptography
import glanceclient.exc
import mock
from oslo_config import cfg
@ -673,6 +674,147 @@ class TestDownloadNoDirectUri(test.NoDBTestCase):
writer.close.assert_called_once_with()
class TestDownloadSignatureVerification(test.NoDBTestCase):
class MockVerifier(object):
def update(self, data):
return
def verify(self):
return True
class BadVerifier(object):
def update(self, data):
return
def verify(self):
raise cryptography.exceptions.InvalidSignature(
'Invalid signature.'
)
def setUp(self):
super(TestDownloadSignatureVerification, self).setUp()
self.flags(verify_glance_signatures=True, group='glance')
self.fake_img_props = {
'properties': {
'img_signature': 'signature',
'img_signature_hash_method': 'SHA-224',
'img_signature_certificate_uuid': 'uuid',
'img_signature_key_type': 'RSA-PSS',
}
}
self.fake_img_data = ['A' * 256, 'B' * 256]
client = mock.MagicMock()
client.call.return_value = self.fake_img_data
self.service = glance.GlanceImageService(client)
@mock.patch('nova.image.glance.LOG')
@mock.patch('nova.image.glance.GlanceImageService.show')
@mock.patch('nova.signature_utils.get_verifier')
def test_download_with_signature_verification(self,
mock_get_verifier,
mock_show,
mock_log):
mock_get_verifier.return_value = self.MockVerifier()
mock_show.return_value = self.fake_img_props
res = self.service.download(context=None, image_id=None,
data=None, dst_path=None)
self.assertEqual(self.fake_img_data, res)
mock_get_verifier.assert_called_once_with(None, 'uuid', 'SHA-224',
'signature', 'RSA-PSS')
mock_log.info.assert_called_once_with(mock.ANY, mock.ANY)
@mock.patch.object(six.moves.builtins, 'open')
@mock.patch('nova.image.glance.LOG')
@mock.patch('nova.image.glance.GlanceImageService.show')
@mock.patch('nova.signature_utils.get_verifier')
def test_download_dst_path_signature_verification(self,
mock_get_verifier,
mock_show,
mock_log,
mock_open):
mock_get_verifier.return_value = self.MockVerifier()
mock_show.return_value = self.fake_img_props
mock_dest = mock.MagicMock()
fake_path = 'FAKE_PATH'
mock_open.return_value = mock_dest
self.service.download(context=None, image_id=None,
data=None, dst_path=fake_path)
mock_get_verifier.assert_called_once_with(None, 'uuid', 'SHA-224',
'signature', 'RSA-PSS')
mock_log.info.assert_called_once_with(mock.ANY, mock.ANY)
self.assertEqual(len(self.fake_img_data), mock_dest.write.call_count)
self.assertTrue(mock_dest.close.called)
@mock.patch('nova.image.glance.LOG')
@mock.patch('nova.image.glance.GlanceImageService.show')
@mock.patch('nova.signature_utils.get_verifier')
def test_download_with_get_verifier_failure(self,
mock_get_verifier,
mock_show,
mock_log):
mock_get_verifier.side_effect = exception.SignatureVerificationError(
reason='Signature verification '
'failed.'
)
mock_show.return_value = self.fake_img_props
self.assertRaises(exception.SignatureVerificationError,
self.service.download,
context=None, image_id=None,
data=None, dst_path=None)
mock_log.error.assert_called_once_with(mock.ANY, mock.ANY)
@mock.patch('nova.image.glance.LOG')
@mock.patch('nova.image.glance.GlanceImageService.show')
@mock.patch('nova.signature_utils.get_verifier')
def test_download_with_invalid_signature(self,
mock_get_verifier,
mock_show,
mock_log):
mock_get_verifier.return_value = self.BadVerifier()
mock_show.return_value = self.fake_img_props
self.assertRaises(cryptography.exceptions.InvalidSignature,
self.service.download,
context=None, image_id=None,
data=None, dst_path=None)
mock_log.error.assert_called_once_with(mock.ANY, mock.ANY)
@mock.patch('nova.image.glance.LOG')
@mock.patch('nova.image.glance.GlanceImageService.show')
def test_download_missing_signature_metadata(self,
mock_show,
mock_log):
mock_show.return_value = {'properties': {}}
self.assertRaisesRegex(exception.SignatureVerificationError,
'Required image properties for signature '
'verification do not exist. Cannot verify '
'signature. Missing property: .*',
self.service.download,
context=None, image_id=None,
data=None, dst_path=None)
@mock.patch.object(six.moves.builtins, 'open')
@mock.patch('nova.signature_utils.get_verifier')
@mock.patch('nova.image.glance.LOG')
@mock.patch('nova.image.glance.GlanceImageService.show')
def test_download_dst_path_signature_fail(self, mock_show,
mock_log, mock_get_verifier,
mock_open):
mock_get_verifier.return_value = self.BadVerifier()
mock_dest = mock.MagicMock()
fake_path = 'FAKE_PATH'
mock_open.return_value = mock_dest
mock_show.return_value = self.fake_img_props
self.assertRaises(cryptography.exceptions.InvalidSignature,
self.service.download,
context=None, image_id=None,
data=None, dst_path=fake_path)
mock_log.error.assert_called_once_with(mock.ANY, mock.ANY)
mock_open.assert_called_once_with(fake_path, 'wb')
mock_dest.truncate.assert_called_once_with(0)
self.assertTrue(mock_dest.close.called)
class TestIsImageAvailable(test.NoDBTestCase):
"""Tests the internal _is_image_available function."""

View File

@ -21,6 +21,7 @@ import six
from nova.network import model as network_model
from nova.objects import fields
from nova import signature_utils
from nova import test
from nova import utils
@ -422,6 +423,25 @@ class TestHVType(TestField):
self.assertRaises(ValueError, self.field.stringify, 'acme')
class TestImageSignatureTypes(TestField):
# Ensure that the object definition is updated
# in step with the signature_utils module
def setUp(self):
super(TestImageSignatureTypes, self).setUp()
self.hash_field = fields.ImageSignatureHashType()
self.key_type_field = fields.ImageSignatureKeyType()
def test_hashes(self):
for hash_name in list(signature_utils.HASH_METHODS.keys()):
self.assertIn(hash_name, self.hash_field.hashes)
def test_key_types(self):
key_type_dict = signature_utils.SignatureKeyType._REGISTERED_TYPES
key_types = list(key_type_dict.keys())
for key_type in key_types:
self.assertIn(key_type, self.key_type_field.key_types)
class TestOSType(TestField):
def setUp(self):
super(TestOSType, self).setUp()

View File

@ -1136,7 +1136,7 @@ object_data = {
'HostMapping': '1.0-1a3390a696792a552ab7bd31a77ba9ac',
'HVSpec': '1.2-db672e73304da86139086d003f3977e7',
'ImageMeta': '1.8-642d1b2eb3e880a367f37d72dd76162d',
'ImageMetaProps': '1.11-96aa14a8ba226701bbd22e63557a63ea',
'ImageMetaProps': '1.12-6a132dee47931447bf86c03c7006d96c',
'Instance': '2.1-416fdd0dfc33dfa12ff2cfdd8cc32e17',
'InstanceAction': '1.1-f9f293e526b66fca0d05c3b3a2d13914',
'InstanceActionEvent': '1.1-e56a64fa4710e43ef7af2ad9d6028b33',