diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index 1b203260..0f37d70e 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -221,11 +221,6 @@ def upload(**kwargs): else: kwargs['roles'] = roles - if kwargs.get('private_key'): - private_key = kwargs['private_key'] - if not isinstance(private_key, bytes): - kwargs['private_key'] = private_key.encode('utf-8') - cert = Certificate(**kwargs) cert.authority = kwargs.get('authority') cert = database.create(cert) @@ -432,10 +427,7 @@ def create_csr(**csr_config): encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, # would like to use PKCS8 but AWS ELBs don't like it encryption_algorithm=serialization.NoEncryption() - ) - - if isinstance(private_key, bytes): - private_key = private_key.decode('utf-8') + ).decode('utf-8') csr = request.public_bytes( encoding=serialization.Encoding.PEM diff --git a/lemur/certificates/views.py b/lemur/certificates/views.py index 54c60924..948c44d6 100644 --- a/lemur/certificates/views.py +++ b/lemur/certificates/views.py @@ -6,6 +6,7 @@ .. moduleauthor:: Kevin Glisson """ import base64 +import arrow from builtins import str from flask import Blueprint, make_response, jsonify, g @@ -660,6 +661,51 @@ class Certificates(AuthenticatedResource): log_service.create(g.current_user, 'update_cert', certificate=cert) return cert + def delete(self, certificate_id, data=None): + """ + .. http:delete:: /certificates/1 + + Delete a certificate + + **Example request**: + + .. sourcecode:: http + + DELETE /certificates/1 HTTP/1.1 + Host: example.com + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + :reqheader Authorization: OAuth token to authenticate + :statuscode 204: no error + :statuscode 403: unauthenticated + :statusoode 404: certificate not found + + """ + cert = service.get(certificate_id) + + if not cert: + return dict(message="Cannot find specified certificate"), 404 + + # allow creators + if g.current_user != cert.user: + owner_role = role_service.get_by_name(cert.owner) + permission = CertificatePermission(owner_role, [x.name for x in cert.roles]) + + 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 + class NotificationCertificatesList(AuthenticatedResource): """ Defines the 'certificates' endpoint """ diff --git a/lemur/common/utils.py b/lemur/common/utils.py index 0504c958..32271e89 100644 --- a/lemur/common/utils.py +++ b/lemur/common/utils.py @@ -48,24 +48,22 @@ def parse_certificate(body): :param body: :return: """ - if isinstance(body, str): - body = body.encode('utf-8') + assert isinstance(body, str) - return x509.load_pem_x509_certificate(body, default_backend()) + return x509.load_pem_x509_certificate(body.encode('utf-8'), default_backend()) def parse_private_key(private_key): """ Parses a PEM-format private key (RSA, DSA, ECDSA or any other supported algorithm). - Raises ValueError for an invalid string. + Raises ValueError for an invalid string. Raises AssertionError when passed value is not str-type. :param private_key: String containing PEM private key """ - if isinstance(private_key, str): - private_key = private_key.encode('utf8') + assert isinstance(private_key, str) - return load_pem_private_key(private_key, password=None, backend=default_backend()) + return load_pem_private_key(private_key.encode('utf8'), password=None, backend=default_backend()) def parse_csr(csr): @@ -75,10 +73,9 @@ def parse_csr(csr): :param csr: :return: """ - if isinstance(csr, str): - csr = csr.encode('utf-8') + assert isinstance(csr, str) - return x509.load_pem_x509_csr(csr, default_backend()) + return x509.load_pem_x509_csr(csr.encode('utf-8'), default_backend()) def get_authority_key(body): diff --git a/lemur/logs/models.py b/lemur/logs/models.py index d4239e59..9f982c24 100644 --- a/lemur/logs/models.py +++ b/lemur/logs/models.py @@ -18,6 +18,6 @@ class Log(db.Model): __tablename__ = 'logs' id = Column(Integer, primary_key=True) certificate_id = Column(Integer, ForeignKey('certificates.id')) - log_type = Column(Enum('key_view', 'create_cert', 'update_cert', 'revoke_cert', name='log_type'), nullable=False) + log_type = Column(Enum('key_view', 'create_cert', 'update_cert', 'revoke_cert', 'delete_cert', name='log_type'), nullable=False) logged_at = Column(ArrowType(), PassiveDefault(func.now()), nullable=False) user_id = Column(Integer, ForeignKey('users.id'), nullable=False) diff --git a/lemur/migrations/versions/9f79024fe67b_.py b/lemur/migrations/versions/9f79024fe67b_.py new file mode 100644 index 00000000..ad22d5f3 --- /dev/null +++ b/lemur/migrations/versions/9f79024fe67b_.py @@ -0,0 +1,22 @@ +""" Add delete_cert to log_type enum + +Revision ID: 9f79024fe67b +Revises: ee827d1e1974 +Create Date: 2019-01-03 15:36:59.181911 + +""" + +# revision identifiers, used by Alembic. +revision = '9f79024fe67b' +down_revision = 'ee827d1e1974' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.sync_enum_values('public', 'log_type', ['create_cert', 'key_view', 'revoke_cert', 'update_cert'], ['create_cert', 'delete_cert', 'key_view', 'revoke_cert', 'update_cert']) + + +def downgrade(): + op.sync_enum_values('public', 'log_type', ['create_cert', 'delete_cert', 'key_view', 'revoke_cert', 'update_cert'], ['create_cert', 'key_view', 'revoke_cert', 'update_cert']) diff --git a/lemur/plugins/lemur_adcs/__init__.py b/lemur/plugins/lemur_adcs/__init__.py new file mode 100644 index 00000000..6b61e936 --- /dev/null +++ b/lemur/plugins/lemur_adcs/__init__.py @@ -0,0 +1,6 @@ +"""Set the version information.""" +try: + VERSION = __import__('pkg_resources') \ + .get_distribution(__name__).version +except Exception as e: + VERSION = 'unknown' diff --git a/lemur/plugins/lemur_adcs/plugin.py b/lemur/plugins/lemur_adcs/plugin.py new file mode 100644 index 00000000..b7698474 --- /dev/null +++ b/lemur/plugins/lemur_adcs/plugin.py @@ -0,0 +1,116 @@ +from lemur.plugins.bases import IssuerPlugin, SourcePlugin +import requests +from lemur.plugins import lemur_adcs as ADCS +from certsrv import Certsrv +from OpenSSL import crypto +from flask import current_app + + +class ADCSIssuerPlugin(IssuerPlugin): + title = 'ADCS' + slug = 'adcs-issuer' + description = 'Enables the creation of certificates by ADCS (Active Directory Certificate Services)' + version = ADCS.VERSION + + author = 'sirferl' + author_url = 'https://github.com/sirferl/lemur' + + def __init__(self, *args, **kwargs): + """Initialize the issuer with the appropriate details.""" + self.session = requests.Session() + super(ADCSIssuerPlugin, self).__init__(*args, **kwargs) + + @staticmethod + def create_authority(options): + """Create an authority. + Creates an authority, this authority is then used by Lemur to + allow a user to specify which Certificate Authority they want + to sign their certificate. + + :param options: + :return: + """ + adcs_root = current_app.config.get('ADCS_ROOT') + adcs_issuing = current_app.config.get('ADCS_ISSUING') + role = {'username': '', 'password': '', 'name': 'adcs'} + return adcs_root, adcs_issuing, [role] + + def create_certificate(self, csr, issuer_options): + adcs_server = current_app.config.get('ADCS_SERVER') + adcs_user = current_app.config.get('ADCS_USER') + adcs_pwd = current_app.config.get('ADCS_PWD') + adcs_auth_method = current_app.config.get('ADCS_AUTH_METHOD') + adcs_template = current_app.config.get('ADCS_TEMPLATE') + ca_server = Certsrv(adcs_server, adcs_user, adcs_pwd, auth_method=adcs_auth_method) + current_app.logger.info("Requesting CSR: {0}".format(csr)) + current_app.logger.info("Issuer options: {0}".format(issuer_options)) + cert, req_id = ca_server.get_cert(csr, adcs_template, encoding='b64').decode('utf-8').replace('\r\n', '\n') + chain = ca_server.get_ca_cert(encoding='b64').decode('utf-8').replace('\r\n', '\n') + return cert, chain, req_id + + def revoke_certificate(self, certificate, comments): + raise NotImplementedError('Not implemented\n', self, certificate, comments) + + def get_ordered_certificate(self, order_id): + raise NotImplementedError('Not implemented\n', self, order_id) + + def canceled_ordered_certificate(self, pending_cert, **kwargs): + raise NotImplementedError('Not implemented\n', self, pending_cert, **kwargs) + + +class ADCSSourcePlugin(SourcePlugin): + title = 'ADCS' + slug = 'adcs-source' + description = 'Enables the collecion of certificates' + version = ADCS.VERSION + + author = 'sirferl' + author_url = 'https://github.com/sirferl/lemur' + options = [ + { + 'name': 'dummy', + 'type': 'str', + 'required': False, + 'validation': '/^[0-9]{12,12}$/', + 'helpMessage': 'Just to prevent error' + } + ] + + def get_certificates(self, options, **kwargs): + adcs_server = current_app.config.get('ADCS_SERVER') + adcs_user = current_app.config.get('ADCS_USER') + adcs_pwd = current_app.config.get('ADCS_PWD') + adcs_auth_method = current_app.config.get('ADCS_AUTH_METHOD') + adcs_start = current_app.config.get('ADCS_START') + adcs_stop = current_app.config.get('ADCS_STOP') + ca_server = Certsrv(adcs_server, adcs_user, adcs_pwd, auth_method=adcs_auth_method) + out_certlist = [] + for id in range(adcs_start, adcs_stop): + try: + cert = ca_server.get_existing_cert(id, encoding='b64').decode('utf-8').replace('\r\n', '\n') + except Exception as err: + if '{0}'.format(err).find("CERTSRV_E_PROPERTY_EMPTY"): + # this error indicates end of certificate list(?), so we stop + break + else: + # We do nothing in case there is no certificate returned for other reasons + current_app.logger.info("Error with id {0}: {1}".format(id, err)) + else: + # we have a certificate + pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, cert) + # loop through extensions to see if we find "TLS Web Server Authentication" + for e_id in range(0, pubkey.get_extension_count() - 1): + try: + extension = '{0}'.format(pubkey.get_extension(e_id)) + except Exception: + extensionn = '' + if extension.find("TLS Web Server Authentication") != -1: + out_certlist.append({ + 'name': format(pubkey.get_subject().CN), + 'body': cert}) + break + return out_certlist + + def get_endpoints(self, options, **kwargs): + # There are no endpoints in the ADCS + raise NotImplementedError('Not implemented\n', self, options, **kwargs) diff --git a/lemur/plugins/lemur_aws/iam.py b/lemur/plugins/lemur_aws/iam.py index 7010c909..49816c2b 100644 --- a/lemur/plugins/lemur_aws/iam.py +++ b/lemur/plugins/lemur_aws/iam.py @@ -64,6 +64,7 @@ def upload_cert(name, body, private_key, path, cert_chain=None, **kwargs): :param path: :return: """ + assert isinstance(private_key, str) client = kwargs.pop('client') if not path or path == '/': @@ -72,8 +73,6 @@ def upload_cert(name, body, private_key, path, cert_chain=None, **kwargs): name = name + '-' + path.strip('/') try: - if isinstance(private_key, bytes): - private_key = private_key.decode("utf-8") if cert_chain: return client.upload_server_certificate( Path=path, diff --git a/lemur/plugins/lemur_cryptography/plugin.py b/lemur/plugins/lemur_cryptography/plugin.py index fe9d7bb3..97060391 100644 --- a/lemur/plugins/lemur_cryptography/plugin.py +++ b/lemur/plugins/lemur_cryptography/plugin.py @@ -14,6 +14,7 @@ from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization +from lemur.common.utils import parse_private_key from lemur.plugins.bases import IssuerPlugin from lemur.plugins import lemur_cryptography as cryptography_issuer @@ -40,7 +41,8 @@ def issue_certificate(csr, options, private_key=None): if options.get("authority"): # Issue certificate signed by an existing lemur_certificates authority issuer_subject = options['authority'].authority_certificate.subject - issuer_private_key = options['authority'].authority_certificate.private_key + assert private_key is None, "Private would be ignored, authority key used instead" + private_key = options['authority'].authority_certificate.private_key chain_cert_pem = options['authority'].authority_certificate.body authority_key_identifier_public = options['authority'].authority_certificate.public_key authority_key_identifier_subject = x509.SubjectKeyIdentifier.from_public_key(authority_key_identifier_public) @@ -52,7 +54,6 @@ def issue_certificate(csr, options, private_key=None): else: # Issue certificate that is self-signed (new lemur_certificates root authority) issuer_subject = csr.subject - issuer_private_key = private_key chain_cert_pem = "" authority_key_identifier_public = csr.public_key() authority_key_identifier_subject = None @@ -112,11 +113,7 @@ def issue_certificate(csr, options, private_key=None): # FIXME: Not implemented in lemur/schemas.py yet https://github.com/Netflix/lemur/issues/662 pass - private_key = serialization.load_pem_private_key( - bytes(str(issuer_private_key).encode('utf-8')), - password=None, - backend=default_backend() - ) + private_key = parse_private_key(private_key) cert = builder.sign(private_key, hashes.SHA256(), default_backend()) cert_pem = cert.public_bytes( diff --git a/lemur/plugins/lemur_csr/plugin.py b/lemur/plugins/lemur_csr/plugin.py index e06035d1..13f42084 100644 --- a/lemur/plugins/lemur_csr/plugin.py +++ b/lemur/plugins/lemur_csr/plugin.py @@ -38,14 +38,9 @@ def create_csr(cert, chain, csr_tmp, key): :param csr_tmp: :param key: """ - if isinstance(cert, bytes): - cert = cert.decode('utf-8') - - if isinstance(chain, bytes): - chain = chain.decode('utf-8') - - if isinstance(key, bytes): - key = key.decode('utf-8') + assert isinstance(cert, str) + assert isinstance(chain, str) + assert isinstance(key, str) with mktempfile() as key_tmp: with open(key_tmp, 'w') as f: diff --git a/lemur/plugins/lemur_java/plugin.py b/lemur/plugins/lemur_java/plugin.py index 151794da..5aab5342 100644 --- a/lemur/plugins/lemur_java/plugin.py +++ b/lemur/plugins/lemur_java/plugin.py @@ -59,11 +59,8 @@ def split_chain(chain): def create_truststore(cert, chain, jks_tmp, alias, passphrase): - if isinstance(cert, bytes): - cert = cert.decode('utf-8') - - if isinstance(chain, bytes): - chain = chain.decode('utf-8') + assert isinstance(cert, str) + assert isinstance(chain, str) with mktempfile() as cert_tmp: with open(cert_tmp, 'w') as f: @@ -98,14 +95,9 @@ def create_truststore(cert, chain, jks_tmp, alias, passphrase): def create_keystore(cert, chain, jks_tmp, key, alias, passphrase): - if isinstance(cert, bytes): - cert = cert.decode('utf-8') - - if isinstance(chain, bytes): - chain = chain.decode('utf-8') - - if isinstance(key, bytes): - key = key.decode('utf-8') + assert isinstance(cert, str) + assert isinstance(chain, str) + assert isinstance(key, str) # Create PKCS12 keystore from private key and public certificate with mktempfile() as cert_tmp: diff --git a/lemur/plugins/lemur_openssl/plugin.py b/lemur/plugins/lemur_openssl/plugin.py index d50b4e43..9ddce925 100644 --- a/lemur/plugins/lemur_openssl/plugin.py +++ b/lemur/plugins/lemur_openssl/plugin.py @@ -44,14 +44,9 @@ def create_pkcs12(cert, chain, p12_tmp, key, alias, passphrase): :param alias: :param passphrase: """ - if isinstance(cert, bytes): - cert = cert.decode('utf-8') - - if isinstance(chain, bytes): - chain = chain.decode('utf-8') - - if isinstance(key, bytes): - key = key.decode('utf-8') + assert isinstance(cert, str) + assert isinstance(chain, str) + assert isinstance(key, str) with mktempfile() as key_tmp: with open(key_tmp, 'w') as f: diff --git a/lemur/tests/conftest.py b/lemur/tests/conftest.py index 9a48eb94..32733e51 100644 --- a/lemur/tests/conftest.py +++ b/lemur/tests/conftest.py @@ -3,19 +3,19 @@ import os import datetime import pytest from cryptography import x509 -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.serialization import load_pem_private_key from flask import current_app from flask_principal import identity_changed, Identity from lemur import create_app +from lemur.common.utils import parse_private_key from lemur.database import db as _db from lemur.auth.service import create_token from lemur.tests.vectors import SAN_CERT_KEY, INTERMEDIATE_KEY from .factories import ApiKeyFactory, AuthorityFactory, NotificationFactory, DestinationFactory, \ CertificateFactory, UserFactory, RoleFactory, SourceFactory, EndpointFactory, \ - RotationPolicyFactory, PendingCertificateFactory, AsyncAuthorityFactory, CryptoAuthorityFactory + RotationPolicyFactory, PendingCertificateFactory, AsyncAuthorityFactory, InvalidCertificateFactory, \ + CryptoAuthorityFactory def pytest_runtest_setup(item): @@ -168,6 +168,15 @@ def pending_certificate(session): return p +@pytest.fixture +def invalid_certificate(session): + u = UserFactory() + a = AsyncAuthorityFactory() + i = InvalidCertificateFactory(user=u, authority=a) + session.commit() + return i + + @pytest.fixture def admin_user(session): u = UserFactory() @@ -235,12 +244,12 @@ def logged_in_admin(session, app): @pytest.fixture def private_key(): - return load_pem_private_key(SAN_CERT_KEY.encode(), password=None, backend=default_backend()) + return parse_private_key(SAN_CERT_KEY) @pytest.fixture def issuer_private_key(): - return load_pem_private_key(INTERMEDIATE_KEY.encode(), password=None, backend=default_backend()) + return parse_private_key(INTERMEDIATE_KEY) @pytest.fixture diff --git a/lemur/tests/factories.py b/lemur/tests/factories.py index 3717c64d..a4af3d43 100644 --- a/lemur/tests/factories.py +++ b/lemur/tests/factories.py @@ -20,7 +20,7 @@ from lemur.policies.models import RotationPolicy from lemur.api_keys.models import ApiKey from .vectors import SAN_CERT_STR, SAN_CERT_KEY, CSR_STR, INTERMEDIATE_CERT_STR, ROOTCA_CERT_STR, INTERMEDIATE_KEY, \ - WILDCARD_CERT_KEY + WILDCARD_CERT_KEY, INVALID_CERT_STR class BaseFactory(SQLAlchemyModelFactory): @@ -137,6 +137,11 @@ class CACertificateFactory(CertificateFactory): private_key = INTERMEDIATE_KEY +class InvalidCertificateFactory(CertificateFactory): + body = INVALID_CERT_STR + private_key = '' + + class AuthorityFactory(BaseFactory): """Authority factory.""" name = Sequence(lambda n: 'authority{0}'.format(n)) diff --git a/lemur/tests/test_certificates.py b/lemur/tests/test_certificates.py index db2d27cf..3ee7f84e 100644 --- a/lemur/tests/test_certificates.py +++ b/lemur/tests/test_certificates.py @@ -653,15 +653,26 @@ def test_certificate_put_with_data(client, certificate, issuer_plugin): @pytest.mark.parametrize("token,status", [ - (VALID_USER_HEADER_TOKEN, 405), - (VALID_ADMIN_HEADER_TOKEN, 405), - (VALID_ADMIN_API_TOKEN, 405), - ('', 405) + (VALID_USER_HEADER_TOKEN, 403), + (VALID_ADMIN_HEADER_TOKEN, 412), + (VALID_ADMIN_API_TOKEN, 412), + ('', 401) ]) def test_certificate_delete(client, token, status): assert client.delete(api.url_for(Certificates, certificate_id=1), headers=token).status_code == status +@pytest.mark.parametrize("token,status", [ + (VALID_USER_HEADER_TOKEN, 403), + (VALID_ADMIN_HEADER_TOKEN, 204), + (VALID_ADMIN_API_TOKEN, 204), + ('', 401) +]) +def test_invalid_certificate_delete(client, invalid_certificate, token, status): + assert client.delete( + api.url_for(Certificates, certificate_id=invalid_certificate.id), headers=token).status_code == status + + @pytest.mark.parametrize("token,status", [ (VALID_USER_HEADER_TOKEN, 405), (VALID_ADMIN_HEADER_TOKEN, 405), diff --git a/requirements-docs.txt b/requirements-docs.txt index 194708ed..e68bfc5e 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -17,6 +17,7 @@ babel==2.6.0 # via sphinx bcrypt==3.1.6 billiard==3.5.0.5 blinker==1.4 + boto3==1.9.89 botocore==1.12.89 celery[redis]==4.2.1 diff --git a/requirements.in b/requirements.in index 9824650b..0aea4591 100644 --- a/requirements.in +++ b/requirements.in @@ -8,6 +8,7 @@ boto3 botocore celery[redis] certifi +certsrv CloudFlare cryptography dnspython3 @@ -42,4 +43,4 @@ retrying six SQLAlchemy-Utils tabulate -xmltodict \ No newline at end of file +xmltodict diff --git a/requirements.txt b/requirements.txt index db661030..f391d016 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,7 @@ boto3==1.9.89 botocore==1.12.89 celery[redis]==4.2.1 certifi==2018.11.29 +certsrv==2.1.0 cffi==1.11.5 # via bcrypt, cryptography, pynacl chardet==3.0.4 # via requests click==7.0 # via flask diff --git a/setup.py b/setup.py index 1511b013..882edb02 100644 --- a/setup.py +++ b/setup.py @@ -154,7 +154,9 @@ setup( 'digicert_cis_issuer = lemur.plugins.lemur_digicert.plugin:DigiCertCISIssuerPlugin', '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' + 'sftp_destination = lemur.plugins.lemur_sftp.plugin:SFTPDestinationPlugin', + 'adcs_issuer = lemur.plugins.lemur_adcs.plugin:ADCSIssuerPlugin', + 'adcs_source = lemur.plugins.lemur_adcs.plugin:ADCSSourcePlugin' ], }, classifiers=[