Merge branch 'master' into hshafagh-src-dst-register

This commit is contained in:
Hossein Shafagh 2019-04-09 20:52:33 -07:00 committed by GitHub
commit bc8c7e114a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 822 additions and 144 deletions

5
.gitignore vendored
View File

@ -26,6 +26,11 @@ package-lock.json
/lemur/static/dist/ /lemur/static/dist/
/lemur/static/app/vendor/ /lemur/static/app/vendor/
/wheelhouse /wheelhouse
/lemur/lib
/lemur/bin
/lemur/lib64
/lemur/include
docs/_build docs/_build
.editorconfig .editorconfig
.idea .idea

View File

@ -161,6 +161,13 @@ Specifying the `SQLALCHEMY_MAX_OVERFLOW` to 0 will enforce limit to not create c
Dump all imported or generated CSR and certificate details to stdout using OpenSSL. (default: `False`) Dump all imported or generated CSR and certificate details to stdout using OpenSSL. (default: `False`)
.. data:: ALLOW_CERT_DELETION
:noindex:
When set to True, certificates can be marked as deleted via the API and deleted certificates will not be displayed
in the UI. When set to False (the default), the certificate delete API will always return "405 method not allowed"
and deleted certificates will always be visible in the UI. (default: `False`)
Certificate Default Options Certificate Default Options
--------------------------- ---------------------------

View File

@ -62,8 +62,8 @@ LEMUR_BLUEPRINTS = (
) )
def create_app(config=None): def create_app(config_path=None):
app = factory.create_app(app_name=__name__, blueprints=LEMUR_BLUEPRINTS, config=config) app = factory.create_app(app_name=__name__, blueprints=LEMUR_BLUEPRINTS, config=config_path)
configure_hook(app) configure_hook(app)
return app return app

View File

@ -101,7 +101,7 @@ class Certificate(db.Model):
issuer = Column(String(128)) issuer = Column(String(128))
serial = Column(String(128)) serial = Column(String(128))
cn = Column(String(128)) cn = Column(String(128))
deleted = Column(Boolean, index=True) deleted = Column(Boolean, index=True, default=False)
dns_provider_id = Column(Integer(), ForeignKey('dns_providers.id', ondelete='CASCADE'), nullable=True) dns_provider_id = Column(Integer(), ForeignKey('dns_providers.id', ondelete='CASCADE'), nullable=True)
not_before = Column(ArrowType) not_before = Column(ArrowType)
@ -192,12 +192,16 @@ class Certificate(db.Model):
def check_integrity(self): def check_integrity(self):
""" """
Integrity checks: Does the cert have a matching private key? Integrity checks: Does the cert have a valid chain and matching private key?
""" """
if self.private_key: if self.private_key:
validators.verify_private_key_match(utils.parse_private_key(self.private_key), self.parsed_cert, validators.verify_private_key_match(utils.parse_private_key(self.private_key), self.parsed_cert,
error_class=AssertionError) error_class=AssertionError)
if self.chain:
chain = [self.parsed_cert] + utils.parse_cert_chain(self.chain)
validators.verify_cert_chain(chain, error_class=AssertionError)
@cached_property @cached_property
def parsed_cert(self): def parsed_cert(self):
assert self.body, "Certificate body not set" assert self.body, "Certificate body not set"

View File

@ -10,6 +10,7 @@ from marshmallow import fields, validate, validates_schema, post_load, pre_load
from marshmallow.exceptions import ValidationError from marshmallow.exceptions import ValidationError
from lemur.authorities.schemas import AuthorityNestedOutputSchema from lemur.authorities.schemas import AuthorityNestedOutputSchema
from lemur.certificates import utils as cert_utils
from lemur.common import missing, utils, validators from lemur.common import missing, utils, validators
from lemur.common.fields import ArrowDateTime, Hex from lemur.common.fields import ArrowDateTime, Hex
from lemur.common.schema import LemurInputSchema, LemurOutputSchema from lemur.common.schema import LemurInputSchema, LemurOutputSchema
@ -96,6 +97,9 @@ class CertificateInputSchema(CertificateCreationSchema):
@validates_schema @validates_schema
def validate_authority(self, data): def validate_authority(self, data):
if isinstance(data['authority'], str):
raise ValidationError("Authority not found.")
if not data['authority'].active: if not data['authority'].active:
raise ValidationError("The authority is inactive.", ['authority']) raise ValidationError("The authority is inactive.", ['authority'])
@ -107,6 +111,11 @@ class CertificateInputSchema(CertificateCreationSchema):
def load_data(self, data): def load_data(self, data):
if data.get('replacements'): if data.get('replacements'):
data['replaces'] = data['replacements'] # TODO remove when field is deprecated 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) return missing.convert_validity_years(data)
@ -245,8 +254,7 @@ class CertificateUploadInputSchema(CertificateCreationSchema):
external_id = fields.String(missing=None, allow_none=True) external_id = fields.String(missing=None, allow_none=True)
private_key = fields.String() private_key = fields.String()
body = fields.String(required=True) body = fields.String(required=True)
chain = fields.String(validate=validators.public_certificate, missing=None, chain = fields.String(missing=None, allow_none=True)
allow_none=True) # TODO this could be multiple certificates
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True) destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True) notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
@ -260,7 +268,7 @@ class CertificateUploadInputSchema(CertificateCreationSchema):
raise ValidationError('Destinations require private key.') raise ValidationError('Destinations require private key.')
@validates_schema @validates_schema
def validate_cert_private_key(self, data): def validate_cert_private_key_chain(self, data):
cert = None cert = None
key = None key = None
if data.get('body'): if data.get('body'):
@ -279,6 +287,15 @@ class CertificateUploadInputSchema(CertificateCreationSchema):
# Throws ValidationError # Throws ValidationError
validators.verify_private_key_match(key, cert) validators.verify_private_key_match(key, cert)
if data.get('chain'):
try:
chain = utils.parse_cert_chain(data['chain'])
except ValueError:
raise ValidationError("Invalid certificate in certificate chain.", field_names=['chain'])
# Throws ValidationError
validators.verify_cert_chain([cert] + chain)
class CertificateExportInputSchema(LemurInputSchema): class CertificateExportInputSchema(LemurInputSchema):
plugin = fields.Nested(PluginInputSchema) plugin = fields.Nested(PluginInputSchema)

View File

@ -54,7 +54,7 @@ def get_by_name(name):
def get_by_serial(serial): def get_by_serial(serial):
""" """
Retrieves certificate by it's Serial. Retrieves certificate(s) by serial number.
:param serial: :param serial:
:return: :return:
""" """
@ -64,6 +64,22 @@ def get_by_serial(serial):
return Certificate.query.filter(Certificate.serial == serial).all() return Certificate.query.filter(Certificate.serial == serial).all()
def get_by_attributes(conditions):
"""
Retrieves certificate(s) by conditions given in a hash of given key=>value pairs.
:param serial:
:return:
"""
# Ensure that each of the given conditions corresponds to actual columns
# if not, silently remove it
for attr in conditions.keys():
if attr not in Certificate.__table__.columns:
conditions.pop(attr)
query = database.session_query(Certificate)
return database.find_all(query, Certificate, conditions).all()
def delete(cert_id): def delete(cert_id):
""" """
Delete's a certificate. Delete's a certificate.
@ -301,7 +317,7 @@ def render(args):
if filt: if filt:
terms = filt.split(';') terms = filt.split(';')
term = '{0}%'.format(terms[1]) term = '%{0}%'.format(terms[1])
# Exact matches for quotes. Only applies to name, issuer, and cn # Exact matches for quotes. Only applies to name, issuer, and cn
if terms[1].startswith('"') and terms[1].endswith('"'): if terms[1].startswith('"') and terms[1].endswith('"'):
term = terms[1][1:-1] term = terms[1][1:-1]
@ -365,6 +381,9 @@ def render(args):
now = arrow.now().format('YYYY-MM-DD') now = arrow.now().format('YYYY-MM-DD')
query = query.filter(Certificate.not_after <= to).filter(Certificate.not_after >= now) query = query.filter(Certificate.not_after <= to).filter(Certificate.not_after >= now)
if current_app.config.get('ALLOW_CERT_DELETION', False):
query = query.filter(Certificate.deleted == False) # noqa
result = database.sort_and_page(query, Certificate, args) result = database.sort_and_page(query, Certificate, args)
return result return result

View File

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

View File

