# 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. import hashlib import hmac from swift.common.exceptions import UnknownSecretIdError from swift.common.middleware.crypto.crypto_utils import CRYPTO_KEY_CALLBACK from swift.common.swob import Request, HTTPException from swift.common.utils import readconf, strict_b64decode, get_logger, \ split_path from swift.common.wsgi import WSGIContext class KeyMasterContext(WSGIContext): """ 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:: = HMAC_SHA256(, ) """ def __init__(self, keymaster, account, container, obj, meta_version_to_write='2'): """ :param keymaster: a Keymaster instance :param account: account name :param container: container name :param obj: object name """ super(KeyMasterContext, self).__init__(keymaster.app) self.keymaster = keymaster self.account = account self.container = container self.obj = obj self._keys = {} self.meta_version_to_write = meta_version_to_write def _make_key_id(self, path, secret_id, version): key_id = {'v': version, 'path': path} if secret_id: # stash secret_id so that decrypter can pass it back to get the # same keys key_id['secret_id'] = secret_id return key_id def fetch_crypto_keys(self, key_id=None, *args, **kwargs): """ Setup container and object keys based on the request path. Keys are derived from request path. The 'id' entry in the results dict includes the part of the path used to derive keys. Other keymaster implementations may use a different strategy to generate keys and may include a different type of 'id', so callers should treat the 'id' as opaque keymaster-specific data. :param key_id: if given this should be a dict with the items included under the ``id`` key of a dict returned by this method. :returns: A dict containing encryption keys for 'object' and 'container', and entries 'id' and 'all_ids'. The 'all_ids' entry is a list of key id dicts for all root secret ids including the one used to generate the returned keys. """ if key_id: secret_id = key_id.get('secret_id') version = key_id['v'] if version not in ('1', '2', '3'): raise ValueError('Unknown key_id version: %s' % version) if version == '1' and not key_id['path'].startswith( '/' + self.account + '/'): # Well shoot. This was the bug that made us notice we needed # a v2! Hope the current account/container was the original! key_acct, key_cont, key_obj = ( self.account, self.container, key_id['path']) else: key_acct, key_cont, key_obj = split_path( key_id['path'], 1, 3, True) check_path = ( self.account, self.container or key_cont, self.obj or key_obj) if version in ('1', '2') and ( key_acct, key_cont, key_obj) != check_path: # Older py3 proxies may have written down crypto meta as WSGI # strings; we still need to be able to read that try: alt_path = tuple( part.decode('utf-8').encode('latin1') for part in (key_acct, key_cont, key_obj)) except UnicodeError: # Well, it was worth a shot pass else: if check_path == alt_path or ( check_path[:2] == alt_path[:2] and not self.obj): # This object is affected by bug #1888037 key_acct, key_cont, key_obj = alt_path if (key_acct, key_cont, key_obj) != check_path: # Pipeline may have been misconfigured, with copy right of # encryption. In that case, path in meta may not be the # request path. self.keymaster.logger.info( "Path stored in meta (%r) does not match path from " "request (%r)! Using path from meta.", key_id['path'], '/' + '/'.join(x for x in [ self.account, self.container, self.obj] if x)) else: secret_id = self.keymaster.active_secret_id # v1 had a bug where we would claim the path was just the object # name if the object started with a slash. # v1 and v2 had a bug on py3 where we'd write the path in meta as # a WSGI string (ie, as Latin-1 chars decoded from UTF-8 bytes). # Bump versions to establish that we can trust the path. version = self.meta_version_to_write key_acct, key_cont, key_obj = ( self.account, self.container, self.obj) if (secret_id, version) in self._keys: return self._keys[(secret_id, version)] keys = {} account_path = '/' + key_acct # self.account/container/obj reflect the level of the *request*, # which may be different from the level of the key_id-path. Only # fetch the keys that the request needs. if self.container: path = account_path + '/' + key_cont keys['container'] = self.keymaster.create_key( path, secret_id=secret_id) if self.obj: if key_obj.startswith('/') and version == '1': path = key_obj else: path = path + '/' + key_obj keys['object'] = self.keymaster.create_key( path, secret_id=secret_id) # For future-proofing include a keymaster version number and the # path used to derive keys in the 'id' entry of the results. The # encrypter will persist this as part of the crypto-meta for # encrypted data and metadata. If we ever change the way keys are # generated then the decrypter could pass the persisted 'id' value # when it calls fetch_crypto_keys to inform the keymaster as to how # that particular data or metadata had its keys generated. # Currently we have no need to do that, so we are simply persisting # this information for future use. keys['id'] = self._make_key_id(path, secret_id, version) # pass back a list of key id dicts for all other secret ids in case # the caller is interested, in which case the caller can call this # method again for different secret ids; this avoided changing the # return type of the callback or adding another callback. Note that # the caller should assume no knowledge of the content of these key # id dicts. keys['all_ids'] = [self._make_key_id(path, id_, version) for id_ in self.keymaster.root_secret_ids] self._keys[(secret_id, version)] = keys return keys def handle_request(self, req, start_response): req.environ[CRYPTO_KEY_CALLBACK] = self.fetch_crypto_keys resp = self._app_call(req.environ) start_response(self._response_status, self._response_headers, self._response_exc_info) return resp class KeyMaster(object): """Middleware for providing encryption keys. The middleware requires at least one encryption root secret(s) to be set. This is the root secret from which encryption keys are derived. This must be set before first use to a value that is at least 256 bits. The security of all encrypted data critically depends on this key, therefore it should be set to a high-entropy value. For example, a suitable value may be obtained by generating a 32 byte (or longer) value using a cryptographically secure random number generator. Changing the root secret is likely to result in data loss. """ log_route = 'keymaster' keymaster_opts = () keymaster_conf_section = 'keymaster' def __init__(self, app, conf): self.app = app self.logger = get_logger(conf, log_route=self.log_route) self.keymaster_config_path = conf.get('keymaster_config_path') if type(self) is KeyMaster: self.keymaster_opts = ('encryption_root_secret*', 'active_root_secret_id') if self.keymaster_config_path: conf = self._load_keymaster_config_file(conf) # The _get_root_secret() function is overridden by other keymasters # which may historically only return a single value self._root_secrets = self._get_root_secret(conf) if not isinstance(self._root_secrets, dict): self._root_secrets = {None: self._root_secrets} self.active_secret_id = conf.get('active_root_secret_id') or None if self.active_secret_id not in self._root_secrets: raise ValueError('No secret loaded for active_root_secret_id %s' % self.active_secret_id) self.meta_version_to_write = conf.get('meta_version_to_write') or '2' if self.meta_version_to_write not in ('1', '2', '3'): raise ValueError('Unknown/unsupported metadata version: %r' % self.meta_version_to_write) @property def root_secret(self): # Returns the default root secret; this is here for historical reasons # to support tests and any third party code that might have used it return self._root_secrets.get(self.active_secret_id) @property def root_secret_ids(self): return sorted(self._root_secrets.keys()) def _load_keymaster_config_file(self, conf): # Keymaster options specified in the filter section would be ignored if # a separate keymaster config file is specified. To avoid confusion, # prohibit them existing in the filter section. bad_opts = [] for opt in conf: for km_opt in self.keymaster_opts: if ((km_opt.endswith('*') and opt.startswith(km_opt[:-1])) or opt == km_opt): bad_opts.append(opt) if bad_opts: raise ValueError('keymaster_config_path is set, but there ' 'are other config options specified: %s' % ", ".join(bad_opts)) return readconf(self.keymaster_config_path, self.keymaster_conf_section) def _decode_root_secret(self, b64_root_secret): binary_root_secret = strict_b64decode(b64_root_secret, allow_line_breaks=True) if len(binary_root_secret) < 32: raise ValueError return binary_root_secret def _load_multikey_opts(self, conf, prefix): result = [] for k, v in conf.items(): if not k.startswith(prefix): continue suffix = k[len(prefix):] if suffix and (suffix[0] != '_' or len(suffix) < 2): raise ValueError('Malformed root secret option name %s' % k) result.append((k, suffix[1:] or None, v)) return sorted(result) def _get_root_secret(self, conf): """ This keymaster requires ``encryption_root_secret[_id]`` options to be set. At least one must be set before first use to a value that is a base64 encoding of at least 32 bytes. The encryption root secrets are specified in either proxy-server.conf, or in an external file referenced from proxy-server.conf using ``keymaster_config_path``. :param conf: the keymaster config section from proxy-server.conf :type conf: dict :return: a dict mapping secret ids to encryption root secret binary bytes :rtype: dict """ root_secrets = {} for opt, secret_id, value in self._load_multikey_opts( conf, 'encryption_root_secret'): try: secret = self._decode_root_secret(value) except ValueError: raise ValueError( '%s option in %s must be a base64 encoding of at ' 'least 32 raw bytes' % (opt, self.keymaster_config_path or 'proxy-server.conf')) root_secrets[secret_id] = secret return root_secrets 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 req.method in ('PUT', 'POST', 'GET', 'HEAD'): # handle only those request methods that may require keys km_context = KeyMasterContext( self, *parts[1:], meta_version_to_write=self.meta_version_to_write) try: return km_context.handle_request(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, path, secret_id=None): """ Creates an encryption key that is unique for the given path. :param path: the path of the resource being encrypted. :param secret_id: the id of the root secret from which the key should be derived. :return: an encryption key. :raises UnknownSecretIdError: if the secret_id is not recognised. """ try: key = self._root_secrets[secret_id] except KeyError: self.logger.warning('Unrecognised secret id: %s' % secret_id) raise UnknownSecretIdError(secret_id) else: return hmac.new(key, path, 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