Initial work at removing openssl

This commit is contained in:
kevgliss 2015-07-02 12:10:09 -07:00
parent 7123e77edf
commit 8cbc6b8325
7 changed files with 78 additions and 207 deletions

View File

@ -5,13 +5,9 @@
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
import os
import arrow import arrow
import string import string
import random import random
import hashlib
import datetime
import subprocess
from sqlalchemy import func, or_ from sqlalchemy import func, or_
from flask import g, current_app 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.common.services.issuers.manager import get_plugin_by_name
from lemur.certificates.models import Certificate from lemur.certificates.models import Certificate
from lemur.certificates.exceptions import UnableToCreateCSR, \
UnableToCreatePrivateKey, MissingFiles
from lemur.accounts.models import Account from lemur.accounts.models import Account
from lemur.accounts import service as account_service 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 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): def get(cert_id):
""" """
@ -128,23 +127,17 @@ def mint(issuer_options):
authority = issuer_options['authority'] authority = issuer_options['authority']
issuer = get_plugin_by_name(authority.plugin_name) 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 issuer_options['creator'] = g.user.email
cert_body, cert_chain = issuer.create_certificate(csr, issuer_options) 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.user = g.user
cert.authority = authority cert.authority = authority
database.update(cert) 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, return cert, private_key, cert_chain,
@ -302,93 +295,83 @@ def create_csr(csr_config):
:param 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 builder = x509.CertificateSigningRequestBuilder()
path = create_path(hashlib.md5(csr_config).hexdigest()) 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() builder = builder.add_extension(
challenge_path = os.path.join(path, 'challenge.txt') x509.BasicConstraints(ca=False, path_length=None), critical=True,
)
with open(challenge_path, 'w') as c: for name in csr_config['extensions']['subAltNames']['names']:
c.write(challenge) 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: request = builder.sign(
f.write(csr_config) private_key, hashes.SHA256(), default_backend()
)
#TODO use cloudCA to seed a -rand file for each call # here we try and support arbitrary oids
#TODO replace openssl shell calls with cryptograph for oid in csr_config['extensions']['custom']:
with open('/dev/null', 'w') as devnull: builder.add_extension(
code = subprocess.call(['openssl', 'genrsa', x509.ObjectIdentifier(oid)
'-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
def create_path(domain_hash): return request.public_bytes("PEM"), private_key.public_bytes("PEM")
"""
: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])
def create_challenge(): def create_challenge():

View File

@ -27,6 +27,3 @@ class Issuer(object):
def get_authorities(self): def get_authorities(self):
raise NotImplementedError raise NotImplementedError
def get_csr_config(self):
raise NotImplementedError

View File

@ -261,15 +261,6 @@ class CloudCA(Issuer):
return cert, "".join(intermediates), 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): def random(self, length=10):
""" """
Uses CloudCA as a decent source of randomness. Uses CloudCA as a decent source of randomness.

View File

@ -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=
"""

View File

@ -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 = """ VERISIGN_INTERMEDIATE = """
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
MIIFFTCCA/2gAwIBAgIQKC4nkXkzkuQo8iGnTsk3rjANBgkqhkiG9w0BAQsFADCB MIIFFTCCA/2gAwIBAgIQKC4nkXkzkuQo8iGnTsk3rjANBgkqhkiG9w0BAQsFADCB

View File

@ -129,39 +129,6 @@ class Verisign(Issuer):
cert = self.handle_response(response.content)['Response']['Certificate'] cert = self.handle_response(response.content)['Response']['Certificate']
return cert, verisign.constants.VERISIGN_INTERMEDIATE, 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 @staticmethod
def create_authority(options): def create_authority(options):

View File

@ -1,4 +1,3 @@
#!/usr/bin/env python
import os import os
import sys import sys
import base64 import base64