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
diff --git a/README.rst b/README.rst
index db32889db..40c054fe3 100644
--- a/README.rst
+++ b/README.rst
@@ -1,3 +1,13 @@
+.. notice for github users
+
+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
==============================
@@ -47,6 +57,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 +98,7 @@ Current Features
Links
-----
-Documentation: https://letsencrypt.readthedocs.org/
+Documentation: https://letsencrypt.readthedocs.org
Software project: https://github.com/letsencrypt/lets-encrypt-preview
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 = <`.
@@ -48,7 +56,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/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
---------
diff --git a/docs/using.rst b/docs/using.rst
index 89cbc48f6..96eb62b05 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,31 @@ 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
+================
+
+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
=============
@@ -30,8 +52,8 @@ 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`_
-* `augeas`_ is required for the ``python-augeas`` bindings
+* `SWIG`_ is required for compiling `M2Crypto`_
+* `Augeas`_ is required for the Python bindings
Ubuntu
@@ -65,25 +87,71 @@ Mac OSX
sudo ./bootstrap/mac.sh
+Fedora
+------
+
+.. code-block:: shell
+
+ sudo ./bootstrap/fedora.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 .
+
+
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 .
+
+.. 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:
+
+ * Fedora 22
+ * some versions of Mac OS X
Usage
=====
-The letsencrypt commandline tool has a builtin help:
+To get a new certificate run:
+
+.. code-block:: shell
+
+ ./venv/bin/letsencrypt auth
+
+The ``letsencrypt`` commandline tool has a builtin help:
.. code-block:: shell
./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/
+.. _Virtualenv: https://virtualenv.pypa.io
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:
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..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.
@@ -336,9 +338,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/cli.py b/letsencrypt/cli.py
index d1e084e08..0217598b1 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/constants.py b/letsencrypt/constants.py
index c407f8825..41c92be50 100644
--- a/letsencrypt/constants.py
+++ b/letsencrypt/constants.py
@@ -34,7 +34,7 @@ RENEWER_DEFAULTS = dict(
EXCLUSIVE_CHALLENGES = frozenset([frozenset([
- challenges.DVSNI, challenges.SimpleHTTPS])])
+ challenges.DVSNI, challenges.SimpleHTTP])])
"""Mutually exclusive challenges."""
diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py
index 17905149a..8ea9241a3 100644
--- a/letsencrypt/interfaces.py
+++ b/letsencrypt/interfaces.py
@@ -183,6 +183,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.
diff --git a/letsencrypt/network2.py b/letsencrypt/network2.py
index faf23f414..a20194a79 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
@@ -24,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)
@@ -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,31 @@ 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))
+ else:
+ raise errors.NetworkError(
+ 'Server {0} response did not include a replay nonce'.format(
+ response.request.method))
+
+ def _get_nonce(self, uri):
+ if not self._nonces:
+ logging.debug('Requesting fresh nonce by sending HEAD to %s', uri)
+ self._add_nonce(requests.head(uri))
+ return self._nonces.pop()
+
+ def _post(self, uri, obj, content_type=JSON_CONTENT_TYPE, **kwargs):
"""Send POST data.
+ :param JSONDeSerializable obj: Will be wrapped in JWS.
:param str content_type: Expected ``Content-Type``, fails if not set.
:raises acme.messages2.NetworkError:
@@ -137,6 +166,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 +175,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 +213,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,20 +250,19 @@ 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
# 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):
@@ -280,7 +310,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 +346,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 +425,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 +576,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/plugins/common.py b/letsencrypt/plugins/common.py
index 32bee2b49..03ddf7df7 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, src, dest):
+ """Move the ssl_options into position and return the path."""
+ option_path = os.path.join(config_dir, dest)
+ shutil.copyfile(src, 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? {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/letsencrypt/tests/acme_util.py b/letsencrypt/tests/acme_util.py
index 8780e8095..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.SimpleHTTPS(
+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 85bcfe8cf..8cbc0e604 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,8 +299,8 @@ class GenChallengePathTest(unittest.TestCase):
return gen_challenge_path(challbs, preferences, combinations)
def test_common_case(self):
- """Given DVSNI and SimpleHTTPS with appropriate combos."""
- challbs = (acme_util.DVSNI_P, acme_util.SIMPLE_HTTPS_P)
+ """Given DVSNI and SimpleHTTP with appropriate combos."""
+ 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,13 +328,13 @@ 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
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):
@@ -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(
diff --git a/letsencrypt/tests/network2_test.py b/letsencrypt/tests/network2_test.py
index 7bffcf0f4..3f745ffa7 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,73 @@ 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()
+ 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', 'data'), content_type='ct')
+ requests_mock.post('uri', mock.sentinel.wrapped), content_type='ct')
+
+ @mock.patch('letsencrypt.network2.requests')
+ def test_post_replay_nonce_handling(self, requests_mock):
+ # pylint: disable=protected-access
+ self.net._check_response = mock.MagicMock()
+ self._mock_wrap_in_jws()
+
+ self.net._nonces.clear()
+ self.assertRaises(
+ errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj)
+
+ nonce2 = jose.b64encode('Nonce2')
+ requests_mock.head('uri').headers = {
+ self.net.REPLAY_NONCE_HEADER: nonce2}
+ requests_mock.post('uri').headers = {
+ self.net.REPLAY_NONCE_HEADER: self.nonce}
+
+ self.net._post('uri', mock.sentinel.obj)
+
+ requests_mock.head.assert_called_with('uri')
+ self.wrap_in_jws.assert_called_once_with(mock.sentinel.obj, nonce2)
+ self.assertEqual(self.net._nonces, set([self.nonce]))
+
+ # wrong nonce
+ requests_mock.post('uri').headers = {self.net.REPLAY_NONCE_HEADER: 'F'}
+ self.assertRaises(
+ errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj)
@mock.patch('letsencrypt.client.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')
+ 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(
- 'uri', data='data', verify=verify_ssl)
+ requests_mock.post.assert_called_with(
+ 'uri', data=mock.sentinel.wrapped, verify=verify_ssl)
requests_mock.reset_mock()
def test_register(self):
@@ -498,8 +551,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
diff --git a/letsencrypt_apache/configurator.py b/letsencrypt_apache/configurator.py
index 078e61564..9bc5af979 100644
--- a/letsencrypt_apache/configurator.py
+++ b/letsencrypt_apache/configurator.py
@@ -18,6 +18,8 @@ from letsencrypt import errors
from letsencrypt import interfaces
from letsencrypt import le_util
+from letsencrypt.plugins import common
+
from letsencrypt_apache import constants
from letsencrypt_apache import dvsni
from letsencrypt_apache import obj
@@ -236,7 +238,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
return vhost
# Checking for domain name in vhost address
# This technique is not recommended by Apache but is technically valid
- target_addr = obj.Addr((target_name, "443"))
+ target_addr = common.Addr((target_name, "443"))
for vhost in self.vhosts:
if target_addr in vhost.addrs:
self.assoc[target_name] = vhost
@@ -327,7 +329,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
addrs = set()
args = self.aug.match(path + "/arg")
for arg in args:
- addrs.add(obj.Addr.fromstring(self.aug.get(arg)))
+ addrs.add(common.Addr.fromstring(self.aug.get(arg)))
is_ssl = False
if self.parser.find_dir(
@@ -493,7 +495,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
addr_match % (ssl_fp, parser.case_i("VirtualHost")))
for addr in ssl_addr_p:
- old_addr = obj.Addr.fromstring(
+ old_addr = common.Addr.fromstring(
str(self.aug.get(addr)))
ssl_addr = old_addr.get_addr_obj("443")
self.aug.set(addr, str(ssl_addr))
@@ -796,8 +798,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Instead... should look for vhost of the form *:80
# Should we prompt the user?
ssl_addrs = ssl_vhost.addrs
- if ssl_addrs == obj.Addr.fromstring("_default_:443"):
- ssl_addrs = [obj.Addr.fromstring("*:443")]
+ if ssl_addrs == common.Addr.fromstring("_default_:443"):
+ ssl_addrs = [common.Addr.fromstring("*:443")]
for vhost in self.vhosts:
found = 0
diff --git a/letsencrypt_apache/dvsni.py b/letsencrypt_apache/dvsni.py
index ed7a216bb..fb78cfced 100644
--- a/letsencrypt_apache/dvsni.py
+++ b/letsencrypt_apache/dvsni.py
@@ -2,10 +2,12 @@
import logging
import os
+from letsencrypt.plugins import common
+
from letsencrypt_apache import parser
-class ApacheDvsni(object):
+class ApacheDvsni(common.Dvsni):
"""Class performs DVSNI challenges within the Apache configurator.
:ivar configurator: ApacheConfigurator object
@@ -18,7 +20,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.
@@ -42,26 +44,6 @@ class ApacheDvsni(object):
"""
- 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 e732e1bce..c8383e71f 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
@@ -111,7 +112,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))
@@ -132,7 +133,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 088ac9557..27e9b2584 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()
@@ -31,32 +29,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?
+# 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 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/setup.py b/setup.py
index 145b75a69..2bd994859 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',
@@ -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',
diff --git a/tox.cover.sh b/tox.cover.sh
index 80b6474d7..172d2e05b 100755
--- a/tox.cover.sh
+++ b/tox.cover.sh
@@ -15,8 +15,10 @@ 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)
cover letsencrypt 95 && cover acme 100 && \
- cover letsencrypt_apache 78 && cover letsencrypt_nginx 96
+ cover letsencrypt_apache 76 && cover letsencrypt_nginx 96
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