def test_report_renewal_status(self, mock_zope): # pylint: disable=protected-access cert = mock.MagicMock() cert.configuration = configobj.ConfigObj() cert.cli_config = configuration.RenewerConfiguration(self.config) cert.configuration["autorenew"] = "True" cert.configuration["autodeploy"] = "True" self.client._report_renewal_status(cert) msg = mock_zope().add_message.call_args[0][0] self.assertTrue("renewal and deployment has been" in msg) self.assertTrue(cert.cli_config.renewal_configs_dir in msg) cert.configuration["autorenew"] = "False" self.client._report_renewal_status(cert) msg = mock_zope().add_message.call_args[0][0] self.assertTrue("deployment but not automatic renewal" in msg) self.assertTrue(cert.cli_config.renewal_configs_dir in msg) cert.configuration["autodeploy"] = "False" self.client._report_renewal_status(cert) msg = mock_zope().add_message.call_args[0][0] self.assertTrue("renewal and deployment has not" in msg) self.assertTrue(cert.cli_config.renewal_configs_dir in msg) cert.configuration["autorenew"] = "True" self.client._report_renewal_status(cert) msg = mock_zope().add_message.call_args[0][0] self.assertTrue("renewal but not automatic deployment" in msg) self.assertTrue(cert.cli_config.renewal_configs_dir in msg)
def obtain_and_enroll_certificate(self, domains): """Obtain and enroll certificate. Get a new certificate for the specified domains using the specified authenticator and installer, and then create a new renewable lineage containing it. :param list domains: Domains to request. :param plugins: A PluginsFactory object. :returns: A new :class:`letsencrypt.storage.RenewableCert` instance referred to the enrolled cert lineage, False if the cert could not be obtained, or None if doing a successful dry run. """ certr, chain, key, _ = self.obtain_certificate(domains) if (self.config.config_dir != constants.CLI_DEFAULTS["config_dir"] or self.config.work_dir != constants.CLI_DEFAULTS["work_dir"]): logger.warning( "Non-standard path(s), might not work with crontab installed " "by your operating system package manager") if self.config.dry_run: logger.info("Dry run: Skipping creating new lineage for %s", domains[0]) return None else: return storage.RenewableCert.new_lineage( domains[0], OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped), key.pem, crypto_util.dump_pyopenssl_chain(chain), configuration.RenewerConfiguration(self.config.namespace))
def setUp(self): from letsencrypt import storage self.tempdir = tempfile.mkdtemp() self.cli_config = configuration.RenewerConfiguration( namespace=mock.MagicMock( config_dir=self.tempdir, work_dir=self.tempdir, logs_dir=self.tempdir, )) # TODO: maybe provide RenewerConfiguration.make_dirs? # TODO: main() should create those dirs, c.f. #902 os.makedirs(os.path.join(self.tempdir, "live", "example.org")) os.makedirs(os.path.join(self.tempdir, "archive", "example.org")) os.makedirs(os.path.join(self.tempdir, "renewal")) config = configobj.ConfigObj() for kind in ALL_FOUR: config[kind] = os.path.join(self.tempdir, "live", "example.org", kind + ".pem") config.filename = os.path.join(self.tempdir, "renewal", "example.org.conf") config.write() self.config = config self.defaults = configobj.ConfigObj() self.test_rc = storage.RenewableCert(config.filename, self.cli_config)
def _find_duplicative_certs(domains, config, renew_config): """Find existing certs that duplicate the request.""" identical_names_cert, subset_names_cert = None, None configs_dir = renew_config.renewal_configs_dir cli_config = configuration.RenewerConfiguration(config) for renewal_file in os.listdir(configs_dir): try: full_path = os.path.join(configs_dir, renewal_file) rc_config = configobj.ConfigObj(renew_config.renewer_config_file) rc_config.merge(configobj.ConfigObj(full_path)) rc_config.filename = full_path candidate_lineage = storage.RenewableCert(rc_config, config_opts=None, cli_config=cli_config) except (configobj.ConfigObjError, errors.CertStorageError, IOError): logger.warning( "Renewal configuration file %s is broken. " "Skipping.", full_path) continue # TODO: Handle these differently depending on whether they are # expired or still valid? candidate_names = set(candidate_lineage.names()) if candidate_names == set(domains): identical_names_cert = candidate_lineage elif candidate_names.issubset(set(domains)): subset_names_cert = candidate_lineage return identical_names_cert, subset_names_cert
def _find_duplicative_certs(config, domains): """Find existing certs that duplicate the request.""" identical_names_cert, subset_names_cert = None, None cli_config = configuration.RenewerConfiguration(config) configs_dir = cli_config.renewal_configs_dir # Verify the directory is there le_util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid()) for renewal_file in renewal.renewal_conf_files(cli_config): try: candidate_lineage = storage.RenewableCert(renewal_file, cli_config) except (errors.CertStorageError, IOError): logger.warning("Renewal conf file %s is broken. Skipping.", renewal_file) logger.debug("Traceback was:\n%s", traceback.format_exc()) continue # TODO: Handle these differently depending on whether they are # expired or still valid? candidate_names = set(candidate_lineage.names()) if candidate_names == set(domains): identical_names_cert = candidate_lineage elif candidate_names.issubset(set(domains)): # This logic finds and returns the largest subset-names cert # in the case where there are several available. if subset_names_cert is None: subset_names_cert = candidate_lineage elif len(candidate_names) > len(subset_names_cert.names()): subset_names_cert = candidate_lineage return identical_names_cert, subset_names_cert
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")
def _find_duplicative_certs(config, domains): """Find existing certs that duplicate the request.""" identical_names_cert, subset_names_cert = None, None cli_config = configuration.RenewerConfiguration(config) configs_dir = cli_config.renewal_configs_dir # Verify the directory is there le_util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid()) for renewal_file in os.listdir(configs_dir): try: full_path = os.path.join(configs_dir, renewal_file) candidate_lineage = storage.RenewableCert(full_path, cli_config) except (CertStorageError, IOError): logger.warning( "Renewal configuration file %s is broken. " "Skipping.", full_path) continue # TODO: Handle these differently depending on whether they are # expired or still valid? candidate_names = set(candidate_lineage.names()) if candidate_names == set(domains): identical_names_cert = candidate_lineage elif candidate_names.issubset(set(domains)): subset_names_cert = candidate_lineage return identical_names_cert, subset_names_cert
def _reconstitute(config, full_path): """Try to instantiate a RenewableCert, updating config with relevant items. This is specifically for use in renewal and enforces several checks and policies to ensure that we can try to proceed with the renwal request. The config argument is modified by including relevant options read from the renewal configuration file. :param configuration.NamespaceConfig config: configuration for the current lineage :param str full_path: Absolute path to the configuration file that defines this lineage :returns: the RenewableCert object or None if a fatal error occurred :rtype: `storage.RenewableCert` or NoneType """ try: renewal_candidate = storage.RenewableCert( full_path, configuration.RenewerConfiguration(config)) except (errors.CertStorageError, IOError): logger.warning("Renewal configuration file %s is broken. Skipping.", full_path) logger.debug("Traceback was:\n%s", traceback.format_exc()) return None if "renewalparams" not in renewal_candidate.configuration: logger.warning( "Renewal configuration file %s lacks " "renewalparams. Skipping.", full_path) return None renewalparams = renewal_candidate.configuration["renewalparams"] if "authenticator" not in renewalparams: logger.warning( "Renewal configuration file %s does not specify " "an authenticator. Skipping.", full_path) return None # Now restore specific values along with their data types, if # those elements are present. try: _restore_required_config_elements(config, renewalparams) _restore_plugin_configs(config, renewalparams) except (ValueError, errors.Error) as error: logger.warning( "An error occurred while parsing %s. The error was %s. " "Skipping the file.", full_path, error.message) logger.debug("Traceback was:\n%s", traceback.format_exc()) return None try: for d in renewal_candidate.names(): cli.process_domain(config, d) except errors.ConfigurationError as error: logger.warning( "Renewal configuration file %s references a cert " "that contains an invalid domain name. The problem " "was: %s. Skipping.", full_path, error) return None return renewal_candidate
def main(cli_args=sys.argv[1:]): """Main function for autorenewer script.""" # TODO: Distinguish automated invocation from manual invocation, # perhaps by looking at sys.argv[0] and inhibiting automated # invocations if /etc/letsencrypt/renewal.conf defaults have # turned it off. (The boolean parameter should probably be # called renewer_enabled.) # TODO: When we have a more elaborate renewer command line, we will # presumably also be able to specify a config file on the # command line, which, if provided, should take precedence over # te default config files zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) args = _create_parser().parse_args(cli_args) uid = os.geteuid() le_util.make_or_verify_dir(args.logs_dir, 0o700, uid) cli.setup_logging(args, _cli_log_handler, logfile='renewer.log') cli_config = configuration.RenewerConfiguration(args) # Ensure that all of the needed folders have been created before continuing le_util.make_or_verify_dir(cli_config.work_dir, constants.CONFIG_DIRS_MODE, uid) for renewal_file in os.listdir(cli_config.renewal_configs_dir): print "Processing", renewal_file try: # TODO: Before trying to initialize the RenewableCert object, # we could check here whether the combination of the config # and the rc_config together disables all autorenewal and # autodeployment applicable to this cert. In that case, we # can simply continue and don't need to instantiate a # RenewableCert object for this cert at all, which could # dramatically improve performance for large deployments # where autorenewal is widely turned off. cert = storage.RenewableCert(renewal_file, cli_config) except errors.CertStorageError: # This indicates an invalid renewal configuration file, such # as one missing a required parameter (in the future, perhaps # also one that is internally inconsistent or is missing a # required parameter). As a TODO, maybe we should warn the # user about the existence of an invalid or corrupt renewal # config rather than simply ignoring it. continue if cert.should_autorenew(): # Note: not cert.current_version() because the basis for # the renewal is the latest version, even if it hasn't been # deployed yet! old_version = cert.latest_common_version() renew(cert, old_version) notify.notify("Autorenewed a cert!!!", "root", "It worked!") # TODO: explain what happened if cert.should_autodeploy(): cert.update_all_links_to(cert.latest_common_version()) # TODO: restart web server (invoke IInstaller.restart() method) notify.notify("Autodeployed a cert!!!", "root", "It worked!")
def obtain_and_enroll_certificate(self, domains, authenticator, installer, plugins): """Obtain and enroll certificate. Get a new certificate for the specified domains using the specified authenticator and installer, and then create a new renewable lineage containing it. :param list domains: Domains to request. :param authenticator: The authenticator to use. :type authenticator: :class:`letsencrypt.interfaces.IAuthenticator` :param installer: The installer to use. :type installer: :class:`letsencrypt.interfaces.IInstaller` :param plugins: A PluginsFactory object. :returns: A new :class:`letsencrypt.storage.RenewableCert` instance referred to the enrolled cert lineage, or False if the cert could not be obtained. """ certr, chain, key, _ = self.obtain_certificate(domains) # TODO: remove this dirty hack self.config.namespace.authenticator = plugins.find_init( authenticator).name if installer is not None: self.config.namespace.installer = plugins.find_init(installer).name # XXX: We clearly need a more general and correct way of getting # options into the configobj for the RenewableCert instance. # This is a quick-and-dirty way to do it to allow integration # testing to start. (Note that the config parameter to new_lineage # ideally should be a ConfigObj, but in this case a dict will be # accepted in practice.) params = vars(self.config.namespace) config = {} cli_config = configuration.RenewerConfiguration(self.config.namespace) if (cli_config.config_dir != constants.CLI_DEFAULTS["config_dir"] or cli_config.work_dir != constants.CLI_DEFAULTS["work_dir"]): logger.warning( "Non-standard path(s), might not work with crontab installed " "by your operating system package manager") # XXX: just to stop RenewableCert from complaining; this is # probably not a good solution chain_pem = "" if chain is None else OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, chain) lineage = storage.RenewableCert.new_lineage( domains[0], OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, certr.body), key.pem, chain_pem, params, config, cli_config) self._report_renewal_status(lineage) return lineage
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
def test_ancient_webroot_renewal_conf(self, mock_set_by_cli): mock_set_by_cli.return_value = False rc_path = self._make_test_renewal_conf('sample-renewal-ancient.conf') args = mock.MagicMock(account=None, email=None, webroot_path=None) config = configuration.NamespaceConfig(args) lineage = storage.RenewableCert( rc_path, configuration.RenewerConfiguration(config)) renewalparams = lineage.configuration["renewalparams"] # pylint: disable=protected-access renewal._restore_webroot_config(config, renewalparams) self.assertEqual(config.webroot_path, ["/var/www/"])
def renew_cert(config, domains, le_client, lineage): "Renew a certificate lineage." original_server = lineage.configuration["renewalparams"]["server"] _avoid_invalidating_lineage(config, lineage, original_server) new_certr, new_chain, new_key, _ = le_client.obtain_certificate(domains) if config.dry_run: logger.info("Dry run: skipping updating lineage at %s", os.path.dirname(lineage.cert)) else: prior_version = lineage.latest_common_version() new_cert = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, new_certr.body.wrapped) new_chain = crypto_util.dump_pyopenssl_chain(new_chain) renewal_conf = configuration.RenewerConfiguration(config.namespace) lineage.save_successor(prior_version, new_cert, new_key.pem, new_chain, renewal_conf) lineage.update_all_links_to(lineage.latest_common_version()) hooks.renew_hook(config, domains, lineage.live_dir)
def setUp(self): from letsencrypt import storage self.tempdir = tempfile.mkdtemp() self.cli_config = configuration.RenewerConfiguration( namespace=mock.MagicMock(config_dir=self.tempdir)) # TODO: maybe provide RenewerConfiguration.make_dirs? os.makedirs(os.path.join(self.tempdir, "live", "example.org")) os.makedirs(os.path.join(self.tempdir, "archive", "example.org")) os.makedirs(os.path.join(self.tempdir, "configs")) config = configobj.ConfigObj() for kind in ALL_FOUR: config[kind] = os.path.join(self.tempdir, "live", "example.org", kind + ".pem") config.filename = os.path.join(self.tempdir, "configs", "example.org.conf") self.defaults = configobj.ConfigObj() self.test_rc = storage.RenewableCert(config, self.defaults, self.cli_config)
def obtain_and_enroll_certificate(self, domains): """Obtain and enroll certificate. Get a new certificate for the specified domains using the specified authenticator and installer, and then create a new renewable lineage containing it. :param list domains: Domains to request. :param plugins: A PluginsFactory object. :returns: A new :class:`letsencrypt.storage.RenewableCert` instance referred to the enrolled cert lineage, or False if the cert could not be obtained. """ certr, chain, key, _ = self.obtain_certificate(domains) # XXX: We clearly need a more general and correct way of getting # options into the configobj for the RenewableCert instance. # This is a quick-and-dirty way to do it to allow integration # testing to start. (Note that the config parameter to new_lineage # ideally should be a ConfigObj, but in this case a dict will be # accepted in practice.) params = vars(self.config.namespace) config = {} cli_config = configuration.RenewerConfiguration(self.config.namespace) if (cli_config.config_dir != constants.CLI_DEFAULTS["config_dir"] or cli_config.work_dir != constants.CLI_DEFAULTS["work_dir"]): logger.warning( "Non-standard path(s), might not work with crontab installed " "by your operating system package manager") lineage = storage.RenewableCert.new_lineage( domains[0], OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, certr.body), key.pem, crypto_util.dump_pyopenssl_chain(chain), params, config, cli_config) return lineage
def setUp(self): from letsencrypt import storage self.tempdir = tempfile.mkdtemp() self.cli_config = configuration.RenewerConfiguration( namespace=mock.MagicMock( config_dir=self.tempdir, work_dir=self.tempdir, logs_dir=self.tempdir, )) # TODO: maybe provide RenewerConfiguration.make_dirs? # TODO: main() should create those dirs, c.f. #902 os.makedirs(os.path.join(self.tempdir, "live", "example.org")) os.makedirs(os.path.join(self.tempdir, "archive", "example.org")) os.makedirs(os.path.join(self.tempdir, "renewal")) config = configobj.ConfigObj() for kind in ALL_FOUR: config[kind] = os.path.join(self.tempdir, "live", "example.org", kind + ".pem") config.filename = os.path.join(self.tempdir, "renewal", "example.org.conf") config.write() self.config = config # We also create a file that isn't a renewal config in the same # location to test that logic that reads in all-and-only renewal # configs will ignore it and NOT attempt to parse it. junk = open(os.path.join(self.tempdir, "renewal", "IGNORE.THIS"), "w") junk.write("This file should be ignored!") junk.close() self.defaults = configobj.ConfigObj() with mock.patch( "letsencrypt.storage.RenewableCert._check_symlinks") as check: check.return_value = True self.test_rc = storage.RenewableCert(config.filename, self.cli_config)
def _treat_as_renewal(config, domains): """Determine whether or not the call should be treated as a renewal. :returns: RenewableCert or None if renewal shouldn't occur. :rtype: :class:`.storage.RenewableCert` :raises .Error: If the user would like to rerun the client again. """ renewal = False # Considering the possibility that the requested certificate is # related to an existing certificate. (config.duplicate, which # is set with --duplicate, skips all of this logic and forces any # kind of certificate to be obtained with renewal = False.) if not config.duplicate: ident_names_cert, subset_names_cert = _find_duplicative_certs( domains, config, configuration.RenewerConfiguration(config)) # I am not sure whether that correctly reads the systemwide # configuration file. question = None if ident_names_cert is not None: question = ( "You have an existing certificate that contains exactly the " "same domains you requested (ref: {0}){br}{br}Do you want to " "renew and replace this certificate with a newly-issued one?" ).format(ident_names_cert.configfile.filename, br=os.linesep) elif subset_names_cert is not None: question = ( "You have an existing certificate that contains a portion of " "the domains you requested (ref: {0}){br}{br}It contains these " "names: {1}{br}{br}You requested these names for the new " "certificate: {2}.{br}{br}Do you want to replace this existing " "certificate with the new certificate?" ).format(subset_names_cert.configfile.filename, ", ".join(subset_names_cert.names()), ", ".join(domains), br=os.linesep) if question is None: # We aren't in a duplicative-names situation at all, so we don't # have to tell or ask the user anything about this. pass elif config.renew_by_default or zope.component.getUtility( interfaces.IDisplay).yesno(question, "Replace", "Cancel"): renewal = True else: reporter_util = zope.component.getUtility(interfaces.IReporter) reporter_util.add_message( "To obtain a new certificate that {0} an existing certificate " "in its domain-name coverage, you must use the --duplicate " "option.{br}{br}For example:{br}{br}{1} --duplicate {2}".format( "duplicates" if ident_names_cert is not None else "overlaps with", sys.argv[0], " ".join(sys.argv[1:]), br=os.linesep ), reporter_util.HIGH_PRIORITY) raise Error( "User did not use proper CLI and would like " "to reinvoke the client.") if renewal: return ident_names_cert if ident_names_cert is not None else subset_names_cert return None
def main(config=None, args=sys.argv[1:]): """Main function for autorenewer script.""" # TODO: Distinguish automated invocation from manual invocation, # perhaps by looking at sys.argv[0] and inhibiting automated # invocations if /etc/letsencrypt/renewal.conf defaults have # turned it off. (The boolean parameter should probably be # called renewer_enabled.) zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) cli_config = configuration.RenewerConfiguration( _create_parser().parse_args(args)) config = storage.config_with_defaults(config) # Now attempt to read the renewer config file and augment or replace # the renewer defaults with any options contained in that file. If # renewer_config_file is undefined or if the file is nonexistent or # empty, this .merge() will have no effect. TODO: when we have a more # elaborate renewer command line, we will presumably also be able to # specify a config file on the command line, which, if provided, should # take precedence over this one. config.merge(configobj.ConfigObj(cli_config.renewer_config_file)) for i in os.listdir(cli_config.renewal_configs_dir): print "Processing", i if not i.endswith(".conf"): continue rc_config = configobj.ConfigObj(cli_config.renewer_config_file) rc_config.merge( configobj.ConfigObj(os.path.join(cli_config.renewal_configs_dir, i))) # TODO: this is a dirty hack! rc_config.filename = os.path.join(cli_config.renewal_configs_dir, i) try: # TODO: Before trying to initialize the RenewableCert object, # we could check here whether the combination of the config # and the rc_config together disables all autorenewal and # autodeployment applicable to this cert. In that case, we # can simply continue and don't need to instantiate a # RenewableCert object for this cert at all, which could # dramatically improve performance for large deployments # where autorenewal is widely turned off. cert = storage.RenewableCert(rc_config, cli_config=cli_config) except errors.CertStorageError: # This indicates an invalid renewal configuration file, such # as one missing a required parameter (in the future, perhaps # also one that is internally inconsistent or is missing a # required parameter). As a TODO, maybe we should warn the # user about the existence of an invalid or corrupt renewal # config rather than simply ignoring it. continue if cert.should_autorenew(): # Note: not cert.current_version() because the basis for # the renewal is the latest version, even if it hasn't been # deployed yet! old_version = cert.latest_common_version() renew(cert, old_version) notify.notify("Autorenewed a cert!!!", "root", "It worked!") # TODO: explain what happened if cert.should_autodeploy(): cert.update_all_links_to(cert.latest_common_version()) # TODO: restart web server (invoke IInstaller.restart() method) notify.notify("Autodeployed a cert!!!", "root", "It worked!")
def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-locals """Obtain a certificate and install.""" if args.configurator is not None and (args.installer is not None or args.authenticator is not None): return ("Either --configurator or --authenticator/--installer" "pair, but not both, is allowed") if args.authenticator is not None or args.installer is not None: installer = display_ops.pick_installer(config, args.installer, plugins) authenticator = display_ops.pick_authenticator(config, args.authenticator, plugins) else: # TODO: this assumes that user doesn't want to pick authenticator # and installer separately... authenticator = installer = display_ops.pick_configurator( config, args.configurator, plugins) if installer is None or authenticator is None: return "Configurator could not be determined" domains = _find_domains(args, installer) treat_as_renewal = False # Considering the possibility that the requested certificate is # related to an existing certificate. (config.duplicate, which # is set with --duplicate, skips all of this logic and forces any # kind of certificate to be obtained with treat_as_renewal = False.) if not config.duplicate: identical_names_cert, subset_names_cert = _find_duplicative_certs( domains, config, configuration.RenewerConfiguration(config)) # I am not sure whether that correctly reads the systemwide # configuration file. question = None if identical_names_cert is not None: question = ( "You have an existing certificate that contains exactly the " "same domains you requested (ref: {0})\n\nDo you want to " "renew and replace this certificate with a newly-issued one?" ).format(identical_names_cert.configfile.filename) elif subset_names_cert is not None: question = ( "You have an existing certificate that contains a portion of " "the domains you requested (ref: {0})\n\nIt contains these " "names: {1}\n\nYou requested these names for the new " "certificate: {2}.\n\nDo you want to replace this existing " "certificate with the new certificate?").format( subset_names_cert.configfile.filename, ", ".join(subset_names_cert.names()), ", ".join(domains)) if question is None: # We aren't in a duplicative-names situation at all, so we don't # have to tell or ask the user anything about this. pass elif zope.component.getUtility(interfaces.IDisplay).yesno( question, "Replace", "Cancel"): treat_as_renewal = True else: reporter_util = zope.component.getUtility(interfaces.IReporter) reporter_util.add_message( "To obtain a new certificate that {0} an existing certificate " "in its domain-name coverage, you must use the --duplicate " "option.\n\nFor example:\n\n{1} --duplicate {2}".format( "duplicates" if identical_names_cert is not None else "overlaps with", sys.argv[0], " ".join(sys.argv[1:])), reporter_util.HIGH_PRIORITY) return 1 # Attempting to obtain the certificate # TODO: Handle errors from _init_le_client? le_client = _init_le_client(args, config, authenticator, installer) if treat_as_renewal: lineage = identical_names_cert if identical_names_cert is not None else subset_names_cert # TODO: Use existing privkey instead of generating a new one new_certr, new_chain, new_key, _ = le_client.obtain_certificate( domains) # TODO: Check whether it worked! 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? le_client.deploy_certificate(domains, lineage.privkey, lineage.cert, lineage.chain) display_ops.success_renewal(domains) else: # TREAT AS NEW REQUEST lineage = le_client.obtain_and_enroll_certificate( domains, authenticator, installer, plugins) if not lineage: return "Certificate could not be obtained" # TODO: This treats the key as changed even when it wasn't # TODO: We also need to pass the fullchain (for Nginx) le_client.deploy_certificate(domains, lineage.privkey, lineage.cert, lineage.chain) le_client.enhance_config(domains, args.redirect) display_ops.success_installation(domains)