mirror of
https://github.com/certbot/certbot.git
synced 2026-08-02 19:31:51 +02:00
Merge remote-tracking branch 'origin/master' into issue_2347
This commit is contained in:
+134
-40
@@ -337,6 +337,7 @@ def _handle_identical_cert_request(config, cert):
|
|||||||
else:
|
else:
|
||||||
assert False, "This is impossible"
|
assert False, "This is impossible"
|
||||||
|
|
||||||
|
|
||||||
def _handle_subset_cert_request(config, domains, cert):
|
def _handle_subset_cert_request(config, domains, cert):
|
||||||
"""Figure out what to do if a previous cert had a subset of the names now requested
|
"""Figure out what to do if a previous cert had a subset of the names now requested
|
||||||
|
|
||||||
@@ -415,8 +416,10 @@ def _suggest_donation_if_appropriate(config):
|
|||||||
reporter_util.add_message(msg, reporter_util.LOW_PRIORITY)
|
reporter_util.add_message(msg, reporter_util.LOW_PRIORITY)
|
||||||
|
|
||||||
|
|
||||||
def _report_successful_dry_run():
|
|
||||||
|
def _report_successful_dry_run(config):
|
||||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||||
|
if config.verb != "renew":
|
||||||
reporter_util.add_message("The dry run was successful.",
|
reporter_util.add_message("The dry run was successful.",
|
||||||
reporter_util.HIGH_PRIORITY, on_crash=False)
|
reporter_util.HIGH_PRIORITY, on_crash=False)
|
||||||
|
|
||||||
@@ -438,7 +441,7 @@ def _auth_from_domains(le_client, config, domains, lineage=None):
|
|||||||
else:
|
else:
|
||||||
# Renewal, where we already know the specific lineage we're
|
# Renewal, where we already know the specific lineage we're
|
||||||
# interested in
|
# interested in
|
||||||
action = "renew" if _should_renew(config, lineage) else "reinstall"
|
action = "renew"
|
||||||
|
|
||||||
if action == "reinstall":
|
if action == "reinstall":
|
||||||
# The lineage already exists; allow the caller to try installing
|
# The lineage already exists; allow the caller to try installing
|
||||||
@@ -470,7 +473,7 @@ def _auth_from_domains(le_client, config, domains, lineage=None):
|
|||||||
if lineage is False:
|
if lineage is False:
|
||||||
raise errors.Error("Certificate could not be obtained")
|
raise errors.Error("Certificate could not be obtained")
|
||||||
|
|
||||||
if not config.dry_run:
|
if not config.dry_run and not config.verb == "renew":
|
||||||
_report_new_cert(lineage.cert, lineage.fullchain)
|
_report_new_cert(lineage.cert, lineage.fullchain)
|
||||||
|
|
||||||
return lineage, action
|
return lineage, action
|
||||||
@@ -487,7 +490,7 @@ def _avoid_invalidating_lineage(config, lineage, original_server):
|
|||||||
open(lineage.cert).read())
|
open(lineage.cert).read())
|
||||||
# all our test certs are from happy hacker fake CA, though maybe one day
|
# all our test certs are from happy hacker fake CA, though maybe one day
|
||||||
# we should test more methodically
|
# we should test more methodically
|
||||||
now_valid = not "fake" in repr(latest_cert.get_issuer()).lower()
|
now_valid = "fake" not in repr(latest_cert.get_issuer()).lower()
|
||||||
|
|
||||||
if _is_staging(config.server):
|
if _is_staging(config.server):
|
||||||
if not _is_staging(original_server) or now_valid:
|
if not _is_staging(original_server) or now_valid:
|
||||||
@@ -546,6 +549,7 @@ def set_configurator(previously, now):
|
|||||||
raise errors.PluginSelectionError(msg.format(repr(previously), repr(now)))
|
raise errors.PluginSelectionError(msg.format(repr(previously), repr(now)))
|
||||||
return now
|
return now
|
||||||
|
|
||||||
|
|
||||||
def cli_plugin_requests(config):
|
def cli_plugin_requests(config):
|
||||||
"""
|
"""
|
||||||
Figure out which plugins the user requested with CLI and config options
|
Figure out which plugins the user requested with CLI and config options
|
||||||
@@ -574,6 +578,7 @@ def cli_plugin_requests(config):
|
|||||||
|
|
||||||
noninstaller_plugins = ["webroot", "manual", "standalone"]
|
noninstaller_plugins = ["webroot", "manual", "standalone"]
|
||||||
|
|
||||||
|
|
||||||
def choose_configurator_plugins(config, plugins, verb):
|
def choose_configurator_plugins(config, plugins, verb):
|
||||||
"""
|
"""
|
||||||
Figure out which configurator we're going to use, modifies
|
Figure out which configurator we're going to use, modifies
|
||||||
@@ -671,16 +676,12 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals
|
|||||||
def obtain_cert(config, plugins, lineage=None):
|
def obtain_cert(config, plugins, lineage=None):
|
||||||
"""Implements "certonly": authenticate & obtain cert, but do not install it."""
|
"""Implements "certonly": authenticate & obtain cert, but do not install it."""
|
||||||
|
|
||||||
if config.domains and config.csr is not None:
|
|
||||||
# TODO: --csr could have a priority, when --domains is
|
|
||||||
# supplied, check if CSR matches given domains?
|
|
||||||
return "--domains and --csr are mutually exclusive"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# installers are used in auth mode to determine domain names
|
# installers are used in auth mode to determine domain names
|
||||||
installer, authenticator = choose_configurator_plugins(config, plugins, "certonly")
|
installer, authenticator = choose_configurator_plugins(config, plugins, "certonly")
|
||||||
except errors.PluginSelectionError as e:
|
except errors.PluginSelectionError as e:
|
||||||
return e.message
|
logger.info("Could not choose appropriate plugin: %s", e)
|
||||||
|
raise
|
||||||
|
|
||||||
# TODO: Handle errors from _init_le_client?
|
# TODO: Handle errors from _init_le_client?
|
||||||
le_client = _init_le_client(config, authenticator, installer)
|
le_client = _init_le_client(config, authenticator, installer)
|
||||||
@@ -688,8 +689,8 @@ def obtain_cert(config, plugins, lineage=None):
|
|||||||
# This is a special case; cert and chain are simply saved
|
# This is a special case; cert and chain are simply saved
|
||||||
if config.csr is not None:
|
if config.csr is not None:
|
||||||
assert lineage is None, "Did not expect a CSR with a RenewableCert"
|
assert lineage is None, "Did not expect a CSR with a RenewableCert"
|
||||||
certr, chain = le_client.obtain_certificate_from_csr(le_util.CSR(
|
csr, typ = config.actual_csr
|
||||||
file=config.csr[0], data=config.csr[1], form="der"))
|
certr, chain = le_client.obtain_certificate_from_csr(config.domains, csr, typ)
|
||||||
if config.dry_run:
|
if config.dry_run:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Dry run: skipping saving certificate to %s", config.cert_path)
|
"Dry run: skipping saving certificate to %s", config.cert_path)
|
||||||
@@ -702,13 +703,19 @@ def obtain_cert(config, plugins, lineage=None):
|
|||||||
_auth_from_domains(le_client, config, domains, lineage)
|
_auth_from_domains(le_client, config, domains, lineage)
|
||||||
|
|
||||||
if config.dry_run:
|
if config.dry_run:
|
||||||
_report_successful_dry_run()
|
_report_successful_dry_run(config)
|
||||||
elif config.verb == "renew" and installer is not None:
|
elif config.verb == "renew":
|
||||||
|
if installer is None:
|
||||||
|
# Tell the user that the server was not restarted.
|
||||||
|
print("new certificate deployed without reload, fullchain is",
|
||||||
|
lineage.fullchain)
|
||||||
|
else:
|
||||||
# In case of a renewal, reload server to pick up new certificate.
|
# In case of a renewal, reload server to pick up new certificate.
|
||||||
# In principle we could have a configuration option to inhibit this
|
# In principle we could have a configuration option to inhibit this
|
||||||
# from happening.
|
# from happening.
|
||||||
installer.restart()
|
installer.restart()
|
||||||
print("reloaded")
|
print("new certificate deployed with reload of",
|
||||||
|
config.installer, "server; fullchain is", lineage.fullchain)
|
||||||
_suggest_donation_if_appropriate(config)
|
_suggest_donation_if_appropriate(config)
|
||||||
|
|
||||||
|
|
||||||
@@ -793,6 +800,7 @@ def _restore_required_config_elements(config, renewalparams):
|
|||||||
raise errors.Error(
|
raise errors.Error(
|
||||||
"Expected a numeric value for {0}".format(config_item))
|
"Expected a numeric value for {0}".format(config_item))
|
||||||
|
|
||||||
|
|
||||||
def _restore_plugin_configs(config, renewalparams):
|
def _restore_plugin_configs(config, renewalparams):
|
||||||
"""Sets plugin specific values in config from renewalparams
|
"""Sets plugin specific values in config from renewalparams
|
||||||
|
|
||||||
@@ -825,7 +833,6 @@ def _restore_plugin_configs(config, renewalparams):
|
|||||||
if config_value == "None":
|
if config_value == "None":
|
||||||
setattr(config.namespace, config_item, None)
|
setattr(config.namespace, config_item, None)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for action in _parser.parser._actions: # pylint: disable=protected-access
|
for action in _parser.parser._actions: # pylint: disable=protected-access
|
||||||
if action.type is not None and action.dest == config_item:
|
if action.type is not None and action.dest == config_item:
|
||||||
setattr(config.namespace, config_item,
|
setattr(config.namespace, config_item,
|
||||||
@@ -905,6 +912,42 @@ def _renewal_conf_files(config):
|
|||||||
return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf"))
|
return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf"))
|
||||||
|
|
||||||
|
|
||||||
|
def _renew_describe_results(config, renew_successes, renew_failures,
|
||||||
|
renew_skipped, parse_failures):
|
||||||
|
status = lambda x, msg: " " + "\n ".join(i + " (" + msg +")" for i in x)
|
||||||
|
if config.dry_run:
|
||||||
|
print("** DRY RUN: simulating 'letsencrypt renew' close to cert expiry")
|
||||||
|
print("** (The test certificates below have not been saved.)")
|
||||||
|
print()
|
||||||
|
if renew_skipped:
|
||||||
|
print("The following certs are not due for renewal yet:")
|
||||||
|
print(status(renew_skipped, "skipped"))
|
||||||
|
if not renew_successes and not renew_failures:
|
||||||
|
print("No renewals were attempted.")
|
||||||
|
elif renew_successes and not renew_failures:
|
||||||
|
print("Congratulations, all renewals succeeded. The following certs "
|
||||||
|
"have been renewed:")
|
||||||
|
print(status(renew_successes, "success"))
|
||||||
|
elif renew_failures and not renew_successes:
|
||||||
|
print("All renewal attempts failed. The following certs could not be "
|
||||||
|
"renewed:")
|
||||||
|
print(status(renew_failures, "failure"))
|
||||||
|
elif renew_failures and renew_successes:
|
||||||
|
print("The following certs were successfully renewed:")
|
||||||
|
print(status(renew_successes, "success"))
|
||||||
|
print("\nThe following certs could not be renewed:")
|
||||||
|
print(status(renew_failures, "failure"))
|
||||||
|
|
||||||
|
if parse_failures:
|
||||||
|
print("\nAdditionally, the following renewal configuration files "
|
||||||
|
"were invalid: ")
|
||||||
|
print(status(parse_failures, "parsefail"))
|
||||||
|
|
||||||
|
if config.dry_run:
|
||||||
|
print("** DRY RUN: simulating 'letsencrypt renew' close to cert expiry")
|
||||||
|
print("** (The test certificates above have not been saved.)")
|
||||||
|
|
||||||
|
|
||||||
def renew(config, unused_plugins):
|
def renew(config, unused_plugins):
|
||||||
"""Renew previously-obtained certificates."""
|
"""Renew previously-obtained certificates."""
|
||||||
|
|
||||||
@@ -921,11 +964,12 @@ def renew(config, unused_plugins):
|
|||||||
"specifying a CSR file. Please try the certonly "
|
"specifying a CSR file. Please try the certonly "
|
||||||
"command instead.")
|
"command instead.")
|
||||||
renewer_config = configuration.RenewerConfiguration(config)
|
renewer_config = configuration.RenewerConfiguration(config)
|
||||||
|
renew_successes = []
|
||||||
|
renew_failures = []
|
||||||
|
renew_skipped = []
|
||||||
|
parse_failures = []
|
||||||
for renewal_file in _renewal_conf_files(renewer_config):
|
for renewal_file in _renewal_conf_files(renewer_config):
|
||||||
print("Processing " + renewal_file)
|
print("Processing " + renewal_file)
|
||||||
# XXX: does this succeed in making a fully independent config object
|
|
||||||
# each time?
|
|
||||||
lineage_config = copy.deepcopy(config)
|
lineage_config = copy.deepcopy(config)
|
||||||
|
|
||||||
# Note that this modifies config (to add back the configuration
|
# Note that this modifies config (to add back the configuration
|
||||||
@@ -933,32 +977,34 @@ def renew(config, unused_plugins):
|
|||||||
try:
|
try:
|
||||||
renewal_candidate = _reconstitute(lineage_config, renewal_file)
|
renewal_candidate = _reconstitute(lineage_config, renewal_file)
|
||||||
except Exception as e: # pylint: disable=broad-except
|
except Exception as e: # pylint: disable=broad-except
|
||||||
# reconstitute encountered an unanticipated problem.
|
|
||||||
logger.warning("Renewal configuration file %s produced an "
|
logger.warning("Renewal configuration file %s produced an "
|
||||||
"unexpected error: %s. Skipping.", renewal_file, e)
|
"unexpected error: %s. Skipping.", renewal_file, e)
|
||||||
logger.debug("Traceback was:\n%s", traceback.format_exc())
|
logger.debug("Traceback was:\n%s", traceback.format_exc())
|
||||||
|
parse_failures.append(renewal_file)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if renewal_candidate is not None:
|
if renewal_candidate is None:
|
||||||
# _reconstitute succeeded in producing a RenewableCert, so we
|
parse_failures.append(renewal_file)
|
||||||
# have something to work with from this particular config file.
|
else:
|
||||||
|
|
||||||
# XXX: ensure that each call here replaces the previous one
|
# XXX: ensure that each call here replaces the previous one
|
||||||
zope.component.provideUtility(lineage_config)
|
zope.component.provideUtility(lineage_config)
|
||||||
print("Trying...")
|
if _should_renew(lineage_config, renewal_candidate):
|
||||||
# Because obtain_cert itself indirectly decides whether to renew
|
plugins = plugins_disco.PluginsRegistry.find_all()
|
||||||
# or not, we couldn't currently make a UI/logging distinction at
|
obtain_cert(lineage_config, plugins, renewal_candidate)
|
||||||
# this stage to indicate whether renewal was actually attempted
|
renew_successes.append(renewal_candidate.fullchain)
|
||||||
# (or successful).
|
else:
|
||||||
obtain_cert(lineage_config,
|
renew_skipped.append(renewal_candidate.fullchain)
|
||||||
plugins_disco.PluginsRegistry.find_all(),
|
|
||||||
renewal_candidate)
|
|
||||||
except Exception as e: # pylint: disable=broad-except
|
except Exception as e: # pylint: disable=broad-except
|
||||||
# obtain_cert (presumably) encountered an unanticipated problem.
|
# obtain_cert (presumably) encountered an unanticipated problem.
|
||||||
logger.warning("Attempting to renew cert from %s produced an "
|
logger.warning("Attempting to renew cert from %s produced an "
|
||||||
"unexpected error: %s. Skipping.", renewal_file, e)
|
"unexpected error: %s. Skipping.", renewal_file, e)
|
||||||
logger.debug("Traceback was:\n%s", traceback.format_exc())
|
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)
|
||||||
|
|
||||||
|
|
||||||
def revoke(config, unused_plugins): # TODO: coop with renewal config
|
def revoke(config, unused_plugins): # TODO: coop with renewal config
|
||||||
@@ -1155,11 +1201,56 @@ class HelpfulArgumentParser(object):
|
|||||||
"'certonly' or 'renew' subcommands (%r)" % self.verb)
|
"'certonly' or 'renew' subcommands (%r)" % self.verb)
|
||||||
parsed_args.break_my_certs = parsed_args.staging = True
|
parsed_args.break_my_certs = parsed_args.staging = True
|
||||||
|
|
||||||
|
if parsed_args.csr:
|
||||||
|
self.handle_csr(parsed_args)
|
||||||
|
|
||||||
if self.detect_defaults: # plumbing
|
if self.detect_defaults: # plumbing
|
||||||
parsed_args.store_false_vars = self.store_false_vars
|
parsed_args.store_false_vars = self.store_false_vars
|
||||||
|
|
||||||
return parsed_args
|
return parsed_args
|
||||||
|
|
||||||
|
def handle_csr(self, parsed_args):
|
||||||
|
"""
|
||||||
|
Process a --csr flag. This needs to happen early enough that the
|
||||||
|
webroot plugin can know about the calls to _process_domain
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="der")
|
||||||
|
typ = OpenSSL.crypto.FILETYPE_ASN1
|
||||||
|
domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1)
|
||||||
|
except OpenSSL.crypto.Error:
|
||||||
|
try:
|
||||||
|
e1 = traceback.format_exc()
|
||||||
|
typ = OpenSSL.crypto.FILETYPE_PEM
|
||||||
|
csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="pem")
|
||||||
|
domains = crypto_util.get_sans_from_csr(csr.data, typ)
|
||||||
|
except OpenSSL.crypto.Error:
|
||||||
|
logger.debug("DER CSR parse error %s", e1)
|
||||||
|
logger.debug("PEM CSR parse error %s", traceback.format_exc())
|
||||||
|
raise errors.Error("Failed to parse CSR file: {0}".format(parsed_args.csr[0]))
|
||||||
|
for d in domains:
|
||||||
|
_process_domain(parsed_args, d)
|
||||||
|
|
||||||
|
for d in domains:
|
||||||
|
sanitised = le_util.enforce_domain_sanity(d)
|
||||||
|
if d.lower() != sanitised:
|
||||||
|
raise errors.ConfigurationError(
|
||||||
|
"CSR domain {0} needs to be sanitised to {1}.".format(d, sanitised))
|
||||||
|
|
||||||
|
if not domains:
|
||||||
|
# TODO: add CN to domains instead:
|
||||||
|
raise errors.Error(
|
||||||
|
"Unfortunately, your CSR %s needs to have a SubjectAltName for every domain"
|
||||||
|
% parsed_args.csr[0])
|
||||||
|
|
||||||
|
parsed_args.actual_csr = (csr, typ)
|
||||||
|
csr_domains, config_domains = set(domains), set(parsed_args.domains)
|
||||||
|
if csr_domains != config_domains:
|
||||||
|
raise errors.ConfigurationError(
|
||||||
|
"Inconsistent domain requests:\nFrom the CSR: {0}\nFrom command line/config: {1}"
|
||||||
|
.format(", ".join(csr_domains), ", ".join(config_domains)))
|
||||||
|
|
||||||
|
|
||||||
def determine_verb(self):
|
def determine_verb(self):
|
||||||
"""Determines the verb/subcommand provided by the user.
|
"""Determines the verb/subcommand provided by the user.
|
||||||
|
|
||||||
@@ -1617,12 +1708,15 @@ def _plugins_parsing(helpful, plugins):
|
|||||||
"www.example.com -w /var/www/thing -d thing.net -d m.thing.net`")
|
"www.example.com -w /var/www/thing -d thing.net -d m.thing.net`")
|
||||||
# --webroot-map still has some awkward properties, so it is undocumented
|
# --webroot-map still has some awkward properties, so it is undocumented
|
||||||
helpful.add("webroot", "--webroot-map", default={}, action=WebrootMapProcessor,
|
helpful.add("webroot", "--webroot-map", default={}, action=WebrootMapProcessor,
|
||||||
help="JSON dictionary mapping domains to webroot paths; this implies -d "
|
help="JSON dictionary mapping domains to webroot paths; this "
|
||||||
"for each entry. You may need to escape this from your shell. "
|
"implies -d for each entry. You may need to escape this "
|
||||||
"""Eg: --webroot-map '{"eg1.is,m.eg1.is":"/www/eg1/", "eg2.is":"/www/eg2"}' """
|
"from your shell. E.g.: --webroot-map "
|
||||||
"This option is merged with, but takes precedence over, -w / -d entries."
|
"""'{"eg1.is,m.eg1.is":"/www/eg1/", "eg2.is":"/www/eg2"}' """
|
||||||
" At present, if you put webroot-map in a config file, it needs to be "
|
"This option is merged with, but takes precedence over, "
|
||||||
' on a single line, like: webroot-map = {"example.com":"/var/www"}.')
|
"-w / -d entries. At present, if you put webroot-map in "
|
||||||
|
"a config file, it needs to be on a single line, like: "
|
||||||
|
'webroot-map = {"example.com":"/var/www"}.')
|
||||||
|
|
||||||
|
|
||||||
class WebrootPathProcessor(argparse.Action): # pylint: disable=missing-docstring
|
class WebrootPathProcessor(argparse.Action): # pylint: disable=missing-docstring
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -1771,8 +1865,8 @@ def _handle_exception(exc_type, exc_value, trace, config):
|
|||||||
# acme.messages.Error: urn:acme:error:malformed :: The request message was
|
# acme.messages.Error: urn:acme:error:malformed :: The request message was
|
||||||
# malformed :: Error creating new registration :: Validation of contact
|
# malformed :: Error creating new registration :: Validation of contact
|
||||||
# mailto:none@longrandomstring.biz failed: Server failure at resolver
|
# mailto:none@longrandomstring.biz failed: Server failure at resolver
|
||||||
if ("urn:acme" in err and ":: " in err
|
if (("urn:acme" in err and ":: " in err and
|
||||||
and config.verbose_count <= flag_default("verbose_count")):
|
config.verbose_count <= flag_default("verbose_count"))):
|
||||||
# prune ACME error code, we have a human description
|
# prune ACME error code, we have a human description
|
||||||
_code, _sep, err = err.partition(":: ")
|
_code, _sep, err = err.partition(":: ")
|
||||||
msg = "An unexpected error occurred:\n" + err + "Please see the "
|
msg = "An unexpected error occurred:\n" + err + "Please see the "
|
||||||
|
|||||||
+5
-19
@@ -195,7 +195,8 @@ class Client(object):
|
|||||||
else:
|
else:
|
||||||
self.auth_handler = None
|
self.auth_handler = None
|
||||||
|
|
||||||
def _obtain_certificate(self, domains, csr):
|
def obtain_certificate_from_csr(self, domains, csr,
|
||||||
|
typ=OpenSSL.crypto.FILETYPE_ASN1):
|
||||||
"""Obtain certificate.
|
"""Obtain certificate.
|
||||||
|
|
||||||
Internal function with precondition that `domains` are
|
Internal function with precondition that `domains` are
|
||||||
@@ -223,26 +224,11 @@ class Client(object):
|
|||||||
|
|
||||||
authzr = self.auth_handler.get_authorizations(domains)
|
authzr = self.auth_handler.get_authorizations(domains)
|
||||||
certr = self.acme.request_issuance(
|
certr = self.acme.request_issuance(
|
||||||
jose.ComparableX509(OpenSSL.crypto.load_certificate_request(
|
jose.ComparableX509(
|
||||||
OpenSSL.crypto.FILETYPE_ASN1, csr.data)),
|
OpenSSL.crypto.load_certificate_request(typ, csr.data)),
|
||||||
authzr)
|
authzr)
|
||||||
return certr, self.acme.fetch_chain(certr)
|
return certr, self.acme.fetch_chain(certr)
|
||||||
|
|
||||||
def obtain_certificate_from_csr(self, csr):
|
|
||||||
"""Obtain certficiate from CSR.
|
|
||||||
|
|
||||||
:param .le_util.CSR csr: DER-encoded Certificate Signing
|
|
||||||
Request.
|
|
||||||
|
|
||||||
:returns: `.CertificateResource` and certificate chain (as
|
|
||||||
returned by `.fetch_chain`).
|
|
||||||
:rtype: tuple
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self._obtain_certificate(
|
|
||||||
# TODO: add CN to domains?
|
|
||||||
crypto_util.get_sans_from_csr(
|
|
||||||
csr.data, OpenSSL.crypto.FILETYPE_ASN1), csr)
|
|
||||||
|
|
||||||
def obtain_certificate(self, domains):
|
def obtain_certificate(self, domains):
|
||||||
"""Obtains a certificate from the ACME server.
|
"""Obtains a certificate from the ACME server.
|
||||||
@@ -263,7 +249,7 @@ class Client(object):
|
|||||||
self.config.rsa_key_size, self.config.key_dir)
|
self.config.rsa_key_size, self.config.key_dir)
|
||||||
csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir)
|
csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir)
|
||||||
|
|
||||||
return self._obtain_certificate(domains, csr) + (key, csr)
|
return self.obtain_certificate_from_csr(domains, csr) + (key, csr)
|
||||||
|
|
||||||
def obtain_and_enroll_certificate(self, domains):
|
def obtain_and_enroll_certificate(self, domains):
|
||||||
"""Obtain and enroll certificate.
|
"""Obtain and enroll certificate.
|
||||||
|
|||||||
@@ -308,7 +308,7 @@ def enforce_domain_sanity(domain):
|
|||||||
|
|
||||||
# Unicode
|
# Unicode
|
||||||
try:
|
try:
|
||||||
domain = domain.encode('ascii')
|
domain = domain.encode('ascii').lower()
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
raise errors.ConfigurationError(
|
raise errors.ConfigurationError(
|
||||||
"Internationalized domain names are not presently supported: {0}"
|
"Internationalized domain names are not presently supported: {0}"
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ s.serve_forever()" """
|
|||||||
help="Automatically allows public IP logging.")
|
help="Automatically allows public IP logging.")
|
||||||
|
|
||||||
def prepare(self): # pylint: disable=missing-docstring,no-self-use
|
def prepare(self): # pylint: disable=missing-docstring,no-self-use
|
||||||
if self.config.noninteractive_mode:
|
if self.config.noninteractive_mode and not self.conf("test-mode"):
|
||||||
raise errors.PluginError("Running manual mode non-interactively is not supported")
|
raise errors.PluginError("Running manual mode non-interactively is not supported")
|
||||||
|
|
||||||
def more_info(self): # pylint: disable=missing-docstring,no-self-use
|
def more_info(self): # pylint: disable=missing-docstring,no-self-use
|
||||||
|
|||||||
@@ -23,16 +23,21 @@ class AuthenticatorTest(unittest.TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
from letsencrypt.plugins.manual import Authenticator
|
from letsencrypt.plugins.manual import Authenticator
|
||||||
self.config = mock.MagicMock(
|
self.config = mock.MagicMock(
|
||||||
http01_port=8080, manual_test_mode=False, manual_public_ip_logging_ok=False)
|
http01_port=8080, manual_test_mode=False,
|
||||||
|
manual_public_ip_logging_ok=False, noninteractive_mode=True)
|
||||||
self.auth = Authenticator(config=self.config, name="manual")
|
self.auth = Authenticator(config=self.config, name="manual")
|
||||||
self.achalls = [achallenges.KeyAuthorizationAnnotatedChallenge(
|
self.achalls = [achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||||
challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)]
|
challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)]
|
||||||
|
|
||||||
config_test_mode = mock.MagicMock(
|
config_test_mode = mock.MagicMock(
|
||||||
http01_port=8080, manual_test_mode=True)
|
http01_port=8080, manual_test_mode=True, noninteractive_mode=True)
|
||||||
self.auth_test_mode = Authenticator(
|
self.auth_test_mode = Authenticator(
|
||||||
config=config_test_mode, name="manual")
|
config=config_test_mode, name="manual")
|
||||||
|
|
||||||
|
def test_prepare(self):
|
||||||
|
self.assertRaises(errors.PluginError, self.auth.prepare)
|
||||||
|
self.auth_test_mode.prepare() # error not raised
|
||||||
|
|
||||||
def test_more_info(self):
|
def test_more_info(self):
|
||||||
self.assertTrue(isinstance(self.auth.more_info(), str))
|
self.assertTrue(isinstance(self.auth.more_info(), str))
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ class ServerManager(object):
|
|||||||
logger.debug("Stopping server at %s:%d...",
|
logger.debug("Stopping server at %s:%d...",
|
||||||
*instance.server.socket.getsockname()[:2])
|
*instance.server.socket.getsockname()[:2])
|
||||||
instance.server.shutdown()
|
instance.server.shutdown()
|
||||||
|
# Not calling server_close causes problems when renewing multiple
|
||||||
|
# certs with `letsencrypt renew` using TLSSNI01 and PyOpenSSL 0.13
|
||||||
|
instance.server.server_close()
|
||||||
instance.thread.join()
|
instance.thread.join()
|
||||||
del self._instances[port]
|
del self._instances[port]
|
||||||
|
|
||||||
|
|||||||
@@ -49,8 +49,10 @@ to serve all files under specified web root ({0})."""
|
|||||||
path_map = self.conf("map")
|
path_map = self.conf("map")
|
||||||
|
|
||||||
if not path_map:
|
if not path_map:
|
||||||
raise errors.PluginError("--{0} must be set".format(
|
raise errors.PluginError(
|
||||||
self.option_name("path")))
|
"Missing parts of webroot configuration; please set either "
|
||||||
|
"--webroot-path and --domains, or --webroot-map. Run with "
|
||||||
|
" --help webroot for examples.")
|
||||||
for name, path in path_map.items():
|
for name, path in path_map.items():
|
||||||
if not os.path.isdir(path):
|
if not os.path.isdir(path):
|
||||||
raise errors.PluginError(path + " does not exist or is not a directory")
|
raise errors.PluginError(path + " does not exist or is not a directory")
|
||||||
|
|||||||
@@ -227,8 +227,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||||||
self.assertTrue("MisconfigurationError" in ret)
|
self.assertTrue("MisconfigurationError" in ret)
|
||||||
|
|
||||||
args = ["certonly", "--webroot"]
|
args = ["certonly", "--webroot"]
|
||||||
ret, _, _, _ = self._call(args)
|
try:
|
||||||
self.assertTrue("--webroot-path must be set" in ret)
|
self._call(args)
|
||||||
|
assert False, "Exception should have been raised"
|
||||||
|
except errors.PluginSelectionError as e:
|
||||||
|
self.assertTrue("please set either --webroot-path" in e.message)
|
||||||
|
|
||||||
self._cli_missing_flag(["--standalone"], "With the standalone plugin, you probably")
|
self._cli_missing_flag(["--standalone"], "With the standalone plugin, you probably")
|
||||||
|
|
||||||
@@ -323,11 +326,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||||||
self.assertEqual(config.fullchain_path, os.path.abspath(fullchain))
|
self.assertEqual(config.fullchain_path, os.path.abspath(fullchain))
|
||||||
|
|
||||||
def test_certonly_bad_args(self):
|
def test_certonly_bad_args(self):
|
||||||
ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR])
|
try:
|
||||||
self.assertEqual(ret, '--domains and --csr are mutually exclusive')
|
self._call(['-a', 'bad_auth', 'certonly'])
|
||||||
|
assert False, "Exception should have been raised"
|
||||||
ret, _, _, _ = self._call(['-a', 'bad_auth', 'certonly'])
|
except errors.PluginSelectionError as e:
|
||||||
self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed')
|
self.assertTrue('The requested bad_auth plugin does not appear' in e.message)
|
||||||
|
|
||||||
def test_check_config_sanity_domain(self):
|
def test_check_config_sanity_domain(self):
|
||||||
# Punycode
|
# Punycode
|
||||||
@@ -633,6 +636,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||||||
self._make_dummy_renewal_config()
|
self._make_dummy_renewal_config()
|
||||||
with mock.patch('letsencrypt.storage.RenewableCert') as mock_rc:
|
with mock.patch('letsencrypt.storage.RenewableCert') as mock_rc:
|
||||||
mock_lineage = mock.MagicMock()
|
mock_lineage = mock.MagicMock()
|
||||||
|
mock_lineage.fullchain = "somepath/fullchain.pem"
|
||||||
if renewalparams is not None:
|
if renewalparams is not None:
|
||||||
mock_lineage.configuration = {'renewalparams': renewalparams}
|
mock_lineage.configuration = {'renewalparams': renewalparams}
|
||||||
if names is not None:
|
if names is not None:
|
||||||
@@ -682,6 +686,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||||||
self._make_dummy_renewal_config()
|
self._make_dummy_renewal_config()
|
||||||
with mock.patch('letsencrypt.storage.RenewableCert') as mock_rc:
|
with mock.patch('letsencrypt.storage.RenewableCert') as mock_rc:
|
||||||
mock_lineage = mock.MagicMock()
|
mock_lineage = mock.MagicMock()
|
||||||
|
mock_lineage.fullchain = "somewhere/fullchain.pem"
|
||||||
mock_rc.return_value = mock_lineage
|
mock_rc.return_value = mock_lineage
|
||||||
mock_lineage.configuration = {
|
mock_lineage.configuration = {
|
||||||
'renewalparams': {'authenticator': 'webroot'}}
|
'renewalparams': {'authenticator': 'webroot'}}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ class ClientTest(unittest.TestCase):
|
|||||||
no_verify_ssl=False, config_dir="/etc/letsencrypt")
|
no_verify_ssl=False, config_dir="/etc/letsencrypt")
|
||||||
# pylint: disable=star-args
|
# pylint: disable=star-args
|
||||||
self.account = mock.MagicMock(**{"key.pem": KEY})
|
self.account = mock.MagicMock(**{"key.pem": KEY})
|
||||||
|
self.eg_domains = ["example.com", "www.example.com"]
|
||||||
|
|
||||||
from letsencrypt.client import Client
|
from letsencrypt.client import Client
|
||||||
with mock.patch("letsencrypt.client.acme_client.Client") as acme:
|
with mock.patch("letsencrypt.client.acme_client.Client") as acme:
|
||||||
@@ -101,22 +102,41 @@ class ClientTest(unittest.TestCase):
|
|||||||
self.acme.fetch_chain.return_value = mock.sentinel.chain
|
self.acme.fetch_chain.return_value = mock.sentinel.chain
|
||||||
|
|
||||||
def _check_obtain_certificate(self):
|
def _check_obtain_certificate(self):
|
||||||
self.client.auth_handler.get_authorizations.assert_called_once_with(
|
self.client.auth_handler.get_authorizations.assert_called_once_with(self.eg_domains)
|
||||||
["example.com", "www.example.com"])
|
|
||||||
self.acme.request_issuance.assert_called_once_with(
|
self.acme.request_issuance.assert_called_once_with(
|
||||||
jose.ComparableX509(OpenSSL.crypto.load_certificate_request(
|
jose.ComparableX509(OpenSSL.crypto.load_certificate_request(
|
||||||
OpenSSL.crypto.FILETYPE_ASN1, CSR_SAN)),
|
OpenSSL.crypto.FILETYPE_ASN1, CSR_SAN)),
|
||||||
self.client.auth_handler.get_authorizations())
|
self.client.auth_handler.get_authorizations())
|
||||||
self.acme.fetch_chain.assert_called_once_with(mock.sentinel.certr)
|
self.acme.fetch_chain.assert_called_once_with(mock.sentinel.certr)
|
||||||
|
|
||||||
def test_obtain_certificate_from_csr(self):
|
# FIXME move parts of this to test_cli.py...
|
||||||
|
@mock.patch("letsencrypt.cli._process_domain")
|
||||||
|
def test_obtain_certificate_from_csr(self, mock_process_domain):
|
||||||
self._mock_obtain_certificate()
|
self._mock_obtain_certificate()
|
||||||
|
from letsencrypt import cli
|
||||||
|
test_csr = le_util.CSR(form="der", file=None, data=CSR_SAN)
|
||||||
|
mock_parsed_args = mock.MagicMock()
|
||||||
|
with mock.patch("letsencrypt.client.le_util.CSR") as mock_CSR:
|
||||||
|
mock_CSR.return_value = test_csr
|
||||||
|
mock_parsed_args.domains = self.eg_domains[:]
|
||||||
|
mock_parser = mock.MagicMock(cli.HelpfulArgumentParser)
|
||||||
|
cli.HelpfulArgumentParser.handle_csr(mock_parser, mock_parsed_args)
|
||||||
|
|
||||||
|
# make sure cli processing occurred
|
||||||
|
cli_processed = (call[0][1] for call in mock_process_domain.call_args_list)
|
||||||
|
self.assertEqual(set(cli_processed), set(("example.com", "www.example.com")))
|
||||||
|
# Now provoke an inconsistent domains error...
|
||||||
|
mock_parsed_args.domains.append("hippopotamus.io")
|
||||||
|
self.assertRaises(errors.ConfigurationError,
|
||||||
|
cli.HelpfulArgumentParser.handle_csr, mock_parser, mock_parsed_args)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
(mock.sentinel.certr, mock.sentinel.chain),
|
(mock.sentinel.certr, mock.sentinel.chain),
|
||||||
self.client.obtain_certificate_from_csr(le_util.CSR(
|
self.client.obtain_certificate_from_csr(self.eg_domains, test_csr))
|
||||||
form="der", file=None, data=CSR_SAN)))
|
# and that the cert was obtained correctly
|
||||||
self._check_obtain_certificate()
|
self._check_obtain_certificate()
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("letsencrypt.client.crypto_util")
|
@mock.patch("letsencrypt.client.crypto_util")
|
||||||
def test_obtain_certificate(self, mock_crypto_util):
|
def test_obtain_certificate(self, mock_crypto_util):
|
||||||
self._mock_obtain_certificate()
|
self._mock_obtain_certificate()
|
||||||
|
|||||||
@@ -20,17 +20,16 @@ else
|
|||||||
readlink="readlink"
|
readlink="readlink"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
common() {
|
common_no_force_renew() {
|
||||||
letsencrypt_test \
|
letsencrypt_test_no_force_renew \
|
||||||
--authenticator standalone \
|
--authenticator standalone \
|
||||||
--installer null \
|
--installer null \
|
||||||
"$@"
|
"$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
common_no_force_renew() {
|
common() {
|
||||||
letsencrypt_test_no_force_renew \
|
common_no_force_renew \
|
||||||
--authenticator standalone \
|
--renew-by-default \
|
||||||
--installer null \
|
|
||||||
"$@"
|
"$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,21 +50,27 @@ common --domains le3.wtf install \
|
|||||||
--cert-path "${root}/csr/cert.pem" \
|
--cert-path "${root}/csr/cert.pem" \
|
||||||
--key-path "${root}/csr/key.pem"
|
--key-path "${root}/csr/key.pem"
|
||||||
|
|
||||||
|
CheckCertCount() {
|
||||||
|
CERTCOUNT=`ls "${root}/conf/archive/le.wtf/cert"* | wc -l`
|
||||||
|
if [ "$CERTCOUNT" -ne "$1" ] ; then
|
||||||
|
echo Wrong cert count, not "$1" `ls "${root}/conf/archive/le.wtf/"*`
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckCertCount 1
|
||||||
# This won't renew (because it's not time yet)
|
# This won't renew (because it's not time yet)
|
||||||
letsencrypt_test_no_force_renew --authenticator standalone --installer null renew
|
common_no_force_renew renew
|
||||||
|
CheckCertCount 1
|
||||||
|
|
||||||
|
# --renew-by-default is used, so renewal should occur
|
||||||
|
common renew
|
||||||
|
CheckCertCount 2
|
||||||
|
|
||||||
# This will renew because the expiry is less than 10 years from now
|
# 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"
|
sed -i "4arenew_before_expiry = 10 years" "$root/conf/renewal/le.wtf.conf"
|
||||||
letsencrypt_test_no_force_renew --authenticator standalone --installer null renew
|
common_no_force_renew renew
|
||||||
|
CheckCertCount 3
|
||||||
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
|
# revoke by account key
|
||||||
common revoke --cert-path "$root/conf/live/le.wtf/cert.pem"
|
common revoke --cert-path "$root/conf/live/le.wtf/cert.pem"
|
||||||
|
|||||||
@@ -12,20 +12,8 @@ store_flags="$store_flags --logs-dir $root/logs"
|
|||||||
export root store_flags
|
export root store_flags
|
||||||
|
|
||||||
letsencrypt_test () {
|
letsencrypt_test () {
|
||||||
letsencrypt \
|
letsencrypt_test_no_force_renew \
|
||||||
--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 \
|
|
||||||
--renew-by-default \
|
--renew-by-default \
|
||||||
--debug \
|
|
||||||
-vvvvvvv \
|
|
||||||
"$@"
|
"$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +25,7 @@ letsencrypt_test_no_force_renew () {
|
|||||||
--http-01-port 5002 \
|
--http-01-port 5002 \
|
||||||
--manual-test-mode \
|
--manual-test-mode \
|
||||||
$store_flags \
|
$store_flags \
|
||||||
--text \
|
--non-interactive \
|
||||||
--no-redirect \
|
--no-redirect \
|
||||||
--agree-tos \
|
--agree-tos \
|
||||||
--register-unsafely-without-email \
|
--register-unsafely-without-email \
|
||||||
|
|||||||
Reference in New Issue
Block a user