diff --git a/acme/setup.py b/acme/setup.py index 6fa49dafc..076c55a89 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -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: diff --git a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py index e209480e3..7cba487cf 100644 --- a/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py +++ b/certbot-compatibility-test/certbot_compatibility_test/configurators/nginx/common.py @@ -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() diff --git a/certbot-dns-gehirn/setup.py b/certbot-dns-gehirn/setup.py index 5f4380847..179b273b0 100644 --- a/certbot-dns-gehirn/setup.py +++ b/certbot-dns-gehirn/setup.py @@ -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', diff --git a/certbot-dns-linode/setup.py b/certbot-dns-linode/setup.py index 2e1e8b41a..d32909458 100644 --- a/certbot-dns-linode/setup.py +++ b/certbot-dns-linode/setup.py @@ -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', diff --git a/certbot-dns-route53/.gitignore b/certbot-dns-route53/.gitignore deleted file mode 100644 index 1dbc687de..000000000 --- a/certbot-dns-route53/.gitignore +++ /dev/null @@ -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 diff --git a/certbot-dns-sakuracloud/setup.py b/certbot-dns-sakuracloud/setup.py index 43702cbf7..47da4204f 100644 --- a/certbot-dns-sakuracloud/setup.py +++ b/certbot-dns-sakuracloud/setup.py @@ -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', diff --git a/certbot-nginx/certbot_nginx/_internal/configurator.py b/certbot-nginx/certbot_nginx/_internal/configurator.py index 62122eef5..07397bfe8 100644 --- a/certbot-nginx/certbot_nginx/_internal/configurator.py +++ b/certbot-nginx/certbot_nginx/_internal/configurator.py @@ -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()]) diff --git a/certbot-nginx/tests/test_util.py b/certbot-nginx/tests/test_util.py index 383a15753..97fe05af0 100644 --- a/certbot-nginx/tests/test_util.py +++ b/certbot-nginx/tests/test_util.py @@ -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 diff --git a/certbot/.gitignore b/certbot/.gitignore deleted file mode 100644 index a4e94ecfc..000000000 --- a/certbot/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.crt diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index 17bce3ab8..f38884884 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -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. diff --git a/certbot/certbot/_internal/auth_handler.py b/certbot/certbot/_internal/auth_handler.py index 036cc91e3..41ae492e2 100644 --- a/certbot/certbot/_internal/auth_handler.py +++ b/certbot/certbot/_internal/auth_handler.py @@ -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. ' diff --git a/certbot/certbot/_internal/client.py b/certbot/certbot/_internal/client.py index d90c0254b..5c0bed220 100644 --- a/certbot/certbot/_internal/client.py +++ b/certbot/certbot/_internal/client.py @@ -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: diff --git a/certbot/certbot/_internal/main.py b/certbot/certbot/_internal/main.py index d1f7019d6..2100a2943 100644 --- a/certbot/certbot/_internal/main.py +++ b/certbot/certbot/_internal/main.py @@ -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. diff --git a/certbot/certbot/_internal/renewal.py b/certbot/certbot/_internal/renewal.py index acb64d7e2..b4fa6aa01 100644 --- a/certbot/certbot/_internal/renewal.py +++ b/certbot/certbot/_internal/renewal.py @@ -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 diff --git a/certbot/certbot/compat/misc.py b/certbot/certbot/compat/misc.py index 7c45e49ec..3932981ac 100644 --- a/certbot/certbot/compat/misc.py +++ b/certbot/certbot/compat/misc.py @@ -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. diff --git a/certbot/certbot/crypto_util.py b/certbot/certbot/crypto_util.py index 51ef2c53a..5cb21b510 100644 --- a/certbot/certbot/crypto_util.py +++ b/certbot/certbot/crypto_util.py @@ -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 diff --git a/certbot/certbot/display/ops.py b/certbot/certbot/display/ops.py index 04a6e73bb..c2051d3d2 100644 --- a/certbot/certbot/display/ops.py +++ b/certbot/certbot/display/ops.py @@ -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: diff --git a/certbot/docs/using.rst b/certbot/docs/using.rst index be4d96c4f..1c68c0ac1 100644 --- a/certbot/docs/using.rst +++ b/certbot/docs/using.rst @@ -84,7 +84,7 @@ Apache The Apache plugin currently `supports `_ -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: diff --git a/certbot/tests/auth_handler_test.py b/certbot/tests/auth_handler_test.py index 751c445fe..25b19def7 100644 --- a/certbot/tests/auth_handler_test.py +++ b/certbot/tests/auth_handler_test.py @@ -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 diff --git a/certbot/tests/client_test.py b/certbot/tests/client_test.py index b42e6992e..51c6767f6 100644 --- a/certbot/tests/client_test.py +++ b/certbot/tests/client_test.py @@ -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', diff --git a/certbot/tests/crypto_util_test.py b/certbot/tests/crypto_util_test.py index 34ad4b09a..858db079c 100644 --- a/certbot/tests/crypto_util_test.py +++ b/certbot/tests/crypto_util_test.py @@ -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.""" diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py index 5214622fb..ea51effc3 100644 --- a/certbot/tests/display/ops_test.py +++ b/certbot/tests/display/ops_test.py @@ -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 diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py index 42e514fea..166b29dea 100644 --- a/certbot/tests/main_test.py +++ b/certbot/tests/main_test.py @@ -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') diff --git a/tools/deactivate.py b/tools/deactivate.py deleted file mode 100644 index 214c0595c..000000000 --- a/tools/deactivate.py +++ /dev/null @@ -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)) diff --git a/tools/half-sign.c b/tools/half-sign.c deleted file mode 100644 index e56bc397c..000000000 --- a/tools/half-sign.c +++ /dev/null @@ -1,123 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -// 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 [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(" 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; -} diff --git a/tools/simple_http_server.py b/tools/simple_http_server.py deleted file mode 100755 index 32f35ec69..000000000 --- a/tools/simple_http_server.py +++ /dev/null @@ -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) diff --git a/windows-installer/assets/preamble.py b/windows-installer/assets/preamble.py new file mode 100644 index 000000000..fea8e9fd8 --- /dev/null +++ b/windows-installer/assets/preamble.py @@ -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"] diff --git a/windows-installer/windows_installer/construct.py b/windows-installer/windows_installer/construct.py index 91df6b714..3c2525dc8 100644 --- a/windows-installer/windows_installer/construct.py +++ b/windows-installer/windows_installer/construct.py @@ -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,