示例#1
0
def renew_all_lineages(config):
    """Examine each lineage; renew if due and report results"""

    if config.domains != []:
        raise errors.Error("Currently, the renew verb is only capable of "
                           "renewing all installed certificates that are due "
                           "to be renewed; individual domains cannot be "
                           "specified with this action. If you would like to "
                           "renew specific certificates, use the certonly "
                           "command. The renew verb may provide other options "
                           "for selecting certificates to renew in the future.")
    renewer_config = configuration.RenewerConfiguration(config)
    renew_successes = []
    renew_failures = []
    renew_skipped = []
    parse_failures = []
    for renewal_file in renewal_conf_files(renewer_config):
        print("Processing " + renewal_file)
        lineage_config = copy.deepcopy(config)

        # Note that this modifies config (to add back the configuration
        # elements from within the renewal configuration file).
        try:
            renewal_candidate = _reconstitute(lineage_config, renewal_file)
        except Exception as e:  # pylint: disable=broad-except
            logger.warning("Renewal configuration file %s produced an "
                           "unexpected error: %s. Skipping.", renewal_file, e)
            logger.debug("Traceback was:\n%s", traceback.format_exc())
            parse_failures.append(renewal_file)
            continue

        try:
            if renewal_candidate is None:
                parse_failures.append(renewal_file)
            else:
                # XXX: ensure that each call here replaces the previous one
                zope.component.provideUtility(lineage_config)
                if should_renew(lineage_config, renewal_candidate):
                    plugins = plugins_disco.PluginsRegistry.find_all()
                    from letsencrypt import main
                    main.obtain_cert(lineage_config, plugins, renewal_candidate)
                    renew_successes.append(renewal_candidate.fullchain)
                else:
                    renew_skipped.append(renewal_candidate.fullchain)
        except Exception as e:  # pylint: disable=broad-except
            # obtain_cert (presumably) encountered an unanticipated problem.
            logger.warning("Attempting to renew cert from %s produced an "
                           "unexpected error: %s. Skipping.", renewal_file, e)
            logger.debug("Traceback was:\n%s", traceback.format_exc())
            renew_failures.append(renewal_candidate.fullchain)

    # Describe all the results
    _renew_describe_results(config, renew_successes, renew_failures,
                            renew_skipped, parse_failures)

    if renew_failures or parse_failures:
        raise errors.Error("{0} renew failure(s), {1} parse failure(s)".format(
            len(renew_failures), len(parse_failures)))
    else:
        logger.debug("no renewal failures")
示例#2
0
    def _obtain_certificate(self, domains, csr):
        """Obtain certificate.

        Internal function with precondition that `domains` are
        consistent with identifiers present in the `csr`.

        :param list domains: Domain names.
        :param .le_util.CSR csr: DER-encoded Certificate Signing
            Request. The key used to generate this CSR can be different
            than `authkey`.

        :returns: `.CertificateResource` and certificate chain (as
            returned by `.fetch_chain`).
        :rtype: tuple

        """
        if self.auth_handler is None:
            msg = ("Unable to obtain certificate because authenticator is "
                   "not set.")
            logger.warning(msg)
            raise errors.Error(msg)
        if self.account.regr is None:
            raise errors.Error("Please register with the ACME server first.")

        logger.debug("CSR: %s, domains: %s", csr, domains)

        authzr = self.auth_handler.get_authorizations(domains)
        certr = self.acme.request_issuance(
            jose.ComparableX509(
                OpenSSL.crypto.load_certificate_request(
                    OpenSSL.crypto.FILETYPE_ASN1, csr.data)), authzr)
        return certr, self.acme.fetch_chain(certr)
示例#3
0
    def parse_args(self):
        """Parses command line arguments and returns the result.

        :returns: parsed command line arguments
        :rtype: argparse.Namespace

        """
        parsed_args = self.parser.parse_args(self.args)
        parsed_args.func = self.VERBS[self.verb]
        parsed_args.verb = self.verb

        # Do any post-parsing homework here

        # we get domains from -d, but also from the webroot map...
        if parsed_args.webroot_map:
            for domain in parsed_args.webroot_map.keys():
                if domain not in parsed_args.domains:
                    parsed_args.domains.append(domain)

        if parsed_args.staging or parsed_args.dry_run:
            if parsed_args.server not in (flag_default("server"),
                                          constants.STAGING_URI):
                conflicts = ["--staging"] if parsed_args.staging else []
                conflicts += ["--dry-run"] if parsed_args.dry_run else []
                if not self.detect_defaults:
                    raise errors.Error(
                        "--server value conflicts with {0}".format(
                            " and ".join(conflicts)))

            parsed_args.server = constants.STAGING_URI

            if parsed_args.dry_run:
                if self.verb not in ["certonly", "renew"]:
                    raise errors.Error(
                        "--dry-run currently only works with the "
                        "'certonly' or 'renew' subcommands (%r)" % self.verb)
                parsed_args.break_my_certs = parsed_args.staging = True
                if glob.glob(
                        os.path.join(parsed_args.config_dir,
                                     constants.ACCOUNTS_DIR, "*")):
                    # The user has a prod account, but might not have a staging
                    # one; we don't want to start trying to perform interactive registration
                    parsed_args.agree_tos = True
                    parsed_args.register_unsafely_without_email = True

        if parsed_args.csr:
            if parsed_args.allow_subset_of_names:
                raise errors.Error("--allow-subset-of-names "
                                   "cannot be used with --csr")
            self.handle_csr(parsed_args)

        if self.detect_defaults:  # plumbing
            parsed_args.store_false_vars = self.store_false_vars

        hooks.validate_hooks(parsed_args)

        return parsed_args
