1
0
mirror of https://github.com/certbot/certbot.git synced 2026-01-21 19:01:07 +03:00

Merge remote-tracking branch 'github/letsencrypt/master' into cli-config-fixes

Conflicts:
	letsencrypt_apache/tests/util.py
	letsencrypt_nginx/tests/util.py
This commit is contained in:
Jakub Warmuz
2015-06-20 20:04:58 +00:00
55 changed files with 1149 additions and 412 deletions

View File

@@ -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

View File

@@ -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)
<https://letsencrypt.org/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

2
Vagrantfile vendored
View File

@@ -8,8 +8,6 @@ VAGRANTFILE_API_VERSION = "2"
$ubuntu_setup_script = <<SETUP_SCRIPT
cd /vagrant
sudo ./bootstrap/ubuntu.sh
sudo apt-get -y --no-install-recommends install git-core
# the above is required by the 'git+https' lines of requirements.txt
if [ ! -d "venv" ]; then
virtualenv --no-site-packages -p python2 venv
./venv/bin/pip install -r requirements.txt -e .[dev,docs,testing]

View File

@@ -42,31 +42,54 @@ 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")
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
characters 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:`~SimpleHTTPS.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

View File

@@ -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,39 +33,63 @@ 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')
self.jmsg = {
'type': 'simpleHttps',
from acme.challenges import SimpleHTTPResponse
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 SimpleHTTPSResponse
from acme.challenges import SimpleHTTPResponse
self.assertEqual(
self.msg, SimpleHTTPSResponse.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 SimpleHTTPSResponse
hash(SimpleHTTPSResponse.from_json(self.jmsg))
from acme.challenges import SimpleHTTPResponse
hash(SimpleHTTPResponse.from_json(self.jmsg_http))
hash(SimpleHTTPResponse.from_json(self.jmsg_https))
class DVSNITest(unittest.TestCase):

View File

@@ -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,

View File

@@ -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?"""
@@ -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)

View File

@@ -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()
@@ -77,6 +85,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

View File

@@ -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):

59
acme/jws.py Normal file
View File

@@ -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)

58
acme/jws_test.py Normal file
View File

@@ -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

View File

@@ -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')

View File

@@ -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,

View File

@@ -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'),
)

View File

@@ -7,7 +7,7 @@
"required": ["type", "token"],
"properties": {
"type": {
"enum" : [ "simpleHttps" ]
"enum" : [ "simpleHttp" ]
},
"token": {
"type": "string"

View File

@@ -7,7 +7,7 @@
"required": ["type", "path"],
"properties": {
"type": {
"enum" : [ "simpleHttps" ]
"enum" : [ "simpleHttp" ]
},
"path": {
"type": "string"

View File

@@ -1,2 +1,7 @@
This directory contains scripts that install necessary OS-specific
prerequisite dependencies (see docs/using.rst).
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://*

View File

@@ -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
git-core \
python \
python-dev \
"$virtualenv" \
gcc \
swig \
dialog \
libaugeas0 \
libssl-dev \
libffi-dev \
ca-certificates \
dpkg-dev \

20
bootstrap/_rpm_common.sh Executable file
View File

@@ -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 \

1
bootstrap/centos.sh Symbolic link
View File

@@ -0,0 +1 @@
_rpm_common.sh

1
bootstrap/fedora.sh Symbolic link
View File

@@ -0,0 +1 @@
_rpm_common.sh

View File

@@ -0,0 +1,5 @@
:mod:`letsencrypt.plugins.manual`
---------------------------------
.. automodule:: letsencrypt.plugins.manual
:members:

View File

@@ -17,6 +17,14 @@ 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.
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
<coding-style>`.
@@ -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.

View File

@@ -51,9 +51,6 @@ Errors
:members:
:members:
Utilities
---------

View File

