Merge "Add support for multiple root encryption secrets"
This commit is contained in:
commit
aae5f7c0da
@ -97,12 +97,6 @@ the `proxy-server.conf` file, for example::
|
|||||||
Root secret values MUST be at least 44 valid base-64 characters and
|
Root secret values MUST be at least 44 valid base-64 characters and
|
||||||
should be consistent across all proxy servers. The minimum length of 44 has
|
should be consistent across all proxy servers. The minimum length of 44 has
|
||||||
been chosen because it is the length of a base-64 encoded 32 byte value.
|
been chosen because it is the length of a base-64 encoded 32 byte value.
|
||||||
Alternatives to specifying the encryption root secret directly in the
|
|
||||||
`proxy-server.conf` file are storing it in a separate file, or storing it in
|
|
||||||
an :ref:`external key management system
|
|
||||||
<encryption_root_secret_in_external_kms>` such as `Barbican
|
|
||||||
<https://docs.openstack.org/barbican>`_ or a
|
|
||||||
`KMIP <https://www.oasis-open.org/committees/kmip/>`_ service.
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
@ -169,6 +163,62 @@ into GET and PUT requests by the :ref:`copy` middleware before reaching the
|
|||||||
encryption middleware and as a result object data and metadata is decrypted and
|
encryption middleware and as a result object data and metadata is decrypted and
|
||||||
re-encrypted when copied.
|
re-encrypted when copied.
|
||||||
|
|
||||||
|
Changing the encryption root secret
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
From time to time it may be desirable to change the root secret that is used to
|
||||||
|
derive encryption keys for new data written to the cluster. The `keymaster`
|
||||||
|
middleware allows alternative root secrets to be specified in its configuration
|
||||||
|
using options of the form::
|
||||||
|
|
||||||
|
encryption_root_secret_<secret_id> = <secret value>
|
||||||
|
|
||||||
|
where ``secret_id`` is a unique identifier for the root secret and ``secret
|
||||||
|
value`` is a value that meets the requirements for a root secret described
|
||||||
|
above.
|
||||||
|
|
||||||
|
Only one root secret is used to encrypt new data at any moment in time. This
|
||||||
|
root secret is specified using the ``active_root_secret_id`` option. If
|
||||||
|
specified, the value of this option should be one of the configured root secret
|
||||||
|
``secret_id`` values; otherwise the value of ``encryption_root_secret`` will be
|
||||||
|
taken as the default active root secret.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The active root secret is only used to derive keys for new data written to
|
||||||
|
the cluster. Changing the active root secret does not cause any existing
|
||||||
|
data to be re-encrypted.
|
||||||
|
|
||||||
|
Existing encrypted data will be decrypted using the root secret that was active
|
||||||
|
when that data was written. All previous active root secrets must therefore
|
||||||
|
remain in the middleware configuration in order for decryption of existing data
|
||||||
|
to succeed. Existing encrypted data will reference previous root secret by
|
||||||
|
the ``secret_id`` so it must be kept consistent in the configuration.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Do not remove or change any previously active ``<secret value>`` or ``<secret_id>``.
|
||||||
|
|
||||||
|
For example, the following keymaster configuration file specifies three root
|
||||||
|
secrets, with the value of ``encryption_root_secret_2`` being the current
|
||||||
|
active root secret::
|
||||||
|
|
||||||
|
[keymaster]
|
||||||
|
active_root_secret_id = 2
|
||||||
|
encryption_root_secret = your_secret
|
||||||
|
encryption_root_secret_1 = your_secret_1
|
||||||
|
encryption_root_secret_2 = your_secret_2
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
To ensure there is no loss of data availability, deploying a new key to
|
||||||
|
your cluster requires a two-stage config change. First, add the new key
|
||||||
|
to the ``key_id_<secret_id>`` option and restart the proxy-server. Do this
|
||||||
|
for all proxies. Next, set the ``active_root_secret_id`` option to the
|
||||||
|
new secret id and restart the proxy. Again, do this for all proxies. This
|
||||||
|
process ensures that all proxies will have the new key available for
|
||||||
|
*decryption* before any proxy uses it for *encryption*.
|
||||||
|
|
||||||
Encryption middleware
|
Encryption middleware
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
@ -1047,6 +1047,19 @@ use = egg:swift#keymaster
|
|||||||
# likely to result in data loss.
|
# likely to result in data loss.
|
||||||
encryption_root_secret = changeme
|
encryption_root_secret = changeme
|
||||||
|
|
||||||
|
# Multiple root secrets may be configured using options named
|
||||||
|
# 'encryption_root_secret_<secret_id>' where 'secret_id' is a unique
|
||||||
|
# identifier. This enables the root secret to be changed from time to time.
|
||||||
|
# Only one root secret is used for object PUTs or POSTs at any moment in time.
|
||||||
|
# This is specified by the 'active_root_secret_id' option. If
|
||||||
|
# 'active_root_secret_id' is not specified then the root secret specified by
|
||||||
|
# 'encryption_root_secret' is considered to be the default. Once a root secret
|
||||||
|
# has been used as the default root secret it must remain in the config file in
|
||||||
|
# order that any objects that were encrypted with it may be subsequently
|
||||||
|
# decrypted. The secret_id used to identify the key cannot change.
|
||||||
|
# encryption_root_secret_myid = changeme
|
||||||
|
# active_root_secret_id = myid
|
||||||
|
|
||||||
# Sets the path from which the keymaster config options should be read. This
|
# Sets the path from which the keymaster config options should be read. This
|
||||||
# allows multiple processes which need to be encryption-aware (for example,
|
# allows multiple processes which need to be encryption-aware (for example,
|
||||||
# proxy-server and container-sync) to share the same config file, ensuring
|
# proxy-server and container-sync) to share the same config file, ensuring
|
||||||
|
@ -223,6 +223,10 @@ class EncryptionException(SwiftException):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownSecretIdError(EncryptionException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ClientException(Exception):
|
class ClientException(Exception):
|
||||||
|
|
||||||
def __init__(self, msg, http_scheme='', http_host='', http_port='',
|
def __init__(self, msg, http_scheme='', http_host='', http_port='',
|
||||||
|
@ -24,7 +24,7 @@ import six
|
|||||||
from six.moves.urllib import parse as urlparse
|
from six.moves.urllib import parse as urlparse
|
||||||
|
|
||||||
from swift import gettext_ as _
|
from swift import gettext_ as _
|
||||||
from swift.common.exceptions import EncryptionException
|
from swift.common.exceptions import EncryptionException, UnknownSecretIdError
|
||||||
from swift.common.swob import HTTPInternalServerError
|
from swift.common.swob import HTTPInternalServerError
|
||||||
from swift.common.utils import get_logger
|
from swift.common.utils import get_logger
|
||||||
from swift.common.wsgi import WSGIContext
|
from swift.common.wsgi import WSGIContext
|
||||||
@ -155,7 +155,7 @@ class CryptoWSGIContext(WSGIContext):
|
|||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.server_type = server_type
|
self.server_type = server_type
|
||||||
|
|
||||||
def get_keys(self, env, required=None):
|
def get_keys(self, env, required=None, key_id=None):
|
||||||
# Get the key(s) from the keymaster
|
# Get the key(s) from the keymaster
|
||||||
required = required if required is not None else [self.server_type]
|
required = required if required is not None else [self.server_type]
|
||||||
try:
|
try:
|
||||||
@ -165,11 +165,14 @@ class CryptoWSGIContext(WSGIContext):
|
|||||||
raise HTTPInternalServerError(
|
raise HTTPInternalServerError(
|
||||||
"Unable to retrieve encryption keys.")
|
"Unable to retrieve encryption keys.")
|
||||||
|
|
||||||
|
err = None
|
||||||
try:
|
try:
|
||||||
keys = fetch_crypto_keys()
|
keys = fetch_crypto_keys(key_id=key_id)
|
||||||
|
except UnknownSecretIdError as err:
|
||||||
|
self.logger.error('get_keys(): unknown key id: %s', err)
|
||||||
|
raise
|
||||||
except Exception as err: # noqa
|
except Exception as err: # noqa
|
||||||
self.logger.exception(_(
|
self.logger.exception('get_keys(): from callback: %s', err)
|
||||||
'ERROR get_keys(): from callback: %s') % err)
|
|
||||||
raise HTTPInternalServerError(
|
raise HTTPInternalServerError(
|
||||||
"Unable to retrieve encryption keys.")
|
"Unable to retrieve encryption keys.")
|
||||||
|
|
||||||
@ -191,6 +194,17 @@ class CryptoWSGIContext(WSGIContext):
|
|||||||
|
|
||||||
return keys
|
return keys
|
||||||
|
|
||||||
|
def get_multiple_keys(self, env):
|
||||||
|
# get a list of keys from the keymaster containing one dict of keys for
|
||||||
|
# each of the keymaster root secret ids
|
||||||
|
keys = [self.get_keys(env)]
|
||||||
|
active_key_id = keys[0]['id']
|
||||||
|
for other_key_id in keys[0].get('all_ids', []):
|
||||||
|
if other_key_id == active_key_id:
|
||||||
|
continue
|
||||||
|
keys.append(self.get_keys(env, key_id=other_key_id))
|
||||||
|
return keys
|
||||||
|
|
||||||
|
|
||||||
def dump_crypto_meta(crypto_meta):
|
def dump_crypto_meta(crypto_meta):
|
||||||
"""
|
"""
|
||||||
|
@ -20,7 +20,7 @@ from swift import gettext_ as _
|
|||||||
from swift.common.http import is_success
|
from swift.common.http import is_success
|
||||||
from swift.common.middleware.crypto.crypto_utils import CryptoWSGIContext, \
|
from swift.common.middleware.crypto.crypto_utils import CryptoWSGIContext, \
|
||||||
load_crypto_meta, extract_crypto_meta, Crypto
|
load_crypto_meta, extract_crypto_meta, Crypto
|
||||||
from swift.common.exceptions import EncryptionException
|
from swift.common.exceptions import EncryptionException, UnknownSecretIdError
|
||||||
from swift.common.request_helpers import get_object_transient_sysmeta, \
|
from swift.common.request_helpers import get_object_transient_sysmeta, \
|
||||||
get_sys_meta_prefix, get_user_meta_prefix
|
get_sys_meta_prefix, get_user_meta_prefix
|
||||||
from swift.common.swob import Request, HTTPException, HTTPInternalServerError
|
from swift.common.swob import Request, HTTPException, HTTPInternalServerError
|
||||||
@ -39,11 +39,12 @@ def purge_crypto_sysmeta_headers(headers):
|
|||||||
|
|
||||||
|
|
||||||
class BaseDecrypterContext(CryptoWSGIContext):
|
class BaseDecrypterContext(CryptoWSGIContext):
|
||||||
def get_crypto_meta(self, header_name):
|
def get_crypto_meta(self, header_name, check=True):
|
||||||
"""
|
"""
|
||||||
Extract a crypto_meta dict from a header.
|
Extract a crypto_meta dict from a header.
|
||||||
|
|
||||||
:param header_name: name of header that may have crypto_meta
|
:param header_name: name of header that may have crypto_meta
|
||||||
|
:param check: if True validate the crypto meta
|
||||||
:return: A dict containing crypto_meta items
|
:return: A dict containing crypto_meta items
|
||||||
:raises EncryptionException: if an error occurs while parsing the
|
:raises EncryptionException: if an error occurs while parsing the
|
||||||
crypto meta
|
crypto meta
|
||||||
@ -53,6 +54,7 @@ class BaseDecrypterContext(CryptoWSGIContext):
|
|||||||
if crypto_meta_json is None:
|
if crypto_meta_json is None:
|
||||||
return None
|
return None
|
||||||
crypto_meta = load_crypto_meta(crypto_meta_json)
|
crypto_meta = load_crypto_meta(crypto_meta_json)
|
||||||
|
if check:
|
||||||
self.crypto.check_crypto_meta(crypto_meta)
|
self.crypto.check_crypto_meta(crypto_meta)
|
||||||
return crypto_meta
|
return crypto_meta
|
||||||
|
|
||||||
@ -64,8 +66,8 @@ class BaseDecrypterContext(CryptoWSGIContext):
|
|||||||
:param crypto_meta: a dict of crypto-meta
|
:param crypto_meta: a dict of crypto-meta
|
||||||
:param wrapping_key: key to be used to decrypt the wrapped key
|
:param wrapping_key: key to be used to decrypt the wrapped key
|
||||||
:return: an unwrapped key
|
:return: an unwrapped key
|
||||||
:raises EncryptionException: if the crypto-meta has no wrapped key or
|
:raises HTTPInternalServerError: if the crypto-meta has no wrapped key
|
||||||
the unwrapped key is invalid
|
or the unwrapped key is invalid
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return self.crypto.unwrap_key(wrapping_key,
|
return self.crypto.unwrap_key(wrapping_key,
|
||||||
@ -129,18 +131,20 @@ class BaseDecrypterContext(CryptoWSGIContext):
|
|||||||
key, crypto_meta['iv'], 0)
|
key, crypto_meta['iv'], 0)
|
||||||
return crypto_ctxt.update(base64.b64decode(value))
|
return crypto_ctxt.update(base64.b64decode(value))
|
||||||
|
|
||||||
def get_decryption_keys(self, req):
|
def get_decryption_keys(self, req, crypto_meta=None):
|
||||||
"""
|
"""
|
||||||
Determine if a response should be decrypted, and if so then fetch keys.
|
Determine if a response should be decrypted, and if so then fetch keys.
|
||||||
|
|
||||||
:param req: a Request object
|
:param req: a Request object
|
||||||
|
:param crypto_meta: a dict of crypto metadata
|
||||||
:returns: a dict of decryption keys
|
:returns: a dict of decryption keys
|
||||||
"""
|
"""
|
||||||
if config_true_value(req.environ.get('swift.crypto.override')):
|
if config_true_value(req.environ.get('swift.crypto.override')):
|
||||||
self.logger.debug('No decryption is necessary because of override')
|
self.logger.debug('No decryption is necessary because of override')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self.get_keys(req.environ)
|
key_id = crypto_meta.get('key_id') if crypto_meta else None
|
||||||
|
return self.get_keys(req.environ, key_id=key_id)
|
||||||
|
|
||||||
|
|
||||||
class DecrypterObjContext(BaseDecrypterContext):
|
class DecrypterObjContext(BaseDecrypterContext):
|
||||||
@ -186,11 +190,12 @@ class DecrypterObjContext(BaseDecrypterContext):
|
|||||||
result.append((new_prefix + short_name, decrypted_value))
|
result.append((new_prefix + short_name, decrypted_value))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def decrypt_resp_headers(self, keys):
|
def decrypt_resp_headers(self, put_keys, post_keys):
|
||||||
"""
|
"""
|
||||||
Find encrypted headers and replace with the decrypted versions.
|
Find encrypted headers and replace with the decrypted versions.
|
||||||
|
|
||||||
:param keys: a dict of decryption keys.
|
:param put_keys: a dict of decryption keys used for object PUT.
|
||||||
|
:param post_keys: a dict of decryption keys used for object POST.
|
||||||
:return: A list of headers with any encrypted headers replaced by their
|
:return: A list of headers with any encrypted headers replaced by their
|
||||||
decrypted values.
|
decrypted values.
|
||||||
:raises HTTPInternalServerError: if any error occurs while decrypting
|
:raises HTTPInternalServerError: if any error occurs while decrypting
|
||||||
@ -198,19 +203,22 @@ class DecrypterObjContext(BaseDecrypterContext):
|
|||||||
"""
|
"""
|
||||||
mod_hdr_pairs = []
|
mod_hdr_pairs = []
|
||||||
|
|
||||||
# Decrypt plaintext etag and place in Etag header for client response
|
if put_keys:
|
||||||
|
# Decrypt plaintext etag and place in Etag header for client
|
||||||
|
# response
|
||||||
etag_header = 'X-Object-Sysmeta-Crypto-Etag'
|
etag_header = 'X-Object-Sysmeta-Crypto-Etag'
|
||||||
encrypted_etag = self._response_header_value(etag_header)
|
encrypted_etag = self._response_header_value(etag_header)
|
||||||
if encrypted_etag:
|
if encrypted_etag:
|
||||||
decrypted_etag = self._decrypt_header(
|
decrypted_etag = self._decrypt_header(
|
||||||
etag_header, encrypted_etag, keys['object'], required=True)
|
etag_header, encrypted_etag, put_keys['object'],
|
||||||
|
required=True)
|
||||||
mod_hdr_pairs.append(('Etag', decrypted_etag))
|
mod_hdr_pairs.append(('Etag', decrypted_etag))
|
||||||
|
|
||||||
etag_header = 'X-Object-Sysmeta-Container-Update-Override-Etag'
|
etag_header = 'X-Object-Sysmeta-Container-Update-Override-Etag'
|
||||||
encrypted_etag = self._response_header_value(etag_header)
|
encrypted_etag = self._response_header_value(etag_header)
|
||||||
if encrypted_etag:
|
if encrypted_etag:
|
||||||
decrypted_etag = self._decrypt_header(
|
decrypted_etag = self._decrypt_header(
|
||||||
etag_header, encrypted_etag, keys['container'])
|
etag_header, encrypted_etag, put_keys['container'])
|
||||||
mod_hdr_pairs.append((etag_header, decrypted_etag))
|
mod_hdr_pairs.append((etag_header, decrypted_etag))
|
||||||
|
|
||||||
# Decrypt all user metadata. Encrypted user metadata values are stored
|
# Decrypt all user metadata. Encrypted user metadata values are stored
|
||||||
@ -220,7 +228,8 @@ class DecrypterObjContext(BaseDecrypterContext):
|
|||||||
# if it does then they will be overwritten by any decrypted headers
|
# if it does then they will be overwritten by any decrypted headers
|
||||||
# that map to the same x-object-meta- header names i.e. decrypted
|
# that map to the same x-object-meta- header names i.e. decrypted
|
||||||
# headers win over unexpected, unencrypted headers.
|
# headers win over unexpected, unencrypted headers.
|
||||||
mod_hdr_pairs.extend(self.decrypt_user_metadata(keys))
|
if post_keys:
|
||||||
|
mod_hdr_pairs.extend(self.decrypt_user_metadata(post_keys))
|
||||||
|
|
||||||
mod_hdr_names = {h.lower() for h, v in mod_hdr_pairs}
|
mod_hdr_names = {h.lower() for h, v in mod_hdr_pairs}
|
||||||
mod_hdr_pairs.extend([(h, v) for h, v in self._response_headers
|
mod_hdr_pairs.extend([(h, v) for h, v in self._response_headers
|
||||||
@ -273,31 +282,39 @@ class DecrypterObjContext(BaseDecrypterContext):
|
|||||||
for chunk in resp:
|
for chunk in resp:
|
||||||
yield decrypt_ctxt.update(chunk)
|
yield decrypt_ctxt.update(chunk)
|
||||||
|
|
||||||
|
def _read_crypto_meta(self, header, check):
|
||||||
|
crypto_meta = None
|
||||||
|
if (is_success(self._get_status_int()) or
|
||||||
|
self._get_status_int() in (304, 412)):
|
||||||
|
try:
|
||||||
|
crypto_meta = self.get_crypto_meta(header, check)
|
||||||
|
except EncryptionException as err:
|
||||||
|
self.logger.error(_('Error decrypting object: %s'), err)
|
||||||
|
raise HTTPInternalServerError(
|
||||||
|
body='Error decrypting object', content_type='text/plain')
|
||||||
|
return crypto_meta
|
||||||
|
|
||||||
def handle_get(self, req, start_response):
|
def handle_get(self, req, start_response):
|
||||||
app_resp = self._app_call(req.environ)
|
app_resp = self._app_call(req.environ)
|
||||||
|
|
||||||
keys = self.get_decryption_keys(req)
|
put_crypto_meta = self._read_crypto_meta(
|
||||||
if keys is None:
|
'X-Object-Sysmeta-Crypto-Body-Meta', True)
|
||||||
|
put_keys = self.get_decryption_keys(req, put_crypto_meta)
|
||||||
|
post_crypto_meta = self._read_crypto_meta(
|
||||||
|
'X-Object-Transient-Sysmeta-Crypto-Meta', False)
|
||||||
|
post_keys = self.get_decryption_keys(req, post_crypto_meta)
|
||||||
|
if put_keys is None and post_keys is None:
|
||||||
# skip decryption
|
# skip decryption
|
||||||
start_response(self._response_status, self._response_headers,
|
start_response(self._response_status, self._response_headers,
|
||||||
self._response_exc_info)
|
self._response_exc_info)
|
||||||
return app_resp
|
return app_resp
|
||||||
|
|
||||||
mod_resp_headers = self.decrypt_resp_headers(keys)
|
mod_resp_headers = self.decrypt_resp_headers(put_keys, post_keys)
|
||||||
|
|
||||||
crypto_meta = None
|
if put_crypto_meta and is_success(self._get_status_int()):
|
||||||
if is_success(self._get_status_int()):
|
|
||||||
try:
|
|
||||||
crypto_meta = self.get_crypto_meta(
|
|
||||||
'X-Object-Sysmeta-Crypto-Body-Meta')
|
|
||||||
except EncryptionException as err:
|
|
||||||
self.logger.error(_('Error decrypting object: %s'), err)
|
|
||||||
raise HTTPInternalServerError(
|
|
||||||
body='Error decrypting object', content_type='text/plain')
|
|
||||||
|
|
||||||
if crypto_meta:
|
|
||||||
# 2xx response and encrypted body
|
# 2xx response and encrypted body
|
||||||
body_key = self.get_unwrapped_key(crypto_meta, keys['object'])
|
body_key = self.get_unwrapped_key(
|
||||||
|
put_crypto_meta, put_keys['object'])
|
||||||
content_type, content_type_attrs = parse_content_type(
|
content_type, content_type_attrs = parse_content_type(
|
||||||
self._response_header_value('Content-Type'))
|
self._response_header_value('Content-Type'))
|
||||||
|
|
||||||
@ -305,7 +322,7 @@ class DecrypterObjContext(BaseDecrypterContext):
|
|||||||
content_type == 'multipart/byteranges'):
|
content_type == 'multipart/byteranges'):
|
||||||
boundary = dict(content_type_attrs)["boundary"]
|
boundary = dict(content_type_attrs)["boundary"]
|
||||||
resp_iter = self.multipart_response_iter(
|
resp_iter = self.multipart_response_iter(
|
||||||
app_resp, boundary, body_key, crypto_meta)
|
app_resp, boundary, body_key, put_crypto_meta)
|
||||||
else:
|
else:
|
||||||
offset = 0
|
offset = 0
|
||||||
content_range = self._response_header_value('Content-Range')
|
content_range = self._response_header_value('Content-Range')
|
||||||
@ -313,7 +330,7 @@ class DecrypterObjContext(BaseDecrypterContext):
|
|||||||
# Determine offset within the whole object if ranged GET
|
# Determine offset within the whole object if ranged GET
|
||||||
offset, end, total = parse_content_range(content_range)
|
offset, end, total = parse_content_range(content_range)
|
||||||
resp_iter = self.response_iter(
|
resp_iter = self.response_iter(
|
||||||
app_resp, body_key, crypto_meta, offset)
|
app_resp, body_key, put_crypto_meta, offset)
|
||||||
else:
|
else:
|
||||||
# don't decrypt body of unencrypted or non-2xx responses
|
# don't decrypt body of unencrypted or non-2xx responses
|
||||||
resp_iter = app_resp
|
resp_iter = app_resp
|
||||||
@ -326,15 +343,19 @@ class DecrypterObjContext(BaseDecrypterContext):
|
|||||||
|
|
||||||
def handle_head(self, req, start_response):
|
def handle_head(self, req, start_response):
|
||||||
app_resp = self._app_call(req.environ)
|
app_resp = self._app_call(req.environ)
|
||||||
|
put_crypto_meta = self._read_crypto_meta(
|
||||||
|
'X-Object-Sysmeta-Crypto-Body-Meta', True)
|
||||||
|
put_keys = self.get_decryption_keys(req, put_crypto_meta)
|
||||||
|
post_crypto_meta = self._read_crypto_meta(
|
||||||
|
'X-Object-Transient-Sysmeta-Crypto-Meta', False)
|
||||||
|
post_keys = self.get_decryption_keys(req, post_crypto_meta)
|
||||||
|
|
||||||
keys = self.get_decryption_keys(req)
|
if put_keys is None and post_keys is None:
|
||||||
|
|
||||||
if keys is None:
|
|
||||||
# skip decryption
|
# skip decryption
|
||||||
start_response(self._response_status, self._response_headers,
|
start_response(self._response_status, self._response_headers,
|
||||||
self._response_exc_info)
|
self._response_exc_info)
|
||||||
else:
|
else:
|
||||||
mod_resp_headers = self.decrypt_resp_headers(keys)
|
mod_resp_headers = self.decrypt_resp_headers(put_keys, post_keys)
|
||||||
mod_resp_headers = purge_crypto_sysmeta_headers(mod_resp_headers)
|
mod_resp_headers = purge_crypto_sysmeta_headers(mod_resp_headers)
|
||||||
start_response(self._response_status, mod_resp_headers,
|
start_response(self._response_status, mod_resp_headers,
|
||||||
self._response_exc_info)
|
self._response_exc_info)
|
||||||
@ -352,19 +373,18 @@ class DecrypterContContext(BaseDecrypterContext):
|
|||||||
|
|
||||||
if is_success(self._get_status_int()):
|
if is_success(self._get_status_int()):
|
||||||
# only decrypt body of 2xx responses
|
# only decrypt body of 2xx responses
|
||||||
handler = keys = None
|
handler = None
|
||||||
for header, value in self._response_headers:
|
for header, value in self._response_headers:
|
||||||
if header.lower() == 'content-type' and \
|
if header.lower() == 'content-type' and \
|
||||||
value.split(';', 1)[0] == 'application/json':
|
value.split(';', 1)[0] == 'application/json':
|
||||||
handler = self.process_json_resp
|
handler = self.process_json_resp
|
||||||
keys = self.get_decryption_keys(req)
|
|
||||||
|
|
||||||
if handler and keys:
|
if handler:
|
||||||
try:
|
try:
|
||||||
app_resp = handler(keys['container'], app_resp)
|
app_resp = handler(req, app_resp)
|
||||||
except EncryptionException as err:
|
except EncryptionException as err:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
_("Error decrypting container listing: %s"),
|
"Error decrypting container listing: %s",
|
||||||
err)
|
err)
|
||||||
raise HTTPInternalServerError(
|
raise HTTPInternalServerError(
|
||||||
body='Error decrypting container listing',
|
body='Error decrypting container listing',
|
||||||
@ -376,7 +396,7 @@ class DecrypterContContext(BaseDecrypterContext):
|
|||||||
|
|
||||||
return app_resp
|
return app_resp
|
||||||
|
|
||||||
def process_json_resp(self, key, resp_iter):
|
def process_json_resp(self, req, resp_iter):
|
||||||
"""
|
"""
|
||||||
Parses json body listing and decrypt encrypted entries. Updates
|
Parses json body listing and decrypt encrypted entries. Updates
|
||||||
Content-Length header with new body length and return a body iter.
|
Content-Length header with new body length and return a body iter.
|
||||||
@ -384,15 +404,33 @@ class DecrypterContContext(BaseDecrypterContext):
|
|||||||
with closing_if_possible(resp_iter):
|
with closing_if_possible(resp_iter):
|
||||||
resp_body = ''.join(resp_iter)
|
resp_body = ''.join(resp_iter)
|
||||||
body_json = json.loads(resp_body)
|
body_json = json.loads(resp_body)
|
||||||
new_body = json.dumps([self.decrypt_obj_dict(obj_dict, key)
|
new_body = json.dumps([self.decrypt_obj_dict(req, obj_dict)
|
||||||
for obj_dict in body_json])
|
for obj_dict in body_json])
|
||||||
self.update_content_length(len(new_body))
|
self.update_content_length(len(new_body))
|
||||||
return [new_body]
|
return [new_body]
|
||||||
|
|
||||||
def decrypt_obj_dict(self, obj_dict, key):
|
def decrypt_obj_dict(self, req, obj_dict):
|
||||||
if 'hash' in obj_dict:
|
if 'hash' in obj_dict:
|
||||||
ciphertext = obj_dict['hash']
|
# each object's etag may have been encrypted with a different key
|
||||||
obj_dict['hash'] = self.decrypt_value_with_meta(ciphertext, key)
|
# so fetch keys based on its crypto meta
|
||||||
|
ciphertext, crypto_meta = extract_crypto_meta(obj_dict['hash'])
|
||||||
|
bad_keys = set()
|
||||||
|
if crypto_meta:
|
||||||
|
try:
|
||||||
|
self.crypto.check_crypto_meta(crypto_meta)
|
||||||
|
keys = self.get_decryption_keys(req, crypto_meta)
|
||||||
|
obj_dict['hash'] = self.decrypt_value(
|
||||||
|
ciphertext, keys['container'], crypto_meta)
|
||||||
|
except EncryptionException as err:
|
||||||
|
if not isinstance(err, UnknownSecretIdError) or \
|
||||||
|
err.args[0] not in bad_keys:
|
||||||
|
# Only warn about an unknown key once per listing
|
||||||
|
self.logger.error(
|
||||||
|
"Error decrypting container listing: %s",
|
||||||
|
err)
|
||||||
|
if isinstance(err, UnknownSecretIdError):
|
||||||
|
bad_keys.add(err.args[0])
|
||||||
|
obj_dict['hash'] = '<unknown>'
|
||||||
return obj_dict
|
return obj_dict
|
||||||
|
|
||||||
|
|
||||||
|
@ -287,6 +287,9 @@ class EncrypterObjContext(CryptoWSGIContext):
|
|||||||
plaintext etag to generate the value of
|
plaintext etag to generate the value of
|
||||||
X-Object-Sysmeta-Crypto-Etag-Mac when the object was PUT. The object
|
X-Object-Sysmeta-Crypto-Etag-Mac when the object was PUT. The object
|
||||||
server can therefore use these HMACs to evaluate conditional requests.
|
server can therefore use these HMACs to evaluate conditional requests.
|
||||||
|
HMACs of the etags are appended for the current root secrets and
|
||||||
|
historic root secrets because it is not known which of them may have
|
||||||
|
been used to generate the on-disk etag HMAC.
|
||||||
|
|
||||||
The existing etag values are left in the list of values to match in
|
The existing etag values are left in the list of values to match in
|
||||||
case the object was not encrypted when it was PUT. It is unlikely that
|
case the object was not encrypted when it was PUT. It is unlikely that
|
||||||
@ -299,14 +302,16 @@ class EncrypterObjContext(CryptoWSGIContext):
|
|||||||
masked = False
|
masked = False
|
||||||
old_etags = req.headers.get(header_name)
|
old_etags = req.headers.get(header_name)
|
||||||
if old_etags:
|
if old_etags:
|
||||||
keys = self.get_keys(req.environ)
|
all_keys = self.get_multiple_keys(req.environ)
|
||||||
new_etags = []
|
new_etags = []
|
||||||
for etag in Match(old_etags).tags:
|
for etag in Match(old_etags).tags:
|
||||||
if etag == '*':
|
if etag == '*':
|
||||||
new_etags.append(etag)
|
new_etags.append(etag)
|
||||||
continue
|
continue
|
||||||
|
new_etags.append('"%s"' % etag)
|
||||||
|
for keys in all_keys:
|
||||||
masked_etag = _hmac_etag(keys['object'], etag)
|
masked_etag = _hmac_etag(keys['object'], etag)
|
||||||
new_etags.extend(('"%s"' % etag, '"%s"' % masked_etag))
|
new_etags.append('"%s"' % masked_etag)
|
||||||
masked = True
|
masked = True
|
||||||
|
|
||||||
req.headers[header_name] = ', '.join(new_etags)
|
req.headers[header_name] = ', '.join(new_etags)
|
||||||
|
@ -16,6 +16,7 @@ import hashlib
|
|||||||
import hmac
|
import hmac
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from swift.common.exceptions import UnknownSecretIdError
|
||||||
from swift.common.middleware.crypto.crypto_utils import CRYPTO_KEY_CALLBACK
|
from swift.common.middleware.crypto.crypto_utils import CRYPTO_KEY_CALLBACK
|
||||||
from swift.common.swob import Request, HTTPException
|
from swift.common.swob import Request, HTTPException
|
||||||
from swift.common.utils import readconf, strict_b64decode, get_logger
|
from swift.common.utils import readconf, strict_b64decode, get_logger
|
||||||
@ -44,9 +45,17 @@ class KeyMasterContext(WSGIContext):
|
|||||||
self.account = account
|
self.account = account
|
||||||
self.container = container
|
self.container = container
|
||||||
self.obj = obj
|
self.obj = obj
|
||||||
self._keys = None
|
self._keys = {}
|
||||||
|
|
||||||
def fetch_crypto_keys(self, *args, **kwargs):
|
def _make_key_id(self, path, secret_id):
|
||||||
|
key_id = {'v': '1', '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.
|
Setup container and object keys based on the request path.
|
||||||
|
|
||||||
@ -56,22 +65,32 @@ class KeyMasterContext(WSGIContext):
|
|||||||
include a different type of 'id', so callers should treat the 'id' as
|
include a different type of 'id', so callers should treat the 'id' as
|
||||||
opaque keymaster-specific data.
|
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
|
:returns: A dict containing encryption keys for 'object' and
|
||||||
'container' and a key 'id'.
|
'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 self._keys:
|
if key_id:
|
||||||
return self._keys
|
secret_id = key_id.get('secret_id')
|
||||||
|
else:
|
||||||
|
secret_id = self.keymaster.active_secret_id
|
||||||
|
if secret_id in self._keys:
|
||||||
|
return self._keys[secret_id]
|
||||||
|
|
||||||
self._keys = {}
|
keys = {}
|
||||||
account_path = os.path.join(os.sep, self.account)
|
account_path = os.path.join(os.sep, self.account)
|
||||||
|
|
||||||
if self.container:
|
if self.container:
|
||||||
path = os.path.join(account_path, self.container)
|
path = os.path.join(account_path, self.container)
|
||||||
self._keys['container'] = self.keymaster.create_key(path)
|
keys['container'] = self.keymaster.create_key(
|
||||||
|
path, secret_id=secret_id)
|
||||||
|
|
||||||
if self.obj:
|
if self.obj:
|
||||||
path = os.path.join(path, self.obj)
|
path = os.path.join(path, self.obj)
|
||||||
self._keys['object'] = self.keymaster.create_key(path)
|
keys['object'] = self.keymaster.create_key(
|
||||||
|
path, secret_id=secret_id)
|
||||||
|
|
||||||
# For future-proofing include a keymaster version number and the
|
# For future-proofing include a keymaster version number and the
|
||||||
# path used to derive keys in the 'id' entry of the results. The
|
# path used to derive keys in the 'id' entry of the results. The
|
||||||
@ -82,9 +101,18 @@ class KeyMasterContext(WSGIContext):
|
|||||||
# that particular data or metadata had its keys generated.
|
# that particular data or metadata had its keys generated.
|
||||||
# Currently we have no need to do that, so we are simply persisting
|
# Currently we have no need to do that, so we are simply persisting
|
||||||
# this information for future use.
|
# this information for future use.
|
||||||
self._keys['id'] = {'v': '1', 'path': path}
|
keys['id'] = self._make_key_id(path, secret_id)
|
||||||
|
# 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_)
|
||||||
|
for id_ in self.keymaster.root_secret_ids]
|
||||||
|
self._keys[secret_id] = keys
|
||||||
|
|
||||||
return self._keys
|
return keys
|
||||||
|
|
||||||
def handle_request(self, req, start_response):
|
def handle_request(self, req, start_response):
|
||||||
req.environ[CRYPTO_KEY_CALLBACK] = self.fetch_crypto_keys
|
req.environ[CRYPTO_KEY_CALLBACK] = self.fetch_crypto_keys
|
||||||
@ -97,14 +125,14 @@ class KeyMasterContext(WSGIContext):
|
|||||||
class KeyMaster(object):
|
class KeyMaster(object):
|
||||||
"""Middleware for providing encryption keys.
|
"""Middleware for providing encryption keys.
|
||||||
|
|
||||||
The middleware requires its encryption root secret to be set. This is the
|
The middleware requires at least one encryption root secret(s) to be set.
|
||||||
root secret from which encryption keys are derived. This must be set before
|
This is the root secret from which encryption keys are derived. This must
|
||||||
first use to a value that is at least 256 bits. The security of all
|
be set before first use to a value that is at least 256 bits. The security
|
||||||
encrypted data critically depends on this key, therefore it should be set
|
of all encrypted data critically depends on this key, therefore it should
|
||||||
to a high-entropy value. For example, a suitable value may be obtained by
|
be set to a high-entropy value. For example, a suitable value may be
|
||||||
generating a 32 byte (or longer) value using a cryptographically secure
|
obtained by generating a 32 byte (or longer) value using a
|
||||||
random number generator. Changing the root secret is likely to result in
|
cryptographically secure random number generator. Changing the root secret
|
||||||
data loss.
|
is likely to result in data loss.
|
||||||
"""
|
"""
|
||||||
log_route = 'keymaster'
|
log_route = 'keymaster'
|
||||||
keymaster_opts = ()
|
keymaster_opts = ()
|
||||||
@ -115,12 +143,30 @@ class KeyMaster(object):
|
|||||||
self.logger = get_logger(conf, log_route=self.log_route)
|
self.logger = get_logger(conf, log_route=self.log_route)
|
||||||
self.keymaster_config_path = conf.get('keymaster_config_path')
|
self.keymaster_config_path = conf.get('keymaster_config_path')
|
||||||
if type(self) is KeyMaster:
|
if type(self) is KeyMaster:
|
||||||
self.keymaster_opts = ('encryption_root_secret', )
|
self.keymaster_opts = ('encryption_root_secret*',
|
||||||
|
'active_root_secret_id')
|
||||||
if self.keymaster_config_path:
|
if self.keymaster_config_path:
|
||||||
conf = self._load_keymaster_config_file(conf)
|
conf = self._load_keymaster_config_file(conf)
|
||||||
|
|
||||||
# The _get_root_secret() function is overridden by other keymasters
|
# The _get_root_secret() function is overridden by other keymasters
|
||||||
self.root_secret = self._get_root_secret(conf)
|
# 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)
|
||||||
|
|
||||||
|
@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):
|
def _load_keymaster_config_file(self, conf):
|
||||||
# Keymaster options specified in the filter section would be ignored if
|
# Keymaster options specified in the filter section would be ignored if
|
||||||
@ -129,7 +175,8 @@ class KeyMaster(object):
|
|||||||
bad_opts = []
|
bad_opts = []
|
||||||
for opt in conf:
|
for opt in conf:
|
||||||
for km_opt in self.keymaster_opts:
|
for km_opt in self.keymaster_opts:
|
||||||
if opt == km_opt:
|
if ((km_opt.endswith('*') and opt.startswith(km_opt[:-1])) or
|
||||||
|
opt == km_opt):
|
||||||
bad_opts.append(opt)
|
bad_opts.append(opt)
|
||||||
if bad_opts:
|
if bad_opts:
|
||||||
raise ValueError('keymaster_config_path is set, but there '
|
raise ValueError('keymaster_config_path is set, but there '
|
||||||
@ -138,32 +185,51 @@ class KeyMaster(object):
|
|||||||
return readconf(self.keymaster_config_path,
|
return readconf(self.keymaster_config_path,
|
||||||
self.keymaster_conf_section)
|
self.keymaster_conf_section)
|
||||||
|
|
||||||
def _get_root_secret(self, conf):
|
def _decode_root_secret(self, b64_root_secret):
|
||||||
"""
|
|
||||||
This keymaster requires its ``encryption_root_secret`` option to be
|
|
||||||
set. This must be set before first use to a value that is a base64
|
|
||||||
encoding of at least 32 bytes. The encryption root secret is stored
|
|
||||||
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: the encryption root secret binary bytes
|
|
||||||
:rtype: bytearray
|
|
||||||
"""
|
|
||||||
b64_root_secret = conf.get('encryption_root_secret')
|
|
||||||
try:
|
|
||||||
binary_root_secret = strict_b64decode(b64_root_secret,
|
binary_root_secret = strict_b64decode(b64_root_secret,
|
||||||
allow_line_breaks=True)
|
allow_line_breaks=True)
|
||||||
if len(binary_root_secret) < 32:
|
if len(binary_root_secret) < 32:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
return binary_root_secret
|
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:
|
except ValueError:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'encryption_root_secret option in %s must be a base64 '
|
'%s option in %s must be a base64 encoding of at '
|
||||||
'encoding of at least 32 raw bytes' % (
|
'least 32 raw bytes' %
|
||||||
self.keymaster_config_path or 'proxy-server.conf'))
|
(opt, self.keymaster_config_path or 'proxy-server.conf'))
|
||||||
|
root_secrets[secret_id] = secret
|
||||||
|
return root_secrets
|
||||||
|
|
||||||
def __call__(self, env, start_response):
|
def __call__(self, env, start_response):
|
||||||
req = Request(env)
|
req = Request(env)
|
||||||
@ -184,9 +250,23 @@ class KeyMaster(object):
|
|||||||
# anything else
|
# anything else
|
||||||
return self.app(env, start_response)
|
return self.app(env, start_response)
|
||||||
|
|
||||||
def create_key(self, key_id):
|
def create_key(self, path, secret_id=None):
|
||||||
return hmac.new(self.root_secret, key_id,
|
"""
|
||||||
digestmod=hashlib.sha256).digest()
|
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):
|
def filter_factory(global_conf, **local_conf):
|
||||||
|
@ -15,14 +15,29 @@
|
|||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
|
from swift.common.exceptions import UnknownSecretIdError
|
||||||
from swift.common.middleware.crypto.crypto_utils import Crypto
|
from swift.common.middleware.crypto.crypto_utils import Crypto
|
||||||
|
|
||||||
|
|
||||||
def fetch_crypto_keys():
|
def fetch_crypto_keys(key_id=None):
|
||||||
return {'account': 'This is an account key 012345678',
|
id_to_keys = {None: {'account': 'This is an account key 012345678',
|
||||||
'container': 'This is a container key 01234567',
|
'container': 'This is a container key 01234567',
|
||||||
'object': 'This is an object key 0123456789',
|
'object': 'This is an object key 0123456789'},
|
||||||
'id': {'v': 'fake', 'path': '/a/c/fake'}}
|
'myid': {'account': 'This is an account key 123456789',
|
||||||
|
'container': 'This is a container key 12345678',
|
||||||
|
'object': 'This is an object key 1234567890'}}
|
||||||
|
key_id = key_id or {}
|
||||||
|
secret_id = key_id.get('secret_id') or None
|
||||||
|
try:
|
||||||
|
keys = dict(id_to_keys[secret_id])
|
||||||
|
except KeyError:
|
||||||
|
raise UnknownSecretIdError(secret_id)
|
||||||
|
keys['id'] = {'v': 'fake', 'path': '/a/c/fake'}
|
||||||
|
if secret_id:
|
||||||
|
keys['id']['secret_id'] = secret_id
|
||||||
|
keys['all_ids'] = [{'v': 'fake', 'path': '/a/c/fake'},
|
||||||
|
{'v': 'fake', 'path': '/a/c/fake', 'secret_id': 'myid'}]
|
||||||
|
return keys
|
||||||
|
|
||||||
|
|
||||||
def md5hex(s):
|
def md5hex(s):
|
||||||
@ -45,7 +60,11 @@ def decrypt(key, iv, enc_val):
|
|||||||
FAKE_IV = "This is an IV123"
|
FAKE_IV = "This is an IV123"
|
||||||
# do not use this example encryption_root_secret in production, use a randomly
|
# do not use this example encryption_root_secret in production, use a randomly
|
||||||
# generated value with high entropy
|
# generated value with high entropy
|
||||||
TEST_KEYMASTER_CONF = {'encryption_root_secret': base64.b64encode(b'x' * 32)}
|
TEST_KEYMASTER_CONF = {
|
||||||
|
'encryption_root_secret': base64.b64encode(b'x' * 32),
|
||||||
|
'encryption_root_secret_1': base64.b64encode(b'y' * 32),
|
||||||
|
'encryption_root_secret_2': base64.b64encode(b'z' * 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def fake_get_crypto_meta(**kwargs):
|
def fake_get_crypto_meta(**kwargs):
|
||||||
|
@ -46,23 +46,41 @@ class TestCryptoWsgiContext(unittest.TestCase):
|
|||||||
|
|
||||||
# only default required keys are checked
|
# only default required keys are checked
|
||||||
subset_keys = {'object': fetch_crypto_keys()['object']}
|
subset_keys = {'object': fetch_crypto_keys()['object']}
|
||||||
env = {CRYPTO_KEY_CALLBACK: lambda: subset_keys}
|
env = {CRYPTO_KEY_CALLBACK: lambda *args, **kwargs: subset_keys}
|
||||||
keys = self.crypto_context.get_keys(env)
|
keys = self.crypto_context.get_keys(env)
|
||||||
self.assertDictEqual(subset_keys, keys)
|
self.assertDictEqual(subset_keys, keys)
|
||||||
|
|
||||||
# only specified required keys are checked
|
# only specified required keys are checked
|
||||||
subset_keys = {'container': fetch_crypto_keys()['container']}
|
subset_keys = {'container': fetch_crypto_keys()['container']}
|
||||||
env = {CRYPTO_KEY_CALLBACK: lambda: subset_keys}
|
env = {CRYPTO_KEY_CALLBACK: lambda *args, **kwargs: subset_keys}
|
||||||
keys = self.crypto_context.get_keys(env, required=['container'])
|
keys = self.crypto_context.get_keys(env, required=['container'])
|
||||||
self.assertDictEqual(subset_keys, keys)
|
self.assertDictEqual(subset_keys, keys)
|
||||||
|
|
||||||
subset_keys = {'object': fetch_crypto_keys()['object'],
|
subset_keys = {'object': fetch_crypto_keys()['object'],
|
||||||
'container': fetch_crypto_keys()['container']}
|
'container': fetch_crypto_keys()['container']}
|
||||||
env = {CRYPTO_KEY_CALLBACK: lambda: subset_keys}
|
env = {CRYPTO_KEY_CALLBACK: lambda *args, **kwargs: subset_keys}
|
||||||
keys = self.crypto_context.get_keys(
|
keys = self.crypto_context.get_keys(
|
||||||
env, required=['object', 'container'])
|
env, required=['object', 'container'])
|
||||||
self.assertDictEqual(subset_keys, keys)
|
self.assertDictEqual(subset_keys, keys)
|
||||||
|
|
||||||
|
def test_get_keys_with_crypto_meta(self):
|
||||||
|
# verify that key_id from crypto_meta is passed to fetch_crypto_keys
|
||||||
|
keys = fetch_crypto_keys()
|
||||||
|
mock_fetch_crypto_keys = mock.MagicMock(return_value=keys)
|
||||||
|
env = {CRYPTO_KEY_CALLBACK: mock_fetch_crypto_keys}
|
||||||
|
key_id = {'secret_id': '123'}
|
||||||
|
keys = self.crypto_context.get_keys(env, key_id=key_id)
|
||||||
|
self.assertDictEqual(fetch_crypto_keys(), keys)
|
||||||
|
mock_fetch_crypto_keys.assert_called_with(key_id={'secret_id': '123'})
|
||||||
|
|
||||||
|
# but it's ok for there to be no crypto_meta
|
||||||
|
keys = self.crypto_context.get_keys(env, key_id={})
|
||||||
|
self.assertDictEqual(fetch_crypto_keys(), keys)
|
||||||
|
mock_fetch_crypto_keys.assert_called_with(key_id={})
|
||||||
|
keys = self.crypto_context.get_keys(env)
|
||||||
|
self.assertDictEqual(fetch_crypto_keys(), keys)
|
||||||
|
mock_fetch_crypto_keys.assert_called_with(key_id=None)
|
||||||
|
|
||||||
def test_get_keys_missing_callback(self):
|
def test_get_keys_missing_callback(self):
|
||||||
with self.assertRaises(HTTPException) as cm:
|
with self.assertRaises(HTTPException) as cm:
|
||||||
self.crypto_context.get_keys({})
|
self.crypto_context.get_keys({})
|
||||||
@ -72,7 +90,7 @@ class TestCryptoWsgiContext(unittest.TestCase):
|
|||||||
self.assertIn('Unable to retrieve encryption keys.', cm.exception.body)
|
self.assertIn('Unable to retrieve encryption keys.', cm.exception.body)
|
||||||
|
|
||||||
def test_get_keys_callback_exception(self):
|
def test_get_keys_callback_exception(self):
|
||||||
def callback():
|
def callback(*args, **kwargs):
|
||||||
raise Exception('boom')
|
raise Exception('boom')
|
||||||
with self.assertRaises(HTTPException) as cm:
|
with self.assertRaises(HTTPException) as cm:
|
||||||
self.crypto_context.get_keys({CRYPTO_KEY_CALLBACK: callback})
|
self.crypto_context.get_keys({CRYPTO_KEY_CALLBACK: callback})
|
||||||
@ -86,7 +104,7 @@ class TestCryptoWsgiContext(unittest.TestCase):
|
|||||||
bad_keys.pop('object')
|
bad_keys.pop('object')
|
||||||
with self.assertRaises(HTTPException) as cm:
|
with self.assertRaises(HTTPException) as cm:
|
||||||
self.crypto_context.get_keys(
|
self.crypto_context.get_keys(
|
||||||
{CRYPTO_KEY_CALLBACK: lambda: bad_keys})
|
{CRYPTO_KEY_CALLBACK: lambda *args, **kwargs: bad_keys})
|
||||||
self.assertIn('500 Internal Error', cm.exception.message)
|
self.assertIn('500 Internal Error', cm.exception.message)
|
||||||
self.assertIn("Missing key for 'object'",
|
self.assertIn("Missing key for 'object'",
|
||||||
self.fake_logger.get_lines_for_level('error')[0])
|
self.fake_logger.get_lines_for_level('error')[0])
|
||||||
@ -97,7 +115,7 @@ class TestCryptoWsgiContext(unittest.TestCase):
|
|||||||
bad_keys.pop('object')
|
bad_keys.pop('object')
|
||||||
with self.assertRaises(HTTPException) as cm:
|
with self.assertRaises(HTTPException) as cm:
|
||||||
self.crypto_context.get_keys(
|
self.crypto_context.get_keys(
|
||||||
{CRYPTO_KEY_CALLBACK: lambda: bad_keys},
|
{CRYPTO_KEY_CALLBACK: lambda *args, **kwargs: bad_keys},
|
||||||
required=['object', 'container'])
|
required=['object', 'container'])
|
||||||
self.assertIn('500 Internal Error', cm.exception.message)
|
self.assertIn('500 Internal Error', cm.exception.message)
|
||||||
self.assertIn("Missing key for 'object'",
|
self.assertIn("Missing key for 'object'",
|
||||||
@ -109,7 +127,7 @@ class TestCryptoWsgiContext(unittest.TestCase):
|
|||||||
bad_keys.pop('container')
|
bad_keys.pop('container')
|
||||||
with self.assertRaises(HTTPException) as cm:
|
with self.assertRaises(HTTPException) as cm:
|
||||||
self.crypto_context.get_keys(
|
self.crypto_context.get_keys(
|
||||||
{CRYPTO_KEY_CALLBACK: lambda: bad_keys},
|
{CRYPTO_KEY_CALLBACK: lambda *args, **kwargs: bad_keys},
|
||||||
required=['object', 'container'])
|
required=['object', 'container'])
|
||||||
self.assertIn('500 Internal Error', cm.exception.message)
|
self.assertIn('500 Internal Error', cm.exception.message)
|
||||||
self.assertIn("Missing key for 'container'",
|
self.assertIn("Missing key for 'container'",
|
||||||
@ -121,7 +139,7 @@ class TestCryptoWsgiContext(unittest.TestCase):
|
|||||||
bad_keys['object'] = 'the minor key'
|
bad_keys['object'] = 'the minor key'
|
||||||
with self.assertRaises(HTTPException) as cm:
|
with self.assertRaises(HTTPException) as cm:
|
||||||
self.crypto_context.get_keys(
|
self.crypto_context.get_keys(
|
||||||
{CRYPTO_KEY_CALLBACK: lambda: bad_keys})
|
{CRYPTO_KEY_CALLBACK: lambda *args, **kwargs: bad_keys})
|
||||||
self.assertIn('500 Internal Error', cm.exception.message)
|
self.assertIn('500 Internal Error', cm.exception.message)
|
||||||
self.assertIn("Bad key for 'object'",
|
self.assertIn("Bad key for 'object'",
|
||||||
self.fake_logger.get_lines_for_level('error')[0])
|
self.fake_logger.get_lines_for_level('error')[0])
|
||||||
@ -132,7 +150,7 @@ class TestCryptoWsgiContext(unittest.TestCase):
|
|||||||
bad_keys['container'] = 'the major key'
|
bad_keys['container'] = 'the major key'
|
||||||
with self.assertRaises(HTTPException) as cm:
|
with self.assertRaises(HTTPException) as cm:
|
||||||
self.crypto_context.get_keys(
|
self.crypto_context.get_keys(
|
||||||
{CRYPTO_KEY_CALLBACK: lambda: bad_keys},
|
{CRYPTO_KEY_CALLBACK: lambda *args, **kwargs: bad_keys},
|
||||||
required=['object', 'container'])
|
required=['object', 'container'])
|
||||||
self.assertIn('500 Internal Error', cm.exception.message)
|
self.assertIn('500 Internal Error', cm.exception.message)
|
||||||
self.assertIn("Bad key for 'container'",
|
self.assertIn("Bad key for 'container'",
|
||||||
@ -142,12 +160,21 @@ class TestCryptoWsgiContext(unittest.TestCase):
|
|||||||
def test_get_keys_not_a_dict(self):
|
def test_get_keys_not_a_dict(self):
|
||||||
with self.assertRaises(HTTPException) as cm:
|
with self.assertRaises(HTTPException) as cm:
|
||||||
self.crypto_context.get_keys(
|
self.crypto_context.get_keys(
|
||||||
{CRYPTO_KEY_CALLBACK: lambda: ['key', 'quay', 'qui']})
|
{CRYPTO_KEY_CALLBACK:
|
||||||
|
lambda *args, **kwargs: ['key', 'quay', 'qui']})
|
||||||
self.assertIn('500 Internal Error', cm.exception.message)
|
self.assertIn('500 Internal Error', cm.exception.message)
|
||||||
self.assertIn("Did not get a keys dict",
|
self.assertIn("Did not get a keys dict",
|
||||||
self.fake_logger.get_lines_for_level('error')[0])
|
self.fake_logger.get_lines_for_level('error')[0])
|
||||||
self.assertIn('Unable to retrieve encryption keys.', cm.exception.body)
|
self.assertIn('Unable to retrieve encryption keys.', cm.exception.body)
|
||||||
|
|
||||||
|
def test_get_multiple_keys(self):
|
||||||
|
env = {CRYPTO_KEY_CALLBACK: fetch_crypto_keys}
|
||||||
|
mutliple_keys = self.crypto_context.get_multiple_keys(env)
|
||||||
|
self.assertEqual(
|
||||||
|
[fetch_crypto_keys(),
|
||||||
|
fetch_crypto_keys(key_id={'secret_id': 'myid'})],
|
||||||
|
mutliple_keys)
|
||||||
|
|
||||||
|
|
||||||
class TestModuleMethods(unittest.TestCase):
|
class TestModuleMethods(unittest.TestCase):
|
||||||
meta = {'iv': '0123456789abcdef', 'cipher': 'AES_CTR_256'}
|
meta = {'iv': '0123456789abcdef', 'cipher': 'AES_CTR_256'}
|
||||||
|
@ -19,11 +19,12 @@ import unittest
|
|||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
|
from swift.common.request_helpers import is_object_transient_sysmeta
|
||||||
from swift.common.utils import MD5_OF_EMPTY_STRING
|
from swift.common.utils import MD5_OF_EMPTY_STRING
|
||||||
from swift.common.header_key_dict import HeaderKeyDict
|
from swift.common.header_key_dict import HeaderKeyDict
|
||||||
from swift.common.middleware.crypto import decrypter
|
from swift.common.middleware.crypto import decrypter
|
||||||
from swift.common.middleware.crypto.crypto_utils import CRYPTO_KEY_CALLBACK, \
|
from swift.common.middleware.crypto.crypto_utils import CRYPTO_KEY_CALLBACK, \
|
||||||
dump_crypto_meta, Crypto
|
dump_crypto_meta, Crypto, load_crypto_meta
|
||||||
from swift.common.swob import Request, HTTPException, HTTPOk, \
|
from swift.common.swob import Request, HTTPException, HTTPOk, \
|
||||||
HTTPPreconditionFailed, HTTPNotFound, HTTPPartialContent
|
HTTPPreconditionFailed, HTTPNotFound, HTTPPartialContent
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ class TestDecrypterObjectRequests(unittest.TestCase):
|
|||||||
self.decrypter.logger = FakeLogger()
|
self.decrypter.logger = FakeLogger()
|
||||||
|
|
||||||
def _make_response_headers(self, content_length, plaintext_etag, keys,
|
def _make_response_headers(self, content_length, plaintext_etag, keys,
|
||||||
body_key):
|
body_key, key_id=None):
|
||||||
# helper method to make a typical set of response headers for a GET or
|
# helper method to make a typical set of response headers for a GET or
|
||||||
# HEAD request
|
# HEAD request
|
||||||
cont_key = keys['container']
|
cont_key = keys['container']
|
||||||
@ -60,24 +61,30 @@ class TestDecrypterObjectRequests(unittest.TestCase):
|
|||||||
body_key_meta = {'key': encrypt(body_key, object_key, FAKE_IV),
|
body_key_meta = {'key': encrypt(body_key, object_key, FAKE_IV),
|
||||||
'iv': FAKE_IV}
|
'iv': FAKE_IV}
|
||||||
body_crypto_meta = fake_get_crypto_meta(body_key=body_key_meta)
|
body_crypto_meta = fake_get_crypto_meta(body_key=body_key_meta)
|
||||||
|
other_crypto_meta = fake_get_crypto_meta()
|
||||||
|
if key_id:
|
||||||
|
body_crypto_meta['key_id'] = key_id
|
||||||
|
other_crypto_meta['key_id'] = key_id
|
||||||
return HeaderKeyDict({
|
return HeaderKeyDict({
|
||||||
'Etag': 'hashOfCiphertext',
|
'Etag': 'hashOfCiphertext',
|
||||||
'content-type': 'text/plain',
|
'content-type': 'text/plain',
|
||||||
'content-length': content_length,
|
'content-length': content_length,
|
||||||
'X-Object-Sysmeta-Crypto-Etag': '%s; swift_meta=%s' % (
|
'X-Object-Sysmeta-Crypto-Etag': '%s; swift_meta=%s' % (
|
||||||
base64.b64encode(encrypt(plaintext_etag, object_key, FAKE_IV)),
|
base64.b64encode(encrypt(plaintext_etag, object_key, FAKE_IV)),
|
||||||
get_crypto_meta_header()),
|
get_crypto_meta_header(other_crypto_meta)),
|
||||||
'X-Object-Sysmeta-Crypto-Body-Meta':
|
'X-Object-Sysmeta-Crypto-Body-Meta':
|
||||||
get_crypto_meta_header(body_crypto_meta),
|
get_crypto_meta_header(body_crypto_meta),
|
||||||
|
'X-Object-Transient-Sysmeta-Crypto-Meta':
|
||||||
|
get_crypto_meta_header(other_crypto_meta),
|
||||||
'x-object-transient-sysmeta-crypto-meta-test':
|
'x-object-transient-sysmeta-crypto-meta-test':
|
||||||
base64.b64encode(encrypt('encrypt me', object_key, FAKE_IV)) +
|
base64.b64encode(encrypt('encrypt me', object_key, FAKE_IV)) +
|
||||||
';swift_meta=' + get_crypto_meta_header(),
|
';swift_meta=' + get_crypto_meta_header(other_crypto_meta),
|
||||||
'x-object-sysmeta-container-update-override-etag':
|
'x-object-sysmeta-container-update-override-etag':
|
||||||
encrypt_and_append_meta('encrypt me, too', cont_key),
|
encrypt_and_append_meta('encrypt me, too', cont_key),
|
||||||
'x-object-sysmeta-test': 'do not encrypt me',
|
'x-object-sysmeta-test': 'do not encrypt me',
|
||||||
})
|
})
|
||||||
|
|
||||||
def _test_request_success(self, method, body):
|
def _test_request_success(self, method, body, key_id=None):
|
||||||
env = {'REQUEST_METHOD': method,
|
env = {'REQUEST_METHOD': method,
|
||||||
CRYPTO_KEY_CALLBACK: fetch_crypto_keys}
|
CRYPTO_KEY_CALLBACK: fetch_crypto_keys}
|
||||||
req = Request.blank('/v1/a/c/o', environ=env)
|
req = Request.blank('/v1/a/c/o', environ=env)
|
||||||
@ -85,8 +92,13 @@ class TestDecrypterObjectRequests(unittest.TestCase):
|
|||||||
body_key = os.urandom(32)
|
body_key = os.urandom(32)
|
||||||
enc_body = encrypt(body, body_key, FAKE_IV)
|
enc_body = encrypt(body, body_key, FAKE_IV)
|
||||||
hdrs = self._make_response_headers(
|
hdrs = self._make_response_headers(
|
||||||
len(enc_body), plaintext_etag, fetch_crypto_keys(), body_key)
|
len(enc_body), plaintext_etag, fetch_crypto_keys(key_id=key_id),
|
||||||
|
body_key, key_id=key_id)
|
||||||
|
if key_id:
|
||||||
|
crypto_meta = load_crypto_meta(
|
||||||
|
hdrs['X-Object-Sysmeta-Crypto-Body-Meta'])
|
||||||
|
# sanity check that the test setup used provided key_id
|
||||||
|
self.assertEqual(key_id, crypto_meta['key_id'])
|
||||||
# there shouldn't be any x-object-meta- headers, but if there are
|
# there shouldn't be any x-object-meta- headers, but if there are
|
||||||
# then the decrypted header will win where there is a name clash...
|
# then the decrypted header will win where there is a name clash...
|
||||||
hdrs.update({
|
hdrs.update({
|
||||||
@ -116,11 +128,143 @@ class TestDecrypterObjectRequests(unittest.TestCase):
|
|||||||
resp = self._test_request_success('GET', body)
|
resp = self._test_request_success('GET', body)
|
||||||
self.assertEqual(body, resp.body)
|
self.assertEqual(body, resp.body)
|
||||||
|
|
||||||
|
key_id_val = {'secret_id': 'myid'}
|
||||||
|
resp = self._test_request_success('GET', body, key_id=key_id_val)
|
||||||
|
self.assertEqual(body, resp.body)
|
||||||
|
|
||||||
|
key_id_val = {'secret_id': ''}
|
||||||
|
resp = self._test_request_success('GET', body, key_id=key_id_val)
|
||||||
|
self.assertEqual(body, resp.body)
|
||||||
|
|
||||||
def test_HEAD_success(self):
|
def test_HEAD_success(self):
|
||||||
body = 'FAKE APP'
|
body = 'FAKE APP'
|
||||||
resp = self._test_request_success('HEAD', body)
|
resp = self._test_request_success('HEAD', body)
|
||||||
self.assertEqual('', resp.body)
|
self.assertEqual('', resp.body)
|
||||||
|
|
||||||
|
key_id_val = {'secret_id': 'myid'}
|
||||||
|
resp = self._test_request_success('HEAD', body, key_id=key_id_val)
|
||||||
|
self.assertEqual('', resp.body)
|
||||||
|
|
||||||
|
key_id_val = {'secret_id': ''}
|
||||||
|
resp = self._test_request_success('HEAD', body, key_id=key_id_val)
|
||||||
|
self.assertEqual('', resp.body)
|
||||||
|
|
||||||
|
def _check_different_keys_for_data_and_metadata(self, method):
|
||||||
|
env = {'REQUEST_METHOD': method,
|
||||||
|
CRYPTO_KEY_CALLBACK: fetch_crypto_keys}
|
||||||
|
req = Request.blank('/v1/a/c/o', environ=env)
|
||||||
|
data_key_id = {}
|
||||||
|
metadata_key_id = {'secret_id': 'myid'}
|
||||||
|
body = 'object data'
|
||||||
|
plaintext_etag = md5hex(body)
|
||||||
|
body_key = os.urandom(32)
|
||||||
|
enc_body = encrypt(body, body_key, FAKE_IV)
|
||||||
|
data_key = fetch_crypto_keys(data_key_id)
|
||||||
|
metadata_key = fetch_crypto_keys(metadata_key_id)
|
||||||
|
# synthesise response headers to mimic different key used for data PUT
|
||||||
|
# vs metadata POST
|
||||||
|
hdrs = self._make_response_headers(
|
||||||
|
len(enc_body), plaintext_etag, data_key, body_key,
|
||||||
|
key_id=data_key_id)
|
||||||
|
metadata_hdrs = self._make_response_headers(
|
||||||
|
len(enc_body), plaintext_etag, metadata_key, body_key,
|
||||||
|
key_id=metadata_key_id)
|
||||||
|
for k, v in metadata_hdrs.items():
|
||||||
|
if is_object_transient_sysmeta(k):
|
||||||
|
self.assertNotEqual(hdrs[k], v) # sanity check
|
||||||
|
hdrs[k] = v
|
||||||
|
|
||||||
|
self.app.register(
|
||||||
|
method, '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs)
|
||||||
|
resp = req.get_response(self.decrypter)
|
||||||
|
self.assertEqual('200 OK', resp.status)
|
||||||
|
self.assertEqual(plaintext_etag, resp.headers['Etag'])
|
||||||
|
self.assertEqual('text/plain', resp.headers['Content-Type'])
|
||||||
|
self.assertEqual('encrypt me', resp.headers['x-object-meta-test'])
|
||||||
|
self.assertEqual(
|
||||||
|
'encrypt me, too',
|
||||||
|
resp.headers['X-Object-Sysmeta-Container-Update-Override-Etag'])
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def test_GET_different_keys_for_data_and_metadata(self):
|
||||||
|
resp = self._check_different_keys_for_data_and_metadata('GET')
|
||||||
|
self.assertEqual('object data', resp.body)
|
||||||
|
|
||||||
|
def test_HEAD_different_keys_for_data_and_metadata(self):
|
||||||
|
resp = self._check_different_keys_for_data_and_metadata('HEAD')
|
||||||
|
self.assertEqual('', resp.body)
|
||||||
|
|
||||||
|
def _check_unencrypted_data_and_encrypted_metadata(self, method):
|
||||||
|
env = {'REQUEST_METHOD': method,
|
||||||
|
CRYPTO_KEY_CALLBACK: fetch_crypto_keys}
|
||||||
|
req = Request.blank('/v1/a/c/o', environ=env)
|
||||||
|
body = 'object data'
|
||||||
|
plaintext_etag = md5hex(body)
|
||||||
|
metadata_key = fetch_crypto_keys()
|
||||||
|
# synthesise headers for unencrypted PUT + headers for encrypted POST
|
||||||
|
hdrs = HeaderKeyDict({
|
||||||
|
'Etag': plaintext_etag,
|
||||||
|
'content-type': 'text/plain',
|
||||||
|
'content-length': len(body)})
|
||||||
|
# we don't the data related headers but need a body key to keep the
|
||||||
|
# helper function happy
|
||||||
|
body_key = os.urandom(32)
|
||||||
|
metadata_hdrs = self._make_response_headers(
|
||||||
|
len(body), plaintext_etag, metadata_key, body_key)
|
||||||
|
for k, v in metadata_hdrs.items():
|
||||||
|
if is_object_transient_sysmeta(k):
|
||||||
|
hdrs[k] = v
|
||||||
|
|
||||||
|
self.app.register(
|
||||||
|
method, '/v1/a/c/o', HTTPOk, body=body, headers=hdrs)
|
||||||
|
resp = req.get_response(self.decrypter)
|
||||||
|
self.assertEqual('200 OK', resp.status)
|
||||||
|
self.assertEqual(plaintext_etag, resp.headers['Etag'])
|
||||||
|
self.assertEqual('text/plain', resp.headers['Content-Type'])
|
||||||
|
self.assertEqual('encrypt me', resp.headers['x-object-meta-test'])
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def test_GET_unencrypted_data_and_encrypted_metadata(self):
|
||||||
|
resp = self._check_unencrypted_data_and_encrypted_metadata('GET')
|
||||||
|
self.assertEqual('object data', resp.body)
|
||||||
|
|
||||||
|
def test_HEAD_unencrypted_data_and_encrypted_metadata(self):
|
||||||
|
resp = self._check_unencrypted_data_and_encrypted_metadata('HEAD')
|
||||||
|
self.assertEqual('', resp.body)
|
||||||
|
|
||||||
|
def _check_encrypted_data_and_unencrypted_metadata(self, method):
|
||||||
|
env = {'REQUEST_METHOD': method,
|
||||||
|
CRYPTO_KEY_CALLBACK: fetch_crypto_keys}
|
||||||
|
req = Request.blank('/v1/a/c/o', environ=env)
|
||||||
|
body = 'object data'
|
||||||
|
plaintext_etag = md5hex(body)
|
||||||
|
body_key = os.urandom(32)
|
||||||
|
enc_body = encrypt(body, body_key, FAKE_IV)
|
||||||
|
data_key = fetch_crypto_keys()
|
||||||
|
hdrs = self._make_response_headers(
|
||||||
|
len(enc_body), plaintext_etag, data_key, body_key)
|
||||||
|
for k, v in hdrs.items():
|
||||||
|
if is_object_transient_sysmeta(k):
|
||||||
|
hdrs.pop(k)
|
||||||
|
hdrs['x-object-meta-test'] = 'unencrypted'
|
||||||
|
|
||||||
|
self.app.register(
|
||||||
|
method, '/v1/a/c/o', HTTPOk, body=enc_body, headers=hdrs)
|
||||||
|
resp = req.get_response(self.decrypter)
|
||||||
|
self.assertEqual('200 OK', resp.status)
|
||||||
|
self.assertEqual(plaintext_etag, resp.headers['Etag'])
|
||||||
|
self.assertEqual('text/plain', resp.headers['Content-Type'])
|
||||||
|
self.assertEqual('unencrypted', resp.headers['x-object-meta-test'])
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def test_GET_encrypted_data_and_unencrypted_metadata(self):
|
||||||
|
resp = self._check_encrypted_data_and_unencrypted_metadata('GET')
|
||||||
|
self.assertEqual('object data', resp.body)
|
||||||
|
|
||||||
|
def test_HEAD_encrypted_data_and_unencrypted_metadata(self):
|
||||||
|
resp = self._check_encrypted_data_and_unencrypted_metadata('HEAD')
|
||||||
|
self.assertEqual('', resp.body)
|
||||||
|
|
||||||
def test_headers_case(self):
|
def test_headers_case(self):
|
||||||
body = 'fAkE ApP'
|
body = 'fAkE ApP'
|
||||||
req = Request.blank('/v1/a/c/o', body='FaKe')
|
req = Request.blank('/v1/a/c/o', body='FaKe')
|
||||||
@ -272,7 +416,7 @@ class TestDecrypterObjectRequests(unittest.TestCase):
|
|||||||
|
|
||||||
def _test_bad_key(self, method):
|
def _test_bad_key(self, method):
|
||||||
# use bad key
|
# use bad key
|
||||||
def bad_fetch_crypto_keys():
|
def bad_fetch_crypto_keys(**kwargs):
|
||||||
keys = fetch_crypto_keys()
|
keys = fetch_crypto_keys()
|
||||||
keys['object'] = 'bad key'
|
keys['object'] = 'bad key'
|
||||||
return keys
|
return keys
|
||||||
@ -734,7 +878,7 @@ class TestDecrypterObjectRequests(unittest.TestCase):
|
|||||||
self.decrypter.logger.get_lines_for_level('error')[0])
|
self.decrypter.logger.get_lines_for_level('error')[0])
|
||||||
|
|
||||||
def test_GET_error_in_key_callback(self):
|
def test_GET_error_in_key_callback(self):
|
||||||
def raise_exc():
|
def raise_exc(**kwargs):
|
||||||
raise Exception('Testing')
|
raise Exception('Testing')
|
||||||
|
|
||||||
env = {'REQUEST_METHOD': 'GET',
|
env = {'REQUEST_METHOD': 'GET',
|
||||||
@ -956,10 +1100,40 @@ class TestDecrypterContainerRequests(unittest.TestCase):
|
|||||||
|
|
||||||
resp = self._make_cont_get_req(fake_body, 'json')
|
resp = self._make_cont_get_req(fake_body, 'json')
|
||||||
|
|
||||||
self.assertEqual('500 Internal Error', resp.status)
|
self.assertEqual('200 OK', resp.status)
|
||||||
self.assertEqual('Error decrypting container listing', resp.body)
|
self.assertEqual(['<unknown>'],
|
||||||
|
[x['hash'] for x in json.loads(resp.body)])
|
||||||
self.assertIn("Cipher must be AES_CTR_256",
|
self.assertIn("Cipher must be AES_CTR_256",
|
||||||
self.decrypter.logger.get_lines_for_level('error')[0])
|
self.decrypter.logger.get_lines_for_level('error')[0])
|
||||||
|
self.assertIn('Error decrypting container listing',
|
||||||
|
self.decrypter.logger.get_lines_for_level('error')[0])
|
||||||
|
|
||||||
|
def test_cont_get_json_req_with_unknown_secret_id(self):
|
||||||
|
bad_crypto_meta = fake_get_crypto_meta()
|
||||||
|
bad_crypto_meta['key_id'] = {'secret_id': 'unknown_key'}
|
||||||
|
key = fetch_crypto_keys()['container']
|
||||||
|
pt_etag = 'c6e8196d7f0fff6444b90861fe8d609d'
|
||||||
|
ct_etag = encrypt_and_append_meta(pt_etag, key,
|
||||||
|
crypto_meta=bad_crypto_meta)
|
||||||
|
|
||||||
|
obj_dict_1 = {"bytes": 16,
|
||||||
|
"last_modified": "2015-04-14T23:33:06.439040",
|
||||||
|
"hash": ct_etag,
|
||||||
|
"name": "testfile",
|
||||||
|
"content_type": "image/jpeg"}
|
||||||
|
|
||||||
|
listing = [obj_dict_1]
|
||||||
|
fake_body = json.dumps(listing)
|
||||||
|
|
||||||
|
resp = self._make_cont_get_req(fake_body, 'json')
|
||||||
|
|
||||||
|
self.assertEqual('200 OK', resp.status)
|
||||||
|
self.assertEqual(['<unknown>'],
|
||||||
|
[x['hash'] for x in json.loads(resp.body)])
|
||||||
|
self.assertEqual(self.decrypter.logger.get_lines_for_level('error'), [
|
||||||
|
'get_keys(): unknown key id: unknown_key',
|
||||||
|
'Error decrypting container listing: unknown_key',
|
||||||
|
])
|
||||||
|
|
||||||
def test_GET_container_json_not_encrypted_obj(self):
|
def test_GET_container_json_not_encrypted_obj(self):
|
||||||
pt_etag = '%s; symlink_path=/a/c/o' % MD5_OF_EMPTY_STRING
|
pt_etag = '%s; symlink_path=/a/c/o' % MD5_OF_EMPTY_STRING
|
||||||
|
@ -604,13 +604,20 @@ class TestEncrypter(unittest.TestCase):
|
|||||||
# verify etags have been supplemented with masked values
|
# verify etags have been supplemented with masked values
|
||||||
self.assertIn(match_header_name, actual_headers)
|
self.assertIn(match_header_name, actual_headers)
|
||||||
actual_etags = set(actual_headers[match_header_name].split(', '))
|
actual_etags = set(actual_headers[match_header_name].split(', '))
|
||||||
|
# masked values for secret_id None
|
||||||
key = fetch_crypto_keys()['object']
|
key = fetch_crypto_keys()['object']
|
||||||
masked_etags = [
|
masked_etags = [
|
||||||
'"%s"' % base64.b64encode(hmac.new(
|
'"%s"' % base64.b64encode(hmac.new(
|
||||||
key, etag.strip('"'), hashlib.sha256).digest())
|
key, etag.strip('"'), hashlib.sha256).digest())
|
||||||
for etag in plain_etags if etag not in ('*', '')]
|
for etag in plain_etags if etag not in ('*', '')]
|
||||||
|
# masked values for secret_id myid
|
||||||
|
key = fetch_crypto_keys(key_id={'secret_id': 'myid'})['object']
|
||||||
|
masked_etags_myid = [
|
||||||
|
'"%s"' % base64.b64encode(hmac.new(
|
||||||
|
key, etag.strip('"'), hashlib.sha256).digest())
|
||||||
|
for etag in plain_etags if etag not in ('*', '')]
|
||||||
expected_etags = set((expected_plain_etags or plain_etags) +
|
expected_etags = set((expected_plain_etags or plain_etags) +
|
||||||
masked_etags)
|
masked_etags + masked_etags_myid)
|
||||||
self.assertEqual(expected_etags, actual_etags)
|
self.assertEqual(expected_etags, actual_etags)
|
||||||
# check that the request environ was returned to original state
|
# check that the request environ was returned to original state
|
||||||
self.assertEqual(set(plain_etags),
|
self.assertEqual(set(plain_etags),
|
||||||
@ -798,7 +805,7 @@ class TestEncrypter(unittest.TestCase):
|
|||||||
self.assertEqual('Unable to retrieve encryption keys.', resp.body)
|
self.assertEqual('Unable to retrieve encryption keys.', resp.body)
|
||||||
|
|
||||||
def test_PUT_error_in_key_callback(self):
|
def test_PUT_error_in_key_callback(self):
|
||||||
def raise_exc():
|
def raise_exc(*args, **kwargs):
|
||||||
raise Exception('Testing')
|
raise Exception('Testing')
|
||||||
|
|
||||||
body = 'FAKE APP'
|
body = 'FAKE APP'
|
||||||
|
@ -59,15 +59,20 @@ class TestCryptoPipelineChanges(unittest.TestCase):
|
|||||||
self.plaintext_etag = md5hex(self.plaintext)
|
self.plaintext_etag = md5hex(self.plaintext)
|
||||||
self._setup_crypto_app()
|
self._setup_crypto_app()
|
||||||
|
|
||||||
def _setup_crypto_app(self, disable_encryption=False):
|
def _setup_crypto_app(self, disable_encryption=False, root_secret_id=None):
|
||||||
# Set up a pipeline of crypto middleware ending in the proxy app so
|
# Set up a pipeline of crypto middleware ending in the proxy app so
|
||||||
# that tests can make requests to either the proxy server directly or
|
# that tests can make requests to either the proxy server directly or
|
||||||
# via the crypto middleware. Make a fresh instance for each test to
|
# via the crypto middleware. Make a fresh instance for each test to
|
||||||
# avoid any state coupling.
|
# avoid any state coupling.
|
||||||
conf = {'disable_encryption': disable_encryption}
|
conf = {'disable_encryption': disable_encryption}
|
||||||
self.encryption = crypto.filter_factory(conf)(self.proxy_app)
|
self.encryption = crypto.filter_factory(conf)(self.proxy_app)
|
||||||
self.km = keymaster.KeyMaster(self.encryption, TEST_KEYMASTER_CONF)
|
self.encryption.logger = self.proxy_app.logger
|
||||||
|
km_conf = dict(TEST_KEYMASTER_CONF)
|
||||||
|
if root_secret_id is not None:
|
||||||
|
km_conf['active_root_secret_id'] = root_secret_id
|
||||||
|
self.km = keymaster.KeyMaster(self.encryption, km_conf)
|
||||||
self.crypto_app = self.km # for clarity
|
self.crypto_app = self.km # for clarity
|
||||||
|
self.crypto_app.logger = self.encryption.logger
|
||||||
|
|
||||||
def _create_container(self, app, policy_name='one', container_path=None):
|
def _create_container(self, app, policy_name='one', container_path=None):
|
||||||
if not container_path:
|
if not container_path:
|
||||||
@ -262,6 +267,36 @@ class TestCryptoPipelineChanges(unittest.TestCase):
|
|||||||
self._check_match_requests('HEAD', self.crypto_app)
|
self._check_match_requests('HEAD', self.crypto_app)
|
||||||
self._check_listing(self.crypto_app)
|
self._check_listing(self.crypto_app)
|
||||||
|
|
||||||
|
def test_write_with_crypto_read_with_crypto_different_root_secrets(self):
|
||||||
|
root_secret = self.crypto_app.root_secret
|
||||||
|
self._create_container(self.proxy_app, policy_name='one')
|
||||||
|
self._put_object(self.crypto_app, self.plaintext)
|
||||||
|
# change root secret
|
||||||
|
self._setup_crypto_app(root_secret_id='1')
|
||||||
|
root_secret_1 = self.crypto_app.root_secret
|
||||||
|
self.assertNotEqual(root_secret, root_secret_1) # sanity check
|
||||||
|
self._post_object(self.crypto_app)
|
||||||
|
self._check_GET_and_HEAD(self.crypto_app)
|
||||||
|
self._check_match_requests('GET', self.crypto_app)
|
||||||
|
self._check_match_requests('HEAD', self.crypto_app)
|
||||||
|
self._check_listing(self.crypto_app)
|
||||||
|
# change root secret
|
||||||
|
self._setup_crypto_app(root_secret_id='2')
|
||||||
|
root_secret_2 = self.crypto_app.root_secret
|
||||||
|
self.assertNotEqual(root_secret_2, root_secret_1) # sanity check
|
||||||
|
self.assertNotEqual(root_secret_2, root_secret) # sanity check
|
||||||
|
self._check_GET_and_HEAD(self.crypto_app)
|
||||||
|
self._check_match_requests('GET', self.crypto_app)
|
||||||
|
self._check_match_requests('HEAD', self.crypto_app)
|
||||||
|
self._check_listing(self.crypto_app)
|
||||||
|
# write object again
|
||||||
|
self._put_object(self.crypto_app, self.plaintext)
|
||||||
|
self._post_object(self.crypto_app)
|
||||||
|
self._check_GET_and_HEAD(self.crypto_app)
|
||||||
|
self._check_match_requests('GET', self.crypto_app)
|
||||||
|
self._check_match_requests('HEAD', self.crypto_app)
|
||||||
|
self._check_listing(self.crypto_app)
|
||||||
|
|
||||||
def test_write_with_crypto_read_with_crypto_ec(self):
|
def test_write_with_crypto_read_with_crypto_ec(self):
|
||||||
self._create_container(self.proxy_app, policy_name='ec')
|
self._create_container(self.proxy_app, policy_name='ec')
|
||||||
self._put_object(self.crypto_app, self.plaintext)
|
self._put_object(self.crypto_app, self.plaintext)
|
||||||
|
@ -13,6 +13,9 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import base64
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
@ -52,7 +55,7 @@ class TestKeymaster(unittest.TestCase):
|
|||||||
self.verify_keys_for_path(
|
self.verify_keys_for_path(
|
||||||
'/a/c', expected_keys=('container',))
|
'/a/c', expected_keys=('container',))
|
||||||
|
|
||||||
def verify_keys_for_path(self, path, expected_keys):
|
def verify_keys_for_path(self, path, expected_keys, key_id=None):
|
||||||
put_keys = None
|
put_keys = None
|
||||||
for method, resp_class, status in (
|
for method, resp_class, status in (
|
||||||
('PUT', swob.HTTPCreated, '201'),
|
('PUT', swob.HTTPCreated, '201'),
|
||||||
@ -71,11 +74,12 @@ class TestKeymaster(unittest.TestCase):
|
|||||||
self.assertNotIn('swift.crypto.override', req.environ)
|
self.assertNotIn('swift.crypto.override', req.environ)
|
||||||
self.assertIn(CRYPTO_KEY_CALLBACK, req.environ,
|
self.assertIn(CRYPTO_KEY_CALLBACK, req.environ,
|
||||||
'%s not set in env' % CRYPTO_KEY_CALLBACK)
|
'%s not set in env' % CRYPTO_KEY_CALLBACK)
|
||||||
keys = req.environ.get(CRYPTO_KEY_CALLBACK)()
|
keys = req.environ.get(CRYPTO_KEY_CALLBACK)(key_id=key_id)
|
||||||
self.assertIn('id', keys)
|
self.assertIn('id', keys)
|
||||||
id = keys.pop('id')
|
id = keys.pop('id')
|
||||||
self.assertEqual(path, id['path'])
|
self.assertEqual(path, id['path'])
|
||||||
self.assertEqual('1', id['v'])
|
self.assertEqual('1', id['v'])
|
||||||
|
keys.pop('all_ids')
|
||||||
self.assertListEqual(sorted(expected_keys), sorted(keys.keys()),
|
self.assertListEqual(sorted(expected_keys), sorted(keys.keys()),
|
||||||
'%s %s got keys %r, but expected %r'
|
'%s %s got keys %r, but expected %r'
|
||||||
% (method, path, keys.keys(), expected_keys))
|
% (method, path, keys.keys(), expected_keys))
|
||||||
@ -134,17 +138,180 @@ class TestKeymaster(unittest.TestCase):
|
|||||||
'keymaster_config_path': conf_file})
|
'keymaster_config_path': conf_file})
|
||||||
|
|
||||||
def test_root_secret(self):
|
def test_root_secret(self):
|
||||||
|
def do_test(dflt_id):
|
||||||
for secret in (os.urandom(32), os.urandom(33), os.urandom(50)):
|
for secret in (os.urandom(32), os.urandom(33), os.urandom(50)):
|
||||||
encoded_secret = base64.b64encode(secret)
|
encoded_secret = base64.b64encode(secret)
|
||||||
for conf_val in (bytes(encoded_secret), unicode(encoded_secret),
|
for conf_val in (
|
||||||
|
bytes(encoded_secret),
|
||||||
|
unicode(encoded_secret),
|
||||||
encoded_secret[:30] + '\n' + encoded_secret[30:]):
|
encoded_secret[:30] + '\n' + encoded_secret[30:]):
|
||||||
try:
|
try:
|
||||||
app = keymaster.KeyMaster(
|
app = keymaster.KeyMaster(
|
||||||
self.swift, {'encryption_root_secret': conf_val,
|
self.swift, {'encryption_root_secret': conf_val,
|
||||||
'encryption_root_secret_path': ''})
|
'active_root_secret_id': dflt_id,
|
||||||
|
'keymaster_config_path': ''})
|
||||||
self.assertEqual(secret, app.root_secret)
|
self.assertEqual(secret, app.root_secret)
|
||||||
except AssertionError as err:
|
except AssertionError as err:
|
||||||
self.fail(str(err) + ' for secret %r' % conf_val)
|
self.fail(str(err) + ' for secret %r' % conf_val)
|
||||||
|
do_test(None)
|
||||||
|
do_test('')
|
||||||
|
|
||||||
|
def test_no_root_secret(self):
|
||||||
|
with self.assertRaises(ValueError) as cm:
|
||||||
|
keymaster.KeyMaster(self.swift, {})
|
||||||
|
self.assertEqual('No secret loaded for active_root_secret_id None',
|
||||||
|
str(cm.exception))
|
||||||
|
|
||||||
|
def test_multiple_root_secrets(self):
|
||||||
|
secrets = {None: os.urandom(32),
|
||||||
|
'22': os.urandom(33),
|
||||||
|
'my_secret_id': os.urandom(50)}
|
||||||
|
|
||||||
|
conf = {}
|
||||||
|
for secret_id, secret in secrets.items():
|
||||||
|
opt = ('encryption_root_secret%s' %
|
||||||
|
(('_%s' % secret_id) if secret_id else ''))
|
||||||
|
conf[opt] = base64.b64encode(secret)
|
||||||
|
app = keymaster.KeyMaster(self.swift, conf)
|
||||||
|
self.assertEqual(secrets, app._root_secrets)
|
||||||
|
self.assertEqual([None, '22', 'my_secret_id'], app.root_secret_ids)
|
||||||
|
|
||||||
|
def test_multiple_root_secrets_with_invalid_secret(self):
|
||||||
|
conf = {'encryption_root_secret': base64.b64encode(os.urandom(32)),
|
||||||
|
# too short...
|
||||||
|
'encryption_root_secret_22': base64.b64encode(os.urandom(31))}
|
||||||
|
with self.assertRaises(ValueError) as err:
|
||||||
|
keymaster.KeyMaster(self.swift, conf)
|
||||||
|
self.assertEqual(
|
||||||
|
'encryption_root_secret_22 option in proxy-server.conf '
|
||||||
|
'must be a base64 encoding of at least 32 raw bytes',
|
||||||
|
str(err.exception))
|
||||||
|
|
||||||
|
def test_multiple_root_secrets_with_invalid_id(self):
|
||||||
|
def do_test(bad_option):
|
||||||
|
conf = {'encryption_root_secret': base64.b64encode(os.urandom(32)),
|
||||||
|
bad_option: base64.b64encode(os.urandom(32))}
|
||||||
|
with self.assertRaises(ValueError) as err:
|
||||||
|
keymaster.KeyMaster(self.swift, conf)
|
||||||
|
self.assertEqual(
|
||||||
|
'Malformed root secret option name %s' % bad_option,
|
||||||
|
str(err.exception))
|
||||||
|
do_test('encryption_root_secret1')
|
||||||
|
do_test('encryption_root_secret123')
|
||||||
|
do_test('encryption_root_secret_')
|
||||||
|
|
||||||
|
def test_multiple_root_secrets_missing_active_root_secret_id(self):
|
||||||
|
conf = {'encryption_root_secret_22': base64.b64encode(os.urandom(32))}
|
||||||
|
with self.assertRaises(ValueError) as err:
|
||||||
|
keymaster.KeyMaster(self.swift, conf)
|
||||||
|
self.assertEqual(
|
||||||
|
'No secret loaded for active_root_secret_id None',
|
||||||
|
str(err.exception))
|
||||||
|
|
||||||
|
conf = {'encryption_root_secret_22': base64.b64encode(os.urandom(32)),
|
||||||
|
'active_root_secret_id': 'missing'}
|
||||||
|
with self.assertRaises(ValueError) as err:
|
||||||
|
keymaster.KeyMaster(self.swift, conf)
|
||||||
|
self.assertEqual(
|
||||||
|
'No secret loaded for active_root_secret_id missing',
|
||||||
|
str(err.exception))
|
||||||
|
|
||||||
|
def test_correct_root_secret_used(self):
|
||||||
|
secrets = {None: os.urandom(32),
|
||||||
|
'22': os.urandom(33),
|
||||||
|
'my_secret_id': os.urandom(50)}
|
||||||
|
|
||||||
|
# no active_root_secret_id configured
|
||||||
|
conf = {}
|
||||||
|
for secret_id, secret in secrets.items():
|
||||||
|
opt = ('encryption_root_secret%s' %
|
||||||
|
(('_%s' % secret_id) if secret_id else ''))
|
||||||
|
conf[opt] = base64.b64encode(secret)
|
||||||
|
self.app = keymaster.KeyMaster(self.swift, conf)
|
||||||
|
keys = self.verify_keys_for_path('/a/c/o', ('container', 'object'))
|
||||||
|
expected_keys = {
|
||||||
|
'container': hmac.new(secrets[None], '/a/c',
|
||||||
|
digestmod=hashlib.sha256).digest(),
|
||||||
|
'object': hmac.new(secrets[None], '/a/c/o',
|
||||||
|
digestmod=hashlib.sha256).digest()}
|
||||||
|
self.assertEqual(expected_keys, keys)
|
||||||
|
|
||||||
|
# active_root_secret_id configured
|
||||||
|
conf['active_root_secret_id'] = '22'
|
||||||
|
self.app = keymaster.KeyMaster(self.swift, conf)
|
||||||
|
keys = self.verify_keys_for_path('/a/c/o', ('container', 'object'))
|
||||||
|
expected_keys = {
|
||||||
|
'container': hmac.new(secrets['22'], '/a/c',
|
||||||
|
digestmod=hashlib.sha256).digest(),
|
||||||
|
'object': hmac.new(secrets['22'], '/a/c/o',
|
||||||
|
digestmod=hashlib.sha256).digest()}
|
||||||
|
self.assertEqual(expected_keys, keys)
|
||||||
|
|
||||||
|
# secret_id passed to fetch_crypto_keys callback
|
||||||
|
for secret_id in ('my_secret_id', None):
|
||||||
|
keys = self.verify_keys_for_path('/a/c/o', ('container', 'object'),
|
||||||
|
key_id={'secret_id': secret_id})
|
||||||
|
expected_keys = {
|
||||||
|
'container': hmac.new(secrets[secret_id], '/a/c',
|
||||||
|
digestmod=hashlib.sha256).digest(),
|
||||||
|
'object': hmac.new(secrets[secret_id], '/a/c/o',
|
||||||
|
digestmod=hashlib.sha256).digest()}
|
||||||
|
self.assertEqual(expected_keys, keys)
|
||||||
|
|
||||||
|
def test_keys_cached(self):
|
||||||
|
secrets = {None: os.urandom(32),
|
||||||
|
'22': os.urandom(33),
|
||||||
|
'my_secret_id': os.urandom(50)}
|
||||||
|
conf = {}
|
||||||
|
for secret_id, secret in secrets.items():
|
||||||
|
opt = ('encryption_root_secret%s' %
|
||||||
|
(('_%s' % secret_id) if secret_id else ''))
|
||||||
|
conf[opt] = base64.b64encode(secret)
|
||||||
|
conf['active_root_secret_id'] = '22'
|
||||||
|
self.app = keymaster.KeyMaster(self.swift, conf)
|
||||||
|
orig_create_key = self.app.create_key
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def mock_create_key(path, secret_id=None):
|
||||||
|
calls.append((path, secret_id))
|
||||||
|
return orig_create_key(path, secret_id)
|
||||||
|
|
||||||
|
context = keymaster.KeyMasterContext(self.app, 'a', 'c', 'o')
|
||||||
|
with mock.patch.object(self.app, 'create_key', mock_create_key):
|
||||||
|
keys = context.fetch_crypto_keys()
|
||||||
|
expected_keys = {
|
||||||
|
'container': hmac.new(secrets['22'], '/a/c',
|
||||||
|
digestmod=hashlib.sha256).digest(),
|
||||||
|
'object': hmac.new(secrets['22'], '/a/c/o',
|
||||||
|
digestmod=hashlib.sha256).digest(),
|
||||||
|
'id': {'path': '/a/c/o', 'secret_id': '22', 'v': '1'},
|
||||||
|
'all_ids': [
|
||||||
|
{'path': '/a/c/o', 'v': '1'},
|
||||||
|
{'path': '/a/c/o', 'secret_id': '22', 'v': '1'},
|
||||||
|
{'path': '/a/c/o', 'secret_id': 'my_secret_id', 'v': '1'}]}
|
||||||
|
self.assertEqual(expected_keys, keys)
|
||||||
|
self.assertEqual([('/a/c', '22'), ('/a/c/o', '22')], calls)
|
||||||
|
with mock.patch.object(self.app, 'create_key', mock_create_key):
|
||||||
|
keys = context.fetch_crypto_keys()
|
||||||
|
# no more calls to create_key
|
||||||
|
self.assertEqual([('/a/c', '22'), ('/a/c/o', '22')], calls)
|
||||||
|
self.assertEqual(expected_keys, keys)
|
||||||
|
with mock.patch.object(self.app, 'create_key', mock_create_key):
|
||||||
|
keys = context.fetch_crypto_keys(key_id={'secret_id': None})
|
||||||
|
expected_keys = {
|
||||||
|
'container': hmac.new(secrets[None], '/a/c',
|
||||||
|
digestmod=hashlib.sha256).digest(),
|
||||||
|
'object': hmac.new(secrets[None], '/a/c/o',
|
||||||
|
digestmod=hashlib.sha256).digest(),
|
||||||
|
'id': {'path': '/a/c/o', 'v': '1'},
|
||||||
|
'all_ids': [
|
||||||
|
{'path': '/a/c/o', 'v': '1'},
|
||||||
|
{'path': '/a/c/o', 'secret_id': '22', 'v': '1'},
|
||||||
|
{'path': '/a/c/o', 'secret_id': 'my_secret_id', 'v': '1'}]}
|
||||||
|
self.assertEqual(expected_keys, keys)
|
||||||
|
self.assertEqual([('/a/c', '22'), ('/a/c/o', '22'),
|
||||||
|
('/a/c', None), ('/a/c/o', None)],
|
||||||
|
calls)
|
||||||
|
|
||||||
@mock.patch('swift.common.middleware.crypto.keymaster.readconf')
|
@mock.patch('swift.common.middleware.crypto.keymaster.readconf')
|
||||||
def test_keymaster_config_path(self, mock_readconf):
|
def test_keymaster_config_path(self, mock_readconf):
|
||||||
@ -179,7 +346,7 @@ class TestKeymaster(unittest.TestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
'encryption_root_secret option in proxy-server.conf '
|
'encryption_root_secret option in proxy-server.conf '
|
||||||
'must be a base64 encoding of at least 32 raw bytes',
|
'must be a base64 encoding of at least 32 raw bytes',
|
||||||
err.exception.message)
|
str(err.exception))
|
||||||
except AssertionError as err:
|
except AssertionError as err:
|
||||||
self.fail(str(err) + ' for conf %s' % str(conf))
|
self.fail(str(err) + ' for conf %s' % str(conf))
|
||||||
|
|
||||||
@ -200,23 +367,35 @@ class TestKeymaster(unittest.TestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
'encryption_root_secret option in /some/other/path '
|
'encryption_root_secret option in /some/other/path '
|
||||||
'must be a base64 encoding of at least 32 raw bytes',
|
'must be a base64 encoding of at least 32 raw bytes',
|
||||||
err.exception.message)
|
str(err.exception))
|
||||||
self.assertEqual(mock_readconf.mock_calls, [
|
self.assertEqual(mock_readconf.mock_calls, [
|
||||||
mock.call('/some/other/path', 'keymaster')])
|
mock.call('/some/other/path', 'keymaster')])
|
||||||
except AssertionError as err:
|
except AssertionError as err:
|
||||||
self.fail(str(err) + ' for secret %r' % secret)
|
self.fail(str(err) + ' for secret %r' % secret)
|
||||||
|
|
||||||
def test_can_only_configure_secret_in_one_place(self):
|
def test_can_only_configure_secret_in_one_place(self):
|
||||||
conf = {'encryption_root_secret': 'a' * 44,
|
def do_test(conf):
|
||||||
'keymaster_config_path': '/ets/swift/keymaster.conf'}
|
|
||||||
with self.assertRaises(ValueError) as err:
|
with self.assertRaises(ValueError) as err:
|
||||||
keymaster.KeyMaster(self.swift, conf)
|
keymaster.KeyMaster(self.swift, conf)
|
||||||
expected_message = ('keymaster_config_path is set, but there are '
|
expected_message = ('keymaster_config_path is set, but there are '
|
||||||
'other config options specified:')
|
'other config options specified:')
|
||||||
self.assertTrue(err.exception.message.startswith(expected_message),
|
self.assertTrue(str(err.exception).startswith(expected_message),
|
||||||
"Error message does not start with '%s'" %
|
"Error message does not start with '%s'" %
|
||||||
expected_message)
|
expected_message)
|
||||||
|
|
||||||
|
conf = {'encryption_root_secret': 'a' * 44,
|
||||||
|
'keymaster_config_path': '/etc/swift/keymaster.conf'}
|
||||||
|
do_test(conf)
|
||||||
|
conf = {'encryption_root_secret_1': 'a' * 44,
|
||||||
|
'keymaster_config_path': '/etc/swift/keymaster.conf'}
|
||||||
|
do_test(conf)
|
||||||
|
conf = {'encryption_root_secret_': 'a' * 44,
|
||||||
|
'keymaster_config_path': '/etc/swift/keymaster.conf'}
|
||||||
|
do_test(conf)
|
||||||
|
conf = {'active_root_secret_id': '1',
|
||||||
|
'keymaster_config_path': '/etc/swift/keymaster.conf'}
|
||||||
|
do_test(conf)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
Loading…
Reference in New Issue
Block a user