diff -Nru python-certbot-0.28.0/certbot/account.py python-certbot-0.31.0/certbot/account.py --- python-certbot-0.28.0/certbot/account.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/account.py 2019-02-07 21:20:29.000000000 +0000 @@ -142,7 +142,7 @@ def __init__(self, config): self.config = config util.make_or_verify_dir(config.accounts_dir, 0o700, compat.os_geteuid(), - self.config.strict_permissions) + self.config.strict_permissions) def _account_dir_path(self, account_id): return self._account_dir_path_for_server_path(account_id, self.config.server_path) diff -Nru python-certbot-0.28.0/certbot/auth_handler.py python-certbot-0.31.0/certbot/auth_handler.py --- python-certbot-0.28.0/certbot/auth_handler.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/auth_handler.py 2019-02-07 21:20:29.000000000 +0000 @@ -31,7 +31,7 @@ :class:`~acme.challenges.Challenge` types :type auth: :class:`certbot.interfaces.IAuthenticator` - :ivar acme.client.BackwardsCompatibleClientV2 acme: ACME client API. + :ivar acme.client.BackwardsCompatibleClientV2 acme_client: ACME client API. :ivar account: Client's Account :type account: :class:`certbot.account.Account` @@ -40,9 +40,9 @@ type strings with the most preferred challenge listed first """ - def __init__(self, auth, acme, account, pref_challs): + def __init__(self, auth, acme_client, account, pref_challs): self.auth = auth - self.acme = acme + self.acme = acme_client self.account = account self.pref_challs = pref_challs @@ -85,19 +85,26 @@ self.verify_authzr_complete(aauthzrs) # Only return valid authorizations - retVal = [aauthzr.authzr for aauthzr in aauthzrs - if aauthzr.authzr.body.status == messages.STATUS_VALID] + ret_val = [aauthzr.authzr for aauthzr in aauthzrs + if aauthzr.authzr.body.status == messages.STATUS_VALID] - if not retVal: + if not ret_val: raise errors.AuthorizationError( "Challenges failed for all domains") - return retVal + return ret_val def _choose_challenges(self, aauthzrs): - """Retrieve necessary challenges to satisfy server.""" - logger.info("Performing the following challenges:") - for aauthzr in aauthzrs: + """ + Retrieve necessary and pending challenges to satisfy server. + NB: Necessary and already validated challenges are not retrieved, + as they can be reused for a certificate issuance. + """ + pending_authzrs = [aauthzr for aauthzr in aauthzrs + if aauthzr.authzr.body.status != messages.STATUS_VALID] + if pending_authzrs: + logger.info("Performing the following challenges:") + for aauthzr in pending_authzrs: aauthzr_challenges = aauthzr.authzr.body.challenges if self.acme.acme_version == 1: combinations = aauthzr.authzr.body.combinations @@ -125,7 +132,7 @@ def _solve_challenges(self, aauthzrs): """Get Responses for challenges from authenticators.""" - resp = [] # type: Collection[acme.challenges.ChallengeResponse] + resp = [] # type: Collection[challenges.ChallengeResponse] all_achalls = self._get_all_achalls(aauthzrs) try: if all_achalls: @@ -531,7 +538,7 @@ """ problems = collections.defaultdict(list)\ - # type: DefaultDict[str, List[achallenges.KeyAuthorizationAnnotatedChallenge]] + # type: DefaultDict[str, List[achallenges.KeyAuthorizationAnnotatedChallenge]] for achall in failed_achalls: if achall.error: problems[achall.error.typ].append(achall) diff -Nru python-certbot-0.28.0/certbot/client.py python-certbot-0.31.0/certbot/client.py --- python-certbot-0.28.0/certbot/client.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/client.py 2019-02-07 21:20:29.000000000 +0000 @@ -203,9 +203,27 @@ :returns: Registration Resource. :rtype: `acme.messages.RegistrationResource` """ + + eab_credentials_supplied = config.eab_kid and config.eab_hmac_key + if eab_credentials_supplied: + account_public_key = acme.client.net.key.public_key() + eab = messages.ExternalAccountBinding.from_data(account_public_key=account_public_key, + kid=config.eab_kid, + hmac_key=config.eab_hmac_key, + directory=acme.client.directory) + else: + eab = None + + if acme.external_account_required(): + if not eab_credentials_supplied: + msg = ("Server requires external account binding." + " Please use --eab-kid and --eab-hmac-key.") + raise errors.Error(msg) + try: - return acme.new_account_and_tos(messages.NewRegistration.from_data(email=config.email), - tos_cb) + newreg = messages.NewRegistration.from_data(email=config.email, + external_account_binding=eab) + return acme.new_account_and_tos(newreg, tos_cb) except messages.Error as e: if e.code == "invalidEmail" or e.code == "invalidContact": if config.noninteractive_mode: diff -Nru python-certbot-0.28.0/certbot/cli.py python-certbot-0.31.0/certbot/cli.py --- python-certbot-0.28.0/certbot/cli.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/cli.py 2019-02-07 21:20:29.000000000 +0000 @@ -101,6 +101,7 @@ manage your account with Let's Encrypt: register Create a Let's Encrypt ACME account + update_account Update a Let's Encrypt ACME account --agree-tos Agree to the ACME server's Subscriber Agreement -m EMAIL Email address for important account notifications """ @@ -172,10 +173,11 @@ # need warnings return if "CERTBOT_AUTO" not in os.environ: - logger.warning("You are running with an old copy of letsencrypt-auto that does " - "not receive updates, and is less reliable than more recent versions. " - "We recommend upgrading to the latest certbot-auto script, or using native " - "OS packages.") + logger.warning("You are running with an old copy of letsencrypt-auto" + " that does not receive updates, and is less reliable than more" + " recent versions. The letsencrypt client has also been renamed" + " to Certbot. We recommend upgrading to the latest certbot-auto" + " script, or using native OS packages.") logger.debug("Deprecation warning circumstances: %s / %s", sys.argv[0], os.environ) @@ -286,7 +288,9 @@ """ try: filename = os.path.abspath(filename) - return filename, open(filename, mode).read() + with open(filename, mode) as the_file: + contents = the_file.read() + return filename, contents except IOError as exc: raise argparse.ArgumentTypeError(exc.strerror) @@ -394,9 +398,14 @@ }), ("register", { "short": "Register for account with Let's Encrypt / other ACME server", - "opts": "Options for account registration & modification", + "opts": "Options for account registration", "usage": "\n\n certbot register --email user@example.com [options]\n\n" }), + ("update_account", { + "short": "Update existing account with Let's Encrypt / other ACME server", + "opts": "Options for account modification", + "usage": "\n\n certbot update_account --email updated_email@example.com [options]\n\n" + }), ("unregister", { "short": "Irrevocably deactivate your account", "opts": "Options for account deactivation.", @@ -462,6 +471,7 @@ "install": main.install, "plugins": main.plugins_cmd, "register": main.register, + "update_account": main.update_account, "unregister": main.unregister, "renew": main.renew, "revoke": main.revoke, @@ -856,7 +866,9 @@ if chosen_topic == "everything": chosen_topic = "run" if chosen_topic == "all": - return dict([(t, True) for t in self.help_topics]) + # Addition of condition closes #6209 (removal of duplicate route53 option). + return dict([(t, True) if t != 'certbot-route53:auth' else (t, False) + for t in self.help_topics]) elif not chosen_topic: return dict([(t, False) for t in self.help_topics]) else: @@ -942,6 +954,18 @@ "name. In the case of a name collision it will append a number " "like 0001 to the file path name. (default: Ask)") helpful.add( + [None, "run", "certonly", "register"], + "--eab-kid", dest="eab_kid", + metavar="EAB_KID", + help="Key Identifier for External Account Binding" + ) + helpful.add( + [None, "run", "certonly", "register"], + "--eab-hmac-key", dest="eab_hmac_key", + metavar="EAB_HMAC_KEY", + help="HMAC key for External Account Binding" + ) + helpful.add( [None, "run", "certonly", "manage", "delete", "certificates", "renew", "enhance"], "--cert-name", dest="certname", metavar="CERTNAME", default=flag_default("certname"), @@ -976,21 +1000,21 @@ "certificates. Updates to the Subscriber Agreement will still " "affect you, and will be effective 14 days after posting an " "update to the web site.") + # TODO: When `certbot register --update-registration` is fully deprecated, + # delete following helpful.add helpful.add( "register", "--update-registration", action="store_true", - default=flag_default("update_registration"), - help="With the register verb, indicates that details associated " - "with an existing registration, such as the e-mail address, " - "should be updated, rather than registering a new account.") + default=flag_default("update_registration"), dest="update_registration", + help=argparse.SUPPRESS) helpful.add( - ["register", "unregister", "automation"], "-m", "--email", + ["register", "update_account", "unregister", "automation"], "-m", "--email", default=flag_default("email"), help=config_help("email")) - helpful.add(["register", "automation"], "--eff-email", action="store_true", + helpful.add(["register", "update_account", "automation"], "--eff-email", action="store_true", default=flag_default("eff_email"), dest="eff_email", help="Share your e-mail address with EFF") - helpful.add(["register", "automation"], "--no-eff-email", action="store_false", - default=flag_default("eff_email"), dest="eff_email", + helpful.add(["register", "update_account", "automation"], "--no-eff-email", + action="store_false", default=flag_default("eff_email"), dest="eff_email", help="Don't share your e-mail address with EFF") helpful.add( ["automation", "certonly", "run"], @@ -1192,6 +1216,10 @@ helpful.add("renew", "--renew-hook", action=_RenewHookAction, help=argparse.SUPPRESS) helpful.add( + "renew", "--no-random-sleep-on-renew", action="store_false", + default=flag_default("random_sleep_on_renew"), dest="random_sleep_on_renew", + help=argparse.SUPPRESS) + helpful.add( "renew", "--deploy-hook", action=_DeployHookAction, help='Command to be run in a shell once for each successfully' ' issued certificate. For this command, the shell variable' @@ -1283,7 +1311,8 @@ helpful.add("revoke", "--delete-after-revoke", action="store_true", default=flag_default("delete_after_revoke"), - help="Delete certificates after revoking them.") + help="Delete certificates after revoking them, along with all previous and later " + "versions of those certificates.") helpful.add("revoke", "--no-delete-after-revoke", action="store_false", dest="delete_after_revoke", diff -Nru python-certbot-0.28.0/certbot/compat.py python-certbot-0.31.0/certbot/compat.py --- python-certbot-0.28.0/certbot/compat.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/compat.py 2019-02-07 21:20:29.000000000 +0000 @@ -1,12 +1,8 @@ """ Compatibility layer to run certbot both on Linux and Windows. -The approach used here is similar to Modernizr for Web browsers. -We do not check the platform type to determine if a particular logic is supported. -Instead, we apply a logic, and then fallback to another logic if first logic -is not supported at runtime. - -Then logic chains are abstracted into single functions to be exposed to certbot. +This module contains all required platform specific code, +allowing the rest of Certbot codebase to be platform agnostic. """ import os import select @@ -27,6 +23,8 @@ UNPRIVILEGED_SUBCOMMANDS_ALLOWED = [ 'certificates', 'enhance', 'revoke', 'delete', 'register', 'unregister', 'config_changes', 'plugins'] + + def raise_for_non_administrative_windows_rights(subcommand): """ On Windows, raise if current shell does not have the administrative rights. @@ -50,6 +48,7 @@ 'Error, "{0}" subcommand must be run on a shell with administrative rights.' .format(subcommand)) + def os_geteuid(): """ Get current user uid @@ -65,6 +64,7 @@ # Windows specific return 0 + def os_rename(src, dst): """ Rename a file to a destination path and handles situations where the destination exists. @@ -117,6 +117,7 @@ # So no timeout on Windows for now. return sys.stdin.readline() + def lock_file(fd): """ Lock the file linked to the specified file descriptor. @@ -131,6 +132,7 @@ # Windows specific msvcrt.locking(fd, msvcrt.LK_NBLCK, 1) + def release_locked_file(fd, path): """ Remove, close, and release a lock file specified by its file descriptor and its path. @@ -164,11 +166,58 @@ finally: os.close(fd) + def compare_file_modes(mode1, mode2): """Return true if the two modes can be considered as equals for this platform""" - if 'fcntl' in sys.modules: + if os.name != 'nt': # Linux specific: standard compare return oct(stat.S_IMODE(mode1)) == oct(stat.S_IMODE(mode2)) # Windows specific: most of mode bits are ignored on Windows. Only check user R/W rights. return (stat.S_IMODE(mode1) & stat.S_IREAD == stat.S_IMODE(mode2) & stat.S_IREAD and stat.S_IMODE(mode1) & stat.S_IWRITE == stat.S_IMODE(mode2) & stat.S_IWRITE) + + +WINDOWS_DEFAULT_FOLDERS = { + 'config': 'C:\\Certbot', + 'work': 'C:\\Certbot\\lib', + 'logs': 'C:\\Certbot\\log', +} +LINUX_DEFAULT_FOLDERS = { + 'config': '/etc/letsencrypt', + 'work': '/var/lib/letsencrypt', + 'logs': '/var/log/letsencrypt', +} + + +def get_default_folder(folder_type): + """ + Return the relevant default folder for the current OS + + :param str folder_type: The type of folder to retrieve (config, work or logs) + + :returns: The relevant default folder. + :rtype: str + + """ + if os.name != 'nt': + # Linux specific + return LINUX_DEFAULT_FOLDERS[folder_type] + # Windows specific + return WINDOWS_DEFAULT_FOLDERS[folder_type] + + +def underscores_for_unsupported_characters_in_path(path): + # type: (str) -> str + """ + Replace unsupported characters in path for current OS by underscores. + :param str path: the path to normalize + :return: the normalized path + :rtype: str + """ + if os.name != 'nt': + # Linux specific + return path + + # Windows specific + drive, tail = os.path.splitdrive(path) + return drive + tail.replace(':', '_') diff -Nru python-certbot-0.28.0/certbot/configuration.py python-certbot-0.31.0/certbot/configuration.py --- python-certbot-0.28.0/certbot/configuration.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/configuration.py 2019-02-07 21:20:29.000000000 +0000 @@ -5,6 +5,7 @@ from six.moves.urllib import parse # pylint: disable=import-error import zope.interface +from certbot import compat from certbot import constants from certbot import errors from certbot import interfaces @@ -69,6 +70,7 @@ def accounts_dir_for_server_path(self, server_path): """Path to accounts directory based on server_path""" + server_path = compat.underscores_for_unsupported_characters_in_path(server_path) return os.path.join( self.namespace.config_dir, constants.ACCOUNTS_DIR, server_path) diff -Nru python-certbot-0.28.0/certbot/constants.py python-certbot-0.31.0/certbot/constants.py --- python-certbot-0.28.0/certbot/constants.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/constants.py 2019-02-07 21:20:29.000000000 +0000 @@ -4,7 +4,7 @@ import pkg_resources from acme import challenges - +from certbot import compat SETUPTOOLS_PLUGINS_ENTRY_POINT = "certbot.plugins" """Setuptools entry point group name for plugins.""" @@ -14,7 +14,7 @@ CLI_DEFAULTS = dict( config_files=[ - "/etc/letsencrypt/cli.ini", + os.path.join(compat.get_default_folder('config'), 'cli.ini'), # http://freedesktop.org/wiki/Software/xdg-user-dirs/ os.path.join(os.environ.get("XDG_CONFIG_HOME", "~/.config"), "letsencrypt", "cli.ini"), @@ -68,6 +68,9 @@ directory_hooks=True, reuse_key=False, disable_renew_updates=False, + random_sleep_on_renew=True, + eab_hmac_key=None, + eab_kid=None, # Subparsers num=None, @@ -85,9 +88,9 @@ auth_cert_path="./cert.pem", auth_chain_path="./chain.pem", key_path=None, - config_dir="/etc/letsencrypt", - work_dir="/var/lib/letsencrypt", - logs_dir="/var/log/letsencrypt", + config_dir=compat.get_default_folder('config'), + work_dir=compat.get_default_folder('work'), + logs_dir=compat.get_default_folder('logs'), server="https://acme-v02.api.letsencrypt.org/directory", # Plugins parsers @@ -143,7 +146,7 @@ """Defaults for renewer script.""" -ENHANCEMENTS = ["redirect", "ensure-http-header", "ocsp-stapling", "spdy"] +ENHANCEMENTS = ["redirect", "ensure-http-header", "ocsp-stapling"] """List of possible :class:`certbot.interfaces.IInstaller` enhancements. @@ -151,7 +154,6 @@ - redirect: None - ensure-http-header: name of header (i.e. Strict-Transport-Security) - ocsp-stapling: certificate chain file path -- spdy: TODO """ diff -Nru python-certbot-0.28.0/certbot/crypto_util.py python-certbot-0.31.0/certbot/crypto_util.py --- python-certbot-0.28.0/certbot/crypto_util.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/crypto_util.py 2019-02-07 21:20:29.000000000 +0000 @@ -458,7 +458,7 @@ :rtype: str """ sha256 = hashlib.sha256() - with open(filename, 'rU') as file_d: + with open(filename, 'r') as file_d: sha256.update(file_d.read().encode('UTF-8')) return sha256.hexdigest() diff -Nru python-certbot-0.28.0/certbot/display/ops.py python-certbot-0.31.0/certbot/display/ops.py --- python-certbot-0.28.0/certbot/display/ops.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/display/ops.py 2019-02-07 21:20:29.000000000 +0000 @@ -4,9 +4,11 @@ import zope.component +from certbot import compat from certbot import errors from certbot import interfaces from certbot import util + from certbot.display import util as display_util logger = logging.getLogger(__name__) @@ -33,7 +35,8 @@ unsafe_suggestion = ("\n\nIf you really want to skip this, you can run " "the client with --register-unsafely-without-email " "but make sure you then backup your account key from " - "/etc/letsencrypt/accounts\n\n") + "{0}\n\n".format(os.path.join( + compat.get_default_folder('config'), 'accounts'))) if optional: if invalid: msg += unsafe_suggestion diff -Nru python-certbot-0.28.0/certbot/__init__.py python-certbot-0.31.0/certbot/__init__.py --- python-certbot-0.28.0/certbot/__init__.py 2018-11-07 21:14:57.000000000 +0000 +++ python-certbot-0.31.0/certbot/__init__.py 2019-02-07 21:20:31.000000000 +0000 @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.28.0' +__version__ = '0.31.0' diff -Nru python-certbot-0.28.0/certbot/interfaces.py python-certbot-0.31.0/certbot/interfaces.py --- python-certbot-0.28.0/certbot/interfaces.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/interfaces.py 2019-02-07 21:20:29.000000000 +0000 @@ -522,56 +522,6 @@ """ -class IValidator(zope.interface.Interface): - """Configuration validator.""" - - def certificate(cert, name, alt_host=None, port=443): - """Verifies the certificate presented at name is cert - - :param OpenSSL.crypto.X509 cert: Expected certificate - :param str name: Server's domain name - :param bytes alt_host: Host to connect to instead of the IP - address of host - :param int port: Port to connect to - - :returns: True if the certificate was verified successfully - :rtype: bool - - """ - - def redirect(name, port=80, headers=None): - """Verify redirect to HTTPS - - :param str name: Server's domain name - :param int port: Port to connect to - :param dict headers: HTTP headers to include in request - - :returns: True if redirect is successfully enabled - :rtype: bool - - """ - - def hsts(name): - """Verify HSTS header is enabled - - :param str name: Server's domain name - - :returns: True if HSTS header is successfully enabled - :rtype: bool - - """ - - def ocsp_stapling(name): - """Verify ocsp stapling for domain - - :param str name: Server's domain name - - :returns: True if ocsp stapling is successfully enabled - :rtype: bool - - """ - - class IReporter(zope.interface.Interface): """Interface to collect and display information to the user.""" diff -Nru python-certbot-0.28.0/certbot/main.py python-certbot-0.31.0/certbot/main.py --- python-certbot-0.28.0/certbot/main.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/main.py 2019-02-07 21:20:29.000000000 +0000 @@ -548,7 +548,8 @@ attempt_deletion = config.delete_after_revoke if attempt_deletion is None: - msg = ("Would you like to delete the cert(s) you just revoked?") + msg = ("Would you like to delete the cert(s) you just revoked, along with all earlier and " + "later versions of the cert?") attempt_deletion = display.yesno(msg, yes_label="Yes (recommended)", no_label="No", force_interactive=True, default=True) @@ -652,7 +653,45 @@ def register(config, unused_plugins): - """Create or modify accounts on the server. + """Create accounts on the server. + + :param config: Configuration object + :type config: interfaces.IConfig + + :param unused_plugins: List of plugins (deprecated) + :type unused_plugins: `list` of `str` + + :returns: `None` or a string indicating and error + :rtype: None or str + + """ + # TODO: When `certbot register --update-registration` is fully deprecated, + # delete the true case of if block + if config.update_registration: + msg = ("Usage 'certbot register --update-registration' is deprecated.\n" + "Please use 'cerbot update_account [options]' instead.\n") + logger.warning(msg) + return update_account(config, unused_plugins) + + # Portion of _determine_account logic to see whether accounts already + # exist or not. + account_storage = account.AccountFileStorage(config) + accounts = account_storage.find_all() + + if len(accounts) > 0: + # TODO: add a flag to register a duplicate account (this will + # also require extending _determine_account's behavior + # or else extracting the registration code from there) + return ("There is an existing account; registration of a " + "duplicate account with this command is currently " + "unsupported.") + # _determine_account will register an account + _determine_account(config) + return + + +def update_account(config, unused_plugins): + """Modify accounts on the server. :param config: Configuration object :type config: interfaces.IConfig @@ -671,20 +710,6 @@ reporter_util = zope.component.getUtility(interfaces.IReporter) add_msg = lambda m: reporter_util.add_message(m, reporter_util.MEDIUM_PRIORITY) - # registering a new account - if not config.update_registration: - if len(accounts) > 0: - # TODO: add a flag to register a duplicate account (this will - # also require extending _determine_account's behavior - # or else extracting the registration code from there) - return ("There is an existing account; registration of a " - "duplicate account with this command is currently " - "unsupported.") - # _determine_account will register an account - _determine_account(config) - return - - # --update-registration if len(accounts) == 0: return "Could not find an existing account to update." if config.email is None: diff -Nru python-certbot-0.28.0/certbot/ocsp.py python-certbot-0.31.0/certbot/ocsp.py --- python-certbot-0.28.0/certbot/ocsp.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/ocsp.py 2019-02-07 21:20:29.000000000 +0000 @@ -114,7 +114,7 @@ logger.info("OCSP revocation warning: %s", warning) return True else: - logger.warn("Unable to properly parse OCSP output: %s\nstderr:%s", + logger.warning("Unable to properly parse OCSP output: %s\nstderr:%s", ocsp_output, ocsp_errors) return False diff -Nru python-certbot-0.28.0/certbot/plugins/dns_common_lexicon.py python-certbot-0.31.0/certbot/plugins/dns_common_lexicon.py --- python-certbot-0.28.0/certbot/plugins/dns_common_lexicon.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/plugins/dns_common_lexicon.py 2019-02-07 21:20:30.000000000 +0000 @@ -1,12 +1,22 @@ """Common code for DNS Authenticator Plugins built on Lexicon.""" - import logging from requests.exceptions import HTTPError, RequestException +from acme.magic_typing import Union, Dict, Any # pylint: disable=unused-import,no-name-in-module from certbot import errors from certbot.plugins import dns_common +# Lexicon is not declared as a dependency in Certbot itself, +# but in the Certbot plugins backed by Lexicon. +# So we catch import error here to allow this module to be +# always importable, even if it does not make sense to use it +# if Lexicon is not available, obviously. +try: + from lexicon.config import ConfigResolver +except ImportError: + ConfigResolver = None # type: ignore + logger = logging.getLogger(__name__) @@ -100,3 +110,28 @@ if not str(e).startswith('No domain found'): return errors.PluginError('Unexpected error determining zone identifier for {0}: {1}' .format(domain_name, e)) + + +def build_lexicon_config(lexicon_provider_name, lexicon_options, provider_options): + # type: (str, Dict, Dict) -> Union[ConfigResolver, Dict] + """ + Convenient function to build a Lexicon 2.x/3.x config object. + :param str lexicon_provider_name: the name of the lexicon provider to use + :param dict lexicon_options: options specific to lexicon + :param dict provider_options: options specific to provider + :return: configuration to apply to the provider + :rtype: ConfigurationResolver or dict + """ + config = {'provider_name': lexicon_provider_name} # type: Dict[str, Any] + config.update(lexicon_options) + if not ConfigResolver: + # Lexicon 2.x + config.update(provider_options) + else: + # Lexicon 3.x + provider_config = {} + provider_config.update(provider_options) + config[lexicon_provider_name] = provider_config + config = ConfigResolver().with_dict(config).with_env() + + return config diff -Nru python-certbot-0.28.0/certbot/plugins/enhancements_test.py python-certbot-0.31.0/certbot/plugins/enhancements_test.py --- python-certbot-0.28.0/certbot/plugins/enhancements_test.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/plugins/enhancements_test.py 2019-02-07 21:20:30.000000000 +0000 @@ -37,11 +37,11 @@ self.assertTrue([i for i in enabled if i["name"] == "somethingelse"]) def test_are_requested(self): - self.assertEquals( + self.assertEqual( len([i for i in enhancements.enabled_enhancements(self.config)]), 0) self.assertFalse(enhancements.are_requested(self.config)) self.config.auto_hsts = True - self.assertEquals( + self.assertEqual( len([i for i in enhancements.enabled_enhancements(self.config)]), 1) self.assertTrue(enhancements.are_requested(self.config)) @@ -57,7 +57,7 @@ lineage = "lineage" enhancements.enable(lineage, domains, self.mockinstaller, self.config) self.assertTrue(self.mockinstaller.enable_autohsts.called) - self.assertEquals(self.mockinstaller.enable_autohsts.call_args[0], + self.assertEqual(self.mockinstaller.enable_autohsts.call_args[0], (lineage, domains)) diff -Nru python-certbot-0.28.0/certbot/plugins/selection_test.py python-certbot-0.31.0/certbot/plugins/selection_test.py --- python-certbot-0.28.0/certbot/plugins/selection_test.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/plugins/selection_test.py 2019-02-07 21:20:30.000000000 +0000 @@ -197,7 +197,7 @@ def test_no_installer_defined(self): self.config.configurator = None - self.assertEquals(self._call(), None) + self.assertEqual(self._call(), None) def test_no_available_installers(self): self.config.configurator = "apache" diff -Nru python-certbot-0.28.0/certbot/plugins/standalone_test.py python-certbot-0.31.0/certbot/plugins/standalone_test.py --- python-certbot-0.28.0/certbot/plugins/standalone_test.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/plugins/standalone_test.py 2019-02-07 21:20:30.000000000 +0000 @@ -72,6 +72,8 @@ errors.StandaloneBindError, self.mgr.run, port, challenge_type=challenges.HTTP01) self.assertEqual(self.mgr.running(), {}) + some_server.close() + maybe_another_server.close() class SupportedChallengesActionTest(unittest.TestCase): diff -Nru python-certbot-0.28.0/certbot/renewal.py python-certbot-0.31.0/certbot/renewal.py --- python-certbot-0.28.0/certbot/renewal.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/renewal.py 2019-02-07 21:20:30.000000000 +0000 @@ -5,6 +5,9 @@ import logging import os import traceback +import sys +import time +import random import six import zope.component @@ -276,8 +279,10 @@ "Do not renew a valid cert with one from a staging server!" # Some lineages may have begun with --staging, but then had production certs # added to them + with open(lineage.cert) as the_file: + contents = the_file.read() latest_cert = OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, open(lineage.cert).read()) + OpenSSL.crypto.FILETYPE_PEM, contents) # all our test certs are from happy hacker fake CA, though maybe one day # we should test more methodically now_valid = "fake" not in repr(latest_cert.get_issuer()).lower() @@ -370,7 +375,7 @@ disp.notification("\n".join(out), wrap=False) -def handle_renewal_request(config): +def handle_renewal_request(config): # pylint: disable=too-many-locals,too-many-branches,too-many-statements """Examine each lineage; renew if due and report results""" # This is trivially False if config.domains is empty @@ -394,6 +399,14 @@ renew_failures = [] renew_skipped = [] parse_failures = [] + + # Noninteractive renewals include a random delay in order to spread + # out the load on the certificate authority servers, even if many + # users all pick the same time for renewals. This delay precedes + # running any hooks, so that side effects of the hooks (such as + # shutting down a web service) aren't prolonged unnecessarily. + apply_random_sleep = not sys.stdin.isatty() and config.random_sleep_on_renew + for renewal_file in conf_files: disp = zope.component.getUtility(interfaces.IDisplay) disp.notification("Processing " + renewal_file, pause=False) @@ -422,6 +435,15 @@ from certbot import main plugins = plugins_disco.PluginsRegistry.find_all() if should_renew(lineage_config, renewal_candidate): + # Apply random sleep upon first renewal if needed + if apply_random_sleep: + sleep_time = random.randint(1, 60 * 8) + logger.info("Non-interactive renewal: random delay of %s seconds", + sleep_time) + time.sleep(sleep_time) + # We will sleep only once this day, folks. + apply_random_sleep = False + # domains have been restored into lineage_config by reconstitute # but they're unnecessary anyway because renew_cert here # will just grab them from the certificate diff -Nru python-certbot-0.28.0/certbot/storage.py python-certbot-0.31.0/certbot/storage.py --- python-certbot-0.28.0/certbot/storage.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/storage.py 2019-02-07 21:20:30.000000000 +0000 @@ -29,6 +29,7 @@ ALL_FOUR = ("cert", "privkey", "chain", "fullchain") README = "README" CURRENT_VERSION = util.get_strict_version(certbot.__version__) +BASE_PRIVKEY_MODE = 0o600 def renewal_conf_files(config): @@ -40,7 +41,9 @@ :rtype: `list` of `str` """ - return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf")) + result = glob.glob(os.path.join(config.renewal_configs_dir, "*.conf")) + result.sort() + return result def renewal_file_for_certname(config, certname): """Return /path/to/certname.conf in the renewal conf directory""" @@ -792,7 +795,7 @@ May need to recover from rare interrupted / crashed states.""" if self.has_pending_deployment(): - logger.warn("Found a new cert /archive/ that was not linked to in /live/; " + logger.warning("Found a new cert /archive/ that was not linked to in /live/; " "fixing...") self.update_all_links_to(self.latest_common_version()) return False @@ -876,45 +879,6 @@ with open(target) as f: return crypto_util.get_names_from_cert(f.read()) - def autodeployment_is_enabled(self): - """Is automatic deployment enabled for this cert? - - If autodeploy is not specified, defaults to True. - - :returns: True if automatic deployment is enabled - :rtype: bool - - """ - return ("autodeploy" not in self.configuration or - self.configuration.as_bool("autodeploy")) - - def should_autodeploy(self, interactive=False): - """Should this lineage now automatically deploy a newer version? - - This is a policy question and does not only depend on whether - there is a newer version of the cert. (This considers whether - autodeployment is enabled, whether a relevant newer version - exists, and whether the time interval for autodeployment has - been reached.) - - :param bool interactive: set to True to examine the question - regardless of whether the renewal configuration allows - automated deployment (for interactive use). Default False. - - :returns: whether the lineage now ought to autodeploy an - existing newer cert version - :rtype: bool - - """ - if interactive or self.autodeployment_is_enabled(): - if self.has_pending_deployment(): - interval = self.configuration.get("deploy_before_expiry", - "5 days") - now = pytz.UTC.fromutc(datetime.datetime.utcnow()) - if self.target_expiry < add_time_interval(now, interval): - return True - return False - def ocsp_revoked(self, version=None): # pylint: disable=no-self-use,unused-argument """Is the specified cert version revoked according to OCSP? @@ -1035,9 +999,11 @@ archive = full_archive_path(None, cli_config, lineagename) live_dir = _full_live_path(cli_config, lineagename) if os.path.exists(archive): + config_file.close() raise errors.CertStorageError( "archive directory exists for " + lineagename) if os.path.exists(live_dir): + config_file.close() raise errors.CertStorageError( "live directory exists for " + lineagename) os.mkdir(archive) @@ -1048,13 +1014,14 @@ # Put the data into the appropriate files on disk target = dict([(kind, os.path.join(live_dir, kind + ".pem")) for kind in ALL_FOUR]) + archive_target = dict([(kind, os.path.join(archive, kind + "1.pem")) + for kind in ALL_FOUR]) for kind in ALL_FOUR: - os.symlink(os.path.join(_relpath_from_file(archive, target[kind]), kind + "1.pem"), - target[kind]) + os.symlink(_relpath_from_file(archive_target[kind], target[kind]), target[kind]) with open(target["cert"], "wb") as f: logger.debug("Writing certificate to %s.", target["cert"]) f.write(cert) - with open(target["privkey"], "wb") as f: + with util.safe_open(archive_target["privkey"], "wb", chmod=BASE_PRIVKEY_MODE) as f: logger.debug("Writing private key to %s.", target["privkey"]) f.write(privkey) # XXX: Let's make sure to get the file permissions right here @@ -1118,14 +1085,15 @@ os.path.join(self.archive_dir, "{0}{1}.pem".format(kind, target_version))) for kind in ALL_FOUR]) + old_privkey = os.path.join( + self.archive_dir, "privkey{0}.pem".format(prior_version)) + # Distinguish the cases where the privkey has changed and where it # has not changed (in the latter case, making an appropriate symlink # to an earlier privkey version) if new_privkey is None: # The behavior below keeps the prior key by creating a new # symlink to the old key or the target of the old key symlink. - old_privkey = os.path.join( - self.archive_dir, "privkey{0}.pem".format(prior_version)) if os.path.islink(old_privkey): old_privkey = os.readlink(old_privkey) else: @@ -1133,9 +1101,16 @@ logger.debug("Writing symlink to old private key, %s.", old_privkey) os.symlink(old_privkey, target["privkey"]) else: - with open(target["privkey"], "wb") as f: + with util.safe_open(target["privkey"], "wb", chmod=BASE_PRIVKEY_MODE) as f: logger.debug("Writing new private key to %s.", target["privkey"]) f.write(new_privkey) + # Preserve gid and (mode & 074) from previous privkey in this lineage. + old_mode = stat.S_IMODE(os.stat(old_privkey).st_mode) & \ + (stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP | \ + stat.S_IROTH) + mode = BASE_PRIVKEY_MODE | old_mode + os.chown(target["privkey"], -1, os.stat(old_privkey).st_gid) + os.chmod(target["privkey"], mode) # Save everything else with open(target["cert"], "wb") as f: diff -Nru python-certbot-0.28.0/certbot/tests/account_test.py python-certbot-0.31.0/certbot/tests/account_test.py --- python-certbot-0.28.0/certbot/tests/account_test.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/tests/account_test.py 2019-02-07 21:20:30.000000000 +0000 @@ -12,6 +12,7 @@ from acme import messages +from certbot import compat from certbot import errors import certbot.tests.util as test_util @@ -114,7 +115,8 @@ self.mock_client.directory.new_authz = new_authzr_uri def test_init_creates_dir(self): - self.assertTrue(os.path.isdir(self.config.accounts_dir)) + self.assertTrue(os.path.isdir( + compat.underscores_for_unsupported_characters_in_path(self.config.accounts_dir))) @test_util.broken_on_windows def test_save_and_restore(self): diff -Nru python-certbot-0.28.0/certbot/tests/auth_handler_test.py python-certbot-0.31.0/certbot/tests/auth_handler_test.py --- python-certbot-0.28.0/certbot/tests/auth_handler_test.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/tests/auth_handler_test.py 2019-02-07 21:20:30.000000000 +0000 @@ -57,7 +57,7 @@ errors.Error, self.handler._challenge_factory, authzr, [0]) -class HandleAuthorizationsTest(unittest.TestCase): +class HandleAuthorizationsTest(unittest.TestCase): # pylint: disable=too-many-public-methods """handle_authorizations test. This tests everything except for all functions under _poll_challenges. @@ -316,6 +316,24 @@ self.assertEqual( self.mock_auth.cleanup.call_args[0][0][0].typ, "tls-sni-01") + def test_validated_challenge_not_rerun(self): + # With pending challenge, we expect the challenge to be tried, and fail. + authzr = acme_util.gen_authzr( + messages.STATUS_PENDING, "0", + [acme_util.HTTP01], + [messages.STATUS_PENDING], False) + mock_order = mock.MagicMock(authorizations=[authzr]) + self.assertRaises( + errors.AuthorizationError, self.handler.handle_authorizations, mock_order) + + # With validated challenge; we expect the challenge not be tried again, and succeed. + authzr = acme_util.gen_authzr( + messages.STATUS_VALID, "0", + [acme_util.HTTP01], + [messages.STATUS_VALID], False) + mock_order = mock.MagicMock(authorizations=[authzr]) + self.handler.handle_authorizations(mock_order) + def _validate_all(self, aauthzrs, unused_1, unused_2): for i, aauthzr in enumerate(aauthzrs): azr = aauthzr.authzr diff -Nru python-certbot-0.28.0/certbot/tests/cert_manager_test.py python-certbot-0.31.0/certbot/tests/cert_manager_test.py --- python-certbot-0.28.0/certbot/tests/cert_manager_test.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/tests/cert_manager_test.py 2019-02-07 21:20:30.000000000 +0000 @@ -39,9 +39,8 @@ # We also create a file that isn't a renewal config in the same # location to test that logic that reads in all-and-only renewal # configs will ignore it and NOT attempt to parse it. - junk = open(os.path.join(self.config.renewal_configs_dir, "IGNORE.THIS"), "w") - junk.write("This file should be ignored!") - junk.close() + with open(os.path.join(self.config.renewal_configs_dir, "IGNORE.THIS"), "w") as junk: + junk.write("This file should be ignored!") def _set_up_config(self, domain, custom_archive): # TODO: maybe provide NamespaceConfig.make_dirs? @@ -589,7 +588,7 @@ from certbot import cert_manager prompt = "Which certificate would you" self.mock_get_utility().menu.return_value = (display_util.OK, 0) - self.assertEquals( + self.assertEqual( cert_manager.get_certnames( self.config, "verb", allow_multiple=False), ['example.com']) self.assertTrue( @@ -603,11 +602,11 @@ from certbot import cert_manager prompt = "custom prompt" self.mock_get_utility().menu.return_value = (display_util.OK, 0) - self.assertEquals( + self.assertEqual( cert_manager.get_certnames( self.config, "verb", allow_multiple=False, custom_prompt=prompt), ['example.com']) - self.assertEquals(self.mock_get_utility().menu.call_args[0][0], + self.assertEqual(self.mock_get_utility().menu.call_args[0][0], prompt) @mock.patch('certbot.storage.renewal_conf_files') @@ -631,7 +630,7 @@ prompt = "Which certificate(s) would you" self.mock_get_utility().checklist.return_value = (display_util.OK, ['example.com']) - self.assertEquals( + self.assertEqual( cert_manager.get_certnames( self.config, "verb", allow_multiple=True), ['example.com']) self.assertTrue( @@ -646,11 +645,11 @@ prompt = "custom prompt" self.mock_get_utility().checklist.return_value = (display_util.OK, ['example.com']) - self.assertEquals( + self.assertEqual( cert_manager.get_certnames( self.config, "verb", allow_multiple=True, custom_prompt=prompt), ['example.com']) - self.assertEquals( + self.assertEqual( self.mock_get_utility().checklist.call_args[0][0], prompt) diff -Nru python-certbot-0.28.0/certbot/tests/client_test.py python-certbot-0.31.0/certbot/tests/client_test.py --- python-certbot-0.28.0/certbot/tests/client_test.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/tests/client_test.py 2019-02-07 21:20:30.000000000 +0000 @@ -13,6 +13,8 @@ import certbot.tests.util as test_util +from josepy import interfaces + KEY = test_util.load_vector("rsa512_key.pem") CSR_SAN = test_util.load_vector("csr-san_512.pem") @@ -64,9 +66,28 @@ tos_cb = mock.MagicMock() return register(self.config, self.account_storage, tos_cb) + @staticmethod + def _public_key_mock(): + m = mock.Mock(__class__=interfaces.JSONDeSerializable) + m.to_partial_json.return_value = '{"a": 1}' + return m + + @staticmethod + def _new_acct_dir_mock(): + return "/acme/new-account" + + @staticmethod + def _true_mock(): + return True + + @staticmethod + def _false_mock(): + return False + def test_no_tos(self): with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as mock_client: mock_client.new_account_and_tos().terms_of_service = "http://tos" + mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot.eff.handle_subscription") as mock_handle: with mock.patch("certbot.account.report_new_account"): mock_client().new_account_and_tos.side_effect = errors.Error @@ -78,7 +99,8 @@ self.assertTrue(mock_handle.called) def test_it(self): - with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2"): + with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as mock_client: + mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot.account.report_new_account"): with mock.patch("certbot.eff.handle_subscription"): self._call() @@ -91,6 +113,7 @@ msg = "DNS problem: NXDOMAIN looking up MX for example.com" mx_err = messages.Error.with_code('invalidContact', detail=msg) with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as mock_client: + mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot.eff.handle_subscription") as mock_handle: mock_client().new_account_and_tos.side_effect = [mx_err, mock.MagicMock()] self._call() @@ -104,6 +127,7 @@ msg = "DNS problem: NXDOMAIN looking up MX for example.com" mx_err = messages.Error.with_code('invalidContact', detail=msg) with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as mock_client: + mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot.eff.handle_subscription"): mock_client().new_account_and_tos.side_effect = [mx_err, mock.MagicMock()] self.assertRaises(errors.Error, self._call) @@ -115,7 +139,8 @@ @mock.patch("certbot.client.logger") def test_without_email(self, mock_logger): with mock.patch("certbot.eff.handle_subscription") as mock_handle: - with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2"): + with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as mock_clnt: + mock_clnt().external_account_required.side_effect = self._false_mock with mock.patch("certbot.account.report_new_account"): self.config.email = None self.config.register_unsafely_without_email = True @@ -129,6 +154,7 @@ def test_dry_run_no_staging_account(self, _rep, mock_get_email): """Tests dry-run for no staging account, expect account created with no email""" with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as mock_client: + mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot.eff.handle_subscription"): with mock.patch("certbot.account.report_new_account"): self.config.dry_run = True @@ -138,11 +164,53 @@ # check Certbot created an account with no email. Contact should return empty self.assertFalse(mock_client().new_account_and_tos.call_args[0][0].contact) + def test_with_eab_arguments(self): + with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as mock_client: + mock_client().client.directory.__getitem__ = mock.Mock( + side_effect=self._new_acct_dir_mock + ) + mock_client().external_account_required.side_effect = self._false_mock + with mock.patch("certbot.eff.handle_subscription"): + target = "certbot.client.messages.ExternalAccountBinding.from_data" + with mock.patch(target) as mock_eab_from_data: + self.config.eab_kid = "test-kid" + self.config.eab_hmac_key = "J2OAqW4MHXsrHVa_PVg0Y-L_R4SYw0_aL1le6mfblbE" + self._call() + + self.assertTrue(mock_eab_from_data.called) + + def test_without_eab_arguments(self): + with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as mock_client: + mock_client().external_account_required.side_effect = self._false_mock + with mock.patch("certbot.eff.handle_subscription"): + target = "certbot.client.messages.ExternalAccountBinding.from_data" + with mock.patch(target) as mock_eab_from_data: + self.config.eab_kid = None + self.config.eab_hmac_key = None + self._call() + + self.assertFalse(mock_eab_from_data.called) + + def test_external_account_required_without_eab_arguments(self): + with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as mock_client: + mock_client().client.net.key.public_key = mock.Mock(side_effect=self._public_key_mock) + mock_client().external_account_required.side_effect = self._true_mock + with mock.patch("certbot.eff.handle_subscription"): + with mock.patch("certbot.client.messages.ExternalAccountBinding.from_data"): + self.config.eab_kid = None + self.config.eab_hmac_key = None + + self.assertRaises(errors.Error, self._call) + def test_unsupported_error(self): from acme import messages msg = "Test" mx_err = messages.Error(detail=msg, typ="malformed", title="title") with mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as mock_client: + mock_client().client.directory.__getitem__ = mock.Mock( + side_effect=self._new_acct_dir_mock + ) + mock_client().external_account_required.side_effect = self._false_mock with mock.patch("certbot.eff.handle_subscription") as mock_handle: mock_client().new_account_and_tos.side_effect = [mx_err, mock.MagicMock()] self.assertRaises(messages.Error, self._call) @@ -487,7 +555,7 @@ self.config.hsts = True self._test_with_already_existing() self.assertTrue(mock_log.warning.called) - self.assertEquals(mock_log.warning.call_args[0][1], + self.assertEqual(mock_log.warning.call_args[0][1], 'Strict-Transport-Security') @mock.patch("certbot.client.logger") @@ -495,7 +563,7 @@ self.config.redirect = True self._test_with_already_existing() self.assertTrue(mock_log.warning.called) - self.assertEquals(mock_log.warning.call_args[0][1], + self.assertEqual(mock_log.warning.call_args[0][1], 'redirect') def test_no_ask_hsts(self): diff -Nru python-certbot-0.28.0/certbot/tests/cli_test.py python-certbot-0.31.0/certbot/tests/cli_test.py --- python-certbot-0.28.0/certbot/tests/cli_test.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/tests/cli_test.py 2019-02-07 21:20:30.000000000 +0000 @@ -4,6 +4,7 @@ import os import tempfile import copy +import sys import mock import six @@ -41,6 +42,15 @@ self.assertEqual(contents, test_contents) +class FlagDefaultTest(unittest.TestCase): + """Tests cli.flag_default""" + + def test_linux_directories(self): + if 'fcntl' in sys.modules: + self.assertEqual(cli.flag_default('config_dir'), '/etc/letsencrypt') + self.assertEqual(cli.flag_default('work_dir'), '/var/lib/letsencrypt') + self.assertEqual(cli.flag_default('logs_dir'), '/var/log/letsencrypt') + class ParseTest(unittest.TestCase): # pylint: disable=too-many-public-methods '''Test the cli args entrypoint''' @@ -431,6 +441,11 @@ self.assertRaises(errors.Error, self.parse, "--allow-subset-of-names -d *.example.org".split()) + def test_route53_no_revert(self): + for help_flag in ['-h', '--help']: + for topic in ['all', 'plugins', 'dns-route53']: + self.assertFalse('certbot-route53:auth' in self._help_output([help_flag, topic])) + class DefaultTest(unittest.TestCase): """Tests for certbot.cli._Default.""" diff -Nru python-certbot-0.28.0/certbot/tests/configuration_test.py python-certbot-0.31.0/certbot/tests/configuration_test.py --- python-certbot-0.28.0/certbot/tests/configuration_test.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/tests/configuration_test.py 2019-02-07 21:20:30.000000000 +0000 @@ -4,6 +4,7 @@ import mock +from certbot import compat from certbot import constants from certbot import errors @@ -47,9 +48,11 @@ mock_constants.KEY_DIR = 'keys' mock_constants.TEMP_CHECKPOINT_DIR = 't' + ref_path = compat.underscores_for_unsupported_characters_in_path( + 'acc/acme-server.org:443/new') self.assertEqual( os.path.normpath(self.config.accounts_dir), - os.path.normpath(os.path.join(self.config.config_dir, 'acc/acme-server.org:443/new'))) + os.path.normpath(os.path.join(self.config.config_dir, ref_path))) self.assertEqual( os.path.normpath(self.config.backup_dir), os.path.normpath(os.path.join(self.config.work_dir, 'backups'))) diff -Nru python-certbot-0.28.0/certbot/tests/display/ops_test.py python-certbot-0.31.0/certbot/tests/display/ops_test.py --- python-certbot-0.28.0/certbot/tests/display/ops_test.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/tests/display/ops_test.py 2019-02-07 21:20:30.000000000 +0000 @@ -502,9 +502,9 @@ items = ["first", "second", "third"] mock_util().checklist.return_value = (display_util.OK, [items[2]]) result = self._call(items, None) - self.assertEquals(result, [items[2]]) + self.assertEqual(result, [items[2]]) self.assertTrue(mock_util().checklist.called) - self.assertEquals(mock_util().checklist.call_args[0][0], None) + self.assertEqual(mock_util().checklist.call_args[0][0], None) @test_util.patch_get_utility("certbot.display.ops.z_util") def test_choose_names_success_question(self, mock_util): @@ -512,9 +512,9 @@ question = "Which one?" mock_util().checklist.return_value = (display_util.OK, [items[1]]) result = self._call(items, question) - self.assertEquals(result, [items[1]]) + self.assertEqual(result, [items[1]]) self.assertTrue(mock_util().checklist.called) - self.assertEquals(mock_util().checklist.call_args[0][0], question) + self.assertEqual(mock_util().checklist.call_args[0][0], question) @test_util.patch_get_utility("certbot.display.ops.z_util") def test_choose_names_user_cancel(self, mock_util): @@ -522,9 +522,9 @@ question = "Want to cancel?" mock_util().checklist.return_value = (display_util.CANCEL, []) result = self._call(items, question) - self.assertEquals(result, []) + self.assertEqual(result, []) self.assertTrue(mock_util().checklist.called) - self.assertEquals(mock_util().checklist.call_args[0][0], question) + self.assertEqual(mock_util().checklist.call_args[0][0], question) if __name__ == "__main__": diff -Nru python-certbot-0.28.0/certbot/tests/display/util_test.py python-certbot-0.31.0/certbot/tests/display/util_test.py --- python-certbot-0.28.0/certbot/tests/display/util_test.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/tests/display/util_test.py 2019-02-07 21:20:30.000000000 +0000 @@ -49,6 +49,7 @@ stdin.listen(1) with mock.patch("certbot.display.util.sys.stdin", stdin): self.assertRaises(errors.Error, self._call, timeout=0.001) + stdin.close() class FileOutputDisplayTest(unittest.TestCase): @@ -314,7 +315,11 @@ # Every IDisplay method implemented by FileDisplay must take # force_interactive to prevent workflow regressions. for name in interfaces.IDisplay.names(): # pylint: disable=no-member - arg_spec = inspect.getargspec(getattr(self.displayer, name)) + if six.PY2: + getargspec = inspect.getargspec # pylint: disable=no-member + else: + getargspec = inspect.getfullargspec # pylint: disable=no-member + arg_spec = getargspec(getattr(self.displayer, name)) self.assertTrue("force_interactive" in arg_spec.args) @@ -371,7 +376,12 @@ for name in interfaces.IDisplay.names(): # pylint: disable=no-member method = getattr(self.displayer, name) # asserts method accepts arbitrary keyword arguments - self.assertFalse(inspect.getargspec(method).keywords is None) + if six.PY2: + result = inspect.getargspec(method).keywords # pylint: disable=no-member + self.assertFalse(result is None) + else: + result = inspect.getfullargspec(method).varkw # pylint: disable=no-member + self.assertFalse(result is None) class SeparateListInputTest(unittest.TestCase): diff -Nru python-certbot-0.28.0/certbot/tests/log_test.py python-certbot-0.31.0/certbot/tests/log_test.py --- python-certbot-0.28.0/certbot/tests/log_test.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/tests/log_test.py 2019-02-07 21:20:30.000000000 +0000 @@ -86,6 +86,7 @@ self.memory_handler.close() self.stream_handler.close() self.temp_handler.close() + self.devnull.close() super(PostArgParseSetupTest, self).tearDown() def test_common(self): diff -Nru python-certbot-0.28.0/certbot/tests/main_test.py python-certbot-0.31.0/certbot/tests/main_test.py --- python-certbot-0.28.0/certbot/tests/main_test.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/tests/main_test.py 2019-02-07 21:20:30.000000000 +0000 @@ -520,6 +520,8 @@ '--work-dir', self.config.work_dir, '--logs-dir', self.config.logs_dir, '--text'] + self.mock_sleep = mock.patch('time.sleep').start() + def tearDown(self): # Reset globals in cli reload_module(cli) @@ -944,8 +946,8 @@ @mock.patch('certbot.crypto_util.notAfter') @test_util.patch_get_utility() def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter): - cert_path = '/etc/letsencrypt/live/foo.bar' - key_path = '/etc/letsencrypt/live/baz.qux' + cert_path = os.path.normpath(os.path.join(self.config.config_dir, 'live/foo.bar')) + key_path = os.path.normpath(os.path.join(self.config.config_dir, 'live/baz.qux')) date = '1970-01-01' mock_notAfter().date.return_value = date @@ -975,7 +977,8 @@ reuse_key=False): # pylint: disable=too-many-locals,too-many-arguments,too-many-branches cert_path = test_util.vector_path('cert_512.pem') - chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem' + chain_path = os.path.normpath(os.path.join(self.config.config_dir, + 'live/foo.bar/fullchain.pem')) mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path, cert_path=cert_path, fullchain_path=chain_path) mock_lineage.should_autorenew.return_value = due_for_renewal @@ -1092,6 +1095,26 @@ args = ["renew", "--reuse-key"] self._test_renewal_common(True, [], args=args, should_renew=True, reuse_key=True) + @mock.patch('sys.stdin') + def test_noninteractive_renewal_delay(self, stdin): + stdin.isatty.return_value = False + test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf') + args = ["renew", "--dry-run", "-tvv"] + self._test_renewal_common(True, [], args=args, should_renew=True) + self.assertEqual(self.mock_sleep.call_count, 1) + # in main.py: + # sleep_time = random.randint(1, 60*8) + sleep_call_arg = self.mock_sleep.call_args[0][0] + self.assertTrue(1 <= sleep_call_arg <= 60*8) + + @mock.patch('sys.stdin') + def test_interactive_no_renewal_delay(self, stdin): + stdin.isatty.return_value = True + test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf') + args = ["renew", "--dry-run", "-tvv"] + self._test_renewal_common(True, [], args=args, should_renew=True) + self.assertEqual(self.mock_sleep.call_count, 0) + @mock.patch('certbot.renewal.should_renew') def test_renew_skips_recent_certs(self, should_renew): should_renew.return_value = False @@ -1375,7 +1398,20 @@ x = self._call_no_clientmock(["register", "--email", "user@example.org"]) self.assertTrue("There is an existing account" in x[0]) - def test_update_registration_no_existing_accounts(self): + def test_update_account_no_existing_accounts(self): + # with mock.patch('certbot.main.client') as mocked_client: + with mock.patch('certbot.main.account') as mocked_account: + mocked_storage = mock.MagicMock() + mocked_account.AccountFileStorage.return_value = mocked_storage + mocked_storage.find_all.return_value = [] + x = self._call_no_clientmock( + ["update_account", "--email", + "user@example.org"]) + self.assertTrue("Could not find an existing account" in x[0]) + + # TODO: When `certbot register --update-registration` is fully deprecated, + # delete the following test + def test_update_registration_no_existing_accounts_deprecated(self): # with mock.patch('certbot.main.client') as mocked_client: with mock.patch('certbot.main.account') as mocked_account: mocked_storage = mock.MagicMock() @@ -1386,7 +1422,9 @@ "user@example.org"]) self.assertTrue("Could not find an existing account" in x[0]) - def test_update_registration_unsafely(self): + # TODO: When `certbot register --update-registration` is fully deprecated, + # delete the following test + def test_update_registration_unsafely_deprecated(self): # This test will become obsolete when register --update-registration # supports removing an e-mail address from the account with mock.patch('certbot.main.account') as mocked_account: @@ -1400,7 +1438,39 @@ @mock.patch('certbot.main.display_ops.get_email') @test_util.patch_get_utility() - def test_update_registration_with_email(self, mock_utility, mock_email): + def test_update_account_with_email(self, mock_utility, mock_email): + email = "user@example.com" + mock_email.return_value = email + with mock.patch('certbot.eff.handle_subscription') as mock_handle: + with mock.patch('certbot.main._determine_account') as mocked_det: + with mock.patch('certbot.main.account') as mocked_account: + with mock.patch('certbot.main.client') as mocked_client: + mocked_storage = mock.MagicMock() + mocked_account.AccountFileStorage.return_value = mocked_storage + mocked_storage.find_all.return_value = ["an account"] + mocked_det.return_value = (mock.MagicMock(), "foo") + cb_client = mock.MagicMock() + mocked_client.Client.return_value = cb_client + x = self._call_no_clientmock( + ["update_account"]) + # When registration change succeeds, the return value + # of register() is None + self.assertTrue(x[0] is None) + # and we got supposedly did update the registration from + # the server + self.assertTrue( + cb_client.acme.update_registration.called) + # and we saved the updated registration on disk + self.assertTrue(mocked_storage.save_regr.called) + self.assertTrue( + email in mock_utility().add_message.call_args[0][0]) + self.assertTrue(mock_handle.called) + + # TODO: When `certbot register --update-registration` is fully deprecated, + # delete the following test + @mock.patch('certbot.main.display_ops.get_email') + @test_util.patch_get_utility() + def test_update_registration_with_email_deprecated(self, mock_utility, mock_email): email = "user@example.com" mock_email.return_value = email with mock.patch('certbot.eff.handle_subscription') as mock_handle: @@ -1652,7 +1722,7 @@ mock_lineage.return_value = mock.MagicMock(chain_path="/tmp/nonexistent") self._call(['enhance', '--auto-hsts']) self.assertTrue(self.mockinstaller.enable_autohsts.called) - self.assertEquals(self.mockinstaller.enable_autohsts.call_args[0][1], + self.assertEqual(self.mockinstaller.enable_autohsts.call_args[0][1], ["example.com", "another.tld"]) @mock.patch('certbot.cert_manager.lineage_for_certname') diff -Nru python-certbot-0.28.0/certbot/tests/ocsp_test.py python-certbot-0.31.0/certbot/tests/ocsp_test.py --- python-certbot-0.28.0/certbot/tests/ocsp_test.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/tests/ocsp_test.py 2019-02-07 21:20:30.000000000 +0000 @@ -96,15 +96,15 @@ self.assertEqual(ocsp._translate_ocsp_query(*openssl_happy), False) self.assertEqual(ocsp._translate_ocsp_query(*openssl_confused), False) self.assertEqual(mock_log.debug.call_count, 1) - self.assertEqual(mock_log.warn.call_count, 0) + self.assertEqual(mock_log.warning.call_count, 0) mock_log.debug.call_count = 0 self.assertEqual(ocsp._translate_ocsp_query(*openssl_unknown), False) self.assertEqual(mock_log.debug.call_count, 1) - self.assertEqual(mock_log.warn.call_count, 0) + self.assertEqual(mock_log.warning.call_count, 0) self.assertEqual(ocsp._translate_ocsp_query(*openssl_expired_ocsp), False) self.assertEqual(mock_log.debug.call_count, 2) self.assertEqual(ocsp._translate_ocsp_query(*openssl_broken), False) - self.assertEqual(mock_log.warn.call_count, 1) + self.assertEqual(mock_log.warning.call_count, 1) mock_log.info.call_count = 0 self.assertEqual(ocsp._translate_ocsp_query(*openssl_revoked), True) self.assertEqual(mock_log.info.call_count, 0) diff -Nru python-certbot-0.28.0/certbot/tests/renewupdater_test.py python-certbot-0.31.0/certbot/tests/renewupdater_test.py --- python-certbot-0.28.0/certbot/tests/renewupdater_test.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/tests/renewupdater_test.py 2019-02-07 21:20:30.000000000 +0000 @@ -53,7 +53,7 @@ self.config.dry_run = True updater.run_generic_updaters(self.config, None, None) self.assertTrue(mock_log.called) - self.assertEquals(mock_log.call_args[0][0], + self.assertEqual(mock_log.call_args[0][0], "Skipping updaters in dry-run mode.") @mock.patch("certbot.updater.logger.debug") @@ -61,7 +61,7 @@ self.config.dry_run = True updater.run_renewal_deployer(self.config, None, None) self.assertTrue(mock_log.called) - self.assertEquals(mock_log.call_args[0][0], + self.assertEqual(mock_log.call_args[0][0], "Skipping renewal deployer in dry-run mode.") @mock.patch('certbot.plugins.selection.get_unprepared_installer') diff -Nru python-certbot-0.28.0/certbot/tests/storage_test.py python-certbot-0.31.0/certbot/tests/storage_test.py --- python-certbot-0.28.0/certbot/tests/storage_test.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/tests/storage_test.py 2019-02-07 21:20:30.000000000 +0000 @@ -13,6 +13,7 @@ import certbot from certbot import cli +from certbot import compat from certbot import errors from certbot.storage import ALL_FOUR @@ -73,9 +74,8 @@ # We also create a file that isn't a renewal config in the same # location to test that logic that reads in all-and-only renewal # configs will ignore it and NOT attempt to parse it. - junk = open(os.path.join(self.config.config_dir, "renewal", "IGNORE.THIS"), "w") - junk.write("This file should be ignored!") - junk.close() + with open(os.path.join(self.config.config_dir, "renewal", "IGNORE.THIS"), "w") as junk: + junk.write("This file should be ignored!") self.defaults = configobj.ConfigObj() @@ -92,6 +92,8 @@ link) with open(link, "wb") as f: f.write(kind.encode('ascii') if value is None else value) + if kind == "privkey": + os.chmod(link, 0o600) def _write_out_ex_kinds(self): for kind in ALL_FOUR: @@ -264,12 +266,12 @@ mock_has_pending.return_value = False self.assertEqual(self.test_rc.ensure_deployed(), True) self.assertEqual(mock_update.call_count, 0) - self.assertEqual(mock_logger.warn.call_count, 0) + self.assertEqual(mock_logger.warning.call_count, 0) mock_has_pending.return_value = True self.assertEqual(self.test_rc.ensure_deployed(), False) self.assertEqual(mock_update.call_count, 1) - self.assertEqual(mock_logger.warn.call_count, 1) + self.assertEqual(mock_logger.warning.call_count, 1) def test_update_link_to(self): @@ -386,8 +388,7 @@ @mock.patch("certbot.storage.cli") @mock.patch("certbot.storage.datetime") def test_time_interval_judgments(self, mock_datetime, mock_cli): - """Test should_autodeploy() and should_autorenew() on the basis - of expiry time windows.""" + """Test should_autorenew() on the basis of expiry time windows.""" test_cert = test_util.load_vector("cert_512.pem") self._write_out_ex_kinds() @@ -428,31 +429,8 @@ mock_datetime.datetime.utcnow.return_value = sometime self.test_rc.configuration["deploy_before_expiry"] = interval self.test_rc.configuration["renew_before_expiry"] = interval - self.assertEqual(self.test_rc.should_autodeploy(), result) self.assertEqual(self.test_rc.should_autorenew(), result) - def test_autodeployment_is_enabled(self): - self.assertTrue(self.test_rc.autodeployment_is_enabled()) - self.test_rc.configuration["autodeploy"] = "1" - self.assertTrue(self.test_rc.autodeployment_is_enabled()) - - self.test_rc.configuration["autodeploy"] = "0" - self.assertFalse(self.test_rc.autodeployment_is_enabled()) - - def test_should_autodeploy(self): - """Test should_autodeploy() on the basis of reasons other than - expiry time window.""" - # pylint: disable=too-many-statements - # Autodeployment turned off - self.test_rc.configuration["autodeploy"] = "0" - self.assertFalse(self.test_rc.should_autodeploy()) - self.test_rc.configuration["autodeploy"] = "1" - # No pending deployment - for ver in six.moves.range(1, 6): - for kind in ALL_FOUR: - self._write_out_kind(kind, ver) - self.assertFalse(self.test_rc.should_autodeploy()) - def test_autorenewal_is_enabled(self): self.test_rc.configuration["renewalparams"] = {} self.assertTrue(self.test_rc.autorenewal_is_enabled()) @@ -544,6 +522,47 @@ self.assertFalse(os.path.islink(self.test_rc.version("privkey", 10))) self.assertFalse(os.path.exists(temp_config_file)) + @test_util.broken_on_windows + @mock.patch("certbot.storage.relevant_values") + def test_save_successor_maintains_group_mode(self, mock_rv): + # Mock relevant_values() to claim that all values are relevant here + # (to avoid instantiating parser) + mock_rv.side_effect = lambda x: x + for kind in ALL_FOUR: + self._write_out_kind(kind, 1) + self.test_rc.update_all_links_to(1) + self.assertTrue(compat.compare_file_modes( + os.stat(self.test_rc.version("privkey", 1)).st_mode, 0o600)) + os.chmod(self.test_rc.version("privkey", 1), 0o444) + # If no new key, permissions should be the same (we didn't write any keys) + self.test_rc.save_successor(1, b"newcert", None, b"new chain", self.config) + self.assertTrue(compat.compare_file_modes( + os.stat(self.test_rc.version("privkey", 2)).st_mode, 0o444)) + # If new key, permissions should be kept as 644 + self.test_rc.save_successor(2, b"newcert", b"new_privkey", b"new chain", self.config) + self.assertTrue(compat.compare_file_modes( + os.stat(self.test_rc.version("privkey", 3)).st_mode, 0o644)) + # If permissions reverted, next renewal will also revert permissions of new key + os.chmod(self.test_rc.version("privkey", 3), 0o400) + self.test_rc.save_successor(3, b"newcert", b"new_privkey", b"new chain", self.config) + self.assertTrue(compat.compare_file_modes( + os.stat(self.test_rc.version("privkey", 4)).st_mode, 0o600)) + + @test_util.broken_on_windows + @mock.patch("certbot.storage.relevant_values") + @mock.patch("certbot.storage.os.chown") + def test_save_successor_maintains_gid(self, mock_chown, mock_rv): + # Mock relevant_values() to claim that all values are relevant here + # (to avoid instantiating parser) + mock_rv.side_effect = lambda x: x + for kind in ALL_FOUR: + self._write_out_kind(kind, 1) + self.test_rc.update_all_links_to(1) + self.test_rc.save_successor(1, b"newcert", None, b"new chain", self.config) + self.assertFalse(mock_chown.called) + self.test_rc.save_successor(2, b"newcert", b"new_privkey", b"new chain", self.config) + self.assertTrue(mock_chown.called) + def _test_relevant_values_common(self, values): defaults = dict((option, cli.flag_default(option)) for option in ("authenticator", "installer", @@ -630,6 +649,7 @@ self.config.live_dir, "README"))) self.assertTrue(os.path.exists(os.path.join( self.config.live_dir, "the-lineage.com", "README"))) + self.assertTrue(compat.compare_file_modes(os.stat(result.key_path).st_mode, 0o600)) with open(result.fullchain, "rb") as f: self.assertEqual(f.read(), b"cert" + b"chain") # Let's do it again and make sure it makes a different lineage diff -Nru python-certbot-0.28.0/certbot/tests/util.py python-certbot-0.31.0/certbot/tests/util.py --- python-certbot-0.28.0/certbot/tests/util.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/tests/util.py 2019-02-07 21:20:30.000000000 +0000 @@ -328,15 +328,16 @@ def tearDown(self): """Execute after test""" - # Then we have various files which are not correctly closed at the time of tearDown. - # On Windows, it is visible for the same reasons as above. + # On Windows we have various files which are not correctly closed at the time of tearDown. # For know, we log them until a proper file close handling is written. + # Useful for development only, so no warning when we are on a CI process. def onerror_handler(_, path, excinfo): """On error handler""" - message = ('Following error occurred when deleting the tempdir {0}' - ' for path {1} during tearDown process: {2}' - .format(self.tempdir, path, str(excinfo))) - warnings.warn(message) + if not os.environ.get('APPVEYOR'): # pragma: no cover + message = ('Following error occurred when deleting the tempdir {0}' + ' for path {1} during tearDown process: {2}' + .format(self.tempdir, path, str(excinfo))) + warnings.warn(message) shutil.rmtree(self.tempdir, onerror=onerror_handler) class ConfigTestCase(TempDirTestCase): diff -Nru python-certbot-0.28.0/certbot/tests/util_test.py python-certbot-0.31.0/certbot/tests/util_test.py --- python-certbot-0.28.0/certbot/tests/util_test.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/certbot/tests/util_test.py 2019-02-07 21:20:30.000000000 +0000 @@ -210,16 +210,21 @@ fd, name = self._call() fd.write("bar") fd.close() - self.assertEqual(open(name).read(), "bar") + with open(name) as f: + self.assertEqual(f.read(), "bar") def test_right_mode(self): - self.assertTrue(compat.compare_file_modes(0o700, os.stat(self._call(0o700)[1]).st_mode)) - self.assertTrue(compat.compare_file_modes(0o600, os.stat(self._call(0o600)[1]).st_mode)) + fd1, name1 = self._call(0o700) + fd2, name2 = self._call(0o600) + self.assertTrue(compat.compare_file_modes(0o700, os.stat(name1).st_mode)) + self.assertTrue(compat.compare_file_modes(0o600, os.stat(name2).st_mode)) + fd1.close() + fd2.close() def test_default_exists(self): - name1 = self._call()[1] # create 0000_foo.txt - name2 = self._call()[1] - name3 = self._call()[1] + fd1, name1 = self._call() # create 0000_foo.txt + fd2, name2 = self._call() + fd3, name3 = self._call() self.assertNotEqual(name1, name2) self.assertNotEqual(name1, name3) @@ -236,6 +241,10 @@ basename3 = os.path.basename(name3) self.assertTrue(basename3.endswith("foo.txt")) + fd1.close() + fd2.close() + fd3.close() + try: file_type = file @@ -255,13 +264,18 @@ f, path = self._call("wow") self.assertTrue(isinstance(f, file_type)) self.assertEqual(os.path.join(self.tempdir, "wow.conf"), path) + f.close() def test_multiple(self): + items = [] for _ in six.moves.range(10): - f, name = self._call("wow") + items.append(self._call("wow")) + f, name = items[-1] self.assertTrue(isinstance(f, file_type)) self.assertTrue(isinstance(name, six.string_types)) self.assertTrue("wow-0009.conf" in name) + for f, _ in items: + f.close() @mock.patch("certbot.util.os.fdopen") def test_failure(self, mock_fdopen): diff -Nru python-certbot-0.28.0/certbot.egg-info/PKG-INFO python-certbot-0.31.0/certbot.egg-info/PKG-INFO --- python-certbot-0.28.0/certbot.egg-info/PKG-INFO 2018-11-07 21:14:58.000000000 +0000 +++ python-certbot-0.31.0/certbot.egg-info/PKG-INFO 2019-02-07 21:20:31.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: certbot -Version: 0.28.0 +Version: 0.31.0 Summary: ACME client Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project @@ -107,8 +107,8 @@ |build-status| |coverage| |docs| |container| - .. |build-status| image:: https://travis-ci.org/certbot/certbot.svg?branch=master - :target: https://travis-ci.org/certbot/certbot + .. |build-status| image:: https://travis-ci.com/certbot/certbot.svg?branch=master + :target: https://travis-ci.com/certbot/certbot :alt: Travis CI status .. |coverage| image:: https://codecov.io/gh/certbot/certbot/branch/master/graph/badge.svg diff -Nru python-certbot-0.28.0/certbot.egg-info/requires.txt python-certbot-0.31.0/certbot.egg-info/requires.txt --- python-certbot-0.28.0/certbot.egg-info/requires.txt 2018-11-07 21:14:58.000000000 +0000 +++ python-certbot-0.31.0/certbot.egg-info/requires.txt 2019-02-07 21:20:31.000000000 +0000 @@ -1,7 +1,7 @@ -acme>=0.26.0 +acme>=0.29.0 ConfigArgParse>=0.9.3 configobj -cryptography>=1.2 +cryptography>=1.2.3 josepy mock parsedatetime>=1.3 @@ -29,5 +29,5 @@ [docs] repoze.sphinx.autointerface -Sphinx>=1.6 +Sphinx>=1.2 sphinx_rtd_theme diff -Nru python-certbot-0.28.0/CHANGELOG.md python-certbot-0.31.0/CHANGELOG.md --- python-certbot-0.28.0/CHANGELOG.md 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/CHANGELOG.md 2019-02-07 21:20:30.000000000 +0000 @@ -1,6 +1,168 @@ # Certbot change log -Certbot adheres to [Semantic Versioning](http://semver.org/). +Certbot adheres to [Semantic Versioning](https://semver.org/). + +## 0.31.0 - 2019-02-07 + +### Added + +* Avoid reprocessing challenges that are already validated + when a certificate is issued. +* Support for initiating (but not solving end-to-end) TLS-ALPN-01 challenges + with the `acme` module. + +### Changed + +* Certbot's official Docker images are now based on Alpine Linux 3.9 rather + than 3.7. The new version comes with OpenSSL 1.1.1. +* Lexicon-based DNS plugins are now fully compatible with Lexicon 3.x (support + on 2.x branch is maintained). +* Apache plugin now attempts to configure all VirtualHosts matching requested + domain name instead of only a single one when answering the HTTP-01 challenge. + +### Fixed + +* Fixed accessing josepy contents through acme.jose when the full acme.jose + path is used. +* Clarify behavior for deleting certs as part of revocation. + +Despite us having broken lockstep, we are continuing to release new versions of +all Certbot components during releases for the time being, however, the only +package with changes other than its version number was: + +* acme +* certbot +* certbot-apache +* certbot-dns-cloudxns +* certbot-dns-dnsimple +* certbot-dns-dnsmadeeasy +* certbot-dns-gehirn +* certbot-dns-linode +* certbot-dns-luadns +* certbot-dns-nsone +* certbot-dns-ovh +* certbot-dns-sakuracloud + +More details about these changes can be found on our GitHub repo. + +## 0.30.2 - 2019-01-25 + +### Fixed + +* Update the version of setuptools pinned in certbot-auto to 40.6.3 to + solve installation problems on newer OSes. + +Despite us having broken lockstep, we are continuing to release new versions of +all Certbot components during releases for the time being, however, this +release only affects certbot-auto. + +More details about these changes can be found on our GitHub repo. + +## 0.30.1 - 2019-01-24 + +### Fixed + +* Always download the pinned version of pip in pipstrap to address breakages +* Rename old,default.conf to old-and-default.conf to address commas in filenames + breaking recent versions of pip. +* Add VIRTUALENV_NO_DOWNLOAD=1 to all calls to virtualenv to address breakages + from venv downloading the latest pip + +Despite us having broken lockstep, we are continuing to release new versions of +all Certbot components during releases for the time being, however, the only +package with changes other than its version number was: + +* certbot-apache + +More details about these changes can be found on our GitHub repo. + +## 0.30.0 - 2019-01-02 + +### Added + +* Added the `update_account` subcommand for account management commands. + +### Changed + +* Copied account management functionality from the `register` subcommand + to the `update_account` subcommand. +* Marked usage `register --update-registration` for deprecation and + removal in a future release. + +### Fixed + +* Older modules in the josepy library can now be accessed through acme.jose + like it could in previous versions of acme. This is only done to preserve + backwards compatibility and support for doing this with new modules in josepy + will not be added. Users of the acme library should switch to using josepy + directly if they haven't done so already. + +Despite us having broken lockstep, we are continuing to release new versions of +all Certbot components during releases for the time being, however, the only +package with changes other than its version number was: + +* acme + +More details about these changes can be found on our GitHub repo. + +## 0.29.1 - 2018-12-05 + +### Added + +* + +### Changed + +* + +### Fixed + +* The default work and log directories have been changed back to + /var/lib/letsencrypt and /var/log/letsencrypt respectively. + +Despite us having broken lockstep, we are continuing to release new versions of +all Certbot components during releases for the time being, however, the only +package with changes other than its version number was: + +* certbot + +More details about these changes can be found on our GitHub repo. + +## 0.29.0 - 2018-12-05 + +### Added + +* Noninteractive renewals with `certbot renew` (those not started from a + terminal) now randomly sleep 1-480 seconds before beginning work in + order to spread out load spikes on the server side. +* Added External Account Binding support in cli and acme library. + Command line arguments --eab-kid and --eab-hmac-key added. + +### Changed + +* Private key permissioning changes: Renewal preserves existing group mode + & gid of previous private key material. Private keys for new + lineages (i.e. new certs, not renewed) default to 0o600. + +### Fixed + +* Update code and dependencies to clean up Resource and Deprecation Warnings. +* Only depend on imgconverter extension for Sphinx >= 1.6 + +Despite us having broken lockstep, we are continuing to release new versions of +all Certbot components during releases for the time being, however, the only +package with changes other than its version number was: + +* acme +* certbot +* certbot-apache +* certbot-dns-cloudflare +* certbot-dns-digitalocean +* certbot-dns-google +* certbot-nginx + +More details about these changes can be found on our GitHub repo: +https://github.com/certbot/certbot/milestone/62?closed=1 ## 0.28.0 - 2018-11-7 diff -Nru python-certbot-0.28.0/debian/changelog python-certbot-0.31.0/debian/changelog --- python-certbot-0.28.0/debian/changelog 2018-12-14 15:50:11.000000000 +0000 +++ python-certbot-0.31.0/debian/changelog 2019-03-12 17:26:27.000000000 +0000 @@ -1,8 +1,23 @@ -python-certbot (0.28.0-1+ubuntu18.04.1+certbot+4) bionic; urgency=medium +python-certbot (0.31.0-1+ubuntu18.04.1+certbot+1) bionic; urgency=medium * No-change backport to bionic - -- Ondřej Surý Fri, 14 Dec 2018 15:50:11 +0000 + -- Ondřej Surý Tue, 12 Mar 2019 17:26:27 +0000 + +python-certbot (0.31.0-1) unstable; urgency=medium + + * New upstream version 0.31.0 + * Bump deps changed by upstream. + * Bump S-V; no changes needed. + * Refresh patches. + + -- Harlan Lieberman-Berg Sat, 09 Feb 2019 19:39:59 -0500 + +python-certbot (0.28.0-2) unstable; urgency=medium + + * Remove unnecessary letsencrypt.postrm (Closes: #921423) + + -- Harlan Lieberman-Berg Tue, 05 Feb 2019 22:15:02 -0500 python-certbot (0.28.0-1) unstable; urgency=medium diff -Nru python-certbot-0.28.0/debian/control python-certbot-0.31.0/debian/control --- python-certbot-0.28.0/debian/control 2018-11-07 23:19:31.000000000 +0000 +++ python-certbot-0.31.0/debian/control 2019-02-10 00:39:59.000000000 +0000 @@ -8,10 +8,10 @@ dh-python, dh-systemd, python3, - python3-acme (>= 0.26.0~), + python3-acme (>= 0.29.0~), python3-configargparse (>= 0.10.0), python3-configobj, - python3-cryptography (>= 1.2), + python3-cryptography (>= 1.2.3), python3-distutils | python3 (<< 3.6.5~), python3-josepy, python3-parsedatetime (>= 1.3), @@ -22,7 +22,7 @@ python3-tz, python3-zope.component, python3-zope.interface -Standards-Version: 4.2.1 +Standards-Version: 4.3.0 Homepage: https://certbot.eff.org/ Vcs-Git: https://salsa.debian.org/letsencrypt-team/certbot/certbot.git Vcs-Browser: https://salsa.debian.org/letsencrypt-team/certbot/certbot @@ -31,7 +31,7 @@ Package: python3-certbot Architecture: all -Depends: python3-acme (>= 0.25.0~), +Depends: python3-acme (>= 0.29.0~), python3-ndg-httpsclient, python3-requests (>= 2.4.3), ${misc:Depends}, diff -Nru python-certbot-0.28.0/debian/letsencrypt.postrm python-certbot-0.31.0/debian/letsencrypt.postrm --- python-certbot-0.28.0/debian/letsencrypt.postrm 2018-11-07 23:19:31.000000000 +0000 +++ python-certbot-0.31.0/debian/letsencrypt.postrm 1970-01-01 00:00:00.000000000 +0000 @@ -1,41 +0,0 @@ -#!/bin/sh -# postrm script for letsencrypt -# -# see: dh_installdeb(1) - -set -e - -# summary of how this script can be called: -# * `remove' -# * `purge' -# * `upgrade' -# * `failed-upgrade' -# * `abort-install' -# * `abort-install' -# * `abort-upgrade' -# * `disappear' -# -# for details, see https://www.debian.org/doc/debian-policy/ or -# the debian-policy package - - -case "$1" in - purge) - rm -rf /var/log/letsencrypt - ;; - - remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) - ;; - - *) - echo "postrm called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -# dh_installdeb will replace this with shell code automatically -# generated by other debhelper scripts. - -#DEBHELPER# - -exit 0 diff -Nru python-certbot-0.28.0/debian/patches/0001-remove-external-images.patch python-certbot-0.31.0/debian/patches/0001-remove-external-images.patch --- python-certbot-0.28.0/debian/patches/0001-remove-external-images.patch 2018-11-07 23:19:31.000000000 +0000 +++ python-certbot-0.31.0/debian/patches/0001-remove-external-images.patch 2019-02-10 00:39:59.000000000 +0000 @@ -13,8 +13,8 @@ -|build-status| |coverage| |docs| |container| - --.. |build-status| image:: https://travis-ci.org/certbot/certbot.svg?branch=master -- :target: https://travis-ci.org/certbot/certbot +-.. |build-status| image:: https://travis-ci.com/certbot/certbot.svg?branch=master +- :target: https://travis-ci.com/certbot/certbot - :alt: Travis CI status - -.. |coverage| image:: https://codecov.io/gh/certbot/certbot/branch/master/graph/badge.svg diff -Nru python-certbot-0.28.0/docs/challenges.rst python-certbot-0.31.0/docs/challenges.rst --- python-certbot-0.28.0/docs/challenges.rst 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/docs/challenges.rst 2019-02-07 21:20:30.000000000 +0000 @@ -3,10 +3,9 @@ To receive a certificate from Let's Encrypt certificate authority (CA), you must pass a *challenge* to prove you control each of the domain names that will be listed in the certificate. A challenge is one of -three tasks that only someone who controls the domain should be able to accomplish: +a list of specified tasks that only someone who controls the domain should be able to accomplish, such as: * Posting a specified file in a specified location on a web site (the HTTP-01 challenge) -* Offering a specified temporary certificate on a web site (the TLS-SNI-01 challenge) * Posting a specified DNS record in the domain name system (the DNS-01 challenge) It’s possible to complete each type of challenge *automatically* (Certbot directly makes the necessary @@ -16,21 +15,21 @@ Some plugins offer an *authenticator*, meaning that they can satisfy challenges: -* Apache plugin: (TLS-SNI-01) Tries to edit your Apache configuration files to temporarily serve - a Certbot-generated certificate for a specified name. Use the Apache plugin when you're running - Certbot on a web server with Apache listening on port 443. -* NGINX plugin: (TLS-SNI-01) Tries to edit your NGINX configuration files to temporarily serve a - Certbot-generated certificate for a specified name. Use the NGINX plugin when you're running - Certbot on a web server with NGINX listening on port 443. +* Apache plugin: (HTTP-01) Tries to edit your Apache configuration files to temporarily serve files to + satisfy challenges from the certificate authority. Use the Apache plugin when you're running Certbot on a + web server with Apache listening on port 80. +* Nginx plugin: (HTTP-01) Tries to edit your nginx configuration files to temporarily serve files to + satisfy challenges from the certificate authority. Use the nginx plugin when you're running Certbot on a + web server with nginx listening on port 80. * Webroot plugin: (HTTP-01) Tries to place a file where it can be served over HTTP on port 80 by a web server running on your system. Use the Webroot plugin when you're running Certbot on a web server with any server application listening on port 80 serving files from a folder on disk in response. -* Standalone plugin: (TLS-SNI-01 or HTTP-01) Tries to run a temporary web server listening on either HTTP on - port 80 (for HTTP-01) or HTTPS on port 443 (for TLS-SNI-01). Use the Standalone plugin if no existing program - is listening to these ports. Choose TLS-SNI-01 or HTTP-01 using the `--preferred-challenges` option. +* Standalone plugin: (HTTP-01) Tries to run a temporary web server listening on HTTP on port 80. Use the + Standalone plugin if no existing program is listening to this port. * Manual plugin: (DNS-01 or HTTP-01) Either tells you what changes to make to your configuration or updates your DNS records using an external script (for DNS-01) or your webroot (for HTTP-01). Use the Manual - plugin if you have the technical knowledge to make configuration changes yourself when asked to do so. + plugin if you have the technical knowledge to make configuration changes yourself when asked to do so, + and are prepared to repeat these steps every time the certificate needs to be renewed. Tips for Challenges ------------------- @@ -63,20 +62,6 @@ * When using the Standalone plugin, make sure another program is not already listening to port 80 on the server. * When using the Webroot plugin, make sure there is a web server listening on port 80. -TLS-SNI-01 Challenge -~~~~~~~~~~~~~~~~~~~~ - -* The TLS-SNI-01 challenge doesn’t work with content delivery networks (CDNs) - like CloudFlare and Akamai because the domain name is pointed at the CDN, not directly at your server. -* Make sure port 443 is open, publicly reachable from the Internet, and not blocked by a router or firewall. -* When using the Apache plugin, make sure you are running Apache and no other web server on port 443. -* When using the NGINX plugin, make sure you are running NGINX and no other web server on port 443. -* With either the Apache or NGINX plugin, certbot modifies your web server configuration. If you get - an error after successfully completing the challenge, then you have received a certificate but the - plugin was unable to modify your web server configuration, meaning that you'll have to install the certificate manually. - In that case, please file a bug to help us improve certbot! -* When using the Standalone plugin, make sure another program is not already listening to port 443 on the server. - DNS-01 Challenge ~~~~~~~~~~~~~~~~ diff -Nru python-certbot-0.28.0/docs/cli-help.txt python-certbot-0.31.0/docs/cli-help.txt --- python-certbot-0.28.0/docs/cli-help.txt 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/docs/cli-help.txt 2019-02-07 21:20:30.000000000 +0000 @@ -24,11 +24,12 @@ manage certificates: certificates Display information about certificates you have from Certbot - revoke Revoke a certificate (supply --cert-path) + revoke Revoke a certificate (supply --cert-path or --cert-name) delete Delete a certificate manage your account with Let's Encrypt: register Create a Let's Encrypt ACME account + update_account Update a Let's Encrypt ACME account --agree-tos Agree to the ACME server's Subscriber Agreement -m EMAIL Email address for important account notifications @@ -67,6 +68,10 @@ with the same name. In the case of a name collision it will append a number like 0001 to the file path name. (default: Ask) + --eab-kid EAB_KID Key Identifier for External Account Binding (default: + None) + --eab-hmac-key EAB_HMAC_KEY + HMAC key for External Account Binding (default: None) --cert-name CERTNAME Certificate name to apply. This name is used by Certbot for housekeeping and in file paths; it doesn't affect the content of the certificate itself. To see @@ -108,7 +113,7 @@ case, and to know when to deprecate support for past Python versions and flags. If you wish to hide this information from the Let's Encrypt server, set this to - "". (default: CertbotACMEClient/0.27.1 + "". (default: CertbotACMEClient/0.30.2 (certbot(-auto); OS_NAME OS_VERSION) Authenticator/XXX Installer/YYY (SUBCOMMAND; flags: FLAGS) Py/major.minor.patchlevel). The flags encoded in the @@ -261,7 +266,8 @@ delete Clean up all files related to a certificate renew Renew all certificates (or one specified with --cert- name) - revoke Revoke a certificate specified with --cert-path + revoke Revoke a certificate specified with --cert-path or + --cert-name update_symlinks Recreate symlinks in your /etc/letsencrypt/live/ directory @@ -354,7 +360,7 @@ certificates. (default: None) register: - Options for account registration & modification + Options for account registration --register-unsafely-without-email Specifying this flag enables registering an account @@ -366,11 +372,6 @@ to the Subscriber Agreement will still affect you, and will be effective 14 days after posting an update to the web site. (default: False) - --update-registration - With the register verb, indicates that details - associated with an existing registration, such as the - e-mail address, should be updated, rather than - registering a new account. (default: False) -m EMAIL, --email EMAIL Email used for registration and recovery contact. Use comma to register multiple emails, ex: @@ -379,6 +380,9 @@ --no-eff-email Don't share your e-mail address with EFF (default: None) +update_account: + Options for account modification + unregister: Options for account deactivation. @@ -472,7 +476,7 @@ using Sakura Cloud for DNS). (default: False) apache: - Apache Web Server plugin - Beta + Apache Web Server plugin --apache-enmod APACHE_ENMOD Path to the Apache 'a2enmod' binary (default: a2enmod) @@ -503,15 +507,6 @@ Full path to Apache control script (default: apache2ctl) -certbot-route53:auth: - Obtain certificates using a DNS TXT record (if you are using AWS Route53 - for DNS). - - --certbot-route53:auth-propagation-seconds CERTBOT_ROUTE53:AUTH_PROPAGATION_SECONDS - The number of seconds to wait for DNS to propagate - before asking the ACME server to verify the DNS - record. (default: 10) - dns-cloudflare: Obtain certificates using a DNS TXT record (if you are using Cloudflare for DNS). @@ -602,7 +597,7 @@ --dns-linode-propagation-seconds DNS_LINODE_PROPAGATION_SECONDS The number of seconds to wait for DNS to propagate before asking the ACME server to verify the DNS - record. (default: 960) + record. (default: 1200) --dns-linode-credentials DNS_LINODE_CREDENTIALS Linode credentials INI file. (default: None) diff -Nru python-certbot-0.28.0/docs/conf.py python-certbot-0.31.0/docs/conf.py --- python-certbot-0.28.0/docs/conf.py 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/docs/conf.py 2019-02-07 21:20:30.000000000 +0000 @@ -17,6 +17,8 @@ import re import sys +import sphinx + here = os.path.abspath(os.path.dirname(__file__)) @@ -33,14 +35,13 @@ # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '1.0' +needs_sphinx = '1.2' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', - 'sphinx.ext.imgconverter', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', @@ -48,6 +49,9 @@ 'repoze.sphinx.autointerface', ] +if sphinx.version_info >= (1, 6): + extensions.append('sphinx.ext.imgconverter') + autodoc_member_order = 'bysource' autodoc_default_flags = ['show-inheritance', 'private-members'] diff -Nru python-certbot-0.28.0/docs/contributing.rst python-certbot-0.31.0/docs/contributing.rst --- python-certbot-0.28.0/docs/contributing.rst 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/docs/contributing.rst 2019-02-07 21:20:30.000000000 +0000 @@ -38,13 +38,13 @@ cd certbot ./certbot-auto --debug --os-packages-only - tools/venv.sh + python tools/venv.py -If you have Python3 available and want to use it, run the ``venv3.sh`` script. +If you have Python3 available and want to use it, run the ``venv3.py`` script. .. code-block:: shell - tools/venv3.sh + python tools/venv3.py .. note:: You may need to repeat this when Certbot's dependencies change or when a new plugin is introduced. @@ -186,8 +186,8 @@ -------------- Authenticators are plugins that prove control of a domain name by solving a -challenge provided by the ACME server. ACME currently defines three types of -challenges: HTTP, TLS-SNI, and DNS, represented by classes in `acme.challenges`. +challenge provided by the ACME server. ACME currently defines several types of +challenges: HTTP, TLS-SNI (deprecated), TLS-ALPR, and DNS, represented by classes in `acme.challenges`. An authenticator plugin should implement support for at least one challenge type. An Authenticator indicates which challenges it supports by implementing @@ -215,7 +215,7 @@ Installers and Authenticators will oftentimes be the same class/object (because for instance both tasks can be performed by a webserver like nginx) though this is not always the case (the standalone plugin is an authenticator -that listens on port 443, but it cannot install certs; a postfix plugin would +that listens on port 80, but it cannot install certs; a postfix plugin would be an installer but not an authenticator). Installers and Authenticators are kept separate because @@ -353,13 +353,16 @@ 1. Write your code! 2. Make sure your environment is set up properly and that you're in your - virtualenv. You can do this by running ``./tools/venv.sh``. + virtualenv. You can do this by running ``pip tools/venv.py``. (this is a **very important** step) 3. Run ``tox -e lint`` to check for pylint errors. Fix any errors. 4. Run ``tox --skip-missing-interpreters`` to run the entire test suite including coverage. The ``--skip-missing-interpreters`` argument ignores missing versions of Python needed for running the tests. Fix any errors. -5. Submit the PR. +5. Submit the PR. Once your PR is open, please do not force push to the branch + containing your pull request to squash or amend commits. We use `squash + merges `_ on PRs and + rewriting commits makes changes harder to track between reviews. 6. Did your tests pass on Travis? If they didn't, fix any errors. Asking for help diff -Nru python-certbot-0.28.0/docs/install.rst python-certbot-0.31.0/docs/install.rst --- python-certbot-0.28.0/docs/install.rst 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/docs/install.rst 2019-02-07 21:20:30.000000000 +0000 @@ -29,7 +29,7 @@ Certbot currently requires Python 2.7 or 3.4+ running on a UNIX-like operating system. By default, it requires root access in order to write to ``/etc/letsencrypt``, ``/var/log/letsencrypt``, ``/var/lib/letsencrypt``; to -bind to ports 80 and 443 (if you use the ``standalone`` plugin) and to read and +bind to port 80 (if you use the ``standalone`` plugin) and to read and modify webserver configurations (if you use the ``apache`` or ``nginx`` plugins). If none of these apply to you, it is theoretically possible to run without root privileges, but for most users who want to avoid running an ACME diff -Nru python-certbot-0.28.0/docs/using.rst python-certbot-0.31.0/docs/using.rst --- python-certbot-0.28.0/docs/using.rst 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/docs/using.rst 2019-02-07 21:20:30.000000000 +0000 @@ -44,14 +44,12 @@ =========== ==== ==== =============================================================== ============================= Plugin Auth Inst Notes Challenge types (and port) =========== ==== ==== =============================================================== ============================= -apache_ Y Y | Automates obtaining and installing a certificate with Apache tls-sni-01_ (443) - | 2.4 on OSes with ``libaugeas0`` 1.0+. +apache_ Y Y | Automates obtaining and installing a certificate with Apache. http-01_ (80) +nginx_ Y Y | Automates obtaining and installing a certificate with Nginx. http-01_ (80) webroot_ Y N | Obtains a certificate by writing to the webroot directory of http-01_ (80) | an already running webserver. -nginx_ Y Y | Automates obtaining and installing a certificate with Nginx. tls-sni-01_ (443) - | Shipped with Certbot 0.9.0. -standalone_ Y N | Uses a "standalone" webserver to obtain a certificate. http-01_ (80) or - | Requires port 80 or 443 to be available. This is useful on tls-sni-01_ (443) +standalone_ Y N | Uses a "standalone" webserver to obtain a certificate. http-01_ (80) + | Requires port 80 to be available. This is useful on | systems with no webserver, or when direct integration with | the local webserver is not supported or not desired. |dns_plugs| Y N | This category of plugins automates obtaining a certificate by dns-01_ (53) @@ -59,17 +57,17 @@ | domain. Doing domain validation in this way is | the only way to obtain wildcard certificates from Let's | Encrypt. -manual_ Y N | Helps you obtain a certificate by giving you instructions to http-01_ (80), - | perform domain validation yourself. Additionally allows you dns-01_ (53) or - | to specify scripts to automate the validation task in a tls-sni-01_ (443) +manual_ Y N | Helps you obtain a certificate by giving you instructions to http-01_ (80) or + | perform domain validation yourself. Additionally allows you dns-01_ (53) + | to specify scripts to automate the validation task in a | customized way. =========== ==== ==== =============================================================== ============================= .. |dns_plugs| replace:: :ref:`DNS plugins ` Under the hood, plugins use one of several ACME protocol challenges_ to -prove you control a domain. The options are http-01_ (which uses port 80), -tls-sni-01_ (port 443) and dns-01_ (requiring configuration of a DNS server on +prove you control a domain. The options are http-01_ (which uses port 80) +and dns-01_ (requiring configuration of a DNS server on port 53, though that's often not the same machine as your webserver). A few plugins support more than one challenge type, in which case you can choose one with ``--preferred-challenges``. @@ -78,15 +76,13 @@ the circumstances in which each plugin can be used, and how to use it. .. _challenges: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7 -.. _tls-sni-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.3 .. _http-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.2 .. _dns-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.4 Apache ------ -The Apache plugin currently requires an OS with augeas version 1.0; currently `it -supports +The Apache plugin currently `supports `_ modern OSes based on Debian, Fedora, SUSE, Gentoo and Darwin. This automates both obtaining *and* installing certificates on an Apache @@ -138,9 +134,8 @@ Nginx ----- -The Nginx plugin has been distributed with Certbot since version 0.9.0 and should -work for most configurations. We recommend backing up Nginx -configurations before using it (though you can also revert changes to +The Nginx plugin should work for most configurations. We recommend backing up +Nginx configurations before using it (though you can also revert changes to configurations with ``certbot --nginx rollback``). You can use it by providing the ``--nginx`` flag on the commandline. @@ -159,13 +154,9 @@ To obtain a certificate using a "standalone" webserver, you can use the standalone plugin by including ``certonly`` and ``--standalone`` -on the command line. This plugin needs to bind to port 80 or 443 in +on the command line. This plugin needs to bind to port 80 in order to perform domain validation, so you may need to stop your -existing webserver. To control which port the plugin uses, include -one of the options shown below on the command line. - - * ``--preferred-challenges http`` to use port 80 - * ``--preferred-challenges tls-sni`` to use port 443 +existing webserver. It must still be possible for your machine to accept inbound connections from the Internet on the specified port using each requested domain name. @@ -222,8 +213,7 @@ to copy and paste commands into another terminal session, which may be on a different computer. -The manual plugin can use either the ``http``, ``dns`` or the -``tls-sni`` challenge. You can use the ``--preferred-challenges`` option +The manual plugin can use either the ``http`` or the ``dns`` challenge. You can use the ``--preferred-challenges`` option to choose the challenge of your preference. The ``http`` challenge will ask you to place a file with a specific name and @@ -241,11 +231,6 @@ _acme-challenge.example.com. 300 IN TXT "gfj9Xq...Rg85nM" -When using the ``tls-sni`` challenge, ``certbot`` will prepare a self-signed -SSL certificate for you with the challenge validation appropriately -encoded into a subjectAlternatNames entry. You will need to configure -your SSL server to present this challenge SSL certificate to the ACME -server using SNI. Additionally you can specify scripts to prepare for validation and perform the authentication procedure and/or clean up after it by using @@ -262,16 +247,21 @@ ``--authenticator`` or ``-a`` and the installer plugin with ``--installer`` or ``-i``. -For instance, you may want to create a certificate using the webroot_ plugin -for authentication and the apache_ plugin for installation, perhaps because you -use a proxy or CDN for SSL and only want to secure the connection between them -and your origin server, which cannot use the tls-sni-01_ challenge due to the -intermediate proxy. +For instance, you could create a certificate using the webroot_ plugin +for authentication and the apache_ plugin for installation. :: certbot run -a webroot -i apache -w /var/www/html -d example.com +Or you could create a certificate using the manual_ plugin for authentication +and the nginx_ plugin for installation. (Note that this certificate cannot +be renewed automatically.) + +:: + + certbot run -a manual -i nginx -d example.com + .. _third-party-plugins: Third-party plugins @@ -496,8 +486,9 @@ renewal, so you can run the above command frequently without unnecessarily stopping your webserver. -``--pre-hook`` and ``--post-hook`` hooks run before and after every renewal -attempt. If you want your hook to run only after a successful renewal, use +When Certbot detects that a certificate is due for renewal, ``--pre-hook`` +and ``--post-hook`` hooks run before and after each attempt to renew it. +If you want your hook to run only after a successful renewal, use ``--deploy-hook`` in a command like this. ``certbot renew --deploy-hook /path/to/deploy-hook-script`` @@ -696,7 +687,9 @@ ========================== All generated keys and issued certificates can be found in -``/etc/letsencrypt/live/$domain``. Rather than copying, please point +``/etc/letsencrypt/live/$domain``. In the case of creating a SAN certificate +with multiple alternative names, ``$domain`` is the first domain passed in +via -d parameter. Rather than copying, please point your (web) server configuration directly to those files (or create symlinks). During the renewal_, ``/etc/letsencrypt/live`` is updated with the latest necessary files. @@ -715,6 +708,10 @@ put it into a safe, however - your server still needs to access this file in order for SSL/TLS to work. + .. note:: As of Certbot version 0.29.0, private keys for new certificate + default to ``0600``. Any changes to the group mode or group owner (gid) + of this file will be preserved on renewals. + This is what Apache needs for `SSLCertificateKeyFile `_, and Nginx for `ssl_certificate_key @@ -775,9 +772,6 @@ - ``CERTBOT_DOMAIN``: The domain being authenticated - ``CERTBOT_VALIDATION``: The validation string (HTTP-01 and DNS-01 only) - ``CERTBOT_TOKEN``: Resource name part of the HTTP-01 challenge (HTTP-01 only) -- ``CERTBOT_CERT_PATH``: The challenge SSL certificate (TLS-SNI-01 only) -- ``CERTBOT_KEY_PATH``: The private key associated with the aforementioned SSL certificate (TLS-SNI-01 only) -- ``CERTBOT_SNI_DOMAIN``: The SNI name for which the ACME server expects to be presented the self-signed certificate located at ``$CERTBOT_CERT_PATH`` (TLS-SNI-01 only) Additionally for cleanup: @@ -910,7 +904,7 @@ Since the directories used by Certbot are configurable, Certbot will write a lock file for all of the directories it uses. This include Certbot's ``--work-dir``, ``--logs-dir``, and ``--config-dir``. By default these are -``/var/lib/letsencrypt``, ``/var/logs/letsencrypt``, and ``/etc/letsencrypt`` +``/var/lib/letsencrypt``, ``/var/log/letsencrypt``, and ``/etc/letsencrypt`` respectively. Additionally if you are using Certbot with Apache or nginx it will lock the configuration folder for that program, which are typically also in the ``/etc`` directory. diff -Nru python-certbot-0.28.0/PKG-INFO python-certbot-0.31.0/PKG-INFO --- python-certbot-0.28.0/PKG-INFO 2018-11-07 21:14:58.000000000 +0000 +++ python-certbot-0.31.0/PKG-INFO 2019-02-07 21:20:31.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: certbot -Version: 0.28.0 +Version: 0.31.0 Summary: ACME client Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project @@ -107,8 +107,8 @@ |build-status| |coverage| |docs| |container| - .. |build-status| image:: https://travis-ci.org/certbot/certbot.svg?branch=master - :target: https://travis-ci.org/certbot/certbot + .. |build-status| image:: https://travis-ci.com/certbot/certbot.svg?branch=master + :target: https://travis-ci.com/certbot/certbot :alt: Travis CI status .. |coverage| image:: https://codecov.io/gh/certbot/certbot/branch/master/graph/badge.svg diff -Nru python-certbot-0.28.0/README.rst python-certbot-0.31.0/README.rst --- python-certbot-0.28.0/README.rst 2018-11-07 21:14:56.000000000 +0000 +++ python-certbot-0.31.0/README.rst 2019-02-07 21:20:29.000000000 +0000 @@ -99,8 +99,8 @@ |build-status| |coverage| |docs| |container| -.. |build-status| image:: https://travis-ci.org/certbot/certbot.svg?branch=master - :target: https://travis-ci.org/certbot/certbot +.. |build-status| image:: https://travis-ci.com/certbot/certbot.svg?branch=master + :target: https://travis-ci.com/certbot/certbot :alt: Travis CI status .. |coverage| image:: https://codecov.io/gh/certbot/certbot/branch/master/graph/badge.svg diff -Nru python-certbot-0.28.0/setup.py python-certbot-0.31.0/setup.py --- python-certbot-0.28.0/setup.py 2018-11-07 21:14:57.000000000 +0000 +++ python-certbot-0.31.0/setup.py 2019-02-07 21:20:31.000000000 +0000 @@ -31,13 +31,13 @@ # specified here to avoid masking the more specific request requirements in # acme. See https://github.com/pypa/pip/issues/988 for more info. install_requires = [ - 'acme>=0.26.0', + 'acme>=0.29.0', # We technically need ConfigArgParse 0.10.0 for Python 2.6 support, but # saying so here causes a runtime error against our temporary fork of 0.9.3 # in which we added 2.6 support (see #2243), so we relax the requirement. 'ConfigArgParse>=0.9.3', 'configobj', - 'cryptography>=1.2', # load_pem_x509_certificate + 'cryptography>=1.2.3', # load_pem_x509_certificate 'josepy', 'mock', 'parsedatetime>=1.3', # Calendar.parseDT @@ -68,9 +68,10 @@ ] docs_extras = [ + # If you have Sphinx<1.5.1, you need docutils<0.13.1 + # https://github.com/sphinx-doc/sphinx/issues/3212 'repoze.sphinx.autointerface', - # sphinx.ext.imgconverter - 'Sphinx >=1.6', + 'Sphinx>=1.2', # Annotation support 'sphinx_rtd_theme', ]