Enabling CSR generation and reducing complexity of encryption/decrypting the 'key' dir.

This commit is contained in:
kevgliss 2015-07-03 10:30:17 -07:00
parent 8cbc6b8325
commit 95bab9331d
8 changed files with 156 additions and 351 deletions

View File

@ -8,8 +8,6 @@
""" """
from flask import jsonify
from lemur import factory from lemur import factory
from lemur.users.views import mod as users_bp from lemur.users.views import mod as users_bp

View File

@ -14,8 +14,6 @@ from flask import g, Blueprint, current_app, abort
from flask.ext.restful import reqparse, Resource, Api from flask.ext.restful import reqparse, Resource, Api
from flask.ext.principal import Identity, identity_changed from flask.ext.principal import Identity, identity_changed
from lemur.common.crypto import unlock
from lemur.auth.permissions import admin_permission from lemur.auth.permissions import admin_permission
from lemur.users import service as user_service from lemur.users import service as user_service
from lemur.roles import service as role_service from lemur.roles import service as role_service
@ -234,24 +232,7 @@ class Ping(Resource):
return dict(token=create_token(user)) 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(Login, '/auth/login', endpoint='login')
api.add_resource(Ping, '/auth/ping', endpoint='ping') api.add_resource(Ping, '/auth/ping', endpoint='ping')
api.add_resource(Unlock, '/auth/unlock', endpoint='unlock')

View File

@ -26,10 +26,11 @@ from lemur.roles.models import Role
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.backends import default_backend 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 from cryptography.hazmat.primitives.asymmetric import rsa
def get(cert_id): def get(cert_id):
""" """
Retrieves certificate by it's 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. 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. should still be tracked.
Internally this is used to bootstrap Lemur with external Internally this is used to bootstrap Lemur with external
@ -315,13 +316,13 @@ def create_csr(csr_config):
x509.BasicConstraints(ca=False, path_length=None), critical=True, x509.BasicConstraints(ca=False, path_length=None), critical=True,
) )
for name in csr_config['extensions']['subAltNames']['names']: for k, v in csr_config.get('extensions', {}).items():
builder.add_extension( if k == 'subAltNames':
x509.SubjectAlternativeName(x509.DNSName, name['value']) builder = builder.add_extension(
x509.SubjectAlternativeName([x509.DNSName(n) for n in v]), critical=True,
) )
# TODO support more CSR options # TODO support more CSR options, none of the authorities support these atm
# csr_config['extensions']['keyUsage']
# builder.add_extension( # builder.add_extension(
# x509.KeyUsage( # x509.KeyUsage(
# digital_signature=digital_signature, # digital_signature=digital_signature,
@ -359,20 +360,27 @@ def create_csr(csr_config):
# builder.add_extension( # builder.add_extension(
# x509.CRLDistributionPoints() # x509.CRLDistributionPoints()
# ) # )
#
# builder.add_extension(
# x509.ObjectIdentifier(oid)
# )
request = builder.sign( request = builder.sign(
private_key, hashes.SHA256(), default_backend() private_key, hashes.SHA256(), default_backend()
) )
# here we try and support arbitrary oids # serialize our private key and CSR
for oid in csr_config['extensions']['custom']: pem = private_key.private_bytes(
builder.add_extension( encoding=serialization.Encoding.PEM,
x509.ObjectIdentifier(oid) 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(): def create_challenge():
""" """

View File

@ -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 <kglisson@netflix.com>
"""
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)

View File

@ -3,6 +3,8 @@ import sys
import base64 import base64
from gunicorn.config import make_settings from gunicorn.config import make_settings
from cryptography.fernet import Fernet
from flask import current_app from flask import current_app
from flask.ext.script import Manager, Command, Option, Group, prompt_pass from flask.ext.script import Manager, Command, Option, Group, prompt_pass
from flask.ext.migrate import Migrate, MigrateCommand, stamp 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.elbs.sync import sync_all_elbs
from lemur import create_app 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 # Needed to be imported so that SQLAlchemy create_all can find our models
from lemur.users.models import User from lemur.users.models import User
@ -132,78 +133,6 @@ def create():
stamp(revision='head') 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 @manager.command
def check_revoked(): def check_revoked():
""" """
@ -491,7 +420,84 @@ def create_config(config_path=None):
with open(config_path, 'w') as f: with open(config_path, 'w') as f:
f.write(config) 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(): def main():

View File

@ -1,3 +1,4 @@
import os
import pytest import pytest
from lemur import create_app from lemur import create_app

View File

@ -1,6 +1,4 @@
import os
import pytest import pytest
from mock import mock_open, patch
from lemur.certificates.views import * from lemur.certificates.views import *
#def test_crud(session): #def test_crud(session):
@ -33,25 +31,23 @@ def test_private_key_str():
private_key_str('dfsdfsdf', 'test') private_key_str('dfsdfsdf', 'test')
def test_create_csr(): def test_create_basic_csr():
from lemur.tests.certs import CSR_CONFIG
from lemur.certificates.service import create_csr from lemur.certificates.service import create_csr
m = mock_open() csr_config = dict(
with patch('lemur.certificates.service.open', m, create=True): commonName=u'example.com',
path = create_csr(CSR_CONFIG) organization=u'Example, Inc.',
assert path == '' 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)
private_key = serialization.load_pem_private_key(pem, password=None, backend=default_backend())
def test_create_path(): csr = x509.load_pem_x509_csr(csr, default_backend())
assert 1 == 2 for name in csr.subject:
assert name.value in csr_config.values()
def test_load_ssl_pack():
assert 1 == 2
def test_delete_ssl_path():
assert 1 == 2
def test_import_certificate(session): def test_import_certificate(session):