From f02178c154922ea67c1b5b6cba64dafca7da6c39 Mon Sep 17 00:00:00 2001 From: sirferl Date: Thu, 20 Dec 2018 11:54:47 +0100 Subject: [PATCH 1/9] added ADCS issuer and source plugin --- lemur/plugins/lemur_adcs/__init__.py | 6 ++ lemur/plugins/lemur_adcs/plugin.py | 120 +++++++++++++++++++++++++++ requirements-dev.txt | 1 + requirements-docs.txt | 18 ++-- requirements-tests.txt | 4 +- requirements.in | 3 +- requirements.txt | 7 +- setup.py | 4 +- 8 files changed, 147 insertions(+), 16 deletions(-) create mode 100644 lemur/plugins/lemur_adcs/__init__.py create mode 100644 lemur/plugins/lemur_adcs/plugin.py 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..48a3e85b --- /dev/null +++ b/lemur/plugins/lemur_adcs/plugin.py @@ -0,0 +1,120 @@ +from lemur.plugins.bases import IssuerPlugin, SourcePlugin +import requests +import datetime +import lemur_adcs as ADCS +from certsrv import Certsrv +import ssl +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 Direcory 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: + """ + role = {'username': '', 'password': '', 'name': 'adcs'} + return constants.ADCS_ROOT, constants.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') + 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): + # requests.put('a third party') + raise NotImplementedError('Not implemented\n', self,certificate, comments) + + def get_ordered_certificate(self, order_id): + # requests.get('already existing certificate') + raise NotImplementedError('Not implemented\n',self, order_id) + + def canceled_ordered_certificate(self, pending_cert, **kwargs): + # requests.put('cancel an order that has yet to be issued') + 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 with the current id 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: + 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/requirements-dev.txt b/requirements-dev.txt index 7b427b20..d8c24e4d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -15,6 +15,7 @@ flake8==3.5.0 identify==1.1.7 # via pre-commit idna==2.8 # via requests importlib-metadata==0.7 # 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 diff --git a/requirements-docs.txt b/requirements-docs.txt index 3f036915..80f38e5f 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -14,11 +14,11 @@ arrow==0.12.1 asn1crypto==0.24.0 asyncpool==1.0 babel==2.6.0 # via sphinx -bcrypt==3.1.4 +bcrypt==3.1.5 billiard==3.5.0.5 blinker==1.4 -boto3==1.9.60 -botocore==1.12.60 +boto3==1.9.67 +botocore==1.12.67 celery[redis]==4.2.1 certifi==2018.11.29 cffi==1.11.5 @@ -35,13 +35,13 @@ flask-cors==3.0.7 flask-mail==0.9.1 flask-migrate==2.3.1 flask-principal==0.4.0 -flask-restful==0.3.6 +flask-restful==0.3.7 flask-script==2.0.6 flask-sqlalchemy==2.3.2 flask==1.0.2 future==0.17.1 gunicorn==19.9.0 -idna==2.7 +idna==2.8 imagesize==1.1.0 # via sphinx inflection==0.3.1 itsdangerous==1.1.0 @@ -66,7 +66,7 @@ pyasn1-modules==0.2.2 pyasn1==0.4.4 pycparser==2.19 pygments==2.3.1 # via sphinx -pyjwt==1.7.0 +pyjwt==1.7.1 pynacl==1.3.0 pyopenssl==18.0.0 pyparsing==2.3.0 # via packaging @@ -78,17 +78,17 @@ pyyaml==3.13 raven[flask]==6.9.0 redis==2.10.6 requests-toolbelt==0.8.0 -requests[security]==2.20.1 +requests[security]==2.21.0 retrying==1.3.3 s3transfer==0.1.13 -six==1.11.0 +six==1.12.0 snowballstemmer==1.2.1 # via sphinx sphinx-rtd-theme==0.4.2 sphinx==1.8.2 sphinxcontrib-httpdomain==1.7.0 sphinxcontrib-websupport==1.1.0 # via sphinx sqlalchemy-utils==0.33.9 -sqlalchemy==1.2.14 +sqlalchemy==1.2.15 tabulate==0.8.2 urllib3==1.24.1 vine==1.1.4 diff --git a/requirements-tests.txt b/requirements-tests.txt index 59c626f7..47b83988 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -8,9 +8,9 @@ asn1crypto==0.24.0 # via cryptography atomicwrites==1.2.1 # via pytest attrs==18.2.0 # via pytest aws-xray-sdk==0.95 # via moto -boto3==1.9.67 # via moto +boto3==1.9.69 # via moto boto==2.49.0 # via moto -botocore==1.12.67 # via boto3, moto, s3transfer +botocore==1.12.69 # via boto3, moto, s3transfer certifi==2018.11.29 # via requests cffi==1.11.5 # via cryptography chardet==3.0.4 # via requests 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 7ee9a167..e88bcb90 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,10 +15,11 @@ asyncpool==1.0 bcrypt==3.1.5 # via flask-bcrypt, paramiko billiard==3.5.0.5 # via celery blinker==1.4 # via flask-mail, flask-principal, raven -boto3==1.9.67 -botocore==1.12.67 +boto3==1.9.69 +botocore==1.12.69 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 @@ -70,7 +71,7 @@ python-editor==1.0.3 # via alembic python-ldap==3.1.0 pytz==2018.7 # via acme, celery, flask-restful, pyrfc3339 pyyaml==3.13 # via cloudflare -raven[flask]==6.9.0 +raven[flask]==6.10.0 redis==2.10.6 requests-toolbelt==0.8.0 # via acme requests[security]==2.21.0 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=[ From c62bcd1456bc35198a5895588e6ab042d0213fe5 Mon Sep 17 00:00:00 2001 From: sirferl Date: Mon, 7 Jan 2019 10:02:37 +0100 Subject: [PATCH 2/9] repaired several lint errors --- lemur/plugins/lemur_adcs/plugin.py | 68 ++++++++++++++---------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/lemur/plugins/lemur_adcs/plugin.py b/lemur/plugins/lemur_adcs/plugin.py index 48a3e85b..31dba7b2 100644 --- a/lemur/plugins/lemur_adcs/plugin.py +++ b/lemur/plugins/lemur_adcs/plugin.py @@ -1,12 +1,11 @@ from lemur.plugins.bases import IssuerPlugin, SourcePlugin import requests -import datetime import lemur_adcs as ADCS from certsrv import Certsrv -import ssl from OpenSSL import crypto from flask import current_app + class ADCSIssuerPlugin(IssuerPlugin): title = 'ADCS' slug = 'adcs-issuer' @@ -27,36 +26,37 @@ class ADCSIssuerPlugin(IssuerPlugin): 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 constants.ADCS_ROOT, constants.ADCS_ISSUING, [role] + 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') - ca_server = Certsrv(adcs_server, adcs_user, adcs_pwd, auth_method = 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') + 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): - # requests.put('a third party') - raise NotImplementedError('Not implemented\n', self,certificate, comments) - + raise NotImplementedError('Not implemented\n', self, certificate, comments) + def get_ordered_certificate(self, order_id): - # requests.get('already existing certificate') - raise NotImplementedError('Not implemented\n',self, order_id) - + raise NotImplementedError('Not implemented\n', self, order_id) + def canceled_ordered_certificate(self, pending_cert, **kwargs): - # requests.put('cancel an order that has yet to be issued') - raise NotImplementedError('Not implemented\n',self, pending_cert, **kwargs) + raise NotImplementedError('Not implemented\n', self, pending_cert, **kwargs) + class ADCSSourcePlugin(SourcePlugin): title = 'ADCS' @@ -67,54 +67,50 @@ class ADCSSourcePlugin(SourcePlugin): 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): + + 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) + 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: + 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 + # this error indicates end of certificate list(?), so we stop break else: # We do nothing in case there is no certificate returned with the current id for other reasons current_app.logger.info("Error with id {0}: {1}".format(id, err)) - else: - #we have a certificate + 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): + # 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: + except Exception: extensionn = '' - if extension.find("TLS Web Server Authentication") != -1: - out_certlist.append ( { + if extension.find("TLS Web Server Authentication") != -1: + out_certlist.append({ 'name': format(pubkey.get_subject().CN), - 'body' : cert}) + 'body': cert}) break - return out_certlist - - def get_endpoints(self, options, **kwargs): + def get_endpoints(self, options, **kwargs): # There are no endpoints in the ADCS - raise NotImplementedError('Not implemented\n',self, options, **kwargs) - + raise NotImplementedError('Not implemented\n', self, options, **kwargs) From a43476bc8702f56ac4261beff09fa8ca59a1f42d Mon Sep 17 00:00:00 2001 From: sirferl Date: Mon, 7 Jan 2019 11:04:27 +0100 Subject: [PATCH 3/9] minor errors after lint fix --- lemur/plugins/lemur_adcs/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lemur/plugins/lemur_adcs/plugin.py b/lemur/plugins/lemur_adcs/plugin.py index 31dba7b2..db068eb3 100644 --- a/lemur/plugins/lemur_adcs/plugin.py +++ b/lemur/plugins/lemur_adcs/plugin.py @@ -1,6 +1,6 @@ from lemur.plugins.bases import IssuerPlugin, SourcePlugin import requests -import lemur_adcs as ADCS +from lemur.plugins import lemur_adcs as ADCS from certsrv import Certsrv from OpenSSL import crypto from flask import current_app @@ -9,7 +9,7 @@ from flask import current_app class ADCSIssuerPlugin(IssuerPlugin): title = 'ADCS' slug = 'adcs-issuer' - description = 'Enables the creation of certificates by ADCS (Active Direcory Certificate Services)' + description = 'Enables the creation of certificates by ADCS (Active Directory Certificate Services)' version = ADCS.VERSION author = 'sirferl' From af88ad0f0da9d2dcee00f4455faf2b345594d905 Mon Sep 17 00:00:00 2001 From: sirferl Date: Mon, 7 Jan 2019 11:35:56 +0100 Subject: [PATCH 4/9] changed broken kombu ref. from 4.2.2 to 4.2.1 because travis build fails --- requirements-docs.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 80f38e5f..9c3cef5e 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -49,7 +49,7 @@ jinja2==2.10 jmespath==0.9.3 josepy==1.1.0 jsonlines==1.2.0 -kombu==4.2.2 +kombu==4.2.1 # 4.2.2 was broken sirferl lockfile==0.12.2 mako==1.0.7 markupsafe==1.1.0 diff --git a/requirements.txt b/requirements.txt index e88bcb90..c1658efc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -47,7 +47,7 @@ jinja2==2.10 jmespath==0.9.3 # via boto3, botocore josepy==1.1.0 # via acme jsonlines==1.2.0 # via cloudflare -kombu==4.2.2 # via celery +kombu==4.2.1 # via celery - 4.2.2. was removed sirferl lockfile==0.12.2 mako==1.0.7 # via alembic markupsafe==1.1.0 # via jinja2, mako From a1ca61d81365b6bfb6a26973ff7fe73337cea32c Mon Sep 17 00:00:00 2001 From: sirferl Date: Wed, 9 Jan 2019 09:50:26 +0100 Subject: [PATCH 5/9] changed a too long comment --- lemur/plugins/lemur_adcs/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lemur/plugins/lemur_adcs/plugin.py b/lemur/plugins/lemur_adcs/plugin.py index db068eb3..b7698474 100644 --- a/lemur/plugins/lemur_adcs/plugin.py +++ b/lemur/plugins/lemur_adcs/plugin.py @@ -93,7 +93,7 @@ class ADCSSourcePlugin(SourcePlugin): # this error indicates end of certificate list(?), so we stop break else: - # We do nothing in case there is no certificate returned with the current id for other reasons + # 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 From cb35f19d6ca4b0d84ed2f96e1e28ca482556411d Mon Sep 17 00:00:00 2001 From: Ronald Moesbergen Date: Mon, 21 Jan 2019 10:22:03 +0100 Subject: [PATCH 6/9] Add 'delete_cert' to enum log_type in logs table --- lemur/logs/models.py | 2 +- lemur/migrations/versions/9f79024fe67b_.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 lemur/migrations/versions/9f79024fe67b_.py 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']) From 4c4fbf3e48d3644ac2869d7d5e1688248fb6f597 Mon Sep 17 00:00:00 2001 From: Ronald Moesbergen Date: Mon, 21 Jan 2019 10:25:28 +0100 Subject: [PATCH 7/9] Implement certificates delete API call by marking a cert as 'deleted' in the database. Only certificates that have expired can be deleted. --- lemur/certificates/views.py | 46 ++++++++++++++++++++++++++++++++ lemur/tests/conftest.py | 12 ++++++++- lemur/tests/factories.py | 7 ++++- lemur/tests/test_certificates.py | 19 ++++++++++--- 4 files changed, 78 insertions(+), 6 deletions(-) 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/tests/conftest.py b/lemur/tests/conftest.py index 9a48eb94..3f5fa2d8 100644 --- a/lemur/tests/conftest.py +++ b/lemur/tests/conftest.py @@ -15,7 +15,8 @@ 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 +169,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() 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 a1df1c0d..4d412563 100644 --- a/lemur/tests/test_certificates.py +++ b/lemur/tests/test_certificates.py @@ -647,15 +647,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), From e24a94d798bd69a0110b1e5ddf532192621ca754 Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Wed, 26 Dec 2018 19:49:56 +0200 Subject: [PATCH 8/9] Enforce that PEM strings (certs, keys, CSR) are internally passed as str, not bytes This was already true in most places but not 100%, leading to lots of redundant checks and conversions. --- lemur/certificates/service.py | 10 +--------- lemur/common/utils.py | 17 +++++++---------- lemur/plugins/lemur_aws/iam.py | 3 +-- lemur/plugins/lemur_cryptography/plugin.py | 11 ++++------- lemur/plugins/lemur_csr/plugin.py | 11 +++-------- lemur/plugins/lemur_java/plugin.py | 18 +++++------------- lemur/plugins/lemur_openssl/plugin.py | 11 +++-------- lemur/tests/conftest.py | 7 +++---- 8 files changed, 27 insertions(+), 61 deletions(-) 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/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/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..3790358e 100644 --- a/lemur/tests/conftest.py +++ b/lemur/tests/conftest.py @@ -3,12 +3,11 @@ 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 @@ -235,12 +234,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 From 176f9bfea6f703467676797a9ecaa4da0189e082 Mon Sep 17 00:00:00 2001 From: Curtis Castrapel Date: Tue, 5 Feb 2019 09:37:04 -0800 Subject: [PATCH 9/9] Updating requirements --- requirements-dev.txt | 4 ++-- requirements-docs.txt | 18 +++++++++--------- requirements-tests.txt | 14 +++++++------- requirements.txt | 16 ++++++++-------- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index ac35f3e9..29f39314 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -19,13 +19,13 @@ 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.2 +pre-commit==1.14.3 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 readme-renderer==24.0 # via twine -requests-toolbelt==0.9.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 diff --git a/requirements-docs.txt b/requirements-docs.txt index 15085766..21dc110c 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -8,7 +8,7 @@ acme==0.30.2 alabaster==0.7.12 # via sphinx alembic-autogenerate-enums==0.0.2 alembic==1.0.7 -amqp==2.4.0 +amqp==2.4.1 aniso8601==4.1.0 arrow==0.13.0 asn1crypto==0.24.0 @@ -17,8 +17,8 @@ babel==2.6.0 # via sphinx bcrypt==3.1.6 billiard==3.5.0.5 blinker==1.4 -boto3==1.9.86 -botocore==1.12.86 +boto3==1.9.87 +botocore==1.12.87 celery[redis]==4.2.1 certifi==2018.11.29 cffi==1.11.5 @@ -53,13 +53,13 @@ kombu==4.2.2.post1 lockfile==0.12.2 mako==1.0.7 markupsafe==1.1.0 -marshmallow-sqlalchemy==0.15.0 +marshmallow-sqlalchemy==0.16.0 marshmallow==2.18.0 mock==2.0.0 ndg-httpsclient==0.5.1 packaging==19.0 # via sphinx paramiko==2.4.2 -pbr==5.1.1 +pbr==5.1.2 pem==18.2.0 psycopg2==2.7.7 pyasn1-modules==0.2.4 @@ -71,20 +71,20 @@ pynacl==1.3.0 pyopenssl==19.0.0 pyparsing==2.3.1 # via packaging pyrfc3339==1.1 -python-dateutil==2.7.5 -python-editor==1.0.3 +python-dateutil==2.8.0 +python-editor==1.0.4 pytz==2018.9 pyyaml==3.13 raven[flask]==6.10.0 redis==2.10.6 -requests-toolbelt==0.9.0 +requests-toolbelt==0.9.1 requests[security]==2.21.0 retrying==1.3.3 s3transfer==0.1.13 six==1.12.0 snowballstemmer==1.2.1 # via sphinx sphinx-rtd-theme==0.4.2 -sphinx==1.8.3 +sphinx==1.8.4 sphinxcontrib-httpdomain==1.7.0 sphinxcontrib-websupport==1.1.0 # via sphinx sqlalchemy-utils==0.33.11 diff --git a/requirements-tests.txt b/requirements-tests.txt index c326e951..354f4f1a 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -5,12 +5,12 @@ # pip-compile --no-index --output-file requirements-tests.txt requirements-tests.in # asn1crypto==0.24.0 # via cryptography -atomicwrites==1.2.1 # via pytest +atomicwrites==1.3.0 # via pytest attrs==18.2.0 # via pytest aws-xray-sdk==0.95 # via moto -boto3==1.9.86 # via moto +boto3==1.9.87 # via moto boto==2.49.0 # via moto -botocore==1.12.86 # via boto3, moto, s3transfer +botocore==1.12.87 # via boto3, moto, s3transfer certifi==2018.11.29 # via requests cffi==1.11.5 # via cryptography chardet==3.0.4 # via requests @@ -37,7 +37,7 @@ mock==2.0.0 # via moto more-itertools==5.0.0 # via pytest moto==1.3.7 nose==1.3.7 -pbr==5.1.1 # via mock +pbr==5.1.2 # via mock pluggy==0.8.1 # via pytest py==1.7.0 # via pytest pyaml==18.11.0 # via moto @@ -45,9 +45,9 @@ pycparser==2.19 # via cffi pycryptodome==3.7.3 # via python-jose pyflakes==2.1.0 pytest-flask==0.14.0 -pytest-mock==1.10.0 -pytest==4.1.1 -python-dateutil==2.7.5 # via botocore, faker, freezegun, moto +pytest-mock==1.10.1 +pytest==4.2.0 +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 diff --git a/requirements.txt b/requirements.txt index c595e509..cb08b22d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ acme==0.30.2 alembic-autogenerate-enums==0.0.2 alembic==1.0.7 # via flask-migrate -amqp==2.4.0 # via kombu +amqp==2.4.1 # via kombu aniso8601==4.1.0 # via flask-restful arrow==0.13.0 asn1crypto==0.24.0 # via cryptography @@ -15,8 +15,8 @@ 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.86 -botocore==1.12.86 +boto3==1.9.87 +botocore==1.12.87 celery[redis]==4.2.1 certifi==2018.11.29 cffi==1.11.5 # via bcrypt, cryptography, pynacl @@ -50,12 +50,12 @@ kombu==4.2.2.post1 # via celery lockfile==0.12.2 mako==1.0.7 # via alembic markupsafe==1.1.0 # via jinja2, mako -marshmallow-sqlalchemy==0.15.0 +marshmallow-sqlalchemy==0.16.0 marshmallow==2.18.0 mock==2.0.0 # via acme ndg-httpsclient==0.5.1 paramiko==2.4.2 -pbr==5.1.1 # via mock +pbr==5.1.2 # via mock pem==18.2.0 psycopg2==2.7.7 pyasn1-modules==0.2.4 # via python-ldap @@ -65,14 +65,14 @@ pyjwt==1.7.1 pynacl==1.3.0 # via paramiko pyopenssl==19.0.0 pyrfc3339==1.1 # via acme -python-dateutil==2.7.5 # via alembic, arrow, botocore -python-editor==1.0.3 # via alembic +python-dateutil==2.8.0 # via alembic, arrow, botocore +python-editor==1.0.4 # via alembic python-ldap==3.1.0 pytz==2018.9 # via acme, celery, flask-restful, pyrfc3339 pyyaml==3.13 # via cloudflare raven[flask]==6.10.0 redis==2.10.6 -requests-toolbelt==0.9.0 # via acme +requests-toolbelt==0.9.1 # via acme requests[security]==2.21.0 retrying==1.3.3 s3transfer==0.1.13 # via boto3