@@ -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 <hacking>` 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

View File

@@ -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:

View File

@@ -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):

View File

@@ -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)

View File

@@ -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 "

View File

@@ -34,7 +34,7 @@ RENEWER_DEFAULTS = dict(
EXCLUSIVE_CHALLENGES = frozenset([frozenset([
challenges.DVSNI, challenges.SimpleHTTPS])])
challenges.DVSNI, challenges.SimpleHTTP])])
"""Mutually exclusive challenges."""

View File

@@ -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.

View File

@@ -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')

View File

@@ -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

View File

@@ -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?<m\xaewmx\xea\xad\xadv\xf4\x02\xc9y\x80"
"\xe2_X\t\xe7\xc7\xa4\t\xca\xf7&\x945",
nonce="Y\xed\x01L\xac\x95\xf7pW\xb1\xd7\xa1\xb2\xc5"
"\x96\xba",
), "pending"),
domain="letsencrypt.demo", key=auth_key),
]
def setUp(self):
from letsencrypt.plugins.common import Dvsni
self.sni = Dvsni(configurator=mock.MagicMock())
def test_setup_challenge_cert(self):
# This is a helper function that can be used for handling
# open context managers more elegantly. It avoids dealing with
# __enter__ and __exit__ calls.
# http://www.voidspace.org.uk/python/mock/helpers.html#mock.mock_open
m_open = mock.mock_open()
response = challenges.DVSNIResponse(s="randomS1")
achall = mock.MagicMock(nonce=self.achalls[0].nonce,
nonce_domain=self.achalls[0].nonce_domain)
achall.gen_cert_and_response.return_value = ("pem", response)
with mock.patch("letsencrypt.plugins.common.open", m_open, create=True):
# pylint: disable=protected-access
self.assertEqual(response, self.sni._setup_challenge_cert(
achall, "randomS1"))
self.assertTrue(m_open.called)
self.assertEqual(
m_open.call_args[0], (self.sni.get_cert_file(achall), "w"))
self.assertEqual(m_open().write.call_args[0][0], "pem")
if __name__ == "__main__":
unittest.main() # pragma: no cover

View File

@@ -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

View File

@@ -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

View File

@@ -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)]

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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):
</VirtualHost>
"""
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")

View File

@@ -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`)

View File

@@ -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)

View File

@@ -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?<m\xaewmx\xea\xad\xadv\xf4\x02\xc9y\x80"
"\xe2_X\t\xe7\xc7\xa4\t\xca\xf7&\x945",
nonce="Y\xed\x01L\xac\x95\xf7pW\xb1\xd7\xa1\xb2\xc5"
"\x96\xba",
), "pending"),
domain="letsencrypt.demo", key=auth_key),
]
def tearDown(self):
shutil.rmtree(self.temp_dir)
shutil.rmtree(self.config_dir)
@@ -66,28 +38,6 @@ class DvsniPerformTest(util.ApacheTest):
resp = self.sni.perform()
self.assertEqual(len(resp), 0)
def test_setup_challenge_cert(self):
# This is a helper function that can be used for handling
# open context managers more elegantly. It avoids dealing with
# __enter__ and __exit__ calls.
# http://www.voidspace.org.uk/python/mock/helpers.html#mock.mock_open
m_open = mock.mock_open()
response = challenges.DVSNIResponse(s="randomS1")
achall = mock.MagicMock(nonce=self.achalls[0].nonce,
nonce_domain=self.achalls[0].nonce_domain)
achall.gen_cert_and_response.return_value = ("pem", response)
with mock.patch("letsencrypt_apache.dvsni.open", m_open, create=True):
# pylint: disable=protected-access
self.assertEqual(response, self.sni._setup_challenge_cert(
achall, "randomS1"))
self.assertTrue(m_open.called)
self.assertEqual(
m_open.call_args[0], (self.sni.get_cert_file(achall), "w"))
self.assertEqual(m_open().write.call_args[0][0], "pem")
def test_perform1(self):
achall = self.achalls[0]
self.sni.add_chall(achall)
@@ -139,8 +89,9 @@ class DvsniPerformTest(util.ApacheTest):
def test_mod_config(self):
for achall in self.achalls:
self.sni.add_chall(achall)
v_addr1 = [obj.Addr(("1.2.3.4", "443")), obj.Addr(("5.6.7.8", "443"))]
v_addr2 = [obj.Addr(("127.0.0.1", "443"))]
v_addr1 = [common.Addr(("1.2.3.4", "443")),
common.Addr(("5.6.7.8", "443"))]
v_addr2 = [common.Addr(("127.0.0.1", "443"))]
ll_addr = []
ll_addr.append(v_addr1)
ll_addr.append(v_addr2)

View File

