Merge branch 'master' into no-local-oldest

This commit is contained in:
Brad Warren 2021-05-28 12:20:18 -07:00
commit eeabb5d59c
28 changed files with 418 additions and 445 deletions

View file

@ -5,7 +5,6 @@ from setuptools import setup
version = '1.16.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
'cryptography>=2.1.4',
# formerly known as acme.jose:

View file

@ -48,7 +48,6 @@ class Proxy(configurators_common.Proxy):
setattr(self.le_config, "nginx_" + k, constants.os_constant(k))
conf = configuration.NamespaceConfig(self.le_config)
zope.component.provideUtility(conf)
self._configurator = configurator.NginxConfigurator(
config=conf, name="nginx")
self._configurator.prepare()

View file

@ -6,7 +6,6 @@ from setuptools import setup
version = '1.16.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
'dns-lexicon>=3.1.0', # Changed `rtype` parameter name
'setuptools>=39.0.1',

View file

@ -6,7 +6,6 @@ from setuptools import setup
version = '1.16.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
'dns-lexicon>=3.1.0', # Changed `rtype` parameter name
'setuptools>=39.0.1',

View file

@ -1,62 +0,0 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
#Ipython Notebook
.ipynb_checkpoints

View file

@ -6,7 +6,6 @@ from setuptools import setup
version = '1.16.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
'dns-lexicon>=3.1.0', # Changed `rtype` parameter name
'setuptools>=39.0.1',

View file

@ -678,8 +678,9 @@ class NginxConfigurator(common.Installer):
"""Generate invalid certs that let us create ssl directives for Nginx"""
# TODO: generate only once
tmp_dir = os.path.join(self.config.work_dir, "snakeoil")
le_key = crypto_util.init_save_key(
key_size=1024, key_dir=tmp_dir, keyname="key.pem")
le_key = crypto_util.generate_key(
key_size=1024, key_dir=tmp_dir, keyname="key.pem",
strict_permissions=self.config.strict_permissions)
key = OpenSSL.crypto.load_privatekey(
OpenSSL.crypto.FILETYPE_PEM, le_key.pem)
cert = acme_crypto_util.gen_ss_cert(key, domains=[socket.gethostname()])

View file

@ -9,7 +9,6 @@ try:
except ImportError: # pragma: no cover
from unittest import mock # type: ignore
import pkg_resources
import zope.component
from certbot import util
from certbot.compat import os
@ -79,9 +78,6 @@ class NginxTest(test_util.ConfigTestCase):
openssl_version=openssl_version)
config.prepare()
# Provide general config utility.
zope.component.provideUtility(self.configuration)
return config

1
certbot/.gitignore vendored
View file

@ -1 +0,0 @@
*.crt

View file

@ -25,11 +25,16 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
of the Certbot package will now always require acme>=X and version Y of a
plugin package will always require acme>=Y and certbot=>Y. Specifying
dependencies in this way simplifies testing and development.
* Functions `certbot.crypto_util.init_save_key` and `certbot.crypto_util.init_save_csr`,
whose behaviors rely on the global Certbot `config` singleton, are deprecated and will
be removed in a future release. Please use `certbot.crypto_util.generate_key` and
`certbot.crypto_util.generate_csr` instead.
### Fixed
* Fix TypeError due to incompatibility with lexicon >= v3.6.0
* Installers (e.g. nginx, Apache) were being restarted unnecessarily after dry-run renewals.
* Colors and bold text should properly render in all supported versions of Windows.
More details about these changes can be found on our GitHub repo.

View file

