Esempio n. 1
0
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)
Esempio n. 2
0
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)
Esempio n. 3
0
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)