Blueprint memcache-protection: enable memcache value encryption/integrity check

DocImpact

Change-Id: I8b733256a3c2cdcf7c2ec5edac491ac4739aa847
This commit is contained in:
Guang Yee
2012-12-19 15:50:34 -08:00
parent 4851cc1751
commit 3dfb8437fc
8 changed files with 905 additions and 10 deletions

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.27.20101213.0545 (20101213.0545)
-->
<!-- Title: AuthComp Pages: 1 -->
<svg width="510pt" height="118pt"
viewBox="0.00 0.00 510.00 118.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph1" class="graph" transform="scale(1 1) rotate(0) translate(4 114)">
<title>AuthComp</title>
<polygon fill="white" stroke="white" points="-4,5 -4,-114 507,-114 507,5 -4,5"/>
<!-- AuthComp -->
<g id="node2" class="node"><title>AuthComp</title>
<polygon fill="#fdefe3" stroke="#c00000" points="292,-65 194,-65 194,-25 292,-25 292,-65"/>
<text text-anchor="middle" x="243" y="-48.4" font-family="Helvetica,sans-Serif" font-size="14.00">Auth</text>
<text text-anchor="middle" x="243" y="-32.4" font-family="Helvetica,sans-Serif" font-size="14.00">Component</text>
</g>
<!-- Reject -->
<!-- AuthComp&#45;&gt;Reject -->
<g id="edge3" class="edge"><title>AuthComp&#45;&gt;Reject</title>
<path fill="none" stroke="black" d="M193.933,-51.2787C157.514,-55.939 108.38,-62.2263 73.8172,-66.649"/>
<polygon fill="black" stroke="black" points="73.0637,-63.2168 63.5888,-67.9578 73.9522,-70.1602 73.0637,-63.2168"/>
<text text-anchor="middle" x="129" y="-97.4" font-family="Times,serif" font-size="14.00">Reject</text>
<text text-anchor="middle" x="129" y="-82.4" font-family="Times,serif" font-size="14.00">Unauthenticated</text>
<text text-anchor="middle" x="129" y="-67.4" font-family="Times,serif" font-size="14.00">Requests</text>
</g>
<!-- Service -->
<g id="node6" class="node"><title>Service</title>
<polygon fill="#d1ebf1" stroke="#1f477d" points="502,-65 408,-65 408,-25 502,-25 502,-65"/>
<text text-anchor="middle" x="455" y="-48.4" font-family="Helvetica,sans-Serif" font-size="14.00">OpenStack</text>
<text text-anchor="middle" x="455" y="-32.4" font-family="Helvetica,sans-Serif" font-size="14.00">Service</text>
</g>
<!-- AuthComp&#45;&gt;Service -->
<g id="edge5" class="edge"><title>AuthComp&#45;&gt;Service</title>
<path fill="none" stroke="black" d="M292.17,-45C323.626,-45 364.563,-45 397.52,-45"/>
<polygon fill="black" stroke="black" points="397.917,-48.5001 407.917,-45 397.917,-41.5001 397.917,-48.5001"/>
<text text-anchor="middle" x="350" y="-77.4" font-family="Times,serif" font-size="14.00">Forward</text>
<text text-anchor="middle" x="350" y="-62.4" font-family="Times,serif" font-size="14.00">Authenticated</text>
<text text-anchor="middle" x="350" y="-47.4" font-family="Times,serif" font-size="14.00">Requests</text>
</g>
<!-- Start -->
<!-- Start&#45;&gt;AuthComp -->
<g id="edge7" class="edge"><title>Start&#45;&gt;AuthComp</title>
<path fill="none" stroke="black" d="M59.1526,-21.4745C90.4482,-25.4792 142.816,-32.1802 183.673,-37.4084"/>
<polygon fill="black" stroke="black" points="183.43,-40.9057 193.793,-38.7034 184.318,-33.9623 183.43,-40.9057"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.27.20101213.0545 (20101213.0545)
-->
<!-- Title: AuthCompDelegate Pages: 1 -->
<svg width="588pt" height="104pt"
viewBox="0.00 0.00 588.00 104.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph1" class="graph" transform="scale(1 1) rotate(0) translate(4 100)">
<title>AuthCompDelegate</title>
<polygon fill="white" stroke="white" points="-4,5 -4,-100 585,-100 585,5 -4,5"/>
<!-- AuthComp -->
<g id="node2" class="node"><title>AuthComp</title>
<polygon fill="#fdefe3" stroke="#c00000" points="338,-65 240,-65 240,-25 338,-25 338,-65"/>
<text text-anchor="middle" x="289" y="-48.4" font-family="Helvetica,sans-Serif" font-size="14.00">Auth</text>
<text text-anchor="middle" x="289" y="-32.4" font-family="Helvetica,sans-Serif" font-size="14.00">Component</text>
</g>
<!-- Reject -->
<!-- AuthComp&#45;&gt;Reject -->
<g id="edge3" class="edge"><title>AuthComp&#45;&gt;Reject</title>
<path fill="none" stroke="black" d="M239.6,-50.1899C191.406,-55.2531 118.917,-62.8686 73.5875,-67.6309"/>
<polygon fill="black" stroke="black" points="73.0928,-64.1635 63.5132,-68.6893 73.8242,-71.1252 73.0928,-64.1635"/>
<text text-anchor="middle" x="152" y="-83.4" font-family="Times,serif" font-size="14.00">Reject Requests</text>
<text text-anchor="middle" x="152" y="-68.4" font-family="Times,serif" font-size="14.00">Indicated by the Service</text>
</g>
<!-- Service -->
<g id="node6" class="node"><title>Service</title>
<polygon fill="#d1ebf1" stroke="#1f477d" points="580,-65 486,-65 486,-25 580,-25 580,-65"/>
<text text-anchor="middle" x="533" y="-48.4" font-family="Helvetica,sans-Serif" font-size="14.00">OpenStack</text>
<text text-anchor="middle" x="533" y="-32.4" font-family="Helvetica,sans-Serif" font-size="14.00">Service</text>
</g>
<!-- AuthComp&#45;&gt;Service -->
<g id="edge5" class="edge"><title>AuthComp&#45;&gt;Service</title>
<path fill="none" stroke="black" d="M338.009,-49.0804C344.065,-49.4598 350.172,-49.7828 356,-50 405.743,-51.8535 418.259,-51.9103 468,-50 470.523,-49.9031 473.101,-49.7851 475.704,-49.6504"/>
<polygon fill="black" stroke="black" points="476.03,-53.1374 485.807,-49.0576 475.62,-46.1494 476.03,-53.1374"/>
<text text-anchor="middle" x="412" y="-68.4" font-family="Times,serif" font-size="14.00">Forward Requests</text>
<text text-anchor="middle" x="412" y="-53.4" font-family="Times,serif" font-size="14.00">with Identiy Status</text>
</g>
<!-- Service&#45;&gt;AuthComp -->
<g id="edge7" class="edge"><title>Service&#45;&gt;AuthComp</title>
<path fill="none" stroke="black" d="M495.062,-24.9037C486.397,-21.2187 477.064,-17.9304 468,-16 419.314,-5.63183 404.743,-5.9037 356,-16 349.891,-17.2653 343.655,-19.116 337.566,-21.2803"/>
<polygon fill="black" stroke="black" points="336.234,-18.0426 328.158,-24.9003 338.748,-24.5757 336.234,-18.0426"/>
<text text-anchor="middle" x="412" y="-33.4" font-family="Times,serif" font-size="14.00">Send Response OR</text>
<text text-anchor="middle" x="412" y="-18.4" font-family="Times,serif" font-size="14.00">Reject Message</text>
</g>
<!-- Start -->
<!-- Start&#45;&gt;AuthComp -->
<g id="edge9" class="edge"><title>Start&#45;&gt;AuthComp</title>
<path fill="none" stroke="black" d="M59.0178,-20.8384C99.2135,-25.0613 175.782,-33.1055 229.492,-38.7482"/>
<polygon fill="black" stroke="black" points="229.265,-42.2435 239.576,-39.8076 229.997,-35.2818 229.265,-42.2435"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,309 @@
..
Copyright 2011-2012 OpenStack, LLC
All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
=======================
Middleware Architecture
=======================
Abstract
========
The Keystone middleware architecture supports a common authentication protocol
in use between the OpenStack projects. By using keystone as a common
authentication and authorization mechanisms, the OpenStack project can plug in
to existing authentication and authorization systems in use by existing
environments.
In this document, we describe the architecture and responsibilities of the
authentication middleware which acts as the internal API mechanism for
OpenStack projects based on the WSGI standard.
For the architecture of keystone and its services, please see
:doc:`architecture`. This documentation primarily describes the implementation
in ``keystoneclient/middleware/auth_token.py``
(:py:class:`keystoneclient.middleware.auth_token.AuthProtocol`)
Specification Overview
======================
'Authentication' is the process of determining that users are who they say they
are. Typically, 'authentication protocols' such as HTTP Basic Auth, Digest
Access, public key, token, etc, are used to verify a user's identity. In this
document, we define an ''authentication component'' as a software module that
implements an authentication protocol for an OpenStack service. OpenStack is
using a token based mechanism to represent authentication and authorization.
At a high level, an authentication middleware component is a proxy that
intercepts HTTP calls from clients and populates HTTP headers in the request
context for other WSGI middleware or applications to use. The general flow
of the middleware processing is:
* clear any existing authorization headers to prevent forgery
* collect the token from the existing HTTP request headers
* validate the token
* if valid, populate additional headers representing the identity that has
been authenticated and authorized
* in invalid, or not token present, reject the request (HTTPUnauthorized)
or pass along a header indicating the request is unauthorized (configurable
in the middleware)
* if the keystone service is unavailable to validate the token, reject
the request with HTTPServiceUnavailable.
.. _authComponent:
Authentication Component
------------------------
Figure 1. Authentication Component
.. image:: images/graphs_authComp.svg
:width: 100%
:height: 180
:alt: An Authentication Component
The middleware may also be configured to operated in a 'delegated mode'.
In this mode, the decision reject an unauthenticated client is delegated to
the OpenStack service, as illustrated in :ref:`authComponentDelegated`.
Here, requests are forwarded to the OpenStack service with an identity status
message that indicates whether the client's identity has been confirmed or is
indeterminate. It is the OpenStack service that decides whether or not a reject
message should be sent to the client.
.. _authComponentDelegated:
Authentication Component (Delegated Mode)
-----------------------------------------
Figure 2. Authentication Component (Delegated Mode)
.. image:: images/graphs_authCompDelegate.svg
:width: 100%
:height: 180
:alt: An Authentication Component (Delegated Mode)
.. _deployStrategies:
Deployment Strategy
===================
The middleware is intended to be used inline with OpenStack wsgi components,
based on the openstack-common WSGI middleware class. It is typically deployed
as a configuration element in a paste configuration pipeline of other
middleware components, with the pipeline terminating in the service
application. The middleware conforms to the python WSGI standard [PEP-333]_.
In initializing the middleware, a configuration item (which acts like a python
dictionary) is passed to the middleware with relevant configuration options.
Configuration
-------------
The middleware is configured within the config file of the main application as
a WSGI component. Example for the auth_token middleware::
[app:myService]
paste.app_factory = myService:app_factory
[pipeline:main]
pipeline = tokenauth myService
[filter:tokenauth]
paste.filter_factory = keystone.middleware.auth_token:filter_factory
auth_host = 127.0.0.1
auth_port = 35357
auth_protocol = http
auth_uri = http://127.0.0.1:5000/
admin_token = Super999Sekret888Password777
admin_user = admin
admin_password = SuperSekretPassword
admin_tenant_name = service
;Uncomment next line to use Swift MemcacheRing
;cache = swift.cache
;Uncomment next line and check ip:port to use memcached to cache tokens
;memcache_servers = 127.0.0.1:11211
;Uncomment next 2 lines to turn on memcache protection
;memcache_security_strategy = ENCRYPT
;memcache_secret_key = change_me
;Uncomment next 2 lines if Keystone server is validating client cert
;certfile = <path to middleware public cert>
;keyfile = <path to middleware private cert>
For services which have separate paste-deploy ini file, auth_token middleware
can be alternatively configured in [keystone_authtoken] section in the main
config file. For example in Nova, all middleware parameters can be removed
from api-paste.ini::
[filter:authtoken]
paste.filter_factory = keystone.middleware.auth_token:filter_factory
and set in nova.conf::
[DEFAULT]
...
auth_strategy=keystone
[keystone_authtoken]
auth_host = 127.0.0.1
auth_port = 35357
auth_protocol = http
auth_uri = http://127.0.0.1:5000/
admin_user = admin
admin_password = SuperSekretPassword
admin_tenant_name = service
Note that middleware parameters in paste config take priority, they must be
removed to use values in [keystone_authtoken] section.
Configuration Options
---------------------
* ``auth_host``: (required) the host providing the keystone service API endpoint
for validating and requesting tokens
* ``admin_token``: either this or the following three options are required. If
set, this is a single shared secret with the keystone configuration used to
validate tokens.
* ``admin_user``, ``admin_password``, ``admin_tenant_name``: if ``admin_token``
is not set, or invalid, then admin_user, admin_password, and
admin_tenant_name are defined as a service account which is expected to have
been previously configured in Keystone to validate user tokens.
* ``delay_auth_decision``: (optional, default `0`) (off). If on, the middleware
will not reject invalid auth requests, but will delegate that decision to
downstream WSGI components.
* ``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`)
* ``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.
Caching for improved response
-----------------------------
In order to prevent every service request, the middleware may be configured
to utilize a cache, and the keystone API returns the tokens with an
expiration (configurable in duration on the keystone service). The middleware
supports memcache based caching.
* ``memcache_servers``: (optonal) if defined, the memcache server(s) to use for
cacheing. It will be ignored if Swift MemcacheRing is used instead.
* ``token_cache_time``: (optional, default 300 seconds) Only valid if
memcache_servers is defined.
When deploying auth_token middleware with Swift, user may elect
to use Swift MemcacheRing instead of the local Keystone memcache.
The Swift MemcacheRing object is passed in from the request environment
and it defaults to 'swift.cache'. However it could be
different, depending on deployment. To use Swift MemcacheRing, you must
provide the ``cache`` option.
* ``cache``: (optional) if defined, the environment key where the Swift
MemcacheRing object is stored.
Memcached and System Time
=========================
When using `memcached`_ with ``auth_token`` middleware, ensure that the system
time of memcached hosts is set to UTC. Memcached uses the host's system
time in determining whether a key has expired, whereas Keystone sets
key expiry in UTC. The timezone used by Keystone and memcached must
match if key expiry is to behave as expected.
.. _`memcached`: http://memcached.org/
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.
* ``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.
Exchanging User Information
===========================
The middleware expects to find a token representing the user with the header
``X-Auth-Token`` or ``X-Storage-Token``. `X-Storage-Token` is supported for
swift/cloud files and for legacy Rackspace use. If the token isn't present and
the middleware is configured to not delegate auth responsibility, it will
respond to the HTTP request with HTTPUnauthorized, returning the header
``WWW-Authenticate`` with the value `Keystone uri='...'` to indicate where to
request a token. The auth_uri returned is configured with the middleware.
The authentication middleware extends the HTTP request with the header
``X-Identity-Status``. If a request is successfully authenticated, the value
is set to `Confirmed`. If the middleware is delegating the auth decision to the
service, then the status is set to `Invalid` if the auth request was
unsuccessful.
Extended the request with additional User Information
-----------------------------------------------------
:py:class:`keystone.middleware.auth_token.AuthProtocol` extends the request
with additional information if the user has been authenticated.
X-Identity-Status
Provides information on whether the request was authenticated or not.
X-Tenant-Id
The unique, immutable tenant Id
X-Tenant-Name
The unique, but mutable (it can change) tenant name.
X-User-Id
The user id of the user used to log in
X-User-Name
The username used to log in
X-Roles
The roles associated with that user
Deprecated additions
--------------------
X-Tenant
Provides the tenant name. This is to support any legacy implementations
before Keystone switched to an ID/Name schema for tenants.
X-User
The username used to log in. This is to support any legacy implementations
before Keystone switched to an ID/Name schema for tenants.
X-Role
The roles associated with that user
References
==========
.. [PEP-333] pep0333 Phillip J Eby. 'Python Web Server Gateway Interface
v1.0.'' http://www.python.org/dev/peps/pep-0333/.

View File

@@ -115,6 +115,7 @@ import webob.exc
from keystoneclient.openstack.common import jsonutils
from keystoneclient.common import cms
from keystoneclient import utils
from keystoneclient.middleware import memcache_crypt
from keystoneclient.openstack.common import timeutils
CONF = None
@@ -171,6 +172,8 @@ opts = [
default=os.path.expanduser('~/keystone-signing')),
cfg.ListOpt('memcache_servers'),
cfg.IntOpt('token_cache_time', default=300),
cfg.StrOpt('memcache_security_strategy', default=None),
cfg.StrOpt('memcache_secret_key', default=None),
]
CONF.register_opts(opts, group='keystone_authtoken')
@@ -267,7 +270,17 @@ class AuthProtocol(object):
# Token caching via memcache
self._cache = None
self._use_keystone_cache = False
self._cache_initialized = False # cache already initialzied?
# memcache value treatment, ENCRYPT or MAC
self._memcache_security_strategy = \
self._conf_get('memcache_security_strategy')
if self._memcache_security_strategy is not None:
self._memcache_security_strategy = \
self._memcache_security_strategy.upper()
self._memcache_secret_key = \
self._conf_get('memcache_secret_key')
self._assert_valid_memcache_protection_config()
# By default the token will be cached for 5 minutes
self.token_cache_time = int(self._conf_get('token_cache_time'))
self._token_revocation_list = None
@@ -275,6 +288,15 @@ class AuthProtocol(object):
cache_timeout = datetime.timedelta(seconds=0)
self.token_revocation_list_cache_timeout = cache_timeout
def _assert_valid_memcache_protection_config(self):
if self._memcache_security_strategy:
if self._memcache_security_strategy not in ('MAC', 'ENCRYPT'):
raise Exception('memcache_security_strategy must be '
'ENCRYPT or MAC')
if not self._memcache_secret_key:
raise Exception('mecmache_secret_key must be defined when '
'a memcache_security_strategy is defined')
def _init_cache(self, env):
cache = self._conf_get('cache')
memcache_servers = self._conf_get('memcache_servers')
@@ -290,6 +312,7 @@ class AuthProtocol(object):
import memcache
self.LOG.info('Using Keystone memcache for caching token')
self._cache = memcache.Client(memcache_servers)
self._use_keystone_cache = True
except ImportError as e:
msg = 'disabled caching due to missing libraries %s' % (e)
self.LOG.warn(msg)
@@ -659,6 +682,54 @@ class AuthProtocol(object):
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):
"""Return token information from cache.
@@ -666,8 +737,9 @@ class AuthProtocol(object):
return token only if fresh (not expired).
"""
if self._cache and token:
key = 'tokens/%s' % token
key = self._get_cache_key(token)
cached = self._cache.get(key)
cached = self._unprotect_cache_value(token, cached)
if cached == 'invalid':
self.LOG.debug('Cached Token %s is marked unauthorized', token)
raise InvalidUserToken('Token authorization failed')
@@ -679,14 +751,32 @@ class AuthProtocol(object):
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)
# 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,
data_to_store,
time=self.token_cache_time)
else:
self._cache.set(key,
data_to_store,
timeout=self.token_cache_time)
def _cache_put(self, token, data):
"""Put token data into the cache.
""" Put token data into the cache.
Stores the parsed expire date in cache allowing
quick check of token freshness on retrieval.
"""
if self._cache and data:
key = 'tokens/%s' % token
if 'token' in data.get('access', {}):
timestamp = data['access']['token']['expires']
expires = timeutils.parse_isotime(timestamp).strftime('%s')
@@ -694,19 +784,14 @@ class AuthProtocol(object):
self.LOG.error('invalid token format')
return
self.LOG.debug('Storing %s token in memcache', token)
self._cache.set(key,
(data, expires),
time=self.token_cache_time)
self._cache_store(token, data, expires)
def _cache_store_invalid(self, token):
"""Store invalid token in cache."""
if self._cache:
key = 'tokens/%s' % token
self.LOG.debug(
'Marking token %s as unauthorized in memcache', token)
self._cache.set(key,
'invalid',
time=self.token_cache_time)
self._cache_store(token, 'invalid')
def cert_file_missing(self, called_proc_err, file_name):
return (called_proc_err.output.find(file_name)

View File

@@ -0,0 +1,157 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010-2012 OpenStack LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Utilities for memcache encryption and integrity check.
Data is serialized before been encrypted or MACed. Encryption have a
dependency on the pycrypto. If pycrypto is not available,
CryptoUnabailableError will be raised.
Encrypted data stored in memcache are prefixed with '{ENCRYPT:AES256}'.
MACed data stored in memcache are prefixed with '{MAC:SHA1}'.
"""
import base64
import functools
import hashlib
import json
import os
# make sure pycrypt is available
try:
from Crypto.Cipher import AES
except ImportError:
AES = None
# prefix marker indicating data is HMACed (signed by a secret key)
MAC_MARKER = '{MAC:SHA1}'
# prefix marker indicating data is encrypted
ENCRYPT_MARKER = '{ENCRYPT:AES256}'
class InvalidMacError(Exception):
""" raise when unable to verify MACed data
This usually indicates that data had been expectedly modified in memcache.
"""
pass
class DecryptError(Exception):
""" raise when unable to decrypt encrypted data
"""
pass
class CryptoUnavailableError(Exception):
""" raise when Python Crypto module is not available
"""
pass
def assert_crypto_availability(f):
""" Ensure Crypto module is available. """
@functools.wraps(f)
def wrapper(*args, **kwds):
if AES is None:
raise CryptoUnavailableError()
return f(*args, **kwds)
return wrapper
def generate_aes_key(token, secret):
""" Generates and returns a 256 bit AES key, based on sha256 hash. """
return hashlib.sha256(token + secret).digest()
def compute_mac(token, serialized_data):
""" Computes and returns the base64 encoded MAC. """
return hash_data(serialized_data + token)
def hash_data(data):
""" Return the base64 encoded SHA1 hash of the data. """
return base64.b64encode(hashlib.sha1(data).digest())
def sign_data(token, data):
""" MAC the data using SHA1. """
mac_data = {}
mac_data['serialized_data'] = json.dumps(data)
mac = compute_mac(token, mac_data['serialized_data'])
mac_data['mac'] = mac
md = MAC_MARKER + base64.b64encode(json.dumps(mac_data))
return md
def verify_signed_data(token, data):
""" Verify data integrity by ensuring MAC is valid. """
if data.startswith(MAC_MARKER):
try:
data = data[len(MAC_MARKER):]
mac_data = json.loads(base64.b64decode(data))
mac = compute_mac(token, mac_data['serialized_data'])
if mac != mac_data['mac']:
raise InvalidMacError('invalid MAC; expect=%s, actual=%s' %
(mac_data['mac'], mac))
return json.loads(mac_data['serialized_data'])
except:
raise InvalidMacError('invalid MAC; data appeared to be corrupted')
else:
# doesn't appear to be MACed data
return data
@assert_crypto_availability
def encrypt_data(token, secret, data):
""" Encryptes the data with the given secret key. """
iv = os.urandom(16)
aes_key = generate_aes_key(token, secret)
cipher = AES.new(aes_key, AES.MODE_CFB, iv)
data = json.dumps(data)
encoded_data = base64.b64encode(iv + cipher.encrypt(data))
encoded_data = ENCRYPT_MARKER + encoded_data
return encoded_data
@assert_crypto_availability
def decrypt_data(token, secret, data):
""" Decrypt the data with the given secret key. """
if data.startswith(ENCRYPT_MARKER):
try:
# encrypted data
encoded_data = data[len(ENCRYPT_MARKER):]
aes_key = generate_aes_key(token, secret)
decoded_data = base64.b64decode(encoded_data)
iv = decoded_data[:16]
encrypted_data = decoded_data[16:]
cipher = AES.new(aes_key, AES.MODE_CFB, iv)
decrypted_data = cipher.decrypt(encrypted_data)
return json.loads(decrypted_data)
except:
raise DecryptError('data appeared to be corrupted')
else:
# doesn't appear to be encrypted data
return data

View File

@@ -26,6 +26,7 @@ import webob
from keystoneclient.common import cms
from keystoneclient import utils
from keystoneclient.middleware import auth_token
from keystoneclient.middleware import memcache_crypt
from keystoneclient.openstack.common import jsonutils
from keystoneclient.openstack.common import timeutils
from keystoneclient.middleware import test
@@ -235,6 +236,29 @@ class FakeMemcache(object):
self.set_key = key
class FakeSwiftMemcacheRing(object):
def __init__(self):
self.set_key = None
self.set_value = None
self.token_expiration = None
def get(self, key):
data = TOKEN_RESPONSES[SIGNED_TOKEN_SCOPED_KEY].copy()
if not data or key != "tokens/%s" % (data['access']['token']['id']):
return
if not self.token_expiration:
dt = datetime.datetime.now() + datetime.timedelta(minutes=5)
self.token_expiration = dt.strftime("%s")
dt = datetime.datetime.now() + datetime.timedelta(hours=24)
ks_expires = dt.isoformat()
data['access']['token']['expires'] = ks_expires
return (data, str(self.token_expiration))
def set(self, key, value, serialize=True, timeout=0):
self.set_value = value
self.set_key = key
class FakeHTTPResponse(object):
def __init__(self, status, body):
self.status = status
@@ -641,6 +665,7 @@ class AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest):
req = webob.Request.blank('/')
req.headers['X-Auth-Token'] = SIGNED_TOKEN_SCOPED
self.middleware._cache = FakeMemcache()
self.middleware._use_keystone_cache = True
self.middleware(req.environ, self.start_fake_response)
self.assertEqual(self.middleware._cache.set_value, None)
@@ -648,6 +673,7 @@ class AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest):
req = webob.Request.blank('/')
req.headers['X-Auth-Token'] = 'invalid-token'
self.middleware._cache = FakeMemcache()
self.middleware._use_keystone_cache = True
self.middleware(req.environ, self.start_fake_response)
self.assertEqual(self.middleware._cache.set_value, "invalid")
@@ -655,6 +681,17 @@ class AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest):
req = webob.Request.blank('/')
req.headers['X-Auth-Token'] = SIGNED_TOKEN_SCOPED
self.middleware._cache = FakeMemcache()
self.middleware._use_keystone_cache = True
expired = datetime.datetime.now() - datetime.timedelta(minutes=1)
self.middleware._cache.token_expiration = float(expired.strftime("%s"))
self.middleware(req.environ, self.start_fake_response)
self.assertEqual(len(self.middleware._cache.set_value), 2)
def test_swift_memcache_set_expired(self):
req = webob.Request.blank('/')
req.headers['X-Auth-Token'] = SIGNED_TOKEN_SCOPED
self.middleware._cache = FakeSwiftMemcacheRing()
self.middleware._use_keystone_cache = False
expired = datetime.datetime.now() - datetime.timedelta(minutes=1)
self.middleware._cache.token_expiration = float(expired.strftime("%s"))
self.middleware(req.environ, self.start_fake_response)
@@ -715,6 +752,155 @@ class AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest):
seconds=40)
self.assertFalse(auth_token.will_expire_soon(fortyseconds))
def test_encrypt_cache_data(self):
conf = {
'admin_token': 'admin_token1',
'auth_host': 'keystone.example.com',
'auth_port': 1234,
'memcache_servers': 'localhost:11211',
'memcache_security_strategy': 'encrypt',
'memcache_secret_key': 'mysecret',
}
auth = auth_token.AuthProtocol(FakeApp(), conf)
encrypted_data = \
auth._protect_cache_value('token',
TOKEN_RESPONSES[UUID_TOKEN_DEFAULT])
self.assertEqual('{ENCRYPT:AES256}', encrypted_data[:16])
self.assertDictEqual(
TOKEN_RESPONSES[UUID_TOKEN_DEFAULT],
auth._unprotect_cache_value('token', encrypted_data))
# should return None if unable to decrypt
self.assertIsNone(
auth._unprotect_cache_value('token', '{ENCRYPT:AES256}corrupted'))
self.assertIsNone(
auth._unprotect_cache_value('mykey', encrypted_data))
def test_sign_cache_data(self):
conf = {
'admin_token': 'admin_token1',
'auth_host': 'keystone.example.com',
'auth_port': 1234,
'memcache_servers': 'localhost:11211',
'memcache_security_strategy': 'mac',
'memcache_secret_key': 'mysecret',
}
auth = auth_token.AuthProtocol(FakeApp(), conf)
signed_data = \
auth._protect_cache_value('mykey',
TOKEN_RESPONSES[UUID_TOKEN_DEFAULT])
expected = '{MAC:SHA1}'
self.assertEqual(
signed_data[:10],
expected)
self.assertDictEqual(
TOKEN_RESPONSES[UUID_TOKEN_DEFAULT],
auth._unprotect_cache_value('mykey', signed_data))
# should return None on corrupted data
self.assertIsNone(
auth._unprotect_cache_value('mykey', '{MAC:SHA1}corrupted'))
def test_no_memcache_protection(self):
conf = {
'admin_token': 'admin_token1',
'auth_host': 'keystone.example.com',
'auth_port': 1234,
'memcache_servers': 'localhost:11211',
'memcache_secret_key': 'mysecret',
}
auth = auth_token.AuthProtocol(FakeApp(), conf)
data = auth._protect_cache_value('mykey', 'This is a test!')
self.assertEqual(data, 'This is a test!')
self.assertEqual(
'This is a test!',
auth._unprotect_cache_value('mykey', data))
def test_get_cache_key(self):
conf = {
'admin_token': 'admin_token1',
'auth_host': 'keystone.example.com',
'auth_port': 1234,
'memcache_servers': 'localhost:11211',
'memcache_secret_key': 'mysecret',
}
auth = auth_token.AuthProtocol(FakeApp(), conf)
self.assertEqual(
'tokens/mytoken',
auth._get_cache_key('mytoken'))
conf = {
'admin_token': 'admin_token1',
'auth_host': 'keystone.example.com',
'auth_port': 1234,
'memcache_servers': 'localhost:11211',
'memcache_security_strategy': 'mac',
'memcache_secret_key': 'mysecret',
}
auth = auth_token.AuthProtocol(FakeApp(), conf)
expected = 'tokens/' + memcache_crypt.hash_data('mytoken' + 'mysecret')
self.assertEqual(auth._get_cache_key('mytoken'), expected)
conf = {
'admin_token': 'admin_token1',
'auth_host': 'keystone.example.com',
'auth_port': 1234,
'memcache_servers': 'localhost:11211',
'memcache_security_strategy': 'Encrypt',
'memcache_secret_key': 'abc!',
}
auth = auth_token.AuthProtocol(FakeApp(), conf)
expected = 'tokens/' + memcache_crypt.hash_data('mytoken' + 'abc!')
self.assertEqual(auth._get_cache_key('mytoken'), expected)
def test_assert_valid_memcache_protection_config(self):
# test missing memcache_secret_key
conf = {
'admin_token': 'admin_token1',
'auth_host': 'keystone.example.com',
'auth_port': 1234,
'memcache_servers': 'localhost:11211',
'memcache_security_strategy': 'Encrypt',
}
self.assertRaises(Exception, auth_token.AuthProtocol,
FakeApp(), conf)
# test invalue memcache_security_strategy
conf = {
'admin_token': 'admin_token1',
'auth_host': 'keystone.example.com',
'auth_port': 1234,
'memcache_servers': 'localhost:11211',
'memcache_security_strategy': 'whatever',
}
self.assertRaises(Exception, auth_token.AuthProtocol,
FakeApp(), conf)
# test missing memcache_secret_key
conf = {
'admin_token': 'admin_token1',
'auth_host': 'keystone.example.com',
'auth_port': 1234,
'memcache_servers': 'localhost:11211',
'memcache_security_strategy': 'mac',
}
self.assertRaises(Exception, auth_token.AuthProtocol,
FakeApp(), conf)
conf = {
'admin_token': 'admin_token1',
'auth_host': 'keystone.example.com',
'auth_port': 1234,
'memcache_servers': 'localhost:11211',
'memcache_security_strategy': 'Encrypt',
'memcache_secret_key': ''
}
self.assertRaises(Exception, auth_token.AuthProtocol,
FakeApp(), conf)
conf = {
'admin_token': 'admin_token1',
'auth_host': 'keystone.example.com',
'auth_port': 1234,
'memcache_servers': 'localhost:11211',
'memcache_security_strategy': 'mAc',
'memcache_secret_key': ''
}
self.assertRaises(Exception, auth_token.AuthProtocol,
FakeApp(), conf)
class TokenEncodingTest(testtools.TestCase):
def test_unquoted_token(self):

View File

@@ -0,0 +1,56 @@
import testtools
from keystoneclient.middleware import memcache_crypt
class MemcacheCryptPositiveTests(testtools.TestCase):
def test_generate_aes_key(self):
self.assertEqual(
len(memcache_crypt.generate_aes_key('Gimme Da Key', 'hush')), 32)
def test_compute_mac(self):
self.assertEqual(
memcache_crypt.compute_mac('mykey', 'This is a test!'),
'tREu41yR5tEgeBWIuv9ag4AeKA8=')
def test_sign_data(self):
expected = '{MAC:SHA1}eyJtYWMiOiAiM0FrQmdPZHRybGo1RFFESHA1eUxqcDVq' +\
'Si9BPSIsICJzZXJpYWxpemVkX2RhdGEiOiAiXCJUaGlzIGlzIGEgdG' +\
'VzdCFcIiJ9'
self.assertEqual(
memcache_crypt.sign_data('mykey', 'This is a test!'),
expected)
def test_verify_signed_data(self):
signed = memcache_crypt.sign_data('mykey', 'Testz')
self.assertEqual(
memcache_crypt.verify_signed_data('mykey', signed),
'Testz')
self.assertEqual(
memcache_crypt.verify_signed_data('aasSFWE13WER', 'not MACed'),
'not MACed')
def test_encrypt_data(self):
expected = '{ENCRYPT:AES256}'
self.assertEqual(
memcache_crypt.encrypt_data('mykey', 'mysecret',
'This is a test!')[:16],
expected)
def test_decrypt_data(self):
encrypted = memcache_crypt.encrypt_data('mykey', 'mysecret', 'Testz')
self.assertEqual(
memcache_crypt.decrypt_data('mykey', 'mysecret', encrypted),
'Testz')
self.assertEqual(
memcache_crypt.decrypt_data('mykey', 'mysecret',
'Not Encrypted!'),
'Not Encrypted!')
def test_no_pycrypt(self):
aes = memcache_crypt.AES
memcache_crypt.AES = None
self.assertRaises(memcache_crypt.CryptoUnavailableError,
memcache_crypt.encrypt_data, 'token', 'secret',
'data')
memcache_crypt.AES = aes

View File

@@ -10,6 +10,7 @@ nose-exclude
openstack.nose_plugin
nosehtmloutput
pep8==1.3.3
pycrypto
sphinx>=1.1.2
testtools>=0.9.22
WebOb>=1.0.8