diff --git a/.gitignore b/.gitignore index 97af00ca..72e85f26 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/docs/administration.rst b/docs/administration.rst index 9d6c8d12..352318f5 100644 --- a/docs/administration.rst +++ b/docs/administration.rst @@ -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`) +.. 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 --------------------------- diff --git a/lemur/__init__.py b/lemur/__init__.py index 1cdb3468..769e0cec 100644 --- a/lemur/__init__.py +++ b/lemur/__init__.py @@ -62,8 +62,8 @@ LEMUR_BLUEPRINTS = ( ) -def create_app(config=None): - app = factory.create_app(app_name=__name__, blueprints=LEMUR_BLUEPRINTS, config=config) +def create_app(config_path=None): + app = factory.create_app(app_name=__name__, blueprints=LEMUR_BLUEPRINTS, config=config_path) configure_hook(app) return app diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index 34305cc2..bd6e8b5e 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -101,7 +101,7 @@ class Certificate(db.Model): issuer = Column(String(128)) serial = 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) not_before = Column(ArrowType) @@ -192,12 +192,16 @@ class Certificate(db.Model): 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: validators.verify_private_key_match(utils.parse_private_key(self.private_key), self.parsed_cert, 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 def parsed_cert(self): assert self.body, "Certificate body not set" diff --git a/lemur/certificates/schemas.py b/lemur/certificates/schemas.py index 946bd541..78217de0 100644 --- a/lemur/certificates/schemas.py +++ b/lemur/certificates/schemas.py @@ -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 @@ -96,6 +97,9 @@ class CertificateInputSchema(CertificateCreationSchema): @validates_schema def validate_authority(self, data): + if isinstance(data['authority'], str): + raise ValidationError("Authority not found.") + if not data['authority'].active: raise ValidationError("The authority is inactive.", ['authority']) @@ -107,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) @@ -245,8 +254,7 @@ class CertificateUploadInputSchema(CertificateCreationSchema): external_id = fields.String(missing=None, allow_none=True) private_key = fields.String() body = fields.String(required=True) - chain = fields.String(validate=validators.public_certificate, missing=None, - allow_none=True) # TODO this could be multiple certificates + chain = fields.String(missing=None, allow_none=True) destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True) notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True) @@ -260,7 +268,7 @@ class CertificateUploadInputSchema(CertificateCreationSchema): raise ValidationError('Destinations require private key.') @validates_schema - def validate_cert_private_key(self, data): + def validate_cert_private_key_chain(self, data): cert = None key = None if data.get('body'): @@ -279,6 +287,15 @@ class CertificateUploadInputSchema(CertificateCreationSchema): # Throws ValidationError 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): plugin = fields.Nested(PluginInputSchema) diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index 0f37d70e..23a9a3b9 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -54,7 +54,7 @@ def get_by_name(name): def get_by_serial(serial): """ - Retrieves certificate by it's Serial. + Retrieves certificate(s) by serial number. :param serial: :return: """ @@ -64,6 +64,22 @@ def get_by_serial(serial): 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): """ Delete's a certificate. @@ -301,7 +317,7 @@ def render(args): if filt: terms = filt.split(';') - term = '{0}%'.format(terms[1]) + term = '%{0}%'.format(terms[1]) # Exact matches for quotes. Only applies to name, issuer, and cn if terms[1].startswith('"') and terms[1].endswith('"'): term = terms[1][1:-1] @@ -365,6 +381,9 @@ def render(args): now = arrow.now().format('YYYY-MM-DD') 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) return result diff --git a/lemur/certificates/utils.py b/lemur/certificates/utils.py new file mode 100644 index 00000000..933fe45e --- /dev/null +++ b/lemur/certificates/utils.py @@ -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 +""" + +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 diff --git a/lemur/certificates/views.py b/lemur/certificates/views.py index 948c44d6..e77160b2 100644 --- a/lemur/certificates/views.py +++ b/lemur/certificates/views.py @@ -6,10 +6,9 @@ .. moduleauthor:: Kevin Glisson """ import base64 -import arrow 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 lemur.common.schema import validate_schema @@ -678,19 +677,26 @@ class Certificates(AuthenticatedResource): .. sourcecode:: http - HTTP/1.1 200 OK + HTTP/1.1 204 OK :reqheader Authorization: OAuth token to authenticate :statuscode 204: no error :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) if not cert: return dict(message="Cannot find specified certificate"), 404 + if cert.deleted: + return dict(message="Certificate is already deleted"), 412 + # allow creators if g.current_user != cert.user: owner_role = role_service.get_by_name(cert.owner) @@ -699,12 +705,9 @@ class Certificates(AuthenticatedResource): if not permission.can(): 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) log_service.create(g.current_user, 'delete_cert', certificate=cert) - return '', 204 + return 'Certificate deleted', 204 class NotificationCertificatesList(AuthenticatedResource): diff --git a/lemur/common/celery.py b/lemur/common/celery.py index c926b390..ed751d9b 100644 --- a/lemur/common/celery.py +++ b/lemur/common/celery.py @@ -50,6 +50,19 @@ def make_celery(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() def fetch_acme_cert(id): """ @@ -227,8 +240,24 @@ def sync_source(source): :param source: :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]) + log_data["message"] = "Done syncing source" + current_app.logger.debug(log_data) @celery.task() diff --git a/lemur/common/defaults.py b/lemur/common/defaults.py index 72e863c1..6b259f6b 100644 --- a/lemur/common/defaults.py +++ b/lemur/common/defaults.py @@ -3,6 +3,8 @@ import unicodedata from cryptography import x509 from flask import current_app + +from lemur.common.utils import is_selfsigned from lemur.extensions import sentry 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. - :param cert: + For self-signed certificates, the special value '' is returned. + If issuer cannot be determined, '' is returned. + + :param cert: Parsed certificate object :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 '' + # Try Common Name or fall back to Organization name attrs = (cert.issuer.get_attributes_for_oid(x509.OID_COMMON_NAME) or cert.issuer.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME)) if not attrs: current_app.logger.error("Unable to get issuer! Cert serial {:x}".format(cert.serial_number)) - return "Unknown" + return '' return text_to_slug(attrs[0].value, '') diff --git a/lemur/common/utils.py b/lemur/common/utils.py index 32271e89..62c3182b 100644 --- a/lemur/common/utils.py +++ b/lemur/common/utils.py @@ -7,13 +7,15 @@ .. moduleauthor:: Kevin Glisson """ import random +import re import string import sqlalchemy from cryptography import x509 +from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm from cryptography.hazmat.backends import default_backend 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 flask_restful.reqparse import RequestParser 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()) +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): """ 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): """ Determines if a given date is on a weekend. diff --git a/lemur/common/validators.py b/lemur/common/validators.py index 90169553..91b831ba 100644 --- a/lemur/common/validators.py +++ b/lemur/common/validators.py @@ -1,27 +1,14 @@ import re from cryptography import x509 +from cryptography.exceptions import UnsupportedAlgorithm, InvalidSignature from cryptography.hazmat.backends import default_backend from cryptography.x509 import NameOID from flask import current_app from marshmallow.exceptions import ValidationError from lemur.auth.permissions import SensitiveDomainPermission -from lemur.common.utils import parse_certificate, 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.') +from lemur.common.utils import check_cert_signature, is_weekend 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(): 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 diff --git a/lemur/manage.py b/lemur/manage.py index 184b9aa6..9161109b 100755 --- a/lemur/manage.py +++ b/lemur/manage.py @@ -50,7 +50,7 @@ from lemur.pending_certificates.models import PendingCertificate # noqa from lemur.dns_providers.models import DnsProvider # noqa 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) @@ -391,7 +391,7 @@ class LemurServer(Command): # run startup tasks on an app like object 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() diff --git a/lemur/migrations/versions/318b66568358_.py b/lemur/migrations/versions/318b66568358_.py new file mode 100644 index 00000000..9d4aa48d --- /dev/null +++ b/lemur/migrations/versions/318b66568358_.py @@ -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 diff --git a/lemur/plugins/lemur_acme/plugin.py b/lemur/plugins/lemur_acme/plugin.py index 66295ed2..59cde380 100644 --- a/lemur/plugins/lemur_acme/plugin.py +++ b/lemur/plugins/lemur_acme/plugin.py @@ -459,7 +459,10 @@ class ACMEIssuerPlugin(IssuerPlugin): "pending_cert": entry["pending_cert"], }) 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({ "cert": False, "pending_cert": entry["pending_cert"], diff --git a/lemur/plugins/lemur_java/plugin.py b/lemur/plugins/lemur_java/plugin.py index 5aab5342..7eb33b90 100644 --- a/lemur/plugins/lemur_java/plugin.py +++ b/lemur/plugins/lemur_java/plugin.py @@ -15,6 +15,8 @@ from cryptography.fernet import Fernet from lemur.utils import mktempfile, mktemppath from lemur.plugins.bases import ExportPlugin 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): @@ -233,7 +235,7 @@ class JavaKeystoreExportPlugin(ExportPlugin): if self.get_option('alias', options): alias = self.get_option('alias', options) else: - alias = "blah" + alias = common_name(parse_certificate(body)) with mktemppath() as jks_tmp: create_keystore(body, chain, jks_tmp, key, alias, passphrase) diff --git a/lemur/plugins/lemur_openssl/plugin.py b/lemur/plugins/lemur_openssl/plugin.py index 9ddce925..6d6f89aa 100644 --- a/lemur/plugins/lemur_openssl/plugin.py +++ b/lemur/plugins/lemur_openssl/plugin.py @@ -14,7 +14,8 @@ from flask import current_app from lemur.utils import mktempfile, mktemppath from lemur.plugins.bases import ExportPlugin 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): @@ -122,7 +123,7 @@ class OpenSSLExportPlugin(ExportPlugin): if self.get_option('alias', options): alias = self.get_option('alias', options) else: - alias = "blah" + alias = common_name(parse_certificate(body)) type = self.get_option('type', options) diff --git a/lemur/plugins/lemur_vault_dest/__init__.py b/lemur/plugins/lemur_vault_dest/__init__.py new file mode 100644 index 00000000..8ce5a7f3 --- /dev/null +++ b/lemur/plugins/lemur_vault_dest/__init__.py @@ -0,0 +1,5 @@ +try: + VERSION = __import__('pkg_resources') \ + .get_distribution(__name__).version +except Exception as e: + VERSION = 'unknown' diff --git a/lemur/plugins/lemur_vault_dest/plugin.py b/lemur/plugins/lemur_vault_dest/plugin.py new file mode 100644 index 00000000..6868b7b0 --- /dev/null +++ b/lemur/plugins/lemur_vault_dest/plugin.py @@ -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 +""" +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 diff --git a/lemur/plugins/lemur_vault_dest/tests/conftest.py b/lemur/plugins/lemur_vault_dest/tests/conftest.py new file mode 100644 index 00000000..0e1cd89f --- /dev/null +++ b/lemur/plugins/lemur_vault_dest/tests/conftest.py @@ -0,0 +1 @@ +from lemur.tests.conftest import * # noqa diff --git a/lemur/sources/service.py b/lemur/sources/service.py index 55d2ee62..47b7f02c 100644 --- a/lemur/sources/service.py +++ b/lemur/sources/service.py @@ -116,7 +116,12 @@ def sync_certificates(source, user): for certificate in certificates: 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']) if result: exists = [result] diff --git a/lemur/tests/conf.py b/lemur/tests/conf.py index bbe155cd..525200cf 100644 --- a/lemur/tests/conf.py +++ b/lemur/tests/conf.py @@ -186,3 +186,5 @@ LDAP_BASE_DN = 'dc=example,dc=com' LDAP_EMAIL_DOMAIN = 'example.com' LDAP_REQUIRED_GROUP = 'Lemur Access' LDAP_DEFAULT_ROLE = 'role1' + +ALLOW_CERT_DELETION = True diff --git a/lemur/tests/conftest.py b/lemur/tests/conftest.py index 32733e51..43fa7163 100644 --- a/lemur/tests/conftest.py +++ b/lemur/tests/conftest.py @@ -3,6 +3,8 @@ import os import datetime import pytest from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes from flask import current_app from flask_principal import identity_changed, Identity @@ -41,7 +43,7 @@ def app(request): Creates a new Flask application for a test duration. 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.push() @@ -263,6 +265,12 @@ def cert_builder(private_key): .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') def aws_credentials(): os.environ['AWS_ACCESS_KEY_ID'] = 'testing' diff --git a/lemur/tests/factories.py b/lemur/tests/factories.py index a4af3d43..de78f8a3 100644 --- a/lemur/tests/factories.py +++ b/lemur/tests/factories.py @@ -140,6 +140,7 @@ class CACertificateFactory(CertificateFactory): class InvalidCertificateFactory(CertificateFactory): body = INVALID_CERT_STR private_key = '' + chain = '' class AuthorityFactory(BaseFactory): diff --git a/lemur/tests/test_certificates.py b/lemur/tests/test_certificates.py index 3ee7f84e..4013d367 100644 --- a/lemur/tests/test_certificates.py +++ b/lemur/tests/test_certificates.py @@ -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) +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): from lemur.certificates.service import get_certificate_primitives @@ -429,7 +512,7 @@ def test_certificate_upload_schema_invalid_chain(client): 'owner': 'pwner@example.com', } 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): @@ -444,6 +527,30 @@ def test_certificate_upload_schema_wrong_pkey(client): 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): csr_config = dict( common_name='example.com', @@ -654,7 +761,7 @@ def test_certificate_put_with_data(client, certificate, issuer_plugin): @pytest.mark.parametrize("token,status", [ (VALID_USER_HEADER_TOKEN, 403), - (VALID_ADMIN_HEADER_TOKEN, 412), + (VALID_ADMIN_HEADER_TOKEN, 204), (VALID_ADMIN_API_TOKEN, 412), ('', 401) ]) diff --git a/lemur/tests/test_defaults.py b/lemur/tests/test_defaults.py index ffa19727..da9d6c79 100644 --- a/lemur/tests/test_defaults.py +++ b/lemur/tests/test_defaults.py @@ -81,6 +81,13 @@ def test_create_name(client): datetime(2015, 5, 12, 0, 0, 0), False ) == 'xn--mnchen-3ya.de-VertrauenswurdigAutoritat-20150507-20150512' + assert certificate_name( + 'selfie.example.org', + '', + 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): @@ -106,4 +113,9 @@ def test_issuer(client, cert_builder, issuer_private_key): cert = (cert_builder .issuer_name(x509.Name([])) .sign(issuer_private_key, hashes.SHA256(), default_backend())) - assert issuer(cert) == 'Unknown' + assert issuer(cert) == '' + + +def test_issuer_selfsigned(selfsigned_cert): + from lemur.common.defaults import issuer + assert issuer(selfsigned_cert) == '' diff --git a/lemur/tests/test_utils.py b/lemur/tests/test_utils.py index 62d021a4..74c11643 100644 --- a/lemur/tests/test_utils.py +++ b/lemur/tests/test_utils.py @@ -1,5 +1,7 @@ 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(): from lemur.common.utils import generate_private_key @@ -71,3 +73,21 @@ KFfxwrO1 -----END CERTIFICATE-----''' authority_key = get_authority_key(test_cert) 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) diff --git a/lemur/tests/vectors.py b/lemur/tests/vectors.py index 6a836b30..06e7445a 100644 --- a/lemur/tests/vectors.py +++ b/lemur/tests/vectors.py @@ -45,6 +45,7 @@ ssvobJ6Xe2D4cCVjUmsqtFEztMgdqgmlcWyGdUKeXdi7CMoeTb4uO+9qRQq46wYW n7K1z+W0Kp5yhnnPAoOioAP4vjASDx3z3RnLaZvMmcO7YdCIwhE5oGV0 -----END CERTIFICATE----- """ +ROOTCA_CERT = parse_certificate(ROOTCA_CERT_STR) ROOTCA_KEY = """\ -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAvyVpe0tfIzri3l3PYH2r7hW86wKF58GLY+Ua52rEO5E3eXQq @@ -393,3 +394,98 @@ zm3Cn4Ul8DO26w9QS4fmZjmnPOZFXYMWoOR6osHzb62PWQ8FBMqXcdToBV2Q9Iw4 PiFAxlc0tVjlLqQ= -----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) diff --git a/requirements-dev.in b/requirements-dev.in index 84104679..2ffc5488 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -4,4 +4,5 @@ flake8==3.5.0 # flake8 3.6.0 is giving erroneous "W605 invalid escape sequence" pre-commit invoke twine -nodeenv \ No newline at end of file +nodeenv +pyyaml>=4.2b1 \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 29f39314..36e2c9a4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,36 +2,35 @@ # This file is autogenerated by pip-compile # 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 -certifi==2018.11.29 # via requests -cfgv==1.4.0 # via pre-commit +certifi==2019.3.9 # via requests +cfgv==1.5.0 # via pre-commit chardet==3.0.4 # via requests docutils==0.14 # via readme-renderer flake8==3.5.0 -identify==1.2.1 # via pre-commit +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 pkginfo==1.5.0.1 # via twine -pre-commit==1.14.3 +pre-commit==1.14.4 pycodestyle==2.3.1 # via flake8 pyflakes==1.6.0 # via flake8 pygments==2.3.1 # via readme-renderer -pyyaml==3.13 # via aspy.yaml, pre-commit +pyyaml==5.1 readme-renderer==24.0 # via twine requests-toolbelt==0.9.1 # via twine requests==2.21.0 # via requests-toolbelt, twine six==1.12.0 # via bleach, cfgv, pre-commit, readme-renderer toml==0.10.0 # via pre-commit -tqdm==4.30.0 # via twine -twine==1.12.1 +tqdm==4.31.1 # via twine +twine==1.13.0 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 zipp==0.3.3 # via importlib-metadata diff --git a/requirements-docs.txt b/requirements-docs.txt index 21dc110c..e99c9cdc 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -2,30 +2,31 @@ # This file is autogenerated by pip-compile # 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 alembic-autogenerate-enums==0.0.2 -alembic==1.0.7 -amqp==2.4.1 -aniso8601==4.1.0 -arrow==0.13.0 +alembic==1.0.8 +amqp==2.4.2 +aniso8601==6.0.0 +arrow==0.13.1 asn1crypto==0.24.0 asyncpool==1.0 babel==2.6.0 # via sphinx bcrypt==3.1.6 billiard==3.5.0.5 blinker==1.4 -boto3==1.9.87 -botocore==1.12.87 -celery[redis]==4.2.1 -certifi==2018.11.29 -cffi==1.11.5 +boto3==1.9.120 +botocore==1.12.120 +celery[redis]==4.2.2 +certifi==2019.3.9 +certsrv==2.1.1 +cffi==1.12.2 chardet==3.0.4 click==7.0 cloudflare==2.1.0 -cryptography==2.5 +cryptography==2.6.1 dnspython3==1.15.0 dnspython==1.15.0 docutils==0.14 @@ -33,7 +34,7 @@ dyn==1.8.1 flask-bcrypt==0.7.1 flask-cors==3.0.7 flask-mail==0.9.1 -flask-migrate==2.3.1 +flask-migrate==2.4.0 flask-principal==0.4.0 flask-restful==0.3.7 flask-script==2.0.6 @@ -41,26 +42,27 @@ 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 itsdangerous==1.1.0 jinja2==2.10 -jmespath==0.9.3 +jmespath==0.9.4 josepy==1.1.0 jsonlines==1.2.0 -kombu==4.2.2.post1 +kombu==4.3.0 lockfile==0.12.2 -mako==1.0.7 -markupsafe==1.1.0 -marshmallow-sqlalchemy==0.16.0 -marshmallow==2.18.0 +mako==1.0.8 +markupsafe==1.1.1 +marshmallow-sqlalchemy==0.16.1 +marshmallow==2.19.1 mock==2.0.0 ndg-httpsclient==0.5.1 packaging==19.0 # via sphinx paramiko==2.4.2 -pbr==5.1.2 -pem==18.2.0 +pbr==5.1.3 +pem==19.1.0 psycopg2==2.7.7 pyasn1-modules==0.2.4 pyasn1==0.4.5 @@ -74,23 +76,23 @@ pyrfc3339==1.1 python-dateutil==2.8.0 python-editor==1.0.4 pytz==2018.9 -pyyaml==3.13 +pyyaml==5.1 raven[flask]==6.10.0 redis==2.10.6 requests-toolbelt==0.9.1 requests[security]==2.21.0 retrying==1.3.3 -s3transfer==0.1.13 +s3transfer==0.2.0 six==1.12.0 snowballstemmer==1.2.1 # via sphinx -sphinx-rtd-theme==0.4.2 -sphinx==1.8.4 +sphinx-rtd-theme==0.4.3 +sphinx==1.8.5 sphinxcontrib-httpdomain==1.7.0 sphinxcontrib-websupport==1.1.0 # via sphinx sqlalchemy-utils==0.33.11 -sqlalchemy==1.2.17 +sqlalchemy==1.3.1 tabulate==0.8.3 urllib3==1.24.1 -vine==1.2.0 -werkzeug==0.14.1 -xmltodict==0.11.0 +vine==1.3.0 +werkzeug==0.15.1 +xmltodict==0.12.0 diff --git a/requirements-tests.in b/requirements-tests.in index 02a2b0ae..dcd3d0c7 100644 --- a/requirements-tests.in +++ b/requirements-tests.in @@ -11,3 +11,4 @@ pytest pytest-flask pytest-mock requests-mock +pyyaml>=4.2b1 \ No newline at end of file diff --git a/requirements-tests.txt b/requirements-tests.txt index 354f4f1a..ed48cfdd 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -2,63 +2,63 @@ # This file is autogenerated by pip-compile # 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 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 -boto3==1.9.87 # via moto +boto3==1.9.120 # via moto boto==2.49.0 # via moto -botocore==1.12.87 # via boto3, moto, s3transfer -certifi==2018.11.29 # via requests -cffi==1.11.5 # via cryptography +botocore==1.12.120 # via boto3, moto, s3transfer +certifi==2019.3.9 # via requests +cffi==1.12.2 # via cryptography chardet==3.0.4 # via requests click==7.0 # via flask -coverage==4.5.2 -cryptography==2.5 # via moto +coverage==4.5.3 +cryptography==2.6.1 # via moto docker-pycreds==0.4.0 # via docker -docker==3.7.0 # via moto +docker==3.7.1 # via moto docutils==0.14 # via botocore ecdsa==0.13 # via python-jose factory-boy==2.11.1 -faker==1.0.2 +faker==1.0.4 flask==1.0.2 # via pytest-flask freezegun==0.3.11 future==0.17.1 # via python-jose idna==2.8 # via requests itsdangerous==1.1.0 # via flask 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 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 -more-itertools==5.0.0 # via pytest +more-itertools==6.0.0 # via pytest moto==1.3.7 nose==1.3.7 -pbr==5.1.2 # via mock -pluggy==0.8.1 # via pytest -py==1.7.0 # via pytest +pbr==5.1.3 # via mock +pluggy==0.9.0 # via pytest +py==1.8.0 # via pytest pyaml==18.11.0 # via moto pycparser==2.19 # via cffi -pycryptodome==3.7.3 # via python-jose -pyflakes==2.1.0 +pycryptodome==3.8.0 # via python-jose +pyflakes==2.1.1 pytest-flask==0.14.0 -pytest-mock==1.10.1 -pytest==4.2.0 +pytest-mock==1.10.2 +pytest==4.3.1 python-dateutil==2.8.0 # via botocore, faker, freezegun, moto python-jose==2.0.2 # via moto pytz==2018.9 # via moto -pyyaml==3.13 # via pyaml +pyyaml==5.1 requests-mock==1.5.2 requests==2.21.0 # via aws-xray-sdk, docker, moto, requests-mock, responses -responses==0.10.5 # via moto -s3transfer==0.1.13 # 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 +responses==0.10.6 # via moto +s3transfer==0.2.0 # via boto3 +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 urllib3==1.24.1 # via botocore, requests -websocket-client==0.54.0 # via docker -werkzeug==0.14.1 # via flask, moto, pytest-flask +websocket-client==0.56.0 # via docker +werkzeug==0.15.1 # via flask, moto, pytest-flask wrapt==1.11.1 # via aws-xray-sdk -xmltodict==0.11.0 # via moto +xmltodict==0.12.0 # via moto diff --git a/requirements.in b/requirements.in index 0aea4591..9b27f604 100644 --- a/requirements.in +++ b/requirements.in @@ -24,8 +24,10 @@ 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 lockfile marshmallow-sqlalchemy marshmallow @@ -44,3 +46,4 @@ six SQLAlchemy-Utils tabulate xmltodict +pyyaml>=4.2b1 #high severity alert diff --git a/requirements.txt b/requirements.txt index 6abd8927..c0e69fb4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,29 +2,29 @@ # This file is autogenerated by pip-compile # 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==1.0.7 # via flask-migrate -amqp==2.4.1 # via kombu -aniso8601==4.1.0 # via flask-restful -arrow==0.13.0 +alembic==1.0.8 # via flask-migrate +amqp==2.4.2 # via kombu +aniso8601==6.0.0 # via flask-restful +arrow==0.13.1 asn1crypto==0.24.0 # via cryptography asyncpool==1.0 bcrypt==3.1.6 # via flask-bcrypt, paramiko billiard==3.5.0.5 # via celery blinker==1.4 # via flask-mail, flask-principal, raven -boto3==1.9.87 -botocore==1.12.87 -celery[redis]==4.2.1 -certifi==2018.11.29 -certsrv==2.1.0 -cffi==1.11.5 # via bcrypt, cryptography, pynacl +boto3==1.9.120 +botocore==1.12.120 +celery[redis]==4.2.2 +certifi==2019.3.9 +certsrv==2.1.1 +cffi==1.12.2 # via bcrypt, cryptography, pynacl chardet==3.0.4 # via requests click==7.0 # via flask cloudflare==2.1.0 -cryptography==2.5 +cryptography==2.6.1 dnspython3==1.15.0 dnspython==1.15.0 # via dnspython3 docutils==0.14 # via botocore @@ -32,7 +32,7 @@ dyn==1.8.1 flask-bcrypt==0.7.1 flask-cors==3.0.7 flask-mail==0.9.1 -flask-migrate==2.3.1 +flask-migrate==2.4.0 flask-principal==0.4.0 flask-restful==0.3.7 flask-script==2.0.6 @@ -40,24 +40,25 @@ 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 jinja2==2.10 -jmespath==0.9.3 # via boto3, botocore +jmespath==0.9.4 # via boto3, botocore josepy==1.1.0 # via acme jsonlines==1.2.0 # via cloudflare -kombu==4.2.2.post1 # via celery +kombu==4.3.0 lockfile==0.12.2 -mako==1.0.7 # via alembic -markupsafe==1.1.0 # via jinja2, mako -marshmallow-sqlalchemy==0.16.0 -marshmallow==2.18.0 +mako==1.0.8 # via alembic +markupsafe==1.1.1 # via jinja2, mako +marshmallow-sqlalchemy==0.16.1 +marshmallow==2.19.1 mock==2.0.0 # via acme ndg-httpsclient==0.5.1 paramiko==2.4.2 -pbr==5.1.2 # via mock -pem==18.2.0 +pbr==5.1.3 # via mock +pem==19.1.0 psycopg2==2.7.7 pyasn1-modules==0.2.4 # via 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 python-dateutil==2.8.0 # via alembic, arrow, botocore 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 -pyyaml==3.13 # via cloudflare +pyyaml==5.1 raven[flask]==6.10.0 redis==2.10.6 requests-toolbelt==0.9.1 # via acme requests[security]==2.21.0 retrying==1.3.3 -s3transfer==0.1.13 # via boto3 +s3transfer==0.2.0 # via boto3 six==1.12.0 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 urllib3==1.24.1 # via botocore, requests -vine==1.2.0 # via amqp -werkzeug==0.14.1 # via flask -xmltodict==0.11.0 +vine==1.3.0 # via amqp +werkzeug==0.15.1 # via flask +xmltodict==0.12.0 diff --git a/setup.py b/setup.py index 882edb02..148f51b1 100644 --- a/setup.py +++ b/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' ],