From 275f083a33402821f789d8f4ebaff75b796dc0c2 Mon Sep 17 00:00:00 2001 From: Dev & Sec Date: Mon, 2 Nov 2015 00:55:29 +0000 Subject: [PATCH 001/124] Use su if sudo is not available, this fixes #1148 --- letsencrypt-auto | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/letsencrypt-auto b/letsencrypt-auto index d163998aa..e14b099a9 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -14,7 +14,15 @@ VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN=${VENV_PATH}/bin if test "`id -u`" -ne "0" ; then - SUDO=sudo + if type sudo &>/dev/null; then + SUDO=sudo + else + args=("$@") + for i in "${!args[@]}"; do + args[$i]="'$(printf "%s" "${args[$i]}" | sed -e "s/'/'\"'\"'/g")' " + done + exec su root -c "$0 ${args[*]}" + fi else SUDO= fi From 8bad8de1c673ee21f8cbdb7dcec1bc425586ba6d Mon Sep 17 00:00:00 2001 From: Dev & Sec Date: Mon, 2 Nov 2015 21:49:22 +0000 Subject: [PATCH 002/124] not running the letsencrypt-auto script as root, but use su if sudo not found --- letsencrypt-auto | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/letsencrypt-auto b/letsencrypt-auto index e14b099a9..514f03f46 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -14,15 +14,16 @@ VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN=${VENV_PATH}/bin if test "`id -u`" -ne "0" ; then - if type sudo &>/dev/null; then - SUDO=sudo - else - args=("$@") - for i in "${!args[@]}"; do - args[$i]="'$(printf "%s" "${args[$i]}" | sed -e "s/'/'\"'\"'/g")' " - done - exec su root -c "$0 ${args[*]}" + if ! type sudo &>/dev/null; then + function sudo (){ + args=("$@") + for i in "${!args[@]}"; do + args[$i]="'$(printf "%s" "${args[$i]}" | sed -e "s/'/'\"'\"'/g")' " + done + su root -c "${args[*]}" + } fi + SUDO=sudo else SUDO= fi From ed173d9c9aac7b0e9b2ffeb782b42ef6663cc58e Mon Sep 17 00:00:00 2001 From: Dev & Sec Date: Tue, 3 Nov 2015 22:22:49 +0000 Subject: [PATCH 003/124] fix sh compatibility --- letsencrypt-auto | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/letsencrypt-auto b/letsencrypt-auto index 514f03f46..cfc830c9f 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -15,12 +15,13 @@ VENV_BIN=${VENV_PATH}/bin if test "`id -u`" -ne "0" ; then if ! type sudo &>/dev/null; then - function sudo (){ - args=("$@") - for i in "${!args[@]}"; do - args[$i]="'$(printf "%s" "${args[$i]}" | sed -e "s/'/'\"'\"'/g")' " + sudo() { + args="" + while [ $# -ne 0 ]; do + args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' " + shift done - su root -c "${args[*]}" + su root -c "$args" } fi SUDO=sudo From 5c8ad3666b9bef423e846453c50450b32a3d983d Mon Sep 17 00:00:00 2001 From: Dev & Sec Date: Tue, 3 Nov 2015 22:34:07 +0000 Subject: [PATCH 004/124] fix sudo function name scope issue, it is not a local function --- letsencrypt-auto | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/letsencrypt-auto b/letsencrypt-auto index cfc830c9f..c352482a2 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -14,8 +14,10 @@ VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN=${VENV_PATH}/bin if test "`id -u`" -ne "0" ; then - if ! type sudo &>/dev/null; then - sudo() { + if type sudo &>/dev/null; then + SUDO=sudo + else + su_sudo() { args="" while [ $# -ne 0 ]; do args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' " @@ -23,8 +25,8 @@ if test "`id -u`" -ne "0" ; then done su root -c "$args" } + SUDO=su_sudo fi - SUDO=sudo else SUDO= fi From a7de3d59dadd9d3dfc88c8a684a4f9b939398739 Mon Sep 17 00:00:00 2001 From: Dev & Sec Date: Tue, 3 Nov 2015 22:46:56 +0000 Subject: [PATCH 005/124] fix `dash` compatibility issue caused by `&>` redirect symbol --- letsencrypt-auto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-auto b/letsencrypt-auto index c352482a2..8626ab329 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -14,7 +14,7 @@ VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN=${VENV_PATH}/bin if test "`id -u`" -ne "0" ; then - if type sudo &>/dev/null; then + if type sudo 1>/dev/null 2>&1; then SUDO=sudo else su_sudo() { From ec5c28980dbcb787c3c89a40a49a8c421294953d Mon Sep 17 00:00:00 2001 From: John Leach Date: Thu, 5 Nov 2015 18:52:10 +0000 Subject: [PATCH 006/124] docker: Use full filename when copying symlink Works around an upstream bug in Docker: https://github.com/docker/docker/issues/17730 Fixes #1374 --- Dockerfile | 3 ++- Dockerfile-dev | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index b9ea168de..e3be01dee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,8 @@ WORKDIR /opt/letsencrypt # If doesn't exist, it is created along with all missing # directories in its path. -COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/ + +COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/ubuntu.sh RUN /opt/letsencrypt/src/ubuntu.sh && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* \ diff --git a/Dockerfile-dev b/Dockerfile-dev index daa8e9af0..db9ecf5b8 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -22,7 +22,7 @@ WORKDIR /opt/letsencrypt # TODO: Install non-default Python versions for tox. # TODO: Install Apache/Nginx for plugin development. -COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/ +COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/ubuntu.sh RUN /opt/letsencrypt/src/ubuntu.sh && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* \ From d054d05ea9d8c5783a4c61c8f5a2402899e895ed Mon Sep 17 00:00:00 2001 From: Kubilay Kocak Date: Fri, 6 Nov 2015 19:54:50 +1100 Subject: [PATCH 007/124] Add FreeBSD port/package to documentation * Rename Distro's to Operating Systems * Add instructions for FreeBSD port and package installation --- docs/using.rst | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index eb8c31595..603f38a9b 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -86,8 +86,15 @@ in ``/etc/letsencrypt/live`` on the host. .. _`install Docker`: https://docs.docker.com/userguide/ -Distro packages ---------------- +Operating System Packages +-------------------------- + +**FreeBSD** + + * Port: ``cd /usr/ports/security/py-letsencrypt && make install clean`` + * Package: ``pkg install py27-letsencrypt`` + +**Other Operating Systems** Unfortunately, this is an ongoing effort. If you'd like to package Let's Encrypt client for your distribution of choice please have a From 9f8ab4677ef4a8caaca077ae93bb288c6b35bc62 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Thu, 5 Nov 2015 14:49:21 +0200 Subject: [PATCH 008/124] Ignoring mod_macro virtualhosts, but displaying a notification why it's done --- .../letsencrypt_apache/configurator.py | 43 +++++++++++++++++-- letsencrypt-apache/letsencrypt_apache/obj.py | 6 ++- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index d376fe4b6..e1f53f269 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -348,8 +348,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ all_names = set() + vhost_macro = [] + for vhost in self.vhosts: all_names.update(vhost.get_names()) + if vhost.modmacro: + vhost_macro.append(vhost.filep) for addr in vhost.addrs: if common.hostname_regex.match(addr.get_addr()): @@ -359,6 +363,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if name: all_names.add(name) + if len(vhost_macro) > 0: + zope.component.getUtility(interfaces.IDisplay).notification( + "Apache mod_macro seems to be in use in file(s):\n{0}" + "\n\nUnfortunately mod_macro is not yet supported".format( + "\n ".join(vhost_macro))) + return all_names def get_name_from_ip(self, addr): # pylint: disable=no-self-use @@ -395,11 +405,34 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): "ServerAlias", None, start=host.path, exclude=False) for alias in serveralias_match: - host.aliases.add(self.parser.get_arg(alias)) + serveralias = self.parser.get_arg(alias) + if not self._is_mod_macro(serveralias): + host.aliases.add(serveralias) + else: + host.modmacro = True if servername_match: # Get last ServerName as each overwrites the previous - host.name = self.parser.get_arg(servername_match[-1]) + servername = self.parser.get_arg(servername_match[-1]) + if not self._is_mod_macro(servername): + host.name = servername + else: + host.modmacro = True + + def _is_mod_macro(self, name): + """Helper function for _add_servernames(). + Checks if the ServerName / ServerAlias belongs to a macro + + :param str name: Name to check and filter out if it's a variable + + :returns: boolean + :rtype: boolean + + """ + + if "$" in name: + return True + return False def _create_vhost(self, path): """Used by get_virtual_hosts to create vhost objects @@ -1236,7 +1269,7 @@ def get_file_path(vhost_path): avail_fp = vhost_path[6:] # This can be optimized... while True: - # Cast both to lowercase to be case insensitive + # Cast all to lowercase to be case insensitive find_if = avail_fp.lower().find("/ifmodule") if find_if != -1: avail_fp = avail_fp[:find_if] @@ -1245,6 +1278,10 @@ def get_file_path(vhost_path): if find_vh != -1: avail_fp = avail_fp[:find_vh] continue + find_macro = avail_fp.lower().find("/macro") + if find_macro != -1: + avail_fp = avail_fp[:find_macro] + continue break return avail_fp diff --git a/letsencrypt-apache/letsencrypt_apache/obj.py b/letsencrypt-apache/letsencrypt_apache/obj.py index 58a6c740e..d71cfb1ad 100644 --- a/letsencrypt-apache/letsencrypt_apache/obj.py +++ b/letsencrypt-apache/letsencrypt_apache/obj.py @@ -102,6 +102,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods :ivar bool ssl: SSLEngine on in vhost :ivar bool enabled: Virtual host is enabled + :ivar bool modmacro: VirtualHost is using mod_macro https://httpd.apache.org/docs/2.4/vhosts/details.html @@ -112,7 +113,9 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods # ?: is used for not returning enclosed characters strip_name = re.compile(r"^(?:.+://)?([^ :$]*)") - def __init__(self, filep, path, addrs, ssl, enabled, name=None, aliases=None): + def __init__(self, filep, path, addrs, ssl, enabled, modmacro=False, + name=None, aliases=None): + # pylint: disable=too-many-arguments """Initialize a VH.""" self.filep = filep @@ -122,6 +125,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods self.aliases = aliases if aliases is not None else set() self.ssl = ssl self.enabled = enabled + self.modmacro = modmacro def get_names(self): """Return a set of all names.""" From aa0161fbec624c08c14a0d7aedb4f1cedbb2ad32 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Thu, 5 Nov 2015 18:53:26 +0200 Subject: [PATCH 009/124] Let's avoid breaking backwards compatibility --- letsencrypt-apache/letsencrypt_apache/obj.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/obj.py b/letsencrypt-apache/letsencrypt_apache/obj.py index d71cfb1ad..80f92cab3 100644 --- a/letsencrypt-apache/letsencrypt_apache/obj.py +++ b/letsencrypt-apache/letsencrypt_apache/obj.py @@ -113,8 +113,8 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods # ?: is used for not returning enclosed characters strip_name = re.compile(r"^(?:.+://)?([^ :$]*)") - def __init__(self, filep, path, addrs, ssl, enabled, modmacro=False, - name=None, aliases=None): + def __init__(self, filep, path, addrs, ssl, enabled, name=None, + aliases=None, modmacro=False): # pylint: disable=too-many-arguments """Initialize a VH.""" From 4bd0330ae7b6a568350e18b358ce66deceeb0f4f Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 6 Nov 2015 10:55:35 +0200 Subject: [PATCH 010/124] Do not suggest mod_macro vhost for the best vhost --- .../letsencrypt_apache/configurator.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index e1f53f269..a1227b0bb 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -307,6 +307,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): best_points = 0 for vhost in self.vhosts: + if vhost.modmacro is True: + continue if target_name in vhost.get_names(): points = 2 elif any(addr.get_addr() == target_name for addr in vhost.addrs): @@ -326,12 +328,27 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # No winners here... is there only one reasonable vhost? if best_candidate is None: # reasonable == Not all _default_ addrs - reasonable_vhosts = self._non_default_vhosts() + # remove mod_macro hosts from reasonable vhosts + reasonable_vhosts = self._without_modmacro( + self._non_default_vhosts()) if len(reasonable_vhosts) == 1: best_candidate = reasonable_vhosts[0] + if best_candidate is not None and best_candidate.modmacro is True: + return None return best_candidate + def _without_modmacro(self, vhosts): + """Return all non mod_macro vhosts + + :param vhosts: List of VirtualHosts + :type vhosts: (:class:`list` of :class:`~letsencrypt_apache.obj.VirtualHost`) + + :returns: List of VirtualHosts without mod_macro + :rtype: (:class:`list` of :class:`~letsencrypt_apache.obj.VirtualHost`) + """ + return [vh for vh in vhosts if vh.modmacro == False] + def _non_default_vhosts(self): """Return all non _default_ only vhosts.""" return [vh for vh in self.vhosts if not all( From ba3db558d5295d0ec2866c40a27a2abb102d7c79 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 6 Nov 2015 10:56:50 +0200 Subject: [PATCH 011/124] Tests taking mod_macro into account --- .../tests/configurator_test.py | 20 ++++++++++++------- .../tests/display_ops_test.py | 4 ++-- .../letsencrypt_apache/tests/parser_test.py | 2 +- .../sites-available/mod_macro-example.conf | 15 ++++++++++++++ .../sites-enabled/mod_macro-example.conf | 1 + .../letsencrypt_apache/tests/util.py | 5 +++++ 6 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/mod_macro-example.conf create mode 120000 letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/mod_macro-example.conf diff --git a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py index 7c2137c45..c74fa3241 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py @@ -59,14 +59,20 @@ class TwoVhost80Test(util.ApacheTest): # Weak test.. ApacheConfigurator.add_parser_arguments(mock.MagicMock()) - def test_get_all_names(self): + @mock.patch("zope.component.getUtility") + def test_get_all_names(self, mock_getutility): + mock_getutility.notification = mock.MagicMock(return_value=True) names = self.config.get_all_names() self.assertEqual(names, set( ["letsencrypt.demo", "encryption-example.demo", "ip-172-30-0-17"])) + @mock.patch("zope.component.getUtility") @mock.patch("letsencrypt_apache.configurator.socket.gethostbyaddr") - def test_get_all_names_addrs(self, mock_gethost): + def test_get_all_names_addrs(self, mock_gethost, mock_getutility): mock_gethost.side_effect = [("google.com", "", ""), socket.error] + notification = mock.Mock() + notification.notification = mock.Mock(return_value=True) + mock_getutility.return_value = notification vhost = obj.VirtualHost( "fp", "ap", set([obj.Addr(("8.8.8.8", "443")), @@ -97,7 +103,7 @@ class TwoVhost80Test(util.ApacheTest): """ vhs = self.config.get_virtual_hosts() - self.assertEqual(len(vhs), 4) + self.assertEqual(len(vhs), 5) found = 0 for vhost in vhs: @@ -108,7 +114,7 @@ class TwoVhost80Test(util.ApacheTest): else: raise Exception("Missed: %s" % vhost) # pragma: no cover - self.assertEqual(found, 4) + self.assertEqual(found, 5) @mock.patch("letsencrypt_apache.display_ops.select_vhost") def test_choose_vhost_none_avail(self, mock_select): @@ -174,7 +180,7 @@ class TwoVhost80Test(util.ApacheTest): def test_non_default_vhosts(self): # pylint: disable=protected-access - self.assertEqual(len(self.config._non_default_vhosts()), 3) + self.assertEqual(len(self.config._non_default_vhosts()), 4) def test_is_site_enabled(self): """Test if site is enabled. @@ -345,7 +351,7 @@ class TwoVhost80Test(util.ApacheTest): self.assertEqual(self.config.is_name_vhost(self.vh_truth[0]), self.config.is_name_vhost(ssl_vhost)) - self.assertEqual(len(self.config.vhosts), 5) + self.assertEqual(len(self.config.vhosts), 6) def test_make_vhost_ssl_extra_vhs(self): self.config.aug.match = mock.Mock(return_value=["p1", "p2"]) @@ -587,7 +593,7 @@ class TwoVhost80Test(util.ApacheTest): self.vh_truth[1].aliases = set(["yes.default.com"]) self.config._enable_redirect(self.vh_truth[1], "") # pylint: disable=protected-access - self.assertEqual(len(self.config.vhosts), 5) + self.assertEqual(len(self.config.vhosts), 6) def get_achalls(self): """Return testing achallenges.""" diff --git a/letsencrypt-apache/letsencrypt_apache/tests/display_ops_test.py b/letsencrypt-apache/letsencrypt_apache/tests/display_ops_test.py index d7cfb09b3..6db319d87 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/display_ops_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/display_ops_test.py @@ -57,7 +57,7 @@ class SelectVhostTest(unittest.TestCase): @mock.patch("letsencrypt_apache.display_ops.zope.component.getUtility") def test_multiple_names(self, mock_util): - mock_util().menu.return_value = (display_util.OK, 4) + mock_util().menu.return_value = (display_util.OK, 5) self.vhosts.append( obj.VirtualHost( @@ -65,7 +65,7 @@ class SelectVhostTest(unittest.TestCase): False, False, "wildcard.com", set(["*.wildcard.com"]))) - self.assertEqual(self.vhosts[4], self._call(self.vhosts)) + self.assertEqual(self.vhosts[5], self._call(self.vhosts)) if __name__ == "__main__": diff --git a/letsencrypt-apache/letsencrypt_apache/tests/parser_test.py b/letsencrypt-apache/letsencrypt_apache/tests/parser_test.py index d2e4dec14..bc1f316f9 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/parser_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/parser_test.py @@ -52,7 +52,7 @@ class BasicParserTest(util.ParserTest): test2 = self.parser.find_dir("documentroot") self.assertEqual(len(test), 1) - self.assertEqual(len(test2), 3) + self.assertEqual(len(test2), 4) def test_add_dir(self): aug_default = "/files" + self.parser.loc["default"] diff --git a/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/mod_macro-example.conf b/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/mod_macro-example.conf new file mode 100644 index 000000000..6a6579007 --- /dev/null +++ b/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/mod_macro-example.conf @@ -0,0 +1,15 @@ + + + ServerName $domain + ServerAlias www.$domain + DocumentRoot /var/www/html + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + +Use VHost macro1 test.com +Use VHost macro2 hostname.org +Use VHost macro3 apache.org + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/mod_macro-example.conf b/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/mod_macro-example.conf new file mode 120000 index 000000000..44f254304 --- /dev/null +++ b/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/mod_macro-example.conf @@ -0,0 +1 @@ +../sites-available/mod_macro-example.conf \ No newline at end of file diff --git a/letsencrypt-apache/letsencrypt_apache/tests/util.py b/letsencrypt-apache/letsencrypt_apache/tests/util.py index 2594ba773..a8bfe0e4b 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/util.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/util.py @@ -124,6 +124,11 @@ def get_vh_truth(temp_dir, config_name): os.path.join(aug_pre, "letsencrypt.conf/VirtualHost"), set([obj.Addr.fromstring("*:80")]), False, True, "letsencrypt.demo"), + obj.VirtualHost( + os.path.join(prefix, "mod_macro-example.conf"), + os.path.join(aug_pre, + "mod_macro-example.conf/Macro/VirtualHost"), + set([obj.Addr.fromstring("*:80")]), False, True, modmacro=True) ] return vh_truth From 93a53d507821ac0612aadb62d70edf5f3856c468 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 6 Nov 2015 11:11:14 +0200 Subject: [PATCH 012/124] Added test for removing mod_macro vhosts from vhost list --- .../letsencrypt_apache/tests/configurator_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py index c74fa3241..4224749bf 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py @@ -178,6 +178,10 @@ class TwoVhost80Test(util.ApacheTest): self.assertEqual( self.config._find_best_vhost("example.demo"), self.vh_truth[2]) + def test_without_modmacro(self): + self.assertEqual(len(self.vh_truth)-1, + len(self.config._without_modmacro(self.vh_truth))) + def test_non_default_vhosts(self): # pylint: disable=protected-access self.assertEqual(len(self.config._non_default_vhosts()), 4) From 88b89a04b1e2aabdfc692c2143223da31ba5c03d Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 6 Nov 2015 12:08:28 +0200 Subject: [PATCH 013/124] Fix pylint in the new test --- letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py index 4224749bf..2e335ea00 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py @@ -179,6 +179,7 @@ class TwoVhost80Test(util.ApacheTest): self.config._find_best_vhost("example.demo"), self.vh_truth[2]) def test_without_modmacro(self): + # pylint: disable=protected-access self.assertEqual(len(self.vh_truth)-1, len(self.config._without_modmacro(self.vh_truth))) From f0c059752fcd8b36862e12c77a98ce351c6d5442 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 6 Nov 2015 12:15:53 +0200 Subject: [PATCH 014/124] Added test for mod_macro check in domain names --- .../letsencrypt_apache/tests/configurator_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py index 2e335ea00..963948d6e 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py @@ -183,6 +183,11 @@ class TwoVhost80Test(util.ApacheTest): self.assertEqual(len(self.vh_truth)-1, len(self.config._without_modmacro(self.vh_truth))) + def test_is_mod_macro(self): + # pylint: disable=protected-access + self.assertEqual(self.config._is_mod_macro("$domain"), True) + self.assertEqual(self.config._is_mod_macro("www.example.com"), False) + def test_non_default_vhosts(self): # pylint: disable=protected-access self.assertEqual(len(self.config._non_default_vhosts()), 4) From 8fb3956ecd2b46c42e18d97b19129f4e31266a89 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 6 Nov 2015 12:45:44 +0200 Subject: [PATCH 015/124] Removed dead code --- letsencrypt-apache/letsencrypt_apache/configurator.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index a1227b0bb..9da00b5c3 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -334,8 +334,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if len(reasonable_vhosts) == 1: best_candidate = reasonable_vhosts[0] - if best_candidate is not None and best_candidate.modmacro is True: - return None return best_candidate def _without_modmacro(self, vhosts): From effc1ed8e4595f784983c83d9063e5cf8ace8d6f Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 6 Nov 2015 19:53:13 +0200 Subject: [PATCH 016/124] Fix VirtualHost magic methods to take mod_macro addition into account --- letsencrypt-apache/letsencrypt_apache/obj.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/obj.py b/letsencrypt-apache/letsencrypt_apache/obj.py index 80f92cab3..175ce3f92 100644 --- a/letsencrypt-apache/letsencrypt_apache/obj.py +++ b/letsencrypt-apache/letsencrypt_apache/obj.py @@ -145,21 +145,25 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods "Name: {name}\n" "Aliases: {aliases}\n" "TLS Enabled: {tls}\n" - "Site Enabled: {active}".format( + "Site Enabled: {active}\n" + "mod_macro Vhost: {modmacro}".format( filename=self.filep, vhpath=self.path, addrs=", ".join(str(addr) for addr in self.addrs), name=self.name if self.name is not None else "", aliases=", ".join(name for name in self.aliases), tls="Yes" if self.ssl else "No", - active="Yes" if self.enabled else "No")) + active="Yes" if self.enabled else "No", + modmacro="Yes" if self.modmacro else "No")) def __eq__(self, other): if isinstance(other, self.__class__): return (self.filep == other.filep and self.path == other.path and self.addrs == other.addrs and self.get_names() == other.get_names() and - self.ssl == other.ssl and self.enabled == other.enabled) + self.ssl == other.ssl and + self.enabled == other.enabled and + self.modmacro == other.modmacro) return False From 980b4ac3fae6ccc9d4def3fdeb35b160af55bdca Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Fri, 6 Nov 2015 20:06:56 +0200 Subject: [PATCH 017/124] PEP8 and sphinx documentation fixes --- letsencrypt-apache/letsencrypt_apache/configurator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index 9da00b5c3..d3849b7fd 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -340,12 +340,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """Return all non mod_macro vhosts :param vhosts: List of VirtualHosts - :type vhosts: (:class:`list` of :class:`~letsencrypt_apache.obj.VirtualHost`) + :type vhosts: :class:`list` of + :class:`~letsencrypt_apache.obj.VirtualHost` :returns: List of VirtualHosts without mod_macro - :rtype: (:class:`list` of :class:`~letsencrypt_apache.obj.VirtualHost`) + :rtype: :class:`list` of :class:`~letsencrypt_apache.obj.VirtualHost` """ - return [vh for vh in vhosts if vh.modmacro == False] + return [vh for vh in vhosts if vh.modmacro is False] def _non_default_vhosts(self): """Return all non _default_ only vhosts.""" From 291c5c71327640a02218d7adda3e1013cd701b71 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 6 Nov 2015 12:42:13 -0800 Subject: [PATCH 018/124] Remove nonsensical Apache config directives - AcceptPathInfo Off was breaking things (fixes: #1274) - Enabling DEFLATE compression for HTML and XML may make the site vulnerable to BREACH! --- .../letsencrypt_apache/options-ssl-apache.conf | 6 ------ 1 file changed, 6 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/options-ssl-apache.conf b/letsencrypt-apache/letsencrypt_apache/options-ssl-apache.conf index 8c28c86a5..2a724d7ec 100644 --- a/letsencrypt-apache/letsencrypt_apache/options-ssl-apache.conf +++ b/letsencrypt-apache/letsencrypt_apache/options-ssl-apache.conf @@ -8,12 +8,6 @@ SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA25 SSLHonorCipherOrder on SSLCompression off - -ServerSignature Off -AcceptPathInfo Off -AddOutputFilterByType DEFLATE text/html text/plain text/xml application/pdf -AddDefaultCharset UTF-8 - SSLOptions +StrictRequire # Add vhost name to log entries: From 63135be7c243a540c5a8bb096416875c74b4f182 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 6 Nov 2015 19:36:22 -0800 Subject: [PATCH 019/124] Downgrade a very excitable security comment to an XXX Resolves: #1400 Also note that this function will need to become more sophisticated as we support autoupdating of config options (eg those changed in #1390) --- .../letsencrypt_apache/configurator.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index d376fe4b6..603a792d2 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -162,7 +162,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Get all of the available vhosts self.vhosts = self.get_virtual_hosts() - temp_install(self.mod_ssl_conf) + install_ssl_options_conf(self.mod_ssl_conf) def deploy_cert(self, domain, cert_path, key_path, chain_path=None, fullchain_path=None): # pylint: disable=unused-argument @@ -1249,12 +1249,18 @@ def get_file_path(vhost_path): return avail_fp -def temp_install(options_ssl): - """Temporary install for convenience.""" - # WARNING: THIS IS A POTENTIAL SECURITY VULNERABILITY - # THIS SHOULD BE HANDLED BY THE PACKAGE MANAGER - # AND TAKEN OUT BEFORE RELEASE, INSTEAD - # SHOWING A NICE ERROR MESSAGE ABOUT THE PROBLEM. +def install_ssl_options_conf(options_ssl): + """ + Copy Let's Encrypt's SSL options file into the system's config dir if + required. + """ + # XXX if we ever try to enforce a local privilege boundary (eg, running + # letsencrypt for unprivileged users via setuid), this function will need + # to be modified. + + # XXX if the user is in security-autoupdate mode, we should be willing to + # overwrite the options_ssl file at least if it's unmodified: + # https://github.com/letsencrypt/letsencrypt/issues/1123 # Check to make sure options-ssl.conf is installed if not os.path.isfile(options_ssl): From ce501f94a38a9ab23a666af34d1390cb4250cc28 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sat, 7 Nov 2015 08:31:46 +0200 Subject: [PATCH 020/124] Simplify the code --- .../letsencrypt_apache/configurator.py | 17 +++-------------- .../tests/configurator_test.py | 5 ----- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index d3849b7fd..6b4caef50 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -328,26 +328,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # No winners here... is there only one reasonable vhost? if best_candidate is None: # reasonable == Not all _default_ addrs + vhosts = self._non_default_vhosts() # remove mod_macro hosts from reasonable vhosts - reasonable_vhosts = self._without_modmacro( - self._non_default_vhosts()) + reasonable_vhosts = [vh for vh + in vhosts if vh.modmacro is False] if len(reasonable_vhosts) == 1: best_candidate = reasonable_vhosts[0] return best_candidate - def _without_modmacro(self, vhosts): - """Return all non mod_macro vhosts - - :param vhosts: List of VirtualHosts - :type vhosts: :class:`list` of - :class:`~letsencrypt_apache.obj.VirtualHost` - - :returns: List of VirtualHosts without mod_macro - :rtype: :class:`list` of :class:`~letsencrypt_apache.obj.VirtualHost` - """ - return [vh for vh in vhosts if vh.modmacro is False] - def _non_default_vhosts(self): """Return all non _default_ only vhosts.""" return [vh for vh in self.vhosts if not all( diff --git a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py index 963948d6e..61009b89f 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py @@ -178,11 +178,6 @@ class TwoVhost80Test(util.ApacheTest): self.assertEqual( self.config._find_best_vhost("example.demo"), self.vh_truth[2]) - def test_without_modmacro(self): - # pylint: disable=protected-access - self.assertEqual(len(self.vh_truth)-1, - len(self.config._without_modmacro(self.vh_truth))) - def test_is_mod_macro(self): # pylint: disable=protected-access self.assertEqual(self.config._is_mod_macro("$domain"), True) From dc60cdbc7de3854fdfc0d604478f38444c5d6f3d Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Nov 2015 08:50:43 +0000 Subject: [PATCH 021/124] User-Agent support in acme (default: `acme-python`, fixes #1351). In order to override the default (`acme-python`), clients (including Let's Encrypt: #858, #1397) should create a custom acme.clietn.ClientNetwork object and pass it to acme.client.Client.__init__. --- acme/acme/client.py | 6 +++++- acme/acme/client_test.py | 20 +++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index 4c89458fb..0e9319f9c 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -481,11 +481,13 @@ class ClientNetwork(object): JSON_ERROR_CONTENT_TYPE = 'application/problem+json' REPLAY_NONCE_HEADER = 'Replay-Nonce' - def __init__(self, key, alg=jose.RS256, verify_ssl=True): + def __init__(self, key, alg=jose.RS256, verify_ssl=True, + user_agent='acme-python'): self.key = key self.alg = alg self.verify_ssl = verify_ssl self._nonces = set() + self.user_agent = user_agent def _wrap_in_jws(self, obj, nonce): """Wrap `JSONDeSerializable` object in JWS. @@ -578,6 +580,8 @@ class ClientNetwork(object): logging.debug('Sending %s request to %s. args: %r, kwargs: %r', method, url, args, kwargs) kwargs['verify'] = self.verify_ssl + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('User-Agent', self.user_agent) response = requests.request(method, url, *args, **kwargs) logging.debug('Received %s. Headers: %s. Content: %r', response, response.headers, response.content) diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index 7e895218c..2df7b5313 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -396,7 +396,8 @@ class ClientNetworkTest(unittest.TestCase): from acme.client import ClientNetwork self.net = ClientNetwork( - key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl) + key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl, + user_agent='acme-python-test') self.response = mock.MagicMock(ok=True, status_code=http_client.OK) self.response.headers = {} @@ -479,7 +480,7 @@ class ClientNetworkTest(unittest.TestCase): self.assertEqual(self.response, self.net._send_request( 'HEAD', 'url', 'foo', bar='baz')) mock_requests.request.assert_called_once_with( - 'HEAD', 'url', 'foo', verify=mock.ANY, bar='baz') + 'HEAD', 'url', 'foo', verify=mock.ANY, bar='baz', headers=mock.ANY) @mock.patch('acme.client.requests') def test_send_request_verify_ssl(self, mock_requests): @@ -492,7 +493,20 @@ class ClientNetworkTest(unittest.TestCase): self.assertEqual( self.response, self.net._send_request('GET', 'url')) mock_requests.request.assert_called_once_with( - 'GET', 'url', verify=verify) + 'GET', 'url', verify=verify, headers=mock.ANY) + + @mock.patch('acme.client.requests') + def test_send_request_user_agent(self, mock_requests): + mock_requests.request.return_value = self.response + # pylint: disable=protected-access + self.net._send_request('GET', 'url', headers={'bar': 'baz'}) + mock_requests.request.assert_called_once_with( + 'GET', 'url', verify=mock.ANY, + headers={'User-Agent': 'acme-python-test', 'bar': 'baz'}) + + self.net._send_request('GET', 'url', headers={'User-Agent': 'foo2'}) + mock_requests.request.assert_called_with( + 'GET', 'url', verify=mock.ANY, headers={'User-Agent': 'foo2'}) @mock.patch('acme.client.requests') def test_requests_error_passthrough(self, mock_requests): From a832070a12caf2fcb5a4d93d829b4d446c1401f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20S=C3=BAkup?= Date: Fri, 6 Nov 2015 12:46:15 +0100 Subject: [PATCH 022/124] Add bootstrap for openSUSE --- bootstrap/_suse_common.sh | 14 ++++++++++++++ bootstrap/install-deps.sh | 3 +++ bootstrap/suse.sh | 1 + letsencrypt-auto | 3 +++ 4 files changed, 21 insertions(+) create mode 100755 bootstrap/_suse_common.sh create mode 120000 bootstrap/suse.sh diff --git a/bootstrap/_suse_common.sh b/bootstrap/_suse_common.sh new file mode 100755 index 000000000..4b41bac36 --- /dev/null +++ b/bootstrap/_suse_common.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# SLE12 dont have python-virtualenv + +zypper -nq in -l git-core \ + python \ + python-devel \ + python-virtualenv \ + gcc \ + dialog \ + augeas-lenses \ + libopenssl-devel \ + libffi-devel \ + ca-certificates \ diff --git a/bootstrap/install-deps.sh b/bootstrap/install-deps.sh index 3cb0fc274..e907e7035 100755 --- a/bootstrap/install-deps.sh +++ b/bootstrap/install-deps.sh @@ -29,6 +29,9 @@ elif [ -f /etc/gentoo-release ] ; then elif uname | grep -iq FreeBSD ; then echo "Bootstrapping dependencies for FreeBSD..." $SUDO $BOOTSTRAP/freebsd.sh +elif `grep -qs openSUSE /etc/os-release` ; then + echo "Bootstrapping dependencies for openSUSE.." + $SUDO $BOOTSTRAP/suse.sh elif uname | grep -iq Darwin ; then echo "Bootstrapping dependencies for Mac OS X..." echo "WARNING: Mac support is very experimental at present..." diff --git a/bootstrap/suse.sh b/bootstrap/suse.sh new file mode 120000 index 000000000..fc4c1dee4 --- /dev/null +++ b/bootstrap/suse.sh @@ -0,0 +1 @@ +_suse_common.sh \ No newline at end of file diff --git a/letsencrypt-auto b/letsencrypt-auto index 2391a7c0b..5d5e23a04 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -82,6 +82,9 @@ then elif [ -f /etc/redhat-release ] ; then echo "Bootstrapping dependencies for RedHat-based OSes..." $SUDO $BOOTSTRAP/_rpm_common.sh + elif `grep -q openSUSE /etc/os-release` ; then + echo "Bootstrapping dependencies for openSUSE-based OSes..." + $SUDO $BOOTSTRAP/_suse_common.sh elif [ -f /etc/arch-release ] ; then echo "Bootstrapping dependencies for Archlinux..." $SUDO $BOOTSTRAP/archlinux.sh From a6a00c108b22e0dfef19b3f8cb053e867622ad4e Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 7 Nov 2015 03:01:33 -0800 Subject: [PATCH 023/124] Update test to reflect name change. --- .../letsencrypt_apache/tests/configurator_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py index 7c2137c45..70825167a 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py @@ -492,10 +492,10 @@ class TwoVhost80Test(util.ApacheTest): def test_get_chall_pref(self): self.assertTrue(isinstance(self.config.get_chall_pref(""), list)) - def test_temp_install(self): - from letsencrypt_apache.configurator import temp_install + def test_install_ssl_options_conf(self): + from letsencrypt_apache.configurator import install_ssl_options_conf path = os.path.join(self.work_dir, "test_it") - temp_install(path) + install_ssl_options_conf(path) self.assertTrue(os.path.isfile(path)) # TEST ENHANCEMENTS From 3a5f7a026b17ae5423e14c0b4377f1ae25f6a84f Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Nov 2015 12:32:23 +0000 Subject: [PATCH 024/124] Fix old reference to SimpleHTTP --- acme/acme/challenges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index c5855a7ca..57e74144b 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -246,7 +246,7 @@ class HTTP01Response(KeyAuthorizationChallengeResponse): # request URI, if it's standard. if port is not None and port != self.PORT: logger.warning( - "Using non-standard port for SimpleHTTP verification: %s", port) + "Using non-standard port for http-01 verification: %s", port) domain += ":{0}".format(port) uri = chall.uri(domain) From dd92e9529011b5f306799de68dbdb61e7609f96f Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Nov 2015 13:59:02 +0000 Subject: [PATCH 025/124] Remove remaints of simpleHttp from standalone plugin --- letsencrypt/plugins/standalone.py | 26 ++++++++++---------------- letsencrypt/plugins/standalone_test.py | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/letsencrypt/plugins/standalone.py b/letsencrypt/plugins/standalone.py index 5041091e4..3975e9292 100644 --- a/letsencrypt/plugins/standalone.py +++ b/letsencrypt/plugins/standalone.py @@ -28,9 +28,9 @@ class ServerManager(object): Manager for `ACMEServer` and `ACMETLSServer` instances. - `certs` and `simple_http_resources` correspond to + `certs` and `http_01_resources` correspond to `acme.crypto_util.SSLSocket.certs` and - `acme.crypto_util.SSLSocket.simple_http_resources` respectively. All + `acme.crypto_util.SSLSocket.http_01_resources` respectively. All created servers share the same certificates and resources, so if you're running both TLS and non-TLS instances, HTTP01 handlers will serve the same URLs! @@ -38,10 +38,10 @@ class ServerManager(object): """ _Instance = collections.namedtuple("_Instance", "server thread") - def __init__(self, certs, simple_http_resources): + def __init__(self, certs, http_01_resources): self._instances = {} self.certs = certs - self.simple_http_resources = simple_http_resources + self.http_01_resources = http_01_resources def run(self, port, challenge_type): """Run ACME server on specified ``port``. @@ -67,7 +67,7 @@ class ServerManager(object): server = acme_standalone.DVSNIServer(address, self.certs) else: # challenges.HTTP01 server = acme_standalone.HTTP01Server( - address, self.simple_http_resources) + address, self.http_01_resources) except socket.error as error: raise errors.StandaloneBindError(error, port) @@ -150,12 +150,9 @@ class Authenticator(common.Plugin): def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) - # one self-signed key for all DVSNI and HTTP01 certificates + # one self-signed key for all DVSNI certificates self.key = OpenSSL.crypto.PKey() self.key.generate_key(OpenSSL.crypto.TYPE_RSA, bits=2048) - # TODO: generate only when the first HTTP01 challenge is solved - self.simple_http_cert = acme_crypto_util.gen_ss_cert( - self.key, domains=["temp server"]) self.served = collections.defaultdict(set) @@ -164,9 +161,9 @@ class Authenticator(common.Plugin): # GIL, the operations are safe, c.f. # https://docs.python.org/2/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe self.certs = {} - self.simple_http_resources = set() + self.http_01_resources = set() - self.servers = ServerManager(self.certs, self.simple_http_resources) + self.servers = ServerManager(self.certs, self.http_01_resources) @classmethod def add_parser_arguments(cls, add): @@ -240,17 +237,14 @@ class Authenticator(common.Plugin): server = self.servers.run( self.config.http01_port, challenges.HTTP01) response, validation = achall.response_and_validation() - self.simple_http_resources.add( + self.http_01_resources.add( acme_standalone.HTTP01RequestHandler.HTTP01Resource( chall=achall.chall, response=response, validation=validation)) - cert = self.simple_http_cert - domain = achall.domain else: # DVSNI server = self.servers.run(self.config.dvsni_port, challenges.DVSNI) response, cert, _ = achall.gen_cert_and_response(self.key) - domain = response.z_domain - self.certs[domain] = (self.key, cert) + self.certs[response.z_domain] = (self.key, cert) self.served[server].add(achall) responses.append(response) diff --git a/letsencrypt/plugins/standalone_test.py b/letsencrypt/plugins/standalone_test.py index 15da04417..c1de52ac8 100644 --- a/letsencrypt/plugins/standalone_test.py +++ b/letsencrypt/plugins/standalone_test.py @@ -24,13 +24,13 @@ class ServerManagerTest(unittest.TestCase): def setUp(self): from letsencrypt.plugins.standalone import ServerManager self.certs = {} - self.simple_http_resources = {} - self.mgr = ServerManager(self.certs, self.simple_http_resources) + self.http_01_resources = {} + self.mgr = ServerManager(self.certs, self.http_01_resources) def test_init(self): self.assertTrue(self.mgr.certs is self.certs) self.assertTrue( - self.mgr.simple_http_resources is self.simple_http_resources) + self.mgr.http_01_resources is self.http_01_resources) def _test_run_stop(self, challenge_type): server = self.mgr.run(port=0, challenge_type=challenge_type) @@ -42,7 +42,7 @@ class ServerManagerTest(unittest.TestCase): def test_run_stop_dvsni(self): self._test_run_stop(challenges.DVSNI) - def test_run_stop_simplehttp(self): + def test_run_stop_http_01(self): self._test_run_stop(challenges.HTTP01) def test_run_idempotent(self): @@ -153,7 +153,7 @@ class AuthenticatorTest(unittest.TestCase): def test_perform2(self): domain = b'localhost' key = jose.JWK.load(test_util.load_vector('rsa512_key.pem')) - simple_http = achallenges.KeyAuthorizationAnnotatedChallenge( + http_01 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.HTTP01_P, domain=domain, account_key=key) dvsni = achallenges.DVSNI( challb=acme_util.DVSNI_P, domain=domain, account_key=key) @@ -164,7 +164,7 @@ class AuthenticatorTest(unittest.TestCase): return "server{0}".format(port) self.auth.servers.run.side_effect = _run - responses = self.auth.perform2([simple_http, dvsni]) + responses = self.auth.perform2([http_01, dvsni]) self.assertTrue(isinstance(responses, list)) self.assertEqual(2, len(responses)) @@ -177,11 +177,11 @@ class AuthenticatorTest(unittest.TestCase): ]) self.assertEqual(self.auth.served, { "server1234": set([dvsni]), - "server4321": set([simple_http]), + "server4321": set([http_01]), }) - self.assertEqual(1, len(self.auth.simple_http_resources)) - self.assertEqual(2, len(self.auth.certs)) - self.assertEqual(list(self.auth.simple_http_resources), [ + self.assertEqual(1, len(self.auth.http_01_resources)) + self.assertEqual(1, len(self.auth.certs)) + self.assertEqual(list(self.auth.http_01_resources), [ acme_standalone.HTTP01RequestHandler.HTTP01Resource( acme_util.HTTP01, responses[0], mock.ANY)]) From c805ebc2bf6205085269f00aced226c6863eddb2 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 1 Nov 2015 11:52:54 +0000 Subject: [PATCH 026/124] Use KEY in DVSNI tests --- acme/acme/challenges_test.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 0708f3782..86291d0e8 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -571,8 +571,6 @@ class ProofOfPossessionResponseTest(unittest.TestCase): class DNSTest(unittest.TestCase): def setUp(self): - self.account_key = jose.JWKRSA.load( - test_util.load_vector('rsa512_key.pem')) from acme.challenges import DNS self.msg = DNS(token=jose.b64decode( b'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA')) @@ -594,34 +592,33 @@ class DNSTest(unittest.TestCase): def test_gen_check_validation(self): self.assertTrue(self.msg.check_validation( - self.msg.gen_validation(self.account_key), - self.account_key.public_key())) + self.msg.gen_validation(KEY), KEY.public_key())) def test_gen_check_validation_wrong_key(self): key2 = jose.JWKRSA.load(test_util.load_vector('rsa1024_key.pem')) self.assertFalse(self.msg.check_validation( - self.msg.gen_validation(self.account_key), key2.public_key())) + self.msg.gen_validation(KEY), key2.public_key())) def test_check_validation_wrong_payload(self): validations = tuple( - jose.JWS.sign(payload=payload, alg=jose.RS256, key=self.account_key) + jose.JWS.sign(payload=payload, alg=jose.RS256, key=KEY) for payload in (b'', b'{}') ) for validation in validations: self.assertFalse(self.msg.check_validation( - validation, self.account_key.public_key())) + validation, KEY.public_key())) def test_check_validation_wrong_fields(self): bad_validation = jose.JWS.sign( payload=self.msg.update(token=b'x' * 20).json_dumps().encode('utf-8'), - alg=jose.RS256, key=self.account_key) + alg=jose.RS256, key=KEY) self.assertFalse(self.msg.check_validation( - bad_validation, self.account_key.public_key())) + bad_validation, KEY.public_key())) def test_gen_response(self): with mock.patch('acme.challenges.DNS.gen_validation') as mock_gen: mock_gen.return_value = mock.sentinel.validation - response = self.msg.gen_response(self.account_key) + response = self.msg.gen_response(KEY) from acme.challenges import DNSResponse self.assertTrue(isinstance(response, DNSResponse)) self.assertEqual(response.validation, mock.sentinel.validation) From b864c77b623bf458b9675f1ce5cf31d9015d18d1 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 1 Nov 2015 12:20:38 +0000 Subject: [PATCH 027/124] Add tls-sni-01 to acme --- acme/acme/challenges.py | 130 +++++++++++++++++++++++++++++++-- acme/acme/challenges_test.py | 134 +++++++++++++++++++++++++++++++++++ 2 files changed, 260 insertions(+), 4 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 57e74144b..5f97547ee 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -187,7 +187,7 @@ class KeyAuthorizationChallenge(_TokenDVChallenge): key_authorization=self.key_authorization(account_key)) @abc.abstractmethod - def validation(self, account_key): + def validation(self, account_key, **kwargs): """Generate validation for the challenge. Subclasses must implement this method, but they are likely to @@ -201,7 +201,7 @@ class KeyAuthorizationChallenge(_TokenDVChallenge): """ raise NotImplementedError() # pragma: no cover - def response_and_validation(self, account_key): + def response_and_validation(self, account_key, *args, **kwargs): """Generate response and validation. Convenience function that return results of `response` and @@ -211,7 +211,8 @@ class KeyAuthorizationChallenge(_TokenDVChallenge): :rtype: tuple """ - return (self.response(account_key), self.validation(account_key)) + return (self.response(account_key), + self.validation(account_key, *args, **kwargs)) @ChallengeResponse.register @@ -308,7 +309,7 @@ class HTTP01(KeyAuthorizationChallenge): """ return "http://" + domain + self.path - def validation(self, account_key): + def validation(self, account_key, **unused_kwargs): """Generate validation. :param JWK account_key: @@ -318,6 +319,127 @@ class HTTP01(KeyAuthorizationChallenge): return self.key_authorization(account_key) +@ChallengeResponse.register +class TLSSNI01Response(KeyAuthorizationChallengeResponse): + """ACME tls-sni-01 challenge response.""" + typ = "tls-sni-01" + + DOMAIN_SUFFIX = b".acme.invalid" + """Domain name suffix.""" + + PORT = 443 + + @property + def z(self): + """``z`` value used for verification.""" + return hashlib.sha256( + self.key_authorization.encode("utf-8")).hexdigest().encode() + + @property + def z_domain(self): + """Domain name used for verification, generated from `z`.""" + return self.z[:32] + b'.' + self.z[32:] + self.DOMAIN_SUFFIX + + def gen_cert(self, key=None, bits=2048): + """Generate tls-sni-01 certificate. + + :param OpenSSL.crypto.PKey key: Optional private key used in + certificate generation. If not provided (``None``), then + fresh key will be generated. + :param int bits: Number of bits for newly generated key. + + :rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey` + + """ + if key is None: + key = OpenSSL.crypto.PKey() + key.generate_key(OpenSSL.crypto.TYPE_RSA, bits) + return crypto_util.gen_ss_cert(key, [ + # z_domain is too big to fit into CN, hence first dummy domain + 'dummy', self.z_domain.decode()], force_san=True), key + + def probe_cert(self, domain, **kwargs): + """Probe tls-sni-01 challenge certificate. + + :param unicode domain: + + """ + # TODO: domain is not necessary if host is provided + if "host" not in kwargs: + host = socket.gethostbyname(domain) + logging.debug('%s resolved to %s', domain, host) + kwargs["host"] = host + + kwargs.setdefault("port", self.PORT) + kwargs["name"] = self.z_domain + # TODO: try different methods? + # pylint: disable=protected-access + return crypto_util.probe_sni(**kwargs) + + def verify_cert(self, cert): + """Verify tls-sni-01 challenge certificate.""" + # pylint: disable=protected-access + sans = crypto_util._pyopenssl_cert_or_req_san(cert) + logging.debug('Certificate %s. SANs: %s', cert.digest('sha1'), sans) + return self.z_domain.decode() in sans + + def simple_verify(self, chall, domain, account_public_key, + cert=None, **kwargs): + """Simple verify. + + Verify ``validation`` using ``account_public_key``, optionally + probe tls-sni-01 certificate and check using `verify_cert`. + + :param .challenges.TLSSNI01 chall: Corresponding challenge. + :param str domain: Domain name being validated. + :param JWK account_public_key: + :param OpenSSL.crypto.X509 cert: Optional certificate. If not + provided (``None``) certificate will be retrieved using + `probe_cert`. + + + :returns: ``True`` iff client's control of the domain has been + verified, ``False`` otherwise. + :rtype: bool + + """ + if not self.verify(chall, account_public_key): + logger.debug("Verification of key authorization in response failed") + return False + + if cert is None: + try: + cert = self.probe_cert(domain=domain, **kwargs) + except errors.Error as error: + logger.debug(error, exc_info=True) + return False + + return self.verify_cert(cert) + + +@Challenge.register # pylint: disable=too-many-ancestors +class TLSSNI01(KeyAuthorizationChallenge): + """ACME tls-sni-01 challenge.""" + response_cls = TLSSNI01Response + typ = response_cls.typ + + # boulder#962, ietf-wg-acme#22 + #n = jose.Field("n", encoder=int, decoder=int) + + def validation(self, account_key, **kwargs): + """Generate validation. + + :param JWK account_key: + :param OpenSSL.crypto.PKey cert_key: Optional private key used + in certificate generation. If not provided (``None``), then + fresh key will be generated. + + :rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey` + + """ + return self.response(account_key).gen_cert(key=kwargs.get('cert_key')) + + @Challenge.register # pylint: disable=too-many-ancestors class DVSNI(_TokenDVChallenge): """ACME "dvsni" challenge. diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 86291d0e8..3fcb01e4d 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -186,6 +186,140 @@ class HTTP01Test(unittest.TestCase): self.msg.update(token=b'..').good_token) +class TLSSNI01ResponseTest(unittest.TestCase): + # pylint: disable=too-many-instance-attributes + + def setUp(self): + from acme.challenges import TLSSNI01 + self.chall = TLSSNI01( + token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e')) + + self.response = self.chall.response(KEY) + self.jmsg = { + 'resource': 'challenge', + 'type': 'tls-sni-01', + 'keyAuthorization': self.response.key_authorization, + } + + # pylint: disable=invalid-name + label1 = b'dc38d9c3fa1a4fdcc3a5501f2d38583f' + label2 = b'b7793728f084394f2a1afd459556bb5c' + self.z = label1 + label2 + self.z_domain = label1 + b'.' + label2 + b'.acme.invalid' + self.domain = 'foo.com' + + def test_z_and_domain(self): + self.assertEqual(self.z, self.response.z) + self.assertEqual(self.z_domain, self.response.z_domain) + + def test_to_partial_json(self): + self.assertEqual(self.jmsg, self.response.to_partial_json()) + + def test_from_json(self): + from acme.challenges import TLSSNI01Response + self.assertEqual(self.response, TLSSNI01Response.from_json(self.jmsg)) + + def test_from_json_hashable(self): + from acme.challenges import TLSSNI01Response + hash(TLSSNI01Response.from_json(self.jmsg)) + + @mock.patch('acme.challenges.socket.gethostbyname') + @mock.patch('acme.challenges.crypto_util.probe_sni') + def test_probe_cert(self, mock_probe_sni, mock_gethostbyname): + mock_gethostbyname.return_value = '127.0.0.1' + self.response.probe_cert('foo.com') + mock_gethostbyname.assert_called_once_with('foo.com') + mock_probe_sni.assert_called_once_with( + host='127.0.0.1', port=self.response.PORT, + name=self.z_domain) + + self.response.probe_cert('foo.com', host='8.8.8.8') + mock_probe_sni.assert_called_with( + host='8.8.8.8', port=mock.ANY, name=mock.ANY) + + self.response.probe_cert('foo.com', port=1234) + mock_probe_sni.assert_called_with( + host=mock.ANY, port=1234, name=mock.ANY) + + self.response.probe_cert('foo.com', bar='baz') + mock_probe_sni.assert_called_with( + host=mock.ANY, port=mock.ANY, name=mock.ANY, bar='baz') + + self.response.probe_cert('foo.com', name=b'xxx') + mock_probe_sni.assert_called_with( + host=mock.ANY, port=mock.ANY, + name=self.z_domain) + + def test_gen_verify_cert(self): + key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem') + cert, key2 = self.response.gen_cert(key1) + self.assertEqual(key1, key2) + self.assertTrue(self.response.verify_cert(cert)) + + def test_gen_verify_cert_gen_key(self): + cert, key = self.response.gen_cert() + self.assertTrue(isinstance(key, OpenSSL.crypto.PKey)) + self.assertTrue(self.response.verify_cert(cert)) + + def test_verify_bad_cert(self): + self.assertFalse(self.response.verify_cert( + test_util.load_cert('cert.pem'))) + + def test_simple_verify_bad_key_authorization(self): + key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) + self.response.simple_verify(self.chall, "local", key2.public_key()) + + @mock.patch('acme.challenges.TLSSNI01Response.verify_cert', autospec=True) + def test_simple_verify(self, mock_verify_cert): + mock_verify_cert.return_value = mock.sentinel.verification + self.assertEqual(mock.sentinel.verification, self.response.simple_verify( + self.chall, self.domain, KEY.public_key(), + cert=mock.sentinel.cert)) + mock_verify_cert.assert_called_once_with(self.response, mock.sentinel.cert) + + @mock.patch('acme.challenges.TLSSNI01Response.probe_cert') + def test_simple_verify_false_on_probe_error(self, mock_probe_cert): + mock_probe_cert.side_effect = errors.Error + self.assertFalse(self.response.simple_verify( + self.chall, self.domain, KEY.public_key())) + + +class TLSSNI01Test(unittest.TestCase): + + def setUp(self): + from acme.challenges import TLSSNI01 + self.msg = TLSSNI01( + token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e')) + self.jmsg = { + 'type': 'tls-sni-01', + 'token': 'a82d5ff8ef740d12881f6d3c2277ab2e', + } + + def test_to_partial_json(self): + self.assertEqual(self.jmsg, self.msg.to_partial_json()) + + def test_from_json(self): + from acme.challenges import TLSSNI01 + self.assertEqual(self.msg, TLSSNI01.from_json(self.jmsg)) + + def test_from_json_hashable(self): + from acme.challenges import TLSSNI01 + hash(TLSSNI01.from_json(self.jmsg)) + + def test_from_json_invalid_token_length(self): + from acme.challenges import TLSSNI01 + self.jmsg['token'] = jose.encode_b64jose(b'abcd') + self.assertRaises( + jose.DeserializationError, TLSSNI01.from_json, self.jmsg) + + @mock.patch('acme.challenges.TLSSNI01Response.gen_cert') + def test_validation(self, mock_gen_cert): + mock_gen_cert.return_value = ('cert', 'key') + self.assertEqual(('cert', 'key'), self.msg.validation( + KEY, cert_key=mock.sentinel.cert_key)) + mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key) + + class DVSNITest(unittest.TestCase): def setUp(self): From 31706a5ef9439bc7950224818234a3efe75354d3 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Nov 2015 14:03:58 +0000 Subject: [PATCH 028/124] tls-sni-01: acme_util and auth_handler --- letsencrypt/tests/acme_util.py | 7 ++-- letsencrypt/tests/auth_handler_test.py | 46 +++++++++++++------------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/letsencrypt/tests/acme_util.py b/letsencrypt/tests/acme_util.py index 300eb453b..de75a2bc9 100644 --- a/letsencrypt/tests/acme_util.py +++ b/letsencrypt/tests/acme_util.py @@ -14,6 +14,8 @@ KEY = test_util.load_rsa_private_key('rsa512_key.pem') # Challenges HTTP01 = challenges.HTTP01( token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") +TLSSNI01 = challenges.TLSSNI01( + token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA")) DVSNI = challenges.DVSNI( token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA")) DNS = challenges.DNS(token="17817c66b60ce2e4012dfad92657527a") @@ -41,7 +43,7 @@ POP = challenges.ProofOfPossession( ) ) -CHALLENGES = [HTTP01, DVSNI, DNS, RECOVERY_CONTACT, POP] +CHALLENGES = [HTTP01, TLSSNI01, DNS, RECOVERY_CONTACT, POP] DV_CHALLENGES = [chall for chall in CHALLENGES if isinstance(chall, challenges.DVChallenge)] CONT_CHALLENGES = [chall for chall in CHALLENGES @@ -80,12 +82,13 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name # Pending ChallengeBody objects DVSNI_P = chall_to_challb(DVSNI, messages.STATUS_PENDING) +TLSSNI01_P = chall_to_challb(TLSSNI01, messages.STATUS_PENDING) HTTP01_P = chall_to_challb(HTTP01, messages.STATUS_PENDING) DNS_P = chall_to_challb(DNS, messages.STATUS_PENDING) RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, messages.STATUS_PENDING) POP_P = chall_to_challb(POP, messages.STATUS_PENDING) -CHALLENGES_P = [HTTP01_P, DVSNI_P, DNS_P, RECOVERY_CONTACT_P, POP_P] +CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS_P, RECOVERY_CONTACT_P, POP_P] DV_CHALLENGES_P = [challb for challb in CHALLENGES_P if isinstance(challb.chall, challenges.DVChallenge)] CONT_CHALLENGES_P = [ diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 7be37c91e..be19ab036 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -45,7 +45,7 @@ class ChallengeFactoryTest(unittest.TestCase): self.assertEqual( [achall.chall for achall in cont_c], [acme_util.RECOVERY_CONTACT]) - self.assertEqual([achall.chall for achall in dv_c], [acme_util.DVSNI]) + self.assertEqual([achall.chall for achall in dv_c], [acme_util.TLSSNI01]) def test_unrecognized(self): self.handler.authzr["failure.com"] = acme_util.gen_authzr( @@ -70,7 +70,7 @@ class GetAuthorizationsTest(unittest.TestCase): self.mock_dv_auth = mock.MagicMock(name="ApacheConfigurator") self.mock_cont_auth = mock.MagicMock(name="ContinuityAuthenticator") - self.mock_dv_auth.get_chall_pref.return_value = [challenges.DVSNI] + self.mock_dv_auth.get_chall_pref.return_value = [challenges.TLSSNI01] self.mock_cont_auth.get_chall_pref.return_value = [ challenges.RecoveryContact] @@ -90,7 +90,7 @@ class GetAuthorizationsTest(unittest.TestCase): logging.disable(logging.NOTSET) @mock.patch("letsencrypt.auth_handler.AuthHandler._poll_challenges") - def test_name1_dvsni1(self, mock_poll): + def test_name1_tls_sni_01_1(self, mock_poll): self.mock_net.request_domain_challenges.side_effect = functools.partial( gen_dom_authzr, challs=acme_util.DV_CHALLENGES) @@ -107,14 +107,14 @@ class GetAuthorizationsTest(unittest.TestCase): self.assertEqual(self.mock_dv_auth.cleanup.call_count, 1) self.assertEqual(self.mock_cont_auth.cleanup.call_count, 0) - # Test if list first element is DVSNI, use typ because it is an achall + # Test if list first element is TLSSNI01, use typ because it is an achall self.assertEqual( - self.mock_dv_auth.cleanup.call_args[0][0][0].typ, "dvsni") + self.mock_dv_auth.cleanup.call_args[0][0][0].typ, "tls-sni-01") self.assertEqual(len(authzr), 1) @mock.patch("letsencrypt.auth_handler.AuthHandler._poll_challenges") - def test_name3_dvsni3_rectok_3(self, mock_poll): + def test_name3_tls_sni_01_3_rectok_3(self, mock_poll): self.mock_net.request_domain_challenges.side_effect = functools.partial( gen_dom_authzr, challs=acme_util.CHALLENGES) @@ -309,9 +309,9 @@ class GenChallengePathTest(unittest.TestCase): return gen_challenge_path(challbs, preferences, combinations) def test_common_case(self): - """Given DVSNI and HTTP01 with appropriate combos.""" - challbs = (acme_util.DVSNI_P, acme_util.HTTP01_P) - prefs = [challenges.DVSNI] + """Given TLSSNI01 and HTTP01 with appropriate combos.""" + challbs = (acme_util.TLSSNI01_P, acme_util.HTTP01_P) + prefs = [challenges.TLSSNI01] combos = ((0,), (1,)) # Smart then trivial dumb path test @@ -324,9 +324,9 @@ class GenChallengePathTest(unittest.TestCase): def test_common_case_with_continuity(self): challbs = (acme_util.POP_P, acme_util.RECOVERY_CONTACT_P, - acme_util.DVSNI_P, + acme_util.TLSSNI01_P, acme_util.HTTP01_P) - prefs = [challenges.ProofOfPossession, challenges.DVSNI] + prefs = [challenges.ProofOfPossession, challenges.TLSSNI01] combos = acme_util.gen_combos(challbs) self.assertEqual(self._call(challbs, prefs, combos), (0, 2)) @@ -336,14 +336,14 @@ class GenChallengePathTest(unittest.TestCase): def test_full_cont_server(self): challbs = (acme_util.RECOVERY_CONTACT_P, acme_util.POP_P, - acme_util.DVSNI_P, + acme_util.TLSSNI01_P, acme_util.HTTP01_P, acme_util.DNS_P) # Typical webserver client that can do everything except DNS # Attempted to make the order realistic prefs = [challenges.ProofOfPossession, challenges.HTTP01, - challenges.DVSNI, + challenges.TLSSNI01, challenges.RecoveryContact] combos = acme_util.gen_combos(challbs) self.assertEqual(self._call(challbs, prefs, combos), (1, 3)) @@ -352,8 +352,8 @@ class GenChallengePathTest(unittest.TestCase): self.assertTrue(self._call(challbs, prefs, None)) def test_not_supported(self): - challbs = (acme_util.POP_P, acme_util.DVSNI_P) - prefs = [challenges.DVSNI] + challbs = (acme_util.POP_P, acme_util.TLSSNI01_P) + prefs = [challenges.TLSSNI01] combos = ((0, 1),) self.assertRaises( @@ -411,7 +411,7 @@ class IsPreferredTest(unittest.TestCase): def _call(cls, chall, satisfied): from letsencrypt.auth_handler import is_preferred return is_preferred(chall, satisfied, exclusive_groups=frozenset([ - frozenset([challenges.DVSNI, challenges.HTTP01]), + frozenset([challenges.TLSSNI01, challenges.HTTP01]), frozenset([challenges.DNS, challenges.HTTP01]), ])) @@ -421,11 +421,11 @@ class IsPreferredTest(unittest.TestCase): def test_mutually_exclusvie(self): self.assertFalse( self._call( - acme_util.DVSNI_P, frozenset([acme_util.HTTP01_P]))) + acme_util.TLSSNI01_P, frozenset([acme_util.HTTP01_P]))) def test_mutually_exclusive_same_type(self): self.assertTrue( - self._call(acme_util.DVSNI_P, frozenset([acme_util.DVSNI_P]))) + self._call(acme_util.TLSSNI01_P, frozenset([acme_util.TLSSNI01_P]))) class ReportFailedChallsTest(unittest.TestCase): @@ -446,15 +446,15 @@ class ReportFailedChallsTest(unittest.TestCase): domain="example.com", account_key="key") - kwargs["chall"] = acme_util.DVSNI - self.dvsni_same = achallenges.DVSNI( + kwargs["chall"] = acme_util.TLSSNI01 + self.tls_sni_same = achallenges.KeyAuthorizationAnnotatedChallenge( # pylint: disable=star-args challb=messages.ChallengeBody(**kwargs), domain="example.com", account_key="key") kwargs["error"] = messages.Error(typ="dnssec", detail="detail") - self.dvsni_diff = achallenges.DVSNI( + self.tls_sni_diff = achallenges.KeyAuthorizationAnnotatedChallenge( # pylint: disable=star-args challb=messages.ChallengeBody(**kwargs), domain="foo.bar", @@ -464,7 +464,7 @@ class ReportFailedChallsTest(unittest.TestCase): def test_same_error_and_domain(self, mock_zope): from letsencrypt import auth_handler - auth_handler._report_failed_challs([self.http01, self.dvsni_same]) + auth_handler._report_failed_challs([self.http01, self.tls_sni_same]) call_list = mock_zope().add_message.call_args_list self.assertTrue(len(call_list) == 1) self.assertTrue("Domains: example.com\n" in call_list[0][0][0]) @@ -473,7 +473,7 @@ class ReportFailedChallsTest(unittest.TestCase): def test_different_errors_and_domains(self, mock_zope): from letsencrypt import auth_handler - auth_handler._report_failed_challs([self.http01, self.dvsni_diff]) + auth_handler._report_failed_challs([self.http01, self.tls_sni_diff]) self.assertTrue(mock_zope().add_message.call_count == 2) From 6bcbe641bd7a9a04873f950362158850bcc003c8 Mon Sep 17 00:00:00 2001 From: Dev & Sec Date: Sat, 7 Nov 2015 16:21:47 +0000 Subject: [PATCH 029/124] fix 2 IndexErrors in the nginx plugin --- letsencrypt-nginx/letsencrypt_nginx/parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/parser.py b/letsencrypt-nginx/letsencrypt_nginx/parser.py index 19483821a..fb79703dc 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/parser.py +++ b/letsencrypt-nginx/letsencrypt_nginx/parser.py @@ -246,7 +246,7 @@ class NginxParser(object): # Can't be a server block return False - if item[0] == 'server_name': + if len(item) > 0 and item[0] == 'server_name': server_names.update(_get_servernames(item[1])) return server_names == names @@ -425,7 +425,7 @@ def _is_include_directive(entry): """ return (isinstance(entry, list) and - entry[0] == 'include' and len(entry) == 2 and + len(entry) == 2 and entry[0] == 'include' and isinstance(entry[1], str)) From 93e69ef7dee7d7315ec44e5567fba1f3bfa145a6 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 1 Nov 2015 12:22:19 +0000 Subject: [PATCH 030/124] tls-sni-01 for standalone --- docs/using.rst | 2 +- examples/cli.ini | 2 +- letsencrypt/achallenges.py | 5 +++-- letsencrypt/plugins/standalone.py | 30 ++++++++++++++------------ letsencrypt/plugins/standalone_test.py | 30 +++++++++++++------------- tests/boulder-integration.sh | 2 +- 6 files changed, 37 insertions(+), 34 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 1bc7fab99..8e8fd132e 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -135,7 +135,7 @@ Plugin A I Notes and status ========== = = ================================================================ standalone Y N Very stable. Uses port 80 (force by ``--standalone-supported-challenges http-01``) or 443 - (force by ``--standalone-supported-challenges dvsni``). + (force by ``--standalone-supported-challenges tls-sni-01``). apache Y Y Alpha. Automates Apache installation, works fairly well but on Debian-based distributions only for now. webroot Y N Works with already running webserver, by writing necessary files diff --git a/examples/cli.ini b/examples/cli.ini index 34fb8ab02..a20764ed8 100644 --- a/examples/cli.ini +++ b/examples/cli.ini @@ -16,7 +16,7 @@ server = https://acme-staging.api.letsencrypt.org/directory # Uncomment to use the standalone authenticator on port 443 # authenticator = standalone -# standalone-supported-challenges = dvsni +# standalone-supported-challenges = tls-sni-01 # Uncomment to use the webroot authenticator. Replace webroot-path with the # path to the public_html / webroot folder being served by your web server. diff --git a/letsencrypt/achallenges.py b/letsencrypt/achallenges.py index f08c6a396..4e46d6af9 100644 --- a/letsencrypt/achallenges.py +++ b/letsencrypt/achallenges.py @@ -49,9 +49,10 @@ class KeyAuthorizationAnnotatedChallenge(AnnotatedChallenge): """Client annotated `KeyAuthorizationChallenge` challenge.""" __slots__ = ('challb', 'domain', 'account_key') - def response_and_validation(self): + def response_and_validation(self, *args, **kwargs): """Generate response and validation.""" - return self.challb.chall.response_and_validation(self.account_key) + return self.challb.chall.response_and_validation( + self.account_key, *args, **kwargs) class DVSNI(AnnotatedChallenge): diff --git a/letsencrypt/plugins/standalone.py b/letsencrypt/plugins/standalone.py index 3975e9292..717434052 100644 --- a/letsencrypt/plugins/standalone.py +++ b/letsencrypt/plugins/standalone.py @@ -11,7 +11,6 @@ import six import zope.interface from acme import challenges -from acme import crypto_util as acme_crypto_util from acme import standalone as acme_standalone from letsencrypt import errors @@ -51,19 +50,19 @@ class ServerManager(object): :param int port: Port to run the server on. :param challenge_type: Subclass of `acme.challenges.Challenge`, - either `acme.challenge.HTTP01` or `acme.challenges.DVSNI`. + either `acme.challenge.HTTP01` or `acme.challenges.TLSSNI01`. :returns: Server instance. :rtype: ACMEServerMixin """ - assert challenge_type in (challenges.DVSNI, challenges.HTTP01) + assert challenge_type in (challenges.TLSSNI01, challenges.HTTP01) if port in self._instances: return self._instances[port].server address = ("", port) try: - if challenge_type is challenges.DVSNI: + if challenge_type is challenges.TLSSNI01: server = acme_standalone.DVSNIServer(address, self.certs) else: # challenges.HTTP01 server = acme_standalone.HTTP01Server( @@ -109,7 +108,7 @@ class ServerManager(object): in six.iteritems(self._instances)) -SUPPORTED_CHALLENGES = set([challenges.DVSNI, challenges.HTTP01]) +SUPPORTED_CHALLENGES = set([challenges.TLSSNI01, challenges.HTTP01]) def supported_challenges_validator(data): @@ -138,7 +137,7 @@ class Authenticator(common.Plugin): """Standalone Authenticator. This authenticator creates its own ephemeral TCP listener on the - necessary port in order to respond to incoming DVSNI and HTTP01 + necessary port in order to respond to incoming tls-sni-01 and http-01 challenges from the certificate authority. Therefore, it does not rely on any existing server program. """ @@ -150,7 +149,7 @@ class Authenticator(common.Plugin): def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) - # one self-signed key for all DVSNI certificates + # one self-signed key for all tls-sni-01 certificates self.key = OpenSSL.crypto.PKey() self.key.generate_key(OpenSSL.crypto.TYPE_RSA, bits=2048) @@ -183,15 +182,16 @@ class Authenticator(common.Plugin): necessary_ports = set() if challenges.HTTP01 in self.supported_challenges: necessary_ports.add(self.config.http01_port) - if challenges.DVSNI in self.supported_challenges: + if challenges.TLSSNI01 in self.supported_challenges: necessary_ports.add(self.config.dvsni_port) return necessary_ports def more_info(self): # pylint: disable=missing-docstring return("This authenticator creates its own ephemeral TCP listener " - "on the necessary port in order to respond to incoming DVSNI " - "and HTTP01 challenges from the certificate authority. " - "Therefore, it does not rely on any existing server program.") + "on the necessary port in order to respond to incoming " + "tls-sni-01 and http-01 challenges from the certificate " + "authority. Therefore, it does not rely on any existing " + "server program.") def prepare(self): # pylint: disable=missing-docstring pass @@ -241,9 +241,11 @@ class Authenticator(common.Plugin): acme_standalone.HTTP01RequestHandler.HTTP01Resource( chall=achall.chall, response=response, validation=validation)) - else: # DVSNI - server = self.servers.run(self.config.dvsni_port, challenges.DVSNI) - response, cert, _ = achall.gen_cert_and_response(self.key) + else: # tls-sni-01 + server = self.servers.run( + self.config.dvsni_port, challenges.TLSSNI01) + response, (cert, _) = achall.response_and_validation( + cert_key=self.key) self.certs[response.z_domain] = (self.key, cert) self.served[server].add(achall) responses.append(response) diff --git a/letsencrypt/plugins/standalone_test.py b/letsencrypt/plugins/standalone_test.py index c1de52ac8..79718e244 100644 --- a/letsencrypt/plugins/standalone_test.py +++ b/letsencrypt/plugins/standalone_test.py @@ -39,8 +39,8 @@ class ServerManagerTest(unittest.TestCase): self.mgr.stop(port=port) self.assertEqual(self.mgr.running(), {}) - def test_run_stop_dvsni(self): - self._test_run_stop(challenges.DVSNI) + def test_run_stop_tls_sni_01(self): + self._test_run_stop(challenges.TLSSNI01) def test_run_stop_http_01(self): self._test_run_stop(challenges.HTTP01) @@ -73,10 +73,10 @@ class SupportedChallengesValidatorTest(unittest.TestCase): return supported_challenges_validator(data) def test_correct(self): - self.assertEqual("dvsni", self._call("dvsni")) + self.assertEqual("tls-sni-01", self._call("tls-sni-01")) self.assertEqual("http-01", self._call("http-01")) - self.assertEqual("dvsni,http-01", self._call("dvsni,http-01")) - self.assertEqual("http-01,dvsni", self._call("http-01,dvsni")) + self.assertEqual("tls-sni-01,http-01", self._call("tls-sni-01,http-01")) + self.assertEqual("http-01,tls-sni-01", self._call("http-01,tls-sni-01")) def test_unrecognized(self): assert "foo" not in challenges.Challenge.TYPES @@ -93,23 +93,23 @@ class AuthenticatorTest(unittest.TestCase): from letsencrypt.plugins.standalone import Authenticator self.config = mock.MagicMock( dvsni_port=1234, http01_port=4321, - standalone_supported_challenges="dvsni,http-01") + standalone_supported_challenges="tls-sni-01,http-01") self.auth = Authenticator(self.config, name="standalone") def test_supported_challenges(self): self.assertEqual(self.auth.supported_challenges, - set([challenges.DVSNI, challenges.HTTP01])) + set([challenges.TLSSNI01, challenges.HTTP01])) def test_more_info(self): self.assertTrue(isinstance(self.auth.more_info(), six.string_types)) def test_get_chall_pref(self): self.assertEqual(set(self.auth.get_chall_pref(domain=None)), - set([challenges.DVSNI, challenges.HTTP01])) + set([challenges.TLSSNI01, challenges.HTTP01])) @mock.patch("letsencrypt.plugins.standalone.util") def test_perform_alredy_listening(self, mock_util): - for chall, port in ((challenges.DVSNI.typ, 1234), + for chall, port in ((challenges.TLSSNI01.typ, 1234), (challenges.HTTP01.typ, 4321)): mock_util.already_listening.return_value = True self.config.standalone_supported_challenges = chall @@ -155,8 +155,8 @@ class AuthenticatorTest(unittest.TestCase): key = jose.JWK.load(test_util.load_vector('rsa512_key.pem')) http_01 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.HTTP01_P, domain=domain, account_key=key) - dvsni = achallenges.DVSNI( - challb=acme_util.DVSNI_P, domain=domain, account_key=key) + tls_sni_01 = achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.TLSSNI01_P, domain=domain, account_key=key) self.auth.servers = mock.MagicMock() @@ -164,19 +164,19 @@ class AuthenticatorTest(unittest.TestCase): return "server{0}".format(port) self.auth.servers.run.side_effect = _run - responses = self.auth.perform2([http_01, dvsni]) + responses = self.auth.perform2([http_01, tls_sni_01]) self.assertTrue(isinstance(responses, list)) self.assertEqual(2, len(responses)) self.assertTrue(isinstance(responses[0], challenges.HTTP01Response)) - self.assertTrue(isinstance(responses[1], challenges.DVSNIResponse)) + self.assertTrue(isinstance(responses[1], challenges.TLSSNI01Response)) self.assertEqual(self.auth.servers.run.mock_calls, [ mock.call(4321, challenges.HTTP01), - mock.call(1234, challenges.DVSNI), + mock.call(1234, challenges.TLSSNI01), ]) self.assertEqual(self.auth.served, { - "server1234": set([dvsni]), + "server1234": set([tls_sni_01]), "server4321": set([http_01]), }) self.assertEqual(1, len(self.auth.http_01_resources)) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 18a996926..d35ecbcff 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -27,7 +27,7 @@ common() { "$@" } -common --domains le1.wtf --standalone-supported-challenges dvsni auth +common --domains le1.wtf --standalone-supported-challenges tls-sni-01 auth common --domains le2.wtf --standalone-supported-challenges http-01 run common -a manual -d le.wtf auth From 2266baf775b3861efa7d1374d4dfbdf5f2f690d1 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Nov 2015 14:15:04 +0000 Subject: [PATCH 031/124] Renames around DVSNIServer --- acme/acme/crypto_util.py | 8 ++++---- acme/acme/standalone.py | 14 +++++++------- acme/acme/standalone_test.py | 24 +++++++++++++----------- letsencrypt/plugins/standalone.py | 2 +- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/acme/acme/crypto_util.py b/acme/acme/crypto_util.py index 5f24e9d9e..72a93141a 100644 --- a/acme/acme/crypto_util.py +++ b/acme/acme/crypto_util.py @@ -13,7 +13,7 @@ from acme import errors logger = logging.getLogger(__name__) -# DVSNI certificate serving and probing is not affected by SSL +# TLSSNI01 certificate serving and probing is not affected by SSL # vulnerabilities: prober needs to check certificate for expected # contents anyway. Working SNI is the only thing that's necessary for # the challenge and thus scoping down SSL/TLS method (version) would @@ -23,7 +23,7 @@ logger = logging.getLogger(__name__) # https://www.openssl.org/docs/ssl/SSLv23_method.html). _serve_sni # should be changed to use "set_options" to disable SSLv2 and SSLv3, # in case it's used for things other than probing/serving! -_DEFAULT_DVSNI_SSL_METHOD = OpenSSL.SSL.SSLv23_METHOD +_DEFAULT_TLSSNI01_SSL_METHOD = OpenSSL.SSL.SSLv23_METHOD class SSLSocket(object): # pylint: disable=too-few-public-methods @@ -35,7 +35,7 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods :ivar method: See `OpenSSL.SSL.Context` for allowed values. """ - def __init__(self, sock, certs, method=_DEFAULT_DVSNI_SSL_METHOD): + def __init__(self, sock, certs, method=_DEFAULT_TLSSNI01_SSL_METHOD): self.sock = sock self.certs = certs self.method = method @@ -103,7 +103,7 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods def probe_sni(name, host, port=443, timeout=300, - method=_DEFAULT_DVSNI_SSL_METHOD, source_address=('0', 0)): + method=_DEFAULT_TLSSNI01_SSL_METHOD, source_address=('0', 0)): """Probe SNI server for SSL certificate. :param bytes name: Byte string to send as the server name in the diff --git a/acme/acme/standalone.py b/acme/acme/standalone.py index 1466671e3..32dd5ae41 100644 --- a/acme/acme/standalone.py +++ b/acme/acme/standalone.py @@ -30,7 +30,7 @@ class TLSServer(socketserver.TCPServer): self.certs = kwargs.pop("certs", {}) self.method = kwargs.pop( # pylint: disable=protected-access - "method", crypto_util._DEFAULT_DVSNI_SSL_METHOD) + "method", crypto_util._DEFAULT_TLSSNI01_SSL_METHOD) self.allow_reuse_address = kwargs.pop("allow_reuse_address", True) socketserver.TCPServer.__init__(self, *args, **kwargs) @@ -50,8 +50,8 @@ class ACMEServerMixin: # pylint: disable=old-style-class allow_reuse_address = True -class DVSNIServer(TLSServer, ACMEServerMixin): - """DVSNI Server.""" +class TLSSNI01Server(TLSServer, ACMEServerMixin): + """TLSSNI01 Server.""" def __init__(self, server_address, certs): TLSServer.__init__( @@ -134,8 +134,8 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): cls, simple_http_resources=simple_http_resources) -def simple_dvsni_server(cli_args, forever=True): - """Run simple standalone DVSNI server.""" +def simple_tls_sni_01_server(cli_args, forever=True): + """Run simple standalone TLSSNI01 server.""" logging.basicConfig(level=logging.DEBUG) parser = argparse.ArgumentParser() @@ -158,7 +158,7 @@ def simple_dvsni_server(cli_args, forever=True): OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, cert_contents)) - server = DVSNIServer(('', int(args.port)), certs=certs) + server = TLSSNI01Server(('', int(args.port)), certs=certs) six.print_("Serving at https://localhost:{0}...".format( server.socket.getsockname()[1])) if forever: # pragma: no cover @@ -168,4 +168,4 @@ def simple_dvsni_server(cli_args, forever=True): if __name__ == "__main__": - sys.exit(simple_dvsni_server(sys.argv)) # pragma: no cover + sys.exit(simple_tls_sni_01_server(sys.argv)) # pragma: no cover diff --git a/acme/acme/standalone_test.py b/acme/acme/standalone_test.py index 85ef6ab14..02b1f69d3 100644 --- a/acme/acme/standalone_test.py +++ b/acme/acme/standalone_test.py @@ -28,8 +28,8 @@ class TLSServerTest(unittest.TestCase): server.server_close() # pylint: disable=no-member -class DVSNIServerTest(unittest.TestCase): - """Test for acme.standalone.DVSNIServer.""" +class TLSSNI01ServerTest(unittest.TestCase): + """Test for acme.standalone.TLSSNI01Server.""" def setUp(self): self.certs = { @@ -37,8 +37,8 @@ class DVSNIServerTest(unittest.TestCase): # pylint: disable=protected-access test_util.load_cert('cert.pem')._wrapped), } - from acme.standalone import DVSNIServer - self.server = DVSNIServer(("", 0), certs=self.certs) + from acme.standalone import TLSSNI01Server + self.server = TLSSNI01Server(("", 0), certs=self.certs) # pylint: disable=no-member self.thread = threading.Thread(target=self.server.serve_forever) self.thread.start() @@ -106,8 +106,8 @@ class HTTP01ServerTest(unittest.TestCase): self.assertFalse(self._test_http01(add=False)) -class TestSimpleDVSNIServer(unittest.TestCase): - """Tests for acme.standalone.simple_dvsni_server.""" +class TestSimpleTLSSNI01Server(unittest.TestCase): + """Tests for acme.standalone.simple_tls_sni_01_server.""" def setUp(self): # mirror ../examples/standalone @@ -118,12 +118,14 @@ class TestSimpleDVSNIServer(unittest.TestCase): shutil.copy(test_util.vector_path('rsa512_key.pem'), os.path.join(localhost_dir, 'key.pem')) - from acme.standalone import simple_dvsni_server + from acme.standalone import simple_tls_sni_01_server self.port = 1234 - self.thread = threading.Thread(target=simple_dvsni_server, kwargs={ - 'cli_args': ('xxx', '--port', str(self.port)), - 'forever': False, - }) + self.thread = threading.Thread( + target=simple_tls_sni_01_server, kwargs={ + 'cli_args': ('xxx', '--port', str(self.port)), + 'forever': False, + }, + ) self.old_cwd = os.getcwd() os.chdir(self.test_cwd) self.thread.start() diff --git a/letsencrypt/plugins/standalone.py b/letsencrypt/plugins/standalone.py index 717434052..afe0b6b39 100644 --- a/letsencrypt/plugins/standalone.py +++ b/letsencrypt/plugins/standalone.py @@ -63,7 +63,7 @@ class ServerManager(object): address = ("", port) try: if challenge_type is challenges.TLSSNI01: - server = acme_standalone.DVSNIServer(address, self.certs) + server = acme_standalone.TLSSNI01Server(address, self.certs) else: # challenges.HTTP01 server = acme_standalone.HTTP01Server( address, self.http_01_resources) From 5e8ed2bbd2707684fd76bc7662e1c835992f2eb8 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Nov 2015 14:21:58 +0000 Subject: [PATCH 032/124] --dvsni-port -> --tls-sni-01-port --- letsencrypt-apache/letsencrypt_apache/dvsni.py | 7 ++++--- letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py | 2 +- letsencrypt-nginx/letsencrypt_nginx/configurator.py | 7 ++++--- letsencrypt-nginx/letsencrypt_nginx/dvsni.py | 2 +- letsencrypt-nginx/letsencrypt_nginx/tests/util.py | 2 +- letsencrypt/cli.py | 5 +++-- letsencrypt/configuration.py | 6 +++--- letsencrypt/constants.py | 4 ++-- letsencrypt/interfaces.py | 4 ++-- letsencrypt/plugins/standalone.py | 4 ++-- letsencrypt/plugins/standalone_test.py | 2 +- letsencrypt/renewer.py | 2 +- letsencrypt/tests/configuration_test.py | 4 ++-- letsencrypt/tests/renewer_test.py | 2 +- tests/integration/_common.sh | 2 +- 15 files changed, 29 insertions(+), 26 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/dvsni.py b/letsencrypt-apache/letsencrypt_apache/dvsni.py index ed88bf8a7..02ae34150 100644 --- a/letsencrypt-apache/letsencrypt_apache/dvsni.py +++ b/letsencrypt-apache/letsencrypt_apache/dvsni.py @@ -62,7 +62,7 @@ class ApacheDvsni(common.Dvsni): # Prepare the server for HTTPS self.configurator.prepare_server_https( - str(self.configurator.config.dvsni_port), True) + str(self.configurator.config.tls_sni_01_port), True) responses = [] @@ -114,14 +114,15 @@ class ApacheDvsni(common.Dvsni): # TODO: Checkout _default_ rules. dvsni_addrs = set() - default_addr = obj.Addr(("*", str(self.configurator.config.dvsni_port))) + default_addr = obj.Addr(("*", str( + self.configurator.config.tls_sni_01_port))) for addr in vhost.addrs: if "_default_" == addr.get_addr(): dvsni_addrs.add(default_addr) else: dvsni_addrs.add( - addr.get_sni_addr(self.configurator.config.dvsni_port)) + addr.get_sni_addr(self.configurator.config.tls_sni_01_port)) return dvsni_addrs diff --git a/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py b/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py index c362d4115..6cb93e0ca 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py @@ -21,7 +21,7 @@ class DvsniPerformTest(util.ApacheTest): config = util.get_apache_configurator( self.config_path, self.config_dir, self.work_dir) - config.config.dvsni_port = 443 + config.config.tls_sni_01_port = 443 from letsencrypt_apache import dvsni self.sni = dvsni.ApacheDvsni(config) diff --git a/letsencrypt-nginx/letsencrypt_nginx/configurator.py b/letsencrypt-nginx/letsencrypt_nginx/configurator.py index 29e69e498..80b4ebc5d 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/configurator.py +++ b/letsencrypt-nginx/letsencrypt_nginx/configurator.py @@ -297,7 +297,7 @@ class NginxConfigurator(common.Plugin): """Make a server SSL. Make a server SSL based on server_name and filename by adding a - ``listen IConfig.dvsni_port ssl`` directive to the server block. + ``listen IConfig.tls_sni_01_port ssl`` directive to the server block. .. todo:: Maybe this should create a new block instead of modifying the existing one? @@ -307,7 +307,7 @@ class NginxConfigurator(common.Plugin): """ snakeoil_cert, snakeoil_key = self._get_snakeoil_paths() - ssl_block = [['listen', '{0} ssl'.format(self.config.dvsni_port)], + ssl_block = [['listen', '{0} ssl'.format(self.config.tls_sni_01_port)], # access and error logs necessary for integration # testing (non-root) ['access_log', os.path.join( @@ -321,7 +321,8 @@ class NginxConfigurator(common.Plugin): vhost.filep, vhost.names, ssl_block) vhost.ssl = True vhost.raw.extend(ssl_block) - vhost.addrs.add(obj.Addr('', str(self.config.dvsni_port), True, False)) + vhost.addrs.add(obj.Addr( + '', str(self.config.tls_sni_01_port), True, False)) def get_all_certs_keys(self): """Find all existing keys, certs from configuration. diff --git a/letsencrypt-nginx/letsencrypt_nginx/dvsni.py b/letsencrypt-nginx/letsencrypt_nginx/dvsni.py index 662f10889..828dac650 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/dvsni.py +++ b/letsencrypt-nginx/letsencrypt_nginx/dvsni.py @@ -48,7 +48,7 @@ class NginxDvsni(common.Dvsni): addresses = [] default_addr = "{0} default_server ssl".format( - self.configurator.config.dvsni_port) + self.configurator.config.tls_sni_01_port) for achall in self.achalls: vhost = self.configurator.choose_vhost(achall.domain) diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/util.py b/letsencrypt-nginx/letsencrypt_nginx/tests/util.py index 953c5d367..cb4e08ddf 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/util.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/util.py @@ -59,7 +59,7 @@ def get_nginx_configurator( temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"), in_progress_dir=os.path.join(backups, "IN_PROGRESS"), server="https://acme-server.org:443/new", - dvsni_port=5001, + tls_sni_01_port=5001, ), name="nginx", version=version) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 5757783cd..c1f3edb70 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -850,8 +850,9 @@ def prepare_and_parse_args(plugins, args): help=config_help("no_verify_ssl"), default=flag_default("no_verify_ssl")) helpful.add( - "testing", "--dvsni-port", type=int, default=flag_default("dvsni_port"), - help=config_help("dvsni_port")) + "testing", "--tls-sni-01-port", type=int, + default=flag_default("tls_sni_01_port"), + help=config_help("tls_sni_01_port")) helpful.add("testing", "--http-01-port", dest="http01_port", type=int, help=config_help("http01_port")) diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index b604651e9..c5a02dfef 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -37,10 +37,10 @@ class NamespaceConfig(object): def __init__(self, namespace): self.namespace = namespace - if self.http01_port == self.dvsni_port: + if self.http01_port == self.tls_sni_01_port: raise errors.Error( - "Trying to run http-01 and DVSNI " - "on the same port ({0})".format(self.dvsni_port)) + "Trying to run http-01 and tls-sni-01 " + "on the same port ({0})".format(self.tls_sni_01_port)) def __getattr__(self, name): return getattr(self.namespace, name) diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index 15f8c53f0..f71bf0329 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -23,7 +23,7 @@ CLI_DEFAULTS = dict( work_dir="/var/lib/letsencrypt", logs_dir="/var/log/letsencrypt", no_verify_ssl=False, - dvsni_port=challenges.DVSNI.PORT, + tls_sni_01_port=challenges.TLSSNI01Response.PORT, auth_cert_path="./cert.pem", auth_chain_path="./chain.pem", @@ -41,7 +41,7 @@ RENEWER_DEFAULTS = dict( EXCLUSIVE_CHALLENGES = frozenset([frozenset([ - challenges.DVSNI, challenges.HTTP01])]) + challenges.TLSSNI01, challenges.HTTP01])]) """Mutually exclusive challenges.""" diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 498b01683..987fdc25e 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -219,8 +219,8 @@ class IConfig(zope.interface.Interface): no_verify_ssl = zope.interface.Attribute( "Disable SSL certificate verification.") - dvsni_port = zope.interface.Attribute( - "Port number to perform DVSNI challenge. " + tls_sni_01_port = zope.interface.Attribute( + "Port number to perform tls-sni-01 challenge. " "Boulder in testing mode defaults to 5001.") http01_port = zope.interface.Attribute( diff --git a/letsencrypt/plugins/standalone.py b/letsencrypt/plugins/standalone.py index afe0b6b39..8b8612fd1 100644 --- a/letsencrypt/plugins/standalone.py +++ b/letsencrypt/plugins/standalone.py @@ -183,7 +183,7 @@ class Authenticator(common.Plugin): if challenges.HTTP01 in self.supported_challenges: necessary_ports.add(self.config.http01_port) if challenges.TLSSNI01 in self.supported_challenges: - necessary_ports.add(self.config.dvsni_port) + necessary_ports.add(self.config.tls_sni_01_port) return necessary_ports def more_info(self): # pylint: disable=missing-docstring @@ -243,7 +243,7 @@ class Authenticator(common.Plugin): validation=validation)) else: # tls-sni-01 server = self.servers.run( - self.config.dvsni_port, challenges.TLSSNI01) + self.config.tls_sni_01_port, challenges.TLSSNI01) response, (cert, _) = achall.response_and_validation( cert_key=self.key) self.certs[response.z_domain] = (self.key, cert) diff --git a/letsencrypt/plugins/standalone_test.py b/letsencrypt/plugins/standalone_test.py index 79718e244..26a040c2e 100644 --- a/letsencrypt/plugins/standalone_test.py +++ b/letsencrypt/plugins/standalone_test.py @@ -92,7 +92,7 @@ class AuthenticatorTest(unittest.TestCase): def setUp(self): from letsencrypt.plugins.standalone import Authenticator self.config = mock.MagicMock( - dvsni_port=1234, http01_port=4321, + tls_sni_01_port=1234, http01_port=4321, standalone_supported_challenges="tls-sni-01,http-01") self.auth = Authenticator(self.config, name="standalone") diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py index 40e49702a..0a490d447 100644 --- a/letsencrypt/renewer.py +++ b/letsencrypt/renewer.py @@ -75,7 +75,7 @@ def renew(cert, old_version): # XXX: this loses type data (for example, the fact that key_size # was an int, not a str) config.rsa_key_size = int(config.rsa_key_size) - config.dvsni_port = int(config.dvsni_port) + config.tls_sni_01_port = int(config.tls_sni_01_port) config.namespace.http01_port = int(config.namespace.http01_port) zope.component.provideUtility(config) try: diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index c7e227ee5..3a8bf40cf 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -14,12 +14,12 @@ class NamespaceConfigTest(unittest.TestCase): self.namespace = mock.MagicMock( config_dir='/tmp/config', work_dir='/tmp/foo', foo='bar', server='https://acme-server.org:443/new', - dvsni_port=1234, http01_port=4321) + tls_sni_01_port=1234, http01_port=4321) from letsencrypt.configuration import NamespaceConfig self.config = NamespaceConfig(self.namespace) def test_init_same_ports(self): - self.namespace.dvsni_port = 4321 + self.namespace.tls_sni_01_port = 4321 from letsencrypt.configuration import NamespaceConfig self.assertRaises(errors.Error, NamespaceConfig, self.namespace) diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index 05d7e123d..0a39b7987 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -688,7 +688,7 @@ class RenewableCertTests(BaseRenewableCertTest): self.test_rc.configfile["renewalparams"]["rsa_key_size"] = "2048" self.test_rc.configfile["renewalparams"]["server"] = "acme.example.com" self.test_rc.configfile["renewalparams"]["authenticator"] = "fake" - self.test_rc.configfile["renewalparams"]["dvsni_port"] = "4430" + self.test_rc.configfile["renewalparams"]["tls_sni_01_port"] = "4430" self.test_rc.configfile["renewalparams"]["http01_port"] = "1234" self.test_rc.configfile["renewalparams"]["account"] = "abcde" mock_auth = mock.MagicMock() diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index cd894fd10..dbc473728 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -15,7 +15,7 @@ letsencrypt_test () { letsencrypt \ --server "${SERVER:-http://localhost:4000/directory}" \ --no-verify-ssl \ - --dvsni-port 5001 \ + --tls-sni-01-port 5001 \ --http-01-port 5002 \ --manual-test-mode \ $store_flags \ From 937e3edfc19ce014b640ace32887a1df76a7b375 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Nov 2015 18:10:56 +0000 Subject: [PATCH 033/124] tls-sni-01 in apache and nginx plugins --- .../letsencrypt_apache/configurator.py | 12 +++++----- .../letsencrypt_apache/dvsni.py | 12 +++++----- .../tests/configurator_test.py | 12 +++++----- .../letsencrypt_apache/tests/dvsni_test.py | 10 ++++----- .../test_driver.py | 14 ++++++------ .../letsencrypt_nginx/configurator.py | 12 +++++----- letsencrypt-nginx/letsencrypt_nginx/dvsni.py | 4 ++-- .../tests/configurator_test.py | 14 ++++++------ .../letsencrypt_nginx/tests/dvsni_test.py | 22 +++++++++---------- letsencrypt/plugins/common.py | 21 +++++++++--------- letsencrypt/plugins/common_test.py | 22 +++++++++---------- 11 files changed, 76 insertions(+), 79 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index d376fe4b6..b8ca05550 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -13,7 +13,6 @@ import zope.interface from acme import challenges -from letsencrypt import achallenges from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util @@ -1117,7 +1116,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): ########################################################################### def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use """Return list of challenge preferences.""" - return [challenges.DVSNI] + return [challenges.TLSSNI01] def perform(self, achalls): """Perform the configuration related challenge. @@ -1132,11 +1131,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): apache_dvsni = dvsni.ApacheDvsni(self) for i, achall in enumerate(achalls): - if isinstance(achall, achallenges.DVSNI): - # Currently also have dvsni hold associated index - # of the challenge. This helps to put all of the responses back - # together when they are all complete. - apache_dvsni.add_chall(achall, i) + # Currently also have dvsni hold associated index + # of the challenge. This helps to put all of the responses back + # together when they are all complete. + apache_dvsni.add_chall(achall, i) sni_response = apache_dvsni.perform() if sni_response: diff --git a/letsencrypt-apache/letsencrypt_apache/dvsni.py b/letsencrypt-apache/letsencrypt_apache/dvsni.py index 02ae34150..2f9e9ed18 100644 --- a/letsencrypt-apache/letsencrypt_apache/dvsni.py +++ b/letsencrypt-apache/letsencrypt_apache/dvsni.py @@ -7,14 +7,14 @@ from letsencrypt_apache import obj from letsencrypt_apache import parser -class ApacheDvsni(common.Dvsni): +class ApacheDvsni(common.TLSSNI01): """Class performs DVSNI challenges within the Apache configurator. :ivar configurator: ApacheConfigurator object :type configurator: :class:`~apache.configurator.ApacheConfigurator` - :ivar list achalls: Annotated :class:`~letsencrypt.achallenges.DVSNI` - challenges. + :ivar list achalls: Annotated tls-sni-01 + (`.KeyAuthorizationAnnotatedChallenge`) challenges. :param list indices: Meant to hold indices of challenges in a larger array. ApacheDvsni is capable of solving many challenges @@ -145,8 +145,8 @@ class ApacheDvsni(common.Dvsni): def _get_config_text(self, achall, ip_addrs): """Chocolate virtual server configuration text - :param achall: Annotated DVSNI challenge. - :type achall: :class:`letsencrypt.achallenges.DVSNI` + :param .KeyAuthorizationAnnotatedChallenge achall: Annotated + DVSNI challenge. :param list ip_addrs: addresses of challenged domain :class:`list` of type `~.obj.Addr` @@ -165,7 +165,7 @@ class ApacheDvsni(common.Dvsni): # https://docs.python.org/2.7/reference/lexical_analysis.html return self.VHOST_TEMPLATE.format( vhost=ips, - server_name=achall.gen_response(achall.account_key).z_domain, + server_name=achall.response(achall.account_key).z_domain, ssl_options_conf_path=self.configurator.mod_ssl_conf, cert_path=self.get_cert_path(achall), key_path=self.get_key_path(achall), diff --git a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py index 7c2137c45..1fce69969 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py @@ -382,8 +382,8 @@ class TwoVhost80Test(util.ApacheTest): account_key, achall1, achall2 = self.get_achalls() dvsni_ret_val = [ - achall1.gen_response(account_key), - achall2.gen_response(account_key), + achall1.response(account_key), + achall2.response(account_key), ] mock_dvsni_perform.return_value = dvsni_ret_val @@ -592,15 +592,15 @@ class TwoVhost80Test(util.ApacheTest): def get_achalls(self): """Return testing achallenges.""" account_key = self.rsa512jwk - achall1 = achallenges.DVSNI( + achall1 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb( - challenges.DVSNI( + challenges.TLSSNI01( token="jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q"), "pending"), domain="encryption-example.demo", account_key=account_key) - achall2 = achallenges.DVSNI( + achall2 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb( - challenges.DVSNI( + challenges.TLSSNI01( token="uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU"), "pending"), domain="letsencrypt.demo", account_key=account_key) diff --git a/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py b/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py index 6cb93e0ca..911c2a36b 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py @@ -13,8 +13,8 @@ from letsencrypt_apache.tests import util class DvsniPerformTest(util.ApacheTest): """Test the ApacheDVSNI challenge.""" - auth_key = common_test.DvsniTest.auth_key - achalls = common_test.DvsniTest.achalls + auth_key = common_test.TLSSNI01Test.auth_key + achalls = common_test.TLSSNI01Test.achalls def setUp(self): # pylint: disable=arguments-differ super(DvsniPerformTest, self).setUp() @@ -46,7 +46,7 @@ class DvsniPerformTest(util.ApacheTest): achall = self.achalls[0] self.sni.add_chall(achall) - response = self.achalls[0].gen_response(self.auth_key) + response = self.achalls[0].response(self.auth_key) mock_setup_cert = mock.MagicMock(return_value=response) # pylint: disable=protected-access self.sni._setup_challenge_cert = mock_setup_cert @@ -72,7 +72,7 @@ class DvsniPerformTest(util.ApacheTest): acme_responses = [] for achall in self.achalls: self.sni.add_chall(achall) - acme_responses.append(achall.gen_response(self.auth_key)) + acme_responses.append(achall.response(self.auth_key)) mock_setup_cert = mock.MagicMock(side_effect=acme_responses) # pylint: disable=protected-access @@ -100,7 +100,7 @@ class DvsniPerformTest(util.ApacheTest): z_domains = [] for achall in self.achalls: self.sni.add_chall(achall) - z_domain = achall.gen_response(self.auth_key).z_domain + z_domain = achall.response(self.auth_key).z_domain z_domains.append(set([z_domain])) self.sni._mod_config() # pylint: disable=protected-access diff --git a/letsencrypt-compatibility-test/letsencrypt_compatibility_test/test_driver.py b/letsencrypt-compatibility-test/letsencrypt_compatibility_test/test_driver.py index b91322c3c..5765003b9 100644 --- a/letsencrypt-compatibility-test/letsencrypt_compatibility_test/test_driver.py +++ b/letsencrypt-compatibility-test/letsencrypt_compatibility_test/test_driver.py @@ -60,7 +60,7 @@ def test_authenticator(plugin, config, temp_dir): "Plugin failed to complete %s for %s in %s", type(achalls[i]), achalls[i].domain, config) success = False - elif isinstance(responses[i], challenges.DVSNIResponse): + elif isinstance(responses[i], challenges.TLSSNI01): verify = functools.partial(responses[i].simple_verify, achalls[i], achalls[i].domain, util.JWK.public_key(), @@ -68,10 +68,10 @@ def test_authenticator(plugin, config, temp_dir): port=plugin.https_port) if _try_until_true(verify): logger.info( - "DVSNI verification for %s succeeded", achalls[i].domain) + "tls-sni-01 verification for %s succeeded", achalls[i].domain) else: logger.error( - "DVSNI verification for %s in %s failed", + "tls-sni-01 verification for %s in %s failed", achalls[i].domain, config) success = False @@ -99,12 +99,12 @@ def _create_achalls(plugin): for domain in names: prefs = plugin.get_chall_pref(domain) for chall_type in prefs: - if chall_type == challenges.DVSNI: - chall = challenges.DVSNI( - token=os.urandom(challenges.DVSNI.TOKEN_SIZE)) + if chall_type == challenges.TLSSNI01: + chall = challenges.TLSSNI01( + token=os.urandom(challenges.TLSSNI01.TOKEN_SIZE)) challb = acme_util.chall_to_challb( chall, messages.STATUS_PENDING) - achall = achallenges.DVSNI( + achall = achallenges.KeyAuthorizationAnnotatedChallenge( challb=challb, domain=domain, account_key=util.JWK) achalls.append(achall) diff --git a/letsencrypt-nginx/letsencrypt_nginx/configurator.py b/letsencrypt-nginx/letsencrypt_nginx/configurator.py index 80b4ebc5d..0123ac321 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/configurator.py +++ b/letsencrypt-nginx/letsencrypt_nginx/configurator.py @@ -14,7 +14,6 @@ import zope.interface from acme import challenges from acme import crypto_util as acme_crypto_util -from letsencrypt import achallenges from letsencrypt import constants as core_constants from letsencrypt import crypto_util from letsencrypt import errors @@ -537,7 +536,7 @@ class NginxConfigurator(common.Plugin): ########################################################################### def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use """Return list of challenge preferences.""" - return [challenges.DVSNI] + return [challenges.TLSSNI01] # Entry point in main.py for performing challenges def perform(self, achalls): @@ -553,11 +552,10 @@ class NginxConfigurator(common.Plugin): nginx_dvsni = dvsni.NginxDvsni(self) for i, achall in enumerate(achalls): - if isinstance(achall, achallenges.DVSNI): - # Currently also have dvsni hold associated index - # of the challenge. This helps to put all of the responses back - # together when they are all complete. - nginx_dvsni.add_chall(achall, i) + # Currently also have dvsni hold associated index + # of the challenge. This helps to put all of the responses back + # together when they are all complete. + nginx_dvsni.add_chall(achall, i) sni_response = nginx_dvsni.perform() # Must restart in order to activate the challenges. diff --git a/letsencrypt-nginx/letsencrypt_nginx/dvsni.py b/letsencrypt-nginx/letsencrypt_nginx/dvsni.py index 828dac650..b388c0267 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/dvsni.py +++ b/letsencrypt-nginx/letsencrypt_nginx/dvsni.py @@ -13,7 +13,7 @@ from letsencrypt_nginx import nginxparser logger = logging.getLogger(__name__) -class NginxDvsni(common.Dvsni): +class NginxDvsni(common.TLSSNI01): """Class performs DVSNI challenges within the Nginx configurator. :ivar configurator: NginxConfigurator object @@ -141,7 +141,7 @@ class NginxDvsni(common.Dvsni): block = [['listen', str(addr)] for addr in addrs] block.extend([['server_name', - achall.gen_response(achall.account_key).z_domain], + achall.response(achall.account_key).z_domain], ['include', self.configurator.parser.loc["ssl_options"]], # access and error logs necessary for # integration testing (non-root) diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py index d8bdf8355..7000f85dc 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py @@ -51,7 +51,7 @@ class NginxConfiguratorTest(util.NginxTest): errors.PluginError, self.config.enhance, 'myhost', 'redirect') def test_get_chall_pref(self): - self.assertEqual([challenges.DVSNI], + self.assertEqual([challenges.TLSSNI01], self.config.get_chall_pref('myhost')) def test_save(self): @@ -210,22 +210,22 @@ class NginxConfiguratorTest(util.NginxTest): def test_perform(self, mock_restart, mock_dvsni_perform): # Only tests functionality specific to configurator.perform # Note: As more challenges are offered this will have to be expanded - achall1 = achallenges.DVSNI( + achall1 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=messages.ChallengeBody( - chall=challenges.DVSNI(token="kNdwjwOeX0I_A8DXt9Msmg"), + chall=challenges.TLSSNI01(token="kNdwjwOeX0I_A8DXt9Msmg"), uri="https://ca.org/chall0_uri", status=messages.Status("pending"), ), domain="localhost", account_key=self.rsa512jwk) - achall2 = achallenges.DVSNI( + achall2 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=messages.ChallengeBody( - chall=challenges.DVSNI(token="m8TdO1qik4JVFtgPPurJmg"), + chall=challenges.TLSSNI01(token="m8TdO1qik4JVFtgPPurJmg"), uri="https://ca.org/chall1_uri", status=messages.Status("pending"), ), domain="example.com", account_key=self.rsa512jwk) dvsni_ret_val = [ - achall1.gen_response(self.rsa512jwk), - achall2.gen_response(self.rsa512jwk), + achall1.response(self.rsa512jwk), + achall2.response(self.rsa512jwk), ] mock_dvsni_perform.return_value = dvsni_ret_val diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/dvsni_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/dvsni_test.py index 9fc0a1ad7..d32e3d98f 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/dvsni_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/dvsni_test.py @@ -19,22 +19,22 @@ from letsencrypt_nginx.tests import util class DvsniPerformTest(util.NginxTest): """Test the NginxDVSNI challenge.""" - account_key = common_test.DvsniTest.auth_key + account_key = common_test.TLSSNI01Test.auth_key achalls = [ - achallenges.DVSNI( + achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb( - challenges.DVSNI(token="kNdwjwOeX0I_A8DXt9Msmg"), "pending"), + challenges.TLSSNI01(token="kNdwjwOeX0I_A8DXt9Msmg"), "pending"), domain="www.example.com", account_key=account_key), - achallenges.DVSNI( + achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb( - challenges.DVSNI( + challenges.TLSSNI01( token="\xba\xa9\xda? Date: Sat, 7 Nov 2015 18:17:24 +0000 Subject: [PATCH 034/124] Kill dvsni in core --- letsencrypt/achallenges.py | 31 --------------------- letsencrypt/auth_handler.py | 7 ++--- letsencrypt/errors.py | 4 +-- letsencrypt/plugins/manual.py | 2 +- letsencrypt/tests/achallenges_test.py | 34 ----------------------- letsencrypt/tests/acme_util.py | 3 -- letsencrypt/tests/continuity_auth_test.py | 4 +-- 7 files changed, 7 insertions(+), 78 deletions(-) delete mode 100644 letsencrypt/tests/achallenges_test.py diff --git a/letsencrypt/achallenges.py b/letsencrypt/achallenges.py index 4e46d6af9..4d85f5d6a 100644 --- a/letsencrypt/achallenges.py +++ b/letsencrypt/achallenges.py @@ -55,37 +55,6 @@ class KeyAuthorizationAnnotatedChallenge(AnnotatedChallenge): self.account_key, *args, **kwargs) -class DVSNI(AnnotatedChallenge): - """Client annotated "dvsni" ACME challenge. - - :ivar .JWK account_key: Authorized Account Key - - """ - __slots__ = ('challb', 'domain', 'account_key') - acme_type = challenges.DVSNI - - def gen_cert_and_response(self, key=None, bits=2048, alg=jose.RS256): - """Generate a DVSNI cert and response. - - :param OpenSSL.crypto.PKey key: Private key used for - certificate generation. If none provided, a fresh key will - be generated. - :param int bits: Number of bits for fresh key generation. - :param .JWAAlgorithm alg: - - :returns: ``(response, cert_pem, key_pem)`` tuple, where - ``response`` is an instance of - `acme.challenges.DVSNIResponse`, ``cert`` is a certificate - (`OpenSSL.crypto.X509`) and ``key`` is a private key - (`OpenSSL.crypto.PKey`). - :rtype: tuple - - """ - response = self.challb.chall.gen_response(self.account_key, alg=alg) - cert, key = response.gen_cert(key=key, bits=bits) - return response, cert, key - - class DNS(AnnotatedChallenge): """Client annotated "dns" ACME challenge.""" __slots__ = ('challb', 'domain') diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 11019daac..027c11158 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -344,8 +344,8 @@ def challb_to_achall(challb, account_key, domain): chall = challb.chall logger.info("%s challenge for %s", chall.typ, domain) - if isinstance(chall, challenges.DVSNI): - return achallenges.DVSNI( + if isinstance(chall, challenges.KeyAuthorizationChallenge): + return achallenges.KeyAuthorizationAnnotatedChallenge( challb=challb, domain=domain, account_key=account_key) elif isinstance(chall, challenges.DNS): return achallenges.DNS(challb=challb, domain=domain) @@ -355,9 +355,6 @@ def challb_to_achall(challb, account_key, domain): elif isinstance(chall, challenges.ProofOfPossession): return achallenges.ProofOfPossession( challb=challb, domain=domain) - elif isinstance(chall, challenges.KeyAuthorizationChallenge): - return achallenges.KeyAuthorizationAnnotatedChallenge( - challb=challb, domain=domain, account_key=account_key) else: raise errors.Error( "Received unsupported challenge of type: %s", chall.typ) diff --git a/letsencrypt/errors.py b/letsencrypt/errors.py index 3bfed4d23..a785d5e5c 100644 --- a/letsencrypt/errors.py +++ b/letsencrypt/errors.py @@ -57,8 +57,8 @@ class DvAuthError(AuthorizationError): # Authenticator - Challenge specific errors -class DvsniError(DvAuthError): - """Let's Encrypt DVSNI error.""" +class TLSSNI01Error(DvAuthError): + """Let's Encrypt TLSSNI01 error.""" # Plugin Errors diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index a2a2f7f34..07f06ccec 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -31,7 +31,7 @@ class Authenticator(common.Plugin): run as a privileged process. Alternatively shows instructions on how to use Python's built-in HTTP server. - .. todo:: Support for `~.challenges.DVSNI`. + .. todo:: Support for `~.challenges.TLSSNI01`. """ zope.interface.implements(interfaces.IAuthenticator) diff --git a/letsencrypt/tests/achallenges_test.py b/letsencrypt/tests/achallenges_test.py deleted file mode 100644 index 66b1a7ca7..000000000 --- a/letsencrypt/tests/achallenges_test.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Tests for letsencrypt.achallenges.""" -import unittest - -import OpenSSL - -from acme import challenges -from acme import jose - -from letsencrypt.tests import acme_util -from letsencrypt.tests import test_util - - -class DVSNITest(unittest.TestCase): - """Tests for letsencrypt.achallenges.DVSNI.""" - - def setUp(self): - self.challb = acme_util.chall_to_challb(acme_util.DVSNI, "pending") - key = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) - from letsencrypt.achallenges import DVSNI - self.achall = DVSNI( - challb=self.challb, domain="example.com", account_key=key) - - def test_proxy(self): - self.assertEqual(self.challb.token, self.achall.token) - - def test_gen_cert_and_response(self): - response, cert, key = self.achall.gen_cert_and_response() - self.assertTrue(isinstance(response, challenges.DVSNIResponse)) - self.assertTrue(isinstance(cert, OpenSSL.crypto.X509)) - self.assertTrue(isinstance(key, OpenSSL.crypto.PKey)) - - -if __name__ == "__main__": - unittest.main() # pragma: no cover diff --git a/letsencrypt/tests/acme_util.py b/letsencrypt/tests/acme_util.py index de75a2bc9..6b07b840f 100644 --- a/letsencrypt/tests/acme_util.py +++ b/letsencrypt/tests/acme_util.py @@ -16,8 +16,6 @@ HTTP01 = challenges.HTTP01( token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") TLSSNI01 = challenges.TLSSNI01( token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA")) -DVSNI = challenges.DVSNI( - token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA")) DNS = challenges.DNS(token="17817c66b60ce2e4012dfad92657527a") RECOVERY_CONTACT = challenges.RecoveryContact( activation_url="https://example.ca/sendrecovery/a5bd99383fb0", @@ -81,7 +79,6 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name # Pending ChallengeBody objects -DVSNI_P = chall_to_challb(DVSNI, messages.STATUS_PENDING) TLSSNI01_P = chall_to_challb(TLSSNI01, messages.STATUS_PENDING) HTTP01_P = chall_to_challb(HTTP01, messages.STATUS_PENDING) DNS_P = chall_to_challb(DNS, messages.STATUS_PENDING) diff --git a/letsencrypt/tests/continuity_auth_test.py b/letsencrypt/tests/continuity_auth_test.py index d80a1cfb4..70287bd01 100644 --- a/letsencrypt/tests/continuity_auth_test.py +++ b/letsencrypt/tests/continuity_auth_test.py @@ -34,7 +34,7 @@ class PerformTest(unittest.TestCase): def test_unexpected(self): self.assertRaises( errors.ContAuthError, self.auth.perform, [ - achallenges.DVSNI( + achallenges.KeyAuthorizationAnnotatedChallenge( challb=None, domain="0", account_key="invalid_key")]) def test_chall_pref(self): @@ -53,7 +53,7 @@ class CleanupTest(unittest.TestCase): mock.MagicMock(server="demo_server.org"), None) def test_unexpected(self): - unexpected = achallenges.DVSNI( + unexpected = achallenges.KeyAuthorizationAnnotatedChallenge( challb=None, domain="0", account_key="dummy_key") self.assertRaises(errors.ContAuthError, self.auth.cleanup, [unexpected]) From bbb7606fe1914e186443b85bf192f55200400285 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Nov 2015 18:17:35 +0000 Subject: [PATCH 035/124] Kill dvsni in acme --- acme/acme/challenges.py | 162 ----------------------------------- acme/acme/challenges_test.py | 155 --------------------------------- docs/contributing.rst | 4 +- 3 files changed, 2 insertions(+), 319 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 5f97547ee..522e701e7 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -440,168 +440,6 @@ class TLSSNI01(KeyAuthorizationChallenge): return self.response(account_key).gen_cert(key=kwargs.get('cert_key')) -@Challenge.register # pylint: disable=too-many-ancestors -class DVSNI(_TokenDVChallenge): - """ACME "dvsni" challenge. - - :ivar bytes token: Random data, **not** base64-encoded. - - """ - typ = "dvsni" - - PORT = 443 - """Port to perform DVSNI challenge.""" - - def gen_response(self, account_key, alg=jose.RS256, **kwargs): - """Generate response. - - :param .JWK account_key: Private account key. - :rtype: .DVSNIResponse - - """ - return DVSNIResponse(validation=jose.JWS.sign( - payload=self.json_dumps(sort_keys=True).encode('utf-8'), - key=account_key, alg=alg, **kwargs)) - - -@ChallengeResponse.register -class DVSNIResponse(ChallengeResponse): - """ACME "dvsni" challenge response. - - :param bytes s: Random data, **not** base64-encoded. - - """ - typ = "dvsni" - - DOMAIN_SUFFIX = b".acme.invalid" - """Domain name suffix.""" - - PORT = DVSNI.PORT - """Port to perform DVSNI challenge.""" - - validation = jose.Field("validation", decoder=jose.JWS.from_json) - - @property - def z(self): # pylint: disable=invalid-name - """The ``z`` parameter. - - :rtype: bytes - - """ - # Instance of 'Field' has no 'signature' member - # pylint: disable=no-member - return hashlib.sha256(self.validation.signature.encode( - "signature").encode("utf-8")).hexdigest().encode() - - @property - def z_domain(self): - """Domain name for certificate subjectAltName. - - :rtype: bytes - - """ - z = self.z # pylint: disable=invalid-name - return z[:32] + b'.' + z[32:] + self.DOMAIN_SUFFIX - - @property - def chall(self): - """Get challenge encoded in the `validation` payload. - - :rtype: challenges.DVSNI - - """ - # pylint: disable=no-member - return DVSNI.json_loads(self.validation.payload.decode('utf-8')) - - def gen_cert(self, key=None, bits=2048): - """Generate DVSNI certificate. - - :param OpenSSL.crypto.PKey key: Optional private key used in - certificate generation. If not provided (``None``), then - fresh key will be generated. - :param int bits: Number of bits for newly generated key. - - :rtype: `tuple` of `OpenSSL.crypto.X509` and - `OpenSSL.crypto.PKey` - - """ - if key is None: - key = OpenSSL.crypto.PKey() - key.generate_key(OpenSSL.crypto.TYPE_RSA, bits) - return crypto_util.gen_ss_cert(key, [ - # z_domain is too big to fit into CN, hence first dummy domain - 'dummy', self.z_domain.decode()], force_san=True), key - - def probe_cert(self, domain, **kwargs): - """Probe DVSNI challenge certificate. - - :param unicode domain: - - """ - if "host" not in kwargs: - host = socket.gethostbyname(domain) - logging.debug('%s resolved to %s', domain, host) - kwargs["host"] = host - - kwargs.setdefault("port", self.PORT) - kwargs["name"] = self.z_domain - # TODO: try different methods? - # pylint: disable=protected-access - return crypto_util.probe_sni(**kwargs) - - def verify_cert(self, cert): - """Verify DVSNI challenge certificate.""" - # pylint: disable=protected-access - sans = crypto_util._pyopenssl_cert_or_req_san(cert) - logging.debug('Certificate %s. SANs: %s', cert.digest('sha1'), sans) - return self.z_domain.decode() in sans - - def simple_verify(self, chall, domain, account_public_key, - cert=None, **kwargs): - """Simple verify. - - Verify ``validation`` using ``account_public_key``, optionally - probe DVSNI certificate and check using `verify_cert`. - - :param .challenges.DVSNI chall: Corresponding challenge. - :param str domain: Domain name being validated. - :param JWK account_public_key: - :param OpenSSL.crypto.X509 cert: Optional certificate. If not - provided (``None``) certificate will be retrieved using - `probe_cert`. - - - :returns: ``True`` iff client's control of the domain has been - verified, ``False`` otherwise. - :rtype: bool - - """ - # pylint: disable=no-member - if not self.validation.verify(key=account_public_key): - return False - - # TODO: it's not checked that payload has exectly 2 fields! - try: - decoded_chall = self.chall - except jose.DeserializationError as error: - logger.debug(error, exc_info=True) - return False - - if decoded_chall.token != chall.token: - logger.debug("Wrong token: expected %r, found %r", - chall.token, decoded_chall.token) - return False - - if cert is None: - try: - cert = self.probe_cert(domain=domain, **kwargs) - except errors.Error as error: - logger.debug(error, exc_info=True) - return False - - return self.verify_cert(cert) - - @Challenge.register class RecoveryContact(ContinuityChallenge): """ACME "recoveryContact" challenge. diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 3fcb01e4d..c4f3d6c61 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -320,161 +320,6 @@ class TLSSNI01Test(unittest.TestCase): mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key) -class DVSNITest(unittest.TestCase): - - def setUp(self): - from acme.challenges import DVSNI - self.msg = DVSNI( - token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e')) - self.jmsg = { - 'type': 'dvsni', - 'token': 'a82d5ff8ef740d12881f6d3c2277ab2e', - } - - def test_to_partial_json(self): - self.assertEqual(self.jmsg, self.msg.to_partial_json()) - - def test_from_json(self): - from acme.challenges import DVSNI - self.assertEqual(self.msg, DVSNI.from_json(self.jmsg)) - - def test_from_json_hashable(self): - from acme.challenges import DVSNI - hash(DVSNI.from_json(self.jmsg)) - - def test_from_json_invalid_token_length(self): - from acme.challenges import DVSNI - self.jmsg['token'] = jose.encode_b64jose(b'abcd') - self.assertRaises( - jose.DeserializationError, DVSNI.from_json, self.jmsg) - - def test_gen_response(self): - from acme.challenges import DVSNI - self.assertEqual(self.msg, DVSNI.json_loads( - self.msg.gen_response(KEY).validation.payload.decode())) - - -class DVSNIResponseTest(unittest.TestCase): - # pylint: disable=too-many-instance-attributes - - def setUp(self): - from acme.challenges import DVSNI - self.chall = DVSNI( - token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e')) - - from acme.challenges import DVSNIResponse - self.validation = jose.JWS.sign( - payload=self.chall.json_dumps(sort_keys=True).encode(), - key=KEY, alg=jose.RS256) - self.msg = DVSNIResponse(validation=self.validation) - self.jmsg_to = { - 'resource': 'challenge', - 'type': 'dvsni', - 'validation': self.validation, - } - self.jmsg_from = { - 'resource': 'challenge', - 'type': 'dvsni', - 'validation': self.validation.to_json(), - } - - # pylint: disable=invalid-name - label1 = b'e2df3498860637c667fedadc5a8494ec' - label2 = b'09dcc75553c9b3bd73662b50e71b1e42' - self.z = label1 + label2 - self.z_domain = label1 + b'.' + label2 + b'.acme.invalid' - self.domain = 'foo.com' - - def test_z_and_domain(self): - self.assertEqual(self.z, self.msg.z) - self.assertEqual(self.z_domain, self.msg.z_domain) - - def test_to_partial_json(self): - self.assertEqual(self.jmsg_to, self.msg.to_partial_json()) - - def test_from_json(self): - from acme.challenges import DVSNIResponse - self.assertEqual(self.msg, DVSNIResponse.from_json(self.jmsg_from)) - - def test_from_json_hashable(self): - from acme.challenges import DVSNIResponse - hash(DVSNIResponse.from_json(self.jmsg_from)) - - @mock.patch('acme.challenges.socket.gethostbyname') - @mock.patch('acme.challenges.crypto_util.probe_sni') - def test_probe_cert(self, mock_probe_sni, mock_gethostbyname): - mock_gethostbyname.return_value = '127.0.0.1' - self.msg.probe_cert('foo.com') - mock_gethostbyname.assert_called_once_with('foo.com') - mock_probe_sni.assert_called_once_with( - host='127.0.0.1', port=self.msg.PORT, - name=self.z_domain) - - self.msg.probe_cert('foo.com', host='8.8.8.8') - mock_probe_sni.assert_called_with( - host='8.8.8.8', port=mock.ANY, name=mock.ANY) - - self.msg.probe_cert('foo.com', port=1234) - mock_probe_sni.assert_called_with( - host=mock.ANY, port=1234, name=mock.ANY) - - self.msg.probe_cert('foo.com', bar='baz') - mock_probe_sni.assert_called_with( - host=mock.ANY, port=mock.ANY, name=mock.ANY, bar='baz') - - self.msg.probe_cert('foo.com', name=b'xxx') - mock_probe_sni.assert_called_with( - host=mock.ANY, port=mock.ANY, - name=self.z_domain) - - def test_gen_verify_cert(self): - key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem') - cert, key2 = self.msg.gen_cert(key1) - self.assertEqual(key1, key2) - self.assertTrue(self.msg.verify_cert(cert)) - - def test_gen_verify_cert_gen_key(self): - cert, key = self.msg.gen_cert() - self.assertTrue(isinstance(key, OpenSSL.crypto.PKey)) - self.assertTrue(self.msg.verify_cert(cert)) - - def test_verify_bad_cert(self): - self.assertFalse(self.msg.verify_cert(test_util.load_cert('cert.pem'))) - - def test_simple_verify_wrong_account_key(self): - self.assertFalse(self.msg.simple_verify( - self.chall, self.domain, jose.JWKRSA.load( - test_util.load_vector('rsa256_key.pem')).public_key())) - - def test_simple_verify_wrong_payload(self): - for payload in b'', b'{}': - msg = self.msg.update(validation=jose.JWS.sign( - payload=payload, key=KEY, alg=jose.RS256)) - self.assertFalse(msg.simple_verify( - self.chall, self.domain, KEY.public_key())) - - def test_simple_verify_wrong_token(self): - msg = self.msg.update(validation=jose.JWS.sign( - payload=self.chall.update(token=(b'b' * 20)).json_dumps().encode(), - key=KEY, alg=jose.RS256)) - self.assertFalse(msg.simple_verify( - self.chall, self.domain, KEY.public_key())) - - @mock.patch('acme.challenges.DVSNIResponse.verify_cert', autospec=True) - def test_simple_verify(self, mock_verify_cert): - mock_verify_cert.return_value = mock.sentinel.verification - self.assertEqual(mock.sentinel.verification, self.msg.simple_verify( - self.chall, self.domain, KEY.public_key(), - cert=mock.sentinel.cert)) - mock_verify_cert.assert_called_once_with(self.msg, mock.sentinel.cert) - - @mock.patch('acme.challenges.DVSNIResponse.probe_cert') - def test_simple_verify_false_on_probe_error(self, mock_probe_cert): - mock_probe_cert.side_effect = errors.Error - self.assertFalse(self.msg.simple_verify( - self.chall, self.domain, KEY.public_key())) - - class RecoveryContactTest(unittest.TestCase): def setUp(self): diff --git a/docs/contributing.rst b/docs/contributing.rst index efc6c27ae..c71aefeec 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -151,7 +151,7 @@ certificate for some domain name by solving challenges received from the ACME server. From the protocol, there are essentially two different types of challenges. Challenges that must be solved by individual plugins in order to satisfy domain validation (subclasses -of `~.DVChallenge`, i.e. `~.challenges.DVSNI`, +of `~.DVChallenge`, i.e. `~.challenges.TLSSNI01`, `~.challenges.HTTP01`, `~.challenges.DNS`) and continuity specific challenges (subclasses of `~.ContinuityChallenge`, i.e. `~.challenges.RecoveryToken`, `~.challenges.RecoveryContact`, @@ -160,7 +160,7 @@ always handled by the `~.ContinuityAuthenticator`, while plugins are expected to handle `~.DVChallenge` types. Right now, we have two authenticator plugins, the `~.ApacheConfigurator` and the `~.StandaloneAuthenticator`. The Standalone and Apache -authenticators only solve the `~.challenges.DVSNI` challenge currently. +authenticators only solve the `~.challenges.TLSSNI01` challenge currently. (You can set which challenges your authenticator can handle through the :meth:`~.IAuthenticator.get_chall_pref`. From d0a2b38457abc015c8204cf27002e910607e743c Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Nov 2015 19:36:08 +0000 Subject: [PATCH 036/124] pep8 for docs/conf.py --- acme/docs/conf.py | 26 +++++++-------- docs/conf.py | 26 +++++++-------- letsencrypt-apache/docs/conf.py | 26 +++++++-------- letsencrypt-compatibility-test/docs/conf.py | 37 ++++++++++++--------- letsencrypt-nginx/docs/conf.py | 26 +++++++-------- letshelp-letsencrypt/docs/conf.py | 26 +++++++-------- 6 files changed, 86 insertions(+), 81 deletions(-) diff --git a/acme/docs/conf.py b/acme/docs/conf.py index 1448aaea3..55f5eee3f 100644 --- a/acme/docs/conf.py +++ b/acme/docs/conf.py @@ -227,25 +227,25 @@ htmlhelp_basename = 'acme-pythondoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + #'preamble': '', -# Latex figure (float) alignment -#'figure_align': 'htbp', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'acme-python.tex', u'acme-python Documentation', - u'Let\'s Encrypt Project', 'manual'), + (master_doc, 'acme-python.tex', u'acme-python Documentation', + u'Let\'s Encrypt Project', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -289,9 +289,9 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'acme-python', u'acme-python Documentation', - author, 'acme-python', 'One line description of project.', - 'Miscellaneous'), + (master_doc, 'acme-python', u'acme-python Documentation', + author, 'acme-python', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. diff --git a/docs/conf.py b/docs/conf.py index 62a7cea07..21bcc6817 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -230,25 +230,25 @@ htmlhelp_basename = 'LetsEncryptdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + #'preamble': '', -# Latex figure (float) alignment -#'figure_align': 'htbp', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'LetsEncrypt.tex', u'Let\'s Encrypt Documentation', - u'Let\'s Encrypt Project', 'manual'), + ('index', 'LetsEncrypt.tex', u'Let\'s Encrypt Documentation', + u'Let\'s Encrypt Project', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -295,9 +295,9 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'LetsEncrypt', u'Let\'s Encrypt Documentation', - u'Let\'s Encrypt Project', 'LetsEncrypt', 'One line description of project.', - 'Miscellaneous'), + ('index', 'LetsEncrypt', u'Let\'s Encrypt Documentation', + u'Let\'s Encrypt Project', 'LetsEncrypt', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. diff --git a/letsencrypt-apache/docs/conf.py b/letsencrypt-apache/docs/conf.py index ddbf09262..aa58038cd 100644 --- a/letsencrypt-apache/docs/conf.py +++ b/letsencrypt-apache/docs/conf.py @@ -232,25 +232,25 @@ htmlhelp_basename = 'letsencrypt-apachedoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + #'preamble': '', -# Latex figure (float) alignment -#'figure_align': 'htbp', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'letsencrypt-apache.tex', u'letsencrypt-apache Documentation', - u'Let\'s Encrypt Project', 'manual'), + (master_doc, 'letsencrypt-apache.tex', u'letsencrypt-apache Documentation', + u'Let\'s Encrypt Project', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -293,9 +293,9 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'letsencrypt-apache', u'letsencrypt-apache Documentation', - author, 'letsencrypt-apache', 'One line description of project.', - 'Miscellaneous'), + (master_doc, 'letsencrypt-apache', u'letsencrypt-apache Documentation', + author, 'letsencrypt-apache', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. diff --git a/letsencrypt-compatibility-test/docs/conf.py b/letsencrypt-compatibility-test/docs/conf.py index 7e9f0d5a4..3ee161efb 100644 --- a/letsencrypt-compatibility-test/docs/conf.py +++ b/letsencrypt-compatibility-test/docs/conf.py @@ -226,25 +226,26 @@ htmlhelp_basename = 'letsencrypt-compatibility-testdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + #'preamble': '', -# Latex figure (float) alignment -#'figure_align': 'htbp', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'letsencrypt-compatibility-test.tex', u'letsencrypt-compatibility-test Documentation', - u'Let\'s Encrypt Project', 'manual'), + (master_doc, 'letsencrypt-compatibility-test.tex', + u'letsencrypt-compatibility-test Documentation', + u'Let\'s Encrypt Project', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -273,7 +274,8 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'letsencrypt-compatibility-test', u'letsencrypt-compatibility-test Documentation', + (master_doc, 'letsencrypt-compatibility-test', + u'letsencrypt-compatibility-test Documentation', [author], 1) ] @@ -287,9 +289,10 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'letsencrypt-compatibility-test', u'letsencrypt-compatibility-test Documentation', - author, 'letsencrypt-compatibility-test', 'One line description of project.', - 'Miscellaneous'), + (master_doc, 'letsencrypt-compatibility-test', + u'letsencrypt-compatibility-test Documentation', + author, 'letsencrypt-compatibility-test', + 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. @@ -309,6 +312,8 @@ intersphinx_mapping = { 'python': ('https://docs.python.org/', None), 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), 'letsencrypt': ('https://letsencrypt.readthedocs.org/en/latest/', None), - 'letsencrypt-apache': ('https://letsencrypt-apache.readthedocs.org/en/latest/', None), - 'letsencrypt-nginx': ('https://letsencrypt-nginx.readthedocs.org/en/latest/', None), + 'letsencrypt-apache': ( + 'https://letsencrypt-apache.readthedocs.org/en/latest/', None), + 'letsencrypt-nginx': ( + 'https://letsencrypt-nginx.readthedocs.org/en/latest/', None), } diff --git a/letsencrypt-nginx/docs/conf.py b/letsencrypt-nginx/docs/conf.py index cdb3490a0..14713a4b2 100644 --- a/letsencrypt-nginx/docs/conf.py +++ b/letsencrypt-nginx/docs/conf.py @@ -225,25 +225,25 @@ htmlhelp_basename = 'letsencrypt-nginxdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + #'preamble': '', -# Latex figure (float) alignment -#'figure_align': 'htbp', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'letsencrypt-nginx.tex', u'letsencrypt-nginx Documentation', - u'Let\'s Encrypt Project', 'manual'), + (master_doc, 'letsencrypt-nginx.tex', u'letsencrypt-nginx Documentation', + u'Let\'s Encrypt Project', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -286,9 +286,9 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'letsencrypt-nginx', u'letsencrypt-nginx Documentation', - author, 'letsencrypt-nginx', 'One line description of project.', - 'Miscellaneous'), + (master_doc, 'letsencrypt-nginx', u'letsencrypt-nginx Documentation', + author, 'letsencrypt-nginx', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. diff --git a/letshelp-letsencrypt/docs/conf.py b/letshelp-letsencrypt/docs/conf.py index 206b0b9e2..a84c4c982 100644 --- a/letshelp-letsencrypt/docs/conf.py +++ b/letshelp-letsencrypt/docs/conf.py @@ -225,25 +225,25 @@ htmlhelp_basename = 'letshelp-letsencryptdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + #'preamble': '', -# Latex figure (float) alignment -#'figure_align': 'htbp', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'letshelp-letsencrypt.tex', u'letshelp-letsencrypt Documentation', - u'Let\'s Encrypt Project', 'manual'), + (master_doc, 'letshelp-letsencrypt.tex', u'letshelp-letsencrypt Documentation', + u'Let\'s Encrypt Project', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -286,9 +286,9 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'letshelp-letsencrypt', u'letshelp-letsencrypt Documentation', - author, 'letshelp-letsencrypt', 'One line description of project.', - 'Miscellaneous'), + (master_doc, 'letshelp-letsencrypt', u'letshelp-letsencrypt Documentation', + author, 'letshelp-letsencrypt', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. From 5ee17f698ecf98c86d4789b8e811ea6f6554da56 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Nov 2015 19:37:00 +0000 Subject: [PATCH 037/124] Fix more pep8 --- letsencrypt/cli.py | 1 - letsencrypt/plugins/disco_test.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 5757783cd..3130471c6 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -879,7 +879,6 @@ def prepare_and_parse_args(plugins, args): # parser (--help should display plugin-specific options last) _plugins_parsing(helpful, plugins) - return helpful.parse_args() diff --git a/letsencrypt/plugins/disco_test.py b/letsencrypt/plugins/disco_test.py index 41d8cd5fe..0df4f88f1 100644 --- a/letsencrypt/plugins/disco_test.py +++ b/letsencrypt/plugins/disco_test.py @@ -51,8 +51,8 @@ class PluginEntryPointTest(unittest.TestCase): def test_description(self): self.assertEqual( - "Automatically use a temporary webserver", - self.plugin_ep.description) + "Automatically use a temporary webserver", + self.plugin_ep.description) def test_description_with_name(self): self.plugin_ep.plugin_cls = mock.MagicMock(description="Desc") From 1f6f6a745109f978db23e49750ecf52709470871 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Nov 2015 22:33:31 +0000 Subject: [PATCH 038/124] Logging in HTTP01RequestHandler: more and not to sys.stderr. --- acme/acme/standalone.py | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/acme/acme/standalone.py b/acme/acme/standalone.py index 1466671e3..3843f453a 100644 --- a/acme/acme/standalone.py +++ b/acme/acme/standalone.py @@ -6,7 +6,6 @@ import logging import os import sys -import six from six.moves import BaseHTTPServer # pylint: disable=import-error from six.moves import http_client # pylint: disable=import-error from six.moves import socketserver # pylint: disable=import-error @@ -55,7 +54,20 @@ class DVSNIServer(TLSServer, ACMEServerMixin): def __init__(self, server_address, certs): TLSServer.__init__( - self, server_address, socketserver.BaseRequestHandler, certs=certs) + self, server_address, DVSNIRequestHandler, certs=certs) + + +class DVSNIRequestHandler(socketserver.BaseRequestHandler): + """DVSNI request handler.""" + + def log_message(self, format, *args): # pylint: disable=redefined-builtin + """Log arbitrary message.""" + logger.debug("%s - - %s", self.client_address[0], format % args) + + def handle(self): + """Handle request.""" + self.log_message("Incoming request") + socketserver.BaseRequestHandler.handle(self) class HTTP01Server(BaseHTTPServer.HTTPServer, ACMEServerMixin): @@ -83,6 +95,15 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): self.simple_http_resources = kwargs.pop("simple_http_resources", set()) socketserver.BaseRequestHandler.__init__(self, *args, **kwargs) + def log_message(self, format, *args): # pylint: disable=redefined-builtin + """Log arbitrary message.""" + logger.debug("%s - - %s", self.client_address[0], format % args) + + def handle(self): + """Handle request.""" + self.log_message("Incoming request") + BaseHTTPServer.BaseHTTPRequestHandler.handle(self) + def do_GET(self): # pylint: disable=invalid-name,missing-docstring if self.path == "/": self.handle_index() @@ -109,17 +130,17 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): """Handle HTTP01 provisioned resources.""" for resource in self.simple_http_resources: if resource.chall.path == self.path: - logger.debug("Serving HTTP01 with token %r", - resource.chall.encode("token")) + self.log_message("Serving HTTP01 with token %r", + resource.chall.encode("token")) self.send_response(http_client.OK) self.send_header("Content-type", resource.chall.CONTENT_TYPE) self.end_headers() self.wfile.write(resource.validation.encode()) return else: # pylint: disable=useless-else-on-loop - logger.debug("No resources to serve") - logger.debug("%s does not correspond to any resource. ignoring", - self.path) + self.log_message("No resources to serve") + self.log_message("%s does not correspond to any resource. ignoring", + self.path) @classmethod def partial_init(cls, simple_http_resources): @@ -159,8 +180,7 @@ def simple_dvsni_server(cli_args, forever=True): OpenSSL.crypto.FILETYPE_PEM, cert_contents)) server = DVSNIServer(('', int(args.port)), certs=certs) - six.print_("Serving at https://localhost:{0}...".format( - server.socket.getsockname()[1])) + logger.info("Serving at https://%s:%s...", *server.socket.getsockname()[:2]) if forever: # pragma: no cover server.serve_forever() else: From 6d32c2e5a4d5dc22c28ef78a67a0df83bfdb3804 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Nov 2015 23:00:43 +0000 Subject: [PATCH 039/124] Rename DVSNIRequestHandler to BaseRequestHandlerWithLogging. --- acme/acme/standalone.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/acme/acme/standalone.py b/acme/acme/standalone.py index 3843f453a..c0e037904 100644 --- a/acme/acme/standalone.py +++ b/acme/acme/standalone.py @@ -54,11 +54,11 @@ class DVSNIServer(TLSServer, ACMEServerMixin): def __init__(self, server_address, certs): TLSServer.__init__( - self, server_address, DVSNIRequestHandler, certs=certs) + self, server_address, BaseRequestHandlerWithLogging, certs=certs) -class DVSNIRequestHandler(socketserver.BaseRequestHandler): - """DVSNI request handler.""" +class BaseRequestHandlerWithLogging(socketserver.BaseRequestHandler): + """BaseRequestHandler with logging.""" def log_message(self, format, *args): # pylint: disable=redefined-builtin """Log arbitrary message.""" From 01c733bed15c300c9fabf4bc3bdd05c7d03698d6 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sat, 7 Nov 2015 20:36:30 -0800 Subject: [PATCH 040/124] Fix ExperimentalBootstrap on Mac Fixes: #1408 --- letsencrypt-auto | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/letsencrypt-auto b/letsencrypt-auto index 2391a7c0b..ba95350e4 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -33,7 +33,11 @@ ExperimentalBootstrap() { if [ "$DEBUG" = 1 ] ; then if [ "$2" != "" ] ; then echo "Bootstrapping dependencies for $1..." - "$3" "$BOOTSTRAP/$2" + if [ "$3" != "" ] ; then + "$3" "$BOOTSTRAP/$2" + else + "$BOOTSTRAP/$2" + fi fi else echo "WARNING: $1 support is very experimental at present..." @@ -43,7 +47,6 @@ ExperimentalBootstrap() { fi } - DeterminePythonVersion() { if which python2 > /dev/null ; then export LE_PYTHON=${LE_PYTHON:-python2} From 8208470395d049a54f2c71c5c17e5f055de18f89 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 8 Nov 2015 06:26:22 +0000 Subject: [PATCH 041/124] More docs about ports --- acme/acme/challenges.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 522e701e7..0f25f771b 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -221,6 +221,12 @@ class HTTP01Response(KeyAuthorizationChallengeResponse): typ = "http-01" PORT = 80 + """Verification port as defined by the protocol. + + You can override it (e.g. for testing) by passing ``port`` to + `simple_verify`. + + """ def simple_verify(self, chall, domain, account_public_key, port=None): """Simple verify. @@ -328,6 +334,12 @@ class TLSSNI01Response(KeyAuthorizationChallengeResponse): """Domain name suffix.""" PORT = 443 + """Verification port as defined by the protocol. + + You can override it (e.g. for testing) by passing ``port`` to + `simple_verify`. + + """ @property def z(self): @@ -396,6 +408,7 @@ class TLSSNI01Response(KeyAuthorizationChallengeResponse): :param OpenSSL.crypto.X509 cert: Optional certificate. If not provided (``None``) certificate will be retrieved using `probe_cert`. + :param int port: Port used to probe the certificate. :returns: ``True`` iff client's control of the domain has been From 37574e60e199a7c99d88313594889dbc154b40bc Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 8 Nov 2015 06:29:48 +0000 Subject: [PATCH 042/124] hexdigest lower() --- acme/acme/challenges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 0f25f771b..1b4f6c2ed 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -345,7 +345,7 @@ class TLSSNI01Response(KeyAuthorizationChallengeResponse): def z(self): """``z`` value used for verification.""" return hashlib.sha256( - self.key_authorization.encode("utf-8")).hexdigest().encode() + self.key_authorization.encode("utf-8")).hexdigest().lower().encode() @property def z_domain(self): From c18f0b7073728d01fa012a2ab2e5705c53a14193 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 8 Nov 2015 06:34:28 +0000 Subject: [PATCH 043/124] Add rtype docs --- acme/acme/challenges.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 1b4f6c2ed..976d7ab12 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -343,13 +343,21 @@ class TLSSNI01Response(KeyAuthorizationChallengeResponse): @property def z(self): - """``z`` value used for verification.""" + """``z`` value used for verification. + + :rtype bytes: + + """ return hashlib.sha256( self.key_authorization.encode("utf-8")).hexdigest().lower().encode() @property def z_domain(self): - """Domain name used for verification, generated from `z`.""" + """Domain name used for verification, generated from `z`. + + :rtype bytes: + + """ return self.z[:32] + b'.' + self.z[32:] + self.DOMAIN_SUFFIX def gen_cert(self, key=None, bits=2048): From 9c12102d0bb4ca5d1e002e6e6e09813ff9bf4412 Mon Sep 17 00:00:00 2001 From: Dev & Sec Date: Sun, 8 Nov 2015 10:26:15 +0000 Subject: [PATCH 044/124] use `command -v` instead of `type`, and add comments for the `su_sudo` function --- letsencrypt-auto | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/letsencrypt-auto b/letsencrypt-auto index 8626ab329..ce58488c4 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -14,11 +14,24 @@ VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN=${VENV_PATH}/bin if test "`id -u`" -ne "0" ; then - if type sudo 1>/dev/null 2>&1; then + if command -v sudo 1>/dev/null 2>&1; then SUDO=sudo else + # `sudo` command does not exist, use `su` instead. + # Because the parameters in `su -c` has to be a string, + # we need properly escape it su_sudo() { args="" + # This `while` loop iterates over all parameters given to this function. + # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string + # will be wrap in a pair of `'`, then append to `$args` string + # For example, `echo "It's only 1\$\!"` will be escaped to: + # 'echo' 'It'"'"'s only 1$!' + # │ │└┼┘│ + # │ │ │ └── `'s only 1$!'` the literal string + # │ │ └── `\"'\"` is a single quote (as a string) + # │ └── `'It'`, to be concatenated with the strings followed it + # └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself while [ $# -ne 0 ]; do args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' " shift From 2ac7a2a9eaeca167fccd8f10aa80f5ee9857ccae Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sun, 8 Nov 2015 20:23:01 +0200 Subject: [PATCH 045/124] Check configuration sanity for domain flag --- letsencrypt/cli.py | 30 +++++++++++++++++++++++++++++- letsencrypt/errors.py | 3 +++ letsencrypt/tests/cli_test.py | 22 ++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index c1f3edb70..94ff59d91 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -7,6 +7,7 @@ import logging import logging.handlers import os import pkg_resources +import re import sys import time import traceback @@ -36,7 +37,12 @@ from letsencrypt import storage from letsencrypt.display import util as display_util from letsencrypt.display import ops as display_ops -from letsencrypt.errors import Error, PluginSelectionError, CertStorageError +from letsencrypt.errors import ( + CertStorageError, + ConfigurationError, + Error, + PluginSelectionError +) from letsencrypt.plugins import disco as plugins_disco @@ -1085,6 +1091,8 @@ def main(cli_args=sys.argv[1:]): # note: arg parser internally handles --help (and exits afterwards) plugins = plugins_disco.PluginsRegistry.find_all() args = prepare_and_parse_args(plugins, cli_args) + # Check command line parameters sanity, and error out in case of problem. + check_config_sanity(args) config = configuration.NamespaceConfig(args) zope.component.provideUtility(config) @@ -1139,6 +1147,26 @@ def main(cli_args=sys.argv[1:]): return args.func(args, config, plugins) +def check_config_sanity(args): + """Validate command line options and display error message if + requirements are not met. + + :param args: Command line options + :type args: :class:`argparse.Namespace` + + """ + # Domain checks + if args.domains is not None: + # Check if there's a wildcard domain + if any(True for d in args.domains if d.startswith("*")): + raise ConfigurationError("Error: Wildcard domains are not supported") + # Punycode + if any(True for d in args.domains if "xn--" in d): + raise ConfigurationError("Error: Punycode domains are not supported") + # Check for FQDN + fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? Date: Sun, 8 Nov 2015 20:42:24 +0200 Subject: [PATCH 046/124] Use python2.6 compatible test assertions --- letsencrypt/tests/cli_test.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index bb6309968..bd41b0ebe 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -177,25 +177,23 @@ class CLITest(unittest.TestCase): def test_check_config_sanity_domain(self): # Punycode - self.assertRaisesRegexp(errors.ConfigurationError, - "Error: Punycode domains are not supported", - self._call, - ['-d', 'this.is.xn--ls8h.tld']) + self.assertRaises(errors.ConfigurationError, + self._call, + ['-d', 'this.is.xn--ls8h.tld']) # FQDN - self.assertRaisesRegexp(errors.ConfigurationError, - "Error: Requested domain is not FQDN", - self._call, - ['-d', 'comma,gotwrong.tld']) + self.assertRaises(errors.ConfigurationError, + "Error: Requested domain is not FQDN", + self._call, + ['-d', 'comma,gotwrong.tld']) # FQDN 2 - self.assertRaisesRegexp(errors.ConfigurationError, - "Error: Requested domain is not FQDN", - self._call, - ['-d', 'illegal.character=.tld']) + self.assertRaises(errors.ConfigurationError, + "Error: Requested domain is not FQDN", + self._call, + ['-d', 'illegal.character=.tld']) # Wildcard - self.assertRaisesRegexp(errors.ConfigurationError, - "Error: Wildcard domains are not supported", - self._call, - ['-d', '*.wildcard.tld']) + self.assertRaises(errors.ConfigurationError, + self._call, + ['-d', '*.wildcard.tld']) @mock.patch('letsencrypt.crypto_util.notAfter') @mock.patch('letsencrypt.cli.zope.component.getUtility') From b35ebafe16f13c20e942a48fdbf66cb71735287b Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sun, 8 Nov 2015 20:49:44 +0200 Subject: [PATCH 047/124] Fixed last half-assed commit to tests --- letsencrypt/tests/cli_test.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index bd41b0ebe..da5286a10 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -182,12 +182,10 @@ class CLITest(unittest.TestCase): ['-d', 'this.is.xn--ls8h.tld']) # FQDN self.assertRaises(errors.ConfigurationError, - "Error: Requested domain is not FQDN", self._call, ['-d', 'comma,gotwrong.tld']) # FQDN 2 self.assertRaises(errors.ConfigurationError, - "Error: Requested domain is not FQDN", self._call, ['-d', 'illegal.character=.tld']) # Wildcard From d6a3286a677ce0c432d4aa082de4853985490511 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sun, 8 Nov 2015 20:50:39 +0200 Subject: [PATCH 048/124] Fixed the wildcard domain string match --- letsencrypt/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 94ff59d91..f6231c98a 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -1158,7 +1158,7 @@ def check_config_sanity(args): # Domain checks if args.domains is not None: # Check if there's a wildcard domain - if any(True for d in args.domains if d.startswith("*")): + if any(True for d in args.domains if d.startswith("*.")): raise ConfigurationError("Error: Wildcard domains are not supported") # Punycode if any(True for d in args.domains if "xn--" in d): From ae080349c54af9fc3ebab40b75c8ea086ef2b672 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sun, 8 Nov 2015 21:04:48 +0200 Subject: [PATCH 049/124] Changed the errors import --- letsencrypt/cli.py | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index f6231c98a..e901246ce 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -29,6 +29,7 @@ from letsencrypt import configuration from letsencrypt import constants from letsencrypt import client from letsencrypt import crypto_util +from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util from letsencrypt import log @@ -37,12 +38,6 @@ from letsencrypt import storage from letsencrypt.display import util as display_util from letsencrypt.display import ops as display_ops -from letsencrypt.errors import ( - CertStorageError, - ConfigurationError, - Error, - PluginSelectionError -) from letsencrypt.plugins import disco as plugins_disco @@ -112,7 +107,7 @@ def _find_domains(args, installer): domains = args.domains if not domains: - raise Error("Please specify --domains, or --installer that " + raise errors.Error("Please specify --domains, or --installer that " "will help in domain names autodiscovery") return domains @@ -165,9 +160,9 @@ def _determine_account(args, config): try: acc, acme = client.register( config, account_storage, tos_cb=_tos_cb) - except Error as error: + except errors.Error as error: logger.debug(error, exc_info=True) - raise Error( + raise errors.Error( "Unable to register an account with ACME server") args.account = acc.id @@ -201,7 +196,7 @@ def _find_duplicative_certs(config, domains): try: full_path = os.path.join(configs_dir, renewal_file) candidate_lineage = storage.RenewableCert(full_path, cli_config) - except (CertStorageError, IOError): + except (errors.CertStorageError, IOError): logger.warning("Renewal configuration file %s is broken. " "Skipping.", full_path) continue @@ -273,7 +268,7 @@ def _treat_as_renewal(config, domains): br=os.linesep ), reporter_util.HIGH_PRIORITY) - raise Error( + raise errors.Error( "User did not use proper CLI and would like " "to reinvoke the client.") @@ -333,7 +328,7 @@ def _auth_from_domains(le_client, config, domains, plugins): # TREAT AS NEW REQUEST lineage = le_client.obtain_and_enroll_certificate(domains, plugins) if not lineage: - raise Error("Certificate could not be obtained") + raise errors.Error("Certificate could not be obtained") _report_new_cert(lineage.cert, lineage.fullchain) @@ -352,7 +347,7 @@ def set_configurator(previously, now): if previously: if previously != now: msg = "Too many flags setting configurators/installers/authenticators {0} -> {1}" - raise PluginSelectionError(msg.format(repr(previously), repr(now))) + raise errors.PluginSelectionError(msg.format(repr(previously), repr(now))) return now @@ -385,7 +380,7 @@ def diagnose_configurator_problem(cfg_type, requested, plugins): '"letsencrypt-auto certonly" to get a cert you can install manually') else: msg = "{0} could not be determined or is not installed".format(cfg_type) - raise PluginSelectionError(msg) + raise errors.PluginSelectionError(msg) def choose_configurator_plugins(args, config, plugins, verb): @@ -445,7 +440,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo """Obtain a certificate and install.""" try: installer, authenticator = choose_configurator_plugins(args, config, plugins, "run") - except PluginSelectionError, e: + except errors.PluginSelectionError, e: return e.message domains = _find_domains(args, installer) @@ -478,7 +473,7 @@ def obtaincert(args, config, plugins): try: # installers are used in auth mode to determine domain names installer, authenticator = choose_configurator_plugins(args, config, plugins, "certonly") - except PluginSelectionError, e: + except errors.PluginSelectionError, e: return e.message # TODO: Handle errors from _init_le_client? @@ -503,7 +498,7 @@ def install(args, config, plugins): try: installer, _ = choose_configurator_plugins(args, config, plugins, "install") - except PluginSelectionError, e: + except errors.PluginSelectionError, e: return e.message domains = _find_domains(args, installer) @@ -1066,7 +1061,7 @@ def _handle_exception(exc_type, exc_value, trace, args): sys.exit("".join( traceback.format_exception(exc_type, exc_value, trace))) - if issubclass(exc_type, Error): + if issubclass(exc_type, errors.Error): sys.exit(exc_value) else: # Tell the user a bit about what happened, without overwhelming @@ -1132,7 +1127,7 @@ def main(cli_args=sys.argv[1:]): disclaimer = pkg_resources.resource_string("letsencrypt", "DISCLAIMER") if not zope.component.getUtility(interfaces.IDisplay).yesno( disclaimer, "Agree", "Cancel"): - raise Error("Must agree to TOS") + raise errors.Error("Must agree to TOS") if not os.geteuid() == 0: logger.warning( @@ -1159,14 +1154,14 @@ def check_config_sanity(args): if args.domains is not None: # Check if there's a wildcard domain if any(True for d in args.domains if d.startswith("*.")): - raise ConfigurationError("Error: Wildcard domains are not supported") + raise errors.ConfigurationError("Error: Wildcard domains are not supported") # Punycode if any(True for d in args.domains if "xn--" in d): - raise ConfigurationError("Error: Punycode domains are not supported") + raise errors.ConfigurationError("Error: Punycode domains are not supported") # Check for FQDN fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? Date: Sun, 8 Nov 2015 21:08:50 +0200 Subject: [PATCH 050/124] Changed the checks to be more readable and better semantically --- letsencrypt/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index e901246ce..88d7e3d30 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -1153,10 +1153,10 @@ def check_config_sanity(args): # Domain checks if args.domains is not None: # Check if there's a wildcard domain - if any(True for d in args.domains if d.startswith("*.")): + if any(d.startswith("*.") for d in args.domains): raise errors.ConfigurationError("Error: Wildcard domains are not supported") # Punycode - if any(True for d in args.domains if "xn--" in d): + if any("xn--" in d for d in args.domains): raise errors.ConfigurationError("Error: Punycode domains are not supported") # Check for FQDN fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? Date: Sun, 8 Nov 2015 21:14:12 +0200 Subject: [PATCH 051/124] Added comment to clarify FQDN regex --- letsencrypt/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 88d7e3d30..1f04ee173 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -1158,7 +1158,8 @@ def check_config_sanity(args): # Punycode if any("xn--" in d for d in args.domains): raise errors.ConfigurationError("Error: Punycode domains are not supported") - # Check for FQDN + # FQDN, checks: + # Characters used, domain parts < 63 chars, tld > 3 < 6 chars fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? Date: Sun, 8 Nov 2015 21:43:48 +0200 Subject: [PATCH 052/124] Refactored domain flag checks into their own helper function to clear space for future checks --- letsencrypt/cli.py | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 1f04ee173..c66060d48 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -1152,17 +1152,29 @@ def check_config_sanity(args): """ # Domain checks if args.domains is not None: - # Check if there's a wildcard domain - if any(d.startswith("*.") for d in args.domains): - raise errors.ConfigurationError("Error: Wildcard domains are not supported") - # Punycode - if any("xn--" in d for d in args.domains): - raise errors.ConfigurationError("Error: Punycode domains are not supported") - # FQDN, checks: - # Characters used, domain parts < 63 chars, tld > 3 < 6 chars - fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? 3 < 6 chars + fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? Date: Sun, 8 Nov 2015 21:46:57 +0200 Subject: [PATCH 053/124] Fixed the messages --- letsencrypt/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index c66060d48..23ef8956a 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -1165,16 +1165,16 @@ def _check_config_domain_sanity(domains): # Check if there's a wildcard domain if any(d.startswith("*.") for d in domains): raise errors.ConfigurationError( - "Error: Wildcard domains are not supported") + "Wildcard domains are not supported") # Punycode if any("xn--" in d for d in domains): raise errors.ConfigurationError( - "Error: Punycode domains are not supported") + "Punycode domains are not supported") # FQDN, checks: # Characters used, domain parts < 63 chars, tld > 3 < 6 chars fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? Date: Sun, 8 Nov 2015 22:21:11 +0200 Subject: [PATCH 054/124] Added domain to renewer test mock --- letsencrypt/tests/renewer_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index 0a39b7987..e76b6eb88 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -691,6 +691,7 @@ class RenewableCertTests(BaseRenewableCertTest): self.test_rc.configfile["renewalparams"]["tls_sni_01_port"] = "4430" self.test_rc.configfile["renewalparams"]["http01_port"] = "1234" self.test_rc.configfile["renewalparams"]["account"] = "abcde" + self.test_rc.configfile["renewalparams"]["domains"] = ["example.com"] mock_auth = mock.MagicMock() mock_pd.PluginsRegistry.find_all.return_value = {"apache": mock_auth} # Fails because "fake" != "apache" From 59b544be3b6ac41f0c6ab1556f2477e59d27cd67 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sun, 8 Nov 2015 22:26:01 +0200 Subject: [PATCH 055/124] Moved the validation to configurator --- letsencrypt/cli.py | 35 -------------------------------- letsencrypt/configuration.py | 39 +++++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 23ef8956a..b8bdeff7d 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -1087,7 +1087,6 @@ def main(cli_args=sys.argv[1:]): plugins = plugins_disco.PluginsRegistry.find_all() args = prepare_and_parse_args(plugins, cli_args) # Check command line parameters sanity, and error out in case of problem. - check_config_sanity(args) config = configuration.NamespaceConfig(args) zope.component.provideUtility(config) @@ -1142,40 +1141,6 @@ def main(cli_args=sys.argv[1:]): return args.func(args, config, plugins) -def check_config_sanity(args): - """Validate command line options and display error message if - requirements are not met. - - :param args: Command line options - :type args: :class:`argparse.Namespace` - - """ - # Domain checks - if args.domains is not None: - _check_config_domain_sanity(args.domains) - -def _check_config_domain_sanity(domains): - """Helper method for check_config_sanity which validates - domain flag values and errors out if the requirements are not met. - - :param domains: List of domains - :type args: `list` of `string` - - """ - # Check if there's a wildcard domain - if any(d.startswith("*.") for d in domains): - raise errors.ConfigurationError( - "Wildcard domains are not supported") - # Punycode - if any("xn--" in d for d in domains): - raise errors.ConfigurationError( - "Punycode domains are not supported") - # FQDN, checks: - # Characters used, domain parts < 63 chars, tld > 3 < 6 chars - fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? 3 < 6 chars + fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? Date: Sun, 8 Nov 2015 22:44:32 +0200 Subject: [PATCH 056/124] Making linter happy by removing the unused import --- letsencrypt/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index b8bdeff7d..d343fcaa5 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -7,7 +7,6 @@ import logging import logging.handlers import os import pkg_resources -import re import sys import time import traceback From 0c4456bd7ed6127d410839086a02828bee15e917 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Sun, 8 Nov 2015 22:57:58 +0200 Subject: [PATCH 057/124] Fixed the comment to be accurate --- letsencrypt/configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index b18807c40..082a69c21 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -144,7 +144,7 @@ def _check_config_domain_sanity(domains): raise errors.ConfigurationError( "Punycode domains are not supported") # FQDN, checks: - # Characters used, domain parts < 63 chars, tld > 3 < 6 chars + # Characters used, domain parts < 63 chars, tld > 1 < 7 chars fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? Date: Sun, 8 Nov 2015 23:05:33 +0200 Subject: [PATCH 058/124] Refactored the port check from NamespaceConfig init to the validation function --- letsencrypt/configuration.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 082a69c21..8fa3cb2dc 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -38,10 +38,6 @@ class NamespaceConfig(object): def __init__(self, namespace): self.namespace = namespace check_config_sanity(self) - if self.http01_port == self.tls_sni_01_port: - raise errors.Error( - "Trying to run http-01 and tls-sni-01 " - "on the same port ({0})".format(self.tls_sni_01_port)) def __getattr__(self, name): return getattr(self.namespace, name) @@ -122,6 +118,12 @@ def check_config_sanity(config): :type args: :class:`letsencrypt.interfaces.IConfig` """ + # Port check + if config.http01_port == config.tls_sni_01_port: + raise errors.Error( + "Trying to run http-01 and tls-sni-01 " + "on the same port ({0})".format(config.tls_sni_01_port)) + # Domain checks if config.namespace.domains is not None: _check_config_domain_sanity(config.namespace.domains) From d29ab2a5238fd4c8f2fefbde9bf80e8af0642c9d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 9 Nov 2015 16:18:51 -0800 Subject: [PATCH 059/124] Make final tweaks for landing #1421 --- letsencrypt/cli.py | 3 +-- letsencrypt/configuration.py | 13 +++++++++---- letsencrypt/errors.py | 1 + 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index d343fcaa5..7227116e3 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -107,7 +107,7 @@ def _find_domains(args, installer): if not domains: raise errors.Error("Please specify --domains, or --installer that " - "will help in domain names autodiscovery") + "will help in domain names autodiscovery") return domains @@ -1085,7 +1085,6 @@ def main(cli_args=sys.argv[1:]): # note: arg parser internally handles --help (and exits afterwards) plugins = plugins_disco.PluginsRegistry.find_all() args = prepare_and_parse_args(plugins, cli_args) - # Check command line parameters sanity, and error out in case of problem. config = configuration.NamespaceConfig(args) zope.component.provideUtility(config) diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 8fa3cb2dc..e70171675 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -37,6 +37,7 @@ class NamespaceConfig(object): def __init__(self, namespace): self.namespace = namespace + # Check command line parameters sanity, and error out in case of problem. check_config_sanity(self) def __getattr__(self, name): @@ -120,7 +121,7 @@ def check_config_sanity(config): """ # Port check if config.http01_port == config.tls_sni_01_port: - raise errors.Error( + raise errors.ConfigurationError( "Trying to run http-01 and tls-sni-01 " "on the same port ({0})".format(config.tls_sni_01_port)) @@ -134,7 +135,9 @@ def _check_config_domain_sanity(domains): domain flag values and errors out if the requirements are not met. :param domains: List of domains - :type args: `list` of `string` + :type domains: `list` of `string` + :raises ConfigurationError: for invalid domains and cases where Let's + Encrypt currently will not issue certificates """ # Check if there's a wildcard domain @@ -145,8 +148,10 @@ def _check_config_domain_sanity(domains): if any("xn--" in d for d in domains): raise errors.ConfigurationError( "Punycode domains are not supported") - # FQDN, checks: + # FQDN checks from + # http://www.mkyong.com/regular-expressions/domain-name-regular-expression-example/ # Characters used, domain parts < 63 chars, tld > 1 < 7 chars + # first and last char is not "-" fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? Date: Mon, 9 Nov 2015 17:42:46 -0800 Subject: [PATCH 060/124] --user-agent flag --- letsencrypt/cli.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 7227116e3..c37533115 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -890,7 +890,13 @@ def _create_subparsers(helpful): helpful.add_group("revoke", description="Options for revocation of certs") helpful.add_group("rollback", description="Options for reverting config changes") helpful.add_group("plugins", description="Plugin options") - + helpful.add( + "certonly", "--user-agent", type=str, default=None, + help="Set a custom user agent string for the client. User agent strings allow " + "the CA to collect high level statistics about success rates by OS and " + "plugin. If you wish to hide your server OS version from the Let's " + 'Encrypt server, set this to "".' + ) helpful.add("certonly", "--csr", type=read_file, help="Path to a Certificate Signing Request (CSR) in DER" From fb35a11c730899a9de13239f50477eaf979baf03 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Nov 2015 17:47:12 -0800 Subject: [PATCH 061/124] get_os_info : wrangle OS and major version number in a semi-portable way --- letsencrypt/le_util.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index 5626902ef..625e6fb51 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -202,6 +202,35 @@ def safely_remove(path): raise +def get_os_info(): + """ + Get Operating System type/distribution and version + """ + + info = platform.system_alias( + platform.system(), + platform.release(), + platform.version() + ) + + os_type, os_ver, _ = info + os_type = os_type.lower() + + if os_type.startswith('linux'): + info = platform.linux_distribution() + os_type, os_ver, _ = info + + elif os_type.startswith('darwin'): + os_ver = subprocess.Popen( + ["sw_vers", "-productVersion"], + stdout=subprocess.PIPE + ).communicate()[0] + + else: + os_ver = '' + + return os_type, os_ver + # 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 From 7b352d3d80f674ecab3923aeb8373503967f0572 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 9 Nov 2015 18:21:39 -0800 Subject: [PATCH 062/124] Handle FreeBSD and Windows --- letsencrypt/le_util.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index 625e6fb51..413cec43d 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -204,31 +204,32 @@ def safely_remove(path): def get_os_info(): """ - Get Operating System type/distribution and version + Get Operating System type/distribution and major version """ - info = platform.system_alias( platform.system(), platform.release(), platform.version() ) - os_type, os_ver, _ = info os_type = os_type.lower() - if os_type.startswith('linux'): info = platform.linux_distribution() os_type, os_ver, _ = info - elif os_type.startswith('darwin'): os_ver = subprocess.Popen( ["sw_vers", "-productVersion"], stdout=subprocess.PIPE ).communicate()[0] - + elif os_type.startswith('freebsd'): + # eg "9.3-RC3-p1" + os_ver = os_ver.parititon("-")[0] + os_ver = os_ver.parititon(".")[0] + elif platform.win32_ver()[1]: + os_ver = win32_ver()[1] else: + # Cases known to fall here: Cygwin python os_ver = '' - return os_type, os_ver # Just make sure we don't get pwned... Make sure that it also doesn't From 0c197c955ef969ffc461f6255f69daed05896561 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 9 Nov 2015 18:44:30 -0800 Subject: [PATCH 063/124] Revert all changes in cleanup, temporary or otherwise --- letsencrypt-apache/letsencrypt_apache/configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index eee7cdbc5..704ecf870 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -1157,7 +1157,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # If all of the challenges have been finished, clean up everything if not self._chall_out: - self.revert_challenge_config() + self.recovery_routine() self.restart() self.parser.init_modules() From 0143d773628fb5d91228eca9f16f8543755b029a Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 9 Nov 2015 18:47:38 -0800 Subject: [PATCH 064/124] Removed revert_challenge_config --- .../letsencrypt_apache/augeas_configurator.py | 12 ------------ .../tests/augeas_configurator_test.py | 14 -------------- 2 files changed, 26 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/augeas_configurator.py b/letsencrypt-apache/letsencrypt_apache/augeas_configurator.py index b0b15d649..9400336eb 100644 --- a/letsencrypt-apache/letsencrypt_apache/augeas_configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/augeas_configurator.py @@ -162,18 +162,6 @@ class AugeasConfigurator(common.Plugin): # Need to reload configuration after these changes take effect self.aug.load() - def revert_challenge_config(self): - """Used to cleanup challenge configurations. - - :raises .errors.PluginError: If unable to revert the challenge config. - - """ - try: - self.reverter.revert_temporary_config() - except errors.ReverterError as err: - raise errors.PluginError(str(err)) - self.aug.load() - def rollback_checkpoints(self, rollback=1): """Rollback saved checkpoints. diff --git a/letsencrypt-apache/letsencrypt_apache/tests/augeas_configurator_test.py b/letsencrypt-apache/letsencrypt_apache/tests/augeas_configurator_test.py index 815e6fc44..afa36ba77 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/augeas_configurator_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/augeas_configurator_test.py @@ -76,20 +76,6 @@ class AugeasConfiguratorTest(util.ApacheTest): self.assertRaises( errors.PluginError, self.config.recovery_routine) - def test_revert_challenge_config(self): - mock_load = mock.Mock() - self.config.aug.load = mock_load - - self.config.revert_challenge_config() - self.assertEqual(mock_load.call_count, 1) - - def test_revert_challenge_config_error(self): - self.config.reverter.revert_temporary_config = mock.Mock( - side_effect=errors.ReverterError) - - self.assertRaises( - errors.PluginError, self.config.revert_challenge_config) - def test_rollback_checkpoints(self): mock_load = mock.Mock() self.config.aug.load = mock_load From 747b7ca507aa6c5a9076311635d9b0550d04c529 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 10 Nov 2015 05:34:15 +0200 Subject: [PATCH 065/124] More robust way of detecting the mod_macro vhosts --- .../letsencrypt_apache/configurator.py | 30 +++++-------------- .../tests/configurator_test.py | 5 ---- 2 files changed, 8 insertions(+), 27 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index 396501c90..32ecb1cdb 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -410,33 +410,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): for alias in serveralias_match: serveralias = self.parser.get_arg(alias) - if not self._is_mod_macro(serveralias): + if not host.modmacro: host.aliases.add(serveralias) - else: - host.modmacro = True if servername_match: # Get last ServerName as each overwrites the previous servername = self.parser.get_arg(servername_match[-1]) - if not self._is_mod_macro(servername): + if not host.modmacro: host.name = servername - else: - host.modmacro = True - - def _is_mod_macro(self, name): - """Helper function for _add_servernames(). - Checks if the ServerName / ServerAlias belongs to a macro - - :param str name: Name to check and filter out if it's a variable - - :returns: boolean - :rtype: boolean - - """ - - if "$" in name: - return True - return False def _create_vhost(self, path): """Used by get_virtual_hosts to create vhost objects @@ -459,7 +440,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): filename = get_file_path(path) is_enabled = self.is_site_enabled(filename) - vhost = obj.VirtualHost(filename, path, addrs, is_ssl, is_enabled) + macro = False + if "/Macro/" in path: + macro = True + + vhost = obj.VirtualHost(filename, path, addrs, is_ssl, + is_enabled, modmacro=macro) self._add_servernames(vhost) return vhost diff --git a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py index 8fc1240ec..0350a32ec 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py @@ -178,11 +178,6 @@ class TwoVhost80Test(util.ApacheTest): self.assertEqual( self.config._find_best_vhost("example.demo"), self.vh_truth[2]) - def test_is_mod_macro(self): - # pylint: disable=protected-access - self.assertEqual(self.config._is_mod_macro("$domain"), True) - self.assertEqual(self.config._is_mod_macro("www.example.com"), False) - def test_non_default_vhosts(self): # pylint: disable=protected-access self.assertEqual(len(self.config._non_default_vhosts()), 4) From 88cc01301a3531f3ca01cf63ae881fde9f3fb5b4 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 9 Nov 2015 22:09:10 -0800 Subject: [PATCH 066/124] Creation & plumbing for user agent --- acme/acme/client.py | 5 +++-- letsencrypt/cli.py | 2 ++ letsencrypt/client.py | 3 ++- letsencrypt/configuration.py | 14 ++++++++++++++ letsencrypt/le_util.py | 3 ++- 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index 0e9319f9c..082dcb5cd 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -37,6 +37,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes :ivar key: `.JWK` (private) :ivar alg: `.JWASignature` :ivar bool verify_ssl: Verify SSL certificates? + :ivar str ua: User agent string to send to the server. :ivar .ClientNetwork net: Client network. Useful for testing. If not supplied, it will be initialized using `key`, `alg` and `verify_ssl`. @@ -45,7 +46,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes DER_CONTENT_TYPE = 'application/pkix-cert' def __init__(self, directory, key, alg=jose.RS256, verify_ssl=True, - net=None): + net=None, ua="acme-python"): """Initialize. :param directory: Directory Resource (`.messages.Directory`) or @@ -53,7 +54,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes """ self.key = key - self.net = ClientNetwork(key, alg, verify_ssl) if net is None else net + self.net = ClientNetwork(key, alg, verify_ssl, user_agent) if net is None else net if isinstance(directory, six.string_types): self.directory = messages.Directory.from_json( diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index c37533115..6f4facc3e 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -169,6 +169,7 @@ def _determine_account(args, config): def _init_le_client(args, config, authenticator, installer): + config.deterimine_user_agent(authenticator, installer) if authenticator is not None: # if authenticator was given, then we will need account... acc, acme = _determine_account(args, config) @@ -521,6 +522,7 @@ def revoke(args, config, unused_plugins): # TODO: coop with renewal config logger.debug("Revoking %s using Account Key", args.cert_path[0]) acc, _ = _determine_account(args, config) # pylint: disable=protected-access + config.determine_user_agent(None, None) acme = client._acme_from_config_key(config, acc.key) acme.revoke(jose.ComparableX509(crypto_util.pyopenssl_load_certificate( args.cert_path[1])[0])) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 8a0ad6af4..0b4297791 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -32,7 +32,8 @@ logger = logging.getLogger(__name__) def _acme_from_config_key(config, key): # TODO: Allow for other alg types besides RS256 return acme_client.Client(directory=config.server, key=key, - verify_ssl=(not config.no_verify_ssl)) + verify_ssl=(not config.no_verify_ssl), + ua=config.user_agent) def register(config, account_storage, tos_cb=None): diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index e70171675..a3fd33d69 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -3,6 +3,7 @@ import os import urlparse import re +import letsencrypt import zope.interface from acme import challenges @@ -82,6 +83,19 @@ class NamespaceConfig(object): else: return challenges.HTTP01Response.PORT + def determine_user_agent(self, authenticator, installer): + # The user agent string isn't knowable until the authenticator and + # installer have been chosen. + + if self.user_agent is None: + ua = "LetsEncryptPythonClient/{0} ({1}) Authenticator/{2} Installer/{3}" + ua = ua.format(letsencrypt.__version__, le_util.get_os_info(), + authenticator.name if authenticator else "none", + installer.name if installer else "none") + self.user_agent=ua + else: + assert isinstance(self.user_agent, str), "User Agent not a string?" + class RenewerConfiguration(object): """Configuration wrapper for renewer.""" diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index 413cec43d..4a780e36b 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -3,6 +3,7 @@ import collections import errno import logging import os +import platform import re import subprocess import stat @@ -226,7 +227,7 @@ def get_os_info(): os_ver = os_ver.parititon("-")[0] os_ver = os_ver.parititon(".")[0] elif platform.win32_ver()[1]: - os_ver = win32_ver()[1] + os_ver = platform.win32_ver()[1] else: # Cases known to fall here: Cygwin python os_ver = '' From baaeab5fa1d752eec726fc1aa8bd4b3d75a2eac1 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 9 Nov 2015 22:44:26 -0800 Subject: [PATCH 067/124] Fix lints & glitches --- letsencrypt/cli.py | 2 +- letsencrypt/configuration.py | 13 ++++++++----- letsencrypt/le_util.py | 4 ++-- letsencrypt/tests/client_test.py | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 6f4facc3e..957ada7af 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -169,7 +169,7 @@ def _determine_account(args, config): def _init_le_client(args, config, authenticator, installer): - config.deterimine_user_agent(authenticator, installer) + config.determine_user_agent(authenticator, installer) if authenticator is not None: # if authenticator was given, then we will need account... acc, acme = _determine_account(args, config) diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index a3fd33d69..345cf75b4 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -11,6 +11,7 @@ from acme import challenges from letsencrypt import constants from letsencrypt import errors from letsencrypt import interfaces +from letsencrypt import le_util class NamespaceConfig(object): @@ -84,17 +85,19 @@ class NamespaceConfig(object): return challenges.HTTP01Response.PORT def determine_user_agent(self, authenticator, installer): - # The user agent string isn't knowable until the authenticator and - # installer have been chosen. + """ + Set a user_agent string in the config based on the choice of plugins. + (this wasn't knowable at construction time) + """ - if self.user_agent is None: + if self.namespace.user_agent is None: ua = "LetsEncryptPythonClient/{0} ({1}) Authenticator/{2} Installer/{3}" ua = ua.format(letsencrypt.__version__, le_util.get_os_info(), authenticator.name if authenticator else "none", installer.name if installer else "none") - self.user_agent=ua + self.namespace.user_agent = ua else: - assert isinstance(self.user_agent, str), "User Agent not a string?" + assert isinstance(self.namespace.user_agent, str), "User Agent not a string?" class RenewerConfiguration(object): diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index 4a780e36b..112c90a6e 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -224,8 +224,8 @@ def get_os_info(): ).communicate()[0] elif os_type.startswith('freebsd'): # eg "9.3-RC3-p1" - os_ver = os_ver.parititon("-")[0] - os_ver = os_ver.parititon(".")[0] + os_ver = os_ver.partition("-")[0] + os_ver = os_ver.partition(".")[0] elif platform.win32_ver()[1]: os_ver = platform.win32_ver()[1] else: diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 4a61194f3..3caea4f05 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -71,7 +71,7 @@ class ClientTest(unittest.TestCase): def test_init_acme_verify_ssl(self): self.acme_client.assert_called_once_with( - directory=mock.ANY, key=mock.ANY, verify_ssl=True) + directory=mock.ANY, key=mock.ANY, ua=mock.ANY, verify_ssl=True) def _mock_obtain_certificate(self): self.client.auth_handler = mock.MagicMock() From 85675d709ccd10c7a57aa632e5a94e7495b8f1d4 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 10 Nov 2015 11:20:33 +0200 Subject: [PATCH 068/124] Case insensitive matching --- letsencrypt-apache/letsencrypt_apache/configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index 32ecb1cdb..f10f0c241 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -441,7 +441,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): is_enabled = self.is_site_enabled(filename) macro = False - if "/Macro/" in path: + if "/macro/" in path.lower(): macro = True vhost = obj.VirtualHost(filename, path, addrs, is_ssl, From 096c689fba07d10dc592e3d74b4ed63ddd5a2b47 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 10 Nov 2015 12:03:15 +0200 Subject: [PATCH 069/124] Added help text for -d flag --- letsencrypt/cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 7227116e3..a41cd3e6d 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -811,7 +811,9 @@ def prepare_and_parse_args(plugins, args): # --domains is useful, because it can be stored in config #for subparser in parser_run, parser_auth, parser_install: # subparser.add_argument("domains", nargs="*", metavar="domain") - helpful.add(None, "-d", "--domains", metavar="DOMAIN", action="append") + helpful.add(None, "-d", "--domains", metavar="DOMAIN", action="append", + help="Domain names to apply. Use multiple -d flags if you want " + "to specify multiple domains") helpful.add( None, "--duplicate", dest="duplicate", action="store_true", help="Allow getting a certificate that duplicates an existing one") From 3074ef996a465d3068cba19f59bc4b345ff4291b Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Tue, 10 Nov 2015 12:29:08 +0200 Subject: [PATCH 070/124] Refactored the argument and the code to use --domain instead of --domains, which was semantically incorrect --- letsencrypt-nginx/tests/boulder-integration.sh | 2 +- letsencrypt/cli.py | 13 +++++++------ letsencrypt/tests/cli_test.py | 2 +- tests/boulder-integration.sh | 4 ++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/letsencrypt-nginx/tests/boulder-integration.sh b/letsencrypt-nginx/tests/boulder-integration.sh index 3cbe9f6b9..0e3e7e77a 100755 --- a/letsencrypt-nginx/tests/boulder-integration.sh +++ b/letsencrypt-nginx/tests/boulder-integration.sh @@ -18,7 +18,7 @@ letsencrypt_test_nginx () { "$@" } -letsencrypt_test_nginx --domains nginx.wtf run +letsencrypt_test_nginx --domain nginx.wtf run echo | openssl s_client -connect localhost:5001 \ | openssl x509 -out $root/nginx.pem diff -q $root/nginx.pem $root/conf/live/nginx.wtf/cert.pem diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index a41cd3e6d..7ce89e184 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -106,7 +106,7 @@ def _find_domains(args, installer): domains = args.domains if not domains: - raise errors.Error("Please specify --domains, or --installer that " + raise errors.Error("Please specify --domain, or --installer that " "will help in domain names autodiscovery") return domains @@ -465,9 +465,9 @@ def obtaincert(args, config, plugins): """Authenticate & obtain cert, but do not install it.""" if args.domains is not None and args.csr is not None: - # TODO: --csr could have a priority, when --domains is + # TODO: --csr could have a priority, when --domain is # supplied, check if CSR matches given domains? - return "--domains and --csr are mutually exclusive" + return "--domain and --csr are mutually exclusive" try: # installers are used in auth mode to determine domain names @@ -807,11 +807,12 @@ def prepare_and_parse_args(plugins, args): None, "-t", "--text", dest="text_mode", action="store_true", help="Use the text output instead of the curses UI.") helpful.add(None, "-m", "--email", help=config_help("email")) - # positional arg shadows --domains, instead of appending, and - # --domains is useful, because it can be stored in config + # positional arg shadows --domain, instead of appending, and + # --domain is useful, because it can be stored in config #for subparser in parser_run, parser_auth, parser_install: # subparser.add_argument("domains", nargs="*", metavar="domain") - helpful.add(None, "-d", "--domains", metavar="DOMAIN", action="append", + helpful.add(None, "-d", "--domain", dest="domains", + metavar="DOMAIN", action="append", help="Domain names to apply. Use multiple -d flags if you want " "to specify multiple domains") helpful.add( diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index da5286a10..31f528cbf 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -170,7 +170,7 @@ class CLITest(unittest.TestCase): def test_certonly_bad_args(self): ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR]) - self.assertEqual(ret, '--domains and --csr are mutually exclusive') + self.assertEqual(ret, '--domain and --csr are mutually exclusive') ret, _, _, _ = self._call(['-a', 'bad_auth', 'certonly']) self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed') diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index d35ecbcff..97babb591 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -27,8 +27,8 @@ common() { "$@" } -common --domains le1.wtf --standalone-supported-challenges tls-sni-01 auth -common --domains le2.wtf --standalone-supported-challenges http-01 run +common --domain le1.wtf --standalone-supported-challenges tls-sni-01 auth +common --domain le2.wtf --standalone-supported-challenges http-01 run common -a manual -d le.wtf auth export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \ From 60147eb5299b5e84a9b6322e51b07850a9d4860b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 10 Nov 2015 14:52:18 -0800 Subject: [PATCH 071/124] Define state of checkpoints when save fails --- .../letsencrypt_apache/augeas_configurator.py | 16 +++++++++------- letsencrypt/interfaces.py | 3 ++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/augeas_configurator.py b/letsencrypt-apache/letsencrypt_apache/augeas_configurator.py index 9400336eb..c47981252 100644 --- a/letsencrypt-apache/letsencrypt_apache/augeas_configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/augeas_configurator.py @@ -73,7 +73,8 @@ class AugeasConfigurator(common.Plugin): This function first checks for save errors, if none are found, all configuration changes made will be saved. According to the - function parameters. + function parameters. If an exception is raised, a new checkpoint + was not created. :param str title: The title of the save. If a title is given, the configuration will be saved as a new checkpoint and put in a @@ -82,8 +83,9 @@ class AugeasConfigurator(common.Plugin): :param bool temporary: Indicates whether the changes made will be quickly reversed in the future (ie. challenges) - :raises .errors.PluginError: If there was an error in Augeas, in an - attempt to save the configuration, or an error creating a checkpoint + :raises .errors.PluginError: If there was an error in Augeas, in + an attempt to save the configuration, or an error creating a + checkpoint """ save_state = self.aug.get("/augeas/save") @@ -122,16 +124,16 @@ class AugeasConfigurator(common.Plugin): except errors.ReverterError as err: raise errors.PluginError(str(err)) + self.aug.set("/augeas/save", save_state) + self.save_notes = "" + self.aug.save() + if title and not temporary: try: self.reverter.finalize_checkpoint(title) except errors.ReverterError as err: raise errors.PluginError(str(err)) - self.aug.set("/augeas/save", save_state) - self.save_notes = "" - self.aug.save() - def _log_save_errors(self, ex_errs): """Log errors due to bad Augeas save. diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 987fdc25e..c8a725fde 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -298,7 +298,8 @@ class IInstaller(IPlugin): Both title and temporary are needed because a save may be intended to be permanent, but the save is not ready to be a full - checkpoint + checkpoint. If an exception is raised, it is assumed a new + checkpoint was not created. :param str title: The title of the save. If a title is given, the configuration will be saved as a new checkpoint and put in a From 414321fca6073cbd99cad32db1632f6fe178cea2 Mon Sep 17 00:00:00 2001 From: Chhatoi Pritam Baral Date: Wed, 11 Nov 2015 04:50:16 +0530 Subject: [PATCH 072/124] Fix #1281: Check if nginx binary exists --- letsencrypt-nginx/letsencrypt_nginx/configurator.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/letsencrypt-nginx/letsencrypt_nginx/configurator.py b/letsencrypt-nginx/letsencrypt_nginx/configurator.py index 0123ac321..d97cf7397 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/configurator.py +++ b/letsencrypt-nginx/letsencrypt_nginx/configurator.py @@ -107,6 +107,10 @@ class NginxConfigurator(common.Plugin): # This is called in determine_authenticator and determine_installer def prepare(self): """Prepare the authenticator/installer.""" + # Verify Nginx is installed + if not le_util.exe_exists(self.conf('ctl')): + raise errors.NoInstallationError + self.parser = parser.NginxParser( self.conf('server-root'), self.mod_ssl_conf) From 1bb063e87072e756db96c5641538ac6879ef9594 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 10 Nov 2015 16:03:18 -0800 Subject: [PATCH 073/124] Corrected crash recovery in client and added tests --- letsencrypt/client.py | 54 ++++++++++++++------- letsencrypt/tests/client_test.py | 80 ++++++++++++++++++++++++++++++-- 2 files changed, 114 insertions(+), 20 deletions(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 8a0ad6af4..452dcac16 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -1,10 +1,12 @@ """Let's Encrypt client API.""" +import functools import logging import os from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import rsa import OpenSSL +import zope.component from acme import client as acme_client from acme import jose @@ -18,6 +20,7 @@ from letsencrypt import continuity_auth from letsencrypt import crypto_util from letsencrypt import errors from letsencrypt import error_handler +from letsencrypt import interfaces from letsencrypt import le_util from letsencrypt import reverter from letsencrypt import storage @@ -331,25 +334,14 @@ class Client(object): self.installer.save("Deployed Let's Encrypt Certificate") # sites may have been enabled / final cleanup - with error_handler.ErrorHandler(self._rollback_and_restart): + restart_error_handler = error_handler.ErrorHandler(functools.partial( + self._rollback_and_restart, + "We were unable to install your certificate, however, we " + "successfully rolled back your server to its previous " + "configuration.")) + with restart_error_handler: self.installer.restart() - def _rollback_and_restart(self): - """Rollback the most recent checkpoint and restart the webserver""" - logger.critical("Rolling back to previous server configuration...") - try: - self.installer.rollback_checkpoints() - self.installer.restart() - except: - # TODO: suggest letshelp-letsencypt here - logger.critical("Failure to rollback config " - "changes and restart your server") - logger.critical("Please submit a bug report to " - "https://github.com/letsencrypt/letsencrypt") - raise - logger.critical("Rollback successful; your server has " - "been restarted with your old configuration") - def enhance_config(self, domains, redirect=None): """Enhance the configuration. @@ -395,8 +387,36 @@ class Client(object): raise self.installer.save("Add Redirects") + + restart_error_handler = error_handler.ErrorHandler(functools.partial( + self._rollback_and_restart, + "We were unable to setup a redirect for your server, however, we " + "successfully installed your certificate. If you'd like to revert " + "these changes as well, run 'letsencrypt rollback'.")) + with restart_error_handler: self.installer.restart() + def _rollback_and_restart(self, reporter_msg): + """Rollback the most recent checkpoint and restart the webserver + + :param str reporter_msg: message to show on successful rollback + + """ + logger.critical("Rolling back to previous server configuration...") + reporter = zope.component.getUtility(interfaces.IReporter) + try: + self.installer.rollback_checkpoints() + self.installer.restart() + except: + # TODO: suggest letshelp-letsencypt here + reporter.add_message( + "An error occured and we failed to rollback your config and " + "restart your server. Please submit a bug report to " + "https://github.com/letsencrypt/letsencrypt", + reporter.HIGH_PRIORITY) + raise + reporter.add_message(reporter_msg, reporter.HIGH_PRIORITY) + def validate_key_csr(privkey, csr=None): """Validate Key and CSR files. diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 4a61194f3..7c53677e4 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -148,7 +148,7 @@ class ClientTest(unittest.TestCase): shutil.rmtree(tmp_path) - def test_deploy_certificate(self): + def test_deploy_certificate_success(self): self.assertRaises(errors.Error, self.client.deploy_certificate, ["foo.bar"], "key", "cert", "chain", "fullchain") @@ -166,17 +166,38 @@ class ClientTest(unittest.TestCase): self.assertEqual(installer.save.call_count, 2) installer.restart.assert_called_once_with() - def test_deploy_certificate_restart_failure_with_recovery(self): + def test_deploy_certificate_failure(self): + installer = mock.MagicMock() + self.client.installer = installer + + installer.deploy_cert.side_effect = errors.PluginError + self.assertRaises(errors.PluginError, self.client.deploy_certificate, + ["foo.bar"], "key", "cert", "chain", "fullchain") + installer.recovery_routine.assert_called_once_with() + + def test_deploy_certificate_save_failure(self): + installer = mock.MagicMock() + self.client.installer = installer + + installer.save.side_effect = errors.PluginError + self.assertRaises(errors.PluginError, self.client.deploy_certificate, + ["foo.bar"], "key", "cert", "chain", "fullchain") + installer.recovery_routine.assert_called_once_with() + + @mock.patch("letsencrypt.client.zope.component.getUtility") + def test_deploy_certificate_restart_failure(self, mock_get_utility): installer = mock.MagicMock() installer.restart.side_effect = [errors.PluginError, None] self.client.installer = installer self.assertRaises(errors.PluginError, self.client.deploy_certificate, ["foo.bar"], "key", "cert", "chain", "fullchain") + self.assertEqual(mock_get_utility().add_message.call_count, 1) installer.rollback_checkpoints.assert_called_once_with() self.assertEqual(installer.restart.call_count, 2) - def test_deploy_certificate_restart_failure_without_recovery(self): + @mock.patch("letsencrypt.client.zope.component.getUtility") + def test_deploy_certificate_restart_failure2(self, mock_get_utility): installer = mock.MagicMock() installer.restart.side_effect = errors.PluginError installer.rollback_checkpoints.side_effect = errors.ReverterError @@ -184,6 +205,7 @@ class ClientTest(unittest.TestCase): self.assertRaises(errors.PluginError, self.client.deploy_certificate, ["foo.bar"], "key", "cert", "chain", "fullchain") + self.assertEqual(mock_get_utility().add_message.call_count, 1) installer.rollback_checkpoints.assert_called_once_with() self.assertEqual(installer.restart.call_count, 1) @@ -201,11 +223,63 @@ class ClientTest(unittest.TestCase): self.assertEqual(installer.save.call_count, 1) installer.restart.assert_called_once_with() + def test_enhance_config_no_installer(self): + self.assertRaises(errors.Error, + self.client.enhance_config, ["foo.bar"]) + + @mock.patch("letsencrypt.client.enhancements") + def test_enhance_config_enhance_failure(self, mock_enhancements): + mock_enhancements.ask.return_value = True + installer = mock.MagicMock() + self.client.installer = installer installer.enhance.side_effect = errors.PluginError + self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], True) installer.recovery_routine.assert_called_once_with() + @mock.patch("letsencrypt.client.enhancements") + def test_enhance_config_save_failure(self, mock_enhancements): + mock_enhancements.ask.return_value = True + installer = mock.MagicMock() + self.client.installer = installer + installer.save.side_effect = errors.PluginError + + self.assertRaises(errors.PluginError, + self.client.enhance_config, ["foo.bar"], True) + installer.recovery_routine.assert_called_once_with() + + @mock.patch("letsencrypt.client.zope.component.getUtility") + @mock.patch("letsencrypt.client.enhancements") + def test_enhance_config_restart_failure(self, mock_enhancements, + mock_get_utility): + mock_enhancements.ask.return_value = True + installer = mock.MagicMock() + self.client.installer = installer + installer.restart.side_effect = [errors.PluginError, None] + + self.assertRaises(errors.PluginError, + self.client.enhance_config, ["foo.bar"], True) + self.assertEqual(mock_get_utility().add_message.call_count, 1) + installer.rollback_checkpoints.assert_called_once_with() + self.assertEqual(installer.restart.call_count, 2) + + @mock.patch("letsencrypt.client.zope.component.getUtility") + @mock.patch("letsencrypt.client.enhancements") + def test_enhance_config_restart_failure2(self, mock_enhancements, + mock_get_utility): + mock_enhancements.ask.return_value = True + installer = mock.MagicMock() + self.client.installer = installer + installer.restart.side_effect = errors.PluginError + installer.rollback_checkpoints.side_effect = errors.ReverterError + + self.assertRaises(errors.PluginError, + self.client.enhance_config, ["foo.bar"], True) + self.assertEqual(mock_get_utility().add_message.call_count, 1) + installer.rollback_checkpoints.assert_called_once_with() + self.assertEqual(installer.restart.call_count, 1) + class RollbackTest(unittest.TestCase): """Tests for letsencrypt.client.rollback.""" From 0bbe69b36a8447dbfddde0b0d6065fd5e47cab94 Mon Sep 17 00:00:00 2001 From: Chhatoi Pritam Baral Date: Wed, 11 Nov 2015 05:49:34 +0530 Subject: [PATCH 074/124] Mock existence of nginx binary --- .../letsencrypt_nginx/tests/util.py | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/util.py b/letsencrypt-nginx/letsencrypt_nginx/tests/util.py index cb4e08ddf..e60feb3d3 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/util.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/util.py @@ -49,21 +49,25 @@ def get_nginx_configurator( backups = os.path.join(work_dir, "backups") - config = configurator.NginxConfigurator( - config=mock.MagicMock( - nginx_server_root=config_path, - le_vhost_ext="-le-ssl.conf", - config_dir=config_dir, - work_dir=work_dir, - backup_dir=backups, - temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"), - in_progress_dir=os.path.join(backups, "IN_PROGRESS"), - server="https://acme-server.org:443/new", - tls_sni_01_port=5001, - ), - name="nginx", - version=version) - config.prepare() + with mock.patch("letsencrypt_nginx.configurator.le_util." + "exe_exists") as mock_exe_exists: + mock_exe_exists.return_value = True + + config = configurator.NginxConfigurator( + config=mock.MagicMock( + nginx_server_root=config_path, + le_vhost_ext="-le-ssl.conf", + config_dir=config_dir, + work_dir=work_dir, + backup_dir=backups, + temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"), + in_progress_dir=os.path.join(backups, "IN_PROGRESS"), + server="https://acme-server.org:443/new", + tls_sni_01_port=5001, + ), + name="nginx", + version=version) + config.prepare() # Provide general config utility. nsconfig = configuration.NamespaceConfig(config.config) From 3c00afd55c1c5f06ee2400f3b8980ebcbe553dea Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 10 Nov 2015 16:31:52 -0800 Subject: [PATCH 075/124] Revert "Removed revert_challenge_config" This reverts commit 0143d773628fb5d91228eca9f16f8543755b029a. --- .../letsencrypt_apache/augeas_configurator.py | 12 ++++++++++++ .../tests/augeas_configurator_test.py | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/letsencrypt-apache/letsencrypt_apache/augeas_configurator.py b/letsencrypt-apache/letsencrypt_apache/augeas_configurator.py index c47981252..9e0948f12 100644 --- a/letsencrypt-apache/letsencrypt_apache/augeas_configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/augeas_configurator.py @@ -164,6 +164,18 @@ class AugeasConfigurator(common.Plugin): # Need to reload configuration after these changes take effect self.aug.load() + def revert_challenge_config(self): + """Used to cleanup challenge configurations. + + :raises .errors.PluginError: If unable to revert the challenge config. + + """ + try: + self.reverter.revert_temporary_config() + except errors.ReverterError as err: + raise errors.PluginError(str(err)) + self.aug.load() + def rollback_checkpoints(self, rollback=1): """Rollback saved checkpoints. diff --git a/letsencrypt-apache/letsencrypt_apache/tests/augeas_configurator_test.py b/letsencrypt-apache/letsencrypt_apache/tests/augeas_configurator_test.py index afa36ba77..815e6fc44 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/augeas_configurator_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/augeas_configurator_test.py @@ -76,6 +76,20 @@ class AugeasConfiguratorTest(util.ApacheTest): self.assertRaises( errors.PluginError, self.config.recovery_routine) + def test_revert_challenge_config(self): + mock_load = mock.Mock() + self.config.aug.load = mock_load + + self.config.revert_challenge_config() + self.assertEqual(mock_load.call_count, 1) + + def test_revert_challenge_config_error(self): + self.config.reverter.revert_temporary_config = mock.Mock( + side_effect=errors.ReverterError) + + self.assertRaises( + errors.PluginError, self.config.revert_challenge_config) + def test_rollback_checkpoints(self): mock_load = mock.Mock() self.config.aug.load = mock_load From 553592b2c81bd4667ba39c15f180f8a8aabea3a3 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 10 Nov 2015 16:32:05 -0800 Subject: [PATCH 076/124] Revert "Revert all changes in cleanup, temporary or otherwise" This reverts commit 0c197c955ef969ffc461f6255f69daed05896561. --- letsencrypt-apache/letsencrypt_apache/configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index 704ecf870..eee7cdbc5 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -1157,7 +1157,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # If all of the challenges have been finished, clean up everything if not self._chall_out: - self.recovery_routine() + self.revert_challenge_config() self.restart() self.parser.init_modules() From a4885e491aba09c1e2adedda7750887f515eae78 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 10 Nov 2015 16:04:56 -0800 Subject: [PATCH 077/124] Finish user agent changes: - revert changes to acme/, instead living with its current baroque API - add an extremely mockable test case --- acme/acme/client.py | 5 ++--- letsencrypt/client.py | 7 +++--- letsencrypt/tests/cli_test.py | 38 +++++++++++++++++++++----------- letsencrypt/tests/client_test.py | 2 +- 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/acme/acme/client.py b/acme/acme/client.py index 082dcb5cd..0e9319f9c 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -37,7 +37,6 @@ class Client(object): # pylint: disable=too-many-instance-attributes :ivar key: `.JWK` (private) :ivar alg: `.JWASignature` :ivar bool verify_ssl: Verify SSL certificates? - :ivar str ua: User agent string to send to the server. :ivar .ClientNetwork net: Client network. Useful for testing. If not supplied, it will be initialized using `key`, `alg` and `verify_ssl`. @@ -46,7 +45,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes DER_CONTENT_TYPE = 'application/pkix-cert' def __init__(self, directory, key, alg=jose.RS256, verify_ssl=True, - net=None, ua="acme-python"): + net=None): """Initialize. :param directory: Directory Resource (`.messages.Directory`) or @@ -54,7 +53,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes """ self.key = key - self.net = ClientNetwork(key, alg, verify_ssl, user_agent) if net is None else net + self.net = ClientNetwork(key, alg, verify_ssl) if net is None else net if isinstance(directory, six.string_types): self.directory = messages.Directory.from_json( diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 0b4297791..d48230f04 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -31,9 +31,9 @@ logger = logging.getLogger(__name__) def _acme_from_config_key(config, key): # TODO: Allow for other alg types besides RS256 - return acme_client.Client(directory=config.server, key=key, - verify_ssl=(not config.no_verify_ssl), - ua=config.user_agent) + net = acme_client.ClientNetwork(key, verify_ssl=(not config.no_verify_ssl), + user_agent=config.user_agent) + return acme_client.Client(directory=config.server, key=key, net=net) def register(config, account_storage, tos_cb=None): @@ -99,6 +99,7 @@ def register(config, account_storage, tos_cb=None): acc = account.Account(regr, key) account.report_new_account(acc, config) account_storage.save(acc) + return acc, acme diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index da5286a10..b6c56558e 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -10,6 +10,7 @@ import unittest import mock from letsencrypt import account +from letsencrypt import cli from letsencrypt import configuration from letsencrypt import errors @@ -30,15 +31,15 @@ class CLITest(unittest.TestCase): self.config_dir = os.path.join(self.tmp_dir, 'config') self.work_dir = os.path.join(self.tmp_dir, 'work') self.logs_dir = os.path.join(self.tmp_dir, 'logs') + self.standard_args = ['--text', '--config-dir', self.config_dir, + '--work-dir', self.work_dir, '--logs-dir', self.logs_dir, + '--agree-dev-preview'] def tearDown(self): shutil.rmtree(self.tmp_dir) def _call(self, args): - from letsencrypt import cli - args = ['--text', '--config-dir', self.config_dir, - '--work-dir', self.work_dir, '--logs-dir', self.logs_dir, - '--agree-dev-preview'] + args + args = self.standard_args + args with mock.patch('letsencrypt.cli.sys.stdout') as stdout: with mock.patch('letsencrypt.cli.sys.stderr') as stderr: with mock.patch('letsencrypt.cli.client') as client: @@ -50,10 +51,7 @@ class CLITest(unittest.TestCase): Variant of _call that preserves stdout so that it can be mocked by the caller. """ - from letsencrypt import cli - args = ['--text', '--config-dir', self.config_dir, - '--work-dir', self.work_dir, '--logs-dir', self.logs_dir, - '--agree-dev-preview'] + args + args = self.standard_args + args with mock.patch('letsencrypt.cli.sys.stderr') as stderr: with mock.patch('letsencrypt.cli.client') as client: ret = cli.main(args) @@ -112,9 +110,27 @@ class CLITest(unittest.TestCase): self.assertTrue("--key-path" not in out) out = self._help_output(['-h']) - from letsencrypt import cli self.assertTrue(cli.usage_strings(plugins)[0] in out) + + @mock.patch('letsencrypt.cli.sys.stdout') + @mock.patch('letsencrypt.cli.sys.stderr') + @mock.patch('letsencrypt.cli.client.acme_client.ClientNetwork') + @mock.patch('letsencrypt.cli.client.acme_client.Client') + @mock.patch('letsencrypt.cli._determine_account') + @mock.patch('letsencrypt.cli.client.Client.obtain_and_enroll_certificate') + @mock.patch('letsencrypt.cli._auth_from_domains') + def test_user_agent(self, _afd, _obt, det, _client, acme_net, _out, _err): + ua = "bandersnatch" + # Normally the client is totally mocked out, but here we need more + # arguments to automate it... + args = ["--user-agent", ua, "--standalone", "certonly", + "-m", "none@none.com", "-d", "example.com", '--agree-tos'] + args += self.standard_args + det.return_value = mock.MagicMock(), None + cli.main(args) + acme_net.assert_called_once_with(mock.ANY, verify_ssl=True, user_agent=ua) + @mock.patch('letsencrypt.cli.display_ops') def test_installer_selection(self, mock_display_ops): self._call(['install', '--domain', 'foo.bar', '--cert-path', 'cert', @@ -281,8 +297,6 @@ class CLITest(unittest.TestCase): @mock.patch('letsencrypt.cli.sys') def test_handle_exception(self, mock_sys): # pylint: disable=protected-access - from letsencrypt import cli - mock_open = mock.mock_open() with mock.patch('letsencrypt.cli.open', mock_open, create=True): exception = Exception('detail') @@ -433,8 +447,6 @@ class MockedVerb(object): """ def __init__(self, verb_name): - from letsencrypt import cli - self.verb_dict = cli.HelpfulArgumentParser.VERBS self.verb_func = None self.verb_name = verb_name diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 3caea4f05..4a61194f3 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -71,7 +71,7 @@ class ClientTest(unittest.TestCase): def test_init_acme_verify_ssl(self): self.acme_client.assert_called_once_with( - directory=mock.ANY, key=mock.ANY, ua=mock.ANY, verify_ssl=True) + directory=mock.ANY, key=mock.ANY, verify_ssl=True) def _mock_obtain_certificate(self): self.client.auth_handler = mock.MagicMock() From 8b9a271900e60b635e0fca4b018fd4a5ca9e0ccb Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 10 Nov 2015 16:40:27 -0800 Subject: [PATCH 078/124] "Really" finalise User Agent tests - Add an extra test for the default UA string - Fixes: get_os_info() is returning a tuple, not a string - Workaround: Argument preprocessing is dangerous for test cases --- letsencrypt/configuration.py | 2 +- letsencrypt/tests/cli_test.py | 31 +++++++++++++++++++++---------- letsencrypt/tests/client_test.py | 4 ++-- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 345cf75b4..9ee702258 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -92,7 +92,7 @@ class NamespaceConfig(object): if self.namespace.user_agent is None: ua = "LetsEncryptPythonClient/{0} ({1}) Authenticator/{2} Installer/{3}" - ua = ua.format(letsencrypt.__version__, le_util.get_os_info(), + ua = ua.format(letsencrypt.__version__, " ".join(le_util.get_os_info()), authenticator.name if authenticator else "none", installer.name if installer else "none") self.namespace.user_agent = ua diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index b6c56558e..21a774a9c 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -13,6 +13,7 @@ from letsencrypt import account from letsencrypt import cli from letsencrypt import configuration from letsencrypt import errors +from letsencrypt import le_util from letsencrypt.plugins import disco @@ -43,7 +44,7 @@ class CLITest(unittest.TestCase): with mock.patch('letsencrypt.cli.sys.stdout') as stdout: with mock.patch('letsencrypt.cli.sys.stderr') as stderr: with mock.patch('letsencrypt.cli.client') as client: - ret = cli.main(args) + ret = cli.main(args[:]) # NOTE: parser can alter its args! return ret, stdout, stderr, client def _call_stdout(self, args): @@ -54,7 +55,7 @@ class CLITest(unittest.TestCase): args = self.standard_args + args with mock.patch('letsencrypt.cli.sys.stderr') as stderr: with mock.patch('letsencrypt.cli.client') as client: - ret = cli.main(args) + ret = cli.main(args[:]) # NOTE: parser can alter its args! return ret, None, stderr, client def test_no_flags(self): @@ -115,21 +116,31 @@ class CLITest(unittest.TestCase): @mock.patch('letsencrypt.cli.sys.stdout') @mock.patch('letsencrypt.cli.sys.stderr') - @mock.patch('letsencrypt.cli.client.acme_client.ClientNetwork') @mock.patch('letsencrypt.cli.client.acme_client.Client') @mock.patch('letsencrypt.cli._determine_account') @mock.patch('letsencrypt.cli.client.Client.obtain_and_enroll_certificate') @mock.patch('letsencrypt.cli._auth_from_domains') - def test_user_agent(self, _afd, _obt, det, _client, acme_net, _out, _err): - ua = "bandersnatch" + def test_user_agent(self, _afd, _obt, det, _client, _err, _out): # Normally the client is totally mocked out, but here we need more # arguments to automate it... - args = ["--user-agent", ua, "--standalone", "certonly", - "-m", "none@none.com", "-d", "example.com", '--agree-tos'] - args += self.standard_args + args = ["--standalone", "certonly", "-m", "none@none.com", + "-d", "example.com", '--agree-tos'] + self.standard_args det.return_value = mock.MagicMock(), None - cli.main(args) - acme_net.assert_called_once_with(mock.ANY, verify_ssl=True, user_agent=ua) + with mock.patch('letsencrypt.cli.client.acme_client.ClientNetwork') as acme_net: + cli.main(args[:]) # Protect args from alteration + os_ver = " ".join(le_util.get_os_info()) + ua = acme_net.call_args[1]["user_agent"] + self.assertTrue(os_ver in ua) + import platform + plat = platform.platform() + if "linux" in plat.lower(): + self.assertTrue(platform.linux_distribution()[0] in ua) + + with mock.patch('letsencrypt.cli.client.acme_client.ClientNetwork') as acme_net: + ua = "bandersnatch" + args += ["--user-agent", ua] + cli.main(args) + acme_net.assert_called_once_with(mock.ANY, verify_ssl=True, user_agent=ua) @mock.patch('letsencrypt.cli.display_ops') def test_installer_selection(self, mock_display_ops): diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 4a61194f3..0007323fa 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -70,8 +70,8 @@ class ClientTest(unittest.TestCase): dv_auth=None, installer=None) def test_init_acme_verify_ssl(self): - self.acme_client.assert_called_once_with( - directory=mock.ANY, key=mock.ANY, verify_ssl=True) + net = self.acme_client.call_args[1]["net"] + self.assertTrue(net.verify_ssl) def _mock_obtain_certificate(self): self.client.auth_handler = mock.MagicMock() From 7dcf8a32f41c1a56ef6b436b79898fe3f436db5b Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 10 Nov 2015 16:47:11 -0800 Subject: [PATCH 079/124] Only send the major version of OS X --- letsencrypt/le_util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index 112c90a6e..a5c1a26a1 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -222,6 +222,7 @@ def get_os_info(): ["sw_vers", "-productVersion"], stdout=subprocess.PIPE ).communicate()[0] + os_ver = os_ver.partition(".")[0] elif os_type.startswith('freebsd'): # eg "9.3-RC3-p1" os_ver = os_ver.partition("-")[0] From 7d4beacce8f9bd234a9eabc2e3f062c9f1874942 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 10 Nov 2015 16:59:22 -0800 Subject: [PATCH 080/124] Added better error messages on redirect failure --- letsencrypt/client.py | 35 ++++++++++++++++++++++++-------- letsencrypt/tests/client_test.py | 10 +++++++-- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 452dcac16..9123eaf95 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -120,6 +120,12 @@ class Client(object): """ + # Message to show if enabling a redirect fails, but recovery is successful + _SUCCESSFUL_REDIRECT_RECOVERY_MSG = ( + "We were unable to setup a redirect for your server, however, we " + "successfully installed your certificate. If you'd like to revert " + "these changes as well, run 'letsencrypt rollback'.") + def __init__(self, config, account_, dv_auth, installer, acme=None): """Initialize a client.""" self.config = config @@ -378,7 +384,10 @@ class Client(object): :type vhost: :class:`letsencrypt.interfaces.IInstaller` """ - with error_handler.ErrorHandler(self.installer.recovery_routine): + enhance_error_handler = error_handler.ErrorHandler( + functools.partial(self._recovery_routine_with_msg, + self._SUCCESSFUL_REDIRECT_RECOVERY_MSG)) + with enhance_error_handler: for dom in domains: try: self.installer.enhance(dom, "redirect") @@ -388,18 +397,26 @@ class Client(object): self.installer.save("Add Redirects") - restart_error_handler = error_handler.ErrorHandler(functools.partial( - self._rollback_and_restart, - "We were unable to setup a redirect for your server, however, we " - "successfully installed your certificate. If you'd like to revert " - "these changes as well, run 'letsencrypt rollback'.")) + restart_error_handler = error_handler.ErrorHandler( + functools.partial(self._rollback_and_restart, + self._SUCCESSFUL_REDIRECT_RECOVERY_MSG)) with restart_error_handler: self.installer.restart() - def _rollback_and_restart(self, reporter_msg): + def _recovery_routine_with_msg(self, success_msg): + """Calls the installer's recovery routine and prints success_msg + + :param str success_msg: message to show on successful recovery + + """ + self.installer.recovery_routine() + reporter = zope.component.getUtility(interfaces.IReporter) + reporter.add_message(success_msg, reporter.HIGH_PRIORITY) + + def _rollback_and_restart(self, success_msg): """Rollback the most recent checkpoint and restart the webserver - :param str reporter_msg: message to show on successful rollback + :param str success_msg: message to show on successful rollback """ logger.critical("Rolling back to previous server configuration...") @@ -415,7 +432,7 @@ class Client(object): "https://github.com/letsencrypt/letsencrypt", reporter.HIGH_PRIORITY) raise - reporter.add_message(reporter_msg, reporter.HIGH_PRIORITY) + reporter.add_message(success_msg, reporter.HIGH_PRIORITY) def validate_key_csr(privkey, csr=None): diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 7c53677e4..f8da90f36 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -227,8 +227,10 @@ class ClientTest(unittest.TestCase): self.assertRaises(errors.Error, self.client.enhance_config, ["foo.bar"]) + @mock.patch("letsencrypt.client.zope.component.getUtility") @mock.patch("letsencrypt.client.enhancements") - def test_enhance_config_enhance_failure(self, mock_enhancements): + def test_enhance_config_enhance_failure(self, mock_enhancements, + mock_get_utility): mock_enhancements.ask.return_value = True installer = mock.MagicMock() self.client.installer = installer @@ -237,9 +239,12 @@ class ClientTest(unittest.TestCase): self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], True) installer.recovery_routine.assert_called_once_with() + self.assertEqual(mock_get_utility().add_message.call_count, 1) + @mock.patch("letsencrypt.client.zope.component.getUtility") @mock.patch("letsencrypt.client.enhancements") - def test_enhance_config_save_failure(self, mock_enhancements): + def test_enhance_config_save_failure(self, mock_enhancements, + mock_get_utility): mock_enhancements.ask.return_value = True installer = mock.MagicMock() self.client.installer = installer @@ -248,6 +253,7 @@ class ClientTest(unittest.TestCase): self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], True) installer.recovery_routine.assert_called_once_with() + self.assertEqual(mock_get_utility().add_message.call_count, 1) @mock.patch("letsencrypt.client.zope.component.getUtility") @mock.patch("letsencrypt.client.enhancements") From 0a72ebb37965e01e9d59559fac3f2dc1406a989a Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 10 Nov 2015 20:00:12 -0800 Subject: [PATCH 081/124] get_os_info review fixes - handle broken platform.linux_distribution() - document return type --- letsencrypt/le_util.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index a5c1a26a1..a0540a723 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -206,6 +206,7 @@ def safely_remove(path): def get_os_info(): """ Get Operating System type/distribution and major version + :returns: (`str` os_name, `str` os_version) """ info = platform.system_alias( platform.system(), @@ -216,7 +217,12 @@ def get_os_info(): os_type = os_type.lower() if os_type.startswith('linux'): info = platform.linux_distribution() - os_type, os_ver, _ = info + # On arch, platform.linux_distribution() is reportedly ('','',''), + # so handle it defensively + if info[0]: + os_type = info[0] + if info[1]: + os_ver = info[1] elif os_type.startswith('darwin'): os_ver = subprocess.Popen( ["sw_vers", "-productVersion"], From 9d30a85b298b61f33437dc19144c5f5fd5a74132 Mon Sep 17 00:00:00 2001 From: Chhatoi Pritam Baral Date: Wed, 11 Nov 2015 05:55:41 +0530 Subject: [PATCH 082/124] Add test for nginx not being installed --- .../letsencrypt_nginx/tests/configurator_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py index 7000f85dc..913c5de27 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py @@ -1,3 +1,4 @@ +# pylint: disable=too-many-public-methods """Test for letsencrypt_nginx.configurator.""" import os import shutil @@ -29,6 +30,12 @@ class NginxConfiguratorTest(util.NginxTest): shutil.rmtree(self.config_dir) shutil.rmtree(self.work_dir) + @mock.patch("letsencrypt_nginx.configurator.le_util.exe_exists") + def test_prepare_no_install(self, mock_exe_exists): + mock_exe_exists.return_value = False + self.assertRaises( + errors.NoInstallationError, self.config.prepare) + def test_prepare(self): self.assertEquals((1, 6, 2), self.config.version) self.assertEquals(5, len(self.config.parser.parsed)) From e64149cae8343efd377ffa256369c16b9152ac66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20L=C3=A9one?= Date: Wed, 11 Nov 2015 13:27:09 +0100 Subject: [PATCH 083/124] Redeclared names without usage --- acme/acme/jose/jwa.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme/acme/jose/jwa.py b/acme/acme/jose/jwa.py index 4ce5ca3f5..1853e0107 100644 --- a/acme/acme/jose/jwa.py +++ b/acme/acme/jose/jwa.py @@ -176,5 +176,5 @@ PS384 = JWASignature.register(_JWAPS('PS384', hashes.SHA384)) PS512 = JWASignature.register(_JWAPS('PS512', hashes.SHA512)) ES256 = JWASignature.register(_JWAES('ES256')) -ES256 = JWASignature.register(_JWAES('ES384')) -ES256 = JWASignature.register(_JWAES('ES512')) +ES384 = JWASignature.register(_JWAES('ES384')) +ES512 = JWASignature.register(_JWAES('ES512')) From f02dcbbc4cab8c925542b391dd2ce259dfa2f499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20L=C3=A9one?= Date: Wed, 11 Nov 2015 13:29:15 +0100 Subject: [PATCH 084/124] Variable key already existing --- letsencrypt-nginx/letsencrypt_nginx/dvsni.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-nginx/letsencrypt_nginx/dvsni.py b/letsencrypt-nginx/letsencrypt_nginx/dvsni.py index b388c0267..8fd705f08 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/dvsni.py +++ b/letsencrypt-nginx/letsencrypt_nginx/dvsni.py @@ -99,8 +99,8 @@ class NginxDvsni(common.TLSSNI01): for key, body in main: if key == ['http']: found_bucket = False - for key, _ in body: - if key == bucket_directive[0]: + for k, _ in body: + if k == bucket_directive[0]: found_bucket = True if not found_bucket: body.insert(0, bucket_directive) From 28ef1eae8da20be5d2beecbf16e2517e9c56c19f Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 11 Nov 2015 11:24:41 -0800 Subject: [PATCH 085/124] Highly experimental Amazon Linux bootstrapping --- bootstrap/_rpm_common.sh | 2 ++ letsencrypt-auto | 2 ++ 2 files changed, 4 insertions(+) diff --git a/bootstrap/_rpm_common.sh b/bootstrap/_rpm_common.sh index 26b91b8c4..9f670da6e 100755 --- a/bootstrap/_rpm_common.sh +++ b/bootstrap/_rpm_common.sh @@ -16,10 +16,12 @@ else fi # "git-core" seems to be an alias for "git" in CentOS 7 (yum search fails) +# Amazon Linux 2015.03 needs python27-virtualenv rather than python-virtualenv $tool install -y \ git-core \ python \ python-devel \ + python27-virtualenv \ python-virtualenv \ gcc \ dialog \ diff --git a/letsencrypt-auto b/letsencrypt-auto index b9f95ac14..efc29394b 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -121,6 +121,8 @@ then ExperimentalBootstrap "FreeBSD" freebsd.sh "$SUDO" elif uname | grep -iq Darwin ; then ExperimentalBootstrap "Mac OS X" mac.sh + elif grep -iq "Amazon Linux" /etc/issue ; then + ExperimentalBootstrap "Amazon Linux" amazon_linux.sh else echo "Sorry, I don't know how to bootstrap Let's Encrypt on your operating system!" echo From 5ce92402008f36ab0f5d54e1456a878a6486ee3f Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 11 Nov 2015 12:42:07 -0800 Subject: [PATCH 086/124] Improve comments for letsencrypt-auto --- letsencrypt-auto | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/letsencrypt-auto b/letsencrypt-auto index b9f95ac14..667dcecfd 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -8,11 +8,25 @@ # without requiring specific versions of its dependencies from the operating # system. +# Note: you can set XDG_DATA_HOME or VENV_PATH before running this script, +# if you want to change where the virtual environment will be installed XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN=${VENV_PATH}/bin +# This script takes the same arguments as the main letsencrypt program, but it +# additionally responds to --verbose (more output) and --debug (allow support +# for experimental platforms) +for arg in "$@" ; do + # This first clause is redundant with the third, but hedging on portability + if [ "$arg" = "-v" ] || [ "$arg" = "--verbose" ] || echo "$arg" | grep -E -- "-v+$" ; then + VERBOSE=1 + elif [ "$arg" = "--debug" ] ; then + DEBUG=1 + fi +done + if test "`id -u`" -ne "0" ; then if command -v sudo 1>/dev/null 2>&1; then SUDO=sudo @@ -44,15 +58,6 @@ else SUDO= fi -for arg in "$@" ; do - # This first clause is redundant with the third, but hedging on portability - if [ "$arg" = "-v" ] || [ "$arg" = "--verbose" ] || echo "$arg" | grep -E -- "-v+$" ; then - VERBOSE=1 - elif [ "$arg" = "--debug" ] ; then - DEBUG=1 - fi -done - ExperimentalBootstrap() { # Arguments: Platform name, boostrap script name, SUDO command (iff needed) if [ "$DEBUG" = 1 ] ; then From b26a87a33c4487c98f7cb8e6a5279feb4c79eb16 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 11 Nov 2015 12:57:32 -0800 Subject: [PATCH 087/124] Comments on SUDO --- letsencrypt-auto | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/letsencrypt-auto b/letsencrypt-auto index 667dcecfd..160de036a 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -27,11 +27,16 @@ for arg in "$@" ; do fi done +# letsencrypt-auto needs root access to bootstrap OS dependencies, and +# letsencrypt itself needs root access for almost all modes of operation +# The "normal" case is that sudo is used for the steps that need root, but +# this script *can* be run as root (not recommended), or fall back to using +# `su` if test "`id -u`" -ne "0" ; then if command -v sudo 1>/dev/null 2>&1; then SUDO=sudo else - # `sudo` command does not exist, use `su` instead. + echo \"sudo\" is not available, will use \"su\" for installation steps... # Because the parameters in `su -c` has to be a string, # we need properly escape it su_sudo() { From 79646dc42d5891ceec138e0819ac59c098a5c18f Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 11 Nov 2015 12:59:36 -0800 Subject: [PATCH 088/124] Fix misplaced verbosity from pip --- letsencrypt-auto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-auto b/letsencrypt-auto index b9f95ac14..6a6254125 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -158,7 +158,7 @@ else $VENV_BIN/pip install -U pip > /dev/null printf . # nginx is buggy / disabled for now... - $VENV_BIN/pip install -r py26reqs.txt + $VENV_BIN/pip install -r py26reqs.txt > /dev/null printf . $VENV_BIN/pip install -U letsencrypt > /dev/null printf . From d2dacef313f98285b6673090bcb883800caed590 Mon Sep 17 00:00:00 2001 From: Liam Marshall Date: Wed, 11 Nov 2015 17:46:39 -0600 Subject: [PATCH 089/124] Try reenabling container-based infrastructure Try explicitly pulling from backports Try travis's whitelisted Augeas PPA (not ours or backports) --- .travis.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 86a0d3e7d..96e28b1b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,12 @@ language: python services: - rabbitmq + - mariadb # http://docs.travis-ci.com/user/ci-environment/#CI-environment-OS # gimme has to be kept in sync with Boulder's Go version setting in .travis.yml before_install: - - sudo apt-get install -y mariadb-server mariadb-server-10.0 + - 'dpkg -s libaugeas0' - '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || eval "$(gimme 1.5.1)"' # using separate envs with different TOXENVs creates 4x1 Travis build @@ -31,9 +32,8 @@ branches: - master - /^test-.*$/ -# enable Trusty beta on travis -sudo: required -dist: trusty +# container-based infrastructure +sudo: false addons: # make sure simplehttp simple verification works (custom /etc/hosts) @@ -41,6 +41,8 @@ addons: - le.wtf mariadb: "10.0" apt: + sources: + - augeas packages: # keep in sync with bootstrap/ubuntu.sh and Boulder - python - python-dev From a0129f7818a0c6922b3db9fb29d19b8ee2097b92 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Thu, 12 Nov 2015 15:02:30 +0200 Subject: [PATCH 090/124] Fix test broken by #1454 if nginx installation not present in system --- letsencrypt/tests/cli_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 31f528cbf..baa2a6e78 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -121,7 +121,9 @@ class CLITest(unittest.TestCase): '--key-path', 'key', '--chain-path', 'chain']) self.assertEqual(mock_display_ops.pick_installer.call_count, 1) - def test_configurator_selection(self): + @mock.patch('letsencrypt.le_util.exe_exists') + def test_configurator_selection(self, mock_exe_exists): + mock_exe_exists.return_value = True real_plugins = disco.PluginsRegistry.find_all() args = ['--agree-dev-preview', '--apache', '--authenticator', 'standalone'] From 14e03f9af0e8b4cc3c22d7b80c79e2fb04fe7b81 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Thu, 12 Nov 2015 16:34:15 +0200 Subject: [PATCH 091/124] Parse possible multiple domain definitions to a list --- letsencrypt/cli.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 999555741..141309174 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -668,8 +668,32 @@ class HelpfulArgumentParser(object): parsed_args = self.parser.parse_args(self.args) parsed_args.func = self.VERBS[self.verb] + parsed_args.domains = self._parse_domains(parsed_args.domains) return parsed_args + def _parse_domains(self, domains): + """Helper function for parse_args() that parses domains from a + (possibly) comma separated list and returns list of unique domains. + + :param domains: List of domain flags + :type domains: `list` of `string` + + :returns: List of unique domains + :rtype: `list` of `string` + + """ + + uniqd = None + + if domains: + dlist = [] + for domain in domains: + dlist.extend([d.strip() for d in domain.split(",")]) + # Make sure we don't have duplicates + uniqd = [d for i,d in enumerate(dlist) if d not in dlist[:i]] + + return uniqd + def determine_verb(self): """Determines the verb/subcommand provided by the user. From 3b8d6ec58b0b355a9c83439626cba5a60d416a19 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Thu, 12 Nov 2015 16:42:58 +0200 Subject: [PATCH 092/124] Modified -d help text to reflect the change --- letsencrypt/cli.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 141309174..bd51e4a0f 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -837,8 +837,9 @@ def prepare_and_parse_args(plugins, args): # subparser.add_argument("domains", nargs="*", metavar="domain") helpful.add(None, "-d", "--domain", dest="domains", metavar="DOMAIN", action="append", - help="Domain names to apply. Use multiple -d flags if you want " - "to specify multiple domains") + help="Domain names to apply. For multiple domains you can use " + "multiple -d flags or enter a comma separated list of domains" + "as a parameter.") helpful.add( None, "--duplicate", dest="duplicate", action="store_true", help="Allow getting a certificate that duplicates an existing one") From 56f21e1d35f82750b1cc8115a28182020c673acc Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Thu, 12 Nov 2015 16:53:40 +0200 Subject: [PATCH 093/124] Refactor --domain flag back to -- domains --- letsencrypt-nginx/tests/boulder-integration.sh | 2 +- letsencrypt/cli.py | 12 ++++++------ letsencrypt/tests/cli_test.py | 4 ++-- tests/boulder-integration.sh | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/letsencrypt-nginx/tests/boulder-integration.sh b/letsencrypt-nginx/tests/boulder-integration.sh index 0e3e7e77a..3cbe9f6b9 100755 --- a/letsencrypt-nginx/tests/boulder-integration.sh +++ b/letsencrypt-nginx/tests/boulder-integration.sh @@ -18,7 +18,7 @@ letsencrypt_test_nginx () { "$@" } -letsencrypt_test_nginx --domain nginx.wtf run +letsencrypt_test_nginx --domains nginx.wtf run echo | openssl s_client -connect localhost:5001 \ | openssl x509 -out $root/nginx.pem diff -q $root/nginx.pem $root/conf/live/nginx.wtf/cert.pem diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index bd51e4a0f..f01bf0b96 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -106,7 +106,7 @@ def _find_domains(args, installer): domains = args.domains if not domains: - raise errors.Error("Please specify --domain, or --installer that " + raise errors.Error("Please specify --domains, or --installer that " "will help in domain names autodiscovery") return domains @@ -465,9 +465,9 @@ def obtaincert(args, config, plugins): """Authenticate & obtain cert, but do not install it.""" if args.domains is not None and args.csr is not None: - # TODO: --csr could have a priority, when --domain is + # TODO: --csr could have a priority, when --domains is # supplied, check if CSR matches given domains? - return "--domain and --csr are mutually exclusive" + return "--domains and --csr are mutually exclusive" try: # installers are used in auth mode to determine domain names @@ -831,11 +831,11 @@ def prepare_and_parse_args(plugins, args): None, "-t", "--text", dest="text_mode", action="store_true", help="Use the text output instead of the curses UI.") helpful.add(None, "-m", "--email", help=config_help("email")) - # positional arg shadows --domain, instead of appending, and - # --domain is useful, because it can be stored in config + # positional arg shadows --domains, instead of appending, and + # --domains is useful, because it can be stored in config #for subparser in parser_run, parser_auth, parser_install: # subparser.add_argument("domains", nargs="*", metavar="domain") - helpful.add(None, "-d", "--domain", dest="domains", + helpful.add(None, "-d", "--domains", dest="domains", metavar="DOMAIN", action="append", help="Domain names to apply. For multiple domains you can use " "multiple -d flags or enter a comma separated list of domains" diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 31f528cbf..83f78e9ab 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -117,7 +117,7 @@ class CLITest(unittest.TestCase): @mock.patch('letsencrypt.cli.display_ops') def test_installer_selection(self, mock_display_ops): - self._call(['install', '--domain', 'foo.bar', '--cert-path', 'cert', + self._call(['install', '--domains', 'foo.bar', '--cert-path', 'cert', '--key-path', 'key', '--chain-path', 'chain']) self.assertEqual(mock_display_ops.pick_installer.call_count, 1) @@ -170,7 +170,7 @@ class CLITest(unittest.TestCase): def test_certonly_bad_args(self): ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR]) - self.assertEqual(ret, '--domain and --csr are mutually exclusive') + self.assertEqual(ret, '--domains and --csr are mutually exclusive') ret, _, _, _ = self._call(['-a', 'bad_auth', 'certonly']) self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed') diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 97babb591..53996cd20 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -27,8 +27,8 @@ common() { "$@" } -common --domain le1.wtf --standalone-supported-challenges tls-sni-01 auth -common --domain le2.wtf --standalone-supported-challenges http-01 run +common --domains le1.wtf --standalone-supported-challenges tls-sni-01 auth +common --domains le2.wtf --standalone-supported-challenges http-01 run common -a manual -d le.wtf auth export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \ @@ -40,7 +40,7 @@ common auth --csr "$CSR_PATH" \ openssl x509 -in "${root}/csr/0000_cert.pem" -text openssl x509 -in "${root}/csr/0000_chain.pem" -text -common --domain le3.wtf install \ +common --domains le3.wtf install \ --cert-path "${root}/csr/cert.pem" \ --key-path "${root}/csr/key.pem" From 37e94e631d428131131243460bce417531e2a62c Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Thu, 12 Nov 2015 17:25:38 +0200 Subject: [PATCH 094/124] Added tests --- letsencrypt/tests/cli_test.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 83f78e9ab..57807555d 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -193,6 +193,27 @@ class CLITest(unittest.TestCase): self._call, ['-d', '*.wildcard.tld']) + def test_parse_domains(self): + from letsencrypt import cli + plugins = disco.PluginsRegistry.find_all() + + short_args = ['-d', 'example.com'] + namespace = cli.prepare_and_parse_args(plugins, short_args) + self.assertEqual(namespace.domains, ['example.com']) + + short_args = ['-d', 'example.com,another.net,third.org,example.com'] + namespace = cli.prepare_and_parse_args(plugins, short_args) + self.assertEqual(namespace.domains, ['example.com', 'another.net', + 'third.org']) + + long_args = ['--domains', 'example.com'] + namespace = cli.prepare_and_parse_args(plugins, long_args) + self.assertEqual(namespace.domains, ['example.com']) + + long_args = ['--domains', 'example.com,another.net,example.com'] + namespace = cli.prepare_and_parse_args(plugins, long_args) + self.assertEqual(namespace.domains, ['example.com', 'another.net']) + @mock.patch('letsencrypt.crypto_util.notAfter') @mock.patch('letsencrypt.cli.zope.component.getUtility') def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter): From 0ccbbdb120de6ce414da6a4951b009620208dbb0 Mon Sep 17 00:00:00 2001 From: Joona Hoikkala Date: Thu, 12 Nov 2015 17:46:44 +0200 Subject: [PATCH 095/124] Style fix --- letsencrypt/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index f01bf0b96..7848c4afc 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -690,7 +690,7 @@ class HelpfulArgumentParser(object): for domain in domains: dlist.extend([d.strip() for d in domain.split(",")]) # Make sure we don't have duplicates - uniqd = [d for i,d in enumerate(dlist) if d not in dlist[:i]] + uniqd = [d for i, d in enumerate(dlist) if d not in dlist[:i]] return uniqd From 4590adf545b00daaf78df060f924f7059038d771 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 12 Nov 2015 13:23:01 -0800 Subject: [PATCH 096/124] Add *args and **kwargs to ErrorHandler --- letsencrypt/error_handler.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/letsencrypt/error_handler.py b/letsencrypt/error_handler.py index 8b0eb7c8b..431e677a1 100644 --- a/letsencrypt/error_handler.py +++ b/letsencrypt/error_handler.py @@ -1,4 +1,5 @@ """Registers functions to be called if an exception or signal occurs.""" +import functools import logging import os import signal @@ -40,11 +41,11 @@ class ErrorHandler(object): to be called again by the next signal handler. """ - def __init__(self, func=None): + def __init__(self, func=None, *args, **kwargs): self.funcs = [] self.prev_handlers = {} if func is not None: - self.register(func) + self.register(func, *args, **kwargs) def __enter__(self): self.set_signal_handlers() @@ -57,9 +58,13 @@ class ErrorHandler(object): self.call_registered() self.reset_signal_handlers() - def register(self, func): - """Registers func to be called if an error occurs.""" - self.funcs.append(func) + def register(self, func, *args, **kwargs): + """Sets func to be called with *args and **kwargs during cleanup + + :param function func: function to be called in case of an error + + """ + self.funcs.append(functools.partial(func, *args, **kwargs)) def call_registered(self): """Calls all registered functions""" From 2d827464f47a35e25b307588608e2b5db7d94210 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 12 Nov 2015 13:35:30 -0800 Subject: [PATCH 097/124] Updated error_handler tests --- letsencrypt/tests/error_handler_test.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/letsencrypt/tests/error_handler_test.py b/letsencrypt/tests/error_handler_test.py index c92f12435..bf1ffbdad 100644 --- a/letsencrypt/tests/error_handler_test.py +++ b/letsencrypt/tests/error_handler_test.py @@ -13,7 +13,11 @@ class ErrorHandlerTest(unittest.TestCase): from letsencrypt import error_handler self.init_func = mock.MagicMock() - self.handler = error_handler.ErrorHandler(self.init_func) + self.init_args = {42} + self.init_kwargs = {'foo': 'bar'} + self.handler = error_handler.ErrorHandler(self.init_func, + *self.init_args, + **self.init_kwargs) # pylint: disable=protected-access self.signals = error_handler._SIGNALS @@ -23,7 +27,8 @@ class ErrorHandlerTest(unittest.TestCase): raise ValueError except ValueError: pass - self.init_func.assert_called_once_with() + self.init_func.assert_called_once_with(*self.init_args, + **self.init_kwargs) @mock.patch('letsencrypt.error_handler.os') @mock.patch('letsencrypt.error_handler.signal') @@ -37,7 +42,8 @@ class ErrorHandlerTest(unittest.TestCase): signum = self.signals[0] signal_handler(signum, None) - self.init_func.assert_called_once_with() + self.init_func.assert_called_once_with(*self.init_args, + **self.init_kwargs) mock_os.kill.assert_called_once_with(mock_os.getpid(), signum) self.handler.reset_signal_handlers() @@ -48,7 +54,8 @@ class ErrorHandlerTest(unittest.TestCase): bad_func = mock.MagicMock(side_effect=[ValueError]) self.handler.register(bad_func) self.handler.call_registered() - self.init_func.assert_called_once_with() + self.init_func.assert_called_once_with(*self.init_args, + **self.init_kwargs) bad_func.assert_called_once_with() def test_sysexit_ignored(self): From 13a4987f694740884aa6dc9bbbbf5d128c6dc146 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 12 Nov 2015 13:58:55 -0800 Subject: [PATCH 098/124] Incorporated pde's and joohi's feedback --- letsencrypt/client.py | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 9123eaf95..0f0ecdc6d 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -1,5 +1,4 @@ """Let's Encrypt client API.""" -import functools import logging import os @@ -120,12 +119,6 @@ class Client(object): """ - # Message to show if enabling a redirect fails, but recovery is successful - _SUCCESSFUL_REDIRECT_RECOVERY_MSG = ( - "We were unable to setup a redirect for your server, however, we " - "successfully installed your certificate. If you'd like to revert " - "these changes as well, run 'letsencrypt rollback'.") - def __init__(self, config, account_, dv_auth, installer, acme=None): """Initialize a client.""" self.config = config @@ -339,13 +332,11 @@ class Client(object): self.installer.save("Deployed Let's Encrypt Certificate") - # sites may have been enabled / final cleanup - restart_error_handler = error_handler.ErrorHandler(functools.partial( - self._rollback_and_restart, - "We were unable to install your certificate, however, we " - "successfully rolled back your server to its previous " - "configuration.")) - with restart_error_handler: + msg = ("We were unable to install your certificate, " + "however, we successfully restored your " + "server to its prior configuration.") + with error_handler.ErrorHandler(self._rollback_and_restart, msg): + # sites may have been enabled / final cleanup self.installer.restart() def enhance_config(self, domains, redirect=None): @@ -384,10 +375,9 @@ class Client(object): :type vhost: :class:`letsencrypt.interfaces.IInstaller` """ - enhance_error_handler = error_handler.ErrorHandler( - functools.partial(self._recovery_routine_with_msg, - self._SUCCESSFUL_REDIRECT_RECOVERY_MSG)) - with enhance_error_handler: + msg = ("We were unable to set up a redirect for your server, " + "however, we successfully installed your certificate.") + with error_handler.ErrorHandler(self._recovery_routine_with_msg, msg): for dom in domains: try: self.installer.enhance(dom, "redirect") @@ -397,10 +387,7 @@ class Client(object): self.installer.save("Add Redirects") - restart_error_handler = error_handler.ErrorHandler( - functools.partial(self._rollback_and_restart, - self._SUCCESSFUL_REDIRECT_RECOVERY_MSG)) - with restart_error_handler: + with error_handler.ErrorHandler(self._rollback_and_restart, msg): self.installer.restart() def _recovery_routine_with_msg(self, success_msg): @@ -427,7 +414,7 @@ class Client(object): except: # TODO: suggest letshelp-letsencypt here reporter.add_message( - "An error occured and we failed to rollback your config and " + "An error occured and we failed to restore your config and " "restart your server. Please submit a bug report to " "https://github.com/letsencrypt/letsencrypt", reporter.HIGH_PRIORITY) From 4158e3b559d34e0471a5b9e691bcb00b738307d6 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 12 Nov 2015 14:22:36 -0800 Subject: [PATCH 099/124] Revert "Removing stray debug statements, fixes #1308" This reverts commit b11e556ae7ba931a9593b0a82623c662eae8c737. --- letsencrypt/cli.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 999555741..7210709c9 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -549,6 +549,7 @@ def plugins_cmd(args, config, plugins): # TODO: Use IDisplay rather than print logger.debug("Filtered plugins: %r", filtered) if not args.init and not args.prepare: + print str(filtered) return filtered.init(config) @@ -556,11 +557,13 @@ def plugins_cmd(args, config, plugins): # TODO: Use IDisplay rather than print logger.debug("Verified plugins: %r", verified) if not args.prepare: + print str(verified) return verified.prepare() available = verified.available() logger.debug("Prepared plugins: %s", available) + print str(available) def read_file(filename, mode="rb"): @@ -936,6 +939,7 @@ def _paths_parser(helpful): section = "paths" if verb in ("install", "revoke"): section = verb + print helpful.help_arg, helpful.help_arg == "install" # revoke --key-path reads a file, install --key-path takes a string add(section, "--key-path", type=((verb == "revoke" and read_file) or str), required=(verb == "install"), From 3ef0ed11916dac59398c1223b6eeec80bf34c184 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 12 Nov 2015 14:24:31 -0800 Subject: [PATCH 100/124] Removed debugging statement --- letsencrypt/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 7210709c9..4c798ee5c 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -939,7 +939,6 @@ def _paths_parser(helpful): section = "paths" if verb in ("install", "revoke"): section = verb - print helpful.help_arg, helpful.help_arg == "install" # revoke --key-path reads a file, install --key-path takes a string add(section, "--key-path", type=((verb == "revoke" and read_file) or str), required=(verb == "install"), From a31be6d5679a5387b1a0ebf2c49904ab8e2e9252 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 12 Nov 2015 15:04:41 -0800 Subject: [PATCH 101/124] Added plugin_cmd tests --- letsencrypt/tests/cli_test.py | 42 +++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 31f528cbf..509b7eb34 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -168,6 +168,48 @@ class CLITest(unittest.TestCase): for r in xrange(len(flags)))): self._call(['plugins'] + list(args)) + @mock.patch('letsencrypt.cli.plugins_disco') + def test_plugins_no_args(self, mock_disco): + ifaces = [] + plugins = mock_disco.PluginsRegistry.find_all() + + _, stdout, _, _ = self._call(['plugins']) + plugins.visible.assert_called_once_with() + plugins.visible().ifaces.assert_called_once_with(ifaces) + filtered = plugins.visible().ifaces() + stdout.write.called_once_with(str(filtered)) + + @mock.patch('letsencrypt.cli.plugins_disco') + def test_plugins_init(self, mock_disco): + ifaces = [] + plugins = mock_disco.PluginsRegistry.find_all() + + _, stdout, _, _ = self._call(['plugins', '--init']) + plugins.visible.assert_called_once_with() + plugins.visible().ifaces.assert_called_once_with(ifaces) + filtered = plugins.visible().ifaces() + self.assertEqual(filtered.init.call_count, 1) + filtered.verify.assert_called_once_with(ifaces) + verified = filtered.verify() + stdout.write.called_once_with(str(verified)) + + @mock.patch('letsencrypt.cli.plugins_disco') + def test_plugins_prepare(self, mock_disco): + ifaces = [] + plugins = mock_disco.PluginsRegistry.find_all() + + _, stdout, _, _ = self._call(['plugins', '--init', '--prepare']) + plugins.visible.assert_called_once_with() + plugins.visible().ifaces.assert_called_once_with(ifaces) + filtered = plugins.visible().ifaces() + self.assertEqual(filtered.init.call_count, 1) + filtered.verify.assert_called_once_with(ifaces) + verified = filtered.verify() + verified.prepare.assert_called_once_with() + verified.available.assert_called_once_with() + available = verified.available() + stdout.write.called_once_with(str(available)) + def test_certonly_bad_args(self): ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR]) self.assertEqual(ret, '--domain and --csr are mutually exclusive') From 08d7c06a5484e8d23391e6c8c4a9cc9cf8857d39 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 12 Nov 2015 15:19:50 -0800 Subject: [PATCH 102/124] Python2.6: It's called a set --- letsencrypt/tests/error_handler_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/tests/error_handler_test.py b/letsencrypt/tests/error_handler_test.py index bf1ffbdad..7fbdcffd8 100644 --- a/letsencrypt/tests/error_handler_test.py +++ b/letsencrypt/tests/error_handler_test.py @@ -13,7 +13,7 @@ class ErrorHandlerTest(unittest.TestCase): from letsencrypt import error_handler self.init_func = mock.MagicMock() - self.init_args = {42} + self.init_args = set((42,)) self.init_kwargs = {'foo': 'bar'} self.handler = error_handler.ErrorHandler(self.init_func, *self.init_args, From 211ca2420f454cea580f11d90c58c51818c5d274 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 12 Nov 2015 17:19:26 -0800 Subject: [PATCH 103/124] Make read_file use abspath --- letsencrypt/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 4c798ee5c..7411ada63 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -569,7 +569,7 @@ def plugins_cmd(args, config, plugins): # TODO: Use IDisplay rather than print def read_file(filename, mode="rb"): """Returns the given file's contents. - :param str filename: Filename + :param str filename: filename as an absolute path :param str mode: open mode (see `open`) :returns: A tuple of filename and its contents @@ -579,6 +579,7 @@ def read_file(filename, mode="rb"): """ try: + filename = os.path.abspath(filename) return filename, open(filename, mode).read() except IOError as exc: raise argparse.ArgumentTypeError(exc.strerror) From 2a820a391fef8e3c2a12afbd87cbc16e8bfa0399 Mon Sep 17 00:00:00 2001 From: Liam Marshall Date: Wed, 11 Nov 2015 17:46:39 -0600 Subject: [PATCH 104/124] Try reenabling container-based infrastructure Try explicitly pulling from backports Try travis's whitelisted Augeas PPA (not ours or backports) --- .travis.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 86a0d3e7d..96e28b1b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,12 @@ language: python services: - rabbitmq + - mariadb # http://docs.travis-ci.com/user/ci-environment/#CI-environment-OS # gimme has to be kept in sync with Boulder's Go version setting in .travis.yml before_install: - - sudo apt-get install -y mariadb-server mariadb-server-10.0 + - 'dpkg -s libaugeas0' - '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || eval "$(gimme 1.5.1)"' # using separate envs with different TOXENVs creates 4x1 Travis build @@ -31,9 +32,8 @@ branches: - master - /^test-.*$/ -# enable Trusty beta on travis -sudo: required -dist: trusty +# container-based infrastructure +sudo: false addons: # make sure simplehttp simple verification works (custom /etc/hosts) @@ -41,6 +41,8 @@ addons: - le.wtf mariadb: "10.0" apt: + sources: + - augeas packages: # keep in sync with bootstrap/ubuntu.sh and Boulder - python - python-dev From 707df3d2c61179fe89b99d03deb142e316befa92 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 12 Nov 2015 17:27:56 -0800 Subject: [PATCH 105/124] Add read_file tests --- letsencrypt/tests/cli_test.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 509b7eb34..008d239d3 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -1,4 +1,5 @@ """Tests for letsencrypt.cli.""" +import argparse import itertools import os import shutil @@ -356,6 +357,20 @@ class CLITest(unittest.TestCase): mock_sys.exit.assert_called_with(''.join( traceback.format_exception_only(KeyboardInterrupt, interrupt))) + def test_read_file(self): + from letsencrypt import cli + rel_test_path = os.path.relpath(os.path.join(self.tmp_dir, 'foo')) + self.assertRaises( + argparse.ArgumentTypeError, cli.read_file, rel_test_path) + + test_contents = 'bar\n' + with open(rel_test_path, 'w') as f: + f.write(test_contents) + + path, contents = cli.read_file(rel_test_path) + self.assertEqual(path, os.path.abspath(path)) + self.assertEqual(contents, test_contents) + class DetermineAccountTest(unittest.TestCase): """Tests for letsencrypt.cli._determine_account.""" From 1bfd44ea34450a08939aef2f44de8f11de1ea935 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 12 Nov 2015 17:31:32 -0800 Subject: [PATCH 106/124] Address review comments: - make --user-agent a general CLI option - get_os_info return type --- letsencrypt/cli.py | 2 +- letsencrypt/le_util.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 957ada7af..df4b45c1a 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -893,7 +893,7 @@ def _create_subparsers(helpful): helpful.add_group("rollback", description="Options for reverting config changes") helpful.add_group("plugins", description="Plugin options") helpful.add( - "certonly", "--user-agent", type=str, default=None, + None, "--user-agent", type=str, default=None, help="Set a custom user agent string for the client. User agent strings allow " "the CA to collect high level statistics about success rates by OS and " "plugin. If you wish to hide your server OS version from the Let's " diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index a0540a723..25260d755 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -206,7 +206,9 @@ def safely_remove(path): def get_os_info(): """ Get Operating System type/distribution and major version - :returns: (`str` os_name, `str` os_version) + + :returns: (os_name, os_version) + :rtype: `tuple` of `str` """ info = platform.system_alias( platform.system(), From dc78f811ce18ca1414e202c2e2a24b2a769e45ed Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 12 Nov 2015 17:33:40 -0800 Subject: [PATCH 107/124] Make cert_path,key_path,chain_path,fullchain_path absolute --- letsencrypt/cli.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 7411ada63..b6181bb64 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -931,26 +931,28 @@ def _paths_parser(helpful): if verb in ("install", "revoke", "certonly"): section = verb if verb == "certonly": - add(section, "--cert-path", default=flag_default("auth_cert_path"), help=cph) + add(section, "--cert-path", type=os.path.abspath, + default=flag_default("auth_cert_path"), help=cph) elif verb == "revoke": add(section, "--cert-path", type=read_file, required=True, help=cph) else: - add(section, "--cert-path", help=cph, required=(verb == "install")) + add(section, "--cert-path", type=os.path.abspath, + help=cph, required=(verb == "install")) section = "paths" if verb in ("install", "revoke"): section = verb # revoke --key-path reads a file, install --key-path takes a string - add(section, "--key-path", type=((verb == "revoke" and read_file) or str), - required=(verb == "install"), + add(section, "--key-path", required=(verb == "install"), + type=((verb == "revoke" and read_file) or os.path.abspath), help="Path to private key for cert creation or revocation (if account key is missing)") default_cp = None if verb == "certonly": default_cp = flag_default("auth_chain_path") - add("paths", "--fullchain-path", default=default_cp, + add("paths", "--fullchain-path", default=default_cp, type=os.path.abspath, help="Accompanying path to a full certificate chain (cert plus chain).") - add("paths", "--chain-path", default=default_cp, + add("paths", "--chain-path", default=default_cp, type=os.path.abspath, help="Accompanying path to a certificate chain.") add("paths", "--config-dir", default=flag_default("config_dir"), help=config_help("config_dir")) From 1dd1523680cadc4d8b8fd59cbf31fdc9c37fcfd0 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 12 Nov 2015 17:47:36 -0800 Subject: [PATCH 108/124] Added cert, key, chain, and fullchain abspath tests --- letsencrypt/tests/cli_test.py | 36 ++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 008d239d3..2f729f71d 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -23,7 +23,7 @@ from letsencrypt.tests import test_util CSR = test_util.vector_path('csr.der') -class CLITest(unittest.TestCase): +class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods """Tests for different commands.""" def setUp(self): @@ -116,6 +116,23 @@ class CLITest(unittest.TestCase): from letsencrypt import cli self.assertTrue(cli.usage_strings(plugins)[0] in out) + def test_install_abspath(self): + cert = 'cert' + key = 'key' + chain = 'chain' + fullchain = 'fullchain' + + with MockedVerb('install') as mock_install: + self._call(['install', '--cert-path', cert, '--key-path', 'key', + '--chain-path', 'chain', + '--fullchain-path', 'fullchain']) + + args = mock_install.call_args[0][0] + self.assertEqual(args.cert_path, os.path.abspath(cert)) + self.assertEqual(args.key_path, os.path.abspath(key)) + self.assertEqual(args.chain_path, os.path.abspath(chain)) + self.assertEqual(args.fullchain_path, os.path.abspath(fullchain)) + @mock.patch('letsencrypt.cli.display_ops') def test_installer_selection(self, mock_display_ops): self._call(['install', '--domain', 'foo.bar', '--cert-path', 'cert', @@ -211,6 +228,23 @@ class CLITest(unittest.TestCase): available = verified.available() stdout.write.called_once_with(str(available)) + def test_certonly_abspath(self): + cert = 'cert' + key = 'key' + chain = 'chain' + fullchain = 'fullchain' + + with MockedVerb('certonly') as mock_obtaincert: + self._call(['certonly', '--cert-path', cert, '--key-path', 'key', + '--chain-path', 'chain', + '--fullchain-path', 'fullchain']) + + args = mock_obtaincert.call_args[0][0] + self.assertEqual(args.cert_path, os.path.abspath(cert)) + self.assertEqual(args.key_path, os.path.abspath(key)) + self.assertEqual(args.chain_path, os.path.abspath(chain)) + self.assertEqual(args.fullchain_path, os.path.abspath(fullchain)) + def test_certonly_bad_args(self): ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR]) self.assertEqual(ret, '--domain and --csr are mutually exclusive') From 778b8797bb3c215a3f0645d7cd11b375ad974054 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 12 Nov 2015 18:17:10 -0800 Subject: [PATCH 109/124] Ensure config_dir, work_dir, and logs_dir have absolute paths --- letsencrypt/configuration.py | 5 +++++ letsencrypt/tests/renewer_test.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index e70171675..4955655f3 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -37,6 +37,11 @@ class NamespaceConfig(object): def __init__(self, namespace): self.namespace = namespace + + self.namespace.config_dir = os.path.abspath(self.namespace.config_dir) + self.namespace.work_dir = os.path.abspath(self.namespace.work_dir) + self.namespace.logs_dir = os.path.abspath(self.namespace.logs_dir) + # Check command line parameters sanity, and error out in case of problem. check_config_sanity(self) diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index e76b6eb88..daec9678f 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -692,6 +692,9 @@ class RenewableCertTests(BaseRenewableCertTest): self.test_rc.configfile["renewalparams"]["http01_port"] = "1234" self.test_rc.configfile["renewalparams"]["account"] = "abcde" self.test_rc.configfile["renewalparams"]["domains"] = ["example.com"] + self.test_rc.configfile["renewalparams"]["config_dir"] = "config" + self.test_rc.configfile["renewalparams"]["work_dir"] = "work" + self.test_rc.configfile["renewalparams"]["logs_dir"] = "logs" mock_auth = mock.MagicMock() mock_pd.PluginsRegistry.find_all.return_value = {"apache": mock_auth} # Fails because "fake" != "apache" From b408ec765dd6a0c217783c5f22d41f46efcf730c Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 12 Nov 2015 18:20:00 -0800 Subject: [PATCH 110/124] Test absolute paths are used --- letsencrypt/tests/configuration_test.py | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index 3a8bf40cf..be48a7c9c 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -59,6 +59,36 @@ class NamespaceConfigTest(unittest.TestCase): self.namespace.http01_port = None self.assertEqual(80, self.config.http01_port) + def test_absolute_paths(self): + from letsencrypt.configuration import NamespaceConfig + + config_base = "foo" + work_base = "bar" + logs_base = "baz" + + config = NamespaceConfig(mock.MagicMock( + config_dir=config_base, work_dir=work_base, logs_dir=logs_base)) + + self.assertTrue(os.path.isabs(config.config_dir)) + self.assertEqual(config.config_dir, + os.path.join(os.getcwd(), config_base)) + self.assertTrue(os.path.isabs(config.work_dir)) + self.assertEqual(config.work_dir, + os.path.join(os.getcwd(), work_base)) + self.assertTrue(os.path.isabs(config.logs_dir)) + self.assertEqual(config.logs_dir, + os.path.join(os.getcwd(), logs_base)) + self.assertTrue(os.path.isabs(config.cert_path)) + self.assertTrue(os.path.isabs(config.key_path)) + self.assertTrue(os.path.isabs(config.chain_path)) + self.assertTrue(os.path.isabs(config.fullchain_path)) + self.assertTrue(os.path.isabs(config.accounts_dir)) + self.assertTrue(os.path.isabs(config.backup_dir)) + self.assertTrue(os.path.isabs(config.csr_dir)) + self.assertTrue(os.path.isabs(config.in_progress_dir)) + self.assertTrue(os.path.isabs(config.key_dir)) + self.assertTrue(os.path.isabs(config.temp_checkpoint_dir)) + class RenewerConfigurationTest(unittest.TestCase): """Test for letsencrypt.configuration.RenewerConfiguration.""" From 82614df6f0bdd7bf20ea3554780e880f84e80db3 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 12 Nov 2015 18:30:37 -0800 Subject: [PATCH 111/124] What do YOU think os.path.isabs(mock.MagicMock()) returns? --- letsencrypt/tests/configuration_test.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index be48a7c9c..16a4da6e1 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -66,8 +66,14 @@ class NamespaceConfigTest(unittest.TestCase): work_base = "bar" logs_base = "baz" - config = NamespaceConfig(mock.MagicMock( - config_dir=config_base, work_dir=work_base, logs_dir=logs_base)) + mock_namespace = mock.MagicMock(spec=['config_dir', 'work_dir', + 'logs_dir', 'http01_port', + 'tls_sni_01_port', + 'domains', 'server']) + mock_namespace.config_dir = config_base + mock_namespace.work_dir = work_base + mock_namespace.logs_dir = logs_base + config = NamespaceConfig(mock_namespace) self.assertTrue(os.path.isabs(config.config_dir)) self.assertEqual(config.config_dir, @@ -78,10 +84,6 @@ class NamespaceConfigTest(unittest.TestCase): self.assertTrue(os.path.isabs(config.logs_dir)) self.assertEqual(config.logs_dir, os.path.join(os.getcwd(), logs_base)) - self.assertTrue(os.path.isabs(config.cert_path)) - self.assertTrue(os.path.isabs(config.key_path)) - self.assertTrue(os.path.isabs(config.chain_path)) - self.assertTrue(os.path.isabs(config.fullchain_path)) self.assertTrue(os.path.isabs(config.accounts_dir)) self.assertTrue(os.path.isabs(config.backup_dir)) self.assertTrue(os.path.isabs(config.csr_dir)) From 31b984adc520eab2066b41360a9b6da58efe0c25 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 12 Nov 2015 18:34:46 -0800 Subject: [PATCH 112/124] Test RenwerConfiguration --- letsencrypt/tests/configuration_test.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index 16a4da6e1..c42b99081 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -113,6 +113,28 @@ class RenewerConfigurationTest(unittest.TestCase): self.config.renewal_configs_dir, '/tmp/config/renewal_configs') self.assertEqual(self.config.renewer_config_file, '/tmp/config/r.conf') + def test_absolute_paths(self): + from letsencrypt.configuration import NamespaceConfig + from letsencrypt.configuration import RenewerConfiguration + + config_base = "foo" + work_base = "bar" + logs_base = "baz" + + mock_namespace = mock.MagicMock(spec=['config_dir', 'work_dir', + 'logs_dir', 'http01_port', + 'tls_sni_01_port', + 'domains', 'server']) + mock_namespace.config_dir = config_base + mock_namespace.work_dir = work_base + mock_namespace.logs_dir = logs_base + config = RenewerConfiguration(NamespaceConfig(mock_namespace)) + + self.assertTrue(os.path.isabs(config.archive_dir)) + self.assertTrue(os.path.isabs(config.live_dir)) + self.assertTrue(os.path.isabs(config.renewal_configs_dir)) + self.assertTrue(os.path.isabs(config.renewer_config_file)) + if __name__ == '__main__': unittest.main() # pragma: no cover From 3687197db8e2d63817e4525c891aa79ca40d11f2 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Thu, 12 Nov 2015 19:52:32 -0800 Subject: [PATCH 113/124] Move user agent selection into client.py Along the way refactoring revocation and recording of chosen plugins. --- letsencrypt/cli.py | 38 ++++++++++++++++++++--------------- letsencrypt/client.py | 37 +++++++++++++++++++++++----------- letsencrypt/configuration.py | 17 ---------------- letsencrypt/tests/cli_test.py | 6 ++++-- 4 files changed, 51 insertions(+), 47 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index df4b45c1a..38f7ee45d 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -17,7 +17,6 @@ import zope.component import zope.interface.exceptions import zope.interface.verify -from acme import client as acme_client from acme import jose import letsencrypt @@ -39,7 +38,6 @@ from letsencrypt.display import util as display_util from letsencrypt.display import ops as display_ops from letsencrypt.plugins import disco as plugins_disco - logger = logging.getLogger(__name__) @@ -169,7 +167,6 @@ def _determine_account(args, config): def _init_le_client(args, config, authenticator, installer): - config.determine_user_agent(authenticator, installer) if authenticator is not None: # if authenticator was given, then we will need account... acc, acme = _determine_account(args, config) @@ -305,7 +302,7 @@ def _report_new_cert(cert_path, fullchain_path): reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY) -def _auth_from_domains(le_client, config, domains, plugins): +def _auth_from_domains(le_client, config, domains): """Authenticate and enroll certificate.""" # Note: This can raise errors... caught above us though. lineage = _treat_as_renewal(config, domains) @@ -326,7 +323,7 @@ def _auth_from_domains(le_client, config, domains, plugins): # configuration values from this attempt? <- Absolutely (jdkasten) else: # TREAT AS NEW REQUEST - lineage = le_client.obtain_and_enroll_certificate(domains, plugins) + lineage = le_client.obtain_and_enroll_certificate(domains) if not lineage: raise errors.Error("Certificate could not be obtained") @@ -426,14 +423,23 @@ def choose_configurator_plugins(args, config, plugins, verb): authenticator = display_ops.pick_authenticator(config, req_auth, plugins) logger.debug("Selected authenticator %s and installer %s", authenticator, installer) + # Report on any failures if need_inst and not installer: diagnose_configurator_problem("installer", req_inst, plugins) if need_auth and not authenticator: diagnose_configurator_problem("authenticator", req_auth, plugins) + record_chosen_plugins(config, plugins, authenticator, installer) return installer, authenticator +def record_chosen_plugins(config, plugins, auth, inst): + "Update the config entries to reflect the plugins we actually selected." + cn = config.namespace + cn.authenticator = plugins.find_init(auth).name if auth else "none" + cn.installer = plugins.find_init(inst).name if inst else "none" + + # TODO: Make run as close to auth + install as possible # Possible difficulties: args.csr was hacked into auth def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-locals @@ -448,7 +454,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo # TODO: Handle errors from _init_le_client? le_client = _init_le_client(args, config, authenticator, installer) - lineage = _auth_from_domains(le_client, config, domains, plugins) + lineage = _auth_from_domains(le_client, config, domains) le_client.deploy_certificate( domains, lineage.privkey, lineage.cert, @@ -462,7 +468,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo display_ops.success_renewal(domains) -def obtaincert(args, config, plugins): +def obtain_cert(args, config, plugins): """Authenticate & obtain cert, but do not install it.""" if args.domains is not None and args.csr is not None: @@ -488,7 +494,7 @@ def obtaincert(args, config, plugins): _report_new_cert(cert_path, cert_fullchain) else: domains = _find_domains(args, installer) - _auth_from_domains(le_client, config, domains, plugins) + _auth_from_domains(le_client, config, domains) def install(args, config, plugins): @@ -513,19 +519,19 @@ def install(args, config, plugins): def revoke(args, config, unused_plugins): # TODO: coop with renewal config """Revoke a previously obtained certificate.""" + # For user-agent construction + config.namespace.installer = config.namespace.authenticator = "none" if args.key_path is not None: # revocation by cert key logger.debug("Revoking %s using cert key %s", args.cert_path[0], args.key_path[0]) - acme = acme_client.Client( - config.server, key=jose.JWK.load(args.key_path[1])) + key = jose.JWK.load(args.key_path[1]) else: # revocation by account key logger.debug("Revoking %s using Account Key", args.cert_path[0]) acc, _ = _determine_account(args, config) - # pylint: disable=protected-access - config.determine_user_agent(None, None) - acme = client._acme_from_config_key(config, acc.key) - acme.revoke(jose.ComparableX509(crypto_util.pyopenssl_load_certificate( - args.cert_path[1])[0])) + key = acc.key + acme = client.acme_from_config_key(config, key) + cert = crypto_util.pyopenssl_load_certificate(args.cert_path[1])[0] + acme.revoke(jose.ComparableX509(cert)) def rollback(args, config, plugins): @@ -623,7 +629,7 @@ class HelpfulArgumentParser(object): """ # Maps verbs/subcommands to the functions that implement them - VERBS = {"auth": obtaincert, "certonly": obtaincert, + VERBS = {"auth": obtain_cert, "certonly": obtain_cert, "config_changes": config_changes, "everything": run, "install": install, "plugins": plugins_cmd, "revoke": revoke, "rollback": rollback, "run": run} diff --git a/letsencrypt/client.py b/letsencrypt/client.py index d48230f04..6201ff1d5 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -10,6 +10,8 @@ from acme import client as acme_client from acme import jose from acme import messages +import letsencrypt + from letsencrypt import account from letsencrypt import auth_handler from letsencrypt import configuration @@ -29,13 +31,31 @@ from letsencrypt.display import enhancements logger = logging.getLogger(__name__) -def _acme_from_config_key(config, key): +def acme_from_config_key(config, key): + "Wrange ACME client construction" # TODO: Allow for other alg types besides RS256 net = acme_client.ClientNetwork(key, verify_ssl=(not config.no_verify_ssl), - user_agent=config.user_agent) + user_agent=_determine_user_agent(config)) return acme_client.Client(directory=config.server, key=key, net=net) +def _determine_user_agent(config): + """ + Set a user_agent string in the config based on the choice of plugins. + (this wasn't knowable at construction time) + + :rtype: `str` + """ + + if config.user_agent is None: + ua = "LetsEncryptPythonClient/{0} ({1}) Authenticator/{2} Installer/{3}" + ua = ua.format(letsencrypt.__version__, " ".join(le_util.get_os_info()), + config.authenticator, config.installer) + else: + ua = config.user_agent + return ua + + def register(config, account_storage, tos_cb=None): """Register new account with an ACME CA. @@ -85,7 +105,7 @@ def register(config, account_storage, tos_cb=None): public_exponent=65537, key_size=config.rsa_key_size, backend=default_backend()))) - acme = _acme_from_config_key(config, key) + acme = acme_from_config_key(config, key) # TODO: add phone? regr = acme.register(messages.NewRegistration.from_data(email=config.email)) @@ -128,7 +148,7 @@ class Client(object): # Initialize ACME if account is provided if acme is None and self.account is not None: - acme = _acme_from_config_key(config, self.account.key) + acme = acme_from_config_key(config, self.account.key) self.acme = acme # TODO: Check if self.config.enroll_autorenew is None. If @@ -213,7 +233,7 @@ class Client(object): return self._obtain_certificate(domains, csr) + (key, csr) - def obtain_and_enroll_certificate(self, domains, plugins): + def obtain_and_enroll_certificate(self, domains): """Obtain and enroll certificate. Get a new certificate for the specified domains using the specified @@ -230,13 +250,6 @@ class Client(object): """ certr, chain, key, _ = self.obtain_certificate(domains) - # TODO: remove this dirty hack - self.config.namespace.authenticator = plugins.find_init( - self.dv_auth).name - if self.installer is not None: - self.config.namespace.installer = plugins.find_init( - self.installer).name - # XXX: We clearly need a more general and correct way of getting # options into the configobj for the RenewableCert instance. # This is a quick-and-dirty way to do it to allow integration diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 9ee702258..e70171675 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -3,7 +3,6 @@ import os import urlparse import re -import letsencrypt import zope.interface from acme import challenges @@ -11,7 +10,6 @@ from acme import challenges from letsencrypt import constants from letsencrypt import errors from letsencrypt import interfaces -from letsencrypt import le_util class NamespaceConfig(object): @@ -84,21 +82,6 @@ class NamespaceConfig(object): else: return challenges.HTTP01Response.PORT - def determine_user_agent(self, authenticator, installer): - """ - Set a user_agent string in the config based on the choice of plugins. - (this wasn't knowable at construction time) - """ - - if self.namespace.user_agent is None: - ua = "LetsEncryptPythonClient/{0} ({1}) Authenticator/{2} Installer/{3}" - ua = ua.format(letsencrypt.__version__, " ".join(le_util.get_os_info()), - authenticator.name if authenticator else "none", - installer.name if installer else "none") - self.namespace.user_agent = ua - else: - assert isinstance(self.namespace.user_agent, str), "User Agent not a string?" - class RenewerConfiguration(object): """Configuration wrapper for renewer.""" diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 21a774a9c..13b0eba7a 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -142,8 +142,9 @@ class CLITest(unittest.TestCase): cli.main(args) acme_net.assert_called_once_with(mock.ANY, verify_ssl=True, user_agent=ua) + @mock.patch('letsencrypt.cli.record_chosen_plugins') @mock.patch('letsencrypt.cli.display_ops') - def test_installer_selection(self, mock_display_ops): + def test_installer_selection(self, mock_display_ops, _rec): self._call(['install', '--domain', 'foo.bar', '--cert-path', 'cert', '--key-path', 'key', '--chain-path', 'chain']) self.assertEqual(mock_display_ops.pick_installer.call_count, 1) @@ -280,7 +281,8 @@ class CLITest(unittest.TestCase): @mock.patch('letsencrypt.cli.display_ops.pick_installer') @mock.patch('letsencrypt.cli.zope.component.getUtility') @mock.patch('letsencrypt.cli._init_le_client') - def test_certonly_csr(self, mock_init, mock_get_utility, + @mock.patch('letsencrypt.cli.record_chosen_plugins') + def test_certonly_csr(self, _rec, mock_init, mock_get_utility, mock_pick_installer, mock_notAfter): cert_path = '/etc/letsencrypt/live/blahcert.pem' date = '1970-01-01' From 0db3814c130396cedcc6c1f013773e909694e51a Mon Sep 17 00:00:00 2001 From: Lee Watson Date: Fri, 13 Nov 2015 08:15:09 +0000 Subject: [PATCH 114/124] Update readme to reflect current commands auth -> certonly --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index d3e89c939..ce0d1b686 100644 --- a/README.rst +++ b/README.rst @@ -35,11 +35,11 @@ It's all automated: All you need to do to sign a single domain is:: - user@www:~$ sudo letsencrypt -d www.example.org auth + user@www:~$ sudo letsencrypt -d www.example.org certonly For multiple domains (SAN) use:: - user@www:~$ sudo letsencrypt -d www.example.org -d example.org auth + user@www:~$ sudo letsencrypt -d www.example.org -d example.org certonly and if you have a compatible web server (Apache or Nginx), Let's Encrypt can not only get a new certificate, but also deploy it and configure your From 3551ffbd648df1d8929e8e5302bee36f08409c90 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 13 Nov 2015 01:30:34 -0800 Subject: [PATCH 115/124] Replace "which" with "command -v" Since the latter is at least semi-POSIX compliant: http://pubs.opengroup.org/onlinepubs/009696899/utilities/command.html Avoids the need for #1486 --- bootstrap/_deb_common.sh | 2 +- letsencrypt-auto | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bootstrap/_deb_common.sh b/bootstrap/_deb_common.sh index 71144ce40..4c6b91a33 100755 --- a/bootstrap/_deb_common.sh +++ b/bootstrap/_deb_common.sh @@ -44,7 +44,7 @@ apt-get install -y --no-install-recommends \ libffi-dev \ ca-certificates \ -if ! which virtualenv > /dev/null ; then +if ! command -v virtualenv > /dev/null ; then echo Failed to install a working \"virtualenv\" command, exiting exit 1 fi diff --git a/letsencrypt-auto b/letsencrypt-auto index 25c83cc08..44de7fbf2 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -83,11 +83,11 @@ ExperimentalBootstrap() { } DeterminePythonVersion() { - if which python2 > /dev/null ; then + if command -v python2 > /dev/null ; then export LE_PYTHON=${LE_PYTHON:-python2} - elif which python2.7 > /dev/null ; then + elif command -v python2.7 > /dev/null ; then export LE_PYTHON=${LE_PYTHON:-python2.7} - elif which python > /dev/null ; then + elif command -v python > /dev/null ; then export LE_PYTHON=${LE_PYTHON:-python} else echo "Cannot find any Pythons... please install one!" From 5c09b24e1928eaeeb99abfa3639894178bb124e5 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 13 Nov 2015 09:00:54 -0800 Subject: [PATCH 116/124] Fixed docstring --- letsencrypt/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index fa743a770..30ac81092 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -569,10 +569,10 @@ def plugins_cmd(args, config, plugins): # TODO: Use IDisplay rather than print def read_file(filename, mode="rb"): """Returns the given file's contents. - :param str filename: filename as an absolute path + :param str filename: path to file :param str mode: open mode (see `open`) - :returns: A tuple of filename and its contents + :returns: absolute path of filename and its contents :rtype: tuple :raises argparse.ArgumentTypeError: File does not exist or is not readable. From e8cf095ef3029c0fb558a7fb2f27bb6288a12d46 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 13 Nov 2015 09:56:50 -0800 Subject: [PATCH 117/124] Fix merge glitch (somehow this was lost from the previous commit) --- letsencrypt/tests/cli_test.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index fd5aba788..04cbdabd3 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -142,8 +142,6 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods cli.main(args) acme_net.assert_called_once_with(mock.ANY, verify_ssl=True, user_agent=ua) - self._call(['install', '--domain', 'foo.bar', '--cert-path', 'cert', - def test_install_abspath(self): cert = 'cert' key = 'key' From eea9aa752afae8f9caffa05fe8ff499e62d679db Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 13 Nov 2015 10:06:49 -0800 Subject: [PATCH 118/124] Tests for the 'revoke' verb --- letsencrypt/tests/cli_test.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 04cbdabd3..634744c71 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -406,6 +406,29 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertTrue( date in mock_get_utility().add_message.call_args[0][0]) + @mock.patch('letsencrypt.cli.acme_client') + def test_revoke_with_key(self, mock_acme_client): + server = 'foo.bar' + self._call(['--cert-path', CERT, '--key-path', KEY, + '--server', server, 'revoke']) + with open(KEY) as f: + mock_acme_client.Client.assert_called_once_with( + server, key=jose.JWK.load(f.read())) + with open(CERT) as f: + mock_acme_client.Client().revoke.assert_called_once_with( + jose.ComparableX509(crypto_util.pyopenssl_load_certificate( + f.read())[0])) + + @mock.patch('letsencrypt.cli._determine_account') + def test_revoke_without_key(self, mock_determine_account): + mock_determine_account.return_value = (mock.MagicMock(), None) + _, _, _, client = self._call(['--cert-path', CERT, 'revoke']) + with open(CERT) as f: + # pylint: disable=protected-access + client._acme_from_config_key().revoke.assert_called_once_with( + jose.ComparableX509(crypto_util.pyopenssl_load_certificate( + f.read())[0])) + @mock.patch('letsencrypt.cli.sys') def test_handle_exception(self, mock_sys): # pylint: disable=protected-access From 7e1a59ab296de7502018c38aae38018a7fda2c65 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 13 Nov 2015 12:48:38 -0800 Subject: [PATCH 119/124] Address review comments --- letsencrypt/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 32519ad05..c5582207a 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -930,7 +930,7 @@ def _create_subparsers(helpful): helpful.add_group("rollback", description="Options for reverting config changes") helpful.add_group("plugins", description="Plugin options") helpful.add( - None, "--user-agent", type=str, default=None, + None, "--user-agent", default=None, help="Set a custom user agent string for the client. User agent strings allow " "the CA to collect high level statistics about success rates by OS and " "plugin. If you wish to hide your server OS version from the Let's " From 2433de8cbaedb0f1f54ec7dc7d6682c5b22a6a2d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 13 Nov 2015 12:48:44 -0800 Subject: [PATCH 120/124] Attempt to update revoke() cases [not yet working] --- letsencrypt/client.py | 3 ++- letsencrypt/tests/cli_test.py | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 44ac76394..4d3dc78e6 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -34,7 +34,7 @@ logger = logging.getLogger(__name__) def acme_from_config_key(config, key): - "Wrange ACME client construction" + "Wrangle ACME client construction" # TODO: Allow for other alg types besides RS256 net = acme_client.ClientNetwork(key, verify_ssl=(not config.no_verify_ssl), user_agent=_determine_user_agent(config)) @@ -46,6 +46,7 @@ def _determine_user_agent(config): Set a user_agent string in the config based on the choice of plugins. (this wasn't knowable at construction time) + :returns: the client's User-Agent string :rtype: `str` """ diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 634744c71..cd087dc67 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -10,9 +10,12 @@ import unittest import mock +from acme import jose + from letsencrypt import account from letsencrypt import cli from letsencrypt import configuration +from letsencrypt import crypto_util from letsencrypt import errors from letsencrypt import le_util @@ -22,7 +25,9 @@ from letsencrypt.tests import renewer_test from letsencrypt.tests import test_util +CERT = test_util.vector_path('cert.pem') CSR = test_util.vector_path('csr.der') +KEY = test_util.vector_path('rsa256_key.pem') class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods @@ -406,13 +411,13 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertTrue( date in mock_get_utility().add_message.call_args[0][0]) - @mock.patch('letsencrypt.cli.acme_client') + @mock.patch('letsencrypt.cli.client.acme_from_config_key') def test_revoke_with_key(self, mock_acme_client): server = 'foo.bar' self._call(['--cert-path', CERT, '--key-path', KEY, '--server', server, 'revoke']) with open(KEY) as f: - mock_acme_client.Client.assert_called_once_with( + mock_acme_client.assert_called_once_with( server, key=jose.JWK.load(f.read())) with open(CERT) as f: mock_acme_client.Client().revoke.assert_called_once_with( From cac39c7504f858c8f4aa022aef7e35e6d87df2b0 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 13 Nov 2015 14:02:34 -0800 Subject: [PATCH 121/124] fixes #1490 --- letsencrypt/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 30ac81092..9c367d41e 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -970,7 +970,8 @@ def _paths_parser(helpful): # revoke --key-path reads a file, install --key-path takes a string add(section, "--key-path", required=(verb == "install"), type=((verb == "revoke" and read_file) or os.path.abspath), - help="Path to private key for cert creation or revocation (if account key is missing)") + help="Path to private key for cert installation " + "or revocation (if account key is missing)") default_cp = None if verb == "certonly": From d8a32eeeb50056bd48bddc0bc717ae70e3ad1fef Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 13 Nov 2015 17:09:54 -0800 Subject: [PATCH 122/124] Chase all the mock threads all over the place --- letsencrypt/client.py | 2 +- letsencrypt/tests/cli_test.py | 45 ++++++++++++++++++----------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 4d3dc78e6..8e053e926 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -38,7 +38,7 @@ def acme_from_config_key(config, key): # TODO: Allow for other alg types besides RS256 net = acme_client.ClientNetwork(key, verify_ssl=(not config.no_verify_ssl), user_agent=_determine_user_agent(config)) - return acme_client.Client(directory=config.server, key=key, net=net) + return acme_client.Client(config.server, key=key, net=net) def _determine_user_agent(config): diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index cd087dc67..500ff074e 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -46,12 +46,18 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods shutil.rmtree(self.tmp_dir) def _call(self, args): + "Run the cli with output streams and actual client mocked out" + with mock.patch('letsencrypt.cli.client') as client: + ret, stdout, stderr = self._call_no_clientmock(args) + return ret, stdout, stderr, client + + def _call_no_clientmock(self, args): + "Run the client with output streams mocked out" args = self.standard_args + args with mock.patch('letsencrypt.cli.sys.stdout') as stdout: with mock.patch('letsencrypt.cli.sys.stderr') as stderr: - with mock.patch('letsencrypt.cli.client') as client: - ret = cli.main(args[:]) # NOTE: parser can alter its args! - return ret, stdout, stderr, client + ret = cli.main(args[:]) # NOTE: parser can alter its args! + return ret, stdout, stderr def _call_stdout(self, args): """ @@ -119,20 +125,18 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods out = self._help_output(['-h']) self.assertTrue(cli.usage_strings(plugins)[0] in out) - @mock.patch('letsencrypt.cli.sys.stdout') - @mock.patch('letsencrypt.cli.sys.stderr') @mock.patch('letsencrypt.cli.client.acme_client.Client') @mock.patch('letsencrypt.cli._determine_account') @mock.patch('letsencrypt.cli.client.Client.obtain_and_enroll_certificate') @mock.patch('letsencrypt.cli._auth_from_domains') - def test_user_agent(self, _afd, _obt, det, _client, _err, _out): + def test_user_agent(self, _afd, _obt, det, _client): # Normally the client is totally mocked out, but here we need more # arguments to automate it... args = ["--standalone", "certonly", "-m", "none@none.com", "-d", "example.com", '--agree-tos'] + self.standard_args det.return_value = mock.MagicMock(), None with mock.patch('letsencrypt.cli.client.acme_client.ClientNetwork') as acme_net: - cli.main(args[:]) # Protect args from alteration + self._call_no_clientmock(args) os_ver = " ".join(le_util.get_os_info()) ua = acme_net.call_args[1]["user_agent"] self.assertTrue(os_ver in ua) @@ -144,7 +148,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods with mock.patch('letsencrypt.cli.client.acme_client.ClientNetwork') as acme_net: ua = "bandersnatch" args += ["--user-agent", ua] - cli.main(args) + self._call_no_clientmock(args) acme_net.assert_called_once_with(mock.ANY, verify_ssl=True, user_agent=ua) def test_install_abspath(self): @@ -305,7 +309,6 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods ['-d', '*.wildcard.tld']) def test_parse_domains(self): - from letsencrypt import cli plugins = disco.PluginsRegistry.find_all() short_args = ['-d', 'example.com'] @@ -411,28 +414,27 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self.assertTrue( date in mock_get_utility().add_message.call_args[0][0]) - @mock.patch('letsencrypt.cli.client.acme_from_config_key') + @mock.patch('letsencrypt.cli.client.acme_client') def test_revoke_with_key(self, mock_acme_client): server = 'foo.bar' - self._call(['--cert-path', CERT, '--key-path', KEY, - '--server', server, 'revoke']) + self._call_no_clientmock(['--cert-path', CERT, '--key-path', KEY, + '--server', server, 'revoke']) with open(KEY) as f: - mock_acme_client.assert_called_once_with( - server, key=jose.JWK.load(f.read())) + mock_acme_client.Client.assert_called_once_with( + server, key=jose.JWK.load(f.read()), net=mock.ANY) with open(CERT) as f: - mock_acme_client.Client().revoke.assert_called_once_with( - jose.ComparableX509(crypto_util.pyopenssl_load_certificate( - f.read())[0])) + cert = crypto_util.pyopenssl_load_certificate(f.read())[0] + mock_revoke = mock_acme_client.Client().revoke + mock_revoke.assert_called_once_with(jose.ComparableX509(cert)) @mock.patch('letsencrypt.cli._determine_account') def test_revoke_without_key(self, mock_determine_account): mock_determine_account.return_value = (mock.MagicMock(), None) _, _, _, client = self._call(['--cert-path', CERT, 'revoke']) with open(CERT) as f: - # pylint: disable=protected-access - client._acme_from_config_key().revoke.assert_called_once_with( - jose.ComparableX509(crypto_util.pyopenssl_load_certificate( - f.read())[0])) + cert = crypto_util.pyopenssl_load_certificate(f.read())[0] + mock_revoke = client.acme_from_config_key().revoke + mock_revoke.assert_called_once_with(jose.ComparableX509(cert)) @mock.patch('letsencrypt.cli.sys') def test_handle_exception(self, mock_sys): @@ -469,7 +471,6 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods traceback.format_exception_only(KeyboardInterrupt, interrupt))) def test_read_file(self): - from letsencrypt import cli rel_test_path = os.path.relpath(os.path.join(self.tmp_dir, 'foo')) self.assertRaises( argparse.ArgumentTypeError, cli.read_file, rel_test_path) From 339bb9713751ee36ab86ef788ccf7d39b58b54b3 Mon Sep 17 00:00:00 2001 From: Sam Lanning Date: Sat, 14 Nov 2015 14:31:29 +0000 Subject: [PATCH 123/124] Check for python2.7 before python2 There are cases where more than one version of python2 is installed, and where the default is not 2.7. For example in CentOS 6, it is common for both 2.6 and 2.7 to be installed, as yum requires 2.6 apparently for some reason. --- letsencrypt-auto | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt-auto b/letsencrypt-auto index 44de7fbf2..4d541d5bc 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -83,10 +83,10 @@ ExperimentalBootstrap() { } DeterminePythonVersion() { - if command -v python2 > /dev/null ; then - export LE_PYTHON=${LE_PYTHON:-python2} - elif command -v python2.7 > /dev/null ; then + if command -v python2.7 > /dev/null ; then export LE_PYTHON=${LE_PYTHON:-python2.7} + elif command -v python2 > /dev/null ; then + export LE_PYTHON=${LE_PYTHON:-python2} elif command -v python > /dev/null ; then export LE_PYTHON=${LE_PYTHON:-python} else From b508815b080f5773e148c4219860f559495d74a6 Mon Sep 17 00:00:00 2001 From: Matt Bostock Date: Sat, 14 Nov 2015 22:03:16 +0000 Subject: [PATCH 124/124] Add missing 'a' before 'safe' Add an 'a' before the word 'safe' so that the sentences about protecting `privkey.pem` makes sense. --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 8e8fd132e..45db2bbad 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -204,7 +204,7 @@ The following files are available: .. warning:: This **must be kept secret at all times**! Never share it with anyone, including Let's Encrypt developers. You cannot - put it into safe, however - your server still needs to access + put it into a safe, however - your server still needs to access this file in order for SSL/TLS to work. This is what Apache needs for `SSLCertificateKeyFile