Merge "Add image signing verification"

This commit is contained in:
Jenkins 2015-09-03 11:39:21 +00:00 committed by Gerrit Code Review
commit e6e8df63bb
8 changed files with 405 additions and 2 deletions

View File

@ -63,6 +63,22 @@ class ImageDataController(object):
'e': encodeutils.exception_to_unicode(e)})
LOG.exception(msg)
def _delete(self, image_repo, image):
"""Delete the image.
:param image_repo: The instance of ImageRepo
:param image: The image that will be deleted
"""
try:
if image_repo and image:
image.status = 'killed'
image_repo.save(image)
except Exception as e:
msg = (_LE("Unable to delete image %(image_id)s: %(e)s") %
{'image_id': image.image_id,
'e': encodeutils.exception_to_unicode(e)})
LOG.exception(msg)
@utils.mutating
def upload(self, req, image_id, data, size):
image_repo = self.gateway.get_repo(req.context)
@ -152,6 +168,14 @@ class ImageDataController(object):
raise webob.exc.HTTPServiceUnavailable(explanation=msg,
request=req)
except exception.SignatureVerificationError as e:
msg = (_LE("Signature verification failed for image %(id)s: %(e)s")
% {'id': image_id,
'e': encodeutils.exception_to_unicode(e)})
LOG.error(msg)
self._delete(image_repo, image)
raise webob.exc.HTTPBadRequest(explanation=msg)
except webob.exc.HTTPGone as e:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Failed to upload image data due to HTTP error"))

View File

@ -447,6 +447,10 @@ class MetadefTagNotFound(NotFound):
" namespace=%(namespace_name)s.")
class SignatureVerificationError(GlanceException):
message = _("Unable to verify signature: %(reason)s")
class InvalidVersion(Invalid):
message = _("Version is invalid: %(reason)s")

View File