@@ -1,63 +1,23 @@
"""Test the helper objects in letsencrypt_apache.obj."""
"""Tests for letsencrypt_apache.obj."""
import unittest
class AddrTest(unittest.TestCase):
"""Test the Addr class."""
def setUp(self):
from letsencrypt_apache.obj 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_apache.obj 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)
from letsencrypt.plugins import common
class VirtualHostTest(unittest.TestCase):
"""Test the VirtualHost class."""
def setUp(self):
from letsencrypt_apache.obj import VirtualHost
from letsencrypt_apache.obj import Addr
self.vhost1 = VirtualHost(
"filep", "vh_path",
set([Addr.fromstring("localhost")]), False, False)
set([common.Addr.fromstring("localhost")]), False, False)
def test_eq(self):
from letsencrypt_apache.obj import Addr
from letsencrypt_apache.obj import VirtualHost
vhost1b = VirtualHost(
"filep", "vh_path",
set([Addr.fromstring("localhost")]), False, False)
set([common.Addr.fromstring("localhost")]), False, False)
self.assertEqual(vhost1b, self.vhost1)
self.assertEqual(str(vhost1b), str(self.vhost1))

View File

@@ -1,13 +1,11 @@
"""Common utilities for letsencrypt_apache."""
import os
import pkg_resources
import shutil
import tempfile
import unittest
import mock
from letsencrypt import constants as core_constants
from letsencrypt.plugins import common
from letsencrypt_apache import configurator
from letsencrypt_apache import constants
@@ -19,10 +17,13 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods
def setUp(self):
super(ApacheTest, self).setUp()
self.temp_dir, self.config_dir, self.work_dir = dir_setup(
"debian_apache_2_4/two_vhost_80")
self.temp_dir, self.config_dir, self.work_dir = common.dir_setup(
test_dir="debian_apache_2_4/two_vhost_80",
pkg="letsencrypt_apache.tests")
self.ssl_options = setup_ssl_options(self.config_dir)
self.ssl_options = common.setup_ssl_options(
self.config_dir, constants.MOD_SSL_CONF_SRC,
constants.MOD_SSL_CONF_DEST)
self.config_path = os.path.join(
self.temp_dir, "debian_apache_2_4/two_vhost_80/apache2")
@@ -33,35 +34,6 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods
"acme.jose", "testdata/rsa256_key.pem")
def dir_setup(test_dir="debian_apache_2_4/two_vhost_80",
pkg="letsencrypt_apache.tests"):
"""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, core_constants.CONFIG_DIRS_MODE)
os.chmod(config_dir, core_constants.CONFIG_DIRS_MODE)
os.chmod(work_dir, core_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
def setup_ssl_options(
config_dir, src=constants.MOD_SSL_CONF_SRC,
dest=constants.MOD_SSL_CONF_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 get_apache_configurator(
config_path, config_dir, work_dir, version=(2, 4, 7)):
"""Create an Apache Configurator with the specified options."""
@@ -99,21 +71,21 @@ def get_vh_truth(temp_dir, config_name):
obj.VirtualHost(
os.path.join(prefix, "encryption-example.conf"),
os.path.join(aug_pre, "encryption-example.conf/VirtualHost"),
set([obj.Addr.fromstring("*:80")]),
set([common.Addr.fromstring("*:80")]),
False, True, set(["encryption-example.demo"])),
obj.VirtualHost(
os.path.join(prefix, "default-ssl.conf"),
os.path.join(aug_pre, "default-ssl.conf/IfModule/VirtualHost"),
set([obj.Addr.fromstring("_default_:443")]), True, False),
set([common.Addr.fromstring("_default_:443")]), True, False),
obj.VirtualHost(
os.path.join(prefix, "000-default.conf"),
os.path.join(aug_pre, "000-default.conf/VirtualHost"),
set([obj.Addr.fromstring("*:80")]), False, True,
set([common.Addr.fromstring("*:80")]), False, True,
set(["ip-172-30-0-17"])),
obj.VirtualHost(
os.path.join(prefix, "letsencrypt.conf"),
os.path.join(aug_pre, "letsencrypt.conf/VirtualHost"),
set([obj.Addr.fromstring("*:80")]), False, True,
set([common.Addr.fromstring("*:80")]), False, True,
set(["letsencrypt.demo"])),
]
return vh_truth

View File

@@ -4,14 +4,13 @@ import logging
import os
from letsencrypt import errors
from letsencrypt_apache.dvsni import ApacheDvsni
from letsencrypt.plugins import common
from letsencrypt_nginx import obj
from letsencrypt_nginx import nginxparser
class NginxDvsni(ApacheDvsni):
class NginxDvsni(common.Dvsni):
"""Class performs DVSNI challenges within the Nginx configurator.
:ivar configurator: NginxConfigurator object
@@ -24,7 +23,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.

