Initial work at removing openssl
This commit is contained in:
parent
7123e77edf
commit
8cbc6b8325
|
@ -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():
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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=
|
|
||||||
"""
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import base64
|
import base64
|
||||||
|
|
Loading…
Reference in New Issue