lemur/lemur/plugins/lemur_entrust/plugin.py

415 lines
15 KiB
Python
Raw Normal View History

2020-09-10 16:03:29 +02:00
import arrow
import requests
import json
2020-09-18 20:09:32 +02:00
import sys
2020-09-10 16:03:29 +02:00
from flask import current_app
2020-10-24 03:03:07 +02:00
from retrying import retry
2020-09-18 20:09:32 +02:00
2020-12-01 05:06:37 +01:00
from lemur.constants import CRLReason
2020-09-18 20:09:32 +02:00
from lemur.plugins import lemur_entrust as entrust
from lemur.plugins.bases import IssuerPlugin, SourcePlugin
2020-09-11 12:24:33 +02:00
from lemur.extensions import metrics
2021-01-16 01:49:14 +01:00
from lemur.common.utils import validate_conf, get_key_type_from_certificate
2020-09-10 16:03:29 +02:00
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:
"""
if r.status_code != 200:
log_data = {
"reason": (r.reason if r.reason else ""),
"status_code": r.status_code,
"url": (r.url if r.url else ""),
}
metrics.send(f"entrust_status_code_{r.status_code}", "counter", 1)
current_app.logger.info(log_data)
2020-09-10 16:03:29 +02:00
2020-09-14 16:34:56 +02:00
def determine_end_date(end_date):
"""
Determine appropriate end date
:param end_date:
2020-09-18 20:09:32 +02:00
:return: validity_end as string
"""
2020-09-14 16:34:56 +02:00
# ENTRUST only allows 13 months of max certificate duration
2020-09-18 20:09:32 +02:00
max_validity_end = arrow.utcnow().shift(years=1, months=+1)
if not end_date:
2020-09-14 16:34:56 +02:00
end_date = max_validity_end
elif end_date > max_validity_end:
end_date = max_validity_end
2020-09-18 20:09:32 +02:00
return end_date.format('YYYY-MM-DD')
2020-09-11 12:24:33 +02:00
2020-09-14 16:34:56 +02:00
2020-11-12 13:51:08 +01:00
def process_options(options, client_id):
2020-09-10 16:03:29 +02:00
"""
Processes and maps the incoming issuer options to fields/options that
Entrust understands
:param options:
:return: dict of valid entrust options
"""
2020-09-11 12:24:33 +02:00
# if there is a config variable ENTRUST_PRODUCT_<upper(authority.name)>
2020-09-10 16:03:29 +02:00
# take the value as Cert product-type
# else default to "STANDARD_SSL"
authority = options.get("authority").name.upper()
2020-09-18 20:09:32 +02:00
# STANDARD_SSL (cn=domain, san=www.domain),
# ADVANTAGE_SSL (cn=domain, san=[www.domain, one_more_option]),
# WILDCARD_SSL (unlimited sans, and wildcard)
2020-09-19 02:16:07 +02:00
product_type = current_app.config.get(f"ENTRUST_PRODUCT_{authority}", "STANDARD_SSL")
if options.get("validity_end"):
validity_end = determine_end_date(options.get("validity_end"))
else:
validity_end = determine_end_date(False)
2020-09-11 12:30:53 +02:00
2020-09-10 16:03:29 +02:00
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",
2020-09-11 12:24:33 +02:00
"certType": product_type,
"certExpiryDate": validity_end,
"tracking": tracking_data,
"org": options.get("organization"),
"clientId": client_id
2020-09-10 16:03:29 +02:00
}
return data
2021-01-18 20:57:49 +01:00
@retry(stop_max_attempt_number=5, wait_fixed=1000)
def get_client_id(session, organization):
2020-11-12 13:51:08 +01:00
"""
Helper function for looking up clientID pased on Organization and parsing the response.
:param session:
:param organization: the validated org with Entrust, for instance "Company, Inc."
:return: ClientID
:raise Exception:
2020-11-12 13:51:08 +01:00
"""
# get the organization ID
url = current_app.config.get("ENTRUST_URL") + "/organizations"
2020-11-12 13:51:08 +01:00
try:
response = session.get(url, timeout=(15, 40))
except requests.exceptions.Timeout:
raise Exception("Timeout for Getting Organizations")
except requests.exceptions.RequestException as e:
raise Exception(f"Error for Getting Organization {e}")
# parse the response
try:
d = json.loads(response.content)
2020-11-12 13:51:08 +01:00
except ValueError:
# catch an empty json object here
d = {'response': 'No detailed message'}
found = False
for y in d["organizations"]:
if y["name"] == organization:
found = True
client_id = y["clientId"]
2020-11-12 13:51:08 +01:00
if found:
return client_id
2020-11-12 13:51:08 +01:00
else:
raise Exception(f"Error on Organization - Use on of the List: {d['organizations']}")
2020-09-14 14:20:11 +02:00
2020-09-14 12:23:58 +02:00
def handle_response(my_response):
"""
Helper function for parsing responses from the Entrust API.
2020-10-24 03:02:54 +02:00
:param my_response:
2020-09-14 12:23:58 +02:00
: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"
}
2020-09-18 20:09:32 +02:00
2020-09-14 14:20:11 +02:00
try:
2020-10-24 03:02:35 +02:00
data = json.loads(my_response.content)
2020-09-18 20:09:32 +02:00
except ValueError:
2020-09-14 15:56:02 +02:00
# catch an empty jason object here
2020-10-24 03:02:35 +02:00
data = {'response': 'No detailed message'}
status_code = my_response.status_code
if status_code > 399:
raise Exception(f"ENTRUST error: {msg.get(status_code, status_code)}\n{data['errors']}")
2020-09-18 20:09:32 +02:00
log_data = {
"function": f"{__name__}.{sys._getframe().f_code.co_name}",
"message": "Response",
2020-10-24 03:02:35 +02:00
"status": status_code,
"response": data
2020-09-18 20:09:32 +02:00
}
current_app.logger.info(log_data)
2020-10-24 03:02:35 +02:00
if data == {'response': 'No detailed message'}:
2020-10-22 04:52:25 +02:00
# status if no data
2020-10-24 03:02:35 +02:00
return status_code
2020-10-22 04:52:25 +02:00
else:
# return data from the response
2020-10-24 03:02:35 +02:00
return data
2020-10-24 03:02:05 +02:00
@retry(stop_max_attempt_number=3, wait_fixed=5000)
2020-10-28 00:13:05 +01:00
def order_and_download_certificate(session, url, data):
2020-10-24 03:02:05 +02:00
"""
2020-10-28 00:13:05 +01:00
Helper function to place a certificacte order and download it
2020-10-24 03:02:05 +02:00
:param session:
:param url: Entrust endpoint url
:param data: CSR, and the required order details, such as validity length
:return: the cert chain
:raise Exception:
"""
try:
response = 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(f"Error for POST {e}")
return handle_response(response)
2020-09-14 12:23:58 +02:00
2020-09-11 12:24:33 +02:00
2020-09-10 16:03:29 +02:00
class EntrustIssuerPlugin(IssuerPlugin):
2020-09-18 20:09:32 +02:00
title = "Entrust"
2020-09-10 16:03:29 +02:00
slug = "entrust-issuer"
description = "Enables the creation of certificates by ENTRUST"
2020-09-18 20:09:32 +02:00
version = entrust.VERSION
2020-09-10 16:03:29 +02:00
author = "sirferl"
author_url = "https://github.com/sirferl/lemur"
def __init__(self, *args, **kwargs):
"""Initialize the issuer with the appropriate details."""
2020-09-14 09:50:55 +02:00
required_vars = [
"ENTRUST_API_CERT",
"ENTRUST_API_KEY",
"ENTRUST_API_USER",
2020-09-14 12:23:58 +02:00
"ENTRUST_API_PASS",
2020-09-14 09:50:55 +02:00
"ENTRUST_URL",
"ENTRUST_ROOT",
"ENTRUST_NAME",
"ENTRUST_EMAIL",
2020-09-14 12:23:58 +02:00
"ENTRUST_PHONE",
2020-09-14 09:50:55 +02:00
]
validate_conf(current_app, required_vars)
2020-09-10 16:03:29 +02:00
self.session = requests.Session()
cert_file = current_app.config.get("ENTRUST_API_CERT")
key_file = current_app.config.get("ENTRUST_API_KEY")
2020-09-10 16:03:29 +02:00
user = current_app.config.get("ENTRUST_API_USER")
password = current_app.config.get("ENTRUST_API_PASS")
2020-09-14 15:56:02 +02:00
self.session.cert = (cert_file, key_file)
self.session.auth = (user, password)
2020-09-10 16:03:29 +02:00
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:
"""
2020-09-18 20:09:32 +02:00
log_data = {
"function": f"{__name__}.{sys._getframe().f_code.co_name}",
"message": "Requesting options",
"options": issuer_options
}
current_app.logger.info(log_data)
2020-09-10 16:03:29 +02:00
if current_app.config.get("ENTRUST_USE_DEFAULT_CLIENT_ID"):
# The ID of the primary client is 1.
client_id = 1
else:
client_id = get_client_id(self.session, issuer_options.get("organization"))
2020-11-12 13:51:08 +01:00
log_data = {
"function": f"{__name__}.{sys._getframe().f_code.co_name}",
"message": f"Organization id: {client_id}"
}
current_app.logger.info(log_data)
2020-09-10 16:03:29 +02:00
url = current_app.config.get("ENTRUST_URL") + "/certificates"
2020-11-12 13:51:08 +01:00
data = process_options(issuer_options, client_id)
2020-09-10 16:03:29 +02:00
data["csr"] = csr
2020-10-28 00:13:05 +01:00
response_dict = order_and_download_certificate(self.session, url, data)
2020-09-10 16:03:29 +02:00
external_id = response_dict['trackingId']
cert = response_dict['endEntityCert']
2020-09-18 20:09:32 +02:00
if len(response_dict['chainCerts']) < 2:
# certificate signed by CA directly, no ICA included in the chain
2020-09-18 20:09:32 +02:00
chain = None
else:
chain = response_dict['chainCerts'][1]
2021-02-05 20:47:47 +01:00
if current_app.config.get("ENTRUST_CROSS_SIGNED_RSA_L1K") and get_key_type_from_certificate(cert) == "RSA2048":
chain = current_app.config.get("ENTRUST_CROSS_SIGNED_RSA_L1K")
if current_app.config.get("ENTRUST_CROSS_SIGNED_ECC_L1F") and get_key_type_from_certificate(cert) == "ECCPRIME256V1":
chain = current_app.config.get("ENTRUST_CROSS_SIGNED_ECC_L1F")
2021-01-16 01:49:14 +01:00
2020-09-18 20:09:32 +02:00
log_data["message"] = "Received Chain"
log_data["options"] = f"chain: {chain}"
current_app.logger.info(log_data)
2020-09-10 16:03:29 +02:00
2020-09-11 12:24:33 +02:00
return cert, chain, external_id
2020-09-10 16:03:29 +02:00
2020-10-24 03:01:14 +02:00
@retry(stop_max_attempt_number=3, wait_fixed=1000)
2020-12-01 05:06:37 +01:00
def revoke_certificate(self, certificate, reason):
2020-09-18 20:09:32 +02:00
"""Revoke an Entrust certificate."""
2020-09-14 14:20:11 +02:00
base_url = current_app.config.get("ENTRUST_URL")
# make certificate revoke request
2020-09-18 20:09:32 +02:00
revoke_url = f"{base_url}/certificates/{certificate.external_id}/revocations"
2020-12-01 05:06:37 +01:00
if "comments" not in reason or reason["comments"] == '':
2020-09-14 15:18:46 +02:00
comments = "revoked via API"
2020-12-01 05:06:37 +01:00
crl_reason = CRLReason.unspecified
if "crl_reason" in reason:
crl_reason = CRLReason[reason["crl_reason"]]
2020-09-14 15:18:46 +02:00
data = {
2020-12-01 05:06:37 +01:00
"crlReason": crl_reason, # per RFC 5280 section 5.3.1
2020-09-14 15:18:46 +02:00
"revocationComment": comments
}
response = self.session.post(revoke_url, json=data)
2020-09-18 20:09:32 +02:00
metrics.send("entrust_revoke_certificate", "counter", 1)
return handle_response(response)
2020-09-14 14:20:11 +02:00
2020-10-24 03:01:14 +02:00
@retry(stop_max_attempt_number=3, wait_fixed=1000)
def deactivate_certificate(self, certificate):
2020-09-18 20:09:32 +02:00
"""Deactivates an Entrust certificate."""
base_url = current_app.config.get("ENTRUST_URL")
deactivate_url = f"{base_url}/certificates/{certificate.external_id}/deactivations"
response = self.session.post(deactivate_url)
metrics.send("entrust_deactivate_certificate", "counter", 1)
2020-09-18 20:09:32 +02:00
return handle_response(response)
2020-09-14 14:20:11 +02:00
2020-09-10 16:03:29 +02:00
@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"}
2020-09-18 20:09:32 +02:00
current_app.logger.info(f"Creating Auth: {options} {entrust_issuing}")
# body, chain, role
2020-09-11 12:24:33 +02:00
return entrust_root, "", [role]
2020-09-10 16:03:29 +02:00
def get_ordered_certificate(self, order_id):
raise NotImplementedError("Not implemented\n", self, order_id)
2020-10-24 03:03:55 +02:00
def cancel_ordered_certificate(self, pending_cert, **kwargs):
2020-09-10 16:03:29 +02:00
raise NotImplementedError("Not implemented\n", self, pending_cert, **kwargs)
class EntrustSourcePlugin(SourcePlugin):
2020-09-18 20:09:32 +02:00
title = "Entrust"
2020-09-10 16:03:29 +02:00
slug = "entrust-source"
2020-09-18 20:09:32 +02:00
description = "Enables the collection of certificates"
version = entrust.VERSION
2020-09-10 16:03:29 +02:00
author = "sirferl"
author_url = "https://github.com/sirferl/lemur"
2020-12-02 13:24:01 +01:00
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",
]
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)
super(EntrustSourcePlugin, self).__init__(*args, **kwargs)
2020-09-10 16:03:29 +02:00
def get_certificates(self, options, **kwargs):
2020-12-02 15:50:51 +01:00
""" Fetch all Entrust certificates """
base_url = current_app.config.get("ENTRUST_URL")
2020-12-02 16:05:34 +01:00
host = base_url.replace('/enterprise/v2', '')
2020-12-02 15:50:51 +01:00
get_url = f"{base_url}/certificates"
2020-12-02 16:05:34 +01:00
certs = []
processed_certs = 0
2020-12-02 15:50:51 +01:00
offset = 0
2020-12-02 16:05:34 +01:00
while True:
response = self.session.get(get_url,
2020-12-02 15:50:51 +01:00
params={
2020-12-02 16:05:34 +01:00
"status": "ACTIVE",
2020-12-02 15:50:51 +01:00
"isThirdParty": "false",
2020-12-02 16:05:34 +01:00
"fields": "uri,dn",
2020-12-02 15:50:51 +01:00
"offset": offset
}
)
try:
data = json.loads(response.content)
except ValueError:
# catch an empty jason object here
data = {'response': 'No detailed message'}
status_code = response.status_code
if status_code > 399:
2020-12-02 16:05:34 +01:00
raise Exception(f"ENTRUST error: {status_code}\n{data['errors']}")
2020-12-02 15:50:51 +01:00
for c in data["certificates"]:
download_url = "{0}{1}".format(
host, c["uri"]
)
cert_response = self.session.get(download_url)
certificate = json.loads(cert_response.content)
# normalize serial
serial = str(int(certificate["serialNumber"], 16))
cert = {
"body": certificate["endEntityCert"],
"serial": serial,
"external_id": str(certificate["trackingId"]),
"csr": certificate["csr"],
2020-12-03 10:17:47 +01:00
"owner": certificate["tracking"]["requesterEmail"],
2020-12-03 21:07:59 +01:00
"description": f"Imported by Lemur; Type: Entrust {certificate['certType']}\nExtended Key Usage: {certificate['eku']}"
2020-12-02 15:50:51 +01:00
}
certs.append(cert)
processed_certs += 1
2020-12-02 15:50:51 +01:00
if data["summary"]["limit"] * offset >= data["summary"]["total"]:
break
2020-12-02 16:05:34 +01:00
else:
2020-12-02 15:50:51 +01:00
offset += 1
current_app.logger.info(f"Retrieved {processed_certs} ertificates")
2020-12-02 15:50:51 +01:00
return certs
2020-09-11 12:24:33 +02:00
2020-09-10 16:03:29 +02:00
def get_endpoints(self, options, **kwargs):
# There are no endpoints in ENTRUST
raise NotImplementedError("Not implemented\n", self, options, **kwargs)