summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimo Sorce <simo@redhat.com>2013-06-27 22:16:25 -0400
committerSimo Sorce <simo@redhat.com>2013-08-20 11:54:39 -0400
commit86cf469e4feed55f5b6dfc8ab0f139b39afb75b8 (patch)
tree0f067718c9bb1ca7e4e80ce8bfd7406b78c6821f
parentffa55f7a8cbc824b03cec8cbfbb380b42f9c3e70 (diff)
downloadkeystone-86cf469e4feed55f5b6dfc8ab0f139b39afb75b8.tar.gz
keystone-86cf469e4feed55f5b6dfc8ab0f139b39afb75b8.tar.xz
keystone-86cf469e4feed55f5b6dfc8ab0f139b39afb75b8.zip
Add group key support
A requestor asking for a key for a target identified as a group object will receive a group_key ticket. Group keys are temporary keys with a limited timelife and are released together with a generation number. Multiple keys with different generation numbers may exist at the same time. When no valid keys are found or if the only valid key has less than 10 minutes of lifetime a new key is generated using the next available generation number. Generation numbers grow monotonically. Group keys can be retrieved using the get_group_key call only by requestors belonging to the group. A requestor is considered as belonging to a group if the first part of the name is the same as the group. Requestors must specify a valid generation number when requesting a group key. The generation number is used to create the destination name by postfixing it to the group name after a colon. Example: requestor: scheduler.xyz.example.com destination: scheduler:123 The requestor is considered part of the scheduler group and asks for a key of generation number 123. If that key exist it will be returned encrypted with the requestor's key. blueprint key-distribution-server Change-Id: I013ae466d626c0a4737d475e1b42b183a88dbe83 Signed-off-by: Simo Sorce <simo@redhat.com>
-rw-r--r--keystone/contrib/kds/backends/sql.py111
-rw-r--r--keystone/contrib/kds/controllers.py7
-rw-r--r--keystone/contrib/kds/core.py129
-rw-r--r--keystone/contrib/kds/routers.py10
4 files changed, 236 insertions, 21 deletions
diff --git a/keystone/contrib/kds/backends/sql.py b/keystone/contrib/kds/backends/sql.py
index b7878f2a..36dec215 100644
--- a/keystone/contrib/kds/backends/sql.py
+++ b/keystone/contrib/kds/backends/sql.py
@@ -15,10 +15,43 @@
# under the License.
import base64
+import copy
import hashlib
+import time
from keystone.common import sql
+'''
+There are 2 types of keys we save here.
+Individual keys and group keys.
+
+Individual keys are identified by an id string and in 'key' is a
+dictionary the contains only the base64-encoded encrypted key in a
+field named 'key_v1'.
+Example:
+ id = d22b35312b77798eef75f195c2ed8ea78d2c26a98dedfa1c87ff0d4660636cc9
+ name = 'compute.abc.example.com'
+ key = {'key_v1': 'ERWERewre.....'}
+
+Group keys use a more complex key payload, where multiple
+'generations' of keys are stored with expiration times, as well as a
+generation counter.
+Example:
+ id = a02ba163f3a02db22fdd14b310119f1a4c2f9e4a773a573f757904dd5433d4dd
+ name = 'scheduler'
+ key = {'generation': 10,
+ 'group_keys': {'9': {'key_v1': 'ERWERewre.....',
+ 'expiration': 1234567890
+ },
+ '10': {'key_v1': 'GFDSAFhjhkd.....',
+ 'expiration': 1234509876
+ }
+ }
+
+'''
+
+EXPIRATION_TIME = 600
+
class Keys(sql.ModelBase, sql.DictBase):
__tablename__ = 'kds_keys'
@@ -58,6 +91,82 @@ class KDS(sql.Base):
session.add(key_ref)
session.flush()
+ def set_group_key(self, kds_id, expiration=None, key=None):
+ """Creates a new group of keys or sets a new key
+
+ If key is empty this function will create a abre entry in the
+ database or fail if one exists, in this case we return 0 on
+ success or an exception.
+ When a key is provided the database entry is updated with the
+ new key. Additionally any expired keys are removed.
+ We return the generation number associated to the new key on
+ success or an exception.
+
+ :param kds_id: the name/id of the key
+ :param expiration: the requested expiration time for the key
+ :param key: the actual key material (will be base64 encoded
+ before being stored in the db)
+ :returns: 0 when crating a new key, or the generation number
+ """
+
+ session = self.get_session()
+
+ if key is None:
+ # Create new group
+ with session.begin():
+ new_group = {'id': self._id_from_name(kds_id),
+ 'name': kds_id,
+ 'key': {'generation': 0, 'group_keys': dict()}}
+ key_ref = Keys.from_dict(new_group)
+ session.add(key_ref)
+ session.flush()
+ return 0
+
+ key_ref = None
+ cur_val = None
+ new_val = None
+ gen = 0
+ with session.begin():
+ id = self._id_from_name(kds_id)
+ key_ref = session.query(Keys).filter_by(id=id).first()
+ cur_val = key_ref.to_dict()
+
+ # get current entry if any, parse it and retrieve each
+ # referenced generation key, then delete all the keys expired
+ # by more than 10 minutes in a deepcopy
+ k = cur_val['key']
+ if 'generation' in k:
+ gen = k['generation']
+ if 'group_keys' in k:
+ purge_time = time.time() - EXPIRATION_TIME
+ ks = k['group_keys']
+ for g in ks:
+ if ks[g]['expiration'] < purge_time:
+ if new_val is None:
+ new_val = copy.deepcopy(cur_val)
+ del new_val['key']['group_keys'][g]
+
+ if new_val is None:
+ new_val = copy.deepcopy(cur_val)
+
+ gen = gen + 1
+ bkey = base64.b64encode(key)
+ new_val['key']['generation'] = gen
+ new_val['key']['group_keys'][gen] = {'expiration': expiration,
+ 'key_v1': bkey}
+
+ # first delete old if any
+ if key_ref is not None:
+ session.delete(key_ref)
+
+ # now store new generation list
+ key_ref = Keys.from_dict(new_val)
+ session.add(key_ref)
+
+ session.flush()
+
+ return gen
+
def get_shared_key(self, kds_id):
session = self.get_session()
id = self._id_from_name(kds_id)
@@ -65,4 +174,4 @@ class KDS(sql.Base):
if not key_ref:
return None
d = key_ref.to_dict()
- return base64.b64decode(d['key']['key_v1'])
+ return d['key']
diff --git a/keystone/contrib/kds/controllers.py b/keystone/contrib/kds/controllers.py
index 5af913d6..949dc35c 100644
--- a/keystone/contrib/kds/controllers.py
+++ b/keystone/contrib/kds/controllers.py
@@ -26,6 +26,13 @@ class KDSController(wsgi.Application):
def get_ticket(self, context, signature, request):
return self.kds_api.get_ticket(signature, request)
+ def get_group_key(self, context, signature, request):
+ return self.kds_api.get_group_key(signature, request)
+
def set_key(self, context, name, request):
self.assert_admin(context)
return self.kds_api.set_key(name, request)
+
+ def create_group(self, context, name):
+ self.assert_admin(context)
+ return self.kds_api.create_group(name)
diff --git a/keystone/contrib/kds/core.py b/keystone/contrib/kds/core.py
index d707482d..53941f80 100644
--- a/keystone/contrib/kds/core.py
+++ b/keystone/contrib/kds/core.py
@@ -47,6 +47,7 @@ CONF.register_opts(kds_opts, group='kds')
LOG = logging.getLogger(__name__)
KEY_SIZE = 16
+GROUP_KEY_TTL = 900 # 15 minutes
@dependency.provider('kds_api')
@@ -134,18 +135,52 @@ class Manager(manager.Manager):
return plain
- def _get_key(self, key_id):
+ def _gen_group_key(self, key_id):
+ """Generates a new group key."""
+ exp = time.time() + GROUP_KEY_TTL
+
+ keyblock = self.crypto.new_key(KEY_SIZE)
+ sig, enc = self._encrypt_keyblock(keyblock)
+
+ gen = self.driver.set_group_key(key_id, exp, sig + enc)
+
+ return keyblock, gen
+
+ def _get_key(self, key_id, gen=0):
"""Decrypts the provided encoded key and returns
the clear text key.
:param key_id: Key Identifier
"""
# Get Requestor's encypted key
- key = self.driver.get_shared_key(key_id)
- if not key:
- raise exception.Unauthorized('Invalid Requestor')
+ k = self.driver.get_shared_key(key_id)
+ if not k:
+ raise exception.Forbidden('Invalid Key ID')
+ if 'key_v1' in k:
+ if gen != 0:
+ raise exception.Forbidden('Invalid Key Type')
+ key = base64.b64decode(k['key_v1'])
+ elif 'group_keys' in k:
+ g = k['group_keys']
+ # find a specific generation or throw back an error
+ if gen != 0:
+ key = base64.b64decode(g[gen]['key_v1'])
+
+ # find one not expired and with at least 10 more
+ # minutes lifetime, otherwise build a new generation
+ else:
+ t = time.time()
+ last_gen = k['generation']
+ if last_gen in g and g[last_gen]['expiration'] > t:
+ key = base64.b64decode(g[last_gen]['key_v1'])
+ else:
+ return self._gen_group_key(key_id)
+ else:
+ raise exception.UnexpectedError('Unknown key format')
- return self._decrypt_keyblock(key_id, key)
+ plain = self._decrypt_keyblock(key_id, key)
+
+ return plain, gen
def _set_key(self, key_id, keyblock):
"""Encrypts the provided key and stores it.
@@ -155,18 +190,24 @@ class Manager(manager.Manager):
sig, enc = self._encrypt_keyblock(key_id, keyblock)
self.driver.set_shared_key(key_id, sig + enc)
- def get_ticket(self, sig, req):
- if not ('metadata' in req):
+ def _common_request(self, sig, req):
+ if 'metadata' not in req:
raise exception.Forbidden('Invalid Request format')
try:
meta = jsonutils.loads(base64.b64decode(req['metadata']))
except Exception:
raise exception.Forbidden('Invalid Request format')
- if not ('requestor' in meta):
+ if 'requestor' not in meta:
raise exception.Forbidden('Invalid Request format')
- rkey = self._get_key(meta['requestor'])
+ # Requests can be made only by non-group identities
+ try:
+ rkey, gen = self._get_key(meta['requestor'], 0)
+ except exception.UnexpectedError:
+ raise exception.Unauthorized('Invalid Request')
+ if gen != 0:
+ raise exception.Unauthorized('Invalid Request')
try:
signature = self.crypto.sign(rkey, req['metadata'])
@@ -176,21 +217,33 @@ class Manager(manager.Manager):
if signature != sig:
raise exception.Unauthorized('Invalid Request')
+ #TODO(simo): check and store signature for replay attack detection
+
timestamp = time.time()
if meta['timestamp'] < (timestamp - self.ttl):
raise exception.Unauthorized('Invalid Request (expired)')
- #TODO(simo): check and store signature for replay attack detection
+ return meta, rkey, timestamp
+
+ def get_ticket(self, sig, req):
- tkey = self._get_key(meta['target'])
+ meta, rkey, timestamp = self._common_request(sig, req)
+
+ source = meta['requestor']
+ target = meta['target']
+
+ tkey, gen = self._get_key(target)
if not tkey:
- raise exception.Unauthorized('Invalid Target')
+ raise exception.Unauthorized('Invalid Destination')
+
+ if gen != 0:
+ target += ':' + str(gen)
# use new_key to get a random salt
rndkey = self.hkdf.extract(rkey, self.crypto.new_key(KEY_SIZE))
- info = '%s,%s,%s' % (meta['requestor'], meta['target'], str(timestamp))
+ info = '%s,%s,%s' % (source, target, str(timestamp))
sek = self.hkdf.expand(rndkey, info, KEY_SIZE * 2)
skey, ekey = self._split_key(sek, KEY_SIZE)
@@ -202,21 +255,53 @@ class Manager(manager.Manager):
'esek': esek})
rep = dict()
- metadata = jsonutils.dumps({'source': meta['requestor'],
- 'destination': meta['target'],
- 'expiration': (keydata['timestamp']
- + keydata['ttl']),
- 'encryption': True})
+ metadata = jsonutils.dumps({'source': source, 'destination': target,
+ 'expiration': (timestamp + self.ttl)})
rep['metadata'] = base64.b64encode(metadata)
rep['ticket'] = self.crypto.encrypt(rkey, ticket)
- rep['signature'] = self.crypto.sign(rkey,
- (rep['metadata'] + rep['ticket']))
+
+ payload = rep['metadata'] + rep['ticket']
+ rep['signature'] = self.crypto.sign(rkey, payload)
+
+ return {'reply': rep}
+
+ def get_group_key(self, sig, req):
+
+ meta, rkey, timestamp = self._common_request(sig, req)
+
+ source = meta['requestor']
+ target = meta['target']
+
+ group = source.split('.')[0]
+
+ if ':' not in target:
+ raise exception.Unauthorized('Invalid Target')
+ grp, generation = target.split(':', 1)
+
+ if group != grp:
+ raise exception.Unauthorized('Invalid Target')
+
+ tkey, gen = self._get_key(group, generation)
+ if not tkey:
+ raise exception.Unauthorized('Invalid Target')
+
+ rep = dict()
+ metadata = jsonutils.dumps({'source': source, 'destination': target,
+ 'expiration': (timestamp + self.ttl)})
+ rep['metadata'] = base64.b64encode(metadata)
+ rep['group_key'] = self.crypto.encrypt(rkey, tkey)
+
+ payload = rep['metadata'] + rep['group_key']
+ rep['signature'] = self.crypto.sign(rkey, payload)
return {'reply': rep}
def set_key(self, name, req):
self._set_key(name, base64.b64decode(req['key']))
+ def create_group(self, name):
+ self.driver.set_group_key(name)
+
class Driver(object):
"""Interface description for a KDS driver."""
@@ -225,6 +310,10 @@ class Driver(object):
"""Set key related to kds_id."""
raise exception.NotImplemented()
+ def set_group_key(self, kds_id, expiration=None, key=None):
+ """Set or Create group key object."""
+ raise exception.NotImplemented()
+
def get_shared_key(self, kds_id):
"""Get key related to kds_id.
diff --git a/keystone/contrib/kds/routers.py b/keystone/contrib/kds/routers.py
index 49b77796..a04b948a 100644
--- a/keystone/contrib/kds/routers.py
+++ b/keystone/contrib/kds/routers.py
@@ -33,7 +33,17 @@ class KDSExtension(wsgi.ExtensionRouter):
action='get_ticket',
conditions=dict(method=['POST']))
+ mapper.connect('/kds/group_key/{signature}',
+ controller=kds_controller,
+ action='get_group_key',
+ conditions=dict(method=['POST']))
+
mapper.connect('/kds/key/{name}',
controller=kds_controller,
action='set_key',
conditions=dict(method=['PUT']))
+
+ mapper.connect('/kds/group/{name}',
+ controller=kds_controller,
+ action='create_group',
+ conditions=dict(method=['PUT']))