Ejemplo n.º 1
0
    def revoke_certificate(self, certificate, reason):
        self.acme = AcmeHandler()

        crl_reason = CRLReason.unspecified
        if "crl_reason" in reason:
            crl_reason = CRLReason[reason["crl_reason"]]

        return self.acme.revoke_certificate(certificate, crl_reason.value)
Ejemplo n.º 2
0
    def get_dns_provider(self, type):
        self.acme = AcmeHandler()

        provider_types = {
            "cloudflare": cloudflare,
            "dyn": dyn,
            "route53": route53,
            "ultradns": ultradns,
            "powerdns": powerdns,
            "nsone": nsone
        }
        provider = provider_types.get(type)
        if not provider:
            raise UnknownProvider("No such DNS provider: {}".format(type))
        return provider
Ejemplo n.º 3
0
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"

    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": "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",
            "default": "",
            "required": False,
            "helpMessage": "Key identifier for the external account.",
        },
        {
            "name": "eab_hmac_key",
            "type": "str",
            "default": "",
            "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",
        },
        {
            "name": "tokenDestination",
            "type": "destinationSelect",
            "required": True,
            "helpMessage": "The destination to use to deploy the token.",
        }
    ]

    def __init__(self, *args, **kwargs):
        super(ACMEHttpIssuerPlugin, self).__init__(*args, **kwargs)

    def create_certificate(self, csr, issuer_options):
        """
        Creates an ACME certificate using the HTTP-01 challenge.

        :param csr:
        :param issuer_options:
        :return: :raise Exception:
        """
        acme_http_challenge = AcmeHttpChallenge()

        return acme_http_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 = AcmeHandler()

        crl_reason = CRLReason.unspecified
        if "crl_reason" in reason:
            crl_reason = CRLReason[reason["crl_reason"]]

        return self.acme.revoke_certificate(certificate, crl_reason.value)
Ejemplo n.º 4
0
    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")
        acme_client, registration = self.acme.setup_acme_client(authority)

        orderr = acme_client.new_order(csr)

        chall = []
        deployed_challenges = []
        all_pre_validated = True
        for authz in orderr.authorizations:
            # Choosing challenge.
            # check if authorizations is already in a valid state
            if authz.body.status != STATUS_VALID:
                all_pre_validated = False
                # 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):
                        chall.append(i)
            else:
                current_app.logger.info(
                    "{} already validated, skipping".format(
                        authz.body.identifier.value))

        if len(chall) == 0 and not all_pre_validated:
            raise Exception(
                'HTTP-01 challenge was not offered by the CA server at {}'.
                format(orderr.uri))
        elif not all_pre_validated:
            validation_target = None
            for option in json.loads(issuer_options["authority"].options):
                if option["name"] == "tokenDestination":
                    validation_target = option["value"]

            if validation_target is None:
                raise Exception(
                    'No token_destination configured for this authority. Cant complete HTTP-01 challenge'
                )

            for challenge in chall:
                try:
                    response = self.deploy(challenge, acme_client,
                                           validation_target)
                    deployed_challenges.append(challenge.chall.path)
                    acme_client.answer_challenge(challenge, response)
                except Exception as e:
                    current_app.logger.error(e)
                    raise Exception(
                        'Failure while trying to deploy token to configure destination. See logs for more information'
                    )

            current_app.logger.info(
                "Uploaded HTTP-01 challenge tokens, trying to poll and finalize the order"
            )

        try:
            finalized_orderr = acme_client.poll_and_finalize(
                orderr,
                datetime.datetime.now() + datetime.timedelta(seconds=90))
        except errors.ValidationError as validationError:
            for authz in validationError.failed_authzrs:
                for chall in authz.body.challenges:
                    if chall.error:
                        current_app.logger.error(
                            "ValidationError occured of type {}, with message {}"
                            .format(chall.error.typ,
                                    ERROR_CODES[chall.error.code]))
            raise Exception(
                'Validation error occured, can\'t complete challenges. See logs for more information.'
            )

        pem_certificate, pem_certificate_chain = self.acme.extract_cert_and_chain(
            finalized_orderr.fullchain_pem)

        if len(deployed_challenges) != 0:
            for token_path in deployed_challenges:
                self.cleanup(token_path, validation_target)

        # validation is a random string, we use it as external id, to make it possible to implement revoke_certificate
        return pem_certificate, pem_certificate_chain, None