示例#4
0
    def handle_csr(self, parsed_args):
        """
        Process a --csr flag. This needs to happen early enough that the
        webroot plugin can know about the calls to process_domain
        """
        if parsed_args.verb != "certonly":
            raise errors.Error("Currently, a CSR file may only be specified "
                               "when obtaining a new or replacement "
                               "via the certonly command. Please try the "
                               "certonly command instead.")

        try:
            csr = le_util.CSR(file=parsed_args.csr[0],
                              data=parsed_args.csr[1],
                              form="der")
            typ = OpenSSL.crypto.FILETYPE_ASN1
            domains = crypto_util.get_sans_from_csr(
                csr.data, OpenSSL.crypto.FILETYPE_ASN1)
        except OpenSSL.crypto.Error:
            try:
                e1 = traceback.format_exc()
                typ = OpenSSL.crypto.FILETYPE_PEM
                csr = le_util.CSR(file=parsed_args.csr[0],
                                  data=parsed_args.csr[1],
                                  form="pem")
                domains = crypto_util.get_sans_from_csr(csr.data, typ)
            except OpenSSL.crypto.Error:
                logger.debug("DER CSR parse error %s", e1)
                logger.debug("PEM CSR parse error %s", traceback.format_exc())
                raise errors.Error("Failed to parse CSR file: {0}".format(
                    parsed_args.csr[0]))
        for d in domains:
            process_domain(parsed_args, d)

        for d in domains:
            sanitised = le_util.enforce_domain_sanity(d)
            if d.lower() != sanitised:
                raise errors.ConfigurationError(
                    "CSR domain {0} needs to be sanitised to {1}.".format(
                        d, sanitised))

        if not domains:
            # TODO: add CN to domains instead:
            raise errors.Error(
                "Unfortunately, your CSR %s needs to have a SubjectAltName for every domain"
                % parsed_args.csr[0])

        parsed_args.actual_csr = (csr, typ)
        csr_domains, config_domains = set(domains), set(parsed_args.domains)
        if csr_domains != config_domains:
            raise errors.ConfigurationError(
                "Inconsistent domain requests:\nFrom the CSR: {0}\nFrom command line/config: {1}"
                .format(", ".join(csr_domains), ", ".join(config_domains)))
示例#5
0
    def deploy_certificate(self, domains, privkey_path, cert_path, chain_path,
                           fullchain_path):
        """Install certificate

        :param list domains: list of domains to install the certificate
        :param str privkey_path: path to certificate private key
        :param str cert_path: certificate file path (optional)
        :param str chain_path: chain file path

        """
        if self.installer is None:
            logger.warning("No installer specified, client is unable to deploy"
                           "the certificate")
            raise errors.Error("No installer available")

        chain_path = None if chain_path is None else os.path.abspath(
            chain_path)

        with error_handler.ErrorHandler(self.installer.recovery_routine):
            for dom in domains:
                # TODO: Provide a fullchain reference for installers like
                #       nginx that want it
                self.installer.deploy_cert(
                    domain=dom,
                    cert_path=os.path.abspath(cert_path),
                    key_path=os.path.abspath(privkey_path),
                    chain_path=chain_path,
                    fullchain_path=fullchain_path)

            self.installer.save("Deployed Let's Encrypt Certificate")
            # sites may have been enabled / final cleanup
            self.installer.restart()
示例#6
0
def _restore_required_config_elements(config, renewalparams):
    """Sets non-plugin specific values in config from renewalparams

    :param configuration.NamespaceConfig config: configuration for the
        current lineage
    :param configobj.Section renewalparams: parameters from the renewal
        configuration file that defines this lineage

    """
    # string-valued items to add if they're present
    for config_item in STR_CONFIG_ITEMS:
        if config_item in renewalparams and not cli.set_by_cli(config_item):
            value = renewalparams[config_item]
            # Unfortunately, we've lost type information from ConfigObj,
            # so we don't know if the original was NoneType or str!
            if value == "None":
                value = None
            setattr(config.namespace, config_item, value)
    # int-valued items to add if they're present
    for config_item in INT_CONFIG_ITEMS:
        if config_item in renewalparams and not cli.set_by_cli(config_item):
            config_value = renewalparams[config_item]
            # the default value for http01_port was None during private beta
            if config_item == "http01_port" and config_value == "None":
                logger.info("updating legacy http01_port value")
                int_value = cli.flag_default("http01_port")
            else:
                try:
                    int_value = int(config_value)
                except ValueError:
                    raise errors.Error(
                        "Expected a numeric value for {0}".format(config_item))
            setattr(config.namespace, config_item, int_value)