View File

@@ -1,10 +1,10 @@
"""Module contains classes used by the Nginx Configurator."""
import re
from letsencrypt_apache.obj import Addr as ApacheAddr
from letsencrypt.plugins import common
class Addr(ApacheAddr):
class Addr(common.Addr):
r"""Represents an Nginx address, i.e. what comes after the 'listen'
directive.

View File

@@ -1,5 +1,4 @@
"""Test for letsencrypt_nginx.dvsni."""
import pkg_resources
import unittest
import shutil
@@ -9,7 +8,8 @@ from acme import challenges
from letsencrypt import achallenges
from letsencrypt import errors
from letsencrypt import le_util
from letsencrypt.plugins import common_test
from letsencrypt.tests import acme_util
from letsencrypt_nginx import obj
@@ -19,49 +19,43 @@ from letsencrypt_nginx.tests import util
class DvsniPerformTest(util.NginxTest):
"""Test the NginxDVSNI challenge."""
achalls = [
achallenges.DVSNI(
challb=acme_util.chall_to_challb(
challenges.DVSNI(
r="foo",
nonce="bar"
), "pending"),
domain="www.example.com", key=common_test.DvsniTest.auth_key),
achallenges.DVSNI(
challb=acme_util.chall_to_challb(
challenges.DVSNI(
r="\xba\xa9\xda?<m\xaewmx\xea\xad\xadv\xf4\x02\xc9y\x80"
"\xe2_X\t\xe7\xc7\xa4\t\xca\xf7&\x945",
nonce="Y\xed\x01L\xac\x95\xf7pW\xb1\xd7"
"\xa1\xb2\xc5\x96\xba"
), "pending"),
domain="blah", key=common_test.DvsniTest.auth_key),
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="www.example.org", key=common_test.DvsniTest.auth_key)
]
def setUp(self):
super(DvsniPerformTest, self).setUp()
config = util.get_nginx_configurator(
self.config_path, self.config_dir, self.work_dir)
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)
from letsencrypt_nginx import dvsni
self.sni = dvsni.NginxDvsni(config)
self.achalls = [
achallenges.DVSNI(
challb=acme_util.chall_to_challb(
challenges.DVSNI(
r="foo",
nonce="bar"
), "pending"),
domain="www.example.com", key=auth_key),
achallenges.DVSNI(
challb=acme_util.chall_to_challb(
challenges.DVSNI(
r="\xba\xa9\xda?<m\xaewmx\xea\xad\xadv\xf4\x02\xc9y\x80"
"\xe2_X\t\xe7\xc7\xa4\t\xca\xf7&\x945",
nonce="Y\xed\x01L\xac\x95\xf7pW\xb1\xd7"
"\xa1\xb2\xc5\x96\xba"
), "pending"),
domain="blah", key=auth_key),
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="www.example.org", key=auth_key)
]
def tearDown(self):
shutil.rmtree(self.temp_dir)
shutil.rmtree(self.config_dir)

View File

@@ -5,7 +5,7 @@ import unittest
import mock
from letsencrypt_apache.tests import util as apache_util
from letsencrypt.plugins import common
from letsencrypt_nginx import constants
from letsencrypt_nginx import configurator
@@ -16,10 +16,10 @@ class NginxTest(unittest.TestCase): # pylint: disable=too-few-public-methods
def setUp(self):
super(NginxTest, self).setUp()
self.temp_dir, self.config_dir, self.work_dir = apache_util.dir_setup(
self.temp_dir, self.config_dir, self.work_dir = common.dir_setup(
"etc_nginx", "letsencrypt_nginx.tests")
self.ssl_options = apache_util.setup_ssl_options(
self.ssl_options = common.setup_ssl_options(
self.config_dir, constants.MOD_SSL_CONF_SRC,
constants.MOD_SSL_CONF_DEST)

View File

@@ -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 <module>
# 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 "<stdin>", line 1, in <module>
# 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 <module>
# 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 <module>
# _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.

View File

@@ -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

View File

@@ -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',

View File

@@ -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

View File

@@ -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