Beispiel #1
0
    def _new_certificate(self, cert_id, key_type_id):
        """Handles new certificate requests. It does the following steps:
            - Generates and persists on disk a private key of key_type_id type
            - Generates and persists a CSR signed by the previously generated key
            - Passes the ball to the next status handler
        """
        logger.info("Handling new certificate event for %s / %s", cert_id,
                    key_type_id)
        cert_details = self.config.certificates[cert_id]
        key_type_details = KEY_TYPES[key_type_id]
        private_key = key_type_details['class']()
        private_key.generate(**key_type_details['params'])
        private_key.save(
            self._get_path(cert_id, key_type_id, public=False, kind='new'))

        csr_filename = '{}.{}.csr.pem'.format(cert_id, key_type_id)
        csr_fullpath = os.path.join(self.csrs_path, csr_filename)
        csr = CertificateSigningRequest(
            private_key=private_key,
            common_name=cert_details['CN'],
            sans=cert_details['SNI'],
        )
        csr.save(csr_fullpath)
        session = self._get_acme_session(cert_details)
        challenges = session.push_csr(csr)
        if not challenges:
            logger.info(
                "Skipping challenge validation for certificate %s / %s",
                cert_id, key_type_id)
            return CertificateStatus.CHALLENGES_PUSHED

        challenge_type = CHALLENGE_TYPES[cert_details['challenge']]
        if challenge_type not in challenges:
            logger.warning(
                "Unable to get required challenge type %s for certificate %s / %s",
                challenge_type, cert_id, key_type_id)
            return CertificateStatus.ACMECHIEF_ERROR
        try:
            for challenge in challenges[challenge_type]:
                challenge.save(
                    os.path.join(self.challenges_path[challenge_type],
                                 challenge.file_name))
        except OSError:
            logger.exception(
                "OSError encountered while saving challenge type %s for certificate %s / %s",
                challenge_type, cert_id, key_type_id)
            return CertificateStatus.ACMECHIEF_ERROR

        if challenge_type == ACMEChallengeType.DNS01:
            if not self._trigger_dns_zone_update(challenges[challenge_type]):
                logger.warning(
                    "Failed to perform DNS zone update for certificate %s / %s",
                    cert_id, key_type_id)
                return CertificateStatus.ACMECHIEF_ERROR

        status = CertificateStatus.CSR_PUSHED
        status = self._handle_pushed_csr(cert_id, key_type_id)

        return status
Beispiel #2
0
    def _handle_order_finalized(self, cert_id, key_type_id):
        """Handles ORDER_FINALIZED status. Performs the following actions:
            - Attempts to fetch the signed certificate from the ACME directory
            - Persists the certificate on disk
        """
        logger.info("Handling order finalized event for %s / %s", cert_id,
                    key_type_id)
        try:
            private_key = PrivateKeyLoader.load(
                self._get_path(cert_id, key_type_id, public=False, kind='new'))
        except (OSError, X509Error):
            logger.exception(
                "Failed to load new private key for certificate %s / %s",
                cert_id, key_type_id)
            return CertificateStatus.ACMECHIEF_ERROR

        cert_details = self.config.certificates[cert_id]

        csr_id = CertificateSigningRequest.generate_csr_id(
            public_key_pem=private_key.public_pem,
            common_name=cert_details['CN'],
            sans=cert_details['SNI'],
        )

        session = self._get_acme_session(cert_details)

        try:
            certificate = session.get_certificate(csr_id)
        except ACMETimeoutFetchingCertificateError:
            logger.exception(
                "Unable to fetch certificate for an already finalized order for certificate %s / %s",
                cert_id, key_type_id)
            return CertificateStatus.ORDER_FINALIZED
        except ACMEIssuedCertificateError:
            logger.warning(
                "Unable to handle certificate issued by the ACME directory for certificate %s / %s",
                cert_id, key_type_id)
            return CertificateStatus.CERTIFICATE_ISSUED
        except ACMEError:
            logger.exception(
                "Problem getting certificate for certificate %s / %s", cert_id,
                key_type_id)
            return CertificateStatus.ACMEDIR_ERROR

        try:
            certificate.save(self._get_path(cert_id,
                                            key_type_id,
                                            public=True,
                                            kind='new',
                                            cert_type='full_chain'),
                             mode=CertificateSaveMode.FULL_CHAIN)
        except OSError:
            logger.exception("Problem persisting certificate %s / %s on disk",
                             cert_id, key_type_id)
            return CertificateStatus.CERTIFICATE_ISSUED

        return self._handle_ready_to_be_pushed(cert_id, key_type_id)