示例#7
0
def challb_to_achall(challb, key, domain):
    """Converts a ChallengeBody object to an AnnotatedChallenge.

    :param challb: ChallengeBody
    :type challb: :class:`acme.messages.ChallengeBody`

    :param key: Key
    :type key: :class:`letsencrypt.le_util.Key`

    :param str domain: Domain of the challb

    :returns: Appropriate AnnotatedChallenge
    :rtype: :class:`letsencrypt.achallenges.AnnotatedChallenge`

    """
    chall = challb.chall
    logger.info("%s challenge for %s", chall.typ, domain)

    if isinstance(chall, challenges.DVSNI):
        return achallenges.DVSNI(challb=challb, domain=domain, key=key)
    elif isinstance(chall, challenges.SimpleHTTP):
        return achallenges.SimpleHTTP(challb=challb, domain=domain, key=key)
    elif isinstance(chall, challenges.DNS):
        return achallenges.DNS(challb=challb, domain=domain)
    elif isinstance(chall, challenges.RecoveryToken):
        return achallenges.RecoveryToken(challb=challb, domain=domain)
    elif isinstance(chall, challenges.RecoveryContact):
        return achallenges.RecoveryContact(challb=challb, domain=domain)
    elif isinstance(chall, challenges.ProofOfPossession):
        return achallenges.ProofOfPossession(challb=challb, domain=domain)

    else:
        raise errors.Error("Received unsupported challenge of type: %s",
                           chall.typ)
示例#8
0
    def enhance_config(self, domains, redirect=None):
        """Enhance the configuration.

        .. todo:: This needs to handle the specific enhancements offered by the
            installer. We will also have to find a method to pass in the chosen
            values efficiently.

        :param list domains: list of domains to configure

        :param redirect: If traffic should be forwarded from HTTP to HTTPS.
        :type redirect: bool or None

        :raises .errors.Error: if no installer is specified in the
            client.

        """
        if self.installer is None:
            logger.warning("No installer is specified, there isn't any "
                           "configuration to enhance.")
            raise errors.Error("No installer available")

        if redirect is None:
            redirect = enhancements.ask("redirect")

        # When support for more enhancements are added, the call to the
        # plugin's `enhance` function should be wrapped by an ErrorHandler
        if redirect:
            self.redirect_to_ssl(domains)
示例#9
0
    def deploy_certificate(self, domains, privkey_path,
                           cert_path, chain_path, fullchain_path):
        """Install certificate

        :param list domains: list of domains to install the certificate
        :param str privkey_path: path to certificate private key
        :param str cert_path: certificate file path (optional)
        :param str chain_path: chain file path

        """
        if self.installer is None:
            logger.warning("No installer specified, client is unable to deploy"
                           "the certificate")
            raise errors.Error("No installer available")

        chain_path = None if chain_path is None else os.path.abspath(chain_path)

        with error_handler.ErrorHandler(self.installer.recovery_routine):
            for dom in domains:
                self.installer.deploy_cert(
                    domain=dom, cert_path=os.path.abspath(cert_path),
                    key_path=os.path.abspath(privkey_path),
                    chain_path=chain_path,
                    fullchain_path=fullchain_path)
                self.installer.save()  # needed by the Apache plugin

            self.installer.save("Deployed Let's Encrypt Certificate")

        msg = ("We were unable to install your certificate, "
               "however, we successfully restored your "
               "server to its prior configuration.")
        with error_handler.ErrorHandler(self._rollback_and_restart, msg):
            # sites may have been enabled / final cleanup
            self.installer.restart()
示例#10
0
def make_or_verify_dir(directory, mode=0o755, uid=0, strict=False):
    """Make sure directory exists with proper permissions.

    :param str directory: Path to a directory.
    :param int mode: Directory mode.
    :param int uid: Directory owner.

    :raises .errors.Error: if a directory already exists,
        but has wrong permissions or owner

    :raises OSError: if invalid or inaccessible file names and
        paths, or other arguments that have the correct type,
        but are not accepted by the operating system.

    """
    try:
        os.makedirs(directory, mode)
    except OSError as exception:
        if exception.errno == errno.EEXIST:
            if strict and not check_permissions(directory, mode, uid):
                raise errors.Error(
                    "%s exists, but it should be owned by user %d with"
                    "permissions %s" % (directory, uid, oct(mode)))
        else:
            raise
示例#11
0
def _auth_from_domains(le_client, config, domains, plugins):
    """Authenticate and enroll certificate."""
    # Note: This can raise errors... caught above us though.
    lineage = _treat_as_renewal(config, domains)

    if lineage is not None:
        # TODO: schoen wishes to reuse key - discussion
        # https://github.com/letsencrypt/letsencrypt/pull/777/files#r40498574
        new_certr, new_chain, new_key, _ = le_client.obtain_certificate(
            domains)
        # TODO: Check whether it worked! <- or make sure errors are thrown (jdk)
        lineage.save_successor(
            lineage.latest_common_version(),
            OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM,
                                            new_certr.body), new_key.pem,
            crypto_util.dump_pyopenssl_chain(new_chain))

        lineage.update_all_links_to(lineage.latest_common_version())
        # TODO: Check return value of save_successor
        # TODO: Also update lineage renewal config with any relevant
        #       configuration values from this attempt? <- Absolutely (jdkasten)
    else:
        # TREAT AS NEW REQUEST
        lineage = le_client.obtain_and_enroll_certificate(domains, plugins)
        if not lineage:
            raise errors.Error("Certificate could not be obtained")

    _report_new_cert(lineage.cert, lineage.fullchain)

    return lineage