@ -44,11 +44,12 @@ class AuthHandler:
self.account = account
self.pref_challs = pref_challs
def handle_authorizations(self, orderr, best_effort=False, max_retries=30):
def handle_authorizations(self, orderr, config, best_effort=False, max_retries=30):
"""
Retrieve all authorizations, perform all challenges required to validate
these authorizations, then poll and wait for the authorization to be checked.
:param acme.messages.OrderResource orderr: must have authorizations filled in
:param interfaces.IConfig config: current Certbot configuration
:param bool best_effort: if True, not all authorizations need to be validated (eg. renew)
:param int max_retries: maximum number of retries to poll authorizations
:returns: list of all validated authorizations
@ -72,7 +73,6 @@ class AuthHandler:
resps = self.auth.perform(achalls)
# If debug is on, wait for user input before starting the verification process.
config = zope.component.getUtility(interfaces.IConfig)
if config.debug_challenges:
notify = zope.component.getUtility(interfaces.IDisplay).notification
notify('Challenges loaded. Press continue to submit to CA. '

View file

@ -334,7 +334,7 @@ class Client:
key = None
key_size = self.config.rsa_key_size
elliptic_curve = None
elliptic_curve = "secp256r1"
# key-type defaults to a list, but we are only handling 1 currently
if isinstance(self.config.key_type, list):
@ -362,13 +362,15 @@ class Client:
data=acme_crypto_util.make_csr(
key.pem, domains, self.config.must_staple))
else:
key = key or crypto_util.init_save_key(
key = key or crypto_util.generate_key(
key_size=key_size,
key_dir=self.config.key_dir,
key_type=self.config.key_type,
elliptic_curve=elliptic_curve,
strict_permissions=self.config.strict_permissions,
)
csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir)
csr = crypto_util.generate_csr(key, domains, self.config.csr_dir,
self.config.must_staple, self.config.strict_permissions)
orderr = self._get_order_and_authorizations(csr.data, self.config.allow_subset_of_names)
authzr = orderr.authorizations
@ -420,7 +422,7 @@ class Client:
logger.warning("Certbot was unable to obtain fresh authorizations for every domain"
". The dry run will continue, but results may not be accurate.")
authzr = self.auth_handler.handle_authorizations(orderr, best_effort)
authzr = self.auth_handler.handle_authorizations(orderr, self.config, best_effort)
return orderr.update(authorizations=authzr)
def obtain_and_enroll_certificate(self, domains, certname):
@ -516,11 +518,9 @@ class Client:
return abs_cert_path, abs_chain_path, abs_fullchain_path
def deploy_certificate(self, cert_name, domains, privkey_path,
cert_path, chain_path, fullchain_path):
def deploy_certificate(self, domains, privkey_path, cert_path, chain_path, fullchain_path):
"""Install certificate
:param str cert_name: name of the certificate lineage (optional)
:param list domains: list of domains to install the certificate
:param str privkey_path: path to certificate private key
:param str cert_path: certificate file path (optional)
@ -536,11 +536,7 @@ class Client:
display_util.notify("Deploying certificate")
msg = f"Failed to install the certificate (installer: {self.config.installer})."
if cert_name:
msg += (" Try again after fixing errors by running:\n\n"
f" {cli.cli_constants.cli_command} install --cert-name {cert_name}\n")
msg = "Could not install certificate"
with error_handler.ErrorHandler(self._recovery_routine_with_msg, msg):
for dom in domains:
self.installer.deploy_cert(
@ -616,9 +612,7 @@ class Client:
"""
msg = ("We were unable to set up enhancement %s for your server, "
"however, we successfully installed your certificate."
% (enhancement))
msg = f"Could not set up {enhancement} enhancement"
with error_handler.ErrorHandler(self._recovery_routine_with_msg, msg):
for dom in domains:
try:

View file

@ -469,6 +469,68 @@ def _find_domains_or_certname(config, installer, question=None):
return domains, certname
def _report_next_steps(config: interfaces.IConfig, installer_err: Optional[errors.Error],
lineage: Optional[storage.RenewableCert],
new_or_renewed_cert: bool = True) -> None:
"""Displays post-run/certonly advice to the user about renewal and installation.
The output varies by runtime configuration and any errors encountered during installation.
:param config: Configuration object
:type config: interfaces.IConfig
:param installer_err: The installer/enhancement error encountered, if any.
:type error: Optional[errors.Error]
:param lineage: The resulting certificate lineage from the issuance, if any.
:type lineage: Optional[storage.RenewableCert]
:param bool new_or_renewed_cert: Whether the verb execution resulted in a certificate
being saved (created or renewed).
"""
steps: List[str] = []
# If the installation or enhancement raised an error, show advice on trying again
if installer_err:
steps.append(
"The certificate was saved, but could not be installed (installer: "
f"{config.installer}). After fixing the error shown below, try installing it again "
f"by running:\n {cli.cli_command} install --cert-name "
f"{_cert_name_from_config_or_lineage(config, lineage)}"
)
# If a certificate was obtained or renewed, show applicable renewal advice
if new_or_renewed_cert:
if config.csr:
steps.append(
"Certificates created using --csr will not be renewed automatically by Certbot. "
"You will need to renew the certificate before it expires, by running the same "
"Certbot command again.")
elif not config.preconfigured_renewal:
steps.append(
"The certificate will need to be renewed before it expires. Certbot can "
"automatically renew the certificate in the background, but you may need "
"to take steps to enable that functionality. "
"See https://certbot.eff.org/docs/using.html#automated-renewals for "
"instructions.")
if not steps:
return
# TODO: refactor ANSI escapes during https://github.com/certbot/certbot/issues/8848
(bold_on, bold_off) = [c if sys.stdout.isatty() and not config.quiet else '' \
for c in (util.ANSI_SGR_BOLD, util.ANSI_SGR_RESET)]
print(bold_on, '\n', 'NEXT STEPS:', bold_off, sep='')
for step in steps:
display_util.notify(f"- {step}")
# If there was an installer error, segregate the error output with a trailing newline
if installer_err:
print()
def _report_new_cert(config, cert_path, fullchain_path, key_path=None):
# type: (interfaces.IConfig, Optional[str], Optional[str], Optional[str]) -> None
"""Reports the creation of a new certificate to the user.
@ -499,18 +561,13 @@ def _report_new_cert(config, cert_path, fullchain_path, key_path=None):
("\nSuccessfully received certificate.\n"
"Certificate is saved at: {cert_path}\n{key_msg}"
"This certificate expires on {expiry}.\n"
"These files will be updated when the certificate renews.\n{renew_msg}{nl}").format(
"These files will be updated when the certificate renews.{renewal_msg}{nl}").format(
cert_path=fullchain_path,
expiry=crypto_util.notAfter(cert_path).date(),
key_msg="Key is saved at: {}\n".format(key_path) if key_path else "",
renew_msg="Certbot will automatically renew this certificate in the background."
if config.preconfigured_renewal else
(f'Run "{cli.cli_constants.cli_command} renew" to renew '
"expiring certificates. "
"We recommend setting up a scheduled task for renewal; see "
"https://certbot.eff.org/docs/using.html#automated-renewals "
"for instructions."),
nl="\n" if config.verb == "run" else "" # visually split output if also deploying
renewal_msg="\nCertbot has set up a scheduled task to automatically renew this "
"certificate in the background." if config.preconfigured_renewal else "",
nl="\n" if config.verb == "run" else "" # Normalize spacing across verbs
)
)
@ -813,6 +870,21 @@ def update_account(config, unused_plugins):
return None
def _cert_name_from_config_or_lineage(config: interfaces.IConfig,
lineage: Optional[storage.RenewableCert]) -> Optional[str]:
if lineage:
return lineage.lineagename
elif config.certname:
return config.certname
try:
cert_name = cert_manager.cert_path_to_lineage(config)
return cert_name
except errors.Error:
pass
return None
def _install_cert(config, le_client, domains, lineage=None):
"""Install a cert
@ -835,20 +907,8 @@ def _install_cert(config, le_client, domains, lineage=None):
path_provider = lineage if lineage else config
assert path_provider.cert_path is not None
cert_name: Optional[str] = None
if isinstance(path_provider, storage.RenewableCert):
cert_name = path_provider.lineagename
elif path_provider.certname:
cert_name = path_provider.certname
else:
# Check if the cert path happens to be part of an existing lineage
try:
cert_name = cert_manager.cert_path_to_lineage(config)
except errors.Error:
pass
le_client.deploy_certificate(cert_name, domains, path_provider.key_path,
path_provider.cert_path, path_provider.chain_path, path_provider.fullchain_path)
le_client.deploy_certificate(domains, path_provider.key_path, path_provider.cert_path,
path_provider.chain_path, path_provider.fullchain_path)
le_client.enhance_config(domains, path_provider.chain_path)
@ -1216,15 +1276,27 @@ def run(config, plugins):
if should_get_cert:
_report_new_cert(config, cert_path, fullchain_path, key_path)
_install_cert(config, le_client, domains, new_lineage)
# The installer error, if any, is being stored as a value here, in order to first print
# relevant advice in a nice way, before re-raising the error for normal processing.
installer_err: Optional[errors.Error] = None
try:
_install_cert(config, le_client, domains, new_lineage)
if enhancements.are_requested(config) and new_lineage:
enhancements.enable(new_lineage, domains, installer, config)
if enhancements.are_requested(config) and new_lineage:
enhancements.enable(new_lineage, domains, installer, config)
if lineage is None or not should_get_cert:
display_ops.success_installation(domains)
else:
display_ops.success_renewal(domains)
if lineage is None or not should_get_cert:
display_ops.success_installation(domains)
else:
display_ops.success_renewal(domains)
except errors.Error as e:
installer_err = e
finally:
_report_next_steps(config, installer_err, new_lineage,
new_or_renewed_cert=should_get_cert)
# If the installer did fail, re-raise the error to bail out
if installer_err:
raise installer_err
_suggest_donation_if_appropriate(config)
eff.handle_subscription(config, le_client.account)
@ -1327,6 +1399,7 @@ def certonly(config, plugins):
if config.csr:
cert_path, chain_path, fullchain_path = _csr_get_and_save_cert(config, le_client)
_csr_report_new_cert(config, cert_path, chain_path, fullchain_path)
_report_next_steps(config, None, None)
_suggest_donation_if_appropriate(config)
eff.handle_subscription(config, le_client.account)
return
@ -1345,6 +1418,7 @@ def certonly(config, plugins):
fullchain_path = lineage.fullchain_path if lineage else None
key_path = lineage.key_path if lineage else None
_report_new_cert(config, cert_path, fullchain_path, key_path)
_report_next_steps(config, None, lineage, new_or_renewed_cert=should_get_cert)
_suggest_donation_if_appropriate(config)
eff.handle_subscription(config, le_client.account)
@ -1445,9 +1519,15 @@ def main(cli_args=None):
logger.debug("Arguments: %r", cli_args)
logger.debug("Discovered plugins: %r", plugins)
# Some releases of Windows require escape sequences to be enable explicitly
misc.prepare_virtual_console()
# note: arg parser internally handles --help (and exits afterwards)
args = cli.prepare_and_parse_args(plugins, cli_args)
config = configuration.NamespaceConfig(args)
# This call is done only for retro-compatibility purposes.
# TODO: Remove this call once zope dependencies are removed from Certbot.
zope.component.provideUtility(config)
# On windows, shell without administrative right cannot create symlinks required by certbot.

View file

@ -450,7 +450,8 @@ def handle_renewal_request(config):
if renewal_candidate is None:
parse_failures.append(renewal_file)
else:
# XXX: ensure that each call here replaces the previous one
# This call is done only for retro-compatibility purposes.
# TODO: Remove this call once zope dependencies are removed from Certbot.
zope.component.provideUtility(lineage_config)
renewal_candidate.ensure_deployed()
from certbot._internal import main

View file

@ -17,6 +17,8 @@ from certbot.compat import os
try:
from win32com.shell import shell as shellwin32
from win32console import GetStdHandle, STD_OUTPUT_HANDLE
from pywintypes import error as pywinerror
POSIX_MODE = False
except ImportError: # pragma: no cover
POSIX_MODE = True
@ -39,6 +41,26 @@ def raise_for_non_administrative_windows_rights() -> None:
raise errors.Error('Error, certbot must be run on a shell with administrative rights.')
def prepare_virtual_console() -> None:
"""
On Windows, ensure that Console Virtual Terminal Sequences are enabled.
"""
if POSIX_MODE:
return
# https://docs.microsoft.com/en-us/windows/console/setconsolemode
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
# stdout/stderr will be the same console screen buffer, but this could return None or raise
try:
h = GetStdHandle(STD_OUTPUT_HANDLE)
if h:
h.SetConsoleMode(h.GetConsoleMode() | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
except pywinerror:
logger.debug("Failed to set console mode", exc_info=True)
def readline_with_timeout(timeout: float, prompt: str) -> str:
"""
Read user input to return the first line entered, or raise after specified timeout.