Beispiel #3
0
 def test_csr_without_sans(self):
     pk = RSAPrivateKey()
     pk.generate()
     csr = CertificateSigningRequest(
         private_key=pk,
         common_name="acmechief.test",
         sans=(),
     )
     self.assertFalse(csr.wildcard)
Beispiel #4
0
    def _handle_pushed_csr(self, cert_id, key_type_id):
        """Handles PUSHED_CSR status. Performs the following actions:
            - Checks that challenges have been validated
            - Passes the ball to the next status handle
        """
        logger.info("Handling pushed CSR event for %s / %s", cert_id,
                    key_type_id)
        try:
            private_key = PrivateKeyLoader.load(
                self._get_path(cert_id, key_type_id, public=False, kind='new'))
        except (OSError, X509Error):
            logger.exception(
                "Failed to load new private key for certificate %s / %s",
                cert_id, key_type_id)
            return CertificateStatus.ACMECHIEF_ERROR

        cert_details = self.config.certificates[cert_id]

        csr_id = CertificateSigningRequest.generate_csr_id(
            public_key_pem=private_key.public_pem,
            common_name=cert_details['CN'],
            sans=cert_details['SNI'],
        )

        challenge_type = CHALLENGE_TYPES[cert_details['challenge']]

        session = self._get_acme_session(cert_details)

        try:
            challenges = session.challenges[csr_id][challenge_type]
        except KeyError:
            logger.exception(
                "Could not find challenge for challenge type %s, certificate %s / %s",
                challenge_type, cert_id, key_type_id)
            return CertificateStatus.ACMECHIEF_ERROR

        for challenge in challenges:
            if challenge.challenge_type is ACMEChallengeType.DNS01:
                validation_params = {
                    'dns_servers':
                    self.config.challenges[ACMEChallengeType.DNS01]
                    ['validation_dns_servers']
                }
            else:
                validation_params = {}

            if challenge.validate(
                    **validation_params) is not ACMEChallengeValidation.VALID:
                # keep the issuance process in this step till all the challenges have been validated
                logger.warning("Unable to validate challenge %s", challenge)
                return CertificateStatus.CSR_PUSHED

        status = CertificateStatus.CHALLENGES_VALIDATED
        status = self._handle_validated_challenges(cert_id, key_type_id)

        return status
Beispiel #5
0
    def _handle_pushed_challenges(self, cert_id, key_type_id):  # pylint: disable=too-many-return-statements
        """Handles CHALLENGES_PUSHED status. Performs the following actions:
            - Attempts to finalize the ACME order.
            - Passes the ball to the next status handler.
        """
        logger.info("Handling pushed challenges event for %s / %s", cert_id,
                    key_type_id)
        try:
            private_key = PrivateKeyLoader.load(
                self._get_path(cert_id, key_type_id, public=False, kind='new'))
        except (OSError, X509Error):
            logger.exception(
                "Failed to load new private key for certificate %s / %s",
                cert_id, key_type_id)
            return CertificateStatus.ACMECHIEF_ERROR

        cert_details = self.config.certificates[cert_id]

        csr_id = CertificateSigningRequest.generate_csr_id(
            public_key_pem=private_key.public_pem,
            common_name=cert_details['CN'],
            sans=cert_details['SNI'],
        )

        session = self._get_acme_session(cert_details)
        try:
            session.finalize_order(csr_id)
        except ACMEChallengeNotValidatedError:
            logger.warning(
                "ACME Directory hasn't validated the challenge(s) yet for certificate %s / %s",
                cert_id, key_type_id)
            return CertificateStatus.CHALLENGES_PUSHED
        except ACMEInvalidChallengeError:
            logger.warning(
                "ACME Directory has rejected the challenge(s) for certificate %s / %s",
                cert_id, key_type_id)
            return CertificateStatus.CHALLENGES_REJECTED
        except ACMEOrderNotFound:
            logger.exception(
                "Could not find ACME order when attempting to get the certificate %s / %s",
                cert_id, key_type_id)
            return CertificateStatus.ACMECHIEF_ERROR
        except ACMEError:
            logger.exception(
                "Problem getting certificate for certificate %s / %s", cert_id,
                key_type_id)
            return CertificateStatus.ACMEDIR_ERROR

        status = CertificateStatus.ORDER_FINALIZED
        status = self._handle_order_finalized(cert_id, key_type_id)

        return status
