From fc59898385628f95553026fc42c94fc422e2c2d2 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Fri, 21 Apr 2017 16:56:59 +0200 Subject: [PATCH 01/11] changes to be compatible Python 2.7 and 3.6 --- src/keas/kmi/README.txt | 5 +---- src/keas/kmi/facility.py | 33 ++++++++++++++++++--------------- src/keas/kmi/keyholder.py | 5 ++--- src/keas/kmi/persistent.py | 11 +++++------ src/keas/kmi/testing.py | 19 ++++++++----------- 5 files changed, 34 insertions(+), 39 deletions(-) diff --git a/src/keas/kmi/README.txt b/src/keas/kmi/README.txt index 2173311..3c5db18 100644 --- a/src/keas/kmi/README.txt +++ b/src/keas/kmi/README.txt @@ -65,10 +65,7 @@ HIPAA and NIST key strength requirement. You can now use this key encrypting key to extract the encryption keys: - >>> try: - ... from hashlib import md5 - ... except ImportError: - ... from md5 import md5 + >>> from hashlib import md5 >>> hash_key = md5(key).hexdigest() >>> len(keys.get(hash_key)) diff --git a/src/keas/kmi/facility.py b/src/keas/kmi/facility.py index a1afe5e..e1ec895 100644 --- a/src/keas/kmi/facility.py +++ b/src/keas/kmi/facility.py @@ -20,27 +20,30 @@ import Crypto.PublicKey.RSA from Crypto.Random import random import binascii -import httplib +try: + # Python 3 + from http.client import HTTPSConnection + from urllib import parse as urlparse +except ImportError: + # Python 2 + from httplib import HTTPSConnection + from urlparse import urlparse import logging import os import struct import time -import urlparse -import zope.interface +from zope.interface import implementer from keas.kmi import interfaces -try: - from hashlib import md5 -except ImportError: - from md5 import md5 +from hashlib import md5 __docformat__ = "reStructuredText" logger = logging.getLogger('kmi') +@implementer(interfaces.IEncryptionService) class EncryptionService(object): - zope.interface.implements(interfaces.IEncryptionService) CipherFactory = Crypto.Cipher.AES CipherMode = Crypto.Cipher.AES.MODE_CBC @@ -185,8 +188,8 @@ def decrypt_file(self, key, fsrc, fdst, chunksize=24*1024): fdst.truncate(origsize) +@implementer(interfaces.IExtendedKeyManagementFacility) class KeyManagementFacility(EncryptionService): - zope.interface.implements(interfaces.IExtendedKeyManagementFacility) timeout = 3600 @@ -194,7 +197,7 @@ class KeyManagementFacility(EncryptionService): rsaKeyExponent = 65537 # Should be sufficiently high and non-symmetric rsaPassphrase = 'key management facility' - keyLength = rsaKeyLength/16 + keyLength = rsaKeyLength // 16 def __init__(self, storage_dir): self.storage_dir = storage_dir @@ -241,7 +244,7 @@ def __contains__(self, name): def __setitem__(self, name, key): fn = os.path.join(self.storage_dir, name+'.dek') - with open(fn, 'w') as file: + with open(fn, 'wb') as file: file.write(key) logger.info('New key added (hash): %s', name) @@ -300,12 +303,12 @@ def __repr__(self): return '<%s (%i)>' %(self.__class__.__name__, len(self)) +@implementer(interfaces.IKeyManagementFacility) class LocalKeyManagementFacility(EncryptionService): """A local facility that requests keys from the master facility.""" - zope.interface.implements(interfaces.IKeyManagementFacility) timeout = 3600 - httpConnFactory = httplib.HTTPSConnection + httpConnFactory = HTTPSConnection def __init__(self, url): self.url = url @@ -313,7 +316,7 @@ def __init__(self, url): def generate(self): """See interfaces.IKeyGenerationService""" - pieces = urlparse.urlparse(self.url) + pieces = urlparse(self.url) conn = self.httpConnFactory(pieces.netloc) conn.request('POST', '/new', '', {}) response = conn.getresponse() @@ -326,7 +329,7 @@ def getEncryptionKey(self, key): if (key in self.__cache and self.__cache[key][0] + self.timeout > time.time()): return self.__cache[key][1] - pieces = urlparse.urlparse(self.url) + pieces = urlparse(self.url) conn = self.httpConnFactory(pieces.netloc) conn.request('POST', '/key', key, {'content-type': 'text/plain'}) response = conn.getresponse() diff --git a/src/keas/kmi/keyholder.py b/src/keas/kmi/keyholder.py index 7c9a280..f3bbc6b 100644 --- a/src/keas/kmi/keyholder.py +++ b/src/keas/kmi/keyholder.py @@ -14,15 +14,14 @@ """Simple Key Holder """ __docformat__ = "reStructuredText" -from zope.interface import implements +from zope.interface import implementer from keas.kmi.interfaces import IKeyHolder +@implementer(IKeyHolder) class KeyHolder(object): """A key holder utility that loads the key from a file and keeps it in RAM.""" - implements(IKeyHolder) - def __init__(self, filename): self.key = file(filename, 'rb').read() diff --git a/src/keas/kmi/persistent.py b/src/keas/kmi/persistent.py index 15bb498..0818d48 100644 --- a/src/keas/kmi/persistent.py +++ b/src/keas/kmi/persistent.py @@ -14,8 +14,7 @@ """Encrypted persistent objects """ from __future__ import absolute_import -import cPickle -import cStringIO +from ZODB._compat import BytesIO, Pickler, Unpickler import persistent import persistent.wref from zope.component import getUtility @@ -52,7 +51,7 @@ def decrypt_state(state): def pickle_nonpersistent(state): - buf = cStringIO.StringIO() + buf = BytesIO() persistent_refs = [] cache = {} def persistent_id(obj): @@ -68,17 +67,17 @@ def persistent_id(obj): idx = cache[id(obj)] = len(persistent_refs) persistent_refs.append(obj) return idx - pickler = cPickle.Pickler(buf, 2) + pickler = Pickler(buf, 2) pickler.persistent_id = persistent_id pickler.dump(state) return buf.getvalue(), persistent_refs def unpickle_nonpersistent(data, persistent_refs): - buf = cStringIO.StringIO(data) + buf = BytesIO(data) def persistent_load(ref): return persistent_refs[ref] - unpickler = cPickle.Unpickler(buf) + unpickler = Unpickler(buf) unpickler.persistent_load = persistent_load return unpickler.load() diff --git a/src/keas/kmi/testing.py b/src/keas/kmi/testing.py index 22d15d8..5192cc4 100644 --- a/src/keas/kmi/testing.py +++ b/src/keas/kmi/testing.py @@ -13,16 +13,12 @@ ############################################################################## """Testing Support """ -import StringIO +from io import BytesIO import webob -from zope.publisher import browser -from zope.interface import implements +from zope.interface import implementer from keas.kmi import facility, rest, interfaces -try: - from hashlib import md5 -except ImportError: - from md5 import md5 +from hashlib import md5 KeyEncyptingKey = '''-----BEGIN RSA PRIVATE KEY----- MIIBOAIBAAJBAL+VS9lDsS9XOaeJppfK9lhxKMRFdcg50MR3aJEQK9rvDEqNwBS9 @@ -53,12 +49,13 @@ class FakeHTTPResponse(object): reason = 'Ok' def __init__(self, data): - self.fp = StringIO.StringIO(data) + self.fp = BytesIO(data) + self.fp_len = len(data) self.msg = FakeHTTPMessage(self) def read(self, amt=10*2**10): data = self.fp.read(amt) - if self.fp.len == self.fp.tell(): + if self.fp_len == self.fp.tell(): self.fp = None return data @@ -83,7 +80,7 @@ def getresponse(self, buffering=False): elif url == '/key': view = rest.get_key - io = StringIO.StringIO(self.request_data[2]) + io = BytesIO(self.request_data[2]) req = webob.Request({'wsgi.input': io}) res = view(self.context, req) return FakeHTTPResponse(res.body) @@ -106,7 +103,7 @@ def generate(self): return KeyEncyptingKey +@implementer(interfaces.IKeyHolder) class TestingKeyHolder(object): - implements(interfaces.IKeyHolder) key = KeyEncyptingKey From 512f5ba07d4798a7d05b74fa3570ee1fdb86f4cd Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Fri, 21 Apr 2017 17:01:37 +0200 Subject: [PATCH 02/11] add special python3.cfg config to use waitress instead of PasteScript (FYI, waitress doesn't support ssl_pem option) --- python3.cfg | 19 +++++++++++++++++++ server-waitress.ini | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 python3.cfg create mode 100644 server-waitress.ini diff --git a/python3.cfg b/python3.cfg new file mode 100644 index 0000000..c679d64 --- /dev/null +++ b/python3.cfg @@ -0,0 +1,19 @@ +[buildout] +extends = + buildout.cfg + +[paster] +recipe = zc.recipe.egg +dependent-scripts = true +eggs = keas.kmi + waitress + pyOpenSSL + +[runserver] +recipe = zc.recipe.egg +dependent-scripts = true +eggs = ${paster:eggs} +scripts = pserve=runserver +initialization = + import sys + sys.argv[1:] = ['server-waitress.ini'] diff --git a/server-waitress.ini b/server-waitress.ini new file mode 100644 index 0000000..4c815e8 --- /dev/null +++ b/server-waitress.ini @@ -0,0 +1,41 @@ +[app:main] +use = egg:keas.kmi +storage-dir=keys/ + +[server:main] +use = egg:waitress#main +listen = *:8080 + +# 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 From edba88c75263c33c11e2c35cd33d7fc982f1757c Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Mon, 8 May 2017 13:54:17 +0200 Subject: [PATCH 03/11] fix str vs bytes on Python 3, tests pass --- src/keas/kmi/README.txt | 43 +++++++++++++++++++------------------ src/keas/kmi/facility.py | 17 +++++++++------ src/keas/kmi/keyholder.py | 3 ++- src/keas/kmi/persistent.txt | 10 ++++----- src/keas/kmi/rest.py | 5 ++++- src/keas/kmi/testing.py | 8 +++---- 6 files changed, 48 insertions(+), 38 deletions(-) diff --git a/src/keas/kmi/README.txt b/src/keas/kmi/README.txt index 3c5db18..915274d 100644 --- a/src/keas/kmi/README.txt +++ b/src/keas/kmi/README.txt @@ -7,6 +7,7 @@ infrastructure. Part of this infrastructure is a key management facility that provides several services related to keys. All keys are stored in a specified storage directory. + >>> from __future__ import print_function >>> import tempfile >>> storage_dir = tempfile.mkdtemp() @@ -54,7 +55,7 @@ key: Let's now use the key generation service's API to generate a key. >>> key = keys.generate() - >>> print key + >>> print(key.decode()) -----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY----- @@ -79,22 +80,22 @@ you to encrypt and decrypt a string given the key encrypting key. Let's now encrypt some data: - >>> encrypted = keys.encrypt(key, 'Stephan Richter') + >>> encrypted = keys.encrypt(key, b'Stephan Richter') >>> len(encrypted) 16 We can also decrypt the data. >>> keys.decrypt(key, encrypted) - 'Stephan Richter' + b'Stephan Richter' We can also encrypt data given by a file descriptor >>> import tempfile >>> tmp_file = tempfile.TemporaryFile() - >>> data="encryptioniscool"*24*1024 - >>> tmp_file.write(data) - >>> tmp_file.seek(0) + >>> data=b"encryptioniscool"*24*1024 + >>> pos = tmp_file.write(data) + >>> pos = tmp_file.seek(0) >>> encrypted_file = tempfile.TemporaryFile() >>> keys.encrypt_file(key, tmp_file, encrypted_file) >>> tmp_file.close() @@ -102,11 +103,11 @@ We can also encrypt data given by a file descriptor And decrypt the file >>> decrypted_file = tempfile.TemporaryFile() - >>> encrypted_file.seek(0) + >>> pos =encrypted_file.seek(0) >>> keys.decrypt_file(key, encrypted_file, decrypted_file) >>> encrypted_file.close() - >>> decrypted_file.seek(0) + >>> pos = decrypted_file.seek(0) >>> decrypted_data = decrypted_file.read() >>> decrypted_file.close() >>> decrypted_data == data @@ -136,7 +137,7 @@ date/time the key has been fetched and the unencrypted DEK. >>> firstTime = keys._KeyManagementFacility__dek_cache[hash_key][0] >>> keys.decrypt(key, encrypted) - 'Stephan Richter' + b'Stephan Richter' >>> secondTime = keys._KeyManagementFacility__dek_cache[hash_key][0] @@ -183,12 +184,12 @@ As with the master facility, the local facility provides the So en- and decryption is very easy to do: - >>> encrypted = localKeys.encrypt(key, 'Stephan Richter') + >>> encrypted = localKeys.encrypt(key, b'Stephan Richter') >>> len(encrypted) 16 >>> localKeys.decrypt(key, encrypted) - 'Stephan Richter' + b'Stephan Richter' Instead of forwarding the en- an decryption request to the master facility, the local facility merely fetches the encryption key pair and executes the @@ -223,7 +224,7 @@ decryption (private) key. >>> firstTime = localKeys._LocalKeyManagementFacility__cache[key][0] >>> localKeys.decrypt(key, encrypted) - 'Stephan Richter' + b'Stephan Richter' >>> secondTime = localKeys._LocalKeyManagementFacility__cache[key][0] @@ -238,7 +239,7 @@ The local facility also provides the ``IKeyGenerationService`` interface: The local method call is identical to the master one: >>> key2 = localKeys.generate() - >>> print key2 + >>> print(key2.decode()) -----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY----- @@ -271,7 +272,7 @@ So let's have a look at the call: >>> request = Request({}) >>> key3 = rest.create_key(keys, request).body - >>> print key3 + >>> print(key3.decode()) -----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY----- @@ -299,13 +300,13 @@ encryption key string: If you try to request a nonexistent key, you get a 404 error: encryption key string: - >>> request.body = 'xxyz' - >>> print rest.get_key(keys, request) + >>> request.body = b'xxyz' + >>> print(rest.get_key(keys, request)) Key not found A `GET` request to the root shows us a server status page - >>> print rest.get_status(keys, Request({})) + >>> print(rest.get_status(keys, Request({}))) 200 OK Content-Type: text/plain Content-Length: 25 @@ -331,7 +332,7 @@ Of course, the key generation service is supported: However, you will always receive the same key: >>> def getKeySegment(key): - ... return key.split('\n')[1] + ... return key.decode().split('\n')[1] >>> getKeySegment(testingKeys.generate()) 'MIIBOAIBAAJBAL+VS9lDsS9XOaeJppfK9lhxKMRFdcg50MR3aJEQK9rvDEqNwBS9' @@ -347,13 +348,13 @@ All other methods remain the same: >>> key = testingKeys.generate() >>> testingKeys.getEncryptionKey(key) - '_\xc4\x04\xbe5B\x7f\xaf\xd6\x92\xbd\xa0\xcf\x156\x1d\x88=p9{\xaa...' + b'_\xc4\x04\xbe5B\x7f\xaf\xd6\x92\xbd\xa0\xcf\x156\x1d\x88=p9{\xaa...' We can also safely en- and decrypt: - >>> encrypted = testingKeys.encrypt(key, 'Stephan Richter') + >>> encrypted = testingKeys.encrypt(key, b'Stephan Richter') >>> testingKeys.decrypt(key, encrypted) - 'Stephan Richter' + b'Stephan Richter' Key Holder diff --git a/src/keas/kmi/facility.py b/src/keas/kmi/facility.py index e1ec895..7ffb9dc 100644 --- a/src/keas/kmi/facility.py +++ b/src/keas/kmi/facility.py @@ -23,7 +23,7 @@ try: # Python 3 from http.client import HTTPSConnection - from urllib import parse as urlparse + from urllib.parse import urlparse except ImportError: # Python 2 from httplib import HTTPSConnection @@ -60,12 +60,15 @@ def _pkcs7Encode(self, text, k=16): return text + binascii.unhexlify(n * ("%02x" % n)) def _pkcs7Decode(self, text, k=16): - n = int(binascii.hexlify(text[-1]), 16) + # In Python 3, text[-1] returns an int, not bytes, we need text[-1:] to + # have bytes. In Python 2, it doesn't matter, both return str. + # Actually it seems we could just do `n = text[-1]` in Python 3. + n = int(binascii.hexlify(text[-1:]), 16) if n > k: raise ValueError("Input is not padded or padding is corrupt") return text[:-n] - _bytesToKeySalt = '12345678' + _bytesToKeySalt = b'12345678' def _bytesToKey(self, data): # Simplified version of M2Crypto.m2.bytes_to_key(). assert len(self._bytesToKeySalt) == 8, len(self._bytesToKeySalt) @@ -103,7 +106,9 @@ def encrypt_file(self, key, fsrc, fdst, chunksize=24*1024): encryptionKey = self._bytesToKey(self.getEncryptionKey(key)) # 2. Create a random initialization vector - iv = ''.join(chr(random.randint(0, 0xFF)) for i in range(16)) + # bytes(bytearray(generator)) is needed for Python 2, + # with Python 3 bytes(generator) works + iv = bytes(bytearray((random.randint(0, 0xFF)) for i in range(16))) # 3. Create a cipher object cipher = self.CipherFactory.new( @@ -132,7 +137,7 @@ def encrypt_file(self, key, fsrc, fdst, chunksize=24*1024): # Apply padding. if len(chunk) % 16 != 0: - chunk += ' ' * (16 - len(chunk) % 16) + chunk += b' ' * (16 - len(chunk) % 16) # Write the chunk fdst.write(cipher.encrypt(chunk)) @@ -318,7 +323,7 @@ def generate(self): """See interfaces.IKeyGenerationService""" pieces = urlparse(self.url) conn = self.httpConnFactory(pieces.netloc) - conn.request('POST', '/new', '', {}) + conn.request('POST', '/new', b'', {}) response = conn.getresponse() data = response.read() response.close() diff --git a/src/keas/kmi/keyholder.py b/src/keas/kmi/keyholder.py index f3bbc6b..ca1f475 100644 --- a/src/keas/kmi/keyholder.py +++ b/src/keas/kmi/keyholder.py @@ -23,5 +23,6 @@ class KeyHolder(object): """A key holder utility that loads the key from a file and keeps it in RAM.""" def __init__(self, filename): - self.key = file(filename, 'rb').read() + with open(filename, 'rb') as f: + self.key = f.read() diff --git a/src/keas/kmi/persistent.txt b/src/keas/kmi/persistent.txt index 6ff1254..cabb877 100644 --- a/src/keas/kmi/persistent.txt +++ b/src/keas/kmi/persistent.txt @@ -27,11 +27,11 @@ that you're supposed to provide in your application. None of the raw data appears in the pickle - >>> import cPickle as pickle + >>> from zodbpickle import pickle >>> pickled_data = pickle.dumps(userdata) - >>> 'Stephan' in pickled_data + >>> b'Stephan' in pickled_data False - >>> '123456789' in pickled_data + >>> b'123456789' in pickled_data False We can successfully load it @@ -48,9 +48,9 @@ from `EncryptedPersistent` will be encrypted. >>> users['mgedmin'] = UserPrivateData('Marius Gedminas', '987654321') >>> pickled_data = pickle.dumps(users) - >>> 'stephan' in pickled_data + >>> b'stephan' in pickled_data True - >>> '123456789' in pickled_data + >>> b'123456789' in pickled_data False diff --git a/src/keas/kmi/rest.py b/src/keas/kmi/rest.py index 6175f43..3b4e80f 100644 --- a/src/keas/kmi/rest.py +++ b/src/keas/kmi/rest.py @@ -17,12 +17,14 @@ def get_status(context, request): return Response( - 'KMS server holding %d keys' %len(context), + 'KMS server holding %d keys' % len(context), + charset='utf-8', headerlist=[('Content-Type', 'text/plain')]) def create_key(context, request): return Response( context.generate(), + charset='utf-8', headerlist=[('Content-Type', 'text/plain')]) def get_key(context, request): @@ -30,6 +32,7 @@ def get_key(context, request): try: return Response( context.getEncryptionKey(key), + charset='utf-8', headerlist=[('Content-Type', 'text/plain')]) except KeyError: return exc.HTTPNotFound('Key not found') diff --git a/src/keas/kmi/testing.py b/src/keas/kmi/testing.py index 5192cc4..a9b2a13 100644 --- a/src/keas/kmi/testing.py +++ b/src/keas/kmi/testing.py @@ -20,7 +20,7 @@ from hashlib import md5 -KeyEncyptingKey = '''-----BEGIN RSA PRIVATE KEY----- +KeyEncyptingKey = b'''-----BEGIN RSA PRIVATE KEY----- MIIBOAIBAAJBAL+VS9lDsS9XOaeJppfK9lhxKMRFdcg50MR3aJEQK9rvDEqNwBS9 rQlU/x/NWxG0vvFCnrDn7VvQN+syb3+a0DMCAgChAkAzKw3lwPxw0VVccq1J7qeO 4DXR1iEMIoWruiCyq0aLkHnQzrZpaHnd4w+JNKIGOVDEWItf3iZNMXkoqj2hoPmp @@ -32,9 +32,9 @@ ''' EncryptedEncryptionKey = ( - '\xbc\x08\xdbo\x04\xe3\xc7G\x13\xd3\x86\x92\xfa\xe8i>,+\xda\xf8/B2]s\xd4' - '\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') + b'\xbc\x08\xdbo\x04\xe3\xc7G\x13\xd3\x86\x92\xfa\xe8i>,+\xda\xf8/B2]s\xd4' + b'\x10}[\xfd\x19\x98\xb1\xfa*V~U\xdf\t\x02\x01\xa6\xa8\xae\x8b\x8cm\xd9n' + b'\xd5\x83\xa1%k\x16lfuY\\q\x8c\x8b') class FakeHTTPMessage(object): From 6c2ece3a84009ee607d48eaec032b63c69fc0260 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Mon, 8 May 2017 14:17:22 +0200 Subject: [PATCH 04/11] make doctests pass on both Python 2 and Python 3 --- src/keas/kmi/README.txt | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/keas/kmi/README.txt b/src/keas/kmi/README.txt index 915274d..b3de365 100644 --- a/src/keas/kmi/README.txt +++ b/src/keas/kmi/README.txt @@ -86,8 +86,8 @@ Let's now encrypt some data: We can also decrypt the data. - >>> keys.decrypt(key, encrypted) - b'Stephan Richter' + >>> keys.decrypt(key, encrypted) == b'Stephan Richter' + True We can also encrypt data given by a file descriptor @@ -136,8 +136,8 @@ date/time the key has been fetched and the unencrypted DEK. >>> firstTime = keys._KeyManagementFacility__dek_cache[hash_key][0] - >>> keys.decrypt(key, encrypted) - b'Stephan Richter' + >>> keys.decrypt(key, encrypted) == b'Stephan Richter' + True >>> secondTime = keys._KeyManagementFacility__dek_cache[hash_key][0] @@ -188,8 +188,8 @@ So en- and decryption is very easy to do: >>> len(encrypted) 16 - >>> localKeys.decrypt(key, encrypted) - b'Stephan Richter' + >>> localKeys.decrypt(key, encrypted) == b'Stephan Richter' + True Instead of forwarding the en- an decryption request to the master facility, the local facility merely fetches the encryption key pair and executes the @@ -223,8 +223,8 @@ decryption (private) key. >>> firstTime = localKeys._LocalKeyManagementFacility__cache[key][0] - >>> localKeys.decrypt(key, encrypted) - b'Stephan Richter' + >>> localKeys.decrypt(key, encrypted) == b'Stephan Richter' + True >>> secondTime = localKeys._LocalKeyManagementFacility__cache[key][0] @@ -332,7 +332,7 @@ Of course, the key generation service is supported: However, you will always receive the same key: >>> def getKeySegment(key): - ... return key.decode().split('\n')[1] + ... return str(key.decode().split('\n')[1]) >>> getKeySegment(testingKeys.generate()) 'MIIBOAIBAAJBAL+VS9lDsS9XOaeJppfK9lhxKMRFdcg50MR3aJEQK9rvDEqNwBS9' @@ -347,14 +347,14 @@ However, you will always receive the same key: All other methods remain the same: >>> key = testingKeys.generate() - >>> testingKeys.getEncryptionKey(key) - b'_\xc4\x04\xbe5B\x7f\xaf\xd6\x92\xbd\xa0\xcf\x156\x1d\x88=p9{\xaa...' + >>> testingKeys.getEncryptionKey(key) == b'_\xc4\x04\xbe5B\x7f\xaf\xd6\x92\xbd\xa0\xcf\x156\x1d\x88=p9{\xaal\xb4\x84M\x1d\xfd\xb2z\xae\x1a' + True We can also safely en- and decrypt: >>> encrypted = testingKeys.encrypt(key, b'Stephan Richter') - >>> testingKeys.decrypt(key, encrypted) - b'Stephan Richter' + >>> testingKeys.decrypt(key, encrypted) == b'Stephan Richter' + True Key Holder From e726867657ba5dde1de0f79f36c3a9fa3db1e2fd Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Mon, 8 May 2017 15:12:17 +0200 Subject: [PATCH 05/11] use gunicorn with the certificate with both Python 2 and Python 3 --- .gitignore | 4 +--- .travis.yml | 2 ++ README.txt | 10 ++++----- buildout.cfg | 8 +++---- generate-sample-cert.sh | 6 ++--- python3.cfg | 19 ---------------- sample.crt | 16 ++++++++++++++ sample.key | 15 +++++++++++++ server-waitress.ini | 41 ---------------------------------- server.ini | 6 +++-- src/keas/kmi/testclient.py | 45 ++++++++++++++++++++++---------------- 11 files changed, 75 insertions(+), 97 deletions(-) delete mode 100644 python3.cfg create mode 100644 sample.crt create mode 100644 sample.key delete mode 100644 server-waitress.ini diff --git a/.gitignore b/.gitignore index 6541845..64b4a3b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,4 @@ local/ # Test data datakey.dat kek.dat -sample.cert -sample.key -source-cache.log \ No newline at end of file +source-cache.log diff --git a/.travis.yml b/.travis.yml index dce0257..1589b6f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: python python: - 2.7 + - 3.4 + - 3.6 install: - python bootstrap.py - bin/buildout diff --git a/README.txt b/README.txt index 9fee358..c01219d 100644 --- a/README.txt +++ b/README.txt @@ -3,9 +3,9 @@ This package provides a NIST SP 800-57 compliant Key Management Infrastructure To get started do:: - $ python bootstrap.py # Must be Python 2.5 or higher - $ ./bin/buildout # Depends on successfull compilation of M2Crypto - $ ./bin/runserver # or ./bin/paster serve server.ini + $ python bootstrap.py # Must be Python 2.7 or higher + $ ./bin/buildout # Depends on successful compilation of M2Crypto + $ ./bin/runserver # or ./bin/gunicorn --paste server.ini The server will come up on port 8080. You can create a new key encrypting key using:: @@ -15,7 +15,7 @@ using:: or, if you want a more convenient tool:: - $ ./bin/testclient https://localhost:8080/new -n > kek.dat + $ ./bin/testclient https://localhost:8080 -n > kek.dat The data encryption key can now be retrieved by posting the KEK to another URL:: @@ -25,7 +25,7 @@ URL:: or :: - $ ./bin/testclient https://localhost:8080/new -g kek.dat > datakey.dat + $ ./bin/testclient https://localhost:8080 -g kek.dat > datakey.dat Note: To be compliant, the server must use an encrypted communication channel of course. The ``--ca-certificate`` tells wget to trust the sample self-signed diff --git a/buildout.cfg b/buildout.cfg index 67bf3d0..ab762ab 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -32,17 +32,17 @@ eggs = keas.kmi [paster] recipe = zc.recipe.egg eggs = keas.kmi - PasteScript + gunicorn pyOpenSSL -scripts = paster +scripts = gunicorn [runserver] recipe = zc.recipe.egg eggs = ${paster:eggs} -scripts = paster=runserver +scripts = gunicorn=runserver initialization = import sys - sys.argv[1:] = ['serve', 'server.ini'] + sys.argv[1:] = ['--paste', 'server.ini'] [testclient] recipe = zc.recipe.egg diff --git a/generate-sample-cert.sh b/generate-sample-cert.sh index ccff186..d515097 100755 --- a/generate-sample-cert.sh +++ b/generate-sample-cert.sh @@ -1,6 +1,4 @@ #!/bin/sh openssl genrsa 1024 > sample.key -openssl req -new -x509 -nodes -sha1 -days 3650 -key sample.key > sample.cert -cat sample.cert sample.key > sample.pem -rm sample.key sample.cert - +openssl req -new -x509 -nodes -sha1 -days 3650 -key sample.key > sample.crt +cat sample.crt sample.key > sample.pem diff --git a/python3.cfg b/python3.cfg deleted file mode 100644 index c679d64..0000000 --- a/python3.cfg +++ /dev/null @@ -1,19 +0,0 @@ -[buildout] -extends = - buildout.cfg - -[paster] -recipe = zc.recipe.egg -dependent-scripts = true -eggs = keas.kmi - waitress - pyOpenSSL - -[runserver] -recipe = zc.recipe.egg -dependent-scripts = true -eggs = ${paster:eggs} -scripts = pserve=runserver -initialization = - import sys - sys.argv[1:] = ['server-waitress.ini'] diff --git a/sample.crt b/sample.crt new file mode 100644 index 0000000..587934d --- /dev/null +++ b/sample.crt @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICcTCCAdqgAwIBAgIJAMvA30EY1rKtMA0GCSqGSIb3DQEBBQUAMDAxCzAJBgNV +BAYTAlVTMQ0wCwYDVQQKEwRLZWFzMRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcNMDgw +OTA0MTU1MTU5WhcNMTgwOTAyMTU1MTU5WjAwMQswCQYDVQQGEwJVUzENMAsGA1UE +ChMES2VhczESMBAGA1UEAxMJbG9jYWxob3N0MIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQDK17rB/KVaK8MVjiEkvA4ZncOOIC3nStZ/erXM+qwkghPM4Tfr2FTU +iTgwwdLdu/ht74oWnppttfaTQ+sVz2rFXnPgfqKTGoJTwWFiuNuZhSRDVssGVnL/ +RatZW6wns8UNf+W4hUe6/vGQP6obNTe2T4R+t2hXP51OkOy4BMcq0QIDAQABo4GS +MIGPMB0GA1UdDgQWBBQDIsX7HoSqbxKrCawi64MkXRmtmzBgBgNVHSMEWTBXgBQD +IsX7HoSqbxKrCawi64MkXRmtm6E0pDIwMDELMAkGA1UEBhMCVVMxDTALBgNVBAoT +BEtlYXMxEjAQBgNVBAMTCWxvY2FsaG9zdIIJAMvA30EY1rKtMAwGA1UdEwQFMAMB +Af8wDQYJKoZIhvcNAQEFBQADgYEAW5UBM7EIMpARzQwpQ8N1gyTR/VqJ9fSm4MIw +Y5m90HRgsDcXVbhn0rRfcC8o4EtGDvCjqsFYXy/ImF9tjEiuaysxbqepl+XMszPE +1kO50quWsV1FLSdcJX6t/ofJYOxiQkqPvg9t/ovTnEZ+w4NfPo+0MJgudjJoD2+w +5UTsKtU= +-----END CERTIFICATE----- diff --git a/sample.key b/sample.key new file mode 100644 index 0000000..7f7deb0 --- /dev/null +++ b/sample.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDK17rB/KVaK8MVjiEkvA4ZncOOIC3nStZ/erXM+qwkghPM4Tfr +2FTUiTgwwdLdu/ht74oWnppttfaTQ+sVz2rFXnPgfqKTGoJTwWFiuNuZhSRDVssG +VnL/RatZW6wns8UNf+W4hUe6/vGQP6obNTe2T4R+t2hXP51OkOy4BMcq0QIDAQAB +AoGAHcDJDx1M784NfoLrj6TZ+J3wik9kDFIo5mgMdLWsPGqsFthOSJTh1I8QI+66 +THX++bkyKyE2i7MuKOnEeN2Ezo2jAThF7XoWhm6/+pSXhSqmL1jKr/1CZRaR9jv0 +cCVJc3mTuAGH+yFVeGpWNvDaCmOUlD5M48xTROJXteDQ0TECQQDuDM9pmQdqkGIp +dvbIviS8donYn0kJ0TKS14pMtb/C63lcld513rHS43ru3FRY9baR/q5vV9vW5RhH +S7w4cYvVAkEA2iNLsFEAkY88oZJYbdyybeKxZdReyes1/zPe4RYzRdbDHRNAa+zk +mZIZDI820E0Y+DeoT+q3nXkXiiOS/iRNDQJBAKdAvOH2sO1AcJetjArS/cCkkIlw +sMKDB0OAyRzIfekXxPc2HU03oD0Jsy/sAh9W1GWTST/VvRIpeHtvTNljfdkCQF5T +UuBcNoW6zXoEYU6oV1Oi6hjhW1eu6PuAv4jPY754XoiNEZdZqYQqo8BFkWtDW1/C +GXrtQRbMDPzD40UYB2UCQQCmJpJp+u2lHj7zuZikHIHQBNyXyoGnzgNs6XUj1Bs6 +Y4vjue8w6RkRLZ1YGP+xqsngVqb9IRygyLDpEgwEnOT4 +-----END RSA PRIVATE KEY----- diff --git a/server-waitress.ini b/server-waitress.ini deleted file mode 100644 index 4c815e8..0000000 --- a/server-waitress.ini +++ /dev/null @@ -1,41 +0,0 @@ -[app:main] -use = egg:keas.kmi -storage-dir=keys/ - -[server:main] -use = egg:waitress#main -listen = *:8080 - -# 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/server.ini b/server.ini index e59917a..770bb07 100644 --- a/server.ini +++ b/server.ini @@ -3,10 +3,12 @@ use = egg:keas.kmi storage-dir=keys/ [server:main] -use = egg:Paste#http +use = egg:gunicorn#main host = 0.0.0.0 port = 8080 -ssl_pem = sample.pem +worker_class = sync +keyfile = sample.key +certfile = sample.crt # Logging Configuration [loggers] diff --git a/src/keas/kmi/testclient.py b/src/keas/kmi/testclient.py index a64d4e3..11781bf 100644 --- a/src/keas/kmi/testclient.py +++ b/src/keas/kmi/testclient.py @@ -1,3 +1,4 @@ +from __future__ import print_function ############################################################################## # # Copyright (c) 2008 Zope Foundation and Contributors. @@ -15,33 +16,39 @@ """ __docformat__ = "reStructuredText" +import os import sys import optparse import textwrap -import urlparse +try: + # Python 3 + from urllib.parse import urlparse +except ImportError: + # Python 2 + from urlparse import urlparse from keas.kmi.facility import LocalKeyManagementFacility def ping(kmf): - pieces = urlparse.urlparse(kmf.url) + pieces = urlparse(kmf.url) conn = kmf.httpConnFactory(pieces.netloc) conn.request('GET', '/') response = conn.getresponse() - print response.status, response.reason - print - print response.read() + print(response.status, response.reason) + print() + print(response.read()) def new_key(kmf): - sys.stdout.write(kmf.generate()) + os.write(sys.stdout.fileno(), kmf.generate()) def read_kek(kekfile): try: - return file(kekfile, 'rb').read() - except IOError, e: - print >> sys.stderr, "Could not read key encrypting key from %s" % kekfile - print >> sys.stderr, e + return open(kekfile, 'rb').read() + except IOError as e: + print("Could not read key encrypting key from %s" % kekfile, file=sys.stderr) + print(e, file=sys.stderr) sys.exit(1) @@ -50,31 +57,31 @@ def read_data(filename=None): return sys.stdin.read() else: try: - return file(filename, 'rb').read() - except IOError, e: - print >> sys.stderr, "Could not read %s" % filename - print >> sys.stderr, e + return open(filename, 'rb').read() + except IOError as e: + print("Could not read %s" % filename, file=sys.stderr) + print(e, file=sys.stderr) sys.exit(1) def get_key(kmf, kekfile): key_encrypting_key = read_kek(kekfile) key = kmf.getEncryptionKey(key_encrypting_key) - sys.stdout.write(key) + os.write(sys.stdout.fileno(), key) def encrypt(kmf, kekfile, filename=None): key_encrypting_key = read_kek(kekfile) data = read_data(filename) encrypted = kmf.encrypt(key_encrypting_key, data) - sys.stdout.write(encrypted) + os.write(sys.stdout.fileno(), encrypted) def decrypt(kmf, kekfile, filename=None): key_encrypting_key = read_kek(kekfile) data = read_data(filename) decrypted = kmf.decrypt(key_encrypting_key, data) - sys.stdout.write(decrypted) + os.write(sys.stdout.fileno(), decrypted) parser = optparse.OptionParser(textwrap.dedent("""\ @@ -88,7 +95,7 @@ def decrypt(kmf, kekfile, filename=None): %prog URL -e key.txt data.txt > encrypted.txt encrypt data - %prog URL -d key.txt encrytped.txt > data.txt + %prog URL -d key.txt encrypted.txt > data.txt decrypt data %prog URL -g key.txt > secretkey.bin @@ -130,5 +137,5 @@ def main(argv=None): try: opts.action(kmf, *args) - except TypeError, err: + except TypeError as err: parser.error('incorrect number of arguments') From 07e92bcbc2f934eafe0e5243af53a2fd7061863b Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Mon, 15 May 2017 11:11:06 +0200 Subject: [PATCH 06/11] stylistic changes --- src/keas/kmi/README.txt | 4 ++-- src/keas/kmi/facility.py | 1 - src/keas/kmi/interfaces.py | 3 --- src/keas/kmi/keyholder.py | 1 - src/keas/kmi/testclient.py | 9 +++++---- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/keas/kmi/README.txt b/src/keas/kmi/README.txt index b3de365..384b0ae 100644 --- a/src/keas/kmi/README.txt +++ b/src/keas/kmi/README.txt @@ -93,7 +93,7 @@ We can also encrypt data given by a file descriptor >>> import tempfile >>> tmp_file = tempfile.TemporaryFile() - >>> data=b"encryptioniscool"*24*1024 + >>> data = b"encryptioniscool"*24*1024 >>> pos = tmp_file.write(data) >>> pos = tmp_file.seek(0) >>> encrypted_file = tempfile.TemporaryFile() @@ -103,7 +103,7 @@ We can also encrypt data given by a file descriptor And decrypt the file >>> decrypted_file = tempfile.TemporaryFile() - >>> pos =encrypted_file.seek(0) + >>> pos = encrypted_file.seek(0) >>> keys.decrypt_file(key, encrypted_file, decrypted_file) >>> encrypted_file.close() diff --git a/src/keas/kmi/facility.py b/src/keas/kmi/facility.py index 7ffb9dc..7d14ddc 100644 --- a/src/keas/kmi/facility.py +++ b/src/keas/kmi/facility.py @@ -37,7 +37,6 @@ from hashlib import md5 -__docformat__ = "reStructuredText" logger = logging.getLogger('kmi') diff --git a/src/keas/kmi/interfaces.py b/src/keas/kmi/interfaces.py index eb38e6e..c99d318 100644 --- a/src/keas/kmi/interfaces.py +++ b/src/keas/kmi/interfaces.py @@ -18,9 +18,6 @@ from zope.interface.common import mapping -__docformat__ = "reStructuredText" - - class IEncryptionService(zope.interface.Interface): """Utility providing encryption mechanism""" diff --git a/src/keas/kmi/keyholder.py b/src/keas/kmi/keyholder.py index ca1f475..3af30ac 100644 --- a/src/keas/kmi/keyholder.py +++ b/src/keas/kmi/keyholder.py @@ -13,7 +13,6 @@ ############################################################################## """Simple Key Holder """ -__docformat__ = "reStructuredText" from zope.interface import implementer from keas.kmi.interfaces import IKeyHolder diff --git a/src/keas/kmi/testclient.py b/src/keas/kmi/testclient.py index 11781bf..77cc037 100644 --- a/src/keas/kmi/testclient.py +++ b/src/keas/kmi/testclient.py @@ -1,4 +1,3 @@ -from __future__ import print_function ############################################################################## # # Copyright (c) 2008 Zope Foundation and Contributors. @@ -14,7 +13,7 @@ ############################################################################## """Test client to access the KMI server API. """ -__docformat__ = "reStructuredText" +from __future__ import print_function import os import sys @@ -45,7 +44,8 @@ def new_key(kmf): def read_kek(kekfile): try: - return open(kekfile, 'rb').read() + with open(kekfile, 'rb') as fp: + return fp.read() except IOError as e: print("Could not read key encrypting key from %s" % kekfile, file=sys.stderr) print(e, file=sys.stderr) @@ -57,7 +57,8 @@ def read_data(filename=None): return sys.stdin.read() else: try: - return open(filename, 'rb').read() + with open(filename, 'rb') as fp: + return fp.read() except IOError as e: print("Could not read %s" % filename, file=sys.stderr) print(e, file=sys.stderr) From 6626c120e17c3be3004e6d3505e43c890ca40d86 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Mon, 15 May 2017 11:11:37 +0200 Subject: [PATCH 07/11] remove sample.pem and use sha256 to generate the sample certificate --- README.txt | 4 ++-- generate-sample-cert.sh | 3 +-- sample.pem | 31 ------------------------------- 3 files changed, 3 insertions(+), 35 deletions(-) delete mode 100644 sample.pem diff --git a/README.txt b/README.txt index c01219d..ce1ca82 100644 --- a/README.txt +++ b/README.txt @@ -10,7 +10,7 @@ To get started do:: The server will come up on port 8080. You can create a new key encrypting key using:: - $ wget https://localhost:8080/new -O kek.dat --ca-certificate sample.pem \ + $ wget https://localhost:8080/new -O kek.dat --ca-certificate sample.crt \ --post-data="" or, if you want a more convenient tool:: @@ -21,7 +21,7 @@ The data encryption key can now be retrieved by posting the KEK to another URL:: $ wget https://localhost:8080/key --header 'Content-Type: text/plain' \ - --post-file kek.dat -O datakey.dat --ca-certificate sample.pem + --post-file kek.dat -O datakey.dat --ca-certificate sample.crt or :: diff --git a/generate-sample-cert.sh b/generate-sample-cert.sh index d515097..42e945a 100755 --- a/generate-sample-cert.sh +++ b/generate-sample-cert.sh @@ -1,4 +1,3 @@ #!/bin/sh openssl genrsa 1024 > sample.key -openssl req -new -x509 -nodes -sha1 -days 3650 -key sample.key > sample.crt -cat sample.crt sample.key > sample.pem +openssl req -new -x509 -nodes -sha256 -days 3650 -key sample.key > sample.crt diff --git a/sample.pem b/sample.pem deleted file mode 100644 index 425e083..0000000 --- a/sample.pem +++ /dev/null @@ -1,31 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICcTCCAdqgAwIBAgIJAMvA30EY1rKtMA0GCSqGSIb3DQEBBQUAMDAxCzAJBgNV -BAYTAlVTMQ0wCwYDVQQKEwRLZWFzMRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcNMDgw -OTA0MTU1MTU5WhcNMTgwOTAyMTU1MTU5WjAwMQswCQYDVQQGEwJVUzENMAsGA1UE -ChMES2VhczESMBAGA1UEAxMJbG9jYWxob3N0MIGfMA0GCSqGSIb3DQEBAQUAA4GN -ADCBiQKBgQDK17rB/KVaK8MVjiEkvA4ZncOOIC3nStZ/erXM+qwkghPM4Tfr2FTU -iTgwwdLdu/ht74oWnppttfaTQ+sVz2rFXnPgfqKTGoJTwWFiuNuZhSRDVssGVnL/ -RatZW6wns8UNf+W4hUe6/vGQP6obNTe2T4R+t2hXP51OkOy4BMcq0QIDAQABo4GS -MIGPMB0GA1UdDgQWBBQDIsX7HoSqbxKrCawi64MkXRmtmzBgBgNVHSMEWTBXgBQD -IsX7HoSqbxKrCawi64MkXRmtm6E0pDIwMDELMAkGA1UEBhMCVVMxDTALBgNVBAoT -BEtlYXMxEjAQBgNVBAMTCWxvY2FsaG9zdIIJAMvA30EY1rKtMAwGA1UdEwQFMAMB -Af8wDQYJKoZIhvcNAQEFBQADgYEAW5UBM7EIMpARzQwpQ8N1gyTR/VqJ9fSm4MIw -Y5m90HRgsDcXVbhn0rRfcC8o4EtGDvCjqsFYXy/ImF9tjEiuaysxbqepl+XMszPE -1kO50quWsV1FLSdcJX6t/ofJYOxiQkqPvg9t/ovTnEZ+w4NfPo+0MJgudjJoD2+w -5UTsKtU= ------END CERTIFICATE----- ------BEGIN RSA PRIVATE KEY----- -MIICXQIBAAKBgQDK17rB/KVaK8MVjiEkvA4ZncOOIC3nStZ/erXM+qwkghPM4Tfr -2FTUiTgwwdLdu/ht74oWnppttfaTQ+sVz2rFXnPgfqKTGoJTwWFiuNuZhSRDVssG -VnL/RatZW6wns8UNf+W4hUe6/vGQP6obNTe2T4R+t2hXP51OkOy4BMcq0QIDAQAB -AoGAHcDJDx1M784NfoLrj6TZ+J3wik9kDFIo5mgMdLWsPGqsFthOSJTh1I8QI+66 -THX++bkyKyE2i7MuKOnEeN2Ezo2jAThF7XoWhm6/+pSXhSqmL1jKr/1CZRaR9jv0 -cCVJc3mTuAGH+yFVeGpWNvDaCmOUlD5M48xTROJXteDQ0TECQQDuDM9pmQdqkGIp -dvbIviS8donYn0kJ0TKS14pMtb/C63lcld513rHS43ru3FRY9baR/q5vV9vW5RhH -S7w4cYvVAkEA2iNLsFEAkY88oZJYbdyybeKxZdReyes1/zPe4RYzRdbDHRNAa+zk -mZIZDI820E0Y+DeoT+q3nXkXiiOS/iRNDQJBAKdAvOH2sO1AcJetjArS/cCkkIlw -sMKDB0OAyRzIfekXxPc2HU03oD0Jsy/sAh9W1GWTST/VvRIpeHtvTNljfdkCQF5T -UuBcNoW6zXoEYU6oV1Oi6hjhW1eu6PuAv4jPY754XoiNEZdZqYQqo8BFkWtDW1/C -GXrtQRbMDPzD40UYB2UCQQCmJpJp+u2lHj7zuZikHIHQBNyXyoGnzgNs6XUj1Bs6 -Y4vjue8w6RkRLZ1YGP+xqsngVqb9IRygyLDpEgwEnOT4 ------END RSA PRIVATE KEY----- From 0b217863d2db5123d1b0f913244ed5657d09606d Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Mon, 15 May 2017 11:12:25 +0200 Subject: [PATCH 08/11] add Python 3.5 to .travis.yml --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 1589b6f..36e72e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: python python: - 2.7 - 3.4 + - 3.5 - 3.6 install: - python bootstrap.py From c7d1ab71f976f321f21ab995ec89f060e955bfdb Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Mon, 15 May 2017 11:18:22 +0200 Subject: [PATCH 09/11] create our own keas/kmi/_compat.py instead of importing ZODB._compat --- setup.py | 1 + src/keas/kmi/_compat.py | 59 ++++++++++++++++++++++++++++++++++++++ src/keas/kmi/persistent.py | 2 +- 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 src/keas/kmi/_compat.py diff --git a/setup.py b/setup.py index 7434f5b..2804429 100644 --- a/setup.py +++ b/setup.py @@ -62,6 +62,7 @@ def read(*rnames): 'pyramid', 'pyramid_zcml', 'setuptools', + 'six', 'transaction', 'zope.interface', 'zope.schema', diff --git a/src/keas/kmi/_compat.py b/src/keas/kmi/_compat.py new file mode 100644 index 0000000..9653466 --- /dev/null +++ b/src/keas/kmi/_compat.py @@ -0,0 +1,59 @@ +############################################################################## +# +# Copyright (c) 2013 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 +# +############################################################################## +import sys +from six import PY3 + +# This code was copied from ZODB/_compat.py + +if not PY3: + # Python 2.x + # PyPy's cPickle doesn't have noload, and noload is broken in Python 2.7, + # so we need zodbpickle. + # Get the fastest working version we can (PyPy has no fastpickle) + try: + import zodbpickle.fastpickle as cPickle + except ImportError: + import zodbpickle.pickle as cPickle + Pickler = cPickle.Pickler + Unpickler = cPickle.Unpickler +else: + # Python 3.x: can't use stdlib's pickle because + # http://bugs.python.org/issue6784 + import zodbpickle.pickle + + class Pickler(zodbpickle.pickle.Pickler): + def __init__(self, f, protocol=None): + super(Pickler, self).__init__(f, protocol) + + class Unpickler(zodbpickle.pickle.Unpickler): + def __init__(self, f): + super(Unpickler, self).__init__(f) + + # Py3: Python 3 doesn't allow assignments to find_global, + # instead, find_class can be overridden + + find_global = None + + def find_class(self, modulename, name): + if self.find_global is None: + return super(Unpickler, self).find_class(modulename, name) + return self.find_global(modulename, name) + + +try: + # XXX: why not just import BytesIO from io? + from cStringIO import StringIO as BytesIO +except ImportError: + # Python 3.x + from io import BytesIO diff --git a/src/keas/kmi/persistent.py b/src/keas/kmi/persistent.py index 0818d48..e1638df 100644 --- a/src/keas/kmi/persistent.py +++ b/src/keas/kmi/persistent.py @@ -14,7 +14,7 @@ """Encrypted persistent objects """ from __future__ import absolute_import -from ZODB._compat import BytesIO, Pickler, Unpickler +from keas.kmi._compat import BytesIO, Pickler, Unpickler import persistent import persistent.wref from zope.component import getUtility From 5c39c28ce88389ef35c74eca48e0042d2394dd76 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Mon, 15 May 2017 11:28:50 +0200 Subject: [PATCH 10/11] fix doctest on Python 3.6 --- src/keas/kmi/persistent.txt | 2 +- src/keas/kmi/tests.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/keas/kmi/persistent.txt b/src/keas/kmi/persistent.txt index cabb877..ac516cd 100644 --- a/src/keas/kmi/persistent.txt +++ b/src/keas/kmi/persistent.txt @@ -152,7 +152,7 @@ error: >>> pwd.password Traceback (most recent call last): ... - ValueError: need more than 1 value to unpack + ValueError: not enough values to unpack (expected 2, got 1) But we can apply the conversion step: diff --git a/src/keas/kmi/tests.py b/src/keas/kmi/tests.py index 4742af7..f022811 100644 --- a/src/keas/kmi/tests.py +++ b/src/keas/kmi/tests.py @@ -14,12 +14,14 @@ """Test Setup """ import doctest +import re import tempfile import transaction import unittest from zope.app.testing import setup from zope.component import provideUtility +from zope.testing.renormalizing import RENormalizing from keas.kmi.testing import TestingKeyManagementFacility from keas.kmi.interfaces import IKeyManagementFacility @@ -39,6 +41,11 @@ def tearDownPersistent(test): def test_suite(): + checker = RENormalizing([ + # fix doctest for ValueError exception on Python < 3.6 + (re.compile(r"ValueError: need more than 1 value to unpack"), + "ValueError: not enough values to unpack (expected 2, got 1)") + ]) return unittest.TestSuite([ doctest.DocFileSuite( 'README.txt', @@ -49,5 +56,6 @@ def test_suite(): doctest.DocFileSuite( 'persistent.txt', setUp=setUpPersistent, tearDown=tearDownPersistent, - optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS), + optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS, + checker=checker), ]) From 12e3f2b9faaf2386b38b0fd0f74005a47abe08e1 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Mon, 15 May 2017 11:31:35 +0200 Subject: [PATCH 11/11] add Python version/implementation classifiers --- setup.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/setup.py b/setup.py index 2804429..cbe5b86 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,13 @@ def read(*rnames): 'Intended Audience :: Developers', 'License :: OSI Approved :: Zope Public License', 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: Implementation :: CPython', 'Natural Language :: English', 'Operating System :: OS Independent', 'Topic :: Internet :: WWW/HTTP'],