View file

@ -9,7 +9,7 @@ import logging
import re
import warnings
from typing import List
from typing import List, Set
# See https://github.com/pyca/cryptography/issues/4275
from cryptography import x509 # type: ignore
from cryptography.exceptions import InvalidSignature
@ -38,9 +38,10 @@ logger = logging.getLogger(__name__)
# High level functions
def init_save_key(
key_size, key_dir, key_type="rsa", elliptic_curve="secp256r1", keyname="key-certbot.pem"
):
def generate_key(key_size: int, key_dir: str, key_type: str = "rsa",
elliptic_curve: str = "secp256r1", keyname: str = "key-certbot.pem",
strict_permissions: bool = True) -> util.Key:
"""Initializes and saves a privkey.
Inits key and saves it in PEM format on the filesystem.
@ -53,6 +54,8 @@ def init_save_key(
:param str key_type: Key Type [rsa, ecdsa]
:param str elliptic_curve: Name of the elliptic curve if key type is ecdsa.
:param str keyname: Filename of key
:param bool strict_permissions: If true and key_dir exists, an exception is raised if
the directory doesn't have 0700 permissions or isn't owned by the current user.
:returns: Key
:rtype: :class:`certbot.util.Key`
@ -69,9 +72,8 @@ def init_save_key(
logger.error("Encountered error while making key: %s", str(err))
raise err
config = zope.component.getUtility(interfaces.IConfig)
# Save file
util.make_or_verify_dir(key_dir, 0o700, config.strict_permissions)
util.make_or_verify_dir(key_dir, 0o700, strict_permissions)
key_f, key_path = util.unique_file(
os.path.join(key_dir, keyname), 0o600, "wb")
with key_f:
@ -84,9 +86,77 @@ def init_save_key(
return util.Key(key_path, key_pem)
# TODO: Remove this call once zope dependencies are removed from Certbot.
def init_save_key(key_size, key_dir, key_type="rsa", elliptic_curve="secp256r1",
keyname="key-certbot.pem"):
"""Initializes and saves a privkey.
Inits key and saves it in PEM format on the filesystem.
.. note:: keyname is the attempted filename, it may be different if a file
already exists at the path.
.. deprecated:: 1.16.0
Use :func:`generate_key` instead.
:param int key_size: key size in bits if key size is rsa.
:param str key_dir: Key save directory.
:param str key_type: Key Type [rsa, ecdsa]
:param str elliptic_curve: Name of the elliptic curve if key type is ecdsa.
:param str keyname: Filename of key
:returns: Key
:rtype: :class:`certbot.util.Key`
:raises ValueError: If unable to generate the key given key_size.
"""
warnings.warn("certbot.crypto_util.init_save_key is deprecated, please use "
"certbot.crypto_util.generate_key instead.", DeprecationWarning)
config = zope.component.getUtility(interfaces.IConfig)
return generate_key(key_size, key_dir, key_type=key_type, elliptic_curve=elliptic_curve,
keyname=keyname, strict_permissions=config.strict_permissions)
def generate_csr(privkey: util.Key, names: Set[str], path: str,
must_staple: bool = False, strict_permissions: bool = True) -> util.CSR:
"""Initialize a CSR with the given private key.
:param privkey: Key to include in the CSR
:type privkey: :class:`certbot.util.Key`
:param set names: `str` names to include in the CSR
:param str path: Certificate save directory.
:param bool must_staple: If true, include the TLS Feature extension "OCSP Must Staple"
:param bool strict_permissions: If true and path exists, an exception is raised if
the directory doesn't have 0755 permissions or isn't owned by the current user.
:returns: CSR
:rtype: :class:`certbot.util.CSR`
"""
csr_pem = acme_crypto_util.make_csr(
privkey.pem, names, must_staple=must_staple)
# Save CSR
util.make_or_verify_dir(path, 0o755, strict_permissions)
csr_f, csr_filename = util.unique_file(
os.path.join(path, "csr-certbot.pem"), 0o644, "wb")
with csr_f:
csr_f.write(csr_pem)
logger.debug("Creating CSR: %s", csr_filename)
return util.CSR(csr_filename, csr_pem, "pem")
# TODO: Remove this call once zope dependencies are removed from Certbot.
def init_save_csr(privkey, names, path):
"""Initialize a CSR with the given private key.
.. deprecated:: 1.16.0
Use :func:`generate_csr` instead.
:param privkey: Key to include in the CSR
:type privkey: :class:`certbot.util.Key`
@ -98,20 +168,13 @@ def init_save_csr(privkey, names, path):
:rtype: :class:`certbot.util.CSR`
"""
warnings.warn("certbot.crypto_util.init_save_csr is deprecated, please use "
"certbot.crypto_util.generate_csr instead.", DeprecationWarning)
config = zope.component.getUtility(interfaces.IConfig)
csr_pem = acme_crypto_util.make_csr(
privkey.pem, names, must_staple=config.must_staple)
# Save CSR
util.make_or_verify_dir(path, 0o755, config.strict_permissions)
csr_f, csr_filename = util.unique_file(
os.path.join(path, "csr-certbot.pem"), 0o644, "wb")
with csr_f:
csr_f.write(csr_pem)
logger.debug("Creating CSR: %s", csr_filename)
return util.CSR(csr_filename, csr_pem, "pem")
return generate_csr(privkey, names, path, must_staple=config.must_staple,
strict_permissions=config.strict_permissions)
# WARNING: the csr and private key file are possible attack vectors for TOCTOU

View file

@ -123,8 +123,7 @@ def choose_names(installer, question=None):
names = get_valid_domains(domains)
if not names:
return _choose_names_manually(
"No names were found in your configuration files. ")
return _choose_names_manually()
code, names = _filter_names(names, question)
if code == display_util.OK and names:
@ -192,7 +191,8 @@ def _choose_names_manually(prompt_prefix=""):
"""
code, input_ = z_util(interfaces.IDisplay).input(
prompt_prefix +
"Please enter in your domain name(s) (comma and/or space separated) ",
"Please enter the domain name(s) you would like on your certificate "
"(comma and/or space separated)",
cli_flag="--domains", force_interactive=True)
if code == display_util.OK:

View file