示例#12
0
    def test_handle_exception(self, mock_sys):
        # pylint: disable=protected-access
        from acme import messages

        args = mock.MagicMock()
        mock_open = mock.mock_open()

        with mock.patch('letsencrypt.cli.open', mock_open, create=True):
            exception = Exception('detail')
            args.verbose_count = 1
            cli._handle_exception(Exception,
                                  exc_value=exception,
                                  trace=None,
                                  args=None)
            mock_open().write.assert_called_once_with(''.join(
                traceback.format_exception_only(Exception, exception)))
            error_msg = mock_sys.exit.call_args_list[0][0][0]
            self.assertTrue('unexpected error' in error_msg)

        with mock.patch('letsencrypt.cli.open', mock_open, create=True):
            mock_open.side_effect = [KeyboardInterrupt]
            error = errors.Error('detail')
            cli._handle_exception(errors.Error,
                                  exc_value=error,
                                  trace=None,
                                  args=None)
            # assert_any_call used because sys.exit doesn't exit in cli.py
            mock_sys.exit.assert_any_call(''.join(
                traceback.format_exception_only(errors.Error, error)))

        exception = messages.Error(detail='alpha',
                                   typ='urn:acme:error:triffid',
                                   title='beta')
        args = mock.MagicMock(debug=False, verbose_count=-3)
        cli._handle_exception(messages.Error,
                              exc_value=exception,
                              trace=None,
                              args=args)
        error_msg = mock_sys.exit.call_args_list[-1][0][0]
        self.assertTrue('unexpected error' in error_msg)
        self.assertTrue('acme:error' not in error_msg)
        self.assertTrue('alpha' in error_msg)
        self.assertTrue('beta' in error_msg)
        args = mock.MagicMock(debug=False, verbose_count=1)
        cli._handle_exception(messages.Error,
                              exc_value=exception,
                              trace=None,
                              args=args)
        error_msg = mock_sys.exit.call_args_list[-1][0][0]
        self.assertTrue('unexpected error' in error_msg)
        self.assertTrue('acme:error' in error_msg)
        self.assertTrue('alpha' in error_msg)

        interrupt = KeyboardInterrupt('detail')
        cli._handle_exception(KeyboardInterrupt,
                              exc_value=interrupt,
                              trace=None,
                              args=None)
        mock_sys.exit.assert_called_with(''.join(
            traceback.format_exception_only(KeyboardInterrupt, interrupt)))
示例#13
0
    def _perform_single(self, achall):
        # same path for each challenge response would be easier for
        # users, but will not work if multiple domains point at the
        # same server: default command doesn't support virtual hosts
        response, validation = achall.response_and_validation()

        port = (response.port if self.config.http01_port is None else int(
            self.config.http01_port))
        command = self.CMD_TEMPLATE.format(
            root=self._root,
            achall=achall,
            response=response,
            # TODO(kuba): pipes still necessary?
            validation=pipes.quote(validation),
            encoded_token=achall.chall.encode("token"),
            port=port)
        if self.conf("test-mode"):
            logger.debug("Test mode. Executing the manual command: %s",
                         command)
            # sh shipped with OS X does't support echo -n, but supports printf
            try:
                self._httpd = subprocess.Popen(
                    command,
                    # don't care about setting stdout and stderr,
                    # we're in test mode anyway
                    shell=True,
                    executable=None,
                    # "preexec_fn" is UNIX specific, but so is "command"
                    preexec_fn=os.setsid)
            except OSError as error:  # ValueError should not happen!
                logger.debug("Couldn't execute manual command: %s",
                             error,
                             exc_info=True)
                return False
            logger.debug("Manual command running as PID %s.", self._httpd.pid)
            # give it some time to bootstrap, before we try to verify
            # (cert generation in case of simpleHttpS might take time)
            self._test_mode_busy_wait(port)
            if self._httpd.poll() is not None:
                raise errors.Error("Couldn't execute manual command")
        else:
            if not self.conf("public-ip-logging-ok"):
                if not zope.component.getUtility(interfaces.IDisplay).yesno(
                        self.IP_DISCLAIMER, "Yes", "No"):
                    raise errors.PluginError(
                        "Must agree to IP logging to proceed")

            self._notify_and_wait(
                self.MESSAGE_TEMPLATE.format(validation=validation,
                                             response=response,
                                             uri=achall.chall.uri(
                                                 achall.domain),
                                             command=command))

        if not response.simple_verify(achall.chall, achall.domain,
                                      achall.account_key.public_key(),
                                      self.config.http01_port):
            logger.warning("Self-verify of challenge failed.")

        return response
示例#14
0
    def __init__(self, namespace):
        self.namespace = namespace

        if self.simple_http_port == self.dvsni_port:
            raise errors.Error("Trying to run SimpleHTTP and DVSNI "
                               "on the same port ({0})".format(
                                   self.dvsni_port))
