From 95bab9331d695be80080088125bc1da866af534f Mon Sep 17 00:00:00 2001 From: kevgliss Date: Fri, 3 Jul 2015 10:30:17 -0700 Subject: [PATCH] Enabling CSR generation and reducing complexity of encryption/decrypting the 'key' dir. --- lemur/__init__.py | 2 - lemur/auth/views.py | 19 ---- lemur/certificates/service.py | 112 ++++++++++--------- lemur/common/crypto.py | 185 ------------------------------- lemur/manage.py | 154 ++++++++++++------------- lemur/tests/conftest.py | 1 + lemur/tests/test_certificates.py | 34 +++--- lemur/tests/test_crypto.py | 0 8 files changed, 156 insertions(+), 351 deletions(-) delete mode 100644 lemur/common/crypto.py delete mode 100644 lemur/tests/test_crypto.py diff --git a/lemur/__init__.py b/lemur/__init__.py index 39432438..aada8abe 100644 --- a/lemur/__init__.py +++ b/lemur/__init__.py @@ -8,8 +8,6 @@ """ -from flask import jsonify - from lemur import factory from lemur.users.views import mod as users_bp diff --git a/lemur/auth/views.py b/lemur/auth/views.py index 06e77f77..fd7255ca 100644 --- a/lemur/auth/views.py +++ b/lemur/auth/views.py @@ -14,8 +14,6 @@ from flask import g, Blueprint, current_app, abort from flask.ext.restful import reqparse, Resource, Api from flask.ext.principal import Identity, identity_changed -from lemur.common.crypto import unlock - from lemur.auth.permissions import admin_permission from lemur.users import service as user_service from lemur.roles import service as role_service @@ -234,24 +232,7 @@ class Ping(Resource): return dict(token=create_token(user)) -class Unlock(AuthenticatedResource): - def __init__(self): - self.reqparse = reqparse.RequestParser() - super(Unlock, self).__init__() - - @admin_permission.require(http_exception=403) - def post(self): - self.reqparse.add_argument('password', type=str, required=True, location='json') - args = self.reqparse.parse_args() - unlock(args['password']) - return { - "message": "You have successfully unlocked this Lemur instance", - "type": "success" - } - - api.add_resource(Login, '/auth/login', endpoint='login') api.add_resource(Ping, '/auth/ping', endpoint='ping') -api.add_resource(Unlock, '/auth/unlock', endpoint='unlock') diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index 9f4fc1f3..04f3b697 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -26,10 +26,11 @@ from lemur.roles.models import Role from cryptography import x509 from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa + def get(cert_id): """ Retrieves certificate by it's ID. @@ -145,7 +146,7 @@ def import_certificate(**kwargs): """ Uploads already minted certificates and pulls the required information into Lemur. - This is to be used for certificates that are reated outside of Lemur but + This is to be used for certificates that are created outside of Lemur but should still be tracked. Internally this is used to bootstrap Lemur with external @@ -315,64 +316,71 @@ def create_csr(csr_config): x509.BasicConstraints(ca=False, path_length=None), critical=True, ) - for name in csr_config['extensions']['subAltNames']['names']: - builder.add_extension( - x509.SubjectAlternativeName(x509.DNSName, name['value']) - ) + for k, v in csr_config.get('extensions', {}).items(): + if k == 'subAltNames': + builder = builder.add_extension( + x509.SubjectAlternativeName([x509.DNSName(n) for n in v]), critical=True, + ) -# TODO support more CSR options -# csr_config['extensions']['keyUsage'] -# builder.add_extension( -# x509.KeyUsage( -# digital_signature=digital_signature, -# content_commitment=content_commitment, -# key_encipherment=key_enipherment, -# data_encipherment=data_encipherment, -# key_agreement=key_agreement, -# key_cert_sign=key_cert_sign, -# crl_sign=crl_sign, -# encipher_only=enchipher_only, -# decipher_only=decipher_only -# ), critical=True -# ) -# -# # we must maintain our own list of OIDs here -# builder.add_extension( -# x509.ExtendedKeyUsage( -# server_authentication=server_authentication, -# email= -# ) -# ) -# -# builder.add_extension( -# x509.AuthorityInformationAccess() -# ) -# -# builder.add_extension( -# x509.AuthorityKeyIdentifier() -# ) -# -# builder.add_extension( -# x509.SubjectKeyIdentifier() -# ) -# -# builder.add_extension( -# x509.CRLDistributionPoints() -# ) + # TODO support more CSR options, none of the authorities support these atm + # builder.add_extension( + # x509.KeyUsage( + # digital_signature=digital_signature, + # content_commitment=content_commitment, + # key_encipherment=key_enipherment, + # data_encipherment=data_encipherment, + # key_agreement=key_agreement, + # key_cert_sign=key_cert_sign, + # crl_sign=crl_sign, + # encipher_only=enchipher_only, + # decipher_only=decipher_only + # ), critical=True + # ) + # + # # we must maintain our own list of OIDs here + # builder.add_extension( + # x509.ExtendedKeyUsage( + # server_authentication=server_authentication, + # email= + # ) + # ) + # + # builder.add_extension( + # x509.AuthorityInformationAccess() + # ) + # + # builder.add_extension( + # x509.AuthorityKeyIdentifier() + # ) + # + # builder.add_extension( + # x509.SubjectKeyIdentifier() + # ) + # + # builder.add_extension( + # x509.CRLDistributionPoints() + # ) + # + # builder.add_extension( + # x509.ObjectIdentifier(oid) + # ) request = builder.sign( private_key, hashes.SHA256(), default_backend() ) - # here we try and support arbitrary oids - for oid in csr_config['extensions']['custom']: - builder.add_extension( - x509.ObjectIdentifier(oid) - ) + # serialize our private key and CSR + pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() + ) + csr = request.public_bytes( + encoding=serialization.Encoding.PEM + ) - return request.public_bytes("PEM"), private_key.public_bytes("PEM") - + return csr, pem def create_challenge(): """ diff --git a/lemur/common/crypto.py b/lemur/common/crypto.py deleted file mode 100644 index 80b03e42..00000000 --- a/lemur/common/crypto.py +++ /dev/null @@ -1,185 +0,0 @@ -""" -.. module: lemur.common.crypto - :platform: Unix - :synopsis: This module contains all cryptographic function's in Lemur - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more - :license: Apache, see LICENSE for more details. -.. moduleauthor:: Kevin Glisson - -""" -import os -import ssl -import StringIO -import functools -from Crypto import Random -from Crypto.Cipher import AES -from hashlib import sha512 - -from flask import current_app - -from lemur.factory import create_app - - -old_init = ssl.SSLSocket.__init__ - -@functools.wraps(old_init) -def ssl_bug(self, *args, **kwargs): - kwargs['ssl_version'] = ssl.PROTOCOL_TLSv1 - old_init(self, *args, **kwargs) - -ssl.SSLSocket.__init__ = ssl_bug - - -def derive_key_and_iv(password, salt, key_length, iv_length): - """ - Derives the key and iv from the password and salt. - - :param password: - :param salt: - :param key_length: - :param iv_length: - :return: key, iv - """ - d = d_i = '' - - while len(d) < key_length + iv_length: - d_i = sha512(d_i + password + salt).digest() - d += d_i - - return d[:key_length], d[key_length:key_length+iv_length] - - -def encrypt(in_file, out_file, password, key_length=32): - """ - Encrypts a file. - - :param in_file: - :param out_file: - :param password: - :param key_length: - """ - bs = AES.block_size - salt = Random.new().read(bs - len('Salted__')) - key, iv = derive_key_and_iv(password, salt, key_length, bs) - cipher = AES.new(key, AES.MODE_CBC, iv) - out_file.write('Salted__' + salt) - finished = False - while not finished: - chunk = in_file.read(1024 * bs) - if len(chunk) == 0 or len(chunk) % bs != 0: - padding_length = bs - (len(chunk) % bs) - chunk += padding_length * chr(padding_length) - finished = True - out_file.write(cipher.encrypt(chunk)) - - -def decrypt(in_file, out_file, password, key_length=32): - """ - Decrypts a file. - - :param in_file: - :param out_file: - :param password: - :param key_length: - :raise ValueError: - """ - bs = AES.block_size - salt = in_file.read(bs)[len('Salted__'):] - key, iv = derive_key_and_iv(password, salt, key_length, bs) - cipher = AES.new(key, AES.MODE_CBC, iv) - next_chunk = '' - finished = False - while not finished: - chunk, next_chunk = next_chunk, cipher.decrypt(in_file.read(1024 * bs)) - if len(next_chunk) == 0: - padding_length = ord(chunk[-1]) - if padding_length < 1 or padding_length > bs: - raise ValueError("bad decrypt pad (%d)" % padding_length) - # all the pad-bytes must be the same - if chunk[-padding_length:] != (padding_length * chr(padding_length)): - # this is similar to the bad decrypt:evp_enc.c from openssl program - raise ValueError("bad decrypt") - chunk = chunk[:-padding_length] - finished = True - out_file.write(chunk) - - -def encrypt_string(string, password): - """ - Encrypts a string. - - :param string: - :param password: - :return: - """ - in_file = StringIO.StringIO(string) - enc_file = StringIO.StringIO() - encrypt(in_file, enc_file, password) - enc_file.seek(0) - return enc_file.read() - - -def decrypt_string(string, password): - """ - Decrypts a string. - - :param string: - :param password: - :return: - """ - in_file = StringIO.StringIO(string) - out_file = StringIO.StringIO() - decrypt(in_file, out_file, password) - out_file.seek(0) - return out_file.read() - - -def lock(password): - """ - Encrypts Lemur's KEY_PATH. This directory can be used to store secrets needed for normal - Lemur operation. This is especially useful for storing secrets needed for communication - with third parties (e.g. external certificate authorities). - - Lemur does not assume anything about the contents of the directory and will attempt to - encrypt all files contained within. Currently this has only been tested against plain - text files. - - :param password: - """ - dest_dir = os.path.join(current_app.config.get("KEY_PATH"), "encrypted") - - if not os.path.exists(dest_dir): - current_app.logger.debug("Creating encryption directory: {0}".format(dest_dir)) - os.makedirs(dest_dir) - - for root, dirs, files in os.walk(os.path.join(current_app.config.get("KEY_PATH"), 'decrypted')): - for f in files: - source = os.path.join(root, f) - dest = os.path.join(dest_dir, f + ".enc") - with open(source, 'rb') as in_file, open(dest, 'wb') as out_file: - encrypt(in_file, out_file, password) - - -def unlock(password): - """ - Decrypts Lemur's KEY_PATH, allowing lemur to use the secrets within. - - This reverses the :func:`lock` function. - - :param password: - """ - dest_dir = os.path.join(current_app.config.get("KEY_PATH"), "decrypted") - source_dir = os.path.join(current_app.config.get("KEY_PATH"), "encrypted") - - if not os.path.exists(dest_dir): - current_app.logger.debug("Creating decryption directory: {0}".format(dest_dir)) - os.makedirs(dest_dir) - - for root, dirs, files in os.walk(source_dir): - for f in files: - source = os.path.join(source_dir, f) - dest = os.path.join(dest_dir, ".".join(f.split(".")[:-1])) - with open(source, 'rb') as in_file, open(dest, 'wb') as out_file: - current_app.logger.debug("Writing file: {0} Source: {1}".format(dest, source)) - decrypt(in_file, out_file, password) - diff --git a/lemur/manage.py b/lemur/manage.py index 93fe8775..33ad5cae 100755 --- a/lemur/manage.py +++ b/lemur/manage.py @@ -3,6 +3,8 @@ import sys import base64 from gunicorn.config import make_settings +from cryptography.fernet import Fernet + from flask import current_app from flask.ext.script import Manager, Command, Option, Group, prompt_pass from flask.ext.migrate import Migrate, MigrateCommand, stamp @@ -19,7 +21,6 @@ from lemur.certificates import sync from lemur.elbs.sync import sync_all_elbs from lemur import create_app -from lemur.common.crypto import encrypt, decrypt, lock, unlock # Needed to be imported so that SQLAlchemy create_all can find our models from lemur.users.models import User @@ -132,78 +133,6 @@ def create(): stamp(revision='head') -@manager.command -def lock(): - """ - Encrypts all of the files in the `keys` directory with the password - given. This is a useful function to ensure that you do no check in - your key files into source code in clear text. - - :return: - """ - password = prompt_pass("Please enter the encryption password") - lock(password) - sys.stdout.write("[+] Lemur keys have been encrypted!\n") - - -@manager.command -def unlock(): - """ - Decrypts all of the files in the `keys` directory with the password - given. This is most commonly used during the startup sequence of Lemur - allowing it to go from source code to something that can communicate - with external services. - - :return: - """ - password = prompt_pass("Please enter the encryption password") - unlock(password) - sys.stdout.write("[+] Lemur keys have been unencrypted!\n") - - -@manager.command -def encrypt_file(source): - """ - Utility to encrypt sensitive files, Lemur will decrypt these - files when admin enters the correct password. - - Uses AES-256-CBC encryption - """ - dest = source + ".encrypted" - password = prompt_pass("Please enter the encryption password") - password1 = prompt_pass("Please confirm the encryption password") - if password != password1: - sys.stdout.write("[!] Encryption passwords do not match!\n") - return - - with open(source, 'rb') as in_file, open(dest, 'wb') as out_file: - encrypt(in_file, out_file, password) - - sys.stdout.write("[+] Writing encryption files... {0}!\n".format(dest)) - - -@manager.command -def decrypt_file(source): - """ - Utility to decrypt, Lemur will decrypt these - files when admin enters the correct password. - - Assumes AES-256-CBC encryption - """ - # cleanup extensions a bit - if ".encrypted" in source: - dest = ".".join(source.split(".")[:-1]) + ".decrypted" - else: - dest = source + ".decrypted" - - password = prompt_pass("Please enter the encryption password") - - with open(source, 'rb') as in_file, open(dest, 'wb') as out_file: - decrypt(in_file, out_file, password) - - sys.stdout.write("[+] Writing decrypted files... {0}!\n".format(dest)) - - @manager.command def check_revoked(): """ @@ -491,7 +420,84 @@ def create_config(config_path=None): with open(config_path, 'w') as f: f.write(config) - sys.stdout.write("Created a new configuration file {0}\n".format(config_path)) + sys.stdout.write("[+] Created a new configuration file {0}\n".format(config_path)) + + +@manager.command +def lock(path=None): + """ + Encrypts a given path. This directory can be used to store secrets needed for normal + Lemur operation. This is especially useful for storing secrets needed for communication + with third parties (e.g. external certificate authorities). + + Lemur does not assume anything about the contents of the directory and will attempt to + encrypt all files contained within. Currently this has only been tested against plain + text files. + + Path defaults ~/.lemur/keys + + :param: path + """ + if not path: + path = os.path.expanduser('~/.lemur/keys') + + dest_dir = os.path.join(path, "encrypted") + sys.stdout.write("[!] Generating a new key...\n") + + key = Fernet.generate_key() + + if not os.path.exists(dest_dir): + sys.stdout.write("[+] Creating encryption directory: {0}\n".format(dest_dir)) + os.makedirs(dest_dir) + + for root, dirs, files in os.walk(os.path.join(path, 'decrypted')): + for f in files: + source = os.path.join(root, f) + dest = os.path.join(dest_dir, f + ".enc") + with open(source, 'rb') as in_file, open(dest, 'wb') as out_file: + f = Fernet(key) + data = f.encrypt(in_file.read()) + out_file.write(data) + sys.stdout.write("[+] Writing file: {0} Source: {1}\n".format(dest, source)) + + sys.stdout.write("[+] Keys have been encrypted with key {0}\n".format(key)) + + +@manager.command +def unlock(path=None): + """ + Decrypts all of the files in a given directory with provided password. + This is most commonly used during the startup sequence of Lemur + allowing it to go from source code to something that can communicate + with external services. + + Path defaults ~/.lemur/keys + + :param: path + """ + key = prompt_pass("[!] Please enter the encryption password") + + if not path: + path = os.path.expanduser('~/.lemur/keys') + + dest_dir = os.path.join(path, "decrypted") + source_dir = os.path.join(path, "encrypted") + + if not os.path.exists(dest_dir): + sys.stdout.write("[+] Creating decryption directory: {0}\n".format(dest_dir)) + os.makedirs(dest_dir) + + for root, dirs, files in os.walk(source_dir): + for f in files: + source = os.path.join(source_dir, f) + dest = os.path.join(dest_dir, ".".join(f.split(".")[:-1])) + with open(source, 'rb') as in_file, open(dest, 'wb') as out_file: + f = Fernet(key) + data = f.decrypt(in_file.read()) + out_file.write(data) + sys.stdout.write("[+] Writing file: {0} Source: {1}\n".format(dest, source)) + + sys.stdout.write("[+] Keys have been unencrypted!\n") def main(): diff --git a/lemur/tests/conftest.py b/lemur/tests/conftest.py index 50bfd144..c4f1a4a8 100644 --- a/lemur/tests/conftest.py +++ b/lemur/tests/conftest.py @@ -1,3 +1,4 @@ +import os import pytest from lemur import create_app diff --git a/lemur/tests/test_certificates.py b/lemur/tests/test_certificates.py index 4470149d..4dc23a6c 100644 --- a/lemur/tests/test_certificates.py +++ b/lemur/tests/test_certificates.py @@ -1,6 +1,4 @@ -import os import pytest -from mock import mock_open, patch from lemur.certificates.views import * #def test_crud(session): @@ -33,25 +31,23 @@ def test_private_key_str(): private_key_str('dfsdfsdf', 'test') -def test_create_csr(): - from lemur.tests.certs import CSR_CONFIG +def test_create_basic_csr(): from lemur.certificates.service import create_csr - m = mock_open() - with patch('lemur.certificates.service.open', m, create=True): - path = create_csr(CSR_CONFIG) - assert path == '' + csr_config = dict( + commonName=u'example.com', + organization=u'Example, Inc.', + organizationalUnit=u'Operations', + country=u'US', + state=u'CA', + location=u'A place', + extensions=dict(subAltNames=[u'test.example.com', u'test2.example.com']) + ) + csr, pem = create_csr(csr_config) - -def test_create_path(): - assert 1 == 2 - - -def test_load_ssl_pack(): - assert 1 == 2 - - -def test_delete_ssl_path(): - assert 1 == 2 + private_key = serialization.load_pem_private_key(pem, password=None, backend=default_backend()) + csr = x509.load_pem_x509_csr(csr, default_backend()) + for name in csr.subject: + assert name.value in csr_config.values() def test_import_certificate(session): diff --git a/lemur/tests/test_crypto.py b/lemur/tests/test_crypto.py deleted file mode 100644 index e69de29b..00000000