Skip to content

Commit

Permalink
Fix memcache encryption middleware
Browse files Browse the repository at this point in the history
This fixes lp1175367 and lp1175368 by redesigning the memcache crypt
middleware to not do dangerous things. It is forward compatible, but
will invalidate any existing ephemeral encrypted or signed memcache
entries.

Change-Id: Ice8724949a48bfad3b8b7c41b5f50a18a9ad9f42
Signed-off-by: Bryan D. Payne <bdpayne@acm.org>
  • Loading branch information
Bryan D. Payne authored and ttx committed Jun 19, 2013
1 parent 1e3cf4b commit eeefb78
Show file tree
Hide file tree
Showing 5 changed files with 277 additions and 273 deletions.
37 changes: 20 additions & 17 deletions doc/source/middlewarearchitecture.rst
@@ -1,5 +1,5 @@
..
Copyright 2011-2012 OpenStack, LLC
Copyright 2011-2013 OpenStack, LLC
All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may
Expand Down Expand Up @@ -188,7 +188,8 @@ Configuration Options
the timeout when validating token by http).
* ``auth_port``: (optional, default `35357`) the port used to validate tokens
* ``auth_protocol``: (optional, default `https`)
* ``auth_uri``: (optional, defaults to `auth_protocol`://`auth_host`:`auth_port`)
* ``auth_uri``: (optional, defaults to
`auth_protocol`://`auth_host`:`auth_port`)
* ``certfile``: (required, if Keystone server requires client cert)
* ``keyfile``: (required, if Keystone server requires client cert) This can be
the same as the certfile if the certfile includes the private key.
Expand Down Expand Up @@ -232,22 +233,24 @@ Memcache Protection
===================

When using memcached, we are storing user tokens and token validation
information into the cache as raw data. Which means anyone who have access
to the memcache servers can read and modify data stored there. To mitigate
this risk, ``auth_token`` middleware provides an option to either encrypt
or authenticate the token data stored in the cache.

* ``memcache_security_strategy``: (optional) if defined, indicate whether token
data should be encrypted or authenticated. Acceptable values are ``ENCRYPT``
or ``MAC``. If ``ENCRYPT``, token data is encrypted in the cache. If
``MAC``, token data is authenticated (with HMAC) in the cache. If its value
is neither ``MAC`` nor ``ENCRYPT``, ``auth_token`` will raise an exception
on initialization.
information into the cache as raw data. Which means that anyone who
has access to the memcache servers can read and modify data stored
there. To mitigate this risk, ``auth_token`` middleware provides an
option to authenticate and optionally encrypt the token data stored in
the cache.

* ``memcache_security_strategy``: (optional) if defined, indicate
whether token data should be authenticated or authenticated and
encrypted. Acceptable values are ``MAC`` or ``ENCRYPT``. If ``MAC``,
token data is authenticated (with HMAC) in the cache. If
``ENCRYPT``, token data is encrypted and authenticated in the
cache. If the value is not one of these options or empty,
``auth_token`` will raise an exception on initialization.
* ``memcache_secret_key``: (optional, mandatory if
``memcache_security_strategy`` is defined) if defined,
a random string to be used for key derivation. If
``memcache_security_strategy`` is defined and ``memcache_secret_key`` is
absent, ``auth_token`` will raise an exception on initialization.
``memcache_security_strategy`` is defined) this string is used for
key derivation. If ``memcache_security_strategy`` is defined and
``memcache_secret_key`` is absent, ``auth_token`` will raise an
exception on initialization.

Exchanging User Information
===========================
Expand Down
131 changes: 61 additions & 70 deletions keystoneclient/middleware/auth_token.py
Expand Up @@ -222,6 +222,7 @@
CONF.register_opts(opts, group='keystone_authtoken')

LIST_OF_VERSIONS_TO_ATTEMPT = ['v2.0', 'v3.0']
CACHE_KEY_TEMPLATE = 'tokens/%s'


def will_expire_soon(expiry):
Expand Down Expand Up @@ -847,91 +848,81 @@ def _get_header(self, env, key, default=None):
env_key = self._header_to_env_var(key)
return env.get(env_key, default)

def _protect_cache_value(self, token, data):
""" Encrypt or sign data if necessary. """
try:
if self._memcache_security_strategy == 'ENCRYPT':
return memcache_crypt.encrypt_data(token,
self._memcache_secret_key,
data)
elif self._memcache_security_strategy == 'MAC':
return memcache_crypt.sign_data(token, data)
else:
return data
except:
msg = 'Failed to encrypt/sign cache data.'
self.LOG.exception(msg)
return data

def _unprotect_cache_value(self, token, data):
""" Decrypt or verify signed data if necessary. """
if data is None:
return data

try:
if self._memcache_security_strategy == 'ENCRYPT':
return memcache_crypt.decrypt_data(token,
self._memcache_secret_key,
data)
elif self._memcache_security_strategy == 'MAC':
return memcache_crypt.verify_signed_data(token, data)
else:
return data
except:
msg = 'Failed to decrypt/verify cache data.'
self.LOG.exception(msg)
# this should have the same effect as data not found in cache
return None

def _get_cache_key(self, token):
""" Return the cache key.
Do not use clear token as key if memcache protection is on.
"""
htoken = token
if self._memcache_security_strategy in ('ENCRYPT', 'MAC'):
derv_token = token + self._memcache_secret_key
htoken = memcache_crypt.hash_data(derv_token)
return 'tokens/%s' % htoken

def _cache_get(self, token):
def _cache_get(self, token, ignore_expires=False):
"""Return token information from cache.
If token is invalid raise InvalidUserToken
return token only if fresh (not expired).
"""

if self._cache and token:
key = self._get_cache_key(token)
cached = self._cache.get(key)
cached = self._unprotect_cache_value(token, cached)
if self._memcache_security_strategy is None:
key = CACHE_KEY_TEMPLATE % token
serialized = self._cache.get(key)
else:
keys = memcache_crypt.derive_keys(
token,
self._memcache_secret_key,
self._memcache_security_strategy)
cache_key = CACHE_KEY_TEMPLATE % (
memcache_crypt.get_cache_key(keys))
raw_cached = self._cache.get(cache_key)
try:
# unprotect_data will return None if raw_cached is None
serialized = memcache_crypt.unprotect_data(keys,
raw_cached)
except Exception:
msg = 'Failed to decrypt/verify cache data'
self.LOG.exception(msg)
# this should have the same effect as data not
# found in cache
serialized = None

if serialized is None:
return None

# Note that 'invalid' and (data, expires) are the only
# valid types of serialized cache entries, so there is not
# a collision with json.loads(serialized) == None.
cached = json.loads(serialized)
if cached == 'invalid':
self.LOG.debug('Cached Token %s is marked unauthorized', token)
raise InvalidUserToken('Token authorization failed')
if cached:
data, expires = cached
if time.time() < float(expires):
self.LOG.debug('Returning cached token %s', token)
return data
else:
self.LOG.debug('Cached Token %s seems expired', token)

def _cache_store(self, token, data, expires=None):
""" Store value into memcache. """
key = self._get_cache_key(token)
data = self._protect_cache_value(token, data)
data_to_store = data
if expires:
data_to_store = (data, expires)

data, expires = cached
if ignore_expires or time.time() < float(expires):
self.LOG.debug('Returning cached token %s', token)
return data
else:
self.LOG.debug('Cached Token %s seems expired', token)

def _cache_store(self, token, data):
""" Store value into memcache.
data may be the string 'invalid' or a tuple like (data, expires)
"""
serialized_data = json.dumps(data)
if self._memcache_security_strategy is None:
cache_key = CACHE_KEY_TEMPLATE % token
data_to_store = serialized_data
else:
keys = memcache_crypt.derive_keys(
token,
self._memcache_secret_key,
self._memcache_security_strategy)
cache_key = CACHE_KEY_TEMPLATE % memcache_crypt.get_cache_key(keys)
data_to_store = memcache_crypt.protect_data(keys, serialized_data)

# we need to special-case set() because of the incompatibility between
# Swift MemcacheRing and python-memcached. See
# https://bugs.launchpad.net/swift/+bug/1095730
if self._use_keystone_cache:
self._cache.set(key,
self._cache.set(cache_key,
data_to_store,
time=self.token_cache_time)
else:
self._cache.set(key,
self._cache.set(cache_key,
data_to_store,
timeout=self.token_cache_time)

Expand Down Expand Up @@ -959,7 +950,7 @@ def _cache_put(self, token, data, expires):
"""
if self._cache:
self.LOG.debug('Storing %s token in memcache', token)
self._cache_store(token, data, expires)
self._cache_store(token, (data, expires))

def _cache_store_invalid(self, token):
"""Store invalid token in cache."""
Expand Down

0 comments on commit eeefb78

Please sign in to comment.