示例#15
0
def _auth_from_domains(le_client, config, domains, plugins):
    """Authenticate and enroll certificate."""
    # Note: This can raise errors... caught above us though.
    lineage = _treat_as_renewal(config, domains)

    if lineage is not None:
        # TODO: schoen wishes to reuse key - discussion
        # https://github.com/letsencrypt/letsencrypt/pull/777/files#r40498574
        new_certr, new_chain, new_key, _ = le_client.obtain_certificate(
            domains)
        # TODO: Check whether it worked! <- or make sure errors are thrown (jdk)
        lineage.save_successor(
            lineage.latest_common_version(),
            OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM,
                                            new_certr.body), new_key.pem,
            crypto_util.dump_pyopenssl_chain(new_chain))

        lineage.update_all_links_to(lineage.latest_common_version())
        # TODO: Check return value of save_successor
        # TODO: Also update lineage renewal config with any relevant
        #       configuration values from this attempt? <- Absolutely (jdkasten)
    else:
        # TREAT AS NEW REQUEST
        lineage = le_client.obtain_and_enroll_certificate(domains, plugins)
        if not lineage:
            raise errors.Error("Certificate could not be obtained")

    _report_new_cert(lineage.cert)
    reporter_util = zope.component.getUtility(interfaces.IReporter)
    reporter_util.add_message(
        "Your certificate will expire on {0}. To obtain a new version of the "
        "certificate in the future, simply run this client again.".format(
            lineage.notafter().date()), reporter_util.MEDIUM_PRIORITY)

    return lineage
示例#16
0
def main(cli_args=sys.argv[1:]):
    """Command line argument parsing and main script execution."""
    sys.excepthook = functools.partial(_handle_exception, args=None)

    # note: arg parser internally handles --help (and exits afterwards)
    plugins = plugins_disco.PluginsRegistry.find_all()
    parser, tweaked_cli_args = create_parser(plugins, cli_args)
    args = parser.parse_args(tweaked_cli_args)
    config = configuration.NamespaceConfig(args)
    zope.component.provideUtility(config)

    # Setup logging ASAP, otherwise "No handlers could be found for
    # logger ..." TODO: this should be done before plugins discovery
    for directory in config.config_dir, config.work_dir:
        le_util.make_or_verify_dir(
            directory, constants.CONFIG_DIRS_MODE, os.geteuid(),
            "--strict-permissions" in cli_args)
    # TODO: logs might contain sensitive data such as contents of the
    # private key! #525
    le_util.make_or_verify_dir(
        args.logs_dir, 0o700, os.geteuid(), "--strict-permissions" in cli_args)
    _setup_logging(args)

    # do not log `args`, as it contains sensitive data (e.g. revoke --key)!
    logger.debug("Arguments: %r", cli_args)
    logger.debug("Discovered plugins: %r", plugins)

    sys.excepthook = functools.partial(_handle_exception, args=args)

    # Displayer
    if args.text_mode:
        displayer = display_util.FileDisplay(sys.stdout)
    else:
        displayer = display_util.NcursesDisplay()
    zope.component.provideUtility(displayer)

    # Reporter
    report = reporter.Reporter()
    zope.component.provideUtility(report)
    atexit.register(report.atexit_print_messages)

    # TODO: remove developer EULA prompt for the launch
    if not config.eula:
        eula = pkg_resources.resource_string("letsencrypt", "EULA")
        if not zope.component.getUtility(interfaces.IDisplay).yesno(
                eula, "Agree", "Cancel"):
            raise errors.Error("Must agree to TOS")

    if not os.geteuid() == 0:
        logger.warning(
            "Root (sudo) is required to run most of letsencrypt functionality.")
        # check must be done after arg parsing as --help should work
        # w/o root; on the other hand, e.g. "letsencrypt run
        # --authenticator dns" or "letsencrypt plugins" does not
        # require root as well
        #return (
        #    "{0}Root is required to run letsencrypt.  Please use sudo.{0}"
        #    .format(os.linesep))

    return args.func(args, config, plugins)
示例#17
0
    def __init__(self, namespace):
        self.namespace = namespace

        if self.http01_port == self.dvsni_port:
            raise errors.Error(
                "Trying to run http-01 and DVSNI "
                "on the same port ({0})".format(self.dvsni_port))
示例#18
0
    def test_handle_exception(self, mock_sys):
        # pylint: disable=protected-access
        from letsencrypt import cli

        mock_open = mock.mock_open()
        with mock.patch("letsencrypt.cli.open", mock_open, create=True):
            exception = Exception("detail")
            cli._handle_exception(
                Exception, exc_value=exception, trace=None, args=None)
            mock_open().write.assert_called_once_with("".join(
                traceback.format_exception_only(Exception, exception)))
            error_msg = mock_sys.exit.call_args_list[0][0][0]
            self.assertTrue("unexpected error" in error_msg)

        with mock.patch("letsencrypt.cli.open", mock_open, create=True):
            mock_open.side_effect = [KeyboardInterrupt]
            error = errors.Error("detail")
            cli._handle_exception(
                errors.Error, exc_value=error, trace=None, args=None)
            # assert_any_call used because sys.exit doesn't exit in cli.py
            mock_sys.exit.assert_any_call("".join(
                traceback.format_exception_only(errors.Error, error)))

        args = mock.MagicMock(debug=False)
        cli._handle_exception(
            Exception, exc_value=Exception("detail"), trace=None, args=args)
        error_msg = mock_sys.exit.call_args_list[-1][0][0]
        self.assertTrue("unexpected error" in error_msg)

        interrupt = KeyboardInterrupt("detail")
        cli._handle_exception(
            KeyboardInterrupt, exc_value=interrupt, trace=None, args=None)
        mock_sys.exit.assert_called_with("".join(
            traceback.format_exception_only(KeyboardInterrupt, interrupt)))
