lemur/lemur/plugins/lemur_acme/plugin.py

889 lines
36 KiB
Python
Raw Normal View History

2016-06-28 00:57:53 +02:00
"""
.. module: lemur.plugins.lemur_acme.plugin
2016-06-28 00:57:53 +02:00
:platform: Unix
:synopsis: This module is responsible for communicating with an ACME CA.
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
2016-06-28 00:57:53 +02:00
:license: Apache, see LICENSE for more details.
Snippets from https://raw.githubusercontent.com/alex/letsencrypt-aws/master/letsencrypt-aws.py
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
.. moduleauthor:: Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>
.. moduleauthor:: Curtis Castrapel <ccastrapel@netflix.com>
2016-06-28 00:57:53 +02:00
"""
2018-05-16 16:46:37 +02:00
import datetime
import json
2018-05-16 16:46:37 +02:00
import time
2018-03-21 20:45:26 +01:00
2018-05-07 18:58:24 +02:00
import OpenSSL.crypto
import josepy as jose
from acme import challenges, errors, messages
2018-05-16 16:46:37 +02:00
from acme.client import BackwardsCompatibleClientV2, ClientNetwork
from acme.errors import PollError, TimeoutError, WildcardUnsupportedError
2018-06-30 00:24:31 +02:00
from acme.messages import Error as AcmeError
2018-05-05 00:00:43 +02:00
from botocore.exceptions import ClientError
2018-05-07 18:58:24 +02:00
from flask import current_app
2016-06-28 00:57:53 +02:00
from lemur.authorizations import service as authorization_service
2018-05-07 18:58:24 +02:00
from lemur.common.utils import generate_private_key
from lemur.destinations import service as destination_service
from lemur.dns_providers import service as dns_provider_service
2018-05-08 20:03:17 +02:00
from lemur.exceptions import InvalidAuthority, InvalidConfiguration, UnknownProvider
from lemur.extensions import metrics, sentry
from lemur.plugins.base import plugins
2016-06-28 00:57:53 +02:00
from lemur.plugins import lemur_acme as acme
2018-05-07 18:58:24 +02:00
from lemur.plugins.bases import IssuerPlugin
from lemur.plugins.lemur_acme import cloudflare, dyn, route53, ultradns, powerdns
2020-09-23 16:38:57 +02:00
from lemur.authorities import service as authorities_service
from retrying import retry
2016-06-28 00:57:53 +02:00
class AuthorizationRecord(object):
def __init__(self, host, authz, dns_challenge, change_id):
self.host = host
self.authz = authz
self.dns_challenge = dns_challenge
self.change_id = change_id
class AcmeHandler(object):
def __init__(self):
self.dns_providers_for_domain = {}
2018-11-05 23:37:52 +01:00
try:
self.all_dns_providers = dns_provider_service.get_all_dns_providers()
except Exception as e:
2019-05-16 16:57:02 +02:00
metrics.send("AcmeHandler_init_error", "counter", 1)
sentry.captureException()
current_app.logger.error(f"Unable to fetch DNS Providers: {e}")
2018-11-05 23:37:52 +01:00
self.all_dns_providers = []
2018-05-16 16:46:37 +02:00
def get_dns_challenges(self, host, authorizations):
2020-03-16 19:24:17 +01:00
"""Get dns challenges for provided domain"""
domain_to_validate, is_wildcard = self.strip_wildcard(host)
dns_challenges = []
for authz in authorizations:
if not authz.body.identifier.value.lower() == domain_to_validate.lower():
continue
if is_wildcard and not authz.body.wildcard:
continue
if not is_wildcard and authz.body.wildcard:
continue
for combo in authz.body.challenges:
if isinstance(combo.chall, challenges.DNS01):
dns_challenges.append(combo)
2018-05-16 16:46:37 +02:00
2020-03-16 19:24:17 +01:00
return dns_challenges
def strip_wildcard(self, host):
"""Removes the leading *. and returns Host and whether it was removed or not (True/False)"""
prefix = "*."
if host.startswith(prefix):
return host[len(prefix):], True
return host, False
def maybe_add_extension(self, host, dns_provider_options):
2019-05-16 16:57:02 +02:00
if dns_provider_options and dns_provider_options.get(
"acme_challenge_extension"
2019-05-16 16:57:02 +02:00
):
host = host + dns_provider_options.get("acme_challenge_extension")
return host
2019-05-16 16:57:02 +02:00
def start_dns_challenge(
self,
acme_client,
account_number,
host,
dns_provider,
order,
dns_provider_options,
2019-05-16 16:57:02 +02:00
):
current_app.logger.debug("Starting DNS challenge for {0}".format(host))
2016-06-28 00:57:53 +02:00
change_ids = []
2020-03-16 19:24:17 +01:00
dns_challenges = self.get_dns_challenges(host, order.authorizations)
host_to_validate, _ = self.strip_wildcard(host)
2019-05-16 16:57:02 +02:00
host_to_validate = self.maybe_add_extension(
host_to_validate, dns_provider_options
)
if not dns_challenges:
sentry.captureException()
2019-05-16 16:57:02 +02:00
metrics.send("start_dns_challenge_error_no_dns_challenges", "counter", 1)
raise Exception("Unable to determine DNS challenges from authorizations")
for dns_challenge in dns_challenges:
change_id = dns_provider.create_txt_record(
dns_challenge.validation_domain_name(host_to_validate),
dns_challenge.validation(acme_client.client.net.key),
2019-05-16 16:57:02 +02:00
account_number,
)
change_ids.append(change_id)
2016-12-19 03:21:22 +01:00
return AuthorizationRecord(
2019-05-16 16:57:02 +02:00
host, order.authorizations, dns_challenges, change_ids
)
2016-06-28 00:57:53 +02:00
def complete_dns_challenge(self, acme_client, authz_record):
2019-05-16 16:57:02 +02:00
current_app.logger.debug(
"Finalizing DNS challenge for {0}".format(
authz_record.authz[0].body.identifier.value
)
)
dns_providers = self.dns_providers_for_domain.get(authz_record.host)
if not dns_providers:
2019-05-16 16:57:02 +02:00
metrics.send("complete_dns_challenge_error_no_dnsproviders", "counter", 1)
raise Exception(
"No DNS providers found for domain: {}".format(authz_record.host)
)
for dns_provider in dns_providers:
# Grab account number (For Route53)
dns_provider_options = json.loads(dns_provider.credentials)
account_number = dns_provider_options.get("account_id")
dns_provider_plugin = self.get_dns_provider(dns_provider.provider_type)
for change_id in authz_record.change_id:
try:
2019-05-16 16:57:02 +02:00
dns_provider_plugin.wait_for_dns_change(
change_id, account_number=account_number
)
except Exception:
2019-05-16 16:57:02 +02:00
metrics.send("complete_dns_challenge_error", "counter", 1)
sentry.captureException()
current_app.logger.debug(
f"Unable to resolve DNS challenge for change_id: {change_id}, account_id: "
2019-05-16 16:57:02 +02:00
f"{account_number}",
exc_info=True,
)
raise
for dns_challenge in authz_record.dns_challenge:
response = dns_challenge.response(acme_client.client.net.key)
verified = response.simple_verify(
dns_challenge.chall,
authz_record.host,
2019-05-16 16:57:02 +02:00
acme_client.client.net.key.public_key(),
)
if not verified:
2019-05-16 16:57:02 +02:00
metrics.send("complete_dns_challenge_verification_error", "counter", 1)
raise ValueError("Failed verification")
time.sleep(5)
res = acme_client.answer_challenge(dns_challenge, response)
current_app.logger.debug(f"answer_challenge response: {res}")
def request_certificate(self, acme_client, authorizations, order):
for authorization in authorizations:
for authz in authorization.authz:
authorization_resource, _ = acme_client.poll(authz)
2016-12-19 03:21:22 +01:00
deadline = datetime.datetime.now() + datetime.timedelta(seconds=360)
2016-06-28 00:57:53 +02:00
try:
orderr = acme_client.poll_and_finalize(order, deadline)
except (AcmeError, TimeoutError):
sentry.captureException(extra={"order_url": str(order.uri)})
metrics.send("request_certificate_error", "counter", 1, metric_tags={"uri": order.uri})
2019-05-16 16:57:02 +02:00
current_app.logger.error(
f"Unable to resolve Acme order: {order.uri}", exc_info=True
)
raise
except errors.ValidationError:
if order.fullchain_pem:
orderr = order
else:
raise
metrics.send("request_certificate_success", "counter", 1, metric_tags={"uri": order.uri})
current_app.logger.info(
2020-02-03 20:05:20 +01:00
f"Successfully resolved Acme order: {order.uri}", exc_info=True
)
2019-05-16 16:57:02 +02:00
pem_certificate = OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM,
OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM, orderr.fullchain_pem
),
).decode()
if current_app.config.get("IDENTRUST_CROSS_SIGNED_LE_ICA", False) \
and datetime.datetime.now() < datetime.datetime.strptime(
2020-09-30 08:39:41 +02:00
current_app.config.get("IDENTRUST_CROSS_SIGNED_LE_ICA_EXPIRATION_DATE", "17/03/21"), '%d/%m/%y'):
pem_certificate_chain = current_app.config.get("IDENTRUST_CROSS_SIGNED_LE_ICA")
else:
pem_certificate_chain = orderr.fullchain_pem[
len(pem_certificate): # noqa
].lstrip()
2019-05-16 16:57:02 +02:00
current_app.logger.debug(
"{0} {1}".format(type(pem_certificate), type(pem_certificate_chain))
)
return pem_certificate, pem_certificate_chain
@retry(stop_max_attempt_number=5, wait_fixed=5000)
def setup_acme_client(self, authority):
if not authority.options:
raise InvalidAuthority("Invalid authority. Options not set")
options = {}
for option in json.loads(authority.options):
options[option["name"]] = option.get("value")
2019-05-16 16:57:02 +02:00
email = options.get("email", current_app.config.get("ACME_EMAIL"))
tel = options.get("telephone", current_app.config.get("ACME_TEL"))
directory_url = options.get(
"acme_url", current_app.config.get("ACME_DIRECTORY_URL")
)
2019-05-16 16:57:02 +02:00
existing_key = options.get(
"acme_private_key", current_app.config.get("ACME_PRIVATE_KEY")
)
existing_regr = options.get("acme_regr", current_app.config.get("ACME_REGR"))
if existing_key and existing_regr:
current_app.logger.debug("Reusing existing ACME account")
# Reuse the same account for each certificate issuance
key = jose.JWK.json_loads(existing_key)
regr = messages.RegistrationResource.json_loads(existing_regr)
2019-05-16 16:57:02 +02:00
current_app.logger.debug(
"Connecting with directory at {0}".format(directory_url)
)
net = ClientNetwork(key, account=regr)
client = BackwardsCompatibleClientV2(net, key, directory_url)
return client, {}
else:
# Create an account for each certificate issuance
2019-05-16 16:57:02 +02:00
key = jose.JWKRSA(key=generate_private_key("RSA2048"))
current_app.logger.debug("Creating a new ACME account")
2019-05-16 16:57:02 +02:00
current_app.logger.debug(
"Connecting with directory at {0}".format(directory_url)
)
2018-08-18 00:36:56 +02:00
net = ClientNetwork(key, account=None, timeout=3600)
client = BackwardsCompatibleClientV2(net, key, directory_url)
2019-05-16 16:57:02 +02:00
registration = client.new_account_and_tos(
messages.NewRegistration.from_data(email=email)
)
2020-09-23 16:38:57 +02:00
# if store_account is checked, add the private_key and registration resources to the options
if options['store_account']:
new_options = json.loads(authority.options)
# the key returned by fields_to_partial_json is missing the key type, so we add it manually
key_dict = key.fields_to_partial_json()
key_dict["kty"] = "RSA"
acme_private_key = {
"name": "acme_private_key",
"value": json.dumps(key_dict)
}
new_options.append(acme_private_key)
acme_regr = {
"name": "acme_regr",
"value": json.dumps({"body": {}, "uri": registration.uri})
}
new_options.append(acme_regr)
authorities_service.update_options(authority.id, options=json.dumps(new_options))
current_app.logger.debug("Connected: {0}".format(registration.uri))
return client, registration
def get_domains(self, options):
"""
Fetches all domains currently requested
:param options:
:return:
"""
current_app.logger.debug("Fetching domains")
2019-05-16 16:57:02 +02:00
domains = [options["common_name"]]
if options.get("extensions"):
for dns_name in options["extensions"]["sub_alt_names"]["names"]:
if dns_name.value not in domains:
domains.append(dns_name.value)
current_app.logger.debug("Got these domains: {0}".format(domains))
return domains
def get_authorizations(self, acme_client, order, order_info):
authorizations = []
for domain in order_info.domains:
if not self.dns_providers_for_domain.get(domain):
2019-05-16 16:57:02 +02:00
metrics.send(
"get_authorizations_no_dns_provider_for_domain", "counter", 1
)
raise Exception("No DNS providers found for domain: {}".format(domain))
for dns_provider in self.dns_providers_for_domain[domain]:
dns_provider_plugin = self.get_dns_provider(dns_provider.provider_type)
dns_provider_options = json.loads(dns_provider.credentials)
account_number = dns_provider_options.get("account_id")
2019-05-16 16:57:02 +02:00
authz_record = self.start_dns_challenge(
acme_client,
account_number,
domain,
dns_provider_plugin,
order,
dns_provider.options,
)
authorizations.append(authz_record)
return authorizations
def autodetect_dns_providers(self, domain):
"""
Get DNS providers associated with a domain when it has not been provided for certificate creation.
:param domain:
:return: dns_providers: List of DNS providers that have the correct zone.
"""
self.dns_providers_for_domain[domain] = []
match_length = 0
for dns_provider in self.all_dns_providers:
2018-08-16 19:12:19 +02:00
if not dns_provider.domains:
continue
for name in dns_provider.domains:
if name == domain or domain.endswith("." + name):
if len(name) > match_length:
self.dns_providers_for_domain[domain] = [dns_provider]
match_length = len(name)
elif len(name) == match_length:
self.dns_providers_for_domain[domain].append(dns_provider)
return self.dns_providers_for_domain
def finalize_authorizations(self, acme_client, authorizations):
for authz_record in authorizations:
self.complete_dns_challenge(acme_client, authz_record)
for authz_record in authorizations:
dns_challenges = authz_record.dns_challenge
for dns_challenge in dns_challenges:
dns_providers = self.dns_providers_for_domain.get(authz_record.host)
for dns_provider in dns_providers:
# Grab account number (For Route53)
2019-05-16 16:57:02 +02:00
dns_provider_plugin = self.get_dns_provider(
dns_provider.provider_type
)
dns_provider_options = json.loads(dns_provider.credentials)
account_number = dns_provider_options.get("account_id")
host_to_validate, _ = self.strip_wildcard(authz_record.host)
2019-05-16 16:57:02 +02:00
host_to_validate = self.maybe_add_extension(
host_to_validate, dns_provider_options
)
dns_provider_plugin.delete_txt_record(
authz_record.change_id,
account_number,
dns_challenge.validation_domain_name(host_to_validate),
2019-05-16 16:57:02 +02:00
dns_challenge.validation(acme_client.client.net.key),
)
return authorizations
def cleanup_dns_challenges(self, acme_client, authorizations):
"""
Best effort attempt to delete DNS challenges that may not have been deleted previously. This is usually called
on an exception
:param acme_client:
:param account_number:
:param dns_provider:
:param authorizations:
:param dns_provider_options:
:return:
"""
for authz_record in authorizations:
dns_providers = self.dns_providers_for_domain.get(authz_record.host)
for dns_provider in dns_providers:
# Grab account number (For Route53)
dns_provider_options = json.loads(dns_provider.credentials)
account_number = dns_provider_options.get("account_id")
dns_challenges = authz_record.dns_challenge
host_to_validate, _ = self.strip_wildcard(authz_record.host)
2019-05-16 16:57:02 +02:00
host_to_validate = self.maybe_add_extension(
host_to_validate, dns_provider_options
)
2019-04-25 22:50:41 +02:00
dns_provider_plugin = self.get_dns_provider(dns_provider.provider_type)
for dns_challenge in dns_challenges:
try:
2019-04-25 22:50:41 +02:00
dns_provider_plugin.delete_txt_record(
authz_record.change_id,
account_number,
dns_challenge.validation_domain_name(host_to_validate),
2019-05-16 16:57:02 +02:00
dns_challenge.validation(acme_client.client.net.key),
)
except Exception as e:
# If this fails, it's most likely because the record doesn't exist (It was already cleaned up)
# or we're not authorized to modify it.
2019-05-16 16:57:02 +02:00
metrics.send("cleanup_dns_challenges_error", "counter", 1)
sentry.captureException()
pass
2016-06-28 00:57:53 +02:00
def get_dns_provider(self, type):
provider_types = {
"cloudflare": cloudflare,
"dyn": dyn,
"route53": route53,
"ultradns": ultradns,
"powerdns": powerdns
}
provider = provider_types.get(type)
if not provider:
raise UnknownProvider("No such DNS provider: {}".format(type))
return provider
2018-07-31 00:25:02 +02:00
2016-06-28 00:57:53 +02:00
class ACMEIssuerPlugin(IssuerPlugin):
2019-05-16 16:57:02 +02:00
title = "Acme"
slug = "acme-issuer"
description = (
"Enables the creation of certificates via ACME CAs (including Let's Encrypt), using the DNS-01 challenge"
2019-05-16 16:57:02 +02:00
)
2016-06-28 00:57:53 +02:00
version = acme.VERSION
2019-05-16 16:57:02 +02:00
author = "Netflix"
author_url = "https://github.com/netflix/lemur.git"
2016-06-28 00:57:53 +02:00
options = [
{
2019-05-16 16:57:02 +02:00
"name": "acme_url",
"type": "str",
"required": True,
"validation": "/^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+$/",
"helpMessage": "Must be a valid web url starting with http[s]://",
},
{
2019-05-16 16:57:02 +02:00
"name": "telephone",
"type": "str",
"default": "",
"helpMessage": "Telephone to use",
},
{
2019-05-16 16:57:02 +02:00
"name": "email",
"type": "str",
"default": "",
"validation": "/^?([-a-zA-Z0-9.`?{}]+@\w+\.\w+)$/",
"helpMessage": "Email to use",
},
{
2019-05-16 16:57:02 +02:00
"name": "certificate",
"type": "textarea",
"default": "",
"validation": "/^-----BEGIN CERTIFICATE-----/",
"helpMessage": "Certificate to use",
},
{
"name": "store_account",
"type": "bool",
"required": False,
"helpMessage": "Disable to create a new account for each ACME request",
2020-09-30 17:46:14 +02:00
"default": False,
}
]
2016-06-28 00:57:53 +02:00
def __init__(self, *args, **kwargs):
super(ACMEIssuerPlugin, self).__init__(*args, **kwargs)
2018-05-05 00:00:43 +02:00
def get_dns_provider(self, type):
2018-11-29 18:29:05 +01:00
self.acme = AcmeHandler()
provider_types = {
"cloudflare": cloudflare,
"dyn": dyn,
"route53": route53,
"ultradns": ultradns,
"powerdns": powerdns
}
2018-05-08 20:03:17 +02:00
provider = provider_types.get(type)
if not provider:
raise UnknownProvider("No such DNS provider: {}".format(type))
return provider
2018-05-05 00:00:43 +02:00
def get_all_zones(self, dns_provider):
2018-11-29 18:29:05 +01:00
self.acme = AcmeHandler()
dns_provider_options = json.loads(dns_provider.credentials)
account_number = dns_provider_options.get("account_id")
dns_provider_plugin = self.get_dns_provider(dns_provider.provider_type)
return dns_provider_plugin.get_zones(account_number=account_number)
def get_ordered_certificate(self, pending_cert):
2018-11-29 18:29:05 +01:00
self.acme = AcmeHandler()
acme_client, registration = self.acme.setup_acme_client(pending_cert.authority)
order_info = authorization_service.get(pending_cert.external_id)
if pending_cert.dns_provider_id:
dns_provider = dns_provider_service.get(pending_cert.dns_provider_id)
for domain in order_info.domains:
# Currently, we only support specifying one DNS provider per certificate, even if that
# certificate has multiple SANs that may belong to different providers.
self.acme.dns_providers_for_domain[domain] = [dns_provider]
else:
for domain in order_info.domains:
self.acme.autodetect_dns_providers(domain)
2018-05-05 00:00:43 +02:00
try:
order = acme_client.new_order(pending_cert.csr)
except WildcardUnsupportedError:
2019-05-16 16:57:02 +02:00
metrics.send("get_ordered_certificate_wildcard_unsupported", "counter", 1)
raise Exception(
"The currently selected ACME CA endpoint does"
" not support issuing wildcard certificates."
)
try:
2019-05-16 16:57:02 +02:00
authorizations = self.acme.get_authorizations(
acme_client, order, order_info
)
2018-05-05 00:00:43 +02:00
except ClientError:
sentry.captureException()
2019-05-16 16:57:02 +02:00
metrics.send("get_ordered_certificate_error", "counter", 1)
current_app.logger.error(
f"Unable to resolve pending cert: {pending_cert.name}", exc_info=True
)
2018-05-05 00:00:43 +02:00
return False
authorizations = self.acme.finalize_authorizations(acme_client, authorizations)
pem_certificate, pem_certificate_chain = self.acme.request_certificate(
2019-05-16 16:57:02 +02:00
acme_client, authorizations, order
)
cert = {
2019-05-16 16:57:02 +02:00
"body": "\n".join(str(pem_certificate).splitlines()),
"chain": "\n".join(str(pem_certificate_chain).splitlines()),
"external_id": str(pending_cert.external_id),
}
return cert
2018-04-30 19:48:48 +02:00
def get_ordered_certificates(self, pending_certs):
2018-11-29 18:29:05 +01:00
self.acme = AcmeHandler()
2018-04-30 19:48:48 +02:00
pending = []
2018-05-05 00:00:43 +02:00
certs = []
2018-04-30 19:48:48 +02:00
for pending_cert in pending_certs:
2018-05-05 00:00:43 +02:00
try:
2019-05-16 16:57:02 +02:00
acme_client, registration = self.acme.setup_acme_client(
pending_cert.authority
)
order_info = authorization_service.get(pending_cert.external_id)
if pending_cert.dns_provider_id:
2019-05-16 16:57:02 +02:00
dns_provider = dns_provider_service.get(
pending_cert.dns_provider_id
)
for domain in order_info.domains:
# Currently, we only support specifying one DNS provider per certificate, even if that
# certificate has multiple SANs that may belong to different providers.
self.acme.dns_providers_for_domain[domain] = [dns_provider]
else:
for domain in order_info.domains:
self.acme.autodetect_dns_providers(domain)
2018-05-16 16:46:37 +02:00
try:
order = acme_client.new_order(pending_cert.csr)
except WildcardUnsupportedError:
sentry.captureException()
2019-05-16 16:57:02 +02:00
metrics.send(
"get_ordered_certificates_wildcard_unsupported_error",
"counter",
1,
)
raise Exception(
"The currently selected ACME CA endpoint does"
" not support issuing wildcard certificates."
)
authorizations = self.acme.get_authorizations(
acme_client, order, order_info
)
pending.append(
{
"acme_client": acme_client,
"authorizations": authorizations,
"pending_cert": pending_cert,
"order": order,
}
)
2018-07-27 23:15:14 +02:00
except (ClientError, ValueError, Exception) as e:
sentry.captureException()
2019-05-16 16:57:02 +02:00
metrics.send(
"get_ordered_certificates_pending_creation_error", "counter", 1
)
current_app.logger.error(
f"Unable to resolve pending cert: {pending_cert}", exc_info=True
)
error = e
if globals().get("order") and order:
error += f" Order uri: {order.uri}"
2019-05-16 16:57:02 +02:00
certs.append(
{"cert": False, "pending_cert": pending_cert, "last_error": e}
)
2018-04-30 19:48:48 +02:00
for entry in pending:
2018-05-05 00:00:43 +02:00
try:
entry["authorizations"] = self.acme.finalize_authorizations(
2019-05-16 16:57:02 +02:00
entry["acme_client"], entry["authorizations"]
)
pem_certificate, pem_certificate_chain = self.acme.request_certificate(
2019-05-16 16:57:02 +02:00
entry["acme_client"], entry["authorizations"], entry["order"]
2018-05-05 00:00:43 +02:00
)
cert = {
2019-05-16 16:57:02 +02:00
"body": "\n".join(str(pem_certificate).splitlines()),
"chain": "\n".join(str(pem_certificate_chain).splitlines()),
"external_id": str(entry["pending_cert"].external_id),
2018-05-05 00:00:43 +02:00
}
2019-05-16 16:57:02 +02:00
certs.append({"cert": cert, "pending_cert": entry["pending_cert"]})
2018-07-31 00:25:02 +02:00
except (PollError, AcmeError, Exception) as e:
sentry.captureException()
2019-05-16 16:57:02 +02:00
metrics.send("get_ordered_certificates_resolution_error", "counter", 1)
order_url = order.uri
error = f"{e}. Order URI: {order_url}"
current_app.logger.error(
f"Unable to resolve pending cert: {pending_cert}. "
2019-05-16 16:57:02 +02:00
f"Check out {order_url} for more information.",
exc_info=True,
)
certs.append(
{
"cert": False,
"pending_cert": entry["pending_cert"],
"last_error": error,
}
)
2018-07-31 00:25:02 +02:00
# Ensure DNS records get deleted
self.acme.cleanup_dns_challenges(
2019-05-16 16:57:02 +02:00
entry["acme_client"], entry["authorizations"]
2018-07-31 00:25:02 +02:00
)
2018-04-30 19:48:48 +02:00
return certs
2016-06-28 00:57:53 +02:00
def create_certificate(self, csr, issuer_options):
"""
Creates an ACME certificate.
2016-06-28 00:57:53 +02:00
:param csr:
:param issuer_options:
:return: :raise Exception:
"""
2018-11-29 18:29:05 +01:00
self.acme = AcmeHandler()
2019-05-16 16:57:02 +02:00
authority = issuer_options.get("authority")
create_immediately = issuer_options.get("create_immediately", False)
acme_client, registration = self.acme.setup_acme_client(authority)
2019-05-16 16:57:02 +02:00
dns_provider = issuer_options.get("dns_provider", {})
2018-08-13 23:25:54 +02:00
if dns_provider:
dns_provider_options = dns_provider.options
credentials = json.loads(dns_provider.credentials)
2019-05-16 16:57:02 +02:00
current_app.logger.debug(
"Using DNS provider: {0}".format(dns_provider.provider_type)
)
dns_provider_plugin = __import__(
dns_provider.provider_type, globals(), locals(), [], 1
)
account_number = credentials.get("account_id")
provider_type = dns_provider.provider_type
if provider_type == "route53" and not account_number:
2019-05-16 16:57:02 +02:00
error = "Route53 DNS Provider {} does not have an account number configured.".format(
dns_provider.name
)
current_app.logger.error(error)
raise InvalidConfiguration(error)
else:
dns_provider = {}
dns_provider_options = None
account_number = None
provider_type = None
acme_client.new_order()
domains = self.acme.get_domains(issuer_options)
if not create_immediately:
# Create pending authorizations that we'll need to do the creation
2019-05-16 16:57:02 +02:00
dns_authorization = authorization_service.create(
account_number, domains, provider_type
2019-05-16 16:57:02 +02:00
)
# Return id of the DNS Authorization
return None, None, dns_authorization.id
2019-05-16 16:57:02 +02:00
authorizations = self.acme.get_authorizations(
acme_client,
account_number,
domains,
dns_provider_plugin,
dns_provider_options,
)
self.acme.finalize_authorizations(
acme_client,
account_number,
dns_provider_plugin,
authorizations,
dns_provider_options,
)
pem_certificate, pem_certificate_chain = self.acme.request_certificate(
acme_client, authorizations, csr
)
# TODO add external ID (if possible)
return pem_certificate, pem_certificate_chain, None
2016-06-28 00:57:53 +02:00
@staticmethod
def create_authority(options):
"""
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:
"""
2019-05-16 16:57:02 +02:00
role = {"username": "", "password": "", "name": "acme"}
plugin_options = options.get("plugin", {}).get("plugin_options")
2018-05-05 00:00:43 +02:00
if not plugin_options:
error = "Invalid options for lemur_acme plugin: {}".format(options)
current_app.logger.error(error)
raise InvalidConfiguration(error)
# Define static acme_root based off configuration variable by default. However, if user has passed a
# certificate, use this certificate as the root.
2019-05-16 16:57:02 +02:00
acme_root = current_app.config.get("ACME_ROOT")
for option in plugin_options:
2019-05-16 16:57:02 +02:00
if option.get("name") == "certificate":
acme_root = option.get("value")
return acme_root, "", [role]
def cancel_ordered_certificate(self, pending_cert, **kwargs):
# Needed to override issuer function.
pass
class ACMEHttpIssuerPlugin(IssuerPlugin):
title = "Acme HTTP-01"
slug = "acme-http-issuer"
description = (
"Enables the creation of certificates via ACME CAs (including Let's Encrypt), using the HTTP-01 challenge"
)
version = acme.VERSION
author = "Netflix"
author_url = "https://github.com/netflix/lemur.git"
destination_list = []
options = [
{
"name": "acme_url",
"type": "str",
"required": True,
"validation": "/^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+$/",
"helpMessage": "Must be a valid web url starting with http[s]://",
},
{
"name": "telephone",
"type": "str",
"default": "",
"helpMessage": "Telephone to use",
},
{
"name": "email",
"type": "str",
"default": "",
"validation": "/^?([-a-zA-Z0-9.`?{}]+@\w+\.\w+)$/",
"helpMessage": "Email to use",
},
{
"name": "certificate",
"type": "textarea",
"default": "",
"validation": "/^-----BEGIN CERTIFICATE-----/",
"helpMessage": "Certificate to use",
},
{
"name": "tokenDestination",
"type": "select",
"required": True,
"available": destination_list,
"helpMessage": "The destination to use to deploy the token.",
},
]
def __init__(self, *args, **kwargs):
super(ACMEHttpIssuerPlugin, self).__init__(*args, **kwargs)
if len(self.destination_list) == 0:
destinations = destination_service.get_all()
for destination in destinations:
# we only want to use sftp destinations here
if destination.plugin_name == "sftp-destination":
self.destination_list.append(destination.label)
def create_certificate(self, csr, issuer_options):
"""
Creates an ACME certificate using the HTTP-01 challenge.
:param csr:
:param issuer_options:
:return: :raise Exception:
"""
self.acme = AcmeHandler()
authority = issuer_options.get("authority")
create_immediately = issuer_options.get("create_immediately", False)
acme_client, registration = self.acme.setup_acme_client(authority)
orderr = acme_client.new_order(csr)
challenge = None
for authz in orderr.authorizations:
# Choosing challenge.
# authz.body.challenges is a set of ChallengeBody objects.
for i in authz.body.challenges:
# Find the supported challenge.
if isinstance(i.chall, challenges.HTTP01):
challenge = i
if challenge is None:
raise Exception('HTTP-01 challenge was not offered by the CA server.')
else:
# Here we probably should create a pending certificate and make use of celery, but for now
# I'll ignore all of that
2020-09-30 11:35:27 +02:00
token_destination = None
for option in json.loads(issuer_options["authority"].options):
if option["name"] == "tokenDestination":
token_destination = destination_service.get_by_label(option["value"])
if token_destination is None:
raise Exception('No token_destination configured for this authority. Cant complete HTTP-01 challenge')
destination_plugin = plugins.get(token_destination.plugin_name)
destination_plugin.upload_acme_token(challenge.chall.path, challenge.chall.token, token_destination.options)
current_app.logger.info("Uploaded HTTP-01 challenge token, trying to poll and finalize the order")
pem_certificate, pem_certificate_chain = self.acme.request_certificate(
acme_client, orderr.authorizations, csr
)
# TODO add external ID (if possible)
return pem_certificate, pem_certificate_chain, None
@staticmethod
def create_authority(options):
"""
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": "acme"}
plugin_options = options.get("plugin", {}).get("plugin_options")
if not plugin_options:
error = "Invalid options for lemur_acme plugin: {}".format(options)
current_app.logger.error(error)
raise InvalidConfiguration(error)
# Define static acme_root based off configuration variable by default. However, if user has passed a
# certificate, use this certificate as the root.
acme_root = current_app.config.get("ACME_ROOT")
for option in plugin_options:
if option.get("name") == "certificate":
acme_root = option.get("value")
return acme_root, "", [role]
def cancel_ordered_certificate(self, pending_cert, **kwargs):
# Needed to override issuer function.
pass