Hello community, here is the log from the commit of package python-certbot for openSUSE:Factory checked in at 2018-12-18 14:57:47 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-certbot (Old) and /work/SRC/openSUSE:Factory/.python-certbot.new.28833 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-certbot" Tue Dec 18 14:57:47 2018 rev:6 rq:658302 version:0.29.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-certbot/python-certbot.changes 2018-12-06 12:19:18.861391188 +0100 +++ /work/SRC/openSUSE:Factory/.python-certbot.new.28833/python-certbot.changes 2018-12-18 14:58:15.242263860 +0100 @@ -1,0 +2,18 @@ +Sat Dec 15 06:34:38 UTC 2018 - Thomas Bechtold <[email protected]> + +- update to 0.29.1: + * The default work and log directories have been changed back + to /var/lib/letsencrypt and /var/log/letsencrypt respectively. + * 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. + * 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. + * Update code and dependencies to clean up Resource and Deprecation Warnings. + * Only depend on imgconverter extension for Sphinx >= 1.6 +- update URL + +------------------------------------------------------------------- Old: ---- certbot-0.28.0.tar.gz New: ---- certbot-0.29.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-certbot.spec ++++++ --- /var/tmp/diff_new_pack.p2tm2z/_old 2018-12-18 14:58:15.930262820 +0100 +++ /var/tmp/diff_new_pack.p2tm2z/_new 2018-12-18 14:58:15.934262814 +0100 @@ -18,12 +18,12 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-certbot -Version: 0.28.0 +Version: 0.29.1 Release: 0 Summary: ACME client License: Apache-2.0 Group: Development/Languages/Python -URL: https://github.com/letsencrypt/letsencrypt +URL: https://github.com/certbot/certbot Source: https://files.pythonhosted.org/packages/source/c/certbot/certbot-%{version}.tar.gz BuildRequires: %{python_module acme >= 0.26.0} BuildRequires: %{python_module configargparse >= 0.9.3} @@ -42,7 +42,7 @@ BuildRequires: fdupes BuildRequires: python-rpm-macros BuildRequires: python2-typing -Requires: python-acme >= 0.26.0 +Requires: python-acme >= 0.29.0 Requires: python-configargparse >= 0.9.3 Requires: python-configobj Requires: python-cryptography >= 1.2 ++++++ certbot-0.28.0.tar.gz -> certbot-0.29.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/CHANGELOG.md new/certbot-0.29.1/CHANGELOG.md --- old/certbot-0.28.0/CHANGELOG.md 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/CHANGELOG.md 2018-12-06 00:47:58.000000000 +0100 @@ -2,6 +2,65 @@ Certbot adheres to [Semantic Versioning](http://semver.org/). +## 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 ### Added diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/PKG-INFO new/certbot-0.29.1/PKG-INFO --- old/certbot-0.28.0/PKG-INFO 2018-11-07 22:14:58.000000000 +0100 +++ new/certbot-0.29.1/PKG-INFO 2018-12-06 00:47:59.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: certbot -Version: 0.28.0 +Version: 0.29.1 Summary: ACME client Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/__init__.py new/certbot-0.29.1/certbot/__init__.py --- old/certbot-0.28.0/certbot/__init__.py 2018-11-07 22:14:57.000000000 +0100 +++ new/certbot-0.29.1/certbot/__init__.py 2018-12-06 00:47:59.000000000 +0100 @@ -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.29.1' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/cli.py new/certbot-0.29.1/certbot/cli.py --- old/certbot-0.28.0/certbot/cli.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/cli.py 2018-12-06 00:47:58.000000000 +0100 @@ -172,10 +172,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 +287,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) @@ -856,7 +859,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 +947,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"), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/client.py new/certbot-0.29.1/certbot/client.py --- old/certbot-0.28.0/certbot/client.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/client.py 2018-12-06 00:47:58.000000000 +0100 @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/compat.py new/certbot-0.29.1/certbot/compat.py --- old/certbot-0.28.0/certbot/compat.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/compat.py 2018-12-06 00:47:58.000000000 +0100 @@ -172,3 +172,30 @@ # 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 'fcntl' in sys.modules: + # Linux specific + return LINUX_DEFAULT_FOLDERS[folder_type] + # Windows specific + return WINDOWS_DEFAULT_FOLDERS[folder_type] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/constants.py new/certbot-0.29.1/certbot/constants.py --- old/certbot-0.28.0/certbot/constants.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/constants.py 2018-12-06 00:47:58.000000000 +0100 @@ -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,8 @@ directory_hooks=True, reuse_key=False, disable_renew_updates=False, + eab_hmac_key=None, + eab_kid=None, # Subparsers num=None, @@ -85,9 +87,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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/crypto_util.py new/certbot-0.29.1/certbot/crypto_util.py --- old/certbot-0.28.0/certbot/crypto_util.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/crypto_util.py 2018-12-06 00:47:58.000000000 +0100 @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/display/ops.py new/certbot-0.29.1/certbot/display/ops.py --- old/certbot-0.28.0/certbot/display/ops.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/display/ops.py 2018-12-06 00:47:58.000000000 +0100 @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/main.py new/certbot-0.29.1/certbot/main.py --- old/certbot-0.28.0/certbot/main.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/main.py 2018-12-06 00:47:58.000000000 +0100 @@ -4,7 +4,9 @@ import functools import logging.handlers import os +import random import sys +import time import configobj import josepy as jose @@ -1243,6 +1245,16 @@ :rtype: None """ + if not sys.stdin.isatty(): + # 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. + sleep_time = random.randint(1, 60*8) + logger.info("Non-interactive renewal: random delay of %s seconds", sleep_time) + time.sleep(sleep_time) + try: renewal.handle_renewal_request(config) finally: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/ocsp.py new/certbot-0.29.1/certbot/ocsp.py --- old/certbot-0.28.0/certbot/ocsp.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/ocsp.py 2018-12-06 00:47:58.000000000 +0100 @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/plugins/enhancements_test.py new/certbot-0.29.1/certbot/plugins/enhancements_test.py --- old/certbot-0.28.0/certbot/plugins/enhancements_test.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/plugins/enhancements_test.py 2018-12-06 00:47:58.000000000 +0100 @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/plugins/selection_test.py new/certbot-0.29.1/certbot/plugins/selection_test.py --- old/certbot-0.28.0/certbot/plugins/selection_test.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/plugins/selection_test.py 2018-12-06 00:47:58.000000000 +0100 @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/plugins/standalone_test.py new/certbot-0.29.1/certbot/plugins/standalone_test.py --- old/certbot-0.28.0/certbot/plugins/standalone_test.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/plugins/standalone_test.py 2018-12-06 00:47:58.000000000 +0100 @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/renewal.py new/certbot-0.29.1/certbot/renewal.py --- old/certbot-0.28.0/certbot/renewal.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/renewal.py 2018-12-06 00:47:58.000000000 +0100 @@ -276,8 +276,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() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/storage.py new/certbot-0.29.1/certbot/storage.py --- old/certbot-0.28.0/certbot/storage.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/storage.py 2018-12-06 00:47:58.000000000 +0100 @@ -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): @@ -792,7 +793,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 @@ -1035,9 +1036,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 +1051,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 +1122,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 +1138,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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/cert_manager_test.py new/certbot-0.29.1/certbot/tests/cert_manager_test.py --- old/certbot-0.28.0/certbot/tests/cert_manager_test.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/tests/cert_manager_test.py 2018-12-06 00:47:58.000000000 +0100 @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/cli_test.py new/certbot-0.29.1/certbot/tests/cli_test.py --- old/certbot-0.28.0/certbot/tests/cli_test.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/tests/cli_test.py 2018-12-06 00:47:58.000000000 +0100 @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/client_test.py new/certbot-0.29.1/certbot/tests/client_test.py --- old/certbot-0.28.0/certbot/tests/client_test.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/tests/client_test.py 2018-12-06 00:47:58.000000000 +0100 @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/display/ops_test.py new/certbot-0.29.1/certbot/tests/display/ops_test.py --- old/certbot-0.28.0/certbot/tests/display/ops_test.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/tests/display/ops_test.py 2018-12-06 00:47:58.000000000 +0100 @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/display/util_test.py new/certbot-0.29.1/certbot/tests/display/util_test.py --- old/certbot-0.28.0/certbot/tests/display/util_test.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/tests/display/util_test.py 2018-12-06 00:47:58.000000000 +0100 @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/log_test.py new/certbot-0.29.1/certbot/tests/log_test.py --- old/certbot-0.28.0/certbot/tests/log_test.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/tests/log_test.py 2018-12-06 00:47:58.000000000 +0100 @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/main_test.py new/certbot-0.29.1/certbot/tests/main_test.py --- old/certbot-0.28.0/certbot/tests/main_test.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/tests/main_test.py 2018-12-06 00:47:58.000000000 +0100 @@ -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 @@ -1652,7 +1675,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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/ocsp_test.py new/certbot-0.29.1/certbot/tests/ocsp_test.py --- old/certbot-0.28.0/certbot/tests/ocsp_test.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/tests/ocsp_test.py 2018-12-06 00:47:58.000000000 +0100 @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/renewupdater_test.py new/certbot-0.29.1/certbot/tests/renewupdater_test.py --- old/certbot-0.28.0/certbot/tests/renewupdater_test.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/tests/renewupdater_test.py 2018-12-06 00:47:58.000000000 +0100 @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/storage_test.py new/certbot-0.29.1/certbot/tests/storage_test.py --- old/certbot-0.28.0/certbot/tests/storage_test.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/tests/storage_test.py 2018-12-06 00:47:58.000000000 +0100 @@ -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): @@ -544,6 +546,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 +673,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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/util.py new/certbot-0.29.1/certbot/tests/util.py --- old/certbot-0.28.0/certbot/tests/util.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/tests/util.py 2018-12-06 00:47:58.000000000 +0100 @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/util_test.py new/certbot-0.29.1/certbot/tests/util_test.py --- old/certbot-0.28.0/certbot/tests/util_test.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/certbot/tests/util_test.py 2018-12-06 00:47:58.000000000 +0100 @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot.egg-info/PKG-INFO new/certbot-0.29.1/certbot.egg-info/PKG-INFO --- old/certbot-0.28.0/certbot.egg-info/PKG-INFO 2018-11-07 22:14:58.000000000 +0100 +++ new/certbot-0.29.1/certbot.egg-info/PKG-INFO 2018-12-06 00:47:59.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: certbot -Version: 0.28.0 +Version: 0.29.1 Summary: ACME client Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/certbot.egg-info/requires.txt new/certbot-0.29.1/certbot.egg-info/requires.txt --- old/certbot-0.28.0/certbot.egg-info/requires.txt 2018-11-07 22:14:58.000000000 +0100 +++ new/certbot-0.29.1/certbot.egg-info/requires.txt 2018-12-06 00:47:59.000000000 +0100 @@ -1,4 +1,4 @@ -acme>=0.26.0 +acme>=0.29.0 ConfigArgParse>=0.9.3 configobj cryptography>=1.2 @@ -29,5 +29,5 @@ [docs] repoze.sphinx.autointerface -Sphinx>=1.6 +Sphinx>=1.2 sphinx_rtd_theme diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/docs/cli-help.txt new/certbot-0.29.1/docs/cli-help.txt --- old/certbot-0.28.0/docs/cli-help.txt 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/docs/cli-help.txt 2018-12-06 00:47:58.000000000 +0100 @@ -24,7 +24,7 @@ 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: @@ -67,6 +67,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 +112,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.29.0 (certbot(-auto); OS_NAME OS_VERSION) Authenticator/XXX Installer/YYY (SUBCOMMAND; flags: FLAGS) Py/major.minor.patchlevel). The flags encoded in the @@ -248,8 +252,8 @@ None) --config-dir CONFIG_DIR Configuration directory. (default: /etc/letsencrypt) - --work-dir WORK_DIR Working directory. (default: /var/lib/letsencrypt) - --logs-dir LOGS_DIR Logs directory. (default: /var/log/letsencrypt) + --work-dir WORK_DIR Working directory. (default: /var/letsencrypt/lib) + --logs-dir LOGS_DIR Logs directory. (default: /var/letsencrypt/log) --server SERVER ACME Directory Resource URI. (default: https://acme-v02.api.letsencrypt.org/directory) @@ -261,7 +265,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 @@ -472,13 +477,12 @@ 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) + Path to the Apache 'a2enmod' binary (default: None) --apache-dismod APACHE_DISMOD - Path to the Apache 'a2dismod' binary (default: - a2dismod) + Path to the Apache 'a2dismod' binary (default: None) --apache-le-vhost-ext APACHE_LE_VHOST_EXT SSL vhost configuration extension (default: -le- ssl.conf) @@ -492,25 +496,16 @@ /var/log/apache2) --apache-challenge-location APACHE_CHALLENGE_LOCATION Directory path for challenge configuration (default: - /etc/apache2) + /etc/apache2/other) --apache-handle-modules APACHE_HANDLE_MODULES Let installer handle enabling required modules for you - (Only Ubuntu/Debian currently) (default: True) + (Only Ubuntu/Debian currently) (default: False) --apache-handle-sites APACHE_HANDLE_SITES Let installer handle enabling sites for you (Only - Ubuntu/Debian currently) (default: True) + Ubuntu/Debian currently) (default: False) --apache-ctl APACHE_CTL 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) + apachectl) dns-cloudflare: Obtain certificates using a DNS TXT record (if you are using Cloudflare @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/docs/conf.py new/certbot-0.29.1/docs/conf.py --- old/certbot-0.28.0/docs/conf.py 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/docs/conf.py 2018-12-06 00:47:58.000000000 +0100 @@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/docs/contributing.rst new/certbot-0.29.1/docs/contributing.rst --- old/certbot-0.28.0/docs/contributing.rst 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/docs/contributing.rst 2018-12-06 00:47:58.000000000 +0100 @@ -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. @@ -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 <https://github.com/blog/2141-squash-your-commits>`_ 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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/docs/using.rst new/certbot-0.29.1/docs/using.rst --- old/certbot-0.28.0/docs/using.rst 2018-11-07 22:14:56.000000000 +0100 +++ new/certbot-0.29.1/docs/using.rst 2018-12-06 00:47:58.000000000 +0100 @@ -696,7 +696,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 +717,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 <https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatekeyfile>`_, and Nginx for `ssl_certificate_key diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.28.0/setup.py new/certbot-0.29.1/setup.py --- old/certbot-0.28.0/setup.py 2018-11-07 22:14:57.000000000 +0100 +++ new/certbot-0.29.1/setup.py 2018-12-06 00:47:59.000000000 +0100 @@ -31,7 +31,7 @@ # 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. @@ -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', ]