Beispiel #6
0
    def test_CSR_generation(self):
        pk = RSAPrivateKey()
        pk.generate()
        csr_sans = (
            '*.acmechief.test',
            ipaddress.IPv4Address('127.0.0.1'),
            ipaddress.IPv6Address('::1'),
        )
        csr = CertificateSigningRequest(
            private_key=pk,
            common_name="acmechief.test",
            sans=csr_sans,
        )

        self.assertIsInstance(csr.request,
                              crypto_x509.CertificateSigningRequest)
        self.assertIsInstance(csr.request.signature_hash_algorithm,
                              hashes.SHA256)
        self.assertTrue(csr.request.is_signature_valid)
        cn = csr.request.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
        self.assertEqual(len(cn), 1)
        self.assertEqual(cn[0].value, "acmechief.test")
        sans = csr.request.extensions.get_extension_for_oid(
            ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
        self.assertEqual(sans.value.get_values_for_type(crypto_x509.DNSName),
                         ['*.acmechief.test'])
        self.assertEqual(
            sans.value.get_values_for_type(crypto_x509.IPAddress),
            [ipaddress.IPv4Address('127.0.0.1'),
             ipaddress.IPv6Address('::1')])
        self.assertEqual(
            csr.csr_id,
            CertificateSigningRequest.generate_csr_id(pk.public_pem,
                                                      'acmechief.test',
                                                      csr_sans))
        self.assertTrue(csr.wildcard)
Beispiel #7
0
    def _handle_validated_challenges(self, cert_id, key_type_id):
        """Handles CHALLENGES_VALIDATED status. Performs the following actions:
            - pushes solved challenges to the ACME directory
            - Passes the ball to the next status handler
        """
        logger.info("Handling validated challenges event for %s / %s", cert_id,
                    key_type_id)
        try:
            private_key = PrivateKeyLoader.load(
                self._get_path(cert_id, key_type_id, public=False, kind='new'))
        except (OSError, X509Error):
            logger.exception(
                "Failed to load new private key for certificate %s / %s",
                cert_id, key_type_id)
            return CertificateStatus.ACMECHIEF_ERROR

        cert_details = self.config.certificates[cert_id]

        csr_id = CertificateSigningRequest.generate_csr_id(
            public_key_pem=private_key.public_pem,
            common_name=cert_details['CN'],
            sans=cert_details['SNI'],
        )
        session = self._get_acme_session(cert_details)
        challenge_type = CHALLENGE_TYPES[cert_details['challenge']]
        try:
            session.push_solved_challenges(csr_id,
                                           challenge_type=challenge_type)
        except ACMEOrderNotFound:
            # unable to find CSR in current ACME session, go back to the initial step
            logger.exception(
                "Could not find ACME order when pushing solved challenges for challenge type %s, "
                "certificate %s / %s", challenge_type, cert_id, key_type_id)
            return CertificateStatus.ACMECHIEF_ERROR

        try:
            return self._handle_pushed_challenges(cert_id, key_type_id)
        except ACMEOrderNotFound:
            logger.exception(
                "Could not find ACME order when handling pushed challenges for certificate %s / %s",
                cert_id, key_type_id)
            return CertificateStatus.ACMECHIEF_ERROR
        except ACMEError:
            logger.exception(
                "ACMEError when handling pushed challenges for certificate %s / %s",
                cert_id, key_type_id)
            return CertificateStatus.CHALLENGES_PUSHED
Beispiel #8
0
    def test_full_workflow_dns_challenge(self):
        """Expects pebble to be invoked with PEBBLE_VA_ALWAYS_VALID=1"""
        with mock.patch('acme_chief.acme_requests.TLS_VERIFY', False):
            account = ACMEAccount.create('*****@*****.**',
                                         directory_url=DIRECTORY_URL)
            session = ACMERequests(account)
        self.assertIsNotNone(account.regr)

        key = RSAPrivateKey()
        key.generate()
        csr = CertificateSigningRequest(
            private_key=key,
            common_name="tests.wmflabs.org",
            sans=[
                "tests.wmflabs.org",
                "*.tests.wmflabs.org",
            ],
        )

        challenges = session.push_csr(csr)
        self.assertIn(ACMEChallengeType.DNS01, challenges)
        self.assertNotIn(ACMEChallengeType.HTTP01, challenges)

        session.push_solved_challenges(csr.csr_id)
        # pebble adds a random delay on validations, so we need to pull until we
        # get the certificate
        while True:
            self.assertTrue(session.orders)
            try:
                session.finalize_order(csr.csr_id)
                certificate = session.get_certificate(csr.csr_id)
                break
            except ACMEChallengeNotValidatedError:
                pass
        self.assertFalse(session.orders)
        self.assertFalse(certificate.needs_renew())
        self.assertFalse(certificate.self_signed)
        self.assertEqual(len(certificate.chain), 2)
        sans = certificate.certificate.extensions.get_extension_for_oid(
            ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
        self.assertEqual(sans.value.get_values_for_type(crypto_x509.DNSName),
                         ['tests.wmflabs.org', '*.tests.wmflabs.org'])

        # if everything goes as expected this shouldn't raise an exception
        session.revoke_certificate(certificate)