Ejemplo n.º 5
0
class AcmeHttpChallenge(AcmeChallenge):
    challengeType = challenges.HTTP01

    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")
        acme_client, registration = self.acme.setup_acme_client(authority)

        orderr = acme_client.new_order(csr)

        chall = []
        deployed_challenges = []
        all_pre_validated = True
        for authz in orderr.authorizations:
            # Choosing challenge.
            if authz.body.status != STATUS_VALID:
                all_pre_validated = False
                # 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):
                        chall.append(i)
            else:
                metrics.send("get_acme_challenges_already_valid", "counter", 1)
                log_data = {
                    "message": "already validated, skipping",
                    "hostname": authz.body.identifier.value
                }
                current_app.logger.info(log_data)

        if len(chall) == 0 and not all_pre_validated:
            raise Exception(
                'HTTP-01 challenge was not offered by the CA server at {}'.
                format(orderr.uri))
        elif not all_pre_validated:
            validation_target = None
            for option in json.loads(issuer_options["authority"].options):
                if option["name"] == "tokenDestination":
                    validation_target = option["value"]

            if validation_target is None:
                raise Exception(
                    'No token_destination configured for this authority. Cant complete HTTP-01 challenge'
                )

            for challenge in chall:
                try:
                    response = self.deploy(challenge, acme_client,
                                           validation_target)
                    deployed_challenges.append(challenge.chall.path)
                    acme_client.answer_challenge(challenge, response)
                except Exception as e:
                    current_app.logger.error(e)
                    raise Exception(
                        'Failure while trying to deploy token to configure destination. See logs for more information'
                    )

            current_app.logger.info(
                "Uploaded HTTP-01 challenge tokens, trying to poll and finalize the order"
            )

        try:
            deadline = datetime.now() + timedelta(seconds=90)
            orderr = acme_client.poll_authorizations(orderr, deadline)
            finalized_orderr = acme_client.finalize_order(
                orderr, deadline, fetch_alternative_chains=True)

        except errors.ValidationError as validationError:
            for authz in validationError.failed_authzrs:
                for chall in authz.body.challenges:
                    if chall.error:
                        current_app.logger.error(
                            "ValidationError occured of type {}, with message {}"
                            .format(chall.error.typ,
                                    ERROR_CODES[chall.error.code]))
            raise Exception(
                'Validation error occured, can\'t complete challenges. See logs for more information.'
            )

        pem_certificate, pem_certificate_chain = self.acme.extract_cert_and_chain(
            finalized_orderr.fullchain_pem,
            finalized_orderr.alternative_fullchains_pem)
        acme_uri = acme_client.client.net.account.uri.replace('https://', '')
        self.acme.log_remaining_validation(finalized_orderr.authorizations,
                                           acme_uri)

        if len(deployed_challenges) != 0:
            for token_path in deployed_challenges:
                self.cleanup(token_path, validation_target)

        # validation is a random string, we use it as external id, to make it possible to implement revoke_certificate
        return pem_certificate, pem_certificate_chain, None

    def deploy(self, challenge, acme_client, validation_target):

        if not isinstance(challenge.chall, challenges.HTTP01):
            raise AcmeChallengeMissmatchError(
                'The provided challenge is not of type HTTP01, but instead of type {}'
                .format(challenge.__class__.__name__))

        destination = destination_service.get(validation_target)

        if destination is None:
            raise Exception(
                'Couldn\'t find the destination with name {}. Cant complete HTTP01 challenge'
                .format(validation_target))

        destination_plugin = plugins.get(destination.plugin_name)

        response, validation = challenge.response_and_validation(
            acme_client.net.key)

        destination_plugin.upload_acme_token(challenge.chall.path, validation,
                                             destination.options)
        current_app.logger.info("Uploaded HTTP-01 challenge token.")

        return response

    def cleanup(self, token_path, validation_target):
        destination = destination_service.get(validation_target)

        if destination is None:
            current_app.logger.info(
                'Couldn\'t find the destination with name {}, won\'t cleanup the challenge'
                .format(validation_target))

        destination_plugin = plugins.get(destination.plugin_name)

        destination_plugin.delete_acme_token(token_path, destination.options)
        current_app.logger.info("Cleaned up HTTP-01 challenge token.")
Ejemplo n.º 6
0
 def revoke_certificate(self, certificate, comments):
     self.acme = AcmeHandler()
     return self.acme.revoke_certificate(certificate)
Ejemplo n.º 7
0
 def get_all_zones(self, dns_provider):
     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)