From 8c6d1ad50aa95091790be8f9d6e289f6c662b7c4 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 2 Jun 2015 13:55:16 +0000 Subject: [PATCH 01/70] letsencrypt_nginx should not depend on letsencrypt_apache. --- letsencrypt/plugins/common.py | 130 ++++++++++++++++++ letsencrypt/plugins/common_test.py | 106 ++++++++++++++ letsencrypt_apache/configurator.py | 12 +- letsencrypt_apache/dvsni.py | 57 +------- letsencrypt_apache/obj.py | 45 +----- letsencrypt_apache/tests/configurator_test.py | 7 +- letsencrypt_apache/tests/dvsni_test.py | 63 +-------- letsencrypt_apache/tests/obj_test.py | 50 +------ letsencrypt_apache/tests/util.py | 48 ++----- letsencrypt_nginx/dvsni.py | 5 +- letsencrypt_nginx/obj.py | 4 +- letsencrypt_nginx/tests/dvsni_test.py | 66 ++++----- letsencrypt_nginx/tests/util.py | 6 +- 13 files changed, 313 insertions(+), 286 deletions(-) diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 32bee2b49..90296c5c7 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -1,8 +1,14 @@ """Plugin common functions.""" +import os +import pkg_resources +import shutil +import tempfile + import zope.interface from acme.jose import util as jose_util +from letsencrypt import constants from letsencrypt import interfaces @@ -69,3 +75,127 @@ class Plugin(object): with unique plugin name prefix. """ + +# other + +class Addr(object): + r"""Represents an virtual host address. + + :param str addr: addr part of vhost address + :param str port: port number or \*, or "" + + """ + def __init__(self, tup): + self.tup = tup + + @classmethod + def fromstring(cls, str_addr): + """Initialize Addr from string.""" + tup = str_addr.partition(':') + return cls((tup[0], tup[2])) + + def __str__(self): + if self.tup[1]: + return "%s:%s" % self.tup + return self.tup[0] + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.tup == other.tup + return False + + def __hash__(self): + return hash(self.tup) + + def get_addr(self): + """Return addr part of Addr object.""" + return self.tup[0] + + def get_port(self): + """Return port.""" + return self.tup[1] + + def get_addr_obj(self, port): + """Return new address object with same addr and new port.""" + return self.__class__((self.tup[0], port)) + + +class Dvsni(object): + """Class that perform DVSNI challenges.""" + + def __init__(self, configurator): + self.configurator = configurator + self.achalls = [] + self.indices = [] + self.challenge_conf = os.path.join( + configurator.config.config_dir, "le_dvsni_cert_challenge.conf") + # self.completed = 0 + + def add_chall(self, achall, idx=None): + """Add challenge to DVSNI object to perform at once. + + :param achall: Annotated DVSNI challenge. + :type achall: :class:`letsencrypt.achallenges.DVSNI` + + :param int idx: index to challenge in a larger array + + """ + self.achalls.append(achall) + if idx is not None: + self.indices.append(idx) + + def get_cert_file(self, achall): + """Returns standardized name for challenge certificate. + + :param achall: Annotated DVSNI challenge. + :type achall: :class:`letsencrypt.achallenges.DVSNI` + + :returns: certificate file name + :rtype: str + + """ + return os.path.join( + self.configurator.config.work_dir, achall.nonce_domain + ".crt") + + def _setup_challenge_cert(self, achall, s=None): + # pylint: disable=invalid-name + """Generate and write out challenge certificate.""" + cert_path = self.get_cert_file(achall) + # Register the path before you write out the file + self.configurator.reverter.register_file_creation(True, cert_path) + + cert_pem, response = achall.gen_cert_and_response(s) + + # Write out challenge cert + with open(cert_path, "w") as cert_chall_fd: + cert_chall_fd.write(cert_pem) + + return response + + +# test utils + +def setup_ssl_options(config_dir, mod_ssl_conf): + """Move the ssl_options into position and return the path.""" + option_path = os.path.join(config_dir, "options-ssl.conf") + shutil.copyfile(mod_ssl_conf, option_path) + return option_path + + +def dir_setup(test_dir, pkg): + """Setup the directories necessary for the configurator.""" + temp_dir = tempfile.mkdtemp("temp") + config_dir = tempfile.mkdtemp("config") + work_dir = tempfile.mkdtemp("work") + + os.chmod(temp_dir, constants.CONFIG_DIRS_MODE) + os.chmod(config_dir, constants.CONFIG_DIRS_MODE) + os.chmod(work_dir, constants.CONFIG_DIRS_MODE) + + test_configs = pkg_resources.resource_filename( + pkg, os.path.join("testdata", test_dir)) + + shutil.copytree( + test_configs, os.path.join(temp_dir, test_dir), symlinks=True) + + return temp_dir, config_dir, work_dir diff --git a/letsencrypt/plugins/common_test.py b/letsencrypt/plugins/common_test.py index 12dd18bdf..6de86f2b8 100644 --- a/letsencrypt/plugins/common_test.py +++ b/letsencrypt/plugins/common_test.py @@ -1,8 +1,16 @@ """Tests for letsencrypt.plugins.common.""" +import pkg_resources import unittest import mock +from acme import challenges + +from letsencrypt import achallenges +from letsencrypt import le_util + +from letsencrypt.tests import acme_util + class NamespaceFunctionsTest(unittest.TestCase): """Tests for letsencrypt.plugins.common.*_namespace functions.""" @@ -57,5 +65,103 @@ class PluginTest(unittest.TestCase): "--mock-foo-bar", dest="different_to_foo_bar", x=1, y=None) +class AddrTest(unittest.TestCase): + """Tests for letsencrypt.client.plugins.common.Addr.""" + + def setUp(self): + from letsencrypt.plugins.common import Addr + self.addr1 = Addr.fromstring("192.168.1.1") + self.addr2 = Addr.fromstring("192.168.1.1:*") + self.addr3 = Addr.fromstring("192.168.1.1:80") + + def test_fromstring(self): + self.assertEqual(self.addr1.get_addr(), "192.168.1.1") + self.assertEqual(self.addr1.get_port(), "") + self.assertEqual(self.addr2.get_addr(), "192.168.1.1") + self.assertEqual(self.addr2.get_port(), "*") + self.assertEqual(self.addr3.get_addr(), "192.168.1.1") + self.assertEqual(self.addr3.get_port(), "80") + + def test_str(self): + self.assertEqual(str(self.addr1), "192.168.1.1") + self.assertEqual(str(self.addr2), "192.168.1.1:*") + self.assertEqual(str(self.addr3), "192.168.1.1:80") + + def test_get_addr_obj(self): + self.assertEqual(str(self.addr1.get_addr_obj("443")), "192.168.1.1:443") + self.assertEqual(str(self.addr2.get_addr_obj("")), "192.168.1.1") + self.assertEqual(str(self.addr1.get_addr_obj("*")), "192.168.1.1:*") + + def test_eq(self): + self.assertEqual(self.addr1, self.addr2.get_addr_obj("")) + self.assertNotEqual(self.addr1, self.addr2) + self.assertFalse(self.addr1 == 3333) + + def test_set_inclusion(self): + from letsencrypt.plugins.common import Addr + set_a = set([self.addr1, self.addr2]) + addr1b = Addr.fromstring("192.168.1.1") + addr2b = Addr.fromstring("192.168.1.1:*") + set_b = set([addr1b, addr2b]) + + self.assertEqual(set_a, set_b) + + +class DvsniTest(unittest.TestCase): + """Tests for letsencrypt.plugins.common.DvsniTest.""" + + rsa256_file = pkg_resources.resource_filename( + "acme.jose", "testdata/rsa256_key.pem") + rsa256_pem = pkg_resources.resource_string( + "acme.jose", "testdata/rsa256_key.pem") + + auth_key = le_util.Key(rsa256_file, rsa256_pem) + achalls = [ + achallenges.DVSNI( + challb=acme_util.chall_to_challb( + challenges.DVSNI( + r="\x8c\x8a\xbf_-f\\cw\xee\xd6\xf8/\xa5\xe3\xfd\xeb9" + "\xf1\xf5\xb9\xefVM\xc9w\xa4u\x9c\xe1\x87\xb4", + nonce="7\xbc^\xb7]>\x00\xa1\x9bOcU\x84^Z\x18", + ), "pending"), + domain="encryption-example.demo", key=auth_key), + achallenges.DVSNI( + challb=acme_util.chall_to_challb( + challenges.DVSNI( + r="\xba\xa9\xda? """ - def __init__(self, configurator): - self.configurator = configurator - self.achalls = [] - self.indices = [] - self.challenge_conf = os.path.join( - configurator.config.config_dir, "le_dvsni_cert_challenge.conf") - # self.completed = 0 - - def add_chall(self, achall, idx=None): - """Add challenge to DVSNI object to perform at once. - - :param achall: Annotated DVSNI challenge. - :type achall: :class:`letsencrypt.achallenges.DVSNI` - - :param int idx: index to challenge in a larger array - - """ - self.achalls.append(achall) - if idx is not None: - self.indices.append(idx) def perform(self): """Peform a DVSNI challenge.""" @@ -107,28 +89,12 @@ class ApacheDvsni(object): return responses - def _setup_challenge_cert(self, achall, s=None): - # pylint: disable=invalid-name - """Generate and write out challenge certificate.""" - cert_path = self.get_cert_file(achall) - # Register the path before you write out the file - self.configurator.reverter.register_file_creation(True, cert_path) - - cert_pem, response = achall.gen_cert_and_response(s) - - # Write out challenge cert - with open(cert_path, "w") as cert_chall_fd: - cert_chall_fd.write(cert_pem) - - return response - def _mod_config(self, ll_addrs): """Modifies Apache config files to include challenge vhosts. Result: Apache config includes virtual servers for issued challs - :param list ll_addrs: list of list of - :class:`letsencrypt.plugins.apache.obj.Addr` to apply + :param list ll_addrs: list of list of `~.common.Addr` to apply """ # TODO: Use ip address of existing vhost instead of relying on FQDN @@ -167,7 +133,7 @@ class ApacheDvsni(object): :type achall: :class:`letsencrypt.achallenges.DVSNI` :param list ip_addrs: addresses of challenged domain - :class:`list` of type :class:`~apache.obj.Addr` + :class:`list` of type `~.common.Addr` :returns: virtual host configuration text :rtype: str @@ -186,16 +152,3 @@ class ApacheDvsni(object): ssl_options_conf_path=self.configurator.parser.loc["ssl_options"], cert_path=self.get_cert_file(achall), key_path=achall.key.file, document_root=document_root).replace("\n", os.linesep) - - def get_cert_file(self, achall): - """Returns standardized name for challenge certificate. - - :param achall: Annotated DVSNI challenge. - :type achall: :class:`letsencrypt.achallenges.DVSNI` - - :returns: certificate file name - :rtype: str - - """ - return os.path.join( - self.configurator.config.work_dir, achall.nonce_domain + ".crt") diff --git a/letsencrypt_apache/obj.py b/letsencrypt_apache/obj.py index 905e3f192..fecf46ff9 100644 --- a/letsencrypt_apache/obj.py +++ b/letsencrypt_apache/obj.py @@ -1,54 +1,13 @@ """Module contains classes used by the Apache Configurator.""" -class Addr(object): - r"""Represents an Apache VirtualHost address. - - :param str addr: addr part of vhost address - :param str port: port number or \*, or "" - - """ - def __init__(self, tup): - self.tup = tup - - @classmethod - def fromstring(cls, str_addr): - """Initialize Addr from string.""" - tup = str_addr.partition(':') - return cls((tup[0], tup[2])) - - def __str__(self): - if self.tup[1]: - return "%s:%s" % self.tup - return self.tup[0] - - def __eq__(self, other): - if isinstance(other, self.__class__): - return self.tup == other.tup - return False - - def __hash__(self): - return hash(self.tup) - - def get_addr(self): - """Return addr part of Addr object.""" - return self.tup[0] - - def get_port(self): - """Return port.""" - return self.tup[1] - - def get_addr_obj(self, port): - """Return new address object with same addr and new port.""" - return self.__class__((self.tup[0], port)) - - class VirtualHost(object): # pylint: disable=too-few-public-methods """Represents an Apache Virtualhost. :ivar str filep: file path of VH :ivar str path: Augeas path to virtual host - :ivar set addrs: Virtual Host addresses (:class:`set` of :class:`Addr`) + :ivar set addrs: Virtual Host addresses (:class:`set` of + :class:`common.Addr`) :ivar set names: Server names/aliases of vhost (:class:`list` of :class:`str`) diff --git a/letsencrypt_apache/tests/configurator_test.py b/letsencrypt_apache/tests/configurator_test.py index 11b88f9e5..c3064eb5b 100644 --- a/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt_apache/tests/configurator_test.py @@ -12,10 +12,11 @@ from letsencrypt import achallenges from letsencrypt import errors from letsencrypt import le_util +from letsencrypt.plugins import common + from letsencrypt.tests import acme_util from letsencrypt_apache import configurator -from letsencrypt_apache import obj from letsencrypt_apache import parser from letsencrypt_apache.tests import util @@ -112,7 +113,7 @@ class TwoVhost80Test(util.ApacheTest): self.vh_truth[1].filep) def test_is_name_vhost(self): - addr = obj.Addr.fromstring("*:80") + addr = common.Addr.fromstring("*:80") self.assertTrue(self.config.is_name_vhost(addr)) self.config.version = (2, 2) self.assertFalse(self.config.is_name_vhost(addr)) @@ -133,7 +134,7 @@ class TwoVhost80Test(util.ApacheTest): self.assertEqual(ssl_vhost.path, "/files" + ssl_vhost.filep + "/IfModule/VirtualHost") self.assertEqual(len(ssl_vhost.addrs), 1) - self.assertEqual(set([obj.Addr.fromstring("*:443")]), ssl_vhost.addrs) + self.assertEqual(set([common.Addr.fromstring("*:443")]), ssl_vhost.addrs) self.assertEqual(ssl_vhost.names, set(["encryption-example.demo"])) self.assertTrue(ssl_vhost.ssl) self.assertFalse(ssl_vhost.enabled) diff --git a/letsencrypt_apache/tests/dvsni_test.py b/letsencrypt_apache/tests/dvsni_test.py index 321dce42c..bf43bc359 100644 --- a/letsencrypt_apache/tests/dvsni_test.py +++ b/letsencrypt_apache/tests/dvsni_test.py @@ -1,5 +1,4 @@ """Test for letsencrypt_apache.dvsni.""" -import pkg_resources import unittest import shutil @@ -7,18 +6,17 @@ import mock from acme import challenges -from letsencrypt import achallenges -from letsencrypt import le_util +from letsencrypt.plugins import common +from letsencrypt.plugins import common_test -from letsencrypt.tests import acme_util - -from letsencrypt_apache import obj from letsencrypt_apache.tests import util class DvsniPerformTest(util.ApacheTest): """Test the ApacheDVSNI challenge.""" + achalls = common_test.DvsniTest.achalls + def setUp(self): super(DvsniPerformTest, self).setUp() @@ -32,32 +30,6 @@ class DvsniPerformTest(util.ApacheTest): from letsencrypt_apache import dvsni self.sni = dvsni.ApacheDvsni(config) - rsa256_file = pkg_resources.resource_filename( - "acme.jose", "testdata/rsa256_key.pem") - rsa256_pem = pkg_resources.resource_string( - "acme.jose", "testdata/rsa256_key.pem") - - auth_key = le_util.Key(rsa256_file, rsa256_pem) - self.achalls = [ - achallenges.DVSNI( - challb=acme_util.chall_to_challb( - challenges.DVSNI( - r="\x8c\x8a\xbf_-f\\cw\xee\xd6\xf8/\xa5\xe3\xfd\xeb9" - "\xf1\xf5\xb9\xefVM\xc9w\xa4u\x9c\xe1\x87\xb4", - nonce="7\xbc^\xb7]>\x00\xa1\x9bOcU\x84^Z\x18", - ), "pending"), - domain="encryption-example.demo", key=auth_key), - achallenges.DVSNI( - challb=acme_util.chall_to_challb( - challenges.DVSNI( - r="\xba\xa9\xda? Date: Tue, 2 Jun 2015 17:49:13 +0000 Subject: [PATCH 02/70] Lower letsencrypt_apache coverage --- tox.cover.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.cover.sh b/tox.cover.sh index 80b6474d7..d1d030ed6 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -19,4 +19,4 @@ cover () { # 0, coveralls submit will be triggered (c.f. .travis.yml, # after_success) cover letsencrypt 95 && cover acme 100 && \ - cover letsencrypt_apache 78 && cover letsencrypt_nginx 96 + cover letsencrypt_apache 76 && cover letsencrypt_nginx 96 From 8cf9a152deac3ecc7dbf27e26724d2fb24f6feeb Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Thu, 4 Jun 2015 14:22:05 -0700 Subject: [PATCH 03/70] Don't suggest optional email. Email is still optional, by the same mechanism, but removing the suggestion to leave it out we will greatly increase the percentage of users that supply one, which in turn will reduce customer support requests. --- letsencrypt/account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/account.py b/letsencrypt/account.py index 3f8e3d012..93a949050 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -186,7 +186,7 @@ class Account(object): """ while True: code, email = zope.component.getUtility(interfaces.IDisplay).input( - "Enter email address (optional, press Enter to skip)") + "Enter email address") if code == display_util.OK: try: From 887f91bdacc79dabaa92d05af81a57a41c51c00b Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 9 Jun 2015 07:38:07 +0000 Subject: [PATCH 04/70] requirements.txt: no editable (-e) mode, no "." install. --- docs/contributing.rst | 2 +- docs/using.rst | 2 +- requirements.txt | 3 +-- tox.ini | 4 ++-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index da28686a2..f527ba421 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -48,7 +48,7 @@ synced to ``/vagrant``, so you can get started with: vagrant ssh cd /vagrant - ./venv/bin/pip install -r requirements.txt + ./venv/bin/pip install -r requirements.txt .[dev,docs,testing] sudo ./venv/bin/letsencrypt Support for other Linux distributions coming soon. diff --git a/docs/using.rst b/docs/using.rst index 89cbc48f6..a93e2240b 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -71,7 +71,7 @@ Installation .. code-block:: shell virtualenv --no-site-packages -p python2 venv - ./venv/bin/pip install -r requirements.txt + ./venv/bin/pip install -r requirements.txt . Usage diff --git a/requirements.txt b/requirements.txt index 0f0223dab..972e87eaf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ # https://github.com/bw2/ConfigArgParse/issues/17 --e git+https://github.com/kuba/ConfigArgParse.git@python2.6#egg=ConfigArgParse --e . +git+https://github.com/kuba/ConfigArgParse.git@python2.6#egg=ConfigArgParse diff --git a/tox.ini b/tox.ini index 0367b5498..aed60f454 100644 --- a/tox.ini +++ b/tox.ini @@ -22,12 +22,12 @@ setenv = [testenv:cover] basepython = python2.7 commands = - pip install -e .[testing] + pip install -r requirements.txt -e .[testing] ./tox.cover.sh [testenv:lint] # recent versions of pylint do not support Python 2.6 (#97, #187) basepython = python2.7 commands = - pip install -e .[dev] + pip install -r requirements.txt -e .[dev] pylint --rcfile=.pylintrc letsencrypt acme letsencrypt_apache letsencrypt_nginx From 22fd9d4cd7b1a1afd3d596086b5b37974abba0e9 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 12:28:46 +0000 Subject: [PATCH 05/70] tox.cover.sh: erase coverage before tests --- tox.cover.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.cover.sh b/tox.cover.sh index 80b6474d7..9a2c3f141 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -15,6 +15,8 @@ cover () { "$1" --cover-min-percentage="$2" "$1" } +rm -f .coverage # --cover-erase is off, make sure stats are correct + # don't use sequential composition (;), if letsencrypt_nginx returns # 0, coveralls submit will be triggered (c.f. .travis.yml, # after_success) From ad5c3ff1b239d634e44286e2c58f8fd713c3dea9 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 19:13:31 +0000 Subject: [PATCH 06/70] Support M2Crypto with swig 3.0.5+ Fixes issues recognized in https://github.com/letsencrypt/lets-encrypt-preview/issues/413#issuecomment-106245456 and https://github.com/letsencrypt/lets-encrypt-preview/issues/493. --- requirements.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/requirements.txt b/requirements.txt index 0f0223dab..fa3dba412 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,9 @@ # https://github.com/bw2/ConfigArgParse/issues/17 -e git+https://github.com/kuba/ConfigArgParse.git@python2.6#egg=ConfigArgParse + +# Support swig 3.0.5 +# https://github.com/M2Crypto/M2Crypto/issues/24 +# https://github.com/M2Crypto/M2Crypto/pull/30 +git+https://github.com/M2Crypto/M2Crypto/commit/d13a3a46c8934c5f50b31d5f95b23e6e06f845c3#egg=M2Crypto + -e . From c4b495aa37085601dd7e5bcb94a66cd7d69acbcb Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 19:24:07 +0000 Subject: [PATCH 07/70] Bootstrap Fedora 22 (fixes: #493) --- bootstrap/fedora.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100755 bootstrap/fedora.sh diff --git a/bootstrap/fedora.sh b/bootstrap/fedora.sh new file mode 100755 index 000000000..0b919b4ce --- /dev/null +++ b/bootstrap/fedora.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# Tested with: +# - Fedora 22 (x64) + +yum install -y \ + git-core \ + python \ + python-devel \ + python-virtualenv \ + python-devel \ + gcc \ + swig \ + dialog \ + augeas \ + openssl-devel \ + libffi-devel \ + ca-certificates \ From 4ed1a1c2d6b31079b47d8a76adbbd9702fa8405d Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 19:25:49 +0000 Subject: [PATCH 08/70] Bootstrap Debian: one dep per line --- bootstrap/_deb_common.sh | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/bootstrap/_deb_common.sh b/bootstrap/_deb_common.sh index 4e4c75b33..5348715eb 100755 --- a/bootstrap/_deb_common.sh +++ b/bootstrap/_deb_common.sh @@ -45,5 +45,15 @@ fi apt-get update apt-get install -y --no-install-recommends \ - python python-setuptools "$virtualenv" python-dev gcc swig \ - dialog libaugeas0 libssl-dev libffi-dev ca-certificates dpkg-dev + python \ + python-setuptools \ + "$virtualenv" \ + python-dev \ + gcc \ + swig \ + dialog \ + libaugeas0 \ + libssl-dev \ + libffi-dev \ + ca-certificates \ + dpkg-dev \ From 19aea3720387414747042683c0ee0df2fb80199a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 19:26:17 +0000 Subject: [PATCH 09/70] Bootstrap Debian: remove python-setuptools dep --- bootstrap/_deb_common.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/bootstrap/_deb_common.sh b/bootstrap/_deb_common.sh index 5348715eb..7e68c7717 100755 --- a/bootstrap/_deb_common.sh +++ b/bootstrap/_deb_common.sh @@ -46,7 +46,6 @@ fi apt-get update apt-get install -y --no-install-recommends \ python \ - python-setuptools \ "$virtualenv" \ python-dev \ gcc \ From b44014b06e7eb320656e954d2812cb9bf068f9b6 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 19:28:15 +0000 Subject: [PATCH 10/70] Bootstrap README: deps rationale --- bootstrap/README | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bootstrap/README b/bootstrap/README index 847129c03..73564aaab 100644 --- a/bootstrap/README +++ b/bootstrap/README @@ -1,2 +1,7 @@ This directory contains scripts that install necessary OS-specific -prerequisite dependencies (see docs/using.rst). \ No newline at end of file +prerequisite dependencies (see docs/using.rst). + +General dependencies: +- git-core: requirements.txt git+https://* +- ca-certificates: communication with demo ACMO server at + https://www.letsencrypt-demo.org, requirements.txt git+https://* \ No newline at end of file From 2139971212b9e534d6593365fb92a51faff9d37d Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 19:35:29 +0000 Subject: [PATCH 11/70] nit: add EOF newline --- bootstrap/README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap/README b/bootstrap/README index 73564aaab..6a04ac0ba 100644 --- a/bootstrap/README +++ b/bootstrap/README @@ -4,4 +4,4 @@ prerequisite dependencies (see docs/using.rst). General dependencies: - git-core: requirements.txt git+https://* - ca-certificates: communication with demo ACMO server at - https://www.letsencrypt-demo.org, requirements.txt git+https://* \ No newline at end of file + https://www.letsencrypt-demo.org, requirements.txt git+https://* From 2be914f0d5ed2d3d27e49f679cd57dc7703c4a27 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 21:06:44 +0000 Subject: [PATCH 12/70] Bootstrap Debian: add git-core dep --- bootstrap/_deb_common.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bootstrap/_deb_common.sh b/bootstrap/_deb_common.sh index 7e68c7717..653daca53 100755 --- a/bootstrap/_deb_common.sh +++ b/bootstrap/_deb_common.sh @@ -45,9 +45,10 @@ fi apt-get update apt-get install -y --no-install-recommends \ + git-core \ python \ - "$virtualenv" \ python-dev \ + "$virtualenv" \ gcc \ swig \ dialog \ From 30545e1c545fe1798ab6cc961a764798510957e4 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 21:11:25 +0000 Subject: [PATCH 13/70] requirements.txt: fix M2Crypto URL --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fa3dba412..6c2535a9d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,6 @@ # Support swig 3.0.5 # https://github.com/M2Crypto/M2Crypto/issues/24 # https://github.com/M2Crypto/M2Crypto/pull/30 -git+https://github.com/M2Crypto/M2Crypto/commit/d13a3a46c8934c5f50b31d5f95b23e6e06f845c3#egg=M2Crypto +git+https://github.com/M2Crypto/M2Crypto.git@d13a3a46c8934c5f50b31d5f95b23e6e06f845c3#egg=M2Crypto -e . From 8ba51665637be5e929969e18f19504dd344f294f Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 12 Jun 2015 11:54:12 +0000 Subject: [PATCH 14/70] Vagrant: remove explicit git-core install --- Vagrantfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 7eb2b4cce..1d3b48f06 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -8,8 +8,6 @@ VAGRANTFILE_API_VERSION = "2" $ubuntu_setup_script = < Date: Fri, 12 Jun 2015 12:55:33 +0000 Subject: [PATCH 15/70] Separate requirements.txt for SWIG 3.0.5+ --- docs/using.rst | 6 ++++ requirements-swig-3.0.5.txt | 67 +++++++++++++++++++++++++++++++++++++ requirements.txt | 6 ---- 3 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 requirements-swig-3.0.5.txt diff --git a/docs/using.rst b/docs/using.rst index 89cbc48f6..909c6eadc 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -31,6 +31,12 @@ In general: * ``sudo`` is required as a suggested way of running privileged process * `swig`_ is required for compiling `m2crypto`_ + + .. note:: If your operating system uses SWIG 3.0.5+, you will need + to run ``pip install -r requirements-swig-3.0.5.txt -r + requirements.txt`` instead of the standard ``pip + install -r requirements.txt``. + * `augeas`_ is required for the ``python-augeas`` bindings diff --git a/requirements-swig-3.0.5.txt b/requirements-swig-3.0.5.txt new file mode 100644 index 000000000..9ef45d950 --- /dev/null +++ b/requirements-swig-3.0.5.txt @@ -0,0 +1,67 @@ +# Support swig 3.0.5+ +# https://github.com/M2Crypto/M2Crypto/issues/24 +# https://github.com/M2Crypto/M2Crypto/pull/30 +git+https://github.com/M2Crypto/M2Crypto.git@d13a3a46c8934c5f50b31d5f95b23e6e06f845c3#egg=M2Crypto + +# This requirements file will fail on Travis CI 12.04 LTS Ubuntu build +# machine under TOX_ENV=py26 with very confusing error (full tracback +# at https://api.travis-ci.org/jobs/66529698/log.txt?deansi=true): + +#Traceback (most recent call last): +# File "setup.py", line 133, in +# include_package_data=True, +# File "/opt/python/2.6.9/lib/python2.6/distutils/core.py", line 152, in setup +# dist.run_commands() +# File "/opt/python/2.6.9/lib/python2.6/distutils/dist.py", line 975, in run_commands +# self.run_command(cmd) +# File "/opt/python/2.6.9/lib/python2.6/distutils/dist.py", line 995, in run_command +# cmd_obj.run() +# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 142, in run +# self.with_project_on_sys_path(self.run_tests) +# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 122, in with_project_on_sys_path +# func() +# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 163, in run_tests +# testRunner=self._resolve_as_ep(self.test_runner), +# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 816, in __init__ +# self.parseArgs(argv) +# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 843, in parseArgs +# self.createTests() +# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 849, in createTests +# self.module) +# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 613, in loadTestsFromNames +# suites = [self.loadTestsFromName(name, module) for name in names] +# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 587, in loadTestsFromName +# return self.loadTestsFromModule(obj) +# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 37, in loadTestsFromModule +# tests.append(self.loadTestsFromName(submodule)) +# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 584, in loadTestsFromName +# parent, obj = obj, getattr(obj, part) +#AttributeError: 'module' object has no attribute 'continuity_auth' + +# the above error happens because letsencrypt.continuity_auth cannot import M2Crypto: + +#>>> import M2Crypto +#Traceback (most recent call last): +# File "", line 1, in +# File "/root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/__init__.py", line 22, in +# import m2crypto +# File "/root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/m2crypto.py", line 26, in +# _m2crypto = swig_import_helper() +# File "/root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/m2crypto.py", line 22, in swig_import_helper +# _mod = imp.load_module('_m2crypto', fp, pathname, description) +#ImportError: /root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/_m2crypto.so: undefined symbol: SSLv2_method + +# For more info see: + +# - https://github.com/martinpaljak/M2Crypto/commit/84977c532c2444c5487db57146d81bb68dd5431d +# - http://stackoverflow.com/questions/10547332/install-m2crypto-on-a-virtualenv-without-system-packages +# - http://stackoverflow.com/questions/8206546/undefined-symbol-sslv2-method + +# In short: Python has been built without SSLv2 support, and +# github.com/M2Crypto/M2Crypto version doesn't contain necessary +# patch, but it's the only one that has a patch for newer versions of +# swig... + +# Problem seems not exists on Python 2.7. It's unlikely that the +# target distribution has swig 3.0.5+ and doesn't have Python 2.7, so +# this file should only be used in conjuction with Python 2.6. diff --git a/requirements.txt b/requirements.txt index 6c2535a9d..0f0223dab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,3 @@ # https://github.com/bw2/ConfigArgParse/issues/17 -e git+https://github.com/kuba/ConfigArgParse.git@python2.6#egg=ConfigArgParse - -# Support swig 3.0.5 -# https://github.com/M2Crypto/M2Crypto/issues/24 -# https://github.com/M2Crypto/M2Crypto/pull/30 -git+https://github.com/M2Crypto/M2Crypto.git@d13a3a46c8934c5f50b31d5f95b23e6e06f845c3#egg=M2Crypto - -e . From d585b4468014f9e2ad27a94f1256ad8ddf5a7353 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 12 Jun 2015 12:58:17 +0000 Subject: [PATCH 16/70] Nit: character upper case fixes. --- docs/using.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 909c6eadc..01dda3ce6 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -30,14 +30,14 @@ are provided mainly for the :ref:`developers ` reference. In general: * ``sudo`` is required as a suggested way of running privileged process -* `swig`_ is required for compiling `m2crypto`_ +* `SWIG`_ is required for compiling `M2Crypto`_ .. note:: If your operating system uses SWIG 3.0.5+, you will need to run ``pip install -r requirements-swig-3.0.5.txt -r requirements.txt`` instead of the standard ``pip install -r requirements.txt``. -* `augeas`_ is required for the ``python-augeas`` bindings +* `Augeas`_ is required for the Python bindings Ubuntu @@ -90,6 +90,6 @@ The letsencrypt commandline tool has a builtin help: ./venv/bin/letsencrypt --help -.. _augeas: http://augeas.net/ -.. _m2crypto: https://github.com/M2Crypto/M2Crypto -.. _swig: http://www.swig.org/ +.. _Augeas: http://augeas.net/ +.. _M2Crypto: https://github.com/M2Crypto/M2Crypto +.. _SWIG: http://www.swig.org/ From 896d2be1db884fb675e1abf66d48e3b1cc1fb430 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 14 Jun 2015 10:01:24 +0000 Subject: [PATCH 17/70] SimpleHTTP.tls -> SimpleHTTPResponse.tls bug, MAX_PATH_LEN, good_path, scheme --- acme/challenges.py | 34 +++++++++++++++---- acme/challenges_test.py | 45 +++++++++++++++++--------- letsencrypt/tests/acme_util.py | 8 ++--- letsencrypt/tests/auth_handler_test.py | 8 ++--- 4 files changed, 66 insertions(+), 29 deletions(-) diff --git a/acme/challenges.py b/acme/challenges.py index 26f71a2e3..05dc89fc4 100644 --- a/acme/challenges.py +++ b/acme/challenges.py @@ -46,7 +46,6 @@ class SimpleHTTP(DVChallenge): """ACME "simpleHttp" challenge.""" typ = "simpleHttp" token = jose.Field("token") - tls = jose.Field("tls", default=True, omitempty=True) @ChallengeResponse.register @@ -54,20 +53,43 @@ class SimpleHTTPResponse(ChallengeResponse): """ACME "simpleHttp" challenge response.""" typ = "simpleHttp" path = jose.Field("path") + tls = jose.Field("tls", default=True, omitempty=True) - URI_TEMPLATE = "https://{domain}/.well-known/acme-challenge/{path}" - """URI template for HTTPS server provisioned resource.""" + URI_ROOT_PATH = ".well-known/acme-challenge" + """URI root path for the server provisioned resource.""" + + _URI_TEMPLATE = "{scheme}://{domain}/" + URI_ROOT_PATH + "/{path}" + + MAX_PATH_LEN = 25 + """Maximum allowed `path` length.""" + + @property + def good_path(self): + """Is `path` good? + + .. todo:: acme-spec: "The value MUST be comprised entirely of + haracters from the URL-safe alphabet for Base64 encoding + [RFC4648]", base64.b64decode ignores those characters + + """ + return len(self.path) <= 25 + + @property + def scheme(self): + """URL scheme for the provisioned resource.""" + return "https" if self.tls else "http" def uri(self, domain): """Create an URI to the provisioned resource. - Forms an URI to the HTTPS server provisioned resource (containing - :attr:`~SimpleHTTP.token`) by populating the :attr:`URI_TEMPLATE`. + Forms an URI to the HTTPS server provisioned resource + (containing :attr:`~SimpleHTTP.token`). :param str domain: Domain name being verified. """ - return self.URI_TEMPLATE.format(domain=domain, path=self.path) + return self._URI_TEMPLATE.format( + scheme=self.scheme, domain=domain, path=self.path) @Challenge.register diff --git a/acme/challenges_test.py b/acme/challenges_test.py index beeec6f73..4c61c0e3d 100644 --- a/acme/challenges_test.py +++ b/acme/challenges_test.py @@ -27,17 +27,8 @@ class SimpleHTTPTest(unittest.TestCase): self.jmsg = { 'type': 'simpleHttp', 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA', - 'tls': True, } - def test_no_tls(self): - from acme.challenges import SimpleHTTP - self.assertEqual(SimpleHTTP(token='tok', tls=False).to_json(), { - 'tls': False, - 'token': 'tok', - 'type': 'simpleHttp', - }) - def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) @@ -54,27 +45,51 @@ class SimpleHTTPResponseTest(unittest.TestCase): def setUp(self): from acme.challenges import SimpleHTTPResponse - self.msg = SimpleHTTPResponse(path='6tbIMBC5Anhl5bOlWT5ZFA') - self.jmsg = { + self.msg_http = SimpleHTTPResponse( + path='6tbIMBC5Anhl5bOlWT5ZFA', tls=False) + self.msg_https = SimpleHTTPResponse(path='6tbIMBC5Anhl5bOlWT5ZFA') + self.jmsg_http = { 'type': 'simpleHttp', 'path': '6tbIMBC5Anhl5bOlWT5ZFA', + 'tls': False, + } + self.jmsg_https = { + 'type': 'simpleHttp', + 'path': '6tbIMBC5Anhl5bOlWT5ZFA', + 'tls': True, } + def test_good_path(self): + self.assertTrue(self.msg_http.good_path) + self.assertTrue(self.msg_https.good_path) + self.assertFalse( + self.msg_http.update(path=(self.msg_http.path * 10)).good_path) + + def test_scheme(self): + self.assertEqual('http', self.msg_http.scheme) + self.assertEqual('https', self.msg_https.scheme) + def test_uri(self): + self.assertEqual('http://example.com/.well-known/acme-challenge/' + '6tbIMBC5Anhl5bOlWT5ZFA', self.msg_http.uri('example.com')) self.assertEqual('https://example.com/.well-known/acme-challenge/' - '6tbIMBC5Anhl5bOlWT5ZFA', self.msg.uri('example.com')) + '6tbIMBC5Anhl5bOlWT5ZFA', self.msg_https.uri('example.com')) def test_to_partial_json(self): - self.assertEqual(self.jmsg, self.msg.to_partial_json()) + 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, SimpleHTTPResponse.from_json(self.jmsg)) + 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)) + hash(SimpleHTTPResponse.from_json(self.jmsg_http)) + hash(SimpleHTTPResponse.from_json(self.jmsg_https)) class DVSNITest(unittest.TestCase): diff --git a/letsencrypt/tests/acme_util.py b/letsencrypt/tests/acme_util.py index 51bb3cfbb..daf651059 100644 --- a/letsencrypt/tests/acme_util.py +++ b/letsencrypt/tests/acme_util.py @@ -16,7 +16,7 @@ KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey( "acme.jose", os.path.join("testdata", "rsa512_key.pem")))) # Challenges -SIMPLE_HTTPS = challenges.SimpleHTTP( +SIMPLE_HTTP = challenges.SimpleHTTP( token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") DVSNI = challenges.DVSNI( r="O*\xb4-\xad\xec\x95>\xed\xa9\r0\x94\xe8\x97\x9c&6\xbf'\xb3" @@ -47,7 +47,7 @@ POP = challenges.ProofOfPossession( ) ) -CHALLENGES = [SIMPLE_HTTPS, DVSNI, DNS, RECOVERY_CONTACT, RECOVERY_TOKEN, POP] +CHALLENGES = [SIMPLE_HTTP, DVSNI, DNS, RECOVERY_CONTACT, RECOVERY_TOKEN, POP] DV_CHALLENGES = [chall for chall in CHALLENGES if isinstance(chall, challenges.DVChallenge)] CONT_CHALLENGES = [chall for chall in CHALLENGES @@ -86,13 +86,13 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name # Pending ChallengeBody objects DVSNI_P = chall_to_challb(DVSNI, messages2.STATUS_PENDING) -SIMPLE_HTTPS_P = chall_to_challb(SIMPLE_HTTPS, messages2.STATUS_PENDING) +SIMPLE_HTTP_P = chall_to_challb(SIMPLE_HTTP, messages2.STATUS_PENDING) DNS_P = chall_to_challb(DNS, messages2.STATUS_PENDING) RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, messages2.STATUS_PENDING) RECOVERY_TOKEN_P = chall_to_challb(RECOVERY_TOKEN, messages2.STATUS_PENDING) POP_P = chall_to_challb(POP, messages2.STATUS_PENDING) -CHALLENGES_P = [SIMPLE_HTTPS_P, DVSNI_P, DNS_P, +CHALLENGES_P = [SIMPLE_HTTP_P, DVSNI_P, DNS_P, RECOVERY_CONTACT_P, RECOVERY_TOKEN_P, POP_P] DV_CHALLENGES_P = [challb for challb in CHALLENGES_P if isinstance(challb.chall, challenges.DVChallenge)] diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index d7fd2c093..8cbc0e604 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -300,7 +300,7 @@ class GenChallengePathTest(unittest.TestCase): def test_common_case(self): """Given DVSNI and SimpleHTTP with appropriate combos.""" - challbs = (acme_util.DVSNI_P, acme_util.SIMPLE_HTTPS_P) + challbs = (acme_util.DVSNI_P, acme_util.SIMPLE_HTTP_P) prefs = [challenges.DVSNI] combos = ((0,), (1,)) @@ -315,7 +315,7 @@ class GenChallengePathTest(unittest.TestCase): challbs = (acme_util.RECOVERY_TOKEN_P, acme_util.RECOVERY_CONTACT_P, acme_util.DVSNI_P, - acme_util.SIMPLE_HTTPS_P) + acme_util.SIMPLE_HTTP_P) prefs = [challenges.RecoveryToken, challenges.DVSNI] combos = acme_util.gen_combos(challbs) self.assertEqual(self._call(challbs, prefs, combos), (0, 2)) @@ -328,7 +328,7 @@ class GenChallengePathTest(unittest.TestCase): acme_util.RECOVERY_CONTACT_P, acme_util.POP_P, acme_util.DVSNI_P, - acme_util.SIMPLE_HTTPS_P, + acme_util.SIMPLE_HTTP_P, acme_util.DNS_P) # Typical webserver client that can do everything except DNS # Attempted to make the order realistic @@ -413,7 +413,7 @@ class IsPreferredTest(unittest.TestCase): def test_mutually_exclusvie(self): self.assertFalse( self._call( - acme_util.DVSNI_P, frozenset([acme_util.SIMPLE_HTTPS_P]))) + acme_util.DVSNI_P, frozenset([acme_util.SIMPLE_HTTP_P]))) def test_mutually_exclusive_same_type(self): self.assertTrue( From d4b9499e2b6f35335e56d512688a23c85763c514 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 14 Jun 2015 09:46:13 +0000 Subject: [PATCH 18/70] ManualAuthenticator for SimpleHTTP. Inspired by quite popular [1] letsencrypt-nosudo [2] by @diafygi. Together with #440 and #473, it allows Let's Encrypt to be used without sudo (root) on the target machine (c.f. [3]). Possibly fixes #500. [1] https://news.ycombinator.com/item?id=9707170 [2] https://github.com/diafygi/letsencrypt-nosudo [3] https://groups.google.com/a/letsencrypt.org/forum/#!topic/client-dev/JAqxSvXlln4 --- docs/api/plugins/manual.rst | 5 ++ letsencrypt/plugins/manual.py | 138 +++++++++++++++++++++++++++++ letsencrypt/plugins/manual_test.py | 59 ++++++++++++ setup.py | 1 + 4 files changed, 203 insertions(+) create mode 100644 docs/api/plugins/manual.rst create mode 100644 letsencrypt/plugins/manual.py create mode 100644 letsencrypt/plugins/manual_test.py diff --git a/docs/api/plugins/manual.rst b/docs/api/plugins/manual.rst new file mode 100644 index 000000000..4661ab7df --- /dev/null +++ b/docs/api/plugins/manual.rst @@ -0,0 +1,5 @@ +:mod:`letsencrypt.plugins.manual` +--------------------------------- + +.. automodule:: letsencrypt.plugins.manual + :members: diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py new file mode 100644 index 000000000..c4c7b890a --- /dev/null +++ b/letsencrypt/plugins/manual.py @@ -0,0 +1,138 @@ +"""Manual plugin.""" +import logging +import os +import sys + +import requests +import zope.component +import zope.interface + +from acme import challenges +from acme import jose + +from letsencrypt import interfaces +from letsencrypt.plugins import common + + +class ManualAuthenticator(common.Plugin): + """Manual Authenticator. + + .. todo:: Support for `~.challenges.DVSNI`. + + """ + zope.interface.implements(interfaces.IAuthenticator) + zope.interface.classProvides(interfaces.IPluginFactory) + + description = "Manual Authenticator" + + MESSAGE_TEMPLATE = """\ +Make sure your web server displays the following content at +{uri} before continuing: + +{achall.token} + +If you don't have HTTP server configured, you can run the following +command on the target server (as root): + +{command} +""" + + HTTP_TEMPLATE = """\ +mkdir -p {response.URI_ROOT_PATH} +echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path} +# run only once per server: +python -m SimpleHTTPServer 80""" + """Non-TLS command template.""" + + # https://www.piware.de/2011/01/creating-an-https-server-in-python/ + HTTPS_TEMPLATE = """\ +mkdir -p {response.URI_ROOT_PATH} # run only once per server +echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path} +# run only once per server: +openssl req -new -newkey rsa:4096 -subj "/" -days 1 -nodes -x509 -keyout key.pem -out cert.pem +python -c "import BaseHTTPServer, SimpleHTTPServer, ssl; \\ +s = BaseHTTPServer.HTTPServer(('', 443), SimpleHTTPServer.SimpleHTTPRequestHandler); \\ +s.socket = ssl.wrap_socket(s.socket, keyfile='key.pem', certfile='cert.pem'); \\ +s.serve_forever()" """ + """TLS command template. + + According to the ACME specification, "the ACME server MUST ignore + the certificate provided by the HTTPS server", so the first command + generates temporary self-signed certificate. For the same reason + ``requests.get`` in `_verify` sets ``verify=False``. Python HTTPS + server command serves the ``token`` on all URIs. + + """ + + def __init__(self, *args, **kwargs): + super(ManualAuthenticator, self).__init__(*args, **kwargs) + self.template = (self.HTTP_TEMPLATE if self.config.no_simple_http_tls + else self.HTTPS_TEMPLATE) + + def prepare(self): # pylint: disable=missing-docstring,no-self-use + pass # pragma: no cover + + 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 +binary for temporary key/certificate generation.""".replace("\n", "") + + def get_chall_pref(self, domain): + # pylint: disable=missing-docstring,no-self-use,unused-argument + return [challenges.SimpleHTTP] + + def perform(self, achalls): # pylint: disable=missing-docstring + responses = [] + # TODO: group achalls by the same socket.gethostbyname(_ex) + # and prompt only once per server (one "echo -n" per domain) + for achall in achalls: + responses.append(self._perform_single(achall)) + return responses + + def _perform_single(self, achall): + # 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 = challenges.SimpleHTTPResponse( + path=jose.b64encode(os.urandom(18)), + tls=(not self.config.no_simple_http_tls)) + assert response.good_path # is encoded os.urandom(18) good? + + self._notify_and_wait(self.MESSAGE_TEMPLATE.format( + achall=achall, response=response, + uri=response.uri(achall.domain), + command=self.template.format(achall=achall, response=response))) + + if self._verify(achall, response): + return response + else: + return None + + def _notify_and_wait(self, message): # pylint: disable=no-self-use + # TODO: IDisplay wraps messages, breaking the command + #answer = zope.component.getUtility(interfaces.IDisplay).notification( + # message=message, height=25, pause=True) + sys.stdout.write(message) + raw_input("Press ENTER to continue") + + def _verify(self, achall, chall_response): # pylint: disable=no-self-use + uri = chall_response.uri(achall.domain) + logging.debug("Verifying %s...", uri) + try: + response = requests.get(uri, verify=False) + except requests.exceptions.ConnectionError as error: + logging.exception(error) + return False + + ret = response.text == achall.token + if not ret: + logging.error("Unable to verify %s! Expected: %r, returned: %r.", + uri, achall.token, response.text) + + return ret + + def cleanup(self, achalls): # pylint: disable=missing-docstring,no-self-use + pass # pragma: no cover diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py new file mode 100644 index 000000000..c95654dec --- /dev/null +++ b/letsencrypt/plugins/manual_test.py @@ -0,0 +1,59 @@ +"""Tests for letsencrypt.plugins.manual.""" +import unittest + +import mock +import requests + +from acme import challenges + +from letsencrypt import achallenges +from letsencrypt.tests import acme_util + + +class ManualAuthenticatorTest(unittest.TestCase): + """Tests for letsencrypt.plugins.manual.ManualAuthenticator.""" + + def setUp(self): + from letsencrypt.plugins.manual import ManualAuthenticator + self.config = mock.MagicMock(no_simple_http_tls=True) + self.auth = ManualAuthenticator(config=self.config, name="manual") + self.achalls = [achallenges.SimpleHTTP( + challb=acme_util.SIMPLE_HTTP, domain="foo.com", key=None)] + + def test_more_info(self): + self.assertTrue(isinstance(self.auth.more_info(), str)) + + def test_get_chall_pref(self): + self.assertTrue(all(issubclass(pref, challenges.Challenge) + for pref in self.auth.get_chall_pref("foo.com"))) + + def test_perform_empty(self): + self.assertEqual([], self.auth.perform([])) + + @mock.patch("letsencrypt.plugins.manual.sys.stdout") + @mock.patch("letsencrypt.plugins.manual.os.urandom") + @mock.patch("letsencrypt.plugins.manual.requests.get") + @mock.patch("__builtin__.raw_input") + def test_perform(self, mock_raw_input, mock_get, mock_urandom, mock_stdout): + mock_urandom.return_value = "foo" + mock_get().text = self.achalls[0].token + + self.assertEqual( + [challenges.SimpleHTTPResponse(tls=False, path='Zm9v')], + self.auth.perform(self.achalls)) + mock_raw_input.assert_called_once() + mock_get.assert_called_with( + "http://foo.com/.well-known/acme-challenge/Zm9v", verify=False) + + message = mock_stdout.write.mock_calls[0][1][0] + self.assertTrue(self.achalls[0].token in message) + self.assertTrue('Zm9v' in message) + + mock_get().text = self.achalls[0].token + '!' + self.assertEqual([None], self.auth.perform(self.achalls)) + + mock_get.side_effect = requests.exceptions.ConnectionError + self.assertEqual([None], self.auth.perform(self.achalls)) + +if __name__ == "__main__": + unittest.main() # pragma: no cover diff --git a/setup.py b/setup.py index 145b75a69..16aafac94 100644 --- a/setup.py +++ b/setup.py @@ -120,6 +120,7 @@ setup( 'jws = letsencrypt.acme.jose.jws:CLI.run', ], 'letsencrypt.plugins': [ + 'manual = letsencrypt.plugins.manual:ManualAuthenticator', 'standalone = letsencrypt.plugins.standalone.authenticator' ':StandaloneAuthenticator', From 63d5273ed1692cd4c02af9e0e421ff362b2d06b9 Mon Sep 17 00:00:00 2001 From: PatrickHeppler Date: Thu, 18 Jun 2015 14:55:12 +0200 Subject: [PATCH 19/70] Create centos.sh --- bootstrap/centos.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 bootstrap/centos.sh diff --git a/bootstrap/centos.sh b/bootstrap/centos.sh new file mode 100644 index 000000000..318a47a8a --- /dev/null +++ b/bootstrap/centos.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +# Tested with: Centos 7 on AWS EC2 t2.micro (x64) + +yum install -y \ + git \ + python \ + python-devel \ + python-virtualenv \ + python-devel \ + gcc \ + swig \ + dialog \ + augeas-libs \ + openssl-devel \ + libffi-devel \ + ca-certificates \ + python-setuptools \ + readline-devel From 8d0334d2de9f355d867210be01f74f25c6c00e2c Mon Sep 17 00:00:00 2001 From: PatrickHeppler Date: Thu, 18 Jun 2015 14:58:55 +0200 Subject: [PATCH 20/70] Update using.rst Additional informations about installing on Centos 7 --- docs/using.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/using.rst b/docs/using.rst index 89cbc48f6..ddfc67738 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -64,6 +64,11 @@ Mac OSX sudo ./bootstrap/mac.sh +Centos 7 +-------- +.. code-block:: shell + + sudo ./bootstrap/centos.sh Installation ============ @@ -73,6 +78,13 @@ Installation virtualenv --no-site-packages -p python2 venv ./venv/bin/pip install -r requirements.txt +Installation on Centos 7 +============ + +.. code-block:: shell + + virtualenv --no-site-packages -p python2 venv + env SWIG_FEATURES="-cpperraswarn -includeall -D__`uname -m`__ -I/usr/include/openssl" ./venv/bin/python setup.py install Usage ===== From 85d9047f4efb9ef6058ab4a4b55222f55ac7be16 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 18 Jun 2015 13:13:29 +0000 Subject: [PATCH 21/70] Fedora: augeas -> augeas-libs --- bootstrap/fedora.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap/fedora.sh b/bootstrap/fedora.sh index 0b919b4ce..97ea1e637 100755 --- a/bootstrap/fedora.sh +++ b/bootstrap/fedora.sh @@ -12,7 +12,7 @@ yum install -y \ gcc \ swig \ dialog \ - augeas \ + augeas-libs \ openssl-devel \ libffi-devel \ ca-certificates \ From 9b4cff8cd60c0d8e4d046ff1c05ccff2db0d0a44 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 18 Jun 2015 13:55:32 +0000 Subject: [PATCH 22/70] bootstrap: _rpm_common.sh, centos fixes --- bootstrap/_rpm_common.sh | 20 ++++++++++++++++++++ bootstrap/centos.sh | 20 +------------------- bootstrap/fedora.sh | 19 +------------------ docs/using.rst | 18 +++++++++++------- 4 files changed, 33 insertions(+), 44 deletions(-) create mode 100755 bootstrap/_rpm_common.sh mode change 100644 => 120000 bootstrap/centos.sh mode change 100755 => 120000 bootstrap/fedora.sh diff --git a/bootstrap/_rpm_common.sh b/bootstrap/_rpm_common.sh new file mode 100755 index 000000000..1209cd44a --- /dev/null +++ b/bootstrap/_rpm_common.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# Tested with: +# - Fedora 22 (x64) +# - Centos 7 (x64: on AWS EC2 t2.micro, DigitalOcean droplet) + +# "git-core" seems to be an alias for "git" in CentOS 7 (yum search fails) +yum install -y \ + git-core \ + python \ + python-devel \ + python-virtualenv \ + python-devel \ + gcc \ + swig \ + dialog \ + augeas-libs \ + openssl-devel \ + libffi-devel \ + ca-certificates \ diff --git a/bootstrap/centos.sh b/bootstrap/centos.sh deleted file mode 100644 index 318a47a8a..000000000 --- a/bootstrap/centos.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh - -# Tested with: Centos 7 on AWS EC2 t2.micro (x64) - -yum install -y \ - git \ - python \ - python-devel \ - python-virtualenv \ - python-devel \ - gcc \ - swig \ - dialog \ - augeas-libs \ - openssl-devel \ - libffi-devel \ - ca-certificates \ - python-setuptools \ - readline-devel diff --git a/bootstrap/centos.sh b/bootstrap/centos.sh new file mode 120000 index 000000000..a0db46d70 --- /dev/null +++ b/bootstrap/centos.sh @@ -0,0 +1 @@ +_rpm_common.sh \ No newline at end of file diff --git a/bootstrap/fedora.sh b/bootstrap/fedora.sh deleted file mode 100755 index 97ea1e637..000000000 --- a/bootstrap/fedora.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -# Tested with: -# - Fedora 22 (x64) - -yum install -y \ - git-core \ - python \ - python-devel \ - python-virtualenv \ - python-devel \ - gcc \ - swig \ - dialog \ - augeas-libs \ - openssl-devel \ - libffi-devel \ - ca-certificates \ diff --git a/bootstrap/fedora.sh b/bootstrap/fedora.sh new file mode 120000 index 000000000..a0db46d70 --- /dev/null +++ b/bootstrap/fedora.sh @@ -0,0 +1 @@ +_rpm_common.sh \ No newline at end of file diff --git a/docs/using.rst b/docs/using.rst index 0e0b493ca..ef64452e1 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -70,12 +70,23 @@ Mac OSX sudo ./bootstrap/mac.sh + Centos 7 -------- + .. code-block:: shell sudo ./bootstrap/centos.sh +For installation run this modified command (note the trailing +backslash): + +.. code-block:: shell + + SWIG_FEATURES="-includeall -D__`uname -m`__-I/usr/include/openssl" \ + ./venv/bin/pip install -r requirements.txt functools32 + + Installation ============ @@ -84,13 +95,6 @@ Installation virtualenv --no-site-packages -p python2 venv ./venv/bin/pip install -r requirements.txt -Installation on Centos 7 -============ - -.. code-block:: shell - - virtualenv --no-site-packages -p python2 venv - env SWIG_FEATURES="-cpperraswarn -includeall -D__`uname -m`__ -I/usr/include/openssl" ./venv/bin/python setup.py install Usage ===== From db6f9ecf862f0175a02cb8d46108ab2f8ec7cc9a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 18 Jun 2015 13:59:59 +0000 Subject: [PATCH 23/70] Fedora installation instructions. --- docs/using.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/using.rst b/docs/using.rst index ef64452e1..227fd69ed 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -32,6 +32,7 @@ In general: * ``sudo`` is required as a suggested way of running privileged process * `swig`_ is required for compiling `m2crypto`_ + .. _new-swig: .. note:: If your operating system uses SWIG 3.0.5+, you will need to run ``pip install -r requirements-swig-3.0.5.txt -r requirements.txt`` instead of the standard ``pip @@ -71,6 +72,16 @@ Mac OSX sudo ./bootstrap/mac.sh +Fedora +------ + +.. code-block:: shell + + sudo ./bootstrap/fedora.sh + +.. note:: Fedora 22 uses SWIG 3.0.5+, use the :ref:`modified pip + command for installation `. + Centos 7 -------- From a873e8ea33ec4c6de298aadfe8699d555e56f3ba Mon Sep 17 00:00:00 2001 From: William Budington Date: Thu, 18 Jun 2015 17:45:04 -0700 Subject: [PATCH 24/70] functools32 required - add to setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 145b75a69..ebcc2c9b3 100644 --- a/setup.py +++ b/setup.py @@ -53,6 +53,7 @@ install_requires = [ # order of items in install_requires DOES matter and M2Crypto has # to go last, see #152 'M2Crypto', + 'functools32' ] dev_extras = [ From b1bb5a6843618dfdc1480a4328d9ff9cdf17b468 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Thu, 18 Jun 2015 18:02:51 -0700 Subject: [PATCH 25/70] Make sure cleanup_challenges happens --- letsencrypt/auth_handler.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 5f9d29e6e..5665fe83d 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -134,9 +134,11 @@ class AuthHandler(object): self._send_responses(self.cont_c, cont_resp, chall_update)) # Check for updated status... - self._poll_challenges(chall_update, best_effort) - # This removes challenges from self.dv_c and self.cont_c - self._cleanup_challenges(active_achalls) + try: + self._poll_challenges(chall_update, best_effort) + finally: + # This removes challenges from self.dv_c and self.cont_c + self._cleanup_challenges(active_achalls) def _send_responses(self, achalls, resps, chall_update): """Send responses and make sure errors are handled. From dee1b7f04921aa5b6e193f20681ef1883db09046 Mon Sep 17 00:00:00 2001 From: William Budington Date: Thu, 18 Jun 2015 18:35:35 -0700 Subject: [PATCH 26/70] Remove support for python2.6 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 167d6ad74..9169a32d7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,6 @@ after_success: '[ "$TOXENV" == "cover" ] && coveralls' # matrix, which allows us to clearly distinguish which component under # test has failed env: - - TOXENV=py26 - TOXENV=py27 - TOXENV=lint - TOXENV=cover From 8afc26a7362ab6aa7705125eb1da4780e7a9967d Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 04:10:51 +0000 Subject: [PATCH 27/70] Fix typo --- acme/challenges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/challenges.py b/acme/challenges.py index 05dc89fc4..9ea06645d 100644 --- a/acme/challenges.py +++ b/acme/challenges.py @@ -68,7 +68,7 @@ class SimpleHTTPResponse(ChallengeResponse): """Is `path` good? .. todo:: acme-spec: "The value MUST be comprised entirely of - haracters from the URL-safe alphabet for Base64 encoding + characters from the URL-safe alphabet for Base64 encoding [RFC4648]", base64.b64decode ignores those characters """ From b3be239061e1e2015987487b46f77382038209ad Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 08:14:25 +0000 Subject: [PATCH 28/70] Fix merge conflicts between #486 and #510 (pip install .). --- docs/using.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index a180a3826..bb27ad8c2 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -35,8 +35,8 @@ In general: .. _new-swig: .. note:: If your operating system uses SWIG 3.0.5+, you will need to run ``pip install -r requirements-swig-3.0.5.txt -r - requirements.txt`` instead of the standard ``pip - install -r requirements.txt``. + requirements.txt .`` instead of the standard ``pip + install -r requirements.txt .``. * `Augeas`_ is required for the Python bindings @@ -95,7 +95,7 @@ backslash): .. code-block:: shell SWIG_FEATURES="-includeall -D__`uname -m`__-I/usr/include/openssl" \ - ./venv/bin/pip install -r requirements.txt functools32 + ./venv/bin/pip install -r requirements.txt functools32 . Installation From 5f41c9f191022ff03b7d4605b000fecee60689d9 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 08:16:03 +0000 Subject: [PATCH 29/70] Dockerfile: note about missing requirements.txt --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index b6a07388c..78aa7a75b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,6 +48,7 @@ COPY letsencrypt_apache /opt/letsencrypt/src/letsencrypt_apache/ COPY letsencrypt_nginx /opt/letsencrypt/src/letsencrypt_nginx/ +# requirements.txt not installed! RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt/venv && \ /opt/letsencrypt/venv/bin/pip install -e /opt/letsencrypt/src From ed7ba282119cdb592b1e9e11d2d51bc2adbda7a8 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 07:36:45 +0000 Subject: [PATCH 30/70] Pin jsonschema (quickfix for missing functools32). https://github.com/Julian/jsonschema/issues/233 --- docs/using.rst | 2 +- setup.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index bb27ad8c2..e377e74ab 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -95,7 +95,7 @@ backslash): .. code-block:: shell SWIG_FEATURES="-includeall -D__`uname -m`__-I/usr/include/openssl" \ - ./venv/bin/pip install -r requirements.txt functools32 . + ./venv/bin/pip install -r requirements.txt . Installation diff --git a/setup.py b/setup.py index ebcc2c9b3..46f53244c 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ install_requires = [ 'argparse', 'ConfigArgParse', 'configobj', - 'jsonschema', + 'jsonschema<2.5.1', # https://github.com/Julian/jsonschema/issues/233 'mock', 'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304) 'parsedatetime', @@ -53,7 +53,6 @@ install_requires = [ # order of items in install_requires DOES matter and M2Crypto has # to go last, see #152 'M2Crypto', - 'functools32' ] dev_extras = [ From 6d2c81138e5ffd4d0102ec0595a0184d6063e109 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 07:38:05 +0000 Subject: [PATCH 31/70] Revert "Remove support for python2.6" This reverts commit dee1b7f04921aa5b6e193f20681ef1883db09046. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 9169a32d7..167d6ad74 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ after_success: '[ "$TOXENV" == "cover" ] && coveralls' # matrix, which allows us to clearly distinguish which component under # test has failed env: + - TOXENV=py26 - TOXENV=py27 - TOXENV=lint - TOXENV=cover From dbd024f77cc6dfba1213fb893ae2f78943ad5f6b Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 10:07:18 +0000 Subject: [PATCH 32/70] Inline docs fixes --- letsencrypt/network2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/network2.py b/letsencrypt/network2.py index 9b846da6c..f2620b4b1 100644 --- a/letsencrypt/network2.py +++ b/letsencrypt/network2.py @@ -25,7 +25,7 @@ class Network(object): .. todo:: Clean up raised error types hierarchy, document, and handle (wrap) - instances of `.DeserializationError` raised in `from_json()``. + instances of `.DeserializationError` raised in `from_json()`. :ivar str new_reg_uri: Location of new-reg :ivar key: `.JWK` (private) From f46e2aeedd69ebe62481f750eb190471b1975676 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 10:06:39 +0000 Subject: [PATCH 33/70] README: documentation link at the top. Hopefully helps to mitigate problems mentioned in https://groups.google.com/a/letsencrypt.org/forum/#!topic/client-dev/4xpVpy4EVz0 --- README.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index db32889db..5b149abe5 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,9 @@ +.. notice for github users + +Official **documentation**, including `installation instructions`_, is +available at https://letsencrypt.readthedocs.org. + + About the Let's Encrypt Client ============================== @@ -47,6 +53,9 @@ server automatically!:: :target: https://quay.io/repository/letsencrypt/lets-encrypt-preview :alt: Docker Repository on Quay.io +.. _`installation instructions`: + https://letsencrypt.readthedocs.org/en/latest/using.html + .. _watch demo video: https://www.youtube.com/watch?v=Gas_sSB-5SU @@ -85,7 +94,7 @@ Current Features Links ----- -Documentation: https://letsencrypt.readthedocs.org/ +Documentation: https://letsencrypt.readthedocs.org Software project: https://github.com/letsencrypt/lets-encrypt-preview From 3382dac793fee6b06618c8aa6d27b8580ab4525e Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 10:17:15 +0000 Subject: [PATCH 34/70] README: FAQ link at the top. --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 5b149abe5..40c054fe3 100644 --- a/README.rst +++ b/README.rst @@ -3,6 +3,10 @@ Official **documentation**, including `installation instructions`_, is available at https://letsencrypt.readthedocs.org. +Generic information about Let's Encrypt project can be found at +https://letsencrypt.org. Please read `Frequently Asked Questions (FAQ) +`_. + About the Let's Encrypt Client ============================== From a1f5ea8e8ce81e1415c6da98325a39dce1f7f9c9 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 09:55:33 +0000 Subject: [PATCH 35/70] Docs: note about pip editable mode. --- docs/contributing.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/contributing.rst b/docs/contributing.rst index f527ba421..eb9854581 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -17,6 +17,15 @@ Now you can install the development packages: ./venv/bin/pip install -r requirements.txt -e .[dev,docs,testing] +.. note:: `-e` (short for `--editable`) turns on *editable mode* in + which any source code changes in the current working + directory are "live" and no further `pip install ...` + invocations are necessary while developing. Any `pip install + .` call will re-install Let's Encrypt in non-editable mode. + + This is roughly equivalent to `python setup.py develop`. For + more info see `man pip`. + The code base, including your pull requests, **must** have 100% test statement coverage **and** be compliant with the :ref:`coding style `. From 673a6d4f3710718b7f82e48f7de55d6636dfbc04 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 10:41:04 +0000 Subject: [PATCH 36/70] Docs: move SWIG notes below installation cmd, Mac OS X note. --- docs/using.rst | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index a180a3826..6709ad51a 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -31,13 +31,6 @@ In general: * ``sudo`` is required as a suggested way of running privileged process * `SWIG`_ is required for compiling `M2Crypto`_ - - .. _new-swig: - .. note:: If your operating system uses SWIG 3.0.5+, you will need - to run ``pip install -r requirements-swig-3.0.5.txt -r - requirements.txt`` instead of the standard ``pip - install -r requirements.txt``. - * `Augeas`_ is required for the Python bindings @@ -79,8 +72,6 @@ Fedora sudo ./bootstrap/fedora.sh -.. note:: Fedora 22 uses SWIG 3.0.5+, use the :ref:`modified pip - command for installation `. Centos 7 -------- @@ -106,6 +97,13 @@ Installation virtualenv --no-site-packages -p python2 venv ./venv/bin/pip install -r requirements.txt . +.. note:: If your operating system uses SWIG 3.0.5+, you will need to + run ``pip install -r requirements-swig-3.0.5.txt -r + requirements.txt`` instead. Known affected systems: + + * Fedora 22 + * some versions of Mac OS X + Usage ===== From 1a013eae6e021d763f3a692e10de44add751d96a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 10:49:18 +0000 Subject: [PATCH 37/70] Docs: no support for setup.py, root, or non-Virtualenv installation. --- docs/using.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/using.rst b/docs/using.rst index 6709ad51a..0014a1192 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -97,6 +97,14 @@ Installation virtualenv --no-site-packages -p python2 venv ./venv/bin/pip install -r requirements.txt . +.. warning:: Please do **not** use ``python setup.py install``. Please + do **not** attempt the installation commands as + superuser/root and/or without Virtualenv_, e.g. ``sudo + python setup.py install``, ``sudo pip install``, ``sudo + ./venv/bin/...``. These modes of operation might corrupt + your operating system and are **not supported** by the + Let's Encrypt team! + .. note:: If your operating system uses SWIG 3.0.5+, you will need to run ``pip install -r requirements-swig-3.0.5.txt -r requirements.txt`` instead. Known affected systems: @@ -118,3 +126,4 @@ The letsencrypt commandline tool has a builtin help: .. _Augeas: http://augeas.net/ .. _M2Crypto: https://github.com/M2Crypto/M2Crypto .. _SWIG: http://www.swig.org/ +.. _Virtualenv: https://virtualenv.pypa.io From ca6b326371a60e5104e5bd53b34ff502ac0839c3 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 11:00:00 +0000 Subject: [PATCH 38/70] Docs: add "Getting the code" section. --- docs/using.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/using.rst b/docs/using.rst index 0014a1192..c829e9c80 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -20,6 +20,25 @@ And follow the instructions. Your new cert will be available in ``/etc/letsencrypt/certs``. +Getting the code +================ + +Please `install Git`_ and run the following commands: + +.. code-block:: shell + + git clone https://github.com/letsencrypt/lets-encrypt-preview + cd lets-encrypt-preview + +Alternatively you could `download the ZIP archive`_ and extract the +snapshot of our repository, but it's strongly recommended to use the +above method instead. + +.. _`install Git`: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git +.. _`download the ZIP archive`: + https://github.com/letsencrypt/lets-encrypt-preview/archive/master.zip + + Prerequisites ============= From 8292eab3f7c6803746c2ff857b82a754b275920b Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 11:01:56 +0000 Subject: [PATCH 39/70] Docs: add link to Docker docs. --- docs/using.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index c829e9c80..51c5527f6 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -5,9 +5,9 @@ Using the Let's Encrypt client Quick start =========== -Using docker you can quickly get yourself a testing cert. From the +Using Docker_ you can quickly get yourself a testing cert. From the server that the domain your requesting a cert for resolves to, -download docker, and issue the following command +`install Docker`_, issue the following command: .. code-block:: shell @@ -16,9 +16,12 @@ download docker, and issue the following command -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ quay.io/letsencrypt/lets-encrypt-preview:latest -And follow the instructions. Your new cert will be available in +and follow the instructions. Your new cert will be available in ``/etc/letsencrypt/certs``. +.. _Docker: https://docker.com +.. _`install Docker`: https://docs.docker.com/docker/userguide/ + Getting the code ================ From b8ebb0ab161c4a27eaaffa14fe6860b44ed96e29 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 11:03:05 +0000 Subject: [PATCH 40/70] Docs: backticks nit. --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 51c5527f6..29e8902a9 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -138,7 +138,7 @@ Installation Usage ===== -The letsencrypt commandline tool has a builtin help: +The ``letsencrypt`` commandline tool has a builtin help: .. code-block:: shell From 4040fd02045d871601c1866f25d8e2da11d78a48 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 11:21:51 +0000 Subject: [PATCH 41/70] Docs: extend usage section with "letsencrypt auth" call. --- docs/using.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/using.rst b/docs/using.rst index 29e8902a9..20f07d5d1 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -138,6 +138,12 @@ Installation Usage ===== +To get a new certificate run: + +.. code-block:: shell + + ./venv/bin/letsencrypt auth + The ``letsencrypt`` commandline tool has a builtin help: .. code-block:: shell From e176ad8f43e3342a346194875c147b66078c8306 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 15 Jun 2015 18:31:50 +0000 Subject: [PATCH 42/70] Remove old Boulder incompatibility issue --- letsencrypt/network2.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/letsencrypt/network2.py b/letsencrypt/network2.py index f2620b4b1..a20194a79 100644 --- a/letsencrypt/network2.py +++ b/letsencrypt/network2.py @@ -257,13 +257,12 @@ class Network(object): # TODO: Boulder does not set Location or Link on update # (c.f. acme-spec #94) - updated_regr = self._regr_from_response( response, uri=regr.uri, new_authzr_uri=regr.new_authzr_uri, terms_of_service=regr.terms_of_service) if updated_regr != regr: - # TODO: Boulder reregisters with new recoveryToken and new URI raise errors.UnexpectedUpdate(regr) + return updated_regr def agree_to_tos(self, regr): From 4d39699befb583820246580a4ad058828b2e81cb Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 18 Jun 2015 10:56:08 +0000 Subject: [PATCH 43/70] Remove doubled :members: from acme errors docs --- docs/pkgs/acme/index.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/pkgs/acme/index.rst b/docs/pkgs/acme/index.rst index 9cca3b795..1c73a4a42 100644 --- a/docs/pkgs/acme/index.rst +++ b/docs/pkgs/acme/index.rst @@ -51,9 +51,6 @@ Errors :members: - :members: - - Utilities --------- From 23c5a1fd90e1140af4455e32f6e2bf2e270923b9 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 19 Jun 2015 16:13:53 +0000 Subject: [PATCH 44/70] Docs: "." and functools32 adjustements --- docs/using.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 20f07d5d1..96eb62b05 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -108,7 +108,7 @@ backslash): .. code-block:: shell SWIG_FEATURES="-includeall -D__`uname -m`__-I/usr/include/openssl" \ - ./venv/bin/pip install -r requirements.txt functools32 + ./venv/bin/pip install -r requirements.txt . Installation @@ -129,7 +129,7 @@ Installation .. note:: If your operating system uses SWIG 3.0.5+, you will need to run ``pip install -r requirements-swig-3.0.5.txt -r - requirements.txt`` instead. Known affected systems: + requirements.txt .`` instead. Known affected systems: * Fedora 22 * some versions of Mac OS X From 50e509604cfc70a776c682a5735572feac7dda6a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 20 Jun 2015 10:20:54 +0000 Subject: [PATCH 45/70] Docs: remove wrong re-install comment --- docs/contributing.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index eb9854581..804cec95c 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -20,8 +20,7 @@ Now you can install the development packages: .. note:: `-e` (short for `--editable`) turns on *editable mode* in which any source code changes in the current working directory are "live" and no further `pip install ...` - invocations are necessary while developing. Any `pip install - .` call will re-install Let's Encrypt in non-editable mode. + invocations are necessary while developing. This is roughly equivalent to `python setup.py develop`. For more info see `man pip`. From 061282fa6674bb8b8fc50d61167f5ff45fbe8779 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 18 Jun 2015 20:27:40 +0000 Subject: [PATCH 46/70] Store temporary DVSNI files in IConfig.work_dir. --- letsencrypt_apache/dvsni.py | 2 +- letsencrypt_nginx/dvsni.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt_apache/dvsni.py b/letsencrypt_apache/dvsni.py index fb78cfced..c25426371 100644 --- a/letsencrypt_apache/dvsni.py +++ b/letsencrypt_apache/dvsni.py @@ -141,7 +141,7 @@ class ApacheDvsni(common.Dvsni): """ ips = " ".join(str(i) for i in ip_addrs) document_root = os.path.join( - self.configurator.config.config_dir, "dvsni_page/") + self.configurator.config.work_dir, "dvsni_page/") # TODO: Python docs is not clear how mutliline string literal # newlines are parsed on different platforms. At least on # Linux (Debian sid), when source file uses CRLF, Python still diff --git a/letsencrypt_nginx/dvsni.py b/letsencrypt_nginx/dvsni.py index 620d144f6..0697f6e1e 100644 --- a/letsencrypt_nginx/dvsni.py +++ b/letsencrypt_nginx/dvsni.py @@ -124,7 +124,7 @@ class NginxDvsni(common.Dvsni): """ document_root = os.path.join( - self.configurator.config.config_dir, "dvsni_page") + self.configurator.config.work_dir, "dvsni_page") block = [['listen', str(addr)] for addr in addrs] From 960b070c223b5901ce3b6d1ce7a27748f584eaee Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 14:51:39 +0000 Subject: [PATCH 47/70] Dummy use of network2 in revoker --- letsencrypt/revoker.py | 14 +++++++------- letsencrypt/tests/revoker_test.py | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/letsencrypt/revoker.py b/letsencrypt/revoker.py index a1ea27e71..d173a1907 100644 --- a/letsencrypt/revoker.py +++ b/letsencrypt/revoker.py @@ -16,12 +16,11 @@ import tempfile import Crypto.PublicKey.RSA import M2Crypto -from acme import messages from acme.jose import util as jose_util from letsencrypt import errors from letsencrypt import le_util -from letsencrypt import network +from letsencrypt import network2 from letsencrypt.display import util as display_util from letsencrypt.display import revocation @@ -45,7 +44,9 @@ class Revoker(object): """ def __init__(self, installer, config, no_confirm=False): - self.network = network.Network(config.server) + # XXX + self.network = network2.Network(new_reg_uri=None, key=None, alg=None) + self.installer = installer self.config = config self.no_confirm = no_confirm @@ -238,6 +239,8 @@ class Revoker(object): :returns: TODO """ + # XXX | pylint: disable=unused-variable + # These will both have to change in the future away from M2Crypto # pylint: disable=protected-access certificate = jose_util.ComparableX509(cert._cert) @@ -250,10 +253,7 @@ class Revoker(object): raise errors.LetsEncryptRevokerError( "Corrupted backup key file: %s" % cert.backup_key_path) - # TODO: Catch error associated with already revoked and proceed. - return self.network.send_and_receive_expected( - messages.RevocationRequest.create(certificate=certificate, key=key), - messages.Revocation) + return self.network.revoke(certr=None) # XXX def _remove_certs_keys(self, cert_list): # pylint: disable=no-self-use """Remove certificate and key. diff --git a/letsencrypt/tests/revoker_test.py b/letsencrypt/tests/revoker_test.py index ae04b5081..35e7d132b 100644 --- a/letsencrypt/tests/revoker_test.py +++ b/letsencrypt/tests/revoker_test.py @@ -63,7 +63,7 @@ class RevokerTest(RevokerBase): def tearDown(self): shutil.rmtree(self.backup_dir) - @mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected") + @mock.patch("letsencrypt.network2.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_key_all(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -89,7 +89,7 @@ class RevokerTest(RevokerBase): self.revoker.revoke_from_key, self.key) - @mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected") + @mock.patch("letsencrypt.network2.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_wrong_key(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -105,7 +105,7 @@ class RevokerTest(RevokerBase): # No revocation went through self.assertEqual(mock_net.call_count, 0) - @mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected") + @mock.patch("letsencrypt.network2.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_cert(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -122,7 +122,7 @@ class RevokerTest(RevokerBase): self.assertEqual(mock_net.call_count, 1) - @mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected") + @mock.patch("letsencrypt.network2.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_cert_not_found(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -141,7 +141,7 @@ class RevokerTest(RevokerBase): self.assertEqual(mock_net.call_count, 1) - @mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected") + @mock.patch("letsencrypt.network2.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_menu(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -165,7 +165,7 @@ class RevokerTest(RevokerBase): self.assertEqual(mock_display.more_info_cert.call_count, 1) @mock.patch("letsencrypt.revoker.logging") - @mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected") + @mock.patch("letsencrypt.network2.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_menu_delete_all(self, mock_display, mock_net, mock_log): mock_display().confirm_revocation.return_value = True From c5d4f91bf77612be1bfe0a972922f4cb1ab962a5 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 13 Jun 2015 13:45:50 +0000 Subject: [PATCH 48/70] Remove old messages and network --- acme/messages.py | 367 ------------------------------- acme/messages_test.py | 480 ----------------------------------------- letsencrypt/network.py | 121 ----------- setup.py | 1 - 4 files changed, 969 deletions(-) delete mode 100644 acme/messages.py delete mode 100644 acme/messages_test.py delete mode 100644 letsencrypt/network.py diff --git a/acme/messages.py b/acme/messages.py deleted file mode 100644 index 6d46f894c..000000000 --- a/acme/messages.py +++ /dev/null @@ -1,367 +0,0 @@ -"""ACME protocol v00 messages. - -.. warning:: This module is an implementation of the draft `ACME - protocol version 00`_, and not the "RESTified" `ACME protocol version - 01`_ or later. It should work with `older Node.js implementation`_, - but will definitely not work with Boulder_. It is kept for reference - purposes only. - - -.. _`ACME protocol version 00`: - https://github.com/letsencrypt/acme-spec/blob/v00/draft-barnes-acme.md - -.. _`ACME protocol version 01`: - https://github.com/letsencrypt/acme-spec/blob/v01/draft-barnes-acme.md - -.. _Boulder: https://github.com/letsencrypt/boulder - -.. _`older Node.js implementation`: - https://github.com/letsencrypt/node-acme/commit/f42aa5b7fad4cd2fc289653c4ab14f18052367b3 - - -""" -import jsonschema - -from acme import challenges -from acme import errors -from acme import jose -from acme import other -from acme import util - - -class Message(jose.TypedJSONObjectWithFields): - # _fields_to_partial_json | pylint: disable=abstract-method - # pylint: disable=too-few-public-methods - """ACME message.""" - TYPES = {} - type_field_name = "type" - - schema = NotImplemented - """JSON schema the object is tested against in :meth:`from_json`. - - Subclasses must overrride it with a value that is acceptable by - :func:`jsonschema.validate`, most probably using - :func:`acme.util.load_schema`. - - """ - - @classmethod - def from_json(cls, jobj): - """Deserialize from (possibly invalid) JSON object. - - Note that the input ``jobj`` has not been sanitized in any way. - - :param jobj: JSON object. - - :raises acme.errors.SchemaValidationError: if the input - JSON object could not be validated against JSON schema specified - in :attr:`schema`. - :raises acme.jose.errors.DeserializationError: for any - other generic error in decoding. - - :returns: instance of the class - - """ - msg_cls = cls.get_type_cls(jobj) - - # TODO: is that schema testing still relevant? - try: - jsonschema.validate(jobj, msg_cls.schema) - except jsonschema.ValidationError as error: - raise errors.SchemaValidationError(error) - - return super(Message, cls).from_json(jobj) - - -@Message.register # pylint: disable=too-few-public-methods -class Challenge(Message): - """ACME "challenge" message. - - :ivar str nonce: Random data, **not** base64-encoded. - :ivar list challenges: List of - :class:`~acme.challenges.Challenge` objects. - - .. todo:: - 1. can challenges contain two challenges of the same type? - 2. can challenges contain duplicates? - 3. check "combinations" indices are in valid range - 4. turn "combinations" elements into sets? - 5. turn "combinations" into set? - - """ - typ = "challenge" - schema = util.load_schema(typ) - - session_id = jose.Field("sessionID") - nonce = jose.Field("nonce", encoder=jose.b64encode, - decoder=jose.decode_b64jose) - challenges = jose.Field("challenges") - combinations = jose.Field("combinations", omitempty=True, default=()) - - @challenges.decoder - def challenges(value): # pylint: disable=missing-docstring,no-self-argument - return tuple(challenges.Challenge.from_json(chall) for chall in value) - - @property - def resolved_combinations(self): - """Combinations with challenges instead of indices.""" - return tuple(tuple(self.challenges[idx] for idx in combo) - for combo in self.combinations) - - -@Message.register # pylint: disable=too-few-public-methods -class ChallengeRequest(Message): - """ACME "challengeRequest" message.""" - typ = "challengeRequest" - schema = util.load_schema(typ) - identifier = jose.Field("identifier") - - -@Message.register # pylint: disable=too-few-public-methods -class Authorization(Message): - """ACME "authorization" message. - - :ivar jwk: :class:`acme.jose.JWK` - - """ - typ = "authorization" - schema = util.load_schema(typ) - - recovery_token = jose.Field("recoveryToken", omitempty=True) - identifier = jose.Field("identifier", omitempty=True) - jwk = jose.Field("jwk", decoder=jose.JWK.from_json, omitempty=True) - - -@Message.register -class AuthorizationRequest(Message): - """ACME "authorizationRequest" message. - - :ivar str nonce: Random data from the corresponding - :attr:`Challenge.nonce`, **not** base64-encoded. - :ivar list responses: List of completed challenges ( - :class:`acme.challenges.ChallengeResponse`). - :ivar signature: Signature (:class:`acme.other.Signature`). - - """ - typ = "authorizationRequest" - schema = util.load_schema(typ) - - session_id = jose.Field("sessionID") - nonce = jose.Field("nonce", encoder=jose.b64encode, - decoder=jose.decode_b64jose) - responses = jose.Field("responses") - signature = jose.Field("signature", decoder=other.Signature.from_json) - contact = jose.Field("contact", omitempty=True, default=()) - - @responses.decoder - def responses(value): # pylint: disable=missing-docstring,no-self-argument - return tuple(challenges.ChallengeResponse.from_json(chall) - for chall in value) - - @classmethod - def create(cls, name, key, sig_nonce=None, **kwargs): - """Create signed "authorizationRequest". - - :param str name: Hostname - - :param key: Key used for signing. - :type key: :class:`Crypto.PublicKey.RSA` - - :param str sig_nonce: Nonce used for signature. Useful for testing. - :kwargs: Any other arguments accepted by the class constructor. - - :returns: Signed "authorizationRequest" ACME message. - :rtype: :class:`AuthorizationRequest` - - """ - # pylint: disable=too-many-arguments - signature = other.Signature.from_msg( - name + kwargs["nonce"], key, sig_nonce) - return cls( - signature=signature, contact=kwargs.pop("contact", ()), **kwargs) - - def verify(self, name): - """Verify signature. - - .. warning:: Caller must check that the public key encoded in the - :attr:`signature`'s :class:`acme.jose.JWK` object - is the correct key for a given context. - - :param str name: Hostname - - :returns: True iff ``signature`` can be verified, False otherwise. - :rtype: bool - - """ - # self.signature is not Field | pylint: disable=no-member - return self.signature.verify(name + self.nonce) - - -@Message.register # pylint: disable=too-few-public-methods -class Certificate(Message): - """ACME "certificate" message. - - :ivar certificate: The certificate (:class:`M2Crypto.X509.X509` - wrapped in :class:`acme.util.ComparableX509`). - - :ivar list chain: Chain of certificates (:class:`M2Crypto.X509.X509` - wrapped in :class:`acme.util.ComparableX509` ). - - """ - typ = "certificate" - schema = util.load_schema(typ) - - certificate = jose.Field("certificate", encoder=jose.encode_cert, - decoder=jose.decode_cert) - chain = jose.Field("chain", omitempty=True, default=()) - refresh = jose.Field("refresh", omitempty=True) - - @chain.decoder - def chain(value): # pylint: disable=missing-docstring,no-self-argument - return tuple(jose.decode_cert(cert) for cert in value) - - @chain.encoder - def chain(value): # pylint: disable=missing-docstring,no-self-argument - return tuple(jose.encode_cert(cert) for cert in value) - - -@Message.register -class CertificateRequest(Message): - """ACME "certificateRequest" message. - - :ivar csr: Certificate Signing Request (:class:`M2Crypto.X509.Request` - wrapped in :class:`acme.util.ComparableX509`. - :ivar signature: Signature (:class:`acme.other.Signature`). - - """ - typ = "certificateRequest" - schema = util.load_schema(typ) - - csr = jose.Field("csr", encoder=jose.encode_csr, - decoder=jose.decode_csr) - signature = jose.Field("signature", decoder=other.Signature.from_json) - - @classmethod - def create(cls, key, sig_nonce=None, **kwargs): - """Create signed "certificateRequest". - - :param key: Key used for signing. - :type key: :class:`Crypto.PublicKey.RSA` - - :param str sig_nonce: Nonce used for signature. Useful for testing. - :kwargs: Any other arguments accepted by the class constructor. - - :returns: Signed "certificateRequest" ACME message. - :rtype: :class:`CertificateRequest` - - """ - return cls(signature=other.Signature.from_msg( - kwargs["csr"].as_der(), key, sig_nonce), **kwargs) - - def verify(self): - """Verify signature. - - .. warning:: Caller must check that the public key encoded in the - :attr:`signature`'s :class:`acme.jose.JWK` object - is the correct key for a given context. - - :returns: True iff ``signature`` can be verified, False otherwise. - :rtype: bool - - """ - # self.signature is not Field | pylint: disable=no-member - return self.signature.verify(self.csr.as_der()) - - -@Message.register # pylint: disable=too-few-public-methods -class Defer(Message): - """ACME "defer" message.""" - typ = "defer" - schema = util.load_schema(typ) - - token = jose.Field("token") - interval = jose.Field("interval", omitempty=True) - message = jose.Field("message", omitempty=True) - - -@Message.register # pylint: disable=too-few-public-methods -class Error(Message): - """ACME "error" message.""" - typ = "error" - schema = util.load_schema(typ) - - error = jose.Field("error") - message = jose.Field("message", omitempty=True) - more_info = jose.Field("moreInfo", omitempty=True) - - MESSAGE_CODES = { - "malformed": "The request message was malformed", - "unauthorized": "The client lacks sufficient authorization", - "serverInternal": "The server experienced an internal error", - "notSupported": "The request type is not supported", - "unknown": "The server does not recognize an ID/token in the request", - "badCSR": "The CSR is unacceptable (e.g., due to a short key)", - } - - -@Message.register # pylint: disable=too-few-public-methods -class Revocation(Message): - """ACME "revocation" message.""" - typ = "revocation" - schema = util.load_schema(typ) - - -@Message.register -class RevocationRequest(Message): - """ACME "revocationRequest" message. - - :ivar certificate: Certificate (:class:`M2Crypto.X509.X509` - wrapped in :class:`acme.util.ComparableX509`). - :ivar signature: Signature (:class:`acme.other.Signature`). - - """ - typ = "revocationRequest" - schema = util.load_schema(typ) - - certificate = jose.Field("certificate", decoder=jose.decode_cert, - encoder=jose.encode_cert) - signature = jose.Field("signature", decoder=other.Signature.from_json) - - @classmethod - def create(cls, key, sig_nonce=None, **kwargs): - """Create signed "revocationRequest". - - :param key: Key used for signing. - :type key: :class:`Crypto.PublicKey.RSA` - - :param str sig_nonce: Nonce used for signature. Useful for testing. - :kwargs: Any other arguments accepted by the class constructor. - - :returns: Signed "revocationRequest" ACME message. - :rtype: :class:`RevocationRequest` - - """ - return cls(signature=other.Signature.from_msg( - kwargs["certificate"].as_der(), key, sig_nonce), **kwargs) - - def verify(self): - """Verify signature. - - .. warning:: Caller must check that the public key encoded in the - :attr:`signature`'s :class:`acme.jose.JWK` object - is the correct key for a given context. - - :returns: True iff ``signature`` can be verified, False otherwise. - :rtype: bool - - """ - # self.signature is not Field | pylint: disable=no-member - return self.signature.verify(self.certificate.as_der()) - - -@Message.register # pylint: disable=too-few-public-methods -class StatusRequest(Message): - """ACME "statusRequest" message.""" - typ = "statusRequest" - schema = util.load_schema(typ) - token = jose.Field("token") diff --git a/acme/messages_test.py b/acme/messages_test.py deleted file mode 100644 index baff2a21a..000000000 --- a/acme/messages_test.py +++ /dev/null @@ -1,480 +0,0 @@ -"""Tests for acme.messages.""" -import os -import pkg_resources -import unittest - -import Crypto.PublicKey.RSA -import M2Crypto - -from acme import challenges -from acme import errors -from acme import jose -from acme import other - - -KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey( - pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'rsa512_key.pem')))) -CERT = jose.ComparableX509(M2Crypto.X509.load_cert( - pkg_resources.resource_filename( - 'letsencrypt.tests', os.path.join('testdata', 'cert.pem')))) -CSR = jose.ComparableX509(M2Crypto.X509.load_request( - pkg_resources.resource_filename( - 'letsencrypt.tests', os.path.join('testdata', 'csr.pem')))) -CSR2 = jose.ComparableX509(M2Crypto.X509.load_request( - pkg_resources.resource_filename( - 'acme.jose', os.path.join('testdata', 'csr2.pem')))) - - -class MessageTest(unittest.TestCase): - """Tests for acme.messages.Message.""" - - def setUp(self): - # pylint: disable=missing-docstring,too-few-public-methods - from acme.messages import Message - - class MockParentMessage(Message): - # pylint: disable=abstract-method - TYPES = {} - - @MockParentMessage.register - class MockMessage(MockParentMessage): - typ = 'test' - schema = { - 'type': 'object', - 'properties': { - 'price': {'type': 'number'}, - 'name': {'type': 'string'}, - }, - } - price = jose.Field('price') - name = jose.Field('name') - - self.parent_cls = MockParentMessage - self.msg = MockMessage(price=123, name='foo') - - def test_from_json_validates(self): - self.assertRaises(errors.SchemaValidationError, - self.parent_cls.from_json, - {'type': 'test', 'price': 'asd'}) - - -class ChallengeTest(unittest.TestCase): - - def setUp(self): - challs = ( - challenges.SimpleHTTP(token='IlirfxKKXAsHtmzK29Pj8A'), - challenges.DNS(token='DGyRejmCefe7v4NfDGDKfA'), - challenges.RecoveryToken(), - ) - combinations = ((0, 2), (1, 2)) - - from acme.messages import Challenge - self.msg = Challenge( - session_id='aefoGaavieG9Wihuk2aufai3aeZ5EeW4', - nonce='\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9', - challenges=challs, combinations=combinations) - - self.jmsg_to = { - 'type': 'challenge', - 'sessionID': 'aefoGaavieG9Wihuk2aufai3aeZ5EeW4', - 'nonce': '7Nbyb1lI6xPVI3Hg3aKSqQ', - 'challenges': challs, - 'combinations': combinations, - } - - self.jmsg_from = { - 'type': 'challenge', - 'sessionID': 'aefoGaavieG9Wihuk2aufai3aeZ5EeW4', - 'nonce': '7Nbyb1lI6xPVI3Hg3aKSqQ', - 'challenges': [chall.to_json() for chall in challs], - 'combinations': [[0, 2], [1, 2]], # TODO array tuples - } - - def test_resolved_combinations(self): - self.assertEqual(self.msg.resolved_combinations, ( - ( - challenges.SimpleHTTP(token='IlirfxKKXAsHtmzK29Pj8A'), - challenges.RecoveryToken() - ), - ( - challenges.DNS(token='DGyRejmCefe7v4NfDGDKfA'), - challenges.RecoveryToken(), - ) - )) - - def test_to_partial_json(self): - self.assertEqual(self.msg.to_partial_json(), self.jmsg_to) - - def test_from_json(self): - from acme.messages import Challenge - self.assertEqual(Challenge.from_json(self.jmsg_from), self.msg) - - def test_json_without_optionals(self): - del self.jmsg_from['combinations'] - del self.jmsg_to['combinations'] - - from acme.messages import Challenge - msg = Challenge.from_json(self.jmsg_from) - - self.assertEqual(msg.combinations, ()) - self.assertEqual(msg.to_partial_json(), self.jmsg_to) - - -class ChallengeRequestTest(unittest.TestCase): - - def setUp(self): - from acme.messages import ChallengeRequest - self.msg = ChallengeRequest(identifier='example.com') - - self.jmsg = { - 'type': 'challengeRequest', - 'identifier': 'example.com', - } - - def test_to_partial_json(self): - self.assertEqual(self.msg.to_partial_json(), self.jmsg) - - def test_from_json(self): - from acme.messages import ChallengeRequest - self.assertEqual(ChallengeRequest.from_json(self.jmsg), self.msg) - - -class AuthorizationTest(unittest.TestCase): - - def setUp(self): - jwk = jose.JWKRSA(key=KEY.publickey()) - - from acme.messages import Authorization - self.msg = Authorization(recovery_token='tok', jwk=jwk, - identifier='example.com') - - self.jmsg = { - 'type': 'authorization', - 'recoveryToken': 'tok', - 'identifier': 'example.com', - 'jwk': jwk, - } - - def test_to_partial_json(self): - self.assertEqual(self.msg.to_partial_json(), self.jmsg) - - def test_from_json(self): - self.jmsg['jwk'] = self.jmsg['jwk'].to_partial_json() - - from acme.messages import Authorization - self.assertEqual(Authorization.from_json(self.jmsg), self.msg) - - def test_json_without_optionals(self): - del self.jmsg['recoveryToken'] - del self.jmsg['identifier'] - del self.jmsg['jwk'] - - from acme.messages import Authorization - msg = Authorization.from_json(self.jmsg) - - self.assertTrue(msg.recovery_token is None) - self.assertTrue(msg.identifier is None) - self.assertTrue(msg.jwk is None) - self.assertEqual(self.jmsg, msg.to_partial_json()) - - -class AuthorizationRequestTest(unittest.TestCase): - - def setUp(self): - self.responses = ( - challenges.SimpleHTTPResponse(path='Hf5GrX4Q7EBax9hc2jJnfw'), - None, # null - challenges.RecoveryTokenResponse(token='23029d88d9e123e'), - ) - self.contact = ("mailto:cert-admin@example.com", "tel:+12025551212") - signature = other.Signature( - alg=jose.RS256, jwk=jose.JWKRSA(key=KEY.publickey()), - sig='-v\xd8\xc2\xa3\xba0\xd6\x92\x16\xb5.\xbe\xa1[\x04\xbe' - '\x1b\xa1X\xd2)\x18\x94\x8f\xd7\xd0\xc0\xbbcI`W\xdf v' - '\xe4\xed\xe8\x03J\xe8\xc8 Date: Thu, 11 Jun 2015 16:05:49 +0000 Subject: [PATCH 49/70] Remove old messages schemata. --- acme/schemata/authorization.json | 21 ---- acme/schemata/authorizationRequest.json | 38 ------- acme/schemata/certificate.json | 25 ----- acme/schemata/certificateRequest.json | 19 ---- acme/schemata/challenge.json | 36 ------- acme/schemata/challengeRequest.json | 15 --- acme/schemata/challengeobject.json | 130 ------------------------ acme/schemata/defer.json | 21 ---- acme/schemata/error.json | 21 ---- acme/schemata/jwk.json | 19 ---- acme/schemata/responseobject.json | 75 -------------- acme/schemata/revocation.json | 12 --- acme/schemata/revocationRequest.json | 18 ---- acme/schemata/signature.json | 71 ------------- acme/schemata/statusRequest.json | 15 --- 15 files changed, 536 deletions(-) delete mode 100644 acme/schemata/authorization.json delete mode 100644 acme/schemata/authorizationRequest.json delete mode 100644 acme/schemata/certificate.json delete mode 100644 acme/schemata/certificateRequest.json delete mode 100644 acme/schemata/challenge.json delete mode 100644 acme/schemata/challengeRequest.json delete mode 100644 acme/schemata/challengeobject.json delete mode 100644 acme/schemata/defer.json delete mode 100644 acme/schemata/error.json delete mode 100644 acme/schemata/jwk.json delete mode 100644 acme/schemata/responseobject.json delete mode 100644 acme/schemata/revocation.json delete mode 100644 acme/schemata/revocationRequest.json delete mode 100644 acme/schemata/signature.json delete mode 100644 acme/schemata/statusRequest.json diff --git a/acme/schemata/authorization.json b/acme/schemata/authorization.json deleted file mode 100644 index 122f263e1..000000000 --- a/acme/schemata/authorization.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/authorization#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for an authorization message", - "type": "object", - "required": ["type"], - "properties": { - "type" : { - "enum" : [ "authorization" ] - }, - "recoveryToken" : { - "type": "string" - }, - "identifier" : { - "type": "string" - }, - "jwk": { - "$ref": "file:acme/schemata/jwk.json" - } - } -} diff --git a/acme/schemata/authorizationRequest.json b/acme/schemata/authorizationRequest.json deleted file mode 100644 index 2d4371cb8..000000000 --- a/acme/schemata/authorizationRequest.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/authorizationRequest#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for an authorizationRequest message", - "type": "object", - "required": ["type", "sessionID", "nonce", "signature", "responses"], - "properties": { - "type" : { - "enum" : [ "authorizationRequest" ] - }, - "sessionID" : { - "type" : "string" - }, - "nonce" : { - "type": "string" - }, - "signature" : { - "$ref": "file:acme/schemata/signature.json" - }, - "responses": { - "type": "array", - "minItems": 1, - "items": { - "anyOf": [ - { "$ref": "file:acme/schemata/responseobject.json" }, - { "type": "null" } - ] - } - }, - "contact": { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - } -} diff --git a/acme/schemata/certificate.json b/acme/schemata/certificate.json deleted file mode 100644 index 1d4e98947..000000000 --- a/acme/schemata/certificate.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/certificate#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a certificate message", - "type": "object", - "required": ["type", "certificate"], - "properties": { - "type" : { - "enum" : [ "certificate" ] - }, - "certificate" : { - "type" : "string" - }, - "chain" : { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - }, - "refresh" : { - "type": "string" - } - } -} diff --git a/acme/schemata/certificateRequest.json b/acme/schemata/certificateRequest.json deleted file mode 100644 index ef3e18f98..000000000 --- a/acme/schemata/certificateRequest.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/certificateRequest#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a certificateRequest message", - "type": "object", - "required": ["type", "csr", "signature"], - "properties": { - "type" : { - "enum" : [ "certificateRequest" ] - }, - "csr" : { - "type" : "string" , - "pattern": "^[-_=0-9A-Za-z]+$" - }, - "signature" : { - "$ref": "file:acme/schemata/signature.json" - } - } -} diff --git a/acme/schemata/challenge.json b/acme/schemata/challenge.json deleted file mode 100644 index 978fcd4c4..000000000 --- a/acme/schemata/challenge.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/challenge#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a challenge message", - "type": "object", - "required": ["type", "sessionID", "nonce", "challenges"], - "properties": { - "type" : { - "enum" : [ "challenge" ] - }, - "sessionID" : { - "type" : "string" - }, - "nonce" : { - "type": "string" - }, - "challenges": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "file:acme/schemata/challengeobject.json" - } - }, - "combinations": { - "type": "array", - "minItems": 1, - "items": { - "type": "array", - "minItems": 1, - "items": { - "type": "integer" - } - } - } - } -} diff --git a/acme/schemata/challengeRequest.json b/acme/schemata/challengeRequest.json deleted file mode 100644 index 0762fa9c8..000000000 --- a/acme/schemata/challengeRequest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/challengeRequest#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a challengeRequest message", - "type": "object", - "required": ["type", "identifier"], - "properties": { - "type" : { - "enum" : [ "challengeRequest" ] - }, - "identifier" : { - "type": "string" - } - } -} diff --git a/acme/schemata/challengeobject.json b/acme/schemata/challengeobject.json deleted file mode 100644 index 7709f315d..000000000 --- a/acme/schemata/challengeobject.json +++ /dev/null @@ -1,130 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/challengeobject#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Subschema for an individual challenge (within challenge)", - "anyOf": [ - { "type": "object", - "required": ["type", "token"], - "properties": { - "type": { - "enum" : [ "simpleHttp" ] - }, - "token": { - "type": "string" - } - } - }, - { "type": "object", - "required": ["type", "r", "nonce"], - "properties": { - "type": { - "enum" : [ "dvsni" ] - }, - "r": { - "type" : [ "string" ], - "pattern": "^[-_=0-9A-Za-z]+$" - }, - "nonce": { - "type": "string", - "pattern": "^[0-9a-f]+$" - } - } - }, - { "type": "object", - "required": ["type"], - "properties": { - "type": { - "enum" : [ "recoveryContact" ] - }, - "activationURL": { - "type" : "string" - }, - "successURL": { - "type": "string" - }, - "contact": { - "type": "string" - } - } - }, - { "type": "object", - "required": ["type"], - "properties": { - "type": { - "enum" : [ "recoveryToken" ] - } - } - }, - { "type": "object", - "required": ["type", "alg", "nonce", "hints"], - "properties": { - "type": { - "enum" : [ "proofOfPossession" ] - }, - "alg": { - "type": "string" - }, - "nonce": { - "type": "string", - "pattern": "^[-_=0-9A-Za-z]+$" - }, - "hints": { - "type": "object", - "properties": { - "jwk": { - "type": "object" - }, - "certFingerprints": { - "type": "array", - "minItems": 1, - "items": { - "type": "string", - "pattern": "^[0-9a-f]+$" - } - }, - "subjectKeyIdentifiers": { - "type": "array", - "minItems": 1, - "items": { - "type": "string", - "pattern": "^[0-9a-f]+$" - } - }, - "serialNumbers": { - "type": "array", - "minItems": 1, - "items": { - "type": "integer" - } - }, - "issuers": { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - }, - "authorizedFor": { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - } - } - } - }, - { "type": "object", - "required": ["type", "token"], - "properties": { - "type": { - "enum" : [ "dns" ] - }, - "token": { - "type": "string" - } - } - } - ] -} diff --git a/acme/schemata/defer.json b/acme/schemata/defer.json deleted file mode 100644 index 21edd614b..000000000 --- a/acme/schemata/defer.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/defer#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a defer message", - "type": "object", - "required": ["type", "token"], - "properties": { - "type" : { - "enum" : [ "defer" ] - }, - "token" : { - "type": "string" - }, - "interval" : { - "type": "integer" - }, - "message": { - "type": "string" - } - } -} diff --git a/acme/schemata/error.json b/acme/schemata/error.json deleted file mode 100644 index 359506b52..000000000 --- a/acme/schemata/error.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/error#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for an error message", - "type": "object", - "required": ["type", "error"], - "properties": { - "type" : { - "enum" : [ "error" ] - }, - "error" : { - "enum" : [ "malformed", "unauthorized", "serverInternal", "nonSupported", "unknown", "badCSR" ] - }, - "message" : { - "type": "string" - }, - "moreInfo": { - "type": "string" - } - } -} diff --git a/acme/schemata/jwk.json b/acme/schemata/jwk.json deleted file mode 100644 index b9cca8840..000000000 --- a/acme/schemata/jwk.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/jwk#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a jwk (**kty RSA/e=65537 ONLY**)", - "type": "object", - "required": ["kty", "e", "n"], - "properties": { - "kty": { - "enum" : [ "RSA" ] - }, - "e": { - "enum" : [ "AQAB" ] - }, - "n": { - "type": "string", - "pattern": "^[-_=0-9A-Za-z]+$" - } - } -} diff --git a/acme/schemata/responseobject.json b/acme/schemata/responseobject.json deleted file mode 100644 index 5773f3a73..000000000 --- a/acme/schemata/responseobject.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/responseobject#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Subschema for an individual challenge response (within authorizationRequest)", - "anyOf": [ - { "type": "object", - "required": ["type", "path"], - "properties": { - "type": { - "enum" : [ "simpleHttp" ] - }, - "path": { - "type": "string" - } - } - }, - { "type": "object", - "required": ["type", "s"], - "properties": { - "type": { - "enum" : [ "dvsni" ] - }, - "s": { - "type" : [ "string" ], - "pattern": "^[-_=0-9A-Za-z]+$" - } - } - }, - { "type": "object", - "required": ["type"], - "properties": { - "type": { - "enum" : [ "recoveryContact" ] - }, - "token": { - "type" : "string" - } - } - }, - { "type": "object", - "required": ["type"], - "properties": { - "type": { - "enum" : [ "recoveryToken" ] - }, - "token": { - "type" : "string" - } - } - }, - { "type": "object", - "required": ["type", "nonce", "signature"], - "properties": { - "type": { - "enum" : [ "proofOfPossession" ] - }, - "nonce": { - "type": "string", - "pattern": "^[-_=0-9A-Za-z]+$" - }, - "signature": { - "$ref": "file:acme/schemata/signature.json" - } - } - }, - { "type": "object", - "required": ["type"], - "properties": { - "type": { - "enum" : [ "dns" ] - } - } - } - ] -} diff --git a/acme/schemata/revocation.json b/acme/schemata/revocation.json deleted file mode 100644 index 53455d506..000000000 --- a/acme/schemata/revocation.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/revocation#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a revocation message", - "type": "object", - "required": ["type"], - "properties": { - "type" : { - "enum" : [ "revocation" ] - } - } -} diff --git a/acme/schemata/revocationRequest.json b/acme/schemata/revocationRequest.json deleted file mode 100644 index 7559d0ee0..000000000 --- a/acme/schemata/revocationRequest.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/revocationRequest#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a revocationRequest message", - "type": "object", - "required": ["type", "certificate", "signature"], - "properties": { - "type" : { - "enum" : [ "revocationRequest" ] - }, - "certificate" : { - "type" : "string" - }, - "signature" : { - "$ref": "file:acme/schemata/signature.json" - } - } -} diff --git a/acme/schemata/signature.json b/acme/schemata/signature.json deleted file mode 100644 index e70652e7c..000000000 --- a/acme/schemata/signature.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/signature#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a signature (alg RS256/e=65537 or P-256 ONLY)", - "type": "object", - "required": ["alg", "nonce", "sig", "jwk"], - "properties": { - "anyOf": [ - { - "alg" : { - "enum" : [ "RS256" ] - }, - "nonce" : { - "type" : "string" - }, - "sig" : { - "type": "string", - "pattern": "^[-_=0-9A-Za-z]+$" - }, - "jwk": { - "type": "object", - "required": ["kty", "e", "n"], - "properties": { - "kty": { - "enum" : [ "RSA" ] - }, - "e": { - "enum" : [ "AQAB" ] - }, - "n": { - "type": "string", - "pattern": "^[-_=0-9A-Za-z]+$" - } - } - } - }, - { - "alg" : { - "enum" : [ "ES256" ] - }, - "nonce" : { - "type" : "string" - }, - "sig" : { - "type": "string", - "pattern": "^[-_=0-9A-Za-z]+$" - }, - "jwk": { - "type": "object", - "required": ["kty", "crv", "x", "y"], - "properties": { - "kty": { - "enum" : [ "EC" ] - }, - "crv": { - "enum" : [ "P-256" ] - }, - "x": { - "type": "string", - "pattern": "^[-_=0-9A-Za-z]+$" - }, - "y": { - "type": "string", - "pattern": "^[-_=0-9A-Za-z]+$" - } - } - } - } - ] - } -} diff --git a/acme/schemata/statusRequest.json b/acme/schemata/statusRequest.json deleted file mode 100644 index 8e4221cbe..000000000 --- a/acme/schemata/statusRequest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "https://letsencrypt.org/schema/01/statusRequest#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Schema for a statusRequest message", - "type": "object", - "required": ["type", "token"], - "properties": { - "type" : { - "enum" : [ "statusRequest" ] - }, - "token" : { - "type": "string" - } - } -} From aa6faadb5c1d99c5d2026cac9a62193ead8ebd01 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 15:07:17 +0000 Subject: [PATCH 50/70] Add ChallangeResponseTest --- acme/challenges_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/acme/challenges_test.py b/acme/challenges_test.py index 4c61c0e3d..f0b025ad3 100644 --- a/acme/challenges_test.py +++ b/acme/challenges_test.py @@ -18,6 +18,13 @@ KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey( 'acme.jose', os.path.join('testdata', 'rsa512_key.pem')))) +class ChallengeResponseTest(unittest.TestCase): + + def test_from_json_none(self): + from acme.challenges import ChallengeResponse + self.assertTrue(ChallengeResponse.from_json(None) is None) + + class SimpleHTTPTest(unittest.TestCase): def setUp(self): From 71a01d139ca25eae0548b35cfc5520fa5c3a808b Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 14:55:11 +0000 Subject: [PATCH 51/70] Rename network2 to network. --- docs/api/network2.rst | 5 ---- examples/restified.py | 4 +-- letsencrypt/auth_handler.py | 2 +- letsencrypt/client.py | 6 ++-- letsencrypt/{network2.py => network.py} | 0 letsencrypt/revoker.py | 4 +-- letsencrypt/tests/auth_handler_test.py | 4 +-- letsencrypt/tests/client_test.py | 6 ++-- .../{network2_test.py => network_test.py} | 28 +++++++++---------- letsencrypt/tests/revoker_test.py | 12 ++++---- 10 files changed, 33 insertions(+), 38 deletions(-) delete mode 100644 docs/api/network2.rst rename letsencrypt/{network2.py => network.py} (100%) rename letsencrypt/tests/{network2_test.py => network_test.py} (97%) diff --git a/docs/api/network2.rst b/docs/api/network2.rst deleted file mode 100644 index a73308e1b..000000000 --- a/docs/api/network2.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`letsencrypt.network2` ---------------------------- - -.. automodule:: letsencrypt.network2 - :members: diff --git a/examples/restified.py b/examples/restified.py index c0252c1eb..07c773575 100644 --- a/examples/restified.py +++ b/examples/restified.py @@ -7,7 +7,7 @@ import M2Crypto from acme import messages2 from acme import jose -from letsencrypt import network2 +from letsencrypt import network logger = logging.getLogger() @@ -17,7 +17,7 @@ NEW_REG_URL = 'https://www.letsencrypt-demo.org/acme/new-reg' key = jose.JWKRSA.load(pkg_resources.resource_string( 'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))) -net = network2.Network(NEW_REG_URL, key) +net = network.Network(NEW_REG_URL, key) regr = net.register(contact=( 'mailto:cert-admin@example.com', 'tel:+12025551212')) diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 5665fe83d..d801613c5 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -24,7 +24,7 @@ class AuthHandler(object): :ivar network: Network object for sending and receiving authorization messages - :type network: :class:`letsencrypt.network2.Network` + :type network: :class:`letsencrypt.network.Network` :ivar account: Client's Account :type account: :class:`letsencrypt.account.Account` diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 17bee6069..d059a777e 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -16,7 +16,7 @@ from letsencrypt import crypto_util from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util -from letsencrypt import network2 +from letsencrypt import network from letsencrypt import reverter from letsencrypt import revoker from letsencrypt import storage @@ -29,7 +29,7 @@ class Client(object): """ACME protocol client. :ivar network: Network object for sending and receiving messages - :type network: :class:`letsencrypt.network2.Network` + :type network: :class:`letsencrypt.network.Network` :ivar account: Account object used for registration :type account: :class:`letsencrypt.account.Account` @@ -62,7 +62,7 @@ class Client(object): self.installer = installer # TODO: Allow for other alg types besides RS256 - self.network = network2.Network( + self.network = network.Network( config.server, jwk.JWKRSA.load(self.account.key.pem), verify_ssl=(not config.no_verify_ssl)) diff --git a/letsencrypt/network2.py b/letsencrypt/network.py similarity index 100% rename from letsencrypt/network2.py rename to letsencrypt/network.py diff --git a/letsencrypt/revoker.py b/letsencrypt/revoker.py index d173a1907..0d3bd8e79 100644 --- a/letsencrypt/revoker.py +++ b/letsencrypt/revoker.py @@ -20,7 +20,7 @@ from acme.jose import util as jose_util from letsencrypt import errors from letsencrypt import le_util -from letsencrypt import network2 +from letsencrypt import network from letsencrypt.display import util as display_util from letsencrypt.display import revocation @@ -45,7 +45,7 @@ class Revoker(object): """ def __init__(self, installer, config, no_confirm=False): # XXX - self.network = network2.Network(new_reg_uri=None, key=None, alg=None) + self.network = network.Network(new_reg_uri=None, key=None, alg=None) self.installer = installer self.config = config diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 8cbc0e604..fddf508b2 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -10,7 +10,7 @@ from acme import messages2 from letsencrypt import errors from letsencrypt import le_util -from letsencrypt import network2 +from letsencrypt import network from letsencrypt.tests import acme_util @@ -86,7 +86,7 @@ class GetAuthorizationsTest(unittest.TestCase): self.mock_dv_auth.perform.side_effect = gen_auth_resp self.mock_account = mock.Mock(key=le_util.Key("file_path", "PEM")) - self.mock_net = mock.MagicMock(spec=network2.Network) + self.mock_net = mock.MagicMock(spec=network.Network) self.handler = AuthHandler( self.mock_dv_auth, self.mock_cont_auth, diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 1fb9c2a03..79e2597ea 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -26,14 +26,14 @@ class ClientTest(unittest.TestCase): self.account = mock.MagicMock(**{"key.pem": KEY}) from letsencrypt.client import Client - with mock.patch("letsencrypt.client.network2") as network2: + with mock.patch("letsencrypt.client.network") as network: self.client = Client( config=self.config, account_=self.account, dv_auth=None, installer=None) - self.network2 = network2 + self.network = network def test_init_network_verify_ssl(self): - self.network2.Network.assert_called_once_with( + self.network.Network.assert_called_once_with( mock.ANY, mock.ANY, verify_ssl=True) @mock.patch("letsencrypt.client.zope.component.getUtility") diff --git a/letsencrypt/tests/network2_test.py b/letsencrypt/tests/network_test.py similarity index 97% rename from letsencrypt/tests/network2_test.py rename to letsencrypt/tests/network_test.py index 3f745ffa7..c0522c2fb 100644 --- a/letsencrypt/tests/network2_test.py +++ b/letsencrypt/tests/network_test.py @@ -1,4 +1,4 @@ -"""Tests for letsencrypt.network2.""" +"""Tests for letsencrypt.network.""" import datetime import httplib import os @@ -36,7 +36,7 @@ KEY2 = jose.JWKRSA.load(pkg_resources.resource_string( class NetworkTest(unittest.TestCase): - """Tests for letsencrypt.network2.Network.""" + """Tests for letsencrypt.network.Network.""" # pylint: disable=too-many-instance-attributes,too-many-public-methods @@ -44,7 +44,7 @@ class NetworkTest(unittest.TestCase): self.verify_ssl = mock.MagicMock() self.wrap_in_jws = mock.MagicMock(return_value=mock.sentinel.wrapped) - from letsencrypt.network2 import Network + from letsencrypt.network import Network self.net = Network( new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg', key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl) @@ -167,14 +167,14 @@ class NetworkTest(unittest.TestCase): # pylint: disable=protected-access self.net._check_response(self.response) - @mock.patch('letsencrypt.network2.requests') + @mock.patch('letsencrypt.network.requests') def test_get_requests_error_passthrough(self, requests_mock): requests_mock.exceptions = requests.exceptions requests_mock.get.side_effect = requests.exceptions.RequestException # pylint: disable=protected-access self.assertRaises(errors.NetworkError, self.net._get, 'uri') - @mock.patch('letsencrypt.network2.requests') + @mock.patch('letsencrypt.network.requests') def test_get(self, requests_mock): # pylint: disable=protected-access self.net._check_response = mock.MagicMock() @@ -186,7 +186,7 @@ class NetworkTest(unittest.TestCase): # pylint: disable=protected-access self.net._wrap_in_jws = self.wrap_in_jws - @mock.patch('letsencrypt.network2.requests') + @mock.patch('letsencrypt.network.requests') def test_post_requests_error_passthrough(self, requests_mock): requests_mock.exceptions = requests.exceptions requests_mock.post.side_effect = requests.exceptions.RequestException @@ -195,7 +195,7 @@ class NetworkTest(unittest.TestCase): self.assertRaises( errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj) - @mock.patch('letsencrypt.network2.requests') + @mock.patch('letsencrypt.network.requests') def test_post(self, requests_mock): # pylint: disable=protected-access self.net._check_response = mock.MagicMock() @@ -206,7 +206,7 @@ class NetworkTest(unittest.TestCase): self.net._check_response.assert_called_once_with( requests_mock.post('uri', mock.sentinel.wrapped), content_type='ct') - @mock.patch('letsencrypt.network2.requests') + @mock.patch('letsencrypt.network.requests') def test_post_replay_nonce_handling(self, requests_mock): # pylint: disable=protected-access self.net._check_response = mock.MagicMock() @@ -233,7 +233,7 @@ class NetworkTest(unittest.TestCase): self.assertRaises( errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj) - @mock.patch('letsencrypt.client.network2.requests') + @mock.patch('letsencrypt.client.network.requests') def test_get_post_verify_ssl(self, requests_mock): # pylint: disable=protected-access self._mock_wrap_in_jws() @@ -372,7 +372,7 @@ class NetworkTest(unittest.TestCase): datetime.datetime(1999, 12, 31, 23, 59, 59), self.net.retry_after(response=self.response, default=10)) - @mock.patch('letsencrypt.network2.datetime') + @mock.patch('letsencrypt.network.datetime') def test_retry_after_invalid(self, dt_mock): dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.timedelta = datetime.timedelta @@ -382,7 +382,7 @@ class NetworkTest(unittest.TestCase): datetime.datetime(2015, 3, 27, 0, 0, 10), self.net.retry_after(response=self.response, default=10)) - @mock.patch('letsencrypt.network2.datetime') + @mock.patch('letsencrypt.network.datetime') def test_retry_after_seconds(self, dt_mock): dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.timedelta = datetime.timedelta @@ -392,7 +392,7 @@ class NetworkTest(unittest.TestCase): datetime.datetime(2015, 3, 27, 0, 0, 50), self.net.retry_after(response=self.response, default=10)) - @mock.patch('letsencrypt.network2.datetime') + @mock.patch('letsencrypt.network.datetime') def test_retry_after_missing(self, dt_mock): dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.timedelta = datetime.timedelta @@ -435,8 +435,8 @@ class NetworkTest(unittest.TestCase): errors.NetworkError, self.net.request_issuance, CSR, (self.authzr,)) - @mock.patch('letsencrypt.network2.datetime') - @mock.patch('letsencrypt.network2.time') + @mock.patch('letsencrypt.network.datetime') + @mock.patch('letsencrypt.network.time') def test_poll_and_request_issuance(self, time_mock, dt_mock): # clock.dt | pylint: disable=no-member clock = mock.MagicMock(dt=datetime.datetime(2015, 3, 27)) diff --git a/letsencrypt/tests/revoker_test.py b/letsencrypt/tests/revoker_test.py index 35e7d132b..cd86594fd 100644 --- a/letsencrypt/tests/revoker_test.py +++ b/letsencrypt/tests/revoker_test.py @@ -63,7 +63,7 @@ class RevokerTest(RevokerBase): def tearDown(self): shutil.rmtree(self.backup_dir) - @mock.patch("letsencrypt.network2.Network.revoke") + @mock.patch("letsencrypt.network.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_key_all(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -89,7 +89,7 @@ class RevokerTest(RevokerBase): self.revoker.revoke_from_key, self.key) - @mock.patch("letsencrypt.network2.Network.revoke") + @mock.patch("letsencrypt.network.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_wrong_key(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -105,7 +105,7 @@ class RevokerTest(RevokerBase): # No revocation went through self.assertEqual(mock_net.call_count, 0) - @mock.patch("letsencrypt.network2.Network.revoke") + @mock.patch("letsencrypt.network.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_cert(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -122,7 +122,7 @@ class RevokerTest(RevokerBase): self.assertEqual(mock_net.call_count, 1) - @mock.patch("letsencrypt.network2.Network.revoke") + @mock.patch("letsencrypt.network.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_cert_not_found(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -141,7 +141,7 @@ class RevokerTest(RevokerBase): self.assertEqual(mock_net.call_count, 1) - @mock.patch("letsencrypt.network2.Network.revoke") + @mock.patch("letsencrypt.network.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_menu(self, mock_display, mock_net): mock_display().confirm_revocation.return_value = True @@ -165,7 +165,7 @@ class RevokerTest(RevokerBase): self.assertEqual(mock_display.more_info_cert.call_count, 1) @mock.patch("letsencrypt.revoker.logging") - @mock.patch("letsencrypt.network2.Network.revoke") + @mock.patch("letsencrypt.network.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") def test_revoke_by_menu_delete_all(self, mock_display, mock_net, mock_log): mock_display().confirm_revocation.return_value = True From a278d53f5200086c2436ccf56675dd718eb6c4c9 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 15:00:18 +0000 Subject: [PATCH 52/70] Rename messages2 to messages. --- acme/{messages2.py => messages.py} | 16 ++--- acme/{messages2_test.py => messages_test.py} | 62 +++++++++---------- docs/pkgs/acme/index.rst | 9 --- examples/restified.py | 8 +-- letsencrypt/account.py | 8 +-- letsencrypt/achallenges.py | 4 +- letsencrypt/auth_handler.py | 24 +++---- letsencrypt/network.py | 48 +++++++------- letsencrypt/tests/account_test.py | 6 +- letsencrypt/tests/acme_util.py | 30 ++++----- letsencrypt/tests/auth_handler_test.py | 42 ++++++------- letsencrypt/tests/network_test.py | 34 +++++----- letsencrypt/tests/proof_of_possession_test.py | 10 +-- letsencrypt_nginx/tests/configurator_test.py | 10 +-- 14 files changed, 151 insertions(+), 160 deletions(-) rename acme/{messages2.py => messages.py} (96%) rename acme/{messages2_test.py => messages_test.py} (83%) diff --git a/acme/messages2.py b/acme/messages.py similarity index 96% rename from acme/messages2.py rename to acme/messages.py index 15b4521de..aa041caed 100644 --- a/acme/messages2.py +++ b/acme/messages.py @@ -100,7 +100,7 @@ IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder class Identifier(jose.JSONObjectWithFields): """ACME identifier. - :ivar acme.messages2.IdentifierType typ: + :ivar acme.messages.IdentifierType typ: """ typ = jose.Field('type', decoder=IdentifierType.from_json) @@ -110,7 +110,7 @@ class Identifier(jose.JSONObjectWithFields): class Resource(jose.ImmutableMap): """ACME Resource. - :ivar acme.messages2.ResourceBody body: Resource body. + :ivar acme.messages.ResourceBody body: Resource body. :ivar str uri: Location of the resource. """ @@ -124,7 +124,7 @@ class ResourceBody(jose.JSONObjectWithFields): class RegistrationResource(Resource): """Registration Resource. - :ivar acme.messages2.Registration body: + :ivar acme.messages.Registration body: :ivar str new_authzr_uri: URI found in the 'next' ``Link`` header :ivar str terms_of_service: URL for the CA TOS. @@ -150,7 +150,7 @@ class Registration(ResourceBody): class ChallengeResource(Resource, jose.JSONObjectWithFields): """Challenge Resource. - :ivar acme.messages2.ChallengeBody body: + :ivar acme.messages.ChallengeBody body: :ivar str authzr_uri: URI found in the 'up' ``Link`` header. """ @@ -175,7 +175,7 @@ class ChallengeBody(ResourceBody): :ivar acme.challenges.Challenge: Wrapped challenge. Conveniently, all challenge fields are proxied, i.e. you can call ``challb.x`` to get ``challb.chall.x`` contents. - :ivar acme.messages2.Status status: + :ivar acme.messages.Status status: :ivar datetime.datetime validated: """ @@ -202,7 +202,7 @@ class ChallengeBody(ResourceBody): class AuthorizationResource(Resource): """Authorization Resource. - :ivar acme.messages2.Authorization body: + :ivar acme.messages.Authorization body: :ivar str new_cert_uri: URI found in the 'next' ``Link`` header """ @@ -212,13 +212,13 @@ class AuthorizationResource(Resource): class Authorization(ResourceBody): """Authorization Resource Body. - :ivar acme.messages2.Identifier identifier: + :ivar acme.messages.Identifier identifier: :ivar list challenges: `list` of `.ChallengeBody` :ivar tuple combinations: Challenge combinations (`tuple` of `tuple` of `int`, as opposed to `list` of `list` from the spec). :ivar acme.jose.jwk.JWK key: Public key. :ivar tuple contact: - :ivar acme.messages2.Status status: + :ivar acme.messages.Status status: :ivar datetime.datetime expires: """ diff --git a/acme/messages2_test.py b/acme/messages_test.py similarity index 83% rename from acme/messages2_test.py rename to acme/messages_test.py index 72ffc954a..4f86d7809 100644 --- a/acme/messages2_test.py +++ b/acme/messages_test.py @@ -1,4 +1,4 @@ -"""Tests for acme.messages2.""" +"""Tests for acme.messages.""" import datetime import os import pkg_resources @@ -17,10 +17,10 @@ KEY = jose.util.HashableRSAKey(RSA.importKey(pkg_resources.resource_string( class ErrorTest(unittest.TestCase): - """Tests for acme.messages2.Error.""" + """Tests for acme.messages.Error.""" def setUp(self): - from acme.messages2 import Error + from acme.messages import Error self.error = Error(detail='foo', typ='malformed', title='title') self.jobj = {'detail': 'foo', 'title': 'some title'} @@ -32,14 +32,14 @@ class ErrorTest(unittest.TestCase): 'malformed', self.error.from_json(self.error.to_partial_json()).typ) def test_typ_decoder_missing_prefix(self): - from acme.messages2 import Error + from acme.messages import Error self.jobj['type'] = 'malformed' self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj) self.jobj['type'] = 'not valid bare type' self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj) def test_typ_decoder_not_recognized(self): - from acme.messages2 import Error + from acme.messages import Error self.jobj['type'] = 'urn:acme:error:baz' self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj) @@ -48,7 +48,7 @@ class ErrorTest(unittest.TestCase): 'The request message was malformed', self.error.description) def test_from_json_hashable(self): - from acme.messages2 import Error + from acme.messages import Error hash(Error.from_json(self.error.to_json())) def test_str(self): @@ -59,10 +59,10 @@ class ErrorTest(unittest.TestCase): class ConstantTest(unittest.TestCase): - """Tests for acme.messages2._Constant.""" + """Tests for acme.messages._Constant.""" def setUp(self): - from acme.messages2 import _Constant + from acme.messages import _Constant class MockConstant(_Constant): # pylint: disable=missing-docstring POSSIBLE_NAMES = {} @@ -95,7 +95,7 @@ class ConstantTest(unittest.TestCase): self.assertFalse(self.const_a != const_a_prime) class RegistrationTest(unittest.TestCase): - """Tests for acme.messages2.Registration.""" + """Tests for acme.messages.Registration.""" def setUp(self): key = jose.jwk.JWKRSA(key=KEY.publickey()) @@ -103,7 +103,7 @@ class RegistrationTest(unittest.TestCase): recovery_token = 'XYZ' agreement = 'https://letsencrypt.org/terms' - from acme.messages2 import Registration + from acme.messages import Registration self.reg = Registration( key=key, contact=contact, recovery_token=recovery_token, agreement=agreement) @@ -121,31 +121,31 @@ class RegistrationTest(unittest.TestCase): self.assertEqual(self.jobj_to, self.reg.to_partial_json()) def test_from_json(self): - from acme.messages2 import Registration + from acme.messages import Registration self.assertEqual(self.reg, Registration.from_json(self.jobj_from)) def test_from_json_hashable(self): - from acme.messages2 import Registration + from acme.messages import Registration hash(Registration.from_json(self.jobj_from)) class ChallengeResourceTest(unittest.TestCase): - """Tests for acme.messages2.ChallengeResource.""" + """Tests for acme.messages.ChallengeResource.""" def test_uri(self): - from acme.messages2 import ChallengeResource + from acme.messages import ChallengeResource self.assertEqual('http://challb', ChallengeResource(body=mock.MagicMock( uri='http://challb'), authzr_uri='http://authz').uri) class ChallengeBodyTest(unittest.TestCase): - """Tests for acme.messages2.ChallengeBody.""" + """Tests for acme.messages.ChallengeBody.""" def setUp(self): self.chall = challenges.DNS(token='foo') - from acme.messages2 import ChallengeBody - from acme.messages2 import STATUS_VALID + from acme.messages import ChallengeBody + from acme.messages import STATUS_VALID self.status = STATUS_VALID self.challb = ChallengeBody( uri='http://challb', chall=self.chall, status=self.status) @@ -163,11 +163,11 @@ class ChallengeBodyTest(unittest.TestCase): self.assertEqual(self.jobj_to, self.challb.to_partial_json()) def test_from_json(self): - from acme.messages2 import ChallengeBody + from acme.messages import ChallengeBody self.assertEqual(self.challb, ChallengeBody.from_json(self.jobj_from)) def test_from_json_hashable(self): - from acme.messages2 import ChallengeBody + from acme.messages import ChallengeBody hash(ChallengeBody.from_json(self.jobj_from)) def test_proxy(self): @@ -175,11 +175,11 @@ class ChallengeBodyTest(unittest.TestCase): class AuthorizationTest(unittest.TestCase): - """Tests for acme.messages2.Authorization.""" + """Tests for acme.messages.Authorization.""" def setUp(self): - from acme.messages2 import ChallengeBody - from acme.messages2 import STATUS_VALID + from acme.messages import ChallengeBody + from acme.messages import STATUS_VALID self.challbs = ( ChallengeBody( uri='http://challb1', status=STATUS_VALID, @@ -191,9 +191,9 @@ class AuthorizationTest(unittest.TestCase): ) combinations = ((0, 2), (1, 2)) - from acme.messages2 import Authorization - from acme.messages2 import Identifier - from acme.messages2 import IDENTIFIER_FQDN + from acme.messages import Authorization + from acme.messages import Identifier + from acme.messages import IDENTIFIER_FQDN identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com') self.authz = Authorization( identifier=identifier, combinations=combinations, @@ -206,11 +206,11 @@ class AuthorizationTest(unittest.TestCase): } def test_from_json(self): - from acme.messages2 import Authorization + from acme.messages import Authorization Authorization.from_json(self.jobj_from) def test_from_json_hashable(self): - from acme.messages2 import Authorization + from acme.messages import Authorization hash(Authorization.from_json(self.jobj_from)) def test_resolved_combinations(self): @@ -221,10 +221,10 @@ class AuthorizationTest(unittest.TestCase): class RevocationTest(unittest.TestCase): - """Tests for acme.messages2.RevocationTest.""" + """Tests for acme.messages.RevocationTest.""" def setUp(self): - from acme.messages2 import Revocation + from acme.messages import Revocation self.rev_now = Revocation(authorizations=(), revoke=Revocation.NOW) self.rev_date = Revocation(authorizations=(), revoke=datetime.datetime( 2015, 3, 27, tzinfo=pytz.utc)) @@ -233,7 +233,7 @@ class RevocationTest(unittest.TestCase): 'revoke': '2015-03-27T00:00:00Z'} def test_revoke_decoder(self): - from acme.messages2 import Revocation + from acme.messages import Revocation self.assertEqual(self.rev_now, Revocation.from_json(self.jobj_now)) self.assertEqual(self.rev_date, Revocation.from_json(self.jobj_date)) @@ -242,7 +242,7 @@ class RevocationTest(unittest.TestCase): self.assertEqual(self.jobj_date, self.rev_date.to_partial_json()) def test_from_json_hashable(self): - from acme.messages2 import Revocation + from acme.messages import Revocation hash(Revocation.from_json(self.rev_now.to_json())) diff --git a/docs/pkgs/acme/index.rst b/docs/pkgs/acme/index.rst index 1c73a4a42..ea0743b1e 100644 --- a/docs/pkgs/acme/index.rst +++ b/docs/pkgs/acme/index.rst @@ -10,18 +10,9 @@ Messages -------- -v00 -~~~ - .. automodule:: acme.messages :members: -v02 -~~~ - -.. automodule:: acme.messages2 - :members: - Challenges ---------- diff --git a/examples/restified.py b/examples/restified.py index 07c773575..cfd7fa8dd 100644 --- a/examples/restified.py +++ b/examples/restified.py @@ -4,7 +4,7 @@ import pkg_resources import M2Crypto -from acme import messages2 +from acme import messages from acme import jose from letsencrypt import network @@ -27,8 +27,8 @@ net.update_registration(regr.update( logging.debug(regr) authzr = net.request_challenges( - identifier=messages2.Identifier( - typ=messages2.IDENTIFIER_FQDN, value='example1.com'), + identifier=messages.Identifier( + typ=messages.IDENTIFIER_FQDN, value='example1.com'), new_authzr_uri=regr.new_authzr_uri) logging.debug(authzr) @@ -38,5 +38,5 @@ csr = M2Crypto.X509.load_request_string(pkg_resources.resource_string( 'letsencrypt.tests', os.path.join('testdata', 'csr.pem'))) try: net.request_issuance(csr, (authzr,)) -except messages2.Error as error: +except messages.Error as error: print error.detail diff --git a/letsencrypt/account.py b/letsencrypt/account.py index 93a949050..9f351387f 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -6,7 +6,7 @@ import re import configobj import zope.component -from acme import messages2 +from acme import messages from letsencrypt import crypto_util from letsencrypt import errors @@ -28,7 +28,7 @@ class Account(object): :ivar str phone: Client's phone number :ivar regr: Registration Resource - :type regr: :class:`~acme.messages2.RegistrationResource` + :type regr: :class:`~acme.messages.RegistrationResource` """ @@ -141,11 +141,11 @@ class Account(object): if "RegistrationResource" in acc_config: acc_config_rr = acc_config["RegistrationResource"] - regr = messages2.RegistrationResource( + regr = messages.RegistrationResource( uri=acc_config_rr["uri"], new_authzr_uri=acc_config_rr["new_authzr_uri"], terms_of_service=acc_config_rr["terms_of_service"], - body=messages2.Registration.from_json(acc_config_rr["body"])) + body=messages.Registration.from_json(acc_config_rr["body"])) else: regr = None diff --git a/letsencrypt/achallenges.py b/letsencrypt/achallenges.py index 46ef167e0..88dcdbe11 100644 --- a/letsencrypt/achallenges.py +++ b/letsencrypt/achallenges.py @@ -5,11 +5,11 @@ Please use names such as ``achall`` to distiguish from variables "of type" and :class:`.ChallengeBody` (denoted by ``challb``):: from acme import challenges - from acme import messages2 + from acme import messages from letsencrypt import achallenges chall = challenges.DNS(token='foo') - challb = messages2.ChallengeBody(chall=chall) + challb = messages.ChallengeBody(chall=chall) achall = achallenges.DNS(chall=challb, domain='example.com') Note, that all annotated challenges act as a proxy objects:: diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index d801613c5..d895c165c 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -4,7 +4,7 @@ import logging import time from acme import challenges -from acme import messages2 +from acme import messages from letsencrypt import achallenges from letsencrypt import constants @@ -30,7 +30,7 @@ class AuthHandler(object): :type account: :class:`letsencrypt.account.Account` :ivar dict authzr: ACME Authorization Resource dict where keys are domains - and values are :class:`acme.messages2.AuthorizationResource` + and values are :class:`acme.messages.AuthorizationResource` :ivar list dv_c: DV challenges in the form of :class:`letsencrypt.achallenges.AnnotatedChallenge` :ivar list cont_c: Continuity challenges in the @@ -82,7 +82,7 @@ class AuthHandler(object): self.verify_authzr_complete() # Only return valid authorizations return [authzr for authzr in self.authzr.values() - if authzr.body.status == messages2.STATUS_VALID] + if authzr.body.status == messages.STATUS_VALID] def _choose_challenges(self, domains): """Retrieve necessary challenges to satisfy server.""" @@ -198,7 +198,7 @@ class AuthHandler(object): failed = [] self.authzr[domain], _ = self.network.poll(self.authzr[domain]) - if self.authzr[domain].body.status == messages2.STATUS_VALID: + if self.authzr[domain].body.status == messages.STATUS_VALID: return achalls, [] # Note: if the whole authorization is invalid, the individual failed @@ -207,9 +207,9 @@ class AuthHandler(object): status = self._get_chall_status(self.authzr[domain], achall) # This does nothing for challenges that have yet to be decided yet. - if status == messages2.STATUS_VALID: + if status == messages.STATUS_VALID: completed.append(achall) - elif status == messages2.STATUS_INVALID: + elif status == messages.STATUS_INVALID: failed.append(achall) return completed, failed @@ -221,7 +221,7 @@ class AuthHandler(object): each challenge resource. :param authzr: Authorization Resource - :type authzr: :class:`acme.messages2.AuthorizationResource` + :type authzr: :class:`acme.messages.AuthorizationResource` :param achall: Annotated challenge for which to get status :type achall: :class:`letsencrypt.achallenges.AnnotatedChallenge` @@ -279,8 +279,8 @@ class AuthHandler(object): """ for authzr in self.authzr.values(): - if (authzr.body.status != messages2.STATUS_VALID and - authzr.body.status != messages2.STATUS_INVALID): + if (authzr.body.status != messages.STATUS_VALID and + authzr.body.status != messages.STATUS_INVALID): raise errors.AuthorizationError("Incomplete authorizations") def _challenge_factory(self, domain, path): @@ -321,7 +321,7 @@ def challb_to_achall(challb, key, domain): """Converts a ChallengeBody object to an AnnotatedChallenge. :param challb: ChallengeBody - :type challb: :class:`acme.messages2.ChallengeBody` + :type challb: :class:`acme.messages.ChallengeBody` :param key: Key :type key: :class:`letsencrypt.le_util.Key` @@ -370,8 +370,8 @@ def gen_challenge_path(challbs, preferences, combinations): .. todo:: This can be possibly be rewritten to use resolved_combinations. :param tuple challbs: A tuple of challenges - (:class:`acme.messages2.Challenge`) from - :class:`acme.messages2.AuthorizationResource` to be + (:class:`acme.messages.Challenge`) from + :class:`acme.messages.AuthorizationResource` to be fulfilled by the client in order to prove possession of the identifier. diff --git a/letsencrypt/network.py b/letsencrypt/network.py index a20194a79..6d3be1afc 100644 --- a/letsencrypt/network.py +++ b/letsencrypt/network.py @@ -11,7 +11,7 @@ import werkzeug from acme import jose from acme import jws as acme_jws -from acme import messages2 +from acme import messages from letsencrypt import errors @@ -75,7 +75,7 @@ class Network(object): function will raise an error. Otherwise, wrong Content-Type is ignored, but logged. - :raises letsencrypt.messages2.Error: If server response body + :raises letsencrypt.messages.Error: If server response body carries HTTP Problem (draft-ietf-appsawg-http-problem-00). :raises letsencrypt.errors.NetworkError: In case of other networking errors. @@ -98,7 +98,7 @@ class Network(object): try: logging.error("Error: %s", jobj) logging.error("Response from server: %s", response.content) - raise messages2.Error.from_json(jobj) + raise messages.Error.from_json(jobj) except jose.DeserializationError as error: # Couldn't deserialize JSON object raise errors.NetworkError((response, error)) @@ -160,7 +160,7 @@ class Network(object): :param JSONDeSerializable obj: Will be wrapped in JWS. :param str content_type: Expected ``Content-Type``, fails if not set. - :raises acme.messages2.NetworkError: + :raises acme.messages.NetworkError: :returns: HTTP Response :rtype: `requests.Response` @@ -192,13 +192,13 @@ class Network(object): except KeyError: raise errors.NetworkError('"next" link missing') - return messages2.RegistrationResource( - body=messages2.Registration.from_json(response.json()), + return messages.RegistrationResource( + body=messages.Registration.from_json(response.json()), uri=response.headers.get('Location', uri), new_authzr_uri=new_authzr_uri, terms_of_service=terms_of_service) - def register(self, contact=messages2.Registration._fields[ + def register(self, contact=messages.Registration._fields[ 'contact'].default): """Register. @@ -211,7 +211,7 @@ class Network(object): :raises letsencrypt.errors.UnexpectedUpdate: """ - new_reg = messages2.Registration(contact=contact) + new_reg = messages.Registration(contact=contact) response = self._post(self.new_reg_uri, new_reg) assert response.status_code == httplib.CREATED # TODO: handle errors @@ -289,8 +289,8 @@ class Network(object): except KeyError: raise errors.NetworkError('"next" link missing') - authzr = messages2.AuthorizationResource( - body=messages2.Authorization.from_json(response.json()), + authzr = messages.AuthorizationResource( + body=messages.Authorization.from_json(response.json()), uri=response.headers.get('Location', uri), new_cert_uri=new_cert_uri) if authzr.body.identifier != identifier: @@ -301,7 +301,7 @@ class Network(object): """Request challenges. :param identifier: Identifier to be challenged. - :type identifier: `.messages2.Identifier` + :type identifier: `.messages.Identifier` :param str new_authzr_uri: new-authorization URI @@ -309,7 +309,7 @@ class Network(object): :rtype: `.AuthorizationResource` """ - new_authz = messages2.Authorization(identifier=identifier) + new_authz = messages.Authorization(identifier=identifier) response = self._post(new_authzr_uri, new_authz) assert response.status_code == httplib.CREATED # TODO: handle errors return self._authzr_from_response(response, identifier) @@ -328,8 +328,8 @@ class Network(object): :rtype: `.AuthorizationResource` """ - return self.request_challenges(messages2.Identifier( - typ=messages2.IDENTIFIER_FQDN, value=domain), new_authz_uri) + return self.request_challenges(messages.Identifier( + typ=messages.IDENTIFIER_FQDN, value=domain), new_authz_uri) def answer_challenge(self, challb, response): """Answer challenge. @@ -351,9 +351,9 @@ class Network(object): authzr_uri = response.links['up']['url'] except KeyError: raise errors.NetworkError('"up" Link header missing') - challr = messages2.ChallengeResource( + challr = messages.ChallengeResource( authzr_uri=authzr_uri, - body=messages2.ChallengeBody.from_json(response.json())) + body=messages.ChallengeBody.from_json(response.json())) # TODO: check that challr.uri == response.headers['Location']? if challr.uri != challb.uri: raise errors.UnexpectedUpdate(challr.uri) @@ -412,14 +412,14 @@ class Network(object): :param authzrs: `list` of `.AuthorizationResource` :returns: Issued certificate - :rtype: `.messages2.CertificateResource` + :rtype: `.messages.CertificateResource` """ assert authzrs, "Authorizations list is empty" logging.debug("Requesting issuance...") # TODO: assert len(authzrs) == number of SANs - req = messages2.CertificateRequest( + req = messages.CertificateRequest( csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs)) content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument @@ -436,7 +436,7 @@ class Network(object): except KeyError: raise errors.NetworkError('"Location" Header missing') - return messages2.CertificateResource( + return messages.CertificateResource( uri=uri, authzrs=authzrs, cert_chain_uri=cert_chain_uri, body=jose.ComparableX509( M2Crypto.X509.load_cert_der_string(response.content))) @@ -459,7 +459,7 @@ class Network(object): ``Retry-After`` is not present in the response. :returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is - the issued certificate (`.messages2.CertificateResource.), + the issued certificate (`.messages.CertificateResource.), and ``updated_authzrs`` is a `tuple` consisting of updated Authorization Resources (`.AuthorizationResource`) as present in the responses from server, and in the same order @@ -488,7 +488,7 @@ class Network(object): updated_authzr, response = self.poll(updated[authzr]) updated[authzr] = updated_authzr - if updated_authzr.body.status != messages2.STATUS_VALID: + if updated_authzr.body.status != messages.STATUS_VALID: # push back to the priority queue, with updated retry_after heapq.heappush(waiting, (self.retry_after( response, default=mintime), authzr)) @@ -561,20 +561,20 @@ class Network(object): else: return None - def revoke(self, certr, when=messages2.Revocation.NOW): + def revoke(self, certr, when=messages.Revocation.NOW): """Revoke certificate. :param certr: Certificate Resource :type certr: `.CertificateResource` :param when: When should the revocation take place? Takes - the same values as `.messages2.Revocation.revoke`. + the same values as `.messages.Revocation.revoke`. :raises letsencrypt.errors.NetworkError: If revocation is unsuccessful. """ - rev = messages2.Revocation(revoke=when, authorizations=tuple( + rev = messages.Revocation(revoke=when, authorizations=tuple( authzr.uri for authzr in certr.authzrs)) response = self._post(certr.uri, rev) if response.status_code != httplib.OK: diff --git a/letsencrypt/tests/account_test.py b/letsencrypt/tests/account_test.py index d14610252..6e9966a55 100644 --- a/letsencrypt/tests/account_test.py +++ b/letsencrypt/tests/account_test.py @@ -7,7 +7,7 @@ import shutil import tempfile import unittest -from acme import messages2 +from acme import messages from letsencrypt import configuration from letsencrypt import errors @@ -40,11 +40,11 @@ class AccountTest(unittest.TestCase): self.key = le_util.Key(key_file, key_pem) self.email = "client@letsencrypt.org" - self.regr = messages2.RegistrationResource( + self.regr = messages.RegistrationResource( uri="uri", new_authzr_uri="new_authzr_uri", terms_of_service="terms_of_service", - body=messages2.Registration( + body=messages.Registration( recovery_token="recovery_token", agreement="agreement") ) diff --git a/letsencrypt/tests/acme_util.py b/letsencrypt/tests/acme_util.py index daf651059..7ac05c1fa 100644 --- a/letsencrypt/tests/acme_util.py +++ b/letsencrypt/tests/acme_util.py @@ -8,7 +8,7 @@ import Crypto.PublicKey.RSA from acme import challenges from acme import jose -from acme import messages2 +from acme import messages KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey( @@ -78,19 +78,19 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name "status": status, } - if status == messages2.STATUS_VALID: + if status == messages.STATUS_VALID: kwargs.update({"validated": datetime.datetime.now()}) - return messages2.ChallengeBody(**kwargs) # pylint: disable=star-args + return messages.ChallengeBody(**kwargs) # pylint: disable=star-args # Pending ChallengeBody objects -DVSNI_P = chall_to_challb(DVSNI, messages2.STATUS_PENDING) -SIMPLE_HTTP_P = chall_to_challb(SIMPLE_HTTP, messages2.STATUS_PENDING) -DNS_P = chall_to_challb(DNS, messages2.STATUS_PENDING) -RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, messages2.STATUS_PENDING) -RECOVERY_TOKEN_P = chall_to_challb(RECOVERY_TOKEN, messages2.STATUS_PENDING) -POP_P = chall_to_challb(POP, messages2.STATUS_PENDING) +DVSNI_P = chall_to_challb(DVSNI, messages.STATUS_PENDING) +SIMPLE_HTTP_P = chall_to_challb(SIMPLE_HTTP, messages.STATUS_PENDING) +DNS_P = chall_to_challb(DNS, messages.STATUS_PENDING) +RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, messages.STATUS_PENDING) +RECOVERY_TOKEN_P = chall_to_challb(RECOVERY_TOKEN, messages.STATUS_PENDING) +POP_P = chall_to_challb(POP, messages.STATUS_PENDING) CHALLENGES_P = [SIMPLE_HTTP_P, DVSNI_P, DNS_P, RECOVERY_CONTACT_P, RECOVERY_TOKEN_P, POP_P] @@ -106,7 +106,7 @@ def gen_authzr(authz_status, domain, challs, statuses, combos=True): """Generate an authorization resource. :param authz_status: Status object - :type authz_status: :class:`acme.messages2.Status` + :type authz_status: :class:`acme.messages.Status` :param list challs: Challenge objects :param list statuses: status of each challenge object :param bool combos: Whether or not to add combinations @@ -118,13 +118,13 @@ def gen_authzr(authz_status, domain, challs, statuses, combos=True): for chall, status in itertools.izip(challs, statuses) ) authz_kwargs = { - "identifier": messages2.Identifier( - typ=messages2.IDENTIFIER_FQDN, value=domain), + "identifier": messages.Identifier( + typ=messages.IDENTIFIER_FQDN, value=domain), "challenges": challbs, } if combos: authz_kwargs.update({"combinations": gen_combos(challbs)}) - if authz_status == messages2.STATUS_VALID: + if authz_status == messages.STATUS_VALID: authz_kwargs.update({ "status": authz_status, "expires": datetime.datetime.now() + datetime.timedelta(days=31), @@ -135,8 +135,8 @@ def gen_authzr(authz_status, domain, challs, statuses, combos=True): }) # pylint: disable=star-args - return messages2.AuthorizationResource( + return messages.AuthorizationResource( uri="https://trusted.ca/new-authz-resource", new_cert_uri="https://trusted.ca/new-cert", - body=messages2.Authorization(**authz_kwargs) + body=messages.Authorization(**authz_kwargs) ) diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index fddf508b2..72fba1d0b 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -6,7 +6,7 @@ import unittest import mock from acme import challenges -from acme import messages2 +from acme import messages from letsencrypt import errors from letsencrypt import le_util @@ -37,8 +37,8 @@ class ChallengeFactoryTest(unittest.TestCase): self.dom = "test" self.handler.authzr[self.dom] = acme_util.gen_authzr( - messages2.STATUS_PENDING, self.dom, acme_util.CHALLENGES, - [messages2.STATUS_PENDING]*6, False) + messages.STATUS_PENDING, self.dom, acme_util.CHALLENGES, + [messages.STATUS_PENDING]*6, False) def test_all(self): cont_c, dv_c = self.handler._challenge_factory(self.dom, range(0, 6)) @@ -57,9 +57,9 @@ class ChallengeFactoryTest(unittest.TestCase): def test_unrecognized(self): self.handler.authzr["failure.com"] = acme_util.gen_authzr( - messages2.STATUS_PENDING, "failure.com", + messages.STATUS_PENDING, "failure.com", [mock.Mock(chall="chall", typ="unrecognized")], - [messages2.STATUS_PENDING]) + [messages.STATUS_PENDING]) self.assertRaises(errors.LetsEncryptClientError, self.handler._challenge_factory, "failure.com", [0]) @@ -160,10 +160,10 @@ class GetAuthorizationsTest(unittest.TestCase): for dom in self.handler.authzr.keys(): azr = self.handler.authzr[dom] self.handler.authzr[dom] = acme_util.gen_authzr( - messages2.STATUS_VALID, + messages.STATUS_VALID, dom, [challb.chall for challb in azr.body.challenges], - [messages2.STATUS_VALID]*len(azr.body.challenges), + [messages.STATUS_VALID]*len(azr.body.challenges), azr.body.combinations) @@ -182,16 +182,16 @@ class PollChallengesTest(unittest.TestCase): self.doms = ["0", "1", "2"] self.handler.authzr[self.doms[0]] = acme_util.gen_authzr( - messages2.STATUS_PENDING, self.doms[0], - acme_util.DV_CHALLENGES, [messages2.STATUS_PENDING]*3, False) + messages.STATUS_PENDING, self.doms[0], + acme_util.DV_CHALLENGES, [messages.STATUS_PENDING]*3, False) self.handler.authzr[self.doms[1]] = acme_util.gen_authzr( - messages2.STATUS_PENDING, self.doms[1], - acme_util.DV_CHALLENGES, [messages2.STATUS_PENDING]*3, False) + messages.STATUS_PENDING, self.doms[1], + acme_util.DV_CHALLENGES, [messages.STATUS_PENDING]*3, False) self.handler.authzr[self.doms[2]] = acme_util.gen_authzr( - messages2.STATUS_PENDING, self.doms[2], - acme_util.DV_CHALLENGES, [messages2.STATUS_PENDING]*3, False) + messages.STATUS_PENDING, self.doms[2], + acme_util.DV_CHALLENGES, [messages.STATUS_PENDING]*3, False) self.chall_update = {} for dom in self.doms: @@ -205,7 +205,7 @@ class PollChallengesTest(unittest.TestCase): self.handler._poll_challenges(self.chall_update, False) for authzr in self.handler.authzr.values(): - self.assertEqual(authzr.body.status, messages2.STATUS_VALID) + self.assertEqual(authzr.body.status, messages.STATUS_VALID) @mock.patch("letsencrypt.auth_handler.time") def test_poll_challenges_failure_best_effort(self, unused_mock_time): @@ -213,7 +213,7 @@ class PollChallengesTest(unittest.TestCase): self.handler._poll_challenges(self.chall_update, True) for authzr in self.handler.authzr.values(): - self.assertEqual(authzr.body.status, messages2.STATUS_PENDING) + self.assertEqual(authzr.body.status, messages.STATUS_PENDING) @mock.patch("letsencrypt.auth_handler.time") def test_poll_challenges_failure(self, unused_mock_time): @@ -241,10 +241,10 @@ class PollChallengesTest(unittest.TestCase): # Basically it didn't raise an error and it stopped earlier than # Making all challenges invalid which would make mock_poll_solve_one # change authzr to invalid - return self._mock_poll_solve_one_chall(authzr, messages2.STATUS_VALID) + return self._mock_poll_solve_one_chall(authzr, messages.STATUS_VALID) def _mock_poll_solve_one_invalid(self, authzr): - return self._mock_poll_solve_one_chall(authzr, messages2.STATUS_INVALID) + return self._mock_poll_solve_one_chall(authzr, messages.STATUS_INVALID) def _mock_poll_solve_one_chall(self, authzr, desired_status): # pylint: disable=no-self-use @@ -269,10 +269,10 @@ class PollChallengesTest(unittest.TestCase): else: status_ = authzr.body.status - new_authzr = messages2.AuthorizationResource( + new_authzr = messages.AuthorizationResource( uri=authzr.uri, new_cert_uri=authzr.new_cert_uri, - body=messages2.Authorization( + body=messages.Authorization( identifier=authzr.body.identifier, challenges=new_challbs, combinations=authzr.body.combinations, @@ -429,8 +429,8 @@ def gen_auth_resp(chall_list): def gen_dom_authzr(domain, unused_new_authzr_uri, challs): """Generates new authzr for domains.""" return acme_util.gen_authzr( - messages2.STATUS_PENDING, domain, challs, - [messages2.STATUS_PENDING]*len(challs)) + messages.STATUS_PENDING, domain, challs, + [messages.STATUS_PENDING]*len(challs)) if __name__ == "__main__": diff --git a/letsencrypt/tests/network_test.py b/letsencrypt/tests/network_test.py index c0522c2fb..586dc7ecb 100644 --- a/letsencrypt/tests/network_test.py +++ b/letsencrypt/tests/network_test.py @@ -14,7 +14,7 @@ import requests from acme import challenges from acme import jose from acme import jws as acme_jws -from acme import messages2 +from acme import messages from letsencrypt import account from letsencrypt import errors @@ -58,37 +58,37 @@ class NetworkTest(unittest.TestCase): self.post = mock.MagicMock(return_value=self.response) self.get = mock.MagicMock(return_value=self.response) - self.identifier = messages2.Identifier( - typ=messages2.IDENTIFIER_FQDN, value='example.com') + self.identifier = messages.Identifier( + typ=messages.IDENTIFIER_FQDN, value='example.com') self.config = mock.Mock(accounts_dir=tempfile.mkdtemp()) # Registration self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212') - reg = messages2.Registration( + reg = messages.Registration( contact=self.contact, key=KEY.public(), recovery_token='t') - self.regr = messages2.RegistrationResource( + self.regr = messages.RegistrationResource( body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1', new_authzr_uri='https://www.letsencrypt-demo.org/acme/new-reg', terms_of_service='https://www.letsencrypt-demo.org/tos') # Authorization authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1' - challb = messages2.ChallengeBody( - uri=(authzr_uri + '/1'), status=messages2.STATUS_VALID, + challb = messages.ChallengeBody( + uri=(authzr_uri + '/1'), status=messages.STATUS_VALID, chall=challenges.DNS(token='foo')) - self.challr = messages2.ChallengeResource( + self.challr = messages.ChallengeResource( body=challb, authzr_uri=authzr_uri) - self.authz = messages2.Authorization( - identifier=messages2.Identifier( - typ=messages2.IDENTIFIER_FQDN, value='example.com'), + self.authz = messages.Authorization( + identifier=messages.Identifier( + typ=messages.IDENTIFIER_FQDN, value='example.com'), challenges=(challb,), combinations=None) - self.authzr = messages2.AuthorizationResource( + self.authzr = messages.AuthorizationResource( body=self.authz, uri=authzr_uri, new_cert_uri='https://www.letsencrypt-demo.org/acme/new-cert') # Request issuance - self.certr = messages2.CertificateResource( + self.certr = messages.CertificateResource( body=CERT, authzrs=(self.authzr,), uri='https://www.letsencrypt-demo.org/acme/cert/1', cert_chain_uri='https://www.letsencrypt-demo.org/ca') @@ -131,11 +131,11 @@ class NetworkTest(unittest.TestCase): def test_check_response_not_ok_jobj_error(self): self.response.ok = False - self.response.json.return_value = messages2.Error( + self.response.json.return_value = messages.Error( detail='foo', typ='serverInternal', title='some title').to_json() # pylint: disable=protected-access self.assertRaises( - messages2.Error, self.net._check_response, self.response) + messages.Error, self.net._check_response, self.response) def test_check_response_not_ok_no_jobj(self): self.response.ok = False @@ -462,7 +462,7 @@ class NetworkTest(unittest.TestCase): if not authzr.retries: # no more retries done = mock.MagicMock(uri=authzr.uri, times=authzr.times) - done.body.status = messages2.STATUS_VALID + done.body.status = messages.STATUS_VALID return done, [] # response (2nd result tuple element) is reduced to only @@ -550,7 +550,7 @@ class NetworkTest(unittest.TestCase): def test_revoke(self): self._mock_post_get() - self.net.revoke(self.certr, when=messages2.Revocation.NOW) + self.net.revoke(self.certr, when=messages.Revocation.NOW) self.post.assert_called_once_with(self.certr.uri, mock.ANY) def test_revoke_bad_status_raises_error(self): diff --git a/letsencrypt/tests/proof_of_possession_test.py b/letsencrypt/tests/proof_of_possession_test.py index 0a044810c..415e4caed 100644 --- a/letsencrypt/tests/proof_of_possession_test.py +++ b/letsencrypt/tests/proof_of_possession_test.py @@ -8,7 +8,7 @@ import mock from acme import challenges from acme import jose -from acme import messages2 +from acme import messages from letsencrypt import achallenges from letsencrypt import proof_of_possession @@ -48,8 +48,8 @@ class ProofOfPossessionTest(unittest.TestCase): issuers=(), authorized_for=()) chall = challenges.ProofOfPossession( alg=jose.RS256, nonce='zczv4HMLVe_0kimJ25Juig', hints=hints) - challb = messages2.ChallengeBody( - chall=chall, uri="http://example", status=messages2.STATUS_PENDING) + challb = messages.ChallengeBody( + chall=chall, uri="http://example", status=messages.STATUS_PENDING) self.achall = achallenges.ProofOfPossession( challb=challb, domain="example.com") @@ -60,8 +60,8 @@ class ProofOfPossessionTest(unittest.TestCase): issuers=(), authorized_for=()) chall = challenges.ProofOfPossession( alg=jose.HS512, nonce='zczv4HMLVe_0kimJ25Juig', hints=hints) - challb = messages2.ChallengeBody( - chall=chall, uri="http://example", status=messages2.STATUS_PENDING) + challb = messages.ChallengeBody( + chall=chall, uri="http://example", status=messages.STATUS_PENDING) self.achall = achallenges.ProofOfPossession( challb=challb, domain="example.com") self.assertEqual(self.proof_of_pos.perform(self.achall), None) diff --git a/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt_nginx/tests/configurator_test.py index 82b80b9d2..94a0901b5 100644 --- a/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt_nginx/tests/configurator_test.py @@ -5,7 +5,7 @@ import unittest import mock from acme import challenges -from acme import messages2 +from acme import messages from letsencrypt import achallenges from letsencrypt import errors @@ -165,20 +165,20 @@ class NginxConfiguratorTest(util.NginxTest): # Note: As more challenges are offered this will have to be expanded auth_key = le_util.Key(self.rsa256_file, self.rsa256_pem) achall1 = achallenges.DVSNI( - challb=messages2.ChallengeBody( + challb=messages.ChallengeBody( chall=challenges.DVSNI( r="foo", nonce="bar"), uri="https://ca.org/chall0_uri", - status=messages2.Status("pending"), + status=messages.Status("pending"), ), domain="localhost", key=auth_key) achall2 = achallenges.DVSNI( - challb=messages2.ChallengeBody( + challb=messages.ChallengeBody( chall=challenges.DVSNI( r="abc", nonce="def"), uri="https://ca.org/chall1_uri", - status=messages2.Status("pending"), + status=messages.Status("pending"), ), domain="example.com", key=auth_key) dvsni_ret_val = [ From b4d63cbbb3e2823ab6b422b3c08b70f310fe4a51 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 18 Jun 2015 10:38:20 +0000 Subject: [PATCH 53/70] Move letsencrypt.network to acme.client. --- acme/client.py | 559 +++++++++++++++++++++++++++++ acme/client_test.py | 530 ++++++++++++++++++++++++++++ acme/errors.py | 7 + acme/jose/testdata/README | 9 +- acme/jose/testdata/cert.der | Bin 0 -> 377 bytes acme/jose/testdata/csr.der | Bin 0 -> 210 bytes acme/jose/testdata/csr2.pem | 10 - docs/pkgs/acme/index.rst | 7 + letsencrypt/errors.py | 8 - letsencrypt/network.py | 568 +----------------------------- letsencrypt/tests/network_test.py | 515 +-------------------------- 11 files changed, 1115 insertions(+), 1098 deletions(-) create mode 100644 acme/client.py create mode 100644 acme/client_test.py create mode 100644 acme/jose/testdata/cert.der create mode 100644 acme/jose/testdata/csr.der delete mode 100644 acme/jose/testdata/csr2.pem diff --git a/acme/client.py b/acme/client.py new file mode 100644 index 000000000..c0eda0fa3 --- /dev/null +++ b/acme/client.py @@ -0,0 +1,559 @@ +"""ACME client API.""" +import datetime +import heapq +import httplib +import logging +import time + +import M2Crypto +import requests +import werkzeug + +from acme import errors +from acme import jose +from acme import jws +from acme import messages + + +# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning +requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() + + +class Client(object): + """ACME client. + + .. todo:: + Clean up raised error types hierarchy, document, and handle (wrap) + instances of `.DeserializationError` raised in `from_json()`. + + :ivar str new_reg_uri: Location of new-reg + :ivar key: `.JWK` (private) + :ivar alg: `.JWASignature` + :ivar bool verify_ssl: Verify SSL certificates? + + """ + DER_CONTENT_TYPE = 'application/pkix-cert' + JSON_CONTENT_TYPE = 'application/json' + JSON_ERROR_CONTENT_TYPE = 'application/problem+json' + REPLAY_NONCE_HEADER = 'Replay-Nonce' + + def __init__(self, new_reg_uri, key, alg=jose.RS256, verify_ssl=True): + self.new_reg_uri = new_reg_uri + self.key = key + self.alg = alg + self.verify_ssl = verify_ssl + self._nonces = set() + + def _wrap_in_jws(self, obj, nonce): + """Wrap `JSONDeSerializable` object in JWS. + + .. todo:: Implement ``acmePath``. + + :param JSONDeSerializable obj: + :rtype: `.JWS` + + """ + dumps = obj.json_dumps() + logging.debug('Serialized JSON: %s', dumps) + return jws.JWS.sign( + payload=dumps, key=self.key, alg=self.alg, nonce=nonce).json_dumps() + + @classmethod + def _check_response(cls, response, content_type=None): + """Check response content and its type. + + .. note:: + Checking is not strict: wrong server response ``Content-Type`` + HTTP header is ignored if response is an expected JSON object + (c.f. Boulder #56). + + :param str content_type: Expected Content-Type response header. + If JSON is expected and not present in server response, this + function will raise an error. Otherwise, wrong Content-Type + is ignored, but logged. + + :raises .messages.Error: If server response body + carries HTTP Problem (draft-ietf-appsawg-http-problem-00). + :raises .ClientError: In case of other networking errors. + + """ + response_ct = response.headers.get('Content-Type') + try: + # TODO: response.json() is called twice, once here, and + # once in _get and _post clients + jobj = response.json() + except ValueError as error: + jobj = None + + if not response.ok: + if jobj is not None: + if response_ct != cls.JSON_ERROR_CONTENT_TYPE: + logging.debug( + 'Ignoring wrong Content-Type (%r) for JSON Error', + response_ct) + try: + logging.error("Error: %s", jobj) + logging.error("Response from server: %s", response.content) + raise messages.Error.from_json(jobj) + except jose.DeserializationError as error: + # Couldn't deserialize JSON object + raise errors.ClientError((response, error)) + else: + # response is not JSON object + raise errors.ClientError(response) + else: + if jobj is not None and response_ct != cls.JSON_CONTENT_TYPE: + logging.debug( + 'Ignoring wrong Content-Type (%r) for JSON decodable ' + 'response', response_ct) + + if content_type == cls.JSON_CONTENT_TYPE and jobj is None: + raise errors.ClientError( + 'Unexpected response Content-Type: {0}'.format(response_ct)) + + def _get(self, uri, content_type=JSON_CONTENT_TYPE, **kwargs): + """Send GET request. + + :raises .ClientError: + + :returns: HTTP Response + :rtype: `requests.Response` + + """ + logging.debug('Sending GET request to %s', uri) + kwargs.setdefault('verify', self.verify_ssl) + try: + response = requests.get(uri, **kwargs) + except requests.exceptions.RequestException as error: + raise errors.ClientError(error) + self._check_response(response, content_type=content_type) + return response + + def _add_nonce(self, response): + if self.REPLAY_NONCE_HEADER in response.headers: + nonce = response.headers[self.REPLAY_NONCE_HEADER] + error = jws.Header.validate_nonce(nonce) + if error is None: + logging.debug('Storing nonce: %r', nonce) + self._nonces.add(nonce) + else: + raise errors.ClientError('Invalid nonce ({0}): {1}'.format( + nonce, error)) + else: + raise errors.ClientError( + 'Server {0} response did not include a replay nonce'.format( + response.request.method)) + + def _get_nonce(self, uri): + if not self._nonces: + logging.debug('Requesting fresh nonce by sending HEAD to %s', uri) + self._add_nonce(requests.head(uri)) + return self._nonces.pop() + + def _post(self, uri, obj, content_type=JSON_CONTENT_TYPE, **kwargs): + """Send POST data. + + :param JSONDeSerializable obj: Will be wrapped in JWS. + :param str content_type: Expected ``Content-Type``, fails if not set. + + :raises acme.messages.ClientError: + + :returns: HTTP Response + :rtype: `requests.Response` + + """ + data = self._wrap_in_jws(obj, self._get_nonce(uri)) + logging.debug('Sending POST data to %s: %s', uri, data) + kwargs.setdefault('verify', self.verify_ssl) + try: + response = requests.post(uri, data=data, **kwargs) + except requests.exceptions.RequestException as error: + raise errors.ClientError(error) + logging.debug('Received response %s: %r', response, response.text) + + self._add_nonce(response) + self._check_response(response, content_type=content_type) + return response + + @classmethod + def _regr_from_response(cls, response, uri=None, new_authzr_uri=None, + terms_of_service=None): + terms_of_service = ( + response.links['terms-of-service']['url'] + if 'terms-of-service' in response.links else terms_of_service) + + if new_authzr_uri is None: + try: + new_authzr_uri = response.links['next']['url'] + except KeyError: + raise errors.ClientError('"next" link missing') + + return messages.RegistrationResource( + body=messages.Registration.from_json(response.json()), + uri=response.headers.get('Location', uri), + new_authzr_uri=new_authzr_uri, + terms_of_service=terms_of_service) + + def register(self, contact=messages.Registration._fields[ + 'contact'].default): + """Register. + + :param contact: Contact list, as accepted by `.Registration` + :type contact: `tuple` + + :returns: Registration Resource. + :rtype: `.RegistrationResource` + + :raises .UnexpectedUpdate: + + """ + new_reg = messages.Registration(contact=contact) + + response = self._post(self.new_reg_uri, new_reg) + assert response.status_code == httplib.CREATED # TODO: handle errors + + regr = self._regr_from_response(response) + if regr.body.key != self.key.public() or regr.body.contact != contact: + raise errors.UnexpectedUpdate(regr) + + return regr + + def update_registration(self, regr): + """Update registration. + + :pram regr: Registration Resource. + :type regr: `.RegistrationResource` + + :returns: Updated Registration Resource. + :rtype: `.RegistrationResource` + + """ + response = self._post(regr.uri, regr.body) + + # TODO: Boulder returns httplib.ACCEPTED + #assert response.status_code == httplib.OK + + # TODO: Boulder does not set Location or Link on update + # (c.f. acme-spec #94) + + updated_regr = self._regr_from_response( + response, uri=regr.uri, new_authzr_uri=regr.new_authzr_uri, + terms_of_service=regr.terms_of_service) + if updated_regr != regr: + raise errors.UnexpectedUpdate(regr) + return updated_regr + + def agree_to_tos(self, regr): + """Agree to the terms-of-service. + + Agree to the terms-of-service in a Registration Resource. + + :param regr: Registration Resource. + :type regr: `.RegistrationResource` + + :returns: Updated Registration Resource. + :rtype: `.RegistrationResource` + + """ + return self.update_registration( + regr.update(body=regr.body.update(agreement=regr.terms_of_service))) + + def _authzr_from_response(self, response, identifier, + uri=None, new_cert_uri=None): + # pylint: disable=no-self-use + if new_cert_uri is None: + try: + new_cert_uri = response.links['next']['url'] + except KeyError: + raise errors.ClientError('"next" link missing') + + authzr = messages.AuthorizationResource( + body=messages.Authorization.from_json(response.json()), + uri=response.headers.get('Location', uri), + new_cert_uri=new_cert_uri) + if authzr.body.identifier != identifier: + raise errors.UnexpectedUpdate(authzr) + return authzr + + def request_challenges(self, identifier, new_authzr_uri): + """Request challenges. + + :param identifier: Identifier to be challenged. + :type identifier: `.messages.Identifier` + + :param str new_authzr_uri: new-authorization URI + + :returns: Authorization Resource. + :rtype: `.AuthorizationResource` + + """ + new_authz = messages.Authorization(identifier=identifier) + response = self._post(new_authzr_uri, new_authz) + assert response.status_code == httplib.CREATED # TODO: handle errors + return self._authzr_from_response(response, identifier) + + def request_domain_challenges(self, domain, new_authz_uri): + """Request challenges for domain names. + + This is simply a convenience function that wraps around + `request_challenges`, but works with domain names instead of + generic identifiers. + + :param str domain: Domain name to be challenged. + :param str new_authzr_uri: new-authorization URI + + :returns: Authorization Resource. + :rtype: `.AuthorizationResource` + + """ + return self.request_challenges(messages.Identifier( + typ=messages.IDENTIFIER_FQDN, value=domain), new_authz_uri) + + def answer_challenge(self, challb, response): + """Answer challenge. + + :param challb: Challenge Resource body. + :type challb: `.ChallengeBody` + + :param response: Corresponding Challenge response + :type response: `.challenges.ChallengeResponse` + + :returns: Challenge Resource with updated body. + :rtype: `.ChallengeResource` + + :raises errors.UnexpectedUpdate: + + """ + response = self._post(challb.uri, response) + try: + authzr_uri = response.links['up']['url'] + except KeyError: + raise errors.ClientError('"up" Link header missing') + challr = messages.ChallengeResource( + authzr_uri=authzr_uri, + body=messages.ChallengeBody.from_json(response.json())) + # TODO: check that challr.uri == response.headers['Location']? + if challr.uri != challb.uri: + raise errors.UnexpectedUpdate(challr.uri) + return challr + + @classmethod + def retry_after(cls, response, default): + """Compute next `poll` time based on response ``Retry-After`` header. + + :param response: Response from `poll`. + :type response: `requests.Response` + + :param int default: Default value (in seconds), used when + ``Retry-After`` header is not present or invalid. + + :returns: Time point when next `poll` should be performed. + :rtype: `datetime.datetime` + + """ + retry_after = response.headers.get('Retry-After', str(default)) + try: + seconds = int(retry_after) + except ValueError: + # pylint: disable=no-member + decoded = werkzeug.parse_date(retry_after) # RFC1123 + if decoded is None: + seconds = default + else: + return decoded + + return datetime.datetime.now() + datetime.timedelta(seconds=seconds) + + def poll(self, authzr): + """Poll Authorization Resource for status. + + :param authzr: Authorization Resource + :type authzr: `.AuthorizationResource` + + :returns: Updated Authorization Resource and HTTP response. + + :rtype: (`.AuthorizationResource`, `requests.Response`) + + """ + response = self._get(authzr.uri) + updated_authzr = self._authzr_from_response( + response, authzr.body.identifier, authzr.uri, authzr.new_cert_uri) + # TODO: check and raise UnexpectedUpdate + return updated_authzr, response + + def request_issuance(self, csr, authzrs): + """Request issuance. + + :param csr: CSR + :type csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509` + + :param authzrs: `list` of `.AuthorizationResource` + + :returns: Issued certificate + :rtype: `.messages.CertificateResource` + + """ + assert authzrs, "Authorizations list is empty" + logging.debug("Requesting issuance...") + + # TODO: assert len(authzrs) == number of SANs + req = messages.CertificateRequest( + csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs)) + + content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument + response = self._post( + authzrs[0].new_cert_uri, # TODO: acme-spec #90 + req, + content_type=content_type, + headers={'Accept': content_type}) + + cert_chain_uri = response.links.get('up', {}).get('url') + + try: + uri = response.headers['Location'] + except KeyError: + raise errors.ClientError('"Location" Header missing') + + return messages.CertificateResource( + uri=uri, authzrs=authzrs, cert_chain_uri=cert_chain_uri, + body=jose.ComparableX509( + M2Crypto.X509.load_cert_der_string(response.content))) + + def poll_and_request_issuance(self, csr, authzrs, mintime=5): + """Poll and request issuance. + + This function polls all provided Authorization Resource URIs + until all challenges are valid, respecting ``Retry-After`` HTTP + headers, and then calls `request_issuance`. + + .. todo:: add `max_attempts` or `timeout` + + :param csr: CSR. + :type csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509` + + :param authzrs: `list` of `.AuthorizationResource` + + :param int mintime: Minimum time before next attempt, used if + ``Retry-After`` is not present in the response. + + :returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is + the issued certificate (`.messages.CertificateResource.), + and ``updated_authzrs`` is a `tuple` consisting of updated + Authorization Resources (`.AuthorizationResource`) as + present in the responses from server, and in the same order + as the input ``authzrs``. + :rtype: `tuple` + + """ + # priority queue with datetime (based on Retry-After) as key, + # and original Authorization Resource as value + waiting = [(datetime.datetime.now(), authzr) for authzr in authzrs] + # mapping between original Authorization Resource and the most + # recently updated one + updated = dict((authzr, authzr) for authzr in authzrs) + + while waiting: + # find the smallest Retry-After, and sleep if necessary + when, authzr = heapq.heappop(waiting) + now = datetime.datetime.now() + if when > now: + seconds = (when - now).seconds + logging.debug('Sleeping for %d seconds', seconds) + time.sleep(seconds) + + # Note that we poll with the latest updated Authorization + # URI, which might have a different URI than initial one + updated_authzr, response = self.poll(updated[authzr]) + updated[authzr] = updated_authzr + + if updated_authzr.body.status != messages.STATUS_VALID: + # push back to the priority queue, with updated retry_after + heapq.heappush(waiting, (self.retry_after( + response, default=mintime), authzr)) + + updated_authzrs = tuple(updated[authzr] for authzr in authzrs) + return self.request_issuance(csr, updated_authzrs), updated_authzrs + + def _get_cert(self, uri): + """Returns certificate from URI. + + :param str uri: URI of certificate + + :returns: tuple of the form + (response, :class:`acme.jose.ComparableX509`) + :rtype: tuple + + """ + content_type = self.DER_CONTENT_TYPE # TODO: make it a param + response = self._get(uri, headers={'Accept': content_type}, + content_type=content_type) + return response, jose.ComparableX509( + M2Crypto.X509.load_cert_der_string(response.content)) + + def check_cert(self, certr): + """Check for new cert. + + :param certr: Certificate Resource + :type certr: `.CertificateResource` + + :returns: Updated Certificate Resource. + :rtype: `.CertificateResource` + + """ + # TODO: acme-spec 5.1 table action should be renamed to + # "refresh cert", and this method integrated with self.refresh + response, cert = self._get_cert(certr.uri) + if 'Location' not in response.headers: + raise errors.ClientError('Location header missing') + if response.headers['Location'] != certr.uri: + raise errors.UnexpectedUpdate(response.text) + return certr.update(body=cert) + + def refresh(self, certr): + """Refresh certificate. + + :param certr: Certificate Resource + :type certr: `.CertificateResource` + + :returns: Updated Certificate Resource. + :rtype: `.CertificateResource` + + """ + # TODO: If a client sends a refresh request and the server is + # not willing to refresh the certificate, the server MUST + # respond with status code 403 (Forbidden) + return self.check_cert(certr) + + def fetch_chain(self, certr): + """Fetch chain for certificate. + + :param certr: Certificate Resource + :type certr: `.CertificateResource` + + :returns: Certificate chain, or `None` if no "up" Link was provided. + :rtype: `M2Crypto.X509.X509` wrapped in `.ComparableX509` + + """ + if certr.cert_chain_uri is not None: + return self._get_cert(certr.cert_chain_uri)[1] + else: + return None + + def revoke(self, certr, when=messages.Revocation.NOW): + """Revoke certificate. + + :param certr: Certificate Resource + :type certr: `.CertificateResource` + + :param when: When should the revocation take place? Takes + the same values as `.messages.Revocation.revoke`. + + :raises .ClientError: If revocation is unsuccessful. + + """ + rev = messages.Revocation(revoke=when, authorizations=tuple( + authzr.uri for authzr in certr.authzrs)) + response = self._post(certr.uri, rev) + if response.status_code != httplib.OK: + raise errors.ClientError( + 'Successful revocation must return HTTP OK status') diff --git a/acme/client_test.py b/acme/client_test.py new file mode 100644 index 000000000..5e4cc1720 --- /dev/null +++ b/acme/client_test.py @@ -0,0 +1,530 @@ +"""Tests for acme.client.""" +import datetime +import httplib +import os +import pkg_resources +import unittest + +import M2Crypto +import mock +import requests + +from acme import challenges +from acme import errors +from acme import jose +from acme import jws as acme_jws +from acme import messages + + +CERT = jose.ComparableX509(M2Crypto.X509.load_cert_string( + pkg_resources.resource_string( + 'acme.jose', os.path.join('testdata', 'cert.der')), + M2Crypto.X509.FORMAT_DER)) +CSR = jose.ComparableX509(M2Crypto.X509.load_request_string( + pkg_resources.resource_string( + 'acme.jose', os.path.join('testdata', 'csr.der')), + M2Crypto.X509.FORMAT_DER)) +KEY = jose.JWKRSA.load(pkg_resources.resource_string( + 'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))) +KEY2 = jose.JWKRSA.load(pkg_resources.resource_string( + 'acme.jose', os.path.join('testdata', 'rsa256_key.pem'))) + + +class ClientTest(unittest.TestCase): + """Tests for acme.client.Client.""" + + # pylint: disable=too-many-instance-attributes,too-many-public-methods + + def setUp(self): + self.verify_ssl = mock.MagicMock() + self.wrap_in_jws = mock.MagicMock(return_value=mock.sentinel.wrapped) + + from acme.client import Client + self.net = Client( + new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg', + key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl) + self.nonce = jose.b64encode('Nonce') + self.net._nonces.add(self.nonce) # pylint: disable=protected-access + + self.response = mock.MagicMock(ok=True, status_code=httplib.OK) + self.response.headers = {} + self.response.links = {} + + self.post = mock.MagicMock(return_value=self.response) + self.get = mock.MagicMock(return_value=self.response) + + self.identifier = messages.Identifier( + typ=messages.IDENTIFIER_FQDN, value='example.com') + + # Registration + self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212') + reg = messages.Registration( + contact=self.contact, key=KEY.public(), recovery_token='t') + self.regr = messages.RegistrationResource( + body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1', + new_authzr_uri='https://www.letsencrypt-demo.org/acme/new-reg', + terms_of_service='https://www.letsencrypt-demo.org/tos') + + # Authorization + authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1' + challb = messages.ChallengeBody( + uri=(authzr_uri + '/1'), status=messages.STATUS_VALID, + chall=challenges.DNS(token='foo')) + self.challr = messages.ChallengeResource( + body=challb, authzr_uri=authzr_uri) + self.authz = messages.Authorization( + identifier=messages.Identifier( + typ=messages.IDENTIFIER_FQDN, value='example.com'), + challenges=(challb,), combinations=None) + self.authzr = messages.AuthorizationResource( + body=self.authz, uri=authzr_uri, + new_cert_uri='https://www.letsencrypt-demo.org/acme/new-cert') + + # Request issuance + self.certr = messages.CertificateResource( + body=CERT, authzrs=(self.authzr,), + uri='https://www.letsencrypt-demo.org/acme/cert/1', + cert_chain_uri='https://www.letsencrypt-demo.org/ca') + + def _mock_post_get(self): + # pylint: disable=protected-access + self.net._post = self.post + self.net._get = self.get + + def test_init(self): + self.assertTrue(self.net.verify_ssl is self.verify_ssl) + + def test_wrap_in_jws(self): + class MockJSONDeSerializable(jose.JSONDeSerializable): + # pylint: disable=missing-docstring + def __init__(self, value): + self.value = value + def to_partial_json(self): + return self.value + @classmethod + def from_json(cls, value): + pass # pragma: no cover + # pylint: disable=protected-access + jws_dump = self.net._wrap_in_jws( + MockJSONDeSerializable('foo'), nonce='Tg') + jws = acme_jws.JWS.json_loads(jws_dump) + self.assertEqual(jws.payload, '"foo"') + self.assertEqual(jws.signature.combined.nonce, 'Tg') + # TODO: check that nonce is in protected header + + def test_check_response_not_ok_jobj_no_error(self): + self.response.ok = False + self.response.json.return_value = {} + # pylint: disable=protected-access + self.assertRaises( + errors.ClientError, self.net._check_response, self.response) + + def test_check_response_not_ok_jobj_error(self): + self.response.ok = False + self.response.json.return_value = messages.Error( + detail='foo', typ='serverInternal', title='some title').to_json() + # pylint: disable=protected-access + self.assertRaises( + messages.Error, self.net._check_response, self.response) + + def test_check_response_not_ok_no_jobj(self): + self.response.ok = False + self.response.json.side_effect = ValueError + # pylint: disable=protected-access + self.assertRaises( + errors.ClientError, self.net._check_response, self.response) + + def test_check_response_ok_no_jobj_ct_required(self): + self.response.json.side_effect = ValueError + for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: + self.response.headers['Content-Type'] = response_ct + # pylint: disable=protected-access + self.assertRaises( + errors.ClientError, self.net._check_response, self.response, + content_type=self.net.JSON_CONTENT_TYPE) + + def test_check_response_ok_no_jobj_no_ct(self): + self.response.json.side_effect = ValueError + for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: + self.response.headers['Content-Type'] = response_ct + # pylint: disable=protected-access + self.net._check_response(self.response) + + def test_check_response_jobj(self): + self.response.json.return_value = {} + for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: + self.response.headers['Content-Type'] = response_ct + # pylint: disable=protected-access + self.net._check_response(self.response) + + @mock.patch('acme.client.requests') + def test_get_requests_error_passthrough(self, requests_mock): + requests_mock.exceptions = requests.exceptions + requests_mock.get.side_effect = requests.exceptions.RequestException + # pylint: disable=protected-access + self.assertRaises(errors.ClientError, self.net._get, 'uri') + + @mock.patch('acme.client.requests') + def test_get(self, requests_mock): + # pylint: disable=protected-access + self.net._check_response = mock.MagicMock() + self.net._get('uri', content_type='ct') + self.net._check_response.assert_called_once_with( + requests_mock.get('uri'), content_type='ct') + + def _mock_wrap_in_jws(self): + # pylint: disable=protected-access + self.net._wrap_in_jws = self.wrap_in_jws + + @mock.patch('acme.client.requests') + def test_post_requests_error_passthrough(self, requests_mock): + requests_mock.exceptions = requests.exceptions + requests_mock.post.side_effect = requests.exceptions.RequestException + # pylint: disable=protected-access + self._mock_wrap_in_jws() + self.assertRaises( + errors.ClientError, self.net._post, 'uri', mock.sentinel.obj) + + @mock.patch('acme.client.requests') + def test_post(self, requests_mock): + # pylint: disable=protected-access + self.net._check_response = mock.MagicMock() + self._mock_wrap_in_jws() + requests_mock.post().headers = { + self.net.REPLAY_NONCE_HEADER: self.nonce} + self.net._post('uri', mock.sentinel.obj, content_type='ct') + self.net._check_response.assert_called_once_with( + requests_mock.post('uri', mock.sentinel.wrapped), content_type='ct') + + @mock.patch('acme.client.requests') + def test_post_replay_nonce_handling(self, requests_mock): + # pylint: disable=protected-access + self.net._check_response = mock.MagicMock() + self._mock_wrap_in_jws() + + self.net._nonces.clear() + self.assertRaises( + errors.ClientError, self.net._post, 'uri', mock.sentinel.obj) + + nonce2 = jose.b64encode('Nonce2') + requests_mock.head('uri').headers = { + self.net.REPLAY_NONCE_HEADER: nonce2} + requests_mock.post('uri').headers = { + self.net.REPLAY_NONCE_HEADER: self.nonce} + + self.net._post('uri', mock.sentinel.obj) + + requests_mock.head.assert_called_with('uri') + self.wrap_in_jws.assert_called_once_with(mock.sentinel.obj, nonce2) + self.assertEqual(self.net._nonces, set([self.nonce])) + + # wrong nonce + requests_mock.post('uri').headers = {self.net.REPLAY_NONCE_HEADER: 'F'} + self.assertRaises( + errors.ClientError, self.net._post, 'uri', mock.sentinel.obj) + + @mock.patch('acme.client.requests') + def test_get_post_verify_ssl(self, requests_mock): + # pylint: disable=protected-access + self._mock_wrap_in_jws() + self.net._check_response = mock.MagicMock() + + for verify_ssl in [True, False]: + self.net.verify_ssl = verify_ssl + self.net._get('uri') + self.net._nonces.add('N') + requests_mock.post().headers = { + self.net.REPLAY_NONCE_HEADER: self.nonce} + self.net._post('uri', mock.sentinel.obj) + requests_mock.get.assert_called_once_with('uri', verify=verify_ssl) + requests_mock.post.assert_called_with( + 'uri', data=mock.sentinel.wrapped, verify=verify_ssl) + requests_mock.reset_mock() + + def test_register(self): + self.response.status_code = httplib.CREATED + self.response.json.return_value = self.regr.body.to_json() + self.response.headers['Location'] = self.regr.uri + self.response.links.update({ + 'next': {'url': self.regr.new_authzr_uri}, + 'terms-of-service': {'url': self.regr.terms_of_service}, + }) + + self._mock_post_get() + self.assertEqual(self.regr, self.net.register(self.contact)) + # TODO: test POST call arguments + + # TODO: split here and separate test + reg_wrong_key = self.regr.body.update(key=KEY2.public()) + self.response.json.return_value = reg_wrong_key.to_json() + self.assertRaises( + errors.UnexpectedUpdate, self.net.register, self.contact) + + def test_register_missing_next(self): + self.response.status_code = httplib.CREATED + self._mock_post_get() + self.assertRaises( + errors.ClientError, self.net.register, self.regr.body) + + def test_update_registration(self): + self.response.headers['Location'] = self.regr.uri + self.response.json.return_value = self.regr.body.to_json() + self._mock_post_get() + self.assertEqual(self.regr, self.net.update_registration(self.regr)) + + # TODO: split here and separate test + self.response.json.return_value = self.regr.body.update( + contact=()).to_json() + self.assertRaises( + errors.UnexpectedUpdate, self.net.update_registration, self.regr) + + def test_agree_to_tos(self): + self.net.update_registration = mock.Mock() + self.net.agree_to_tos(self.regr) + regr = self.net.update_registration.call_args[0][0] + self.assertEqual(self.regr.terms_of_service, regr.body.agreement) + + def test_request_challenges(self): + self.response.status_code = httplib.CREATED + self.response.headers['Location'] = self.authzr.uri + self.response.json.return_value = self.authz.to_json() + self.response.links = { + 'next': {'url': self.authzr.new_cert_uri}, + } + + self._mock_post_get() + self.net.request_challenges(self.identifier, self.authzr.uri) + # TODO: test POST call arguments + + # TODO: split here and separate test + self.response.json.return_value = self.authz.update( + identifier=self.identifier.update(value='foo')).to_json() + self.assertRaises(errors.UnexpectedUpdate, self.net.request_challenges, + self.identifier, self.authzr.uri) + + def test_request_challenges_missing_next(self): + self.response.status_code = httplib.CREATED + self._mock_post_get() + self.assertRaises( + errors.ClientError, self.net.request_challenges, + self.identifier, self.regr) + + def test_request_domain_challenges(self): + self.net.request_challenges = mock.MagicMock() + self.assertEqual( + self.net.request_challenges(self.identifier), + self.net.request_domain_challenges('example.com', self.regr)) + + def test_answer_challenge(self): + self.response.links['up'] = {'url': self.challr.authzr_uri} + self.response.json.return_value = self.challr.body.to_json() + + chall_response = challenges.DNSResponse() + + self._mock_post_get() + self.net.answer_challenge(self.challr.body, chall_response) + + # TODO: split here and separate test + self.assertRaises(errors.UnexpectedUpdate, self.net.answer_challenge, + self.challr.body.update(uri='foo'), chall_response) + + def test_answer_challenge_missing_next(self): + self._mock_post_get() + self.assertRaises(errors.ClientError, self.net.answer_challenge, + self.challr.body, challenges.DNSResponse()) + + def test_retry_after_date(self): + self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT' + self.assertEqual( + datetime.datetime(1999, 12, 31, 23, 59, 59), + self.net.retry_after(response=self.response, default=10)) + + @mock.patch('acme.client.datetime') + def test_retry_after_invalid(self, dt_mock): + dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) + dt_mock.timedelta = datetime.timedelta + + self.response.headers['Retry-After'] = 'foooo' + self.assertEqual( + datetime.datetime(2015, 3, 27, 0, 0, 10), + self.net.retry_after(response=self.response, default=10)) + + @mock.patch('acme.client.datetime') + def test_retry_after_seconds(self, dt_mock): + dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) + dt_mock.timedelta = datetime.timedelta + + self.response.headers['Retry-After'] = '50' + self.assertEqual( + datetime.datetime(2015, 3, 27, 0, 0, 50), + self.net.retry_after(response=self.response, default=10)) + + @mock.patch('acme.client.datetime') + def test_retry_after_missing(self, dt_mock): + dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) + dt_mock.timedelta = datetime.timedelta + + self.assertEqual( + datetime.datetime(2015, 3, 27, 0, 0, 10), + self.net.retry_after(response=self.response, default=10)) + + def test_poll(self): + self.response.json.return_value = self.authzr.body.to_json() + self._mock_post_get() + self.assertEqual((self.authzr, self.response), + self.net.poll(self.authzr)) + + # TODO: split here and separate test + self.response.json.return_value = self.authz.update( + identifier=self.identifier.update(value='foo')).to_json() + self.assertRaises(errors.UnexpectedUpdate, self.net.poll, self.authzr) + + def test_request_issuance(self): + self.response.content = CERT.as_der() + self.response.headers['Location'] = self.certr.uri + self.response.links['up'] = {'url': self.certr.cert_chain_uri} + self._mock_post_get() + self.assertEqual( + self.certr, self.net.request_issuance(CSR, (self.authzr,))) + # TODO: check POST args + + def test_request_issuance_missing_up(self): + self.response.content = CERT.as_der() + self.response.headers['Location'] = self.certr.uri + self._mock_post_get() + self.assertEqual( + self.certr.update(cert_chain_uri=None), + self.net.request_issuance(CSR, (self.authzr,))) + + def test_request_issuance_missing_location(self): + self._mock_post_get() + self.assertRaises( + errors.ClientError, self.net.request_issuance, + CSR, (self.authzr,)) + + @mock.patch('acme.client.datetime') + @mock.patch('acme.client.time') + def test_poll_and_request_issuance(self, time_mock, dt_mock): + # clock.dt | pylint: disable=no-member + clock = mock.MagicMock(dt=datetime.datetime(2015, 3, 27)) + + def sleep(seconds): + """increment clock""" + clock.dt += datetime.timedelta(seconds=seconds) + time_mock.sleep.side_effect = sleep + + def now(): + """return current clock value""" + return clock.dt + dt_mock.datetime.now.side_effect = now + dt_mock.timedelta = datetime.timedelta + + def poll(authzr): # pylint: disable=missing-docstring + # record poll start time based on the current clock value + authzr.times.append(clock.dt) + + # suppose it takes 2 seconds for server to produce the + # result, increment clock + clock.dt += datetime.timedelta(seconds=2) + + if not authzr.retries: # no more retries + done = mock.MagicMock(uri=authzr.uri, times=authzr.times) + done.body.status = messages.STATUS_VALID + return done, [] + + # response (2nd result tuple element) is reduced to only + # Retry-After header contents represented as integer + # seconds; authzr.retries is a list of Retry-After + # headers, head(retries) is peeled of as a current + # Retry-After header, and tail(retries) is persisted for + # later poll() calls + return (mock.MagicMock(retries=authzr.retries[1:], + uri=authzr.uri + '.', times=authzr.times), + authzr.retries[0]) + self.net.poll = mock.MagicMock(side_effect=poll) + + mintime = 7 + + def retry_after(response, default): # pylint: disable=missing-docstring + # check that poll_and_request_issuance correctly passes mintime + self.assertEqual(default, mintime) + return clock.dt + datetime.timedelta(seconds=response) + self.net.retry_after = mock.MagicMock(side_effect=retry_after) + + def request_issuance(csr, authzrs): # pylint: disable=missing-docstring + return csr, authzrs + self.net.request_issuance = mock.MagicMock(side_effect=request_issuance) + + csr = mock.MagicMock() + authzrs = ( + mock.MagicMock(uri='a', times=[], retries=(8, 20, 30)), + mock.MagicMock(uri='b', times=[], retries=(5,)), + ) + + cert, updated_authzrs = self.net.poll_and_request_issuance( + csr, authzrs, mintime=mintime) + self.assertTrue(cert[0] is csr) + self.assertTrue(cert[1] is updated_authzrs) + self.assertEqual(updated_authzrs[0].uri, 'a...') + self.assertEqual(updated_authzrs[1].uri, 'b.') + self.assertEqual(updated_authzrs[0].times, [ + datetime.datetime(2015, 3, 27), + # a is scheduled for 10, but b is polling [9..11), so it + # will be picked up as soon as b is finished, without + # additional sleeping + datetime.datetime(2015, 3, 27, 0, 0, 11), + datetime.datetime(2015, 3, 27, 0, 0, 33), + datetime.datetime(2015, 3, 27, 0, 1, 5), + ]) + self.assertEqual(updated_authzrs[1].times, [ + datetime.datetime(2015, 3, 27, 0, 0, 2), + datetime.datetime(2015, 3, 27, 0, 0, 9), + ]) + self.assertEqual(clock.dt, datetime.datetime(2015, 3, 27, 0, 1, 7)) + + def test_check_cert(self): + self.response.headers['Location'] = self.certr.uri + self.response.content = CERT.as_der() + self._mock_post_get() + self.assertEqual( + self.certr.update(body=CERT), self.net.check_cert(self.certr)) + + # TODO: split here and separate test + self.response.headers['Location'] = 'foo' + self.assertRaises( + errors.UnexpectedUpdate, self.net.check_cert, self.certr) + + def test_check_cert_missing_location(self): + self.response.content = CERT.as_der() + self._mock_post_get() + self.assertRaises(errors.ClientError, self.net.check_cert, self.certr) + + def test_refresh(self): + self.net.check_cert = mock.MagicMock() + self.assertEqual( + self.net.check_cert(self.certr), self.net.refresh(self.certr)) + + def test_fetch_chain(self): + # pylint: disable=protected-access + self.net._get_cert = mock.MagicMock() + self.net._get_cert.return_value = ("response", "certificate") + self.assertEqual(self.net._get_cert(self.certr.cert_chain_uri)[1], + self.net.fetch_chain(self.certr)) + + def test_fetch_chain_no_up_link(self): + self.assertTrue(self.net.fetch_chain(self.certr.update( + cert_chain_uri=None)) is None) + + def test_revoke(self): + self._mock_post_get() + self.net.revoke(self.certr, when=messages.Revocation.NOW) + self.post.assert_called_once_with(self.certr.uri, mock.ANY) + + def test_revoke_bad_status_raises_error(self): + self.response.status_code = httplib.METHOD_NOT_ALLOWED + self._mock_post_get() + self.assertRaises(errors.ClientError, self.net.revoke, self.certr) + + +if __name__ == '__main__': + unittest.main() # pragma: no cover diff --git a/acme/errors.py b/acme/errors.py index 957e781af..5046d7aee 100644 --- a/acme/errors.py +++ b/acme/errors.py @@ -1,8 +1,15 @@ """ACME errors.""" from acme.jose import errors as jose_errors + class Error(Exception): """Generic ACME error.""" class SchemaValidationError(jose_errors.DeserializationError): """JSON schema ACME object validation error.""" + +class ClientError(Error): + """Network error.""" + +class UnexpectedUpdate(ClientError): + """Unexpected update.""" diff --git a/acme/jose/testdata/README b/acme/jose/testdata/README index 72ec557e0..be3d8b2f7 100644 --- a/acme/jose/testdata/README +++ b/acme/jose/testdata/README @@ -4,7 +4,8 @@ The following command has been used to generate test keys: and for the CSR: - python -c from 'letsencrypt.crypto_util import make_csr; - import pkg_resources; open("csr2.pem", - "w").write(make_csr(pkg_resources.resource_string("letsencrypt.tests", - "testdata/rsa512_key.pem"), ["example2.com"])[0])' + openssl req -key rsa512_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 diff --git a/acme/jose/testdata/cert.der b/acme/jose/testdata/cert.der new file mode 100644 index 0000000000000000000000000000000000000000..5f1018505d81a50ed3239d829533deac5fcc2085 GIT binary patch literal 377 zcmXqLVk|XiVw7LN%*4pV#L2Ms(6oH-+lDa)ylk9WZ60mkc^MhGSs4t(3`Got*qB3E zn0dHUD-v@Ha#Hn@^K%X4#CZ)(4a|&;3``6RjLoCKTyr=Vr#=)57+D#Zy%`KVm>e0_ zlooFZd@FxGb01z;s66b16iPJW%*ddSVYv$pLpwieafaMs>~58{VWGc zu1@bVkOxUCvq%_-HDFi315zN&!fL?G$oL;EIG7z7c)I@!HO%vwut#mfG=7{>z}O6i?Kd^cJYN9>LqE5%h;CwZnWA40OQJj AMgRZ+ literal 0 HcmV?d00001 diff --git a/acme/jose/testdata/csr.der b/acme/jose/testdata/csr.der new file mode 100644 index 0000000000000000000000000000000000000000..adc29ff18463752b4b9ab26a0dd77d2621363725 GIT binary patch literal 210 zcmXqLJa16V#K>SEW+-AH#Ks)T!py^+T9KGrkdvyHoS$nDW5CPCsnzDu_MMlJk&%^w z*_*+@gUOL$O=u*A)ag|G9rW`hrfWw8)N9Mizmm^318OckeHsZ?f+1zL%^m z_ua)BZ+3d0>&un-HPz+C`j!&^w}+lGF*7nSE?_`5of~MnBZF(de`9^8<1$WWX~ABh zzuPx+%v!0Zd&ubDYwJ2|v1jw;{~Wg5`)^L5{K`Q6q!i`vv$n1I^5AE`uuA%MLEjl{ F*8oY6Puc(g literal 0 HcmV?d00001 diff --git a/acme/jose/testdata/csr2.pem b/acme/jose/testdata/csr2.pem deleted file mode 100644 index bd059a448..000000000 --- a/acme/jose/testdata/csr2.pem +++ /dev/null @@ -1,10 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIBXzCCAQkCAQAwejELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw -EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy -c2l0eSBvZiBNaWNoaWdhbjEVMBMGA1UEAwwMZXhhbXBsZTIuY29tMFwwDQYJKoZI -hvcNAQEBBQADSwAwSAJBAPS2EXFRNza/qpXnnBHF/CcFQ543htV+7nLAmrLrmTNH -tPXJmLlM8SJDIzv/ceAFXL110VzxFfi81lpH5E5c0TMCAwEAAaAqMCgGCSqGSIb3 -DQEJDjEbMBkwFwYDVR0RBBAwDoIMZXhhbXBsZTIuY29tMA0GCSqGSIb3DQEBCwUA -A0EAwsdL4FLIgISKV4vXFmc6QTV7CjBiP4XmPFbeN+gMFdR7QcnRyyxSpXxB0v8Z -oqYboP5LGFt9zC6/9GyjcI9/IQ== ------END CERTIFICATE REQUEST----- diff --git a/docs/pkgs/acme/index.rst b/docs/pkgs/acme/index.rst index ea0743b1e..2df2615a5 100644 --- a/docs/pkgs/acme/index.rst +++ b/docs/pkgs/acme/index.rst @@ -7,6 +7,13 @@ :members: +Client +------ + +.. automodule:: acme.client + :members: + + Messages -------- diff --git a/letsencrypt/errors.py b/letsencrypt/errors.py index f5d9f5f44..d9078dbf2 100644 --- a/letsencrypt/errors.py +++ b/letsencrypt/errors.py @@ -5,14 +5,6 @@ class LetsEncryptClientError(Exception): """Generic Let's Encrypt client error.""" -class NetworkError(LetsEncryptClientError): - """Network error.""" - - -class UnexpectedUpdate(NetworkError): - """Unexpected update.""" - - class LetsEncryptReverterError(LetsEncryptClientError): """Let's Encrypt Reverter error.""" diff --git a/letsencrypt/network.py b/letsencrypt/network.py index 6d3be1afc..0f4d9d29b 100644 --- a/letsencrypt/network.py +++ b/letsencrypt/network.py @@ -1,230 +1,15 @@ -"""Networking for ACME protocol v02.""" -import datetime -import heapq -import httplib -import logging -import time - -import M2Crypto -import requests -import werkzeug - -from acme import jose -from acme import jws as acme_jws -from acme import messages - -from letsencrypt import errors +"""Networking for ACME protocol.""" +from acme import client -# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning -requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() - - -class Network(object): - """ACME networking. - - .. todo:: - Clean up raised error types hierarchy, document, and handle (wrap) - instances of `.DeserializationError` raised in `from_json()`. - - :ivar str new_reg_uri: Location of new-reg - :ivar key: `.JWK` (private) - :ivar alg: `.JWASignature` - :ivar bool verify_ssl: Verify SSL certificates? - - """ - - # TODO: Move below to acme module? - DER_CONTENT_TYPE = 'application/pkix-cert' - JSON_CONTENT_TYPE = 'application/json' - JSON_ERROR_CONTENT_TYPE = 'application/problem+json' - REPLAY_NONCE_HEADER = 'Replay-Nonce' - - def __init__(self, new_reg_uri, key, alg=jose.RS256, verify_ssl=True): - self.new_reg_uri = new_reg_uri - self.key = key - self.alg = alg - self.verify_ssl = verify_ssl - self._nonces = set() - - def _wrap_in_jws(self, obj, nonce): - """Wrap `JSONDeSerializable` object in JWS. - - .. todo:: Implement ``acmePath``. - - :param JSONDeSerializable obj: - :rtype: `.JWS` - - """ - dumps = obj.json_dumps() - logging.debug('Serialized JSON: %s', dumps) - return acme_jws.JWS.sign( - payload=dumps, key=self.key, alg=self.alg, nonce=nonce).json_dumps() - - @classmethod - def _check_response(cls, response, content_type=None): - """Check response content and its type. - - .. note:: - Checking is not strict: wrong server response ``Content-Type`` - HTTP header is ignored if response is an expected JSON object - (c.f. Boulder #56). - - :param str content_type: Expected Content-Type response header. - If JSON is expected and not present in server response, this - function will raise an error. Otherwise, wrong Content-Type - is ignored, but logged. - - :raises letsencrypt.messages.Error: If server response body - carries HTTP Problem (draft-ietf-appsawg-http-problem-00). - :raises letsencrypt.errors.NetworkError: In case of other - networking errors. - - """ - response_ct = response.headers.get('Content-Type') - try: - # TODO: response.json() is called twice, once here, and - # once in _get and _post clients - jobj = response.json() - except ValueError as error: - jobj = None - - if not response.ok: - if jobj is not None: - if response_ct != cls.JSON_ERROR_CONTENT_TYPE: - logging.debug( - 'Ignoring wrong Content-Type (%r) for JSON Error', - response_ct) - try: - logging.error("Error: %s", jobj) - logging.error("Response from server: %s", response.content) - raise messages.Error.from_json(jobj) - except jose.DeserializationError as error: - # Couldn't deserialize JSON object - raise errors.NetworkError((response, error)) - else: - # response is not JSON object - raise errors.NetworkError(response) - else: - if jobj is not None and response_ct != cls.JSON_CONTENT_TYPE: - logging.debug( - 'Ignoring wrong Content-Type (%r) for JSON decodable ' - 'response', response_ct) - - if content_type == cls.JSON_CONTENT_TYPE and jobj is None: - raise errors.NetworkError( - 'Unexpected response Content-Type: {0}'.format(response_ct)) - - def _get(self, uri, content_type=JSON_CONTENT_TYPE, **kwargs): - """Send GET request. - - :raises letsencrypt.errors.NetworkError: - - :returns: HTTP Response - :rtype: `requests.Response` - - """ - logging.debug('Sending GET request to %s', uri) - kwargs.setdefault('verify', self.verify_ssl) - try: - response = requests.get(uri, **kwargs) - except requests.exceptions.RequestException as error: - raise errors.NetworkError(error) - self._check_response(response, content_type=content_type) - return response - - def _add_nonce(self, response): - if self.REPLAY_NONCE_HEADER in response.headers: - nonce = response.headers[self.REPLAY_NONCE_HEADER] - error = acme_jws.Header.validate_nonce(nonce) - if error is None: - logging.debug('Storing nonce: %r', nonce) - self._nonces.add(nonce) - else: - raise errors.NetworkError('Invalid nonce ({0}): {1}'.format( - nonce, error)) - else: - raise errors.NetworkError( - 'Server {0} response did not include a replay nonce'.format( - response.request.method)) - - def _get_nonce(self, uri): - if not self._nonces: - logging.debug('Requesting fresh nonce by sending HEAD to %s', uri) - self._add_nonce(requests.head(uri)) - return self._nonces.pop() - - def _post(self, uri, obj, content_type=JSON_CONTENT_TYPE, **kwargs): - """Send POST data. - - :param JSONDeSerializable obj: Will be wrapped in JWS. - :param str content_type: Expected ``Content-Type``, fails if not set. - - :raises acme.messages.NetworkError: - - :returns: HTTP Response - :rtype: `requests.Response` - - """ - data = self._wrap_in_jws(obj, self._get_nonce(uri)) - logging.debug('Sending POST data to %s: %s', uri, data) - kwargs.setdefault('verify', self.verify_ssl) - try: - response = requests.post(uri, data=data, **kwargs) - except requests.exceptions.RequestException as error: - raise errors.NetworkError(error) - logging.debug('Received response %s: %r', response, response.text) - - self._add_nonce(response) - self._check_response(response, content_type=content_type) - return response - - @classmethod - def _regr_from_response(cls, response, uri=None, new_authzr_uri=None, - terms_of_service=None): - terms_of_service = ( - response.links['terms-of-service']['url'] - if 'terms-of-service' in response.links else terms_of_service) - - if new_authzr_uri is None: - try: - new_authzr_uri = response.links['next']['url'] - except KeyError: - raise errors.NetworkError('"next" link missing') - - return messages.RegistrationResource( - body=messages.Registration.from_json(response.json()), - uri=response.headers.get('Location', uri), - new_authzr_uri=new_authzr_uri, - terms_of_service=terms_of_service) - - def register(self, contact=messages.Registration._fields[ - 'contact'].default): - """Register. - - :param contact: Contact list, as accepted by `.Registration` - :type contact: `tuple` - - :returns: Registration Resource. - :rtype: `.RegistrationResource` - - :raises letsencrypt.errors.UnexpectedUpdate: - - """ - new_reg = messages.Registration(contact=contact) - - response = self._post(self.new_reg_uri, new_reg) - assert response.status_code == httplib.CREATED # TODO: handle errors - - regr = self._regr_from_response(response) - if regr.body.key != self.key.public() or regr.body.contact != contact: - raise errors.UnexpectedUpdate(regr) - - return regr +class Network(client.Client): + """ACME networking.""" def register_from_account(self, account): """Register with server. + .. todo:: this should probably not be a part of network... + :param account: Account :type account: :class:`letsencrypt.account.Account` @@ -239,344 +24,3 @@ class Network(object): account.regr = self.register(contact=tuple( det for det in details if det is not None)) return account - - def update_registration(self, regr): - """Update registration. - - :pram regr: Registration Resource. - :type regr: `.RegistrationResource` - - :returns: Updated Registration Resource. - :rtype: `.RegistrationResource` - - """ - response = self._post(regr.uri, regr.body) - - # TODO: Boulder returns httplib.ACCEPTED - #assert response.status_code == httplib.OK - - # TODO: Boulder does not set Location or Link on update - # (c.f. acme-spec #94) - updated_regr = self._regr_from_response( - response, uri=regr.uri, new_authzr_uri=regr.new_authzr_uri, - terms_of_service=regr.terms_of_service) - if updated_regr != regr: - raise errors.UnexpectedUpdate(regr) - - return updated_regr - - def agree_to_tos(self, regr): - """Agree to the terms-of-service. - - Agree to the terms-of-service in a Registration Resource. - - :param regr: Registration Resource. - :type regr: `.RegistrationResource` - - :returns: Updated Registration Resource. - :rtype: `.RegistrationResource` - - """ - return self.update_registration( - regr.update(body=regr.body.update(agreement=regr.terms_of_service))) - - def _authzr_from_response(self, response, identifier, - uri=None, new_cert_uri=None): - # pylint: disable=no-self-use - if new_cert_uri is None: - try: - new_cert_uri = response.links['next']['url'] - except KeyError: - raise errors.NetworkError('"next" link missing') - - authzr = messages.AuthorizationResource( - body=messages.Authorization.from_json(response.json()), - uri=response.headers.get('Location', uri), - new_cert_uri=new_cert_uri) - if authzr.body.identifier != identifier: - raise errors.UnexpectedUpdate(authzr) - return authzr - - def request_challenges(self, identifier, new_authzr_uri): - """Request challenges. - - :param identifier: Identifier to be challenged. - :type identifier: `.messages.Identifier` - - :param str new_authzr_uri: new-authorization URI - - :returns: Authorization Resource. - :rtype: `.AuthorizationResource` - - """ - new_authz = messages.Authorization(identifier=identifier) - response = self._post(new_authzr_uri, new_authz) - assert response.status_code == httplib.CREATED # TODO: handle errors - return self._authzr_from_response(response, identifier) - - def request_domain_challenges(self, domain, new_authz_uri): - """Request challenges for domain names. - - This is simply a convenience function that wraps around - `request_challenges`, but works with domain names instead of - generic identifiers. - - :param str domain: Domain name to be challenged. - :param str new_authzr_uri: new-authorization URI - - :returns: Authorization Resource. - :rtype: `.AuthorizationResource` - - """ - return self.request_challenges(messages.Identifier( - typ=messages.IDENTIFIER_FQDN, value=domain), new_authz_uri) - - def answer_challenge(self, challb, response): - """Answer challenge. - - :param challb: Challenge Resource body. - :type challb: `.ChallengeBody` - - :param response: Corresponding Challenge response - :type response: `.challenges.ChallengeResponse` - - :returns: Challenge Resource with updated body. - :rtype: `.ChallengeResource` - - :raises errors.UnexpectedUpdate: - - """ - response = self._post(challb.uri, response) - try: - authzr_uri = response.links['up']['url'] - except KeyError: - raise errors.NetworkError('"up" Link header missing') - challr = messages.ChallengeResource( - authzr_uri=authzr_uri, - body=messages.ChallengeBody.from_json(response.json())) - # TODO: check that challr.uri == response.headers['Location']? - if challr.uri != challb.uri: - raise errors.UnexpectedUpdate(challr.uri) - return challr - - @classmethod - def retry_after(cls, response, default): - """Compute next `poll` time based on response ``Retry-After`` header. - - :param response: Response from `poll`. - :type response: `requests.Response` - - :param int default: Default value (in seconds), used when - ``Retry-After`` header is not present or invalid. - - :returns: Time point when next `poll` should be performed. - :rtype: `datetime.datetime` - - """ - retry_after = response.headers.get('Retry-After', str(default)) - try: - seconds = int(retry_after) - except ValueError: - # pylint: disable=no-member - decoded = werkzeug.parse_date(retry_after) # RFC1123 - if decoded is None: - seconds = default - else: - return decoded - - return datetime.datetime.now() + datetime.timedelta(seconds=seconds) - - def poll(self, authzr): - """Poll Authorization Resource for status. - - :param authzr: Authorization Resource - :type authzr: `.AuthorizationResource` - - :returns: Updated Authorization Resource and HTTP response. - - :rtype: (`.AuthorizationResource`, `requests.Response`) - - """ - response = self._get(authzr.uri) - updated_authzr = self._authzr_from_response( - response, authzr.body.identifier, authzr.uri, authzr.new_cert_uri) - # TODO: check and raise UnexpectedUpdate - return updated_authzr, response - - def request_issuance(self, csr, authzrs): - """Request issuance. - - :param csr: CSR - :type csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509` - - :param authzrs: `list` of `.AuthorizationResource` - - :returns: Issued certificate - :rtype: `.messages.CertificateResource` - - """ - assert authzrs, "Authorizations list is empty" - logging.debug("Requesting issuance...") - - # TODO: assert len(authzrs) == number of SANs - req = messages.CertificateRequest( - csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs)) - - content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument - response = self._post( - authzrs[0].new_cert_uri, # TODO: acme-spec #90 - req, - content_type=content_type, - headers={'Accept': content_type}) - - cert_chain_uri = response.links.get('up', {}).get('url') - - try: - uri = response.headers['Location'] - except KeyError: - raise errors.NetworkError('"Location" Header missing') - - return messages.CertificateResource( - uri=uri, authzrs=authzrs, cert_chain_uri=cert_chain_uri, - body=jose.ComparableX509( - M2Crypto.X509.load_cert_der_string(response.content))) - - def poll_and_request_issuance(self, csr, authzrs, mintime=5): - """Poll and request issuance. - - This function polls all provided Authorization Resource URIs - until all challenges are valid, respecting ``Retry-After`` HTTP - headers, and then calls `request_issuance`. - - .. todo:: add `max_attempts` or `timeout` - - :param csr: CSR. - :type csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509` - - :param authzrs: `list` of `.AuthorizationResource` - - :param int mintime: Minimum time before next attempt, used if - ``Retry-After`` is not present in the response. - - :returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is - the issued certificate (`.messages.CertificateResource.), - and ``updated_authzrs`` is a `tuple` consisting of updated - Authorization Resources (`.AuthorizationResource`) as - present in the responses from server, and in the same order - as the input ``authzrs``. - :rtype: `tuple` - - """ - # priority queue with datetime (based on Retry-After) as key, - # and original Authorization Resource as value - waiting = [(datetime.datetime.now(), authzr) for authzr in authzrs] - # mapping between original Authorization Resource and the most - # recently updated one - updated = dict((authzr, authzr) for authzr in authzrs) - - while waiting: - # find the smallest Retry-After, and sleep if necessary - when, authzr = heapq.heappop(waiting) - now = datetime.datetime.now() - if when > now: - seconds = (when - now).seconds - logging.debug('Sleeping for %d seconds', seconds) - time.sleep(seconds) - - # Note that we poll with the latest updated Authorization - # URI, which might have a different URI than initial one - updated_authzr, response = self.poll(updated[authzr]) - updated[authzr] = updated_authzr - - if updated_authzr.body.status != messages.STATUS_VALID: - # push back to the priority queue, with updated retry_after - heapq.heappush(waiting, (self.retry_after( - response, default=mintime), authzr)) - - updated_authzrs = tuple(updated[authzr] for authzr in authzrs) - return self.request_issuance(csr, updated_authzrs), updated_authzrs - - def _get_cert(self, uri): - """Returns certificate from URI. - - :param str uri: URI of certificate - - :returns: tuple of the form - (response, :class:`acme.jose.ComparableX509`) - :rtype: tuple - - """ - content_type = self.DER_CONTENT_TYPE # TODO: make it a param - response = self._get(uri, headers={'Accept': content_type}, - content_type=content_type) - return response, jose.ComparableX509( - M2Crypto.X509.load_cert_der_string(response.content)) - - def check_cert(self, certr): - """Check for new cert. - - :param certr: Certificate Resource - :type certr: `.CertificateResource` - - :returns: Updated Certificate Resource. - :rtype: `.CertificateResource` - - """ - # TODO: acme-spec 5.1 table action should be renamed to - # "refresh cert", and this method integrated with self.refresh - response, cert = self._get_cert(certr.uri) - if 'Location' not in response.headers: - raise errors.NetworkError('Location header missing') - if response.headers['Location'] != certr.uri: - raise errors.UnexpectedUpdate(response.text) - return certr.update(body=cert) - - def refresh(self, certr): - """Refresh certificate. - - :param certr: Certificate Resource - :type certr: `.CertificateResource` - - :returns: Updated Certificate Resource. - :rtype: `.CertificateResource` - - """ - # TODO: If a client sends a refresh request and the server is - # not willing to refresh the certificate, the server MUST - # respond with status code 403 (Forbidden) - return self.check_cert(certr) - - def fetch_chain(self, certr): - """Fetch chain for certificate. - - :param certr: Certificate Resource - :type certr: `.CertificateResource` - - :returns: Certificate chain, or `None` if no "up" Link was provided. - :rtype: `M2Crypto.X509.X509` wrapped in `.ComparableX509` - - """ - if certr.cert_chain_uri is not None: - return self._get_cert(certr.cert_chain_uri)[1] - else: - return None - - def revoke(self, certr, when=messages.Revocation.NOW): - """Revoke certificate. - - :param certr: Certificate Resource - :type certr: `.CertificateResource` - - :param when: When should the revocation take place? Takes - the same values as `.messages.Revocation.revoke`. - - :raises letsencrypt.errors.NetworkError: If revocation is - unsuccessful. - - """ - rev = messages.Revocation(revoke=when, authorizations=tuple( - authzr.uri for authzr in certr.authzrs)) - response = self._post(certr.uri, rev) - if response.status_code != httplib.OK: - raise errors.NetworkError( - 'Successful revocation must return HTTP OK status') diff --git a/letsencrypt/tests/network_test.py b/letsencrypt/tests/network_test.py index 586dc7ecb..6acb11315 100644 --- a/letsencrypt/tests/network_test.py +++ b/letsencrypt/tests/network_test.py @@ -1,281 +1,27 @@ """Tests for letsencrypt.network.""" -import datetime -import httplib -import os -import pkg_resources import shutil import tempfile import unittest -import M2Crypto import mock -import requests - -from acme import challenges -from acme import jose -from acme import jws as acme_jws -from acme import messages from letsencrypt import account -from letsencrypt import errors - - -CERT = jose.ComparableX509(M2Crypto.X509.load_cert_string( - pkg_resources.resource_string( - __name__, os.path.join('testdata', 'cert.pem')))) -CERT2 = jose.ComparableX509(M2Crypto.X509.load_cert_string( - pkg_resources.resource_string( - __name__, os.path.join('testdata', 'cert-san.pem')))) -CSR = jose.ComparableX509(M2Crypto.X509.load_request_string( - pkg_resources.resource_string( - __name__, os.path.join('testdata', 'csr.pem')))) -KEY = jose.JWKRSA.load(pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))) -KEY2 = jose.JWKRSA.load(pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'rsa256_key.pem'))) class NetworkTest(unittest.TestCase): """Tests for letsencrypt.network.Network.""" - # pylint: disable=too-many-instance-attributes,too-many-public-methods - def setUp(self): - self.verify_ssl = mock.MagicMock() - self.wrap_in_jws = mock.MagicMock(return_value=mock.sentinel.wrapped) - from letsencrypt.network import Network self.net = Network( - new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg', - key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl) - self.nonce = jose.b64encode('Nonce') - self.net._nonces.add(self.nonce) # pylint: disable=protected-access - - self.response = mock.MagicMock(ok=True, status_code=httplib.OK) - self.response.headers = {} - self.response.links = {} - - self.post = mock.MagicMock(return_value=self.response) - self.get = mock.MagicMock(return_value=self.response) - - self.identifier = messages.Identifier( - typ=messages.IDENTIFIER_FQDN, value='example.com') + new_reg_uri=None, key=None, alg=None, verify_ssl=None) self.config = mock.Mock(accounts_dir=tempfile.mkdtemp()) - - # Registration self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212') - reg = messages.Registration( - contact=self.contact, key=KEY.public(), recovery_token='t') - self.regr = messages.RegistrationResource( - body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1', - new_authzr_uri='https://www.letsencrypt-demo.org/acme/new-reg', - terms_of_service='https://www.letsencrypt-demo.org/tos') - - # Authorization - authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1' - challb = messages.ChallengeBody( - uri=(authzr_uri + '/1'), status=messages.STATUS_VALID, - chall=challenges.DNS(token='foo')) - self.challr = messages.ChallengeResource( - body=challb, authzr_uri=authzr_uri) - self.authz = messages.Authorization( - identifier=messages.Identifier( - typ=messages.IDENTIFIER_FQDN, value='example.com'), - challenges=(challb,), combinations=None) - self.authzr = messages.AuthorizationResource( - body=self.authz, uri=authzr_uri, - new_cert_uri='https://www.letsencrypt-demo.org/acme/new-cert') - - # Request issuance - self.certr = messages.CertificateResource( - body=CERT, authzrs=(self.authzr,), - uri='https://www.letsencrypt-demo.org/acme/cert/1', - cert_chain_uri='https://www.letsencrypt-demo.org/ca') def tearDown(self): shutil.rmtree(self.config.accounts_dir) - def _mock_post_get(self): - # pylint: disable=protected-access - self.net._post = self.post - self.net._get = self.get - - def test_init(self): - self.assertTrue(self.net.verify_ssl is self.verify_ssl) - - def test_wrap_in_jws(self): - class MockJSONDeSerializable(jose.JSONDeSerializable): - # pylint: disable=missing-docstring - def __init__(self, value): - self.value = value - def to_partial_json(self): - return self.value - @classmethod - def from_json(cls, value): - pass # pragma: no cover - # pylint: disable=protected-access - jws_dump = self.net._wrap_in_jws( - MockJSONDeSerializable('foo'), nonce='Tg') - jws = acme_jws.JWS.json_loads(jws_dump) - self.assertEqual(jws.payload, '"foo"') - self.assertEqual(jws.signature.combined.nonce, 'Tg') - # TODO: check that nonce is in protected header - - def test_check_response_not_ok_jobj_no_error(self): - self.response.ok = False - self.response.json.return_value = {} - # pylint: disable=protected-access - self.assertRaises( - errors.NetworkError, self.net._check_response, self.response) - - def test_check_response_not_ok_jobj_error(self): - self.response.ok = False - self.response.json.return_value = messages.Error( - detail='foo', typ='serverInternal', title='some title').to_json() - # pylint: disable=protected-access - self.assertRaises( - messages.Error, self.net._check_response, self.response) - - def test_check_response_not_ok_no_jobj(self): - self.response.ok = False - self.response.json.side_effect = ValueError - # pylint: disable=protected-access - self.assertRaises( - errors.NetworkError, self.net._check_response, self.response) - - def test_check_response_ok_no_jobj_ct_required(self): - self.response.json.side_effect = ValueError - for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: - self.response.headers['Content-Type'] = response_ct - # pylint: disable=protected-access - self.assertRaises( - errors.NetworkError, self.net._check_response, self.response, - content_type=self.net.JSON_CONTENT_TYPE) - - def test_check_response_ok_no_jobj_no_ct(self): - self.response.json.side_effect = ValueError - for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: - self.response.headers['Content-Type'] = response_ct - # pylint: disable=protected-access - self.net._check_response(self.response) - - def test_check_response_jobj(self): - self.response.json.return_value = {} - for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: - self.response.headers['Content-Type'] = response_ct - # pylint: disable=protected-access - self.net._check_response(self.response) - - @mock.patch('letsencrypt.network.requests') - def test_get_requests_error_passthrough(self, requests_mock): - requests_mock.exceptions = requests.exceptions - requests_mock.get.side_effect = requests.exceptions.RequestException - # pylint: disable=protected-access - self.assertRaises(errors.NetworkError, self.net._get, 'uri') - - @mock.patch('letsencrypt.network.requests') - def test_get(self, requests_mock): - # pylint: disable=protected-access - self.net._check_response = mock.MagicMock() - self.net._get('uri', content_type='ct') - self.net._check_response.assert_called_once_with( - requests_mock.get('uri'), content_type='ct') - - def _mock_wrap_in_jws(self): - # pylint: disable=protected-access - self.net._wrap_in_jws = self.wrap_in_jws - - @mock.patch('letsencrypt.network.requests') - def test_post_requests_error_passthrough(self, requests_mock): - requests_mock.exceptions = requests.exceptions - requests_mock.post.side_effect = requests.exceptions.RequestException - # pylint: disable=protected-access - self._mock_wrap_in_jws() - self.assertRaises( - errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj) - - @mock.patch('letsencrypt.network.requests') - def test_post(self, requests_mock): - # pylint: disable=protected-access - self.net._check_response = mock.MagicMock() - self._mock_wrap_in_jws() - requests_mock.post().headers = { - self.net.REPLAY_NONCE_HEADER: self.nonce} - self.net._post('uri', mock.sentinel.obj, content_type='ct') - self.net._check_response.assert_called_once_with( - requests_mock.post('uri', mock.sentinel.wrapped), content_type='ct') - - @mock.patch('letsencrypt.network.requests') - def test_post_replay_nonce_handling(self, requests_mock): - # pylint: disable=protected-access - self.net._check_response = mock.MagicMock() - self._mock_wrap_in_jws() - - self.net._nonces.clear() - self.assertRaises( - errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj) - - nonce2 = jose.b64encode('Nonce2') - requests_mock.head('uri').headers = { - self.net.REPLAY_NONCE_HEADER: nonce2} - requests_mock.post('uri').headers = { - self.net.REPLAY_NONCE_HEADER: self.nonce} - - self.net._post('uri', mock.sentinel.obj) - - requests_mock.head.assert_called_with('uri') - self.wrap_in_jws.assert_called_once_with(mock.sentinel.obj, nonce2) - self.assertEqual(self.net._nonces, set([self.nonce])) - - # wrong nonce - requests_mock.post('uri').headers = {self.net.REPLAY_NONCE_HEADER: 'F'} - self.assertRaises( - errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj) - - @mock.patch('letsencrypt.client.network.requests') - def test_get_post_verify_ssl(self, requests_mock): - # pylint: disable=protected-access - self._mock_wrap_in_jws() - self.net._check_response = mock.MagicMock() - - for verify_ssl in [True, False]: - self.net.verify_ssl = verify_ssl - self.net._get('uri') - self.net._nonces.add('N') - requests_mock.post().headers = { - self.net.REPLAY_NONCE_HEADER: self.nonce} - self.net._post('uri', mock.sentinel.obj) - requests_mock.get.assert_called_once_with('uri', verify=verify_ssl) - requests_mock.post.assert_called_with( - 'uri', data=mock.sentinel.wrapped, verify=verify_ssl) - requests_mock.reset_mock() - - def test_register(self): - self.response.status_code = httplib.CREATED - self.response.json.return_value = self.regr.body.to_json() - self.response.headers['Location'] = self.regr.uri - self.response.links.update({ - 'next': {'url': self.regr.new_authzr_uri}, - 'terms-of-service': {'url': self.regr.terms_of_service}, - }) - - self._mock_post_get() - self.assertEqual(self.regr, self.net.register(self.contact)) - # TODO: test POST call arguments - - # TODO: split here and separate test - reg_wrong_key = self.regr.body.update(key=KEY2.public()) - self.response.json.return_value = reg_wrong_key.to_json() - self.assertRaises( - errors.UnexpectedUpdate, self.net.register, self.contact) - - def test_register_missing_next(self): - self.response.status_code = httplib.CREATED - self._mock_post_get() - self.assertRaises( - errors.NetworkError, self.net.register, self.regr.body) - def test_register_from_account(self): self.net.register = mock.Mock() acc = account.Account( @@ -299,265 +45,6 @@ class NetworkTest(unittest.TestCase): self.net.register_from_account(acc2) self.net.register.assert_called_with(contact=()) - def test_update_registration(self): - self.response.headers['Location'] = self.regr.uri - self.response.json.return_value = self.regr.body.to_json() - self._mock_post_get() - self.assertEqual(self.regr, self.net.update_registration(self.regr)) - - # TODO: split here and separate test - self.response.json.return_value = self.regr.body.update( - contact=()).to_json() - self.assertRaises( - errors.UnexpectedUpdate, self.net.update_registration, self.regr) - - def test_agree_to_tos(self): - self.net.update_registration = mock.Mock() - self.net.agree_to_tos(self.regr) - regr = self.net.update_registration.call_args[0][0] - self.assertEqual(self.regr.terms_of_service, regr.body.agreement) - - def test_request_challenges(self): - self.response.status_code = httplib.CREATED - self.response.headers['Location'] = self.authzr.uri - self.response.json.return_value = self.authz.to_json() - self.response.links = { - 'next': {'url': self.authzr.new_cert_uri}, - } - - self._mock_post_get() - self.net.request_challenges(self.identifier, self.authzr.uri) - # TODO: test POST call arguments - - # TODO: split here and separate test - self.response.json.return_value = self.authz.update( - identifier=self.identifier.update(value='foo')).to_json() - self.assertRaises(errors.UnexpectedUpdate, self.net.request_challenges, - self.identifier, self.authzr.uri) - - def test_request_challenges_missing_next(self): - self.response.status_code = httplib.CREATED - self._mock_post_get() - self.assertRaises( - errors.NetworkError, self.net.request_challenges, - self.identifier, self.regr) - - def test_request_domain_challenges(self): - self.net.request_challenges = mock.MagicMock() - self.assertEqual( - self.net.request_challenges(self.identifier), - self.net.request_domain_challenges('example.com', self.regr)) - - def test_answer_challenge(self): - self.response.links['up'] = {'url': self.challr.authzr_uri} - self.response.json.return_value = self.challr.body.to_json() - - chall_response = challenges.DNSResponse() - - self._mock_post_get() - self.net.answer_challenge(self.challr.body, chall_response) - - # TODO: split here and separate test - self.assertRaises(errors.UnexpectedUpdate, self.net.answer_challenge, - self.challr.body.update(uri='foo'), chall_response) - - def test_answer_challenge_missing_next(self): - self._mock_post_get() - self.assertRaises(errors.NetworkError, self.net.answer_challenge, - self.challr.body, challenges.DNSResponse()) - - def test_retry_after_date(self): - self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT' - self.assertEqual( - datetime.datetime(1999, 12, 31, 23, 59, 59), - self.net.retry_after(response=self.response, default=10)) - - @mock.patch('letsencrypt.network.datetime') - def test_retry_after_invalid(self, dt_mock): - dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) - dt_mock.timedelta = datetime.timedelta - - self.response.headers['Retry-After'] = 'foooo' - self.assertEqual( - datetime.datetime(2015, 3, 27, 0, 0, 10), - self.net.retry_after(response=self.response, default=10)) - - @mock.patch('letsencrypt.network.datetime') - def test_retry_after_seconds(self, dt_mock): - dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) - dt_mock.timedelta = datetime.timedelta - - self.response.headers['Retry-After'] = '50' - self.assertEqual( - datetime.datetime(2015, 3, 27, 0, 0, 50), - self.net.retry_after(response=self.response, default=10)) - - @mock.patch('letsencrypt.network.datetime') - def test_retry_after_missing(self, dt_mock): - dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) - dt_mock.timedelta = datetime.timedelta - - self.assertEqual( - datetime.datetime(2015, 3, 27, 0, 0, 10), - self.net.retry_after(response=self.response, default=10)) - - def test_poll(self): - self.response.json.return_value = self.authzr.body.to_json() - self._mock_post_get() - self.assertEqual((self.authzr, self.response), - self.net.poll(self.authzr)) - - # TODO: split here and separate test - self.response.json.return_value = self.authz.update( - identifier=self.identifier.update(value='foo')).to_json() - self.assertRaises(errors.UnexpectedUpdate, self.net.poll, self.authzr) - - def test_request_issuance(self): - self.response.content = CERT.as_der() - self.response.headers['Location'] = self.certr.uri - self.response.links['up'] = {'url': self.certr.cert_chain_uri} - self._mock_post_get() - self.assertEqual( - self.certr, self.net.request_issuance(CSR, (self.authzr,))) - # TODO: check POST args - - def test_request_issuance_missing_up(self): - self.response.content = CERT.as_der() - self.response.headers['Location'] = self.certr.uri - self._mock_post_get() - self.assertEqual( - self.certr.update(cert_chain_uri=None), - self.net.request_issuance(CSR, (self.authzr,))) - - def test_request_issuance_missing_location(self): - self._mock_post_get() - self.assertRaises( - errors.NetworkError, self.net.request_issuance, - CSR, (self.authzr,)) - - @mock.patch('letsencrypt.network.datetime') - @mock.patch('letsencrypt.network.time') - def test_poll_and_request_issuance(self, time_mock, dt_mock): - # clock.dt | pylint: disable=no-member - clock = mock.MagicMock(dt=datetime.datetime(2015, 3, 27)) - - def sleep(seconds): - """increment clock""" - clock.dt += datetime.timedelta(seconds=seconds) - time_mock.sleep.side_effect = sleep - - def now(): - """return current clock value""" - return clock.dt - dt_mock.datetime.now.side_effect = now - dt_mock.timedelta = datetime.timedelta - - def poll(authzr): # pylint: disable=missing-docstring - # record poll start time based on the current clock value - authzr.times.append(clock.dt) - - # suppose it takes 2 seconds for server to produce the - # result, increment clock - clock.dt += datetime.timedelta(seconds=2) - - if not authzr.retries: # no more retries - done = mock.MagicMock(uri=authzr.uri, times=authzr.times) - done.body.status = messages.STATUS_VALID - return done, [] - - # response (2nd result tuple element) is reduced to only - # Retry-After header contents represented as integer - # seconds; authzr.retries is a list of Retry-After - # headers, head(retries) is peeled of as a current - # Retry-After header, and tail(retries) is persisted for - # later poll() calls - return (mock.MagicMock(retries=authzr.retries[1:], - uri=authzr.uri + '.', times=authzr.times), - authzr.retries[0]) - self.net.poll = mock.MagicMock(side_effect=poll) - - mintime = 7 - - def retry_after(response, default): # pylint: disable=missing-docstring - # check that poll_and_request_issuance correctly passes mintime - self.assertEqual(default, mintime) - return clock.dt + datetime.timedelta(seconds=response) - self.net.retry_after = mock.MagicMock(side_effect=retry_after) - - def request_issuance(csr, authzrs): # pylint: disable=missing-docstring - return csr, authzrs - self.net.request_issuance = mock.MagicMock(side_effect=request_issuance) - - csr = mock.MagicMock() - authzrs = ( - mock.MagicMock(uri='a', times=[], retries=(8, 20, 30)), - mock.MagicMock(uri='b', times=[], retries=(5,)), - ) - - cert, updated_authzrs = self.net.poll_and_request_issuance( - csr, authzrs, mintime=mintime) - self.assertTrue(cert[0] is csr) - self.assertTrue(cert[1] is updated_authzrs) - self.assertEqual(updated_authzrs[0].uri, 'a...') - self.assertEqual(updated_authzrs[1].uri, 'b.') - self.assertEqual(updated_authzrs[0].times, [ - datetime.datetime(2015, 3, 27), - # a is scheduled for 10, but b is polling [9..11), so it - # will be picked up as soon as b is finished, without - # additional sleeping - datetime.datetime(2015, 3, 27, 0, 0, 11), - datetime.datetime(2015, 3, 27, 0, 0, 33), - datetime.datetime(2015, 3, 27, 0, 1, 5), - ]) - self.assertEqual(updated_authzrs[1].times, [ - datetime.datetime(2015, 3, 27, 0, 0, 2), - datetime.datetime(2015, 3, 27, 0, 0, 9), - ]) - self.assertEqual(clock.dt, datetime.datetime(2015, 3, 27, 0, 1, 7)) - - def test_check_cert(self): - self.response.headers['Location'] = self.certr.uri - self.response.content = CERT2.as_der() - self._mock_post_get() - self.assertEqual( - self.certr.update(body=CERT2), self.net.check_cert(self.certr)) - - # TODO: split here and separate test - self.response.headers['Location'] = 'foo' - self.assertRaises( - errors.UnexpectedUpdate, self.net.check_cert, self.certr) - - def test_check_cert_missing_location(self): - self.response.content = CERT2.as_der() - self._mock_post_get() - self.assertRaises(errors.NetworkError, self.net.check_cert, self.certr) - - def test_refresh(self): - self.net.check_cert = mock.MagicMock() - self.assertEqual( - self.net.check_cert(self.certr), self.net.refresh(self.certr)) - - def test_fetch_chain(self): - # pylint: disable=protected-access - self.net._get_cert = mock.MagicMock() - self.net._get_cert.return_value = ("response", "certificate") - self.assertEqual(self.net._get_cert(self.certr.cert_chain_uri)[1], - self.net.fetch_chain(self.certr)) - - def test_fetch_chain_no_up_link(self): - self.assertTrue(self.net.fetch_chain(self.certr.update( - cert_chain_uri=None)) is None) - - def test_revoke(self): - self._mock_post_get() - self.net.revoke(self.certr, when=messages.Revocation.NOW) - self.post.assert_called_once_with(self.certr.uri, mock.ANY) - - def test_revoke_bad_status_raises_error(self): - self.response.status_code = httplib.METHOD_NOT_ALLOWED - self._mock_post_get() - self.assertRaises(errors.NetworkError, self.net.revoke, self.certr) - if __name__ == '__main__': unittest.main() # pragma: no cover From 90dae9fd880b7ff07ee3f1b76d862b8419eaf22a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 18 Jun 2015 11:07:20 +0000 Subject: [PATCH 54/70] Update restified example script and rename to acme_client.py --- examples/acme_client.py | 45 +++++++++++++++++++++++++++++++++++++++++ examples/restified.py | 42 -------------------------------------- 2 files changed, 45 insertions(+), 42 deletions(-) create mode 100644 examples/acme_client.py delete mode 100644 examples/restified.py diff --git a/examples/acme_client.py b/examples/acme_client.py new file mode 100644 index 000000000..09ff2bfc3 --- /dev/null +++ b/examples/acme_client.py @@ -0,0 +1,45 @@ +"""Example script showing how to use acme client API.""" +import logging +import os +import pkg_resources + +import Crypto.PublicKey.RSA +import M2Crypto + +from acme import client +from acme import messages +from acme import jose + + +logging.basicConfig(level=logging.DEBUG) + + +NEW_REG_URL = 'https://www.letsencrypt-demo.org/acme/new-reg' +BITS = 2048 # minimum for Boulder +DOMAIN = 'example1.com' # example.com is ignored by Boulder + +key = jose.JWKRSA.load( + Crypto.PublicKey.RSA.generate(BITS).exportKey(format="PEM")) +acme = client.Client(NEW_REG_URL, key) + +regr = acme.register(contact=()) +logging.info('Auto-accepting TOS: %s', regr.terms_of_service) +acme.update_registration(regr.update( + body=regr.body.update(agreement=regr.terms_of_service))) +logging.debug(regr) + +authzr = acme.request_challenges( + identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=DOMAIN), + new_authzr_uri=regr.new_authzr_uri) +logging.debug(authzr) + +authzr, authzr_response = acme.poll(authzr) + +csr = M2Crypto.X509.load_request_string(pkg_resources.resource_string( + 'acme.jose', os.path.join('testdata', 'csr.der')), + M2Crypto.X509.FORMAT_DER) +try: + acme.request_issuance(csr, (authzr,)) +except messages.Error as error: + print ("This script is doomed to fail as no authorization " + "challenges are ever solved. Error from server: {0}".format(error)) diff --git a/examples/restified.py b/examples/restified.py deleted file mode 100644 index cfd7fa8dd..000000000 --- a/examples/restified.py +++ /dev/null @@ -1,42 +0,0 @@ -import logging -import os -import pkg_resources - -import M2Crypto - -from acme import messages -from acme import jose - -from letsencrypt import network - - -logger = logging.getLogger() -logger.setLevel(logging.DEBUG) - -NEW_REG_URL = 'https://www.letsencrypt-demo.org/acme/new-reg' - -key = jose.JWKRSA.load(pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))) -net = network.Network(NEW_REG_URL, key) - -regr = net.register(contact=( - 'mailto:cert-admin@example.com', 'tel:+12025551212')) -logging.info('Auto-accepting TOS: %s', regr.terms_of_service) -net.update_registration(regr.update( - body=regr.body.update(agreement=regr.terms_of_service))) -logging.debug(regr) - -authzr = net.request_challenges( - identifier=messages.Identifier( - typ=messages.IDENTIFIER_FQDN, value='example1.com'), - new_authzr_uri=regr.new_authzr_uri) -logging.debug(authzr) - -authzr, authzr_response = net.poll(authzr) - -csr = M2Crypto.X509.load_request_string(pkg_resources.resource_string( - 'letsencrypt.tests', os.path.join('testdata', 'csr.pem'))) -try: - net.request_issuance(csr, (authzr,)) -except messages.Error as error: - print error.detail From cf76593fa70f4fb5743946feab5df939a23ef153 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 22 Jun 2015 18:03:04 +0000 Subject: [PATCH 55/70] Remove constants.NETSTAT. Update docs for IConfig.server. --- letsencrypt/constants.py | 4 ---- letsencrypt/interfaces.py | 3 +-- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index 47539615d..356b8ed14 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -81,7 +81,3 @@ ACCOUNT_KEYS_DIR = "keys" REC_TOKEN_DIR = "recovery_tokens" """Directory where all recovery tokens are saved (relative to IConfig.work_dir).""" - -NETSTAT = "/bin/netstat" -"""Location of netstat binary for checking whether a listener is already -running on the specified port (Linux-specific).""" diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index c0d44a134..d2a420d00 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -148,8 +148,7 @@ class IConfig(zope.interface.Interface): """ server = zope.interface.Attribute( - "CA hostname (and optionally :port). The server certificate must " - "be trusted in order to avoid further modifications to the client.") + "ACME new registration URI (including /acme/new-reg).") email = zope.interface.Attribute( "Email used for registration and recovery contact.") rsa_key_size = zope.interface.Attribute("Size of the RSA key.") From 1720864b443351c473a130b9a6897f0b65cb18c2 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 22 Jun 2015 19:55:47 +0000 Subject: [PATCH 56/70] acme.client: locally disable too-many-instance-attributes. --- acme/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/client.py b/acme/client.py index c0eda0fa3..629048d03 100644 --- a/acme/client.py +++ b/acme/client.py @@ -19,7 +19,7 @@ from acme import messages requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() -class Client(object): +class Client(object): # pylint: disable=too-many-instance-attributes """ACME client. .. todo:: From 52d6e9b67435ee1d597c5a11474aba566e0aaaf3 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 17 Jun 2015 16:13:26 +0000 Subject: [PATCH 57/70] acme-spec#118 revoke. --- acme/client.py | 14 +++++--------- acme/client_test.py | 5 +++-- acme/messages.py | 34 +++++++++++++++------------------- acme/messages_test.py | 31 +++++++++++++------------------ letsencrypt/revoker.py | 2 +- 5 files changed, 37 insertions(+), 49 deletions(-) diff --git a/acme/client.py b/acme/client.py index 629048d03..73c962581 100644 --- a/acme/client.py +++ b/acme/client.py @@ -539,21 +539,17 @@ class Client(object): # pylint: disable=too-many-instance-attributes else: return None - def revoke(self, certr, when=messages.Revocation.NOW): + def revoke(self, cert): """Revoke certificate. - :param certr: Certificate Resource - :type certr: `.CertificateResource` - - :param when: When should the revocation take place? Takes - the same values as `.messages.Revocation.revoke`. + :param .ComparableX509 body: `M2Crypto.X509.X509` wrapped in + `.ComparableX509` :raises .ClientError: If revocation is unsuccessful. """ - rev = messages.Revocation(revoke=when, authorizations=tuple( - authzr.uri for authzr in certr.authzrs)) - response = self._post(certr.uri, rev) + response = self._post(messages.Revocation.url(self.new_reg_uri), + messages.Revocation(certificate=cert)) if response.status_code != httplib.OK: raise errors.ClientError( 'Successful revocation must return HTTP OK status') diff --git a/acme/client_test.py b/acme/client_test.py index 5e4cc1720..dfa8d7607 100644 --- a/acme/client_test.py +++ b/acme/client_test.py @@ -517,8 +517,9 @@ class ClientTest(unittest.TestCase): def test_revoke(self): self._mock_post_get() - self.net.revoke(self.certr, when=messages.Revocation.NOW) - self.post.assert_called_once_with(self.certr.uri, mock.ANY) + self.net.revoke(self.certr.body) + self.post.assert_called_once_with(messages.Revocation.url( + self.net.new_reg_uri), mock.ANY) def test_revoke_bad_status_raises_error(self): self.response.status_code = httplib.METHOD_NOT_ALLOWED diff --git a/acme/messages.py b/acme/messages.py index aa041caed..bfc452a70 100644 --- a/acme/messages.py +++ b/acme/messages.py @@ -1,4 +1,6 @@ """ACME protocol messages.""" +import urlparse + from acme import challenges from acme import fields from acme import jose @@ -271,28 +273,22 @@ class CertificateResource(Resource): class Revocation(jose.JSONObjectWithFields): """Revocation message. - :ivar revoke: Either a `datetime.datetime` or `Revocation.NOW`. - :ivar tuple authorizations: Same as `CertificateRequest.authorizations` + :ivar .ComparableX509 certificate: `M2Crypto.X509.X509` wrapped in + `.ComparableX509` """ + certificate = jose.Field( + 'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert) - NOW = 'now' - """A possible value for `revoke`, denoting that certificate should - be revoked now.""" + # TODO: acme-spec#138, this allows only one ACME server instance per domain + PATH = '/acme/revoke-cert' + """Path to revocation URL, see `url`""" - revoke = jose.Field('revoke') - authorizations = CertificateRequest._fields['authorizations'] + @classmethod + def url(cls, base): + """Get revocation URL. - @revoke.decoder - def revoke(value): # pylint: disable=missing-docstring,no-self-argument - if value == Revocation.NOW: - return value - else: - return fields.RFC3339Field.default_decoder(value) + :param str base: New Registration Resource or server (root) URL. - @revoke.encoder - def revoke(value): # pylint: disable=missing-docstring,no-self-argument - if value == Revocation.NOW: - return value - else: - return fields.RFC3339Field.default_encoder(value) + """ + return urlparse.urljoin(base, cls.PATH) diff --git a/acme/messages_test.py b/acme/messages_test.py index 4f86d7809..65c080ee7 100644 --- a/acme/messages_test.py +++ b/acme/messages_test.py @@ -1,11 +1,10 @@ """Tests for acme.messages.""" -import datetime import os import pkg_resources import unittest +import M2Crypto.X509 import mock -import pytz from Crypto.PublicKey import RSA from acme import challenges @@ -14,6 +13,9 @@ from acme import jose KEY = jose.util.HashableRSAKey(RSA.importKey(pkg_resources.resource_string( 'acme.jose', os.path.join('testdata', 'rsa512_key.pem')))) +CERT = jose.ComparableX509(M2Crypto.X509.load_cert( + format=M2Crypto.X509.FORMAT_DER, file=pkg_resources.resource_filename( + 'acme.jose', os.path.join('testdata', 'cert.der')))) class ErrorTest(unittest.TestCase): @@ -223,27 +225,20 @@ class AuthorizationTest(unittest.TestCase): class RevocationTest(unittest.TestCase): """Tests for acme.messages.RevocationTest.""" + def test_url(self): + from acme.messages import Revocation + url = 'https://letsencrypt-demo.org/acme/revoke-cert' + self.assertEqual(url, Revocation.url('https://letsencrypt-demo.org')) + self.assertEqual( + url, Revocation.url('https://letsencrypt-demo.org/acme/new-reg')) + def setUp(self): from acme.messages import Revocation - self.rev_now = Revocation(authorizations=(), revoke=Revocation.NOW) - self.rev_date = Revocation(authorizations=(), revoke=datetime.datetime( - 2015, 3, 27, tzinfo=pytz.utc)) - self.jobj_now = {'authorizations': (), 'revoke': Revocation.NOW} - self.jobj_date = {'authorizations': (), - 'revoke': '2015-03-27T00:00:00Z'} - - def test_revoke_decoder(self): - from acme.messages import Revocation - self.assertEqual(self.rev_now, Revocation.from_json(self.jobj_now)) - self.assertEqual(self.rev_date, Revocation.from_json(self.jobj_date)) - - def test_revoke_encoder(self): - self.assertEqual(self.jobj_now, self.rev_now.to_partial_json()) - self.assertEqual(self.jobj_date, self.rev_date.to_partial_json()) + self.rev = Revocation(certificate=CERT) def test_from_json_hashable(self): from acme.messages import Revocation - hash(Revocation.from_json(self.rev_now.to_json())) + hash(Revocation.from_json(self.rev.to_json())) if __name__ == '__main__': diff --git a/letsencrypt/revoker.py b/letsencrypt/revoker.py index 0d3bd8e79..402157721 100644 --- a/letsencrypt/revoker.py +++ b/letsencrypt/revoker.py @@ -253,7 +253,7 @@ class Revoker(object): raise errors.LetsEncryptRevokerError( "Corrupted backup key file: %s" % cert.backup_key_path) - return self.network.revoke(certr=None) # XXX + return self.network.revoke(cert=None) # XXX def _remove_certs_keys(self, cert_list): # pylint: disable=no-self-use """Remove certificate and key. From d970987b79c1f370ac1400ae9a31f01ee6f2722a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 22 Jun 2015 20:30:17 +0000 Subject: [PATCH 58/70] Fix comment typo --- acme/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/client.py b/acme/client.py index 73c962581..1c0975849 100644 --- a/acme/client.py +++ b/acme/client.py @@ -542,7 +542,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes def revoke(self, cert): """Revoke certificate. - :param .ComparableX509 body: `M2Crypto.X509.X509` wrapped in + :param .ComparableX509 cert: `M2Crypto.X509.X509` wrapped in `.ComparableX509` :raises .ClientError: If revocation is unsuccessful. From e0a1e8f4e819cb1fad4d5760591264d03f2fb1ed Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 2 Jun 2015 19:08:17 +0000 Subject: [PATCH 59/70] JSONDeSerializable acme.messages.Resource. Provides API necessary to implement JSON-based account storage as described at https://github.com/letsencrypt/lets-encrypt-preview/pull/362#issuecomment-97946817 --- acme/client.py | 1 + acme/client_test.py | 32 +++++------- acme/messages.py | 118 +++++++++++++++++++++++++++++++----------- acme/messages_test.py | 99 ++++++++++++++++++++++++++++++++++- 4 files changed, 197 insertions(+), 53 deletions(-) diff --git a/acme/client.py b/acme/client.py index 629048d03..43c659bb8 100644 --- a/acme/client.py +++ b/acme/client.py @@ -466,6 +466,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes updated_authzr, response = self.poll(updated[authzr]) updated[authzr] = updated_authzr + # pylint: disable=no-member if updated_authzr.body.status != messages.STATUS_VALID: # push back to the priority queue, with updated retry_after heapq.heappush(waiting, (self.retry_after( diff --git a/acme/client_test.py b/acme/client_test.py index 5e4cc1720..7f09f8bdf 100644 --- a/acme/client_test.py +++ b/acme/client_test.py @@ -5,7 +5,6 @@ import os import pkg_resources import unittest -import M2Crypto import mock import requests @@ -14,16 +13,9 @@ from acme import errors from acme import jose from acme import jws as acme_jws from acme import messages +from acme import messages_test -CERT = jose.ComparableX509(M2Crypto.X509.load_cert_string( - pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'cert.der')), - M2Crypto.X509.FORMAT_DER)) -CSR = jose.ComparableX509(M2Crypto.X509.load_request_string( - pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'csr.der')), - M2Crypto.X509.FORMAT_DER)) KEY = jose.JWKRSA.load(pkg_resources.resource_string( 'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))) KEY2 = jose.JWKRSA.load(pkg_resources.resource_string( @@ -82,7 +74,7 @@ class ClientTest(unittest.TestCase): # Request issuance self.certr = messages.CertificateResource( - body=CERT, authzrs=(self.authzr,), + body=messages_test.CERT, authzrs=(self.authzr,), uri='https://www.letsencrypt-demo.org/acme/cert/1', cert_chain_uri='https://www.letsencrypt-demo.org/ca') @@ -380,27 +372,27 @@ class ClientTest(unittest.TestCase): self.assertRaises(errors.UnexpectedUpdate, self.net.poll, self.authzr) def test_request_issuance(self): - self.response.content = CERT.as_der() + self.response.content = messages_test.CERT.as_der() self.response.headers['Location'] = self.certr.uri self.response.links['up'] = {'url': self.certr.cert_chain_uri} self._mock_post_get() - self.assertEqual( - self.certr, self.net.request_issuance(CSR, (self.authzr,))) + self.assertEqual(self.certr, self.net.request_issuance( + messages_test.CSR, (self.authzr,))) # TODO: check POST args def test_request_issuance_missing_up(self): - self.response.content = CERT.as_der() + self.response.content = messages_test.CERT.as_der() self.response.headers['Location'] = self.certr.uri self._mock_post_get() self.assertEqual( self.certr.update(cert_chain_uri=None), - self.net.request_issuance(CSR, (self.authzr,))) + self.net.request_issuance(messages_test.CSR, (self.authzr,))) def test_request_issuance_missing_location(self): self._mock_post_get() self.assertRaises( errors.ClientError, self.net.request_issuance, - CSR, (self.authzr,)) + messages_test.CSR, (self.authzr,)) @mock.patch('acme.client.datetime') @mock.patch('acme.client.time') @@ -484,10 +476,10 @@ class ClientTest(unittest.TestCase): def test_check_cert(self): self.response.headers['Location'] = self.certr.uri - self.response.content = CERT.as_der() + self.response.content = messages_test.CERT.as_der() self._mock_post_get() - self.assertEqual( - self.certr.update(body=CERT), self.net.check_cert(self.certr)) + self.assertEqual(self.certr.update(body=messages_test.CERT), + self.net.check_cert(self.certr)) # TODO: split here and separate test self.response.headers['Location'] = 'foo' @@ -495,7 +487,7 @@ class ClientTest(unittest.TestCase): errors.UnexpectedUpdate, self.net.check_cert, self.certr) def test_check_cert_missing_location(self): - self.response.content = CERT.as_der() + self.response.content = messages_test.CERT.as_der() self._mock_post_get() self.assertRaises(errors.ClientError, self.net.check_cert, self.certr) diff --git a/acme/messages.py b/acme/messages.py index aa041caed..e355e9fd0 100644 --- a/acme/messages.py +++ b/acme/messages.py @@ -51,6 +51,7 @@ class Error(jose.JSONObjectWithFields, Exception): else: return str(self.detail) + class _Constant(jose.JSONDeSerializable): """ACME constant.""" __slots__ = ('name',) @@ -107,31 +108,29 @@ class Identifier(jose.JSONObjectWithFields): value = jose.Field('value') -class Resource(jose.ImmutableMap): +class Resource(jose.JSONObjectWithFields): """ACME Resource. + :ivar str uri: Location of the resource. :ivar acme.messages.ResourceBody body: Resource body. + + """ + body = jose.Field('body') + + +class ResourceWithURI(Resource): + """ACME Resource with URI. + :ivar str uri: Location of the resource. """ - __slots__ = ('body', 'uri') + uri = jose.Field('uri') # no ChallengeResource.uri class ResourceBody(jose.JSONObjectWithFields): """ACME Resource Body.""" -class RegistrationResource(Resource): - """Registration Resource. - - :ivar acme.messages.Registration body: - :ivar str new_authzr_uri: URI found in the 'next' ``Link`` header - :ivar str terms_of_service: URL for the CA TOS. - - """ - __slots__ = ('body', 'uri', 'new_authzr_uri', 'terms_of_service') - - class Registration(ResourceBody): """Registration Resource Body. @@ -146,21 +145,59 @@ class Registration(ResourceBody): recovery_token = jose.Field('recoveryToken', omitempty=True) agreement = jose.Field('agreement', omitempty=True) + phone_prefix = 'tel:' + email_prefix = 'mailto:' -class ChallengeResource(Resource, jose.JSONObjectWithFields): - """Challenge Resource. + @classmethod + def from_data(cls, phone=None, email=None, **kwargs): + """Create registration resource from contact detauls.""" + details = list(kwargs.pop('contact', ())) + if phone is not None: + details.append(cls.phone_prefix + phone) + if email is not None: + details.append(cls.email_prefix + email) + kwargs['contact'] = tuple(details) + return cls(**kwargs) - :ivar acme.messages.ChallengeBody body: - :ivar str authzr_uri: URI found in the 'up' ``Link`` header. - - """ - __slots__ = ('body', 'authzr_uri') + def _filter_contact(self, prefix): + return tuple( + detail[len(prefix):] for detail in self.contact + if detail.startswith(prefix)) @property - def uri(self): # pylint: disable=missing-docstring,no-self-argument - # bug? 'method already defined line None' - # pylint: disable=function-redefined - return self.body.uri + def phones(self): + """All phones found in the ``contact`` field.""" + return self._filter_contact(self.phone_prefix) + + @property + def emails(self): + """All emails found in the ``contact`` field.""" + return self._filter_contact(self.email_prefix) + + @property + def phone(self): + """Phone.""" + assert len(self.phones) == 1 + return self.phones[0] + + @property + def email(self): + """Email.""" + assert len(self.emails) == 1 + return self.emails[0] + + +class RegistrationResource(ResourceWithURI): + """Registration Resource. + + :ivar acme.messages.Registration body: + :ivar str new_authzr_uri: URI found in the 'next' ``Link`` header + :ivar str terms_of_service: URL for the CA TOS. + + """ + body = jose.Field('body', decoder=Registration.from_json) + new_authzr_uri = jose.Field('new_authzr_uri') + terms_of_service = jose.Field('terms_of_service', omitempty=True) class ChallengeBody(ResourceBody): @@ -199,14 +236,21 @@ class ChallengeBody(ResourceBody): return getattr(self.chall, name) -class AuthorizationResource(Resource): - """Authorization Resource. +class ChallengeResource(Resource, jose.JSONObjectWithFields): + """Challenge Resource. - :ivar acme.messages.Authorization body: - :ivar str new_cert_uri: URI found in the 'next' ``Link`` header + :ivar acme.messages.ChallengeBody body: + :ivar str authzr_uri: URI found in the 'up' ``Link`` header. """ - __slots__ = ('body', 'uri', 'new_cert_uri') + body = jose.Field('body', decoder=ChallengeBody.from_json) + authzr_uri = jose.Field('authzr_uri') + + @property + def uri(self): # pylint: disable=missing-docstring,no-self-argument + # bug? 'method already defined line None' + # pylint: disable=function-redefined + return self.body.uri # pylint: disable=no-member class Authorization(ResourceBody): @@ -244,6 +288,17 @@ class Authorization(ResourceBody): for combo in self.combinations) +class AuthorizationResource(ResourceWithURI): + """Authorization Resource. + + :ivar acme.messages.Authorization body: + :ivar str new_cert_uri: URI found in the 'next' ``Link`` header + + """ + body = jose.Field('body', decoder=Authorization.from_json) + new_cert_uri = jose.Field('new_cert_uri') + + class CertificateRequest(jose.JSONObjectWithFields): """ACME new-cert request. @@ -256,7 +311,7 @@ class CertificateRequest(jose.JSONObjectWithFields): authorizations = jose.Field('authorizations', decoder=tuple) -class CertificateResource(Resource): +class CertificateResource(ResourceWithURI): """Certificate Resource. :ivar acme.jose.util.ComparableX509 body: @@ -265,7 +320,8 @@ class CertificateResource(Resource): :ivar tuple authzrs: `tuple` of `AuthorizationResource`. """ - __slots__ = ('body', 'uri', 'cert_chain_uri', 'authzrs') + cert_chain_uri = jose.Field('cert_chain_uri') + authzrs = jose.Field('authzrs') class Revocation(jose.JSONObjectWithFields): diff --git a/acme/messages_test.py b/acme/messages_test.py index 4f86d7809..6749f55dc 100644 --- a/acme/messages_test.py +++ b/acme/messages_test.py @@ -4,14 +4,23 @@ import os import pkg_resources import unittest +from Crypto.PublicKey import RSA +import M2Crypto import mock import pytz -from Crypto.PublicKey import RSA from acme import challenges from acme import jose +CERT = jose.ComparableX509(M2Crypto.X509.load_cert_string( + pkg_resources.resource_string( + 'acme.jose', os.path.join('testdata', 'cert.der')), + M2Crypto.X509.FORMAT_DER)) +CSR = jose.ComparableX509(M2Crypto.X509.load_request_string( + pkg_resources.resource_string( + 'acme.jose', os.path.join('testdata', 'csr.der')), + M2Crypto.X509.FORMAT_DER)) KEY = jose.util.HashableRSAKey(RSA.importKey(pkg_resources.resource_string( 'acme.jose', os.path.join('testdata', 'rsa512_key.pem')))) @@ -94,12 +103,16 @@ class ConstantTest(unittest.TestCase): self.assertTrue(self.const_a != self.const_b) self.assertFalse(self.const_a != const_a_prime) + class RegistrationTest(unittest.TestCase): """Tests for acme.messages.Registration.""" def setUp(self): key = jose.jwk.JWKRSA(key=KEY.publickey()) - contact = ('mailto:letsencrypt-client@letsencrypt.org',) + contact = ( + 'mailto:admin@foo.com', + 'tel:1234', + ) recovery_token = 'XYZ' agreement = 'https://letsencrypt.org/terms' @@ -117,6 +130,26 @@ class RegistrationTest(unittest.TestCase): self.jobj_from = self.jobj_to.copy() self.jobj_from['key'] = key.to_json() + def test_from_data(self): + from acme.messages import Registration + reg = Registration.from_data(phone='1234', email='admin@foo.com') + self.assertEqual(reg.contact, ( + 'tel:1234', + 'mailto:admin@foo.com', + )) + + def test_phones(self): + self.assertEqual(('1234',), self.reg.phones) + + def test_emails(self): + self.assertEqual(('admin@foo.com',), self.reg.emails) + + def test_phone(self): + self.assertEqual('1234', self.reg.phone) + + def test_email(self): + self.assertEqual('admin@foo.com', self.reg.email) + def test_to_partial_json(self): self.assertEqual(self.jobj_to, self.reg.to_partial_json()) @@ -129,6 +162,25 @@ class RegistrationTest(unittest.TestCase): hash(Registration.from_json(self.jobj_from)) +class RegistrationResourceTest(unittest.TestCase): + """Tests for acme.messages.RegistrationResource.""" + + def setUp(self): + from acme.messages import RegistrationResource + self.regr = RegistrationResource( + body=mock.sentinel.body, uri=mock.sentinel.uri, + new_authzr_uri=mock.sentinel.new_authzr_uri, + terms_of_service=mock.sentinel.terms_of_service) + + def test_to_partial_json(self): + self.assertEqual(self.regr.to_json(), { + 'body': mock.sentinel.body, + 'uri': mock.sentinel.uri, + 'new_authzr_uri': mock.sentinel.new_authzr_uri, + 'terms_of_service': mock.sentinel.terms_of_service, + }) + + class ChallengeResourceTest(unittest.TestCase): """Tests for acme.messages.ChallengeResource.""" @@ -220,6 +272,49 @@ class AuthorizationTest(unittest.TestCase): )) +class AuthorizationResourceTest(unittest.TestCase): + """Tests for acme.messages.AuthorizationResource.""" + + def test_json_de_serializable(self): + from acme.messages import AuthorizationResource + authzr = AuthorizationResource( + uri=mock.sentinel.uri, + body=mock.sentinel.body, + new_cert_uri=mock.sentinel.new_cert_uri, + ) + self.assertTrue(isinstance(authzr, jose.JSONDeSerializable)) + + +class CertificateRequestTest(unittest.TestCase): + """Tests for acme.messages.CertificateRequest.""" + + def setUp(self): + from acme.messages import CertificateRequest + self.req = CertificateRequest(csr=CSR, authorizations=('foo',)) + + def test_json_de_serializable(self): + self.assertTrue(isinstance(self.req, jose.JSONDeSerializable)) + from acme.messages import CertificateRequest + self.assertEqual( + self.req, CertificateRequest.from_json(self.req.to_json())) + + +class CertificateResourceTest(unittest.TestCase): + """Tests for acme.messages.CertificateResourceTest.""" + + def setUp(self): + from acme.messages import CertificateResource + self.certr = CertificateResource( + body=CERT, uri=mock.sentinel.uri, authzrs=(), + cert_chain_uri=mock.sentinel.cert_chain_uri) + + def test_json_de_serializable(self): + self.assertTrue(isinstance(self.certr, jose.JSONDeSerializable)) + from acme.messages import CertificateResource + self.assertEqual( + self.certr, CertificateResource.from_json(self.certr.to_json())) + + class RevocationTest(unittest.TestCase): """Tests for acme.messages.RevocationTest.""" From c5bf2730246bdf6fcea1747eb0b39445deb6c113 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 18 Jun 2015 08:01:28 +0000 Subject: [PATCH 60/70] setup.py: separate install_requires --- setup.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/setup.py b/setup.py index 150dfb24d..ef819f50b 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,61 @@ meta = dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", read_file(init_fn))) readme = read_file(os.path.join(here, 'README.rst')) changes = read_file(os.path.join(here, 'CHANGES.rst')) +# #358: acme, letsencrypt, letsencrypt_apache, letsencrypt_nginx, etc. +# shall be distributed separately. Please make sure to keep the +# dependecy lists up to date: this is being somewhat checked below +# using an assert statement! Separate lists are helpful for OS package +# maintainers. and will make the future migration a lot easier. +acme_install_requires = [ + 'argparse', + #'letsencrypt' # TODO: uses testdata vectors + 'mock', + 'pycrypto', + 'pyrfc3339', + 'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304) + 'pyasn1', # urllib3 InsecurePlatformWarning (#304) + 'pytz', + 'requests', + 'werkzeug', + 'M2Crypto', +] +letsencrypt_install_requires = [ + #'acme', + 'argparse', + 'ConfigArgParse', + 'configobj', + 'M2Crypto', + 'mock', + 'parsedatetime', + 'psutil>=2.1.0', # net_connections introduced in 2.1.0 + 'pycrypto', + # https://pyopenssl.readthedocs.org/en/latest/api/crypto.html#OpenSSL.crypto.X509Req.get_extensions + 'PyOpenSSL>=0.15', + 'pyrfc3339', + 'python-augeas', + 'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280 + 'pytz', + 'requests', + 'zope.component', + 'zope.interface', + 'M2Crypto', +] +letsencrypt_apache_install_requires = [ + #'acme', + #'letsencrypt', + 'mock', + 'python-augeas', + 'zope.component', + 'zope.interface', +] +letsencrypt_nginx_install_requires = [ + #'acme', + #'letsencrypt', + 'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary? + 'mock', + 'zope.interface', +] + install_requires = [ 'argparse', 'ConfigArgParse', @@ -54,6 +109,13 @@ install_requires = [ 'M2Crypto', ] +assert set(install_requires) == set.union(*(set(ireq) for ireq in ( + acme_install_requires, + letsencrypt_install_requires, + letsencrypt_apache_install_requires, + letsencrypt_nginx_install_requires +))), "*install_requires don't match up!" + dev_extras = [ # Pin astroid==1.3.5, pylint==1.4.2 as a workaround for #289 'astroid==1.3.5', From b6ef25e911f8304d7a2a22f615ed7a0187eb9914 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 22 Jun 2015 21:06:08 +0000 Subject: [PATCH 61/70] Fix review comments (typo, inheritance fix). --- acme/messages.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme/messages.py b/acme/messages.py index cd5a65f79..c6d15bbf1 100644 --- a/acme/messages.py +++ b/acme/messages.py @@ -152,7 +152,7 @@ class Registration(ResourceBody): @classmethod def from_data(cls, phone=None, email=None, **kwargs): - """Create registration resource from contact detauls.""" + """Create registration resource from contact details.""" details = list(kwargs.pop('contact', ())) if phone is not None: details.append(cls.phone_prefix + phone) @@ -238,7 +238,7 @@ class ChallengeBody(ResourceBody): return getattr(self.chall, name) -class ChallengeResource(Resource, jose.JSONObjectWithFields): +class ChallengeResource(Resource): """Challenge Resource. :ivar acme.messages.ChallengeBody body: From e17bd684bb0e3d66650ad264f9c37950def75b76 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 2 Jun 2015 07:55:22 +0000 Subject: [PATCH 62/70] Debug log received response for GET/POST --- acme/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/acme/client.py b/acme/client.py index 1c0975849..7e0e3ebd1 100644 --- a/acme/client.py +++ b/acme/client.py @@ -77,6 +77,9 @@ class Client(object): # pylint: disable=too-many-instance-attributes :raises .ClientError: In case of other networking errors. """ + logging.debug('Received response %s (headers: %s): %r', + response, response.headers, response.content) + response_ct = response.headers.get('Content-Type') try: # TODO: response.json() is called twice, once here, and @@ -169,7 +172,6 @@ class Client(object): # pylint: disable=too-many-instance-attributes response = requests.post(uri, data=data, **kwargs) except requests.exceptions.RequestException as error: raise errors.ClientError(error) - logging.debug('Received response %s: %r', response, response.text) self._add_nonce(response) self._check_response(response, content_type=content_type) From 28f5c7d6665b004290e99aad5a51fcdc73edec95 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 22 Jun 2015 10:11:59 +0000 Subject: [PATCH 63/70] logs: collate omitted empty fields --- acme/jose/json_util.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/acme/jose/json_util.py b/acme/jose/json_util.py index a08145459..f38ebc62f 100644 --- a/acme/jose/json_util.py +++ b/acme/jose/json_util.py @@ -218,11 +218,12 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable): def fields_to_partial_json(self): """Serialize fields to JSON.""" jobj = {} + omitted = set() for slot, field in self._fields.iteritems(): value = getattr(self, slot) if field.omit(value): - logging.debug('Omitting empty field "%s" (%s)', slot, value) + omitted.add((slot, value)) else: try: jobj[field.json_name] = field.encode(value) @@ -230,6 +231,10 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable): raise errors.SerializationError( 'Could not encode {0} ({1}): {2}'.format( slot, value, error)) + if omitted: + # pylint: disable=star-args + logging.debug('Omitted empty fields: %s', ', '.join( + '{0!s}={1!r}'.format(*field) for field in omitted)) return jobj def to_partial_json(self): From 8e39a3a0ef530b35ee028b0902deeca7c29f369c Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 13 Jun 2015 14:36:51 +0000 Subject: [PATCH 64/70] Collate multi-line logs, use logging.exception, other fixes. --- acme/client.py | 2 -- letsencrypt/account.py | 2 +- letsencrypt/augeas_configurator.py | 19 ++++++++----------- letsencrypt/auth_handler.py | 8 +------- letsencrypt/crypto_util.py | 2 +- letsencrypt_apache/configurator.py | 21 ++++++++------------- letsencrypt_apache/dvsni.py | 7 +++---- letsencrypt_nginx/configurator.py | 16 +++++----------- letsencrypt_nginx/dvsni.py | 4 ++-- 9 files changed, 29 insertions(+), 52 deletions(-) diff --git a/acme/client.py b/acme/client.py index 7e0e3ebd1..fe4c42799 100644 --- a/acme/client.py +++ b/acme/client.py @@ -95,8 +95,6 @@ class Client(object): # pylint: disable=too-many-instance-attributes 'Ignoring wrong Content-Type (%r) for JSON Error', response_ct) try: - logging.error("Error: %s", jobj) - logging.error("Response from server: %s", response.content) raise messages.Error.from_json(jobj) except jose.DeserializationError as error: # Couldn't deserialize JSON object diff --git a/letsencrypt/account.py b/letsencrypt/account.py index 9f351387f..a97e07504 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -227,5 +227,5 @@ class Account(object): if cls.EMAIL_REGEX.match(email): return not email.startswith(".") and ".." not in email else: - logging.warn("Invalid email address.") + logging.warn("Invalid email address: %s.", email) return False diff --git a/letsencrypt/augeas_configurator.py b/letsencrypt/augeas_configurator.py index c59d755c2..a375b2e17 100644 --- a/letsencrypt/augeas_configurator.py +++ b/letsencrypt/augeas_configurator.py @@ -52,10 +52,10 @@ class AugeasConfigurator(common.Plugin): lens_path = self.aug.get(path + "/lens") # As aug.get may return null if lens_path and lens in lens_path: - # Strip off /augeas/files and /error - logging.error("There has been an error in parsing the file: %s", - path[13:len(path) - 6]) - logging.error(self.aug.get(path + "/message")) + logging.error( + "There has been an error in parsing the file (%s): %s", + # Strip off /augeas/files and /error + path[13:len(path) - 6], self.aug.get(path + "/message")) def save(self, title=None, temporary=False): """Saves all changes to the configuration files. @@ -122,13 +122,10 @@ class AugeasConfigurator(common.Plugin): # Check for the root of save problems new_errs = self.aug.match("/augeas//error") # logging.error("During Save - %s", mod_conf) - # Only print new errors caused by recent save - for err in new_errs: - if err not in ex_errs: - logging.error( - "Unable to save file - %s", err[13:len(err) - 6]) - logging.error("Attempted Save Notes") - logging.error(self.save_notes) + logging.error("Unable to save files: %s. Attempted Save Notes: %s", + ", ".join(err[13:len(err) - 6] for err in new_errs + # Only new errors caused by recent save + if err not in ex_errs), self.save_notes) # Wrapper functions for Reverter class def recovery_routine(self): diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index d895c165c..50a66c0d0 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -333,28 +333,22 @@ def challb_to_achall(challb, key, domain): """ chall = challb.chall + logging.info("%s challenge for %s", chall.typ, domain) if isinstance(chall, challenges.DVSNI): - logging.info(" DVSNI challenge for %s.", domain) return achallenges.DVSNI( challb=challb, domain=domain, key=key) elif isinstance(chall, challenges.SimpleHTTP): - logging.info(" SimpleHTTP challenge for %s.", domain) return achallenges.SimpleHTTP( challb=challb, domain=domain, key=key) elif isinstance(chall, challenges.DNS): - logging.info(" DNS challenge for %s.", domain) return achallenges.DNS(challb=challb, domain=domain) - elif isinstance(chall, challenges.RecoveryToken): - logging.info(" Recovery Token Challenge for %s.", domain) return achallenges.RecoveryToken(challb=challb, domain=domain) elif isinstance(chall, challenges.RecoveryContact): - logging.info(" Recovery Contact Challenge for %s.", domain) return achallenges.RecoveryContact( challb=challb, domain=domain) elif isinstance(chall, challenges.ProofOfPossession): - logging.info(" Proof-of-Possession Challenge for %s", domain) return achallenges.ProofOfPossession( challb=challb, domain=domain) diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index 1eb565289..7e1bb58fb 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -40,7 +40,7 @@ def init_save_key(key_size, key_dir, keyname="key-letsencrypt.pem"): try: key_pem = make_key(key_size) except ValueError as err: - logging.fatal(str(err)) + logging.exception(err) raise err # Save file diff --git a/letsencrypt_apache/configurator.py b/letsencrypt_apache/configurator.py index 0cff94bbd..5b0dbdea9 100644 --- a/letsencrypt_apache/configurator.py +++ b/letsencrypt_apache/configurator.py @@ -181,8 +181,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if not path["cert_path"] or not path["cert_key"]: # Throw some can't find all of the directives error" logging.warn( - "Cannot find a cert or key directive in %s", vhost.path) - logging.warn("VirtualHost was not modified") + "Cannot find a cert or key directive in %s. " + "VirtualHost was not modified", vhost.path) # Presumably break here so that the virtualhost is not modified return False @@ -408,8 +408,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Note: This could be made to also look for ip:443 combo # TODO: Need to search only open directives and IfMod mod_ssl.c if len(self.parser.find_dir(parser.case_i("Listen"), "443")) == 0: - logging.debug("No Listen 443 directive found") - logging.debug("Setting the Apache Server to Listen on port 443") + logging.debug("No Listen 443 directive found. Setting the " + "Apache Server to Listen on port 443") path = self.parser.add_dir_to_ifmodssl( parser.get_aug_path(self.parser.loc["listen"]), "Listen", "443") self.save_notes += "Added Listen 443 directive to %s\n" % path @@ -922,9 +922,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if proc.returncode != 0: # Enter recovery routine... - logging.error("Configtest failed") - logging.error(stdout) - logging.error(stderr) + logging.error("Configtest failed\n%s\n%s", stdout, stderr) return False return True @@ -1054,9 +1052,8 @@ def enable_mod(mod_name, apache_init_script, apache_enmod): stdout=open("/dev/null", "w"), stderr=open("/dev/null", "w")) apache_restart(apache_init_script) - except (OSError, subprocess.CalledProcessError) as err: - logging.error("Error enabling mod_%s", mod_name) - logging.error("Exception: %s", err) + except (OSError, subprocess.CalledProcessError): + logging.exception("Error enabling mod_%s", mod_name) sys.exit(1) @@ -1119,9 +1116,7 @@ def apache_restart(apache_init_script): if proc.returncode != 0: # Enter recovery routine... - logging.error("Apache Restart Failed!") - logging.error(stdout) - logging.error(stderr) + logging.error("Apache Restart Failed!\n%s\n%s", stdout, stderr) return False except (OSError, ValueError): diff --git a/letsencrypt_apache/dvsni.py b/letsencrypt_apache/dvsni.py index c25426371..5ff09aa50 100644 --- a/letsencrypt_apache/dvsni.py +++ b/letsencrypt_apache/dvsni.py @@ -59,10 +59,9 @@ class ApacheDvsni(common.Dvsni): vhost = self.configurator.choose_vhost(achall.domain) if vhost is None: logging.error( - "No vhost exists with servername or alias of: %s", - achall.domain) - logging.error("No _default_:443 vhost exists") - logging.error("Please specify servernames in the Apache config") + "No vhost exists with servername or alias of: %s. " + "No _default_:443 vhost exists. Please specify servernames " + "in the Apache config", achall.domain) return None # TODO - @jdkasten review this code to make sure it makes sense diff --git a/letsencrypt_nginx/configurator.py b/letsencrypt_nginx/configurator.py index f7b53f3fa..f74ad0a3a 100644 --- a/letsencrypt_nginx/configurator.py +++ b/letsencrypt_nginx/configurator.py @@ -130,9 +130,8 @@ class NginxConfigurator(common.Plugin): vhost.filep, vhost.names) except errors.LetsEncryptMisconfigurationError: logging.warn( - "Cannot find a cert or key directive in %s for %s", - vhost.filep, vhost.names) - logging.warn("VirtualHost was not modified") + "Cannot find a cert or key directive in %s for %s. " + "VirtualHost was not modified.", vhost.filep, vhost.names) # Presumably break here so that the virtualhost is not modified return False @@ -352,9 +351,7 @@ class NginxConfigurator(common.Plugin): if proc.returncode != 0: # Enter recovery routine... - logging.error("Config test failed") - logging.error(stdout) - logging.error(stderr) + logging.error("Config test failed\n%s\n%s", stdout, stderr) return False return True @@ -570,14 +567,11 @@ def nginx_restart(nginx_ctl): if nginx_proc.returncode != 0: # Enter recovery routine... - logging.error("Nginx Restart Failed!") - logging.error(stdout) - logging.error(stderr) + logging.error("Nginx Restart Failed!\n%s\n%s", stdout, stderr) return False except (OSError, ValueError): - logging.fatal( - "Nginx Restart Failed - Please Check the Configuration") + logging.fatal("Nginx Restart Failed - Please Check the Configuration") sys.exit(1) return True diff --git a/letsencrypt_nginx/dvsni.py b/letsencrypt_nginx/dvsni.py index 0697f6e1e..f6f82c5cb 100644 --- a/letsencrypt_nginx/dvsni.py +++ b/letsencrypt_nginx/dvsni.py @@ -50,9 +50,9 @@ class NginxDvsni(common.Dvsni): vhost = self.configurator.choose_vhost(achall.domain) if vhost is None: logging.error( - "No nginx vhost exists with server_name matching: %s", + "No nginx vhost exists with server_name matching: %s. " + "Please specify server_names in the Nginx config.", achall.domain) - logging.error("Please specify server_names in the Nginx config") return None for addr in vhost.addrs: From cfa7e281065419227454ee2dc1b90d7bdf8d0419 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 12 Jun 2015 14:10:39 +0000 Subject: [PATCH 65/70] errors.LetsEncrypt -> errors. (fixes: #487) --- letsencrypt/account.py | 8 +-- letsencrypt/auth_handler.py | 4 +- letsencrypt/cli.py | 6 +-- letsencrypt/client.py | 22 ++++---- letsencrypt/continuity_auth.py | 4 +- letsencrypt/display/enhancements.py | 4 +- letsencrypt/errors.py | 20 +++---- letsencrypt/interfaces.py | 4 +- letsencrypt/le_util.py | 4 +- letsencrypt/plugins/disco.py | 6 +-- letsencrypt/plugins/disco_test.py | 8 +-- letsencrypt/reverter.py | 52 +++++++++---------- letsencrypt/revoker.py | 12 ++--- letsencrypt/tests/account_test.py | 6 +-- letsencrypt/tests/auth_handler_test.py | 2 +- letsencrypt/tests/continuity_auth_test.py | 4 +- .../tests/display/enhancements_test.py | 2 +- letsencrypt/tests/le_util_test.py | 2 +- letsencrypt/tests/reverter_test.py | 40 +++++++------- letsencrypt/tests/revoker_test.py | 12 ++--- letsencrypt_apache/configurator.py | 18 +++---- letsencrypt_apache/parser.py | 2 +- letsencrypt_apache/tests/configurator_test.py | 6 +-- letsencrypt_apache/tests/parser_test.py | 2 +- letsencrypt_nginx/configurator.py | 18 +++---- letsencrypt_nginx/dvsni.py | 4 +- letsencrypt_nginx/parser.py | 4 +- letsencrypt_nginx/tests/configurator_test.py | 12 ++--- letsencrypt_nginx/tests/dvsni_test.py | 2 +- letsencrypt_nginx/tests/parser_test.py | 4 +- 30 files changed, 147 insertions(+), 147 deletions(-) diff --git a/letsencrypt/account.py b/letsencrypt/account.py index a97e07504..e6e46d098 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -127,7 +127,7 @@ class Account(object): acc_config = configobj.ConfigObj( infile=config_fp, file_error=True, create_empty=False) except IOError: - raise errors.LetsEncryptClientError( + raise errors.Error( "Account for %s does not exist" % os.path.basename(config_fp)) if os.path.basename(config_fp) != "default": @@ -191,7 +191,7 @@ class Account(object): if code == display_util.OK: try: return cls.from_email(config, email) - except errors.LetsEncryptClientError: + except errors.Error: continue else: return None @@ -205,7 +205,7 @@ class Account(object): :param str email: Email address - :raises letsencrypt.errors.LetsEncryptClientError: If invalid + :raises letsencrypt.errors.Error: If invalid email address is given. """ @@ -219,7 +219,7 @@ class Account(object): cls._get_config_filename(email)) return cls(config, key, email) - raise errors.LetsEncryptClientError("Invalid email address.") + raise errors.Error("Invalid email address.") @classmethod def safe_email(cls, email): diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 50a66c0d0..a86cdd69d 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -296,7 +296,7 @@ class AuthHandler(object): :class:`letsencrypt.achallenges.Indexed` :rtype: tuple - :raises errors.LetsEncryptClientError: If Challenge type is not + :raises errors.Error: If Challenge type is not recognized """ @@ -353,7 +353,7 @@ def challb_to_achall(challb, key, domain): challb=challb, domain=domain) else: - raise errors.LetsEncryptClientError( + raise errors.Error( "Received unsupported challenge of type: %s", chall.typ) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 3bdf2bfc6..f04727706 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -41,11 +41,11 @@ def _account_init(args, config): # The way to get the default would be args.email = "" # First try existing account return account.Account.from_existing_account(config, args.email) - except errors.LetsEncryptClientError: + except errors.Error: try: # Try to make an account based on the email address return account.Account.from_email(config, args.email) - except errors.LetsEncryptClientError: + except errors.Error: return None @@ -68,7 +68,7 @@ def _common_run(args, config, acc, authenticator, installer): if acc.regr is None: try: acme.register() - except errors.LetsEncryptClientError: + except errors.Error: sys.exit("Unable to register an account with ACME server") return acme, doms diff --git a/letsencrypt/client.py b/letsencrypt/client.py index d059a777e..e29064a56 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -96,7 +96,7 @@ class Client(object): self.account.regr = self.network.agree_to_tos(self.account.regr) else: # What is the proper response here... - raise errors.LetsEncryptClientError("Must agree to TOS") + raise errors.Error("Must agree to TOS") self.account.save() self._report_new_account() @@ -145,9 +145,9 @@ class Client(object): msg = ("Unable to obtain certificate because authenticator is " "not set.") logging.warning(msg) - raise errors.LetsEncryptClientError(msg) + raise errors.Error(msg) if self.account.regr is None: - raise errors.LetsEncryptClientError( + raise errors.Error( "Please register with the ACME server first.") # Perform Challenges/Get Authorizations @@ -310,7 +310,7 @@ class Client(object): if self.installer is None: logging.warning("No installer specified, client is unable to deploy" "the certificate") - raise errors.LetsEncryptClientError("No installer available") + raise errors.Error("No installer available") chain_path = None if chain_path is None else os.path.abspath(chain_path) @@ -339,14 +339,14 @@ class Client(object): :param redirect: If traffic should be forwarded from HTTP to HTTPS. :type redirect: bool or None - :raises letsencrypt.errors.LetsEncryptClientError: if + :raises letsencrypt.errors.Error: if no installer is specified in the client. """ if self.installer is None: logging.warning("No installer is specified, there isn't any " "configuration to enhance.") - raise errors.LetsEncryptClientError("No installer available") + raise errors.Error("No installer available") if redirect is None: redirect = enhancements.ask("redirect") @@ -364,7 +364,7 @@ class Client(object): for dom in domains: try: self.installer.enhance(dom, "redirect") - except errors.LetsEncryptConfiguratorError: + except errors.ConfiguratorError: logging.warn("Unable to perform redirect for %s", dom) self.installer.save("Add Redirects") @@ -386,7 +386,7 @@ def validate_key_csr(privkey, csr=None): :param csr: CSR :type csr: :class:`letsencrypt.le_util.CSR` - :raises letsencrypt.errors.LetsEncryptClientError: when + :raises letsencrypt.errors.Error: when validation fails """ @@ -396,7 +396,7 @@ def validate_key_csr(privkey, csr=None): # Key must be readable and valid. if privkey.pem and not crypto_util.valid_privkey(privkey.pem): - raise errors.LetsEncryptClientError( + raise errors.Error( "The provided key is not a valid key") if csr: @@ -406,7 +406,7 @@ def validate_key_csr(privkey, csr=None): # If CSR is provided, it must be readable and valid. if csr.data and not crypto_util.valid_csr(csr.data): - raise errors.LetsEncryptClientError( + raise errors.Error( "The provided CSR is not a valid CSR") # If both CSR and key are provided, the key must be the same key used @@ -414,7 +414,7 @@ def validate_key_csr(privkey, csr=None): if csr.data and privkey.pem: if not crypto_util.csr_matches_pubkey( csr.data, privkey.pem): - raise errors.LetsEncryptClientError( + raise errors.Error( "The key and CSR do not match") diff --git a/letsencrypt/continuity_auth.py b/letsencrypt/continuity_auth.py index 739e33d43..2eb1c22bf 100644 --- a/letsencrypt/continuity_auth.py +++ b/letsencrypt/continuity_auth.py @@ -52,7 +52,7 @@ class ContinuityAuthenticator(object): elif isinstance(achall, achallenges.RecoveryToken): responses.append(self.rec_token.perform(achall)) else: - raise errors.LetsEncryptContAuthError("Unexpected Challenge") + raise errors.ContAuthError("Unexpected Challenge") return responses def cleanup(self, achalls): @@ -61,4 +61,4 @@ class ContinuityAuthenticator(object): if isinstance(achall, achallenges.RecoveryToken): self.rec_token.cleanup(achall) elif not isinstance(achall, achallenges.ProofOfPossession): - raise errors.LetsEncryptContAuthError("Unexpected Challenge") + raise errors.ContAuthError("Unexpected Challenge") diff --git a/letsencrypt/display/enhancements.py b/letsencrypt/display/enhancements.py index 48f168441..7855b7fba 100644 --- a/letsencrypt/display/enhancements.py +++ b/letsencrypt/display/enhancements.py @@ -21,7 +21,7 @@ def ask(enhancement): :returns: True if feature is desired, False otherwise :rtype: bool - :raises letsencrypt.errors.LetsEncryptClientError: If + :raises letsencrypt.errors.Error: If the enhancement provided is not supported. """ @@ -30,7 +30,7 @@ def ask(enhancement): return DISPATCH[enhancement]() except KeyError: logging.error("Unsupported enhancement given to ask(): %s", enhancement) - raise errors.LetsEncryptClientError("Unsupported Enhancement") + raise errors.Error("Unsupported Enhancement") def redirect_by_default(): diff --git a/letsencrypt/errors.py b/letsencrypt/errors.py index d9078dbf2..85f4a69d9 100644 --- a/letsencrypt/errors.py +++ b/letsencrypt/errors.py @@ -1,44 +1,44 @@ """Let's Encrypt client errors.""" -class LetsEncryptClientError(Exception): +class Error(Exception): """Generic Let's Encrypt client error.""" -class LetsEncryptReverterError(LetsEncryptClientError): +class ReverterError(Error): """Let's Encrypt Reverter error.""" # Auth Handler Errors -class AuthorizationError(LetsEncryptClientError): +class AuthorizationError(Error): """Authorization error.""" -class LetsEncryptContAuthError(AuthorizationError): +class ContAuthError(AuthorizationError): """Let's Encrypt Continuity Authenticator error.""" -class LetsEncryptDvAuthError(AuthorizationError): +class DvAuthError(AuthorizationError): """Let's Encrypt DV Authenticator error.""" # Authenticator - Challenge specific errors -class LetsEncryptDvsniError(LetsEncryptDvAuthError): +class DvsniError(DvAuthError): """Let's Encrypt DVSNI error.""" # Configurator Errors -class LetsEncryptConfiguratorError(LetsEncryptClientError): +class ConfiguratorError(Error): """Let's Encrypt Configurator error.""" -class LetsEncryptNoInstallationError(LetsEncryptConfiguratorError): +class NoInstallationError(ConfiguratorError): """Let's Encrypt No Installation error.""" -class LetsEncryptMisconfigurationError(LetsEncryptConfiguratorError): +class MisconfigurationError(ConfiguratorError): """Let's Encrypt Misconfiguration error.""" -class LetsEncryptRevokerError(LetsEncryptClientError): +class RevokerError(Error): """Let's Encrypt Revoker error.""" diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index d2a420d00..539683d30 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -68,10 +68,10 @@ class IPlugin(zope.interface.Interface): Finish up any additional initialization. - :raises letsencrypt.errors.LetsEncryptMisconfigurationError: + :raises letsencrypt.errors.MisconfigurationError: when full initialization cannot be completed. Plugin will be displayed on a list of available plugins. - :raises letsencrypt.errors.LetsEncryptNoInstallationError: + :raises letsencrypt.errors.NoInstallationError: when the necessary programs/files cannot be located. Plugin will NOT be displayed on a list of available plugins. diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index ba2427c79..2b0a4a495 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -19,7 +19,7 @@ def make_or_verify_dir(directory, mode=0o755, uid=0): :param int mode: Directory mode. :param int uid: Directory owner. - :raises LetsEncryptClientError: if a directory already exists, + :raises Error: if a directory already exists, but has wrong permissions or owner :raises OSError: if invalid or inaccessible file names and @@ -32,7 +32,7 @@ def make_or_verify_dir(directory, mode=0o755, uid=0): except OSError as exception: if exception.errno == errno.EEXIST: if not check_permissions(directory, mode, uid): - raise errors.LetsEncryptClientError( + raise errors.Error( "%s exists, but does not have the proper " "permissions or owner" % directory) else: diff --git a/letsencrypt/plugins/disco.py b/letsencrypt/plugins/disco.py index d70dfc751..229e152e2 100644 --- a/letsencrypt/plugins/disco.py +++ b/letsencrypt/plugins/disco.py @@ -89,10 +89,10 @@ class PluginEntryPoint(object): if self._prepared is None: try: self._initialized.prepare() - except errors.LetsEncryptMisconfigurationError as error: + except errors.MisconfigurationError as error: logging.debug("Misconfigured %r: %s", self, error) self._prepared = error - except errors.LetsEncryptNoInstallationError as error: + except errors.NoInstallationError as error: logging.debug("No installation (%r): %s", self, error) self._prepared = error else: @@ -103,7 +103,7 @@ class PluginEntryPoint(object): def misconfigured(self): """Is plugin misconfigured?""" return isinstance( - self._prepared, errors.LetsEncryptMisconfigurationError) + self._prepared, errors.MisconfigurationError) @property def available(self): diff --git a/letsencrypt/plugins/disco_test.py b/letsencrypt/plugins/disco_test.py index 0dd65e5de..1cd74385e 100644 --- a/letsencrypt/plugins/disco_test.py +++ b/letsencrypt/plugins/disco_test.py @@ -124,22 +124,22 @@ class PluginEntryPointTest(unittest.TestCase): def test_prepare_misconfigured(self): plugin = mock.MagicMock() - plugin.prepare.side_effect = errors.LetsEncryptMisconfigurationError + plugin.prepare.side_effect = errors.MisconfigurationError # pylint: disable=protected-access self.plugin_ep._initialized = plugin self.assertTrue(isinstance(self.plugin_ep.prepare(), - errors.LetsEncryptMisconfigurationError)) + errors.MisconfigurationError)) self.assertTrue(self.plugin_ep.prepared) self.assertTrue(self.plugin_ep.misconfigured) self.assertTrue(self.plugin_ep.available) def test_prepare_no_installation(self): plugin = mock.MagicMock() - plugin.prepare.side_effect = errors.LetsEncryptNoInstallationError + plugin.prepare.side_effect = errors.NoInstallationError # pylint: disable=protected-access self.plugin_ep._initialized = plugin self.assertTrue(isinstance(self.plugin_ep.prepare(), - errors.LetsEncryptNoInstallationError)) + errors.NoInstallationError)) self.assertTrue(self.plugin_ep.prepared) self.assertFalse(self.plugin_ep.misconfigured) self.assertFalse(self.plugin_ep.available) diff --git a/letsencrypt/reverter.py b/letsencrypt/reverter.py index 604c3999a..2743be97e 100644 --- a/letsencrypt/reverter.py +++ b/letsencrypt/reverter.py @@ -30,18 +30,18 @@ class Reverter(object): This function should reinstall the users original configuration files for all saves with temporary=True - :raises letsencrypt.errors.LetsEncryptReverterError: when + :raises letsencrypt.errors.ReverterError: when unable to revert config """ if os.path.isdir(self.config.temp_checkpoint_dir): try: self._recover_checkpoint(self.config.temp_checkpoint_dir) - except errors.LetsEncryptReverterError: + except errors.ReverterError: # We have a partial or incomplete recovery logging.fatal("Incomplete or failed recovery for %s", self.config.temp_checkpoint_dir) - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Unable to revert temporary config") def rollback_checkpoints(self, rollback=1): @@ -50,7 +50,7 @@ class Reverter(object): :param int rollback: Number of checkpoints to reverse. A str num will be cast to an integer. So "2" is also acceptable. - :raises letsencrypt.errors.LetsEncryptReverterError: If + :raises letsencrypt.errors.ReverterError: If there is a problem with the input or if the function is unable to correctly revert the configuration checkpoints. @@ -59,11 +59,11 @@ class Reverter(object): rollback = int(rollback) except ValueError: logging.error("Rollback argument must be a positive integer") - raise errors.LetsEncryptReverterError("Invalid Input") + raise errors.ReverterError("Invalid Input") # Sanity check input if rollback < 0: logging.error("Rollback argument must be a positive integer") - raise errors.LetsEncryptReverterError("Invalid Input") + raise errors.ReverterError("Invalid Input") backups = os.listdir(self.config.backup_dir) backups.sort() @@ -76,9 +76,9 @@ class Reverter(object): cp_dir = os.path.join(self.config.backup_dir, backups.pop()) try: self._recover_checkpoint(cp_dir) - except errors.LetsEncryptReverterError: + except errors.ReverterError: logging.fatal("Failed to load checkpoint during rollback") - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Unable to load checkpoint during rollback") rollback -= 1 @@ -104,7 +104,7 @@ class Reverter(object): for bkup in backups: float(bkup) except ValueError: - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Invalid directories in {0}".format(self.config.backup_dir)) output = [] @@ -162,7 +162,7 @@ class Reverter(object): :param str save_notes: notes about changes made during the save :raises IOError: If unable to open cp_dir + FILEPATHS file - :raises letsencrypt.errors.LetsEncryptReverterError: If + :raises letsencrypt.errors.ReverterError: If unable to add checkpoint """ @@ -191,7 +191,7 @@ class Reverter(object): logging.error( "Unable to add file %s to checkpoint %s", filename, cp_dir) - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Unable to add file {0} to checkpoint " "{1}".format(filename, cp_dir)) idx += 1 @@ -224,7 +224,7 @@ class Reverter(object): :param str cp_dir: checkpoint directory file path - :raises errors.LetsEncryptReverterError: If unable to recover checkpoint + :raises errors.ReverterError: If unable to recover checkpoint """ if os.path.isfile(os.path.join(cp_dir, "FILEPATHS")): @@ -238,7 +238,7 @@ class Reverter(object): except (IOError, OSError): # This file is required in all checkpoints. logging.error("Unable to recover files from %s", cp_dir) - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Unable to recover files from %s" % cp_dir) # Remove any newly added files if they exist @@ -248,7 +248,7 @@ class Reverter(object): shutil.rmtree(cp_dir) except OSError: logging.error("Unable to remove directory: %s", cp_dir) - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Unable to remove directory: %s" % cp_dir) def _check_tempfile_saves(self, save_files): @@ -256,7 +256,7 @@ class Reverter(object): :param set save_files: Set of files about to be saved. - :raises letsencrypt.errors.LetsEncryptReverterError: + :raises letsencrypt.errors.ReverterError: when save is attempting to overwrite a temporary file. """ @@ -277,7 +277,7 @@ class Reverter(object): # Verify no save_file is in protected_files for filename in protected_files: if filename in save_files: - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Attempting to overwrite challenge " "file - %s" % filename) @@ -292,7 +292,7 @@ class Reverter(object): a temp or permanent save. :param \*files: file paths (str) to be registered - :raises letsencrypt.errors.LetsEncryptReverterError: If + :raises letsencrypt.errors.ReverterError: If call does not contain necessary parameters or if the file creation is unable to be registered. @@ -300,7 +300,7 @@ class Reverter(object): # Make sure some files are provided... as this is an error # Made this mistake in my initial implementation of apache.dvsni.py if not files: - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Forgot to provide files to registration call") if temporary: @@ -322,7 +322,7 @@ class Reverter(object): new_fd.write("{0}{1}".format(path, os.linesep)) except (IOError, OSError): logging.error("Unable to register file creation(s) - %s", files) - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Unable to register file creation(s) - {0}".format(files)) finally: if new_fd is not None: @@ -345,12 +345,12 @@ class Reverter(object): if os.path.isdir(self.config.in_progress_dir): try: self._recover_checkpoint(self.config.in_progress_dir) - except errors.LetsEncryptReverterError: + except errors.ReverterError: # We have a partial or incomplete recovery logging.fatal("Incomplete or failed recovery for IN_PROGRESS " "checkpoint - %s", self.config.in_progress_dir) - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Incomplete or failed recovery for IN_PROGRESS checkpoint " "- %s" % self.config.in_progress_dir) @@ -362,7 +362,7 @@ class Reverter(object): :returns: Success :rtype: bool - :raises letsencrypt.errors.LetsEncryptReverterError: If + :raises letsencrypt.errors.ReverterError: If all files within file_list cannot be removed """ @@ -386,7 +386,7 @@ class Reverter(object): except (IOError, OSError): logging.fatal( "Unable to remove filepaths contained within %s", file_list) - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Unable to remove filepaths contained within " "{0}".format(file_list)) @@ -400,7 +400,7 @@ class Reverter(object): :param str title: Title describing checkpoint - :raises letsencrypt.errors.LetsEncryptReverterError: when the + :raises letsencrypt.errors.ReverterError: when the checkpoint is not able to be finalized. """ @@ -426,7 +426,7 @@ class Reverter(object): shutil.move(changes_since_tmp_path, changes_since_path) except (IOError, OSError): logging.error("Unable to finalize checkpoint - adding title") - raise errors.LetsEncryptReverterError("Unable to add title") + raise errors.ReverterError("Unable to add title") self._timestamp_progress_dir() @@ -451,5 +451,5 @@ class Reverter(object): logging.error( "Unable to finalize checkpoint, %s -> %s", self.config.in_progress_dir, final_dir) - raise errors.LetsEncryptReverterError( + raise errors.ReverterError( "Unable to finalize checkpoint renaming") diff --git a/letsencrypt/revoker.py b/letsencrypt/revoker.py index 402157721..66d359a6b 100644 --- a/letsencrypt/revoker.py +++ b/letsencrypt/revoker.py @@ -71,7 +71,7 @@ class Revoker(object): authkey.pem).exportKey("PEM") # https://www.dlitz.net/software/pycrypto/api/current/Crypto.PublicKey.RSA-module.html except (IndexError, ValueError, TypeError): - raise errors.LetsEncryptRevokerError( + raise errors.RevokerError( "Invalid key file specified to revoke_from_key") with open(self.list_path, "rb") as csvfile: @@ -89,7 +89,7 @@ class Revoker(object): # This should never happen given the assumptions of the # module. If it does, it is probably best to delete the # the offending key/cert. For now... just raise an exception - raise errors.LetsEncryptRevokerError( + raise errors.RevokerError( "%s - backup file is corrupted.") if clean_pem == test_pem: @@ -218,7 +218,7 @@ class Revoker(object): if self.no_confirm or revocation.confirm_revocation(cert): try: self._acme_revoke(cert) - except errors.LetsEncryptClientError: + except errors.Error: # TODO: Improve error handling when networking is set... logging.error( "Unable to revoke cert:%s%s", os.linesep, str(cert)) @@ -250,7 +250,7 @@ class Revoker(object): # If the key file doesn't exist... or is corrupted except (IndexError, ValueError, TypeError): - raise errors.LetsEncryptRevokerError( + raise errors.RevokerError( "Corrupted backup key file: %s" % cert.backup_key_path) return self.network.revoke(cert=None) # XXX @@ -293,7 +293,7 @@ class Revoker(object): # This should never happen... if idx != len(cert_list): - raise errors.LetsEncryptRevokerError( + raise errors.RevokerError( "Did not find all cert_list items to remove from LIST") shutil.copy2(list_path2, self.list_path) @@ -398,7 +398,7 @@ class Cert(object): try: self._cert = M2Crypto.X509.load_cert(cert_path) except (IOError, M2Crypto.X509.X509Error): - raise errors.LetsEncryptRevokerError( + raise errors.RevokerError( "Error loading certificate: %s" % cert_path) self.idx = -1 diff --git a/letsencrypt/tests/account_test.py b/letsencrypt/tests/account_test.py index 6e9966a55..03f1958f1 100644 --- a/letsencrypt/tests/account_test.py +++ b/letsencrypt/tests/account_test.py @@ -73,7 +73,7 @@ class AccountTest(unittest.TestCase): def test_prompts_bad_email(self, mock_from_email, mock_util): from letsencrypt.account import Account - mock_from_email.side_effect = (errors.LetsEncryptClientError, "acc") + mock_from_email.side_effect = (errors.Error, "acc") mock_util().input.return_value = (display_util.OK, self.email) self.assertEqual(Account.from_prompts(self.config), "acc") @@ -102,7 +102,7 @@ class AccountTest(unittest.TestCase): def test_from_email(self): from letsencrypt.account import Account - self.assertRaises(errors.LetsEncryptClientError, + self.assertRaises(errors.Error, Account.from_email, self.config, "not_valid...email") def test_save_from_existing_account(self): @@ -171,7 +171,7 @@ class AccountTest(unittest.TestCase): from letsencrypt.account import Account self.assertRaises( - errors.LetsEncryptClientError, + errors.Error, Account.from_existing_account, self.config, "non-existant@email.org") diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 72fba1d0b..7abc891bc 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -61,7 +61,7 @@ class ChallengeFactoryTest(unittest.TestCase): [mock.Mock(chall="chall", typ="unrecognized")], [messages.STATUS_PENDING]) - self.assertRaises(errors.LetsEncryptClientError, + self.assertRaises(errors.Error, self.handler._challenge_factory, "failure.com", [0]) diff --git a/letsencrypt/tests/continuity_auth_test.py b/letsencrypt/tests/continuity_auth_test.py index 829af736d..95526d265 100644 --- a/letsencrypt/tests/continuity_auth_test.py +++ b/letsencrypt/tests/continuity_auth_test.py @@ -58,7 +58,7 @@ class PerformTest(unittest.TestCase): def test_unexpected(self): self.assertRaises( - errors.LetsEncryptContAuthError, self.auth.perform, [ + errors.ContAuthError, self.auth.perform, [ achallenges.DVSNI(challb=None, domain="0", key="invalid_key")]) def test_chall_pref(self): @@ -91,7 +91,7 @@ class CleanupTest(unittest.TestCase): token = achallenges.RecoveryToken(challb=None, domain="0") unexpected = achallenges.DVSNI(challb=None, domain="0", key="dummy_key") - self.assertRaises(errors.LetsEncryptContAuthError, + self.assertRaises(errors.ContAuthError, self.auth.cleanup, [token, unexpected]) diff --git a/letsencrypt/tests/display/enhancements_test.py b/letsencrypt/tests/display/enhancements_test.py index 54e27aa01..b3a6922d8 100644 --- a/letsencrypt/tests/display/enhancements_test.py +++ b/letsencrypt/tests/display/enhancements_test.py @@ -28,7 +28,7 @@ class AskTest(unittest.TestCase): def test_key_error(self): self.assertRaises( - errors.LetsEncryptClientError, self._call, "unknown_enhancement") + errors.Error, self._call, "unknown_enhancement") class RedirectTest(unittest.TestCase): diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index 1ad6968a1..267a930f1 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -45,7 +45,7 @@ class MakeOrVerifyDirTest(unittest.TestCase): def test_existing_wrong_mode_fails(self): self.assertRaises( - errors.LetsEncryptClientError, self._call, self.path, 0o600) + errors.Error, self._call, self.path, 0o600) def test_reraises_os_error(self): with mock.patch.object(os, 'makedirs') as makedirs: diff --git a/letsencrypt/tests/reverter_test.py b/letsencrypt/tests/reverter_test.py index 9da584f58..00d770bcc 100644 --- a/letsencrypt/tests/reverter_test.py +++ b/letsencrypt/tests/reverter_test.py @@ -50,7 +50,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): def test_add_to_checkpoint_copy_failure(self): with mock.patch("letsencrypt.reverter.shutil.copy2") as mock_copy2: mock_copy2.side_effect = IOError("bad copy") - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.add_to_checkpoint, self.sets[0], "save1") @@ -66,14 +66,14 @@ class ReverterCheckpointLocalTest(unittest.TestCase): self.reverter.add_to_temp_checkpoint(self.sets[0], "save2") # Raise error self.assertRaises( - errors.LetsEncryptReverterError, self.reverter.add_to_checkpoint, + errors.ReverterError, self.reverter.add_to_checkpoint, self.sets[2], "save3") # Should not cause an error self.reverter.add_to_checkpoint(self.sets[1], "save4") # Check to make sure new files are also checked... self.assertRaises( - errors.LetsEncryptReverterError, + errors.ReverterError, self.reverter.add_to_checkpoint, set([config3]), "invalid save") @@ -120,13 +120,13 @@ class ReverterCheckpointLocalTest(unittest.TestCase): m_open = mock.mock_open() with mock.patch("letsencrypt.reverter.open", m_open, create=True): m_open.side_effect = OSError("bad open") - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.register_file_creation, True, self.config1) def test_bad_registration(self): # Made this mistake and want to make sure it doesn't happen again... - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.register_file_creation, "filepath") @@ -135,33 +135,33 @@ class ReverterCheckpointLocalTest(unittest.TestCase): # pylint: disable=protected-access self.reverter._recover_checkpoint = mock.MagicMock( - side_effect=errors.LetsEncryptReverterError) - self.assertRaises(errors.LetsEncryptReverterError, + side_effect=errors.ReverterError) + self.assertRaises(errors.ReverterError, self.reverter.recovery_routine) def test_recover_checkpoint_revert_temp_failures(self): # pylint: disable=invalid-name mock_recover = mock.MagicMock( - side_effect=errors.LetsEncryptReverterError("e")) + side_effect=errors.ReverterError("e")) # pylint: disable=protected-access self.reverter._recover_checkpoint = mock_recover self.reverter.add_to_temp_checkpoint(self.sets[0], "config1 save") - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.revert_temporary_config) def test_recover_checkpoint_rollback_failure(self): mock_recover = mock.MagicMock( - side_effect=errors.LetsEncryptReverterError("e")) + side_effect=errors.ReverterError("e")) # pylint: disable=protected-access self.reverter._recover_checkpoint = mock_recover self.reverter.add_to_checkpoint(self.sets[0], "config1 save") self.reverter.finalize_checkpoint("Title") - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.rollback_checkpoints, 1) def test_recover_checkpoint_copy_failure(self): @@ -169,7 +169,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): with mock.patch("letsencrypt.reverter.shutil.copy2") as mock_copy2: mock_copy2.side_effect = OSError("bad copy") - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.revert_temporary_config) def test_recover_checkpoint_rm_failure(self): @@ -177,7 +177,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): with mock.patch("letsencrypt.reverter.shutil.rmtree") as mock_rmtree: mock_rmtree.side_effect = OSError("Cannot remove tree") - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.revert_temporary_config) @mock.patch("letsencrypt.reverter.logging.warning") @@ -191,7 +191,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): def test_recover_checkpoint_remove_failure(self, mock_remove): self.reverter.register_file_creation(True, self.config1) mock_remove.side_effect = OSError("Can't remove") - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.revert_temporary_config) def test_recovery_routine_temp_and_perm(self): @@ -251,13 +251,13 @@ class TestFullCheckpointsReverter(unittest.TestCase): def test_rollback_improper_inputs(self): self.assertRaises( - errors.LetsEncryptReverterError, + errors.ReverterError, self.reverter.rollback_checkpoints, "-1") self.assertRaises( - errors.LetsEncryptReverterError, + errors.ReverterError, self.reverter.rollback_checkpoints, -1000) self.assertRaises( - errors.LetsEncryptReverterError, + errors.ReverterError, self.reverter.rollback_checkpoints, "one") def test_rollback_finalize_checkpoint_valid_inputs(self): @@ -299,7 +299,7 @@ class TestFullCheckpointsReverter(unittest.TestCase): self.reverter.add_to_checkpoint(self.sets[0], "perm save") mock_move.side_effect = OSError("cannot move") - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.finalize_checkpoint, "Title") @@ -309,7 +309,7 @@ class TestFullCheckpointsReverter(unittest.TestCase): self.reverter.add_to_checkpoint(self.sets[0], "perm save") mock_rename.side_effect = OSError - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.finalize_checkpoint, "Title") @@ -347,7 +347,7 @@ class TestFullCheckpointsReverter(unittest.TestCase): # It must just be clean checkpoints os.makedirs(os.path.join(self.config.backup_dir, "in_progress")) - self.assertRaises(errors.LetsEncryptReverterError, + self.assertRaises(errors.ReverterError, self.reverter.view_config_changes) def _setup_three_checkpoints(self): diff --git a/letsencrypt/tests/revoker_test.py b/letsencrypt/tests/revoker_test.py index cd86594fd..fa756c4ee 100644 --- a/letsencrypt/tests/revoker_test.py +++ b/letsencrypt/tests/revoker_test.py @@ -80,12 +80,12 @@ class RevokerTest(RevokerBase): @mock.patch("letsencrypt.revoker.Crypto.PublicKey.RSA.importKey") def test_revoke_by_invalid_keys(self, mock_import): mock_import.side_effect = ValueError - self.assertRaises(errors.LetsEncryptRevokerError, + self.assertRaises(errors.RevokerError, self.revoker.revoke_from_key, self.key) mock_import.side_effect = [mock.Mock(), IndexError] - self.assertRaises(errors.LetsEncryptRevokerError, + self.assertRaises(errors.RevokerError, self.revoker.revoke_from_key, self.key) @@ -188,7 +188,7 @@ class RevokerTest(RevokerBase): @mock.patch("letsencrypt.revoker.logging") def test_safe_revoke_acme_fail(self, mock_log, mock_revoke, mock_display): # pylint: disable=protected-access - mock_revoke.side_effect = errors.LetsEncryptClientError + mock_revoke.side_effect = errors.Error mock_display().confirm_revocation.return_value = True self.revoker._safe_revoke(self.certs) @@ -198,7 +198,7 @@ class RevokerTest(RevokerBase): def test_acme_revoke_failure(self, mock_crypto): # pylint: disable=protected-access mock_crypto.side_effect = ValueError - self.assertRaises(errors.LetsEncryptClientError, + self.assertRaises(errors.Error, self.revoker._acme_revoke, self.certs[0]) @@ -215,7 +215,7 @@ class RevokerTest(RevokerBase): new_cert.orig = Cert.PathStatus("false path", "not here") new_cert.orig_key = Cert.PathStatus("false path", "not here") - self.assertRaises(errors.LetsEncryptRevokerError, + self.assertRaises(errors.RevokerError, self.revoker._remove_certs_from_list, [new_cert]) @@ -330,7 +330,7 @@ class CertTest(unittest.TestCase): def test_failed_load(self): from letsencrypt.revoker import Cert - self.assertRaises(errors.LetsEncryptRevokerError, Cert, self.key_path) + self.assertRaises(errors.RevokerError, Cert, self.key_path) def test_no_row(self): self.assertEqual(self.certs[0].get_row(), None) diff --git a/letsencrypt_apache/configurator.py b/letsencrypt_apache/configurator.py index 5b0dbdea9..256ada81d 100644 --- a/letsencrypt_apache/configurator.py +++ b/letsencrypt_apache/configurator.py @@ -554,9 +554,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return self._enhance_func[enhancement]( self.choose_vhost(domain), options) except ValueError: - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Unsupported enhancement: {}".format(enhancement)) - except errors.LetsEncryptConfiguratorError: + except errors.ConfiguratorError: logging.warn("Failed %s for %s", enhancement, domain) def _enable_redirect(self, ssl_vhost, unused_options): @@ -602,7 +602,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): return else: logging.info("Unknown redirect exists for this vhost") - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Unknown redirect already exists " "in {}".format(general_v.filep)) # Add directives to server @@ -673,7 +673,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Make sure adding the vhost will be safe conflict, host_or_addrs = self._conflicting_host(ssl_vhost) if conflict: - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Unable to create a redirection vhost " "- {}".format(host_or_addrs)) @@ -951,7 +951,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :returns: version :rtype: tuple - :raises errors.LetsEncryptConfiguratorError: + :raises errors.ConfiguratorError: Unable to find Apache version """ @@ -962,14 +962,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): stderr=subprocess.PIPE) text = proc.communicate()[0] except (OSError, ValueError): - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Unable to run %s -v" % self.conf('ctl')) regex = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE) matches = regex.findall(text) if len(matches) != 1: - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Unable to find Apache version") return tuple([int(i) for i in matches[0].split(".")]) @@ -1079,12 +1079,12 @@ def mod_loaded(module, apache_ctl): except (OSError, ValueError): logging.error( "Error accessing %s for loaded modules!", apache_ctl) - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Error accessing loaded modules") # Small errors that do not impede if proc.returncode != 0: logging.warn("Error in checking loaded module list: %s", stderr) - raise errors.LetsEncryptMisconfigurationError( + raise errors.MisconfigurationError( "Apache is unable to check whether or not the module is " "loaded because Apache is misconfigured.") diff --git a/letsencrypt_apache/parser.py b/letsencrypt_apache/parser.py index 9e6e9efe6..4317df757 100644 --- a/letsencrypt_apache/parser.py +++ b/letsencrypt_apache/parser.py @@ -347,7 +347,7 @@ class ApacheParser(object): if os.path.isfile(os.path.join(self.root, name)): return os.path.join(self.root, name) - raise errors.LetsEncryptNoInstallationError( + raise errors.NoInstallationError( "Could not find configuration root") def _set_user_config_file(self, root): diff --git a/letsencrypt_apache/tests/configurator_test.py b/letsencrypt_apache/tests/configurator_test.py index c3064eb5b..92cc9762a 100644 --- a/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt_apache/tests/configurator_test.py @@ -198,16 +198,16 @@ class TwoVhost80Test(util.ApacheTest): mock_popen().communicate.return_value = ( "Server Version: Apache (Debian)", "") self.assertRaises( - errors.LetsEncryptConfiguratorError, self.config.get_version) + errors.ConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( "Server Version: Apache/2.3{0} Apache/2.4.7".format(os.linesep), "") self.assertRaises( - errors.LetsEncryptConfiguratorError, self.config.get_version) + errors.ConfiguratorError, self.config.get_version) mock_popen.side_effect = OSError("Can't find program") self.assertRaises( - errors.LetsEncryptConfiguratorError, self.config.get_version) + errors.ConfiguratorError, self.config.get_version) if __name__ == "__main__": diff --git a/letsencrypt_apache/tests/parser_test.py b/letsencrypt_apache/tests/parser_test.py index 06bb20e2a..85cc8abbd 100644 --- a/letsencrypt_apache/tests/parser_test.py +++ b/letsencrypt_apache/tests/parser_test.py @@ -112,7 +112,7 @@ class ApacheParserTest(util.ApacheTest): mock_path.isfile.return_value = False # pylint: disable=protected-access - self.assertRaises(errors.LetsEncryptConfiguratorError, + self.assertRaises(errors.ConfiguratorError, self.parser._set_locations, self.ssl_options) mock_path.isfile.side_effect = [True, False, False] diff --git a/letsencrypt_nginx/configurator.py b/letsencrypt_nginx/configurator.py index f74ad0a3a..2a8ac8299 100644 --- a/letsencrypt_nginx/configurator.py +++ b/letsencrypt_nginx/configurator.py @@ -128,7 +128,7 @@ class NginxConfigurator(common.Plugin): directives, True) logging.info("Deployed Certificate to VirtualHost %s for %s", vhost.filep, vhost.names) - except errors.LetsEncryptMisconfigurationError: + except errors.MisconfigurationError: logging.warn( "Cannot find a cert or key directive in %s for %s. " "VirtualHost was not modified.", vhost.filep, vhost.names) @@ -315,9 +315,9 @@ class NginxConfigurator(common.Plugin): return self._enhance_func[enhancement]( self.choose_vhost(domain), options) except (KeyError, ValueError): - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Unsupported enhancement: {0}".format(enhancement)) - except errors.LetsEncryptConfiguratorError: + except errors.ConfiguratorError: logging.warn("Failed %s for %s", enhancement, domain) ###################################### @@ -380,7 +380,7 @@ class NginxConfigurator(common.Plugin): :returns: version :rtype: tuple - :raises errors.LetsEncryptConfiguratorError: + :raises errors.ConfiguratorError: Unable to find Nginx version or version is unsupported """ @@ -391,7 +391,7 @@ class NginxConfigurator(common.Plugin): stderr=subprocess.PIPE) text = proc.communicate()[1] # nginx prints output to stderr except (OSError, ValueError): - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Unable to run %s -V" % self.conf('ctl')) version_regex = re.compile(r"nginx/([0-9\.]*)", re.IGNORECASE) @@ -404,13 +404,13 @@ class NginxConfigurator(common.Plugin): ssl_matches = ssl_regex.findall(text) if not version_matches: - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Unable to find Nginx version") if not ssl_matches: - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Nginx build is missing SSL module (--with-http_ssl_module).") if not sni_matches: - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Nginx build doesn't support SNI") nginx_version = tuple([int(i) for i in version_matches[0].split(".")]) @@ -418,7 +418,7 @@ class NginxConfigurator(common.Plugin): # nginx < 0.8.48 uses machine hostname as default server_name instead of # the empty string if nginx_version < (0, 8, 48): - raise errors.LetsEncryptConfiguratorError( + raise errors.ConfiguratorError( "Nginx version must be 0.8.48+") return nginx_version diff --git a/letsencrypt_nginx/dvsni.py b/letsencrypt_nginx/dvsni.py index f6f82c5cb..1704d92c8 100644 --- a/letsencrypt_nginx/dvsni.py +++ b/letsencrypt_nginx/dvsni.py @@ -79,7 +79,7 @@ class NginxDvsni(common.Dvsni): :param list ll_addrs: list of lists of :class:`letsencrypt_nginx.obj.Addr` to apply - :raises errors.LetsEncryptMisconfigurationError: + :raises errors.MisconfigurationError: Unable to find a suitable HTTP block to include DVSNI hosts. """ @@ -97,7 +97,7 @@ class NginxDvsni(common.Dvsni): included = True break if not included: - raise errors.LetsEncryptMisconfigurationError( + raise errors.MisconfigurationError( 'LetsEncrypt could not find an HTTP block to include DVSNI ' 'challenges in %s.' % root) diff --git a/letsencrypt_nginx/parser.py b/letsencrypt_nginx/parser.py index b25471ef3..6b52dfc15 100644 --- a/letsencrypt_nginx/parser.py +++ b/letsencrypt_nginx/parser.py @@ -195,7 +195,7 @@ class NginxParser(object): if os.path.isfile(os.path.join(self.root, name)): return os.path.join(self.root, name) - raise errors.LetsEncryptNoInstallationError( + raise errors.NoInstallationError( "Could not find configuration root") def filedump(self, ext='tmp'): @@ -486,7 +486,7 @@ def _add_directives(block, directives, replace=False): block[index] = directive changed = True if not changed: - raise errors.LetsEncryptMisconfigurationError( + raise errors.MisconfigurationError( 'LetsEncrypt expected directive for %s in the Nginx ' 'config but did not find it.' % directive[0]) else: diff --git a/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt_nginx/tests/configurator_test.py index 94a0901b5..6b880b14d 100644 --- a/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt_nginx/tests/configurator_test.py @@ -45,7 +45,7 @@ class NginxConfiguratorTest(util.NginxTest): self.assertEqual([], self.config.supported_enhancements()) def test_enhance(self): - self.assertRaises(errors.LetsEncryptConfiguratorError, + self.assertRaises(errors.ConfiguratorError, self.config.enhance, 'myhost', 'redirect') @@ -218,13 +218,13 @@ class NginxConfiguratorTest(util.NginxTest): " (based on LLVM 3.5svn)", "TLS SNI support enabled", "configure arguments: --with-http_ssl_module"])) - self.assertRaises(errors.LetsEncryptConfiguratorError, + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( "", "\n".join(["nginx version: nginx/1.4.2", "TLS SNI support enabled"])) - self.assertRaises(errors.LetsEncryptConfiguratorError, + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( @@ -232,7 +232,7 @@ class NginxConfiguratorTest(util.NginxTest): "built by clang 6.0 (clang-600.0.56)" " (based on LLVM 3.5svn)", "configure arguments: --with-http_ssl_module"])) - self.assertRaises(errors.LetsEncryptConfiguratorError, + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( @@ -241,12 +241,12 @@ class NginxConfiguratorTest(util.NginxTest): " (based on LLVM 3.5svn)", "TLS SNI support enabled", "configure arguments: --with-http_ssl_module"])) - self.assertRaises(errors.LetsEncryptConfiguratorError, + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen.side_effect = OSError("Can't find program") self.assertRaises( - errors.LetsEncryptConfiguratorError, self.config.get_version) + errors.ConfiguratorError, self.config.get_version) @mock.patch("letsencrypt_nginx.configurator.subprocess.Popen") def test_nginx_restart(self, mock_popen): diff --git a/letsencrypt_nginx/tests/dvsni_test.py b/letsencrypt_nginx/tests/dvsni_test.py index 88c25c234..b539c4d78 100644 --- a/letsencrypt_nginx/tests/dvsni_test.py +++ b/letsencrypt_nginx/tests/dvsni_test.py @@ -163,7 +163,7 @@ class DvsniPerformTest(util.NginxTest): root = self.sni.configurator.parser.loc["root"] self.sni.configurator.parser.parsed[root] = [['include', 'foo.conf']] # pylint: disable=protected-access - self.assertRaises(errors.LetsEncryptMisconfigurationError, + self.assertRaises(errors.MisconfigurationError, self.sni._mod_config, []) if __name__ == "__main__": diff --git a/letsencrypt_nginx/tests/parser_test.py b/letsencrypt_nginx/tests/parser_test.py index f2e6dfe1e..0af81aefe 100644 --- a/letsencrypt_nginx/tests/parser_test.py +++ b/letsencrypt_nginx/tests/parser_test.py @@ -5,7 +5,7 @@ import re import shutil import unittest -from letsencrypt.errors import LetsEncryptMisconfigurationError +from letsencrypt import errors from letsencrypt_nginx import nginxparser from letsencrypt_nginx import obj @@ -163,7 +163,7 @@ class NginxParserTest(util.NginxTest): ['listen', '127.0.0.1'], ['server_name', 'foo bar'], ['server_name', 'foo bar']]]]) - self.assertRaises(LetsEncryptMisconfigurationError, + self.assertRaises(errors.MisconfigurationError, nparser.add_server_directives, filep, set(['foo', 'bar']), [['ssl_certificate', 'cert.pem']], True) From 57f67c4109a072a741a3fa925e464ee8abdec3e7 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 12 Jun 2015 14:45:28 +0000 Subject: [PATCH 66/70] Rewrap after errors rename, doc fixes. --- acme/client.py | 2 +- letsencrypt/account.py | 3 +- letsencrypt/auth_handler.py | 8 +- letsencrypt/client.py | 19 ++--- letsencrypt/display/enhancements.py | 3 +- letsencrypt/interfaces.py | 8 +- letsencrypt/le_util.py | 2 +- letsencrypt/plugins/disco.py | 3 +- letsencrypt/reverter.py | 17 ++--- letsencrypt/revoker.py | 3 +- letsencrypt/tests/account_test.py | 10 +-- letsencrypt/tests/auth_handler_test.py | 22 +++--- letsencrypt/tests/continuity_auth_test.py | 4 +- .../tests/display/enhancements_test.py | 3 +- letsencrypt/tests/le_util_test.py | 3 +- letsencrypt/tests/reverter_test.py | 76 ++++++++----------- letsencrypt/tests/revoker_test.py | 18 ++--- letsencrypt_apache/configurator.py | 13 ++-- letsencrypt_apache/parser.py | 3 +- letsencrypt_apache/tests/configurator_test.py | 9 +-- letsencrypt_nginx/configurator.py | 11 +-- letsencrypt_nginx/dvsni.py | 2 +- letsencrypt_nginx/tests/configurator_test.py | 21 ++--- letsencrypt_nginx/tests/dvsni_test.py | 4 +- 24 files changed, 109 insertions(+), 158 deletions(-) diff --git a/acme/client.py b/acme/client.py index 4979bce8e..6bb0f5412 100644 --- a/acme/client.py +++ b/acme/client.py @@ -321,7 +321,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes :returns: Challenge Resource with updated body. :rtype: `.ChallengeResource` - :raises errors.UnexpectedUpdate: + :raises .UnexpectedUpdate: """ response = self._post(challb.uri, response) diff --git a/letsencrypt/account.py b/letsencrypt/account.py index e6e46d098..f651bfdb2 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -205,8 +205,7 @@ class Account(object): :param str email: Email address - :raises letsencrypt.errors.Error: If invalid - email address is given. + :raises .errors.Error: If invalid email address is given. """ if not email or cls.safe_email(email): diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index a86cdd69d..c3711a244 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -60,7 +60,7 @@ class AuthHandler(object): form of (`completed`, `failed`) :rtype: tuple - :raises AuthorizationError: If unable to retrieve all + :raises .AuthorizationError: If unable to retrieve all authorizations """ @@ -296,8 +296,7 @@ class AuthHandler(object): :class:`letsencrypt.achallenges.Indexed` :rtype: tuple - :raises errors.Error: If Challenge type is not - recognized + :raises .errors.Error: if challenge type is not recognized """ dv_chall = [] @@ -354,8 +353,7 @@ def challb_to_achall(challb, key, domain): else: raise errors.Error( - "Received unsupported challenge of type: %s", - chall.typ) + "Received unsupported challenge of type: %s", chall.typ) def gen_challenge_path(challbs, preferences, combinations): diff --git a/letsencrypt/client.py b/letsencrypt/client.py index e29064a56..279813640 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -147,8 +147,7 @@ class Client(object): logging.warning(msg) raise errors.Error(msg) if self.account.regr is None: - raise errors.Error( - "Please register with the ACME server first.") + raise errors.Error("Please register with the ACME server first.") # Perform Challenges/Get Authorizations authzr = self.auth_handler.get_authorizations(domains) @@ -339,8 +338,8 @@ class Client(object): :param redirect: If traffic should be forwarded from HTTP to HTTPS. :type redirect: bool or None - :raises letsencrypt.errors.Error: if - no installer is specified in the client. + :raises .errors.Error: if no installer is specified in the + client. """ if self.installer is None: @@ -386,8 +385,7 @@ def validate_key_csr(privkey, csr=None): :param csr: CSR :type csr: :class:`letsencrypt.le_util.CSR` - :raises letsencrypt.errors.Error: when - validation fails + :raises .errors.Error: when validation fails """ # TODO: Handle all of these problems appropriately @@ -396,8 +394,7 @@ def validate_key_csr(privkey, csr=None): # Key must be readable and valid. if privkey.pem and not crypto_util.valid_privkey(privkey.pem): - raise errors.Error( - "The provided key is not a valid key") + raise errors.Error("The provided key is not a valid key") if csr: if csr.form == "der": @@ -406,16 +403,14 @@ def validate_key_csr(privkey, csr=None): # If CSR is provided, it must be readable and valid. if csr.data and not crypto_util.valid_csr(csr.data): - raise errors.Error( - "The provided CSR is not a valid CSR") + raise errors.Error("The provided CSR is not a valid CSR") # If both CSR and key are provided, the key must be the same key used # in the CSR. if csr.data and privkey.pem: if not crypto_util.csr_matches_pubkey( csr.data, privkey.pem): - raise errors.Error( - "The key and CSR do not match") + raise errors.Error("The key and CSR do not match") def determine_account(config): diff --git a/letsencrypt/display/enhancements.py b/letsencrypt/display/enhancements.py index 7855b7fba..6d7c78d7d 100644 --- a/letsencrypt/display/enhancements.py +++ b/letsencrypt/display/enhancements.py @@ -21,8 +21,7 @@ def ask(enhancement): :returns: True if feature is desired, False otherwise :rtype: bool - :raises letsencrypt.errors.Error: If - the enhancement provided is not supported. + :raises .errors.Error: if the enhancement provided is not supported """ try: diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 539683d30..d529127e9 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -68,10 +68,10 @@ class IPlugin(zope.interface.Interface): Finish up any additional initialization. - :raises letsencrypt.errors.MisconfigurationError: - when full initialization cannot be completed. Plugin will be - displayed on a list of available plugins. - :raises letsencrypt.errors.NoInstallationError: + :raises .MisconfigurationError: + when full initialization cannot be completed. Plugin will + be displayed on a list of available plugins. + :raises .NoInstallationError: when the necessary programs/files cannot be located. Plugin will NOT be displayed on a list of available plugins. diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index 2b0a4a495..e5654a03d 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -19,7 +19,7 @@ def make_or_verify_dir(directory, mode=0o755, uid=0): :param int mode: Directory mode. :param int uid: Directory owner. - :raises Error: if a directory already exists, + :raises .errors.Error: if a directory already exists, but has wrong permissions or owner :raises OSError: if invalid or inaccessible file names and diff --git a/letsencrypt/plugins/disco.py b/letsencrypt/plugins/disco.py index 229e152e2..6b0d8e4f3 100644 --- a/letsencrypt/plugins/disco.py +++ b/letsencrypt/plugins/disco.py @@ -102,8 +102,7 @@ class PluginEntryPoint(object): @property def misconfigured(self): """Is plugin misconfigured?""" - return isinstance( - self._prepared, errors.MisconfigurationError) + return isinstance(self._prepared, errors.MisconfigurationError) @property def available(self): diff --git a/letsencrypt/reverter.py b/letsencrypt/reverter.py index 2743be97e..72a6c0b67 100644 --- a/letsencrypt/reverter.py +++ b/letsencrypt/reverter.py @@ -30,8 +30,7 @@ class Reverter(object): This function should reinstall the users original configuration files for all saves with temporary=True - :raises letsencrypt.errors.ReverterError: when - unable to revert config + :raises .ReverterError: when unable to revert config """ if os.path.isdir(self.config.temp_checkpoint_dir): @@ -41,8 +40,7 @@ class Reverter(object): # We have a partial or incomplete recovery logging.fatal("Incomplete or failed recovery for %s", self.config.temp_checkpoint_dir) - raise errors.ReverterError( - "Unable to revert temporary config") + raise errors.ReverterError("Unable to revert temporary config") def rollback_checkpoints(self, rollback=1): """Revert 'rollback' number of configuration checkpoints. @@ -50,9 +48,9 @@ class Reverter(object): :param int rollback: Number of checkpoints to reverse. A str num will be cast to an integer. So "2" is also acceptable. - :raises letsencrypt.errors.ReverterError: If - there is a problem with the input or if the function is unable to - correctly revert the configuration checkpoints. + :raises .ReverterError: + if there is a problem with the input or if the function is + unable to correctly revert the configuration checkpoints """ try: @@ -161,9 +159,8 @@ class Reverter(object): :param set save_files: set of files to save :param str save_notes: notes about changes made during the save - :raises IOError: If unable to open cp_dir + FILEPATHS file - :raises letsencrypt.errors.ReverterError: If - unable to add checkpoint + :raises IOError: if unable to open cp_dir + FILEPATHS file + :raises .ReverterError: if unable to add checkpoint """ le_util.make_or_verify_dir( diff --git a/letsencrypt/revoker.py b/letsencrypt/revoker.py index 66d359a6b..a3ea543fb 100644 --- a/letsencrypt/revoker.py +++ b/letsencrypt/revoker.py @@ -89,8 +89,7 @@ class Revoker(object): # This should never happen given the assumptions of the # module. If it does, it is probably best to delete the # the offending key/cert. For now... just raise an exception - raise errors.RevokerError( - "%s - backup file is corrupted.") + raise errors.RevokerError("%s - backup file is corrupted.") if clean_pem == test_pem: certs.append( diff --git a/letsencrypt/tests/account_test.py b/letsencrypt/tests/account_test.py index 03f1958f1..6b9fafe31 100644 --- a/letsencrypt/tests/account_test.py +++ b/letsencrypt/tests/account_test.py @@ -102,8 +102,8 @@ class AccountTest(unittest.TestCase): def test_from_email(self): from letsencrypt.account import Account - self.assertRaises(errors.Error, - Account.from_email, self.config, "not_valid...email") + self.assertRaises( + errors.Error, Account.from_email, self.config, "not_valid...email") def test_save_from_existing_account(self): from letsencrypt.account import Account @@ -170,10 +170,8 @@ class AccountTest(unittest.TestCase): def test_failed_existing_account(self): from letsencrypt.account import Account - self.assertRaises( - errors.Error, - Account.from_existing_account, - self.config, "non-existant@email.org") + self.assertRaises(errors.Error, Account.from_existing_account, + self.config, "non-existant@email.org") class SafeEmailTest(unittest.TestCase): """Test safe_email.""" diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 7abc891bc..24bceb5f8 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -61,8 +61,8 @@ class ChallengeFactoryTest(unittest.TestCase): [mock.Mock(chall="chall", typ="unrecognized")], [messages.STATUS_PENDING]) - self.assertRaises(errors.Error, - self.handler._challenge_factory, "failure.com", [0]) + self.assertRaises( + errors.Error, self.handler._challenge_factory, "failure.com", [0]) class GetAuthorizationsTest(unittest.TestCase): @@ -153,8 +153,8 @@ class GetAuthorizationsTest(unittest.TestCase): gen_dom_authzr, challs=acme_util.CHALLENGES) self.mock_dv_auth.perform.side_effect = errors.AuthorizationError - self.assertRaises(errors.AuthorizationError, - self.handler.get_authorizations, ["0"]) + self.assertRaises( + errors.AuthorizationError, self.handler.get_authorizations, ["0"]) def _validate_all(self, unused_1, unused_2): for dom in self.handler.authzr.keys(): @@ -218,9 +218,9 @@ class PollChallengesTest(unittest.TestCase): @mock.patch("letsencrypt.auth_handler.time") def test_poll_challenges_failure(self, unused_mock_time): self.mock_net.poll.side_effect = self._mock_poll_solve_one_invalid - self.assertRaises(errors.AuthorizationError, - self.handler._poll_challenges, - self.chall_update, False) + self.assertRaises( + errors.AuthorizationError, self.handler._poll_challenges, + self.chall_update, False) @mock.patch("letsencrypt.auth_handler.time") def test_unable_to_find_challenge_status(self, unused_mock_time): @@ -229,8 +229,8 @@ class PollChallengesTest(unittest.TestCase): self.chall_update[self.doms[0]].append( challb_to_achall(acme_util.RECOVERY_CONTACT_P, "key", self.doms[0])) self.assertRaises( - errors.AuthorizationError, - self.handler._poll_challenges, self.chall_update, False) + errors.AuthorizationError, self.handler._poll_challenges, + self.chall_update, False) def test_verify_authzr_failure(self): self.assertRaises( @@ -348,8 +348,8 @@ class GenChallengePathTest(unittest.TestCase): prefs = [challenges.DVSNI] combos = ((0, 1),) - self.assertRaises(errors.AuthorizationError, - self._call, challbs, prefs, combos) + self.assertRaises( + errors.AuthorizationError, self._call, challbs, prefs, combos) class MutuallyExclusiveTest(unittest.TestCase): diff --git a/letsencrypt/tests/continuity_auth_test.py b/letsencrypt/tests/continuity_auth_test.py index 95526d265..509dc8bdf 100644 --- a/letsencrypt/tests/continuity_auth_test.py +++ b/letsencrypt/tests/continuity_auth_test.py @@ -91,8 +91,8 @@ class CleanupTest(unittest.TestCase): token = achallenges.RecoveryToken(challb=None, domain="0") unexpected = achallenges.DVSNI(challb=None, domain="0", key="dummy_key") - self.assertRaises(errors.ContAuthError, - self.auth.cleanup, [token, unexpected]) + self.assertRaises( + errors.ContAuthError, self.auth.cleanup, [token, unexpected]) def gen_client_resp(chall): diff --git a/letsencrypt/tests/display/enhancements_test.py b/letsencrypt/tests/display/enhancements_test.py index b3a6922d8..6375316bf 100644 --- a/letsencrypt/tests/display/enhancements_test.py +++ b/letsencrypt/tests/display/enhancements_test.py @@ -27,8 +27,7 @@ class AskTest(unittest.TestCase): self.assertTrue(self._call("redirect")) def test_key_error(self): - self.assertRaises( - errors.Error, self._call, "unknown_enhancement") + self.assertRaises(errors.Error, self._call, "unknown_enhancement") class RedirectTest(unittest.TestCase): diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index 267a930f1..7ce619d95 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -44,8 +44,7 @@ class MakeOrVerifyDirTest(unittest.TestCase): self.assertEqual(stat.S_IMODE(os.stat(self.path).st_mode), 0o400) def test_existing_wrong_mode_fails(self): - self.assertRaises( - errors.Error, self._call, self.path, 0o600) + self.assertRaises(errors.Error, self._call, self.path, 0o600) def test_reraises_os_error(self): with mock.patch.object(os, 'makedirs') as makedirs: diff --git a/letsencrypt/tests/reverter_test.py b/letsencrypt/tests/reverter_test.py index 00d770bcc..dda867e4f 100644 --- a/letsencrypt/tests/reverter_test.py +++ b/letsencrypt/tests/reverter_test.py @@ -50,10 +50,9 @@ class ReverterCheckpointLocalTest(unittest.TestCase): def test_add_to_checkpoint_copy_failure(self): with mock.patch("letsencrypt.reverter.shutil.copy2") as mock_copy2: mock_copy2.side_effect = IOError("bad copy") - self.assertRaises(errors.ReverterError, - self.reverter.add_to_checkpoint, - self.sets[0], - "save1") + self.assertRaises( + errors.ReverterError, self.reverter.add_to_checkpoint, + self.sets[0], "save1") def test_checkpoint_conflict(self): """Make sure that checkpoint errors are thrown appropriately.""" @@ -65,17 +64,14 @@ class ReverterCheckpointLocalTest(unittest.TestCase): # This shouldn't throw an error self.reverter.add_to_temp_checkpoint(self.sets[0], "save2") # Raise error - self.assertRaises( - errors.ReverterError, self.reverter.add_to_checkpoint, - self.sets[2], "save3") + self.assertRaises(errors.ReverterError, self.reverter.add_to_checkpoint, + self.sets[2], "save3") # Should not cause an error self.reverter.add_to_checkpoint(self.sets[1], "save4") # Check to make sure new files are also checked... - self.assertRaises( - errors.ReverterError, - self.reverter.add_to_checkpoint, - set([config3]), "invalid save") + self.assertRaises(errors.ReverterError, self.reverter.add_to_checkpoint, + set([config3]), "invalid save") def test_multiple_saves_and_temp_revert(self): self.reverter.add_to_temp_checkpoint(self.sets[0], "save1") @@ -120,15 +116,15 @@ class ReverterCheckpointLocalTest(unittest.TestCase): m_open = mock.mock_open() with mock.patch("letsencrypt.reverter.open", m_open, create=True): m_open.side_effect = OSError("bad open") - self.assertRaises(errors.ReverterError, - self.reverter.register_file_creation, - True, self.config1) + self.assertRaises( + errors.ReverterError, self.reverter.register_file_creation, + True, self.config1) def test_bad_registration(self): # Made this mistake and want to make sure it doesn't happen again... - self.assertRaises(errors.ReverterError, - self.reverter.register_file_creation, - "filepath") + self.assertRaises( + errors.ReverterError, self.reverter.register_file_creation, + "filepath") def test_recovery_routine_in_progress_failure(self): self.reverter.add_to_checkpoint(self.sets[0], "perm save") @@ -136,8 +132,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase): # pylint: disable=protected-access self.reverter._recover_checkpoint = mock.MagicMock( side_effect=errors.ReverterError) - self.assertRaises(errors.ReverterError, - self.reverter.recovery_routine) + self.assertRaises(errors.ReverterError, self.reverter.recovery_routine) def test_recover_checkpoint_revert_temp_failures(self): # pylint: disable=invalid-name @@ -149,8 +144,8 @@ class ReverterCheckpointLocalTest(unittest.TestCase): self.reverter.add_to_temp_checkpoint(self.sets[0], "config1 save") - self.assertRaises(errors.ReverterError, - self.reverter.revert_temporary_config) + self.assertRaises( + errors.ReverterError, self.reverter.revert_temporary_config) def test_recover_checkpoint_rollback_failure(self): mock_recover = mock.MagicMock( @@ -161,24 +156,24 @@ class ReverterCheckpointLocalTest(unittest.TestCase): self.reverter.add_to_checkpoint(self.sets[0], "config1 save") self.reverter.finalize_checkpoint("Title") - self.assertRaises(errors.ReverterError, - self.reverter.rollback_checkpoints, 1) + self.assertRaises( + errors.ReverterError, self.reverter.rollback_checkpoints, 1) def test_recover_checkpoint_copy_failure(self): self.reverter.add_to_temp_checkpoint(self.sets[0], "save1") with mock.patch("letsencrypt.reverter.shutil.copy2") as mock_copy2: mock_copy2.side_effect = OSError("bad copy") - self.assertRaises(errors.ReverterError, - self.reverter.revert_temporary_config) + self.assertRaises( + errors.ReverterError, self.reverter.revert_temporary_config) def test_recover_checkpoint_rm_failure(self): self.reverter.add_to_temp_checkpoint(self.sets[0], "temp save") with mock.patch("letsencrypt.reverter.shutil.rmtree") as mock_rmtree: mock_rmtree.side_effect = OSError("Cannot remove tree") - self.assertRaises(errors.ReverterError, - self.reverter.revert_temporary_config) + self.assertRaises( + errors.ReverterError, self.reverter.revert_temporary_config) @mock.patch("letsencrypt.reverter.logging.warning") def test_recover_checkpoint_missing_new_files(self, mock_warn): @@ -191,8 +186,8 @@ class ReverterCheckpointLocalTest(unittest.TestCase): def test_recover_checkpoint_remove_failure(self, mock_remove): self.reverter.register_file_creation(True, self.config1) mock_remove.side_effect = OSError("Can't remove") - self.assertRaises(errors.ReverterError, - self.reverter.revert_temporary_config) + self.assertRaises( + errors.ReverterError, self.reverter.revert_temporary_config) def test_recovery_routine_temp_and_perm(self): # Register a new perm checkpoint file @@ -251,14 +246,11 @@ class TestFullCheckpointsReverter(unittest.TestCase): def test_rollback_improper_inputs(self): self.assertRaises( - errors.ReverterError, - self.reverter.rollback_checkpoints, "-1") + errors.ReverterError, self.reverter.rollback_checkpoints, "-1") self.assertRaises( - errors.ReverterError, - self.reverter.rollback_checkpoints, -1000) + errors.ReverterError, self.reverter.rollback_checkpoints, -1000) self.assertRaises( - errors.ReverterError, - self.reverter.rollback_checkpoints, "one") + errors.ReverterError, self.reverter.rollback_checkpoints, "one") def test_rollback_finalize_checkpoint_valid_inputs(self): # pylint: disable=invalid-name @@ -299,9 +291,8 @@ class TestFullCheckpointsReverter(unittest.TestCase): self.reverter.add_to_checkpoint(self.sets[0], "perm save") mock_move.side_effect = OSError("cannot move") - self.assertRaises(errors.ReverterError, - self.reverter.finalize_checkpoint, - "Title") + self.assertRaises( + errors.ReverterError, self.reverter.finalize_checkpoint, "Title") @mock.patch("letsencrypt.reverter.os.rename") def test_finalize_checkpoint_no_rename_directory(self, mock_rename): @@ -309,9 +300,8 @@ class TestFullCheckpointsReverter(unittest.TestCase): self.reverter.add_to_checkpoint(self.sets[0], "perm save") mock_rename.side_effect = OSError - self.assertRaises(errors.ReverterError, - self.reverter.finalize_checkpoint, - "Title") + self.assertRaises( + errors.ReverterError, self.reverter.finalize_checkpoint, "Title") @mock.patch("letsencrypt.reverter.logging") def test_rollback_too_many(self, mock_logging): @@ -347,8 +337,8 @@ class TestFullCheckpointsReverter(unittest.TestCase): # It must just be clean checkpoints os.makedirs(os.path.join(self.config.backup_dir, "in_progress")) - self.assertRaises(errors.ReverterError, - self.reverter.view_config_changes) + self.assertRaises( + errors.ReverterError, self.reverter.view_config_changes) def _setup_three_checkpoints(self): """Generate some finalized checkpoints.""" diff --git a/letsencrypt/tests/revoker_test.py b/letsencrypt/tests/revoker_test.py index fa756c4ee..490ff9f01 100644 --- a/letsencrypt/tests/revoker_test.py +++ b/letsencrypt/tests/revoker_test.py @@ -80,14 +80,12 @@ class RevokerTest(RevokerBase): @mock.patch("letsencrypt.revoker.Crypto.PublicKey.RSA.importKey") def test_revoke_by_invalid_keys(self, mock_import): mock_import.side_effect = ValueError - self.assertRaises(errors.RevokerError, - self.revoker.revoke_from_key, - self.key) + self.assertRaises( + errors.RevokerError, self.revoker.revoke_from_key, self.key) mock_import.side_effect = [mock.Mock(), IndexError] - self.assertRaises(errors.RevokerError, - self.revoker.revoke_from_key, - self.key) + self.assertRaises( + errors.RevokerError, self.revoker.revoke_from_key, self.key) @mock.patch("letsencrypt.network.Network.revoke") @mock.patch("letsencrypt.revoker.revocation") @@ -198,9 +196,8 @@ class RevokerTest(RevokerBase): def test_acme_revoke_failure(self, mock_crypto): # pylint: disable=protected-access mock_crypto.side_effect = ValueError - self.assertRaises(errors.Error, - self.revoker._acme_revoke, - self.certs[0]) + self.assertRaises( + errors.Error, self.revoker._acme_revoke, self.certs[0]) def test_remove_certs_from_list_bad_certs(self): # pylint: disable=protected-access @@ -216,8 +213,7 @@ class RevokerTest(RevokerBase): new_cert.orig_key = Cert.PathStatus("false path", "not here") self.assertRaises(errors.RevokerError, - self.revoker._remove_certs_from_list, - [new_cert]) + self.revoker._remove_certs_from_list, [new_cert]) def _backups_exist(self, row): # pylint: disable=protected-access diff --git a/letsencrypt_apache/configurator.py b/letsencrypt_apache/configurator.py index 256ada81d..2a0bf518b 100644 --- a/letsencrypt_apache/configurator.py +++ b/letsencrypt_apache/configurator.py @@ -674,8 +674,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): conflict, host_or_addrs = self._conflicting_host(ssl_vhost) if conflict: raise errors.ConfiguratorError( - "Unable to create a redirection vhost " - "- {}".format(host_or_addrs)) + "Unable to create a redirection vhost - {}".format( + host_or_addrs)) redirect_addrs = host_or_addrs @@ -951,8 +951,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): :returns: version :rtype: tuple - :raises errors.ConfiguratorError: - Unable to find Apache version + :raises .ConfiguratorError: if unable to find Apache version """ try: @@ -969,8 +968,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): matches = regex.findall(text) if len(matches) != 1: - raise errors.ConfiguratorError( - "Unable to find Apache version") + raise errors.ConfiguratorError("Unable to find Apache version") return tuple([int(i) for i in matches[0].split(".")]) @@ -1079,8 +1077,7 @@ def mod_loaded(module, apache_ctl): except (OSError, ValueError): logging.error( "Error accessing %s for loaded modules!", apache_ctl) - raise errors.ConfiguratorError( - "Error accessing loaded modules") + raise errors.ConfiguratorError("Error accessing loaded modules") # Small errors that do not impede if proc.returncode != 0: logging.warn("Error in checking loaded module list: %s", stderr) diff --git a/letsencrypt_apache/parser.py b/letsencrypt_apache/parser.py index 4317df757..5483b96ba 100644 --- a/letsencrypt_apache/parser.py +++ b/letsencrypt_apache/parser.py @@ -347,8 +347,7 @@ class ApacheParser(object): if os.path.isfile(os.path.join(self.root, name)): return os.path.join(self.root, name) - raise errors.NoInstallationError( - "Could not find configuration root") + raise errors.NoInstallationError("Could not find configuration root") def _set_user_config_file(self, root): """Set the appropriate user configuration file diff --git a/letsencrypt_apache/tests/configurator_test.py b/letsencrypt_apache/tests/configurator_test.py index 92cc9762a..3cdabe0b1 100644 --- a/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt_apache/tests/configurator_test.py @@ -197,17 +197,14 @@ class TwoVhost80Test(util.ApacheTest): mock_popen().communicate.return_value = ( "Server Version: Apache (Debian)", "") - self.assertRaises( - errors.ConfiguratorError, self.config.get_version) + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( "Server Version: Apache/2.3{0} Apache/2.4.7".format(os.linesep), "") - self.assertRaises( - errors.ConfiguratorError, self.config.get_version) + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen.side_effect = OSError("Can't find program") - self.assertRaises( - errors.ConfiguratorError, self.config.get_version) + self.assertRaises(errors.ConfiguratorError, self.config.get_version) if __name__ == "__main__": diff --git a/letsencrypt_nginx/configurator.py b/letsencrypt_nginx/configurator.py index 2a8ac8299..8490e7183 100644 --- a/letsencrypt_nginx/configurator.py +++ b/letsencrypt_nginx/configurator.py @@ -380,7 +380,7 @@ class NginxConfigurator(common.Plugin): :returns: version :rtype: tuple - :raises errors.ConfiguratorError: + :raises .ConfiguratorError: Unable to find Nginx version or version is unsupported """ @@ -404,22 +404,19 @@ class NginxConfigurator(common.Plugin): ssl_matches = ssl_regex.findall(text) if not version_matches: - raise errors.ConfiguratorError( - "Unable to find Nginx version") + raise errors.ConfiguratorError("Unable to find Nginx version") if not ssl_matches: raise errors.ConfiguratorError( "Nginx build is missing SSL module (--with-http_ssl_module).") if not sni_matches: - raise errors.ConfiguratorError( - "Nginx build doesn't support SNI") + raise errors.ConfiguratorError("Nginx build doesn't support SNI") nginx_version = tuple([int(i) for i in version_matches[0].split(".")]) # nginx < 0.8.48 uses machine hostname as default server_name instead of # the empty string if nginx_version < (0, 8, 48): - raise errors.ConfiguratorError( - "Nginx version must be 0.8.48+") + raise errors.ConfiguratorError("Nginx version must be 0.8.48+") return nginx_version diff --git a/letsencrypt_nginx/dvsni.py b/letsencrypt_nginx/dvsni.py index 1704d92c8..b8f9e328a 100644 --- a/letsencrypt_nginx/dvsni.py +++ b/letsencrypt_nginx/dvsni.py @@ -79,7 +79,7 @@ class NginxDvsni(common.Dvsni): :param list ll_addrs: list of lists of :class:`letsencrypt_nginx.obj.Addr` to apply - :raises errors.MisconfigurationError: + :raises .MisconfigurationError: Unable to find a suitable HTTP block to include DVSNI hosts. """ diff --git a/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt_nginx/tests/configurator_test.py index 6b880b14d..4accfa8ea 100644 --- a/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt_nginx/tests/configurator_test.py @@ -45,10 +45,8 @@ class NginxConfiguratorTest(util.NginxTest): self.assertEqual([], self.config.supported_enhancements()) def test_enhance(self): - self.assertRaises(errors.ConfiguratorError, - self.config.enhance, - 'myhost', - 'redirect') + self.assertRaises( + errors.ConfiguratorError, self.config.enhance, 'myhost', 'redirect') def test_get_chall_pref(self): self.assertEqual([challenges.DVSNI], @@ -218,22 +216,19 @@ class NginxConfiguratorTest(util.NginxTest): " (based on LLVM 3.5svn)", "TLS SNI support enabled", "configure arguments: --with-http_ssl_module"])) - self.assertRaises(errors.ConfiguratorError, - self.config.get_version) + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( "", "\n".join(["nginx version: nginx/1.4.2", "TLS SNI support enabled"])) - self.assertRaises(errors.ConfiguratorError, - self.config.get_version) + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( "", "\n".join(["nginx version: nginx/1.4.2", "built by clang 6.0 (clang-600.0.56)" " (based on LLVM 3.5svn)", "configure arguments: --with-http_ssl_module"])) - self.assertRaises(errors.ConfiguratorError, - self.config.get_version) + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen().communicate.return_value = ( "", "\n".join(["nginx version: nginx/0.8.1", @@ -241,12 +236,10 @@ class NginxConfiguratorTest(util.NginxTest): " (based on LLVM 3.5svn)", "TLS SNI support enabled", "configure arguments: --with-http_ssl_module"])) - self.assertRaises(errors.ConfiguratorError, - self.config.get_version) + self.assertRaises(errors.ConfiguratorError, self.config.get_version) mock_popen.side_effect = OSError("Can't find program") - self.assertRaises( - errors.ConfiguratorError, self.config.get_version) + self.assertRaises(errors.ConfiguratorError, self.config.get_version) @mock.patch("letsencrypt_nginx.configurator.subprocess.Popen") def test_nginx_restart(self, mock_popen): diff --git a/letsencrypt_nginx/tests/dvsni_test.py b/letsencrypt_nginx/tests/dvsni_test.py index b539c4d78..ef28e6918 100644 --- a/letsencrypt_nginx/tests/dvsni_test.py +++ b/letsencrypt_nginx/tests/dvsni_test.py @@ -163,8 +163,8 @@ class DvsniPerformTest(util.NginxTest): root = self.sni.configurator.parser.loc["root"] self.sni.configurator.parser.parsed[root] = [['include', 'foo.conf']] # pylint: disable=protected-access - self.assertRaises(errors.MisconfigurationError, - self.sni._mod_config, []) + self.assertRaises( + errors.MisconfigurationError, self.sni._mod_config, []) if __name__ == "__main__": unittest.main() # pragma: no cover From a1e750f432ef0c46e6866a68be3309127262680e Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 22 Jun 2015 22:45:45 +0000 Subject: [PATCH 67/70] Errors prefix: do not touch CLI. --- letsencrypt/cli.py | 6 +++--- letsencrypt/errors.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index f04727706..3bdf2bfc6 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -41,11 +41,11 @@ def _account_init(args, config): # The way to get the default would be args.email = "" # First try existing account return account.Account.from_existing_account(config, args.email) - except errors.Error: + except errors.LetsEncryptClientError: try: # Try to make an account based on the email address return account.Account.from_email(config, args.email) - except errors.Error: + except errors.LetsEncryptClientError: return None @@ -68,7 +68,7 @@ def _common_run(args, config, acc, authenticator, installer): if acc.regr is None: try: acme.register() - except errors.Error: + except errors.LetsEncryptClientError: sys.exit("Unable to register an account with ACME server") return acme, doms diff --git a/letsencrypt/errors.py b/letsencrypt/errors.py index 85f4a69d9..bdcb92164 100644 --- a/letsencrypt/errors.py +++ b/letsencrypt/errors.py @@ -3,6 +3,7 @@ class Error(Exception): """Generic Let's Encrypt client error.""" +LetsEncryptClientError = Error # TODO: blocked by #485 class ReverterError(Error): From 4fb1685b553c519f2ae4b02d95d413b36f9445d8 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 23 Jun 2015 15:01:23 +0000 Subject: [PATCH 68/70] Update error codes, add "error" field to ChallengeBody (acme-spec#158). --- acme/messages.py | 15 +++++++++++---- acme/messages_test.py | 18 ++++++++++++++---- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/acme/messages.py b/acme/messages.py index c6d15bbf1..31acd6000 100644 --- a/acme/messages.py +++ b/acme/messages.py @@ -14,11 +14,15 @@ class Error(jose.JSONObjectWithFields, Exception): """ ERROR_TYPE_NAMESPACE = 'urn:acme:error:' ERROR_TYPE_DESCRIPTIONS = { - 'malformed': 'The request message was malformed', - 'unauthorized': 'The client lacks sufficient authorization', - 'serverInternal': 'The server experienced an internal error', 'badCSR': 'The CSR is unacceptable (e.g., due to a short key)', 'badNonce': 'The client sent an unacceptable anti-replay nonce', + 'connection': 'The server could not connect to the client for DV', + 'dnssec': 'The server could not validate a DNSSEC signed domain', + 'malformed': 'The request message was malformed', + 'serverInternal': 'The server experienced an internal error', + 'tls': 'The server experienced a TLS error during DV', + 'unauthorized': 'The client lacks sufficient authorization', + 'unknownHost': 'The server could not resolve a domain name', } typ = jose.Field('type') @@ -220,8 +224,11 @@ class ChallengeBody(ResourceBody): """ __slots__ = ('chall',) uri = jose.Field('uri') - status = jose.Field('status', decoder=Status.from_json) + status = jose.Field('status', decoder=Status.from_json, + omitempty=True, default=STATUS_PENDING) validated = fields.RFC3339Field('validated', omitempty=True) + error = jose.Field('error', decoder=Error.from_json, + omitempty=True, default=None) def to_partial_json(self): jobj = super(ChallengeBody, self).to_partial_json() diff --git a/acme/messages_test.py b/acme/messages_test.py index 9b3c03fbc..dca1cd280 100644 --- a/acme/messages_test.py +++ b/acme/messages_test.py @@ -198,19 +198,29 @@ class ChallengeBodyTest(unittest.TestCase): self.chall = challenges.DNS(token='foo') from acme.messages import ChallengeBody - from acme.messages import STATUS_VALID - self.status = STATUS_VALID + from acme.messages import Error + from acme.messages import STATUS_INVALID + self.status = STATUS_INVALID + error = Error(typ='serverInternal', + detail='Unable to communicate with DNS server') self.challb = ChallengeBody( - uri='http://challb', chall=self.chall, status=self.status) + uri='http://challb', chall=self.chall, status=self.status, + error=error) self.jobj_to = { 'uri': 'http://challb', 'status': self.status, 'type': 'dns', 'token': 'foo', + 'error': error, } self.jobj_from = self.jobj_to.copy() - self.jobj_from['status'] = 'valid' + self.jobj_from['status'] = 'invalid' + self.jobj_from['error'] = { + 'type': 'urn:acme:error:serverInternal', + 'detail': 'Unable to communicate with DNS server', + } + def test_to_partial_json(self): self.assertEqual(self.jobj_to, self.challb.to_partial_json()) From 457279adb2cc6c886dc743ee4735a7329b841423 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 23 Jun 2015 20:10:20 +0000 Subject: [PATCH 69/70] Add errors.FailedChallenges and update AuthHandler to use it. --- letsencrypt/auth_handler.py | 46 ++++++++++++++++++-------------- letsencrypt/errors.py | 18 +++++++++++++ letsencrypt/tests/errors_test.py | 26 ++++++++++++++++++ 3 files changed, 70 insertions(+), 20 deletions(-) create mode 100644 letsencrypt/tests/errors_test.py diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 50a66c0d0..fb0e71ca1 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -152,6 +152,9 @@ class AuthHandler(object): # Don't send challenges for None and False authenticator responses if resp: self.network.answer_challenge(achall.challb, resp) + # TODO: answer_challenge returns challr, with URI, + # that can be used in _find_updated_challr + # comparisons... active_achalls.append(achall) if achall.domain in chall_update: chall_update[achall.domain].append(achall) @@ -170,23 +173,27 @@ class AuthHandler(object): while dom_to_check and rounds < max_rounds: # TODO: Use retry-after... time.sleep(min_sleep) + all_failed_achalls = set() for domain in dom_to_check: - comp_challs, failed_challs = self._handle_check( + comp_achalls, failed_achalls = self._handle_check( domain, chall_update[domain]) - if len(comp_challs) == len(chall_update[domain]): + if len(comp_achalls) == len(chall_update[domain]): comp_domains.add(domain) - elif not failed_challs: - for chall in comp_challs: - chall_update[domain].remove(chall) + elif not failed_achalls: + for achall, _ in comp_achalls: + chall_update[domain].remove(achall) # We failed some challenges... damage control else: # Right now... just assume a loss and carry on... if best_effort: comp_domains.add(domain) else: - raise errors.AuthorizationError( - "Failed Authorization procedure for %s" % domain) + all_failed_achalls.update( + updated for _, updated in failed_achalls) + + if all_failed_achalls: + raise errors.FailedChallenges(all_failed_achalls) dom_to_check -= comp_domains comp_domains.clear() @@ -204,32 +211,31 @@ class AuthHandler(object): # Note: if the whole authorization is invalid, the individual failed # challenges will be determined here... for achall in achalls: - status = self._get_chall_status(self.authzr[domain], achall) + updated_achall = achall.update(challb=self._find_updated_challb( + self.authzr[domain], achall)) # This does nothing for challenges that have yet to be decided yet. - if status == messages.STATUS_VALID: - completed.append(achall) - elif status == messages.STATUS_INVALID: - failed.append(achall) + if updated_achall.status == messages.STATUS_VALID: + completed.append((achall, updated_achall)) + elif updated_achall.status == messages.STATUS_INVALID: + failed.append((achall, updated_achall)) return completed, failed - def _get_chall_status(self, authzr, achall): # pylint: disable=no-self-use - """Get the status of the challenge. + def _find_updated_challb(self, authzr, achall): # pylint: disable=no-self-use + """Find updated challenge body within Authorization Resource. .. warning:: This assumes only one instance of type of challenge in each challenge resource. - :param authzr: Authorization Resource - :type authzr: :class:`acme.messages.AuthorizationResource` - - :param achall: Annotated challenge for which to get status - :type achall: :class:`letsencrypt.achallenges.AnnotatedChallenge` + :param .AuthorizationResource authzr: Authorization Resource + :param .AnnotatedChallenge achall: Annotated challenge for which + to get status """ for authzr_challb in authzr.body.challenges: if type(authzr_challb.chall) is type(achall.challb.chall): - return authzr_challb.status + return authzr_challb raise errors.AuthorizationError( "Target challenge not found in authorization resource") diff --git a/letsencrypt/errors.py b/letsencrypt/errors.py index d9078dbf2..a9c2e5c1f 100644 --- a/letsencrypt/errors.py +++ b/letsencrypt/errors.py @@ -14,6 +14,24 @@ class AuthorizationError(LetsEncryptClientError): """Authorization error.""" +class FailedChallenges(AuthorizationError): + """Failed challenges error. + + :ivar set failed_achalls: Failed `.AnnotatedChallenge` instances. + + """ + def __init__(self, failed_achalls): + assert failed_achalls + self.failed_achalls = failed_achalls + super(FailedChallenges, self).__init__() + + def __str__(self): + return "Failed authorization procedure. {0}".format( + ", ".join( + "{0} ({1}): {2}".format(achall.domain, achall.typ, achall.error) + for achall in self.failed_achalls if achall.error is not None)) + + class LetsEncryptContAuthError(AuthorizationError): """Let's Encrypt Continuity Authenticator error.""" diff --git a/letsencrypt/tests/errors_test.py b/letsencrypt/tests/errors_test.py new file mode 100644 index 000000000..a99d84719 --- /dev/null +++ b/letsencrypt/tests/errors_test.py @@ -0,0 +1,26 @@ +"""Tests for letsencrypt.errors.""" +import unittest + +from acme import messages + +from letsencrypt import achallenges +from letsencrypt.tests import acme_util + + +class FaiiledChallengesTest(unittest.TestCase): + """Tests for letsencrypt.errors.FailedChallenges.""" + + def setUp(self): + from letsencrypt.errors import FailedChallenges + self.error = FailedChallenges(set([achallenges.DNS( + domain="example.com", challb=messages.ChallengeBody( + chall=acme_util.DNS, uri=None, + error=messages.Error(typ="tls", detail="detail")))])) + + def test_str(self): + self.assertTrue(str(self.error).startswith( + "Failed authorization procedure. example.com (dns): tls")) + + +if __name__ == "__main__": + unittest.main() # pragma: no cover From fc4c5991877b87d24305503cbfb618c9535208d1 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 23 Jun 2015 18:38:24 -0400 Subject: [PATCH 70/70] Remove preview from github and travis --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 40c054fe3..784e06436 100644 --- a/README.rst +++ b/README.rst @@ -41,8 +41,8 @@ server automatically!:: **Encrypt ALL the things!** -.. |build-status| image:: https://travis-ci.org/letsencrypt/lets-encrypt-preview.svg?branch=master - :target: https://travis-ci.org/letsencrypt/lets-encrypt-preview +.. |build-status| image:: https://travis-ci.org/letsencrypt/letsencrypt.svg?branch=master + :target: https://travis-ci.org/letsencrypt/letsencrypt :alt: Travis CI status .. |coverage| image:: https://coveralls.io/repos/letsencrypt/lets-encrypt-preview/badge.svg?branch=master @@ -100,7 +100,7 @@ Links Documentation: https://letsencrypt.readthedocs.org -Software project: https://github.com/letsencrypt/lets-encrypt-preview +Software project: https://github.com/letsencrypt/letsencrypt Notes for developers: CONTRIBUTING.md_ @@ -113,4 +113,4 @@ email to client-dev+subscribe@letsencrypt.org) .. _Freenode: https://freenode.net .. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev -.. _CONTRIBUTING.md: https://github.com/letsencrypt/lets-encrypt-preview/blob/master/CONTRIBUTING.md +.. _CONTRIBUTING.md: https://github.com/letsencrypt/letsencrypt/blob/master/CONTRIBUTING.md