mirror of
https://github.com/certbot/certbot.git
synced 2026-07-28 08:45:22 +02:00
Merge branch 'master' into donate-spamfilter
Conflicts: letsencrypt/cli.py
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
:mod:`letsencrypt.renewer`
|
||||
--------------------------
|
||||
|
||||
.. automodule:: letsencrypt.renewer
|
||||
:members:
|
||||
@@ -281,8 +281,6 @@ man_pages = [
|
||||
[project], 7),
|
||||
('man/letsencrypt', 'letsencrypt', u'letsencrypt script documentation',
|
||||
[project], 1),
|
||||
('man/letsencrypt-renewer', 'letsencrypt-renewer',
|
||||
u'letsencrypt-renewer script documentation', [project], 1),
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
.. program-output:: letsencrypt-renewer --help
|
||||
+1
-3
@@ -101,9 +101,7 @@ DeterminePythonVersion() {
|
||||
fi
|
||||
|
||||
PYVER=`$LE_PYTHON --version 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'`
|
||||
if [ $PYVER -eq 26 ] ; then
|
||||
ExperimentalBootstrap "Python 2.6"
|
||||
elif [ $PYVER -lt 26 ] ; then
|
||||
if [ $PYVER -lt 26 ] ; then
|
||||
echo "You have an ancient version of Python entombed in your operating system..."
|
||||
echo "This isn't going to work; you'll need at least version 2.6."
|
||||
exit 1
|
||||
|
||||
@@ -105,9 +105,7 @@ DeterminePythonVersion() {
|
||||
fi
|
||||
|
||||
PYVER=`"$LE_PYTHON" --version 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'`
|
||||
if [ $PYVER -eq 26 ]; then
|
||||
ExperimentalBootstrap "Python 2.6"
|
||||
elif [ $PYVER -lt 26 ]; then
|
||||
if [ $PYVER -lt 26 ]; then
|
||||
echo "You have an ancient version of Python entombed in your operating system..."
|
||||
echo "This isn't going to work; you'll need at least version 2.6."
|
||||
exit 1
|
||||
|
||||
@@ -105,9 +105,7 @@ DeterminePythonVersion() {
|
||||
fi
|
||||
|
||||
PYVER=`"$LE_PYTHON" --version 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'`
|
||||
if [ $PYVER -eq 26 ]; then
|
||||
ExperimentalBootstrap "Python 2.6"
|
||||
elif [ $PYVER -lt 26 ]; then
|
||||
if [ $PYVER -lt 26 ]; then
|
||||
echo "You have an ancient version of Python entombed in your operating system..."
|
||||
echo "This isn't going to work; you'll need at least version 2.6."
|
||||
exit 1
|
||||
|
||||
+125
-78
@@ -8,6 +8,7 @@ import argparse
|
||||
import atexit
|
||||
import copy
|
||||
import functools
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import logging.handlers
|
||||
@@ -53,7 +54,7 @@ _parser = None
|
||||
# file's renewalparams and actually used in the client configuration
|
||||
# during the renewal process. We have to record their types here because
|
||||
# the renewal configuration process loses this information.
|
||||
STR_CONFIG_ITEMS = ["config_dir", "log_dir", "work_dir", "user_agent",
|
||||
STR_CONFIG_ITEMS = ["config_dir", "logs_dir", "work_dir", "user_agent",
|
||||
"server", "account", "authenticator", "installer",
|
||||
"standalone_supported_challenges"]
|
||||
INT_CONFIG_ITEMS = ["rsa_key_size", "tls_sni_01_port", "http01_port"]
|
||||
@@ -174,7 +175,7 @@ def _determine_account(config):
|
||||
acc = accounts[0]
|
||||
else: # no account registered yet
|
||||
if config.email is None and not config.register_unsafely_without_email:
|
||||
config.email = display_ops.get_email()
|
||||
config.namespace.email = display_ops.get_email()
|
||||
|
||||
def _tos_cb(regr):
|
||||
if config.tos:
|
||||
@@ -196,7 +197,7 @@ def _determine_account(config):
|
||||
raise errors.Error(
|
||||
"Unable to register an account with ACME server")
|
||||
|
||||
config.account = acc.id
|
||||
config.namespace.account = acc.id
|
||||
return acc, acme
|
||||
|
||||
|
||||
@@ -223,15 +224,12 @@ def _find_duplicative_certs(config, domains):
|
||||
# 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):
|
||||
if not renewal_file.endswith(".conf"):
|
||||
continue
|
||||
for renewal_file in _renewal_conf_files(cli_config):
|
||||
try:
|
||||
full_path = os.path.join(configs_dir, renewal_file)
|
||||
candidate_lineage = storage.RenewableCert(full_path, cli_config)
|
||||
candidate_lineage = storage.RenewableCert(renewal_file, cli_config)
|
||||
except (errors.CertStorageError, IOError):
|
||||
logger.warning("Renewal configuration file %s is broken. "
|
||||
"Skipping.", full_path)
|
||||
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?
|
||||
@@ -250,8 +248,10 @@ def _find_duplicative_certs(config, domains):
|
||||
|
||||
|
||||
def _treat_as_renewal(config, domains):
|
||||
"""Determine whether there are duplicated names and how to handle them
|
||||
(renew, reinstall, newcert, or no action).
|
||||
"""Determine whether there are duplicated names and how to handle
|
||||
them (renew, reinstall, newcert, or raising an error to stop
|
||||
the client run if the user chooses to cancel the operation when
|
||||
prompted).
|
||||
|
||||
:returns: Two-element tuple containing desired new-certificate behavior as
|
||||
a string token ("reinstall", "renew", or "newcert"), plus either
|
||||
@@ -463,8 +463,8 @@ def _auth_from_domains(le_client, config, domains, lineage=None):
|
||||
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))
|
||||
|
||||
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
|
||||
@@ -709,7 +709,13 @@ def obtain_cert(config, plugins, lineage=None):
|
||||
|
||||
if config.dry_run:
|
||||
_report_successful_dry_run()
|
||||
_suggest_donation_if_appropriate(config, action)
|
||||
elif config.verb == "renew" and installer is not None:
|
||||
# In case of a renewal, reload server to pick up new certificate.
|
||||
# In principle we could have a configuration option to inhibit this
|
||||
# from happening.
|
||||
installer.restart()
|
||||
print("reloaded")
|
||||
_suggest_donation_if_appropriate(config)
|
||||
|
||||
|
||||
def install(config, plugins):
|
||||
@@ -732,7 +738,15 @@ def install(config, plugins):
|
||||
le_client.enhance_config(domains, config)
|
||||
|
||||
|
||||
def _restore_required_config_elements(full_path, config, renewalparams):
|
||||
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:
|
||||
@@ -741,50 +755,59 @@ def _restore_required_config_elements(full_path, config, renewalparams):
|
||||
# so we don't know if the original was NoneType or str!
|
||||
if value == "None":
|
||||
value = None
|
||||
config.__setattr__(config_item, value)
|
||||
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:
|
||||
try:
|
||||
value = int(renewalparams[config_item])
|
||||
config.__setattr__(config_item, value)
|
||||
setattr(config.namespace, config_item, value)
|
||||
except ValueError:
|
||||
logger.warning("Renewal configuration file %s specifies "
|
||||
"a non-numeric value for %s. Skipping.",
|
||||
full_path, config_item)
|
||||
raise
|
||||
raise errors.Error(
|
||||
"Expected a numeric value for {0}".format(config_item))
|
||||
|
||||
def _restore_plugin_configs(config, renewalparams):
|
||||
"""Sets 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
|
||||
|
||||
"""
|
||||
# Now use parser to get plugin-prefixed items with correct types
|
||||
# XXX: the current approach of extracting only prefixed items
|
||||
# related to the actually-used installer and authenticator
|
||||
# works as long as plugins don't need to read plugin-specific
|
||||
# variables set by someone else (e.g., assuming Apache
|
||||
# configurator doesn't need to read webroot_ variables).
|
||||
# XXX: is it true that an item will end up in _parser._actions even
|
||||
# when no action was explicitly specified?
|
||||
# Note: if a parameter that used to be defined in the parser is no
|
||||
# longer defined, stored copies of that parameter will be
|
||||
# deserialized as strings by this logic even if they were
|
||||
# originally meant to be some other type.
|
||||
plugin_prefixes = [renewalparams["authenticator"]]
|
||||
if "installer" in renewalparams and renewalparams["installer"] != None:
|
||||
if renewalparams.get("installer", None) is not None:
|
||||
plugin_prefixes.append(renewalparams["installer"])
|
||||
for plugin_prefix in set(renewalparams):
|
||||
for config_item in renewalparams.keys():
|
||||
if renewalparams[config_item] == "None":
|
||||
for plugin_prefix in set(plugin_prefixes):
|
||||
for config_item, config_value in renewalparams.iteritems():
|
||||
if config_item.startswith(plugin_prefix + "_"):
|
||||
# Avoid confusion when, for example, "csr = None" (avoid
|
||||
# trying to read the file called "None")
|
||||
# Should we omit the item entirely rather than setting
|
||||
# its value to None?
|
||||
config.__setattr__(config_item, None)
|
||||
continue
|
||||
if config_item.startswith(plugin_prefix + "_"):
|
||||
for action in _parser.parser._actions:
|
||||
if action.dest == config_item:
|
||||
if action.type is not None:
|
||||
config.__setattr__(config_item, action.type(renewalparams[config_item]))
|
||||
break
|
||||
if config_value == "None":
|
||||
setattr(config.namespace, config_item, None)
|
||||
continue
|
||||
for action in _parser.parser._actions: # pylint: disable=protected-access
|
||||
if action.type is not None and action.dest == config_item:
|
||||
setattr(config.namespace, config_item,
|
||||
action.type(config_value))
|
||||
break
|
||||
else:
|
||||
config.__setattr__(config_item, str(renewalparams[config_item]))
|
||||
return True
|
||||
setattr(config.namespace, config_item, str(config_value))
|
||||
|
||||
|
||||
def _reconstitute(full_path, config):
|
||||
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
|
||||
@@ -792,15 +815,21 @@ def _reconstitute(full_path, config):
|
||||
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, config)
|
||||
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.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 "
|
||||
@@ -814,17 +843,20 @@ def _reconstitute(full_path, config):
|
||||
# Now restore specific values along with their data types, if
|
||||
# those elements are present.
|
||||
try:
|
||||
_restore_required_config_elements(full_path, config, renewalparams)
|
||||
except ValueError:
|
||||
# There was a data type error which has already been
|
||||
# logged.
|
||||
_restore_required_config_elements(config, renewalparams)
|
||||
_restore_plugin_configs(config, renewalparams)
|
||||
except (ValueError, errors.Error) as error:
|
||||
logger.warning(
|
||||
"An error occured 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
|
||||
|
||||
# webroot_map is, uniquely, a dict, and the general-purpose
|
||||
# configuration restoring logic is not able to correctly parse it
|
||||
# from the serialized form.
|
||||
if "webroot_map" in renewalparams:
|
||||
config.__setattr__("webroot_map", renewalparams["webroot_map"])
|
||||
setattr(config.namespace, "webroot_map", renewalparams["webroot_map"])
|
||||
|
||||
try:
|
||||
domains = [le_util.enforce_domain_sanity(x) for x in
|
||||
@@ -835,14 +867,18 @@ def _reconstitute(full_path, config):
|
||||
"invalid. Skipping.", full_path)
|
||||
return None
|
||||
|
||||
config.__setattr__("domains", domains)
|
||||
setattr(config.namespace, "domains", domains)
|
||||
return renewal_candidate
|
||||
|
||||
|
||||
def renew(cli_config, plugins):
|
||||
def _renewal_conf_files(config):
|
||||
"""Return /path/to/*.conf in the renewal conf directory"""
|
||||
return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf"))
|
||||
|
||||
|
||||
def renew(config, unused_plugins):
|
||||
"""Renew previously-obtained certificates."""
|
||||
cli_config = configuration.RenewerConfiguration(cli_config)
|
||||
if cli_config.domains != []:
|
||||
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 "
|
||||
@@ -850,40 +886,48 @@ def renew(cli_config, plugins):
|
||||
"renew specific certificates, use the certonly "
|
||||
"command. The renew verb may provide other options "
|
||||
"for selecting certificates to renew in the future.")
|
||||
configs_dir = cli_config.renewal_configs_dir
|
||||
for renewal_file in os.listdir(configs_dir):
|
||||
if not renewal_file.endswith(".conf"):
|
||||
continue
|
||||
if config.csr is not None:
|
||||
raise errors.Error("Currently, the renew verb cannot be used when "
|
||||
"specifying a CSR file. Please try the certonly "
|
||||
"command instead.")
|
||||
renewer_config = configuration.RenewerConfiguration(config)
|
||||
for renewal_file in _renewal_conf_files(renewer_config):
|
||||
print("Processing " + renewal_file)
|
||||
# XXX: does this succeed in making a fully independent config object
|
||||
# each time?
|
||||
config = configuration.RenewerConfiguration(copy.deepcopy(cli_config))
|
||||
config.noninteractive_mode = True
|
||||
full_path = os.path.join(configs_dir, 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(full_path, config)
|
||||
except Exception as e:
|
||||
renewal_candidate = _reconstitute(lineage_config, renewal_file)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
# reconstitute encountered an unanticipated problem.
|
||||
logger.warning("Renewal configuration file %s produced an "
|
||||
"unexpected error: %s. Skipping.", full_path, e)
|
||||
"unexpected error: %s. Skipping.", renewal_file, e)
|
||||
logger.debug("Traceback was:\n%s", traceback.format_exc())
|
||||
continue
|
||||
|
||||
if renewal_candidate is None:
|
||||
# reconstitute indicated an error or problem which has
|
||||
# already been logged. Go on to the next config.
|
||||
continue
|
||||
# XXX: ensure that each call here replaces the previous one
|
||||
zope.component.provideUtility(config)
|
||||
try:
|
||||
if renewal_candidate is not None:
|
||||
# _reconstitute succeeded in producing a RenewableCert, so we
|
||||
# have something to work with from this particular config file.
|
||||
|
||||
print("Trying...")
|
||||
# Because obtain_cert itself indirectly decides whether to renew
|
||||
# or not, we couldn't currently make a UI/logging distinction at
|
||||
# this stage to indicate whether renewal was actually attempted
|
||||
# (or successful).
|
||||
obtain_cert(config, plugins, renewal_candidate)
|
||||
# XXX: ensure that each call here replaces the previous one
|
||||
zope.component.provideUtility(lineage_config)
|
||||
print("Trying...")
|
||||
# Because obtain_cert itself indirectly decides whether to renew
|
||||
# or not, we couldn't currently make a UI/logging distinction at
|
||||
# this stage to indicate whether renewal was actually attempted
|
||||
# (or successful).
|
||||
obtain_cert(lineage_config,
|
||||
plugins_disco.PluginsRegistry.find_all(),
|
||||
renewal_candidate)
|
||||
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())
|
||||
|
||||
|
||||
def revoke(config, unused_plugins): # TODO: coop with renewal config
|
||||
@@ -1068,9 +1112,9 @@ class HelpfulArgumentParser(object):
|
||||
parsed_args.server = constants.STAGING_URI
|
||||
|
||||
if parsed_args.dry_run:
|
||||
if self.verb != "certonly":
|
||||
if self.verb not in ["certonly", "renew"]:
|
||||
raise errors.Error("--dry-run currently only works with the "
|
||||
"'certonly' subcommand")
|
||||
"'certonly' or 'renew' subcommands")
|
||||
parsed_args.break_my_certs = parsed_args.staging = True
|
||||
|
||||
return parsed_args
|
||||
@@ -1359,7 +1403,7 @@ def prepare_and_parse_args(plugins, args):
|
||||
# parser (--help should display plugin-specific options last)
|
||||
_plugins_parsing(helpful, plugins)
|
||||
|
||||
global _parser
|
||||
global _parser # pylint: disable=global-statement
|
||||
_parser = helpful
|
||||
return helpful.parse_args()
|
||||
|
||||
@@ -1582,7 +1626,7 @@ def setup_log_file_handler(config, logfile, fmt):
|
||||
|
||||
|
||||
def _cli_log_handler(config, level, fmt):
|
||||
if config.text_mode:
|
||||
if config.text_mode or config.noninteractive_mode or config.verb == "renew":
|
||||
handler = colored_logging.StreamHandler()
|
||||
handler.setFormatter(logging.Formatter(fmt))
|
||||
else:
|
||||
@@ -1699,6 +1743,9 @@ def main(cli_args=sys.argv[1:]):
|
||||
displayer = display_util.NoninteractiveDisplay(sys.stdout)
|
||||
elif config.text_mode:
|
||||
displayer = display_util.FileDisplay(sys.stdout)
|
||||
elif config.verb == "renew":
|
||||
config.noninteractive_mode = True
|
||||
displayer = display_util.NoninteractiveDisplay(sys.stdout)
|
||||
else:
|
||||
displayer = display_util.NcursesDisplay()
|
||||
zope.component.provideUtility(displayer)
|
||||
|
||||
+5
-15
@@ -249,7 +249,7 @@ class Client(object):
|
||||
|
||||
`.register` must be called before `.obtain_certificate`
|
||||
|
||||
:param set domains: domains to get a certificate
|
||||
:param list domains: domains to get a certificate
|
||||
|
||||
:returns: `.CertificateResource`, certificate chain (as
|
||||
returned by `.fetch_chain`), and newly generated private key
|
||||
@@ -282,23 +282,13 @@ class Client(object):
|
||||
"""
|
||||
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"]):
|
||||
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 cli_config.dry_run:
|
||||
if self.config.dry_run:
|
||||
logger.info("Dry run: Skipping creating new lineage for %s",
|
||||
domains[0])
|
||||
return None
|
||||
@@ -307,7 +297,7 @@ class Client(object):
|
||||
domains[0], OpenSSL.crypto.dump_certificate(
|
||||
OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped),
|
||||
key.pem, crypto_util.dump_pyopenssl_chain(chain),
|
||||
params, config, cli_config)
|
||||
configuration.RenewerConfiguration(self.config.namespace))
|
||||
|
||||
def save_certificate(self, certr, chain_cert,
|
||||
cert_path, chain_path, fullchain_path):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Let's Encrypt user-supplied configuration."""
|
||||
import copy
|
||||
import os
|
||||
import urlparse
|
||||
|
||||
@@ -43,12 +44,6 @@ class NamespaceConfig(object):
|
||||
# Check command line parameters sanity, and error out in case of problem.
|
||||
check_config_sanity(self)
|
||||
|
||||
# We're done setting up the attic. Now pull up the ladder after ourselves...
|
||||
self.__setattr__ = self.__setattr_implementation__
|
||||
|
||||
def __setattr_implementation__(self, var, value):
|
||||
return self.namespace.__setattr__(var, value)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.namespace, name)
|
||||
|
||||
@@ -84,21 +79,22 @@ class NamespaceConfig(object):
|
||||
return os.path.join(
|
||||
self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR)
|
||||
|
||||
def __deepcopy__(self, _memo):
|
||||
# Work around https://bugs.python.org/issue1515 for py26 tests :( :(
|
||||
# https://travis-ci.org/letsencrypt/letsencrypt/jobs/106900743#L3276
|
||||
new_ns = copy.deepcopy(self.namespace)
|
||||
return type(self)(new_ns)
|
||||
|
||||
|
||||
class RenewerConfiguration(object):
|
||||
"""Configuration wrapper for renewer."""
|
||||
|
||||
def __init__(self, namespace):
|
||||
self.namespace = namespace
|
||||
# We're done setting up the attic. Now pull up the ladder after ourselves...
|
||||
self.__setattr__ = self.__setattr_implementation__
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.namespace, name)
|
||||
|
||||
def __setattr_implementation__(self, var, value):
|
||||
return self.namespace.__setattr__(var, value)
|
||||
|
||||
@property
|
||||
def archive_dir(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(self.namespace.config_dir, constants.ARCHIVE_DIR)
|
||||
|
||||
@@ -93,7 +93,6 @@ s.serve_forever()" """
|
||||
def prepare(self): # pylint: disable=missing-docstring,no-self-use
|
||||
if self.config.noninteractive_mode:
|
||||
raise errors.PluginError("Running manual mode non-interactively is not supported")
|
||||
pass # pragma: no cover
|
||||
|
||||
def more_info(self): # pylint: disable=missing-docstring,no-self-use
|
||||
return ("This plugin requires user's manual intervention in setting "
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
"""Renewer tool.
|
||||
|
||||
Renewer tool handles autorenewal and autodeployment of renewed certs
|
||||
within lineages of successor certificates, according to configuration.
|
||||
|
||||
.. todo:: Sanity checking consistency, validity, freshness?
|
||||
.. todo:: Call new installer API to restart servers after deployment
|
||||
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import OpenSSL
|
||||
import zope.component
|
||||
|
||||
from letsencrypt import account
|
||||
from letsencrypt import configuration
|
||||
from letsencrypt import constants
|
||||
from letsencrypt import colored_logging
|
||||
from letsencrypt import cli
|
||||
from letsencrypt import client
|
||||
from letsencrypt import crypto_util
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import le_util
|
||||
from letsencrypt import notify
|
||||
from letsencrypt import storage
|
||||
|
||||
from letsencrypt.display import util as display_util
|
||||
from letsencrypt.plugins import disco as plugins_disco
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _AttrDict(dict):
|
||||
"""Attribute dictionary.
|
||||
|
||||
A trick to allow accessing dictionary keys as object attributes.
|
||||
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(_AttrDict, self).__init__(*args, **kwargs)
|
||||
self.__dict__ = self
|
||||
|
||||
|
||||
def renew(cert, old_version):
|
||||
"""Perform automated renewal of the referenced cert, if possible.
|
||||
|
||||
:param letsencrypt.storage.RenewableCert cert: The certificate
|
||||
lineage to attempt to renew.
|
||||
:param int old_version: The version of the certificate lineage
|
||||
relative to which the renewal should be attempted.
|
||||
|
||||
:returns: A number referring to newly created version of this cert
|
||||
lineage, or ``False`` if renewal was not successful.
|
||||
:rtype: `int` or `bool`
|
||||
|
||||
"""
|
||||
# TODO: handle partial success (some names can be renewed but not
|
||||
# others)
|
||||
# TODO: handle obligatory key rotation vs. optional key rotation vs.
|
||||
# requested key rotation
|
||||
if "renewalparams" not in cert.configfile:
|
||||
# TODO: notify user?
|
||||
return False
|
||||
renewalparams = cert.configfile["renewalparams"]
|
||||
if "authenticator" not in renewalparams:
|
||||
# TODO: notify user?
|
||||
return False
|
||||
# Instantiate the appropriate authenticator
|
||||
plugins = plugins_disco.PluginsRegistry.find_all()
|
||||
config = configuration.NamespaceConfig(_AttrDict(renewalparams))
|
||||
# XXX: this loses type data (for example, the fact that key_size
|
||||
# was an int, not a str)
|
||||
config.rsa_key_size = int(config.rsa_key_size)
|
||||
config.tls_sni_01_port = int(config.tls_sni_01_port)
|
||||
config.namespace.http01_port = int(config.namespace.http01_port)
|
||||
zope.component.provideUtility(config)
|
||||
try:
|
||||
authenticator = plugins[renewalparams["authenticator"]]
|
||||
except KeyError:
|
||||
# TODO: Notify user? (authenticator could not be found)
|
||||
return False
|
||||
authenticator = authenticator.init(config)
|
||||
|
||||
authenticator.prepare()
|
||||
acc = account.AccountFileStorage(config).load(
|
||||
account_id=renewalparams["account"])
|
||||
|
||||
le_client = client.Client(config, acc, authenticator, None)
|
||||
with open(cert.version("cert", old_version)) as f:
|
||||
sans = crypto_util.get_sans_from_cert(f.read())
|
||||
new_certr, new_chain, new_key, _ = le_client.obtain_certificate(sans)
|
||||
if new_chain:
|
||||
# XXX: Assumes that there was a key change. We need logic
|
||||
# for figuring out whether there was or not. Probably
|
||||
# best is to have obtain_certificate return None for
|
||||
# new_key if the old key is to be used (since save_successor
|
||||
# already understands this distinction!)
|
||||
return cert.save_successor(
|
||||
old_version, OpenSSL.crypto.dump_certificate(
|
||||
OpenSSL.crypto.FILETYPE_PEM, new_certr.body.wrapped),
|
||||
new_key.pem, crypto_util.dump_pyopenssl_chain(new_chain))
|
||||
# TODO: Notify results
|
||||
else:
|
||||
# TODO: Notify negative results
|
||||
return False
|
||||
# TODO: Consider the case where the renewal was partially successful
|
||||
# (where fewer than all names were renewed)
|
||||
|
||||
|
||||
def _cli_log_handler(args, level, fmt): # pylint: disable=unused-argument
|
||||
handler = colored_logging.StreamHandler()
|
||||
handler.setFormatter(logging.Formatter(fmt))
|
||||
handler.setLevel(level)
|
||||
return handler
|
||||
|
||||
|
||||
def _paths_parser(parser):
|
||||
add = parser.add_argument_group("paths").add_argument
|
||||
add("--config-dir", default=cli.flag_default("config_dir"),
|
||||
help=cli.config_help("config_dir"))
|
||||
add("--work-dir", default=cli.flag_default("work_dir"),
|
||||
help=cli.config_help("work_dir"))
|
||||
add("--logs-dir", default=cli.flag_default("logs_dir"),
|
||||
help="Path to a directory where logs are stored.")
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def _create_parser():
|
||||
parser = argparse.ArgumentParser()
|
||||
#parser.add_argument("--cron", action="store_true", help="Run as cronjob.")
|
||||
parser.add_argument(
|
||||
"-v", "--verbose", dest="verbose_count", action="count",
|
||||
default=cli.flag_default("verbose_count"), help="This flag can be used "
|
||||
"multiple times to incrementally increase the verbosity of output, "
|
||||
"e.g. -vvv.")
|
||||
|
||||
return _paths_parser(parser)
|
||||
|
||||
|
||||
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):
|
||||
if not renewal_file.endswith(".conf"):
|
||||
continue
|
||||
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(
|
||||
os.path.join(cli_config.renewal_configs_dir, 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!")
|
||||
# TODO: explain what happened
|
||||
+82
-31
@@ -50,6 +50,68 @@ def add_time_interval(base_time, interval, textparser=parsedatetime.Calendar()):
|
||||
return textparser.parseDT(interval, base_time, tzinfo=tzinfo)[0]
|
||||
|
||||
|
||||
def write_renewal_config(filename, target, cli_config):
|
||||
"""Writes a renewal config file with the specified name and values.
|
||||
|
||||
:param str filename: Absolute path to the config file
|
||||
:param dict target: Maps ALL_FOUR to their symlink paths
|
||||
:param .RenewerConfiguration cli_config: parsed command line
|
||||
arguments
|
||||
|
||||
:returns: Configuration object for the new config file
|
||||
:rtype: configobj.ConfigObj
|
||||
|
||||
"""
|
||||
# create_empty creates a new config file if filename does not exist
|
||||
config = configobj.ConfigObj(filename, create_empty=True)
|
||||
for kind in ALL_FOUR:
|
||||
config[kind] = target[kind]
|
||||
|
||||
# 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.)
|
||||
renewalparams = vars(cli_config.namespace)
|
||||
if renewalparams:
|
||||
config["renewalparams"] = renewalparams
|
||||
config.comments["renewalparams"] = ["",
|
||||
"Options and defaults used"
|
||||
" in the renewal process"]
|
||||
|
||||
# TODO: add human-readable comments explaining other available
|
||||
# parameters
|
||||
logger.debug("Writing new config %s.", filename)
|
||||
config.write()
|
||||
return config
|
||||
|
||||
|
||||
def update_configuration(lineagename, target, cli_config):
|
||||
"""Modifies lineagename's config to contain the specified values.
|
||||
|
||||
:param str lineagename: Name of the lineage being modified
|
||||
:param dict target: Maps ALL_FOUR to their symlink paths
|
||||
:param .RenewerConfiguration cli_config: parsed command line
|
||||
arguments
|
||||
|
||||
:returns: Configuration object for the updated config file
|
||||
:rtype: configobj.ConfigObj
|
||||
|
||||
"""
|
||||
config_filename = os.path.join(
|
||||
cli_config.renewal_configs_dir, lineagename) + ".conf"
|
||||
temp_filename = config_filename + ".new"
|
||||
|
||||
# If an existing tempfile exists, delete it
|
||||
if os.path.exists(temp_filename):
|
||||
os.unlink(temp_filename)
|
||||
write_renewal_config(temp_filename, target, cli_config)
|
||||
os.rename(temp_filename, config_filename)
|
||||
|
||||
return configobj.ConfigObj(config_filename)
|
||||
|
||||
|
||||
class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
"""Renewable certificate.
|
||||
|
||||
@@ -589,9 +651,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def new_lineage(cls, lineagename, cert, privkey, chain,
|
||||
renewalparams=None, config=None, cli_config=None):
|
||||
# pylint: disable=too-many-locals,too-many-arguments
|
||||
def new_lineage(cls, lineagename, cert, privkey, chain, cli_config):
|
||||
# pylint: disable=too-many-locals
|
||||
"""Create a new certificate lineage.
|
||||
|
||||
Attempts to create a certificate lineage -- enrolled for
|
||||
@@ -611,26 +672,13 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
:param str cert: the initial certificate version in PEM format
|
||||
:param str privkey: the private key in PEM format
|
||||
:param str chain: the certificate chain in PEM format
|
||||
:param configobj.ConfigObj renewalparams: parameters that
|
||||
should be used when instantiating authenticator and installer
|
||||
objects in the future to attempt to renew this cert or deploy
|
||||
new versions of it
|
||||
:param configobj.ConfigObj config: renewal configuration
|
||||
defaults, affecting, for example, the locations of the
|
||||
directories where the associated files will be saved
|
||||
:param .RenewerConfiguration cli_config: parsed command line
|
||||
arguments
|
||||
|
||||
:returns: the newly-created RenewalCert object
|
||||
:rtype: :class:`storage.renewableCert`"""
|
||||
|
||||
config = config_with_defaults(config)
|
||||
# This attempts 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.
|
||||
config.merge(configobj.ConfigObj(cli_config.renewer_config_file))
|
||||
:rtype: :class:`storage.renewableCert`
|
||||
|
||||
"""
|
||||
# Examine the configuration and find the new lineage's name
|
||||
for i in (cli_config.renewal_configs_dir, cli_config.archive_dir,
|
||||
cli_config.live_dir):
|
||||
@@ -685,21 +733,11 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
# Document what we've done in a new renewal config file
|
||||
config_file.close()
|
||||
new_config = configobj.ConfigObj(config_filename, create_empty=True)
|
||||
for kind in ALL_FOUR:
|
||||
new_config[kind] = target[kind]
|
||||
if renewalparams:
|
||||
new_config["renewalparams"] = renewalparams
|
||||
new_config.comments["renewalparams"] = ["",
|
||||
"Options and defaults used"
|
||||
" in the renewal process"]
|
||||
# TODO: add human-readable comments explaining other available
|
||||
# parameters
|
||||
logger.debug("Writing new config %s.", config_filename)
|
||||
new_config.write()
|
||||
new_config = write_renewal_config(config_filename, target, cli_config)
|
||||
return cls(new_config.filename, cli_config)
|
||||
|
||||
def save_successor(self, prior_version, new_cert, new_privkey, new_chain):
|
||||
def save_successor(self, prior_version, new_cert,
|
||||
new_privkey, new_chain, cli_config):
|
||||
"""Save new cert and chain as a successor of a prior version.
|
||||
|
||||
Returns the new version number that was created.
|
||||
@@ -715,6 +753,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
:param str new_privkey: the new private key, in PEM format,
|
||||
or ``None``, if the private key has not changed
|
||||
:param str new_chain: the new chain, in PEM format
|
||||
:param .RenewerConfiguration cli_config: parsed command line
|
||||
arguments
|
||||
|
||||
:returns: the new version number that was created
|
||||
:rtype: int
|
||||
@@ -726,8 +766,12 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
# if needed (ensuring their permissions are correct)
|
||||
# Figure out what the new version is and hence where to save things
|
||||
|
||||
self.cli_config = cli_config
|
||||
target_version = self.next_free_version()
|
||||
archive = self.cli_config.archive_dir
|
||||
# XXX if anyone ever moves a renewal configuration file, this will
|
||||
# break... perhaps prefix should be the dirname of the previous
|
||||
# cert.pem?
|
||||
prefix = os.path.join(archive, self.lineagename)
|
||||
target = dict(
|
||||
[(kind,
|
||||
@@ -763,4 +807,11 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
with open(target["fullchain"], "w") as f:
|
||||
logger.debug("Writing full chain to %s.", target["fullchain"])
|
||||
f.write(new_cert + new_chain)
|
||||
|
||||
symlinks = dict((kind, self.configuration[kind]) for kind in ALL_FOUR)
|
||||
# Update renewal config file
|
||||
self.configfile = update_configuration(
|
||||
self.lineagename, symlinks, cli_config)
|
||||
self.configuration = config_with_defaults(self.configfile)
|
||||
|
||||
return target_version
|
||||
|
||||
@@ -24,7 +24,7 @@ from letsencrypt import le_util
|
||||
from letsencrypt.plugins import disco
|
||||
from letsencrypt.plugins import manual
|
||||
|
||||
from letsencrypt.tests import renewer_test
|
||||
from letsencrypt.tests import storage_test
|
||||
from letsencrypt.tests import test_util
|
||||
|
||||
|
||||
@@ -531,7 +531,9 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
||||
self.assertRaises(errors.Error,
|
||||
self._certonly_new_request_common, mock_client)
|
||||
|
||||
def _test_renewal_common(self, due_for_renewal, extra_args, outstring, renew=True):
|
||||
def _test_renewal_common(self, due_for_renewal, extra_args, log_out=None,
|
||||
args=None, renew=True):
|
||||
# pylint: disable=too-many-locals
|
||||
cert_path = 'letsencrypt/tests/testdata/cert.pem'
|
||||
chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem'
|
||||
mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path)
|
||||
@@ -547,27 +549,34 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
||||
mock_init.return_value = mock_client
|
||||
get_utility_path = 'letsencrypt.cli.zope.component.getUtility'
|
||||
with mock.patch(get_utility_path) as mock_get_utility:
|
||||
with mock.patch('letsencrypt.cli.OpenSSL'):
|
||||
with mock.patch('letsencrypt.cli.OpenSSL') as mock_ssl:
|
||||
mock_latest = mock.MagicMock()
|
||||
mock_latest.get_issuer.return_value = "Fake fake"
|
||||
mock_ssl.crypto.load_certificate.return_value = mock_latest
|
||||
with mock.patch('letsencrypt.cli.crypto_util'):
|
||||
args = ['-d', 'foo.bar', '-a',
|
||||
'standalone', 'certonly']
|
||||
if not args:
|
||||
args = ['-d', 'isnot.org', '-a', 'standalone', 'certonly']
|
||||
if extra_args:
|
||||
args += extra_args
|
||||
self._call(args)
|
||||
|
||||
if outstring:
|
||||
with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf:
|
||||
self.assertTrue(outstring in lf.read())
|
||||
try:
|
||||
if log_out:
|
||||
with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf:
|
||||
self.assertTrue(log_out in lf.read())
|
||||
|
||||
if renew:
|
||||
mock_client.obtain_certificate.assert_called_once_with(['foo.bar'])
|
||||
else:
|
||||
self.assertEqual(mock_client.obtain_certificate.call_count, 0)
|
||||
if renew:
|
||||
mock_client.obtain_certificate.assert_called_once_with(['isnot.org'])
|
||||
else:
|
||||
self.assertEqual(mock_client.obtain_certificate.call_count, 0)
|
||||
except:
|
||||
self._dump_log()
|
||||
raise
|
||||
|
||||
return mock_lineage, mock_get_utility
|
||||
|
||||
def test_certonly_renewal(self):
|
||||
lineage, get_utility = self._test_renewal_common(True, [], None)
|
||||
lineage, get_utility = self._test_renewal_common(True, [])
|
||||
self.assertEqual(lineage.save_successor.call_count, 1)
|
||||
lineage.update_all_links_to.assert_called_once_with(
|
||||
lineage.latest_common_version())
|
||||
@@ -578,17 +587,34 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
||||
def test_certonly_renewal_triggers(self):
|
||||
# --dry-run should force renewal
|
||||
_, get_utility = self._test_renewal_common(False, ['--dry-run', '--keep'],
|
||||
"simulating renewal")
|
||||
log_out="simulating renewal")
|
||||
self.assertEqual(get_utility().add_message.call_count, 1)
|
||||
self.assertTrue('dry run' in get_utility().add_message.call_args[0][0])
|
||||
|
||||
_, _ = self._test_renewal_common(False, ['--renew-by-default', '-tvv', '--debug'],
|
||||
"Auto-renewal forced")
|
||||
log_out="Auto-renewal forced")
|
||||
self.assertEqual(get_utility().add_message.call_count, 1)
|
||||
|
||||
_, _ = self._test_renewal_common(False, ['-tvv', '--debug', '--keep'],
|
||||
"not yet due", renew=False)
|
||||
log_out="not yet due", renew=False)
|
||||
|
||||
def _dump_log(self):
|
||||
with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf:
|
||||
print "Logs:"
|
||||
print lf.read()
|
||||
|
||||
|
||||
def test_renewal_verb(self):
|
||||
with open(test_util.vector_path('sample-renewal.conf')) as src:
|
||||
# put the correct path for cert.pem, chain.pem etc in the renewal conf
|
||||
renewal_conf = src.read().replace("MAGICDIR", test_util.vector_path())
|
||||
rd = os.path.join(self.config_dir, "renewal")
|
||||
os.makedirs(rd)
|
||||
rc = os.path.join(rd, "sample-renewal.conf")
|
||||
with open(rc, "w") as dest:
|
||||
dest.write(renewal_conf)
|
||||
args = ["renew", "--dry-run", "-tvv"]
|
||||
self._test_renewal_common(True, [], args=args, renew=True)
|
||||
|
||||
@mock.patch('letsencrypt.cli.zope.component.getUtility')
|
||||
@mock.patch('letsencrypt.cli._treat_as_renewal')
|
||||
@@ -800,7 +826,7 @@ class DetermineAccountTest(unittest.TestCase):
|
||||
self.assertEqual('other email', self.config.email)
|
||||
|
||||
|
||||
class DuplicativeCertsTest(renewer_test.BaseRenewableCertTest):
|
||||
class DuplicativeCertsTest(storage_test.BaseRenewableCertTest):
|
||||
"""Test to avoid duplicate lineages."""
|
||||
|
||||
def setUp(self):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for letsencrypt.renewer."""
|
||||
"""Tests for letsencrypt.storage."""
|
||||
import datetime
|
||||
import pytz
|
||||
import os
|
||||
@@ -9,8 +9,6 @@ import unittest
|
||||
import configobj
|
||||
import mock
|
||||
|
||||
from acme import jose
|
||||
|
||||
from letsencrypt import configuration
|
||||
from letsencrypt import errors
|
||||
from letsencrypt.storage import ALL_FOUR
|
||||
@@ -100,7 +98,7 @@ class BaseRenewableCertTest(unittest.TestCase):
|
||||
|
||||
class RenewableCertTests(BaseRenewableCertTest):
|
||||
# pylint: disable=too-many-public-methods
|
||||
"""Tests for letsencrypt.renewer.*."""
|
||||
"""Tests for letsencrypt.storage."""
|
||||
|
||||
def test_initialization(self):
|
||||
self.assertEqual(self.test_rc.lineagename, "example.org")
|
||||
@@ -506,8 +504,9 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
with open(where, "w") as f:
|
||||
f.write(kind)
|
||||
self.test_rc.update_all_links_to(3)
|
||||
self.assertEqual(6, self.test_rc.save_successor(3, "new cert", None,
|
||||
"new chain"))
|
||||
self.assertEqual(
|
||||
6, self.test_rc.save_successor(3, "new cert", None,
|
||||
"new chain", self.cli_config))
|
||||
with open(self.test_rc.version("cert", 6)) as f:
|
||||
self.assertEqual(f.read(), "new cert")
|
||||
with open(self.test_rc.version("chain", 6)) as f:
|
||||
@@ -518,10 +517,12 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
self.assertFalse(os.path.islink(self.test_rc.version("privkey", 3)))
|
||||
self.assertTrue(os.path.islink(self.test_rc.version("privkey", 6)))
|
||||
# Let's try two more updates
|
||||
self.assertEqual(7, self.test_rc.save_successor(6, "again", None,
|
||||
"newer chain"))
|
||||
self.assertEqual(8, self.test_rc.save_successor(7, "hello", None,
|
||||
"other chain"))
|
||||
self.assertEqual(
|
||||
7, self.test_rc.save_successor(6, "again", None,
|
||||
"newer chain", self.cli_config))
|
||||
self.assertEqual(
|
||||
8, self.test_rc.save_successor(7, "hello", None,
|
||||
"other chain", self.cli_config))
|
||||
# All of the subsequent versions should link directly to the original
|
||||
# privkey.
|
||||
for i in (6, 7, 8):
|
||||
@@ -534,27 +535,33 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
self.assertEqual(self.test_rc.current_version(kind), 3)
|
||||
# Test updating from latest version rather than old version
|
||||
self.test_rc.update_all_links_to(8)
|
||||
self.assertEqual(9, self.test_rc.save_successor(8, "last", None,
|
||||
"attempt"))
|
||||
self.assertEqual(
|
||||
9, self.test_rc.save_successor(8, "last", None,
|
||||
"attempt", self.cli_config))
|
||||
for kind in ALL_FOUR:
|
||||
self.assertEqual(self.test_rc.available_versions(kind),
|
||||
range(1, 10))
|
||||
self.assertEqual(self.test_rc.current_version(kind), 8)
|
||||
with open(self.test_rc.version("fullchain", 9)) as f:
|
||||
self.assertEqual(f.read(), "last" + "attempt")
|
||||
temp_config_file = os.path.join(self.cli_config.renewal_configs_dir,
|
||||
self.test_rc.lineagename) + ".conf.new"
|
||||
with open(temp_config_file, "w") as f:
|
||||
f.write("We previously crashed while writing me :(")
|
||||
# Test updating when providing a new privkey. The key should
|
||||
# be saved in a new file rather than creating a new symlink.
|
||||
self.assertEqual(10, self.test_rc.save_successor(9, "with", "a",
|
||||
"key"))
|
||||
self.assertEqual(
|
||||
10, self.test_rc.save_successor(9, "with", "a",
|
||||
"key", self.cli_config))
|
||||
self.assertTrue(os.path.exists(self.test_rc.version("privkey", 10)))
|
||||
self.assertFalse(os.path.islink(self.test_rc.version("privkey", 10)))
|
||||
self.assertFalse(os.path.exists(temp_config_file))
|
||||
|
||||
def test_new_lineage(self):
|
||||
"""Test for new_lineage() class method."""
|
||||
from letsencrypt import storage
|
||||
result = storage.RenewableCert.new_lineage(
|
||||
"the-lineage.com", "cert", "privkey", "chain", None,
|
||||
self.defaults, self.cli_config)
|
||||
"the-lineage.com", "cert", "privkey", "chain", self.cli_config)
|
||||
# This consistency check tests most relevant properties about the
|
||||
# newly created cert lineage.
|
||||
# pylint: disable=protected-access
|
||||
@@ -565,27 +572,23 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
self.assertEqual(f.read(), "cert" + "chain")
|
||||
# Let's do it again and make sure it makes a different lineage
|
||||
result = storage.RenewableCert.new_lineage(
|
||||
"the-lineage.com", "cert2", "privkey2", "chain2", None,
|
||||
self.defaults, self.cli_config)
|
||||
"the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config)
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.cli_config.renewal_configs_dir, "the-lineage.com-0001.conf")))
|
||||
# Now trigger the detection of already existing files
|
||||
os.mkdir(os.path.join(
|
||||
self.cli_config.live_dir, "the-lineage.com-0002"))
|
||||
self.assertRaises(errors.CertStorageError,
|
||||
storage.RenewableCert.new_lineage,
|
||||
"the-lineage.com", "cert3", "privkey3", "chain3",
|
||||
None, self.defaults, self.cli_config)
|
||||
storage.RenewableCert.new_lineage, "the-lineage.com",
|
||||
"cert3", "privkey3", "chain3", self.cli_config)
|
||||
os.mkdir(os.path.join(self.cli_config.archive_dir, "other-example.com"))
|
||||
self.assertRaises(errors.CertStorageError,
|
||||
storage.RenewableCert.new_lineage,
|
||||
"other-example.com", "cert4", "privkey4", "chain4",
|
||||
None, self.defaults, self.cli_config)
|
||||
"other-example.com", "cert4",
|
||||
"privkey4", "chain4", self.cli_config)
|
||||
# Make sure it can accept renewal parameters
|
||||
params = {"stuff": "properties of stuff", "great": "awesome"}
|
||||
result = storage.RenewableCert.new_lineage(
|
||||
"the-lineage.com", "cert2", "privkey2", "chain2",
|
||||
params, self.defaults, self.cli_config)
|
||||
"the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config)
|
||||
# TODO: Conceivably we could test that the renewal parameters actually
|
||||
# got saved
|
||||
|
||||
@@ -597,8 +600,7 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
shutil.rmtree(self.cli_config.live_dir)
|
||||
|
||||
storage.RenewableCert.new_lineage(
|
||||
"the-lineage.com", "cert2", "privkey2", "chain2",
|
||||
None, self.defaults, self.cli_config)
|
||||
"the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config)
|
||||
self.assertTrue(os.path.exists(
|
||||
os.path.join(
|
||||
self.cli_config.renewal_configs_dir, "the-lineage.com.conf")))
|
||||
@@ -612,9 +614,8 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
from letsencrypt import storage
|
||||
mock_uln.return_value = "this_does_not_end_with_dot_conf", "yikes"
|
||||
self.assertRaises(errors.CertStorageError,
|
||||
storage.RenewableCert.new_lineage,
|
||||
"example.com", "cert", "privkey", "chain",
|
||||
None, self.defaults, self.cli_config)
|
||||
storage.RenewableCert.new_lineage, "example.com",
|
||||
"cert", "privkey", "chain", self.cli_config)
|
||||
|
||||
def test_bad_kind(self):
|
||||
self.assertRaises(
|
||||
@@ -681,114 +682,6 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
self.assertEqual(storage.add_time_interval(base_time, interval),
|
||||
excepted)
|
||||
|
||||
@mock.patch("letsencrypt.renewer.plugins_disco")
|
||||
@mock.patch("letsencrypt.account.AccountFileStorage")
|
||||
@mock.patch("letsencrypt.client.Client")
|
||||
def test_renew(self, mock_c, mock_acc_storage, mock_pd):
|
||||
from letsencrypt import renewer
|
||||
|
||||
test_cert = test_util.load_vector("cert-san.pem")
|
||||
for kind in ALL_FOUR:
|
||||
os.symlink(os.path.join("..", "..", "archive", "example.org",
|
||||
kind + "1.pem"),
|
||||
getattr(self.test_rc, kind))
|
||||
fill_with_sample_data(self.test_rc)
|
||||
with open(self.test_rc.cert, "w") as f:
|
||||
f.write(test_cert)
|
||||
|
||||
# Fails because renewalparams are missing
|
||||
self.assertFalse(renewer.renew(self.test_rc, 1))
|
||||
self.test_rc.configfile["renewalparams"] = {"some": "stuff"}
|
||||
# Fails because there's no authenticator specified
|
||||
self.assertFalse(renewer.renew(self.test_rc, 1))
|
||||
self.test_rc.configfile["renewalparams"]["rsa_key_size"] = "2048"
|
||||
self.test_rc.configfile["renewalparams"]["server"] = "acme.example.com"
|
||||
self.test_rc.configfile["renewalparams"]["authenticator"] = "fake"
|
||||
self.test_rc.configfile["renewalparams"]["tls_sni_01_port"] = "4430"
|
||||
self.test_rc.configfile["renewalparams"]["http01_port"] = "1234"
|
||||
self.test_rc.configfile["renewalparams"]["account"] = "abcde"
|
||||
self.test_rc.configfile["renewalparams"]["domains"] = ["example.com"]
|
||||
self.test_rc.configfile["renewalparams"]["config_dir"] = "config"
|
||||
self.test_rc.configfile["renewalparams"]["work_dir"] = "work"
|
||||
self.test_rc.configfile["renewalparams"]["logs_dir"] = "logs"
|
||||
mock_auth = mock.MagicMock()
|
||||
mock_pd.PluginsRegistry.find_all.return_value = {"apache": mock_auth}
|
||||
# Fails because "fake" != "apache"
|
||||
self.assertFalse(renewer.renew(self.test_rc, 1))
|
||||
self.test_rc.configfile["renewalparams"]["authenticator"] = "apache"
|
||||
mock_client = mock.MagicMock()
|
||||
# pylint: disable=star-args
|
||||
comparable_cert = jose.ComparableX509(CERT)
|
||||
mock_client.obtain_certificate.return_value = (
|
||||
mock.MagicMock(body=comparable_cert), [comparable_cert],
|
||||
mock.Mock(pem="key"), mock.sentinel.csr)
|
||||
mock_c.return_value = mock_client
|
||||
self.assertEqual(2, renewer.renew(self.test_rc, 1))
|
||||
# TODO: We could also make several assertions about calls that should
|
||||
# have been made to the mock functions here.
|
||||
mock_acc_storage().load.assert_called_once_with(account_id="abcde")
|
||||
mock_client.obtain_certificate.return_value = (
|
||||
mock.sentinel.certr, [], mock.sentinel.key, mock.sentinel.csr)
|
||||
# This should fail because the renewal itself appears to fail
|
||||
self.assertFalse(renewer.renew(self.test_rc, 1))
|
||||
|
||||
def _common_cli_args(self):
|
||||
return [
|
||||
"--config-dir", self.cli_config.config_dir,
|
||||
"--work-dir", self.cli_config.work_dir,
|
||||
"--logs-dir", self.cli_config.logs_dir,
|
||||
]
|
||||
|
||||
@mock.patch("letsencrypt.renewer.notify")
|
||||
@mock.patch("letsencrypt.storage.RenewableCert")
|
||||
@mock.patch("letsencrypt.renewer.renew")
|
||||
def test_main(self, mock_renew, mock_rc, mock_notify):
|
||||
from letsencrypt import renewer
|
||||
mock_rc_instance = mock.MagicMock()
|
||||
mock_rc_instance.should_autodeploy.return_value = True
|
||||
mock_rc_instance.should_autorenew.return_value = True
|
||||
mock_rc_instance.latest_common_version.return_value = 10
|
||||
mock_rc.return_value = mock_rc_instance
|
||||
with open(os.path.join(self.cli_config.renewal_configs_dir,
|
||||
"example.org.conf"), "w") as f:
|
||||
# This isn't actually parsed in this test; we have a separate
|
||||
# test_initialization that tests the initialization, assuming
|
||||
# that configobj can correctly parse the config file.
|
||||
f.write("cert = cert.pem\nprivkey = privkey.pem\n")
|
||||
f.write("chain = chain.pem\nfullchain = fullchain.pem\n")
|
||||
with open(os.path.join(self.cli_config.renewal_configs_dir,
|
||||
"example.com.conf"), "w") as f:
|
||||
f.write("cert = cert.pem\nprivkey = privkey.pem\n")
|
||||
f.write("chain = chain.pem\nfullchain = fullchain.pem\n")
|
||||
renewer.main(cli_args=self._common_cli_args())
|
||||
self.assertEqual(mock_rc.call_count, 2)
|
||||
self.assertEqual(mock_rc_instance.update_all_links_to.call_count, 2)
|
||||
self.assertEqual(mock_notify.notify.call_count, 4)
|
||||
self.assertEqual(mock_renew.call_count, 2)
|
||||
# If we have instances that don't need any work done, no work should
|
||||
# be done (call counts associated with processing deployments or
|
||||
# renewals should not increase).
|
||||
mock_happy_instance = mock.MagicMock()
|
||||
mock_happy_instance.should_autodeploy.return_value = False
|
||||
mock_happy_instance.should_autorenew.return_value = False
|
||||
mock_happy_instance.latest_common_version.return_value = 10
|
||||
mock_rc.return_value = mock_happy_instance
|
||||
renewer.main(cli_args=self._common_cli_args())
|
||||
self.assertEqual(mock_rc.call_count, 4)
|
||||
self.assertEqual(mock_happy_instance.update_all_links_to.call_count, 0)
|
||||
self.assertEqual(mock_notify.notify.call_count, 4)
|
||||
self.assertEqual(mock_renew.call_count, 2)
|
||||
|
||||
def test_bad_config_file(self):
|
||||
from letsencrypt import renewer
|
||||
os.unlink(os.path.join(self.cli_config.renewal_configs_dir,
|
||||
"example.org.conf"))
|
||||
with open(os.path.join(self.cli_config.renewal_configs_dir,
|
||||
"bad.conf"), "w") as f:
|
||||
f.write("incomplete = configfile\n")
|
||||
renewer.main(cli_args=self._common_cli_args())
|
||||
# The errors.CertStorageError is caught inside and nothing happens.
|
||||
|
||||
def test_missing_cert(self):
|
||||
from letsencrypt import storage
|
||||
self.assertRaises(errors.CertStorageError,
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIE1DCCA7ygAwIBAgITAPoz/CBluNQV/Eh9F+CS6dSxEDANBgkqhkiG9w0BAQsF
|
||||
ADAfMR0wGwYDVQQDDBRoYXBweSBoYWNrZXIgZmFrZSBDQTAeFw0xNjAyMDIyMzQ5
|
||||
MDBaFw0xNjA1MDIyMzQ5MDBaMBQxEjAQBgNVBAMTCWlzbm90Lm9yZzCCASIwDQYJ
|
||||
KoZIhvcNAQEBBQADggEPADCCAQoCggEBALyudqLKcIdWZ5VaK1fuhlEDbZtvs2E+
|
||||
slm4dmSS1nFve7MdlZ69K0gdtnhkiPQ0wGQTligeDZ8fY8iL87GZO0tp5f7S+QJN
|
||||
NYCiYw6j4qp5JBy/zG22kJz1Quu7/vXMYLzLvK6x6YixiWAWyqqvlUVBLS1r4W3h
|
||||
A5Z+F1EIsXeyz7TJe3lAzIWAAxpfH9OviIz2rEDotuCdU771USLLNSw4qJojNlTx
|
||||
UpZG6lGFs8KGb8tqROXknaMKE4PvN3SITixSUTFbktt1Wz60moWbNdLMKvgkzuUP
|
||||
r4viO2P4SO5slNAY0ZeEssPpVAelN3EvrAcEZtoKmG5fnQDVo8uVag0CAwEAAaOC
|
||||
AhIwggIOMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB
|
||||
BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUqhI4u6aaPrcYQnmypxV8Tap8
|
||||
L54wHwYDVR0jBBgwFoAU+3hPEvlgFYMsnxd/NBmzLjbqQYkweAYIKwYBBQUHAQEE
|
||||
bDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5zdGFnaW5nLXgxLmxldHNlbmNy
|
||||
eXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9jZXJ0LnN0YWdpbmcteDEubGV0
|
||||
c2VuY3J5cHQub3JnLzAUBgNVHREEDTALgglpc25vdC5vcmcwgf4GA1UdIASB9jCB
|
||||
8zAIBgZngQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRw
|
||||
Oi8vY3BzLmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENl
|
||||
cnRpZmljYXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFy
|
||||
dGllcyBhbmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRl
|
||||
IFBvbGljeSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0
|
||||
b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAbAhX6FfQwELayneY4l5RvYSdw/Jj5CRy
|
||||
KzrM7ISld7x9YPpxX6Pmht/YyMhLWrtxvFUR2+RNhSIYB8IjQEjmKjvR7UNeiUve
|
||||
jzPEAuTg/9m3i0FJpPHc2aKGzlLFQCMm5/RrvnXI6ljIcyhocLvMiN46iexcExI2
|
||||
Ese3w8GoH6wARYKxU/QBexfoXQLgtAbYzNRE6EgKWtB+txV+7+d2MgbhCEit5VwU
|
||||
+ydT8inp9URsA7iKM03hDdGOBysddkrm1/yEhVy/Oo6bT9WMAUHVvz61hHekWcSf
|
||||
rAQ6BayubvWOUx06eTowXr1gln/rl+WXOxcsJeag127NuhmHOCXZxQ==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDETCCAfmgAwIBAgIJAJzxkS6o1QkIMA0GCSqGSIb3DQEBCwUAMB8xHTAbBgNV
|
||||
BAMMFGhhcHB5IGhhY2tlciBmYWtlIENBMB4XDTE1MDQwNzIzNTAzOFoXDTI1MDQw
|
||||
NDIzNTAzOFowHzEdMBsGA1UEAwwUaGFwcHkgaGFja2VyIGZha2UgQ0EwggEiMA0G
|
||||
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCCkd5mgXFErJ3F2M0E9dw+Ta/md5i
|
||||
8TDId01HberAApqmydG7UZYF3zLTSzNjlNSOmtybvrSGUnZ9r9tSQcL8VM6WUOM8
|
||||
tnIpiIjEA2QkBycMwvRmZ/B2ltPdYs/R9BqNwO1g18GDZrHSzUYtNKNeFI6Glamj
|
||||
7GK2Vr0SmiEamlNIR5ktAFsEErzf/d4jCF7sosMsJpMCm1p58QkP4LHLShVLXDa8
|
||||
BMfVoI+ipYcA08iNUFkgW8VWDclIDxcysa0psDDtMjX3+4aPkE/cefmP+1xOfUuD
|
||||
HOGV8XFynsP4EpTfVOZr0/g9gYQ7ZArqXX7GTQkFqduwPm/w5qxSPTarAgMBAAGj
|
||||
UDBOMB0GA1UdDgQWBBT7eE8S+WAVgyyfF380GbMuNupBiTAfBgNVHSMEGDAWgBT7
|
||||
eE8S+WAVgyyfF380GbMuNupBiTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA
|
||||
A4IBAQAd9Da+Zv+TjMv7NTAmliqnWHY6d3UxEZN3hFEJ58IQVHbBZVZdW7zhRktB
|
||||
vR05Kweac0HJeK91TKmzvXl21IXLvh0gcNLU/uweD3no/snfdB4OoFompljThmgl
|
||||
zBqiqWoKBJQrLCA8w5UB+ReomRYd/EYXF/6TAfzm6hr//Xt5mPiUHPdvYt75lMAo
|
||||
vRxLSbF8TSQ6b7BYxISWjPgFASNNqJNHEItWsmQMtAjjwzb9cs01XH9pChVAWn9L
|
||||
oeMKa+SlHSYrWG93+EcrIH/dGU76uNOiaDzBSKvaehG53h25MHuO1anNICJvZovW
|
||||
rFo4Uv1EnkKJm3vJFe50eJGhEKlx
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,47 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIE1DCCA7ygAwIBAgITAPoz/CBluNQV/Eh9F+CS6dSxEDANBgkqhkiG9w0BAQsF
|
||||
ADAfMR0wGwYDVQQDDBRoYXBweSBoYWNrZXIgZmFrZSBDQTAeFw0xNjAyMDIyMzQ5
|
||||
MDBaFw0xNjA1MDIyMzQ5MDBaMBQxEjAQBgNVBAMTCWlzbm90Lm9yZzCCASIwDQYJ
|
||||
KoZIhvcNAQEBBQADggEPADCCAQoCggEBALyudqLKcIdWZ5VaK1fuhlEDbZtvs2E+
|
||||
slm4dmSS1nFve7MdlZ69K0gdtnhkiPQ0wGQTligeDZ8fY8iL87GZO0tp5f7S+QJN
|
||||
NYCiYw6j4qp5JBy/zG22kJz1Quu7/vXMYLzLvK6x6YixiWAWyqqvlUVBLS1r4W3h
|
||||
A5Z+F1EIsXeyz7TJe3lAzIWAAxpfH9OviIz2rEDotuCdU771USLLNSw4qJojNlTx
|
||||
UpZG6lGFs8KGb8tqROXknaMKE4PvN3SITixSUTFbktt1Wz60moWbNdLMKvgkzuUP
|
||||
r4viO2P4SO5slNAY0ZeEssPpVAelN3EvrAcEZtoKmG5fnQDVo8uVag0CAwEAAaOC
|
||||
AhIwggIOMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB
|
||||
BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUqhI4u6aaPrcYQnmypxV8Tap8
|
||||
L54wHwYDVR0jBBgwFoAU+3hPEvlgFYMsnxd/NBmzLjbqQYkweAYIKwYBBQUHAQEE
|
||||
bDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5zdGFnaW5nLXgxLmxldHNlbmNy
|
||||
eXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9jZXJ0LnN0YWdpbmcteDEubGV0
|
||||
c2VuY3J5cHQub3JnLzAUBgNVHREEDTALgglpc25vdC5vcmcwgf4GA1UdIASB9jCB
|
||||
8zAIBgZngQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRw
|
||||
Oi8vY3BzLmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENl
|
||||
cnRpZmljYXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFy
|
||||
dGllcyBhbmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRl
|
||||
IFBvbGljeSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0
|
||||
b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAbAhX6FfQwELayneY4l5RvYSdw/Jj5CRy
|
||||
KzrM7ISld7x9YPpxX6Pmht/YyMhLWrtxvFUR2+RNhSIYB8IjQEjmKjvR7UNeiUve
|
||||
jzPEAuTg/9m3i0FJpPHc2aKGzlLFQCMm5/RrvnXI6ljIcyhocLvMiN46iexcExI2
|
||||
Ese3w8GoH6wARYKxU/QBexfoXQLgtAbYzNRE6EgKWtB+txV+7+d2MgbhCEit5VwU
|
||||
+ydT8inp9URsA7iKM03hDdGOBysddkrm1/yEhVy/Oo6bT9WMAUHVvz61hHekWcSf
|
||||
rAQ6BayubvWOUx06eTowXr1gln/rl+WXOxcsJeag127NuhmHOCXZxQ==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDETCCAfmgAwIBAgIJAJzxkS6o1QkIMA0GCSqGSIb3DQEBCwUAMB8xHTAbBgNV
|
||||
BAMMFGhhcHB5IGhhY2tlciBmYWtlIENBMB4XDTE1MDQwNzIzNTAzOFoXDTI1MDQw
|
||||
NDIzNTAzOFowHzEdMBsGA1UEAwwUaGFwcHkgaGFja2VyIGZha2UgQ0EwggEiMA0G
|
||||
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCCkd5mgXFErJ3F2M0E9dw+Ta/md5i
|
||||
8TDId01HberAApqmydG7UZYF3zLTSzNjlNSOmtybvrSGUnZ9r9tSQcL8VM6WUOM8
|
||||
tnIpiIjEA2QkBycMwvRmZ/B2ltPdYs/R9BqNwO1g18GDZrHSzUYtNKNeFI6Glamj
|
||||
7GK2Vr0SmiEamlNIR5ktAFsEErzf/d4jCF7sosMsJpMCm1p58QkP4LHLShVLXDa8
|
||||
BMfVoI+ipYcA08iNUFkgW8VWDclIDxcysa0psDDtMjX3+4aPkE/cefmP+1xOfUuD
|
||||
HOGV8XFynsP4EpTfVOZr0/g9gYQ7ZArqXX7GTQkFqduwPm/w5qxSPTarAgMBAAGj
|
||||
UDBOMB0GA1UdDgQWBBT7eE8S+WAVgyyfF380GbMuNupBiTAfBgNVHSMEGDAWgBT7
|
||||
eE8S+WAVgyyfF380GbMuNupBiTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA
|
||||
A4IBAQAd9Da+Zv+TjMv7NTAmliqnWHY6d3UxEZN3hFEJ58IQVHbBZVZdW7zhRktB
|
||||
vR05Kweac0HJeK91TKmzvXl21IXLvh0gcNLU/uweD3no/snfdB4OoFompljThmgl
|
||||
zBqiqWoKBJQrLCA8w5UB+ReomRYd/EYXF/6TAfzm6hr//Xt5mPiUHPdvYt75lMAo
|
||||
vRxLSbF8TSQ6b7BYxISWjPgFASNNqJNHEItWsmQMtAjjwzb9cs01XH9pChVAWn9L
|
||||
oeMKa+SlHSYrWG93+EcrIH/dGU76uNOiaDzBSKvaehG53h25MHuO1anNICJvZovW
|
||||
rFo4Uv1EnkKJm3vJFe50eJGhEKlx
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC8rnaiynCHVmeV
|
||||
WitX7oZRA22bb7NhPrJZuHZkktZxb3uzHZWevStIHbZ4ZIj0NMBkE5YoHg2fH2PI
|
||||
i/OxmTtLaeX+0vkCTTWAomMOo+KqeSQcv8xttpCc9ULru/71zGC8y7yusemIsYlg
|
||||
Fsqqr5VFQS0ta+Ft4QOWfhdRCLF3ss+0yXt5QMyFgAMaXx/Tr4iM9qxA6LbgnVO+
|
||||
9VEiyzUsOKiaIzZU8VKWRupRhbPChm/LakTl5J2jChOD7zd0iE4sUlExW5LbdVs+
|
||||
tJqFmzXSzCr4JM7lD6+L4jtj+EjubJTQGNGXhLLD6VQHpTdxL6wHBGbaCphuX50A
|
||||
1aPLlWoNAgMBAAECggEAfKKWFWS6PnwSAnNErFoQeZVVItb/XB5JO8EA2+CvLNFi
|
||||
mefR/MCixYlzDkYCvaXW7ISPrMJlZxYaGNBx0oAQzfkPB2wfNqj/zY/29SXGxast
|
||||
8puzk0mEb1oHsaZGfeFaiXvfkFpPlI8J2uJTT7qaVNv/1sArciSv9QonpsyiRhlB
|
||||
yqT49juNVoR1tJHyXzkkRfHKTG8OlJd4kuFOl3fM9dTFPQ/ft0kTNAQ/B4SFvSwF
|
||||
RJsbLbsbFGsUdV9ekE6UX6oWD/Ah707rvgtCyS0Bc+0O3t2EKwmm3RXPRUMHCVxE
|
||||
bKdTxRB4etbjMVXMuVhB8Y4GbfrtMCy+qxZQ6znCAQKBgQDr7bcYAZVZp/nBMVB+
|
||||
lBO9w73J6lnEWm6bZ9728KlGAKETaRhxZQSi6TN6MWwNwnk6rinyz4uVwVr9ZRCs
|
||||
WkB1TbvW0JNcWdr3YClwsKXAt8X22bjGe0LagDJHG6r1TPS+MdovOS2M6IMaxlbT
|
||||
rzFhSJ8ojLX3tqnOsmc7YAFLjQKBgQDMu8E9hoJt82lQzOGrjHmGzGEu2GLx9WKO
|
||||
e4nkj335kX6fIhMMqSXBFbTJZwXoYvk5J8ZnaARbYG0m5nxDCwRjX5HWa8q0B2Po
|
||||
ta53w01sKKznzlPjUhsdhEthun7MCFfLZpgvcZ9xVzOXo3/Zfn2+RrsPSjrVDqBy
|
||||
hj+k5mW4gQKBgHFWKf3LTO7cBdvsD8ou4mjn7nVgMi1kb/wR4wdnxzmMtdR4STi4
|
||||
GYkVVBhgQ5M8mDY7UoWFdH3FfCt8cI0Lcimn5ROl8RSNSeZKeL3c7lNtNRmHr/8R
|
||||
WaVTrlOAlBjxFiWEF1dWNW6ah9jF7RIV+DfOxj6ZkhTk2CAmjfb1AMpFAoGABf96
|
||||
KdNG/vGipDtcYSo8ZTaXoke0nmISARqdb5TEnAsnKoJVDInoEUARi9T411YO9x2z
|
||||
MlRZzFOG3xzhhxVLi53BKAcAaUXOJ4MrGVcfbYvDhQcGbiJ5qOO3UaWlEVUtPUhE
|
||||
LR+nDCsB1+9yT2zlQi3QTSJflt5W1QQZ2TrmwAECgYEAvQ7+sTcHs1K9yKj7koEu
|
||||
A19FbMA0IwvrVRcV/VqmlsoW6e6wW2YND+GtaDbKdD0aBPivqLJwpNFrsRA+W0iB
|
||||
vzmML6sKhhL+j7tjSgq+iQdBkKz0j9PyReuhe9CRnljMmyun+4qKEk0KUvxBrjPY
|
||||
Skn+ML18qyUoEPnmbpfHxCs=
|
||||
-----END PRIVATE KEY-----
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../archive/sample-renewal/cert1.pem
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../archive/sample-renewal/chain1.pem
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../archive/sample-renewal/fullchain1.pem
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../archive/sample-renewal/privkey1.pem
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
cert = MAGICDIR/live/sample-renewal/cert.pem
|
||||
privkey = MAGICDIR/live/sample-renewal/privkey.pem
|
||||
chain = MAGICDIR/live/sample-renewal/chain.pem
|
||||
fullchain = MAGICDIR/live/sample-renewal/fullchain.pem
|
||||
renew_before_expiry = 1 year
|
||||
|
||||
# Options and defaults used in the renewal process
|
||||
[renewalparams]
|
||||
no_self_upgrade = False
|
||||
apache_enmod = a2enmod
|
||||
no_verify_ssl = False
|
||||
ifaces = None
|
||||
apache_dismod = a2dismod
|
||||
register_unsafely_without_email = False
|
||||
apache_handle_modules = True
|
||||
uir = None
|
||||
installer = none
|
||||
nginx_ctl = nginx
|
||||
config_dir = MAGICDIR
|
||||
text_mode = False
|
||||
func = <function obtain_cert at 0x7f093a163c08>
|
||||
staging = True
|
||||
prepare = False
|
||||
work_dir = /var/lib/letsencrypt
|
||||
tos = False
|
||||
init = False
|
||||
http01_port = 80
|
||||
duplicate = False
|
||||
noninteractive_mode = True
|
||||
key_path = None
|
||||
nginx = False
|
||||
nginx_server_root = /etc/nginx
|
||||
fullchain_path = /home/ubuntu/letsencrypt/chain.pem
|
||||
email = None
|
||||
csr = None
|
||||
agree_dev_preview = None
|
||||
redirect = None
|
||||
verb = certonly
|
||||
verbose_count = -3
|
||||
config_file = None
|
||||
renew_by_default = False
|
||||
hsts = False
|
||||
apache_handle_sites = True
|
||||
authenticator = standalone
|
||||
domains = isnot.org,
|
||||
rsa_key_size = 2048
|
||||
apache_challenge_location = /etc/apache2
|
||||
checkpoints = 1
|
||||
manual_test_mode = False
|
||||
apache = False
|
||||
cert_path = /home/ubuntu/letsencrypt/cert.pem
|
||||
webroot_path = None
|
||||
reinstall = False
|
||||
expand = False
|
||||
strict_permissions = False
|
||||
apache_server_root = /etc/apache2
|
||||
account = None
|
||||
dry_run = False
|
||||
manual_public_ip_logging_ok = False
|
||||
chain_path = /home/ubuntu/letsencrypt/chain.pem
|
||||
break_my_certs = False
|
||||
standalone = True
|
||||
manual = False
|
||||
server = https://acme-staging.api.letsencrypt.org/directory
|
||||
standalone_supported_challenges = "tls-sni-01,http-01"
|
||||
webroot = False
|
||||
os_packages_only = False
|
||||
apache_init_script = None
|
||||
user_agent = None
|
||||
apache_le_vhost_ext = -le-ssl.conf
|
||||
debug = False
|
||||
tls_sni_01_port = 443
|
||||
logs_dir = /var/log/letsencrypt
|
||||
apache_vhost_root = /etc/apache2/sites-available
|
||||
configurator = None
|
||||
[[webroot_map]]
|
||||
@@ -130,7 +130,6 @@ setup(
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'letsencrypt = letsencrypt.cli:main',
|
||||
'letsencrypt-renewer = letsencrypt.renewer:main',
|
||||
],
|
||||
'letsencrypt.plugins': [
|
||||
'manual = letsencrypt.plugins.manual:Authenticator',
|
||||
|
||||
@@ -27,6 +27,13 @@ common() {
|
||||
"$@"
|
||||
}
|
||||
|
||||
common_no_force_renew() {
|
||||
letsencrypt_test_no_force_renew \
|
||||
--authenticator standalone \
|
||||
--installer null \
|
||||
"$@"
|
||||
}
|
||||
|
||||
common --domains le1.wtf --standalone-supported-challenges tls-sni-01 auth
|
||||
common --domains le2.wtf --standalone-supported-challenges http-01 run
|
||||
common -a manual -d le.wtf auth
|
||||
@@ -44,20 +51,21 @@ common --domains le3.wtf install \
|
||||
--cert-path "${root}/csr/cert.pem" \
|
||||
--key-path "${root}/csr/key.pem"
|
||||
|
||||
# the following assumes that Boulder issues certificates for less than
|
||||
# 10 years, otherwise renewal will not take place
|
||||
cat <<EOF > "$root/conf/renewer.conf"
|
||||
renew_before_expiry = 10 years
|
||||
deploy_before_expiry = 10 years
|
||||
EOF
|
||||
letsencrypt-renewer $store_flags
|
||||
dir="$root/conf/archive/le1.wtf"
|
||||
for x in cert chain fullchain privkey;
|
||||
do
|
||||
latest="$(ls -1t $dir/ | grep -e "^${x}" | head -n1)"
|
||||
live="$($readlink -f "$root/conf/live/le1.wtf/${x}.pem")"
|
||||
[ "${dir}/${latest}" = "$live" ] # renewer fails this test
|
||||
done
|
||||
# This won't renew (because it's not time yet)
|
||||
letsencrypt_test_no_force_renew --authenticator standalone --installer null renew
|
||||
|
||||
# This will renew because the expiry is less than 10 years from now
|
||||
sed -i "4arenew_before_expiry = 10 years" "$root/conf/renewal/le1.wtf.conf"
|
||||
letsencrypt_test_no_force_renew --authenticator standalone --installer null renew
|
||||
|
||||
ls "$root/conf/archive/le1.wtf"
|
||||
# dir="$root/conf/archive/le1.wtf"
|
||||
# for x in cert chain fullchain privkey;
|
||||
# do
|
||||
# latest="$(ls -1t $dir/ | grep -e "^${x}" | head -n1)"
|
||||
# live="$($readlink -f "$root/conf/live/le1.wtf/${x}.pem")"
|
||||
# [ "${dir}/${latest}" = "$live" ] # renewer fails this test
|
||||
# done
|
||||
|
||||
# revoke by account key
|
||||
common revoke --cert-path "$root/conf/live/le.wtf/cert.pem"
|
||||
|
||||
@@ -28,3 +28,20 @@ letsencrypt_test () {
|
||||
-vvvvvvv \
|
||||
"$@"
|
||||
}
|
||||
|
||||
letsencrypt_test_no_force_renew () {
|
||||
letsencrypt \
|
||||
--server "${SERVER:-http://localhost:4000/directory}" \
|
||||
--no-verify-ssl \
|
||||
--tls-sni-01-port 5001 \
|
||||
--http-01-port 5002 \
|
||||
--manual-test-mode \
|
||||
$store_flags \
|
||||
--text \
|
||||
--no-redirect \
|
||||
--agree-tos \
|
||||
--register-unsafely-without-email \
|
||||
--debug \
|
||||
-vvvvvvv \
|
||||
"$@"
|
||||
}
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
#!/bin/bash -x
|
||||
|
||||
# $OS_TYPE $PUBLIC_IP $PRIVATE_IP $PUBLIC_HOSTNAME $BOULDER_URL
|
||||
# are dynamically set at execution
|
||||
|
||||
# run letsencrypt-apache2 via letsencrypt-auto
|
||||
cd letsencrypt
|
||||
|
||||
export SUDO=sudo
|
||||
if [ -f /etc/debian_version ] ; then
|
||||
echo "Bootstrapping dependencies for Debian-based OSes..."
|
||||
$SUDO bootstrap/_deb_common.sh
|
||||
elif [ -f /etc/redhat-release ] ; then
|
||||
echo "Bootstrapping dependencies for RedHat-based OSes..."
|
||||
$SUDO bootstrap/_rpm_common.sh
|
||||
else
|
||||
echo "Dont have bootstrapping for this OS!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bootstrap/dev/venv.sh
|
||||
sudo venv/bin/letsencrypt certonly --debug --standalone -t --agree-dev-preview --agree-tos \
|
||||
--renew-by-default --redirect --register-unsafely-without-email \
|
||||
--domain $PUBLIC_HOSTNAME --server $BOULDER_URL -v
|
||||
if [ $? -ne 0 ] ; then
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
if [ "$OS_TYPE" = "ubuntu" ] ; then
|
||||
venv/bin/tox -e apacheconftest
|
||||
else
|
||||
echo Not running hackish apache tests on $OS_TYPE
|
||||
fi
|
||||
|
||||
if [ $? -ne 0 ] ; then
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
sudo venv/bin/letsencrypt renew --renew-by-default
|
||||
|
||||
if [ $? -ne 0 ] ; then
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
|
||||
ls /etc/letsencrypt/archive/$PUBLIC_HOSTNAME | grep -q 2.pem
|
||||
|
||||
if [ $? -ne 0 ] ; then
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
# return error if any of the subtests failed
|
||||
if [ "$FAIL" = 1 ] ; then
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user