@ -0,0 +1,281 @@
# Copyright (c) The Johns Hopkins University/Applied Physics Laboratory
# 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.
"""Support signature verification."""
import base64
from castellan import key_manager
from cryptography import exceptions as crypto_exception
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 import x509
from oslo_log import log as logging
from oslo_utils import encodeutils
from glance.common import exception
from glance import i18n
LOG = logging.getLogger(__name__)
_ = i18n._
_LE = i18n._LE
# Note: This is the signature hash method, which is independent from the
# image data checksum hash method (which is handled elsewhere).
HASH_METHODS = {
'SHA-224': hashes.SHA224(),
'SHA-256': hashes.SHA256(),
'SHA-384': hashes.SHA384(),
'SHA-512': hashes.SHA512()
}
# These are the currently supported signature formats
(RSA_PSS,) = (
'RSA-PSS',
)
SIGNATURE_KEY_TYPES = {
RSA_PSS
}
# These are the currently supported certificate formats
(X_509,) = (
'X.509',
)
CERTIFICATE_FORMATS = {
X_509
}
# These are the currently supported MGF formats, used for RSA-PSS signatures
MASK_GEN_ALGORITHMS = {
'MGF1': padding.MGF1
}
# Required image property names
(SIGNATURE, HASH_METHOD, KEY_TYPE, CERT_UUID) = (
'signature',
'signature_hash_method',
'signature_key_type',
'signature_certificate_uuid'
)
# Optional image property names for RSA-PSS
(MASK_GEN_ALG, PSS_SALT_LENGTH) = (
'mask_gen_algorithm',
'pss_salt_length'
)
def should_verify_signature(image_properties):
"""Determine whether a signature should be verified.
Using the image properties, determine whether existing properties indicate
that signature verification should be done.
:param image_properties: the key-value properties about the image
:return: True, if signature metadata properties exist, False otherwise
"""
return (image_properties is not None and
CERT_UUID in image_properties and
HASH_METHOD in image_properties and
SIGNATURE in image_properties and
KEY_TYPE in image_properties)
def verify_signature(context, checksum_hash, image_properties):
"""Retrieve the image properties and use them to verify the signature.
:param context: the user context for authentication
:param checksum_hash: the 'checksum' hash of the image data
:param image_properties: the key-value properties about the image
:return: True if verification succeeds
:raises: SignatureVerificationError if verification fails
"""
if not should_verify_signature(image_properties):
raise exception.SignatureVerificationError(
'Required image properties for signature verification do not'
' exist. Cannot verify signature.')
signature = get_signature(image_properties[SIGNATURE])
hash_method = get_hash_method(image_properties[HASH_METHOD])
signature_key_type = get_signature_key_type(
image_properties[KEY_TYPE])
public_key = get_public_key(context,
image_properties[CERT_UUID],
signature_key_type)
# Initialize the verifier
verifier = None
# create the verifier based on the signature key type
if signature_key_type == RSA_PSS:
# retrieve other needed properties, or use defaults if not there
if MASK_GEN_ALG in image_properties:
mask_gen_algorithm = image_properties[MASK_GEN_ALG]
if mask_gen_algorithm in MASK_GEN_ALGORITHMS:
mgf = MASK_GEN_ALGORITHMS[mask_gen_algorithm](hash_method)
else:
raise exception.SignatureVerificationError(
'Invalid mask_gen_algorithm: %s' % mask_gen_algorithm)
else:
# default to MGF1
mgf = padding.MGF1(hash_method)
if PSS_SALT_LENGTH in image_properties:
pss_salt_length = image_properties[PSS_SALT_LENGTH]
try:
salt_length = int(pss_salt_length)
except ValueError:
raise exception.SignatureVerificationError(
'Invalid pss_salt_length: %s' % pss_salt_length)
else:
# default to max salt length
salt_length = padding.PSS.MAX_LENGTH
# Create the verifier
verifier = public_key.verifier(
signature,
padding.PSS(
mgf=mgf,
salt_length=salt_length
),
hash_method
)
if verifier:
# Verify the signature
verifier.update(checksum_hash)
try:
verifier.verify()
return True
except crypto_exception.InvalidSignature:
raise exception.SignatureVerificationError(
'Signature verification failed.')
else:
# Error creating the verifier
raise exception.SignatureVerificationError(
'Error occurred while verifying the signature')
def get_signature(signature_data):
"""Decode the signature data and returns the signature.
:param siganture_data: the base64-encoded signature data
:return: the decoded signature
:raises: SignatureVerificationError if the signature data is malformatted
"""
try:
signature = base64.b64decode(signature_data)
except TypeError:
raise exception.SignatureVerificationError(
'The signature data was not properly encoded using base64')
return signature
def get_hash_method(hash_method_name):
"""Verify the hash method name and create the hash method.
:param hash_method_name: the name of the hash method to retrieve
:return: the hash method, a cryptography object
:raises: SignatureVerificationError if the hash method name is invalid
"""
if hash_method_name not in HASH_METHODS:
raise exception.SignatureVerificationError(
'Invalid signature hash method: %s' % hash_method_name)
return HASH_METHODS[hash_method_name]
def get_signature_key_type(signature_key_type):
"""Verify the signature key type.
:param signature_key_type: the key type of the signature
:return: the validated signature key type
:raises: SignatureVerificationError if the signature key type is invalid
"""
if signature_key_type not in SIGNATURE_KEY_TYPES:
raise exception.SignatureVerificationError(
'Invalid signature key type: %s' % signature_key_type)
return signature_key_type
def get_public_key(context, signature_certificate_uuid, signature_key_type):
"""Create the public key object from a retrieved certificate.
:param context: the user context for authentication
:param signature_certificate_uuid: the uuid to use to retrieve the
certificate
:param signature_key_type: the key type of the signature
:return: the public key cryptography object
:raises: SignatureVerificationError if public key format is invalid
"""
certificate = get_certificate(context, signature_certificate_uuid)
# Note that this public key could either be
# RSAPublicKey, DSAPublicKey, or EllipticCurvePublicKey
public_key = certificate.public_key()
# Confirm the type is of the type expected based on the signature key type
if signature_key_type == RSA_PSS:
if not isinstance(public_key, rsa.RSAPublicKey):
raise exception.SignatureVerificationError(
'Invalid public key type for signature key type: %s'
% signature_key_type)
return public_key
def get_certificate(context, signature_certificate_uuid):
"""Create the certificate object from the retrieved certificate data.
:param context: the user context for authentication
:param signature_certificate_uuid: the uuid to use to retrieve the
certificate
:return: the certificate cryptography object
:raises: SignatureVerificationError if the retrieval fails or the format
is invalid
"""
keymgr_api = key_manager.API()
try:
# The certificate retrieved here is a castellan certificate object
cert = keymgr_api.get(context, signature_certificate_uuid)
except Exception as e:
# The problem encountered may be backend-specific, since castellan
# can use different backends. Rather than importing all possible
# backends here, the generic "Exception" is used.
msg = (_LE("Unable to retrieve certificate with ID %(id)s: %(e)s")
% {'id': signature_certificate_uuid,
'e': encodeutils.exception_to_unicode(e)})
LOG.error(msg)
raise exception.SignatureVerificationError(
'Unable to retrieve certificate with ID: %s'
% signature_certificate_uuid)
if cert.format not in CERTIFICATE_FORMATS:
raise exception.SignatureVerificationError(
'Invalid certificate format: %s' % cert.format)
if cert.format == X_509:
# castellan always encodes certificates in DER format
cert_data = cert.get_encoded()
certificate = x509.load_der_x509_certificate(cert_data,
default_backend())
return certificate

View File

@ -23,6 +23,7 @@ from oslo_utils import encodeutils
from oslo_utils import excutils
from glance.common import exception
from glance.common import signature_utils
from glance.common import utils
import glance.domain.proxy
from glance import i18n
@ -30,6 +31,7 @@ from glance import i18n
_ = i18n._
_LE = i18n._LE
_LI = i18n._LI
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
@ -375,6 +377,18 @@ class ImageProxy(glance.domain.proxy.Image):
CONF.image_size_cap),
size,
context=self.context)
# Verify the signature (if correct properties are present)
if (signature_utils.should_verify_signature(
self.image.extra_properties)):
# NOTE(bpoulos): if verification fails, exception will be raised
result = signature_utils.verify_signature(
self.context, checksum, self.image.extra_properties)
if result:
msg = (_LI("Successfully verified signature for image "
"%s") % self.image.image_id)
LOG.info(msg)
self.image.locations = [{'url': location, 'metadata': loc_meta,
'status': 'active'}]
self.image.size = size

