diff --git a/CHANGES.txt b/CHANGES.txt index 4fbac9d..fe98535 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,10 +2,14 @@ CHANGES ======= -1.1.2 (unreleased) +2.0.0 (unreleased) ------------------ -- Nothing changed yet. +- Refactored REST server to be a simple repoze.bfg application. + +- The encrypted data encrypting keys (DEKs) are now stored in a directory + instead of the ZODB. This increases transparency in the data store and makes + backups easier. 1.1.1 (2010-08-27) diff --git a/buildout.cfg b/buildout.cfg index 9c57f36..573c009 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -1,7 +1,8 @@ [buildout] develop = . extends = http://download.zope.org/bluebream/bluebream-1.0b3.cfg -parts = test coverage-test coverage-report python paster runserver testclient ctags +parts = test coverage-test coverage-report python paster runserver testclient + ctags versions = versions [test] @@ -11,12 +12,12 @@ eggs = keas.kmi [test] [coverage-test] recipe = zc.recipe.testrunner eggs = keas.kmi [test] -defaults = ['--coverage', '../../coverage'] +defaults = ['--coverage', '${buildout:directory}/coverage'] [coverage-report] recipe = zc.recipe.egg eggs = z3c.coverage -scripts = coverage=coverage-report +scripts = coveragereport=coverage-report arguments = ('coverage', 'coverage/report') [python] @@ -30,19 +31,11 @@ eggs = keas.kmi [paster] recipe = zc.recipe.egg -eggs = Paste - PasteScript - PasteDeploy +dependent-scripts = true +eggs = keas.kmi + Paste pyOpenSSL - zope.app.component - zope.app.publication - zope.app.publisher - zope.app.security - zope.component - zope.error - zope.publisher - zope.securitypolicy - keas.kmi +scripts = paster [runserver] recipe = zc.recipe.egg @@ -57,3 +50,4 @@ eggs = keas.kmi [versions] setuptools = 0.6c12dev-r84273 zc.buildout = 1.5.0 +zc.recipe.egg = 1.3.0 diff --git a/server.ini b/server.ini index 180fb04..e59917a 100644 --- a/server.ini +++ b/server.ini @@ -1,9 +1,43 @@ [app:main] use = egg:keas.kmi -conf=zope.conf +storage-dir=keys/ [server:main] use = egg:Paste#http host = 0.0.0.0 port = 8080 ssl_pem = sample.pem + +# Logging Configuration +[loggers] +keys = root, kmi + +[handlers] +keys = console, file + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_kmi] +level = INFO +handlers = console +propagate = 0 +qualname = kmi + +[handler_file] +class = FileHandler +args = ('source-cache.log',) +formatter = generic + +[handler_console] +class = StreamHandler +args = (sys.stdout,) +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s +datefmt= %Y-%m-%d %H:%M:%S diff --git a/setup.py b/setup.py index 2d33305..93c0f73 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def read(*rnames): setup ( name='keas.kmi', - version = '1.1.2dev', + version = '2.0.0dev', author = "Stephan Richter and the Zope Community", author_email = "zope-dev@zope.org", description = "A Key Management Infrastructure", @@ -36,7 +36,7 @@ def read(*rnames): read('CHANGES.txt') ), license = "ZPL 2.1", - keywords = "zope3 security key management infrastructure nist 800-57", + keywords = "security key management infrastructure nist 800-57", classifiers = [ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', @@ -46,7 +46,7 @@ def read(*rnames): 'Natural Language :: English', 'Operating System :: OS Independent', 'Topic :: Internet :: WWW/HTTP', - 'Framework :: Zope3'], + 'Framework :: Repoze'], url = 'http://pypi.python.org/pypi/keas.kmi', packages = find_packages('src'), include_package_data = True, @@ -60,14 +60,9 @@ def read(*rnames): ), install_requires = [ 'M2Crypto', - 'ZODB3', + 'repoze.bfg', 'setuptools', - 'z3c.rest', - 'zope.app.wsgi', - 'zope.annotation', - 'zope.component', - 'zope.container', - 'zope.dublincore', + 'transaction', 'zope.interface', 'zope.schema', ], diff --git a/src/keas/kmi/README.txt b/src/keas/kmi/README.txt index 9f78ef4..75a0d3f 100644 --- a/src/keas/kmi/README.txt +++ b/src/keas/kmi/README.txt @@ -4,10 +4,14 @@ Key Management Infrastructure This package provides a NIST SP 800-57 compliant key management infrastructure. Part of this infrastructure is a key management facility that -provides several services related to keys. +provides several services related to keys. All keys are stored in a specified +storage directory. + + >>> import tempfile + >>> storage_dir = tempfile.mkdtemp() >>> from keas.kmi import facility - >>> keys = facility.KeyManagementFacility() + >>> keys = facility.KeyManagementFacility(storage_dir) >>> keys @@ -67,8 +71,8 @@ You can now use this key encrypting key to extract the encryption keys: ... from md5 import md5 >>> hash = md5(key) - >>> keys.get(hash.hexdigest()) - + >>> len(keys.get(hash.hexdigest())) + 64 Our key management facility also supports the encryption service, which allows you to encrypt and decrypt a string given the key encrypting key. @@ -214,13 +218,10 @@ key. So let's have a look at the call: >>> from keas.kmi import rest - >>> from zope.publisher.browser import TestRequest - - >>> request = TestRequest() - >>> request.method = 'POST' + >>> from webob import Request - >>> newCall = rest.NewView(keys, request) - >>> key3 = newCall() + >>> request = Request({}) + >>> key3 = rest.create_key(keys, request).body >>> print key3 -----BEGIN RSA PRIVATE KEY----- ... @@ -229,7 +230,6 @@ So let's have a look at the call: The key is available in the facility of course: >>> hash = md5(key3) - >>> hash.hexdigest() in keys True @@ -240,39 +240,27 @@ We can now fetch the encryption key pair using a `POST` call to this URL:: The request sends the key encrypting key in its body. The response is the encryption key string: - >>> import cStringIO - >>> io = cStringIO.StringIO(key3) - - >>> request = TestRequest(io) - >>> request.method = 'POST' + >>> request = Request({}) + >>> request.body = key3 - >>> keyCall = rest.KeyView(keys, request) - >>> encKey = keyCall() - >>> len(encKey) + >>> encKey = rest.get_key(keys, request) + >>> len(encKey.body) 32 -If you try to request a nonexistent key, you get a 404 error: -encryption key string: - - >>> import cStringIO - >>> io = cStringIO.StringIO('xyzzy') +If you try to request a nonexistent key, you get a 404 error: encryption key +string: - >>> request = TestRequest(io) - >>> request.method = 'POST' - - >>> keyCall = rest.KeyView(keys, request) - >>> print keyCall() + >>> request.body = 'xxyz' + >>> print rest.get_key(keys, request) Key not found - >>> request.response.getStatus() - 404 A `GET` request to the root shows us a server status page - >>> request = TestRequest() - >>> request.method = 'GET' - - >>> newCall = rest.StatusView(keys, request) - >>> print newCall() + >>> print rest.get_status(keys, Request({})) + 200 OK + Content-Type: text/plain + Content-Length: 25 + KMS server holding 3 keys @@ -283,7 +271,8 @@ The testing facility only manages a single key that is always constant. This allows you to install a testing facility globally, not storing the keys in the database and still reuse a ZODB over multiple sessions. - >>> testingKeys = testing.TestingKeyManagementFacility() + >>> storage_dir = tempfile.mkdtemp() + >>> testingKeys = testing.TestingKeyManagementFacility(storage_dir) Of course, the key generation service is supported: @@ -299,7 +288,9 @@ However, you will always receive the same key: 'MIIBOAIBAAJBAL+VS9lDsS9XOaeJppfK9lhxKMRFdcg50MR3aJEQK9rvDEqNwBS9' >>> getKeySegment(testingKeys.generate()) 'MIIBOAIBAAJBAL+VS9lDsS9XOaeJppfK9lhxKMRFdcg50MR3aJEQK9rvDEqNwBS9' - >>> testingKeys = testing.TestingKeyManagementFacility() + + >>> storage_dir = tempfile.mkdtemp() + >>> testingKeys = testing.TestingKeyManagementFacility(storage_dir) >>> getKeySegment(testingKeys.generate()) 'MIIBOAIBAAJBAL+VS9lDsS9XOaeJppfK9lhxKMRFdcg50MR3aJEQK9rvDEqNwBS9' @@ -314,3 +305,15 @@ We can also safely en- and decrypt: >>> encrypted = testingKeys.encrypt(key, 'Stephan Richter') >>> testingKeys.decrypt(key, encrypted) 'Stephan Richter' + + +Key Holder +---------- + +The key holder is a simple class designed to store a key in RAM: + + >>> from keas.kmi import keyholder + >>> holder = keyholder.KeyHolder(__file__) + + >>> verify.verifyObject(interfaces.IKeyHolder, holder) + True diff --git a/src/keas/kmi/apidoc.zcml b/src/keas/kmi/apidoc.zcml deleted file mode 100644 index 2766770..0000000 --- a/src/keas/kmi/apidoc.zcml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - diff --git a/src/keas/kmi/application.py b/src/keas/kmi/application.py deleted file mode 100644 index 3715ae3..0000000 --- a/src/keas/kmi/application.py +++ /dev/null @@ -1,21 +0,0 @@ -############################################################################## -# -# Copyright (c) 2008 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -""" -$Id$ -""" -__docformat__ = "reStructuredText" -from zope.error.error import RootErrorReportingUtility - -globalErrorReportingUtility = RootErrorReportingUtility() -globalErrorReportingUtility.setProperties(20, True, ()) diff --git a/src/keas/kmi/application.zcml b/src/keas/kmi/application.zcml deleted file mode 100644 index 8328469..0000000 --- a/src/keas/kmi/application.zcml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/keas/kmi/configure.zcml b/src/keas/kmi/configure.zcml index fd81f15..dae30b6 100644 --- a/src/keas/kmi/configure.zcml +++ b/src/keas/kmi/configure.zcml @@ -1,35 +1,26 @@ - + - - + - - - - - - + - - - - + + + diff --git a/src/keas/kmi/db.py b/src/keas/kmi/db.py deleted file mode 100644 index b2974a2..0000000 --- a/src/keas/kmi/db.py +++ /dev/null @@ -1,41 +0,0 @@ -############################################################################## -# -# Copyright (c) 2008 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -"""Automatic ZODB installation - -$Id$ -""" -__docformat__ = "reStructuredText" - -import transaction -from zope.component import adapter -from zope.app.appsetup.interfaces import IDatabaseOpenedEvent -from zope.app.appsetup.bootstrap import getInformationFromEvent -from zope.app.publication.zopepublication import ZopePublication - -from keas.kmi.facility import KeyManagementFacility -from keas.kmi.interfaces import IKeyManagementFacility - - -@adapter(IDatabaseOpenedEvent) -def bootstrapKeyManagementFacility(event): - """Installs KeyManagementFacility as the root object of the DB.""" - db, connection, root, root_object = getInformationFromEvent(event) - if root_object is None: - root[ZopePublication.root_name] = KeyManagementFacility() - transaction.commit() - elif not IKeyManagementFacility.providedBy(root_object): - raise RuntimeError('Your database root object is not a key management' - ' facility. Remove your Data.fs and try again.') - connection.close() - diff --git a/src/keas/kmi/db.zcml b/src/keas/kmi/db.zcml deleted file mode 100644 index fa35677..0000000 --- a/src/keas/kmi/db.zcml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/src/keas/kmi/facility.py b/src/keas/kmi/facility.py index 1fc671b..3fe6575 100644 --- a/src/keas/kmi/facility.py +++ b/src/keas/kmi/facility.py @@ -12,51 +12,31 @@ # ############################################################################## """Implementation of Key Management Facility - -$Id$ """ from __future__ import absolute_import __docformat__ = "reStructuredText" -import datetime +import M2Crypto +import os +import httplib +import logging import time +import urlparse +import zope.interface +from keas.kmi import interfaces + try: from hashlib import md5 except ImportError: from md5 import md5 -import M2Crypto -import persistent -import zope.interface -import zope.location -from z3c.rest import client -from zope.annotation.interfaces import IAttributeAnnotatable -from zope.container import btree -from zope.dublincore import property -from zope.schema.fieldproperty import FieldProperty - -from keas.kmi import interfaces - - -class Key(zope.location.Location, persistent.Persistent): - zope.interface.implements(interfaces.IKey, IAttributeAnnotatable) - - created = property.DCProperty('created') - creator = property.DCProperty('creator') - key = FieldProperty(interfaces.IKey['key']) - - def __init__(self, key): - self.key = key - - def __repr__(self): - return '<%s %r>' %(self.__class__.__name__, self.key) - +logger = logging.getLogger('kmi') class EncryptionService(object): cipher = 'aes_256_cbc' - # Note: decryption fails if you use an empty initialization vector; but it + # Note: Decryption fails if you use an empty initialization vector; but it # only fails when you restart the Python process. The length of the # initialization vector is assumed to be 16 bytes because that's what # openssl aes-256-cbc -nosalt -P -k 'a' @@ -90,7 +70,7 @@ def decrypt(self, key, data): return decrypted -class KeyManagementFacility(EncryptionService, btree.BTreeContainer): +class KeyManagementFacility(EncryptionService): zope.interface.implements(interfaces.IExtendedKeyManagementFacility) rsaKeyLength = 512 # The length of the key encrypting key @@ -100,6 +80,54 @@ class KeyManagementFacility(EncryptionService, btree.BTreeContainer): keyLength = rsaKeyLength/16 + def __init__(self, storage_dir): + self.storage_dir = storage_dir + + def keys(self): + return [filename[:-4] for filename in os.listdir(self.storage_dir) + if filename.endswith('.dek')] + + def __iter__(self): + return iter(self.keys()) + + def __getitem__(self, name): + if name+'.dek' not in os.listdir(self.storage_dir): + raise KeyError(name) + fn = os.path.join(self.storage_dir, name+'.dek') + with open(fn, 'rb') as file: + return file.read() + + def get(self, name, default=None): + try: + return self[name] + except KeyError: + return default + + def values(self): + return [value for name, value in self.items()] + + def __len__(self): + return len(self.keys()) + + def items(self): + return [(name, self[name]) for name in self.keys()] + + def __contains__(self, name): + return name in self.keys() + + has_key = __contains__ + + def __setitem__(self, name, key): + fn = os.path.join(self.storage_dir, name+'.dek') + with open(fn, 'w') as file: + return file.write(key) + logger.info('New key added (hash): %s', name) + + def __delitem__(self, name): + fn = os.path.join(self.storage_dir, name+'.dek') + os.remove(fn) + logger.info('Key removed (hash): %s', name) + def generate(self): """See interfaces.IKeyGenerationService""" # 1. Generate the private/public RSA key encrypting key @@ -117,7 +145,7 @@ def generate(self): hash.update(privateKey) # 5. Save the encryption key encryptedKey = rsa.public_encrypt(key, self.rsaPadding) - self[hash.hexdigest()] = Key(encryptedKey) + self[hash.hexdigest()] = encryptedKey # 6. Return the private key encrypting key return privateKey @@ -127,11 +155,12 @@ def getEncryptionKey(self, key): hash = md5() hash.update(key) # 2. Extract the encrypted encryption key - encryptedKey = self[hash.hexdigest()].key + encryptedKey = self[hash.hexdigest()] # 3. Decrypt the key. rsa = M2Crypto.RSA.load_key_string(key) decryptedKey = rsa.private_decrypt(encryptedKey, self.rsaPadding) # 4. Return decrypted encryption key + logger.info('Encryption key requested: %s', hash.hexdigest()) return decryptedKey def __repr__(self): @@ -143,7 +172,7 @@ class LocalKeyManagementFacility(EncryptionService): zope.interface.implements(interfaces.IKeyManagementFacility) timeout = 3600 - clientClass = client.RESTClient + httpConnFactory = httplib.HTTPSConnection def __init__(self, url): self.url = url @@ -151,18 +180,21 @@ def __init__(self, url): def generate(self): """See interfaces.IKeyGenerationService""" - client = self.clientClass(self.url) - client.post('/new') - return client.contents + pieces = urlparse.urlparse(self.url) + conn = self.httpConnFactory(pieces.netloc) + conn.request('POST', '/new', '', {}) + response = conn.getresponse() + return response.read() def getEncryptionKey(self, key): """Given the key encrypting key, get the encryption key.""" if (key in self._cache and self._cache[key][0] + self.timeout > time.time()): return self._cache[key][1] - client = self.clientClass(self.url) - client.post('/key', key, headers={'content-type': 'text/plain'}) - encryptionKey = client.contents + pieces = urlparse.urlparse(self.url) + conn = self.httpConnFactory(pieces.netloc) + conn.request('POST', '/key', key, {'content-type': 'text/plain'}) + encryptionKey = conn.getresponse().read() self._cache[key] = (time.time(), encryptionKey) return encryptionKey diff --git a/src/keas/kmi/facility.txt b/src/keas/kmi/facility.txt new file mode 100644 index 0000000..1d654a2 --- /dev/null +++ b/src/keas/kmi/facility.txt @@ -0,0 +1,58 @@ +========================= +Key Management Facilities +========================= + +The default key managment facility implements the +``IExtendedKeyManagementFacility``. This interface extends ``IMapping`` so +that the entire mapping interface is supported + + >>> import tempfile + >>> from keas.kmi import facility + >>> kmf = facility.KeyManagementFacility(tempfile.mkdtemp()) + + >>> key1 = kmf.generate() + >>> key2 = kmf.generate() + >>> key3 = kmf.generate() + +There should be 3 keys now: + + >>> len(kmf) + 3 + +And their hash values are the keys of the map: + + >>> kmf.keys() + ['...', '...', '...'] + >>> list(iter(kmf)) + ['...', '...', '...'] + +Now let's get one key: + + >>> hash = kmf.keys()[0] + >>> len(kmf[hash]) + 64 + + >>> len(kmf.get(hash)) + 64 + >>> kmf.get('xyz', 'default') + 'default' + +We can also check whether a hash is int he facility: + + >>> hash in kmf + True + >>> 'xyz' in hash + False + +Of course you can get values and items as well: + + >>> len(kmf.values()) + 3 + >>> len(kmf.items()) + 3 + +Finally, one can delete keys using their hash as well. + + >>> del kmf[hash] + >>> len(kmf) + 2 diff --git a/src/keas/kmi/interfaces.py b/src/keas/kmi/interfaces.py index 841102c..00d519c 100644 --- a/src/keas/kmi/interfaces.py +++ b/src/keas/kmi/interfaces.py @@ -11,32 +11,12 @@ # FOR A PARTICULAR PURPOSE. # ############################################################################## -""" -$Id$ +"""Key Management Interfaces """ __docformat__ = "reStructuredText" import zope.interface import zope.schema -from zope.container import interfaces - - -class IKey(zope.interface.Interface): - """Encryption Key""" - - created = zope.schema.Datetime( - title=u'Creation Date/Time', - description=u'The date/time the key pair was created.', - required=True) - - creator = zope.schema.TextLine( - title=u'Creator', - description=u'The principal/user that requested the new key.', - required=True) - - key = zope.schema.Bytes( - title=u'Key', - description=u'The key used to encrypt and decrypt the data.', - required=True) +from zope.interface.common import mapping class IEncryptionService(zope.interface.Interface): @@ -65,11 +45,10 @@ class IKeyManagementFacility(IEncryptionService, IKeyGenerationService): """ class IExtendedKeyManagementFacility(IKeyManagementFacility, - interfaces.IContainer): + mapping.IMapping): """Extended Key Management Facility. - This facility also also the management of the keys via Python's mapping - API. + This facility also allows access of the keys via Python's mapping API. """ diff --git a/src/keas/kmi/rest.py b/src/keas/kmi/rest.py index e98c693..6175f43 100644 --- a/src/keas/kmi/rest.py +++ b/src/keas/kmi/rest.py @@ -12,42 +12,25 @@ # ############################################################################## """REST-API to master key management facility - -$Id$ """ -from zope.publisher.browser import BrowserPage - -class RestView(BrowserPage): - - def __call__(self): - method = self.request.method - if not hasattr(self, method): - self.request.response.setStatus(405) - return 'Method not allowed.' - return getattr(self, method)() - -class StatusView(RestView): - - def GET(self): - self.request.response.setHeader('content-type', 'text/plain') - return 'KMS server holding %d keys' % len(self.context) - -class NewView(RestView): - - def POST(self): - self.request.response.setHeader('content-type', 'text/plain') - return self.context.generate() - -class KeyView(RestView): - - def POST(self): - stream = self.request.bodyStream.getCacheStream() - stream.seek(0) - key = stream.read() - self.request.response.setHeader('content-type', 'text/plain') - try: - return self.context.getEncryptionKey(key) - except KeyError: - self.request.response.setStatus(404) - return 'Key not found' +from webob import Response, exc + +def get_status(context, request): + return Response( + 'KMS server holding %d keys' %len(context), + headerlist=[('Content-Type', 'text/plain')]) + +def create_key(context, request): + return Response( + context.generate(), + headerlist=[('Content-Type', 'text/plain')]) + +def get_key(context, request): + key = request.body + try: + return Response( + context.getEncryptionKey(key), + headerlist=[('Content-Type', 'text/plain')]) + except KeyError: + return exc.HTTPNotFound('Key not found') diff --git a/src/keas/kmi/security.zcml b/src/keas/kmi/security.zcml deleted file mode 100644 index 286b5b5..0000000 --- a/src/keas/kmi/security.zcml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - diff --git a/src/keas/kmi/testclient.py b/src/keas/kmi/testclient.py index 1660ca8..bcbb164 100644 --- a/src/keas/kmi/testclient.py +++ b/src/keas/kmi/testclient.py @@ -19,15 +19,18 @@ import sys import optparse import textwrap +import urlparse from keas.kmi.facility import LocalKeyManagementFacility - def ping(kmf): - client = kmf.clientClass(kmf.url) - print client.fullStatus + pieces = urlparse.urlparse(kmf.url) + conn = kmf.httpConnFactory(pieces.netloc) + conn.request('GET', '/') + response = conn.getresponse() + print response.status, response.reason print - print client.contents + print response.read() def new_key(kmf): @@ -75,50 +78,58 @@ def decrypt(kmf, kekfile, filename=None): sys.stdout.write(decrypted) -def main(): - parser = optparse.OptionParser(textwrap.dedent("""\ - usage: %prog URL - see if the server is alive - - %prog URL -n > key.txt - generate a new key and key encrypting key - - %prog URL -e key.txt data.txt > encrypted.txt - encrypt data - - %prog URL -d key.txt encrytped.txt > data.txt - decrypt data - - %prog URL -g key.txt > secretkey.bin - get the secret encryption key - """.rstrip()), - description="Client for a Key Management Server.") - parser.add_option('-n', '--new', - help='generate a new key', - action='store_const', dest='action', - const=new_key) - parser.add_option('-g', '--get-key', - help='get key', - action='store_const', dest='action', - const=get_key) - parser.add_option('-e', '--encrypt', - help='encrypt data', - action='store_const', dest='action', - const=encrypt) - parser.add_option('-d', '--decrypt', - help='decrypt data', - action='store_const', dest='action', - const=decrypt) - opts, args = parser.parse_args() +parser = optparse.OptionParser(textwrap.dedent("""\ + %prog URL + + see if the server is alive + + %prog URL -n > key.txt + generate a new key and key encrypting key + + %prog URL -e key.txt data.txt > encrypted.txt + encrypt data + + %prog URL -d key.txt encrytped.txt > data.txt + decrypt data + + %prog URL -g key.txt > secretkey.bin + get the secret encryption key + """.rstrip()), + description="Client for a Key Management Server.") +parser.add_option( + '-n', '--new', + help='generate a new key', + action='store_const', dest='action', + const=new_key) +parser.add_option( + '-g', '--get-key', + help='get key', + action='store_const', dest='action', + const=get_key) +parser.add_option( + '-e', '--encrypt', + help='encrypt data', + action='store_const', dest='action', + const=encrypt) +parser.add_option( + '-d', '--decrypt', + help='decrypt data', + action='store_const', dest='action', + const=decrypt) + +def main(argv=None): + if argv is None: + argv = sys.argv[1:] + opts, args = parser.parse_args(argv) if not opts.action: opts.action = ping if not args: - parser.error('please specify the KMS server URL') + parser.error('Please specify the KMS server URL') url = args.pop(0) kmf = LocalKeyManagementFacility(url) try: opts.action(kmf, *args) - except TypeError: + except TypeError, err: parser.error('incorrect number of arguments') diff --git a/src/keas/kmi/testing.py b/src/keas/kmi/testing.py index 91007f9..7b6dc36 100644 --- a/src/keas/kmi/testing.py +++ b/src/keas/kmi/testing.py @@ -12,20 +12,18 @@ # ############################################################################## """ -$Id$ """ +import StringIO +import webob +from zope.publisher import browser +from zope.interface import implements +from keas.kmi import facility, rest, interfaces -import cStringIO try: from hashlib import md5 except ImportError: from md5 import md5 -from zope.publisher import browser -from zope.interface import implements - -from keas.kmi import facility, rest, interfaces - KeyEncyptingKey = '''-----BEGIN RSA PRIVATE KEY----- MIIBOAIBAAJBAL+VS9lDsS9XOaeJppfK9lhxKMRFdcg50MR3aJEQK9rvDEqNwBS9 rQlU/x/NWxG0vvFCnrDn7VvQN+syb3+a0DMCAgChAkAzKw3lwPxw0VVccq1J7qeO @@ -42,44 +40,79 @@ '\x10}[\xfd\x19\x98\xb1\xfa*V~U\xdf\t\x02\x01\xa6\xa8\xae\x8b\x8cm\xd9n' '\xd5\x83\xa1%k\x16lfuY\\q\x8c\x8b') -class FakeRESTClient(object): +class FakeHTTPMessage(object): + + def __init__(self, res): + self.res = res + self.headers = ['Server: Fake/1.0'] + +class FakeHTTPResponse(object): + + # These attributes should be overridden by the test setup. + status = 200 + reason = 'Ok' + + def __init__(self, data): + self.fp = StringIO.StringIO(data) + self.msg = FakeHTTPMessage(self) + + def read(self, amt=10*2**10): + data = self.fp.read(amt) + if self.fp.len == self.fp.tell(): + self.fp = None + return data + + def getheader(self, name): + if name.lower() == 'content-length': + return str(len(self.fp.getvalue())) + + def close(self): + pass + + +class FakeHTTPConnection(object): - context = None + def __init__(self, host, port=None, timeout=10): + self.host = host + self.port = port + self.request_data = None - def __init__(self, url): - self.url = url + def request(self, method, url, body=None, headers=None): + self.request_data = (method, url, body, headers) - def post(self, url, data=None, headers={}): - io = cStringIO.StringIO(data) if data else None - request = browser.TestRequest(io) - request.method = 'POST' + def getresponse(self, buffering=False): + url = self.request_data[1] if url == '/new': - klass = rest.NewView + view = rest.create_key elif url == '/key': - if headers.get('content-type') != 'text/plain': + if self.request_data[3].get('content-type') != 'text/plain': # ensure we don't trip on # http://trac.pythonpaste.org/pythonpaste/ticket/294 raise ValueError('bad content type') - klass = rest.KeyView + view = rest.get_key else: raise ValueError(url) - view = klass(self.context, request) - self.contents = view() + io = StringIO.StringIO(self.request_data[2]) + req = webob.Request({'wsgi.input': io}) + res = view(self.context, req) + return FakeHTTPResponse(res.body) + def close(self): + pass def setupRestApi(localFacility, masterFacility): - MyFakeRESTClient = type( - 'FakeRESTClient', (FakeRESTClient,), {'context': masterFacility}) - localFacility.clientClass = MyFakeRESTClient + localFacility.httpConnFactory = type( + 'MyFakeHTTPConnection', (FakeHTTPConnection,), + {'context': masterFacility}) class TestingKeyManagementFacility(facility.KeyManagementFacility): - def __init__(self): - super(TestingKeyManagementFacility, self).__init__() + def __init__(self, storage_dir): + super(TestingKeyManagementFacility, self).__init__(storage_dir) md5Key = md5(KeyEncyptingKey).hexdigest() - self[md5Key] = facility.Key(EncryptedEncryptionKey) + self[md5Key] = EncryptedEncryptionKey def generate(self): return KeyEncyptingKey diff --git a/src/keas/kmi/testing.zcml b/src/keas/kmi/testing.zcml deleted file mode 100644 index 2238301..0000000 --- a/src/keas/kmi/testing.zcml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - diff --git a/src/keas/kmi/tests.py b/src/keas/kmi/tests.py index 1682cfb..4b051c5 100644 --- a/src/keas/kmi/tests.py +++ b/src/keas/kmi/tests.py @@ -14,24 +14,22 @@ """ $Id$ """ -import unittest import doctest - +import tempfile import transaction +import unittest + from zope.app.testing import setup from zope.component import provideUtility -from zope.interface.verify import verifyObject from keas.kmi.testing import TestingKeyManagementFacility -from keas.kmi.keyholder import KeyHolder from keas.kmi.interfaces import IKeyManagementFacility -from keas.kmi.interfaces import IKeyHolder def setUpPersistent(test): setup.setUpTestAsModule(test, name='keas.kmi.tests.doctestfile') setup.placelessSetUp() - provideUtility(TestingKeyManagementFacility(), + provideUtility(TestingKeyManagementFacility(tempfile.mkdtemp()), provides=IKeyManagementFacility) @@ -41,25 +39,16 @@ def tearDownPersistent(test): setup.tearDownTestAsModule(test) -def doctest_KeyHolder(): - """Smoke test for the KeyHolder class. - - >>> holder = KeyHolder(__file__) - >>> verifyObject(IKeyHolder, holder) - True - - """ - - def test_suite(): return unittest.TestSuite([ doctest.DocFileSuite( 'README.txt', - setUp=setup.placelessSetUp, tearDown=setup.placelessTearDown, + optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS), + doctest.DocFileSuite( + 'facility.txt', optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS), doctest.DocFileSuite( 'persistent.txt', setUp=setUpPersistent, tearDown=tearDownPersistent, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS), - doctest.DocTestSuite(), ]) diff --git a/src/keas/kmi/wsgi.py b/src/keas/kmi/wsgi.py index 12df7f3..c05196f 100644 --- a/src/keas/kmi/wsgi.py +++ b/src/keas/kmi/wsgi.py @@ -1,14 +1,34 @@ +############################################################################## +# +# Copyright (c) 2008 Zope Foundation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## """ WSGI application for the Key Management Server. """ - +import keas.kmi import os +from repoze.bfg.router import make_app +from keas.kmi import facility -from zope.app.wsgi import getWSGIApplication +FACILITY = None +def get_facility(environ): + return FACILITY -def application_factory(global_conf, conf='kms.conf', **local_conf): - configfile = os.path.join(global_conf['here'], conf) - application = getWSGIApplication(configfile) - return application +def application_factory(global_config, **kw): + storage_dir = os.path.abspath(kw['storage-dir']) + if not os.path.exists(storage_dir): + os.mkdir(storage_dir) + global FACILITY + FACILITY = facility.KeyManagementFacility(storage_dir) + return make_app(get_facility, keas.kmi, options=kw)