class AcmeDnsChallenge(AcmeChallenge): challengeType = challenges.DNS01 def create_certificate(self, csr, issuer_options): """ Creates an ACME certificate. :param csr: :param issuer_options: :return: :raise Exception: """ self.acme = AcmeDnsHandler() authority = issuer_options.get("authority") create_immediately = issuer_options.get("create_immediately", False) acme_client, registration = self.acme.setup_acme_client(authority) dns_provider = issuer_options.get("dns_provider", {}) if dns_provider: dns_provider_options = dns_provider.options credentials = json.loads(dns_provider.credentials) 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: 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 domains = self.acme.get_domains(issuer_options) if not create_immediately: # Create pending authorizations that we'll need to do the creation dns_authorization = authorization_service.create( account_number, domains, provider_type) # Return id of the DNS Authorization return None, None, dns_authorization.id 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 def deploy(self, challenge, acme_client, validation_target): pass def cleanup(self, authorizations, acme_client, validation_target): """ Best effort attempt to delete DNS challenges that may not have been deleted previously. This is usually called on an exception :param authorizations: all the authorizations to be cleaned up :param acme_client: an already bootstrapped acme_client, to avoid passing all issuer_options and so on :param validation_target: Unused right now :return: """ acme = AcmeDnsHandler() acme.cleanup_dns_challenges(acme_client, authorizations)
class ACMEIssuerPlugin(IssuerPlugin): title = "Acme" slug = "acme-issuer" description = ( "Enables the creation of certificates via ACME CAs (including Let's Encrypt), using the DNS-01 challenge" ) version = acme.VERSION author = "Netflix" author_url = "https://github.com/netflix/lemur.git" options = [ { "name": "acme_url", "type": "str", "required": True, "validation": check_validation(r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+"), "helpMessage": "ACME resource URI. 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": EMAIL_RE.pattern, "helpMessage": "Email to use", }, { "name": "certificate", "type": "textarea", "default": "", "validation": check_validation("^-----BEGIN CERTIFICATE-----"), "helpMessage": "ACME root certificate", }, { "name": "store_account", "type": "bool", "required": False, "helpMessage": "Disable to create a new account for each ACME request", "default": False, }, { "name": "eab_kid", "type": "str", "required": False, "helpMessage": "Key identifier for the external account.", }, { "name": "eab_hmac_key", "type": "str", "required": False, "helpMessage": "HMAC key for the external account.", }, { "name": "acme_private_key", "type": "textarea", "default": "", "required": False, "helpMessage": "Account Private Key. Will be encrypted.", }, { "name": "acme_regr", "type": "textarea", "default": "", "required": False, "helpMessage": "Account Registration", } ] def __init__(self, *args, **kwargs): super(ACMEIssuerPlugin, self).__init__(*args, **kwargs) def get_ordered_certificate(self, pending_cert): self.acme = AcmeDnsHandler() 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) try: order = acme_client.new_order(pending_cert.csr) except WildcardUnsupportedError: metrics.send("get_ordered_certificate_wildcard_unsupported", "counter", 1) raise Exception( "The currently selected ACME CA endpoint does" " not support issuing wildcard certificates." ) try: authorizations = self.acme.get_authorizations( acme_client, order, order_info ) except ClientError: capture_exception() metrics.send("get_ordered_certificate_error", "counter", 1) current_app.logger.error( f"Unable to resolve pending cert: {pending_cert.name}", exc_info=True ) return False authorizations = self.acme.finalize_authorizations(acme_client, authorizations) pem_certificate, pem_certificate_chain = self.acme.request_certificate( acme_client, authorizations, order ) cert = { "body": "\n".join(str(pem_certificate).splitlines()), "chain": "\n".join(str(pem_certificate_chain).splitlines()), "external_id": str(pending_cert.external_id), } return cert def get_ordered_certificates(self, pending_certs): self.acme = AcmeDnsHandler() self.acme_dns_challenge = AcmeDnsChallenge() pending = [] certs = [] for pending_cert in pending_certs: try: 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) try: order = acme_client.new_order(pending_cert.csr) except WildcardUnsupportedError: capture_exception() 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, } ) except (ClientError, ValueError, Exception) as e: capture_exception() 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}" certs.append( {"cert": False, "pending_cert": pending_cert, "last_error": e} ) for entry in pending: try: entry["authorizations"] = self.acme.finalize_authorizations( entry["acme_client"], entry["authorizations"] ) pem_certificate, pem_certificate_chain = self.acme.request_certificate( entry["acme_client"], entry["authorizations"], entry["order"] ) cert = { "body": "\n".join(str(pem_certificate).splitlines()), "chain": "\n".join(str(pem_certificate_chain).splitlines()), "external_id": str(entry["pending_cert"].external_id), } certs.append({"cert": cert, "pending_cert": entry["pending_cert"]}) except (PollError, AcmeError, Exception) as e: capture_exception() 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}. " f"Check out {order_url} for more information.", exc_info=True, ) certs.append( { "cert": False, "pending_cert": entry["pending_cert"], "last_error": error, } ) # Ensure DNS records get deleted self.acme_dns_challenge.cleanup( entry["authorizations"], entry["acme_client"] ) return certs def create_certificate(self, csr, issuer_options): """ Creates an ACME certificate using the DNS-01 challenge. :param csr: :param issuer_options: :return: :raise Exception: """ acme_dns_challenge = AcmeDnsChallenge() return acme_dns_challenge.create_certificate(csr, issuer_options) @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: """ name = "acme_" + "_".join(options['name'].split(" ")) + "_admin" role = {"username": "", "password": "", "name": name} 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 def revoke_certificate(self, certificate, reason): self.acme = AcmeDnsHandler() crl_reason = CRLReason.unspecified if "crl_reason" in reason: crl_reason = CRLReason[reason["crl_reason"]] return self.acme.revoke_certificate(certificate, crl_reason.value)
class AcmeDnsChallenge(AcmeChallenge): challengeType = challenges.DNS01 def create_certificate(self, csr, issuer_options): """ Creates an ACME certificate. :param csr: :param issuer_options: :return: :raise Exception: """ self.acme = AcmeDnsHandler() authority = issuer_options.get("authority") create_immediately = issuer_options.get("create_immediately", False) acme_client, registration = self.acme.setup_acme_client(authority) domains = self.acme.get_domains(issuer_options) dns_provider = issuer_options.get("dns_provider", {}) if dns_provider: for domain in domains: # Currently, we only support specifying one DNS provider per certificate, even if that # certificate has multiple SANs that may belong to different provid self.acme.dns_providers_for_domain[domain] = [dns_provider] credentials = json.loads(dns_provider.credentials) current_app.logger.debug("Using DNS provider: {0}".format( dns_provider.provider_type)) account_number = credentials.get("account_id") provider_type = dns_provider.provider_type if provider_type == "route53" and not account_number: 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 = {} account_number = None provider_type = None for domain in domains: self.acme.autodetect_dns_providers(domain) # Create pending authorizations that we'll need to do the creation dns_authorization = authorization_service.create( account_number, domains, provider_type) if not create_immediately: # Return id of the DNS Authorization return None, None, dns_authorization.id pem_certificate, pem_certificate_chain = self.create_certificate_immediately( acme_client, dns_authorization, csr) # TODO add external ID (if possible) return pem_certificate, pem_certificate_chain, None @retry(stop_max_attempt_number=ACME_ADDITIONAL_ATTEMPTS, wait_fixed=5000) def create_certificate_immediately(self, acme_client, order_info, csr): try: order = acme_client.new_order(csr) except WildcardUnsupportedError: metrics.send("create_certificte_immediately_wildcard_unsupported", "counter", 1) raise Exception("The currently selected ACME CA endpoint does" " not support issuing wildcard certificates.") try: authorizations = self.acme.get_authorizations( acme_client, order, order_info) except ClientError: capture_exception() metrics.send("create_certificate_immediately_error", "counter", 1) current_app.logger.error( f"Unable to resolve cert for domains: {', '.join(order_info.domains)}", exc_info=True) return False authorizations = self.acme.finalize_authorizations( acme_client, authorizations) return self.acme.request_certificate(acme_client, authorizations, order) def deploy(self, challenge, acme_client, validation_target): pass def cleanup(self, authorizations, acme_client, validation_target=None): """ Best effort attempt to delete DNS challenges that may not have been deleted previously. This is usually called on an exception :param authorizations: all the authorizations to be cleaned up :param acme_client: an already bootstrapped acme_client, to avoid passing all issuer_options and so on :param validation_target: Unused right now :return: """ acme = AcmeDnsHandler() acme.cleanup_dns_challenges(acme_client, authorizations)