@ -84,7 +84,7 @@ Apache
The Apache plugin currently `supports
<https://github.com/certbot/certbot/blob/master/certbot-apache/certbot_apache/_internal/entrypoint.py>`_
modern OSes based on Debian, Fedora, SUSE, Gentoo and Darwin.
modern OSes based on Debian, Fedora, SUSE, Gentoo, CentOS and Darwin.
This automates both obtaining *and* installing certificates on an Apache
webserver. To specify this plugin on the command line, simply include
``--apache``.
@ -695,10 +695,50 @@ is done by means of a scheduled task which runs ``certbot renew`` periodically.
If you are unsure whether you need to configure automated renewal:
1. Review the instructions for your system at https://certbot.eff.org/instructions.
They will describe how to set up a scheduled task, if necessary.
2. (Linux/BSD): Check your system's crontab (typically `/etc/crontab` and
`/etc/cron.*/*`) and systemd timers (``systemctl list-timers``).
1. Review the instructions for your system and installation method at
https://certbot.eff.org/instructions. They will describe how to set up a scheduled task,
if necessary. If no step is listed, your system comes with automated renewal pre-installed,
and you should not need to take any additional actions.
2. On Linux and BSD, you can check to see if your installation method has pre-installed a timer
for you. To do so, look for the ``certbot renew`` command in either your system's crontab
(typically `/etc/crontab` or `/etc/cron.*/*`) or systemd timers (``systemctl list-timers``).
3. If you're still not sure, you can configure automated renewal manually by following the steps
in the next section. Certbot has been carefully engineered to handle the case where both manual
automated renewal and pre-installed automated renewal are set up.
Setting up automated renewal
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you think you may need to set up automated renewal, follow these instructions to set up a
scheduled task to automatically renew your certificates in the background. If you are unsure
whether your system has a pre-installed scheduled task for Certbot, it is safe to follow these
instructions to create one.
If you're using Windows, these instructions are not neccessary as Certbot on Windows comes with
a scheduled task for automated renewal pre-installed.
Run the following line, which will add a cron job to `/etc/crontab`:
.. code-block:: shell
SLEEPTIME=$(awk 'BEGIN{srand(); print int(rand()*(3600+1))}'); echo "0 0,12 * * * root sleep $SLEEPTIME && certbot renew -q" | sudo tee -a /etc/crontab > /dev/null
If you needed to stop your webserver to run Certbot, you'll want to
add ``pre`` and ``post`` hooks to stop and start your webserver automatically.
For example, if your webserver is HAProxy, run the following commands to create the hook files
in the appropriate directory:
.. code-block:: shell
sudo sh -c 'printf "#!/bin/sh\nservice haproxy stop\n" > /etc/letsencrypt/renewal-hooks/pre/haproxy.sh'
sudo sh -c 'printf "#!/bin/sh\nservice haproxy start\n" > /etc/letsencrypt/renewal-hooks/post/haproxy.sh'
sudo chmod 755 /etc/letsencrypt/renewal-hooks/pre/haproxy.sh
sudo chmod 755 /etc/letsencrypt/renewal-hooks/post/haproxy.sh
Congratulations, Certbot will now automatically renew your certificates in the background.
If you are interested in learning more about how Certbot renews your certificates, see the
`Renewing certificates`_ section above.
.. _where-certs:

View file

