Add ticket handling to KDS
Adds creation of tickets to transmit communication keys between peers. SecurityImpact Change-Id: I4dbd23adb0bdd9011eb9a0b45e30dd862d390473
This commit is contained in:
parent
33c5b9f2e3
commit
6b54c09bad
|
@ -13,6 +13,7 @@
|
||||||
import pecan
|
import pecan
|
||||||
|
|
||||||
from kite.api.v1.controllers import key as key_controller
|
from kite.api.v1.controllers import key as key_controller
|
||||||
|
from kite.api.v1.controllers import ticket as ticket_controller
|
||||||
|
|
||||||
|
|
||||||
class Controller(object):
|
class Controller(object):
|
||||||
|
@ -27,6 +28,7 @@ class Controller(object):
|
||||||
'rel': 'self'}]}
|
'rel': 'self'}]}
|
||||||
|
|
||||||
keys = key_controller.KeyController()
|
keys = key_controller.KeyController()
|
||||||
|
tickets = ticket_controller.TicketController()
|
||||||
|
|
||||||
@pecan.expose('json')
|
@pecan.expose('json')
|
||||||
def index(self):
|
def index(self):
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
import wsme
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from kite.api.v1 import models
|
||||||
|
from kite.openstack.common import jsonutils
|
||||||
|
|
||||||
|
|
||||||
|
class TicketController(rest.RestController):
|
||||||
|
|
||||||
|
@wsme.validate(models.Ticket)
|
||||||
|
@wsme_pecan.wsexpose(models.Ticket, body=models.TicketRequest)
|
||||||
|
def post(self, ticket_request):
|
||||||
|
# verify all required fields present and the signature is correct
|
||||||
|
ticket_request.verify()
|
||||||
|
|
||||||
|
# create a new random base key. With the combination of this base key
|
||||||
|
# and the information available in the metadata a client will be able
|
||||||
|
# to re-generate the keys required for this session.
|
||||||
|
rndkey = pecan.request.crypto.extract(ticket_request.source.key,
|
||||||
|
pecan.request.crypto.new_key())
|
||||||
|
|
||||||
|
# generate the keys to communicate between these two endpoints.
|
||||||
|
s_key, e_key = pecan.request.crypto.generate_keys(rndkey,
|
||||||
|
ticket_request.info)
|
||||||
|
|
||||||
|
# encrypt the base key for the target, this can be used to generate
|
||||||
|
# the sek on the target
|
||||||
|
esek_data = {'key': base64.b64encode(rndkey),
|
||||||
|
'timestamp': ticket_request.time_str,
|
||||||
|
'ttl': ticket_request.ttl.seconds}
|
||||||
|
|
||||||
|
# encrypt returns a base64 encrypted string
|
||||||
|
esek = pecan.request.crypto.encrypt(ticket_request.destination.key,
|
||||||
|
jsonutils.dumps(esek_data))
|
||||||
|
|
||||||
|
return ticket_request.new_response(e_key, s_key, esek)
|
|
@ -11,8 +11,11 @@
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from kite.api.v1.models import key
|
from kite.api.v1.models import key
|
||||||
|
from kite.api.v1.models import ticket
|
||||||
|
|
||||||
KeyInput = key.KeyInput
|
KeyInput = key.KeyInput
|
||||||
KeyData = key.KeyData
|
KeyData = key.KeyData
|
||||||
|
Ticket = ticket.Ticket
|
||||||
|
TicketRequest = ticket.TicketRequest
|
||||||
|
|
||||||
__all__ = [KeyInput, KeyData]
|
__all__ = [KeyInput, KeyData, Ticket, TicketRequest]
|
||||||
|
|
|
@ -0,0 +1,171 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import functools
|
||||||
|
|
||||||
|
import pecan
|
||||||
|
import wsme
|
||||||
|
|
||||||
|
from kite.common import exception
|
||||||
|
from kite.common import utils
|
||||||
|
from kite.openstack.common import jsonutils
|
||||||
|
from kite.openstack.common import timeutils
|
||||||
|
|
||||||
|
|
||||||
|
def memoize(f):
|
||||||
|
"""Create a property and cache the return value for future."""
|
||||||
|
@property
|
||||||
|
@functools.wraps(f)
|
||||||
|
def wrapper(self):
|
||||||
|
try:
|
||||||
|
val = self._cache[f.func_name]
|
||||||
|
except KeyError:
|
||||||
|
val = f(self)
|
||||||
|
self._cache[f.func_name] = val
|
||||||
|
|
||||||
|
return val
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def malformed(msg):
|
||||||
|
"""Raise a malformed message exception if something goes wrong."""
|
||||||
|
def wrap(f):
|
||||||
|
@functools.wraps(f)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
except Exception:
|
||||||
|
pecan.abort(400, 'Invalid %s' % msg)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
|
class Endpoint(object):
|
||||||
|
"""A source or destination for a ticket."""
|
||||||
|
|
||||||
|
def __init__(self, endpoint_str):
|
||||||
|
self._cache = dict()
|
||||||
|
self._set_endpoint(endpoint_str)
|
||||||
|
|
||||||
|
@malformed('endpoint')
|
||||||
|
def _set_endpoint(self, endpoint_str):
|
||||||
|
self.host, self.generation = utils.split_host(endpoint_str)
|
||||||
|
|
||||||
|
@memoize
|
||||||
|
def key_data(self):
|
||||||
|
try:
|
||||||
|
return pecan.request.storage.get_key(self.host, self.generation)
|
||||||
|
except exception.CryptoError:
|
||||||
|
pecan.abort(500, "Failed to decrypt key for '%s:%s'. " %
|
||||||
|
(self.host, self.generation))
|
||||||
|
except exception.KeyNotFound:
|
||||||
|
pecan.abort(404, "Could not find key")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def key(self):
|
||||||
|
return self.key_data['key']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def key_generation(self):
|
||||||
|
return self.key_data['generation']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def key_str(self):
|
||||||
|
return utils.join_host(self.host, self.key_generation)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseRequest(wsme.types.Base):
|
||||||
|
|
||||||
|
metadata = wsme.wsattr(wsme.types.text, mandatory=True)
|
||||||
|
signature = wsme.wsattr(wsme.types.text, mandatory=True)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(BaseRequest, self).__init__(**kwargs)
|
||||||
|
self._cache = dict()
|
||||||
|
self.now = timeutils.utcnow()
|
||||||
|
|
||||||
|
@memoize
|
||||||
|
@malformed("metadata")
|
||||||
|
def meta(self):
|
||||||
|
return jsonutils.loads(base64.decodestring(self.metadata))
|
||||||
|
|
||||||
|
@memoize
|
||||||
|
@malformed("source")
|
||||||
|
def source(self):
|
||||||
|
return Endpoint(self.meta['source'])
|
||||||
|
|
||||||
|
@memoize
|
||||||
|
@malformed("destination")
|
||||||
|
def destination(self):
|
||||||
|
return Endpoint(self.meta['destination'])
|
||||||
|
|
||||||
|
@memoize
|
||||||
|
@malformed("timestamp")
|
||||||
|
def timestamp(self):
|
||||||
|
return timeutils.parse_strtime(self.meta['timestamp'])
|
||||||
|
|
||||||
|
@property
|
||||||
|
@malformed("nonce")
|
||||||
|
def nonce(self):
|
||||||
|
return self.meta['nonce']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_str(self):
|
||||||
|
return timeutils.strtime(self.now)
|
||||||
|
|
||||||
|
def verify(self):
|
||||||
|
"""Ensure that the ticket request is recent enough to be valid and
|
||||||
|
the signature is correct for the requestor.
|
||||||
|
"""
|
||||||
|
if (self.now - self.timestamp) > self.ttl:
|
||||||
|
pecan.abort(401, 'Ticket validity expired')
|
||||||
|
|
||||||
|
if not self.nonce:
|
||||||
|
# just check this until we actually use it
|
||||||
|
pecan.abort(400, 'Invalid nonce')
|
||||||
|
|
||||||
|
try:
|
||||||
|
sigc = pecan.request.crypto.sign(self.source.key, self.metadata)
|
||||||
|
except exception.CryptoError:
|
||||||
|
pecan.abort(400, "Unexpected error: Couldn't reproduce signature")
|
||||||
|
|
||||||
|
if sigc != self.signature:
|
||||||
|
pecan.abort(401, 'Invalid Signature')
|
||||||
|
|
||||||
|
|
||||||
|
class BaseResponse(wsme.types.Base):
|
||||||
|
|
||||||
|
metadata = wsme.wsattr(wsme.types.text, mandatory=True)
|
||||||
|
signature = wsme.wsattr(wsme.types.text, mandatory=True)
|
||||||
|
|
||||||
|
def set_metadata(self, source, destination, expiration):
|
||||||
|
"""Attach the generation metadata to the ticket.
|
||||||
|
|
||||||
|
This informs the client and server of expiration and the expect sending
|
||||||
|
and receiving host and will be validated by both client and server.
|
||||||
|
"""
|
||||||
|
metadata = jsonutils.dumps({'source': source,
|
||||||
|
'destination': destination,
|
||||||
|
'expiration': expiration,
|
||||||
|
'encryption': True})
|
||||||
|
self.metadata = base64.b64encode(metadata)
|
||||||
|
|
||||||
|
def sign(self, key, data):
|
||||||
|
"""Sign the response.
|
||||||
|
|
||||||
|
This will be signed with the requestor's key so that it knows that the
|
||||||
|
issuing server has a correct copy of the key.
|
||||||
|
"""
|
||||||
|
self.signature = pecan.request.crypto.sign(key, self.metadata + data)
|
|
@ -0,0 +1,81 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import pecan
|
||||||
|
import wsme
|
||||||
|
|
||||||
|
from kite.api.v1.models import base
|
||||||
|
from kite.openstack.common import jsonutils
|
||||||
|
|
||||||
|
|
||||||
|
class Ticket(base.BaseResponse):
|
||||||
|
|
||||||
|
ticket = wsme.wsattr(wsme.types.text, mandatory=True)
|
||||||
|
|
||||||
|
def set_ticket(self, rkey, enc_key, signature, esek):
|
||||||
|
"""Create and encrypt a ticket to the requestor.
|
||||||
|
|
||||||
|
The requestor will be able to decrypt the ticket with their key and the
|
||||||
|
information in the metadata to get the new point-to-point key.
|
||||||
|
"""
|
||||||
|
ticket = jsonutils.dumps({'skey': base64.b64encode(signature),
|
||||||
|
'ekey': base64.b64encode(enc_key),
|
||||||
|
'esek': esek})
|
||||||
|
|
||||||
|
self.ticket = pecan.request.crypto.encrypt(rkey, ticket)
|
||||||
|
|
||||||
|
def sign(self, key):
|
||||||
|
"""Sign the ticket response.
|
||||||
|
|
||||||
|
This will be signed with the requestor's key so that it knows that the
|
||||||
|
issuing server has a correct copy of the key.
|
||||||
|
"""
|
||||||
|
super(Ticket, self).sign(key, self.ticket)
|
||||||
|
|
||||||
|
|
||||||
|
class TicketRequest(base.BaseRequest):
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(TicketRequest, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
seconds = int(pecan.request.conf.ticket_lifetime)
|
||||||
|
self.ttl = datetime.timedelta(seconds=seconds)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def info(self):
|
||||||
|
"""A predictable text string that can be used as the base for
|
||||||
|
generating keys.
|
||||||
|
"""
|
||||||
|
return "%s,%s,%s" % (self.source.key_str,
|
||||||
|
self.destination.key_str,
|
||||||
|
self.time_str)
|
||||||
|
|
||||||
|
def new_response(self, enc_key, signature, esek):
|
||||||
|
response = Ticket()
|
||||||
|
|
||||||
|
response.set_metadata(source=self.source.key_str,
|
||||||
|
destination=self.destination.key_str,
|
||||||
|
expiration=self.now + self.ttl)
|
||||||
|
|
||||||
|
# encrypt the sig and key back to the requester as well as the esek
|
||||||
|
# to forward with messages.
|
||||||
|
response.set_ticket(self.source.key, enc_key, signature, esek)
|
||||||
|
|
||||||
|
# finish building response and sign it, we sign it with the requester's
|
||||||
|
# key at the end because the ticket doesn't have to be encrypted and we
|
||||||
|
# still have to provide integrity of the ticket.
|
||||||
|
response.sign(self.source.key)
|
||||||
|
|
||||||
|
return response
|
|
@ -12,6 +12,7 @@
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import errno
|
import errno
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
|
@ -41,6 +42,8 @@ CONF.register_group(cfg.OptGroup(name='crypto',
|
||||||
title='Cryptographic Options'))
|
title='Cryptographic Options'))
|
||||||
CONF.register_opts(CRYPTO_OPTS, group='crypto')
|
CONF.register_opts(CRYPTO_OPTS, group='crypto')
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CryptoManager(utils.SingletonManager):
|
class CryptoManager(utils.SingletonManager):
|
||||||
|
|
||||||
|
@ -73,6 +76,10 @@ class CryptoManager(utils.SingletonManager):
|
||||||
try:
|
try:
|
||||||
f = os.open(CONF.crypto.master_key_file, flags, 0o600)
|
f = os.open(CONF.crypto.master_key_file, flags, 0o600)
|
||||||
os.write(f, base64.b64encode(mkey))
|
os.write(f, base64.b64encode(mkey))
|
||||||
|
except Exception as x:
|
||||||
|
_logger.warn('Failed to read master key initially: %s', e)
|
||||||
|
_logger.warn('Failed to create new master key: %s', x)
|
||||||
|
raise x
|
||||||
finally:
|
finally:
|
||||||
if f:
|
if f:
|
||||||
os.close(f)
|
os.close(f)
|
||||||
|
@ -83,7 +90,10 @@ class CryptoManager(utils.SingletonManager):
|
||||||
|
|
||||||
return mkey
|
return mkey
|
||||||
|
|
||||||
def generate_keys(self, prk, info, key_size):
|
def new_key(self, key_size=KEY_SIZE):
|
||||||
|
return self.crypto.new_key(key_size)
|
||||||
|
|
||||||
|
def generate_keys(self, prk, info, key_size=KEY_SIZE):
|
||||||
"""Generate a new key from an existing key and information.
|
"""Generate a new key from an existing key and information.
|
||||||
|
|
||||||
:param string prk: Existing pseudo-random key
|
:param string prk: Existing pseudo-random key
|
||||||
|
@ -94,6 +104,15 @@ class CryptoManager(utils.SingletonManager):
|
||||||
key = self.hkdf.expand(prk, info, 2 * key_size)
|
key = self.hkdf.expand(prk, info, 2 * key_size)
|
||||||
return key[:key_size], key[key_size:]
|
return key[:key_size], key[key_size:]
|
||||||
|
|
||||||
|
def extract(self, key, rnd_data):
|
||||||
|
return self.hkdf.extract(key, rnd_data)
|
||||||
|
|
||||||
|
def encrypt(self, key, data):
|
||||||
|
return self.crypto.encrypt(key, data)
|
||||||
|
|
||||||
|
def sign(self, key, data):
|
||||||
|
return self.crypto.sign(key, data)
|
||||||
|
|
||||||
def get_storage_keys(self, name):
|
def get_storage_keys(self, name):
|
||||||
"""Get a set of keys that will be used to encrypt the data for this
|
"""Get a set of keys that will be used to encrypt the data for this
|
||||||
identity in the database.
|
identity in the database.
|
||||||
|
|
|
@ -73,3 +73,21 @@ class SingletonManager(object):
|
||||||
@classmethod
|
@classmethod
|
||||||
def reset(cls):
|
def reset(cls):
|
||||||
cls._instance = None
|
cls._instance = None
|
||||||
|
|
||||||
|
|
||||||
|
def split_host(string):
|
||||||
|
if not string:
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
host, generation = string.rsplit(':', 1)
|
||||||
|
generation = int(generation)
|
||||||
|
except ValueError:
|
||||||
|
host = string
|
||||||
|
generation = None
|
||||||
|
|
||||||
|
return (host, generation)
|
||||||
|
|
||||||
|
|
||||||
|
def join_host(host, generation):
|
||||||
|
return "%s:%d" % (host, generation)
|
||||||
|
|
|
@ -14,6 +14,7 @@ import webtest
|
||||||
|
|
||||||
import pecan.testing
|
import pecan.testing
|
||||||
|
|
||||||
|
from kite.common import crypto
|
||||||
from kite.common import storage
|
from kite.common import storage
|
||||||
from kite.db import api as db_api
|
from kite.db import api as db_api
|
||||||
from kite.openstack.common import jsonutils
|
from kite.openstack.common import jsonutils
|
||||||
|
@ -56,13 +57,14 @@ class BaseTestCase(base.BaseTestCase):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# self.useFixture(fixture.SqliteDb())
|
self.CRYPTO = crypto.CryptoManager.get_instance()
|
||||||
self.DB = db_api.get_instance()
|
self.DB = db_api.get_instance()
|
||||||
self.STORAGE = storage.StorageManager.get_instance()
|
self.STORAGE = storage.StorageManager.get_instance()
|
||||||
|
|
||||||
self.app = pecan.testing.load_test_app(self.app_config)
|
self.app = pecan.testing.load_test_app(self.app_config)
|
||||||
self.addCleanup(pecan.set_config, {}, overwrite=True)
|
self.addCleanup(pecan.set_config, {}, overwrite=True)
|
||||||
|
|
||||||
def request(self, url, method, **kwargs):
|
def request(self, url, method, expected_status=None, **kwargs):
|
||||||
try:
|
try:
|
||||||
json = kwargs.pop('json')
|
json = kwargs.pop('json')
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|
|
@ -0,0 +1,181 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from kite.openstack.common.crypto import utils as cryptoutils
|
||||||
|
from kite.openstack.common import jsonutils
|
||||||
|
from kite.openstack.common import timeutils
|
||||||
|
from kite.tests.api.v1 import base
|
||||||
|
|
||||||
|
SOURCE_KEY = base64.b64decode('LDIVKc+m4uFdrzMoxIhQOQ==')
|
||||||
|
DEST_KEY = base64.b64decode('EEGfTxGFcZiT7oPO+brs+A==')
|
||||||
|
|
||||||
|
TEST_KEY = base64.b64decode('Jx5CVBcxuA86050355mTrg==')
|
||||||
|
|
||||||
|
DEFAULT_SOURCE = 'home.local'
|
||||||
|
DEFAULT_DEST = 'tests.openstack.remote'
|
||||||
|
DEFAULT_GROUP = 'home'
|
||||||
|
DEFAULT_NONCE = '42'
|
||||||
|
|
||||||
|
|
||||||
|
class TicketTest(base.BaseTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TicketTest, self).setUp()
|
||||||
|
|
||||||
|
self.crypto = cryptoutils.SymmetricCrypto(
|
||||||
|
enctype=self.CONF.crypto.enctype,
|
||||||
|
hashtype=self.CONF.crypto.hashtype)
|
||||||
|
|
||||||
|
def _ticket_metadata(self, source=DEFAULT_SOURCE,
|
||||||
|
destination=DEFAULT_DEST, nonce=DEFAULT_NONCE,
|
||||||
|
timestamp=None, b64encode=True):
|
||||||
|
if not timestamp:
|
||||||
|
timestamp = timeutils.utcnow()
|
||||||
|
|
||||||
|
return {'source': source, 'destination': destination,
|
||||||
|
'nonce': nonce, 'timestamp': timestamp}
|
||||||
|
|
||||||
|
def _add_key(self, name, key=None, b64encode=True):
|
||||||
|
if not key:
|
||||||
|
if name == DEFAULT_SOURCE:
|
||||||
|
key = SOURCE_KEY
|
||||||
|
elif name == DEFAULT_DEST:
|
||||||
|
key = DEST_KEY
|
||||||
|
else:
|
||||||
|
raise ValueError("No default key available")
|
||||||
|
|
||||||
|
if b64encode:
|
||||||
|
key = base64.b64encode(key)
|
||||||
|
|
||||||
|
resp = self.put('keys/%s' % name,
|
||||||
|
status=200,
|
||||||
|
json={'key': key}).json
|
||||||
|
|
||||||
|
return "%s:%s" % (resp['name'], resp['generation'])
|
||||||
|
|
||||||
|
def _request_ticket(self, metadata=None, signature=None,
|
||||||
|
source=DEFAULT_SOURCE, destination=DEFAULT_DEST,
|
||||||
|
nonce=DEFAULT_NONCE, timestamp=None,
|
||||||
|
source_key=None, status=200):
|
||||||
|
if not metadata:
|
||||||
|
metadata = self._ticket_metadata(source=source,
|
||||||
|
nonce=nonce,
|
||||||
|
destination=destination,
|
||||||
|
timestamp=timestamp)
|
||||||
|
|
||||||
|
if not isinstance(metadata, six.text_type):
|
||||||
|
metadata = base64.b64encode(jsonutils.dumps(metadata))
|
||||||
|
|
||||||
|
if not signature:
|
||||||
|
if not source_key and source == DEFAULT_SOURCE:
|
||||||
|
source_key = SOURCE_KEY
|
||||||
|
|
||||||
|
signature = self.crypto.sign(source_key, metadata)
|
||||||
|
|
||||||
|
return self.post('tickets',
|
||||||
|
json={'metadata': metadata, 'signature': signature},
|
||||||
|
status=status)
|
||||||
|
|
||||||
|
def test_valid_ticket(self):
|
||||||
|
self._add_key(DEFAULT_SOURCE)
|
||||||
|
self._add_key(DEFAULT_DEST)
|
||||||
|
|
||||||
|
response = self._request_ticket().json
|
||||||
|
|
||||||
|
b64m = response['metadata']
|
||||||
|
metadata = jsonutils.loads(base64.b64decode(b64m))
|
||||||
|
signature = response['signature']
|
||||||
|
b64t = response['ticket']
|
||||||
|
|
||||||
|
# check signature was signed to source
|
||||||
|
csig = self.crypto.sign(SOURCE_KEY, b64m + b64t)
|
||||||
|
self.assertEqual(signature, csig)
|
||||||
|
|
||||||
|
# decrypt the ticket base if required, done by source
|
||||||
|
if metadata['encryption']:
|
||||||
|
ticket = self.crypto.decrypt(SOURCE_KEY, b64t)
|
||||||
|
|
||||||
|
ticket = jsonutils.loads(ticket)
|
||||||
|
|
||||||
|
skey = base64.b64decode(ticket['skey'])
|
||||||
|
ekey = base64.b64decode(ticket['ekey'])
|
||||||
|
b64esek = ticket['esek']
|
||||||
|
|
||||||
|
# the esek part is sent to the destination, so destination should be
|
||||||
|
# able to decrypt it from here.
|
||||||
|
esek = self.crypto.decrypt(DEST_KEY, b64esek)
|
||||||
|
esek = jsonutils.loads(esek)
|
||||||
|
|
||||||
|
self.assertEqual(int(self.CONF.ticket_lifetime), esek['ttl'])
|
||||||
|
|
||||||
|
# now should be able to reconstruct skey, ekey from esek data
|
||||||
|
info = '%s,%s,%s' % (metadata['source'], metadata['destination'],
|
||||||
|
esek['timestamp'])
|
||||||
|
|
||||||
|
key = base64.b64decode(esek['key'])
|
||||||
|
new_sig, new_key = self.CRYPTO.generate_keys(key, info)
|
||||||
|
|
||||||
|
self.assertEqual(new_key, ekey)
|
||||||
|
self.assertEqual(new_sig, skey)
|
||||||
|
|
||||||
|
def test_missing_source_key(self):
|
||||||
|
self._add_key(DEFAULT_DEST)
|
||||||
|
self._request_ticket(status=404)
|
||||||
|
|
||||||
|
def test_missing_dest_key(self):
|
||||||
|
self._add_key(DEFAULT_SOURCE)
|
||||||
|
self._request_ticket(status=404)
|
||||||
|
|
||||||
|
def test_wrong_source_key(self):
|
||||||
|
# install TEST_KEY but sign with SOURCE_KEY
|
||||||
|
self._add_key(DEFAULT_SOURCE, TEST_KEY)
|
||||||
|
self._add_key(DEFAULT_DEST)
|
||||||
|
|
||||||
|
self._request_ticket(status=401)
|
||||||
|
|
||||||
|
def test_invalid_signature(self):
|
||||||
|
self._add_key(DEFAULT_SOURCE)
|
||||||
|
self._add_key(DEFAULT_DEST)
|
||||||
|
|
||||||
|
self._request_ticket(status=401, signature='bad-signature')
|
||||||
|
|
||||||
|
def test_invalid_expired_request(self):
|
||||||
|
self._add_key(DEFAULT_SOURCE)
|
||||||
|
self._add_key(DEFAULT_DEST)
|
||||||
|
|
||||||
|
timestamp = timeutils.utcnow() - datetime.timedelta(hours=5)
|
||||||
|
|
||||||
|
self._request_ticket(status=401, timestamp=timestamp)
|
||||||
|
|
||||||
|
def test_fails_on_garbage_metadata(self):
|
||||||
|
self._request_ticket(metadata='garbage',
|
||||||
|
signature='signature',
|
||||||
|
status=400)
|
||||||
|
|
||||||
|
self._request_ticket(metadata='{"json": "string"}',
|
||||||
|
signature='signature',
|
||||||
|
status=400)
|
||||||
|
|
||||||
|
def test_missing_attributes_in_metadata(self):
|
||||||
|
self._add_key(DEFAULT_SOURCE)
|
||||||
|
self._add_key(DEFAULT_DEST)
|
||||||
|
|
||||||
|
for attr in ['source', 'timestamp', 'destination', 'nonce']:
|
||||||
|
metadata = self._ticket_metadata(b64encode=False)
|
||||||
|
del metadata[attr]
|
||||||
|
|
||||||
|
self._request_ticket(metadata=metadata, status=400)
|
|
@ -10,6 +10,8 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
from oslotest import base
|
from oslotest import base
|
||||||
|
|
||||||
|
@ -17,7 +19,7 @@ from kite.common import crypto
|
||||||
from kite.common import service
|
from kite.common import service
|
||||||
from kite.common import storage
|
from kite.common import storage
|
||||||
from kite.openstack.common.fixture import config
|
from kite.openstack.common.fixture import config
|
||||||
from kite.tests import paths
|
from kite.openstack.common.fixture import mockpatch
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
CONF.import_opt('master_key_file', 'kite.common.crypto', group='crypto')
|
CONF.import_opt('master_key_file', 'kite.common.crypto', group='crypto')
|
||||||
|
@ -33,12 +35,13 @@ class BaseTestCase(base.BaseTestCase):
|
||||||
storage.StorageManager.reset()
|
storage.StorageManager.reset()
|
||||||
crypto.CryptoManager.reset()
|
crypto.CryptoManager.reset()
|
||||||
|
|
||||||
service.parse_args(args=[])
|
self.mkey = os.urandom(crypto.CryptoManager.KEY_SIZE)
|
||||||
|
patch = mockpatch.Patch(
|
||||||
|
'kite.common.crypto.CryptoManager._load_master_key',
|
||||||
|
new=lambda x: self.mkey)
|
||||||
|
self.useFixture(patch)
|
||||||
|
|
||||||
self.master_key_file = paths.tmp_path('mkey.key')
|
service.parse_args(args=[])
|
||||||
self.config(group='crypto',
|
|
||||||
master_key_file=self.master_key_file,
|
|
||||||
)
|
|
||||||
|
|
||||||
def config(self, *args, **kwargs):
|
def config(self, *args, **kwargs):
|
||||||
self.config_fixture.config(*args, **kwargs)
|
self.config_fixture.config(*args, **kwargs)
|
||||||
|
|
|
@ -12,9 +12,13 @@
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import os
|
import os
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from oslotest import base as oslo_base
|
||||||
|
|
||||||
from kite.common import crypto
|
from kite.common import crypto
|
||||||
from kite.common import exception
|
from kite.common import exception
|
||||||
|
from kite.openstack.common.fixture import config
|
||||||
from kite.tests import paths
|
from kite.tests import paths
|
||||||
from kite.tests.unit import base
|
from kite.tests.unit import base
|
||||||
|
|
||||||
|
@ -74,7 +78,13 @@ class CryptoTests(base.BaseTestCase):
|
||||||
anot_name, enc_key, sig)
|
anot_name, enc_key, sig)
|
||||||
|
|
||||||
|
|
||||||
class CryptoMasterKeyTests(base.BaseTestCase):
|
class CryptoMasterKeyTests(oslo_base.BaseTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(CryptoMasterKeyTests, self).setUp()
|
||||||
|
|
||||||
|
self.config_fixture = self.useFixture(config.Config())
|
||||||
|
self.CONF = self.config_fixture.conf
|
||||||
|
|
||||||
def _remove_file(self, f):
|
def _remove_file(self, f):
|
||||||
try:
|
try:
|
||||||
|
@ -83,11 +93,11 @@ class CryptoMasterKeyTests(base.BaseTestCase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_key_creation(self):
|
def test_key_creation(self):
|
||||||
keyfile = paths.test_path('test-kds.mkey')
|
keyfile = paths.test_path('%s.mkey' % uuid.uuid4().hex)
|
||||||
self._remove_file(keyfile)
|
self._remove_file(keyfile)
|
||||||
self.addCleanup(self._remove_file, keyfile)
|
self.addCleanup(self._remove_file, keyfile)
|
||||||
|
|
||||||
self.config(group='crypto', master_key_file=keyfile)
|
self.config_fixture.config(group='crypto', master_key_file=keyfile)
|
||||||
|
|
||||||
CRYPTO = crypto.CryptoManager()
|
CRYPTO = crypto.CryptoManager()
|
||||||
self.assertTrue(os.path.exists(keyfile))
|
self.assertTrue(os.path.exists(keyfile))
|
||||||
|
|
Loading…
Reference in New Issue