diff --git a/docs/administration.rst b/docs/administration.rst index 2f71c0bf..a3225fc2 100644 --- a/docs/administration.rst +++ b/docs/administration.rst @@ -653,13 +653,20 @@ Active Directory Certificate Services Plugin :noindex: Template to be used for certificate issuing. Usually display name w/o spaces + +.. data:: ADCS_TEMPLATE_ + :noindex: + If there is a config variable ADCS_TEMPLATE_ take the value as Cert template else default to ADCS_TEMPLATE to be compatible with former versions. Template to be used for certificate issuing. Usually display name w/o spaces .. data:: ADCS_START :noindex: + Used in ADCS-Sourceplugin. Minimum id of the first certificate to be returned. ID is increased by one until ADCS_STOP. Missing cert-IDs are ignored .. data:: ADCS_STOP :noindex: + Used for ADCS-Sourceplugin. Maximum id of the certificates returned. + .. data:: ADCS_ISSUING :noindex: @@ -672,6 +679,68 @@ Active Directory Certificate Services Plugin Contains the root cert of the CA +Entrust Plugin +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Enables the creation of Entrust certificates. You need to set the API access up with Entrust support. Check the information in the Entrust Portal as well. +Certificates are created as "SERVER_AND_CLIENT_AUTH". +Caution: Sometimes the entrust API does not respond in a timely manner. This error is handled and reported by the plugin. Should this happen you just have to hit the create button again after to create a valid certificate. +The following parameters have to be set in the configuration files. + +.. data:: ENTRUST_URL + :noindex: + + This is the url for the Entrust API. Refer to the API documentation. + +.. data:: ENTRUST_API_CERT + :noindex: + + Path to the certificate file in PEM format. This certificate is created in the onboarding process. Refer to the API documentation. + +.. data:: ENTRUST_API_KEY + :noindex: + + Path to the key file in RSA format. This certificate is created in the onboarding process. Refer to the API documentation. Caution: the request library cannot handle encrypted keys. The keyfile therefore has to contain the unencrypted key. Please put this in a secure location on the server. + +.. data:: ENTRUST_API_USER + :noindex: + + String with the API user. This user is created in the onboarding process. Refer to the API documentation. + +.. data:: ENTRUST_API_PASS + :noindex: + + String with the password for the API user. This password is created in the onboarding process. Refer to the API documentation. + +.. data:: ENTRUST_NAME + :noindex: + + String with the name that should appear as certificate owner in the Entrust portal. Refer to the API documentation. + +.. data:: ENTRUST_EMAIL + :noindex: + + String with the email address that should appear as certificate contact email in the Entrust portal. Refer to the API documentation. + +.. data:: ENTRUST_PHONE + :noindex: + + String with the phone number that should appear as certificate contact in the Entrust portal. Refer to the API documentation. + +.. data:: ENTRUST_ISSUING + :noindex: + + Contains the issuing cert of the CA + +.. data:: ENTRUST_ROOT + :noindex: + + Contains the root cert of the CA + +.. data:: ENTRUST_PRODUCT_ + :noindex: + + If there is a config variable ENTRUST_PRODUCT_ take the value as cert product name else default to "STANDARD_SSL". Refer to the API documentation for valid products names. Verisign Issuer Plugin ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/lemur/plugins/lemur_entrust/__init__.py b/lemur/plugins/lemur_entrust/__init__.py new file mode 100644 index 00000000..b902ed7a --- /dev/null +++ b/lemur/plugins/lemur_entrust/__init__.py @@ -0,0 +1,5 @@ +"""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_entrust/plugin.py b/lemur/plugins/lemur_entrust/plugin.py new file mode 100644 index 00000000..315da8bd --- /dev/null +++ b/lemur/plugins/lemur_entrust/plugin.py @@ -0,0 +1,228 @@ +from lemur.plugins.bases import IssuerPlugin, SourcePlugin +import arrow +import requests +import json +from lemur.plugins import lemur_entrust as ENTRUST +from flask import current_app +from lemur.extensions import metrics +from lemur.common.utils import validate_conf + + +def log_status_code(r, *args, **kwargs): + """ + Is a request hook that logs all status codes to the ENTRUST api. + + :param r: + :param args: + :param kwargs: + :return: + """ + metrics.send("ENTRUST_status_code_{}".format(r.status_code), "counter", 1) + + +def determine_end_date(end_date): + """ + Determine appropriate end date + :param end_date: + :return: validity_end + """ + # ENTRUST only allows 13 months of max certificate duration + max_validity_end = arrow.utcnow().shift(years=1, months=+1).format('YYYY-MM-DD') + + if not end_date: + end_date = max_validity_end + + if end_date > max_validity_end: + end_date = max_validity_end + return end_date + + +def process_options(options): + """ + Processes and maps the incoming issuer options to fields/options that + Entrust understands + + :param options: + :return: dict of valid entrust options + """ + # if there is a config variable ENTRUST_PRODUCT_ + # take the value as Cert product-type + # else default to "STANDARD_SSL" + authority = options.get("authority").name.upper() + product_type = current_app.config.get("ENTRUST_PRODUCT_{0}".format(authority), "STANDARD_SSL") + + if options.get("validity_end"): + validity_end = determine_end_date(options.get("validity_end")) + else: + validity_end = determine_end_date(False) + + tracking_data = { + "requesterName": current_app.config.get("ENTRUST_NAME"), + "requesterEmail": current_app.config.get("ENTRUST_EMAIL"), + "requesterPhone": current_app.config.get("ENTRUST_PHONE") + } + + data = { + "signingAlg": "SHA-2", + "eku": "SERVER_AND_CLIENT_AUTH", + "certType": product_type, + "certExpiryDate": validity_end, + "tracking": tracking_data + } + return data + + +def handle_response(my_response): + """ + Helper function for parsing responses from the Entrust API. + :param content: + :return: :raise Exception: + """ + msg = { + 200: "The request had the validateOnly flag set to true and validation was successful.", + 201: "Certificate created", + 202: "Request accepted and queued for approval", + 400: "Invalid request parameters", + 404: "Unknown jobId", + 429: "Too many requests" + } + try: + d = json.loads(my_response.content) + except Exception as e: + # catch an empty jason object here + d = {'errors': 'No detailled message'} + s = my_response.status_code + if s > 399: + raise Exception("ENTRUST error: {0}\n{1}".format(msg.get(s, s), d['errors'])) + current_app.logger.info("Response: {0}, {1} ".format(s, d)) + return d + + +class EntrustIssuerPlugin(IssuerPlugin): + title = "ENTRUST" + slug = "entrust-issuer" + description = "Enables the creation of certificates by ENTRUST" + version = ENTRUST.VERSION + + author = "sirferl" + author_url = "https://github.com/sirferl/lemur" + + def __init__(self, *args, **kwargs): + """Initialize the issuer with the appropriate details.""" + required_vars = [ + "ENTRUST_API_CERT", + "ENTRUST_API_KEY", + "ENTRUST_API_USER", + "ENTRUST_API_PASS", + "ENTRUST_URL", + "ENTRUST_ROOT", + "ENTRUST_NAME", + "ENTRUST_EMAIL", + "ENTRUST_PHONE", + "ENTRUST_ISSUING", + ] + validate_conf(current_app, required_vars) + + self.session = requests.Session() + cert_file = current_app.config.get("ENTRUST_API_CERT") + key_file = current_app.config.get("ENTRUST_API_KEY") + user = current_app.config.get("ENTRUST_API_USER") + password = current_app.config.get("ENTRUST_API_PASS") + self.session.cert = (cert_file, key_file) + self.session.auth = (user, password) + self.session.hooks = dict(response=log_status_code) + # self.session.config['keep_alive'] = False + super(EntrustIssuerPlugin, self).__init__(*args, **kwargs) + + def create_certificate(self, csr, issuer_options): + """ + Creates an Entrust certificate. + + :param csr: + :param issuer_options: + :return: :raise Exception: + """ + current_app.logger.info( + "Requesting options: {0}".format(issuer_options) + ) + + url = current_app.config.get("ENTRUST_URL") + "/certificates" + + data = process_options(issuer_options) + data["csr"] = csr + + try: + response = self.session.post(url, json=data, timeout=(15, 40)) + except requests.exceptions.Timeout: + raise Exception("Timeout for POST") + except requests.exceptions.RequestException as e: + raise Exception("Error for POST {0}".format(e)) + + response_dict = handle_response(response) + external_id = response_dict['trackingId'] + cert = response_dict['endEntityCert'] + chain = response_dict['chainCerts'][1] + current_app.logger.info( + "Received Chain: {0}".format(chain) + ) + + return cert, chain, external_id + + def revoke_certificate(self, certificate, comments): + """Revoke a Digicert certificate.""" + base_url = current_app.config.get("ENTRUST_URL") + + # make certificate revoke request + revoke_url = "{0}/certificates/{1}/revocations".format( + base_url, certificate.external_id + ) + metrics.send("entrust_revoke_certificate", "counter", 1) + if comments == '' or not comments: + comments = "revoked via API" + data = { + "crlReason": "superseded", + "revocationComment": comments + } + response = self.session.post(revoke_url, json=data) + + data = handle_response(response) + + @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: + """ + entrust_root = current_app.config.get("ENTRUST_ROOT") + entrust_issuing = current_app.config.get("ENTRUST_ISSUING") + role = {"username": "", "password": "", "name": "entrust"} + current_app.logger.info("Creating Auth: {0} {1}".format(options, entrust_issuing)) + return entrust_root, "", [role] + + 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 EntrustSourcePlugin(SourcePlugin): + title = "ENTRUST" + slug = "entrust-source" + description = "Enables the collecion of certificates" + version = ENTRUST.VERSION + + author = "sirferl" + author_url = "https://github.com/sirferl/lemur" + + def get_certificates(self, options, **kwargs): + # Not needed for ENTRUST + raise NotImplementedError("Not implemented\n", self, options, **kwargs) + + def get_endpoints(self, options, **kwargs): + # There are no endpoints in ENTRUST + raise NotImplementedError("Not implemented\n", self, options, **kwargs) diff --git a/requirements-docs.txt b/requirements-docs.txt index 37d50804..3ee96dd7 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -4,7 +4,7 @@ # # pip-compile --no-index --output-file=requirements-docs.txt requirements-docs.in # -acme==1.7.0 # via -r requirements.txt +acme==1.8.0 # via -r requirements.txt alabaster==0.7.12 # via sphinx alembic-autogenerate-enums==0.0.2 # via -r requirements.txt alembic==1.4.2 # via -r requirements.txt, flask-migrate diff --git a/requirements-tests.txt b/requirements-tests.txt index e9106767..bb25b5e5 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -18,14 +18,14 @@ cffi==1.14.0 # via cryptography cfn-lint==0.29.5 # via moto chardet==3.0.4 # via requests click==7.1.2 # via black, flask -coverage==5.2.1 # via -r requirements-tests.in -cryptography==3.1 # via moto, sshpubkeys +coverage==5.3 # via -r requirements-tests.in +cryptography==3.1 # via moto, python-jose, sshpubkeys decorator==4.4.2 # via networkx docker==4.2.0 # via moto docutils==0.15.2 # via botocore -ecdsa==0.15 # via python-jose, sshpubkeys +ecdsa==0.14.1 # via moto, python-jose, sshpubkeys factory-boy==3.0.1 # via -r requirements-tests.in -faker==4.1.2 # via -r requirements-tests.in, factory-boy +faker==4.1.3 # via -r requirements-tests.in, factory-boy fakeredis==1.4.3 # via -r requirements-tests.in flask==1.1.2 # via pytest-flask freezegun==1.0.0 # via -r requirements-tests.in @@ -43,10 +43,10 @@ jsonpatch==1.25 # via cfn-lint jsonpickle==1.4 # via aws-xray-sdk jsonpointer==2.0 # via jsonpatch jsonschema==3.2.0 # via aws-sam-translator, cfn-lint -markupsafe==1.1.1 # via jinja2 +markupsafe==1.1.1 # via jinja2, moto mock==4.0.2 # via moto -more-itertools==8.2.0 # via pytest -moto==1.3.14 # via -r requirements-tests.in +more-itertools==8.2.0 # via moto, pytest +moto==1.3.16 # via -r requirements-tests.in mypy-extensions==0.4.3 # via black networkx==2.4 # via cfn-lint nose==1.3.7 # via -r requirements-tests.in @@ -62,9 +62,9 @@ pyparsing==2.4.7 # via packaging pyrsistent==0.16.0 # via jsonschema pytest-flask==1.0.0 # via -r requirements-tests.in pytest-mock==3.3.1 # via -r requirements-tests.in -pytest==6.0.1 # via -r requirements-tests.in, pytest-flask, pytest-mock +pytest==6.0.2 # via -r requirements-tests.in, pytest-flask, pytest-mock python-dateutil==2.8.1 # via botocore, faker, freezegun, moto -python-jose==3.1.0 # via moto +python-jose[cryptography]==3.1.0 # via moto pytz==2019.3 # via moto pyyaml==5.3.1 # via -r requirements-tests.in, bandit, cfn-lint, moto redis==3.5.3 # via fakeredis @@ -88,7 +88,7 @@ websocket-client==0.57.0 # via docker werkzeug==1.0.1 # via flask, moto, pytest-flask wrapt==1.12.1 # via aws-xray-sdk xmltodict==0.12.0 # via moto -zipp==3.1.0 # via importlib-metadata +zipp==3.1.0 # via importlib-metadata, moto # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/requirements.txt b/requirements.txt index 64e41b3c..fcb06cd9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # # pip-compile --no-index --output-file=requirements.txt requirements.in # -acme==1.7.0 # via -r requirements.in +acme==1.8.0 # via -r requirements.in alembic-autogenerate-enums==0.0.2 # via -r requirements.in alembic==1.4.2 # via flask-migrate amqp==2.5.2 # via kombu diff --git a/setup.py b/setup.py index a612cd18..4da14c3d 100644 --- a/setup.py +++ b/setup.py @@ -153,7 +153,9 @@ setup( 'vault_source = lemur.plugins.lemur_vault_dest.plugin:VaultSourcePlugin', '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' + 'adcs_source = lemur.plugins.lemur_adcs.plugin:ADCSSourcePlugin', + 'entrust_issuer = lemur.plugins.lemur_entrust.plugin:EntrustIssuerPlugin', + 'entrust_source = lemur.plugins.lemur_entrust.plugin:EntrustSourcePlugin' ], }, classifiers=[