示例#19
0
def challb_to_achall(challb, account_key, domain):
    """Converts a ChallengeBody object to an AnnotatedChallenge.

    :param .ChallengeBody challb: ChallengeBody
    :param .JWK account_key: Authorized Account Key
    :param str domain: Domain of the challb

    :returns: Appropriate AnnotatedChallenge
    :rtype: :class:`letsencrypt.achallenges.AnnotatedChallenge`

    """
    chall = challb.chall
    logger.info("%s challenge for %s", chall.typ, domain)

    if isinstance(chall, challenges.KeyAuthorizationChallenge):
        return achallenges.KeyAuthorizationAnnotatedChallenge(
            challb=challb, domain=domain, account_key=account_key)
    elif isinstance(chall, challenges.DNS):
        return achallenges.DNS(challb=challb, domain=domain)
    elif isinstance(chall, challenges.RecoveryContact):
        return achallenges.RecoveryContact(challb=challb, domain=domain)
    elif isinstance(chall, challenges.ProofOfPossession):
        return achallenges.ProofOfPossession(challb=challb, domain=domain)
    else:
        raise errors.Error("Received unsupported challenge of type: %s",
                           chall.typ)
示例#20
0
    def _from_config_fp(cls, config, config_fp):
        try:
            acc_config = configobj.ConfigObj(
                infile=config_fp, file_error=True, create_empty=False)
        except IOError:
            raise errors.Error(
                "Account for %s does not exist" % os.path.basename(config_fp))

        if os.path.basename(config_fp) != "default":
            email = os.path.basename(config_fp)
        else:
            email = None
        phone = acc_config["phone"] if acc_config["phone"] != "None" else None

        with open(acc_config["key"]) as key_file:
            key = le_util.Key(acc_config["key"], key_file.read())

        if "RegistrationResource" in acc_config:
            acc_config_rr = acc_config["RegistrationResource"]
            regr = messages.RegistrationResource(
                uri=acc_config_rr["uri"],
                new_authzr_uri=acc_config_rr["new_authzr_uri"],
                terms_of_service=acc_config_rr["terms_of_service"],
                body=messages.Registration.from_json(acc_config_rr["body"]))
        else:
            regr = None

        return cls(config, key, email, phone, regr)
示例#21
0
    def _perform_single(self, achall):
        # same path for each challenge response would be easier for
        # users, but will not work if multiple domains point at the
        # same server: default command doesn't support virtual hosts
        response, validation = achall.gen_response_and_validation(
            tls=False)  # SimpleHTTP TLS is dead: ietf-wg-acme/acme#7

        port = (response.port if self.config.simple_http_port is None else int(
            self.config.simple_http_port))
        command = self.CMD_TEMPLATE.format(
            root=self._root,
            achall=achall,
            response=response,
            validation=pipes.quote(validation.json_dumps()),
            encoded_token=achall.chall.encode("token"),
            ct=response.CONTENT_TYPE,
            port=port)
        if self.conf("test-mode"):
            logger.debug("Test mode. Executing the manual command: %s",
                         command)
            try:
                self._httpd = subprocess.Popen(
                    command,
                    # don't care about setting stdout and stderr,
                    # we're in test mode anyway
                    shell=True,
                    # "preexec_fn" is UNIX specific, but so is "command"
                    preexec_fn=os.setsid)
            except OSError as error:  # ValueError should not happen!
                logger.debug("Couldn't execute manual command: %s",
                             error,
                             exc_info=True)
                return False
            logger.debug("Manual command running as PID %s.", self._httpd.pid)
            # give it some time to bootstrap, before we try to verify
            # (cert generation in case of simpleHttpS might take time)
            self._test_mode_busy_wait(port)
            if self._httpd.poll() is not None:
                raise errors.Error("Couldn't execute manual command")
        else:
            self._notify_and_wait(
                self.MESSAGE_TEMPLATE.format(
                    validation=validation.json_dumps(),
                    response=response,
                    uri=response.uri(achall.domain, achall.challb.chall),
                    ct=response.CONTENT_TYPE,
                    command=command))

        if response.simple_verify(achall.chall, achall.domain,
                                  achall.account_key.public_key(),
                                  self.config.simple_http_port):
            return response
        else:
            logger.error(
                "Self-verify of challenge failed, authorization abandoned.")
            if self.conf("test-mode") and self._httpd.poll() is not None:
                # simply verify cause command failure...
                return False
            return None
