diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index 354c9868..9f4fc1f3 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -5,13 +5,9 @@ :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ -import os import arrow import string import random -import hashlib -import datetime -import subprocess from sqlalchemy import func, or_ from flask import g, current_app @@ -21,8 +17,6 @@ from lemur.common.services.aws import iam from lemur.common.services.issuers.manager import get_plugin_by_name from lemur.certificates.models import Certificate -from lemur.certificates.exceptions import UnableToCreateCSR, \ - UnableToCreatePrivateKey, MissingFiles from lemur.accounts.models import Account from lemur.accounts import service as account_service @@ -30,6 +24,11 @@ from lemur.authorities.models import Authority 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.asymmetric import rsa + def get(cert_id): """ @@ -128,23 +127,17 @@ def mint(issuer_options): authority = issuer_options['authority'] issuer = get_plugin_by_name(authority.plugin_name) - # NOTE if we wanted to support more issuers it might make sense to - # push CSR creation down to the plugin - path = create_csr(issuer.get_csr_config(issuer_options)) - challenge, csr, csr_config, private_key = load_ssl_pack(path) - issuer_options['challenge'] = challenge + csr, private_key = create_csr(issuer_options) + + issuer_options['challenge'] = create_challenge() issuer_options['creator'] = g.user.email cert_body, cert_chain = issuer.create_certificate(csr, issuer_options) - cert = save_cert(cert_body, private_key, cert_chain, challenge, csr_config, issuer_options.get('accounts')) + cert = save_cert(cert_body, private_key, cert_chain, issuer_options.get('accounts')) cert.user = g.user cert.authority = authority database.update(cert) - - # securely delete pack after saving it to RDS and IAM (if applicable) - delete_ssl_pack(path) - return cert, private_key, cert_chain, @@ -302,93 +295,83 @@ def create_csr(csr_config): :param csr_config: """ + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=default_backend() + ) - # we create a no colliding file name - path = create_path(hashlib.md5(csr_config).hexdigest()) + builder = x509.CertificateSigningRequestBuilder() + builder = builder.subject_name(x509.Name([ + x509.NameAttribute(x509.OID_COMMON_NAME, csr_config['commonName']), + x509.NameAttribute(x509.OID_ORGANIZATION_NAME, csr_config['organization']), + x509.NameAttribute(x509.OID_ORGANIZATIONAL_UNIT_NAME, csr_config['organizationalUnit']), + x509.NameAttribute(x509.OID_COUNTRY_NAME, csr_config['country']), + x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, csr_config['state']), + x509.NameAttribute(x509.OID_LOCALITY_NAME, csr_config['location']) + ])) - challenge = create_challenge() - challenge_path = os.path.join(path, 'challenge.txt') + builder = builder.add_extension( + x509.BasicConstraints(ca=False, path_length=None), critical=True, + ) - with open(challenge_path, 'w') as c: - c.write(challenge) + for name in csr_config['extensions']['subAltNames']['names']: + builder.add_extension( + x509.SubjectAlternativeName(x509.DNSName, name['value']) + ) - csr_path = os.path.join(path, 'csr_config.txt') +# 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() +# ) - with open(csr_path, 'w') as f: - f.write(csr_config) + request = builder.sign( + private_key, hashes.SHA256(), default_backend() + ) - #TODO use cloudCA to seed a -rand file for each call - #TODO replace openssl shell calls with cryptograph - with open('/dev/null', 'w') as devnull: - code = subprocess.call(['openssl', 'genrsa', - '-out', os.path.join(path, 'private.key'), '2048'], - stdout=devnull, stderr=devnull) - - if code != 0: - raise UnableToCreatePrivateKey(code) - - with open('/dev/null', 'w') as devnull: - code = subprocess.call(['openssl', 'req', '-new', '-sha256', '-nodes', - '-config', csr_path, "-key", os.path.join(path, 'private.key'), - "-out", os.path.join(path, 'request.csr')], stdout=devnull, stderr=devnull) - - if code != 0: - raise UnableToCreateCSR(code) - - return path + # here we try and support arbitrary oids + for oid in csr_config['extensions']['custom']: + builder.add_extension( + x509.ObjectIdentifier(oid) + ) -def create_path(domain_hash): - """ - - :param domain_hash: - :return: - """ - path = os.path.join('/tmp', domain_hash) - - try: - os.mkdir(path) - except OSError as e: - now = datetime.datetime.now() - path = os.path.join('/tmp', "{}.{}".format(domain_hash, now.strftime('%s'))) - os.mkdir(path) - current_app.logger.warning(e) - - current_app.logger.debug("Writing ssl files to: {}".format(path)) - return path - - -def load_ssl_pack(path): - """ - Loads the information created by openssl to be used by other functions. - - :param path: - """ - if len(os.listdir(path)) != 4: - raise MissingFiles(path) - - with open(os.path.join(path, 'challenge.txt')) as c: - challenge = c.read() - - with open(os.path.join(path, 'request.csr')) as r: - csr = r.read() - - with open(os.path.join(path, 'csr_config.txt')) as config: - csr_config = config.read() - - with open(os.path.join(path, 'private.key')) as key: - private_key = key.read() - - return (challenge, csr, csr_config, private_key,) - - -def delete_ssl_pack(path): - """ - Removes the temporary files associated with CSR creation. - - :param path: - """ - subprocess.check_call(['srm', '-r', path]) + return request.public_bytes("PEM"), private_key.public_bytes("PEM") def create_challenge(): diff --git a/lemur/common/services/issuers/issuer.py b/lemur/common/services/issuers/issuer.py index 4950a9b9..937c3f69 100644 --- a/lemur/common/services/issuers/issuer.py +++ b/lemur/common/services/issuers/issuer.py @@ -27,6 +27,3 @@ class Issuer(object): def get_authorities(self): raise NotImplementedError - def get_csr_config(self): - raise NotImplementedError - diff --git a/lemur/common/services/issuers/plugins/cloudca/cloudca.py b/lemur/common/services/issuers/plugins/cloudca/cloudca.py index d6612b4e..2e03d778 100644 --- a/lemur/common/services/issuers/plugins/cloudca/cloudca.py +++ b/lemur/common/services/issuers/plugins/cloudca/cloudca.py @@ -261,15 +261,6 @@ class CloudCA(Issuer): return cert, "".join(intermediates), - def get_csr_config(self, issuer_options): - """ - Get a valid CSR for use with CloudCA - - :param issuer_options: - :return: - """ - return cloudca.constants.CSR_CONFIG.format(**issuer_options) - def random(self, length=10): """ Uses CloudCA as a decent source of randomness. diff --git a/lemur/common/services/issuers/plugins/cloudca/constants.py b/lemur/common/services/issuers/plugins/cloudca/constants.py deleted file mode 100644 index 229910bf..00000000 --- a/lemur/common/services/issuers/plugins/cloudca/constants.py +++ /dev/null @@ -1,27 +0,0 @@ -CSR_CONFIG = """ - # Configuration for standard CSR generation for Netflix - # Used for procuring CloudCA certificates - # Author: kglisson - # Contact: secops@netflix.com - - [ req ] - # Use a 2048 bit private key - default_bits = 2048 - default_keyfile = key.pem - prompt = no - encrypt_key = no - - # base request - distinguished_name = req_distinguished_name - - # distinguished_name - [ req_distinguished_name ] - countryName = "{country}" # C= - stateOrProvinceName = "{state}" # ST= - localityName = "{location}" # L= - organizationName = "{organization}" # O= - organizationalUnitName = "{organizationalUnit}" # OU= - # This is the hostname/subject name on the certificate - commonName = "{commonName}" # CN= - """ - diff --git a/lemur/common/services/issuers/plugins/verisign/constants.py b/lemur/common/services/issuers/plugins/verisign/constants.py index e5d84c49..7382dc38 100644 --- a/lemur/common/services/issuers/plugins/verisign/constants.py +++ b/lemur/common/services/issuers/plugins/verisign/constants.py @@ -1,42 +1,3 @@ -CSR_CONFIG = """ - # Configuration for standard CSR generation for Netflix - # Used for procuring VeriSign certificates - # Author: jachan - # Contact: cloudsecurity@netflix.com - - [ req ] - # Use a 2048 bit private key - default_bits = 2048 - default_keyfile = key.pem - prompt = no - encrypt_key = no - - # base request - distinguished_name = req_distinguished_name - - # extensions - # Uncomment the following line if you are requesting a SAN cert - {is_san_comment}req_extensions = req_ext - - # distinguished_name - [ req_distinguished_name ] - countryName = "US" # C= - stateOrProvinceName = "CALIFORNIA" # ST= - localityName = "Los Gatos" # L= - organizationName = "Netflix, Inc." # O= - organizationalUnitName = "{OU}" # OU= - # This is the hostname/subject name on the certificate - commonName = "{DNS[0]}" # CN= - - [ req_ext ] - # Uncomment the following line if you are requesting a SAN cert - {is_san_comment}subjectAltName = @alt_names - - [alt_names] - # Put your SANs here - {DNS_LINES} - """ - VERISIGN_INTERMEDIATE = """ -----BEGIN CERTIFICATE----- MIIFFTCCA/2gAwIBAgIQKC4nkXkzkuQo8iGnTsk3rjANBgkqhkiG9w0BAQsFADCB diff --git a/lemur/common/services/issuers/plugins/verisign/verisign.py b/lemur/common/services/issuers/plugins/verisign/verisign.py index 2b3ca1cd..7f3a2867 100644 --- a/lemur/common/services/issuers/plugins/verisign/verisign.py +++ b/lemur/common/services/issuers/plugins/verisign/verisign.py @@ -129,39 +129,6 @@ class Verisign(Issuer): cert = self.handle_response(response.content)['Response']['Certificate'] return cert, verisign.constants.VERISIGN_INTERMEDIATE, - def get_csr_config(self, issuer_options): - """ - Used to generate a valid CSR for the given Certificate Authority. - - :param issuer_options: - :return: :raise InsufficientDomains: - """ - domains = [] - - if issuer_options.get('commonName'): - domains.append(issuer_options.get('commonName')) - - if issuer_options.get('extensions'): - for n in issuer_options['extensions']['subAltNames']['names']: - if n['value']: - domains.append(n['value']) - - is_san_comment = "#" - - dns_lines = [] - if len(domains) < 1: - raise InsufficientDomains - - elif len(domains) > 1: - is_san_comment = "" - for domain_line in list(set(domains)): - dns_lines.append("DNS.{} = {}".format(len(dns_lines) + 1, domain_line)) - - return verisign.constants.CSR_CONFIG.format( - is_san_comment=is_san_comment, - OU=issuer_options.get('organizationalUnit', 'Operations'), - DNS=domains, - DNS_LINES="\n".join(dns_lines)) @staticmethod def create_authority(options): diff --git a/lemur/manage.py b/lemur/manage.py index 2f4aee15..93fe8775 100755 --- a/lemur/manage.py +++ b/lemur/manage.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python import os import sys import base64