Hello community, here is the log from the commit of package python-certbot for openSUSE:Factory checked in at 2019-10-02 14:56:05 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-certbot (Old) and /work/SRC/openSUSE:Factory/.python-certbot.new.2352 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Package is "python-certbot" Wed Oct 2 14:56:05 2019 rev:18 rq:734565 version:0.39.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-certbot/python-certbot.changes 2019-09-13 14:59:03.553279119 +0200 +++ /work/SRC/openSUSE:Factory/.python-certbot.new.2352/python-certbot.changes 2019-10-02 14:56:06.151253827 +0200 @@ -1,0 +2,7 @@ +Wed Oct 2 10:02:37 UTC 2019 - Marketa Calabkova <mcalabkova@suse.com> + +- update to version 0.39.0 + * Support for Python 3.8 was added to Certbot and all of its components. + * Don't send OCSP requests for expired certificates + +------------------------------------------------------------------- Old: ---- certbot-0.38.0.tar.gz New: ---- certbot-0.39.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-certbot.spec ++++++ --- /var/tmp/diff_new_pack.3o0GUS/_old 2019-10-02 14:56:06.991251624 +0200 +++ /var/tmp/diff_new_pack.3o0GUS/_new 2019-10-02 14:56:06.995251613 +0200 @@ -18,7 +18,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-certbot -Version: 0.38.0 +Version: 0.39.0 Release: 0 Summary: ACME client License: Apache-2.0 ++++++ certbot-0.38.0.tar.gz -> certbot-0.39.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/CHANGELOG.md new/certbot-0.39.0/CHANGELOG.md --- old/certbot-0.38.0/CHANGELOG.md 2019-09-03 21:42:35.000000000 +0200 +++ new/certbot-0.39.0/CHANGELOG.md 2019-10-01 21:48:40.000000000 +0200 @@ -2,11 +2,30 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). +## 0.39.0 - 2019-10-01 + +### Added + +* Support for Python 3.8 was added to Certbot and all of its components. +* Support for CentOS 8 was added to certbot-auto. + +### Changed + +* Don't send OCSP requests for expired certificates +* Return to using platform.linux_distribution instead of distro.linux_distribution in OS fingerprinting for Python < 3.8 +* Updated the Nginx plugin's TLS configuration to keep support for some versions of IE11. + +### Fixed + +* Fixed OS detection in the Apache plugin on RHEL 6. + +More details about these changes can be found on our GitHub repo. + ## 0.38.0 - 2019-09-03 ### Added -* +* Disable session tickets for Nginx users when appropriate. ### Changed diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/PKG-INFO new/certbot-0.39.0/PKG-INFO --- old/certbot-0.38.0/PKG-INFO 2019-09-03 21:42:38.000000000 +0200 +++ new/certbot-0.39.0/PKG-INFO 2019-10-01 21:48:41.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: certbot -Version: 0.38.0 +Version: 0.39.0 Summary: ACME client Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project @@ -153,6 +153,7 @@ Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Classifier: Topic :: System :: Installation/Setup diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/certbot/__init__.py new/certbot-0.39.0/certbot/__init__.py --- old/certbot-0.38.0/certbot/__init__.py 2019-09-03 21:42:37.000000000 +0200 +++ new/certbot-0.39.0/certbot/__init__.py 2019-10-01 21:48:41.000000000 +0200 @@ -1,4 +1,4 @@ """Certbot client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 -__version__ = '0.38.0' +__version__ = '0.39.0' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/certbot/cert_manager.py new/certbot-0.39.0/certbot/cert_manager.py --- old/certbot-0.38.0/certbot/cert_manager.py 2019-09-03 21:42:35.000000000 +0200 +++ new/certbot-0.39.0/certbot/cert_manager.py 2019-10-01 21:48:40.000000000 +0200 @@ -262,7 +262,7 @@ reasons.append('TEST_CERT') if cert.target_expiry <= now: reasons.append('EXPIRED') - if checker.ocsp_revoked(cert.cert, cert.chain): + elif checker.ocsp_revoked(cert): reasons.append('REVOKED') if reasons: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/certbot/compat/filesystem.py new/certbot-0.39.0/certbot/compat/filesystem.py --- old/certbot-0.38.0/certbot/compat/filesystem.py 2019-09-03 21:42:35.000000000 +0200 +++ new/certbot-0.39.0/certbot/compat/filesystem.py 2019-10-01 21:48:40.000000000 +0200 @@ -20,7 +20,7 @@ else: POSIX_MODE = False -from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module +from acme.magic_typing import List, Union, Tuple # pylint: disable=unused-import, no-name-in-module def chmod(file_path, mode): @@ -264,6 +264,7 @@ def realpath(file_path): + # type: (str) -> str """ Find the real path for the given path. This method resolves symlinks, including recursive symlinks, and is protected against symlinks that creates an infinite loop. @@ -300,10 +301,11 @@ # requires to be run under a privileged shell, so the user will always benefit # from the highest (privileged one) set of permissions on a given file. def is_executable(path): + # type: (str) -> bool """ Is path an executable file? :param str path: path to test - :returns: True if path is an executable file + :return: True if path is an executable file :rtype: bool """ if POSIX_MODE: @@ -312,6 +314,118 @@ return _win_is_executable(path) +def has_world_permissions(path): + # type: (str) -> bool + """ + Check if everybody/world has any right (read/write/execute) on a file given its path + :param str path: path to test + :return: True if everybody/world has any right to the file + :rtype: bool + """ + if POSIX_MODE: + return bool(stat.S_IMODE(os.stat(path).st_mode) & stat.S_IRWXO) + + security = win32security.GetFileSecurity(path, win32security.DACL_SECURITY_INFORMATION) + dacl = security.GetSecurityDescriptorDacl() + + return bool(dacl.GetEffectiveRightsFromAcl({ + 'TrusteeForm': win32security.TRUSTEE_IS_SID, + 'TrusteeType': win32security.TRUSTEE_IS_USER, + 'Identifier': win32security.ConvertStringSidToSid('S-1-1-0'), + })) + + +def compute_private_key_mode(old_key, base_mode): + # type: (str, int) -> int + """ + Calculate the POSIX mode to apply to a private key given the previous private key + :param str old_key: path to the previous private key + :param int base_mode: the minimum modes to apply to a private key + :return: the POSIX mode to apply + :rtype: int + """ + if POSIX_MODE: + # On Linux, we keep read/write/execute permissions + # for group and read permissions for everybody. + old_mode = (stat.S_IMODE(os.stat(old_key).st_mode) & + (stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP | stat.S_IROTH)) + return base_mode | old_mode + + # On Windows, the mode returned by os.stat is not reliable, + # so we do not keep any permission from the previous private key. + return base_mode + + +def has_same_ownership(path1, path2): + # type: (str, str) -> bool + """ + Return True if the ownership of two files given their respective path is the same. + On Windows, ownership is checked against owner only, since files do not have a group owner. + :param str path1: path to the first file + :param str path2: path to the second file + :return: True if both files have the same ownership, False otherwise + :rtype: bool + + """ + if POSIX_MODE: + stats1 = os.stat(path1) + stats2 = os.stat(path2) + return (stats1.st_uid, stats1.st_gid) == (stats2.st_uid, stats2.st_gid) + + security1 = win32security.GetFileSecurity(path1, win32security.OWNER_SECURITY_INFORMATION) + user1 = security1.GetSecurityDescriptorOwner() + + security2 = win32security.GetFileSecurity(path2, win32security.OWNER_SECURITY_INFORMATION) + user2 = security2.GetSecurityDescriptorOwner() + + return user1 == user2 + + +def has_min_permissions(path, min_mode): + # type: (str, int) -> bool + """ + Check if a file given its path has at least the permissions defined by the given minimal mode. + On Windows, group permissions are ignored since files do not have a group owner. + :param str path: path to the file to check + :param int min_mode: the minimal permissions expected + :return: True if the file matches the minimal permissions expectations, False otherwise + :rtype: bool + """ + if POSIX_MODE: + st_mode = os.stat(path).st_mode + return st_mode == st_mode | min_mode + + # Resolve symlinks, to get a consistent result with os.stat on Linux, + # that follows symlinks by default. + path = realpath(path) + + # Get owner sid of the file + security = win32security.GetFileSecurity( + path, win32security.OWNER_SECURITY_INFORMATION | win32security.DACL_SECURITY_INFORMATION) + user = security.GetSecurityDescriptorOwner() + dacl = security.GetSecurityDescriptorDacl() + min_dacl = _generate_dacl(user, min_mode) + + for index in range(min_dacl.GetAceCount()): + min_ace = min_dacl.GetAce(index) + + # On a given ACE, index 0 is the ACE type, 1 is the permission mask, and 2 is the SID. + # See: http://timgolden.me.uk/pywin32-docs/PyACL__GetAce_meth.html + mask = min_ace[1] + user = min_ace[2] + + effective_mask = dacl.GetEffectiveRightsFromAcl({ + 'TrusteeForm': win32security.TRUSTEE_IS_SID, + 'TrusteeType': win32security.TRUSTEE_IS_USER, + 'Identifier': user, + }) + + if effective_mask != effective_mask | mask: + return False + + return True + + def _win_is_executable(path): if not os.path.isfile(path): return False @@ -472,8 +586,8 @@ This method compare the two given DACLs to check if they are identical. Identical means here that they contains the same set of ACEs in the same order. """ - return ([dacl1.GetAce(index) for index in range(0, dacl1.GetAceCount())] == - [dacl2.GetAce(index) for index in range(0, dacl2.GetAceCount())]) + return ([dacl1.GetAce(index) for index in range(dacl1.GetAceCount())] == + [dacl2.GetAce(index) for index in range(dacl2.GetAceCount())]) def _get_current_user(): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/certbot/compat/misc.py new/certbot-0.39.0/certbot/compat/misc.py --- old/certbot-0.38.0/certbot/compat/misc.py 2019-09-03 21:42:35.000000000 +0200 +++ new/certbot-0.39.0/certbot/compat/misc.py 2019-10-01 21:48:40.000000000 +0200 @@ -5,7 +5,6 @@ from __future__ import absolute_import import select -import stat import sys try: @@ -18,18 +17,6 @@ from certbot.compat import os -# MASK_FOR_PRIVATE_KEY_PERMISSIONS defines what are the permissions flags to keep -# when transferring the permissions from an old private key to a new one. -if POSIX_MODE: - # On Linux, we keep read/write/execute permissions - # for group and read permissions for everybody. - MASK_FOR_PRIVATE_KEY_PERMISSIONS = stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP | stat.S_IROTH -else: - # On Windows, the mode returned by os.stat is not reliable, - # so we do not keep any permission from the previous private key. - MASK_FOR_PRIVATE_KEY_PERMISSIONS = 0 - - # For Linux: define OS specific standard binary directories STANDARD_BINARY_DIRS = ["/usr/sbin", "/usr/local/bin", "/usr/local/sbin"] if POSIX_MODE else [] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/certbot/compat/os.py new/certbot-0.39.0/certbot/compat/os.py --- old/certbot-0.38.0/certbot/compat/os.py 2019-09-03 21:42:35.000000000 +0200 +++ new/certbot-0.39.0/certbot/compat/os.py 2019-10-01 21:48:40.000000000 +0200 @@ -116,3 +116,21 @@ raise RuntimeError('Usage of os.access() is forbidden. ' 'Use certbot.compat.filesystem.check_mode() or ' 'certbot.compat.filesystem.is_executable() instead.') + + +# On Windows os.stat call result is inconsistent, with a lot of flags that are not set or +# meaningless. We need to use specialized functions from the certbot.compat.filesystem module. +def stat(*unused_args, **unused_kwargs): + """Method os.stat() is forbidden""" + raise RuntimeError('Usage of os.stat() is forbidden. ' + 'Use certbot.compat.filesystem functions instead ' + '(eg. has_min_permissions, has_same_ownership).') + + +# Method os.fstat has the same problem than os.stat, since it is the same function, +# but accepting a file descriptor instead of a path. +def fstat(*unused_args, **unused_kwargs): + """Method os.stat() is forbidden""" + raise RuntimeError('Usage of os.fstat() is forbidden. ' + 'Use certbot.compat.filesystem functions instead ' + '(eg. has_min_permissions, has_same_ownership).') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/certbot/lock.py new/certbot-0.39.0/certbot/lock.py --- old/certbot-0.38.0/certbot/lock.py 2019-09-03 21:42:35.000000000 +0200 +++ new/certbot-0.39.0/certbot/lock.py 2019-10-01 21:48:40.000000000 +0200 @@ -167,14 +167,18 @@ :returns: True if the lock was successfully acquired :rtype: bool """ + # Normally os module should not be imported in certbot codebase except in certbot.compat + # for the sake of compatibility over Windows and Linux. + # We make an exception here, since _lock_success is private and called only on Linux. + from os import stat, fstat # pylint: disable=os-module-forbidden try: - stat1 = os.stat(self._path) + stat1 = stat(self._path) except OSError as err: if err.errno == errno.ENOENT: return False raise - stat2 = os.fstat(fd) + stat2 = fstat(fd) # If our locked file descriptor and the file on disk refer to # the same device and inode, they're the same file. return stat1.st_dev == stat2.st_dev and stat1.st_ino == stat2.st_ino diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/certbot/ocsp.py new/certbot-0.39.0/certbot/ocsp.py --- old/certbot-0.38.0/certbot/ocsp.py 2019-09-03 21:42:35.000000000 +0200 +++ new/certbot-0.39.0/certbot/ocsp.py 2019-10-01 21:48:40.000000000 +0200 @@ -16,11 +16,13 @@ from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import hashes # type: ignore from cryptography.exceptions import UnsupportedAlgorithm, InvalidSignature +import pytz import requests from acme.magic_typing import Optional, Tuple # pylint: disable=unused-import, no-name-in-module from certbot import crypto_util from certbot import errors +from certbot.storage import RenewableCert # pylint: disable=unused-import from certbot import util logger = logging.getLogger(__name__) @@ -48,21 +50,29 @@ else: self.host_args = lambda host: ["Host", host] - def ocsp_revoked(self, cert_path, chain_path): - # type: (str, str) -> bool + def ocsp_revoked(self, cert): + # type: (RenewableCert) -> bool """Get revoked status for a particular cert version. .. todo:: Make this a non-blocking call - :param str cert_path: Path to certificate - :param str chain_path: Path to intermediate cert - :returns: True if revoked; False if valid or the check failed + :param `.storage.RenewableCert` cert: Certificate object + :returns: True if revoked; False if valid or the check failed or cert is expired. :rtype: bool """ + cert_path, chain_path = cert.cert, cert.chain + if self.broken: return False + # Let's Encrypt doesn't update OCSP for expired certificates, + # so don't check OCSP if the cert is expired. + # https://github.com/certbot/certbot/issues/7152 + now = pytz.UTC.fromutc(datetime.utcnow()) + if cert.target_expiry <= now: + return False + url, host = _determine_ocsp_server(cert_path) if not host or not url: return False diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/certbot/plugins/dns_common.py new/certbot-0.39.0/certbot/plugins/dns_common.py --- old/certbot-0.38.0/certbot/plugins/dns_common.py 2019-09-03 21:42:35.000000000 +0200 +++ new/certbot-0.39.0/certbot/plugins/dns_common.py 2019-10-01 21:48:40.000000000 +0200 @@ -2,7 +2,6 @@ import abc import logging -import stat from time import sleep import configobj @@ -12,6 +11,7 @@ from certbot import errors from certbot import interfaces +from certbot.compat import filesystem from certbot.compat import os from certbot.display import ops from certbot.display import util as display_util @@ -312,8 +312,7 @@ validate_file(filename) - permissions = stat.S_IMODE(os.stat(filename).st_mode) - if permissions & stat.S_IRWXO: + if filesystem.has_world_permissions(filename): logger.warning('Unsafe permissions on credentials configuration file: %s', filename) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/certbot/plugins/dns_common_test.py new/certbot-0.39.0/certbot/plugins/dns_common_test.py --- old/certbot-0.38.0/certbot/plugins/dns_common_test.py 2019-09-03 21:42:35.000000000 +0200 +++ new/certbot-0.39.0/certbot/plugins/dns_common_test.py 2019-10-01 21:48:40.000000000 +0200 @@ -7,14 +7,15 @@ import mock from certbot import errors +from certbot import util from certbot.compat import os from certbot.display import util as display_util from certbot.plugins import dns_common from certbot.plugins import dns_test_common -from certbot.tests import util +from certbot.tests import util as test_util -class DNSAuthenticatorTest(util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest): +class DNSAuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest): # pylint: disable=protected-access class _FakeDNSAuthenticator(dns_common.DNSAuthenticator): @@ -50,7 +51,7 @@ self.auth._cleanup.assert_called_once_with(dns_test_common.DOMAIN, mock.ANY, mock.ANY) - @util.patch_get_utility() + @test_util.patch_get_utility() def test_prompt(self, mock_get_utility): mock_display = mock_get_utility() mock_display.input.side_effect = ((display_util.OK, "",), @@ -59,14 +60,14 @@ self.auth._configure("other_key", "") self.assertEqual(self.auth.config.fake_other_key, "value") - @util.patch_get_utility() + @test_util.patch_get_utility() def test_prompt_canceled(self, mock_get_utility): mock_display = mock_get_utility() mock_display.input.side_effect = ((display_util.CANCEL, "c",),) self.assertRaises(errors.PluginError, self.auth._configure, "other_key", "") - @util.patch_get_utility() + @test_util.patch_get_utility() def test_prompt_file(self, mock_get_utility): path = os.path.join(self.tempdir, 'file.ini') open(path, "wb").close() @@ -80,7 +81,7 @@ self.auth._configure_file("file_path", "") self.assertEqual(self.auth.config.fake_file_path, path) - @util.patch_get_utility() + @test_util.patch_get_utility() def test_prompt_file_canceled(self, mock_get_utility): mock_display = mock_get_utility() mock_display.directory_select.side_effect = ((display_util.CANCEL, "c",),) @@ -96,7 +97,7 @@ self.assertEqual(credentials.conf("test"), "value") - @util.patch_get_utility() + @test_util.patch_get_utility() def test_prompt_credentials(self, mock_get_utility): bad_path = os.path.join(self.tempdir, 'bad-file.ini') dns_test_common.write({"fake_other": "other_value"}, bad_path) @@ -116,7 +117,7 @@ self.assertEqual(credentials.conf("test"), "value") -class CredentialsConfigurationTest(util.TempDirTestCase): +class CredentialsConfigurationTest(test_util.TempDirTestCase): class _MockLoggingHandler(logging.Handler): messages = None @@ -150,14 +151,14 @@ dns_common.logger.addHandler(log) path = os.path.join(self.tempdir, 'too-permissive-file.ini') - open(path, "wb").close() + util.safe_open(path, "wb", 0o744).close() dns_common.CredentialsConfiguration(path) self.assertEqual(1, len([_ for _ in log.messages['warning'] if _.startswith("Unsafe")])) -class CredentialsConfigurationRequireTest(util.TempDirTestCase): +class CredentialsConfigurationRequireTest(test_util.TempDirTestCase): def setUp(self): super(CredentialsConfigurationRequireTest, self).setUp() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/certbot/plugins/webroot_test.py new/certbot-0.39.0/certbot/plugins/webroot_test.py --- old/certbot-0.38.0/certbot/plugins/webroot_test.py 2019-09-03 21:42:35.000000000 +0200 +++ new/certbot-0.39.0/certbot/plugins/webroot_test.py 2019-10-01 21:48:40.000000000 +0200 @@ -34,7 +34,13 @@ def setUp(self): from certbot.plugins.webroot import Authenticator - self.path = tempfile.mkdtemp() + # On Linux directories created by tempfile.mkdtemp inherit their permissions from their + # parent directory. So the actual permissions are inconsistent over various tests env. + # To circumvent this, a dedicated sub-workspace is created under the workspace, using + # filesystem.mkdir to get consistent permissions. + self.workspace = tempfile.mkdtemp() + self.path = os.path.join(self.workspace, 'webroot') + filesystem.mkdir(self.path) self.partial_root_challenge_path = os.path.join( self.path, ".well-known") self.root_challenge_path = os.path.join( @@ -170,17 +176,12 @@ self.assertTrue(filesystem.check_mode(self.validation_path, 0o644)) # Check permissions of the directories - for dirpath, dirnames, _ in os.walk(self.path): for directory in dirnames: full_path = os.path.join(dirpath, directory) self.assertTrue(filesystem.check_mode(full_path, 0o755)) - parent_gid = os.stat(self.path).st_gid - parent_uid = os.stat(self.path).st_uid - - self.assertEqual(os.stat(self.validation_path).st_gid, parent_gid) - self.assertEqual(os.stat(self.validation_path).st_uid, parent_uid) + self.assertTrue(filesystem.has_same_ownership(self.validation_path, self.path)) def test_perform_cleanup(self): self.auth.prepare() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/certbot/storage.py new/certbot-0.39.0/certbot/storage.py --- old/certbot-0.38.0/certbot/storage.py 2019-09-03 21:42:35.000000000 +0200 +++ new/certbot-0.39.0/certbot/storage.py 2019-10-01 21:48:40.000000000 +0200 @@ -18,7 +18,6 @@ from certbot import error_handler from certbot import errors from certbot import util -from certbot.compat import misc from certbot.compat import os from certbot.compat import filesystem from certbot.plugins import common as plugins_common @@ -1107,9 +1106,7 @@ f.write(new_privkey) # Preserve gid and (mode & MASK_FOR_PRIVATE_KEY_PERMISSIONS) # from previous privkey in this lineage. - old_mode = (stat.S_IMODE(os.stat(old_privkey).st_mode) & - misc.MASK_FOR_PRIVATE_KEY_PERMISSIONS) - mode = BASE_PRIVKEY_MODE | old_mode + mode = filesystem.compute_private_key_mode(old_privkey, BASE_PRIVKEY_MODE) filesystem.copy_ownership_and_apply_mode( old_privkey, target["privkey"], mode, copy_user=False, copy_group=True) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/certbot/tests/compat/filesystem_test.py new/certbot-0.39.0/certbot/tests/compat/filesystem_test.py --- old/certbot-0.38.0/certbot/tests/compat/filesystem_test.py 2019-09-03 21:42:35.000000000 +0200 +++ new/certbot-0.39.0/certbot/tests/compat/filesystem_test.py 2019-10-01 21:48:40.000000000 +0200 @@ -150,6 +150,24 @@ self.assertEqual(security_dacl.GetSecurityDescriptorDacl().GetAceCount(), 2) +class ComputePrivateKeyModeTest(TempDirTestCase): + def setUp(self): + super(ComputePrivateKeyModeTest, self).setUp() + self.probe_path = _create_probe(self.tempdir) + + def test_compute_private_key_mode(self): + filesystem.chmod(self.probe_path, 0o777) + new_mode = filesystem.compute_private_key_mode(self.probe_path, 0o600) + + if POSIX_MODE: + # On Linux RWX permissions for group and R permission for world + # are persisted from the existing moe + self.assertEqual(new_mode, 0o674) + else: + # On Windows no permission is persisted + self.assertEqual(new_mode, 0o600) + + @unittest.skipIf(POSIX_MODE, reason='Tests specific to Windows security') class WindowsOpenTest(TempDirTestCase): def test_new_file_correct_permissions(self): @@ -262,14 +280,14 @@ self.assertEqual(original_mkdir, std_os.mkdir) -class CopyOwnershipTest(test_util.TempDirTestCase): - """Tests about replacement of chown: copy_ownership_and_apply_mode""" +class OwnershipTest(test_util.TempDirTestCase): + """Tests about copy_ownership_and_apply_mode and has_same_ownership""" def setUp(self): - super(CopyOwnershipTest, self).setUp() + super(OwnershipTest, self).setUp() self.probe_path = _create_probe(self.tempdir) @unittest.skipIf(POSIX_MODE, reason='Test specific to Windows security') - def test_windows(self): + def test_copy_ownership_windows(self): system = win32security.ConvertStringSidToSid(SYSTEM_SID) security = win32security.SECURITY_ATTRIBUTES().SECURITY_DESCRIPTOR security.SetSecurityDescriptorOwner(system, False) @@ -295,7 +313,7 @@ if dacl.GetAce(index)[2] == everybody]) @unittest.skipUnless(POSIX_MODE, reason='Test specific to Linux security') - def test_linux(self): + def test_copy_ownership_linux(self): with mock.patch('os.chown') as mock_chown: with mock.patch('os.chmod') as mock_chmod: with mock.patch('os.stat') as mock_stat: @@ -307,8 +325,18 @@ mock_chown.assert_called_once_with(self.probe_path, 50, 51) mock_chmod.assert_called_once_with(self.probe_path, 0o700) + def test_has_same_ownership(self): + path1 = os.path.join(self.tempdir, 'test1') + path2 = os.path.join(self.tempdir, 'test2') + + util.safe_open(path1, 'w').close() + util.safe_open(path2, 'w').close() + + self.assertTrue(filesystem.has_same_ownership(path1, path2)) + class CheckPermissionsTest(test_util.TempDirTestCase): + """Tests relative to functions that check modes.""" def setUp(self): super(CheckPermissionsTest, self).setUp() self.probe_path = _create_probe(self.tempdir) @@ -353,6 +381,23 @@ mock_owner.return_value = False self.assertFalse(filesystem.check_permissions(self.probe_path, 0o744)) + def test_check_min_permissions(self): + filesystem.chmod(self.probe_path, 0o744) + self.assertTrue(filesystem.has_min_permissions(self.probe_path, 0o744)) + + filesystem.chmod(self.probe_path, 0o700) + self.assertFalse(filesystem.has_min_permissions(self.probe_path, 0o744)) + + filesystem.chmod(self.probe_path, 0o741) + self.assertFalse(filesystem.has_min_permissions(self.probe_path, 0o744)) + + def test_is_world_reachable(self): + filesystem.chmod(self.probe_path, 0o744) + self.assertTrue(filesystem.has_world_permissions(self.probe_path)) + + filesystem.chmod(self.probe_path, 0o700) + self.assertFalse(filesystem.has_world_permissions(self.probe_path)) + class OsReplaceTest(test_util.TempDirTestCase): """Test to ensure consistent behavior of rename method""" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/certbot/tests/compat/os_test.py new/certbot-0.39.0/certbot/tests/compat/os_test.py --- old/certbot-0.38.0/certbot/tests/compat/os_test.py 2019-09-03 21:42:35.000000000 +0200 +++ new/certbot-0.39.0/certbot/tests/compat/os_test.py 2019-10-01 21:48:40.000000000 +0200 @@ -8,8 +8,8 @@ """Unit tests for os module.""" def test_forbidden_methods(self): # Checks for os module - for method in ['chmod', 'chown', 'open', 'mkdir', - 'makedirs', 'rename', 'replace', 'access']: + for method in ['chmod', 'chown', 'open', 'mkdir', 'makedirs', 'rename', + 'replace', 'access', 'stat', 'fstat']: self.assertRaises(RuntimeError, getattr(os, method)) # Checks for os.path module for method in ['realpath']: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/certbot/tests/lock_test.py new/certbot-0.39.0/certbot/tests/lock_test.py --- old/certbot-0.38.0/certbot/tests/lock_test.py 2019-09-03 21:42:35.000000000 +0200 +++ new/certbot-0.39.0/certbot/tests/lock_test.py 2019-10-01 21:48:40.000000000 +0200 @@ -82,7 +82,10 @@ 'Race conditions on lock are specific to the non-blocking file access approach on Linux.') def test_race(self): should_delete = [True, False] - stat = os.stat + # Normally os module should not be imported in certbot codebase except in certbot.compat + # for the sake of compatibility over Windows and Linux. + # We make an exception here, since test_race is a test function called only on Linux. + from os import stat # pylint: disable=os-module-forbidden def delete_and_stat(path): """Wrap os.stat and maybe delete the file first.""" @@ -90,7 +93,7 @@ os.remove(path) return stat(path) - with mock.patch('certbot.lock.os.stat') as mock_stat: + with mock.patch('certbot.lock.filesystem.os.stat') as mock_stat: mock_stat.side_effect = delete_and_stat self._call(self.lock_path) self.assertFalse(should_delete) @@ -117,7 +120,7 @@ def test_unexpected_os_err(self): if POSIX_MODE: - mock_function = 'certbot.lock.os.stat' + mock_function = 'certbot.lock.filesystem.os.stat' else: mock_function = 'certbot.lock.msvcrt.locking' # The only expected errno are ENOENT and EACCES in lock module. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/certbot/tests/ocsp_test.py new/certbot-0.39.0/certbot/tests/ocsp_test.py --- old/certbot-0.38.0/certbot/tests/ocsp_test.py 2019-09-03 21:42:35.000000000 +0200 +++ new/certbot-0.39.0/certbot/tests/ocsp_test.py 2019-10-01 21:48:40.000000000 +0200 @@ -16,6 +16,7 @@ except (ImportError, AttributeError): # pragma: no cover ocsp_lib = None # type: ignore import mock +import pytz from certbot import errors from certbot.tests import util as test_util @@ -72,21 +73,34 @@ @mock.patch('certbot.ocsp._determine_ocsp_server') @mock.patch('certbot.util.run_script') def test_ocsp_revoked(self, mock_run, mock_determine): + now = pytz.UTC.fromutc(datetime.utcnow()) + cert_obj = mock.MagicMock() + cert_obj.cert = "x" + cert_obj.chain = "y" + cert_obj.target_expiry = now + timedelta(hours=2) + self.checker.broken = True mock_determine.return_value = ("", "") - self.assertEqual(self.checker.ocsp_revoked("x", "y"), False) + self.assertEqual(self.checker.ocsp_revoked(cert_obj), False) self.checker.broken = False mock_run.return_value = tuple(openssl_happy[1:]) - self.assertEqual(self.checker.ocsp_revoked("x", "y"), False) + self.assertEqual(self.checker.ocsp_revoked(cert_obj), False) self.assertEqual(mock_run.call_count, 0) mock_determine.return_value = ("http://x.co", "x.co") - self.assertEqual(self.checker.ocsp_revoked("blah.pem", "chain.pem"), False) + self.assertEqual(self.checker.ocsp_revoked(cert_obj), False) mock_run.side_effect = errors.SubprocessError("Unable to load certificate launcher") - self.assertEqual(self.checker.ocsp_revoked("x", "y"), False) + self.assertEqual(self.checker.ocsp_revoked(cert_obj), False) self.assertEqual(mock_run.call_count, 2) + # cert expired + cert_obj.target_expiry = now + mock_determine.return_value = ("", "") + count_before = mock_determine.call_count + self.assertEqual(self.checker.ocsp_revoked(cert_obj), False) + self.assertEqual(mock_determine.call_count, count_before) + def test_determine_ocsp_server(self): cert_path = test_util.vector_path('ocsp_certificate.pem') @@ -131,18 +145,23 @@ self.checker = ocsp.RevocationChecker() self.cert_path = test_util.vector_path('ocsp_certificate.pem') self.chain_path = test_util.vector_path('ocsp_issuer_certificate.pem') + self.cert_obj = mock.MagicMock() + self.cert_obj.cert = self.cert_path + self.cert_obj.chain = self.chain_path + now = pytz.UTC.fromutc(datetime.utcnow()) + self.cert_obj.target_expiry = now + timedelta(hours=2) @mock.patch('certbot.ocsp._determine_ocsp_server') @mock.patch('certbot.ocsp._check_ocsp_cryptography') def test_ensure_cryptography_toggled(self, mock_revoke, mock_determine): mock_determine.return_value = ('http://example.com', 'example.com') - self.checker.ocsp_revoked(self.cert_path, self.chain_path) + self.checker.ocsp_revoked(self.cert_obj) mock_revoke.assert_called_once_with(self.cert_path, self.chain_path, 'http://example.com') def test_revoke(self): with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL): - revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path) + revoked = self.checker.ocsp_revoked(self.cert_obj) self.assertTrue(revoked) def test_responder_is_issuer(self): @@ -152,7 +171,7 @@ with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL) as mocks: mocks['mock_response'].return_value.responder_name = issuer.subject - self.checker.ocsp_revoked(self.cert_path, self.chain_path) + self.checker.ocsp_revoked(self.cert_obj) # Here responder and issuer are the same. So only the signature of the OCSP # response is checked (using the issuer/responder public key). self.assertEqual(mocks['mock_check'].call_count, 1) @@ -167,7 +186,7 @@ with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL) as mocks: - self.checker.ocsp_revoked(self.cert_path, self.chain_path) + self.checker.ocsp_revoked(self.cert_obj) # Here responder and issuer are not the same. Two signatures will be checked then, # first to verify the responder cert (using the issuer public key), second to # to verify the OCSP response itself (using the responder public key). @@ -181,17 +200,17 @@ # Server return an invalid HTTP response with _ocsp_mock(ocsp_lib.OCSPCertStatus.UNKNOWN, ocsp_lib.OCSPResponseStatus.SUCCESSFUL, http_status_code=400): - revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path) + revoked = self.checker.ocsp_revoked(self.cert_obj) self.assertFalse(revoked) # OCSP response in invalid with _ocsp_mock(ocsp_lib.OCSPCertStatus.UNKNOWN, ocsp_lib.OCSPResponseStatus.UNAUTHORIZED): - revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path) + revoked = self.checker.ocsp_revoked(self.cert_obj) self.assertFalse(revoked) # OCSP response is valid, but certificate status is unknown with _ocsp_mock(ocsp_lib.OCSPCertStatus.UNKNOWN, ocsp_lib.OCSPResponseStatus.SUCCESSFUL): - revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path) + revoked = self.checker.ocsp_revoked(self.cert_obj) self.assertFalse(revoked) # The OCSP response says that the certificate is revoked, but certificate @@ -200,32 +219,32 @@ with mock.patch('cryptography.x509.Extensions.get_extension_for_class', side_effect=x509.ExtensionNotFound( 'Not found', x509.AuthorityInformationAccessOID.OCSP)): - revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path) + revoked = self.checker.ocsp_revoked(self.cert_obj) self.assertFalse(revoked) # OCSP response uses an unsupported signature. with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL, check_signature_side_effect=UnsupportedAlgorithm('foo')): - revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path) + revoked = self.checker.ocsp_revoked(self.cert_obj) self.assertFalse(revoked) # OSCP signature response is invalid. with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL, check_signature_side_effect=InvalidSignature('foo')): - revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path) + revoked = self.checker.ocsp_revoked(self.cert_obj) self.assertFalse(revoked) # Assertion error on OCSP response validity with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL, check_signature_side_effect=AssertionError('foo')): - revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path) + revoked = self.checker.ocsp_revoked(self.cert_obj) self.assertFalse(revoked) # No responder cert in OCSP response with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL) as mocks: mocks['mock_response'].return_value.certificates = [] - revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path) + revoked = self.checker.ocsp_revoked(self.cert_obj) self.assertFalse(revoked) # Responder cert is not signed by certificate issuer @@ -234,7 +253,7 @@ cert = mocks['mock_response'].return_value.certificates[0] mocks['mock_response'].return_value.certificates[0] = mock.Mock( issuer='fake', subject=cert.subject) - revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path) + revoked = self.checker.ocsp_revoked(self.cert_obj) self.assertFalse(revoked) with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL): @@ -245,7 +264,7 @@ with mock.patch('cryptography.x509.Extensions.get_extension_for_class', side_effect=x509.ExtensionNotFound( 'Not found', x509.AuthorityInformationAccessOID.OCSP)): - revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path) + revoked = self.checker.ocsp_revoked(self.cert_obj) self.assertFalse(revoked) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/certbot/tests/util_test.py new/certbot-0.39.0/certbot/tests/util_test.py --- old/certbot-0.38.0/certbot/tests/util_test.py 2019-09-03 21:42:35.000000000 +0200 +++ new/certbot-0.39.0/certbot/tests/util_test.py 2019-10-01 21:48:40.000000000 +0200 @@ -520,13 +520,16 @@ with mock.patch('platform.system_alias', return_value=('linux', '', '')): - with mock.patch('distro.linux_distribution', - return_value=('', '', '')): - self.assertEqual(get_python_os_info(), ("linux", "")) + with mock.patch('platform.linux_distribution', + side_effect=AttributeError, + create=True): + with mock.patch('distro.linux_distribution', + return_value=('', '', '')): + self.assertEqual(get_python_os_info(), ("linux", "")) - with mock.patch('distro.linux_distribution', - return_value=('testdist', '42', '')): - self.assertEqual(get_python_os_info(), ("testdist", "42")) + with mock.patch('distro.linux_distribution', + return_value=('testdist', '42', '')): + self.assertEqual(get_python_os_info(), ("testdist", "42")) with mock.patch('platform.system_alias', return_value=('freebsd', '9.3-RC3-p1', '')): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/certbot/util.py new/certbot-0.39.0/certbot/util.py --- old/certbot-0.38.0/certbot/util.py 2019-09-03 21:42:35.000000000 +0200 +++ new/certbot-0.39.0/certbot/util.py 2019-10-01 21:48:40.000000000 +0200 @@ -392,7 +392,7 @@ os_type, os_ver, _ = info os_type = os_type.lower() if os_type.startswith('linux'): - info = distro.linux_distribution() + info = _get_linux_distribution() # On arch, distro.linux_distribution() is reportedly ('','',''), # so handle it defensively if info[0]: @@ -424,6 +424,14 @@ os_ver = '' return os_type, os_ver +def _get_linux_distribution(): + """Gets the linux distribution name from the underlying OS""" + + try: + return platform.linux_distribution() + except AttributeError: + return distro.linux_distribution() + # Just make sure we don't get pwned... Make sure that it also doesn't # start with a period or have two consecutive periods <- this needs to # be done in addition to the regex diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/certbot.egg-info/PKG-INFO new/certbot-0.39.0/certbot.egg-info/PKG-INFO --- old/certbot-0.38.0/certbot.egg-info/PKG-INFO 2019-09-03 21:42:37.000000000 +0200 +++ new/certbot-0.39.0/certbot.egg-info/PKG-INFO 2019-10-01 21:48:41.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: certbot -Version: 0.38.0 +Version: 0.39.0 Summary: ACME client Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project @@ -153,6 +153,7 @@ Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Classifier: Topic :: System :: Installation/Setup diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/docs/cli-help.txt new/certbot-0.39.0/docs/cli-help.txt --- old/certbot-0.38.0/docs/cli-help.txt 2019-09-03 21:42:35.000000000 +0200 +++ new/certbot-0.39.0/docs/cli-help.txt 2019-10-01 21:48:40.000000000 +0200 @@ -113,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.37.2 + "". (default: CertbotACMEClient/0.38.0 (certbot(-auto); OS_NAME OS_VERSION) Authenticator/XXX Installer/YYY (SUBCOMMAND; flags: FLAGS) Py/major.minor.patchlevel). The flags encoded in the diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/docs/using.rst new/certbot-0.39.0/docs/using.rst --- old/certbot-0.38.0/docs/using.rst 2019-09-03 21:42:35.000000000 +0200 +++ new/certbot-0.39.0/docs/using.rst 2019-10-01 21:48:40.000000000 +0200 @@ -276,10 +276,8 @@ gandi_ Y N Obtain certificates via the Gandi LiveDNS API varnish_ Y N Obtain certificates via a Varnish server external_ Y N A plugin for convenient scripting (See also ticket 2782_) -icecast_ N Y Deploy certificates to Icecast 2 streaming media servers pritunl_ N Y Install certificates in pritunl distributed OpenVPN servers proxmox_ N Y Install certificates in Proxmox Virtualization servers -heroku_ Y Y Integration with Heroku SSL dns-standalone_ Y N Obtain certificates via an integrated DNS server dns-ispconfig_ Y N DNS Authentication using ISPConfig as DNS server ================== ==== ==== =============================================================== @@ -287,13 +285,11 @@ .. _haproxy: https://github.com/greenhost/certbot-haproxy .. _s3front: https://github.com/dlapiduz/letsencrypt-s3front .. _gandi: https://github.com/obynio/certbot-plugin-gandi -.. _icecast: https://github.com/e00E/lets-encrypt-icecast .. _varnish: http://git.sesse.net/?p=letsencrypt-varnish-plugin .. _2782: https://github.com/certbot/certbot/issues/2782 .. _pritunl: https://github.com/kharkevich/letsencrypt-pritunl .. _proxmox: https://github.com/kharkevich/letsencrypt-proxmox .. _external: https://github.com/marcan/letsencrypt-external -.. _heroku: https://github.com/gboudreau/certbot-heroku .. _dns-standalone: https://github.com/siilike/certbot-dns-standalone .. _dns-ispconfig: https://github.com/m42e/certbot-dns-ispconfig diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-0.38.0/setup.py new/certbot-0.39.0/setup.py --- old/certbot-0.38.0/setup.py 2019-09-03 21:42:36.000000000 +0200 +++ new/certbot-0.39.0/setup.py 2019-10-01 21:48:41.000000000 +0200 @@ -59,11 +59,17 @@ # However environment markers are supported only with setuptools >= 36.2. # So this dependency is not added for old Linux distributions with old setuptools, # in order to allow these systems to build certbot from sources. +pywin32_req = 'pywin32>=224' if StrictVersion(setuptools_version) >= StrictVersion('36.2'): - install_requires.append("pywin32>=224 ; sys_platform == 'win32'") + install_requires.append(pywin32_req + " ; sys_platform == 'win32'") elif 'bdist_wheel' in sys.argv[1:]: raise RuntimeError('Error, you are trying to build certbot wheels using an old version ' 'of setuptools. Version 36.2+ of setuptools is required.') +elif os.name == 'nt': + # This branch exists to improve this package's behavior on Windows. Without + # it, if the sdist is installed on Windows with an old version of + # setuptools, pywin32 will not be specified as a dependency. + install_requires.append(pywin32_req) dev_extras = [ 'astroid==1.6.5', @@ -132,6 +138,7 @@ 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup',