diff options
Diffstat (limited to 'keystone/contrib/kds')
-rw-r--r-- | keystone/contrib/kds/backends/sql.py | 111 | ||||
-rw-r--r-- | keystone/contrib/kds/controllers.py | 7 | ||||
-rw-r--r-- | keystone/contrib/kds/core.py | 129 | ||||
-rw-r--r-- | keystone/contrib/kds/routers.py | 10 |
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'])) |