Merge branch 'master' into authority_validation_LE_errors
This commit is contained in:
commit
4018c68d49
|
@ -26,6 +26,11 @@ package-lock.json
|
|||
/lemur/static/dist/
|
||||
/lemur/static/app/vendor/
|
||||
/wheelhouse
|
||||
/lemur/lib
|
||||
/lemur/bin
|
||||
/lemur/lib64
|
||||
/lemur/include
|
||||
|
||||
docs/_build
|
||||
.editorconfig
|
||||
.idea
|
||||
|
|
|
@ -10,6 +10,7 @@ from marshmallow import fields, validate, validates_schema, post_load, pre_load
|
|||
from marshmallow.exceptions import ValidationError
|
||||
|
||||
from lemur.authorities.schemas import AuthorityNestedOutputSchema
|
||||
from lemur.certificates import utils as cert_utils
|
||||
from lemur.common import missing, utils, validators
|
||||
from lemur.common.fields import ArrowDateTime, Hex
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
|
@ -110,6 +111,11 @@ class CertificateInputSchema(CertificateCreationSchema):
|
|||
def load_data(self, data):
|
||||
if data.get('replacements'):
|
||||
data['replaces'] = data['replacements'] # TODO remove when field is deprecated
|
||||
if data.get('csr'):
|
||||
dns_names = cert_utils.get_dns_names_from_csr(data['csr'])
|
||||
if not data['extensions']['subAltNames']['names']:
|
||||
data['extensions']['subAltNames']['names'] = []
|
||||
data['extensions']['subAltNames']['names'] += dns_names
|
||||
return missing.convert_validity_years(data)
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
"""
|
||||
Utils to parse certificate data.
|
||||
|
||||
.. module: lemur.certificates.hooks
|
||||
:platform: Unix
|
||||
:copyright: (c) 2019 by Javier Ramos, see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Javier Ramos <javier.ramos@booking.com>
|
||||
"""
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from marshmallow.exceptions import ValidationError
|
||||
|
||||
|
||||
def get_dns_names_from_csr(data):
|
||||
"""
|
||||
Fetches DNSNames from CSR.
|
||||
Potentially extendable to any kind of SubjectAlternativeName
|
||||
:param data: PEM-encoded string with CSR
|
||||
:return:
|
||||
"""
|
||||
dns_names = []
|
||||
try:
|
||||
request = x509.load_pem_x509_csr(data.encode('utf-8'), default_backend())
|
||||
except Exception:
|
||||
raise ValidationError('CSR presented is not valid.')
|
||||
|
||||
try:
|
||||
alt_names = request.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
||||
|
||||
for name in alt_names.value.get_values_for_type(x509.DNSName):
|
||||
dns_name = {
|
||||
'nameType': 'DNSName',
|
||||
'value': name
|
||||
}
|
||||
dns_names.append(dns_name)
|
||||
except x509.ExtensionNotFound:
|
||||
pass
|
||||
|
||||
return dns_names
|
|
@ -0,0 +1,5 @@
|
|||
try:
|
||||
VERSION = __import__('pkg_resources') \
|
||||
.get_distribution(__name__).version
|
||||
except Exception as e:
|
||||
VERSION = 'unknown'
|
|
@ -0,0 +1,156 @@
|
|||
"""
|
||||
.. module: lemur.plugins.lemur_vault_dest.plugin
|
||||
:platform: Unix
|
||||
:copyright: (c) 2019
|
||||
:license: Apache, see LICENCE for more details.
|
||||
|
||||
Plugin for uploading certificates and private key as secret to hashi vault
|
||||
that can be pulled down by end point nodes.
|
||||
|
||||
.. moduleauthor:: Christopher Jolley <chris@alwaysjolley.com>
|
||||
"""
|
||||
import hvac
|
||||
from flask import current_app
|
||||
|
||||
from lemur.common.defaults import common_name
|
||||
from lemur.common.utils import parse_certificate
|
||||
from lemur.plugins.bases import DestinationPlugin
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
|
||||
class VaultDestinationPlugin(DestinationPlugin):
|
||||
"""Hashicorp Vault Destination plugin for Lemur"""
|
||||
title = 'Vault'
|
||||
slug = 'hashi-vault-destination'
|
||||
description = 'Allow the uploading of certificates to Hashi Vault as secret'
|
||||
|
||||
author = 'Christopher Jolley'
|
||||
author_url = 'https://github.com/alwaysjolley/lemur'
|
||||
|
||||
options = [
|
||||
{
|
||||
'name': 'vaultUrl',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'validation': '^https?://[a-zA-Z0-9.:-]+$',
|
||||
'helpMessage': 'Valid URL to Hashi Vault instance'
|
||||
},
|
||||
{
|
||||
'name': 'vaultAuthTokenFile',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'validation': '(/[^/]+)+',
|
||||
'helpMessage': 'Must be a valid file path!'
|
||||
},
|
||||
{
|
||||
'name': 'vaultMount',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'validation': '^\S+$',
|
||||
'helpMessage': 'Must be a valid Vault secrets mount name!'
|
||||
},
|
||||
{
|
||||
'name': 'vaultPath',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'validation': '^([a-zA-Z0-9_-]+/?)+$',
|
||||
'helpMessage': 'Must be a valid Vault secrets path'
|
||||
},
|
||||
{
|
||||
'name': 'objectName',
|
||||
'type': 'str',
|
||||
'required': False,
|
||||
'validation': '[0-9a-zA-Z:_-]+',
|
||||
'helpMessage': 'Name to bundle certs under, if blank use cn'
|
||||
},
|
||||
{
|
||||
'name': 'bundleChain',
|
||||
'type': 'select',
|
||||
'value': 'cert only',
|
||||
'available': [
|
||||
'Nginx',
|
||||
'Apache',
|
||||
'no chain'
|
||||
],
|
||||
'required': True,
|
||||
'helpMessage': 'Bundle the chain into the certificate'
|
||||
}
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(VaultDestinationPlugin, self).__init__(*args, **kwargs)
|
||||
|
||||
def upload(self, name, body, private_key, cert_chain, options, **kwargs):
|
||||
"""
|
||||
Upload certificate and private key
|
||||
|
||||
:param private_key:
|
||||
:param cert_chain:
|
||||
:return:
|
||||
"""
|
||||
cname = common_name(parse_certificate(body))
|
||||
|
||||
url = self.get_option('vaultUrl', options)
|
||||
token_file = self.get_option('vaultAuthTokenFile', options)
|
||||
mount = self.get_option('vaultMount', options)
|
||||
path = self.get_option('vaultPath', options)
|
||||
bundle = self.get_option('bundleChain', options)
|
||||
obj_name = self.get_option('objectName', options)
|
||||
|
||||
with open(token_file, 'r') as file:
|
||||
token = file.readline().rstrip('\n')
|
||||
|
||||
client = hvac.Client(url=url, token=token)
|
||||
if obj_name:
|
||||
path = '{0}/{1}'.format(path, obj_name)
|
||||
else:
|
||||
path = '{0}/{1}'.format(path, cname)
|
||||
|
||||
secret = get_secret(url, token, mount, path)
|
||||
secret['data'][cname] = {}
|
||||
|
||||
if bundle == 'Nginx' and cert_chain:
|
||||
secret['data'][cname]['crt'] = '{0}\n{1}'.format(body, cert_chain)
|
||||
elif bundle == 'Apache' and cert_chain:
|
||||
secret['data'][cname]['crt'] = body
|
||||
secret['data'][cname]['chain'] = cert_chain
|
||||
else:
|
||||
secret['data'][cname]['crt'] = body
|
||||
secret['data'][cname]['key'] = private_key
|
||||
san_list = get_san_list(body)
|
||||
if isinstance(san_list, list):
|
||||
secret['data'][cname]['san'] = san_list
|
||||
try:
|
||||
client.secrets.kv.v1.create_or_update_secret(
|
||||
path=path, mount_point=mount, secret=secret['data'])
|
||||
except ConnectionError as err:
|
||||
current_app.logger.exception(
|
||||
"Exception uploading secret to vault: {0}".format(err), exc_info=True)
|
||||
|
||||
|
||||
def get_san_list(body):
|
||||
""" parse certificate for SAN names and return list, return empty list on error """
|
||||
san_list = []
|
||||
try:
|
||||
byte_body = body.encode('utf-8')
|
||||
cert = x509.load_pem_x509_certificate(byte_body, default_backend())
|
||||
ext = cert.extensions.get_extension_for_oid(x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
|
||||
san_list = ext.value.get_values_for_type(x509.DNSName)
|
||||
except x509.extensions.ExtensionNotFound:
|
||||
pass
|
||||
finally:
|
||||
return san_list
|
||||
|
||||
|
||||
def get_secret(url, token, mount, path):
|
||||
""" retreiive existing data from mount path and return dictionary """
|
||||
result = {'data': {}}
|
||||
try:
|
||||
client = hvac.Client(url=url, token=token)
|
||||
result = client.secrets.kv.v1.read_secret(path=path, mount_point=mount)
|
||||
except ConnectionError:
|
||||
pass
|
||||
finally:
|
||||
return result
|
|
@ -0,0 +1 @@
|
|||
from lemur.tests.conftest import * # noqa
|
|
@ -14,7 +14,6 @@ flake8==3.5.0
|
|||
identify==1.4.0 # via pre-commit
|
||||
idna==2.8 # via requests
|
||||
importlib-metadata==0.8 # via pre-commit
|
||||
importlib-resources==1.0.2 # via pre-commit
|
||||
invoke==1.2.0
|
||||
mccabe==0.6.1 # via flake8
|
||||
nodeenv==1.3.3
|
||||
|
|
|
@ -42,6 +42,7 @@ flask-sqlalchemy==2.3.2
|
|||
flask==1.0.2
|
||||
future==0.17.1
|
||||
gunicorn==19.9.0
|
||||
hvac==0.7.2
|
||||
idna==2.8
|
||||
imagesize==1.1.0 # via sphinx
|
||||
inflection==0.3.1
|
||||
|
|
|
@ -24,6 +24,7 @@ Flask
|
|||
Flask-Cors
|
||||
future
|
||||
gunicorn
|
||||
hvac # required for the vault destination plugin
|
||||
inflection
|
||||
jinja2
|
||||
kombu==4.3.0 # kombu 4.4.0 requires redis 3
|
||||
|
@ -46,4 +47,3 @@ SQLAlchemy-Utils
|
|||
tabulate
|
||||
xmltodict
|
||||
pyyaml>=4.2b1 #high severity alert
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ flask-sqlalchemy==2.3.2
|
|||
flask==1.0.2
|
||||
future==0.17.1
|
||||
gunicorn==19.9.0
|
||||
hvac==0.7.2
|
||||
idna==2.8 # via requests
|
||||
inflection==0.3.1
|
||||
itsdangerous==1.1.0 # via flask
|
||||
|
|
1
setup.py
1
setup.py
|
@ -155,6 +155,7 @@ setup(
|
|||
'digicert_cis_source = lemur.plugins.lemur_digicert.plugin:DigiCertCISSourcePlugin',
|
||||
'csr_export = lemur.plugins.lemur_csr.plugin:CSRExportPlugin',
|
||||
'sftp_destination = lemur.plugins.lemur_sftp.plugin:SFTPDestinationPlugin',
|
||||
'vault_desination = lemur.plugins.lemur_vault_dest.plugin:VaultDestinationPlugin',
|
||||
'adcs_issuer = lemur.plugins.lemur_adcs.plugin:ADCSIssuerPlugin',
|
||||
'adcs_source = lemur.plugins.lemur_adcs.plugin:ADCSSourcePlugin'
|
||||
],
|
||||
|
|
Loading…
Reference in New Issue