Adds option to encrypt 'location' metadata.
Implements blueprint swift-location-credentials When the new option is enabled the location metadata (which may include user credentials) is encrypted before being sent to the registry server. Change-Id: I072e3f5c81f187435b1c156627076d5fde087af5
This commit is contained in:
parent
2674e8bbbc
commit
5e6fb33b22
1
Authors
1
Authors
@ -26,6 +26,7 @@ Monty Taylor <mordred@inaugust.com>
|
|||||||
Rick Clark <rick@openstack.org>
|
Rick Clark <rick@openstack.org>
|
||||||
Rick Harris <rconradharris@gmail.com>
|
Rick Harris <rconradharris@gmail.com>
|
||||||
Soren Hansen <soren.hansen@rackspace.com>
|
Soren Hansen <soren.hansen@rackspace.com>
|
||||||
|
Stuart McLaren <stuart.mclaren@hp.com>
|
||||||
Taku Fukushima <tfukushima@dcl.info.waseda.ac.jp>
|
Taku Fukushima <tfukushima@dcl.info.waseda.ac.jp>
|
||||||
Thierry Carrez <thierry@openstack.org>
|
Thierry Carrez <thierry@openstack.org>
|
||||||
Tom Hancock <tom.hancock@hp.com>
|
Tom Hancock <tom.hancock@hp.com>
|
||||||
|
@ -34,6 +34,13 @@ backlog = 4096
|
|||||||
# Private key file to use when starting API server securely
|
# Private key file to use when starting API server securely
|
||||||
# key_file = /path/to/keyfile
|
# key_file = /path/to/keyfile
|
||||||
|
|
||||||
|
# ================= Security Options ==========================
|
||||||
|
|
||||||
|
# AES key for encrypting store 'location' metadata, including
|
||||||
|
# -- if used -- Swift or S3 credentials
|
||||||
|
# Should be set to a random string of length 16, 24 or 32 bytes
|
||||||
|
# metadata_encryption_key = <16, 24 or 32 char registry metadata key>
|
||||||
|
|
||||||
# ============ Registry Options ===============================
|
# ============ Registry Options ===============================
|
||||||
|
|
||||||
# Address to find the registry server
|
# Address to find the registry server
|
||||||
|
70
glance/common/crypt.py
Normal file
70
glance/common/crypt.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Routines for URL-safe encrypting/decrypting
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import string
|
||||||
|
import os
|
||||||
|
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
from Crypto import Random
|
||||||
|
from Crypto.Random import random
|
||||||
|
|
||||||
|
|
||||||
|
def urlsafe_encrypt(key, plaintext, blocksize=16):
|
||||||
|
"""
|
||||||
|
Encrypts plaintext. Resulting ciphertext will contain URL-safe characters
|
||||||
|
:param key: AES secret key
|
||||||
|
:param plaintext: Input text to be encrypted
|
||||||
|
:param blocksize: Non-zero integer multiple of AES blocksize in bytes (16)
|
||||||
|
|
||||||
|
:returns : Resulting ciphertext
|
||||||
|
"""
|
||||||
|
def pad(text):
|
||||||
|
"""
|
||||||
|
Pads text to be encrypted
|
||||||
|
"""
|
||||||
|
pad_length = (blocksize - len(text) % blocksize)
|
||||||
|
sr = random.StrongRandom()
|
||||||
|
pad = ''.join(chr(sr.randint(1, 0xFF)) for i in range(pad_length - 1))
|
||||||
|
# We use chr(0) as a delimiter between text and padding
|
||||||
|
return text + chr(0) + pad
|
||||||
|
|
||||||
|
# random initial 16 bytes for CBC
|
||||||
|
init_vector = Random.get_random_bytes(16)
|
||||||
|
cypher = AES.new(key, AES.MODE_CBC, init_vector)
|
||||||
|
padded = cypher.encrypt(pad(str(plaintext)))
|
||||||
|
return base64.urlsafe_b64encode(init_vector + padded)
|
||||||
|
|
||||||
|
|
||||||
|
def urlsafe_decrypt(key, ciphertext):
|
||||||
|
"""
|
||||||
|
Decrypts URL-safe base64 encoded ciphertext
|
||||||
|
:param key: AES secret key
|
||||||
|
:param ciphertext: The encrypted text to decrypt
|
||||||
|
|
||||||
|
:returns : Resulting plaintext
|
||||||
|
"""
|
||||||
|
# Cast from unicode
|
||||||
|
ciphertext = base64.urlsafe_b64decode(str(ciphertext))
|
||||||
|
cypher = AES.new(key, AES.MODE_CBC, ciphertext[:16])
|
||||||
|
padded = cypher.decrypt(ciphertext[16:])
|
||||||
|
return padded[:padded.rfind(chr(0))]
|
@ -30,6 +30,8 @@ logger = logging.getLogger('glance.registry')
|
|||||||
_CLIENT_HOST = None
|
_CLIENT_HOST = None
|
||||||
_CLIENT_PORT = None
|
_CLIENT_PORT = None
|
||||||
_CLIENT_KWARGS = {}
|
_CLIENT_KWARGS = {}
|
||||||
|
# AES key used to encrypt 'location' metadata
|
||||||
|
_METADATA_ENCRYPTION_KEY = None
|
||||||
|
|
||||||
|
|
||||||
def configure_registry_client(options):
|
def configure_registry_client(options):
|
||||||
@ -38,7 +40,7 @@ def configure_registry_client(options):
|
|||||||
|
|
||||||
:param options: Configuration options coming from controller
|
:param options: Configuration options coming from controller
|
||||||
"""
|
"""
|
||||||
global _CLIENT_KWARGS, _CLIENT_HOST, _CLIENT_PORT
|
global _CLIENT_KWARGS, _CLIENT_HOST, _CLIENT_PORT, _METADATA_ENCRYPTION_KEY
|
||||||
try:
|
try:
|
||||||
host = options['registry_host']
|
host = options['registry_host']
|
||||||
port = int(options['registry_port'])
|
port = int(options['registry_port'])
|
||||||
@ -56,7 +58,7 @@ def configure_registry_client(options):
|
|||||||
key_file = options.get('registry_client_key_file')
|
key_file = options.get('registry_client_key_file')
|
||||||
cert_file = options.get('registry_client_cert_file')
|
cert_file = options.get('registry_client_cert_file')
|
||||||
ca_file = options.get('registry_client_ca_file')
|
ca_file = options.get('registry_client_ca_file')
|
||||||
|
_METADATA_ENCRYPTION_KEY = options.get('metadata_encryption_key')
|
||||||
_CLIENT_HOST = host
|
_CLIENT_HOST = host
|
||||||
_CLIENT_PORT = port
|
_CLIENT_PORT = port
|
||||||
_CLIENT_KWARGS = {'use_ssl': use_ssl,
|
_CLIENT_KWARGS = {'use_ssl': use_ssl,
|
||||||
@ -66,10 +68,11 @@ def configure_registry_client(options):
|
|||||||
|
|
||||||
|
|
||||||
def get_registry_client(cxt):
|
def get_registry_client(cxt):
|
||||||
global _CLIENT_KWARGS, _CLIENT_HOST, _CLIENT_PORT
|
global _CLIENT_KWARGS, _CLIENT_HOST, _CLIENT_PORT, _METADATA_ENCRYPTION_KEY
|
||||||
kwargs = _CLIENT_KWARGS.copy()
|
kwargs = _CLIENT_KWARGS.copy()
|
||||||
kwargs['auth_tok'] = cxt.auth_tok
|
kwargs['auth_tok'] = cxt.auth_tok
|
||||||
return client.RegistryClient(_CLIENT_HOST, _CLIENT_PORT, **kwargs)
|
return client.RegistryClient(_CLIENT_HOST, _CLIENT_PORT,
|
||||||
|
_METADATA_ENCRYPTION_KEY, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def get_images_list(context, **kwargs):
|
def get_images_list(context, **kwargs):
|
||||||
|
@ -24,6 +24,7 @@ import json
|
|||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
from glance.common.client import BaseClient
|
from glance.common.client import BaseClient
|
||||||
|
from glance.common import crypt
|
||||||
from glance.registry.api.v1 import images
|
from glance.registry.api.v1 import images
|
||||||
|
|
||||||
|
|
||||||
@ -33,6 +34,32 @@ class RegistryClient(BaseClient):
|
|||||||
|
|
||||||
DEFAULT_PORT = 9191
|
DEFAULT_PORT = 9191
|
||||||
|
|
||||||
|
def __init__(self, host=None, port=None, metadata_encryption_key=None,
|
||||||
|
**kwargs):
|
||||||
|
"""
|
||||||
|
:param metadata_encryption_key: Key used to encrypt 'location' metadata
|
||||||
|
"""
|
||||||
|
self.metadata_encryption_key = metadata_encryption_key
|
||||||
|
BaseClient.__init__(self, host, port, **kwargs)
|
||||||
|
|
||||||
|
def decrypt_metadata(self, image_metadata):
|
||||||
|
if (self.metadata_encryption_key is not None
|
||||||
|
and 'location' in image_metadata.keys()
|
||||||
|
and image_metadata['location'] is not None):
|
||||||
|
location = crypt.urlsafe_decrypt(self.metadata_encryption_key,
|
||||||
|
image_metadata['location'])
|
||||||
|
image_metadata['location'] = location
|
||||||
|
return image_metadata
|
||||||
|
|
||||||
|
def encrypt_metadata(self, image_metadata):
|
||||||
|
if (self.metadata_encryption_key is not None
|
||||||
|
and 'location' in image_metadata.keys()
|
||||||
|
and image_metadata['location'] is not None):
|
||||||
|
location = crypt.urlsafe_encrypt(self.metadata_encryption_key,
|
||||||
|
image_metadata['location'], 64)
|
||||||
|
image_metadata['location'] = location
|
||||||
|
return image_metadata
|
||||||
|
|
||||||
def get_images(self, **kwargs):
|
def get_images(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Returns a list of image id/name mappings from Registry
|
Returns a list of image id/name mappings from Registry
|
||||||
@ -45,8 +72,10 @@ class RegistryClient(BaseClient):
|
|||||||
"""
|
"""
|
||||||
params = self._extract_params(kwargs, images.SUPPORTED_PARAMS)
|
params = self._extract_params(kwargs, images.SUPPORTED_PARAMS)
|
||||||
res = self.do_request("GET", "/images", params=params)
|
res = self.do_request("GET", "/images", params=params)
|
||||||
data = json.loads(res.read())['images']
|
image_list = json.loads(res.read())['images']
|
||||||
return data
|
for image in image_list:
|
||||||
|
image = self.decrypt_metadata(image)
|
||||||
|
return image_list
|
||||||
|
|
||||||
def get_images_detailed(self, **kwargs):
|
def get_images_detailed(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -60,14 +89,16 @@ class RegistryClient(BaseClient):
|
|||||||
"""
|
"""
|
||||||
params = self._extract_params(kwargs, images.SUPPORTED_PARAMS)
|
params = self._extract_params(kwargs, images.SUPPORTED_PARAMS)
|
||||||
res = self.do_request("GET", "/images/detail", params=params)
|
res = self.do_request("GET", "/images/detail", params=params)
|
||||||
data = json.loads(res.read())['images']
|
image_list = json.loads(res.read())['images']
|
||||||
return data
|
for image in image_list:
|
||||||
|
image = self.decrypt_metadata(image)
|
||||||
|
return image_list
|
||||||
|
|
||||||
def get_image(self, image_id):
|
def get_image(self, image_id):
|
||||||
"""Returns a mapping of image metadata from Registry"""
|
"""Returns a mapping of image metadata from Registry"""
|
||||||
res = self.do_request("GET", "/images/%s" % image_id)
|
res = self.do_request("GET", "/images/%s" % image_id)
|
||||||
data = json.loads(res.read())['image']
|
data = json.loads(res.read())['image']
|
||||||
return data
|
return self.decrypt_metadata(data)
|
||||||
|
|
||||||
def add_image(self, image_metadata):
|
def add_image(self, image_metadata):
|
||||||
"""
|
"""
|
||||||
@ -80,12 +111,15 @@ class RegistryClient(BaseClient):
|
|||||||
if 'image' not in image_metadata.keys():
|
if 'image' not in image_metadata.keys():
|
||||||
image_metadata = dict(image=image_metadata)
|
image_metadata = dict(image=image_metadata)
|
||||||
|
|
||||||
|
image_metadata['image'] = self.encrypt_metadata(
|
||||||
|
image_metadata['image'])
|
||||||
body = json.dumps(image_metadata)
|
body = json.dumps(image_metadata)
|
||||||
|
|
||||||
res = self.do_request("POST", "/images", body, headers=headers)
|
res = self.do_request("POST", "/images", body, headers=headers)
|
||||||
# Registry returns a JSONified dict(image=image_info)
|
# Registry returns a JSONified dict(image=image_info)
|
||||||
data = json.loads(res.read())
|
data = json.loads(res.read())
|
||||||
return data['image']
|
image = data['image']
|
||||||
|
return self.decrypt_metadata(image)
|
||||||
|
|
||||||
def update_image(self, image_id, image_metadata, purge_props=False):
|
def update_image(self, image_id, image_metadata, purge_props=False):
|
||||||
"""
|
"""
|
||||||
@ -94,6 +128,8 @@ class RegistryClient(BaseClient):
|
|||||||
if 'image' not in image_metadata.keys():
|
if 'image' not in image_metadata.keys():
|
||||||
image_metadata = dict(image=image_metadata)
|
image_metadata = dict(image=image_metadata)
|
||||||
|
|
||||||
|
image_metadata['image'] = self.encrypt_metadata(
|
||||||
|
image_metadata['image'])
|
||||||
body = json.dumps(image_metadata)
|
body = json.dumps(image_metadata)
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
@ -106,7 +142,7 @@ class RegistryClient(BaseClient):
|
|||||||
res = self.do_request("PUT", "/images/%s" % image_id, body, headers)
|
res = self.do_request("PUT", "/images/%s" % image_id, body, headers)
|
||||||
data = json.loads(res.read())
|
data = json.loads(res.read())
|
||||||
image = data['image']
|
image = data['image']
|
||||||
return image
|
return self.decrypt_metadata(image)
|
||||||
|
|
||||||
def delete_image(self, image_id):
|
def delete_image(self, image_id):
|
||||||
"""
|
"""
|
||||||
|
@ -150,6 +150,7 @@ class ApiServer(Server):
|
|||||||
self.default_store = 'file'
|
self.default_store = 'file'
|
||||||
self.key_file = ""
|
self.key_file = ""
|
||||||
self.cert_file = ""
|
self.cert_file = ""
|
||||||
|
self.metadata_encryption_key = "012345678901234567890123456789ab"
|
||||||
self.image_dir = os.path.join(self.test_dir,
|
self.image_dir = os.path.join(self.test_dir,
|
||||||
"images")
|
"images")
|
||||||
self.pid_file = os.path.join(self.test_dir,
|
self.pid_file = os.path.join(self.test_dir,
|
||||||
@ -187,6 +188,7 @@ bind_host = 0.0.0.0
|
|||||||
bind_port = %(bind_port)s
|
bind_port = %(bind_port)s
|
||||||
key_file = %(key_file)s
|
key_file = %(key_file)s
|
||||||
cert_file = %(cert_file)s
|
cert_file = %(cert_file)s
|
||||||
|
metadata_encryption_key = %(metadata_encryption_key)s
|
||||||
registry_host = 0.0.0.0
|
registry_host = 0.0.0.0
|
||||||
registry_port = %(registry_port)s
|
registry_port = %(registry_port)s
|
||||||
log_file = %(log_file)s
|
log_file = %(log_file)s
|
||||||
|
@ -39,6 +39,7 @@ import unittest
|
|||||||
|
|
||||||
import httplib2
|
import httplib2
|
||||||
|
|
||||||
|
from glance.common import crypt
|
||||||
from glance.common import utils
|
from glance.common import utils
|
||||||
from glance.tests.functional import test_api
|
from glance.tests.functional import test_api
|
||||||
from glance.tests.utils import execute, skip_if_disabled
|
from glance.tests.utils import execute, skip_if_disabled
|
||||||
@ -193,7 +194,12 @@ class TestS3(test_api.TestApi):
|
|||||||
|
|
||||||
http = httplib2.Http()
|
http = httplib2.Http()
|
||||||
response, content = http.request(path % args, 'GET')
|
response, content = http.request(path % args, 'GET')
|
||||||
s3_store_location = json.loads(content)['image']['location']
|
if hasattr(self, 'metadata_encryption_key'):
|
||||||
|
key = self.metadata_encryption_key
|
||||||
|
else:
|
||||||
|
key = self.api_server.metadata_encryption_key
|
||||||
|
loc = json.loads(content)['image']['location']
|
||||||
|
s3_store_location = crypt.urlsafe_decrypt(key, loc)
|
||||||
|
|
||||||
# 4. POST /images using location generated by Image1
|
# 4. POST /images using location generated by Image1
|
||||||
image_id2 = utils.generate_uuid()
|
image_id2 = utils.generate_uuid()
|
||||||
|
@ -39,6 +39,7 @@ import os
|
|||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
from glance.common import crypt
|
||||||
import glance.store.swift # Needed to register driver for location
|
import glance.store.swift # Needed to register driver for location
|
||||||
from glance.store.location import get_location_from_uri
|
from glance.store.location import get_location_from_uri
|
||||||
from glance.tests.functional import test_api
|
from glance.tests.functional import test_api
|
||||||
@ -263,7 +264,13 @@ class TestSwift(test_api.TestApi):
|
|||||||
response, content = http.request(path, 'GET')
|
response, content = http.request(path, 'GET')
|
||||||
self.assertEqual(response.status, 200)
|
self.assertEqual(response.status, 200)
|
||||||
data = json.loads(content)
|
data = json.loads(content)
|
||||||
image_loc = get_location_from_uri(data['image']['location'])
|
image_loc = data['image']['location']
|
||||||
|
if hasattr(self, 'metadata_encryption_key'):
|
||||||
|
key = self.metadata_encryption_key
|
||||||
|
else:
|
||||||
|
key = self.api_server.metadata_encryption_key
|
||||||
|
image_loc = crypt.urlsafe_decrypt(key, image_loc)
|
||||||
|
image_loc = get_location_from_uri(image_loc)
|
||||||
swift_loc = image_loc.store_location
|
swift_loc = image_loc.store_location
|
||||||
|
|
||||||
from swift.common import client as swift_client
|
from swift.common import client as swift_client
|
||||||
@ -449,7 +456,12 @@ class TestSwift(test_api.TestApi):
|
|||||||
self.assertEqual(response.status, 200)
|
self.assertEqual(response.status, 200)
|
||||||
data = json.loads(content)
|
data = json.loads(content)
|
||||||
self.assertTrue('location' in data['image'].keys())
|
self.assertTrue('location' in data['image'].keys())
|
||||||
swift_location = data['image']['location']
|
loc = data['image']['location']
|
||||||
|
if hasattr(self, 'metadata_encryption_key'):
|
||||||
|
key = self.metadata_encryption_key
|
||||||
|
else:
|
||||||
|
key = self.api_server.metadata_encryption_key
|
||||||
|
swift_location = crypt.urlsafe_decrypt(key, loc)
|
||||||
|
|
||||||
# POST /images with public image named Image1 without uploading data
|
# POST /images with public image named Image1 without uploading data
|
||||||
image_data = "*" * FIVE_KB
|
image_data = "*" * FIVE_KB
|
||||||
|
@ -21,6 +21,7 @@ import datetime
|
|||||||
import re
|
import re
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
from glance.common import crypt
|
||||||
from glance.common import exception
|
from glance.common import exception
|
||||||
from glance.common import utils
|
from glance.common import utils
|
||||||
|
|
||||||
@ -126,3 +127,21 @@ class UtilsTestCase(unittest.TestCase):
|
|||||||
iso_re = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z')
|
iso_re = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z')
|
||||||
now_iso = utils.isotime()
|
now_iso = utils.isotime()
|
||||||
self.assertTrue(iso_re.match(now_iso) is not None)
|
self.assertTrue(iso_re.match(now_iso) is not None)
|
||||||
|
|
||||||
|
def test_encryption(self):
|
||||||
|
# Check that original plaintext and unencrypted ciphertext match
|
||||||
|
# Check keys of the three allowed lengths
|
||||||
|
key_list = ["1234567890abcdef",
|
||||||
|
"12345678901234567890abcd",
|
||||||
|
"1234567890abcdef1234567890ABCDEF"]
|
||||||
|
plaintext_list = ['']
|
||||||
|
blocksize = 64
|
||||||
|
for i in range(3 * blocksize):
|
||||||
|
plaintext_list.append(os.urandom(i))
|
||||||
|
|
||||||
|
for key in key_list:
|
||||||
|
for plaintext in plaintext_list:
|
||||||
|
ciphertext = crypt.urlsafe_encrypt(key, plaintext, blocksize)
|
||||||
|
self.assertTrue(ciphertext != plaintext)
|
||||||
|
text = crypt.urlsafe_decrypt(key, ciphertext)
|
||||||
|
self.assertTrue(plaintext == text)
|
||||||
|
@ -20,3 +20,4 @@ bzr
|
|||||||
httplib2
|
httplib2
|
||||||
xattr>=0.6.0
|
xattr>=0.6.0
|
||||||
kombu
|
kombu
|
||||||
|
pycrypto
|
||||||
|
Loading…
Reference in New Issue
Block a user