@ -6,10 +6,9 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
import base64 import base64
import arrow
from builtins import str from builtins import str
from flask import Blueprint, make_response, jsonify, g from flask import Blueprint, make_response, jsonify, g, current_app
from flask_restful import reqparse, Api, inputs from flask_restful import reqparse, Api, inputs
from lemur.common.schema import validate_schema from lemur.common.schema import validate_schema
@ -678,19 +677,26 @@ class Certificates(AuthenticatedResource):
.. sourcecode:: http .. sourcecode:: http
HTTP/1.1 200 OK HTTP/1.1 204 OK
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 204: no error :statuscode 204: no error
:statuscode 403: unauthenticated :statuscode 403: unauthenticated
:statusoode 404: certificate not found :statuscode 404: certificate not found
:statuscode 405: certificate deletion is disabled
""" """
if not current_app.config.get('ALLOW_CERT_DELETION', False):
return dict(message="Certificate deletion is disabled"), 405
cert = service.get(certificate_id) cert = service.get(certificate_id)
if not cert: if not cert:
return dict(message="Cannot find specified certificate"), 404 return dict(message="Cannot find specified certificate"), 404
if cert.deleted:
return dict(message="Certificate is already deleted"), 412
# allow creators # allow creators
if g.current_user != cert.user: if g.current_user != cert.user:
owner_role = role_service.get_by_name(cert.owner) owner_role = role_service.get_by_name(cert.owner)
@ -699,12 +705,9 @@ class Certificates(AuthenticatedResource):
if not permission.can(): if not permission.can():
return dict(message='You are not authorized to delete this certificate'), 403 return dict(message='You are not authorized to delete this certificate'), 403
if arrow.get(cert.not_after) > arrow.utcnow():
return dict(message='Certificate is still valid, only expired certificates can be deleted'), 412
service.update(certificate_id, deleted=True) service.update(certificate_id, deleted=True)
log_service.create(g.current_user, 'delete_cert', certificate=cert) log_service.create(g.current_user, 'delete_cert', certificate=cert)
return '', 204 return 'Certificate deleted', 204
class NotificationCertificatesList(AuthenticatedResource): class NotificationCertificatesList(AuthenticatedResource):

View File

@ -47,6 +47,19 @@ def make_celery(app):
celery = make_celery(flask_app) celery = make_celery(flask_app)
def is_task_active(fun, task_id, args):
from celery.task.control import inspect
i = inspect()
active_tasks = i.active()
for _, tasks in active_tasks.items():
for task in tasks:
if task.get("id") == task_id:
continue
if task.get("name") == fun and task.get("args") == str(args):
return True
return False
@celery.task() @celery.task()
def fetch_acme_cert(id): def fetch_acme_cert(id):
""" """
@ -224,5 +237,21 @@ def sync_source(source):
:param source: :param source:
:return: :return:
""" """
current_app.logger.debug("Syncing source {}".format(source))
function = "{}.{}".format(__name__, sys._getframe().f_code.co_name)
task_id = celery.current_task.request.id
log_data = {
"function": function,
"message": "Syncing source",
"source": source,
"task_id": task_id,
}
current_app.logger.debug(log_data)
if is_task_active(function, task_id, (source,)):
log_data["message"] = "Skipping task: Task is already active"
current_app.logger.debug(log_data)
return
sync([source]) sync([source])
log_data["message"] = "Done syncing source"
current_app.logger.debug(log_data)

View File

@ -3,6 +3,8 @@ import unicodedata
from cryptography import x509 from cryptography import x509
from flask import current_app from flask import current_app
from lemur.common.utils import is_selfsigned
from lemur.extensions import sentry from lemur.extensions import sentry
from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE
@ -229,15 +231,22 @@ def issuer(cert):
""" """
Gets a sane issuer slug from a given certificate, stripping non-alphanumeric characters. Gets a sane issuer slug from a given certificate, stripping non-alphanumeric characters.
:param cert: For self-signed certificates, the special value '<selfsigned>' is returned.
If issuer cannot be determined, '<unknown>' is returned.
:param cert: Parsed certificate object
:return: Issuer slug :return: Issuer slug
""" """
# If certificate is self-signed, we return a special value -- there really is no distinct "issuer" for it
if is_selfsigned(cert):
return '<selfsigned>'
# Try Common Name or fall back to Organization name # Try Common Name or fall back to Organization name
attrs = (cert.issuer.get_attributes_for_oid(x509.OID_COMMON_NAME) or attrs = (cert.issuer.get_attributes_for_oid(x509.OID_COMMON_NAME) or
cert.issuer.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME)) cert.issuer.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME))
if not attrs: if not attrs:
current_app.logger.error("Unable to get issuer! Cert serial {:x}".format(cert.serial_number)) current_app.logger.error("Unable to get issuer! Cert serial {:x}".format(cert.serial_number))
return "Unknown" return '<unknown>'
return text_to_slug(attrs[0].value, '') return text_to_slug(attrs[0].value, '')

View File

@ -7,13 +7,15 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
import random import random
import re
import string import string
import sqlalchemy import sqlalchemy
from cryptography import x509 from cryptography import x509
from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm
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
from cryptography.hazmat.primitives.asymmetric import rsa, ec from cryptography.hazmat.primitives.asymmetric import rsa, ec, padding
from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.hazmat.primitives.serialization import load_pem_private_key
from flask_restful.reqparse import RequestParser from flask_restful.reqparse import RequestParser
from sqlalchemy import and_, func from sqlalchemy import and_, func
@ -66,6 +68,26 @@ def parse_private_key(private_key):
return load_pem_private_key(private_key.encode('utf8'), password=None, backend=default_backend()) return load_pem_private_key(private_key.encode('utf8'), password=None, backend=default_backend())
def split_pem(data):
"""
Split a string of several PEM payloads to a list of strings.
:param data: String
:return: List of strings
"""
return re.split("\n(?=-----BEGIN )", data)
def parse_cert_chain(pem_chain):
"""
Helper function to split and parse a series of PEM certificates.
:param pem_chain: string
:return: List of parsed certificates
"""
return [parse_certificate(cert) for cert in split_pem(pem_chain) if pem_chain]
def parse_csr(csr): def parse_csr(csr):
""" """
Helper function that parses a CSR. Helper function that parses a CSR.
@ -143,6 +165,42 @@ def generate_private_key(key_type):
) )
def check_cert_signature(cert, issuer_public_key):
"""
Check a certificate's signature against an issuer public key.
Before EC validation, make sure we support the algorithm, otherwise raise UnsupportedAlgorithm
On success, returns None; on failure, raises UnsupportedAlgorithm or InvalidSignature.
"""
if isinstance(issuer_public_key, rsa.RSAPublicKey):
# RSA requires padding, just to make life difficult for us poor developers :(
if cert.signature_algorithm_oid == x509.SignatureAlgorithmOID.RSASSA_PSS:
# In 2005, IETF devised a more secure padding scheme to replace PKCS #1 v1.5. To make sure that
# nobody can easily support or use it, they mandated lots of complicated parameters, unlike any
# other X.509 signature scheme.
# https://tools.ietf.org/html/rfc4056
raise UnsupportedAlgorithm("RSASSA-PSS not supported")
else:
padder = padding.PKCS1v15()
issuer_public_key.verify(cert.signature, cert.tbs_certificate_bytes, padder, cert.signature_hash_algorithm)
elif isinstance(issuer_public_key, ec.EllipticCurvePublicKey) and isinstance(ec.ECDSA(cert.signature_hash_algorithm), ec.ECDSA):
issuer_public_key.verify(cert.signature, cert.tbs_certificate_bytes, ec.ECDSA(cert.signature_hash_algorithm))
else:
raise UnsupportedAlgorithm("Unsupported Algorithm '{var}'.".format(var=cert.signature_algorithm_oid._name))
def is_selfsigned(cert):
"""
Returns True if the certificate is self-signed.
Returns False for failed verification or unsupported signing algorithm.
"""
try:
check_cert_signature(cert, cert.public_key())
# If verification was successful, it's self-signed.
return True
except InvalidSignature:
return False
def is_weekend(date): def is_weekend(date):
""" """
Determines if a given date is on a weekend. Determines if a given date is on a weekend.

View File