@ -69,10 +69,9 @@ class HandleAuthorizationsTest(unittest.TestCase):
from certbot._internal.auth_handler import AuthHandler
self.mock_display = mock.Mock()
self.mock_config = mock.Mock(debug_challenges=False)
zope.component.provideUtility(
self.mock_display, interfaces.IDisplay)
zope.component.provideUtility(
mock.Mock(debug_challenges=False), interfaces.IConfig)
self.mock_auth = mock.MagicMock(name="ApacheConfigurator")
@ -99,7 +98,7 @@ class HandleAuthorizationsTest(unittest.TestCase):
self.mock_net.poll.side_effect = _gen_mock_on_poll(retry=1, wait_value=30)
with mock.patch('certbot._internal.auth_handler.time') as mock_time:
authzr = self.handler.handle_authorizations(mock_order)
authzr = self.handler.handle_authorizations(mock_order, self.mock_config)
self.assertEqual(self.mock_net.answer_challenge.call_count, 1)
@ -131,7 +130,7 @@ class HandleAuthorizationsTest(unittest.TestCase):
authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=False)
mock_order = mock.MagicMock(authorizations=[authzr])
authzr = self.handler.handle_authorizations(mock_order)
authzr = self.handler.handle_authorizations(mock_order, self.mock_config)
self.assertEqual(self.mock_net.answer_challenge.call_count, 2)
@ -152,7 +151,7 @@ class HandleAuthorizationsTest(unittest.TestCase):
authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=False)
mock_order = mock.MagicMock(authorizations=[authzr])
authzr = self.handler.handle_authorizations(mock_order)
authzr = self.handler.handle_authorizations(mock_order, self.mock_config)
self.assertEqual(self.mock_net.answer_challenge.call_count, 1)
@ -176,7 +175,7 @@ class HandleAuthorizationsTest(unittest.TestCase):
mock_order = mock.MagicMock(authorizations=authzrs)
self.mock_net.poll.side_effect = _gen_mock_on_poll()
authzr = self.handler.handle_authorizations(mock_order)
authzr = self.handler.handle_authorizations(mock_order, self.mock_config)
self.assertEqual(self.mock_net.answer_challenge.call_count, 3)
@ -195,14 +194,13 @@ class HandleAuthorizationsTest(unittest.TestCase):
self._test_name3_http_01_3_common(combos=False)
def test_debug_challenges(self):
zope.component.provideUtility(
mock.Mock(debug_challenges=True), interfaces.IConfig)
config = mock.Mock(debug_challenges=True)
authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)]
mock_order = mock.MagicMock(authorizations=authzrs)
self.mock_net.poll.side_effect = _gen_mock_on_poll()
self.handler.handle_authorizations(mock_order)
self.handler.handle_authorizations(mock_order, config)
self.assertEqual(self.mock_net.answer_challenge.call_count, 1)
self.assertEqual(self.mock_display.notification.call_count, 1)
@ -214,7 +212,8 @@ class HandleAuthorizationsTest(unittest.TestCase):
self.mock_auth.perform.side_effect = errors.AuthorizationError
self.assertRaises(
errors.AuthorizationError, self.handler.handle_authorizations, mock_order)
errors.AuthorizationError, self.handler.handle_authorizations,
mock_order, self.mock_config)
def test_max_retries_exceeded(self):
authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES)]
@ -225,12 +224,13 @@ class HandleAuthorizationsTest(unittest.TestCase):
with self.assertRaises(errors.AuthorizationError) as error:
# We retry only once, so retries will be exhausted before STATUS_VALID is returned.
self.handler.handle_authorizations(mock_order, False, 1)
self.handler.handle_authorizations(mock_order, self.mock_config, False, 1)
self.assertIn('All authorizations were not finalized by the CA.', str(error.exception))
def test_no_domains(self):
mock_order = mock.MagicMock(authorizations=[])
self.assertRaises(errors.AuthorizationError, self.handler.handle_authorizations, mock_order)
self.assertRaises(errors.AuthorizationError, self.handler.handle_authorizations,
mock_order, self.mock_config)
def _test_preferred_challenge_choice_common(self, combos):
authzrs = [gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=combos)]
@ -242,7 +242,7 @@ class HandleAuthorizationsTest(unittest.TestCase):
challenges.DNS01.typ,))
self.mock_net.poll.side_effect = _gen_mock_on_poll()
self.handler.handle_authorizations(mock_order)
self.handler.handle_authorizations(mock_order, self.mock_config)
self.assertEqual(self.mock_auth.cleanup.call_count, 1)
self.assertEqual(
@ -260,7 +260,8 @@ class HandleAuthorizationsTest(unittest.TestCase):
mock_order = mock.MagicMock(authorizations=authzrs)
self.handler.pref_challs.append(challenges.DNS01.typ)
self.assertRaises(
errors.AuthorizationError, self.handler.handle_authorizations, mock_order)
errors.AuthorizationError, self.handler.handle_authorizations,
mock_order, self.mock_config)
def test_preferred_challenges_not_supported_acme_1(self):
self._test_preferred_challenges_not_supported_common(combos=True)
@ -273,14 +274,16 @@ class HandleAuthorizationsTest(unittest.TestCase):
authzrs = [gen_dom_authzr(domain="0", challs=[acme_util.DNS01])]
mock_order = mock.MagicMock(authorizations=authzrs)
self.assertRaises(
errors.AuthorizationError, self.handler.handle_authorizations, mock_order)
errors.AuthorizationError, self.handler.handle_authorizations,
mock_order, self.mock_config)
def test_perform_error(self):
self.mock_auth.perform.side_effect = errors.AuthorizationError
authzr = gen_dom_authzr(domain="0", challs=acme_util.CHALLENGES, combos=True)
mock_order = mock.MagicMock(authorizations=[authzr])
self.assertRaises(errors.AuthorizationError, self.handler.handle_authorizations, mock_order)
self.assertRaises(errors.AuthorizationError, self.handler.handle_authorizations,
mock_order, self.mock_config)
self.assertEqual(self.mock_auth.cleanup.call_count, 1)
self.assertEqual(
@ -293,7 +296,8 @@ class HandleAuthorizationsTest(unittest.TestCase):
mock_order = mock.MagicMock(authorizations=authzrs)
self.assertRaises(
errors.AuthorizationError, self.handler.handle_authorizations, mock_order)
errors.AuthorizationError, self.handler.handle_authorizations,
mock_order, self.mock_config)
self.assertEqual(self.mock_auth.cleanup.call_count, 1)
self.assertEqual(
self.mock_auth.cleanup.call_args[0][0][0].typ, "http-01")
@ -305,7 +309,7 @@ class HandleAuthorizationsTest(unittest.TestCase):
with test_util.patch_get_utility():
with self.assertRaises(errors.AuthorizationError) as error:
self.handler.handle_authorizations(mock_order, False)
self.handler.handle_authorizations(mock_order, self.mock_config, False)
self.assertIn('Some challenges have failed.', str(error.exception))
self.assertEqual(self.mock_auth.cleanup.call_count, 1)
self.assertEqual(
@ -330,7 +334,7 @@ class HandleAuthorizationsTest(unittest.TestCase):
with mock.patch('certbot._internal.auth_handler.AuthHandler._report_failed_authzrs') \
as mock_report:
valid_authzr = self.handler.handle_authorizations(mock_order, True)
valid_authzr = self.handler.handle_authorizations(mock_order, self.mock_config, True)
# Because best_effort=True, we did not blow up. Instead ...
self.assertEqual(len(valid_authzr), 1) # ... the valid authzr has been processed
@ -340,7 +344,7 @@ class HandleAuthorizationsTest(unittest.TestCase):
with test_util.patch_get_utility():
with self.assertRaises(errors.AuthorizationError) as error:
self.handler.handle_authorizations(mock_order, True)
self.handler.handle_authorizations(mock_order, self.mock_config, True)
# Despite best_effort=True, process will fail because no authzr is valid.
self.assertIn('All challenges have failed.', str(error.exception))
@ -354,7 +358,8 @@ class HandleAuthorizationsTest(unittest.TestCase):
[messages.STATUS_PENDING], False)
mock_order = mock.MagicMock(authorizations=[authzr])
self.assertRaises(
errors.AuthorizationError, self.handler.handle_authorizations, mock_order)
errors.AuthorizationError, self.handler.handle_authorizations,
mock_order, self.mock_config)
# With a validated challenge that is not supported by the plugin, we
# expect the challenge to not be solved again and
@ -364,7 +369,7 @@ class HandleAuthorizationsTest(unittest.TestCase):
[acme_util.DNS01],
[messages.STATUS_VALID], False)
mock_order = mock.MagicMock(authorizations=[authzr])
self.handler.handle_authorizations(mock_order)
self.handler.handle_authorizations(mock_order, self.mock_config)
def test_valid_authzrs_deactivated(self):
"""When we deactivate valid authzrs in an orderr, we expect them to become deactivated

View file

@ -242,6 +242,7 @@ class ClientTest(ClientTestCommon):
self.config.allow_subset_of_names = False
self.config.dry_run = False
self.config.strict_permissions = True
self.eg_domains = ["example.com", "www.example.com"]
self.eg_order = mock.MagicMock(
authorizations=[None],
@ -263,6 +264,7 @@ class ClientTest(ClientTestCommon):
if auth_count == 1:
self.client.auth_handler.handle_authorizations.assert_called_once_with(
self.eg_order,
self.config,
self.config.allow_subset_of_names)
else:
self.assertEqual(self.client.auth_handler.handle_authorizations.call_count, auth_count)
@ -273,16 +275,14 @@ class ClientTest(ClientTestCommon):
@mock.patch("certbot._internal.client.crypto_util")
@mock.patch("certbot._internal.client.logger")
@test_util.patch_get_utility()
def test_obtain_certificate_from_csr(self, unused_mock_get_utility,
mock_logger, mock_crypto_util):
def test_obtain_certificate_from_csr(self, mock_logger, mock_crypto_util):
self._mock_obtain_certificate()
test_csr = util.CSR(form="pem", file=None, data=CSR_SAN)
auth_handler = self.client.auth_handler
self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)
orderr = self.acme.new_order(test_csr.data)
auth_handler.handle_authorizations(orderr, False)
auth_handler.handle_authorizations(orderr, self.config, False)
self.assertEqual(
(mock.sentinel.cert, mock.sentinel.chain),
self.client.obtain_certificate_from_csr(
@ -310,7 +310,7 @@ class ClientTest(ClientTestCommon):
self.client.obtain_certificate_from_csr(
test_csr,
orderr=None))
auth_handler.handle_authorizations.assert_called_with(self.eg_order, False)
auth_handler.handle_authorizations.assert_called_with(self.eg_order, self.config, False)
# Test for no auth_handler
self.client.auth_handler = None
@ -323,20 +323,21 @@ class ClientTest(ClientTestCommon):
@mock.patch("certbot._internal.client.crypto_util")
def test_obtain_certificate(self, mock_crypto_util):
csr = util.CSR(form="pem", file=None, data=CSR_SAN)
mock_crypto_util.init_save_csr.return_value = csr
mock_crypto_util.init_save_key.return_value = mock.sentinel.key
mock_crypto_util.generate_csr.return_value = csr
mock_crypto_util.generate_key.return_value = mock.sentinel.key
self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)
self._test_obtain_certificate_common(mock.sentinel.key, csr)
mock_crypto_util.init_save_key.assert_called_once_with(
mock_crypto_util.generate_key.assert_called_once_with(
key_size=self.config.rsa_key_size,
key_dir=self.config.key_dir,
key_type=self.config.key_type,
elliptic_curve=None, # elliptic curve is not set
elliptic_curve="secp256r1",
strict_permissions=True,
)
mock_crypto_util.init_save_csr.assert_called_once_with(
mock.sentinel.key, self.eg_domains, self.config.csr_dir)
mock_crypto_util.generate_csr.assert_called_once_with(
mock.sentinel.key, self.eg_domains, self.config.csr_dir, False, True)
mock_crypto_util.cert_and_chain_from_fullchain.assert_called_once_with(
self.eg_order.fullchain_pem)
@ -345,16 +346,16 @@ class ClientTest(ClientTestCommon):
def test_obtain_certificate_partial_success(self, mock_remove, mock_crypto_util):
csr = util.CSR(form="pem", file=mock.sentinel.csr_file, data=CSR_SAN)
key = util.CSR(form="pem", file=mock.sentinel.key_file, data=CSR_SAN)
mock_crypto_util.init_save_csr.return_value = csr
mock_crypto_util.init_save_key.return_value = key
mock_crypto_util.generate_csr.return_value = csr
mock_crypto_util.generate_key.return_value = key
self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)
authzr = self._authzr_from_domains(["example.com"])
self.config.allow_subset_of_names = True
self._test_obtain_certificate_common(key, csr, authzr_ret=authzr, auth_count=2)
self.assertEqual(mock_crypto_util.init_save_key.call_count, 2)
self.assertEqual(mock_crypto_util.init_save_csr.call_count, 2)
self.assertEqual(mock_crypto_util.generate_key.call_count, 2)
self.assertEqual(mock_crypto_util.generate_csr.call_count, 2)
self.assertEqual(mock_remove.call_count, 2)
self.assertEqual(mock_crypto_util.cert_and_chain_from_fullchain.call_count, 1)
@ -372,13 +373,13 @@ class ClientTest(ClientTestCommon):
mock_crypto.make_key.assert_called_once_with(
bits=self.config.rsa_key_size,
elliptic_curve=None, # not making an elliptic private key
elliptic_curve="secp256r1",
key_type=self.config.key_type,
)
mock_acme_crypto.make_csr.assert_called_once_with(
mock.sentinel.key_pem, self.eg_domains, self.config.must_staple)
mock_crypto.init_save_key.assert_not_called()
mock_crypto.init_save_csr.assert_not_called()
mock_crypto.generate_key.assert_not_called()
mock_crypto.generate_csr.assert_not_called()
self.assertEqual(mock_crypto.cert_and_chain_from_fullchain.call_count, 1)
@mock.patch("certbot._internal.client.logger")
@ -521,13 +522,12 @@ class ClientTest(ClientTestCommon):
@test_util.patch_get_utility()
def test_deploy_certificate_success(self, mock_util):
self.assertRaises(errors.Error, self.client.deploy_certificate,
"foo.bar", ["foo.bar"], "key", "cert", "chain", "fullchain")
["foo.bar"], "key", "cert", "chain", "fullchain")
installer = mock.MagicMock()
self.client.installer = installer
self.client.deploy_certificate(
"foo.bar", ["foo.bar"], "key", "cert", "chain", "fullchain")
self.client.deploy_certificate(["foo.bar"], "key", "cert", "chain", "fullchain")
installer.deploy_cert.assert_called_once_with(
cert_path=os.path.abspath("cert"),
chain_path=os.path.abspath("chain"),
@ -546,31 +546,10 @@ class ClientTest(ClientTestCommon):
installer.deploy_cert.side_effect = errors.PluginError
self.assertRaises(errors.PluginError, self.client.deploy_certificate,
"foo.bar", ["foo.bar"], "key", "cert", "chain", "fullchain")
["foo.bar"], "key", "cert", "chain", "fullchain")
installer.recovery_routine.assert_called_once_with()
mock_notify.assert_any_call('Deploying certificate')
mock_notify.assert_any_call(
'Failed to install the certificate (installer: foobar). '
'Try again after fixing errors by running:\n\n certbot install --cert-name foo.bar\n'
)
@mock.patch('certbot._internal.client.display_util.notify')
@test_util.patch_get_utility()
def test_deploy_certificate_failure_no_certname(self, mock_util, mock_notify):
installer = mock.MagicMock()
self.client.installer = installer
self.config.installer = "foobar"
installer.deploy_cert.side_effect = errors.PluginError
self.assertRaises(errors.PluginError, self.client.deploy_certificate,
None, ["foo.bar"], "key", "cert", "chain", "fullchain")
installer.recovery_routine.assert_called_once_with()
mock_notify.assert_any_call('Deploying certificate')
mock_notify.assert_any_call(
'Failed to install the certificate (installer: foobar).'
)
@test_util.patch_get_utility()
@ -580,7 +559,7 @@ class ClientTest(ClientTestCommon):
installer.save.side_effect = errors.PluginError
self.assertRaises(errors.PluginError, self.client.deploy_certificate,
"foo.bar", ["foo.bar"], "key", "cert", "chain", "fullchain")
["foo.bar"], "key", "cert", "chain", "fullchain")
installer.recovery_routine.assert_called_once_with()
@mock.patch('certbot._internal.client.display_util.notify')
@ -591,7 +570,7 @@ class ClientTest(ClientTestCommon):
self.client.installer = installer
self.assertRaises(errors.PluginError, self.client.deploy_certificate,
"foo.bar", ["foo.bar"], "key", "cert", "chain", "fullchain")
["foo.bar"], "key", "cert", "chain", "fullchain")
mock_notify.assert_called_with(
'We were unable to install your certificate, however, we successfully restored '
'your server to its prior configuration.')
@ -607,7 +586,7 @@ class ClientTest(ClientTestCommon):
self.client.installer = installer
self.assertRaises(errors.PluginError, self.client.deploy_certificate,
"foo.bar", ["foo.bar"], "key", "cert", "chain", "fullchain")
["foo.bar"], "key", "cert", "chain", "fullchain")
self.assertEqual(mock_logger.error.call_count, 1)
self.assertIn(
'An error occurred and we failed to restore your config',

View file

@ -2,15 +2,15 @@
import logging
import unittest
import certbot.util
try:
import mock
except ImportError: # pragma: no cover
from unittest import mock
import OpenSSL
import zope.component
from certbot import errors
from certbot import interfaces
from certbot import util
from certbot.compat import filesystem
from certbot.compat import os
@ -33,8 +33,8 @@ CERT_ISSUER = test_util.load_vector('cert_intermediate_1.pem')
CERT_ALT_ISSUER = test_util.load_vector('cert_intermediate_2.pem')
class InitSaveKeyTest(test_util.TempDirTestCase):
"""Tests for certbot.crypto_util.init_save_key."""
class GenerateKeyTest(test_util.TempDirTestCase):
"""Tests for certbot.crypto_util.generate_key."""
def setUp(self):
super().setUp()
@ -42,8 +42,6 @@ class InitSaveKeyTest(test_util.TempDirTestCase):
filesystem.mkdir(self.workdir, mode=0o700)
logging.disable(logging.CRITICAL)
zope.component.provideUtility(
mock.Mock(strict_permissions=True), interfaces.IConfig)
def tearDown(self):
super().tearDown()
@ -52,8 +50,8 @@ class InitSaveKeyTest(test_util.TempDirTestCase):
@classmethod
def _call(cls, key_size, key_dir):
from certbot.crypto_util import init_save_key
return init_save_key(key_size, key_dir, 'key-certbot.pem')
from certbot.crypto_util import generate_key
return generate_key(key_size, key_dir, 'key-certbot.pem', strict_permissions=True)
@mock.patch('certbot.crypto_util.make_key')
def test_success(self, mock_make):
@ -69,29 +67,57 @@ class InitSaveKeyTest(test_util.TempDirTestCase):
self.assertRaises(ValueError, self._call, 431, self.workdir)
class InitSaveCSRTest(test_util.TempDirTestCase):
"""Tests for certbot.crypto_util.init_save_csr."""
class InitSaveKey(unittest.TestCase):
"""Test for certbot.crypto_util.init_save_key."""
@mock.patch("certbot.crypto_util.generate_key")
@mock.patch("certbot.crypto_util.zope.component")
def test_it(self, mock_zope, mock_generate):
from certbot.crypto_util import init_save_key
def setUp(self):
super().setUp()
mock_zope.getUtility.return_value = mock.MagicMock(strict_permissions=True)
zope.component.provideUtility(
mock.Mock(strict_permissions=True), interfaces.IConfig)
with self.assertWarns(DeprecationWarning):
init_save_key(4096, "/some/path")
mock_generate.assert_called_with(4096, "/some/path", elliptic_curve="secp256r1",
key_type="rsa", keyname="key-certbot.pem",
strict_permissions=True)
class GenerateCSRTest(test_util.TempDirTestCase):
"""Tests for certbot.crypto_util.generate_csr."""
@mock.patch('acme.crypto_util.make_csr')
@mock.patch('certbot.crypto_util.util.make_or_verify_dir')
def test_it(self, unused_mock_verify, mock_csr):
from certbot.crypto_util import init_save_csr
from certbot.crypto_util import generate_csr
mock_csr.return_value = b'csr_pem'
csr = init_save_csr(
mock.Mock(pem='dummy_key'), 'example.com', self.tempdir)
csr = generate_csr(
mock.Mock(pem='dummy_key'), 'example.com', self.tempdir, strict_permissions=True)
self.assertEqual(csr.data, b'csr_pem')
self.assertIn('csr-certbot.pem', csr.file)
class InitSaveCsr(unittest.TestCase):
"""Tests for certbot.crypto_util.init_save_csr."""
@mock.patch("certbot.crypto_util.generate_csr")
@mock.patch("certbot.crypto_util.zope.component")
def test_it(self, mock_zope, mock_generate):
from certbot.crypto_util import init_save_csr
mock_zope.getUtility.return_value = mock.MagicMock(must_staple=True,
strict_permissions=True)
key = certbot.util.Key(file=None, pem=None)
with self.assertWarns(DeprecationWarning):
init_save_csr(key, {"dummy"}, "/some/path")
mock_generate.assert_called_with(key, {"dummy"}, "/some/path",
must_staple=True, strict_permissions=True)
class ValidCSRTest(unittest.TestCase):
"""Tests for certbot.crypto_util.valid_csr."""

View file

@ -209,7 +209,6 @@ class ChooseNamesTest(unittest.TestCase):
actual_doms = self._call(self.mock_install)
self.assertEqual(mock_util().input.call_count, 1)
self.assertEqual(actual_doms, [domain])
self.assertIn("configuration files", mock_util().input.call_args[0][0])
def test_sort_names_trivial(self):
from certbot.display.ops import _sort_names

View file

@ -112,6 +112,7 @@ class RunTest(test_util.ConfigTestCase):
mock.patch('certbot._internal.main._report_new_cert'),
mock.patch('certbot._internal.main._find_cert'),
mock.patch('certbot._internal.eff.handle_subscription'),
mock.patch('certbot._internal.main._report_next_steps')
]
self.mock_auth = patches[0].start()
@ -122,6 +123,7 @@ class RunTest(test_util.ConfigTestCase):
self.mock_report_cert = patches[5].start()
self.mock_find_cert = patches[6].start()
self.mock_subscription = patches[7].start()
self.mock_report_next_steps = patches[8].start()
for patch in patches:
self.addCleanup(patch.stop)
@ -139,6 +141,8 @@ class RunTest(test_util.ConfigTestCase):
self.mock_find_cert.return_value = True, None
self._call()
self.mock_success_installation.assert_called_once_with([self.domain])
self.mock_report_next_steps.assert_called_once_with(mock.ANY, None, mock.ANY,
new_or_renewed_cert=True)
def test_reinstall_success(self):
self.mock_auth.return_value = mock.Mock()
@ -161,6 +165,18 @@ class RunTest(test_util.ConfigTestCase):
main.run,
self.config, plugins)
@mock.patch('certbot._internal.main._install_cert')
def test_cert_success_install_error(self, mock_install_cert):
mock_install_cert.side_effect = errors.PluginError("Fake installation error")
self.mock_auth.return_value = mock.Mock()
self.mock_find_cert.return_value = True, None
self.assertRaises(errors.PluginError, self._call)
# Next steps should contain both renewal advice and installation error
self.mock_report_next_steps.assert_called_once_with(
mock.ANY, mock_install_cert.side_effect, mock.ANY, new_or_renewed_cert=True)
# The final success message shouldn't be shown
self.mock_success_installation.assert_not_called()
class CertonlyTest(unittest.TestCase):
"""Tests for certbot._internal.main.certonly."""
@ -198,13 +214,14 @@ class CertonlyTest(unittest.TestCase):
def _assert_no_pause(self, message, pause=True): # pylint: disable=unused-argument
self.assertIs(pause, False)
@mock.patch('certbot._internal.main._report_next_steps')
@mock.patch('certbot._internal.cert_manager.lineage_for_certname')
@mock.patch('certbot._internal.cert_manager.domains_for_certname')
@mock.patch('certbot._internal.renewal.renew_cert')
@mock.patch('certbot._internal.main._handle_unexpected_key_type_migration')
@mock.patch('certbot._internal.main._report_new_cert')
def test_find_lineage_for_domains_and_certname(self, mock_report_cert,
mock_handle_type, mock_renew_cert, mock_domains, mock_lineage):
mock_handle_type, mock_renew_cert, mock_domains, mock_lineage, mock_report_next_steps):
domains = ['example.com', 'test.org']
mock_domains.return_value = domains
mock_lineage.names.return_value = domains
@ -216,6 +233,8 @@ class CertonlyTest(unittest.TestCase):
self.assertEqual(mock_renew_cert.call_count, 1)
self.assertEqual(mock_report_cert.call_count, 1)
self.assertEqual(mock_handle_type.call_count, 1)
mock_report_next_steps.assert_called_once_with(
mock.ANY, None, mock.ANY, new_or_renewed_cert=True)
# user confirms updating lineage with new domains
self._call(('certonly --webroot -d example.com -d test.com '
@ -231,12 +250,13 @@ class CertonlyTest(unittest.TestCase):
self.assertRaises(errors.ConfigurationError, self._call,
'certonly --webroot -d example.com -d test.com --cert-name example.com'.split())
@mock.patch('certbot._internal.main._report_next_steps')
@mock.patch('certbot._internal.cert_manager.domains_for_certname')
@mock.patch('certbot.display.ops.choose_names')
@mock.patch('certbot._internal.cert_manager.lineage_for_certname')
@mock.patch('certbot._internal.main._report_new_cert')
def test_find_lineage_for_domains_new_certname(self, mock_report_cert,
mock_lineage, mock_choose_names, mock_domains_for_certname):
mock_lineage, mock_choose_names, mock_domains_for_certname, unused_mock_report_next_steps):
mock_lineage.return_value = None
# no lineage with this name but we specified domains so create a new cert
@ -1823,7 +1843,8 @@ class ReportNewCertTest(unittest.TestCase):
'Key is saved at: /path/to/privkey.pem\n'
'This certificate expires on 1970-01-01.\n'
'These files will be updated when the certificate renews.\n'
'Certbot will automatically renew this certificate in the background.'
'Certbot has set up a scheduled task to automatically renew this '
'certificate in the background.'
)
def test_report_no_key(self):
@ -1836,7 +1857,8 @@ class ReportNewCertTest(unittest.TestCase):
'Certificate is saved at: /path/to/fullchain.pem\n'
'This certificate expires on 1970-01-01.\n'
'These files will be updated when the certificate renews.\n'
'Certbot will automatically renew this certificate in the background.'
'Certbot has set up a scheduled task to automatically renew this '
'certificate in the background.'
)
def test_report_no_preconfigured_renewal(self):
@ -1849,13 +1871,9 @@ class ReportNewCertTest(unittest.TestCase):
'Certificate is saved at: /path/to/fullchain.pem\n'
'Key is saved at: /path/to/privkey.pem\n'
'This certificate expires on 1970-01-01.\n'
'These files will be updated when the certificate renews.\n'
'Run "certbot renew" to renew expiring certificates. We recommend setting up a '
'scheduled task for renewal; see https://certbot.eff.org/docs/using.html#automated'
'-renewals for instructions.'
'These files will be updated when the certificate renews.'
)
def test_csr_report(self):
self._call_csr(mock.Mock(dry_run=False), '/path/to/cert.pem',
'/path/to/chain.pem', '/path/to/fullchain.pem')

View file

@ -1,48 +0,0 @@
"""
Given an ACME account key as input, deactivate the account.
This can be useful if you created an account with a non-Certbot client and now
want to deactivate it.
Private key should be in PKCS#8 PEM form.
To provide the URL for the ACME server you want to use, set it in the $DIRECTORY
environment variable, e.g.:
DIRECTORY=https://acme-staging.api.letsencrypt.org/directory python \
deactivate.py private_key.pem
"""
import os
import sys
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import josepy as jose
from acme import client as acme_client
from acme import errors as acme_errors
from acme import messages
DIRECTORY = os.getenv('DIRECTORY', 'http://localhost:4000/directory')
if len(sys.argv) != 2:
print("Usage: python deactivate.py private_key.pem")
sys.exit(1)
data = open(sys.argv[1], "r").read()
key = jose.JWKRSA(key=serialization.load_pem_private_key(
data, None, default_backend()))
net = acme_client.ClientNetwork(key, verify_ssl=False,
user_agent="acme account deactivator")
client = acme_client.Client(DIRECTORY, key=key, net=net)
try:
# We expect this to fail and give us a Conflict response with a Location
# header pointing at the account's URL.
client.register()
except acme_errors.ConflictError as e:
location = e.location
if location is None:
raise "Key was not previously registered (but now is)."
client.deactivate_registration(messages.RegistrationResource(uri=location))

View file

@ -1,123 +0,0 @@
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <openssl/evp.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
// This program can be used to perform RSA public key signatures given only
// the hash of the file to be signed as input.
// To compile:
// gcc half-sign.c -lssl -lcrypto -o half-sign
// Sign with SHA256
#define HASH_SIZE 32
void usage() {
printf("half-sign <private key file> [binary hash file]\n");
printf("\n");
printf(" Computes and prints a binary RSA signature over data given the SHA256 hash of\n");
printf(" the data as input.\n");
printf("\n");
printf(" <private key file> should be PEM encoded.\n");
printf("\n");
printf(" The input SHA256 hash should be %d bytes in length. If no binary hash file is\n", HASH_SIZE);
printf(" specified, it will be read from stdin.\n");
exit(1);
}
void sign_hashed_data(EVP_PKEY *signing_key, unsigned char *md, size_t mdlen) {
// cribbed from the openssl EVP_PKEY_sign man page
EVP_PKEY_CTX *ctx;
unsigned char *sig;
size_t siglen;
/* NB: assumes signing_key, md and mdlen are already set up
* and that signing_key is an RSA private key
*/
ctx = EVP_PKEY_CTX_new(signing_key, NULL);
if ((!ctx)
|| (EVP_PKEY_sign_init(ctx) <= 0)
|| (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING) <= 0)
|| (EVP_PKEY_CTX_set_signature_md(ctx, EVP_sha256()) <= 0)) {
fprintf(stderr, "Failure establishing ctx for signature\n");
exit(1);
}
/* Determine buffer length */
if (EVP_PKEY_sign(ctx, NULL, &siglen, md, mdlen) <= 0) {
fprintf(stderr, "Unable to determine buffer length for signature\n");
exit(1);
}
sig = OPENSSL_malloc(siglen);
if (!sig) {
fprintf(stderr, "Malloc failed\n");
exit(1);
}
if (EVP_PKEY_sign(ctx, sig, &siglen, md, mdlen) <= 0) {
fprintf(stderr, "Signature error\n");
exit(1);
}
/* Signature is siglen bytes written to buffer sig */
fwrite(sig, siglen, 1, stdout);
}
EVP_PKEY *read_private_key(char *filename) {
FILE *keyfile;
EVP_PKEY *privkey;
keyfile = fopen(filename, "r");
if (!keyfile) {
fprintf(stderr, "Failed to open private key.pem file %s\n", filename);
exit(1);
}
privkey = PEM_read_PrivateKey(keyfile, NULL, NULL, NULL);
if (!privkey) {
fprintf(stderr, "Failed to read PEM private key from %s\n", filename);
exit(1);
}
if (EVP_PKEY_type(privkey->type) != EVP_PKEY_RSA) {
fprintf(stderr, "%s was a non-RSA key\n", filename);
exit(1);
}
return privkey;
}
int main(int argc, char *argv[]) {
FILE *input;
unsigned char *buffer;
int test;
EVP_PKEY *privkey;
if (argc > 3 || argc < 2)
usage();
if (argc < 3 || strcmp(argv[2],"-") == 0)
input = stdin;
else {
input = fopen(argv[2], "r");
if (!input) usage();
}
privkey = read_private_key(argv[1]);
buffer = malloc(HASH_SIZE);
if (!buffer) {
fprintf(stderr, "Argh, malloc failed\n");
exit(1);
}
if (fread(buffer, HASH_SIZE, 1, input) != 1) {
perror("half-sign: Failed to read SHA256 from input\n");
exit(1);
}
test = fgetc(input);
if (test != EOF && test != '\n') {
fprintf(stderr,"Error, more than %d bytes fed to half-sign\n", HASH_SIZE);
fprintf(stderr,"Last byte was :%d\n" , (int) test);
exit(1);
}
sign_hashed_data(privkey, buffer, HASH_SIZE);
return 0;
}

View file

@ -1,31 +0,0 @@
#!/usr/bin/env python3
"""A version of Python's SimpleHTTPServer that flushes its output."""
import sys
try:
from http.server import HTTPServer, SimpleHTTPRequestHandler
except ImportError:
from BaseHTTPServer import HTTPServer
from SimpleHTTPServer import SimpleHTTPRequestHandler
def serve_forever(port=0):
"""Spins up an HTTP server on all interfaces and the given port.
A message is printed to stdout specifying the address and port being used
by the server.
:param int port: port number to use.
"""
server = HTTPServer(('', port), SimpleHTTPRequestHandler)
print('Serving HTTP on {0} port {1} ...'.format(*server.server_address))
sys.stdout.flush()
server.serve_forever()
if __name__ == '__main__':
kwargs = {}
if len(sys.argv) > 1:
kwargs['port'] = int(sys.argv[1])
serve_forever(**kwargs)

