From 2c66c7b3339ab1ccb326000fed63698fc5b2ec92 Mon Sep 17 00:00:00 2001 From: Dev & Sec Date: Mon, 26 Oct 2015 23:22:22 +0000 Subject: [PATCH 01/74] use more accurate method to calculate time interval in should_autodeploy() and should_autorenew() functions --- letsencrypt/storage.py | 29 +++++++++-------- letsencrypt/tests/renewer_test.py | 52 ++++++++++++++++++++++++------- 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/letsencrypt/storage.py b/letsencrypt/storage.py index ece3df4b7..52be94f68 100644 --- a/letsencrypt/storage.py +++ b/letsencrypt/storage.py @@ -2,7 +2,6 @@ import datetime import os import re -import time import configobj import parsedatetime @@ -24,8 +23,8 @@ def config_with_defaults(config=None): return defaults_copy -def parse_time_interval(interval, textparser=parsedatetime.Calendar()): - """Parse the time specified time interval. +def add_time_interval(base_time, interval, textparser=parsedatetime.Calendar()): + """Parse the time specified time interval, and add it to the base_time The interval can be in the English-language format understood by parsedatetime, e.g., '10 days', '3 weeks', '6 months', '9 hours', or @@ -33,15 +32,19 @@ def parse_time_interval(interval, textparser=parsedatetime.Calendar()): hours'. If an integer is found with no associated unit, it is interpreted by default as a number of days. + :param datetime.datetime base_time: The time to be added with the interval. :param str interval: The time interval to parse. - :returns: The interpretation of the time interval. - :rtype: :class:`datetime.timedelta`""" + :returns: The base_time plus the interpretation of the time interval. + :rtype: :class:`datetime.datetime`""" if interval.strip().isdigit(): interval += " days" - return datetime.timedelta(0, time.mktime(textparser.parse( - interval, time.localtime(0))[0])) + + # try to use the same timezone, but fallback to UTC + tzinfo = base_time.tzinfo or pytz.UTC + + return textparser.parseDT(interval, base_time, tzinfo=tzinfo)[0] class RenewableCert(object): # pylint: disable=too-many-instance-attributes @@ -465,11 +468,9 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes if self.has_pending_deployment(): interval = self.configuration.get("deploy_before_expiry", "5 days") - autodeploy_interval = parse_time_interval(interval) expiry = crypto_util.notAfter(self.current_target("cert")) - now = datetime.datetime.utcnow().replace(tzinfo=pytz.UTC) - remaining = expiry - now - if remaining < autodeploy_interval: + now = pytz.UTC.fromutc(datetime.datetime.utcnow()) + if expiry < add_time_interval(now, interval): return True return False @@ -532,12 +533,10 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # Renewals on the basis of expiry time interval = self.configuration.get("renew_before_expiry", "10 days") - autorenew_interval = parse_time_interval(interval) expiry = crypto_util.notAfter(self.version( "cert", self.latest_common_version())) - now = datetime.datetime.utcnow().replace(tzinfo=pytz.UTC) - remaining = expiry - now - if remaining < autorenew_interval: + now = pytz.UTC.fromutc(datetime.datetime.utcnow()) + if expiry < add_time_interval(now, interval): return True return False diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index a856fcf4f..2123db367 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -1,5 +1,6 @@ """Tests for letsencrypt.renewer.""" import datetime +import pytz import os import tempfile import shutil @@ -622,18 +623,47 @@ class RenewableCertTests(BaseRenewableCertTest): # OCSP server to test against. self.assertFalse(self.test_rc.ocsp_revoked()) - def test_parse_time_interval(self): + def test_add_time_interval(self): from letsencrypt import storage - # XXX: I'm not sure if intervals related to years and months - # take account of the current date (if so, some of these - # may fail in the future, like in leap years or even in - # months of different lengths!) - intended = {"": 0, "17 days": 17, "23": 23, "1 month": 31, - "7 weeks": 49, "1 year 1 day": 366, "1 year-1 day": 364, - "4 years": 1461} - for time in intended: - self.assertEqual(storage.parse_time_interval(time), - datetime.timedelta(intended[time])) + + # this month has 30 days, and the next year is a leap year + time_1 = pytz.UTC.fromutc(datetime.datetime(2003, 11, 20, 11, 59, 21)) + + # this month has 31 days, and the next year is not a leap year + time_2 = pytz.UTC.fromutc(datetime.datetime(2012, 10, 18, 21, 31, 16)) + + # in different time zone (GMT+8) + time_3 = pytz.timezone('Asia/Shanghai').fromutc( + datetime.datetime(2015, 10, 26, 22, 25, 41)) + + intended = { + (time_1, ""): time_1, + (time_2, ""): time_2, + (time_3, ""): time_3, + (time_1, "17 days"): time_1 + datetime.timedelta(17), + (time_2, "17 days"): time_2 + datetime.timedelta(17), + (time_1, "30"): time_1 + datetime.timedelta(30), + (time_2, "30"): time_2 + datetime.timedelta(30), + (time_1, "7 weeks"): time_1 + datetime.timedelta(49), + (time_2, "7 weeks"): time_2 + datetime.timedelta(49), + # 1 month is always 30 days, no matter which month it is + (time_1, "1 month"): time_1 + datetime.timedelta(30), + (time_2, "1 month"): time_2 + datetime.timedelta(31), + # 1 year could be 365 or 366 days, depends on the year + (time_1, "1 year"): time_1 + datetime.timedelta(366), + (time_2, "1 year"): time_2 + datetime.timedelta(365), + (time_1, "1 year 1 day"): time_1 + datetime.timedelta(367), + (time_2, "1 year 1 day"): time_2 + datetime.timedelta(366), + (time_1, "1 year-1 day"): time_1 + datetime.timedelta(365), + (time_2, "1 year-1 day"): time_2 + datetime.timedelta(364), + (time_1, "4 years"): time_1 + datetime.timedelta(1461), + (time_2, "4 years"): time_2 + datetime.timedelta(1461), + } + + for parameters, excepted in intended.items(): + base_time, interval = parameters + self.assertEqual(storage.add_time_interval(base_time, interval), + excepted) @mock.patch("letsencrypt.renewer.plugins_disco") @mock.patch("letsencrypt.account.AccountFileStorage") From 1d13938dbfe4c3712e64936ec0ea9192e2cfd19a Mon Sep 17 00:00:00 2001 From: Scott Merrill Date: Mon, 26 Oct 2015 19:50:37 -0400 Subject: [PATCH 02/74] Correct typo and whitespace issues * s/privilidged/privileged/ * s/a HTTP/an HTTP/ * Add whitespace at the end of the lines to improve user experience The lack of trailing whitespace on these entries causes Debian's debconf interface to join the last word of a line with the first word of the next line, with no space in between. --- letsencrypt/plugins/manual.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index 61a5acdb3..60b816821 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -90,10 +90,10 @@ s.serve_forever()" """ def more_info(self): # pylint: disable=missing-docstring,no-self-use return """\ -This plugin requires user's manual intervention in setting up a HTTP -server for solving SimpleHTTP challenges and thus does not need to be -run as a privilidged process. Alternatively shows instructions on how -to use Python's built-in HTTP server and, in case of HTTPS, openssl +This plugin requires user's manual intervention in setting up an HTTP +server for solving SimpleHTTP challenges and thus does not need to be +run as a privileged process. Alternatively shows instructions on how +to use Python's built-in HTTP server and, in case of HTTPS, openssl binary for temporary key/certificate generation.""".replace("\n", "") def get_chall_pref(self, domain): From e9a2180d16bf8efef4795ccd25ef81f58f2f184f Mon Sep 17 00:00:00 2001 From: Thom Wiggers Date: Tue, 27 Oct 2015 22:53:20 +0100 Subject: [PATCH 03/74] Add gitattributes file to mark bat file as CRLF Gitattributes files can be used to mark files as crlf, which is useful for the `.bat` files in case people use eol=lf in their git config. --- .gitattributes | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..5eee84cce --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +* text=auto eol=lf + +# special files +*.bat text eol=crlf +*.jpeg binary +*.jpg binary +*.png binary From 3ecf38b401eaf1ff1efc174172876b2e2e0b9748 Mon Sep 17 00:00:00 2001 From: Christopher Manning Date: Tue, 27 Oct 2015 17:28:04 -0500 Subject: [PATCH 04/74] Vagrantfile: use recommended bootstrap scripts for provisioning Also remove an incorrect command from the Vagrant instructions in `docs/contributing.rst`. --- Vagrantfile | 9 +++------ docs/contributing.rst | 1 - 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index d54e4ea23..a2759440c 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -4,14 +4,11 @@ # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! VAGRANTFILE_API_VERSION = "2" -# Setup instructions from docs/using.rst +# Setup instructions from docs/contributing.rst $ubuntu_setup_script = < Date: Wed, 28 Oct 2015 07:10:13 +0000 Subject: [PATCH 05/74] Move example ACME client to acme subpkg --- examples/acme_client.py => acme/examples/example_client.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/acme_client.py => acme/examples/example_client.py (100%) diff --git a/examples/acme_client.py b/acme/examples/example_client.py similarity index 100% rename from examples/acme_client.py rename to acme/examples/example_client.py From f42515ebe4df620921db592e3cde9ddb75a7005b Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 28 Oct 2015 07:16:40 +0000 Subject: [PATCH 06/74] Include example ACME client in docs --- acme/docs/index.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/acme/docs/index.rst b/acme/docs/index.rst index 8d298054e..a200808da 100644 --- a/acme/docs/index.rst +++ b/acme/docs/index.rst @@ -17,6 +17,12 @@ Contents: :members: +Example client: + +.. include:: ../examples/example_client.py + :code: python + + Indices and tables ================== From 323f9a10a1e20e616cbf22e5ccf2d5e5516b36cf Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 28 Oct 2015 07:26:52 +0000 Subject: [PATCH 07/74] Update example ACME client to work with Boulder --- acme/acme/testdata/README | 6 +++--- acme/acme/testdata/cert.der | Bin 377 -> 771 bytes acme/acme/testdata/csr.der | Bin 210 -> 607 bytes acme/acme/testdata/rsa2048_key.pem | 27 +++++++++++++++++++++++++++ acme/examples/example_client.py | 4 ++-- 5 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 acme/acme/testdata/rsa2048_key.pem diff --git a/acme/acme/testdata/README b/acme/acme/testdata/README index 11bca55e5..dfe3f5405 100644 --- a/acme/acme/testdata/README +++ b/acme/acme/testdata/README @@ -4,12 +4,12 @@ to use appropriate extension for vector filenames: .pem for PEM and The following command has been used to generate test keys: - for x in 256 512 1024; do openssl genrsa -out rsa${k}_key.pem $k; done + for x in 256 512 1024 2048; do openssl genrsa -out rsa${k}_key.pem $k; done and for the CSR: - openssl req -key rsa512_key.pem -new -subj '/CN=example.com' -outform DER > csr.der + openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -outform DER > csr.der and for the certificate: - openssl req -key rsa512_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der + openssl req -key rsa2047_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der diff --git a/acme/acme/testdata/cert.der b/acme/acme/testdata/cert.der index 5f1018505d81a50ed3239d829533deac5fcc2085..ab231982f25ba5b804bc3e4249ec71f8a98109ab 100644 GIT binary patch delta 676 zcmV;V0$ct00)qw^FoFXAFoFT+paTK{0s;vD!GbbZT8w0>kr*u*F)%VXFgG$ZG%;Ei z4KXz_F)}wWH!?IdF_B&xe}VxbFbxI?Duzgg_YDC70R;d9f&mWzFoFRJ0)hbn0PuVn zFn5TUAL295=kO%LM}nSV0Cs?}j^6(Oi-f*9qo2j6=x!V{K8;&aU<&F-RI29O8% zld7Z4fcu;4&+FkVruZ1kp7d(v3v-9~!hGOJB9JhHd1LNh4#-X9kUhXI+oHN?r!`88 z97z;ty1!m4{8!1ZBA&QL?j6why;pO+J3mKr<%2n26wHQY1l7i#{!}z6vumf3&=fWF zHVyL02Ouf)^_JEvWuvqeyjV9d9|i+e9U}x7FcyFm^fnFh$p;`Q^Yxb2D`lg!6}(tC zlNkXXe}Vx40PR%mGTHF`qfZ|f`W9WB?Zir{Hw4`a_UhE50Ig1562Q&I3%9!Yc`^h9>(8O2@#PSQ6nHZUvI2qO+nwIZ<+c0LLgs!B4nURr!iGhK!d6YP>p{ap6 zj5{$-sy@bmmyJ`a&7@EAwlr%Zv3bCH`*@J!N8MWMEtz zVBlvU%f=ik%f}+dBBFNw#9Qy{Q#L(OWsXqkXE{)Eb-GfQfjmfFnMJ}ttO1*fuE`G> z<(wQDc)I@!HO%vwut#mfG=7{>z}O6 Yi?Kd^cJYN9>LqE5%h;CwZnWA40D^aK-~a#s diff --git a/acme/acme/testdata/csr.der b/acme/acme/testdata/csr.der index adc29ff18463752b4b9ab26a0dd77d2621363725..d43ac85a16a5ec7cb3db7895f0c67f9e0a52f492 100644 GIT binary patch literal 607 zcmV-l0-*gcf&yDGf&oJU0RS)-F%&Qo1_M&L zNQUJrzh?pPZGtcMnB*I66o?!rXfU%C=|1mCW zK0RUgWxQN(A@qET;Gv%bxUV&LNQU#G~q&8=D4keLkuj9-(Pt^OR@O_IPL=QHku|DZK zfPJiAQB4p_Swh3a05oAZ^u*Ninui6~Hq0^p1q5)wAV5^0MOfjL!Rrv=Ri3Y<#X`fc zl^_!WP#SW@J_cxpHkAXbCT@t28BvIGy*v3BV8s=BjO0B=Nwm)83Y7o= delta 192 zcmcc5a*2`Epz*vx^+XPt`WORVHcqWJkGAi;jEsz|49wmP1|Cd~3~Ne@w* Date: Wed, 28 Oct 2015 11:35:39 +0100 Subject: [PATCH 08/74] Fix readthedocs Intersphinx URLs, fix #1140 --- docs/conf.py | 2 +- letsencrypt-apache/docs/conf.py | 4 ++-- letsencrypt-compatibility-test/docs/conf.py | 4 ++-- letsencrypt-nginx/docs/conf.py | 4 ++-- letshelp-letsencrypt/docs/conf.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 124f0f9ad..62a7cea07 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -315,5 +315,5 @@ texinfo_documents = [ intersphinx_mapping = { 'python': ('https://docs.python.org/', None), - 'acme': ('https://acme-python.readthedocs.org', None), + 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), } diff --git a/letsencrypt-apache/docs/conf.py b/letsencrypt-apache/docs/conf.py index e439428af..ddbf09262 100644 --- a/letsencrypt-apache/docs/conf.py +++ b/letsencrypt-apache/docs/conf.py @@ -313,6 +313,6 @@ texinfo_documents = [ intersphinx_mapping = { 'python': ('https://docs.python.org/', None), - 'acme': ('https://acme-python.readthedocs.org', None), - 'letsencrypt': ('https://letsencrypt.readthedocs.org', None), + 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), + 'letsencrypt': ('https://letsencrypt.readthedocs.org/en/latest/', None), } diff --git a/letsencrypt-compatibility-test/docs/conf.py b/letsencrypt-compatibility-test/docs/conf.py index 5a63c1dca..cd7a970a3 100644 --- a/letsencrypt-compatibility-test/docs/conf.py +++ b/letsencrypt-compatibility-test/docs/conf.py @@ -307,8 +307,8 @@ texinfo_documents = [ intersphinx_mapping = { 'python': ('https://docs.python.org/', None), - 'acme': ('https://acme-python.readthedocs.org', None), - 'letsencrypt': ('https://letsencrypt.readthedocs.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', None), 'letsencrypt-nginx': ('https://letsencrypt-nginx.readthedocs.org', None), } diff --git a/letsencrypt-nginx/docs/conf.py b/letsencrypt-nginx/docs/conf.py index 8bcae3a78..cdb3490a0 100644 --- a/letsencrypt-nginx/docs/conf.py +++ b/letsencrypt-nginx/docs/conf.py @@ -306,6 +306,6 @@ texinfo_documents = [ intersphinx_mapping = { 'python': ('https://docs.python.org/', None), - 'acme': ('https://acme-python.readthedocs.org', None), - 'letsencrypt': ('https://letsencrypt.readthedocs.org', None), + 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), + 'letsencrypt': ('https://letsencrypt.readthedocs.org/en/latest/', None), } diff --git a/letshelp-letsencrypt/docs/conf.py b/letshelp-letsencrypt/docs/conf.py index abbf3621d..206b0b9e2 100644 --- a/letshelp-letsencrypt/docs/conf.py +++ b/letshelp-letsencrypt/docs/conf.py @@ -306,6 +306,6 @@ texinfo_documents = [ intersphinx_mapping = { 'python': ('https://docs.python.org/', None), - 'acme': ('https://acme-python.readthedocs.org', None), - 'letsencrypt': ('https://letsencrypt.readthedocs.org', None), + 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), + 'letsencrypt': ('https://letsencrypt.readthedocs.org/en/latest/', None), } From eb41678bcd65e5ca4e353d4c45a7ff914d3a9ea8 Mon Sep 17 00:00:00 2001 From: Christoph Kisfeld Date: Wed, 28 Oct 2015 11:49:32 +0100 Subject: [PATCH 09/74] Fix one more readthedocs Intersphinx URL --- letsencrypt-compatibility-test/docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-compatibility-test/docs/conf.py b/letsencrypt-compatibility-test/docs/conf.py index cd7a970a3..7e9f0d5a4 100644 --- a/letsencrypt-compatibility-test/docs/conf.py +++ b/letsencrypt-compatibility-test/docs/conf.py @@ -309,6 +309,6 @@ 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', None), - 'letsencrypt-nginx': ('https://letsencrypt-nginx.readthedocs.org', None), + 'letsencrypt-apache': ('https://letsencrypt-apache.readthedocs.org/en/latest/', None), + 'letsencrypt-nginx': ('https://letsencrypt-nginx.readthedocs.org/en/latest/', None), } From df2ba1ba46e550cc2b7fac54b8e259755aca9186 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 28 Oct 2015 19:13:41 -0700 Subject: [PATCH 10/74] standalone_description += pde_feedback --- letsencrypt/plugins/standalone.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/plugins/standalone.py b/letsencrypt/plugins/standalone.py index 459988008..1b56fbdbc 100644 --- a/letsencrypt/plugins/standalone.py +++ b/letsencrypt/plugins/standalone.py @@ -144,7 +144,7 @@ class Authenticator(common.Plugin): zope.interface.implements(interfaces.IAuthenticator) zope.interface.classProvides(interfaces.IPluginFactory) - description = "Automatically configure and run a simple webserver" + description = "Automatically use a temporary webserver" def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) From c93512d2e18359667754f09813bc34c339df8e8d Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sun, 25 Oct 2015 18:47:52 -0700 Subject: [PATCH 11/74] Make "certonly" a synonym for "auth", and document it Closes: #657 --- letsencrypt/cli.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 94bda18ea..e88302c81 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -56,7 +56,7 @@ default, it will attempt to use a webserver both for obtaining and installing the cert. Major SUBCOMMANDS are: (default) run Obtain & install a cert in your current webserver - auth Authenticate & obtain cert, but do not install it + certonly Aka "auth": obtain cert, but do not install it install Install a previously obtained cert in a server revoke Revoke a previously obtained certificate rollback Rollback server configuration changes made during install @@ -358,11 +358,11 @@ def diagnose_configurator_problem(cfg_type, requested, plugins): if os.path.exists("/etc/debian_version"): # Debian... installers are at least possible msg = ('No installers seem to be present and working on your system; ' - 'fix that or try running letsencrypt with the "auth" command') + 'fix that or try running letsencrypt with the "certonly" command') else: # XXX update this logic as we make progress on #788 and nginx support msg = ('No installers are available on your OS yet; try running ' - '"letsencrypt-auto auth" to get a cert you can install manually') + '"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) @@ -377,7 +377,7 @@ def choose_configurator_plugins(args, config, plugins, verb): # Which plugins do we need? need_inst = need_auth = (verb == "run") - if verb == "auth": + if verb in ("auth","certonly"): need_auth = True if verb == "install": need_inst = True @@ -457,7 +457,7 @@ def auth(args, config, plugins): try: # installers are used in auth mode to determine domain names - installer, authenticator = choose_configurator_plugins(args, config, plugins, "auth") + installer, authenticator = choose_configurator_plugins(args, config, plugins, "certonly") except PluginSelectionError, e: return e.message @@ -481,7 +481,7 @@ def install(args, config, plugins): # XXX: Update for renewer/RenewableCert try: - installer, _ = choose_configurator_plugins(args, config, plugins, "auth") + installer, _ = choose_configurator_plugins(args, config, plugins, "certonly") except PluginSelectionError, e: return e.message @@ -609,7 +609,7 @@ class HelpfulArgumentParser(object): """ # Maps verbs/subcommands to the functions that implement them - VERBS = {"auth": auth, "config_changes": config_changes, + VERBS = {"auth": auth, "certonly": auth, "config_changes": config_changes, "install": install, "plugins": plugins_cmd, "revoke": revoke, "rollback": rollback, "run": run} @@ -894,7 +894,7 @@ def _paths_parser(helpful): "paths", description="Arguments changing execution paths & servers") cph = "Path to where cert is saved (with auth), installed (with install --csr) or revoked." - if verb == "auth": + if verb in ("auth", "certonly"): add("paths", "--cert-path", default=flag_default("auth_cert_path"), help=cph) elif verb == "revoke": add("paths", "--cert-path", type=read_file, required=True, help=cph) @@ -907,7 +907,7 @@ def _paths_parser(helpful): help="Path to private key for cert creation or revocation (if account key is missing)") default_cp = None - if verb == "auth": + if verb in ("auth", "certonly"): default_cp = flag_default("auth_chain_path") add("paths", "--fullchain-path", default=default_cp, help="Accompanying path to a full certificate chain (cert plus chain).") From 585e6cee6c685033bcb7488589e328bab7703933 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 26 Oct 2015 10:56:32 -0700 Subject: [PATCH 12/74] Make "everything" a synonym for "run" --- letsencrypt/cli.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index e88302c81..700c752c2 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -377,7 +377,7 @@ def choose_configurator_plugins(args, config, plugins, verb): # Which plugins do we need? need_inst = need_auth = (verb == "run") - if verb in ("auth","certonly"): + if verb in ("auth", "certonly"): need_auth = True if verb == "install": need_inst = True @@ -669,7 +669,12 @@ class HelpfulArgumentParser(object): for i, token in enumerate(self.args): if token in self.VERBS: - self.verb = token + verb = token + if verb == "certonly": + verb = "auth" + if verb == "everything": + verb = "run" + self.verb = verb self.args.pop(i) return From ec25612d6026610e5f8be5e59bd838d8bd453394 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 26 Oct 2015 10:57:46 -0700 Subject: [PATCH 13/74] For now, use the absolutely minimal implementation of verb synonyms Under the hood, everything can remain "run" and "auth" for now. --- letsencrypt/cli.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 700c752c2..b7af4bf9f 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -377,7 +377,7 @@ def choose_configurator_plugins(args, config, plugins, verb): # Which plugins do we need? need_inst = need_auth = (verb == "run") - if verb in ("auth", "certonly"): + if verb == "auth": need_auth = True if verb == "install": need_inst = True @@ -457,7 +457,7 @@ def auth(args, config, plugins): try: # installers are used in auth mode to determine domain names - installer, authenticator = choose_configurator_plugins(args, config, plugins, "certonly") + installer, authenticator = choose_configurator_plugins(args, config, plugins, "auth") except PluginSelectionError, e: return e.message @@ -481,7 +481,7 @@ def install(args, config, plugins): # XXX: Update for renewer/RenewableCert try: - installer, _ = choose_configurator_plugins(args, config, plugins, "certonly") + installer, _ = choose_configurator_plugins(args, config, plugins, "auth") except PluginSelectionError, e: return e.message @@ -899,7 +899,7 @@ def _paths_parser(helpful): "paths", description="Arguments changing execution paths & servers") cph = "Path to where cert is saved (with auth), installed (with install --csr) or revoked." - if verb in ("auth", "certonly"): + if verb == "auth": add("paths", "--cert-path", default=flag_default("auth_cert_path"), help=cph) elif verb == "revoke": add("paths", "--cert-path", type=read_file, required=True, help=cph) @@ -912,7 +912,7 @@ def _paths_parser(helpful): help="Path to private key for cert creation or revocation (if account key is missing)") default_cp = None - if verb in ("auth", "certonly"): + if verb == "auth": default_cp = flag_default("auth_chain_path") add("paths", "--fullchain-path", default=default_cp, help="Accompanying path to a full certificate chain (cert plus chain).") From 84dad86d6197ba5de2b2c9dd96f6b3eae2cb9712 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 26 Oct 2015 11:27:31 -0700 Subject: [PATCH 14/74] Update the help for modern verbiage --- letsencrypt/cli.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index b7af4bf9f..a49a33f26 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -56,7 +56,7 @@ default, it will attempt to use a webserver both for obtaining and installing the cert. Major SUBCOMMANDS are: (default) run Obtain & install a cert in your current webserver - certonly Aka "auth": obtain cert, but do not install it + certonly Obtain cert, but do not install it (aka "auth") install Install a previously obtained cert in a server revoke Revoke a previously obtained certificate rollback Rollback server configuration changes made during install @@ -67,12 +67,14 @@ the cert. Major SUBCOMMANDS are: # This is the short help for letsencrypt --help, where we disable argparse # altogether -USAGE = SHORT_USAGE + """Choice of server for authentication/installation: +USAGE = SHORT_USAGE + """Choice of server plugins for obtaining and installing cert: --apache Use the Apache plugin for authentication & installation --nginx Use the Nginx plugin for authentication & installation --standalone Run a standalone webserver for authentication - OR: + +OR use different servers to obtain (authenticate) the cert and then install it: + --authenticator standalone --installer nginx More detailed help: @@ -80,8 +82,8 @@ More detailed help: -h, --help [topic] print this message, or detailed help on a topic; the available topics are: - all, apache, automation, manual, nginx, paths, security, testing, or any of - the subcommands + all, automation, paths, security, testing, or any of the subcommands or + plugins (certonly, install, nginx, apache, standalone, etc) """ @@ -759,6 +761,10 @@ class HelpfulArgumentParser(object): """ # topics maps each topic to whether it should be documented by # argparse on the command line + if chosen_topic == "certonly": + chosen_topic = "auth" + if chosen_topic == "everything": + chosen_topic = "run" if chosen_topic == "all": return dict([(t, True) for t in self.help_topics]) elif not chosen_topic: From 9a6ecbe669fd250b055cbab062f2540734b5b28b Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 26 Oct 2015 11:53:41 -0700 Subject: [PATCH 15/74] Test case for new verb variant --- letsencrypt/tests/cli_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index cb2360dd0..27892cdc4 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -115,6 +115,12 @@ class CLITest(unittest.TestCase): # (we can only do that if letsencrypt-nginx is actually present) ret, _, _, _ = self._call(args) self.assertTrue("The nginx plugin is not working" in ret) + self.assertTrue("Could not find configuration root" in ret) + + with MockedVerb("auth") as mock_auth: + from letsencrypt import cli + self._call(["certonly", "--standalone"]) + self.assertEqual(1, mock_auth.call_count) def test_rollback(self): _, _, _, client = self._call(['rollback']) From 00dcff6de9ed41d411a5e37f42b1d723f887ec77 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 26 Oct 2015 11:54:47 -0700 Subject: [PATCH 16/74] Also slip in some extra conditional plugin cli unit tests --- letsencrypt/tests/cli_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 27892cdc4..d8940e22c 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -116,6 +116,7 @@ class CLITest(unittest.TestCase): ret, _, _, _ = self._call(args) self.assertTrue("The nginx plugin is not working" in ret) self.assertTrue("Could not find configuration root" in ret) + self.assertTrue("NoInstallationError" in ret) with MockedVerb("auth") as mock_auth: from letsencrypt import cli From ed69ae0292a016ce9bf5038c5d32ed0977f9b1f6 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 26 Oct 2015 12:06:05 -0700 Subject: [PATCH 17/74] Don't suggest that --nginx is available if it isn't Fixes: #932 --- letsencrypt/cli.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index a49a33f26..9fa765a2e 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -64,18 +64,24 @@ the cert. Major SUBCOMMANDS are: """ +plugins = plugins_disco.PluginsRegistry.find_all() +if "nginx" in plugins: + nginx_doc = "--nginx Use the Nginx plugin for authentication & installation" +else: + nginx_doc = "(nginx support is experimental, buggy, and not installed by default)" + # This is the short help for letsencrypt --help, where we disable argparse # altogether USAGE = SHORT_USAGE + """Choice of server plugins for obtaining and installing cert: --apache Use the Apache plugin for authentication & installation - --nginx Use the Nginx plugin for authentication & installation --standalone Run a standalone webserver for authentication + %s OR use different servers to obtain (authenticate) the cert and then install it: - --authenticator standalone --installer nginx + --authenticator standalone --installer apache More detailed help: @@ -84,7 +90,7 @@ More detailed help: all, automation, paths, security, testing, or any of the subcommands or plugins (certonly, install, nginx, apache, standalone, etc) -""" +""" % nginx_doc def _find_domains(args, installer): @@ -1061,7 +1067,7 @@ def main(cli_args=sys.argv[1:]): sys.excepthook = functools.partial(_handle_exception, args=None) # note: arg parser internally handles --help (and exits afterwards) - plugins = plugins_disco.PluginsRegistry.find_all() + global plugins args = prepare_and_parse_args(plugins, cli_args) config = configuration.NamespaceConfig(args) zope.component.provideUtility(config) From 01fabbe893128916d87e47bb5488e2c5314036b4 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 26 Oct 2015 12:07:40 -0700 Subject: [PATCH 18/74] Also make apache docs conditional? --- letsencrypt/cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 9fa765a2e..61b62e320 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -69,6 +69,11 @@ if "nginx" in plugins: nginx_doc = "--nginx Use the Nginx plugin for authentication & installation" else: nginx_doc = "(nginx support is experimental, buggy, and not installed by default)" +if "apache" in plugins: + apache_doc = "--apache Use the Apache plugin for authentication & installation" +else: + apache_doc = "(the apache plugin is not installed)" + # This is the short help for letsencrypt --help, where we disable argparse From 60a0b01d5993a47f0db16752b986a3c95901e95c Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 27 Oct 2015 11:30:02 -0700 Subject: [PATCH 19/74] Don't initialise plugins super early Also extra unit testing --- letsencrypt/cli.py | 36 ++++++++++++++++++----------------- letsencrypt/tests/cli_test.py | 16 ++++++++++++++-- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 61b62e320..c63cb3c5c 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -64,23 +64,11 @@ the cert. Major SUBCOMMANDS are: """ -plugins = plugins_disco.PluginsRegistry.find_all() -if "nginx" in plugins: - nginx_doc = "--nginx Use the Nginx plugin for authentication & installation" -else: - nginx_doc = "(nginx support is experimental, buggy, and not installed by default)" -if "apache" in plugins: - apache_doc = "--apache Use the Apache plugin for authentication & installation" -else: - apache_doc = "(the apache plugin is not installed)" - - - # This is the short help for letsencrypt --help, where we disable argparse # altogether USAGE = SHORT_USAGE + """Choice of server plugins for obtaining and installing cert: - --apache Use the Apache plugin for authentication & installation + %s --standalone Run a standalone webserver for authentication %s @@ -95,7 +83,20 @@ More detailed help: all, automation, paths, security, testing, or any of the subcommands or plugins (certonly, install, nginx, apache, standalone, etc) -""" % nginx_doc +""" + +def usage_strings(plugins): + """Make usage strings late so that plugins can be initialised late""" + print plugins + if "nginx" in plugins: + nginx_doc = "--nginx Use the Nginx plugin for authentication & installation" + else: + nginx_doc = "(nginx support is experimental, buggy, and not installed by default)" + if "apache" in plugins: + apache_doc = "--apache Use the Apache plugin for authentication & installation" + else: + apache_doc = "(the apache plugin is not installed)" + return USAGE % (apache_doc, nginx_doc), SHORT_USAGE def _find_domains(args, installer): @@ -633,8 +634,9 @@ class HelpfulArgumentParser(object): def __init__(self, args, plugins): plugin_names = [name for name, _p in plugins.iteritems()] self.help_topics = self.HELP_TOPICS + plugin_names + [None] + usage, short_usage = usage_strings(plugins) self.parser = configargparse.ArgParser( - usage=SHORT_USAGE, + usage=short_usage, formatter_class=argparse.ArgumentDefaultsHelpFormatter, args_for_setting_config_path=["-c", "--config"], default_config_files=flag_default("config_files")) @@ -651,7 +653,7 @@ class HelpfulArgumentParser(object): help_arg = max(help1, help2) if help_arg is True: # just --help with no topic; avoid argparse altogether - print USAGE + print usage sys.exit(0) self.visible_topics = self.determine_help_topics(help_arg) #print self.visible_topics @@ -1072,7 +1074,7 @@ def main(cli_args=sys.argv[1:]): sys.excepthook = functools.partial(_handle_exception, args=None) # note: arg parser internally handles --help (and exits afterwards) - global plugins + plugins = plugins_disco.PluginsRegistry.find_all() args = prepare_and_parse_args(plugins, cli_args) config = configuration.NamespaceConfig(args) zope.component.provideUtility(config) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index d8940e22c..3ef2ea161 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -69,30 +69,42 @@ class CLITest(unittest.TestCase): self.assertRaises(SystemExit, self._call, ['--help', 'all']) output = StringIO.StringIO() with mock.patch('letsencrypt.cli.sys.stdout', new=output): + plugins = disco.PluginsRegistry.find_all() self.assertRaises(SystemExit, self._call_stdout, ['--help', 'all']) out = output.getvalue() self.assertTrue("--configurator" in out) self.assertTrue("how a cert is deployed" in out) self.assertTrue("--manual-test-mode" in out) output.truncate(0) + self.assertRaises(SystemExit, self._call_stdout, ['-h', 'nginx']) out = output.getvalue() - if "nginx" in disco.PluginsRegistry.find_all(): + if "nginx" in plugins: # may be false while building distributions without plugins self.assertTrue("--nginx-ctl" in out) self.assertTrue("--manual-test-mode" not in out) self.assertTrue("--checkpoints" not in out) output.truncate(0) + + self.assertRaises(SystemExit, self._call_stdout, ['-h']) + out = output.getvalue() + if "nginx" in plugins: + self.assertTrue("Use the Nginx plugin" in out) + else: + self.assertTrue("(nginx support is experimental" in out) + output.truncate(0) + self.assertRaises(SystemExit, self._call_stdout, ['--help', 'plugins']) out = output.getvalue() self.assertTrue("--manual-test-mode" not in out) self.assertTrue("--prepare" in out) self.assertTrue("Plugin options" in out) output.truncate(0) + self.assertRaises(SystemExit, self._call_stdout, ['-h']) out = output.getvalue() from letsencrypt import cli - self.assertTrue(cli.USAGE in out) + self.assertTrue(cli.usage_strings(plugins)[0] in out) def test_configurator_selection(self): real_plugins = disco.PluginsRegistry.find_all() From 7e717a7a15f9107fd94bf26bd77016e61d38583a Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 27 Oct 2015 11:49:25 -0700 Subject: [PATCH 20/74] lint --- letsencrypt/tests/cli_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 3ef2ea161..d8f6215dc 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -131,7 +131,6 @@ class CLITest(unittest.TestCase): self.assertTrue("NoInstallationError" in ret) with MockedVerb("auth") as mock_auth: - from letsencrypt import cli self._call(["certonly", "--standalone"]) self.assertEqual(1, mock_auth.call_count) From 8476efe86b7e45dabc018543b3baab8a0503ff41 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 28 Oct 2015 10:59:47 -0700 Subject: [PATCH 21/74] Inversion: make certonly the real verb, and "auth" an alias for it --- letsencrypt/cli.py | 27 +++++++++++++-------------- letsencrypt/tests/cli_test.py | 30 +++++++++++++++--------------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index c63cb3c5c..db3cd051a 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -87,7 +87,6 @@ More detailed help: def usage_strings(plugins): """Make usage strings late so that plugins can be initialised late""" - print plugins if "nginx" in plugins: nginx_doc = "--nginx Use the Nginx plugin for authentication & installation" else: @@ -391,7 +390,7 @@ def choose_configurator_plugins(args, config, plugins, verb): # Which plugins do we need? need_inst = need_auth = (verb == "run") - if verb == "auth": + if verb == "certonly": need_auth = True if verb == "install": need_inst = True @@ -461,7 +460,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo display_ops.success_renewal(domains) -def auth(args, config, plugins): +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: @@ -471,7 +470,7 @@ def auth(args, config, plugins): try: # installers are used in auth mode to determine domain names - installer, authenticator = choose_configurator_plugins(args, config, plugins, "auth") + installer, authenticator = choose_configurator_plugins(args, config, plugins, "certonly") except PluginSelectionError, e: return e.message @@ -495,7 +494,7 @@ def install(args, config, plugins): # XXX: Update for renewer/RenewableCert try: - installer, _ = choose_configurator_plugins(args, config, plugins, "auth") + installer, _ = choose_configurator_plugins(args, config, plugins, "certonly") except PluginSelectionError, e: return e.message @@ -623,7 +622,7 @@ class HelpfulArgumentParser(object): """ # Maps verbs/subcommands to the functions that implement them - VERBS = {"auth": auth, "certonly": auth, "config_changes": config_changes, + VERBS = {"auth": obtaincert, "certonly": obtaincert, "config_changes": config_changes, "install": install, "plugins": plugins_cmd, "revoke": revoke, "rollback": rollback, "run": run} @@ -685,8 +684,8 @@ class HelpfulArgumentParser(object): for i, token in enumerate(self.args): if token in self.VERBS: verb = token - if verb == "certonly": - verb = "auth" + if verb == "auth": + verb = "certonly" if verb == "everything": verb = "run" self.verb = verb @@ -774,8 +773,8 @@ class HelpfulArgumentParser(object): """ # topics maps each topic to whether it should be documented by # argparse on the command line - if chosen_topic == "certonly": - chosen_topic = "auth" + if chosen_topic == "auth": + chosen_topic = "certonly" if chosen_topic == "everything": chosen_topic = "run" if chosen_topic == "all": @@ -884,13 +883,13 @@ def prepare_and_parse_args(plugins, args): def _create_subparsers(helpful): - helpful.add_group("auth", description="Options for modifying how a cert is obtained") + helpful.add_group("certonly", description="Options for modifying how a cert is obtained") helpful.add_group("install", description="Options for modifying how a cert is deployed") 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("auth", + helpful.add("certonly", "--csr", type=read_file, help="Path to a Certificate Signing Request (CSR) in DER" " format; note that the .csr file *must* contain a Subject" @@ -918,7 +917,7 @@ def _paths_parser(helpful): "paths", description="Arguments changing execution paths & servers") cph = "Path to where cert is saved (with auth), installed (with install --csr) or revoked." - if verb == "auth": + if verb == "certonly": add("paths", "--cert-path", default=flag_default("auth_cert_path"), help=cph) elif verb == "revoke": add("paths", "--cert-path", type=read_file, required=True, help=cph) @@ -931,7 +930,7 @@ def _paths_parser(helpful): help="Path to private key for cert creation or revocation (if account key is missing)") default_cp = None - if verb == "auth": + if verb == "certonly": default_cp = flag_default("auth_chain_path") add("paths", "--fullchain-path", default=default_cp, help="Accompanying path to a full certificate chain (cert plus chain).") diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index d8f6215dc..e7062f675 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -130,8 +130,8 @@ class CLITest(unittest.TestCase): self.assertTrue("Could not find configuration root" in ret) self.assertTrue("NoInstallationError" in ret) - with MockedVerb("auth") as mock_auth: - self._call(["certonly", "--standalone"]) + with MockedVerb("certonly") as mock_certonly: + self._call(["auth", "--standalone"]) self.assertEqual(1, mock_auth.call_count) def test_rollback(self): @@ -153,16 +153,16 @@ class CLITest(unittest.TestCase): for r in xrange(len(flags)))): self._call(['plugins'] + list(args)) - def test_auth_bad_args(self): - ret, _, _, _ = self._call(['-d', 'foo.bar', 'auth', '--csr', CSR]) + def test_certonly_bad_args(self): + ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR]) self.assertEqual(ret, '--domains and --csr are mutually exclusive') - ret, _, _, _ = self._call(['-a', 'bad_auth', 'auth']) + ret, _, _, _ = self._call(['-a', 'bad_auth', 'certonly']) self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed') @mock.patch('letsencrypt.crypto_util.notAfter') @mock.patch('letsencrypt.cli.zope.component.getUtility') - def test_auth_new_request_success(self, mock_get_utility, mock_notAfter): + def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter): cert_path = '/etc/letsencrypt/live/foo.bar' date = '1970-01-01' mock_notAfter().date.return_value = date @@ -170,7 +170,7 @@ class CLITest(unittest.TestCase): mock_lineage = mock.MagicMock(cert=cert_path, fullchain=cert_path) mock_client = mock.MagicMock() mock_client.obtain_and_enroll_certificate.return_value = mock_lineage - self._auth_new_request_common(mock_client) + self._certonly_new_request_common(mock_client) self.assertEqual( mock_client.obtain_and_enroll_certificate.call_count, 1) self.assertTrue( @@ -178,23 +178,23 @@ class CLITest(unittest.TestCase): self.assertTrue( date in mock_get_utility().add_message.call_args[0][0]) - def test_auth_new_request_failure(self): + def test_certonly_new_request_failure(self): mock_client = mock.MagicMock() mock_client.obtain_and_enroll_certificate.return_value = False self.assertRaises(errors.Error, - self._auth_new_request_common, mock_client) + self._certonly_new_request_common, mock_client) - def _auth_new_request_common(self, mock_client): + def _certonly_new_request_common(self, mock_client): with mock.patch('letsencrypt.cli._treat_as_renewal') as mock_renewal: mock_renewal.return_value = None with mock.patch('letsencrypt.cli._init_le_client') as mock_init: mock_init.return_value = mock_client - self._call(['-d', 'foo.bar', '-a', 'standalone', 'auth']) + self._call(['-d', 'foo.bar', '-a', 'standalone', 'certonly']) @mock.patch('letsencrypt.cli.zope.component.getUtility') @mock.patch('letsencrypt.cli._treat_as_renewal') @mock.patch('letsencrypt.cli._init_le_client') - def test_auth_renewal(self, mock_init, mock_renewal, mock_get_utility): + def test_certonly_renewal(self, mock_init, mock_renewal, mock_get_utility): cert_path = '/etc/letsencrypt/live/foo.bar/cert.pem' chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem' @@ -208,7 +208,7 @@ class CLITest(unittest.TestCase): mock_init.return_value = mock_client with mock.patch('letsencrypt.cli.OpenSSL'): with mock.patch('letsencrypt.cli.crypto_util'): - self._call(['-d', 'foo.bar', '-a', 'standalone', 'auth']) + self._call(['-d', 'foo.bar', '-a', 'standalone', 'certonly']) mock_client.obtain_certificate.assert_called_once_with(['foo.bar']) self.assertEqual(mock_lineage.save_successor.call_count, 1) mock_lineage.update_all_links_to.assert_called_once_with( @@ -220,7 +220,7 @@ 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_auth_csr(self, mock_init, mock_get_utility, + def test_certonly_csr(self, mock_init, mock_get_utility, mock_pick_installer, mock_notAfter): cert_path = '/etc/letsencrypt/live/blahcert.pem' date = '1970-01-01' @@ -234,7 +234,7 @@ class CLITest(unittest.TestCase): installer = 'installer' self._call( - ['-a', 'standalone', '-i', installer, 'auth', '--csr', CSR, + ['-a', 'standalone', '-i', installer, 'certonly', '--csr', CSR, '--cert-path', cert_path, '--fullchain-path', '/', '--chain-path', '/']) self.assertEqual(mock_pick_installer.call_args[0][1], installer) From 28071a3e72a2d00e6d5ad82ba1c3028fa72ff4ca Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 28 Oct 2015 11:06:56 -0700 Subject: [PATCH 22/74] Fix stray ref --- letsencrypt/tests/cli_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index e7062f675..669fa1ce8 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -132,7 +132,7 @@ class CLITest(unittest.TestCase): with MockedVerb("certonly") as mock_certonly: self._call(["auth", "--standalone"]) - self.assertEqual(1, mock_auth.call_count) + self.assertEqual(1, mock_certonly.call_count) def test_rollback(self): _, _, _, client = self._call(['rollback']) From 0d12ded6febb9912491b1daa44e8c07426107cc9 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Wed, 28 Oct 2015 18:55:52 -0700 Subject: [PATCH 23/74] Alias "everything" correctly --- letsencrypt/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index db3cd051a..641d0d341 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -622,7 +622,8 @@ class HelpfulArgumentParser(object): """ # Maps verbs/subcommands to the functions that implement them - VERBS = {"auth": obtaincert, "certonly": obtaincert, "config_changes": config_changes, + VERBS = {"auth": obtaincert, "certonly": obtaincert, + "config_changes": config_changes, "everything": run, "install": install, "plugins": plugins_cmd, "revoke": revoke, "rollback": rollback, "run": run} From e9661c9634a2dc9ce28afae0401f427db7519889 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 28 Oct 2015 19:57:56 -0700 Subject: [PATCH 24/74] Fixed tests --- letsencrypt/plugins/disco_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/plugins/disco_test.py b/letsencrypt/plugins/disco_test.py index 773dbe0a1..41d8cd5fe 100644 --- a/letsencrypt/plugins/disco_test.py +++ b/letsencrypt/plugins/disco_test.py @@ -51,7 +51,7 @@ class PluginEntryPointTest(unittest.TestCase): def test_description(self): self.assertEqual( - "Automatically configure and run a simple webserver", + "Automatically use a temporary webserver", self.plugin_ep.description) def test_description_with_name(self): From 7fdea8dd1ae951bc1d6348a90b25cfa5148e9081 Mon Sep 17 00:00:00 2001 From: Axel Beckert Date: Thu, 29 Oct 2015 09:19:38 +0100 Subject: [PATCH 25/74] Use git instead of git-core in bootstrapping on Debian and friends Fixes #1179. --- bootstrap/_deb_common.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap/_deb_common.sh b/bootstrap/_deb_common.sh index 2f44c4e91..71144ce40 100755 --- a/bootstrap/_deb_common.sh +++ b/bootstrap/_deb_common.sh @@ -33,7 +33,7 @@ if apt-cache show python-virtualenv > /dev/null ; then fi apt-get install -y --no-install-recommends \ - git-core \ + git \ python \ python-dev \ $virtualenv \ From c3fbed1f81484daf1cb440b55ca3dc2f0af55ee7 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 29 Oct 2015 08:19:54 +0000 Subject: [PATCH 26/74] Offline unittest v2. Supersedes https://github.com/letsencrypt/letsencrypt/pull/1183. --- acme/acme/challenges_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index ed44d4c45..2a12b4a64 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -359,9 +359,9 @@ class DVSNIResponseTest(unittest.TestCase): cert=mock.sentinel.cert)) mock_verify_cert.assert_called_once_with(self.msg, mock.sentinel.cert) - def test_simple_verify_false_on_probe_error(self): - chall = mock.Mock() - chall.probe_cert.side_effect = errors.Error + @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, self.key.public_key())) From 39c83d17d7adb790ec68658a957481c73f63e7e0 Mon Sep 17 00:00:00 2001 From: David Kreitschmann Date: Thu, 29 Oct 2015 10:06:32 +0100 Subject: [PATCH 27/74] Add -t to apache2ctl -D DUMP_RUN_CFG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calling apache2ctl -D DUMP_RUN_CFG on Debian Wheezy (Apache 2.2) will open a socket which breaks standalone auth. letsencrypt still won’t work with apache 2.2 because it only returns „Syntax OK“. Apache 2.4 works the same with or without -t. --- letsencrypt-apache/letsencrypt_apache/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt-apache/letsencrypt_apache/parser.py b/letsencrypt-apache/letsencrypt_apache/parser.py index 0a3643064..ec5211ae4 100644 --- a/letsencrypt-apache/letsencrypt_apache/parser.py +++ b/letsencrypt-apache/letsencrypt_apache/parser.py @@ -122,7 +122,7 @@ class ApacheParser(object): """ try: proc = subprocess.Popen( - [ctl, "-D", "DUMP_RUN_CFG"], + [ctl, "-t", "-D", "DUMP_RUN_CFG"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = proc.communicate() From faa61da2a6e98b393c38ce13f4376280f8f73820 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 29 Oct 2015 10:46:39 -0700 Subject: [PATCH 28/74] manual_description += kuba_feedback --- letsencrypt/plugins/manual.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index eba53f696..459d771ce 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -37,7 +37,7 @@ class Authenticator(common.Plugin): zope.interface.classProvides(interfaces.IPluginFactory) hidden = True - description = "Manually configure and run a simple Python webserver" + description = "Manually edit your server configuration" MESSAGE_TEMPLATE = """\ Make sure your web server displays the following content at From de30a28555f9fec2dce56c38ad234c5fd4ec6839 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Thu, 29 Oct 2015 13:20:52 -0700 Subject: [PATCH 29/74] Another shot at a description --- letsencrypt/plugins/manual.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index 459d771ce..b3f41d1f2 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -37,7 +37,7 @@ class Authenticator(common.Plugin): zope.interface.classProvides(interfaces.IPluginFactory) hidden = True - description = "Manually edit your server configuration" + description = "Manually configure an HTTP server" MESSAGE_TEMPLATE = """\ Make sure your web server displays the following content at From 4cc06106799dafc6b525e433a5d12f61445cce88 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 29 Oct 2015 20:46:43 +0000 Subject: [PATCH 30/74] Remove serve_forever2/shutdown2 (reduces probability of #1085). I'm not even sure why `serve_forever2` and `shutdown2` were introduced in the first place... It probably follows from my misconception about the SocketServer module. After having studied the module again, I come to the conclusion that we can get rid of my crap, simultanously reducing probability of #1085 (hopefully down to 0)! `server_forever` is used throughout tests instead of `handle_request`, because `shutdown`, following docs, "must be called while serve_forever() is running in another thread, or it will deadlock", and our `probe_sni` HTTP request is already enough to kill single `handle_request`. We don't need to use any busy waiting block or `sleep` between serve and shutdown; studying CPython source code leads to the conclusion that the following construction is non-blocking: ```python import threading, SocketServer s = SocketServer.TCPServer(("", 0), None) t = threading.Thread(target=s.shutdown) t.start() s.serve_forever() # returns immediately t.join() # returns immediately ``` --- acme/acme/standalone.py | 28 ------------- acme/acme/standalone_test.py | 65 +++---------------------------- letsencrypt/plugins/standalone.py | 6 ++- 3 files changed, 10 insertions(+), 89 deletions(-) diff --git a/acme/acme/standalone.py b/acme/acme/standalone.py index 49759bc07..310e61995 100644 --- a/acme/acme/standalone.py +++ b/acme/acme/standalone.py @@ -4,7 +4,6 @@ import collections import functools import logging import os -import socket import sys import six @@ -50,37 +49,11 @@ class ACMEServerMixin: # pylint: disable=old-style-class server_version = "ACME client standalone challenge solver" allow_reuse_address = True - def __init__(self): - self._stopped = False - - def serve_forever2(self): - """Serve forever, until other thread calls `shutdown2`.""" - logger.debug("Starting server at %s:%d...", - *self.socket.getsockname()[:2]) - while not self._stopped: - self.handle_request() - - def shutdown2(self): - """Shutdown server loop from `serve_forever2`.""" - self._stopped = True - - # dummy request to terminate last server_forever2.handle_request() - sock = socket.socket() - try: - sock.connect(self.socket.getsockname()) - except socket.error: - pass # thread is probably already finished - finally: - sock.close() - - self.server_close() - class DVSNIServer(TLSServer, ACMEServerMixin): """DVSNI Server.""" def __init__(self, server_address, certs): - ACMEServerMixin.__init__(self) TLSServer.__init__( self, server_address, socketserver.BaseRequestHandler, certs=certs) @@ -89,7 +62,6 @@ class SimpleHTTPServer(BaseHTTPServer.HTTPServer, ACMEServerMixin): """SimpleHTTP Server.""" def __init__(self, server_address, resources): - ACMEServerMixin.__init__(self) BaseHTTPServer.HTTPServer.__init__( self, server_address, SimpleHTTPRequestHandler.partial_init( simple_http_resources=resources)) diff --git a/acme/acme/standalone_test.py b/acme/acme/standalone_test.py index 349581a3d..ed015b826 100644 --- a/acme/acme/standalone_test.py +++ b/acme/acme/standalone_test.py @@ -1,7 +1,6 @@ """Tests for acme.standalone.""" import os import shutil -import socket import threading import tempfile import time @@ -29,54 +28,6 @@ class TLSServerTest(unittest.TestCase): server.server_close() # pylint: disable=no-member -class ACMEServerMixinTest(unittest.TestCase): - """Tests for acme.standalone.ACMEServerMixin.""" - - def setUp(self): - from acme.standalone import ACMEServerMixin - - class _MockHandler(socketserver.BaseRequestHandler): - # pylint: disable=missing-docstring,no-member,no-init - - def handle(self): - self.request.sendall(b"DONE") - - class _MockServer(socketserver.TCPServer, ACMEServerMixin): - def __init__(self, *args, **kwargs): - socketserver.TCPServer.__init__(self, *args, **kwargs) - ACMEServerMixin.__init__(self) - - self.server = _MockServer(("", 0), _MockHandler) - - def _busy_wait(self): # pragma: no cover - # This function is used to avoid race conditions in tests, but - # not all of the functionality is always used, hence "no - # cover" - while True: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - # pylint: disable=no-member - sock.connect(self.server.socket.getsockname()) - except socket.error: - pass - else: - sock.recv(4) # wait until handle_request is actually called - break - finally: - sock.close() - time.sleep(1) - - def test_serve_shutdown(self): - thread = threading.Thread(target=self.server.serve_forever2) - thread.start() - self._busy_wait() - self.server.shutdown2() - - def test_shutdown2_not_running(self): - self.server.shutdown2() - self.server.shutdown2() - - class DVSNIServerTest(unittest.TestCase): """Test for acme.standalone.DVSNIServer.""" @@ -89,20 +40,16 @@ class DVSNIServerTest(unittest.TestCase): from acme.standalone import DVSNIServer self.server = DVSNIServer(("", 0), certs=self.certs) # pylint: disable=no-member - self.thread = threading.Thread(target=self.server.handle_request) + self.thread = threading.Thread(target=self.server.serve_forever) self.thread.start() def tearDown(self): - self.server.shutdown2() + self.server.shutdown() # pylint: disable=no-member self.thread.join() - def test_init(self): - # pylint: disable=protected-access - self.assertFalse(self.server._stopped) - - def test_dvsni(self): + def test_it(self): host, port = self.server.socket.getsockname()[:2] - cert = crypto_util.probe_sni(b'localhost', host=host, port=port) + cert = crypto_util.probe_sni(b'localhost', host=host, port=port, timeout=1) self.assertEqual(jose.ComparableX509(cert), jose.ComparableX509(self.certs[b'localhost'][1])) @@ -120,11 +67,11 @@ class SimpleHTTPServerTest(unittest.TestCase): # pylint: disable=no-member self.port = self.server.socket.getsockname()[1] - self.thread = threading.Thread(target=self.server.handle_request) + self.thread = threading.Thread(target=self.server.serve_forever) self.thread.start() def tearDown(self): - self.server.shutdown2() + self.server.shutdown() # pylint: disable=no-member self.thread.join() def test_index(self): diff --git a/letsencrypt/plugins/standalone.py b/letsencrypt/plugins/standalone.py index 1b56fbdbc..ccff03319 100644 --- a/letsencrypt/plugins/standalone.py +++ b/letsencrypt/plugins/standalone.py @@ -72,7 +72,9 @@ class ServerManager(object): except socket.error as error: raise errors.StandaloneBindError(error, port) - thread = threading.Thread(target=server.serve_forever2) + thread = threading.Thread( + # pylint: disable=no-member + target=server.serve_forever) thread.start() # if port == 0, then random free port on OS is taken @@ -90,7 +92,7 @@ class ServerManager(object): instance = self._instances[port] logger.debug("Stopping server at %s:%d...", *instance.server.socket.getsockname()[:2]) - instance.server.shutdown2() + instance.server.shutdown() instance.thread.join() del self._instances[port] From 05be568e9eda7227ac9b31b36c3d9a4cebb9e455 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Thu, 29 Oct 2015 15:18:48 -0700 Subject: [PATCH 31/74] added todo notes --- letsencrypt-apache/letsencrypt_apache/configurator.py | 1 + letsencrypt/cli.py | 1 + letsencrypt/client.py | 1 + 3 files changed, 3 insertions(+) diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index d376fe4b6..47e748f9e 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -219,6 +219,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.parser.add_dir( vhost.path, "SSLCertificateChainFile", chain_path) else: + # TODO: THIS??? self.aug.set(path["chain_path"][-1], chain_path) # Save notes about the transaction that took place diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 1b396b0b8..e2c11ad98 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -468,6 +468,7 @@ def install(args, config, plugins): # XXX: Update for renewer/RenewableCert try: + #TODO: maybe an error? not choose_configurator_plugins? installer, _ = choose_configurator_plugins(args, config, plugins, "auth") except PluginSelectionError, e: return e.message diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 732bdcf03..0517abc2b 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -334,6 +334,7 @@ class Client(object): key_path=os.path.abspath(privkey_path), chain_path=chain_path, fullchain_path=fullchain_path) + # TODO: call self.installer.save() << in progress save self.installer.save("Deployed Let's Encrypt Certificate") # sites may have been enabled / final cleanup From 724d06eec5845fe2adb4d61ed6858fb305dd74a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mantas=20Mikul=C4=97nas?= Date: Fri, 30 Oct 2015 17:11:55 +0200 Subject: [PATCH 32/74] bootstrap: use a proper dependency test for Arch `pacman -T` exists for this exact purpose; it respects provides without having to manually code them into the script. --- bootstrap/_arch_common.sh | 42 +++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/bootstrap/_arch_common.sh b/bootstrap/_arch_common.sh index 6895addf4..f66067ffb 100755 --- a/bootstrap/_arch_common.sh +++ b/bootstrap/_arch_common.sh @@ -1,29 +1,27 @@ #!/bin/sh # Tested with: -# - Manjaro 15.09 (x86_64) # - ArchLinux (x86_64) - -# Both "gcc-multilib" and "gcc" packages provide gcc. If user already has -# "gcc-multilib" installed, let's stick to their choice -if pacman -Qc gcc-multilib &>/dev/null -then - GCC_PACKAGE="gcc-multilib"; -else - GCC_PACKAGE="gcc"; -fi - +# # "python-virtualenv" is Python3, but "python2-virtualenv" provides # only "virtualenv2" binary, not "virtualenv" necessary in # ./bootstrap/dev/_common_venv.sh -pacman -S --needed \ - git \ - python2 \ - python-virtualenv \ - "$GCC_PACKAGE" \ - dialog \ - augeas \ - openssl \ - libffi \ - ca-certificates \ - pkg-config \ + +deps=" + git + python2 + python-virtualenv + gcc + dialog + augeas + openssl + libffi + ca-certificates + pkg-config +" + +missing=$(pacman -T $deps) + +if [ "$missing" ]; then + pacman -S --needed $missing +fi From edabce9a96bd2a91a84e6f91d5e729808d4ceea4 Mon Sep 17 00:00:00 2001 From: Steve Desmond Date: Fri, 30 Oct 2015 14:47:23 -0400 Subject: [PATCH 33/74] improved language consistency in error/help messages --- letsencrypt/auth_handler.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 5f6365167..4f2a6fa3e 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -486,29 +486,29 @@ def is_preferred(offered_challb, satisfied, _ERROR_HELP_COMMON = ( "To fix these errors, please make sure that your domain name was entered " - "correctly and the DNS A record(s) for that domain contains the " + "correctly and the DNS A record(s) for that domain contain(s) the " "right IP address.") _ERROR_HELP = { "connection": _ERROR_HELP_COMMON + " Additionally, please check that your computer " - "has publicly routable IP address and no firewalls are preventing the " - "server from communicating with the client.", + "has a publicly routable IP address and that no firewalls are preventing " + "the server from communicating with the client.", "dnssec": _ERROR_HELP_COMMON + " Additionally, if you have DNSSEC enabled for " - "your domain, please ensure the signature is valid.", + "your domain, please ensure that the signature is valid.", "malformed": "To fix these errors, please make sure that you did not provide any " - "invalid information to the client and try running Let's Encrypt " + "invalid information to the client, and try running Let's Encrypt " "again.", "serverInternal": "Unfortunately, an error on the ACME server prevented you from completing " "authorization. Please try again later.", "tls": - _ERROR_HELP_COMMON + " Additionally, please check that you have an up " - "to date TLS configuration that allows the server to communicate with " - "the Let's Encrypt client.", + _ERROR_HELP_COMMON + " Additionally, please check that you have an " + "up-to-date TLS configuration that allows the server to communicate " + "with the Let's Encrypt client.", "unauthorized": _ERROR_HELP_COMMON, "unknownHost": _ERROR_HELP_COMMON, } From 40706e294707e4b864df25f3122a5999ffce1095 Mon Sep 17 00:00:00 2001 From: Steve Desmond Date: Fri, 30 Oct 2015 14:52:36 -0400 Subject: [PATCH 34/74] add "--" to CLI arg for consistency --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 4f488e2a2..6a3c39e54 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -128,7 +128,7 @@ Plugin A I Notes and status ========== = = ================================================================ standalone Y N Very stable. Uses port 80 (force by ``--standalone-supported-challenges simpleHttp``) or 443 - (force by ``standalone-supported-challenges dvsni``). + (force by ``--standalone-supported-challenges dvsni``). 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 From 537fcf581c267e1bb2628adc5363d97746772bde Mon Sep 17 00:00:00 2001 From: Alexander Mankuta Date: Fri, 30 Oct 2015 21:17:02 +0200 Subject: [PATCH 35/74] Add Gentoo bootstrapping Includes support for all three major package managers. --- bootstrap/_gentoo_common.sh | 23 +++++++++++++++++++++++ bootstrap/gentoo.sh | 1 + bootstrap/install-deps.sh | 3 +++ letsencrypt-auto | 3 +++ 4 files changed, 30 insertions(+) create mode 100755 bootstrap/_gentoo_common.sh create mode 120000 bootstrap/gentoo.sh diff --git a/bootstrap/_gentoo_common.sh b/bootstrap/_gentoo_common.sh new file mode 100755 index 000000000..a718db7ff --- /dev/null +++ b/bootstrap/_gentoo_common.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +PACKAGES="dev-vcs/git + dev-lang/python:2.7 + dev-python/virtualenv + dev-util/dialog + app-admin/augeas + dev-libs/openssl + dev-libs/libffi + app-misc/ca-certificates + virtual/pkgconfig" + +case "$PACKAGE_MANAGER" in + (paludis) + cave resolve --keep-targets if-possible $PACKAGES -x + ;; + (pkgcore) + pmerge --noreplace $PACKAGES + ;; + (portage|*) + emerge --noreplace $PACKAGES + ;; +esac diff --git a/bootstrap/gentoo.sh b/bootstrap/gentoo.sh new file mode 120000 index 000000000..125d6a592 --- /dev/null +++ b/bootstrap/gentoo.sh @@ -0,0 +1 @@ +_gentoo_common.sh \ No newline at end of file diff --git a/bootstrap/install-deps.sh b/bootstrap/install-deps.sh index c159858c5..3cb0fc274 100755 --- a/bootstrap/install-deps.sh +++ b/bootstrap/install-deps.sh @@ -23,6 +23,9 @@ elif [ -f /etc/arch-release ] ; then elif [ -f /etc/redhat-release ] ; then echo "Bootstrapping dependencies for RedHat-based OSes..." $SUDO $BOOTSTRAP/_rpm_common.sh +elif [ -f /etc/gentoo-release ] ; then + echo "Bootstrapping dependencies for Gentoo-based OSes..." + $SUDO $BOOTSTRAP/_gentoo_common.sh elif uname | grep -iq FreeBSD ; then echo "Bootstrapping dependencies for FreeBSD..." $SUDO $BOOTSTRAP/freebsd.sh diff --git a/letsencrypt-auto b/letsencrypt-auto index 769f5a1d9..aba8baec1 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -47,6 +47,9 @@ then elif [ -f /etc/redhat-release ] ; then echo "Bootstrapping dependencies for RedHat-based OSes..." $SUDO $BOOTSTRAP/_rpm_common.sh + elif [ -f /etc/gentoo-release ] ; then + echo "Bootstrapping dependencies for Gentoo-based OSes..." + $SUDO $BOOTSTRAP/_gentoo_common.sh elif uname | grep -iq FreeBSD ; then echo "Bootstrapping dependencies for FreeBSD..." $SUDO $BOOTSTRAP/freebsd.sh From fbd09ddbf61451b768a936154d064f66817d5f79 Mon Sep 17 00:00:00 2001 From: Noah Swartz Date: Fri, 30 Oct 2015 13:24:55 -0700 Subject: [PATCH 36/74] added interesticial saves to apache changes which fixed bug when multiple vhosts were specified --- letsencrypt-apache/letsencrypt_apache/configurator.py | 1 - letsencrypt/cli.py | 1 - letsencrypt/client.py | 2 +- letsencrypt/tests/client_test.py | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index 47e748f9e..d376fe4b6 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -219,7 +219,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.parser.add_dir( vhost.path, "SSLCertificateChainFile", chain_path) else: - # TODO: THIS??? self.aug.set(path["chain_path"][-1], chain_path) # Save notes about the transaction that took place diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index e2c11ad98..1b396b0b8 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -468,7 +468,6 @@ def install(args, config, plugins): # XXX: Update for renewer/RenewableCert try: - #TODO: maybe an error? not choose_configurator_plugins? installer, _ = choose_configurator_plugins(args, config, plugins, "auth") except PluginSelectionError, e: return e.message diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 0517abc2b..937526bbb 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -334,7 +334,7 @@ class Client(object): key_path=os.path.abspath(privkey_path), chain_path=chain_path, fullchain_path=fullchain_path) - # TODO: call self.installer.save() << in progress save + self.installer.save() self.installer.save("Deployed Let's Encrypt Certificate") # sites may have been enabled / final cleanup diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 3f7b84a64..0f8b2df61 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -159,7 +159,7 @@ class ClientTest(unittest.TestCase): domain='foo.bar', fullchain_path='fullchain', key_path=os.path.abspath("key")) - self.assertEqual(installer.save.call_count, 1) + self.assertEqual(installer.save.call_count, 2) installer.restart.assert_called_once_with() @mock.patch("letsencrypt.client.enhancements") From 20ae2debe45ce615f1948786ee58a805b48293c3 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 30 Oct 2015 20:50:55 +0000 Subject: [PATCH 37/74] Docs: --a -> -a (fixes #1217) --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 6a3c39e54..73c3fe140 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -135,7 +135,7 @@ webroot Y N Works with already running webserver, by writing necessary files to the disk (``--webroot-path`` should be pointed to your ``public_html``). Currently, when multiple domains are specified (`-d`), they must all use the same web root path. -manual Y N Hidden from standard UI, use with ``--a manual``. Requires to +manual Y N Hidden from standard UI, use with ``-a manual``. Requires to copy and paste commands into a new terminal session. Allows to run client on machine different than target webserver, e.g. your laptop. From 4e62a4bfe2465817c1b47a59c2cef00d98eac333 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 30 Oct 2015 13:54:46 -0700 Subject: [PATCH 38/74] fixes #1227 --- letsencrypt/cli.py | 4 +++- letsencrypt/tests/cli_test.py | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 641d0d341..fdfa5bbfb 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -85,6 +85,7 @@ More detailed help: plugins (certonly, install, nginx, apache, standalone, etc) """ + def usage_strings(plugins): """Make usage strings late so that plugins can be initialised late""" if "nginx" in plugins: @@ -494,7 +495,8 @@ def install(args, config, plugins): # XXX: Update for renewer/RenewableCert try: - installer, _ = choose_configurator_plugins(args, config, plugins, "certonly") + installer, _ = choose_configurator_plugins(args, config, + plugins, "install") except PluginSelectionError, e: return e.message diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 669fa1ce8..55946e7aa 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -106,6 +106,12 @@ class CLITest(unittest.TestCase): from letsencrypt import cli self.assertTrue(cli.usage_strings(plugins)[0] in out) + @mock.patch('letsencrypt.cli.display_ops') + def test_installer_selection(self, mock_display_ops): + 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) + def test_configurator_selection(self): real_plugins = disco.PluginsRegistry.find_all() args = ['--agree-dev-preview', '--apache', @@ -221,7 +227,7 @@ class CLITest(unittest.TestCase): @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_pick_installer, mock_notAfter): + mock_pick_installer, mock_notAfter): cert_path = '/etc/letsencrypt/live/blahcert.pem' date = '1970-01-01' mock_notAfter().date.return_value = date From fa7aed4d63184e0e8a428cb03ae5c0794103e06e Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 30 Oct 2015 14:44:17 -0700 Subject: [PATCH 39/74] Ensure that mandatory flags are displayed under the relevant verb help topics Closes: #996 In part our problem was trying to pick a single topic ("paths") for flags that become essential in some cases. But we were also calling _paths_parser before all the topic groups had been created. --- letsencrypt/cli.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 641d0d341..a2d3652b0 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -650,12 +650,12 @@ class HelpfulArgumentParser(object): help1 = self.prescan_for_flag("-h", self.help_topics) help2 = self.prescan_for_flag("--help", self.help_topics) assert max(True, "a") == "a", "Gravity changed direction" - help_arg = max(help1, help2) - if help_arg is True: + self.help_arg = max(help1, help2) + if self.help_arg is True: # just --help with no topic; avoid argparse altogether print usage sys.exit(0) - self.visible_topics = self.determine_help_topics(help_arg) + self.visible_topics = self.determine_help_topics(self.help_arg) #print self.visible_topics self.groups = {} # elements are added by .add_group() @@ -873,12 +873,12 @@ def prepare_and_parse_args(plugins, args): help="Require that all configuration files are owned by the current " "user; only needed if your config is somewhere unsafe like /tmp/") + _create_subparsers(helpful) _paths_parser(helpful) # _plugins_parsing should be the last thing to act upon the main # parser (--help should display plugin-specific options last) _plugins_parsing(helpful, plugins) - _create_subparsers(helpful) return helpful.parse_args() @@ -914,19 +914,28 @@ def _create_subparsers(helpful): def _paths_parser(helpful): add = helpful.add verb = helpful.verb + if verb == "help": + verb = helpful.help_arg helpful.add_group( "paths", description="Arguments changing execution paths & servers") - cph = "Path to where cert is saved (with auth), installed (with install --csr) or revoked." + cph = "Path to where cert is saved (with auth --csr), installed from or revoked." + section = "paths" + if verb in ("install", "revoke", "certonly"): + section = verb if verb == "certonly": - add("paths", "--cert-path", default=flag_default("auth_cert_path"), help=cph) + add(section, "--cert-path", default=flag_default("auth_cert_path"), help=cph) elif verb == "revoke": - add("paths", "--cert-path", type=read_file, required=True, help=cph) + add(section, "--cert-path", type=read_file, required=True, help=cph) else: - add("paths", "--cert-path", help=cph, required=(verb == "install")) + add(section, "--cert-path", help=cph, required=(verb == "install")) + 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("paths", "--key-path", type=((verb == "revoke" and read_file) or str), + add(section, "--key-path", type=((verb == "revoke" and read_file) or str), required=(verb == "install"), help="Path to private key for cert creation or revocation (if account key is missing)") From 3356ce75586ff79c6f6d4e24f0645b5269464c0b Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 30 Oct 2015 14:55:51 -0700 Subject: [PATCH 40/74] This is a pretty silly lint warning that we're hitting a lot --- .pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index 268d61ec6..e882d0858 100644 --- a/.pylintrc +++ b/.pylintrc @@ -38,7 +38,7 @@ load-plugins=linter_plugin # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=fixme,locally-disabled,abstract-class-not-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name +disable=fixme,locally-disabled,abstract-class-not-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name,too-many-instance-attributes # abstract-class-not-used cannot be disabled locally (at least in pylint 1.4.1) From e4b10f76f9e491f0008ea7a10b8ffe05f62f85a9 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 30 Oct 2015 14:56:08 -0700 Subject: [PATCH 41/74] Delint --- letsencrypt/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index a2d3652b0..3477e4eb4 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -915,14 +915,14 @@ def _paths_parser(helpful): add = helpful.add verb = helpful.verb if verb == "help": - verb = helpful.help_arg + verb = helpful.help_arg helpful.add_group( "paths", description="Arguments changing execution paths & servers") cph = "Path to where cert is saved (with auth --csr), installed from or revoked." section = "paths" if verb in ("install", "revoke", "certonly"): - section = verb + section = verb if verb == "certonly": add(section, "--cert-path", default=flag_default("auth_cert_path"), help=cph) elif verb == "revoke": @@ -932,7 +932,7 @@ def _paths_parser(helpful): section = "paths" if verb in ("install", "revoke"): - section = verb + 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), From f00280f71adda2fb3957c5c3a35144fb41988853 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 30 Oct 2015 15:12:54 -0700 Subject: [PATCH 42/74] Testiness --- letsencrypt/tests/cli_test.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 669fa1ce8..2f12f055d 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -101,6 +101,24 @@ class CLITest(unittest.TestCase): self.assertTrue("Plugin options" in out) output.truncate(0) + self.assertRaises(SystemExit, self._call_stdout, ['--help', 'install']) + out = output.getvalue() + self.assertTrue("--cert-path" in out) + self.assertTrue("--key-path" in out) + output.truncate(0) + + self.assertRaises(SystemExit, self._call_stdout, ['--help', 'revoke']) + out = output.getvalue() + self.assertTrue("--cert-path" in out) + self.assertTrue("--key-path" in out) + output.truncate(0) + + self.assertRaises(SystemExit, self._call_stdout, ['--help', 'config_changes']) + out = output.getvalue() + self.assertTrue("--cert-path" not in out) + self.assertTrue("--key-path" not in out) + output.truncate(0) + self.assertRaises(SystemExit, self._call_stdout, ['-h']) out = output.getvalue() from letsencrypt import cli From a5e815008e43bfd904e2bc73469a20c6604192fe Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 30 Oct 2015 15:22:05 -0700 Subject: [PATCH 43/74] Factor out all the stdout wrangling from help tests --- letsencrypt/tests/cli_test.py | 88 ++++++++++++++++------------------- 1 file changed, 40 insertions(+), 48 deletions(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 2f12f055d..e38448249 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -64,65 +64,57 @@ class CLITest(unittest.TestCase): self._call([]) self.assertEqual(1, mock_run.call_count) - def test_help(self): - self.assertRaises(SystemExit, self._call, ['--help']) - self.assertRaises(SystemExit, self._call, ['--help', 'all']) + def _help_output(self, args): + "Run a help command, and return the help string for scrutiny" output = StringIO.StringIO() with mock.patch('letsencrypt.cli.sys.stdout', new=output): plugins = disco.PluginsRegistry.find_all() - self.assertRaises(SystemExit, self._call_stdout, ['--help', 'all']) + self.assertRaises(SystemExit, self._call_stdout, args) out = output.getvalue() - self.assertTrue("--configurator" in out) - self.assertTrue("how a cert is deployed" in out) - self.assertTrue("--manual-test-mode" in out) - output.truncate(0) + return out - self.assertRaises(SystemExit, self._call_stdout, ['-h', 'nginx']) - out = output.getvalue() - if "nginx" in plugins: - # may be false while building distributions without plugins - self.assertTrue("--nginx-ctl" in out) - self.assertTrue("--manual-test-mode" not in out) - self.assertTrue("--checkpoints" not in out) - output.truncate(0) + def test_help(self): + self.assertRaises(SystemExit, self._call, ['--help']) + self.assertRaises(SystemExit, self._call, ['--help', 'all']) + plugins = disco.PluginsRegistry.find_all() + out = self._help_output(['--help', 'all']) + self.assertTrue("--configurator" in out) + self.assertTrue("how a cert is deployed" in out) + self.assertTrue("--manual-test-mode" in out) - self.assertRaises(SystemExit, self._call_stdout, ['-h']) - out = output.getvalue() - if "nginx" in plugins: - self.assertTrue("Use the Nginx plugin" in out) - else: - self.assertTrue("(nginx support is experimental" in out) - output.truncate(0) + out = self._help_output(['-h', 'nginx']) + if "nginx" in plugins: + # may be false while building distributions without plugins + self.assertTrue("--nginx-ctl" in out) + self.assertTrue("--manual-test-mode" not in out) + self.assertTrue("--checkpoints" not in out) - self.assertRaises(SystemExit, self._call_stdout, ['--help', 'plugins']) - out = output.getvalue() - self.assertTrue("--manual-test-mode" not in out) - self.assertTrue("--prepare" in out) - self.assertTrue("Plugin options" in out) - output.truncate(0) + out = self._help_output(['-h']) + if "nginx" in plugins: + self.assertTrue("Use the Nginx plugin" in out) + else: + self.assertTrue("(nginx support is experimental" in out) - self.assertRaises(SystemExit, self._call_stdout, ['--help', 'install']) - out = output.getvalue() - self.assertTrue("--cert-path" in out) - self.assertTrue("--key-path" in out) - output.truncate(0) + out = self._help_output(['--help', 'plugins']) + self.assertTrue("--manual-test-mode" not in out) + self.assertTrue("--prepare" in out) + self.assertTrue("Plugin options" in out) - self.assertRaises(SystemExit, self._call_stdout, ['--help', 'revoke']) - out = output.getvalue() - self.assertTrue("--cert-path" in out) - self.assertTrue("--key-path" in out) - output.truncate(0) + out = self._help_output(['--help', 'install']) + self.assertTrue("--cert-path" in out) + self.assertTrue("--key-path" in out) - self.assertRaises(SystemExit, self._call_stdout, ['--help', 'config_changes']) - out = output.getvalue() - self.assertTrue("--cert-path" not in out) - self.assertTrue("--key-path" not in out) - output.truncate(0) + out = self._help_output(['--help', 'revoke']) + self.assertTrue("--cert-path" in out) + self.assertTrue("--key-path" in out) - self.assertRaises(SystemExit, self._call_stdout, ['-h']) - out = output.getvalue() - from letsencrypt import cli - self.assertTrue(cli.usage_strings(plugins)[0] in out) + out = self._help_output(['-h', 'config_changes']) + self.assertTrue("--cert-path" not in out) + 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) def test_configurator_selection(self): real_plugins = disco.PluginsRegistry.find_all() From d5ccbdbcd29d9b79ba11d42f15c9661d7d60ab24 Mon Sep 17 00:00:00 2001 From: Dev & Sec Date: Fri, 30 Oct 2015 22:54:10 +0000 Subject: [PATCH 44/74] use `printf` instead of `echo -n` for better portability --- bootstrap/venv.sh | 6 +++--- letsencrypt-auto | 10 +++++----- letsencrypt/plugins/manual.py | 7 +++---- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/bootstrap/venv.sh b/bootstrap/venv.sh index ce31e6703..6ca082446 100755 --- a/bootstrap/venv.sh +++ b/bootstrap/venv.sh @@ -25,9 +25,9 @@ pip install -U letsencrypt letsencrypt-apache # letsencrypt-nginx echo echo "Congratulations, Let's Encrypt has been successfully installed/updated!" echo -echo -n "Your prompt should now be prepended with ($VENV_NAME). Next " -echo -n "time, if the prompt is different, 'source' this script again " -echo -n "before running 'letsencrypt'." +printf "%s" "Your prompt should now be prepended with ($VENV_NAME). Next " +printf "time, if the prompt is different, 'source' this script again " +printf "before running 'letsencrypt'." echo echo echo "You can now run 'letsencrypt --help'." diff --git a/letsencrypt-auto b/letsencrypt-auto index 769f5a1d9..41d2e4c97 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -70,7 +70,7 @@ then fi fi -echo -n "Updating letsencrypt and virtual environment dependencies..." +printf "Updating letsencrypt and virtual environment dependencies..." if [ "$VERBOSE" = 1 ] ; then echo $VENV_BIN/pip install -U setuptools @@ -83,15 +83,15 @@ if [ "$VERBOSE" = 1 ] ; then fi else $VENV_BIN/pip install -U setuptools > /dev/null - echo -n . + printf . $VENV_BIN/pip install -U pip > /dev/null - echo -n . + printf . # nginx is buggy / disabled for now... $VENV_BIN/pip install -U letsencrypt > /dev/null - echo -n . + printf . $VENV_BIN/pip install -U letsencrypt-apache > /dev/null if $VENV_BIN/pip freeze | grep -q letsencrypt-nginx ; then - echo -n . + printf . $VENV_BIN/pip install -U letsencrypt-nginx > /dev/null fi echo diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index 866965200..9d1857edd 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -70,7 +70,7 @@ Are you OK with your IP being logged? CMD_TEMPLATE = """\ mkdir -p {root}/public_html/{response.URI_ROOT_PATH} cd {root}/public_html -echo -n {validation} > {response.URI_ROOT_PATH}/{encoded_token} +printf "%s" "{validation}" > {response.URI_ROOT_PATH}/{encoded_token} # run only once per server: $(command -v python2 || command -v python2.7 || command -v python2.6) -c \\ "import BaseHTTPServer, SimpleHTTPServer; \\ @@ -142,15 +142,14 @@ s.serve_forever()" """ ct=response.CONTENT_TYPE, port=port) if self.conf("test-mode"): logger.debug("Test mode. Executing the manual command: %s", command) - # sh shipped with OS X does't support echo -n - executable = "/bin/bash" if sys.platform == "darwin" else None + # sh shipped with OS X does't support echo -n, but supports printf try: self._httpd = subprocess.Popen( command, # don't care about setting stdout and stderr, # we're in test mode anyway shell=True, - executable=executable, + executable=None, # "preexec_fn" is UNIX specific, but so is "command" preexec_fn=os.setsid) except OSError as error: # ValueError should not happen! From 69365a7a0608731ad484fc1d91cbcc93935578ec Mon Sep 17 00:00:00 2001 From: Dev & Sec Date: Fri, 30 Oct 2015 23:27:16 +0000 Subject: [PATCH 45/74] fix drop quotes issue --- letsencrypt/plugins/manual.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index 9d1857edd..c76463f85 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -70,7 +70,7 @@ Are you OK with your IP being logged? CMD_TEMPLATE = """\ mkdir -p {root}/public_html/{response.URI_ROOT_PATH} cd {root}/public_html -printf "%s" "{validation}" > {response.URI_ROOT_PATH}/{encoded_token} +printf "%s" {validation} > {response.URI_ROOT_PATH}/{encoded_token} # run only once per server: $(command -v python2 || command -v python2.7 || command -v python2.6) -c \\ "import BaseHTTPServer, SimpleHTTPServer; \\ From 8558ad38605bdca702f5ea21d4e0073ccc6ef4ed Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 30 Oct 2015 16:53:04 -0700 Subject: [PATCH 46/74] Added comment about installer.save() --- letsencrypt/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index a3472cc59..6eb33f3fe 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -326,7 +326,7 @@ class Client(object): key_path=os.path.abspath(privkey_path), chain_path=chain_path, fullchain_path=fullchain_path) - self.installer.save() + self.installer.save() # needed by the Apache plugin self.installer.save("Deployed Let's Encrypt Certificate") # sites may have been enabled / final cleanup From f8185c1913610c4911d03e2512c1d0b7d59fb774 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Oct 2015 11:47:25 +0000 Subject: [PATCH 47/74] Add Python 2.6 setup.py classifiers. --- acme/setup.py | 1 + letsencrypt-apache/setup.py | 1 + letsencrypt-compatibility-test/setup.py | 1 + letsencrypt-nginx/setup.py | 1 + letshelp-letsencrypt/setup.py | 1 + 5 files changed, 5 insertions(+) diff --git a/acme/setup.py b/acme/setup.py index 2495a42ff..a6551a023 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -58,6 +58,7 @@ setup( 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python', 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py index 2180219f1..e4dd11935 100644 --- a/letsencrypt-apache/setup.py +++ b/letsencrypt-apache/setup.py @@ -41,6 +41,7 @@ setup( 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', diff --git a/letsencrypt-compatibility-test/setup.py b/letsencrypt-compatibility-test/setup.py index 1608769e2..c791d51c4 100644 --- a/letsencrypt-compatibility-test/setup.py +++ b/letsencrypt-compatibility-test/setup.py @@ -38,6 +38,7 @@ setup( 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python', 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py index 5d80807d1..a669ad841 100644 --- a/letsencrypt-nginx/setup.py +++ b/letsencrypt-nginx/setup.py @@ -41,6 +41,7 @@ setup( 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', diff --git a/letshelp-letsencrypt/setup.py b/letshelp-letsencrypt/setup.py index a63e0aad4..04b879e14 100644 --- a/letshelp-letsencrypt/setup.py +++ b/letshelp-letsencrypt/setup.py @@ -34,6 +34,7 @@ setup( 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', From d5a2c7fa18f8dce9f08e0bfe25fb7b06c74ac980 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Oct 2015 11:48:19 +0000 Subject: [PATCH 48/74] We don't use lsb-release any more --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 862e963b1..8586c3840 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,7 +39,6 @@ addons: mariadb: "10.0" apt: packages: # keep in sync with bootstrap/ubuntu.sh and Boulder - - lsb-release - python - python-dev - python-virtualenv From bd3d373d99fc1f8c58d5d0c0770d4b010cc125ba Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Oct 2015 11:48:33 +0000 Subject: [PATCH 49/74] Fix docs for deps.sh --- tools/deps.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/deps.sh b/tools/deps.sh index 28bfdaff5..6fb2bf63b 100755 --- a/tools/deps.sh +++ b/tools/deps.sh @@ -2,9 +2,9 @@ # # Find all Python imports. # -# ./deps.sh letsencrypt -# ./deps.sh acme -# ./deps.sh letsencrypt-apache +# ./tools/deps.sh letsencrypt +# ./tools/deps.sh acme +# ./tools/deps.sh letsencrypt-apache # ... # # Manually compare the output with deps in setup.py. From d019316ed37b2fd232e990f5ea0d7cdb69716bf7 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Sat, 31 Oct 2015 08:52:49 -0700 Subject: [PATCH 50/74] Documentation on our plans for ciphersuites --- docs/ciphers.rst | 197 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 docs/ciphers.rst diff --git a/docs/ciphers.rst b/docs/ciphers.rst new file mode 100644 index 000000000..ea4d328c3 --- /dev/null +++ b/docs/ciphers.rst @@ -0,0 +1,197 @@ +============ +Ciphersuites +============ + +.. contents:: Table of Contents + :local: + + +.. _ciphersuites: + +Introduction +============ + +Autoupdates +----------- + +Within certain limits, TLS server software can choose what kind of +cryptography to use when a client connects. These choices can affect +security, compatibility, and performance in complex ways. Most of +these options are independent of a particular certificate. The Let's +Encrypt client tries to provide defaults that we think are most useful +to our users. + +As described below, the Let's Encrypt client will default to modifying +server software's cryptographic settings to keep these up-to-date with +what we think are appropriate defaults when new versions of the Let's +Encrypt client are installed (for example, by an operating system package +manager). + +When this feature is implemented, this document will be updated +to describe how to disable these automatic changes. + + +Cryptographic choices +--------------------- + +Software that uses cryptography must inevitably make choices about what +kind of cryptography to use and how. These choices entail assumptions +about how well particular cryptographic mechanisms resist attack, and what +trade-offs are available and appropriate. The choices are constrained +by compatibility issues (in order to interoperate with other software, +an implementation must agree to use cryptographic mechanisms that the +other side also supports) and protocol issues (cryptographic mechanisms +must be specified in protocols and there must be a way to agree to use +them in a particular context). + +The best choices for a particular application may change over time in +response to new research, new standardization events, changes in computer +hardware, and changes in the prevalence of legacy software. Much important +research on cryptanalysis and cryptographic vulnerabilities is unpublished +because many researchers have been working in the interest of improving +some entities' communications security while weakening, or failing to +improve, others' security. But important information that improves our +understanding of the state of the art is published regularly. + +When enabling TLS support in a compatible web server (which is a separate +step from obtaining a certificate), Let's Encrypt has the ability to +update that web server's TLS configuration. Again, this is *different +from the cryptographic particulars of the certificate itself*; the +certificate as of the initial release will be RSA-signed using one of +Let's Encrypt's 2048-bit RSA keys, and will describe the subscriber's +RSA public key ("subject public key") of at least 2048 bits, which is +used for key establishment. + +Note that the subscriber's RSA public key can be used in a wide variety +of key establishment methods, most of which do not use RSA directly +for key exchange, but only for authenticating the server! For example, +in DHE and ECDHE key exchanges, the subject public key is just used to +sign other parameters for authentication. You do not have to "use RSA" +for other purposes just because you're using an RSA key for authentication. + +The certificate doesn't specify other cryptographic or ciphersuite +particulars; for example, it doesn't say whether or not parties should +use a particular symmetric algorithm like 3DES, or what cipher modes +they should use. All of these details are negotiated between client +and server independent of the content of the ciphersuite. The +Let's Encrypt project hopes to provide useful defaults that reflect +good security choices with respect to the publicly-known state of the +art. However, the Let's Encrypt certificate authority does *not* +dictate end-users' security policy, and any site is welcome to change +its preferences in accordance with its own policy or its administrators' +preferences, and use different cryptographic mechanisms or parameters, +or a different priority order, than the defaults provided by the Let's +Encrypt client. + +If you don't use the Let's Encrypt client to configure your server +directly, because the client doesn't integrate with your server software +or because you chose not to use this integration, then the cryptographic +defaults haven't been modified, and the cryptography chosen by the server +will still be whatever the default for your software was. For example, +if you obtain a certificate using *standalone* mode and then manually +install it in an IMAP or LDAP server, your cryptographic settings will +not be modified by the client in any way. + + +Sources of defaults +------------------- + +Initially, the Let's Encrypt client will configure users' servers to +use the cryptographic defaults recommended by the Mozilla project. +These settings are well-reasoned recommendations that carefully +consider client software compatibility. They are described at + +https://wiki.mozilla.org/Security/Server_Side_TLS + +and the version implemented by the Let's Encrypt client will be the +version that was most current as of the release date of each client +version. Mozilla offers three seperate sets of cryptographic options, +which trade off security and compatibility differently. These are +referred to as as the "Modern", "Intermediate", and "Old" configurations +(in order from most secure to least secure, and least-backwards compatible +to most-backwards compatible). The client will follow the Mozilla defaults +for the *Intermediate* configuration by default, at least with regards to +ciphersuites and TLS versions. Mozilla's web site describes which client +software will be compatible with each configuration. You can also use +the Qualys SSL Labs site, which the Let's Encrypt software will suggest +when installing a certificate, to test your server and see whether it +will be compatible with particular software versions. + +It will be possible to ask the Let's Encrypt client to instead apply +(and track) Modern or Old configurations. + +The Let's Encrypt project expects to follow the Mozilla recommendations +in the future as those recommendations are updated. (For example, some +users have proposed prioritizing a new ciphersuite known as ``0xcc13`` +which uses the ChaCha and Poly1305 algorithms, and which is already +implemented by the Chrome browser. Mozilla has delayed recommending +``0xcc13`` over compatibility and standardization concerns, but is likely +to recommend it in the future once these concerns have been addressed. At +that point, the Let's Encrypt client would likely follow the Mozilla +recommendations and favor the use of this ciphersuite as well.) + +The Let's Encrypt project may deviate from the Mozilla recommendations +in the future if good cause is shown and we believe our users' +priorities would be well-served by doing so. In general, please address +relevant proposals for changing priorities to the Mozilla security +team first, before asking the Let's Encrypt project to change the +client's priorities. The Mozilla security team is likely to have more +resources and expertise to bring to bear on evaluating reasons why its +recommendations should be updated. + +The Let's Encrpyt project will entertain proposals to create a *very* +small number of alternative configurations (apart from Modern, +Intermediate, and Old) that there's reason to believe would be widely +used by sysadmins; this would usually be a preferable course to modifying +an existing configuration. For example, if many sysadmins want their +servers configured to track a different expert recommendation, Let's +Encrypt could add an option to do so. + + +Resources for recommendations +----------------------------- + +In the course of considering how to handle this issue, we received +recommendations with sources of expert guidance on ciphersuites and other +cryptographic parameters. We're grateful to everyone who contributed +suggestions. The recommendations we received are available at + +https://github.com/letsencrypt/letsencrypt/wiki/Ciphersuite-guidance + +Let's Encrypt client users are welcome to review these authorities to +better inform their own cryptographic parameter choices. We also +welcome suggestions of other resources to add to this list. Please keep +in mind that different recommendations may reflect different priorities +or evaluations of trade-offs, especially related to compatibility! + + +Changing your settings +---------------------- + +This will probably look something like + +..code-block: shell + + letsencrypt --cipher-recommendations mozilla-secure + letsencrypt --cipher-recommendations mozilla-intermediate + letsencrypt --cipher-recommendations mozilla-old + +to track Mozilla's *Secure*, *Intermediate*, or *Old* recommendations, +and + +..code-block: shell + + letsencrypt --update-ciphers on + +to enable updating ciphers with each new Let's Encrypt client release, +or + +..code-block: shell + + letsencrypt --update-ciphers off + +to disable automatic configuration updates. These features have not yet +been implemented and this syntax may change then they are implemented. +The status of this feature is tracked as issue #1123 in our bug tracker. + +https://github.com/letsencrypt/letsencrypt/issues/1123 From f37aee39b2dc3219e33c0094aea3231cd6365bb4 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Sat, 31 Oct 2015 08:58:54 -0700 Subject: [PATCH 51/74] Mention that this hasn't been implemented at all --- docs/ciphers.rst | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/ciphers.rst b/docs/ciphers.rst index ea4d328c3..12c403d09 100644 --- a/docs/ciphers.rst +++ b/docs/ciphers.rst @@ -192,6 +192,20 @@ or to disable automatic configuration updates. These features have not yet been implemented and this syntax may change then they are implemented. -The status of this feature is tracked as issue #1123 in our bug tracker. + + +TODO +---- + +The status of this feature is tracked as part of issue #1123 in our +bug tracker. https://github.com/letsencrypt/letsencrypt/issues/1123 + +Prior to implementation of #1123, the client does not actually modify +ciphersuites (this is intended to be implemented as a "configuration +enhancement", but the only configuration enhancement implemented +so far is redirecting HTTP requests to HTTPS in web servers, the +"redirect" enhancement). The changes here would probably be either a new +"ciphersuite" enhancement in each plugin that provides an installer, +or a family of enhancements, one per selectable ciphersuite configuration. From fb736135c50aa4660f4ba37ccd8a59ef3ec486d0 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Oct 2015 18:24:32 +0000 Subject: [PATCH 52/74] Fix manual plugin __doc__ indentantion --- letsencrypt/plugins/manual.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index c76463f85..18654c629 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -26,12 +26,13 @@ logger = logging.getLogger(__name__) class Authenticator(common.Plugin): """Manual Authenticator. - This plugin requires user's manual intervention in setting up a HTTP - server for solving SimpleHTTP challenges and thus does not need to be - run as a privileged process. Alternatively shows instructions on how - to use Python's built-in HTTP server. + This plugin requires user's manual intervention in setting up a HTTP + server for solving SimpleHTTP challenges and thus does not need to + be run as a privileged process. Alternatively shows instructions on + how to use Python's built-in HTTP server. .. todo:: Support for `~.challenges.DVSNI`. + """ zope.interface.implements(interfaces.IAuthenticator) zope.interface.classProvides(interfaces.IPluginFactory) From 3c2ea0c878c718948cf8f25ec6f3a47f808351ba Mon Sep 17 00:00:00 2001 From: Martin Brugger Date: Sat, 31 Oct 2015 20:09:04 +0100 Subject: [PATCH 53/74] Only works for me with port 80 and 443 forwarded For the docker client to work correctly I needed to forward both ports 443 and 80. I will take a closer look on how this is supposed to work. --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 73c3fe140..6d8130886 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -73,7 +73,7 @@ to, `install Docker`_, then issue the following command: .. code-block:: shell - sudo docker run -it --rm -p 443:443 --name letsencrypt \ + sudo docker run -it --rm -p 443:443 -p 80:80 --name letsencrypt \ -v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ quay.io/letsencrypt/letsencrypt:latest auth From dc81575527eb680d8101d94a0654c6ee30a211fa Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 19 Oct 2015 20:37:55 +0000 Subject: [PATCH 54/74] Factor out _TokenDVChallenge. --- acme/acme/challenges.py | 42 ++++++++++++++++------------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 31095c04d..fd65b5e0f 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -76,26 +76,21 @@ class UnrecognizedChallenge(Challenge): return cls(jobj) -@Challenge.register -class SimpleHTTP(DVChallenge): - """ACME "simpleHttp" challenge. +class _TokenDVChallenge(DVChallenge): + """DV Challenge with token. :ivar unicode token: """ - typ = "simpleHttp" - TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec """Minimum size of the :attr:`token` in bytes.""" - URI_ROOT_PATH = ".well-known/acme-challenge" - """URI root path for the server provisioned resource.""" - # TODO: acme-spec doesn't specify token as base64-encoded value token = jose.Field( "token", encoder=jose.encode_b64jose, decoder=functools.partial( jose.decode_b64jose, size=TOKEN_SIZE, minimum=True)) + # XXX: rename to ~token_good_for_url @property def good_token(self): # XXX: @token.decoder """Is `token` good? @@ -109,6 +104,15 @@ class SimpleHTTP(DVChallenge): # URI_ROOT_PATH! return b'..' not in self.token and b'/' not in self.token + +@Challenge.register # pylint: disable=too-many-ancestors +class SimpleHTTP(_TokenDVChallenge): + """ACME "simpleHttp" challenge.""" + typ = "simpleHttp" + + URI_ROOT_PATH = ".well-known/acme-challenge" + """URI root path for the server provisioned resource.""" + @property def path(self): """Path (starting with '/') for provisioned resource.""" @@ -274,8 +278,8 @@ class SimpleHTTPProvisionedResource(jose.JSONObjectWithFields): tls = jose.Field("tls", omitempty=False) -@Challenge.register -class DVSNI(DVChallenge): +@Challenge.register # pylint: disable=too-many-ancestors +class DVSNI(_TokenDVChallenge): """ACME "dvsni" challenge. :ivar bytes token: Random data, **not** base64-encoded. @@ -286,13 +290,6 @@ class DVSNI(DVChallenge): PORT = 443 """Port to perform DVSNI challenge.""" - TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec - """Minimum size of the :attr:`token` in bytes.""" - - token = jose.Field( - "token", encoder=jose.encode_b64jose, decoder=functools.partial( - jose.decode_b64jose, size=TOKEN_SIZE, minimum=True)) - def gen_response(self, account_key, alg=jose.RS256, **kwargs): """Generate response. @@ -548,8 +545,8 @@ class ProofOfPossessionResponse(ChallengeResponse): return self.signature.verify(self.nonce) -@Challenge.register -class DNS(DVChallenge): +@Challenge.register # pylint: disable=too-many-ancestors +class DNS(_TokenDVChallenge): """ACME "dns" challenge. :ivar unicode token: @@ -560,13 +557,6 @@ class DNS(DVChallenge): LABEL = "_acme-challenge" """Label clients prepend to the domain name being validated.""" - TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec - """Minimum size of the :attr:`token` in bytes.""" - - token = jose.Field( - "token", encoder=jose.encode_b64jose, decoder=functools.partial( - jose.decode_b64jose, size=TOKEN_SIZE, minimum=True)) - def gen_validation(self, account_key, alg=jose.RS256, **kwargs): """Generate validation. From d2c5b87b953074255fda87a8d25bbde65803a5de Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 28 Oct 2015 19:42:06 +0000 Subject: [PATCH 55/74] Fix documentation for account{,_public}_key docs in acme.challenges. account_key and account_public_key are JWK, not ComparableKey. --- acme/acme/challenges.py | 45 ++++++------------------------------ acme/acme/challenges_test.py | 33 +++++++++++--------------- acme/acme/jose/jws.py | 8 +++---- 3 files changed, 24 insertions(+), 62 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index fd65b5e0f..3ff16e1b3 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -189,13 +189,7 @@ class SimpleHTTPResponse(ChallengeResponse): :param .JWS validation: :param challenges.SimpleHTTP chall: - :type account_public_key: - `~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` - wrapped in `.ComparableKey` + :param .JWK account_public_key: :rtype: bool @@ -221,16 +215,9 @@ class SimpleHTTPResponse(ChallengeResponse): :param challenges.SimpleHTTP chall: Corresponding challenge. :param unicode domain: Domain name being verified. - :param account_public_key: Public key for the key pair + :param JWK account_public_key: Public key for the key pair being authorized. If ``None`` key verification is not performed! - :type account_public_key: - `~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` - wrapped in `.ComparableKey` :param int port: Port used in the validation. :returns: ``True`` iff validation is successful, ``False`` @@ -403,17 +390,12 @@ class DVSNIResponse(ChallengeResponse): :param .challenges.DVSNI chall: Corresponding challenge. :param str domain: Domain name being validated. - :type account_public_key: - `~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` - wrapped in `.ComparableKey` + :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 @@ -488,7 +470,7 @@ class ProofOfPossession(ContinuityChallenge): class Hints(jose.JSONObjectWithFields): """Hints for "proofOfPossession" challenge. - :ivar jwk: JSON Web Key (:class:`acme.jose.JWK`) + :ivar JWK jwk: JSON Web Key :ivar tuple cert_fingerprints: `tuple` of `unicode` :ivar tuple certs: Sequence of :class:`acme.jose.ComparableX509` certificates. @@ -575,14 +557,7 @@ class DNS(_TokenDVChallenge): """Check validation. :param JWS validation: - :type account_public_key: - `~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` - wrapped in `.ComparableKey` - + :param JWK account_public_key: :rtype: bool """ @@ -631,13 +606,7 @@ class DNSResponse(ChallengeResponse): """Check validation. :param challenges.DNS chall: - :type account_public_key: - `~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` - wrapped in `.ComparableKey` + :param JWK account_public_key: :rtype: bool diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 2a12b4a64..4a8af2347 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -14,7 +14,7 @@ from acme import test_util CERT = test_util.load_cert('cert.pem') -KEY = test_util.load_rsa_private_key('rsa512_key.pem') +KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem')) class ChallengeTest(unittest.TestCase): @@ -237,18 +237,15 @@ class DVSNITest(unittest.TestCase): jose.DeserializationError, DVSNI.from_json, self.jmsg) def test_gen_response(self): - key = jose.JWKRSA(key=KEY) from acme.challenges import DVSNI self.assertEqual(self.msg, DVSNI.json_loads( - self.msg.gen_response(key).validation.payload.decode())) + self.msg.gen_response(KEY).validation.payload.decode())) class DVSNIResponseTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes def setUp(self): - self.key = jose.JWKRSA(key=KEY) - from acme.challenges import DVSNI self.chall = DVSNI( token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e')) @@ -256,7 +253,7 @@ class DVSNIResponseTest(unittest.TestCase): from acme.challenges import DVSNIResponse self.validation = jose.JWS.sign( payload=self.chall.json_dumps(sort_keys=True).encode(), - key=self.key, alg=jose.RS256) + key=KEY, alg=jose.RS256) self.msg = DVSNIResponse(validation=self.validation) self.jmsg_to = { 'resource': 'challenge', @@ -340,22 +337,22 @@ class DVSNIResponseTest(unittest.TestCase): def test_simple_verify_wrong_payload(self): for payload in b'', b'{}': msg = self.msg.update(validation=jose.JWS.sign( - payload=payload, key=self.key, alg=jose.RS256)) + payload=payload, key=KEY, alg=jose.RS256)) self.assertFalse(msg.simple_verify( - self.chall, self.domain, self.key.public_key())) + 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=self.key, alg=jose.RS256)) + key=KEY, alg=jose.RS256)) self.assertFalse(msg.simple_verify( - self.chall, self.domain, self.key.public_key())) + 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, self.key.public_key(), + self.chall, self.domain, KEY.public_key(), cert=mock.sentinel.cert)) mock_verify_cert.assert_called_once_with(self.msg, mock.sentinel.cert) @@ -363,7 +360,7 @@ class DVSNIResponseTest(unittest.TestCase): 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, self.key.public_key())) + self.chall, self.domain, KEY.public_key())) class RecoveryContactTest(unittest.TestCase): @@ -442,7 +439,7 @@ class RecoveryContactResponseTest(unittest.TestCase): class ProofOfPossessionHintsTest(unittest.TestCase): def setUp(self): - jwk = jose.JWKRSA(key=KEY.public_key()) + jwk = KEY.public_key() issuers = ( 'C=US, O=SuperT LLC, CN=SuperTrustworthy Public CA', 'O=LessTrustworthy CA Inc, CN=LessTrustworthy But StillSecure', @@ -511,7 +508,7 @@ class ProofOfPossessionTest(unittest.TestCase): def setUp(self): from acme.challenges import ProofOfPossession hints = ProofOfPossession.Hints( - jwk=jose.JWKRSA(key=KEY.public_key()), cert_fingerprints=(), + jwk=KEY.public_key(), cert_fingerprints=(), certs=(), serial_numbers=(), subject_key_identifiers=(), issuers=(), authorized_for=()) self.msg = ProofOfPossession( @@ -551,7 +548,7 @@ class ProofOfPossessionResponseTest(unittest.TestCase): # nonce and challenge nonce are the same, don't make the same # mistake here... signature = other.Signature( - alg=jose.RS256, jwk=jose.JWKRSA(key=KEY.public_key()), + alg=jose.RS256, jwk=KEY.public_key(), sig=b'\xa7\xc1\xe7\xe82o\xbc\xcd\xd0\x1e\x010#Z|\xaf\x15\x83' b'\x94\x8f#\x9b\nQo(\x80\x15,\x08\xfcz\x1d\xfd\xfd.\xaap' b'\xfa\x06\xd1\xa2f\x8d8X2>%d\xbd%\xe1T\xdd\xaa0\x18\xde' @@ -659,14 +656,12 @@ class DNSTest(unittest.TestCase): class DNSResponseTest(unittest.TestCase): def setUp(self): - self.key = jose.JWKRSA(key=KEY) - from acme.challenges import DNS self.chall = DNS(token=jose.b64decode( b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA")) self.validation = jose.JWS.sign( payload=self.chall.json_dumps(sort_keys=True).encode(), - key=self.key, alg=jose.RS256) + key=KEY, alg=jose.RS256) from acme.challenges import DNSResponse self.msg = DNSResponse(validation=self.validation) @@ -694,7 +689,7 @@ class DNSResponseTest(unittest.TestCase): def test_check_validation(self): self.assertTrue( - self.msg.check_validation(self.chall, self.key.public_key())) + self.msg.check_validation(self.chall, KEY.public_key())) if __name__ == '__main__': diff --git a/acme/acme/jose/jws.py b/acme/acme/jose/jws.py index 61a3b5aea..1a073e17d 100644 --- a/acme/acme/jose/jws.py +++ b/acme/acme/jose/jws.py @@ -104,7 +104,7 @@ class Header(json_util.JSONObjectWithFields): .. todo:: Supports only "jwk" header parameter lookup. :returns: (Public) key found in the header. - :rtype: :class:`acme.jose.jwk.JWK` + :rtype: .JWK :raises acme.jose.errors.Error: if key could not be found @@ -194,8 +194,7 @@ class Signature(json_util.JSONObjectWithFields): def verify(self, payload, key=None): """Verify. - :param key: Key used for verification. - :type key: :class:`acme.jose.jwk.JWK` + :param JWK key: Key used for verification. """ key = self.combined.find_key() if key is None else key @@ -208,8 +207,7 @@ class Signature(json_util.JSONObjectWithFields): protect=frozenset(), **kwargs): """Sign. - :param key: Key for signature. - :type key: :class:`acme.jose.jwk.JWK` + :param JWK key: Key for signature. """ assert isinstance(key, alg.kty) From 2a4ebb90ef5cbdc7eaa291a21eb784f87ce48124 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Oct 2015 20:31:32 +0000 Subject: [PATCH 56/74] Contet-Type snippets for webroot --- letsencrypt/plugins/webroot.py | 41 +++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/letsencrypt/plugins/webroot.py b/letsencrypt/plugins/webroot.py index f11325f57..f58c33970 100644 --- a/letsencrypt/plugins/webroot.py +++ b/letsencrypt/plugins/webroot.py @@ -1,4 +1,43 @@ -"""Webroot plugin.""" +"""Webroot plugin. + +Content-Type +------------ + +This plugin requires your webserver to use a specific `Content-Type` +header in the HTTP response. + +Apache2 +~~~~~~~ + +.. note:: Instructions written and tested for Debian Jessie. Other + operating systems might use something very similar, but you might + still need to readjust some commands. + +Create ``/etc/apache2/conf-available/letsencrypt-simplehttp.conf``, with +the following contents:: + + + + Header set Content-Type "application/jose+json" + + + +and then run ``a2enmod headers; a2enconf letsencrypt``; depending on the +output you will have to either ``service apache2 restart`` or ``service +apache2 reload``. + +nginx +~~~~~ + +Use the following snippet in your ``server{...}`` stanza:: + + location ~ /.well-known/acme-challenge/(.*) { + default_type application/jose+json; + } + +and reload your daemon. + +""" import errno import logging import os From c529f742f28adeec07522d5aaba5a8643ce4b7ab Mon Sep 17 00:00:00 2001 From: poly Date: Sun, 1 Nov 2015 08:46:34 +0400 Subject: [PATCH 57/74] added email reminder documentation --- docs/using.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/using.rst b/docs/using.rst index 6d8130886..78e4c6d37 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -163,6 +163,9 @@ sure that UI doesn't prompt for any details you can add the command to ``crontab`` (make it less than every 90 days to avoid problems, say every month). +Please note that the CA will send notification emails to the address +you provide if you do not renew certificates that are about to expire. + Let's Encrypt is working hard on automating the renewal process. Until the tool is ready, we are sorry for the inconvenience! From 602f0b2dbefd2321e0f7a7da523569d826b2d354 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 28 Oct 2015 20:41:15 +0000 Subject: [PATCH 58/74] Add http-01 to acme --- acme/acme/challenges.py | 223 +++++++++++++++++++++++++++++++++-- acme/acme/challenges_test.py | 147 +++++++++++++++++++++++ acme/acme/jose/jwk.py | 2 + acme/acme/messages_test.py | 2 +- docs/contributing.rst | 2 +- 5 files changed, 367 insertions(+), 9 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 3ff16e1b3..750f2be8d 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -1,9 +1,11 @@ """ACME Identifier Validation Challenges.""" +import abc import functools import hashlib import logging import socket +from cryptography.hazmat.primitives import hashes import OpenSSL import requests @@ -79,7 +81,7 @@ class UnrecognizedChallenge(Challenge): class _TokenDVChallenge(DVChallenge): """DV Challenge with token. - :ivar unicode token: + :ivar bytes token: """ TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec @@ -105,6 +107,217 @@ class _TokenDVChallenge(DVChallenge): return b'..' not in self.token and b'/' not in self.token +class KeyAuthorizationChallengeResponse(ChallengeResponse): + """Response to Challenges based on Key Authorization. + + :param unicode key_authorization: + + """ + key_authorization = jose.Field("keyAuthorization") + thumbprint_hash_function = hashes.SHA256 + + def verify(self, chall, account_public_key): + """Verify the key authorization. + + :param KeyAuthorization chall: Challenge that corresponds to + this response. + :param JWK account_public_key: + + :return: ``True`` iff verification of the key authorization was + successful. + :rtype: bool + + """ + parts = self.key_authorization.split('.') # pylint: disable=no-member + if len(parts) != 2: + logger.debug("Key authorization (%r) is not well formed", + self.key_authorization) + return False + + if parts[0] != chall.encode("token"): + logger.debug("Mismatching token in key authorization: " + "%r instead of %r", parts[0], chall.encode("token")) + return False + + thumbprint = jose.b64encode(account_public_key.thumbprint( + hash_function=self.thumbprint_hash_function)).decode() + if parts[1] != thumbprint: + logger.debug("Mismatching thumbprint in key authorization: " + "%r instead of %r", parts[0], thumbprint) + return False + + return True + + +class KeyAuthorizationChallenge(_TokenDVChallenge): + # pylint: disable=abstract-class-little-used,too-many-ancestors + """Challenge based on Key Authorization. + + :param response_cls: Subclass of `KeyAuthorizationChallengeResponse` + that will be used to generate `response`. + + """ + __metaclass__ = abc.ABCMeta + + response_cls = NotImplemented + thumbprint_hash_function = ( + KeyAuthorizationChallengeResponse.thumbprint_hash_function) + + def key_authorization(self, account_key): + """Generate Key Authorization. + + :param JWK account_key: + :rtype unicode: + + """ + return self.encode("token") + "." + jose.b64encode( + account_key.thumbprint( + hash_function=self.thumbprint_hash_function)).decode() + + def response(self, account_key): + """Generate response to the challenge. + + :param JWK account_key: + + :returns: Response (initialized `response_cls`) to the challenge. + :rtype: KeyAuthorizationChallengeResponse + + """ + return self.response_cls( + key_authorization=self.key_authorization(account_key)) + + @abc.abstractmethod + def validation(self, account_key): + """Generate validation for the challenge. + + Subclasses must implement this method, but they are likely to + return completely different data structures, depending on what's + necessary to complete the challenge. Interepretation of that + return value must be known to the caller. + + :param JWK account_key: + :returns: Challenge-specific validation. + + """ + raise NotImplementedError() # pragma: no cover + + def response_and_validation(self, account_key): + """Generate response and validation. + + Convenience function that return results of `response` and + `validation`. + + :param JWK account_key: + :rtype: tuple + + """ + return (self.response(account_key), self.validation(account_key)) + + +@ChallengeResponse.register +class HTTP01Response(KeyAuthorizationChallengeResponse): + """ACME http-01 challenge response.""" + typ = "http-01" + + PORT = 80 + + def simple_verify(self, chall, domain, account_public_key, port=None): + """Simple verify. + + :param challenges.SimpleHTTP chall: Corresponding challenge. + :param unicode domain: Domain name being verified. + :param account_public_key: Public key for the key pair + being authorized. If ``None`` key verification is not + performed! + :param JWK account_public_key: + :param int port: Port used in the validation. + + :returns: ``True`` iff validation is successful, ``False`` + otherwise. + :rtype: bool + + """ + if not self.verify(chall, account_public_key): + logger.debug("Verification of key authorization in response failed") + return False + + # TODO: ACME specification defines URI template that doesn't + # allow to use a custom port... Make sure port is not in the + # 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) + domain += ":{0}".format(port) + + uri = self.uri(domain, chall) + logger.debug("Verifying %s at %s...", chall.typ, uri) + try: + http_response = requests.get(uri) + except requests.exceptions.RequestException as error: + logger.error("Unable to reach %s: %s", uri, error) + return False + logger.debug("Received %s: %s. Headers: %s", http_response, + http_response.text, http_response.headers) + + found_ct = http_response.headers.get( + "Content-Type", chall.CONTENT_TYPE) + if found_ct != chall.CONTENT_TYPE: + logger.debug("Wrong Content-Type: found %r, expected %r", + found_ct, chall.CONTENT_TYPE) + return False + + if self.key_authorization != http_response.text: + logger.debug("Key authorization from response (%r) doesn't match " + "HTTP response (%r)", self.key_authorization, + http_response.text) + return False + + return True + + +@Challenge.register # pylint: disable=too-many-ancestors +class HTTP01(KeyAuthorizationChallenge): + """ACME http-01 challenge.""" + response_cls = HTTP01Response + typ = response_cls.typ + + CONTENT_TYPE = "text/plain" + """Content-Type header that must be used for provisioned resource.""" + + URI_ROOT_PATH = ".well-known/acme-challenge" + """URI root path for the server provisioned resource.""" + + @property + def path(self): + """Path (starting with '/') for provisioned resource. + + :rtype: string + + """ + return '/' + self.URI_ROOT_PATH + '/' + self.encode('token') + + def uri(self, domain): + """Create an URI to the provisioned resource. + + Forms an URI to the HTTPS server provisioned resource + (containing :attr:`~SimpleHTTP.token`). + + :param unicode domain: Domain name being verified. + :rtype: string + + """ + return "http://" + domain + self.path + + def validation(self, account_key): + """Generate validation. + + :param JWK account_key: + :rtype: unicode + + """ + return self.key_authorization(account_key) + + @Challenge.register # pylint: disable=too-many-ancestors class SimpleHTTP(_TokenDVChallenge): """ACME "simpleHttp" challenge.""" @@ -229,7 +442,7 @@ class SimpleHTTPResponse(ChallengeResponse): # allow to use a custom port... Make sure port is not in the # request URI, if it's standard. if port is not None and port != self.port: - logger.warn( + logger.warning( "Using non-standard port for SimpleHTTP verification: %s", port) domain += ":{0}".format(port) @@ -529,11 +742,7 @@ class ProofOfPossessionResponse(ChallengeResponse): @Challenge.register # pylint: disable=too-many-ancestors class DNS(_TokenDVChallenge): - """ACME "dns" challenge. - - :ivar unicode token: - - """ + """ACME "dns" challenge.""" typ = "dns" LABEL = "_acme-challenge" diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 4a8af2347..53e6b528a 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -43,6 +43,149 @@ class UnrecognizedChallengeTest(unittest.TestCase): self.chall, UnrecognizedChallenge.from_json(self.jobj)) +class KeyAuthorizationChallengeResponseTest(unittest.TestCase): + + def setUp(self): + def _encode(name): + assert name == "token" + return "foo" + self.chall = mock.Mock() + self.chall.encode.side_effect = _encode + + def test_verify_ok(self): + from acme.challenges import KeyAuthorizationChallengeResponse + response = KeyAuthorizationChallengeResponse( + key_authorization='foo.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY') + self.assertTrue(response.verify(self.chall, KEY.public_key())) + + def test_verify_wrong_token(self): + from acme.challenges import KeyAuthorizationChallengeResponse + response = KeyAuthorizationChallengeResponse( + key_authorization='bar.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY') + self.assertFalse(response.verify(self.chall, KEY.public_key())) + + def test_verify_wrong_thumbprint(self): + from acme.challenges import KeyAuthorizationChallengeResponse + response = KeyAuthorizationChallengeResponse( + key_authorization='foo.oKGqedy-b-acd5eoybm2f-NVFxv') + self.assertFalse(response.verify(self.chall, KEY.public_key())) + + def test_verify_wrong_form(self): + from acme.challenges import KeyAuthorizationChallengeResponse + response = KeyAuthorizationChallengeResponse( + key_authorization='.foo.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY') + self.assertFalse(response.verify(self.chall, KEY.public_key())) + + +class HTTP01ResponseTest(unittest.TestCase): + # pylint: disable=too-many-instance-attributes + + def setUp(self): + from acme.challenges import HTTP01Response + self.msg = HTTP01Response(key_authorization=u'foo') + self.jmsg = { + 'resource': 'challenge', + 'type': 'http-01', + 'keyAuthorization': u'foo', + } + + from acme.challenges import HTTP01 + self.chall = HTTP01(token=(b'x' * 16)) + self.response = self.chall.response(KEY) + self.good_headers = {'Content-Type': HTTP01.CONTENT_TYPE} + + def test_to_partial_json(self): + self.assertEqual(self.jmsg, self.msg.to_partial_json()) + + def test_from_json(self): + from acme.challenges import HTTP01Response + self.assertEqual( + self.msg, HTTP01Response.from_json(self.jmsg)) + + def test_from_json_hashable(self): + from acme.challenges import HTTP01Response + hash(HTTP01Response.from_json(self.jmsg)) + + 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.requests.get") + def test_simple_verify_good_validation(self, mock_get): + validation = self.chall.validation(KEY) + mock_get.return_value = mock.MagicMock( + text=validation, headers=self.good_headers) + self.assertTrue(self.response.simple_verify( + self.chall, "local", KEY.public_key())) + mock_get.assert_called_once_with(self.chall.uri("local")) + + @mock.patch("acme.challenges.requests.get") + def test_simple_verify_bad_validation(self, mock_get): + mock_get.return_value = mock.MagicMock( + text="!", headers=self.good_headers) + self.assertFalse(self.response.simple_verify( + self.chall, "local", KEY.public_key())) + + @mock.patch("acme.challenges.requests.get") + def test_simple_verify_bad_content_type(self, mock_get): + mock_get().text = self.chall.token + self.assertFalse(self.response.simple_verify( + self.chall, "local", KEY.public_key())) + + @mock.patch("acme.challenges.requests.get") + def test_simple_verify_connection_error(self, mock_get): + mock_get.side_effect = requests.exceptions.RequestException + self.assertFalse(self.response.simple_verify( + self.chall, "local", KEY.public_key())) + + @mock.patch("acme.challenges.requests.get") + def test_simple_verify_port(self, mock_get): + self.response.simple_verify( + self.chall, domain="local", + account_public_key=KEY.public_key(), port=8080) + self.assertEqual("local:8080", urllib_parse.urlparse( + mock_get.mock_calls[0][1][0]).netloc) + + +class HTTP01Test(unittest.TestCase): + + def setUp(self): + from acme.challenges import HTTP01 + self.msg = HTTP01( + token=jose.decode_b64jose( + 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')) + self.jmsg = { + 'type': 'http-01', + 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', + } + + def test_path(self): + self.assertEqual(self.msg.path, '/.well-known/acme-challenge/' + 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA') + + def test_uri(self): + self.assertEqual( + 'http://example.com/.well-known/acme-challenge/' + 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', + self.msg.uri('example.com')) + + def test_to_partial_json(self): + self.assertEqual(self.jmsg, self.msg.to_partial_json()) + + def test_from_json(self): + from acme.challenges import HTTP01 + self.assertEqual(self.msg, HTTP01.from_json(self.jmsg)) + + def test_from_json_hashable(self): + from acme.challenges import HTTP01 + hash(HTTP01.from_json(self.jmsg)) + + def test_good_token(self): + self.assertTrue(self.msg.good_token) + self.assertFalse( + self.msg.update(token=b'..').good_token) + + class SimpleHTTPTest(unittest.TestCase): def setUp(self): @@ -55,6 +198,10 @@ class SimpleHTTPTest(unittest.TestCase): 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', } + def test_path(self): + self.assertEqual(self.msg.path, '/.well-known/acme-challenge/' + 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA') + def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) diff --git a/acme/acme/jose/jwk.py b/acme/acme/jose/jwk.py index da61b0c4e..4d07229b3 100644 --- a/acme/acme/jose/jwk.py +++ b/acme/acme/jose/jwk.py @@ -47,6 +47,8 @@ class JWK(json_util.TypedJSONObjectWithFields): https://tools.ietf.org/html/rfc7638 + :returns bytes: + """ digest = hashes.Hash(hash_function(), backend=default_backend()) digest.update(json.dumps( diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index d2d859bc5..6c1c4f596 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -278,7 +278,7 @@ class AuthorizationTest(unittest.TestCase): self.challbs = ( ChallengeBody( uri='http://challb1', status=STATUS_VALID, - chall=challenges.SimpleHTTP(token=b'IlirfxKKXAsHtmzK29Pj8A')), + chall=challenges.HTTP01(token=b'IlirfxKKXAsHtmzK29Pj8A')), ChallengeBody(uri='http://challb2', status=STATUS_VALID, chall=challenges.DNS( token=b'DGyRejmCefe7v4NfDGDKfA')), diff --git a/docs/contributing.rst b/docs/contributing.rst index f82d7a583..a3baa7bc5 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -152,7 +152,7 @@ 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`, -`~.challenges.SimpleHTTPS`, `~.challenges.DNS`) and continuity specific +`~.challenges.HTTP01`, `~.challenges.DNS`) and continuity specific challenges (subclasses of `~.ContinuityChallenge`, i.e. `~.challenges.RecoveryToken`, `~.challenges.RecoveryContact`, `~.challenges.ProofOfPossession`). Continuity challenges are From 01bc073111d139183e24c9d702d5942beb864022 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 1 Nov 2015 10:54:01 +0000 Subject: [PATCH 59/74] Quickfix for misterious abstract-class-little-used --- acme/acme/jose/jwk.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/acme/acme/jose/jwk.py b/acme/acme/jose/jwk.py index 4d07229b3..c82134da9 100644 --- a/acme/acme/jose/jwk.py +++ b/acme/acme/jose/jwk.py @@ -21,6 +21,12 @@ from acme.jose import util logger = logging.getLogger(__name__) +# TODO: bug in pylint? +# ************* Module acme.challenges +# R:153, 0: Abstract class is only referenced 1 times (abstract-class-little-used) +# pylint: disable=abstract-class-little-used + + class JWK(json_util.TypedJSONObjectWithFields): # pylint: disable=too-few-public-methods """JSON Web Key.""" From bb0843b6c6662fd75a78523151ae3543e17e070e Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Oct 2015 21:35:12 +0000 Subject: [PATCH 60/74] acme_util.HTTP01* --- letsencrypt/tests/acme_util.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/letsencrypt/tests/acme_util.py b/letsencrypt/tests/acme_util.py index 235810435..1e3086b18 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 SIMPLE_HTTP = challenges.SimpleHTTP( token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") +HTTP01 = challenges.HTTP01( + token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") DVSNI = challenges.DVSNI( token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA")) DNS = challenges.DNS(token="17817c66b60ce2e4012dfad92657527a") @@ -81,6 +83,7 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name # Pending ChallengeBody objects DVSNI_P = chall_to_challb(DVSNI, messages.STATUS_PENDING) SIMPLE_HTTP_P = chall_to_challb(SIMPLE_HTTP, 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) From 5ef2507b0c33031e5df25f7cd3b966b325475860 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 28 Oct 2015 20:42:27 +0000 Subject: [PATCH 61/74] KeyAuthorizationAnnotatedChallenge --- letsencrypt/achallenges.py | 9 +++++++++ letsencrypt/auth_handler.py | 4 +++- letsencrypt/tests/auth_handler_test.py | 19 +++++++++++++++++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/letsencrypt/achallenges.py b/letsencrypt/achallenges.py index e86f51a3f..7aa2bd35a 100644 --- a/letsencrypt/achallenges.py +++ b/letsencrypt/achallenges.py @@ -45,6 +45,15 @@ class AnnotatedChallenge(jose.ImmutableMap): return getattr(self.challb, name) +class KeyAuthorizationAnnotatedChallenge(AnnotatedChallenge): + """Client annotated `KeyAuthorizationChallenge` challenge.""" + __slots__ = ('challb', 'domain', 'account_key') + + def response_and_validation(self): + """Generate response and validation.""" + return self.challb.chall.response_and_validation(self.account_key) + + class DVSNI(AnnotatedChallenge): """Client annotated "dvsni" ACME challenge. diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 4f2a6fa3e..f7b6bd507 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -358,7 +358,9 @@ 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/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 18ee56081..247fca4e1 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -9,6 +9,7 @@ from acme import challenges from acme import client as acme_client from acme import messages +from letsencrypt import achallenges from letsencrypt import errors from letsencrypt import le_util @@ -283,6 +284,22 @@ class PollChallengesTest(unittest.TestCase): return (new_authzr, "response") +class ChallbToAchallTest(unittest.TestCase): + """Tests for letsencrypt.auth_handler.challb_to_achall.""" + + def _call(self, challb): + from letsencrypt.auth_handler import challb_to_achall + return challb_to_achall(challb, "account_key", "domain") + + def test_it(self): + self.assertEqual( + self._call(acme_util.HTTP01_P), + achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.HTTP01_P, account_key="account_key", + domain="domain"), + ) + + class GenChallengePathTest(unittest.TestCase): """Tests for letsencrypt.auth_handler.gen_challenge_path. @@ -425,8 +442,6 @@ class ReportFailedChallsTest(unittest.TestCase): # pylint: disable=protected-access def setUp(self): - from letsencrypt import achallenges - kwargs = { "chall": acme_util.SIMPLE_HTTP, "uri": "uri", From fd209eab66f8818796c6be5fed2bc2ace9776d7e Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 28 Oct 2015 20:42:44 +0000 Subject: [PATCH 62/74] http-01 for manual plugin --- letsencrypt/plugins/manual.py | 24 ++++++++++++------------ letsencrypt/plugins/manual_test.py | 10 +++++----- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index c76463f85..ffaad3d99 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -27,7 +27,7 @@ class Authenticator(common.Plugin): """Manual Authenticator. This plugin requires user's manual intervention in setting up a HTTP - server for solving SimpleHTTP challenges and thus does not need to be + server for solving http-01 challenges and thus does not need to be run as a privileged process. Alternatively shows instructions on how to use Python's built-in HTTP server. @@ -68,9 +68,9 @@ Are you OK with your IP being logged? # anything recursively under the cwd CMD_TEMPLATE = """\ -mkdir -p {root}/public_html/{response.URI_ROOT_PATH} +mkdir -p {root}/public_html/{achall.URI_ROOT_PATH} cd {root}/public_html -printf "%s" {validation} > {response.URI_ROOT_PATH}/{encoded_token} +printf "%s" {validation} > {achall.URI_ROOT_PATH}/{encoded_token} # run only once per server: $(command -v python2 || command -v python2.7 || command -v python2.6) -c \\ "import BaseHTTPServer, SimpleHTTPServer; \\ @@ -95,14 +95,14 @@ s.serve_forever()" """ def more_info(self): # pylint: disable=missing-docstring,no-self-use return ("This plugin requires user's manual intervention in setting " - "up an HTTP server for solving SimpleHTTP challenges and thus " + "up an HTTP server for solving http-01 challenges and thus " "does not need to be run as a privileged process. " "Alternatively shows instructions on how to use Python's " "built-in HTTP server.") def get_chall_pref(self, domain): # pylint: disable=missing-docstring,no-self-use,unused-argument - return [challenges.SimpleHTTP] + return [challenges.HTTP01] def perform(self, achalls): # pylint: disable=missing-docstring responses = [] @@ -130,16 +130,16 @@ s.serve_forever()" """ # same path for each challenge response would be easier for # users, but will not work if multiple domains point at the # same server: default command doesn't support virtual hosts - response, validation = achall.gen_response_and_validation( - tls=False) # SimpleHTTP TLS is dead: ietf-wg-acme/acme#7 + response, validation = achall.response_and_validation() port = (response.port if self.config.simple_http_port is None else int(self.config.simple_http_port)) command = self.CMD_TEMPLATE.format( root=self._root, achall=achall, response=response, - validation=pipes.quote(validation.json_dumps()), + # TODO(kuba): pipes still necessary? + validation=pipes.quote(validation), encoded_token=achall.chall.encode("token"), - ct=response.CONTENT_TYPE, port=port) + ct=achall.CONTENT_TYPE, port=port) if self.conf("test-mode"): logger.debug("Test mode. Executing the manual command: %s", command) # sh shipped with OS X does't support echo -n, but supports printf @@ -168,9 +168,9 @@ s.serve_forever()" """ raise errors.PluginError("Must agree to IP logging to proceed") self._notify_and_wait(self.MESSAGE_TEMPLATE.format( - validation=validation.json_dumps(), response=response, - uri=response.uri(achall.domain, achall.challb.chall), - ct=response.CONTENT_TYPE, command=command)) + validation=validation, response=response, + uri=achall.chall.uri(achall.domain), + ct=achall.CONTENT_TYPE, command=command)) if response.simple_verify( achall.chall, achall.domain, diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py index a52129635..431cd07a7 100644 --- a/letsencrypt/plugins/manual_test.py +++ b/letsencrypt/plugins/manual_test.py @@ -25,8 +25,8 @@ class AuthenticatorTest(unittest.TestCase): self.config = mock.MagicMock( simple_http_port=8080, manual_test_mode=False) self.auth = Authenticator(config=self.config, name="manual") - self.achalls = [achallenges.SimpleHTTP( - challb=acme_util.SIMPLE_HTTP_P, domain="foo.com", account_key=KEY)] + self.achalls = [achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)] config_test_mode = mock.MagicMock( simple_http_port=8080, manual_test_mode=True) @@ -45,13 +45,13 @@ class AuthenticatorTest(unittest.TestCase): @mock.patch("letsencrypt.plugins.manual.zope.component.getUtility") @mock.patch("letsencrypt.plugins.manual.sys.stdout") - @mock.patch("acme.challenges.SimpleHTTPResponse.simple_verify") + @mock.patch("acme.challenges.HTTP01Response.simple_verify") @mock.patch("__builtin__.raw_input") def test_perform(self, mock_raw_input, mock_verify, mock_stdout, mock_interaction): mock_verify.return_value = True mock_interaction().yesno.return_value = True - resp = challenges.SimpleHTTPResponse(tls=False) + resp = self.achalls[0].response(KEY) self.assertEqual([resp], self.auth.perform(self.achalls)) self.assertEqual(1, mock_raw_input.call_count) mock_verify.assert_called_with( @@ -89,7 +89,7 @@ class AuthenticatorTest(unittest.TestCase): @mock.patch("letsencrypt.plugins.manual.socket.socket") @mock.patch("letsencrypt.plugins.manual.time.sleep", autospec=True) - @mock.patch("acme.challenges.SimpleHTTPResponse.simple_verify", + @mock.patch("acme.challenges.HTTP01Response.simple_verify", autospec=True) @mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True) def test_perform_test_mode(self, mock_popen, mock_verify, mock_sleep, From ea3611afe6b7e5e605dff6bcd31eb1dbdf235486 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 28 Oct 2015 21:27:04 +0000 Subject: [PATCH 63/74] http-01 for standalone --- acme/acme/challenges.py | 2 +- acme/acme/standalone.py | 26 +++++++++--------- acme/acme/standalone_test.py | 29 ++++++++++---------- docs/using.rst | 2 +- letsencrypt/plugins/standalone.py | 36 ++++++++++++------------- letsencrypt/plugins/standalone_test.py | 37 +++++++++++++------------- tests/boulder-integration.sh | 2 +- 7 files changed, 66 insertions(+), 68 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 750f2be8d..86ff0a902 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -249,7 +249,7 @@ class HTTP01Response(KeyAuthorizationChallengeResponse): "Using non-standard port for SimpleHTTP verification: %s", port) domain += ":{0}".format(port) - uri = self.uri(domain, chall) + uri = chall.uri(domain) logger.debug("Verifying %s at %s...", chall.typ, uri) try: http_response = requests.get(uri) diff --git a/acme/acme/standalone.py b/acme/acme/standalone.py index 310e61995..1466671e3 100644 --- a/acme/acme/standalone.py +++ b/acme/acme/standalone.py @@ -58,26 +58,26 @@ class DVSNIServer(TLSServer, ACMEServerMixin): self, server_address, socketserver.BaseRequestHandler, certs=certs) -class SimpleHTTPServer(BaseHTTPServer.HTTPServer, ACMEServerMixin): - """SimpleHTTP Server.""" +class HTTP01Server(BaseHTTPServer.HTTPServer, ACMEServerMixin): + """HTTP01 Server.""" def __init__(self, server_address, resources): BaseHTTPServer.HTTPServer.__init__( - self, server_address, SimpleHTTPRequestHandler.partial_init( + self, server_address, HTTP01RequestHandler.partial_init( simple_http_resources=resources)) -class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): - """SimpleHTTP challenge handler. +class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + """HTTP01 challenge handler. Adheres to the stdlib's `socketserver.BaseRequestHandler` interface. - :ivar set simple_http_resources: A set of `SimpleHTTPResource` + :ivar set simple_http_resources: A set of `HTTP01Resource` objects. TODO: better name? """ - SimpleHTTPResource = collections.namedtuple( - "SimpleHTTPResource", "chall response validation") + HTTP01Resource = collections.namedtuple( + "HTTP01Resource", "chall response validation") def __init__(self, *args, **kwargs): self.simple_http_resources = kwargs.pop("simple_http_resources", set()) @@ -86,7 +86,7 @@ class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): def do_GET(self): # pylint: disable=invalid-name,missing-docstring if self.path == "/": self.handle_index() - elif self.path.startswith("/" + challenges.SimpleHTTP.URI_ROOT_PATH): + elif self.path.startswith("/" + challenges.HTTP01.URI_ROOT_PATH): self.handle_simple_http_resource() else: self.handle_404() @@ -106,15 +106,15 @@ class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): self.wfile.write(b"404") def handle_simple_http_resource(self): - """Handle SimpleHTTP provisioned resources.""" + """Handle HTTP01 provisioned resources.""" for resource in self.simple_http_resources: if resource.chall.path == self.path: - logger.debug("Serving SimpleHTTP with token %r", + logger.debug("Serving HTTP01 with token %r", resource.chall.encode("token")) self.send_response(http_client.OK) - self.send_header("Content-type", resource.response.CONTENT_TYPE) + self.send_header("Content-type", resource.chall.CONTENT_TYPE) self.end_headers() - self.wfile.write(resource.validation.json_dumps().encode()) + self.wfile.write(resource.validation.encode()) return else: # pylint: disable=useless-else-on-loop logger.debug("No resources to serve") diff --git a/acme/acme/standalone_test.py b/acme/acme/standalone_test.py index ed015b826..85ef6ab14 100644 --- a/acme/acme/standalone_test.py +++ b/acme/acme/standalone_test.py @@ -54,16 +54,16 @@ class DVSNIServerTest(unittest.TestCase): jose.ComparableX509(self.certs[b'localhost'][1])) -class SimpleHTTPServerTest(unittest.TestCase): - """Tests for acme.standalone.SimpleHTTPServer.""" +class HTTP01ServerTest(unittest.TestCase): + """Tests for acme.standalone.HTTP01Server.""" def setUp(self): self.account_key = jose.JWK.load( test_util.load_vector('rsa1024_key.pem')) self.resources = set() - from acme.standalone import SimpleHTTPServer - self.server = SimpleHTTPServer(('', 0), resources=self.resources) + from acme.standalone import HTTP01Server + self.server = HTTP01Server(('', 0), resources=self.resources) # pylint: disable=no-member self.port = self.server.socket.getsockname()[1] @@ -86,25 +86,24 @@ class SimpleHTTPServerTest(unittest.TestCase): 'http://localhost:{0}/foo'.format(self.port), verify=False) self.assertEqual(response.status_code, http_client.NOT_FOUND) - def _test_simple_http(self, add): - chall = challenges.SimpleHTTP(token=(b'x' * 16)) - response = challenges.SimpleHTTPResponse(tls=False) + def _test_http01(self, add): + chall = challenges.HTTP01(token=(b'x' * 16)) + response, validation = chall.response_and_validation(self.account_key) - from acme.standalone import SimpleHTTPRequestHandler - resource = SimpleHTTPRequestHandler.SimpleHTTPResource( - chall=chall, response=response, validation=response.gen_validation( - chall, self.account_key)) + from acme.standalone import HTTP01RequestHandler + resource = HTTP01RequestHandler.HTTP01Resource( + chall=chall, response=response, validation=validation) if add: self.resources.add(resource) return resource.response.simple_verify( resource.chall, 'localhost', self.account_key.public_key(), port=self.port) - def test_simple_http_found(self): - self.assertTrue(self._test_simple_http(add=True)) + def test_http01_found(self): + self.assertTrue(self._test_http01(add=True)) - def test_simple_http_not_found(self): - self.assertFalse(self._test_simple_http(add=False)) + def test_http01_not_found(self): + self.assertFalse(self._test_http01(add=False)) class TestSimpleDVSNIServer(unittest.TestCase): diff --git a/docs/using.rst b/docs/using.rst index 73c3fe140..312e55510 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -127,7 +127,7 @@ Officially supported plugins: Plugin A I Notes and status ========== = = ================================================================ standalone Y N Very stable. Uses port 80 (force by - ``--standalone-supported-challenges simpleHttp``) or 443 + ``--standalone-supported-challenges http-01``) or 443 (force by ``--standalone-supported-challenges dvsni``). apache Y Y Alpha. Automates Apache installation, works fairly well but on Debian-based distributions only for now. diff --git a/letsencrypt/plugins/standalone.py b/letsencrypt/plugins/standalone.py index ccff03319..c45337507 100644 --- a/letsencrypt/plugins/standalone.py +++ b/letsencrypt/plugins/standalone.py @@ -14,7 +14,6 @@ from acme import challenges from acme import crypto_util as acme_crypto_util from acme import standalone as acme_standalone -from letsencrypt import achallenges from letsencrypt import errors from letsencrypt import interfaces @@ -33,7 +32,7 @@ class ServerManager(object): `acme.crypto_util.SSLSocket.certs` and `acme.crypto_util.SSLSocket.simple_http_resources` respectively. All created servers share the same certificates and resources, so if - you're running both TLS and non-TLS instances, SimpleHTTP handlers + you're running both TLS and non-TLS instances, HTTP01 handlers will serve the same URLs! """ @@ -52,13 +51,13 @@ class ServerManager(object): :param int port: Port to run the server on. :param challenge_type: Subclass of `acme.challenges.Challenge`, - either `acme.challenge.SimpleHTTP` or `acme.challenges.DVSNI`. + either `acme.challenge.HTTP01` or `acme.challenges.DVSNI`. :returns: Server instance. :rtype: ACMEServerMixin """ - assert challenge_type in (challenges.DVSNI, challenges.SimpleHTTP) + assert challenge_type in (challenges.DVSNI, challenges.HTTP01) if port in self._instances: return self._instances[port].server @@ -66,8 +65,8 @@ class ServerManager(object): try: if challenge_type is challenges.DVSNI: server = acme_standalone.DVSNIServer(address, self.certs) - else: # challenges.SimpleHTTP - server = acme_standalone.SimpleHTTPServer( + else: # challenges.HTTP01 + server = acme_standalone.HTTP01Server( address, self.simple_http_resources) except socket.error as error: raise errors.StandaloneBindError(error, port) @@ -110,7 +109,7 @@ class ServerManager(object): in six.iteritems(self._instances)) -SUPPORTED_CHALLENGES = set([challenges.DVSNI, challenges.SimpleHTTP]) +SUPPORTED_CHALLENGES = set([challenges.DVSNI, challenges.HTTP01]) def supported_challenges_validator(data): @@ -139,7 +138,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 SimpleHTTP + 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. """ @@ -151,10 +150,10 @@ class Authenticator(common.Plugin): def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) - # one self-signed key for all DVSNI and SimpleHTTP certificates + # one self-signed key for all DVSNI and HTTP01 certificates self.key = OpenSSL.crypto.PKey() self.key.generate_key(OpenSSL.crypto.TYPE_RSA, bits=2048) - # TODO: generate only when the first SimpleHTTP challenge is solved + # 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"]) @@ -185,7 +184,7 @@ class Authenticator(common.Plugin): @property def _necessary_ports(self): necessary_ports = set() - if challenges.SimpleHTTP in self.supported_challenges: + if challenges.HTTP01 in self.supported_challenges: necessary_ports.add(self.config.simple_http_port) if challenges.DVSNI in self.supported_challenges: necessary_ports.add(self.config.dvsni_port) @@ -193,9 +192,9 @@ class Authenticator(common.Plugin): 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 SimpleHTTP 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 DVSNI " + "and HTTP01 challenges from the certificate authority. " + "Therefore, it does not rely on any existing server program.") def prepare(self): # pylint: disable=missing-docstring pass @@ -237,13 +236,12 @@ class Authenticator(common.Plugin): responses = [] for achall in achalls: - if isinstance(achall, achallenges.SimpleHTTP): + if isinstance(achall.chall, challenges.HTTP01): server = self.servers.run( - self.config.simple_http_port, challenges.SimpleHTTP) - response, validation = achall.gen_response_and_validation( - tls=False) + self.config.simple_http_port, challenges.HTTP01) + response, validation = achall.response_and_validation() self.simple_http_resources.add( - acme_standalone.SimpleHTTPRequestHandler.SimpleHTTPResource( + acme_standalone.HTTP01RequestHandler.HTTP01Resource( chall=achall.chall, response=response, validation=validation)) cert = self.simple_http_cert diff --git a/letsencrypt/plugins/standalone_test.py b/letsencrypt/plugins/standalone_test.py index 3a6941be8..6bf84cb73 100644 --- a/letsencrypt/plugins/standalone_test.py +++ b/letsencrypt/plugins/standalone_test.py @@ -43,12 +43,12 @@ class ServerManagerTest(unittest.TestCase): self._test_run_stop(challenges.DVSNI) def test_run_stop_simplehttp(self): - self._test_run_stop(challenges.SimpleHTTP) + self._test_run_stop(challenges.HTTP01) def test_run_idempotent(self): - server = self.mgr.run(port=0, challenge_type=challenges.SimpleHTTP) + server = self.mgr.run(port=0, challenge_type=challenges.HTTP01) port = server.socket.getsockname()[1] # pylint: disable=no-member - server2 = self.mgr.run(port=port, challenge_type=challenges.SimpleHTTP) + server2 = self.mgr.run(port=port, challenge_type=challenges.HTTP01) self.assertEqual(self.mgr.running(), {port: server}) self.assertTrue(server is server2) self.mgr.stop(port) @@ -60,7 +60,7 @@ class ServerManagerTest(unittest.TestCase): port = some_server.getsockname()[1] self.assertRaises( errors.StandaloneBindError, self.mgr.run, port, - challenge_type=challenges.SimpleHTTP) + challenge_type=challenges.HTTP01) self.assertEqual(self.mgr.running(), {}) @@ -74,9 +74,9 @@ class SupportedChallengesValidatorTest(unittest.TestCase): def test_correct(self): self.assertEqual("dvsni", self._call("dvsni")) - self.assertEqual("simpleHttp", self._call("simpleHttp")) - self.assertEqual("dvsni,simpleHttp", self._call("dvsni,simpleHttp")) - self.assertEqual("simpleHttp,dvsni", self._call("simpleHttp,dvsni")) + 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")) def test_unrecognized(self): assert "foo" not in challenges.Challenge.TYPES @@ -91,25 +91,26 @@ class AuthenticatorTest(unittest.TestCase): def setUp(self): from letsencrypt.plugins.standalone import Authenticator - self.config = mock.MagicMock(dvsni_port=1234, simple_http_port=4321, - standalone_supported_challenges="dvsni,simpleHttp") + self.config = mock.MagicMock( + dvsni_port=1234, simple_http_port=4321, + standalone_supported_challenges="dvsni,http-01") self.auth = Authenticator(self.config, name="standalone") def test_supported_challenges(self): self.assertEqual(self.auth.supported_challenges, - set([challenges.DVSNI, challenges.SimpleHTTP])) + set([challenges.DVSNI, 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.SimpleHTTP])) + set([challenges.DVSNI, challenges.HTTP01])) @mock.patch("letsencrypt.plugins.standalone.util") def test_perform_alredy_listening(self, mock_util): for chall, port in ((challenges.DVSNI.typ, 1234), - (challenges.SimpleHTTP.typ, 4321)): + (challenges.HTTP01.typ, 4321)): mock_util.already_listening.return_value = True self.config.standalone_supported_challenges = chall self.assertRaises( @@ -152,8 +153,8 @@ 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.SimpleHTTP( - challb=acme_util.SIMPLE_HTTP_P, domain=domain, account_key=key) + simple_http = 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) @@ -167,11 +168,11 @@ class AuthenticatorTest(unittest.TestCase): self.assertTrue(isinstance(responses, list)) self.assertEqual(2, len(responses)) - self.assertTrue(isinstance(responses[0], challenges.SimpleHTTPResponse)) + self.assertTrue(isinstance(responses[0], challenges.HTTP01Response)) self.assertTrue(isinstance(responses[1], challenges.DVSNIResponse)) self.assertEqual(self.auth.servers.run.mock_calls, [ - mock.call(4321, challenges.SimpleHTTP), + mock.call(4321, challenges.HTTP01), mock.call(1234, challenges.DVSNI), ]) self.assertEqual(self.auth.served, { @@ -181,8 +182,8 @@ class AuthenticatorTest(unittest.TestCase): self.assertEqual(1, len(self.auth.simple_http_resources)) self.assertEqual(2, len(self.auth.certs)) self.assertEqual(list(self.auth.simple_http_resources), [ - acme_standalone.SimpleHTTPRequestHandler.SimpleHTTPResource( - acme_util.SIMPLE_HTTP, responses[0], mock.ANY)]) + acme_standalone.HTTP01RequestHandler.HTTP01Resource( + acme_util.HTTP01, responses[0], mock.ANY)]) def test_cleanup(self): self.auth.servers = mock.Mock() diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 7c0ee8fea..18a996926 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -28,7 +28,7 @@ common() { } common --domains le1.wtf --standalone-supported-challenges dvsni auth -common --domains le2.wtf --standalone-supported-challenges simpleHttp run +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" \ From e62490c0514a6883d9b91aa5d4d6c6747f0364e1 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Oct 2015 15:02:47 +0000 Subject: [PATCH 64/74] http-01 for webroot --- letsencrypt/plugins/webroot.py | 13 ++++++------- letsencrypt/plugins/webroot_test.py | 13 ++++++++----- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/letsencrypt/plugins/webroot.py b/letsencrypt/plugins/webroot.py index f11325f57..879da2527 100644 --- a/letsencrypt/plugins/webroot.py +++ b/letsencrypt/plugins/webroot.py @@ -23,7 +23,7 @@ class Authenticator(common.Plugin): description = "Webroot Authenticator" MORE_INFO = """\ -Authenticator plugin that performs SimpleHTTP challenge by saving +Authenticator plugin that performs http-01 challenge by saving necessary validation resources to appropriate paths on the file system. It expects that there is some other HTTP server configured to serve all files under specified web root ({0}).""" @@ -37,7 +37,7 @@ to serve all files under specified web root ({0}).""" def get_chall_pref(self, domain): # pragma: no cover # pylint: disable=missing-docstring,no-self-use,unused-argument - return [challenges.SimpleHTTP] + return [challenges.HTTP01] def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) @@ -51,8 +51,7 @@ to serve all files under specified web root ({0}).""" if not os.path.isdir(path): raise errors.PluginError( path + " does not exist or is not a directory") - self.full_root = os.path.join( - path, challenges.SimpleHTTPResponse.URI_ROOT_PATH) + self.full_root = os.path.join(path, challenges.HTTP01.URI_ROOT_PATH) logger.debug("Creating root challenges validation dir at %s", self.full_root) @@ -61,7 +60,7 @@ to serve all files under specified web root ({0}).""" except OSError as exception: if exception.errno != errno.EEXIST: raise errors.PluginError( - "Couldn't create root for SimpleHTTP " + "Couldn't create root for http-01 " "challenge responses: {0}", exception) def perform(self, achalls): # pylint: disable=missing-docstring @@ -72,11 +71,11 @@ to serve all files under specified web root ({0}).""" return os.path.join(self.full_root, achall.chall.encode("token")) def _perform_single(self, achall): - response, validation = achall.gen_response_and_validation(tls=False) + response, validation = achall.response_and_validation() path = self._path_for_achall(achall) logger.debug("Attempting to save validation to %s", path) with open(path, "w") as validation_file: - validation_file.write(validation.json_dumps()) + validation_file.write(validation.encode()) return response def cleanup(self, achalls): # pylint: disable=missing-docstring diff --git a/letsencrypt/plugins/webroot_test.py b/letsencrypt/plugins/webroot_test.py index d8c0e2aa2..aa8f16e38 100644 --- a/letsencrypt/plugins/webroot_test.py +++ b/letsencrypt/plugins/webroot_test.py @@ -6,6 +6,7 @@ import unittest import mock +from acme import challenges from acme import jose from letsencrypt import achallenges @@ -21,8 +22,8 @@ KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) class AuthenticatorTest(unittest.TestCase): """Tests for letsencrypt.plugins.webroot.Authenticator.""" - achall = achallenges.SimpleHTTP( - challb=acme_util.SIMPLE_HTTP_P, domain=None, account_key=KEY) + achall = achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.HTTP01_P, domain=None, account_key=KEY) def setUp(self): from letsencrypt.plugins.webroot import Authenticator @@ -70,9 +71,11 @@ class AuthenticatorTest(unittest.TestCase): self.assertEqual(1, len(responses)) self.assertTrue(os.path.exists(self.validation_path)) with open(self.validation_path) as validation_f: - validation = jose.JWS.json_loads(validation_f.read()) - self.assertTrue(responses[0].check_validation( - validation, self.achall.chall, KEY.public_key())) + validation = validation_f.read() + self.assertTrue( + challenges.KeyAuthorizationChallengeResponse( + key_authorization=validation).verify( + self.achall.chall, KEY.public_key())) self.auth.cleanup([self.achall]) self.assertFalse(os.path.exists(self.validation_path)) From 8d913b42c56f2d5998a7c69a5926687768a74277 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Oct 2015 22:12:17 +0000 Subject: [PATCH 65/74] Remove auth_handler_test.TRANSLATE --- letsencrypt/tests/auth_handler_test.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 247fca4e1..8a4c371f5 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -16,15 +16,6 @@ from letsencrypt import le_util from letsencrypt.tests import acme_util -TRANSLATE = { - "dvsni": "DVSNI", - "simpleHttp": "SimpleHTTP", - "dns": "DNS", - "recoveryContact": "RecoveryContact", - "proofOfPossession": "ProofOfPossession", -} - - class ChallengeFactoryTest(unittest.TestCase): # pylint: disable=protected-access From 581a701cd19c7b7236d018109ace58e5a9ba75c8 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Oct 2015 22:23:57 +0000 Subject: [PATCH 66/74] Kill simpleHttp in core --- letsencrypt/achallenges.py | 25 ------------------------- letsencrypt/auth_handler.py | 3 --- letsencrypt/configuration.py | 4 ++-- letsencrypt/constants.py | 2 +- letsencrypt/tests/acme_util.py | 7 ++----- letsencrypt/tests/auth_handler_test.py | 24 ++++++++++++------------ 6 files changed, 17 insertions(+), 48 deletions(-) diff --git a/letsencrypt/achallenges.py b/letsencrypt/achallenges.py index 7aa2bd35a..f08c6a396 100644 --- a/letsencrypt/achallenges.py +++ b/letsencrypt/achallenges.py @@ -85,31 +85,6 @@ class DVSNI(AnnotatedChallenge): return response, cert, key -class SimpleHTTP(AnnotatedChallenge): - """Client annotated "simpleHttp" ACME challenge.""" - __slots__ = ('challb', 'domain', 'account_key') - acme_type = challenges.SimpleHTTP - - def gen_response_and_validation(self, tls): - """Generates a SimpleHTTP response and validation. - - :param bool tls: True if TLS should be used - - :returns: ``(response, validation)`` tuple, where ``response`` is - an instance of `acme.challenges.SimpleHTTPResponse` and - ``validation`` is an instance of - `acme.challenges.SimpleHTTPProvisionedResource`. - :rtype: tuple - - """ - response = challenges.SimpleHTTPResponse(tls=tls) - - validation = response.gen_validation( - self.challb.chall, self.account_key) - logger.debug("Simple HTTP validation payload: %s", validation.payload) - return response, validation - - class DNS(AnnotatedChallenge): """Client annotated "dns" ACME challenge.""" __slots__ = ('challb', 'domain') diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index f7b6bd507..11019daac 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -347,9 +347,6 @@ def challb_to_achall(challb, account_key, domain): if isinstance(chall, challenges.DVSNI): return achallenges.DVSNI( challb=challb, domain=domain, account_key=account_key) - elif isinstance(chall, challenges.SimpleHTTP): - return achallenges.SimpleHTTP( - challb=challb, domain=domain, account_key=account_key) elif isinstance(chall, challenges.DNS): return achallenges.DNS(challb=challb, domain=domain) elif isinstance(chall, challenges.RecoveryContact): diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index f72005233..4fb417127 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -39,7 +39,7 @@ class NamespaceConfig(object): if self.simple_http_port == self.dvsni_port: raise errors.Error( - "Trying to run SimpleHTTP and DVSNI " + "Trying to run http-01 and DVSNI " "on the same port ({0})".format(self.dvsni_port)) def __getattr__(self, name): @@ -82,7 +82,7 @@ class NamespaceConfig(object): if self.namespace.simple_http_port is not None: return self.namespace.simple_http_port else: - return challenges.SimpleHTTPResponse.PORT + return challenges.HTTP01Response.PORT class RenewerConfiguration(object): diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index 362009ec6..15f8c53f0 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -41,7 +41,7 @@ RENEWER_DEFAULTS = dict( EXCLUSIVE_CHALLENGES = frozenset([frozenset([ - challenges.DVSNI, challenges.SimpleHTTP])]) + challenges.DVSNI, challenges.HTTP01])]) """Mutually exclusive challenges.""" diff --git a/letsencrypt/tests/acme_util.py b/letsencrypt/tests/acme_util.py index 1e3086b18..300eb453b 100644 --- a/letsencrypt/tests/acme_util.py +++ b/letsencrypt/tests/acme_util.py @@ -12,8 +12,6 @@ from letsencrypt.tests import test_util KEY = test_util.load_rsa_private_key('rsa512_key.pem') # Challenges -SIMPLE_HTTP = challenges.SimpleHTTP( - token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") HTTP01 = challenges.HTTP01( token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") DVSNI = challenges.DVSNI( @@ -43,7 +41,7 @@ POP = challenges.ProofOfPossession( ) ) -CHALLENGES = [SIMPLE_HTTP, DVSNI, DNS, RECOVERY_CONTACT, POP] +CHALLENGES = [HTTP01, DVSNI, DNS, RECOVERY_CONTACT, POP] DV_CHALLENGES = [chall for chall in CHALLENGES if isinstance(chall, challenges.DVChallenge)] CONT_CHALLENGES = [chall for chall in CHALLENGES @@ -82,13 +80,12 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name # Pending ChallengeBody objects DVSNI_P = chall_to_challb(DVSNI, messages.STATUS_PENDING) -SIMPLE_HTTP_P = chall_to_challb(SIMPLE_HTTP, 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 = [SIMPLE_HTTP_P, DVSNI_P, DNS_P, RECOVERY_CONTACT_P, POP_P] +CHALLENGES_P = [HTTP01_P, DVSNI_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 8a4c371f5..7be37c91e 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -309,8 +309,8 @@ class GenChallengePathTest(unittest.TestCase): return gen_challenge_path(challbs, preferences, combinations) def test_common_case(self): - """Given DVSNI and SimpleHTTP with appropriate combos.""" - challbs = (acme_util.DVSNI_P, acme_util.SIMPLE_HTTP_P) + """Given DVSNI and HTTP01 with appropriate combos.""" + challbs = (acme_util.DVSNI_P, acme_util.HTTP01_P) prefs = [challenges.DVSNI] combos = ((0,), (1,)) @@ -325,7 +325,7 @@ class GenChallengePathTest(unittest.TestCase): challbs = (acme_util.POP_P, acme_util.RECOVERY_CONTACT_P, acme_util.DVSNI_P, - acme_util.SIMPLE_HTTP_P) + acme_util.HTTP01_P) prefs = [challenges.ProofOfPossession, challenges.DVSNI] combos = acme_util.gen_combos(challbs) self.assertEqual(self._call(challbs, prefs, combos), (0, 2)) @@ -337,12 +337,12 @@ class GenChallengePathTest(unittest.TestCase): challbs = (acme_util.RECOVERY_CONTACT_P, acme_util.POP_P, acme_util.DVSNI_P, - acme_util.SIMPLE_HTTP_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.SimpleHTTP, + challenges.HTTP01, challenges.DVSNI, challenges.RecoveryContact] combos = acme_util.gen_combos(challbs) @@ -411,8 +411,8 @@ 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.SimpleHTTP]), - frozenset([challenges.DNS, challenges.SimpleHTTP]), + frozenset([challenges.DVSNI, challenges.HTTP01]), + frozenset([challenges.DNS, challenges.HTTP01]), ])) def test_empty_satisfied(self): @@ -421,7 +421,7 @@ class IsPreferredTest(unittest.TestCase): def test_mutually_exclusvie(self): self.assertFalse( self._call( - acme_util.DVSNI_P, frozenset([acme_util.SIMPLE_HTTP_P]))) + acme_util.DVSNI_P, frozenset([acme_util.HTTP01_P]))) def test_mutually_exclusive_same_type(self): self.assertTrue( @@ -434,13 +434,13 @@ class ReportFailedChallsTest(unittest.TestCase): def setUp(self): kwargs = { - "chall": acme_util.SIMPLE_HTTP, + "chall": acme_util.HTTP01, "uri": "uri", "status": messages.STATUS_INVALID, "error": messages.Error(typ="tls", detail="detail"), } - self.simple_http = achallenges.SimpleHTTP( + self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge( # pylint: disable=star-args challb=messages.ChallengeBody(**kwargs), domain="example.com", @@ -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.simple_http, self.dvsni_same]) + auth_handler._report_failed_challs([self.http01, self.dvsni_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.simple_http, self.dvsni_diff]) + auth_handler._report_failed_challs([self.http01, self.dvsni_diff]) self.assertTrue(mock_zope().add_message.call_count == 2) From 063544817b9138d7cf7fcb7806977e532b67c580 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 1 Nov 2015 08:46:00 +0000 Subject: [PATCH 67/74] Kill simplehttp apache/nginx --- letsencrypt-apache/letsencrypt_apache/dvsni.py | 2 +- letsencrypt-nginx/letsencrypt_nginx/dvsni.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/dvsni.py b/letsencrypt-apache/letsencrypt_apache/dvsni.py index c6c41dc51..ed88bf8a7 100644 --- a/letsencrypt-apache/letsencrypt_apache/dvsni.py +++ b/letsencrypt-apache/letsencrypt_apache/dvsni.py @@ -20,7 +20,7 @@ class ApacheDvsni(common.Dvsni): larger array. ApacheDvsni is capable of solving many challenges at once which causes an indexing issue within ApacheConfigurator who must return all responses in order. Imagine ApacheConfigurator - maintaining state about where all of the SimpleHTTP Challenges, + maintaining state about where all of the http-01 Challenges, Dvsni Challenges belong in the response array. This is an optional utility. diff --git a/letsencrypt-nginx/letsencrypt_nginx/dvsni.py b/letsencrypt-nginx/letsencrypt_nginx/dvsni.py index 9ac2fcd7c..662f10889 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/dvsni.py +++ b/letsencrypt-nginx/letsencrypt_nginx/dvsni.py @@ -26,7 +26,7 @@ class NginxDvsni(common.Dvsni): larger array. NginxDvsni is capable of solving many challenges at once which causes an indexing issue within NginxConfigurator who must return all responses in order. Imagine NginxConfigurator - maintaining state about where all of the SimpleHTTP Challenges, + maintaining state about where all of the http-01 Challenges, Dvsni Challenges belong in the response array. This is an optional utility. From 1d36a15ab702594072a1b46a379de1e1594b2475 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 1 Nov 2015 10:41:57 +0000 Subject: [PATCH 68/74] Kill simpleHttp in acme --- acme/acme/challenges.py | 160 --------------------------------- acme/acme/challenges_test.py | 169 ----------------------------------- 2 files changed, 329 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 86ff0a902..b424071a1 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -318,166 +318,6 @@ class HTTP01(KeyAuthorizationChallenge): return self.key_authorization(account_key) -@Challenge.register # pylint: disable=too-many-ancestors -class SimpleHTTP(_TokenDVChallenge): - """ACME "simpleHttp" challenge.""" - typ = "simpleHttp" - - URI_ROOT_PATH = ".well-known/acme-challenge" - """URI root path for the server provisioned resource.""" - - @property - def path(self): - """Path (starting with '/') for provisioned resource.""" - return '/' + self.URI_ROOT_PATH + '/' + self.encode('token') - - -@ChallengeResponse.register -class SimpleHTTPResponse(ChallengeResponse): - """ACME "simpleHttp" challenge response. - - :ivar bool tls: - - """ - typ = "simpleHttp" - tls = jose.Field("tls", default=True, omitempty=True) - - URI_ROOT_PATH = SimpleHTTP.URI_ROOT_PATH - _URI_TEMPLATE = "{scheme}://{domain}/" + URI_ROOT_PATH + "/{token}" - - CONTENT_TYPE = "application/jose+json" - PORT = 80 - TLS_PORT = 443 - - @property - def scheme(self): - """URL scheme for the provisioned resource.""" - return "https" if self.tls else "http" - - @property - def port(self): - """Port that the ACME client should be listening for validation.""" - return self.TLS_PORT if self.tls else self.PORT - - def uri(self, domain, chall): - """Create an URI to the provisioned resource. - - Forms an URI to the HTTPS server provisioned resource - (containing :attr:`~SimpleHTTP.token`). - - :param unicode domain: Domain name being verified. - :param challenges.SimpleHTTP chall: - - """ - return self._URI_TEMPLATE.format( - scheme=self.scheme, domain=domain, token=chall.encode("token")) - - def gen_resource(self, chall): - """Generate provisioned resource. - - :param challenges.SimpleHTTP chall: - :rtype: SimpleHTTPProvisionedResource - - """ - return SimpleHTTPProvisionedResource(token=chall.token, tls=self.tls) - - def gen_validation(self, chall, account_key, alg=jose.RS256, **kwargs): - """Generate validation. - - :param challenges.SimpleHTTP chall: - :param .JWK account_key: Private account key. - :param .JWA alg: - - :returns: `.SimpleHTTPProvisionedResource` signed in `.JWS` - :rtype: .JWS - - """ - return jose.JWS.sign( - payload=self.gen_resource(chall).json_dumps( - sort_keys=True).encode('utf-8'), - key=account_key, alg=alg, **kwargs) - - def check_validation(self, validation, chall, account_public_key): - """Check validation. - - :param .JWS validation: - :param challenges.SimpleHTTP chall: - :param .JWK account_public_key: - - :rtype: bool - - """ - if not validation.verify(key=account_public_key): - return False - - try: - resource = SimpleHTTPProvisionedResource.json_loads( - validation.payload.decode('utf-8')) - except jose.DeserializationError as error: - logger.debug(error) - return False - - return resource.token == chall.token and resource.tls == self.tls - - def simple_verify(self, chall, domain, account_public_key, port=None): - """Simple verify. - - According to the ACME specification, "the ACME server MUST - ignore the certificate provided by the HTTPS server", so - ``requests.get`` is called with ``verify=False``. - - :param challenges.SimpleHTTP chall: Corresponding challenge. - :param unicode domain: Domain name being verified. - :param JWK account_public_key: Public key for the key pair - being authorized. If ``None`` key verification is not - performed! - :param int port: Port used in the validation. - - :returns: ``True`` iff validation is successful, ``False`` - otherwise. - :rtype: bool - - """ - # TODO: ACME specification defines URI template that doesn't - # allow to use a custom port... Make sure port is not in the - # 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) - domain += ":{0}".format(port) - - uri = self.uri(domain, chall) - logger.debug("Verifying %s at %s...", chall.typ, uri) - try: - http_response = requests.get(uri, verify=False) - except requests.exceptions.RequestException as error: - logger.error("Unable to reach %s: %s", uri, error) - return False - logger.debug("Received %s: %s. Headers: %s", http_response, - http_response.text, http_response.headers) - - if self.CONTENT_TYPE != http_response.headers.get( - "Content-Type", self.CONTENT_TYPE): - return False - - try: - validation = jose.JWS.json_loads(http_response.text) - except jose.DeserializationError as error: - logger.debug(error) - return False - - return self.check_validation(validation, chall, account_public_key) - - -class SimpleHTTPProvisionedResource(jose.JSONObjectWithFields): - """SimpleHTTP provisioned resource.""" - typ = fields.Fixed("type", SimpleHTTP.typ) - token = SimpleHTTP._fields["token"] - # If the "tls" field is not included in the response, then - # validation object MUST have its "tls" field set to "true". - tls = jose.Field("tls", omitempty=False) - - @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 53e6b528a..0708f3782 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -186,175 +186,6 @@ class HTTP01Test(unittest.TestCase): self.msg.update(token=b'..').good_token) -class SimpleHTTPTest(unittest.TestCase): - - def setUp(self): - from acme.challenges import SimpleHTTP - self.msg = SimpleHTTP( - token=jose.decode_b64jose( - 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')) - self.jmsg = { - 'type': 'simpleHttp', - 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', - } - - def test_path(self): - self.assertEqual(self.msg.path, '/.well-known/acme-challenge/' - 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA') - - def test_to_partial_json(self): - self.assertEqual(self.jmsg, self.msg.to_partial_json()) - - def test_from_json(self): - from acme.challenges import SimpleHTTP - self.assertEqual(self.msg, SimpleHTTP.from_json(self.jmsg)) - - def test_from_json_hashable(self): - from acme.challenges import SimpleHTTP - hash(SimpleHTTP.from_json(self.jmsg)) - - def test_good_token(self): - self.assertTrue(self.msg.good_token) - self.assertFalse( - self.msg.update(token=b'..').good_token) - - -class SimpleHTTPResponseTest(unittest.TestCase): - # pylint: disable=too-many-instance-attributes - - def setUp(self): - from acme.challenges import SimpleHTTPResponse - self.msg_http = SimpleHTTPResponse(tls=False) - self.msg_https = SimpleHTTPResponse(tls=True) - self.jmsg_http = { - 'resource': 'challenge', - 'type': 'simpleHttp', - 'tls': False, - } - self.jmsg_https = { - 'resource': 'challenge', - 'type': 'simpleHttp', - 'tls': True, - } - - from acme.challenges import SimpleHTTP - self.chall = SimpleHTTP(token=(b"x" * 16)) - self.resp_http = SimpleHTTPResponse(tls=False) - self.resp_https = SimpleHTTPResponse(tls=True) - self.good_headers = {'Content-Type': SimpleHTTPResponse.CONTENT_TYPE} - - def test_to_partial_json(self): - self.assertEqual(self.jmsg_http, self.msg_http.to_partial_json()) - self.assertEqual(self.jmsg_https, self.msg_https.to_partial_json()) - - def test_from_json(self): - from acme.challenges import SimpleHTTPResponse - self.assertEqual( - self.msg_http, SimpleHTTPResponse.from_json(self.jmsg_http)) - self.assertEqual( - self.msg_https, SimpleHTTPResponse.from_json(self.jmsg_https)) - - def test_from_json_hashable(self): - from acme.challenges import SimpleHTTPResponse - hash(SimpleHTTPResponse.from_json(self.jmsg_http)) - hash(SimpleHTTPResponse.from_json(self.jmsg_https)) - - def test_scheme(self): - self.assertEqual('http', self.msg_http.scheme) - self.assertEqual('https', self.msg_https.scheme) - - def test_port(self): - self.assertEqual(80, self.msg_http.port) - self.assertEqual(443, self.msg_https.port) - - def test_uri(self): - self.assertEqual( - 'http://example.com/.well-known/acme-challenge/' - 'eHh4eHh4eHh4eHh4eHh4eA', self.msg_http.uri( - 'example.com', self.chall)) - self.assertEqual( - 'https://example.com/.well-known/acme-challenge/' - 'eHh4eHh4eHh4eHh4eHh4eA', self.msg_https.uri( - 'example.com', self.chall)) - - def test_gen_check_validation(self): - account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) - self.assertTrue(self.resp_http.check_validation( - validation=self.resp_http.gen_validation(self.chall, account_key), - chall=self.chall, account_public_key=account_key.public_key())) - - def test_gen_check_validation_wrong_key(self): - key1 = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) - key2 = jose.JWKRSA.load(test_util.load_vector('rsa1024_key.pem')) - self.assertFalse(self.resp_http.check_validation( - validation=self.resp_http.gen_validation(self.chall, key1), - chall=self.chall, account_public_key=key2.public_key())) - - def test_check_validation_wrong_payload(self): - account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) - validations = tuple( - jose.JWS.sign(payload=payload, alg=jose.RS256, key=account_key) - for payload in (b'', b'{}', self.chall.json_dumps().encode('utf-8'), - self.resp_http.json_dumps().encode('utf-8')) - ) - for validation in validations: - self.assertFalse(self.resp_http.check_validation( - validation=validation, chall=self.chall, - account_public_key=account_key.public_key())) - - def test_check_validation_wrong_fields(self): - resource = self.resp_http.gen_resource(self.chall) - account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) - validations = tuple( - jose.JWS.sign(payload=bad_resource.json_dumps().encode('utf-8'), - alg=jose.RS256, key=account_key) - for bad_resource in (resource.update(tls=True), - resource.update(token=(b'x' * 20))) - ) - for validation in validations: - self.assertFalse(self.resp_http.check_validation( - validation=validation, chall=self.chall, - account_public_key=account_key.public_key())) - - @mock.patch("acme.challenges.requests.get") - def test_simple_verify_good_validation(self, mock_get): - account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) - for resp in self.resp_http, self.resp_https: - mock_get.reset_mock() - validation = resp.gen_validation(self.chall, account_key) - mock_get.return_value = mock.MagicMock( - text=validation.json_dumps(), headers=self.good_headers) - self.assertTrue(resp.simple_verify(self.chall, "local", None)) - mock_get.assert_called_once_with(resp.uri( - "local", self.chall), verify=False) - - @mock.patch("acme.challenges.requests.get") - def test_simple_verify_bad_validation(self, mock_get): - mock_get.return_value = mock.MagicMock( - text="!", headers=self.good_headers) - self.assertFalse(self.resp_http.simple_verify( - self.chall, "local", None)) - - @mock.patch("acme.challenges.requests.get") - def test_simple_verify_bad_content_type(self, mock_get): - mock_get().text = self.chall.token - self.assertFalse(self.resp_http.simple_verify( - self.chall, "local", None)) - - @mock.patch("acme.challenges.requests.get") - def test_simple_verify_connection_error(self, mock_get): - mock_get.side_effect = requests.exceptions.RequestException - self.assertFalse(self.resp_http.simple_verify( - self.chall, "local", None)) - - @mock.patch("acme.challenges.requests.get") - def test_simple_verify_port(self, mock_get): - self.resp_http.simple_verify( - self.chall, domain="local", account_public_key=None, port=4430) - self.assertEqual("local:4430", urllib_parse.urlparse( - mock_get.mock_calls[0][1][0]).netloc) - - class DVSNITest(unittest.TestCase): def setUp(self): From 23d3c3b1e2bc5fbed5fb61c855e1a207ab1a9dda Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Oct 2015 22:04:52 +0000 Subject: [PATCH 69/74] Rename --simple-http-port to --http-01-port --- letsencrypt/cli.py | 4 ++-- letsencrypt/configuration.py | 8 ++++---- letsencrypt/interfaces.py | 2 +- letsencrypt/plugins/manual.py | 6 +++--- letsencrypt/plugins/manual_test.py | 4 ++-- letsencrypt/plugins/standalone.py | 4 ++-- letsencrypt/plugins/standalone_test.py | 2 +- letsencrypt/renewer.py | 2 +- letsencrypt/tests/configuration_test.py | 10 +++++----- letsencrypt/tests/renewer_test.py | 2 +- tests/integration/_common.sh | 2 +- 11 files changed, 23 insertions(+), 23 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index fdfa5bbfb..f8c0da85a 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -854,8 +854,8 @@ def prepare_and_parse_args(plugins, args): helpful.add( "testing", "--dvsni-port", type=int, default=flag_default("dvsni_port"), help=config_help("dvsni_port")) - helpful.add("testing", "--simple-http-port", type=int, - help=config_help("simple_http_port")) + helpful.add("testing", "--http-01-port", dest="http01_port", type=int, + help=config_help("http01_port")) helpful.add_group( "security", description="Security parameters & server settings") diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 4fb417127..b604651e9 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -37,7 +37,7 @@ class NamespaceConfig(object): def __init__(self, namespace): self.namespace = namespace - if self.simple_http_port == self.dvsni_port: + if self.http01_port == self.dvsni_port: raise errors.Error( "Trying to run http-01 and DVSNI " "on the same port ({0})".format(self.dvsni_port)) @@ -78,9 +78,9 @@ class NamespaceConfig(object): self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR) @property - def simple_http_port(self): # pylint: disable=missing-docstring - if self.namespace.simple_http_port is not None: - return self.namespace.simple_http_port + def http01_port(self): # pylint: disable=missing-docstring + if self.namespace.http01_port is not None: + return self.namespace.http01_port else: return challenges.HTTP01Response.PORT diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 8bf714c88..498b01683 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -223,7 +223,7 @@ class IConfig(zope.interface.Interface): "Port number to perform DVSNI challenge. " "Boulder in testing mode defaults to 5001.") - simple_http_port = zope.interface.Attribute( + http01_port = zope.interface.Attribute( "Port used in the SimpleHttp challenge.") diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index ffaad3d99..823de89ec 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -132,8 +132,8 @@ s.serve_forever()" """ # same server: default command doesn't support virtual hosts response, validation = achall.response_and_validation() - port = (response.port if self.config.simple_http_port is None - else int(self.config.simple_http_port)) + port = (response.port if self.config.http01_port is None + else int(self.config.http01_port)) command = self.CMD_TEMPLATE.format( root=self._root, achall=achall, response=response, # TODO(kuba): pipes still necessary? @@ -174,7 +174,7 @@ s.serve_forever()" """ if response.simple_verify( achall.chall, achall.domain, - achall.account_key.public_key(), self.config.simple_http_port): + achall.account_key.public_key(), self.config.http01_port): return response else: logger.error( diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py index 431cd07a7..a9281902f 100644 --- a/letsencrypt/plugins/manual_test.py +++ b/letsencrypt/plugins/manual_test.py @@ -23,13 +23,13 @@ class AuthenticatorTest(unittest.TestCase): def setUp(self): from letsencrypt.plugins.manual import Authenticator self.config = mock.MagicMock( - simple_http_port=8080, manual_test_mode=False) + http01_port=8080, manual_test_mode=False) self.auth = Authenticator(config=self.config, name="manual") self.achalls = [achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)] config_test_mode = mock.MagicMock( - simple_http_port=8080, manual_test_mode=True) + http01_port=8080, manual_test_mode=True) self.auth_test_mode = Authenticator( config=config_test_mode, name="manual") diff --git a/letsencrypt/plugins/standalone.py b/letsencrypt/plugins/standalone.py index c45337507..5041091e4 100644 --- a/letsencrypt/plugins/standalone.py +++ b/letsencrypt/plugins/standalone.py @@ -185,7 +185,7 @@ class Authenticator(common.Plugin): def _necessary_ports(self): necessary_ports = set() if challenges.HTTP01 in self.supported_challenges: - necessary_ports.add(self.config.simple_http_port) + necessary_ports.add(self.config.http01_port) if challenges.DVSNI in self.supported_challenges: necessary_ports.add(self.config.dvsni_port) return necessary_ports @@ -238,7 +238,7 @@ class Authenticator(common.Plugin): for achall in achalls: if isinstance(achall.chall, challenges.HTTP01): server = self.servers.run( - self.config.simple_http_port, challenges.HTTP01) + self.config.http01_port, challenges.HTTP01) response, validation = achall.response_and_validation() self.simple_http_resources.add( acme_standalone.HTTP01RequestHandler.HTTP01Resource( diff --git a/letsencrypt/plugins/standalone_test.py b/letsencrypt/plugins/standalone_test.py index 6bf84cb73..15da04417 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, simple_http_port=4321, + dvsni_port=1234, http01_port=4321, standalone_supported_challenges="dvsni,http-01") self.auth = Authenticator(self.config, name="standalone") diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py index c0014f4b2..40e49702a 100644 --- a/letsencrypt/renewer.py +++ b/letsencrypt/renewer.py @@ -76,7 +76,7 @@ def renew(cert, old_version): # was an int, not a str) config.rsa_key_size = int(config.rsa_key_size) config.dvsni_port = int(config.dvsni_port) - config.namespace.simple_http_port = int(config.namespace.simple_http_port) + config.namespace.http01_port = int(config.namespace.http01_port) zope.component.provideUtility(config) try: authenticator = plugins[renewalparams["authenticator"]] diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index 44bccb577..c7e227ee5 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -14,7 +14,7 @@ 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, simple_http_port=4321) + dvsni_port=1234, http01_port=4321) from letsencrypt.configuration import NamespaceConfig self.config = NamespaceConfig(self.namespace) @@ -54,10 +54,10 @@ class NamespaceConfigTest(unittest.TestCase): self.assertEqual(self.config.key_dir, '/tmp/config/keys') self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t') - def test_simple_http_port(self): - self.assertEqual(4321, self.config.simple_http_port) - self.namespace.simple_http_port = None - self.assertEqual(80, self.config.simple_http_port) + def test_http01_port(self): + self.assertEqual(4321, self.config.http01_port) + self.namespace.http01_port = None + self.assertEqual(80, self.config.http01_port) class RenewerConfigurationTest(unittest.TestCase): diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index 2123db367..05d7e123d 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -689,7 +689,7 @@ class RenewableCertTests(BaseRenewableCertTest): 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"]["simple_http_port"] = "1234" + self.test_rc.configfile["renewalparams"]["http01_port"] = "1234" self.test_rc.configfile["renewalparams"]["account"] = "abcde" mock_auth = mock.MagicMock() mock_pd.PluginsRegistry.find_all.return_value = {"apache": mock_auth} diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index 07eca44b2..cd894fd10 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -16,7 +16,7 @@ letsencrypt_test () { --server "${SERVER:-http://localhost:4000/directory}" \ --no-verify-ssl \ --dvsni-port 5001 \ - --simple-http-port 5002 \ + --http-01-port 5002 \ --manual-test-mode \ $store_flags \ --text \ From 99c5c2034fd54aacef1b29794e0551bf6bbf262e Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 1 Nov 2015 11:19:35 +0000 Subject: [PATCH 70/74] Revert "Quickfix for misterious abstract-class-little-used" This reverts commit 01bc073111d139183e24c9d702d5942beb864022. --- acme/acme/jose/jwk.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/acme/acme/jose/jwk.py b/acme/acme/jose/jwk.py index c82134da9..4d07229b3 100644 --- a/acme/acme/jose/jwk.py +++ b/acme/acme/jose/jwk.py @@ -21,12 +21,6 @@ from acme.jose import util logger = logging.getLogger(__name__) -# TODO: bug in pylint? -# ************* Module acme.challenges -# R:153, 0: Abstract class is only referenced 1 times (abstract-class-little-used) -# pylint: disable=abstract-class-little-used - - class JWK(json_util.TypedJSONObjectWithFields): # pylint: disable=too-few-public-methods """JSON Web Key.""" From ee68d5eb8607edfd7628e0426047fc072399c7a7 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 1 Nov 2015 11:23:56 +0000 Subject: [PATCH 71/74] Another attempt at abstract-class-little-used --- .pylintrc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.pylintrc b/.pylintrc index 268d61ec6..f31b77e9a 100644 --- a/.pylintrc +++ b/.pylintrc @@ -38,8 +38,9 @@ load-plugins=linter_plugin # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=fixme,locally-disabled,abstract-class-not-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name -# abstract-class-not-used cannot be disabled locally (at least in pylint 1.4.1) +disable=fixme,locally-disabled,abstract-class-not-used,abstract-class-little-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name +# abstract-class-not-used cannot be disabled locally (at least in +# pylint 1.4.1), same for abstract-class-little-used [REPORTS] From 21bfe5b98329be348a15cfd97b93f21f7aef26c6 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sun, 1 Nov 2015 22:48:58 -0800 Subject: [PATCH 72/74] Remove stray variable --- letsencrypt/tests/cli_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index e38448249..7f8fbb351 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -68,7 +68,6 @@ class CLITest(unittest.TestCase): "Run a help command, and return the help string for scrutiny" output = StringIO.StringIO() with mock.patch('letsencrypt.cli.sys.stdout', new=output): - plugins = disco.PluginsRegistry.find_all() self.assertRaises(SystemExit, self._call_stdout, args) out = output.getvalue() return out From eb09190656e1eb61aca88a047ee0432533abf50f Mon Sep 17 00:00:00 2001 From: Dev & Sec Date: Mon, 2 Nov 2015 21:21:42 +0000 Subject: [PATCH 73/74] Remove duplicated python-devel in redhat bootstrap script --- bootstrap/_rpm_common.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/bootstrap/_rpm_common.sh b/bootstrap/_rpm_common.sh index 3fd0f59f9..26b91b8c4 100755 --- a/bootstrap/_rpm_common.sh +++ b/bootstrap/_rpm_common.sh @@ -21,7 +21,6 @@ $tool install -y \ python \ python-devel \ python-virtualenv \ - python-devel \ gcc \ dialog \ augeas-libs \ From 353240470527f5716df2887dadb9d45bf86e5f31 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 2 Nov 2015 17:18:44 -0800 Subject: [PATCH 74/74] Nit fix --- 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 b424071a1..c5855a7ca 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -282,7 +282,7 @@ class HTTP01(KeyAuthorizationChallenge): typ = response_cls.typ CONTENT_TYPE = "text/plain" - """Content-Type header that must be used for provisioned resource.""" + """Only valid value for Content-Type if the header is included.""" URI_ROOT_PATH = ".well-known/acme-challenge" """URI root path for the server provisioned resource."""