@ -1,27 +1,14 @@
import re import re
from cryptography import x509 from cryptography import x509
from cryptography.exceptions import UnsupportedAlgorithm, InvalidSignature
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.x509 import NameOID from cryptography.x509 import NameOID
from flask import current_app from flask import current_app
from marshmallow.exceptions import ValidationError from marshmallow.exceptions import ValidationError
from lemur.auth.permissions import SensitiveDomainPermission from lemur.auth.permissions import SensitiveDomainPermission
from lemur.common.utils import parse_certificate, is_weekend from lemur.common.utils import check_cert_signature, is_weekend
def public_certificate(body):
"""
Determines if specified string is valid public certificate.
:param body:
:return:
"""
try:
parse_certificate(body)
except Exception as e:
current_app.logger.exception(e)
raise ValidationError('Public certificate presented is not valid.')
def common_name(value): def common_name(value):
@ -138,3 +125,34 @@ def verify_private_key_match(key, cert, error_class=ValidationError):
""" """
if key.public_key().public_numbers() != cert.public_key().public_numbers(): if key.public_key().public_numbers() != cert.public_key().public_numbers():
raise error_class("Private key does not match certificate.") raise error_class("Private key does not match certificate.")
def verify_cert_chain(certs, error_class=ValidationError):
"""
Verifies that the certificates in the chain are correct.
We don't bother with full cert validation but just check that certs in the chain are signed by the next, to avoid
basic human errors -- such as pasting the wrong certificate.
:param certs: List of parsed certificates, use parse_cert_chain()
:param error_class: Exception class to raise on error
"""
cert = certs[0]
for issuer in certs[1:]:
# Use the current cert's public key to verify the previous signature.
# "certificate validation is a complex problem that involves much more than just signature checks"
try:
check_cert_signature(cert, issuer.public_key())
except InvalidSignature:
# Avoid circular import.
from lemur.common import defaults
raise error_class("Incorrect chain certificate(s) provided: '%s' is not signed by '%s'"
% (defaults.common_name(cert) or 'Unknown', defaults.common_name(issuer)))
except UnsupportedAlgorithm as err:
current_app.logger.warning("Skipping chain validation: %s", err)
# Next loop will validate that *this issuer* cert is signed by the next chain cert.
cert = issuer

View File

@ -50,7 +50,7 @@ from lemur.pending_certificates.models import PendingCertificate # noqa
from lemur.dns_providers.models import DnsProvider # noqa from lemur.dns_providers.models import DnsProvider # noqa
manager = Manager(create_app) manager = Manager(create_app)
manager.add_option('-c', '--config', dest='config') manager.add_option('-c', '--config', dest='config_path', required=False)
migrate = Migrate(create_app) migrate = Migrate(create_app)
@ -391,7 +391,7 @@ class LemurServer(Command):
# run startup tasks on an app like object # run startup tasks on an app like object
validate_conf(current_app, REQUIRED_VARIABLES) validate_conf(current_app, REQUIRED_VARIABLES)
app.app_uri = 'lemur:create_app(config="{0}")'.format(current_app.config.get('CONFIG_PATH')) app.app_uri = 'lemur:create_app(config_path="{0}")'.format(current_app.config.get('CONFIG_PATH'))
return app.run() return app.run()

View File

@ -0,0 +1,23 @@
""" Set 'deleted' flag from null to false on all certificates once
Revision ID: 318b66568358
Revises: 9f79024fe67b
Create Date: 2019-02-05 15:42:25.477587
"""
# revision identifiers, used by Alembic.
revision = '318b66568358'
down_revision = '9f79024fe67b'
from alembic import op
def upgrade():
connection = op.get_bind()
# Delete duplicate entries
connection.execute('UPDATE certificates SET deleted = false WHERE deleted IS NULL')
def downgrade():
pass

View File