View File

@ -16,6 +16,7 @@ import glance_store
import mock
from glance.common import exception
from glance.common import signature_utils
import glance.location
from glance.tests.unit import base as unit_test_base
from glance.tests.unit import utils as unit_test_utils
@ -41,12 +42,13 @@ class ImageRepoStub(object):
class ImageStub(object):
def __init__(self, image_id, status=None, locations=None,
visibility=None):
visibility=None, extra_properties=None):
self.image_id = image_id
self.status = status
self.locations = locations or []
self.visibility = visibility
self.size = 1
self.extra_properties = extra_properties or {}
def delete(self):
self.status = 'deleted'
@ -60,7 +62,8 @@ class ImageFactoryStub(object):
min_disk=0, min_ram=0, protected=False, owner=None,
disk_format=None, container_format=None,
extra_properties=None, tags=None, **other_args):
return ImageStub(image_id, visibility=visibility, **other_args)
return ImageStub(image_id, visibility=visibility,
extra_properties=extra_properties, **other_args)
class FakeMemberRepo(object):
@ -185,6 +188,62 @@ class TestStoreImage(utils.BaseTestCase):
self.store_api.get_from_backend,
image.locations[0]['url'], context={})
def test_image_set_data_valid_signature(self):
context = glance.context.RequestContext(user=USER1)
extra_properties = {
'signature_certificate_uuid': 'UUID',
'signature_hash_method': 'METHOD',
'signature_key_type': 'TYPE',
'signature': 'VALID'
}
image_stub = ImageStub(UUID2, status='queued',
extra_properties=extra_properties)
self.stubs.Set(signature_utils, 'verify_signature',
unit_test_utils.fake_verify_signature)
image = glance.location.ImageProxy(image_stub, context,
self.store_api, self.store_utils)
image.set_data('YYYY', 4)
self.assertEqual(UUID2, image.locations[0]['url'])
self.assertEqual('Z', image.checksum)
self.assertEqual('active', image.status)
def test_image_set_data_invalid_signature(self):
context = glance.context.RequestContext(user=USER1)
extra_properties = {
'signature_certificate_uuid': 'UUID',
'signature_hash_method': 'METHOD',
'signature_key_type': 'TYPE',
'signature': 'INVALID'
}
image_stub = ImageStub(UUID2, status='queued',
extra_properties=extra_properties)
self.stubs.Set(signature_utils, 'verify_signature',
unit_test_utils.fake_verify_signature)
image = glance.location.ImageProxy(image_stub, context,
self.store_api, self.store_utils)
self.assertRaises(exception.SignatureVerificationError,
image.set_data,
'YYYY', 4)
def test_image_set_data_invalid_signature_missing_metadata(self):
context = glance.context.RequestContext(user=USER1)
extra_properties = {
'signature_hash_method': 'METHOD',
'signature_key_type': 'TYPE',
'signature': 'INVALID'
}
image_stub = ImageStub(UUID2, status='queued',
extra_properties=extra_properties)
self.stubs.Set(signature_utils, 'verify_signature',
unit_test_utils.fake_verify_signature)
image = glance.location.ImageProxy(image_stub, context,
self.store_api, self.store_utils)
image.set_data('YYYY', 4)
self.assertEqual(UUID2, image.locations[0]['url'])
self.assertEqual('Z', image.checksum)
# Image is still active, since invalid signature was ignored
self.assertEqual('active', image.status)
def _add_image(self, context, image_id, data, len):
image_stub = ImageStub(image_id, status='queued', locations=[])
image = glance.location.ImageProxy(image_stub, context,

View File

@ -83,6 +83,15 @@ def fake_get_size_from_backend(uri, context=None):
return 1
def fake_verify_signature(context, checksum_hash, image_properties):
if (image_properties is not None and 'signature' in image_properties and
image_properties['signature'] == 'VALID'):
return True
else:
raise exception.SignatureVerificationError(
'Signature verification failed.')
class FakeDB(object):
def __init__(self, initialize=True):

View File

@ -249,6 +249,15 @@ class TestImagesController(base.StoreClearingUnitTest):
self.controller.upload,
request, unit_test_utils.UUID2, 'YYYYYYY', 7)
def test_upload_signature_verification_fails(self):
request = unit_test_utils.get_fake_request()
image = FakeImage()
image.set_data = Raise(exception.SignatureVerificationError)
self.image_repo.result = image
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.upload,
request, unit_test_utils.UUID1, 'YYYY', 4)
self.assertEqual('killed', self.image_repo.saved_image.status)
def test_image_size_limit_exceeded(self):
request = unit_test_utils.get_fake_request()
image = FakeImage()

View File

@ -67,3 +67,6 @@ glance-store>=0.7.1 # Apache-2.0
# Artifact repository
semantic-version>=2.3.1
castellan>=0.2.0 # Apache-2.0
cryptography>=1.0 # Apache-2.0