View file

@ -0,0 +1,12 @@
"""Pynsist extra_preamble for the Certbot entry point.
This preamble ensures that Certbot on Windows always runs with the --preconfigured-renewal
flag set. Since Pynsist creates a Scheduled Task for renewal, we want this flag to be set
so that we can provide the right automated renewal advice to Certbot on Windows users.
"""
import sys
sys.argv += ["--preconfigured-renewal"]

View file

@ -82,6 +82,7 @@ def _copy_assets(build_path, repo_path):
shutil.copy(os.path.join(repo_path, 'windows-installer', 'assets', 'template.nsi'), build_path)
shutil.copy(os.path.join(repo_path, 'windows-installer', 'assets', 'renew-up.ps1'), build_path)
shutil.copy(os.path.join(repo_path, 'windows-installer', 'assets', 'renew-down.ps1'), build_path)
shutil.copy(os.path.join(repo_path, 'windows-installer', 'assets', 'preamble.py'), build_path)
def _generate_pynsist_config(repo_path, build_path):
@ -121,6 +122,7 @@ files=run.bat
[Command certbot]
entry_point=certbot.main:main
extra_preamble=preamble.py
'''.format(certbot_version=certbot_version,
installer_suffix='win_amd64' if PYTHON_BITNESS == 64 else 'win32',
python_bitness=PYTHON_BITNESS,