示例#22
0
    def enhance_config(self, domains, config):
        """Enhance the configuration.

        :param list domains: list of domains to configure

        :ivar config: Namespace typically produced by
            :meth:`argparse.ArgumentParser.parse_args`.
            it must have the redirect, hsts and uir attributes.
        :type namespace: :class:`argparse.Namespace`

        :raises .errors.Error: if no installer is specified in the
            client.

        """

        if self.installer is None:
            logger.warning("No installer is specified, there isn't any "
                           "configuration to enhance.")
            raise errors.Error("No installer available")

        if config is None:
            logger.warning("No config is specified.")
            raise errors.Error("No config available")

        supported = self.installer.supported_enhancements()
        redirect = config.redirect if "redirect" in supported else False
        hsts = config.hsts if "ensure-http-header" in supported else False
        uir = config.uir if "ensure-http-header" in supported else False

        if redirect is None:
            redirect = enhancements.ask("redirect")

        if redirect:
            self.apply_enhancement(domains, "redirect")

        if hsts:
            self.apply_enhancement(domains, "ensure-http-header",
                    "Strict-Transport-Security")
        if uir:
            self.apply_enhancement(domains, "ensure-http-header",
                    "Upgrade-Insecure-Requests")

        msg = ("We were unable to restart web server")
        if redirect or hsts or uir:
            with error_handler.ErrorHandler(self._rollback_and_restart, msg):
                self.installer.restart()
示例#23
0
    def handle_csr(self, parsed_args):
        """Process a --csr flag."""
        if parsed_args.verb != "certonly":
            raise errors.Error("Currently, a CSR file may only be specified "
                               "when obtaining a new or replacement "
                               "via the certonly command. Please try the "
                               "certonly command instead.")

        try:
            csr = le_util.CSR(file=parsed_args.csr[0],
                              data=parsed_args.csr[1],
                              form="der")
            typ = OpenSSL.crypto.FILETYPE_ASN1
            domains = crypto_util.get_sans_from_csr(
                csr.data, OpenSSL.crypto.FILETYPE_ASN1)
        except OpenSSL.crypto.Error:
            try:
                e1 = traceback.format_exc()
                typ = OpenSSL.crypto.FILETYPE_PEM
                csr = le_util.CSR(file=parsed_args.csr[0],
                                  data=parsed_args.csr[1],
                                  form="pem")
                domains = crypto_util.get_sans_from_csr(csr.data, typ)
            except OpenSSL.crypto.Error:
                logger.debug("DER CSR parse error %s", e1)
                logger.debug("PEM CSR parse error %s", traceback.format_exc())
                raise errors.Error("Failed to parse CSR file: {0}".format(
                    parsed_args.csr[0]))

        # This is not necessary for webroot to work, however,
        # obtain_certificate_from_csr requires parsed_args.domains to be set
        for domain in domains:
            add_domains(parsed_args, domain)

        if not domains:
            # TODO: add CN to domains instead:
            raise errors.Error(
                "Unfortunately, your CSR %s needs to have a SubjectAltName for every domain"
                % parsed_args.csr[0])

        parsed_args.actual_csr = (csr, typ)
        csr_domains, config_domains = set(domains), set(parsed_args.domains)
        if csr_domains != config_domains:
            raise errors.ConfigurationError(
                "Inconsistent domain requests:\nFrom the CSR: {0}\nFrom command line/config: {1}"
                .format(", ".join(csr_domains), ", ".join(config_domains)))
示例#24
0
文件: main.py 项目: mej/letsencrypt
def _determine_account(config):
    """Determine which account to use.

    In order to make the renewer (configuration de/serialization) happy,
    if ``config.account`` is ``None``, it will be updated based on the
    user input. Same for ``config.email``.

    :param argparse.Namespace config: CLI arguments
    :param letsencrypt.interface.IConfig config: Configuration object
    :param .AccountStorage account_storage: Account storage.

    :returns: Account and optionally ACME client API (biproduct of new
        registration).
    :rtype: `tuple` of `letsencrypt.account.Account` and
        `acme.client.Client`

    """
    account_storage = account.AccountFileStorage(config)
    acme = None

    if config.account is not None:
        acc = account_storage.load(config.account)
    else:
        accounts = account_storage.find_all()
        if len(accounts) > 1:
            acc = display_ops.choose_account(accounts)
        elif len(accounts) == 1:
            acc = accounts[0]
        else:  # no account registered yet
            if config.email is None and not config.register_unsafely_without_email:
                config.namespace.email = display_ops.get_email()

            def _tos_cb(regr):
                if config.tos:
                    return True
                msg = ("Please read the Terms of Service at {0}. You "
                       "must agree in order to register with the ACME "
                       "server at {1}".format(regr.terms_of_service,
                                              config.server))
                obj = zope.component.getUtility(interfaces.IDisplay)
                return obj.yesno(msg,
                                 "Agree",
                                 "Cancel",
                                 cli_flag="--agree-tos")

            try:
                acc, acme = client.register(config,
                                            account_storage,
                                            tos_cb=_tos_cb)
            except errors.MissingCommandlineFlag:
                raise
            except errors.Error as error:
                logger.debug(error, exc_info=True)
                raise errors.Error(
                    "Unable to register an account with ACME server")

    config.namespace.account = acc.id
    return acc, acme
