swift/swift/common/middleware/keymaster.py

219 lines
8.7 KiB
Python

# Copyright (c) 2015 OpenStack Foundation
#
# 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.
"""
The simple scheme for key derivation is as follows:
every path is associated with a key, where the key is derived from the
path itself in a deterministic fashion such that the key does not need to be
stored. Specifically, the key for any path is an HMAC of a root key and the
path itself, calculated using an SHA256 hash function::
<path_key> = HMAC_SHA256(<root_secret>, <path>)
"""
import base64
import hashlib
import hmac
import os
from swift.common.utils import get_logger, split_path
from swift.common.request_helpers import get_obj_persisted_sysmeta_prefix, \
is_sys_meta, strip_sys_meta_prefix
from swift.common.wsgi import WSGIContext
from swift.common.swob import Request, HTTPException, HTTPUnprocessableEntity
class KeyMasterContext(WSGIContext):
def __init__(self, keymaster, account, container, obj):
super(KeyMasterContext, self).__init__(keymaster.app)
self.keymaster = keymaster
self.logger = keymaster.logger
self.account = account
self.container = container
self.obj = obj
self._init_keys()
def _init_keys(self):
"""
Setup default container and object keys based on the request path.
"""
self.keys = {}
self.account_path = os.path.join(os.sep, self.account)
self.container_path = self.obj_path = None
self.server_type = 'account'
if self.container:
self.server_type = 'container'
self.container_path = os.path.join(self.account_path,
self.container)
self.keys['container'] = self.keymaster.create_key(
self.container_path)
if self.obj:
self.server_type = 'object'
self.obj_path = os.path.join(self.container_path, self.obj)
self.keys['object'] = self.keymaster.create_key(
self.obj_path)
def _handle_post_or_put(self, req, start_response):
req.environ['swift.crypto.fetch_crypto_keys'] = self.fetch_crypto_keys
resp = self._app_call(req.environ)
start_response(self._response_status, self._response_headers,
self._response_exc_info)
return resp
def PUT(self, req, start_response):
if self.obj_path:
# TODO: re-examine need for this special handling once COPY has
# been moved to middleware.
# For object PUT we save a key_id as obj sysmeta so that if the
# object is copied to another location we can use the key_id
# (rather than its new path) to calculate its key for a GET or
# HEAD.
id_name = "%scrypto-id" % get_obj_persisted_sysmeta_prefix()
req.headers[id_name] = \
base64.b64encode(self.obj_path)
return self._handle_post_or_put(req, start_response)
def POST(self, req, start_response):
return self._handle_post_or_put(req, start_response)
def GET(self, req, start_response):
return self._handle_get_or_head(req, start_response)
def HEAD(self, req, start_response):
return self._handle_get_or_head(req, start_response)
def _handle_get_or_head(self, req, start_response):
resp = self._app_call(req.environ)
self.provide_keys_get_or_head(req)
start_response(self._response_status, self._response_headers,
self._response_exc_info)
return resp
def error_if_need_keys(self, req):
# Determine if keys will actually be needed
# Look for any "x-<server_type>-sysmeta-crypto-meta" headers
if not hasattr(self, '_response_headers'):
return
if any(strip_sys_meta_prefix(self.server_type, h).lower().startswith(
'crypto-meta-')
for (h, v) in self._response_headers
if is_sys_meta(self.server_type, h)):
self.logger.error("Cannot get necessary keys for path %s" %
req.path)
raise HTTPUnprocessableEntity(
"Cannot get necessary keys for path %s" % req.path)
self.logger.debug("No encryption keys necessary for path %s" %
req.path)
def provide_keys_get_or_head(self, req):
if self.obj_path:
# TODO: re-examine need for this special handling once COPY has
# been moved to middleware.
# For object GET or HEAD we look for a key_id that may have been
# stored in the object sysmeta during a PUT and use that to
# calculate the object key, in case the object has been copied to a
# new path.
try:
id_name = \
"%scrypto-id" % get_obj_persisted_sysmeta_prefix()
obj_key_path = self._response_header_value(id_name)
if not obj_key_path:
raise ValueError('No object key was found.')
try:
obj_key_path = base64.b64decode(obj_key_path)
except TypeError:
self.logger.warning("path %s could not be decoded" %
obj_key_path)
raise ValueError("path %s could not be decoded" %
obj_key_path)
path_acc, path_cont, path_obj = \
split_path(obj_key_path, 3, 3, True)
cont_key_path = os.path.join(os.sep, path_acc, path_cont)
self.keys['container'] = self.keymaster.create_key(
cont_key_path)
self.logger.debug("obj key id: %s" % obj_key_path)
self.logger.debug("cont key id: %s" % cont_key_path)
self.keys['object'] = self.keymaster.create_key(
obj_key_path)
except ValueError:
req.environ['swift.crypto.override'] = True
# TODO: uncomment when FakeFooters has been replaced with
# real footer support. Fake Footers will insert crypto sysmeta
# headers into all responses including 4xx that may have been
# generated in the proxy (e.g. auth failures). This will cause
# error_if_need_keys to replace the expected 4xx with a 422.
# So disable the check for now.
# self.error_if_need_keys(req)
if not req.environ.get('swift.crypto.override'):
req.environ['swift.crypto.fetch_crypto_keys'] = \
self.fetch_crypto_keys
def fetch_crypto_keys(self):
return self.keys
class KeyMaster(object):
def __init__(self, app, conf):
self.app = app
self.logger = get_logger(conf, log_route="keymaster")
self.root_secret = conf.get('encryption_root_secret', None)
if not self.root_secret:
raise ValueError('encryption_root_secret not set in '
'proxy-server.conf')
self.root_secret = self.root_secret.encode('utf-8')
def __call__(self, env, start_response):
req = Request(env)
try:
parts = req.split_path(2, 4, True)
except ValueError:
return self.app(env, start_response)
if hasattr(KeyMasterContext, req.method):
# handle only those request methods that may require keys
km_context = KeyMasterContext(self, *parts[1:])
try:
return getattr(km_context, req.method)(req, start_response)
except HTTPException as err_resp:
return err_resp(env, start_response)
# anything else
return self.app(env, start_response)
def create_key(self, key_id):
key_id = 'fixed'
# TODO: setting key_id to 'fixed' is a temporary workaround for
# problems caused by the lack of copy middleware. Once that is merged,
# we can remove the above line.
return hmac.new(self.root_secret, key_id,
digestmod=hashlib.sha256).digest()
def filter_factory(global_conf, **local_conf):
conf = global_conf.copy()
conf.update(local_conf)
def keymaster_filter(app):
return KeyMaster(app, conf)
return keymaster_filter