From 8c6d1ad50aa95091790be8f9d6e289f6c662b7c4 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 2 Jun 2015 13:55:16 +0000 Subject: [PATCH 01/51] 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/51] 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/51] 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/51] 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 fd39479810db4bcd73604253ec6e4dff955f9afb Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 10:11:49 +0000 Subject: [PATCH 05/51] Add an anti-replay nonce facility (fixes: #488). --- acme/jose/__init__.py | 6 ++- acme/jose/json_util.py | 12 ++++-- acme/jose/json_util_test.py | 41 ++++++++++++++++++ acme/jose/jws.py | 14 +++--- acme/jws.py | 59 +++++++++++++++++++++++++ acme/jws_test.py | 58 +++++++++++++++++++++++++ acme/messages2.py | 1 + letsencrypt/network2.py | 47 +++++++++++++++----- letsencrypt/tests/network2_test.py | 69 ++++++++++++++++++++++++------ 9 files changed, 275 insertions(+), 32 deletions(-) create mode 100644 acme/jws.py create mode 100644 acme/jws_test.py diff --git a/acme/jose/__init__.py b/acme/jose/__init__.py index db3258a3d..a4fe7008b 100644 --- a/acme/jose/__init__.py +++ b/acme/jose/__init__.py @@ -66,7 +66,11 @@ from acme.jose.jwk import ( JWKRSA, ) -from acme.jose.jws import JWS +from acme.jose.jws import ( + Header, + JWS, + Signature, +) from acme.jose.util import ( ComparableX509, diff --git a/acme/jose/json_util.py b/acme/jose/json_util.py index 0c91c3412..c7698ed8d 100644 --- a/acme/jose/json_util.py +++ b/acme/jose/json_util.py @@ -129,7 +129,8 @@ class JSONObjectWithFieldsMeta(abc.ABCMeta): keys are field attribute names and values are fields themselves. 2. ``cls.__slots__`` is extended by all field attribute names - (i.e. not :attr:`Field.json_name`). + (i.e. not :attr:`Field.json_name`). Original ``cls.__slots__`` + are stored in ``cls._orig_slots``. In a consequence, for a field attribute name ``some_field``, ``cls.some_field`` will be a slot descriptor and not an instance @@ -143,6 +144,7 @@ class JSONObjectWithFieldsMeta(abc.ABCMeta): some_field = some_field assert Foo.__slots__ == ('some_field', 'baz') + assert Foo._orig_slots == () assert Foo.some_field is not Field assert Foo._fields.keys() == ['some_field'] @@ -158,12 +160,16 @@ class JSONObjectWithFieldsMeta(abc.ABCMeta): def __new__(mcs, name, bases, dikt): fields = {} + + for base in bases: + fields.update(getattr(base, '_fields', {})) + # Do not reorder, this class might override fields from base classes! for key, value in dikt.items(): # not iterkeys() (in-place edit!) if isinstance(value, Field): fields[key] = dikt.pop(key) - dikt['__slots__'] = tuple( - list(dikt.get('__slots__', ())) + fields.keys()) + dikt['_orig_slots'] = dikt.get('__slots__', ()) + dikt['__slots__'] = tuple(list(dikt['_orig_slots']) + fields.keys()) dikt['_fields'] = fields return abc.ABCMeta.__new__(mcs, name, bases, dikt) diff --git a/acme/jose/json_util_test.py b/acme/jose/json_util_test.py index 5726ef2a8..a37ac08de 100644 --- a/acme/jose/json_util_test.py +++ b/acme/jose/json_util_test.py @@ -77,6 +77,47 @@ class FieldTest(unittest.TestCase): self.assertTrue(Field.default_decoder(mock_value) is mock_value) +class JSONObjectWithFieldsMetaTest(unittest.TestCase): + """Tests for acme.jose.json_util.JSONObjectWithFieldsMeta.""" + + def setUp(self): + from acme.jose.json_util import Field + from acme.jose.json_util import JSONObjectWithFieldsMeta + self.field = Field('Baz') + self.field2 = Field('Baz2') + # pylint: disable=invalid-name,missing-docstring,too-few-public-methods + # pylint: disable=blacklisted-name + class A(object): + __metaclass__ = JSONObjectWithFieldsMeta + __slots__ = ('bar',) + baz = self.field + class B(A): + pass + class C(A): + baz = self.field2 + self.a_cls = A + self.b_cls = B + self.c_cls = C + + def test_fields(self): + # pylint: disable=protected-access,no-member + self.assertEqual({'baz': self.field}, self.a_cls._fields) + self.assertEqual({'baz': self.field}, self.b_cls._fields) + + def test_fields_inheritance(self): + # pylint: disable=protected-access,no-member + self.assertEqual({'baz': self.field2}, self.c_cls._fields) + + def test_slots(self): + self.assertEqual(('bar', 'baz'), self.a_cls.__slots__) + self.assertEqual(('baz',), self.b_cls.__slots__) + + def test_orig_slots(self): + # pylint: disable=protected-access,no-member + self.assertEqual(('bar',), self.a_cls._orig_slots) + self.assertEqual((), self.b_cls._orig_slots) + + class JSONObjectWithFieldsTest(unittest.TestCase): """Tests for acme.jose.json_util.JSONObjectWithFields.""" # pylint: disable=protected-access diff --git a/acme/jose/jws.py b/acme/jose/jws.py index 06923e145..3ba60d40c 100644 --- a/acme/jose/jws.py +++ b/acme/jose/jws.py @@ -247,6 +247,8 @@ class JWS(json_util.JSONObjectWithFields): """ __slots__ = ('payload', 'signatures') + signature_cls = Signature + def verify(self, key=None): """Verify.""" return all(sig.verify(self.payload, key) for sig in self.signatures) @@ -255,13 +257,13 @@ class JWS(json_util.JSONObjectWithFields): def sign(cls, payload, **kwargs): """Sign.""" return cls(payload=payload, signatures=( - Signature.sign(payload=payload, **kwargs),)) + cls.signature_cls.sign(payload=payload, **kwargs),)) @property def signature(self): """Get a singleton signature. - :rtype: :class:`Signature` + :rtype: `signature_cls` """ assert len(self.signatures) == 1 @@ -288,8 +290,8 @@ class JWS(json_util.JSONObjectWithFields): raise errors.DeserializationError( 'Compact JWS serialization should comprise of exactly' ' 3 dot-separated components') - sig = Signature(protected=json_util.decode_b64jose(protected), - signature=json_util.decode_b64jose(signature)) + sig = cls.signature_cls(protected=json_util.decode_b64jose(protected), + signature=json_util.decode_b64jose(signature)) return cls(payload=json_util.decode_b64jose(payload), signatures=(sig,)) def to_partial_json(self, flat=True): # pylint: disable=arguments-differ @@ -312,10 +314,10 @@ class JWS(json_util.JSONObjectWithFields): raise errors.DeserializationError('Flat mixed with non-flat') elif 'signature' in jobj: # flat return cls(payload=json_util.decode_b64jose(jobj.pop('payload')), - signatures=(Signature.from_json(jobj),)) + signatures=(cls.signature_cls.from_json(jobj),)) else: return cls(payload=json_util.decode_b64jose(jobj['payload']), - signatures=tuple(Signature.from_json(sig) + signatures=tuple(cls.signature_cls.from_json(sig) for sig in jobj['signatures'])) class CLI(object): diff --git a/acme/jws.py b/acme/jws.py new file mode 100644 index 000000000..a23015d93 --- /dev/null +++ b/acme/jws.py @@ -0,0 +1,59 @@ +"""ACME JOSE JWS.""" +from acme import errors +from acme import jose + + +class Header(jose.Header): + """ACME JOSE Header. + + .. todo:: Implement ``acmePath``. + + """ + nonce = jose.Field('nonce', omitempty=True) + + @classmethod + def validate_nonce(cls, nonce): + """Validate nonce. + + :returns: ``None`` if ``nonce`` is valid, decoding errors otherwise. + + """ + try: + jose.b64decode(nonce) + except (ValueError, TypeError) as error: + return error + else: + return None + + @nonce.decoder + def nonce(value): # pylint: disable=missing-docstring,no-self-argument + error = Header.validate_nonce(value) + if error is not None: + # TODO: custom error + raise errors.Error("Invalid nonce: {0}".format(error)) + return value + + +class Signature(jose.Signature): + """ACME Signature.""" + __slots__ = jose.Signature._orig_slots # pylint: disable=no-member + + # TODO: decoder/encoder should accept cls? Otherwise, subclassing + # JSONObjectWithFields is tricky... + header_cls = Header + header = jose.Field( + 'header', omitempty=True, default=header_cls(), + decoder=header_cls.from_json) + + # TODO: decoder should check that nonce is in the protected header + + +class JWS(jose.JWS): + """ACME JWS.""" + signature_cls = Signature + __slots__ = jose.JWS._orig_slots # pylint: disable=no-member + + @classmethod + def sign(cls, payload, key, alg, nonce): # pylint: disable=arguments-differ + return super(JWS, cls).sign(payload, key=key, alg=alg, + protect=frozenset(['nonce']), nonce=nonce) diff --git a/acme/jws_test.py b/acme/jws_test.py new file mode 100644 index 000000000..f4a03f70d --- /dev/null +++ b/acme/jws_test.py @@ -0,0 +1,58 @@ +"""Tests for acme.jws.""" +import os +import pkg_resources +import unittest + +import Crypto.PublicKey.RSA + +from acme import errors +from acme import jose + + +RSA512_KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string( + 'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))) + + +class HeaderTest(unittest.TestCase): + """Tests for acme.jws.Header.""" + + good_nonce = jose.b64encode('foo') + wrong_nonce = 'F' + # Following just makes sure wrong_nonce is wrong + try: + jose.b64decode(wrong_nonce) + except (ValueError, TypeError): + assert True + else: + assert False # pragma: no cover + + def test_validate_nonce(self): + from acme.jws import Header + self.assertTrue(Header.validate_nonce(self.good_nonce) is None) + self.assertFalse(Header.validate_nonce(self.wrong_nonce) is None) + + def test_nonce_decoder(self): + from acme.jws import Header + nonce_field = Header._fields['nonce'] + + self.assertRaises(errors.Error, nonce_field.decode, self.wrong_nonce) + self.assertEqual(self.good_nonce, nonce_field.decode(self.good_nonce)) + + +class JWSTest(unittest.TestCase): + """Tests for acme.jws.JWS.""" + + def setUp(self): + self.privkey = jose.JWKRSA(key=RSA512_KEY) + self.pubkey = self.privkey.public() + self.nonce = jose.b64encode('Nonce') + + def test_it(self): + from acme.jws import JWS + jws = JWS.sign(payload='foo', key=self.privkey, + alg=jose.RS256, nonce=self.nonce) + JWS.from_json(jws.to_json()) + + +if __name__ == '__main__': + unittest.main() # pragma: no cover diff --git a/acme/messages2.py b/acme/messages2.py index 253aaa95b..15b4521de 100644 --- a/acme/messages2.py +++ b/acme/messages2.py @@ -16,6 +16,7 @@ class Error(jose.JSONObjectWithFields, Exception): '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', } typ = jose.Field('type') diff --git a/letsencrypt/network2.py b/letsencrypt/network2.py index faf23f414..ae8aa43af 100644 --- a/letsencrypt/network2.py +++ b/letsencrypt/network2.py @@ -10,6 +10,7 @@ import requests import werkzeug from acme import jose +from acme import jws as acme_jws from acme import messages2 from letsencrypt import errors @@ -33,26 +34,32 @@ class Network(object): """ + # 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): + 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 jose.JWS.sign( - payload=dumps, key=self.key, alg=self.alg).json_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): @@ -126,9 +133,27 @@ class Network(object): self._check_response(response, content_type=content_type) return response - def _post(self, uri, data, content_type=JSON_CONTENT_TYPE, **kwargs): + 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)) + + 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.messages2.NetworkError: @@ -137,6 +162,7 @@ class Network(object): :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: @@ -145,6 +171,7 @@ class Network(object): 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 @@ -182,7 +209,7 @@ class Network(object): """ new_reg = messages2.Registration(contact=contact) - response = self._post(self.new_reg_uri, self._wrap_in_jws(new_reg)) + response = self._post(self.new_reg_uri, new_reg) assert response.status_code == httplib.CREATED # TODO: handle errors regr = self._regr_from_response(response) @@ -219,7 +246,7 @@ class Network(object): :rtype: `.RegistrationResource` """ - response = self._post(regr.uri, self._wrap_in_jws(regr.body)) + response = self._post(regr.uri, regr.body) # TODO: Boulder returns httplib.ACCEPTED #assert response.status_code == httplib.OK @@ -280,7 +307,7 @@ class Network(object): """ new_authz = messages2.Authorization(identifier=identifier) - response = self._post(new_authzr_uri, self._wrap_in_jws(new_authz)) + response = self._post(new_authzr_uri, new_authz) assert response.status_code == httplib.CREATED # TODO: handle errors return self._authzr_from_response(response, identifier) @@ -316,7 +343,7 @@ class Network(object): :raises errors.UnexpectedUpdate: """ - response = self._post(challb.uri, self._wrap_in_jws(response)) + response = self._post(challb.uri, response) try: authzr_uri = response.links['up']['url'] except KeyError: @@ -395,7 +422,7 @@ class Network(object): content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument response = self._post( authzrs[0].new_cert_uri, # TODO: acme-spec #90 - self._wrap_in_jws(req), + req, content_type=content_type, headers={'Accept': content_type}) @@ -546,7 +573,7 @@ class Network(object): """ rev = messages2.Revocation(revoke=when, authorizations=tuple( authzr.uri for authzr in certr.authzrs)) - response = self._post(certr.uri, self._wrap_in_jws(rev)) + 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/network2_test.py b/letsencrypt/tests/network2_test.py index 7bffcf0f4..ed155df2e 100644 --- a/letsencrypt/tests/network2_test.py +++ b/letsencrypt/tests/network2_test.py @@ -13,6 +13,7 @@ import requests from acme import challenges from acme import jose +from acme import jws as acme_jws from acme import messages2 from letsencrypt import account @@ -40,15 +41,23 @@ class NetworkTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes,too-many-public-methods def setUp(self): - from letsencrypt.network2 import Network self.verify_ssl = mock.MagicMock() + self.wrap_in_jws = mock.MagicMock(return_value=mock.sentinel.wrapped) + + from letsencrypt.network2 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 = messages2.Identifier( typ=messages2.IDENTIFIER_FQDN, value='example.com') @@ -89,8 +98,8 @@ class NetworkTest(unittest.TestCase): def _mock_post_get(self): # pylint: disable=protected-access - self.net._post = mock.MagicMock(return_value=self.response) - self.net._get = mock.MagicMock(return_value=self.response) + self.net._post = self.post + self.net._get = self.get def test_init(self): self.assertTrue(self.net.verify_ssl is self.verify_ssl) @@ -106,8 +115,12 @@ class NetworkTest(unittest.TestCase): def from_json(cls, value): pass # pragma: no cover # pylint: disable=protected-access - jws = self.net._wrap_in_jws(MockJSONDeSerializable('foo')) - self.assertEqual(jose.JWS.json_loads(jws).payload, '"foo"') + 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 @@ -169,33 +182,66 @@ class NetworkTest(unittest.TestCase): 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.network2.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.assertRaises(errors.NetworkError, self.net._post, 'uri', 'data') + self._mock_wrap_in_jws() + self.assertRaises( + errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj) @mock.patch('letsencrypt.network2.requests') def test_post(self, requests_mock): # pylint: disable=protected-access self.net._check_response = mock.MagicMock() - self.net._post('uri', 'data', content_type='ct') + self._mock_wrap_in_jws() + self.net._post('uri', mock.sentinel.obj, content_type='ct') self.net._check_response.assert_called_once_with( - requests_mock.post('uri', 'data'), content_type='ct') + requests_mock.post('uri', mock.sentinel.wrapped), content_type='ct') + + @mock.patch('letsencrypt.network2.requests') + def test_post_reply_nonce_handling(self, requests_mock): + # pylint: disable=protected-access + self.net._check_response = mock.MagicMock() + self._mock_wrap_in_jws() + + self.net._nonces.clear() + 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.network2.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._post('uri', 'data') + self.net._nonces.add('N') + self.net._post('uri', mock.sentinel.obj) requests_mock.get.assert_called_once_with('uri', verify=verify_ssl) requests_mock.post.assert_called_once_with( - 'uri', data='data', verify=verify_ssl) + 'uri', data=mock.sentinel.wrapped, verify=verify_ssl) requests_mock.reset_mock() def test_register(self): @@ -498,8 +544,7 @@ class NetworkTest(unittest.TestCase): def test_revoke(self): self._mock_post_get() self.net.revoke(self.certr, when=messages2.Revocation.NOW) - # pylint: disable=protected-access - self.net._post.assert_called_once_with(self.certr.uri, mock.ANY) + 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 From 22fd9d4cd7b1a1afd3d596086b5b37974abba0e9 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 12:28:46 +0000 Subject: [PATCH 06/51] 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 0543f040bf9f523920b076434cab23045f7a4086 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 13:06:56 +0000 Subject: [PATCH 07/51] Raise error on missing replay nonce. --- letsencrypt/network2.py | 4 ++++ letsencrypt/tests/network2_test.py | 11 +++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/letsencrypt/network2.py b/letsencrypt/network2.py index ae8aa43af..9b846da6c 100644 --- a/letsencrypt/network2.py +++ b/letsencrypt/network2.py @@ -143,6 +143,10 @@ class Network(object): 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: diff --git a/letsencrypt/tests/network2_test.py b/letsencrypt/tests/network2_test.py index ed155df2e..3f745ffa7 100644 --- a/letsencrypt/tests/network2_test.py +++ b/letsencrypt/tests/network2_test.py @@ -200,17 +200,22 @@ class NetworkTest(unittest.TestCase): # 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.network2.requests') - def test_post_reply_nonce_handling(self, requests_mock): + 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} @@ -238,9 +243,11 @@ class NetworkTest(unittest.TestCase): 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_once_with( + requests_mock.post.assert_called_with( 'uri', data=mock.sentinel.wrapped, verify=verify_ssl) requests_mock.reset_mock() From ad5c3ff1b239d634e44286e2c58f8fd713c3dea9 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 19:13:31 +0000 Subject: [PATCH 08/51] 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 09/51] 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 10/51] 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 11/51] 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 12/51] 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 13/51] 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 14/51] 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 15/51] 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 7f5abba83e809c23d74704dce68047852ac09109 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 16:04:46 +0000 Subject: [PATCH 16/51] Rename SimpleHTTPS to SimpleHTTP. --- acme/challenges.py | 14 +++++------ acme/challenges_test.py | 32 +++++++++++++------------- acme/messages2_test.py | 2 +- acme/messages_test.py | 6 ++--- acme/schemata/challengeobject.json | 2 +- acme/schemata/responseobject.json | 2 +- letsencrypt/achallenges.py | 6 ++--- letsencrypt/auth_handler.py | 6 ++--- letsencrypt/constants.py | 2 +- letsencrypt/tests/acme_util.py | 2 +- letsencrypt/tests/auth_handler_test.py | 10 ++++---- letsencrypt_apache/dvsni.py | 2 +- letsencrypt_nginx/dvsni.py | 2 +- 13 files changed, 44 insertions(+), 44 deletions(-) diff --git a/acme/challenges.py b/acme/challenges.py index 11a1c9a60..36c29d6c6 100644 --- a/acme/challenges.py +++ b/acme/challenges.py @@ -42,16 +42,16 @@ class ChallengeResponse(jose.TypedJSONObjectWithFields): @Challenge.register -class SimpleHTTPS(DVChallenge): - """ACME "simpleHttps" challenge.""" - typ = "simpleHttps" +class SimpleHTTP(DVChallenge): + """ACME "simpleHttp" challenge.""" + typ = "simpleHttp" token = jose.Field("token") @ChallengeResponse.register -class SimpleHTTPSResponse(ChallengeResponse): - """ACME "simpleHttps" challenge response.""" - typ = "simpleHttps" +class SimpleHTTPResponse(ChallengeResponse): + """ACME "simpleHttp" challenge response.""" + typ = "simpleHttp" path = jose.Field("path") URI_TEMPLATE = "https://{domain}/.well-known/acme-challenge/{path}" @@ -61,7 +61,7 @@ class SimpleHTTPSResponse(ChallengeResponse): """Create an URI to the provisioned resource. Forms an URI to the HTTPS server provisioned resource (containing - :attr:`~SimpleHTTPS.token`) by populating the :attr:`URI_TEMPLATE`. + :attr:`~SimpleHTTP.token`) by populating the :attr:`URI_TEMPLATE`. :param str domain: Domain name being verified. diff --git a/acme/challenges_test.py b/acme/challenges_test.py index 0669dd581..7018b8e2e 100644 --- a/acme/challenges_test.py +++ b/acme/challenges_test.py @@ -18,14 +18,14 @@ KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey( 'acme.jose', os.path.join('testdata', 'rsa512_key.pem')))) -class SimpleHTTPSTest(unittest.TestCase): +class SimpleHTTPTest(unittest.TestCase): def setUp(self): - from acme.challenges import SimpleHTTPS - self.msg = SimpleHTTPS( + from acme.challenges import SimpleHTTP + self.msg = SimpleHTTP( token='evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA') self.jmsg = { - 'type': 'simpleHttps', + 'type': 'simpleHttp', 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA', } @@ -33,21 +33,21 @@ class SimpleHTTPSTest(unittest.TestCase): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): - from acme.challenges import SimpleHTTPS - self.assertEqual(self.msg, SimpleHTTPS.from_json(self.jmsg)) + from acme.challenges import SimpleHTTP + self.assertEqual(self.msg, SimpleHTTP.from_json(self.jmsg)) def test_from_json_hashable(self): - from acme.challenges import SimpleHTTPS - hash(SimpleHTTPS.from_json(self.jmsg)) + from acme.challenges import SimpleHTTP + hash(SimpleHTTP.from_json(self.jmsg)) -class SimpleHTTPSResponseTest(unittest.TestCase): +class SimpleHTTPResponseTest(unittest.TestCase): def setUp(self): - from acme.challenges import SimpleHTTPSResponse - self.msg = SimpleHTTPSResponse(path='6tbIMBC5Anhl5bOlWT5ZFA') + from acme.challenges import SimpleHTTPResponse + self.msg = SimpleHTTPResponse(path='6tbIMBC5Anhl5bOlWT5ZFA') self.jmsg = { - 'type': 'simpleHttps', + 'type': 'simpleHttp', 'path': '6tbIMBC5Anhl5bOlWT5ZFA', } @@ -59,13 +59,13 @@ class SimpleHTTPSResponseTest(unittest.TestCase): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): - from acme.challenges import SimpleHTTPSResponse + from acme.challenges import SimpleHTTPResponse self.assertEqual( - self.msg, SimpleHTTPSResponse.from_json(self.jmsg)) + self.msg, SimpleHTTPResponse.from_json(self.jmsg)) def test_from_json_hashable(self): - from acme.challenges import SimpleHTTPSResponse - hash(SimpleHTTPSResponse.from_json(self.jmsg)) + from acme.challenges import SimpleHTTPResponse + hash(SimpleHTTPResponse.from_json(self.jmsg)) class DVSNITest(unittest.TestCase): diff --git a/acme/messages2_test.py b/acme/messages2_test.py index c1521e2c3..72ffc954a 100644 --- a/acme/messages2_test.py +++ b/acme/messages2_test.py @@ -183,7 +183,7 @@ class AuthorizationTest(unittest.TestCase): self.challbs = ( ChallengeBody( uri='http://challb1', status=STATUS_VALID, - chall=challenges.SimpleHTTPS(token='IlirfxKKXAsHtmzK29Pj8A')), + chall=challenges.SimpleHTTP(token='IlirfxKKXAsHtmzK29Pj8A')), ChallengeBody(uri='http://challb2', status=STATUS_VALID, chall=challenges.DNS(token='DGyRejmCefe7v4NfDGDKfA')), ChallengeBody(uri='http://challb3', status=STATUS_VALID, diff --git a/acme/messages_test.py b/acme/messages_test.py index 4e0823085..baff2a21a 100644 --- a/acme/messages_test.py +++ b/acme/messages_test.py @@ -63,7 +63,7 @@ class ChallengeTest(unittest.TestCase): def setUp(self): challs = ( - challenges.SimpleHTTPS(token='IlirfxKKXAsHtmzK29Pj8A'), + challenges.SimpleHTTP(token='IlirfxKKXAsHtmzK29Pj8A'), challenges.DNS(token='DGyRejmCefe7v4NfDGDKfA'), challenges.RecoveryToken(), ) @@ -94,7 +94,7 @@ class ChallengeTest(unittest.TestCase): def test_resolved_combinations(self): self.assertEqual(self.msg.resolved_combinations, ( ( - challenges.SimpleHTTPS(token='IlirfxKKXAsHtmzK29Pj8A'), + challenges.SimpleHTTP(token='IlirfxKKXAsHtmzK29Pj8A'), challenges.RecoveryToken() ), ( @@ -183,7 +183,7 @@ class AuthorizationRequestTest(unittest.TestCase): def setUp(self): self.responses = ( - challenges.SimpleHTTPSResponse(path='Hf5GrX4Q7EBax9hc2jJnfw'), + challenges.SimpleHTTPResponse(path='Hf5GrX4Q7EBax9hc2jJnfw'), None, # null challenges.RecoveryTokenResponse(token='23029d88d9e123e'), ) diff --git a/acme/schemata/challengeobject.json b/acme/schemata/challengeobject.json index 5641b407e..7709f315d 100644 --- a/acme/schemata/challengeobject.json +++ b/acme/schemata/challengeobject.json @@ -7,7 +7,7 @@ "required": ["type", "token"], "properties": { "type": { - "enum" : [ "simpleHttps" ] + "enum" : [ "simpleHttp" ] }, "token": { "type": "string" diff --git a/acme/schemata/responseobject.json b/acme/schemata/responseobject.json index 5ca6babf1..5773f3a73 100644 --- a/acme/schemata/responseobject.json +++ b/acme/schemata/responseobject.json @@ -7,7 +7,7 @@ "required": ["type", "path"], "properties": { "type": { - "enum" : [ "simpleHttps" ] + "enum" : [ "simpleHttp" ] }, "path": { "type": "string" diff --git a/letsencrypt/achallenges.py b/letsencrypt/achallenges.py index 77e362f22..46ef167e0 100644 --- a/letsencrypt/achallenges.py +++ b/letsencrypt/achallenges.py @@ -62,10 +62,10 @@ class DVSNI(AnnotatedChallenge): return cert_pem, response -class SimpleHTTPS(AnnotatedChallenge): - """Client annotated "simpleHttps" ACME challenge.""" +class SimpleHTTP(AnnotatedChallenge): + """Client annotated "simpleHttp" ACME challenge.""" __slots__ = ('challb', 'domain', 'key') - acme_type = challenges.SimpleHTTPS + acme_type = challenges.SimpleHTTP class DNS(AnnotatedChallenge): diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 37d818dbe..5f9d29e6e 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -336,9 +336,9 @@ def challb_to_achall(challb, key, domain): logging.info(" DVSNI challenge for %s.", domain) return achallenges.DVSNI( challb=challb, domain=domain, key=key) - elif isinstance(chall, challenges.SimpleHTTPS): - logging.info(" SimpleHTTPS challenge for %s.", domain) - return achallenges.SimpleHTTPS( + 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) diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index dacbe9040..47539615d 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -41,7 +41,7 @@ RENEWER_DEFAULTS = dict( EXCLUSIVE_CHALLENGES = frozenset([frozenset([ - challenges.DVSNI, challenges.SimpleHTTPS])]) + challenges.DVSNI, challenges.SimpleHTTP])]) """Mutually exclusive challenges.""" diff --git a/letsencrypt/tests/acme_util.py b/letsencrypt/tests/acme_util.py index 8780e8095..51bb3cfbb 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.SimpleHTTPS( +SIMPLE_HTTPS = 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" diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 85bcfe8cf..d7fd2c093 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -17,7 +17,7 @@ from letsencrypt.tests import acme_util TRANSLATE = { "dvsni": "DVSNI", - "simpleHttps": "SimpleHTTPS", + "simpleHttp": "SimpleHTTP", "dns": "DNS", "recoveryToken": "RecoveryToken", "recoveryContact": "RecoveryContact", @@ -299,7 +299,7 @@ class GenChallengePathTest(unittest.TestCase): return gen_challenge_path(challbs, preferences, combinations) def test_common_case(self): - """Given DVSNI and SimpleHTTPS with appropriate combos.""" + """Given DVSNI and SimpleHTTP with appropriate combos.""" challbs = (acme_util.DVSNI_P, acme_util.SIMPLE_HTTPS_P) prefs = [challenges.DVSNI] combos = ((0,), (1,)) @@ -334,7 +334,7 @@ class GenChallengePathTest(unittest.TestCase): # Attempted to make the order realistic prefs = [challenges.RecoveryToken, challenges.ProofOfPossession, - challenges.SimpleHTTPS, + challenges.SimpleHTTP, challenges.DVSNI, challenges.RecoveryContact] combos = acme_util.gen_combos(challbs) @@ -403,8 +403,8 @@ class IsPreferredTest(unittest.TestCase): def _call(cls, chall, satisfied): from letsencrypt.auth_handler import is_preferred return is_preferred(chall, satisfied, exclusive_groups=frozenset([ - frozenset([challenges.DVSNI, challenges.SimpleHTTPS]), - frozenset([challenges.DNS, challenges.SimpleHTTPS]), + frozenset([challenges.DVSNI, challenges.SimpleHTTP]), + frozenset([challenges.DNS, challenges.SimpleHTTP]), ])) def test_empty_satisfied(self): diff --git a/letsencrypt_apache/dvsni.py b/letsencrypt_apache/dvsni.py index ed7a216bb..6865afe26 100644 --- a/letsencrypt_apache/dvsni.py +++ b/letsencrypt_apache/dvsni.py @@ -18,7 +18,7 @@ class ApacheDvsni(object): larger array. ApacheDvsni is capable of solving many challenges at once which causes an indexing issue within ApacheConfigurator who must return all responses in order. Imagine ApacheConfigurator - maintaining state about where all of the SimpleHTTPS Challenges, + maintaining state about where all of the SimpleHTTP Challenges, Dvsni Challenges belong in the response array. This is an optional utility. diff --git a/letsencrypt_nginx/dvsni.py b/letsencrypt_nginx/dvsni.py index 5c188099c..3792230d3 100644 --- a/letsencrypt_nginx/dvsni.py +++ b/letsencrypt_nginx/dvsni.py @@ -24,7 +24,7 @@ class NginxDvsni(ApacheDvsni): larger array. NginxDvsni is capable of solving many challenges at once which causes an indexing issue within NginxConfigurator who must return all responses in order. Imagine NginxConfigurator - maintaining state about where all of the SimpleHTTPS Challenges, + maintaining state about where all of the SimpleHTTP Challenges, Dvsni Challenges belong in the response array. This is an optional utility. From bc9373929a4c0f21cf855eaffadfeb7b465b1d2e Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 16:19:41 +0000 Subject: [PATCH 17/51] Add SimpleHTTP.tls --- acme/challenges.py | 1 + 1 file changed, 1 insertion(+) diff --git a/acme/challenges.py b/acme/challenges.py index 36c29d6c6..26f71a2e3 100644 --- a/acme/challenges.py +++ b/acme/challenges.py @@ -46,6 +46,7 @@ class SimpleHTTP(DVChallenge): """ACME "simpleHttp" challenge.""" typ = "simpleHttp" token = jose.Field("token") + tls = jose.Field("tls", default=True, omitempty=True) @ChallengeResponse.register From 8883bd76fd8beb44de6e1f7fe0addc551b4bc78e Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 11 Jun 2015 18:05:00 +0000 Subject: [PATCH 18/51] Add --no-simple-http-tls. --- letsencrypt/cli.py | 3 +++ letsencrypt/interfaces.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 4b0e271f7..3bdf2bfc6 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -252,6 +252,9 @@ def create_parser(plugins): add("-t", "--text", dest="text_mode", action="store_true", help="Use the text output instead of the curses UI.") + add("--no-simple-http-tls", action="store_true", + help=config_help("no_simple_http_tls")) + testing_group = parser.add_argument_group( "testing", description="The following flags are meant for " "testing purposes only! Do NOT change them, unless you " diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index e47eea6cc..c0d44a134 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -188,6 +188,10 @@ class IConfig(zope.interface.Interface): "Port number to perform DVSNI challenge. " "Boulder in testing mode defaults to 5001.") + # TODO: not implemented + no_simple_http_tls = zope.interface.Attribute( + "Do not use TLS when solving SimpleHTTP challenges.") + class IInstaller(IPlugin): """Generic Let's Encrypt Installer Interface. From d53120f25f2ab8ffe4300dab4b47085a6b8e9da1 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 12 Jun 2015 09:21:30 +0000 Subject: [PATCH 19/51] Fix SimpleHTTP tests and omitempty bug. --- acme/challenges_test.py | 9 +++++++++ acme/jose/json_util.py | 2 +- acme/jose/json_util_test.py | 8 ++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/acme/challenges_test.py b/acme/challenges_test.py index 7018b8e2e..beeec6f73 100644 --- a/acme/challenges_test.py +++ b/acme/challenges_test.py @@ -27,8 +27,17 @@ 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()) diff --git a/acme/jose/json_util.py b/acme/jose/json_util.py index c7698ed8d..a08145459 100644 --- a/acme/jose/json_util.py +++ b/acme/jose/json_util.py @@ -62,7 +62,7 @@ class Field(object): definition of being empty, e.g. for some more exotic data types. """ - return not value + return not isinstance(value, bool) and not value def omit(self, value): """Omit the value in output?""" diff --git a/acme/jose/json_util_test.py b/acme/jose/json_util_test.py index a37ac08de..242e37589 100644 --- a/acme/jose/json_util_test.py +++ b/acme/jose/json_util_test.py @@ -1,4 +1,5 @@ """Tests for acme.jose.json_util.""" +import itertools import os import pkg_resources import unittest @@ -20,6 +21,13 @@ CSR = M2Crypto.X509.load_request(pkg_resources.resource_filename( class FieldTest(unittest.TestCase): """Tests for acme.jose.json_util.Field.""" + def test_no_omit_boolean(self): + from acme.jose.json_util import Field + for default, omitempty, value in itertools.product( + [True, False], [True, False], [True, False]): + self.assertFalse( + Field("foo", default=default, omitempty=omitempty).omit(value)) + def test_descriptors(self): mock_value = mock.MagicMock() From 8ba51665637be5e929969e18f19504dd344f294f Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 12 Jun 2015 11:54:12 +0000 Subject: [PATCH 20/51] 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 21/51] 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 22/51] 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 23/51] 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 24/51] 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 25/51] 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 26/51] 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 27/51] 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 28/51] 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 29/51] 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 30/51] 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 31/51] 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 32/51] 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 33/51] 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 34/51] 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 35/51] 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 36/51] 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 37/51] 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 38/51] 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 39/51] 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 40/51] 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 41/51] 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 42/51] 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 43/51] 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 44/51] 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 45/51] 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 46/51] 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 47/51] 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 48/51] 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 49/51] 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 50/51] 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 51/51] 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`.