示例#25
0
def validate_key_csr(privkey, csr=None):
    """Validate Key and CSR files.

    Verifies that the client key and csr arguments are valid and correspond to
    one another. This does not currently check the names in the CSR due to
    the inability to read SANs from CSRs in python crypto libraries.

    If csr is left as None, only the key will be validated.

    :param privkey: Key associated with CSR
    :type privkey: :class:`letsencrypt.le_util.Key`

    :param .le_util.CSR csr: CSR

    :raises .errors.Error: when validation fails

    """
    # TODO: Handle all of these problems appropriately
    # The client can eventually do things like prompt the user
    # and allow the user to take more appropriate actions

    # Key must be readable and valid.
    if privkey.pem and not crypto_util.valid_privkey(privkey.pem):
        raise errors.Error("The provided key is not a valid key")

    if csr:
        if csr.form == "der":
            csr_obj = OpenSSL.crypto.load_certificate_request(
                OpenSSL.crypto.FILETYPE_ASN1, csr.data)
            csr = le_util.CSR(
                csr.file,
                OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM,
                                                csr_obj), "pem")

        # If CSR is provided, it must be readable and valid.
        if csr.data and not crypto_util.valid_csr(csr.data):
            raise errors.Error("The provided CSR is not a valid CSR")

        # If both CSR and key are provided, the key must be the same key used
        # in the CSR.
        if csr.data and privkey.pem:
            if not crypto_util.csr_matches_pubkey(csr.data, privkey.pem):
                raise errors.Error("The key and CSR do not match")
示例#26
0
def _auth_from_domains(le_client, config, domains, lineage=None):
    """Authenticate and enroll certificate."""
    # Note: This can raise errors... caught above us though. This is now
    # a three-way case: reinstall (which results in a no-op here because
    # although there is a relevant lineage, we don't do anything to it
    # inside this function -- we don't obtain a new certificate), renew
    # (which results in treating the request as a renewal), or newcert
    # (which results in treating the request as a new certificate request).

    # If lineage is specified, use that one instead of looking around for
    # a matching one.
    if lineage is None:
        # This will find a relevant matching lineage that exists
        action, lineage = _treat_as_renewal(config, domains)
    else:
        # Renewal, where we already know the specific lineage we're
        # interested in
        action = "renew"

    if action == "reinstall":
        # The lineage already exists; allow the caller to try installing
        # it without getting a new certificate at all.
        return lineage, "reinstall"
    elif action == "renew":
        original_server = lineage.configuration["renewalparams"]["server"]
        _avoid_invalidating_lineage(config, lineage, original_server)
        # TODO: schoen wishes to reuse key - discussion
        # https://github.com/letsencrypt/letsencrypt/pull/777/files#r40498574
        new_certr, new_chain, new_key, _ = le_client.obtain_certificate(
            domains)
        # TODO: Check whether it worked! <- or make sure errors are thrown (jdk)
        if config.dry_run:
            logger.info("Dry run: skipping updating lineage at %s",
                        os.path.dirname(lineage.cert))
        else:
            lineage.save_successor(
                lineage.latest_common_version(),
                OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM,
                                                new_certr.body.wrapped),
                new_key.pem, crypto_util.dump_pyopenssl_chain(new_chain),
                configuration.RenewerConfiguration(config.namespace))
            lineage.update_all_links_to(lineage.latest_common_version())
        # TODO: Check return value of save_successor
        # TODO: Also update lineage renewal config with any relevant
        #       configuration values from this attempt? <- Absolutely (jdkasten)
    elif action == "newcert":
        # TREAT AS NEW REQUEST
        lineage = le_client.obtain_and_enroll_certificate(domains)
        if lineage is False:
            raise errors.Error("Certificate could not be obtained")

    if not config.dry_run and not config.verb == "renew":
        _report_new_cert(lineage.cert, lineage.fullchain)

    return lineage, action
示例#27
0
def _find_domains(config, installer):
    if config.domains:
        domains = config.domains
    else:
        domains = display_ops.choose_names(installer)

    if not domains:
        raise errors.Error("Please specify --domains, or --installer that "
                           "will help in domain names autodiscovery")

    return domains
示例#28
0
def _find_domains(args, installer):
    if args.domains is None:
        domains = display_ops.choose_names(installer)
    else:
        domains = args.domains

    if not domains:
        raise errors.Error("Please specify --domains, or --installer that "
                           "will help in domain names autodiscovery")

    return domains
def _pyopenssl_load(data,
                    method,
                    types=(OpenSSL.crypto.FILETYPE_PEM,
                           OpenSSL.crypto.FILETYPE_ASN1)):
    openssl_errors = []
    for filetype in types:
        try:
            return method(filetype, data), filetype
        except OpenSSL.crypto.Error as error:  # TODO: anything else?
            openssl_errors.append(error)
    raise errors.Error("Unable to load: {0}".format(",".join(
        str(error) for error in openssl_errors)))
示例#30
0
文件: main.py 项目: mej/letsencrypt
def _find_domains(config, installer):
    if not config.domains:
        domains = display_ops.choose_names(installer)
        # record in config.domains (so that it can be serialised in renewal config files),
        # and set webroot_map entries if applicable
        for d in domains:
            cli.process_domain(config, d)
    else:
        domains = config.domains

    if not domains:
        raise errors.Error("Please specify --domains, or --installer that "
                           "will help in domain names autodiscovery")

    return domains