@ -459,7 +459,10 @@ class ACMEIssuerPlugin(IssuerPlugin):
"pending_cert": entry["pending_cert"], "pending_cert": entry["pending_cert"],
}) })
except (PollError, AcmeError, Exception) as e: except (PollError, AcmeError, Exception) as e:
current_app.logger.error("Unable to resolve pending cert: {}".format(pending_cert), exc_info=True) order_url = order.uri
current_app.logger.error(
"Unable to resolve pending cert: {}. "
"Check out {} for more information.".format(pending_cert, order_url), exc_info=True)
certs.append({ certs.append({
"cert": False, "cert": False,
"pending_cert": entry["pending_cert"], "pending_cert": entry["pending_cert"],

View File

@ -15,6 +15,8 @@ from cryptography.fernet import Fernet
from lemur.utils import mktempfile, mktemppath from lemur.utils import mktempfile, mktemppath
from lemur.plugins.bases import ExportPlugin from lemur.plugins.bases import ExportPlugin
from lemur.plugins import lemur_java as java from lemur.plugins import lemur_java as java
from lemur.common.utils import parse_certificate
from lemur.common.defaults import common_name
def run_process(command): def run_process(command):
@ -233,7 +235,7 @@ class JavaKeystoreExportPlugin(ExportPlugin):
if self.get_option('alias', options): if self.get_option('alias', options):
alias = self.get_option('alias', options) alias = self.get_option('alias', options)
else: else:
alias = "blah" alias = common_name(parse_certificate(body))
with mktemppath() as jks_tmp: with mktemppath() as jks_tmp:
create_keystore(body, chain, jks_tmp, key, alias, passphrase) create_keystore(body, chain, jks_tmp, key, alias, passphrase)

View File

@ -14,7 +14,8 @@ from flask import current_app
from lemur.utils import mktempfile, mktemppath from lemur.utils import mktempfile, mktemppath
from lemur.plugins.bases import ExportPlugin from lemur.plugins.bases import ExportPlugin
from lemur.plugins import lemur_openssl as openssl from lemur.plugins import lemur_openssl as openssl
from lemur.common.utils import get_psuedo_random_string from lemur.common.utils import get_psuedo_random_string, parse_certificate
from lemur.common.defaults import common_name
def run_process(command): def run_process(command):
@ -122,7 +123,7 @@ class OpenSSLExportPlugin(ExportPlugin):
if self.get_option('alias', options): if self.get_option('alias', options):
alias = self.get_option('alias', options) alias = self.get_option('alias', options)
else: else:
alias = "blah" alias = common_name(parse_certificate(body))
type = self.get_option('type', options) type = self.get_option('type', options)

View File

@ -0,0 +1,5 @@
try:
VERSION = __import__('pkg_resources') \
.get_distribution(__name__).version
except Exception as e:
VERSION = 'unknown'

View File

@ -0,0 +1,173 @@
"""
.. 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': 'vaultKvApiVersion',
'type': 'select',
'value': '2',
'available': [
'1',
'2'
],
'required': True,
'helpMessage': 'Version of the Vault KV API to use'
},
{
'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)
api_version = self.get_option('vaultKvApiVersion', options)
with open(token_file, 'r') as file:
token = file.readline().rstrip('\n')
client = hvac.Client(url=url, token=token)
client.secrets.kv.default_kv_version = api_version
if obj_name:
path = '{0}/{1}'.format(path, obj_name)
else:
path = '{0}/{1}'.format(path, cname)
secret = get_secret(client, 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.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(client, mount, path):
""" retreiive existing data from mount path and return dictionary """
result = {'data': {}}
try:
if client.secrets.kv.default_kv_version == '1':
result = client.secrets.kv.v1.read_secret(path=path, mount_point=mount)
else:
result = client.secrets.kv.v2.read_secret_version(path=path, mount_point=mount)
except ConnectionError:
pass
finally:
return result

View File

@ -0,0 +1 @@
from lemur.tests.conftest import * # noqa

View File

@ -116,7 +116,12 @@ def sync_certificates(source, user):
for certificate in certificates: for certificate in certificates:
exists = False exists = False
if certificate.get('name'):
if certificate.get('search', None):
conditions = certificate.pop('search')
exists = certificate_service.get_by_attributes(conditions)
if not exists and certificate.get('name'):
result = certificate_service.get_by_name(certificate['name']) result = certificate_service.get_by_name(certificate['name'])
if result: if result:
exists = [result] exists = [result]

View File

@ -186,3 +186,5 @@ LDAP_BASE_DN = 'dc=example,dc=com'
LDAP_EMAIL_DOMAIN = 'example.com' LDAP_EMAIL_DOMAIN = 'example.com'
LDAP_REQUIRED_GROUP = 'Lemur Access' LDAP_REQUIRED_GROUP = 'Lemur Access'
LDAP_DEFAULT_ROLE = 'role1' LDAP_DEFAULT_ROLE = 'role1'
ALLOW_CERT_DELETION = True

View File

@ -3,6 +3,8 @@ import os
import datetime import datetime
import pytest import pytest
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from flask import current_app from flask import current_app
from flask_principal import identity_changed, Identity from flask_principal import identity_changed, Identity
@ -41,7 +43,7 @@ def app(request):
Creates a new Flask application for a test duration. Creates a new Flask application for a test duration.
Uses application factory `create_app`. Uses application factory `create_app`.
""" """
_app = create_app(os.path.dirname(os.path.realpath(__file__)) + '/conf.py') _app = create_app(config_path=os.path.dirname(os.path.realpath(__file__)) + '/conf.py')
ctx = _app.app_context() ctx = _app.app_context()
ctx.push() ctx.push()
@ -263,6 +265,12 @@ def cert_builder(private_key):
.not_valid_after(datetime.datetime(2040, 1, 1))) .not_valid_after(datetime.datetime(2040, 1, 1)))
@pytest.fixture
def selfsigned_cert(cert_builder, private_key):
# cert_builder uses the same cert public key as 'private_key'
return cert_builder.sign(private_key, hashes.SHA256(), default_backend())
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def aws_credentials(): def aws_credentials():
os.environ['AWS_ACCESS_KEY_ID'] = 'testing' os.environ['AWS_ACCESS_KEY_ID'] = 'testing'

View File

@ -140,6 +140,7 @@ class CACertificateFactory(CertificateFactory):
class InvalidCertificateFactory(CertificateFactory): class InvalidCertificateFactory(CertificateFactory):
body = INVALID_CERT_STR body = INVALID_CERT_STR
private_key = '' private_key = ''
chain = ''
class AuthorityFactory(BaseFactory): class AuthorityFactory(BaseFactory):

View File

@ -41,6 +41,89 @@ def test_get_or_increase_name(session, certificate):
assert get_or_increase_name('certificate1', int(serial, 16)) == 'certificate1-{}-1'.format(serial) assert get_or_increase_name('certificate1', int(serial, 16)) == 'certificate1-{}-1'.format(serial)
def test_get_all_certs(session, certificate):
from lemur.certificates.service import get_all_certs
assert len(get_all_certs()) > 1
def test_get_by_name(session, certificate):
from lemur.certificates.service import get_by_name
found = get_by_name(certificate.name)
assert found
def test_get_by_serial(session, certificate):
from lemur.certificates.service import get_by_serial
found = get_by_serial(certificate.serial)
assert found
def test_delete_cert(session):
from lemur.certificates.service import delete, get
from lemur.tests.factories import CertificateFactory
delete_this = CertificateFactory(name='DELETEME')
session.commit()
cert_exists = get(delete_this.id)
# it needs to exist first
assert cert_exists
delete(delete_this.id)
cert_exists = get(delete_this.id)
# then not exist after delete
assert not cert_exists
def test_get_by_attributes(session, certificate):
from lemur.certificates.service import get_by_attributes
# Should get one cert
certificate1 = get_by_attributes({
'name': 'SAN-san.example.org-LemurTrustUnittestsClass1CA2018-20171231-20471231'
})
# Should get one cert using multiple attrs
certificate2 = get_by_attributes({
'name': 'test-cert-11111111-1',
'cn': 'san.example.org'
})
# Should get multiple certs
multiple = get_by_attributes({
'cn': 'LemurTrust Unittests Class 1 CA 2018',
'issuer': 'LemurTrustUnittestsRootCA2018'
})
assert len(certificate1) == 1
assert len(certificate2) == 1
assert len(multiple) > 1
def test_find_duplicates(session):
from lemur.certificates.service import find_duplicates
cert = {
'body': SAN_CERT_STR,
'chain': INTERMEDIATE_CERT_STR
}
dups1 = find_duplicates(cert)
cert['chain'] = ''
dups2 = find_duplicates(cert)
assert len(dups1) > 0
assert len(dups2) > 0
def test_get_certificate_primitives(certificate): def test_get_certificate_primitives(certificate):
from lemur.certificates.service import get_certificate_primitives from lemur.certificates.service import get_certificate_primitives
@ -429,7 +512,7 @@ def test_certificate_upload_schema_invalid_chain(client):
'owner': 'pwner@example.com', 'owner': 'pwner@example.com',
} }
data, errors = CertificateUploadInputSchema().load(data) data, errors = CertificateUploadInputSchema().load(data)
assert errors == {'chain': ['Public certificate presented is not valid.']} assert errors == {'chain': ['Invalid certificate in certificate chain.']}
def test_certificate_upload_schema_wrong_pkey(client): def test_certificate_upload_schema_wrong_pkey(client):
@ -444,6 +527,30 @@ def test_certificate_upload_schema_wrong_pkey(client):
assert errors == {'_schema': ['Private key does not match certificate.']} assert errors == {'_schema': ['Private key does not match certificate.']}
def test_certificate_upload_schema_wrong_chain(client):
from lemur.certificates.schemas import CertificateUploadInputSchema
data = {
'owner': 'pwner@example.com',
'body': SAN_CERT_STR,
'chain': ROOTCA_CERT_STR,
}
data, errors = CertificateUploadInputSchema().load(data)
assert errors == {'_schema': ["Incorrect chain certificate(s) provided: 'san.example.org' is not signed by "
"'LemurTrust Unittests Root CA 2018'"]}
def test_certificate_upload_schema_wrong_chain_2nd(client):
from lemur.certificates.schemas import CertificateUploadInputSchema
data = {
'owner': 'pwner@example.com',
'body': SAN_CERT_STR,
'chain': INTERMEDIATE_CERT_STR + '\n' + SAN_CERT_STR,
}
data, errors = CertificateUploadInputSchema().load(data)
assert errors == {'_schema': ["Incorrect chain certificate(s) provided: 'LemurTrust Unittests Class 1 CA 2018' is "
"not signed by 'san.example.org'"]}
def test_create_basic_csr(client): def test_create_basic_csr(client):
csr_config = dict( csr_config = dict(
common_name='example.com', common_name='example.com',
@ -654,7 +761,7 @@ def test_certificate_put_with_data(client, certificate, issuer_plugin):
@pytest.mark.parametrize("token,status", [ @pytest.mark.parametrize("token,status", [
(VALID_USER_HEADER_TOKEN, 403), (VALID_USER_HEADER_TOKEN, 403),
(VALID_ADMIN_HEADER_TOKEN, 412), (VALID_ADMIN_HEADER_TOKEN, 204),
(VALID_ADMIN_API_TOKEN, 412), (VALID_ADMIN_API_TOKEN, 412),
('', 401) ('', 401)
]) ])

View File

@ -81,6 +81,13 @@ def test_create_name(client):
datetime(2015, 5, 12, 0, 0, 0), datetime(2015, 5, 12, 0, 0, 0),
False False
) == 'xn--mnchen-3ya.de-VertrauenswurdigAutoritat-20150507-20150512' ) == 'xn--mnchen-3ya.de-VertrauenswurdigAutoritat-20150507-20150512'
assert certificate_name(
'selfie.example.org',
'<selfsigned>',
datetime(2015, 5, 7, 0, 0, 0),
datetime(2025, 5, 12, 13, 37, 0),
False
) == 'selfie.example.org-selfsigned-20150507-20250512'
def test_issuer(client, cert_builder, issuer_private_key): def test_issuer(client, cert_builder, issuer_private_key):
@ -106,4 +113,9 @@ def test_issuer(client, cert_builder, issuer_private_key):
cert = (cert_builder cert = (cert_builder
.issuer_name(x509.Name([])) .issuer_name(x509.Name([]))
.sign(issuer_private_key, hashes.SHA256(), default_backend())) .sign(issuer_private_key, hashes.SHA256(), default_backend()))
assert issuer(cert) == 'Unknown' assert issuer(cert) == '<unknown>'
def test_issuer_selfsigned(selfsigned_cert):
from lemur.common.defaults import issuer
assert issuer(selfsigned_cert) == '<selfsigned>'

View File

@ -1,5 +1,7 @@
import pytest import pytest
from lemur.tests.vectors import SAN_CERT, INTERMEDIATE_CERT, ROOTCA_CERT, EC_CERT_EXAMPLE, ECDSA_PRIME256V1_CERT, ECDSA_SECP384r1_CERT, DSA_CERT
def test_generate_private_key(): def test_generate_private_key():
from lemur.common.utils import generate_private_key from lemur.common.utils import generate_private_key
@ -71,3 +73,21 @@ KFfxwrO1
-----END CERTIFICATE-----''' -----END CERTIFICATE-----'''
authority_key = get_authority_key(test_cert) authority_key = get_authority_key(test_cert)
assert authority_key == 'feacb541be81771293affa412d8dc9f66a3ebb80' assert authority_key == 'feacb541be81771293affa412d8dc9f66a3ebb80'
def test_is_selfsigned(selfsigned_cert):
from lemur.common.utils import is_selfsigned
assert is_selfsigned(selfsigned_cert) is True
assert is_selfsigned(SAN_CERT) is False
assert is_selfsigned(INTERMEDIATE_CERT) is False
# Root CA certificates are also technically self-signed
assert is_selfsigned(ROOTCA_CERT) is True
assert is_selfsigned(EC_CERT_EXAMPLE) is False
# selfsigned certs
assert is_selfsigned(ECDSA_PRIME256V1_CERT) is True
assert is_selfsigned(ECDSA_SECP384r1_CERT) is True
# unsupported algorithm (DSA)
with pytest.raises(Exception):
is_selfsigned(DSA_CERT)

View File

@ -45,6 +45,7 @@ ssvobJ6Xe2D4cCVjUmsqtFEztMgdqgmlcWyGdUKeXdi7CMoeTb4uO+9qRQq46wYW
n7K1z+W0Kp5yhnnPAoOioAP4vjASDx3z3RnLaZvMmcO7YdCIwhE5oGV0 n7K1z+W0Kp5yhnnPAoOioAP4vjASDx3z3RnLaZvMmcO7YdCIwhE5oGV0
-----END CERTIFICATE----- -----END CERTIFICATE-----
""" """
ROOTCA_CERT = parse_certificate(ROOTCA_CERT_STR)
ROOTCA_KEY = """\ ROOTCA_KEY = """\
-----BEGIN RSA PRIVATE KEY----- -----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAvyVpe0tfIzri3l3PYH2r7hW86wKF58GLY+Ua52rEO5E3eXQq MIIEowIBAAKCAQEAvyVpe0tfIzri3l3PYH2r7hW86wKF58GLY+Ua52rEO5E3eXQq
@ -393,3 +394,98 @@ zm3Cn4Ul8DO26w9QS4fmZjmnPOZFXYMWoOR6osHzb62PWQ8FBMqXcdToBV2Q9Iw4
PiFAxlc0tVjlLqQ= PiFAxlc0tVjlLqQ=
-----END CERTIFICATE REQUEST----- -----END CERTIFICATE REQUEST-----
""" """
EC_CERT_STR = """
-----BEGIN CERTIFICATE-----
MIIDxzCCAq+gAwIBAgIIHsJeci1JWAkwDQYJKoZIhvcNAQELBQAwVDELMAkGA1UE
BhMCVVMxHjAcBgNVBAoTFUdvb2dsZSBUcnVzdCBTZXJ2aWNlczElMCMGA1UEAxMc
R29vZ2xlIEludGVybmV0IEF1dGhvcml0eSBHMzAeFw0xOTAyMTMxNTM1NTdaFw0x
OTA1MDgxNTM1MDBaMGgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlh
MRYwFAYDVQQHDA1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKDApHb29nbGUgTExDMRcw
FQYDVQQDDA53d3cuZ29vZ2xlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA
BKwMlIbd4rAwf6eWoa6RrR2w0s5k1M40XOORPf96PByPmld+qhjRMLvA/xcAxdCR
XdcMfaX6EUr0Zw8CepitMB2jggFSMIIBTjATBgNVHSUEDDAKBggrBgEFBQcDATAO
BgNVHQ8BAf8EBAMCB4AwGQYDVR0RBBIwEIIOd3d3Lmdvb2dsZS5jb20waAYIKwYB
BQUHAQEEXDBaMC0GCCsGAQUFBzAChiFodHRwOi8vcGtpLmdvb2cvZ3NyMi9HVFNH
SUFHMy5jcnQwKQYIKwYBBQUHMAGGHWh0dHA6Ly9vY3NwLnBraS5nb29nL0dUU0dJ
QUczMB0GA1UdDgQWBBQLovm8GG0oG91gOGCL58YPNoAlejAMBgNVHRMBAf8EAjAA
MB8GA1UdIwQYMBaAFHfCuFCaZ3Z2sS3ChtCDoH6mfrpLMCEGA1UdIAQaMBgwDAYK
KwYBBAHWeQIFAzAIBgZngQwBAgIwMQYDVR0fBCowKDAmoCSgIoYgaHR0cDovL2Ny
bC5wa2kuZ29vZy9HVFNHSUFHMy5jcmwwDQYJKoZIhvcNAQELBQADggEBAKFbmNOA
e3pJ7UVI5EmkAMZgSDRdrsLHV6F7WluuyYCyE/HFpZjBd6y8xgGtYWcask6edwrq
zrcXNEN/GY34AYre0M+p0xAs+lKSwkrJd2sCgygmzsBFtGwjW6lhjm+rg83zPHhH
mQZ0ShUR1Kp4TvzXgxj44RXOsS5ZyDe3slGiG4aw/hl+igO8Y8JMvcv/Tpzo+V75
BkDAFmLRi08NayfeyCqK/TcRpzxKMKhS7jEHK8Pzu5P+FyFHKqIsobi+BA+psOix
5nZLhrweLdKNz387mE2lSSKzr7qeLGHSOMt+ajQtZio4YVyZqJvg4Y++J0n5+Rjw
MXp8GrvTfn1DQ+o=
-----END CERTIFICATE-----
"""
EC_CERT_EXAMPLE = parse_certificate(EC_CERT_STR)
ECDSA_PRIME256V1_CERT_STR = """
-----BEGIN CERTIFICATE-----
MIICUTCCAfYCCQCvH7H/e2nuiDAKBggqhkjOPQQDAjCBrzELMAkGA1UEBhMCVVMx
EzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcMCUxvcyBHYXRvczEjMCEGA1UE
CgwaTGVtdXJUcnVzdCBFbnRlcnByaXNlcyBMdGQxJjAkBgNVBAsMHVVuaXR0ZXN0
aW5nIE9wZXJhdGlvbnMgQ2VudGVyMSowKAYDVQQDDCFMZW11clRydXN0IFVuaXR0
ZXN0cyBSb290IENBIDIwMTkwHhcNMTkwMjI2MTgxMTUyWhcNMjkwMjIzMTgxMTUy
WjCBrzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcM
CUxvcyBHYXRvczEjMCEGA1UECgwaTGVtdXJUcnVzdCBFbnRlcnByaXNlcyBMdGQx
JjAkBgNVBAsMHVVuaXR0ZXN0aW5nIE9wZXJhdGlvbnMgQ2VudGVyMSowKAYDVQQD
DCFMZW11clRydXN0IFVuaXR0ZXN0cyBSb290IENBIDIwMTkwWTATBgcqhkjOPQIB
BggqhkjOPQMBBwNCAAQsnAVUtpDCFMK/k9Chynu8BWRVUBUYbGQ9Q9xeLR60J4fD
uBt48YpTqg5RMZEclVknMReXqTmqphOBo37/YVdlMAoGCCqGSM49BAMCA0kAMEYC
IQDQZ6xfBiCTHxY4GM4+zLeG1iPBUSfIJOjkFNViFZY/XAIhAJYmrkVQb/YjWCdd
Vl89McYhmV4IV7WDgUmUhkUSFXgy
-----END CERTIFICATE-----
"""
ECDSA_PRIME256V1_CERT = parse_certificate(ECDSA_PRIME256V1_CERT_STR)
ECDSA_SECP384r1_CERT_STR = """
-----BEGIN CERTIFICATE-----
MIICjjCCAhMCCQD2UadeQ7ub1jAKBggqhkjOPQQDAjCBrzELMAkGA1UEBhMCVVMx
EzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcMCUxvcyBHYXRvczEjMCEGA1UE
CgwaTGVtdXJUcnVzdCBFbnRlcnByaXNlcyBMdGQxJjAkBgNVBAsMHVVuaXR0ZXN0
aW5nIE9wZXJhdGlvbnMgQ2VudGVyMSowKAYDVQQDDCFMZW11clRydXN0IFVuaXR0
ZXN0cyBSb290IENBIDIwMTgwHhcNMTkwMjI2MTgxODU2WhcNMjkwMjIzMTgxODU2
WjCBrzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcM
CUxvcyBHYXRvczEjMCEGA1UECgwaTGVtdXJUcnVzdCBFbnRlcnByaXNlcyBMdGQx
JjAkBgNVBAsMHVVuaXR0ZXN0aW5nIE9wZXJhdGlvbnMgQ2VudGVyMSowKAYDVQQD
DCFMZW11clRydXN0IFVuaXR0ZXN0cyBSb290IENBIDIwMTgwdjAQBgcqhkjOPQIB
BgUrgQQAIgNiAARuKyHIRp2e6PB5UcY8L/bUdavkL5Zf3IegNKvaAsvkDenhDGAI
zwWgsk3rOo7jmpMibn7yJQn404uZovwyeKcApn8uVv8ltheeYAx+ySzzn/APxNGy
cye/nv1D9cDW628wCgYIKoZIzj0EAwIDaQAwZgIxANl1ljDH4ykNK2OaRqKOkBOW
cKk1SvtiEZDS/wytiZGCeaxYteSYF+3GE8V2W1geWAIxAI8D7DY0HU5zw+oxAlTD
Uw/TeHA6q0QV4otPvrINW3V09iXDwFSPe265fTkHSfT6hQ==
-----END CERTIFICATE-----
"""
ECDSA_SECP384r1_CERT = parse_certificate(ECDSA_SECP384r1_CERT_STR)
DSA_CERT_STR = """
-----BEGIN CERTIFICATE-----
MIIDmTCCA1YCCQD5h/cM7xYO9jALBglghkgBZQMEAwIwga8xCzAJBgNVBAYTAlVT
MRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlMb3MgR2F0b3MxIzAhBgNV
BAoMGkxlbXVyVHJ1c3QgRW50ZXJwcmlzZXMgTHRkMSYwJAYDVQQLDB1Vbml0dGVz
dGluZyBPcGVyYXRpb25zIENlbnRlcjEqMCgGA1UEAwwhTGVtdXJUcnVzdCBVbml0
dGVzdHMgUm9vdCBDQSAyMDE4MB4XDTE5MDIyNjE4MjUyMloXDTI5MDIyMzE4MjUy
Mlowga8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQH
DAlMb3MgR2F0b3MxIzAhBgNVBAoMGkxlbXVyVHJ1c3QgRW50ZXJwcmlzZXMgTHRk
MSYwJAYDVQQLDB1Vbml0dGVzdGluZyBPcGVyYXRpb25zIENlbnRlcjEqMCgGA1UE
AwwhTGVtdXJUcnVzdCBVbml0dGVzdHMgUm9vdCBDQSAyMDE4MIIBtjCCASsGByqG
SM44BAEwggEeAoGBAO2+6wO20rn9K7RtXJ7/kCSVFzYZsY1RKvmJ6BBkMFIepBkz
2pk62tRhJgNH07GKF7pyTPRRKqt38CaPK4ERUpavx3Ok6vZ3PKq8tMac/PMKBmT1
Xfpch54KDlCdreEMJqYiCwbIyiSCR4+PCH+7xC5Uh0PIZo6otNWe3Wkk53CfAhUA
8d4YAtto6D30f7qkEa7DMAccUS8CgYAiv8r0k0aUEaeioblcCAjmhvE0v8/tD5u1
anHO4jZIIv7uOrNFIGfqcNEOBs5AQkt5Bxn6x0b/VvtZ0FSrD0j4f36pTgro6noG
/0oRt0JngxsMSfo0LV4+bY62v21A0SneNgTgY+ugdfgGWvb0+9tpsIhiY69T+7c8
Oa0S6OWSPAOBhAACgYB5wa+nJJNZPoTWFum27JlWGYLO2flg5EpWlOvcEE0o5RfB
FPnMM033kKQQEI0YpCAq9fIMKhhUMk1X4mKUBUTt+Nrn1pY2l/wt5G6AQdHI8QXz
P1ecBbHPNZtWe3iVnfOgz/Pd8tU9slcXP9z5XbZ7R/oGcF/TPRTtbLEkYZNaDDAL
BglghkgBZQMEAwIDMAAwLQIVANubSNMSLt8plN9ZV3cp4pe3lMYCAhQPLLE7rTgm
92X+hWfyz000QEpYEQ==
-----END CERTIFICATE-----
"""
DSA_CERT = parse_certificate(DSA_CERT_STR)

View File

@ -4,4 +4,5 @@ flake8==3.5.0 # flake8 3.6.0 is giving erroneous "W605 invalid escape sequence"
pre-commit pre-commit
invoke invoke
twine twine
nodeenv nodeenv
pyyaml>=4.2b1

View File

@ -2,36 +2,35 @@
# This file is autogenerated by pip-compile # This file is autogenerated by pip-compile
# To update, run: # To update, run:
# #
# pip-compile --no-index --output-file requirements-dev.txt requirements-dev.in # pip-compile --output-file requirements-dev.txt requirements-dev.in -U --no-index
# #
aspy.yaml==1.1.2 # via pre-commit aspy.yaml==1.2.0 # via pre-commit
bleach==3.1.0 # via readme-renderer bleach==3.1.0 # via readme-renderer
certifi==2018.11.29 # via requests certifi==2019.3.9 # via requests
cfgv==1.4.0 # via pre-commit cfgv==1.5.0 # via pre-commit
chardet==3.0.4 # via requests chardet==3.0.4 # via requests
docutils==0.14 # via readme-renderer docutils==0.14 # via readme-renderer
flake8==3.5.0 flake8==3.5.0
identify==1.2.1 # via pre-commit identify==1.4.0 # via pre-commit
idna==2.8 # via requests idna==2.8 # via requests
importlib-metadata==0.8 # via pre-commit importlib-metadata==0.8 # via pre-commit
importlib-resources==1.0.2 # via pre-commit
invoke==1.2.0 invoke==1.2.0
mccabe==0.6.1 # via flake8 mccabe==0.6.1 # via flake8
nodeenv==1.3.3 nodeenv==1.3.3
pkginfo==1.5.0.1 # via twine pkginfo==1.5.0.1 # via twine
pre-commit==1.14.3 pre-commit==1.14.4
pycodestyle==2.3.1 # via flake8 pycodestyle==2.3.1 # via flake8
pyflakes==1.6.0 # via flake8 pyflakes==1.6.0 # via flake8
pygments==2.3.1 # via readme-renderer pygments==2.3.1 # via readme-renderer
pyyaml==3.13 # via aspy.yaml, pre-commit pyyaml==5.1
readme-renderer==24.0 # via twine readme-renderer==24.0 # via twine
requests-toolbelt==0.9.1 # via twine requests-toolbelt==0.9.1 # via twine
requests==2.21.0 # via requests-toolbelt, twine requests==2.21.0 # via requests-toolbelt, twine
six==1.12.0 # via bleach, cfgv, pre-commit, readme-renderer six==1.12.0 # via bleach, cfgv, pre-commit, readme-renderer
toml==0.10.0 # via pre-commit toml==0.10.0 # via pre-commit
tqdm==4.30.0 # via twine tqdm==4.31.1 # via twine
twine==1.12.1 twine==1.13.0
urllib3==1.24.1 # via requests urllib3==1.24.1 # via requests
virtualenv==16.3.0 # via pre-commit virtualenv==16.4.3 # via pre-commit
webencodings==0.5.1 # via bleach webencodings==0.5.1 # via bleach
zipp==0.3.3 # via importlib-metadata zipp==0.3.3 # via importlib-metadata

View File

@ -2,30 +2,31 @@
# This file is autogenerated by pip-compile # This file is autogenerated by pip-compile
# To update, run: # To update, run:
# #
# pip-compile --no-index --output-file requirements-docs.txt requirements-docs.in # pip-compile --output-file requirements-docs.txt requirements-docs.in -U --no-index
# #
acme==0.30.2 acme==0.32.0
alabaster==0.7.12 # via sphinx alabaster==0.7.12 # via sphinx
alembic-autogenerate-enums==0.0.2 alembic-autogenerate-enums==0.0.2
alembic==1.0.7 alembic==1.0.8
amqp==2.4.1 amqp==2.4.2
aniso8601==4.1.0 aniso8601==6.0.0
arrow==0.13.0 arrow==0.13.1
asn1crypto==0.24.0 asn1crypto==0.24.0
asyncpool==1.0 asyncpool==1.0
babel==2.6.0 # via sphinx babel==2.6.0 # via sphinx
bcrypt==3.1.6 bcrypt==3.1.6
billiard==3.5.0.5 billiard==3.5.0.5
blinker==1.4 blinker==1.4
boto3==1.9.87 boto3==1.9.120
botocore==1.12.87 botocore==1.12.120
celery[redis]==4.2.1 celery[redis]==4.2.2
certifi==2018.11.29 certifi==2019.3.9
cffi==1.11.5 certsrv==2.1.1
cffi==1.12.2
chardet==3.0.4 chardet==3.0.4
click==7.0 click==7.0
cloudflare==2.1.0 cloudflare==2.1.0
cryptography==2.5 cryptography==2.6.1
dnspython3==1.15.0 dnspython3==1.15.0
dnspython==1.15.0 dnspython==1.15.0
docutils==0.14 docutils==0.14
@ -33,7 +34,7 @@ dyn==1.8.1
flask-bcrypt==0.7.1 flask-bcrypt==0.7.1
flask-cors==3.0.7 flask-cors==3.0.7
flask-mail==0.9.1 flask-mail==0.9.1
flask-migrate==2.3.1 flask-migrate==2.4.0
flask-principal==0.4.0 flask-principal==0.4.0
flask-restful==0.3.7 flask-restful==0.3.7
flask-script==2.0.6 flask-script==2.0.6
@ -41,26 +42,27 @@ flask-sqlalchemy==2.3.2
flask==1.0.2 flask==1.0.2
future==0.17.1 future==0.17.1
gunicorn==19.9.0 gunicorn==19.9.0
hvac==0.7.2
idna==2.8 idna==2.8
imagesize==1.1.0 # via sphinx imagesize==1.1.0 # via sphinx
inflection==0.3.1 inflection==0.3.1
itsdangerous==1.1.0 itsdangerous==1.1.0
jinja2==2.10 jinja2==2.10
jmespath==0.9.3 jmespath==0.9.4
josepy==1.1.0 josepy==1.1.0
jsonlines==1.2.0 jsonlines==1.2.0
kombu==4.2.2.post1 kombu==4.3.0
lockfile==0.12.2 lockfile==0.12.2
mako==1.0.7 mako==1.0.8
markupsafe==1.1.0 markupsafe==1.1.1
marshmallow-sqlalchemy==0.16.0 marshmallow-sqlalchemy==0.16.1
marshmallow==2.18.0 marshmallow==2.19.1
mock==2.0.0 mock==2.0.0
ndg-httpsclient==0.5.1 ndg-httpsclient==0.5.1
packaging==19.0 # via sphinx packaging==19.0 # via sphinx
paramiko==2.4.2 paramiko==2.4.2
pbr==5.1.2 pbr==5.1.3
pem==18.2.0 pem==19.1.0
psycopg2==2.7.7 psycopg2==2.7.7
pyasn1-modules==0.2.4 pyasn1-modules==0.2.4
pyasn1==0.4.5 pyasn1==0.4.5
@ -74,23 +76,23 @@ pyrfc3339==1.1
python-dateutil==2.8.0 python-dateutil==2.8.0
python-editor==1.0.4 python-editor==1.0.4
pytz==2018.9 pytz==2018.9
pyyaml==3.13 pyyaml==5.1
raven[flask]==6.10.0 raven[flask]==6.10.0
redis==2.10.6 redis==2.10.6
requests-toolbelt==0.9.1 requests-toolbelt==0.9.1
requests[security]==2.21.0 requests[security]==2.21.0
retrying==1.3.3 retrying==1.3.3
s3transfer==0.1.13 s3transfer==0.2.0
six==1.12.0 six==1.12.0
snowballstemmer==1.2.1 # via sphinx snowballstemmer==1.2.1 # via sphinx
sphinx-rtd-theme==0.4.2 sphinx-rtd-theme==0.4.3
sphinx==1.8.4 sphinx==1.8.5
sphinxcontrib-httpdomain==1.7.0 sphinxcontrib-httpdomain==1.7.0
sphinxcontrib-websupport==1.1.0 # via sphinx sphinxcontrib-websupport==1.1.0 # via sphinx
sqlalchemy-utils==0.33.11 sqlalchemy-utils==0.33.11
sqlalchemy==1.2.17 sqlalchemy==1.3.1
tabulate==0.8.3 tabulate==0.8.3
urllib3==1.24.1 urllib3==1.24.1
vine==1.2.0 vine==1.3.0
werkzeug==0.14.1 werkzeug==0.15.1
xmltodict==0.11.0 xmltodict==0.12.0

View File

@ -11,3 +11,4 @@ pytest
pytest-flask pytest-flask
pytest-mock pytest-mock
requests-mock requests-mock
pyyaml>=4.2b1

View File

@ -2,63 +2,63 @@
# This file is autogenerated by pip-compile # This file is autogenerated by pip-compile
# To update, run: # To update, run:
# #
# pip-compile --no-index --output-file requirements-tests.txt requirements-tests.in # pip-compile --output-file requirements-tests.txt requirements-tests.in -U --no-index
# #
asn1crypto==0.24.0 # via cryptography asn1crypto==0.24.0 # via cryptography
atomicwrites==1.3.0 # via pytest atomicwrites==1.3.0 # via pytest
attrs==18.2.0 # via pytest attrs==19.1.0 # via pytest
aws-xray-sdk==0.95 # via moto aws-xray-sdk==0.95 # via moto
boto3==1.9.87 # via moto boto3==1.9.120 # via moto
boto==2.49.0 # via moto boto==2.49.0 # via moto
botocore==1.12.87 # via boto3, moto, s3transfer botocore==1.12.120 # via boto3, moto, s3transfer
certifi==2018.11.29 # via requests certifi==2019.3.9 # via requests
cffi==1.11.5 # via cryptography cffi==1.12.2 # via cryptography
chardet==3.0.4 # via requests chardet==3.0.4 # via requests
click==7.0 # via flask click==7.0 # via flask
coverage==4.5.2 coverage==4.5.3
cryptography==2.5 # via moto cryptography==2.6.1 # via moto
docker-pycreds==0.4.0 # via docker docker-pycreds==0.4.0 # via docker
docker==3.7.0 # via moto docker==3.7.1 # via moto
docutils==0.14 # via botocore docutils==0.14 # via botocore
ecdsa==0.13 # via python-jose ecdsa==0.13 # via python-jose
factory-boy==2.11.1 factory-boy==2.11.1
faker==1.0.2 faker==1.0.4
flask==1.0.2 # via pytest-flask flask==1.0.2 # via pytest-flask
freezegun==0.3.11 freezegun==0.3.11
future==0.17.1 # via python-jose future==0.17.1 # via python-jose
idna==2.8 # via requests idna==2.8 # via requests
itsdangerous==1.1.0 # via flask itsdangerous==1.1.0 # via flask
jinja2==2.10 # via flask, moto jinja2==2.10 # via flask, moto
jmespath==0.9.3 # via boto3, botocore jmespath==0.9.4 # via boto3, botocore
jsondiff==1.1.1 # via moto jsondiff==1.1.1 # via moto
jsonpickle==1.1 # via aws-xray-sdk jsonpickle==1.1 # via aws-xray-sdk
markupsafe==1.1.0 # via jinja2 markupsafe==1.1.1 # via jinja2
mock==2.0.0 # via moto mock==2.0.0 # via moto
more-itertools==5.0.0 # via pytest more-itertools==6.0.0 # via pytest
moto==1.3.7 moto==1.3.7
nose==1.3.7 nose==1.3.7
pbr==5.1.2 # via mock pbr==5.1.3 # via mock
pluggy==0.8.1 # via pytest pluggy==0.9.0 # via pytest
py==1.7.0 # via pytest py==1.8.0 # via pytest
pyaml==18.11.0 # via moto pyaml==18.11.0 # via moto
pycparser==2.19 # via cffi pycparser==2.19 # via cffi
pycryptodome==3.7.3 # via python-jose pycryptodome==3.8.0 # via python-jose
pyflakes==2.1.0 pyflakes==2.1.1
pytest-flask==0.14.0 pytest-flask==0.14.0
pytest-mock==1.10.1 pytest-mock==1.10.2
pytest==4.2.0 pytest==4.3.1
python-dateutil==2.8.0 # via botocore, faker, freezegun, moto python-dateutil==2.8.0 # via botocore, faker, freezegun, moto
python-jose==2.0.2 # via moto python-jose==2.0.2 # via moto
pytz==2018.9 # via moto pytz==2018.9 # via moto
pyyaml==3.13 # via pyaml pyyaml==5.1
requests-mock==1.5.2 requests-mock==1.5.2
requests==2.21.0 # via aws-xray-sdk, docker, moto, requests-mock, responses requests==2.21.0 # via aws-xray-sdk, docker, moto, requests-mock, responses
responses==0.10.5 # via moto responses==0.10.6 # via moto
s3transfer==0.1.13 # via boto3 s3transfer==0.2.0 # via boto3
six==1.12.0 # via cryptography, docker, docker-pycreds, faker, freezegun, mock, more-itertools, moto, pytest, python-dateutil, python-jose, requests-mock, responses, websocket-client six==1.12.0 # via cryptography, docker, docker-pycreds, faker, freezegun, mock, moto, pytest, python-dateutil, python-jose, requests-mock, responses, websocket-client
text-unidecode==1.2 # via faker text-unidecode==1.2 # via faker
urllib3==1.24.1 # via botocore, requests urllib3==1.24.1 # via botocore, requests
websocket-client==0.54.0 # via docker websocket-client==0.56.0 # via docker
werkzeug==0.14.1 # via flask, moto, pytest-flask werkzeug==0.15.1 # via flask, moto, pytest-flask
wrapt==1.11.1 # via aws-xray-sdk wrapt==1.11.1 # via aws-xray-sdk
xmltodict==0.11.0 # via moto xmltodict==0.12.0 # via moto

View File

@ -24,8 +24,10 @@ Flask
Flask-Cors Flask-Cors
future future
gunicorn gunicorn
hvac # required for the vault destination plugin
inflection inflection
jinja2 jinja2
kombu==4.3.0 # kombu 4.4.0 requires redis 3
lockfile lockfile
marshmallow-sqlalchemy marshmallow-sqlalchemy
marshmallow marshmallow
@ -44,3 +46,4 @@ six
SQLAlchemy-Utils SQLAlchemy-Utils
tabulate tabulate
xmltodict xmltodict
pyyaml>=4.2b1 #high severity alert

View File

@ -2,29 +2,29 @@
# This file is autogenerated by pip-compile # This file is autogenerated by pip-compile
# To update, run: # To update, run:
# #
# pip-compile --no-index --output-file requirements.txt requirements.in # pip-compile --output-file requirements.txt requirements.in -U --no-index
# #
acme==0.30.2 acme==0.32.0
alembic-autogenerate-enums==0.0.2 alembic-autogenerate-enums==0.0.2
alembic==1.0.7 # via flask-migrate alembic==1.0.8 # via flask-migrate
amqp==2.4.1 # via kombu amqp==2.4.2 # via kombu
aniso8601==4.1.0 # via flask-restful aniso8601==6.0.0 # via flask-restful
arrow==0.13.0 arrow==0.13.1
asn1crypto==0.24.0 # via cryptography asn1crypto==0.24.0 # via cryptography
asyncpool==1.0 asyncpool==1.0
bcrypt==3.1.6 # via flask-bcrypt, paramiko bcrypt==3.1.6 # via flask-bcrypt, paramiko
billiard==3.5.0.5 # via celery billiard==3.5.0.5 # via celery
blinker==1.4 # via flask-mail, flask-principal, raven blinker==1.4 # via flask-mail, flask-principal, raven
boto3==1.9.87 boto3==1.9.120
botocore==1.12.87 botocore==1.12.120
celery[redis]==4.2.1 celery[redis]==4.2.2
certifi==2018.11.29 certifi==2019.3.9
certsrv==2.1.0 certsrv==2.1.1
cffi==1.11.5 # via bcrypt, cryptography, pynacl cffi==1.12.2 # via bcrypt, cryptography, pynacl
chardet==3.0.4 # via requests chardet==3.0.4 # via requests
click==7.0 # via flask click==7.0 # via flask
cloudflare==2.1.0 cloudflare==2.1.0
cryptography==2.5 cryptography==2.6.1
dnspython3==1.15.0 dnspython3==1.15.0
dnspython==1.15.0 # via dnspython3 dnspython==1.15.0 # via dnspython3
docutils==0.14 # via botocore docutils==0.14 # via botocore
@ -32,7 +32,7 @@ dyn==1.8.1
flask-bcrypt==0.7.1 flask-bcrypt==0.7.1
flask-cors==3.0.7 flask-cors==3.0.7
flask-mail==0.9.1 flask-mail==0.9.1
flask-migrate==2.3.1 flask-migrate==2.4.0
flask-principal==0.4.0 flask-principal==0.4.0
flask-restful==0.3.7 flask-restful==0.3.7
flask-script==2.0.6 flask-script==2.0.6
@ -40,24 +40,25 @@ flask-sqlalchemy==2.3.2
flask==1.0.2 flask==1.0.2
future==0.17.1 future==0.17.1
gunicorn==19.9.0 gunicorn==19.9.0
hvac==0.7.2
idna==2.8 # via requests idna==2.8 # via requests
inflection==0.3.1 inflection==0.3.1
itsdangerous==1.1.0 # via flask itsdangerous==1.1.0 # via flask
jinja2==2.10 jinja2==2.10
jmespath==0.9.3 # via boto3, botocore jmespath==0.9.4 # via boto3, botocore
josepy==1.1.0 # via acme josepy==1.1.0 # via acme
jsonlines==1.2.0 # via cloudflare jsonlines==1.2.0 # via cloudflare
kombu==4.2.2.post1 # via celery kombu==4.3.0
lockfile==0.12.2 lockfile==0.12.2
mako==1.0.7 # via alembic mako==1.0.8 # via alembic
markupsafe==1.1.0 # via jinja2, mako markupsafe==1.1.1 # via jinja2, mako
marshmallow-sqlalchemy==0.16.0 marshmallow-sqlalchemy==0.16.1
marshmallow==2.18.0 marshmallow==2.19.1
mock==2.0.0 # via acme mock==2.0.0 # via acme
ndg-httpsclient==0.5.1 ndg-httpsclient==0.5.1
paramiko==2.4.2 paramiko==2.4.2
pbr==5.1.2 # via mock pbr==5.1.3 # via mock
pem==18.2.0 pem==19.1.0
psycopg2==2.7.7 psycopg2==2.7.7
pyasn1-modules==0.2.4 # via python-ldap pyasn1-modules==0.2.4 # via python-ldap
pyasn1==0.4.5 # via ndg-httpsclient, paramiko, pyasn1-modules, python-ldap pyasn1==0.4.5 # via ndg-httpsclient, paramiko, pyasn1-modules, python-ldap
@ -68,20 +69,20 @@ pyopenssl==19.0.0
pyrfc3339==1.1 # via acme pyrfc3339==1.1 # via acme
python-dateutil==2.8.0 # via alembic, arrow, botocore python-dateutil==2.8.0 # via alembic, arrow, botocore
python-editor==1.0.4 # via alembic python-editor==1.0.4 # via alembic
python-ldap==3.1.0 python-ldap==3.2.0
pytz==2018.9 # via acme, celery, flask-restful, pyrfc3339 pytz==2018.9 # via acme, celery, flask-restful, pyrfc3339
pyyaml==3.13 # via cloudflare pyyaml==5.1
raven[flask]==6.10.0 raven[flask]==6.10.0
redis==2.10.6 redis==2.10.6
requests-toolbelt==0.9.1 # via acme requests-toolbelt==0.9.1 # via acme
requests[security]==2.21.0 requests[security]==2.21.0
retrying==1.3.3 retrying==1.3.3
s3transfer==0.1.13 # via boto3 s3transfer==0.2.0 # via boto3
six==1.12.0 six==1.12.0
sqlalchemy-utils==0.33.11 sqlalchemy-utils==0.33.11
sqlalchemy==1.2.17 # via alembic, flask-sqlalchemy, marshmallow-sqlalchemy, sqlalchemy-utils sqlalchemy==1.3.1 # via alembic, flask-sqlalchemy, marshmallow-sqlalchemy, sqlalchemy-utils
tabulate==0.8.3 tabulate==0.8.3
urllib3==1.24.1 # via botocore, requests urllib3==1.24.1 # via botocore, requests
vine==1.2.0 # via amqp vine==1.3.0 # via amqp
werkzeug==0.14.1 # via flask werkzeug==0.15.1 # via flask
xmltodict==0.11.0 xmltodict==0.12.0

View File

@ -155,6 +155,7 @@ setup(
'digicert_cis_source = lemur.plugins.lemur_digicert.plugin:DigiCertCISSourcePlugin', 'digicert_cis_source = lemur.plugins.lemur_digicert.plugin:DigiCertCISSourcePlugin',
'csr_export = lemur.plugins.lemur_csr.plugin:CSRExportPlugin', 'csr_export = lemur.plugins.lemur_csr.plugin:CSRExportPlugin',
'sftp_destination = lemur.plugins.lemur_sftp.plugin:SFTPDestinationPlugin', '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_issuer = lemur.plugins.lemur_adcs.plugin:ADCSIssuerPlugin',
'adcs_source = lemur.plugins.lemur_adcs.plugin:ADCSSourcePlugin' 'adcs_source = lemur.plugins.lemur_adcs.plugin:ADCSSourcePlugin'
], ],