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:
Stuart McLaren 2011-10-28 15:58:49 +01:00
parent 2674e8bbbc
commit 5e6fb33b22
10 changed files with 171 additions and 14 deletions

View File

@ -26,6 +26,7 @@ Monty Taylor <mordred@inaugust.com>
Rick Clark <rick@openstack.org>
Rick Harris <rconradharris@gmail.com>
Soren Hansen <soren.hansen@rackspace.com>
Stuart McLaren <stuart.mclaren@hp.com>
Taku Fukushima <tfukushima@dcl.info.waseda.ac.jp>
Thierry Carrez <thierry@openstack.org>
Tom Hancock <tom.hancock@hp.com>

View File

@ -34,6 +34,13 @@ backlog = 4096
# Private key file to use when starting API server securely
# 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 ===============================
# Address to find the registry server

70
glance/common/crypt.py Normal file
View 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))]

View File

@ -30,6 +30,8 @@ logger = logging.getLogger('glance.registry')
_CLIENT_HOST = None
_CLIENT_PORT = None
_CLIENT_KWARGS = {}
# AES key used to encrypt 'location' metadata
_METADATA_ENCRYPTION_KEY = None
def configure_registry_client(options):
@ -38,7 +40,7 @@ def configure_registry_client(options):
: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:
host = options['registry_host']
port = int(options['registry_port'])
@ -56,7 +58,7 @@ def configure_registry_client(options):
key_file = options.get('registry_client_key_file')
cert_file = options.get('registry_client_cert_file')
ca_file = options.get('registry_client_ca_file')
_METADATA_ENCRYPTION_KEY = options.get('metadata_encryption_key')
_CLIENT_HOST = host
_CLIENT_PORT = port
_CLIENT_KWARGS = {'use_ssl': use_ssl,
@ -66,10 +68,11 @@ def configure_registry_client(options):
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['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):

View File

@ -24,6 +24,7 @@ import json
import urllib
from glance.common.client import BaseClient
from glance.common import crypt
from glance.registry.api.v1 import images
@ -33,6 +34,32 @@ class RegistryClient(BaseClient):
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):
"""
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)
res = self.do_request("GET", "/images", params=params)
data = json.loads(res.read())['images']
return data
image_list = json.loads(res.read())['images']
for image in image_list:
image = self.decrypt_metadata(image)
return image_list
def get_images_detailed(self, **kwargs):
"""
@ -60,14 +89,16 @@ class RegistryClient(BaseClient):
"""
params = self._extract_params(kwargs, images.SUPPORTED_PARAMS)
res = self.do_request("GET", "/images/detail", params=params)
data = json.loads(res.read())['images']
return data
image_list = json.loads(res.read())['images']
for image in image_list:
image = self.decrypt_metadata(image)
return image_list
def get_image(self, image_id):
"""Returns a mapping of image metadata from Registry"""
res = self.do_request("GET", "/images/%s" % image_id)
data = json.loads(res.read())['image']
return data
return self.decrypt_metadata(data)
def add_image(self, image_metadata):
"""
@ -80,12 +111,15 @@ class RegistryClient(BaseClient):
if 'image' not in image_metadata.keys():
image_metadata = dict(image=image_metadata)
image_metadata['image'] = self.encrypt_metadata(
image_metadata['image'])
body = json.dumps(image_metadata)
res = self.do_request("POST", "/images", body, headers=headers)
# Registry returns a JSONified dict(image=image_info)
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):
"""
@ -94,6 +128,8 @@ class RegistryClient(BaseClient):
if 'image' not in image_metadata.keys():
image_metadata = dict(image=image_metadata)
image_metadata['image'] = self.encrypt_metadata(
image_metadata['image'])
body = json.dumps(image_metadata)
headers = {
@ -106,7 +142,7 @@ class RegistryClient(BaseClient):
res = self.do_request("PUT", "/images/%s" % image_id, body, headers)
data = json.loads(res.read())
image = data['image']
return image
return self.decrypt_metadata(image)
def delete_image(self, image_id):
"""

View File

@ -150,6 +150,7 @@ class ApiServer(Server):
self.default_store = 'file'
self.key_file = ""
self.cert_file = ""
self.metadata_encryption_key = "012345678901234567890123456789ab"
self.image_dir = os.path.join(self.test_dir,
"images")
self.pid_file = os.path.join(self.test_dir,
@ -187,6 +188,7 @@ bind_host = 0.0.0.0
bind_port = %(bind_port)s
key_file = %(key_file)s
cert_file = %(cert_file)s
metadata_encryption_key = %(metadata_encryption_key)s
registry_host = 0.0.0.0
registry_port = %(registry_port)s
log_file = %(log_file)s

View File

@ -39,6 +39,7 @@ import unittest
import httplib2
from glance.common import crypt
from glance.common import utils
from glance.tests.functional import test_api
from glance.tests.utils import execute, skip_if_disabled
@ -193,7 +194,12 @@ class TestS3(test_api.TestApi):
http = httplib2.Http()
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
image_id2 = utils.generate_uuid()

View File

@ -39,6 +39,7 @@ import os
import tempfile
import unittest
from glance.common import crypt
import glance.store.swift # Needed to register driver for location
from glance.store.location import get_location_from_uri
from glance.tests.functional import test_api
@ -263,7 +264,13 @@ class TestSwift(test_api.TestApi):
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 200)
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
from swift.common import client as swift_client
@ -449,7 +456,12 @@ class TestSwift(test_api.TestApi):
self.assertEqual(response.status, 200)
data = json.loads(content)
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
image_data = "*" * FIVE_KB

View File

@ -21,6 +21,7 @@ import datetime
import re
import unittest
from glance.common import crypt
from glance.common import exception
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')
now_iso = utils.isotime()
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)

View File

@ -20,3 +20,4 @@ bzr
httplib2
xattr>=0.6.0
kombu
pycrypto