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 bugs/302

This commit is contained in:
Jakub Warmuz
2015-04-02 09:40:58 +00:00
103 changed files with 2378 additions and 472 deletions

View File

@@ -19,4 +19,10 @@ env:
notifications:
email: false
irc: "chat.freenode.net#letsencrypt"
irc:
channels:
- "chat.freenode.net#letsencrypt"
on_success: never
on_failure: always
use_notice: true
skip_join: true

View File

@@ -1,6 +1,6 @@
include README.rst
include CHANGES.rst
include CONTRIBUTING.rst
include CONTRIBUTING.md
include linter_plugin.py
include letsencrypt/EULA
recursive-include letsencrypt *.json

View File

@@ -80,7 +80,7 @@ Documentation: https://letsencrypt.readthedocs.org/
Software project: https://github.com/letsencrypt/lets-encrypt-preview
Notes for developers: CONTRIBUTING.rst_
Notes for developers: CONTRIBUTING.md_
Main Website: https://letsencrypt.org/
@@ -91,4 +91,4 @@ email to client-dev+subscribe@letsencrypt.org)
.. _Freenode: https://freenode.net
.. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev
.. _CONTRIBUTING.rst: https://github.com/letsencrypt/lets-encrypt-preview/blob/master/CONTRIBUTING.rst
.. _CONTRIBUTING.md: https://github.com/letsencrypt/lets-encrypt-preview/blob/master/CONTRIBUTING.md

View File

@@ -1,6 +1,8 @@
:mod:`letsencrypt.acme`
=======================
.. contents::
.. automodule:: letsencrypt.acme
:members:
@@ -8,9 +10,18 @@
Messages
--------
v00
~~~
.. automodule:: letsencrypt.acme.messages
:members:
v02
~~~
.. automodule:: letsencrypt.acme.messages2
:members:
Challenges
----------
@@ -21,10 +32,18 @@ Challenges
Other ACME objects
------------------
.. automodule:: letsencrypt.acme.other
:members:
Fields
------
.. automodule:: letsencrypt.acme.fields
:members:
Errors
------

View File

@@ -1,29 +0,0 @@
:mod:`letsencrypt.client.apache`
--------------------------------
.. automodule:: letsencrypt.client.apache
:members:
:mod:`letsencrypt.client.apache.configurator`
=============================================
.. automodule:: letsencrypt.client.apache.configurator
:members:
:mod:`letsencrypt.client.apache.dvsni`
=============================================
.. automodule:: letsencrypt.client.apache.dvsni
:members:
:mod:`letsencrypt.client.apache.obj`
====================================
.. automodule:: letsencrypt.client.apache.obj
:members:
:mod:`letsencrypt.client.apache.parser`
=======================================
.. automodule:: letsencrypt.client.apache.parser
:members:

View File

@@ -1,5 +0,0 @@
:mod:`letsencrypt.client.client_authenticator`
----------------------------------------------
.. automodule:: letsencrypt.client.client_authenticator
:members:

View File

@@ -0,0 +1,5 @@
:mod:`letsencrypt.client.continuity_auth`
-----------------------------------------
.. automodule:: letsencrypt.client.continuity_auth
:members:

View File

@@ -0,0 +1,5 @@
:mod:`letsencrypt.client.network2`
----------------------------------
.. automodule:: letsencrypt.client.network2
:members:

View File

@@ -0,0 +1,29 @@
:mod:`letsencrypt.client.plugins.apache`
----------------------------------------
.. automodule:: letsencrypt.client.plugins.apache
:members:
:mod:`letsencrypt.client.plugins.apache.configurator`
=====================================================
.. automodule:: letsencrypt.client.plugins.apache.configurator
:members:
:mod:`letsencrypt.client.plugins.apache.dvsni`
==============================================
.. automodule:: letsencrypt.client.plugins.apache.dvsni
:members:
:mod:`letsencrypt.client.plugins.apache.obj`
============================================
.. automodule:: letsencrypt.client.plugins.apache.obj
:members:
:mod:`letsencrypt.client.plugins.apache.parser`
===============================================
.. automodule:: letsencrypt.client.plugins.apache.parser
:members:

View File

@@ -0,0 +1,11 @@
:mod:`letsencrypt.client.plugins.standalone`
--------------------------------------------
.. automodule:: letsencrypt.client.plugins.standalone
:members:
:mod:`letsencrypt.client.plugins.standalone.authenticator`
==========================================================
.. automodule:: letsencrypt.client.plugins.standalone.authenticator
:members:

View File

@@ -1,5 +0,0 @@
:mod:`letsencrypt.client.standalone_authenticator`
--------------------------------------------------
.. automodule:: letsencrypt.client.standalone_authenticator
:members:

View File

@@ -98,15 +98,16 @@ the ACME server. From the protocol, there are essentially two
different types of challenges. Challenges that must be solved by
individual plugins in order to satisfy domain validation (subclasses
of `~.DVChallenge`, i.e. `~.challenges.DVSNI`,
`~.challenges.SimpleHTTPS`, `~.challenges.DNS`) and client specific
challenges (subclasses of `~.ClientChallenge`,
`~.challenges.SimpleHTTPS`, `~.challenges.DNS`) and continuity specific
challenges (subclasses of `~.ContinuityChallenge`,
i.e. `~.challenges.RecoveryToken`, `~.challenges.RecoveryContact`,
`~.challenges.ProofOfPossession`). Client specific challenges are
always handled by the `~.ClientAuthenticator`. Right now we have two
DV Authenticators, `~.ApacheConfigurator` and the
`~.StandaloneAuthenticator`. The Standalone and Apache authenticators
only solve the `~.challenges.DVSNI` challenge currently. (You can set
which challenges your authenticator can handle through the
`~.challenges.ProofOfPossession`). Continuity challenges are
always handled by the `~.ContinuityAuthenticator`, while plugins are
expected to handle `~.DVChallenge` types.
Right now, we have two authenticator plugins, the `~.ApacheConfigurator`
and the `~.StandaloneAuthenticator`. The Standalone and Apache
authenticators only solve the `~.challenges.DVSNI` challenge currently.
(You can set which challenges your authenticator can handle through the
:meth:`~.IAuthenticator.get_chall_pref`.
(FYI: We also have a partial implementation for a `~.DNSAuthenticator`
@@ -126,26 +127,27 @@ Installers and Authenticators will oftentimes be the same
class/object. Installers and Authenticators are kept separate because
it should be possible to use the `~.StandaloneAuthenticator` (it sets
up its own Python server to perform challenges) with a program that
cannot solve challenges itself. (I am imagining MTA installers).
cannot solve challenges itself. (Imagine MTA installers).
Installer Development
---------------------
There are a few existing classes that may be beneficial while
developing a new `~letsencrypt.client.interfaces.IInstaller`.
Installers aimed to reconfigure UNIX servers may use Augeas for
configuration parsing and can inherit from `~.AugeasConfigurator` class
to handle much of the interface. Installers that are unable to use
Augeas may still find the `~.Reverter` class helpful in handling
configuration checkpoints and rollback.
Display
~~~~~~~
We currently offer a pythondialog and "text" mode for displays. I have
rewritten the interface which should be merged within the next day
(the rewrite is in the revoker branch of the repo and should be merged
within the next day). Display plugins implement
`~letsencrypt.client.interfaces.IDisplay` interface.
Augeas
------
Some plugins, especially those designed to reconfigure UNIX servers,
can take inherit from `~.AugeasConfigurator` class in order to more
efficiently handle common operations on UNIX server configuration
files.
We currently offer a pythondialog and "text" mode for displays. Display
plugins implement the `~letsencrypt.client.interfaces.IDisplay`
interface.
.. _coding-style:

View File

@@ -7,6 +7,7 @@ Welcome to the Let's Encrypt client documentation!
intro
using
contributing
plugins
.. toctree::
:maxdepth: 1

19
docs/plugins.rst Normal file
View File

@@ -0,0 +1,19 @@
=======
Plugins
=======
Let's Encrypt client supports dynamic discovery of plugins through the
`setuptools entry points`_. This way you can, for example, create a
custom implementation of
`~letsencrypt.client.interfaces.IAuthenticator` or the
'~letsencrypt.client.interfaces.IInstaller' without having to
merge it with the core upstream source code. An example is provided in
``examples/plugins/`` directory.
Please be aware though that as this client is still in a developer-preview
stage, the API may undergo a few changes. If you believe the plugin will be
beneficial to the community, please consider submitting a pull request to the
repo and we will update it with any necessary API changes.
.. _`setuptools entry points`:
https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins

View File

@@ -0,0 +1,18 @@
"""Example Let's Encrypt plugins."""
import zope.interface
from letsencrypt.client import interfaces
class Authenticator(object):
zope.interface.implements(interfaces.IAuthenticator)
description = 'Example Authenticator plugin'
def __init__(self, config):
self.config = config
# Implement all methods from IAuthenticator, remembering to add
# "self" as first argument, e.g. def prepare(self)...
# For full examples, see letsencrypt.client.plugins

16
examples/plugins/setup.py Normal file
View File

@@ -0,0 +1,16 @@
from setuptools import setup
setup(
name='letsencrypt-example-plugins',
package='letsencrypt_example_plugins.py',
install_requires=[
'letsencrypt',
'zope.interface',
],
entry_points={
'letsencrypt.authenticators': [
'example = letsencrypt_example_plugins:Authenticator',
],
},
)

42
examples/restified.py Normal file
View File

@@ -0,0 +1,42 @@
import logging
import os
import pkg_resources
import M2Crypto
from letsencrypt.acme import messages2
from letsencrypt.acme import jose
from letsencrypt.client import network2
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
NEW_REG_URL = 'https://www.letsencrypt-demo.org/acme/new-reg'
key = jose.JWKRSA.load(pkg_resources.resource_string(
'letsencrypt.acme.jose', os.path.join('testdata', 'rsa512_key.pem')))
net = network2.Network(NEW_REG_URL, key)
regr = net.register(contact=(
'mailto:cert-admin@example.com', 'tel:+12025551212'))
logging.info('Auto-accepting TOS: %s', regr.terms_of_service)
net.update_registration(regr.update(
body=regr.body.update(agreement=regr.terms_of_service)))
logging.debug(regr)
authzr = net.request_challenges(
identifier=messages2.Identifier(
typ=messages2.IDENTIFIER_FQDN, value='example1.com'),
regr=regr)
logging.debug(authzr)
authzr, authzr_response = net.poll(authzr)
csr = M2Crypto.X509.load_request_string(pkg_resources.resource_string(
'letsencrypt.client.tests', os.path.join('testdata', 'csr.pem')))
try:
net.request_issuance(csr, (authzr,))
except messages2.Error as error:
print error.detail

View File

@@ -1 +0,0 @@
letsencrypt/scripts/main.py

View File

@@ -18,7 +18,7 @@ class Challenge(jose.TypedJSONObjectWithFields):
TYPES = {}
class ClientChallenge(Challenge): # pylint: disable=abstract-method
class ContinuityChallenge(Challenge): # pylint: disable=abstract-method
"""Client validation challenges."""
@@ -139,7 +139,7 @@ class DVSNIResponse(ChallengeResponse):
return self.z(chall) + self.DOMAIN_SUFFIX
@Challenge.register
class RecoveryContact(ClientChallenge):
class RecoveryContact(ContinuityChallenge):
"""ACME "recoveryContact" challenge."""
typ = "recoveryContact"
@@ -156,7 +156,7 @@ class RecoveryContactResponse(ChallengeResponse):
@Challenge.register
class RecoveryToken(ClientChallenge):
class RecoveryToken(ContinuityChallenge):
"""ACME "recoveryToken" challenge."""
typ = "recoveryToken"
@@ -169,7 +169,7 @@ class RecoveryTokenResponse(ChallengeResponse):
@Challenge.register
class ProofOfPossession(ClientChallenge):
class ProofOfPossession(ContinuityChallenge):
"""ACME "proofOfPossession" challenge.
:ivar str nonce: Random data, **not** base64-encoded.
@@ -184,7 +184,8 @@ class ProofOfPossession(ClientChallenge):
"""Hints for "proofOfPossession" challenge.
:ivar jwk: JSON Web Key (:class:`letsencrypt.acme.jose.JWK`)
:ivar list certs: List of :class:`M2Crypto.X509.X509` cetificates.
:ivar list certs: List of :class:`letsencrypt.acme.jose.ComparableX509`
certificates.
"""
jwk = jose.Field("jwk", decoder=jose.JWK.from_json)

View File

@@ -13,8 +13,10 @@ from letsencrypt.acme import other
CERT = jose.ComparableX509(M2Crypto.X509.load_cert(
pkg_resources.resource_filename(
'letsencrypt.client.tests', 'testdata/cert.pem')))
KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
'letsencrypt.client.tests', os.path.join('testdata', 'rsa256_key.pem')))
KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
pkg_resources.resource_string(
'letsencrypt.client.tests',
os.path.join('testdata', 'rsa256_key.pem'))))
class SimpleHTTPSTest(unittest.TestCase):

View File

@@ -0,0 +1,25 @@
"""ACME JSON fields."""
import pyrfc3339
from letsencrypt.acme import jose
class RFC3339Field(jose.Field):
"""RFC3339 field encoder/decoder.
Handles decoding/encoding between RFC3339 strings and aware (not
naive) `datetime.datetime` objects
(e.g. ``datetime.datetime.now(pytz.utc)``).
"""
@classmethod
def default_encoder(cls, value):
return pyrfc3339.generate(value)
@classmethod
def default_decoder(cls, value):
try:
return pyrfc3339.parse(value)
except ValueError as error:
raise jose.DeserializationError(error)

View File

@@ -0,0 +1,35 @@
"""Tests for letsencrypt.acme.fields."""
import datetime
import unittest
import pytz
from letsencrypt.acme import jose
class RFC3339FieldTest(unittest.TestCase):
"""Tests for letsencrypt.acme.fields.RFC3339Field."""
def setUp(self):
self.decoded = datetime.datetime(2015, 3, 27, tzinfo=pytz.utc)
self.encoded = '2015-03-27T00:00:00Z'
def test_default_encoder(self):
from letsencrypt.acme.fields import RFC3339Field
self.assertEqual(
self.encoded, RFC3339Field.default_encoder(self.decoded))
def test_default_encoder_naive_fails(self):
from letsencrypt.acme.fields import RFC3339Field
self.assertRaises(
ValueError, RFC3339Field.default_encoder, datetime.datetime.now())
def test_default_decoder(self):
from letsencrypt.acme.fields import RFC3339Field
self.assertEqual(
self.decoded, RFC3339Field.default_decoder(self.encoded))
def test_default_decoder_raises_deserialization_error(self):
from letsencrypt.acme.fields import RFC3339Field
self.assertRaises(
jose.DeserializationError, RFC3339Field.default_decoder, '')

View File

@@ -70,5 +70,6 @@ from letsencrypt.acme.jose.jws import JWS
from letsencrypt.acme.jose.util import (
ComparableX509,
HashableRSAKey,
ImmutableMap,
)

View File

@@ -129,18 +129,24 @@ class JSONDeSerializable(object):
:returns: Fully serialized object.
"""
partial = self.to_json()
try_serialize = (lambda x: x.fully_serialize()
if isinstance(x, JSONDeSerializable) else x)
if isinstance(partial, basestring): # strings are sequences
return partial
if isinstance(partial, collections.Sequence):
return [try_serialize(elem) for elem in partial]
elif isinstance(partial, collections.Mapping):
return dict([(try_serialize(key), try_serialize(value))
for key, value in partial.iteritems()])
else:
return partial
def _serialize(obj):
if isinstance(obj, JSONDeSerializable):
return _serialize(obj.to_json())
if isinstance(obj, basestring): # strings are sequence
return obj
elif isinstance(obj, list):
return [_serialize(subobj) for subobj in obj]
elif isinstance(obj, collections.Sequence):
# default to tuple, otherwise Mapping could get
# unhashable list
return tuple(_serialize(subobj) for subobj in obj)
elif isinstance(obj, collections.Mapping):
return dict((_serialize(key), _serialize(value))
for key, value in obj.iteritems())
else:
return obj
return _serialize(self)
@util.abstractclassmethod
def from_json(cls, unused_jobj):

View File

@@ -3,6 +3,7 @@ import unittest
class JSONDeSerializableTest(unittest.TestCase):
# pylint: disable=too-many-instance-attributes
def setUp(self):
from letsencrypt.acme.jose.interfaces import JSONDeSerializable
@@ -50,6 +51,8 @@ class JSONDeSerializableTest(unittest.TestCase):
self.basic2 = Basic('foo2')
self.seq = Sequence(self.basic1, self.basic2)
self.mapping = Mapping(self.basic1, self.basic2)
self.nested = Basic([[self.basic1]])
self.tuple = Basic(('foo',))
# pylint: disable=invalid-name
self.Basic = Basic
@@ -66,6 +69,12 @@ class JSONDeSerializableTest(unittest.TestCase):
mock_value = object()
self.assertTrue(self.Basic(mock_value).fully_serialize() is mock_value)
def test_fully_serialize_nested(self):
self.assertEqual(self.nested.fully_serialize(), [['foo1']])
def test_fully_serialize(self):
self.assertEqual(self.tuple.fully_serialize(), (('foo', )))
def test_from_json_not_implemented(self):
from letsencrypt.acme.jose.interfaces import JSONDeSerializable
self.assertRaises(TypeError, JSONDeSerializable.from_json, 'xxx')

View File

@@ -83,7 +83,11 @@ class JWKOct(JWK):
@JWK.register
class JWKRSA(JWK):
"""RSA JWK."""
"""RSA JWK.
:ivar key: `Crypto.PublicKey.RSA` wrapped in `.HashableRSAKey`
"""
typ = 'RSA'
__slots__ = ('key',)
@@ -114,7 +118,8 @@ class JWKRSA(JWK):
:rtype: :class:`JWKRSA`
"""
return cls(key=Crypto.PublicKey.RSA.importKey(string))
return cls(key=util.HashableRSAKey(
Crypto.PublicKey.RSA.importKey(string)))
def public(self):
return type(self)(key=self.key.publickey())

View File

@@ -3,7 +3,7 @@ import argparse
import base64
import sys
import M2Crypto.X509
import M2Crypto
from letsencrypt.acme.jose import b64
from letsencrypt.acme.jose import errors

View File

@@ -5,7 +5,7 @@ import pkg_resources
import unittest
import Crypto.PublicKey.RSA
import M2Crypto.X509
import M2Crypto
import mock
from letsencrypt.acme.jose import b64

View File

@@ -41,6 +41,26 @@ class ComparableX509(object): # pylint: disable=too-few-public-methods
return self.as_der() == other.as_der()
class HashableRSAKey(object): # pylint: disable=too-few-public-methods
"""Wrapper for `Crypto.PublicKey.RSA` objects that supports hashing."""
def __init__(self, wrapped):
self._wrapped = wrapped
def __getattr__(self, name):
return getattr(self._wrapped, name)
def __eq__(self, other):
return self._wrapped == other
def __hash__(self):
return hash((type(self), self.exportKey(format='DER')))
def publickey(self):
"""Get wrapped public key."""
return type(self)(self._wrapped.publickey())
class ImmutableMap(collections.Mapping, collections.Hashable):
# pylint: disable=too-few-public-methods
"""Immutable key to value mapping with attribute access."""
@@ -57,6 +77,12 @@ class ImmutableMap(collections.Mapping, collections.Hashable):
for slot in self.__slots__:
object.__setattr__(self, slot, kwargs.pop(slot))
def update(self, **kwargs):
"""Return updated map."""
items = dict(self)
items.update(kwargs)
return type(self)(**items) # pylint: disable=star-args
def __getitem__(self, key):
try:
return getattr(self, key)

View File

@@ -1,7 +1,36 @@
"""Tests for letsencrypt.acme.jose.util."""
import functools
import os
import pkg_resources
import unittest
import Crypto.PublicKey.RSA
class HashableRSAKeyTest(unittest.TestCase):
"""Tests for letsencrypt.acme.jose.util.HashableRSAKey."""
def setUp(self):
from letsencrypt.acme.jose.util import HashableRSAKey
self.key = HashableRSAKey(Crypto.PublicKey.RSA.importKey(
pkg_resources.resource_string(
__name__, os.path.join('testdata', 'rsa256_key.pem'))))
self.key_same = HashableRSAKey(Crypto.PublicKey.RSA.importKey(
pkg_resources.resource_string(
__name__, os.path.join('testdata', 'rsa256_key.pem'))))
def test_eq(self):
# if __eq__ is not defined, then two HashableRSAKeys with same
# _wrapped do not equate
self.assertEqual(self.key, self.key_same)
def test_hash(self):
self.assertTrue(isinstance(hash(self.key), int))
def test_publickey(self):
from letsencrypt.acme.jose.util import HashableRSAKey
self.assertTrue(isinstance(self.key.publickey(), HashableRSAKey))
class ImmutableMapTest(unittest.TestCase):
"""Tests for letsencrypt.acme.jose.util.ImmutableMap."""
@@ -25,6 +54,10 @@ class ImmutableMapTest(unittest.TestCase):
self.a2 = self.A(x=3, y=4)
self.b = self.B(x=1, y=2)
def test_update(self):
self.assertEqual(self.A(x=2, y=2), self.a1.update(x=2))
self.assertEqual(self.a2, self.a1.update(x=3, y=4))
def test_get_missing_item_raises_key_error(self):
self.assertRaises(KeyError, self.a1.__getitem__, 'z')

View File

@@ -0,0 +1,298 @@
"""ACME protocol v02 messages."""
from letsencrypt.acme import challenges
from letsencrypt.acme import fields
from letsencrypt.acme import jose
class Error(jose.JSONObjectWithFields, Exception):
"""ACME error.
https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00
"""
ERROR_TYPE_NAMESPACE = 'urn:acme:error:'
ERROR_TYPE_DESCRIPTIONS = {
'malformed': 'The request message was malformed',
'unauthorized': 'The client lacks sufficient authorization',
'serverInternal': 'The server experienced an internal error',
'badCSR': 'The CSR is unacceptable (e.g., due to a short key)',
}
# TODO: Boulder omits 'type' and 'instance', spec requires
typ = jose.Field('type', omitempty=True)
title = jose.Field('title', omitempty=True)
detail = jose.Field('detail')
instance = jose.Field('instance', omitempty=True)
@typ.encoder
def typ(value): # pylint: disable=missing-docstring,no-self-argument
return Error.ERROR_TYPE_NAMESPACE + value
@typ.decoder
def typ(value): # pylint: disable=missing-docstring,no-self-argument
# pylint thinks isinstance(value, Error), so startswith is not found
# pylint: disable=no-member
if not value.startswith(Error.ERROR_TYPE_NAMESPACE):
raise jose.DeserializationError('Missing error type prefix')
without_prefix = value[len(Error.ERROR_TYPE_NAMESPACE):]
if without_prefix not in Error.ERROR_TYPE_DESCRIPTIONS:
raise jose.DeserializationError('Error type not recognized')
return without_prefix
@property
def description(self):
"""Hardcoded error description based on its type."""
return self.ERROR_TYPE_DESCRIPTIONS[self.typ]
class _Constant(jose.JSONDeSerializable):
"""ACME constant."""
__slots__ = ('name',)
POSSIBLE_NAMES = NotImplemented
def __init__(self, name):
self.POSSIBLE_NAMES[name] = self
self.name = name
def to_json(self):
return self.name
@classmethod
def from_json(cls, value):
if value not in cls.POSSIBLE_NAMES:
raise jose.DeserializationError(
'{0} not recognized'.format(cls.__name__))
return cls.POSSIBLE_NAMES[value]
def __repr__(self):
return '{0}({1})'.format(self.__class__.__name__, self.name)
def __eq__(self, other):
return isinstance(other, type(self)) and other.name == self.name
class Status(_Constant):
"""ACME "status" field."""
POSSIBLE_NAMES = {}
STATUS_UNKNOWN = Status('unknown')
STATUS_PENDING = Status('pending')
STATUS_PROCESSING = Status('processing')
STATUS_VALID = Status('valid')
STATUS_INVALID = Status('invalid')
STATUS_REVOKED = Status('revoked')
class IdentifierType(_Constant):
"""ACME identifier type."""
POSSIBLE_NAMES = {}
IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder
class Identifier(jose.JSONObjectWithFields):
"""ACME identifier.
:ivar letsencrypt.acme.messages2.IdentifierType typ:
"""
typ = jose.Field('type', decoder=IdentifierType.from_json)
value = jose.Field('value')
class Resource(jose.ImmutableMap):
"""ACME Resource.
:ivar letsencrypt.acme.messages2.ResourceBody body: Resource body.
:ivar str uri: Location of the resource.
"""
__slots__ = ('body', 'uri')
class ResourceBody(jose.JSONObjectWithFields):
"""ACME Resource Body."""
class RegistrationResource(Resource):
"""Registration Resource.
:ivar letsencrypt.acme.messages2.Registration body:
:ivar str new_authzr_uri: URI found in the 'next' ``Link`` header
:ivar str terms_of_service: URL for the CA TOS.
"""
__slots__ = ('body', 'uri', 'new_authzr_uri', 'terms_of_service')
class Registration(ResourceBody):
"""Registration Resource Body.
:ivar letsencrypt.acme.jose.jwk.JWK key: Public key.
:ivar tuple contact:
"""
# on new-reg key server ignores 'key' and populates it based on
# JWS.signature.combined.jwk
key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json)
contact = jose.Field('contact', omitempty=True, default=())
recovery_token = jose.Field('recoveryToken', omitempty=True)
agreement = jose.Field('agreement', omitempty=True)
class ChallengeResource(Resource, jose.JSONObjectWithFields):
"""Challenge Resource.
:ivar letsencrypt.acme.messages2.ChallengeBody body:
:ivar str authzr_uri: URI found in the 'up' ``Link`` header.
"""
__slots__ = ('body', 'authzr_uri')
@property
def uri(self): # pylint: disable=missing-docstring,no-self-argument
# bug? 'method already defined line None'
# pylint: disable=function-redefined
return self.body.uri
class ChallengeBody(ResourceBody):
"""Challenge Resource Body.
.. todo::
Confusingly, this has a similar name to `.challenges.Challenge`,
as well as `.achallenges.AnnotatedChallenge` or
`.achallenges.Indexed`... Once `messages2` and `network2` is
integrated with the rest of the client, this class functionality
will be merged with `.challenges.Challenge`. Meanwhile,
separation allows the ``master`` to be still interoperable with
Node.js server (protocol v00). For the time being use names such
as ``challb`` to distinguish instances of this class from
``achall`` or ``ichall``.
:ivar letsencrypt.acme.messages2.Status status:
:ivar datetime.datetime validated:
"""
__slots__ = ('chall',)
uri = jose.Field('uri')
status = jose.Field('status', decoder=Status.from_json)
validated = fields.RFC3339Field('validated', omitempty=True)
def to_json(self):
jobj = super(ChallengeBody, self).to_json()
jobj.update(self.chall.to_json())
return jobj
@classmethod
def fields_from_json(cls, jobj):
jobj_fields = super(ChallengeBody, cls).fields_from_json(jobj)
jobj_fields['chall'] = challenges.Challenge.from_json(jobj)
return jobj_fields
class AuthorizationResource(Resource):
"""Authorization Resource.
:ivar letsencrypt.acme.messages2.Authorization body:
:ivar str new_cert_uri: URI found in the 'next' ``Link`` header
"""
__slots__ = ('body', 'uri', 'new_cert_uri')
class Authorization(ResourceBody):
"""Authorization Resource Body.
:ivar letsencrypt.acme.messages2.Identifier identifier:
:ivar list challenges: `list` of `Challenge`
:ivar tuple combinations: Challenge combinations (`tuple` of `tuple`
of `int`, as opposed to `list` of `list` from the spec).
:ivar letsencrypt.acme.jose.jwk.JWK key: Public key.
:ivar tuple contact:
:ivar letsencrypt.acme.messages2.Status status:
:ivar datetime.datetime expires:
"""
identifier = jose.Field('identifier', decoder=Identifier.from_json)
challenges = jose.Field('challenges', omitempty=True)
combinations = jose.Field('combinations', omitempty=True)
# TODO: acme-spec #92, #98
key = Registration._fields['key']
contact = Registration._fields['contact']
status = jose.Field('status', omitempty=True, decoder=Status.from_json)
# TODO: 'expires' is allowed for Authorization Resources in
# general, but for Key Authorization '[t]he "expires" field MUST
# be absent'... then acme-spec gives example with 'expires'
# present... That's confusing!
expires = fields.RFC3339Field('expires', omitempty=True)
@challenges.decoder
def challenges(value): # pylint: disable=missing-docstring,no-self-argument
return tuple(ChallengeBody.from_json(chall) for chall in value)
@property
def resolved_combinations(self):
"""Combinations with challenges instead of indices."""
return tuple(tuple(self.challenges[idx] for idx in combo)
for combo in self.combinations)
class CertificateRequest(jose.JSONObjectWithFields):
"""ACME new-cert request.
:ivar letsencrypt.acme.jose.util.ComparableX509 csr:
`M2Crypto.X509.Request` wrapped in `.ComparableX509`
:ivar tuple authorizations: `tuple` of URIs (`str`)
"""
csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
authorizations = jose.Field('authorizations', decoder=tuple)
class CertificateResource(Resource):
"""Certificate Resource.
:ivar letsencrypt.acme.jose.util.ComparableX509 body:
`M2Crypto.X509.X509` wrapped in `.ComparableX509`
:ivar str cert_chain_uri: URI found in the 'up' ``Link`` header
:ivar tuple authzrs: `tuple` of `AuthorizationResource`.
"""
__slots__ = ('body', 'uri', 'cert_chain_uri', 'authzrs')
class Revocation(jose.JSONObjectWithFields):
"""Revocation message.
:ivar revoke: Either a `datetime.datetime` or `Revocation.NOW`.
:ivar tuple authorizations: Same as `CertificateRequest.authorizations`
"""
NOW = 'now'
"""A possible value for `revoke`, denoting that certificate should
be revoked now."""
revoke = jose.Field('revoke')
authorizations = CertificateRequest._fields['authorizations']
@revoke.decoder
def revoke(value): # pylint: disable=missing-docstring,no-self-argument
if value == Revocation.NOW:
return value
else:
return fields.RFC3339Field.default_decoder(value)
@revoke.encoder
def revoke(value): # pylint: disable=missing-docstring,no-self-argument
if value == Revocation.NOW:
return value
else:
return fields.RFC3339Field.default_encoder(value)

View File

@@ -0,0 +1,172 @@
"""Tests for letsencrypt.acme.messages2."""
import datetime
import unittest
import mock
import pytz
from letsencrypt.acme import challenges
from letsencrypt.acme import jose
class ErrorTest(unittest.TestCase):
"""Tests for letsencrypt.acme.messages2.Error."""
def setUp(self):
from letsencrypt.acme.messages2 import Error
self.error = Error(detail='foo', typ='malformed')
def test_typ_prefix(self):
self.assertEqual('malformed', self.error.typ)
self.assertEqual(
'urn:acme:error:malformed', self.error.to_json()['type'])
self.assertEqual(
'malformed', self.error.from_json(self.error.to_json()).typ)
def test_typ_decoder_missing_prefix(self):
from letsencrypt.acme.messages2 import Error
self.assertRaises(jose.DeserializationError, Error.from_json,
{'detail': 'foo', 'type': 'malformed'})
self.assertRaises(jose.DeserializationError, Error.from_json,
{'detail': 'foo', 'type': 'not valid bare type'})
def test_typ_decoder_not_recognized(self):
from letsencrypt.acme.messages2 import Error
self.assertRaises(jose.DeserializationError, Error.from_json,
{'detail': 'foo', 'type': 'urn:acme:error:baz'})
def test_description(self):
self.assertEqual(
'The request message was malformed', self.error.description)
class ConstantTest(unittest.TestCase):
"""Tests for letsencrypt.acme.messages2._Constant."""
def setUp(self):
from letsencrypt.acme.messages2 import _Constant
class MockConstant(_Constant): # pylint: disable=missing-docstring
POSSIBLE_NAMES = {}
self.MockConstant = MockConstant # pylint: disable=invalid-name
self.const_a = MockConstant('a')
self.const_b = MockConstant('b')
def test_to_json(self):
self.assertEqual('a', self.const_a.to_json())
self.assertEqual('b', self.const_b.to_json())
def test_from_json(self):
self.assertEqual(self.const_a, self.MockConstant.from_json('a'))
self.assertRaises(
jose.DeserializationError, self.MockConstant.from_json, 'c')
def test_repr(self):
self.assertEqual('MockConstant(a)', repr(self.const_a))
self.assertEqual('MockConstant(b)', repr(self.const_b))
class ChallengeResourceTest(unittest.TestCase):
"""Tests for letsencrypt.acme.messages2.ChallengeResource."""
def test_uri(self):
from letsencrypt.acme.messages2 import ChallengeResource
self.assertEqual('http://challb', ChallengeResource(body=mock.MagicMock(
uri='http://challb'), authzr_uri='http://authz').uri)
class ChallengeBodyTest(unittest.TestCase):
"""Tests for letsencrypt.acme.messages2.ChallengeBody."""
def setUp(self):
self.chall = challenges.DNS(token='foo')
from letsencrypt.acme.messages2 import ChallengeBody
from letsencrypt.acme.messages2 import STATUS_VALID
self.status = STATUS_VALID
self.challb = ChallengeBody(
uri='http://challb', chall=self.chall, status=self.status)
self.jobj_to = {
'uri': 'http://challb',
'status': self.status,
'type': 'dns',
'token': 'foo',
}
self.jobj_from = self.jobj_to.copy()
self.jobj_from['status'] = 'valid'
def test_to_json(self):
self.assertEqual(self.jobj_to, self.challb.to_json())
def test_fields_from_json(self):
from letsencrypt.acme.messages2 import ChallengeBody
self.assertEqual(self.challb, ChallengeBody.from_json(self.jobj_from))
class AuthorizationTest(unittest.TestCase):
"""Tests for letsencrypt.acme.messages2.Authorization."""
def setUp(self):
from letsencrypt.acme.messages2 import ChallengeBody
from letsencrypt.acme.messages2 import STATUS_VALID
self.challbs = (
ChallengeBody(
uri='http://challb1', status=STATUS_VALID,
chall=challenges.SimpleHTTPS(token='IlirfxKKXAsHtmzK29Pj8A')),
ChallengeBody(uri='http://challb2', status=STATUS_VALID,
chall=challenges.DNS(token='DGyRejmCefe7v4NfDGDKfA')),
ChallengeBody(uri='http://challb3', status=STATUS_VALID,
chall=challenges.RecoveryToken()),
)
combinations = ((0, 2), (1, 2))
from letsencrypt.acme.messages2 import Authorization
from letsencrypt.acme.messages2 import Identifier
from letsencrypt.acme.messages2 import IDENTIFIER_FQDN
identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com')
self.authz = Authorization(
identifier=identifier, combinations=combinations,
challenges=self.challbs)
self.jobj_from = {
'identifier': identifier.fully_serialize(),
'challenges': [challb.fully_serialize() for challb in self.challbs],
'combinations': combinations,
}
def test_from_json(self):
from letsencrypt.acme.messages2 import Authorization
Authorization.from_json(self.jobj_from)
def test_resolved_combinations(self):
self.assertEqual(self.authz.resolved_combinations, (
(self.challbs[0], self.challbs[2]),
(self.challbs[1], self.challbs[2]),
))
class RevocationTest(unittest.TestCase):
"""Tests for letsencrypt.acme.messages2.RevocationTest."""
def setUp(self):
from letsencrypt.acme.messages2 import Revocation
self.rev_now = Revocation(authorizations=(), revoke=Revocation.NOW)
self.rev_date = Revocation(authorizations=(), revoke=datetime.datetime(
2015, 3, 27, tzinfo=pytz.utc))
self.jobj_now = {'authorizations': (), 'revoke': Revocation.NOW}
self.jobj_date = {'authorizations': (),
'revoke': '2015-03-27T00:00:00Z'}
def test_revoke_decoder(self):
from letsencrypt.acme.messages2 import Revocation
self.assertEqual(self.rev_now, Revocation.from_json(self.jobj_now))
self.assertEqual(self.rev_date, Revocation.from_json(self.jobj_date))
def test_revoke_encoder(self):
self.assertEqual(self.jobj_now, self.rev_now.to_json())
self.assertEqual(self.jobj_date, self.rev_date.to_json())
if __name__ == '__main__':
unittest.main()

View File

@@ -11,8 +11,9 @@ from letsencrypt.acme import jose
from letsencrypt.acme import other
KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
'letsencrypt.client.tests', 'testdata/rsa256_key.pem'))
KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
pkg_resources.resource_string(
'letsencrypt.client.tests', 'testdata/rsa256_key.pem')))
CERT = jose.ComparableX509(M2Crypto.X509.load_cert(
pkg_resources.resource_filename(
'letsencrypt.client.tests', 'testdata/cert.pem')))

View File

@@ -7,10 +7,12 @@ import Crypto.PublicKey.RSA
from letsencrypt.acme import jose
RSA256_KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
'letsencrypt.client.tests', 'testdata/rsa256_key.pem'))
RSA512_KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
'letsencrypt.client.tests', 'testdata/rsa512_key.pem'))
RSA256_KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
pkg_resources.resource_string(
'letsencrypt.client.tests', 'testdata/rsa256_key.pem')))
RSA512_KEY = jose.HashableRSAKey(
Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
'letsencrypt.client.tests', 'testdata/rsa512_key.pem')))
class SignatureTest(unittest.TestCase):

View File

@@ -1 +0,0 @@
"""Let's Encrypt client.apache."""

View File

@@ -5,6 +5,7 @@ import sys
import Crypto.PublicKey.RSA
from letsencrypt.acme import challenges
from letsencrypt.acme import jose
from letsencrypt.acme import messages
from letsencrypt.client import achallenges
@@ -16,12 +17,12 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
"""ACME Authorization Handler for a client.
:ivar dv_auth: Authenticator capable of solving
:const:`~letsencrypt.client.constants.DV_CHALLENGES`
:class:`~letsencrypt.acme.challenges.DVChallenge` types
:type dv_auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
:ivar client_auth: Authenticator capable of solving
:const:`~letsencrypt.client_auth.constants.CLIENT_CHALLENGES`
:type client_auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
:ivar cont_auth: Authenticator capable of solving
:class:`~letsencrypt.acme.challenges.ContinuityChallenge` types
:type cont_auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
:ivar network: Network object for sending and receiving authorization
messages
@@ -36,13 +37,13 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
:ivar dict paths: optimal path for authorization. eg. paths[domain]
:ivar dict dv_c: Keys - domain, Values are DV challenges in the form of
:class:`letsencrypt.client.achallenges.Indexed`
:ivar dict client_c: Keys - domain, Values are Client challenges in the form
of :class:`letsencrypt.client.achallenges.Indexed`
:ivar dict cont_c: Keys - domain, Values are Continuity challenges in the
form of :class:`letsencrypt.client.achallenges.Indexed`
"""
def __init__(self, dv_auth, client_auth, network):
def __init__(self, dv_auth, cont_auth, network):
self.dv_auth = dv_auth
self.client_auth = client_auth
self.cont_auth = cont_auth
self.network = network
self.domains = []
@@ -52,7 +53,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
self.paths = dict()
self.dv_c = dict()
self.client_c = dict()
self.cont_c = dict()
def add_chall_msg(self, domain, msg, authkey):
"""Add a challenge message to the AuthHandler.
@@ -76,7 +77,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
self.authkey[domain] = authkey
def get_authorizations(self):
"""Retreive all authorizations for challenges.
"""Retrieve all authorizations for challenges.
:raises LetsEncryptAuthHandlerError: If unable to retrieve all
authorizations
@@ -119,8 +120,8 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
nonce=self.msgs[domain].nonce,
responses=self.responses[domain],
name=domain,
key=Crypto.PublicKey.RSA.importKey(
self.authkey[domain].pem)),
key=jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
self.authkey[domain].pem))),
messages.Authorization)
logging.info("Received Authorization for %s", domain)
return auth
@@ -147,24 +148,24 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
self._get_chall_pref(dom),
self.msgs[dom].combinations)
self.dv_c[dom], self.client_c[dom] = self._challenge_factory(
self.dv_c[dom], self.cont_c[dom] = self._challenge_factory(
dom, self.paths[dom])
# Flatten challs for authenticator functions and remove index
# Order is important here as we will not expose the outside
# Authenticator to our own indices.
flat_client = []
flat_cont = []
flat_dv = []
for dom in self.domains:
flat_client.extend(ichall.achall for ichall in self.client_c[dom])
flat_cont.extend(ichall.achall for ichall in self.cont_c[dom])
flat_dv.extend(ichall.achall for ichall in self.dv_c[dom])
client_resp = []
cont_resp = []
dv_resp = []
try:
if flat_client:
client_resp = self.client_auth.perform(flat_client)
if flat_cont:
cont_resp = self.cont_auth.perform(flat_cont)
if flat_dv:
dv_resp = self.dv_auth.perform(flat_dv)
# This will catch both specific types of errors.
@@ -181,8 +182,8 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
logging.info("Ready for verification...")
# Assemble Responses
if client_resp:
self._assign_responses(client_resp, self.client_c)
if cont_resp:
self._assign_responses(cont_resp, self.cont_c)
if dv_resp:
self._assign_responses(dv_resp, self.dv_c)
@@ -191,7 +192,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
:param list flat_list: flat_list of responses from an IAuthenticator
:param dict ichall_dict: Master dict mapping all domains to a list of
their associated 'client' and 'dv' Indexed challenges, or their
their associated 'continuity' and 'dv' Indexed challenges, or their
:class:`letsencrypt.client.achallenges.Indexed` list
"""
@@ -203,7 +204,8 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
def _path_satisfied(self, dom):
"""Returns whether a path has been completely satisfied."""
return all(self.responses[dom][i] is not None for i in self.paths[dom])
# Make sure that there are no 'None' or 'False' entries along path.
return all(self.responses[dom][i] for i in self.paths[dom])
def _get_chall_pref(self, domain):
"""Return list of challenge preferences.
@@ -212,7 +214,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
"""
chall_prefs = []
chall_prefs.extend(self.client_auth.get_chall_pref(domain))
chall_prefs.extend(self.cont_auth.get_chall_pref(domain))
chall_prefs.extend(self.dv_auth.get_chall_pref(domain))
return chall_prefs
@@ -227,11 +229,11 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
# Chose to make these lists instead of a generator to make it easier to
# work with...
dv_list = [ichall.achall for ichall in self.dv_c[domain]]
client_list = [ichall.achall for ichall in self.client_c[domain]]
cont_list = [ichall.achall for ichall in self.cont_c[domain]]
if dv_list:
self.dv_auth.cleanup(dv_list)
if client_list:
self.client_auth.cleanup(client_list)
if cont_list:
self.cont_auth.cleanup(cont_list)
def _cleanup_state(self, delete_list):
"""Cleanup state after an authorization is received.
@@ -246,7 +248,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
del self.authkey[domain]
del self.client_c[domain]
del self.cont_c[domain]
del self.dv_c[domain]
self.domains.remove(domain)
@@ -258,9 +260,9 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
:param list path: List of indices from `challenges`.
:returns: dv_chall, list of
:returns: dv_chall, list of DVChallenge type
:class:`letsencrypt.client.achallenges.Indexed`
client_chall, list of
cont_chall, list of ContinuityChallenge type
:class:`letsencrypt.client.achallenges.Indexed`
:rtype: tuple
@@ -269,7 +271,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
"""
dv_chall = []
client_chall = []
cont_chall = []
for index in path:
chall = self.msgs[domain].challenges[index]
@@ -303,35 +305,38 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
ichall = achallenges.Indexed(achall=achall, index=index)
if isinstance(chall, challenges.ClientChallenge):
client_chall.append(ichall)
if isinstance(chall, challenges.ContinuityChallenge):
cont_chall.append(ichall)
elif isinstance(chall, challenges.DVChallenge):
dv_chall.append(ichall)
return dv_chall, client_chall
return dv_chall, cont_chall
def gen_challenge_path(challs, preferences, combinations):
"""Generate a plan to get authority over the identity.
.. todo:: Make sure that the challenges are feasible...
Example: Do you have the recovery key?
.. todo:: This can be possibly be rewritten to use resolved_combinations.
:param list challs: A list of challenges
:param tuple challs: A tuple of challenges
(:class:`letsencrypt.acme.challenges.Challenge`) from
:class:`letsencrypt.acme.messages.Challenge` server message to
be fulfilled by the client in order to prove possession of the
identifier.
:param list preferences: List of challenge preferences for domain
(:class:`letsencrypt.acme.challenges.Challege` subclasses)
(:class:`letsencrypt.acme.challenges.Challenge` subclasses)
:param list combinations: A collection of sets of challenges from
:param tuple combinations: A collection of sets of challenges from
:class:`letsencrypt.acme.messages.Challenge`, each of which would
be sufficient to prove possession of the identifier.
:returns: List of indices from ``challenges``.
:rtype: list
:returns: tuple of indices from ``challenges``.
:rtype: tuple
:raises letsencrypt.client.errors.LetsEncryptAuthHandlerError: If a
path cannot be created that satisfies the CA given the preferences and
combinations.
"""
if combinations:
@@ -348,29 +353,34 @@ def _find_smart_path(challs, preferences, combinations):
"""
chall_cost = {}
max_cost = 0
max_cost = 1
for i, chall_cls in enumerate(preferences):
chall_cost[chall_cls] = i
max_cost += i
# max_cost is now equal to sum(indices) + 1
best_combo = []
# Set above completing all of the available challenges
best_combo_cost = max_cost + 1
best_combo_cost = max_cost
combo_total = 0
for combo in combinations:
for challenge_index in combo:
combo_total += chall_cost.get(challs[
challenge_index].__class__, max_cost)
if combo_total < best_combo_cost:
best_combo = combo
best_combo_cost = combo_total
combo_total = 0
combo_total = 0
if not best_combo:
logging.fatal("Client does not support any combination of "
"challenges to satisfy ACME server")
sys.exit(22)
msg = ("Client does not support any combination of challenges that "
"will satisfy the CA.")
logging.fatal(msg)
raise errors.LetsEncryptAuthHandlerError(msg)
return best_combo

View File

@@ -6,11 +6,11 @@ import sys
import Crypto.PublicKey.RSA
import M2Crypto
from letsencrypt.acme import jose
from letsencrypt.acme import messages
from letsencrypt.acme.jose import util as jose_util
from letsencrypt.client import auth_handler
from letsencrypt.client import client_authenticator
from letsencrypt.client import continuity_auth
from letsencrypt.client import crypto_util
from letsencrypt.client import errors
from letsencrypt.client import le_util
@@ -18,7 +18,7 @@ from letsencrypt.client import network
from letsencrypt.client import reverter
from letsencrypt.client import revoker
from letsencrypt.client.apache import configurator
from letsencrypt.client.plugins.apache import configurator
from letsencrypt.client.display import ops as display_ops
from letsencrypt.client.display import enhancements
@@ -33,7 +33,8 @@ class Client(object):
:type authkey: :class:`letsencrypt.client.le_util.Key`
:ivar auth_handler: Object that supports the IAuthenticator interface.
auth_handler contains both a dv_authenticator and a client_authenticator
auth_handler contains both a dv_authenticator and a
continuity_authenticator
:type auth_handler: :class:`letsencrypt.client.auth_handler.AuthHandler`
:ivar installer: Object supporting the IInstaller interface.
@@ -60,9 +61,9 @@ class Client(object):
self.config = config
if dv_auth is not None:
client_auth = client_authenticator.ClientAuthenticator(config)
cont_auth = continuity_auth.ContinuityAuthenticator(config)
self.auth_handler = auth_handler.AuthHandler(
dv_auth, client_auth, self.network)
dv_auth, cont_auth, self.network)
else:
self.auth_handler = None
@@ -130,9 +131,10 @@ class Client(object):
logging.info("Preparing and sending CSR...")
return self.network.send_and_receive_expected(
messages.CertificateRequest.create(
csr=jose_util.ComparableX509(
csr=jose.ComparableX509(
M2Crypto.X509.load_request_der_string(csr_der)),
key=Crypto.PublicKey.RSA.importKey(self.authkey.pem)),
key=jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
self.authkey.pem))),
messages.Certificate)
def save_certificate(self, certificate_msg, cert_path, chain_path):

View File

@@ -31,7 +31,7 @@ List of expected options parameters:
APACHE_MOD_SSL_CONF = pkg_resources.resource_filename(
"letsencrypt.client.apache", "options-ssl.conf")
"letsencrypt.client.plugins.apache", "options-ssl.conf")
"""Path to the Apache mod_ssl config file found in the Let's Encrypt
distribution."""

View File

@@ -1,4 +1,4 @@
"""Client Authenticator"""
"""Continuity Authenticator"""
import zope.interface
from letsencrypt.acme import challenges
@@ -9,9 +9,9 @@ from letsencrypt.client import interfaces
from letsencrypt.client import recovery_token
class ClientAuthenticator(object):
class ContinuityAuthenticator(object):
"""IAuthenticator for
:const:`~letsencrypt.client.constants.CLIENT_CHALLENGES`.
:const:`~letsencrypt.acme.challenges.ContinuityChallenge` class challenges.
:ivar rec_token: Performs "recoveryToken" challenges
:type rec_token: :class:`letsencrypt.client.recovery_token.RecoveryToken`
@@ -41,7 +41,7 @@ class ClientAuthenticator(object):
if isinstance(achall, achallenges.RecoveryToken):
responses.append(self.rec_token.perform(achall))
else:
raise errors.LetsEncryptClientAuthError("Unexpected Challenge")
raise errors.LetsEncryptContAuthError("Unexpected Challenge")
return responses
def cleanup(self, achalls):
@@ -50,4 +50,4 @@ class ClientAuthenticator(object):
if isinstance(achall, achallenges.RecoveryToken):
self.rec_token.cleanup(achall)
else:
raise errors.LetsEncryptClientAuthError("Unexpected Challenge")
raise errors.LetsEncryptContAuthError("Unexpected Challenge")

View File

@@ -5,6 +5,14 @@ class LetsEncryptClientError(Exception):
"""Generic Let's Encrypt client error."""
class NetworkError(LetsEncryptClientError):
"""Network error."""
class UnexpectedUpdate(NetworkError):
"""Unexpected update."""
class LetsEncryptReverterError(LetsEncryptClientError):
"""Let's Encrypt Reverter error."""
@@ -14,7 +22,7 @@ class LetsEncryptAuthHandlerError(LetsEncryptClientError):
"""Let's Encrypt Auth Handler error."""
class LetsEncryptClientAuthError(LetsEncryptAuthHandlerError):
class LetsEncryptContAuthError(LetsEncryptAuthHandlerError):
"""Let's Encrypt Client Authenticator error."""

View File

@@ -11,6 +11,9 @@ from letsencrypt.acme import messages
from letsencrypt.client import errors
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
logging.getLogger("requests").setLevel(logging.WARNING)

View File

@@ -0,0 +1,506 @@
"""Networking for ACME protocol v02."""
import datetime
import heapq
import httplib
import itertools
import logging
import time
import M2Crypto
import requests
import werkzeug
from letsencrypt.acme import jose
from letsencrypt.acme import messages2
from letsencrypt.client import errors
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
class Network(object):
"""ACME networking.
.. todo::
Clean up raised error types hierarchy, document, and handle (wrap)
instances of `.DeserializationError` raised in `from_json()``.
:ivar str new_reg_uri: Location of new-reg
:ivar key: `.JWK` (private)
:ivar alg: `.JWASignature`
"""
DER_CONTENT_TYPE = 'application/pkix-cert'
JSON_CONTENT_TYPE = 'application/json'
JSON_ERROR_CONTENT_TYPE = 'application/problem+json'
def __init__(self, new_reg_uri, key, alg=jose.RS256):
self.new_reg_uri = new_reg_uri
self.key = key
self.alg = alg
def _wrap_in_jws(self, obj):
"""Wrap `JSONDeSerializable` object in JWS.
: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()
@classmethod
def _check_response(cls, response, content_type=None):
"""Check response content and its type.
.. note::
Checking is not strict: wrong server response ``Content-Type``
HTTP header is ignored if response is an expected JSON object
(c.f. Boulder #56).
:param str content_type: Expected Content-Type response header.
If JSON is expected and not present in server response, this
function will raise an error. Otherwise, wrong Content-Type
is ignored, but logged.
:raises letsencrypt.messages2.Error: If server response body
carries HTTP Problem (draft-ietf-appsawg-http-problem-00).
:raises letsencrypt.errors.NetworkError: In case of other
networking errors.
"""
response_ct = response.headers.get('Content-Type')
try:
# TODO: response.json() is called twice, once here, and
# once in _get and _post clients
jobj = response.json()
except ValueError as error:
jobj = None
if not response.ok:
if jobj is not None:
if response_ct != cls.JSON_ERROR_CONTENT_TYPE:
logging.debug(
'Ignoring wrong Content-Type (%r) for JSON Error',
response_ct)
try:
raise messages2.Error.from_json(jobj)
except jose.DeserializationError as error:
# Couldn't deserialize JSON object
raise errors.NetworkError((response, error))
else:
# response is not JSON object
raise errors.NetworkError(response)
else:
if jobj is not None and response_ct != cls.JSON_CONTENT_TYPE:
logging.debug(
'Ignoring wrong Content-Type (%r) for JSON decodable '
'response', response_ct)
if content_type == cls.JSON_CONTENT_TYPE and jobj is None:
raise errors.NetworkError(
'Unexpected response Content-Type: {0}'.format(response_ct))
def _get(self, uri, content_type=JSON_CONTENT_TYPE, **kwargs):
"""Send GET request.
:raises letsencrypt.client.errors.NetworkError:
:returns: HTTP Response
:rtype: `requests.Response`
"""
try:
response = requests.get(uri, **kwargs)
except requests.exceptions.RequestException as error:
raise errors.NetworkError(error)
self._check_response(response, content_type=content_type)
return response
def _post(self, uri, data, content_type=JSON_CONTENT_TYPE, **kwargs):
"""Send POST data.
:param str content_type: Expected ``Content-Type``, fails if not set.
:raises letsencrypt.acme.messages2.NetworkError:
:returns: HTTP Response
:rtype: `requests.Response`
"""
logging.debug('Sending POST data: %s', data)
try:
response = requests.post(uri, data=data, **kwargs)
except requests.exceptions.RequestException as error:
raise errors.NetworkError(error)
logging.debug('Received response %s: %s', response, response.text)
self._check_response(response, content_type=content_type)
return response
@classmethod
def _regr_from_response(cls, response, uri=None, new_authzr_uri=None,
terms_of_service=None):
terms_of_service = (
response.links['terms-of-service']['url']
if 'terms-of-service' in response.links else terms_of_service)
if new_authzr_uri is None:
try:
new_authzr_uri = response.links['next']['url']
except KeyError:
raise errors.NetworkError('"next" link missing')
return messages2.RegistrationResource(
body=messages2.Registration.from_json(response.json()),
uri=response.headers.get('Location', uri),
new_authzr_uri=new_authzr_uri,
terms_of_service=terms_of_service)
def register(self, contact=messages2.Registration._fields[
'contact'].default):
"""Register.
:param contact: Contact list, as accpeted by `.RegistrationResource`
:type contact: `tuple`
:returns: Registration Resource.
:rtype: `.RegistrationResource`
:raises letsencrypt.client.errors.UnexpectedUpdate:
"""
new_reg = messages2.Registration(contact=contact)
response = self._post(self.new_reg_uri, self._wrap_in_jws(new_reg))
assert response.status_code == httplib.CREATED # TODO: handle errors
regr = self._regr_from_response(response)
if regr.body.key != self.key.public() or regr.body.contact != contact:
raise errors.UnexpectedUpdate(regr)
return regr
def update_registration(self, regr):
"""Update registration.
:pram regr: Registration Resource.
:type regr: `.RegistrationResource`
:returns: Updated Registration Resource.
:rtype: `.RegistrationResource`
"""
response = self._post(regr.uri, self._wrap_in_jws(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 _authzr_from_response(self, response, identifier,
uri=None, new_cert_uri=None):
if new_cert_uri is None:
try:
new_cert_uri = response.links['next']['url']
except KeyError:
raise errors.NetworkError('"next" link missing')
authzr = messages2.AuthorizationResource(
body=messages2.Authorization.from_json(response.json()),
uri=response.headers.get('Location', uri),
new_cert_uri=new_cert_uri)
if (authzr.body.key != self.key.public()
or authzr.body.identifier != identifier):
raise errors.UnexpectedUpdate(authzr)
return authzr
def request_challenges(self, identifier, regr):
"""Request challenges.
:param identifier: Identifier to be challenged.
:type identifier: `.messages2.Identifier`
:param regr: Registration Resource.
:type regr: `.RegistrationResource`
:returns: Authorization Resource.
:rtype: `.AuthorizationResource`
"""
new_authz = messages2.Authorization(identifier=identifier)
response = self._post(regr.new_authzr_uri, self._wrap_in_jws(new_authz))
assert response.status_code == httplib.CREATED # TODO: handle errors
return self._authzr_from_response(response, identifier)
def request_domain_challenges(self, domain, regr):
"""Request challenges for domain names.
This is simply a convenience function that wraps around
`request_challenges`, but works with domain names instead of
generic identifiers.
:param str domain: Domain name to be challenged.
"""
return self.request_challenges(messages2.Identifier(
typ=messages2.IDENTIFIER_FQDN, value=domain), regr)
def answer_challenge(self, challb, response):
"""Answer challenge.
:param challb: Challenge Resource body.
:type challb: `.ChallengeBody`
:param response: Corresponding Challenge response
:type response: `.challenges.ChallengeResponse`
:returns: Challenge Resource with updated body.
:rtype: `.ChallengeResource`
:raises errors.UnexpectedUpdate:
"""
response = self._post(challb.uri, self._wrap_in_jws(response))
try:
authzr_uri = response.links['up']['url']
except KeyError:
raise errors.NetworkError('"up" Link header missing')
challr = messages2.ChallengeResource(
authzr_uri=authzr_uri,
body=messages2.ChallengeBody.from_json(response.json()))
# TODO: check that challr.uri == response.headers['Location']?
if challr.uri != challb.uri:
raise errors.UnexpectedUpdate(challr.uri)
return challr
def answer_challenges(self, challbs, responses):
"""Answer multiple challenges.
.. note:: This is a convenience function to make integration
with old proto code easier and shall probably be removed
once restification is over.
"""
return [self.answer_challenge(challb, response)
for challb, response in itertools.izip(challbs, responses)]
@classmethod
def retry_after(cls, response, default):
"""Compute next `poll` time based on response ``Retry-After`` header.
:param response: Response from `poll`.
:type response: `requests.Response`
:param int default: Default value (in seconds), used when
``Retry-After`` header is not present or invalid.
:returns: Time point when next `poll` should be performed.
:rtype: `datetime.datetime`
"""
retry_after = response.headers.get('Retry-After', str(default))
try:
seconds = int(retry_after)
except ValueError:
# pylint: disable=no-member
decoded = werkzeug.parse_date(retry_after) # RFC1123
if decoded is None:
seconds = default
else:
return decoded
return datetime.datetime.now() + datetime.timedelta(seconds=seconds)
def poll(self, authzr):
"""Poll Authorization Resource for status.
:param authzr: Authorization Resource
:type authzr: `.AuthorizationResource`
:returns: Updated Authorization Resource and HTTP response.
:rtype: (`.AuthorizationResource`, `requests.Response`)
"""
response = self._get(authzr.uri)
updated_authzr = self._authzr_from_response(
response, authzr.body.identifier, authzr.uri, authzr.new_cert_uri)
# TODO: check and raise UnexpectedUpdate
return updated_authzr, response
def request_issuance(self, csr, authzrs):
"""Request issuance.
:param csr: CSR
:type csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509`
:param authzrs: `list` of `.AuthorizationResource`
:returns: Issued certificate
:rtype: `.messages2.CertificateResource`
"""
assert authzrs, "Authorizations list is empty"
# TODO: assert len(authzrs) == number of SANs
req = messages2.CertificateRequest(
csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs))
content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument
response = self._post(
authzrs[0].new_cert_uri, # TODO: acme-spec #90
self._wrap_in_jws(req),
content_type=content_type,
headers={'Accept': content_type})
cert_chain_uri = response.links.get('up', {}).get('url')
try:
uri = response.headers['Location']
except KeyError:
raise errors.NetworkError('"Location" Header missing')
return messages2.CertificateResource(
uri=uri, authzrs=authzrs, cert_chain_uri=cert_chain_uri,
body=jose.ComparableX509(
M2Crypto.X509.load_cert_der_string(response.content)))
def poll_and_request_issuance(self, csr, authzrs, mintime=5):
"""Poll and request issuance.
This function polls all provided Authorization Resource URIs
until all challenges are valid, respecting ``Retry-After`` HTTP
headers, and then calls `request_issuance`.
.. todo:: add `max_attempts` or `timeout`
:param csr: CSR.
:type csr: `M2Crypto.X509.Request` wrapped in `.ComparableX509`
:param authzrs: `list` of `.AuthorizationResource`
:param int mintime: Minimum time before next attempt, used if
``Retry-After`` is not present in the response.
:returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is
the issued certificate (`.messages2.CertificateResource.),
and ``updated_authzrs`` is a `tuple` consisting of updated
Authorization Resources (`.AuthorizationResource`) as
present in the responses from server, and in the same order
as the input ``authzrs``.
:rtype: `tuple`
"""
# priority queue with datetime (based od Retry-After) as key,
# and original Authorization Resource as value
waiting = [(datetime.datetime.now(), authzr) for authzr in authzrs]
# mapping between original Authorization Resource and the most
# recently updated one
updated = dict((authzr, authzr) for authzr in authzrs)
while waiting:
# find the smallest Retry-After, and sleep if necessary
when, authzr = heapq.heappop(waiting)
now = datetime.datetime.now()
if when > now:
seconds = (when - now).seconds
logging.debug('Sleeping for %d seconds', seconds)
time.sleep(seconds)
# Note that we poll with the latest updated Authorization
# URI, which might have a different URI than initial one
updated_authzr, response = self.poll(updated[authzr])
updated[authzr] = updated_authzr
if updated_authzr.body.status != messages2.STATUS_VALID:
# push back to the priority queue, with updated retry_after
heapq.heappush(waiting, (self.retry_after(
response, default=mintime), authzr))
updated_authzrs = tuple(updated[authzr] for authzr in authzrs)
return self.request_issuance(csr, updated_authzrs), updated_authzrs
def _get_cert(self, uri):
content_type = self.DER_CONTENT_TYPE # TODO: make it a param
response = self._get(uri, headers={'Accept': content_type},
content_type=content_type)
return response, jose.ComparableX509(
M2Crypto.X509.load_cert_der_string(response.content))
def check_cert(self, certr):
"""Check for new cert.
:param certr: Certificate Resource
:type certr: `.CertificateResource`
:returns: Updated Certificate Resource.
:rtype: `.CertificateResource`
"""
# TODO: acme-spec 5.1 table action should be renamed to
# "refresh cert", and this method integrated with self.refresh
response, cert = self._get_cert(certr.uri)
if 'Location' not in response.headers:
raise errors.NetworkError('Location header missing')
if response.headers['Location'] != certr.uri:
raise errors.UnexpectedUpdate(response.text)
return certr.update(body=cert)
def refresh(self, certr):
"""Refresh certificate.
:param certr: Certificate Resource
:type certr: `.CertificateResource`
:returns: Updated Certificate Resource.
:rtype: `.CertificateResource`
"""
# TODO: If a client sends a refresh request and the server is
# not willing to refresh the certificate, the server MUST
# respond with status code 403 (Forbidden)
return self.check_cert(certr)
def fetch_chain(self, certr):
"""Fetch chain for certificate.
:param certr: Certificate Resource
:type certr: `.CertificateResource`
:returns: Certificate chain, or `None` if no "up" Link was provided.
:rtype: `M2Crypto.X509.X509` wrapped in `.ComparableX509`
"""
if certr.cert_chain_uri is not None:
return self._get_cert(certr.cert_chain_uri)
def revoke(self, certr, when=messages2.Revocation.NOW):
"""Revoke certificate.
:param when: When should the revocation take place? Takes
the same values as `.messages2.Revocation.revoke`.
"""
rev = messages2.Revocation(revoke=when, authorizations=tuple(
authzr.uri for authzr in certr.authzrs))
response = self._post(certr.uri, self._wrap_in_jws(rev))
if response.status_code != httplib.OK:
raise errors.NetworkError(
'Successful revocation must return HTTP OK status')

View File

@@ -0,0 +1 @@
"""Let's Encrypt client.plugins."""

View File

@@ -0,0 +1 @@
"""Let's Encrypt client.plugins.apache."""

View File

@@ -18,9 +18,9 @@ from letsencrypt.client import errors
from letsencrypt.client import interfaces
from letsencrypt.client import le_util
from letsencrypt.client.apache import dvsni
from letsencrypt.client.apache import obj
from letsencrypt.client.apache import parser
from letsencrypt.client.plugins.apache import dvsni
from letsencrypt.client.plugins.apache import obj
from letsencrypt.client.plugins.apache import parser
# TODO: Augeas sections ie. <VirtualHost>, <IfModule> beginning and closing
@@ -68,11 +68,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:type config: :class:`~letsencrypt.client.interfaces.IConfig`
:ivar parser: Handles low level parsing
:type parser: :class:`letsencrypt.client.apache.parser`
:type parser: :class:`~letsencrypt.client.plugins.apache.parser`
:ivar tup version: version of Apache
:ivar list vhosts: All vhosts found in the configuration
(:class:`list` of :class:`letsencrypt.client.apache.obj.VirtualHost`)
(:class:`list` of
:class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`)
:ivar dict assoc: Mapping between domains and vhosts
@@ -164,7 +165,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
parser.case_i("SSLCertificateChainFile"), None, vhost.path)
if len(path["cert_file"]) == 0 or len(path["cert_key"]) == 0:
# Throw some "can't find all of the directives error"
# Throw some can't find all of the directives error"
logging.warn(
"Cannot find a cert or key directive in %s", vhost.path)
logging.warn("VirtualHost was not modified")
@@ -203,7 +204,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:param str target_name: domain name
:returns: ssl vhost associated with name
:rtype: :class:`letsencrypt.client.apache.obj.VirtualHost`
:rtype: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
"""
# Allows for domain names to be associated with a virtual host
@@ -223,7 +224,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.assoc[target_name] = vhost
return vhost
# Check for non ssl vhosts with servernames/aliases == 'name'
# Check for non ssl vhosts with servernames/aliases == "name"
for vhost in self.vhosts:
if not vhost.ssl and target_name in vhost.names:
vhost = self.make_vhost_ssl(vhost)
@@ -244,7 +245,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:param str domain: domain name to associate
:param vhost: virtual host to associate with domain
:type vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
:type vhost: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
"""
self.assoc[domain] = vhost
@@ -281,15 +282,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""Helper function for get_virtual_hosts().
:param host: In progress vhost whose names will be added
:type host: :class:`letsencrypt.client.apache.obj.VirtualHost`
:type host: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
"""
name_match = self.aug.match(("%s//*[self::directive=~regexp('%s')] | "
"%s//*[self::directive=~regexp('%s')]" %
(host.path,
parser.case_i('ServerName'),
parser.case_i("ServerName"),
host.path,
parser.case_i('ServerAlias'))))
parser.case_i("ServerAlias"))))
for name in name_match:
args = self.aug.match(name + "/*")
@@ -302,7 +303,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:param str path: Augeas path to virtual host
:returns: newly created vhost
:rtype: :class:`letsencrypt.client.apache.obj.VirtualHost`
:rtype: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
"""
addrs = set()
@@ -326,7 +327,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""Returns list of virtual hosts found in the Apache configuration.
:returns: List of
:class:`letsencrypt.client.apache.obj.VirtualHost` objects
:class:`~letsencrypt.client.plugins.apache.obj.VirtualHost` objects
found in configuration
:rtype: list
@@ -334,7 +335,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Search sites-available, httpd.conf for possible virtual hosts
paths = self.aug.match(
("/files%s/sites-available//*[label()=~regexp('%s')]" %
(self.parser.root, parser.case_i('VirtualHost'))))
(self.parser.root, parser.case_i("VirtualHost"))))
vhs = []
for path in paths:
@@ -404,7 +405,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""Checks to see if the server is ready for SNI challenges.
:param vhost: VirtualHost to check SNI compatibility
:type vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
:type vhost: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
:param str default_addr: TODO - investigate function further
@@ -436,10 +437,11 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
.. note:: This function saves the configuration
:param nonssl_vhost: Valid VH that doesn't have SSLEngine on
:type nonssl_vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
:type nonssl_vhost:
:class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
:returns: SSL vhost
:rtype: :class:`letsencrypt.client.apache.obj.VirtualHost`
:rtype: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
"""
avail_fp = nonssl_vhost.filep
@@ -454,8 +456,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.reverter.register_file_creation(False, ssl_fp)
try:
with open(avail_fp, 'r') as orig_file:
with open(ssl_fp, 'w') as new_file:
with open(avail_fp, "r") as orig_file:
with open(ssl_fp, "w") as new_file:
new_file.write("<IfModule mod_ssl.c>\n")
for line in orig_file:
new_file.write(line)
@@ -471,7 +473,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# change address to address:443
addr_match = "/files%s//* [label()=~regexp('%s')]/arg"
ssl_addr_p = self.aug.match(
addr_match % (ssl_fp, parser.case_i('VirtualHost')))
addr_match % (ssl_fp, parser.case_i("VirtualHost")))
for addr in ssl_addr_p:
old_addr = obj.Addr.fromstring(
@@ -482,7 +484,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Add directives
vh_p = self.aug.match("/files%s//* [label()=~regexp('%s')]" %
(ssl_fp, parser.case_i('VirtualHost')))
(ssl_fp, parser.case_i("VirtualHost")))
if len(vh_p) != 1:
logging.error("Error: should only be one vhost in %s", avail_fp)
sys.exit(1)
@@ -495,7 +497,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Log actions and create save notes
logging.info("Created an SSL vhost at %s", ssl_fp)
self.save_notes += 'Created ssl vhost at %s\n' % ssl_fp
self.save_notes += "Created ssl vhost at %s\n" % ssl_fp
self.save()
# We know the length is one because of the assertion above
@@ -548,8 +550,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
def _enable_redirect(self, ssl_vhost, unused_options):
"""Redirect all equivalent HTTP traffic to ssl_vhost.
.. todo:: This enhancement should be rewritten and will unfortunately
require lots of debugging by hand.
.. todo:: This enhancement should be rewritten and will
unfortunately require lots of debugging by hand.
Adds Redirect directive to the port 80 equivalent of ssl_vhost
First the function attempts to find the vhost with equivalent
ip addresses that serves on non-ssl ports
@@ -558,13 +561,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
.. note:: This function saves the configuration
:param ssl_vhost: Destination of traffic, an ssl enabled vhost
:type ssl_vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
:type ssl_vhost:
:class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
:param unused_options: Not currently used
:type unused_options: Not Available
:returns: Success, general_vhost (HTTP vhost)
:rtype: (bool, :class:`letsencrypt.client.apache.obj.VirtualHost`)
:rtype: (bool,
:class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`)
"""
if not mod_loaded("rewrite_module", self.config.apache_ctl):
@@ -595,7 +600,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.parser.add_dir(general_v.path, "RewriteEngine", "On")
self.parser.add_dir(general_v.path, "RewriteRule",
constants.APACHE_REWRITE_HTTPS_ARGS)
self.save_notes += ('Redirecting host in %s to ssl vhost in %s\n' %
self.save_notes += ("Redirecting host in %s to ssl vhost in %s\n" %
(general_v.filep, ssl_vhost.filep))
self.save()
@@ -616,7 +621,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
-1 is also returned in case of no redirection/rewrite directives
:param vhost: vhost to check
:type vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
:type vhost: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
:returns: Success, code value... see documentation
:rtype: bool, int
@@ -648,10 +653,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""Creates an http_vhost specifically to redirect for the ssl_vhost.
:param ssl_vhost: ssl vhost
:type ssl_vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
:type ssl_vhost:
:class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
:returns: Success, vhost
:rtype: (bool, :class:`letsencrypt.client.apache.obj.VirtualHost`)
:returns: tuple of the form
(`success`,
:class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`)
:rtype: tuple
"""
# Consider changing this to a dictionary check
@@ -699,7 +707,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
redirect_filename = "le-redirect-%s.conf" % ssl_vhost.names[0]
redirect_filepath = os.path.join(
self.parser.root, 'sites-available', redirect_filename)
self.parser.root, "sites-available", redirect_filename)
# Register the new file that will be created
# Note: always register the creation before writing to ensure file will
@@ -707,7 +715,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.reverter.register_file_creation(False, redirect_filepath)
# Write out file
with open(redirect_filepath, 'w') as redirect_fd:
with open(redirect_filepath, "w") as redirect_fd:
redirect_fd.write(redirect_file)
logging.info("Created redirect file: %s", redirect_filename)
@@ -717,8 +725,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.vhosts.append(new_vhost)
# Finally create documentation for the change
self.save_notes += ('Created a port 80 vhost, %s, for redirection to '
'ssl vhost %s\n' %
self.save_notes += ("Created a port 80 vhost, %s, for redirection to "
"ssl vhost %s\n" %
(new_vhost.filep, ssl_vhost.filep))
def _conflicting_host(self, ssl_vhost):
@@ -733,7 +741,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if not conflict: returns space separated list of new host addrs
:param ssl_vhost: SSL Vhost to check for possible port 80 redirection
:type ssl_vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
:type ssl_vhost:
:class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
:returns: TODO
:rtype: TODO
@@ -766,10 +775,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
Consider changing this into a dict check
:param ssl_vhost: ssl vhost to check
:type ssl_vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
:type ssl_vhost:
:class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
:returns: HTTP vhost or None if unsuccessful
:rtype: :class:`letsencrypt.client.apache.obj.VirtualHost` or None
:rtype: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
or None
"""
# _default_:443 check
@@ -859,7 +870,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
.. todo:: Make sure link is not broken...
:param vhost: vhost to enable
:type vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
:type vhost: :class:`~letsencrypt.client.plugins.apache.obj.VirtualHost`
:returns: Success
:rtype: bool
@@ -875,7 +886,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
os.symlink(vhost.filep, enabled_path)
vhost.enabled = True
logging.info("Enabling available site: %s", vhost.filep)
self.save_notes += 'Enabled site %s\n' % vhost.filep
self.save_notes += "Enabled site %s\n" % vhost.filep
return True
return False
@@ -897,7 +908,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
try:
proc = subprocess.Popen(
['sudo', self.config.apache_ctl, 'configtest'], # TODO: sudo?
["sudo", self.config.apache_ctl, "configtest"], # TODO: sudo?
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
@@ -941,7 +952,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
try:
proc = subprocess.Popen(
[self.config.apache_ctl, '-v'],
[self.config.apache_ctl, "-v"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
text = proc.communicate()[0]
@@ -956,7 +967,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
raise errors.LetsEncryptConfiguratorError(
"Unable to find Apache version")
return tuple([int(i) for i in matches[0].split('.')])
return tuple([int(i) for i in matches[0].split(".")])
def more_info(self):
"""Human-readable string to help understand the module"""
@@ -1031,8 +1042,8 @@ def enable_mod(mod_name, apache_init_script, apache_enmod):
# Use check_output so the command will finish before reloading
# TODO: a2enmod is debian specific...
subprocess.check_call(["sudo", apache_enmod, mod_name], # TODO: sudo?
stdout=open("/dev/null", 'w'),
stderr=open("/dev/null", 'w'))
stdout=open("/dev/null", "w"),
stderr=open("/dev/null", "w"))
apache_restart(apache_init_script)
except (OSError, subprocess.CalledProcessError) as err:
logging.error("Error enabling mod_%s", mod_name)
@@ -1054,7 +1065,7 @@ def mod_loaded(module, apache_ctl):
"""
try:
proc = subprocess.Popen(
[apache_ctl, '-M'],
[apache_ctl, "-M"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
@@ -1092,7 +1103,7 @@ def apache_restart(apache_init_script):
"""
try:
proc = subprocess.Popen([apache_init_script, 'restart'],
proc = subprocess.Popen([apache_init_script, "restart"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()

View File

@@ -2,20 +2,19 @@
import logging
import os
from letsencrypt.client.apache import parser
from letsencrypt.client.plugins.apache import parser
class ApacheDvsni(object):
"""Class performs DVSNI challenges within the Apache configurator.
:ivar configurator: ApacheConfigurator object
:type configurator:
:class:`letsencrypt.client.apache.configurator.ApacheConfigurator`
:type configurator: :class:`~apache.configurator.ApacheConfigurator`
:ivar list achalls: Annotated :class:`~letsencrypt.client.achallenges.DVSNI`
challenges.
:param list indicies: Meant to hold indices of challenges in a
:param list indices: Meant to hold indices of challenges in a
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
@@ -26,6 +25,23 @@ class ApacheDvsni(object):
:param str challenge_conf: location of the challenge config file
"""
VHOST_TEMPLATE = """\
<VirtualHost {vhost}>
ServerName {server_name}
UseCanonicalName on
SSLStrictSNIVHostCheck on
LimitRequestBody 1048576
Include {ssl_options_conf_path}
SSLCertificateFile {cert_path}
SSLCertificateKeyFile {key_path}
DocumentRoot {document_root}
</VirtualHost>
"""
def __init__(self, configurator):
self.configurator = configurator
self.achalls = []
@@ -50,7 +66,7 @@ class ApacheDvsni(object):
def perform(self):
"""Peform a DVSNI challenge."""
if not self.achalls:
return None
return []
# Save any changes to the configuration as a precaution
# About to make temporary changes to the config
self.configurator.save()
@@ -101,7 +117,7 @@ class ApacheDvsni(object):
cert_pem, response = achall.gen_cert_and_response(s)
# Write out challenge cert
with open(cert_path, 'w') as cert_chall_fd:
with open(cert_path, "w") as cert_chall_fd:
cert_chall_fd.write(cert_pem)
return response
@@ -112,7 +128,7 @@ class ApacheDvsni(object):
Result: Apache config includes virtual servers for issued challs
:param list ll_addrs: list of list of
:class:`letsencrypt.client.apache.obj.Addr` to apply
:class:`letsencrypt.client.plugins.apache.obj.Addr` to apply
"""
# TODO: Use ip address of existing vhost instead of relying on FQDN
@@ -125,7 +141,7 @@ class ApacheDvsni(object):
self.configurator.reverter.register_file_creation(
True, self.challenge_conf)
with open(self.challenge_conf, 'w') as new_conf:
with open(self.challenge_conf, "w") as new_conf:
new_conf.write(config_text)
def _conf_include_check(self, main_config):
@@ -151,7 +167,7 @@ class ApacheDvsni(object):
:type achall: :class:`letsencrypt.client.achallenges.DVSNI`
:param list ip_addrs: addresses of challenged domain
:class:`list` of type :class:`letsencrypt.client.apache.obj.Addr`
:class:`list` of type :class:`~apache.obj.Addr`
:returns: virtual host configuration text
:rtype: str
@@ -160,19 +176,16 @@ class ApacheDvsni(object):
ips = " ".join(str(i) for i in ip_addrs)
document_root = os.path.join(
self.configurator.config.config_dir, "dvsni_page/")
return ("<VirtualHost " + ips + ">\n"
"ServerName " + achall.nonce_domain + "\n"
"UseCanonicalName on\n"
"SSLStrictSNIVHostCheck on\n"
"\n"
"LimitRequestBody 1048576\n"
"\n"
"Include " + self.configurator.parser.loc["ssl_options"] + "\n"
"SSLCertificateFile " + self.get_cert_file(achall) + "\n"
"SSLCertificateKeyFile " + achall.key.file + "\n"
"\n"
"DocumentRoot " + document_root + "\n"
"</VirtualHost>\n\n")
# TODO: Python docs is not clear how mutliline string literal
# newlines are parsed on different platforms. At least on
# Linux (Debian sid), when source file uses CRLF, Python still
# parses it as "\n"... c.f.:
# https://docs.python.org/2.7/reference/lexical_analysis.html
return self.VHOST_TEMPLATE.format(
vhost=ips, server_name=achall.nonce_domain,
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.

View File

@@ -1,4 +1,4 @@
"""Test for letsencrypt.client.apache.configurator."""
"""Test for letsencrypt.client.plugins.apache.configurator."""
import os
import re
import shutil
@@ -12,11 +12,11 @@ from letsencrypt.client import achallenges
from letsencrypt.client import errors
from letsencrypt.client import le_util
from letsencrypt.client.apache import configurator
from letsencrypt.client.apache import obj
from letsencrypt.client.apache import parser
from letsencrypt.client.plugins.apache import configurator
from letsencrypt.client.plugins.apache import obj
from letsencrypt.client.plugins.apache import parser
from letsencrypt.client.tests.apache import util
from letsencrypt.client.plugins.apache.tests import util
class TwoVhost80Test(util.ApacheTest):
@@ -25,7 +25,7 @@ class TwoVhost80Test(util.ApacheTest):
def setUp(self):
super(TwoVhost80Test, self).setUp()
with mock.patch("letsencrypt.client.apache.configurator."
with mock.patch("letsencrypt.client.plugins.apache.configurator."
"mod_loaded") as mock_load:
mock_load.return_value = True
self.config = util.get_apache_configurator(
@@ -43,9 +43,15 @@ class TwoVhost80Test(util.ApacheTest):
def test_get_all_names(self):
names = self.config.get_all_names()
self.assertEqual(names, set(
['letsencrypt.demo', 'encryption-example.demo', 'ip-172-30-0-17']))
["letsencrypt.demo", "encryption-example.demo", "ip-172-30-0-17"]))
def test_get_virtual_hosts(self):
"""Make sure all vhosts are being properly found.
.. note:: If test fails, only finding 1 Vhost... it is likely that
it is a problem with is_enabled.
"""
vhs = self.config.get_virtual_hosts()
self.assertEqual(len(vhs), 4)
found = 0
@@ -59,6 +65,14 @@ class TwoVhost80Test(util.ApacheTest):
self.assertEqual(found, 4)
def test_is_site_enabled(self):
"""Test if site is enabled.
.. note:: This test currently fails for hard links
(which may happen if you move dirs incorrectly)
.. warning:: This test does not work when running using the
unittest.main() function. It incorrectly copies symlinks.
"""
self.assertTrue(self.config.is_site_enabled(self.vh_truth[0].filep))
self.assertFalse(self.config.is_site_enabled(self.vh_truth[1].filep))
self.assertTrue(self.config.is_site_enabled(self.vh_truth[2].filep))
@@ -134,9 +148,9 @@ class TwoVhost80Test(util.ApacheTest):
self.assertEqual(len(self.config.vhosts), 5)
@mock.patch("letsencrypt.client.apache.configurator."
@mock.patch("letsencrypt.client.plugins.apache.configurator."
"dvsni.ApacheDvsni.perform")
@mock.patch("letsencrypt.client.apache.configurator."
@mock.patch("letsencrypt.client.plugins.apache.configurator."
"ApacheConfigurator.restart")
def test_perform(self, mock_restart, mock_dvsni_perform):
# Only tests functionality specific to configurator.perform
@@ -166,7 +180,7 @@ class TwoVhost80Test(util.ApacheTest):
self.assertEqual(mock_restart.call_count, 1)
@mock.patch("letsencrypt.client.apache.configurator."
@mock.patch("letsencrypt.client.plugins.apache.configurator."
"subprocess.Popen")
def test_get_version(self, mock_popen):
mock_popen().communicate.return_value = (
@@ -183,7 +197,7 @@ class TwoVhost80Test(util.ApacheTest):
errors.LetsEncryptConfiguratorError, self.config.get_version)
mock_popen().communicate.return_value = (
"Server Version: Apache/2.3\n Apache/2.4.7", "")
"Server Version: Apache/2.3{0} Apache/2.4.7".format(os.linesep), "")
self.assertRaises(
errors.LetsEncryptConfiguratorError, self.config.get_version)
@@ -192,5 +206,5 @@ class TwoVhost80Test(util.ApacheTest):
errors.LetsEncryptConfiguratorError, self.config.get_version)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()

View File

@@ -1,4 +1,4 @@
"""Test for letsencrypt.client.apache.dvsni."""
"""Test for letsencrypt.client.plugins.apache.dvsni."""
import pkg_resources
import unittest
import shutil
@@ -10,9 +10,9 @@ from letsencrypt.acme import challenges
from letsencrypt.client import achallenges
from letsencrypt.client import le_util
from letsencrypt.client.apache.obj import Addr
from letsencrypt.client.plugins.apache.obj import Addr
from letsencrypt.client.tests.apache import util
from letsencrypt.client.plugins.apache.tests import util
class DvsniPerformTest(util.ApacheTest):
@@ -21,20 +21,20 @@ class DvsniPerformTest(util.ApacheTest):
def setUp(self):
super(DvsniPerformTest, self).setUp()
with mock.patch("letsencrypt.client.apache.configurator."
with mock.patch("letsencrypt.client.plugins.apache.configurator."
"mod_loaded") as mock_load:
mock_load.return_value = True
config = util.get_apache_configurator(
self.config_path, self.config_dir, self.work_dir,
self.ssl_options)
from letsencrypt.client.apache import dvsni
from letsencrypt.client.plugins.apache import dvsni
self.sni = dvsni.ApacheDvsni(config)
rsa256_file = pkg_resources.resource_filename(
"letsencrypt.client.tests", 'testdata/rsa256_key.pem')
"letsencrypt.client.tests", "testdata/rsa256_key.pem")
rsa256_pem = pkg_resources.resource_string(
"letsencrypt.client.tests", 'testdata/rsa256_key.pem')
"letsencrypt.client.tests", "testdata/rsa256_key.pem")
auth_key = le_util.Key(rsa256_file, rsa256_pem)
self.achalls = [
@@ -60,7 +60,7 @@ class DvsniPerformTest(util.ApacheTest):
def test_perform0(self):
resp = self.sni.perform()
self.assertTrue(resp is None)
self.assertEqual(len(resp), 0)
def test_setup_challenge_cert(self):
# This is a helper function that can be used for handling
@@ -74,7 +74,7 @@ class DvsniPerformTest(util.ApacheTest):
nonce_domain=self.achalls[0].nonce_domain)
achall.gen_cert_and_response.return_value = ("pem", response)
with mock.patch("letsencrypt.client.apache.dvsni.open",
with mock.patch("letsencrypt.client.plugins.apache.dvsni.open",
m_open, create=True):
# pylint: disable=protected-access
self.assertEqual(response, self.sni._setup_challenge_cert(
@@ -82,7 +82,7 @@ class DvsniPerformTest(util.ApacheTest):
self.assertTrue(m_open.called)
self.assertEqual(
m_open.call_args[0], (self.sni.get_cert_file(achall), 'w'))
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):
@@ -166,5 +166,5 @@ class DvsniPerformTest(util.ApacheTest):
set([self.achalls[1].nonce_domain]))
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()

View File

@@ -1,11 +1,11 @@
"""Test the helper objects in apache.obj.py."""
"""Test the helper objects in letsencrypt.client.plugins.apache.obj."""
import unittest
class AddrTest(unittest.TestCase):
"""Test the Addr class."""
def setUp(self):
from letsencrypt.client.apache.obj import Addr
from letsencrypt.client.plugins.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")
@@ -34,7 +34,7 @@ class AddrTest(unittest.TestCase):
self.assertFalse(self.addr1 == 3333)
def test_set_inclusion(self):
from letsencrypt.client.apache.obj import Addr
from letsencrypt.client.plugins.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:*")
@@ -46,15 +46,15 @@ class AddrTest(unittest.TestCase):
class VirtualHostTest(unittest.TestCase):
"""Test the VirtualHost class."""
def setUp(self):
from letsencrypt.client.apache.obj import VirtualHost
from letsencrypt.client.apache.obj import Addr
from letsencrypt.client.plugins.apache.obj import VirtualHost
from letsencrypt.client.plugins.apache.obj import Addr
self.vhost1 = VirtualHost(
"filep", "vh_path",
set([Addr.fromstring("localhost")]), False, False)
def test_eq(self):
from letsencrypt.client.apache.obj import Addr
from letsencrypt.client.apache.obj import VirtualHost
from letsencrypt.client.plugins.apache.obj import Addr
from letsencrypt.client.plugins.apache.obj import VirtualHost
vhost1b = VirtualHost(
"filep", "vh_path",
set([Addr.fromstring("localhost")]), False, False)

View File

@@ -1,4 +1,4 @@
"""Tests the ApacheParser class."""
"""Tests for letsencrypt.client.plugins.apache.parser."""
import os
import shutil
import sys
@@ -11,7 +11,7 @@ import zope.component
from letsencrypt.client import errors
from letsencrypt.client.display import util as display_util
from letsencrypt.client.tests.apache import util
from letsencrypt.client.plugins.apache.tests import util
class ApacheParserTest(util.ApacheTest):
@@ -22,7 +22,7 @@ class ApacheParserTest(util.ApacheTest):
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
from letsencrypt.client.apache.parser import ApacheParser
from letsencrypt.client.plugins.apache.parser import ApacheParser
self.aug = augeas.Augeas(flags=augeas.Augeas.NONE)
self.parser = ApacheParser(self.aug, self.config_path, self.ssl_options)
@@ -32,19 +32,19 @@ class ApacheParserTest(util.ApacheTest):
shutil.rmtree(self.work_dir)
def test_root_normalized(self):
from letsencrypt.client.apache.parser import ApacheParser
from letsencrypt.client.plugins.apache.parser import ApacheParser
path = os.path.join(self.temp_dir, "debian_apache_2_4/////"
"two_vhost_80/../two_vhost_80/apache2")
parser = ApacheParser(self.aug, path, None)
self.assertEqual(parser.root, self.config_path)
def test_root_absolute(self):
from letsencrypt.client.apache.parser import ApacheParser
from letsencrypt.client.plugins.apache.parser import ApacheParser
parser = ApacheParser(self.aug, os.path.relpath(self.config_path), None)
self.assertEqual(parser.root, self.config_path)
def test_root_no_trailing_slash(self):
from letsencrypt.client.apache.parser import ApacheParser
from letsencrypt.client.plugins.apache.parser import ApacheParser
parser = ApacheParser(self.aug, self.config_path + os.path.sep, None)
self.assertEqual(parser.root, self.config_path)
@@ -67,7 +67,7 @@ class ApacheParserTest(util.ApacheTest):
self.assertTrue(matches)
def test_find_dir(self):
from letsencrypt.client.apache.parser import case_i
from letsencrypt.client.plugins.apache.parser import case_i
test = self.parser.find_dir(case_i("Listen"), "443")
# This will only look in enabled hosts
test2 = self.parser.find_dir(case_i("documentroot"))
@@ -92,7 +92,7 @@ class ApacheParserTest(util.ApacheTest):
Path must be valid before attempting to add to augeas
"""
from letsencrypt.client.apache.parser import get_aug_path
from letsencrypt.client.plugins.apache.parser import get_aug_path
self.parser.add_dir_to_ifmodssl(
get_aug_path(self.parser.loc["default"]),
"FakeDirective", "123")
@@ -103,11 +103,11 @@ class ApacheParserTest(util.ApacheTest):
self.assertTrue("IfModule" in matches[0])
def test_get_aug_path(self):
from letsencrypt.client.apache.parser import get_aug_path
from letsencrypt.client.plugins.apache.parser import get_aug_path
self.assertEqual("/files/etc/apache", get_aug_path("/etc/apache"))
def test_set_locations(self):
with mock.patch("letsencrypt.client.apache.parser."
with mock.patch("letsencrypt.client.plugins.apache.parser."
"os.path") as mock_path:
mock_path.isfile.return_value = False
@@ -125,5 +125,5 @@ class ApacheParserTest(util.ApacheTest):
self.assertEqual(results["default"], results["name"])
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()

View File

@@ -1,4 +1,4 @@
"""Common utilities for letsencrypt.client.apache."""
"""Common utilities for letsencrypt.client.plugins.apache."""
import os
import pkg_resources
import shutil
@@ -8,8 +8,8 @@ import unittest
import mock
from letsencrypt.client import constants
from letsencrypt.client.apache import configurator
from letsencrypt.client.apache import obj
from letsencrypt.client.plugins.apache import configurator
from letsencrypt.client.plugins.apache import obj
class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods
@@ -26,9 +26,9 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods
self.temp_dir, "debian_apache_2_4/two_vhost_80/apache2")
self.rsa256_file = pkg_resources.resource_filename(
"letsencrypt.client.tests", 'testdata/rsa256_key.pem')
"letsencrypt.client.tests", "testdata/rsa256_key.pem")
self.rsa256_pem = pkg_resources.resource_string(
"letsencrypt.client.tests", 'testdata/rsa256_key.pem')
"letsencrypt.client.tests", "testdata/rsa256_key.pem")
def dir_setup(test_dir="debian_apache_2_4/two_vhost_80"):
@@ -38,7 +38,7 @@ def dir_setup(test_dir="debian_apache_2_4/two_vhost_80"):
work_dir = tempfile.mkdtemp("work")
test_configs = pkg_resources.resource_filename(
"letsencrypt.client.tests", "testdata/%s" % test_dir)
"letsencrypt.client.plugins.apache.tests", "testdata/%s" % test_dir)
shutil.copytree(
test_configs, os.path.join(temp_dir, test_dir), symlinks=True)
@@ -59,7 +59,7 @@ def get_apache_configurator(
backups = os.path.join(work_dir, "backups")
with mock.patch("letsencrypt.client.apache.configurator."
with mock.patch("letsencrypt.client.plugins.apache.configurator."
"subprocess.Popen") as mock_popen:
# This just states that the ssl module is already loaded
mock_popen().communicate.return_value = ("ssl_module", "")

View File

@@ -0,0 +1 @@
"""Let's Encrypt client.plugins.standalone."""

View File

@@ -33,7 +33,7 @@ class StandaloneAuthenticator(object):
description = "Standalone Authenticator"
def __init__(self):
def __init__(self, unused_config):
self.child_pid = None
self.parent_pid = os.getpid()
self.subproc_state = None
@@ -410,5 +410,5 @@ class StandaloneAuthenticator(object):
"on port 443 and perform DVSNI challenges. Once a certificate"
"is attained, it will be saved in the "
"(TODO) current working directory.{0}{0}"
"Port 443 must be open in order to use the "
"TCP port 443 must be available in order to use the "
"Standalone Authenticator.".format(os.linesep))

View File

@@ -0,0 +1 @@
"""Let's Encrypt Standalone Tests"""

View File

@@ -1,4 +1,4 @@
"""Tests for letsencrypt.client.standalone_authenticator."""
"""Tests for letsencrypt.client.plugins.standalone.authenticator."""
import os
import pkg_resources
import psutil
@@ -49,9 +49,9 @@ class CallableExhausted(Exception):
class ChallPrefTest(unittest.TestCase):
"""Tests for chall_pref() method."""
def setUp(self):
from letsencrypt.client.standalone_authenticator import \
from letsencrypt.client.plugins.standalone.authenticator import \
StandaloneAuthenticator
self.authenticator = StandaloneAuthenticator()
self.authenticator = StandaloneAuthenticator(None)
def test_chall_pref(self):
self.assertEqual(self.authenticator.get_chall_pref("example.com"),
@@ -61,11 +61,11 @@ class ChallPrefTest(unittest.TestCase):
class SNICallbackTest(unittest.TestCase):
"""Tests for sni_callback() method."""
def setUp(self):
from letsencrypt.client.standalone_authenticator import \
from letsencrypt.client.plugins.standalone.authenticator import \
StandaloneAuthenticator
self.authenticator = StandaloneAuthenticator()
self.authenticator = StandaloneAuthenticator(None)
test_key = pkg_resources.resource_string(
__name__, "testdata/rsa256_key.pem")
"letsencrypt.client.tests", "testdata/rsa256_key.pem")
key = le_util.Key("foo", test_key)
self.cert = achallenges.DVSNI(
chall=challenges.DVSNI(r="x"*32, nonce="abcdef"),
@@ -104,9 +104,9 @@ class SNICallbackTest(unittest.TestCase):
class ClientSignalHandlerTest(unittest.TestCase):
"""Tests for client_signal_handler() method."""
def setUp(self):
from letsencrypt.client.standalone_authenticator import \
from letsencrypt.client.plugins.standalone.authenticator import \
StandaloneAuthenticator
self.authenticator = StandaloneAuthenticator()
self.authenticator = StandaloneAuthenticator(None)
self.authenticator.tasks = {"foononce.acme.invalid": "stuff"}
self.authenticator.child_pid = 12345
@@ -133,15 +133,15 @@ class ClientSignalHandlerTest(unittest.TestCase):
class SubprocSignalHandlerTest(unittest.TestCase):
"""Tests for subproc_signal_handler() method."""
def setUp(self):
from letsencrypt.client.standalone_authenticator import \
from letsencrypt.client.plugins.standalone.authenticator import \
StandaloneAuthenticator
self.authenticator = StandaloneAuthenticator()
self.authenticator = StandaloneAuthenticator(None)
self.authenticator.tasks = {"foononce.acme.invalid": "stuff"}
self.authenticator.child_pid = 12345
self.authenticator.parent_pid = 23456
@mock.patch("letsencrypt.client.standalone_authenticator.os.kill")
@mock.patch("letsencrypt.client.standalone_authenticator.sys.exit")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.kill")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.sys.exit")
def test_subproc_signal_handler(self, mock_exit, mock_kill):
self.authenticator.ssl_conn = mock.MagicMock()
self.authenticator.connection = mock.MagicMock()
@@ -155,8 +155,8 @@ class SubprocSignalHandlerTest(unittest.TestCase):
self.authenticator.parent_pid, signal.SIGUSR1)
mock_exit.assert_called_once_with(0)
@mock.patch("letsencrypt.client.standalone_authenticator.os.kill")
@mock.patch("letsencrypt.client.standalone_authenticator.sys.exit")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.kill")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.sys.exit")
def test_subproc_signal_handler_trouble(self, mock_exit, mock_kill):
"""Test attempting to shut down a non-existent connection.
@@ -185,14 +185,15 @@ class SubprocSignalHandlerTest(unittest.TestCase):
class AlreadyListeningTest(unittest.TestCase):
"""Tests for already_listening() method."""
def setUp(self):
from letsencrypt.client.standalone_authenticator import \
from letsencrypt.client.plugins.standalone.authenticator import \
StandaloneAuthenticator
self.authenticator = StandaloneAuthenticator()
self.authenticator = StandaloneAuthenticator(None)
@mock.patch("letsencrypt.client.standalone_authenticator.psutil."
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.psutil."
"net_connections")
@mock.patch("letsencrypt.client.standalone_authenticator.psutil.Process")
@mock.patch("letsencrypt.client.standalone_authenticator."
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"psutil.Process")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"zope.component.getUtility")
def test_race_condition(self, mock_get_utility, mock_process, mock_net):
# This tests a race condition, or permission problem, or OS
@@ -200,14 +201,14 @@ class AlreadyListeningTest(unittest.TestCase):
# found to match the identified listening PID.
from psutil._common import sconn
conns = [
sconn(fd=-1, family=2, type=1, laddr=('0.0.0.0', 30),
raddr=(), status='LISTEN', pid=None),
sconn(fd=3, family=2, type=1, laddr=('192.168.5.10', 32783),
raddr=('20.40.60.80', 22), status='ESTABLISHED', pid=1234),
sconn(fd=-1, family=10, type=1, laddr=('::1', 54321),
raddr=('::1', 111), status='CLOSE_WAIT', pid=None),
sconn(fd=3, family=2, type=1, laddr=('0.0.0.0', 17),
raddr=(), status='LISTEN', pid=4416)]
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
raddr=(), status="LISTEN", pid=None),
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
raddr=("::1", 111), status="CLOSE_WAIT", pid=None),
sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17),
raddr=(), status="LISTEN", pid=4416)]
mock_net.return_value = conns
mock_process.side_effect = psutil.NoSuchProcess("No such PID")
# We simulate being unable to find the process name of PID 4416,
@@ -216,42 +217,44 @@ class AlreadyListeningTest(unittest.TestCase):
self.assertEqual(mock_get_utility.generic_notification.call_count, 0)
mock_process.assert_called_once_with(4416)
@mock.patch("letsencrypt.client.standalone_authenticator.psutil."
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.psutil."
"net_connections")
@mock.patch("letsencrypt.client.standalone_authenticator.psutil.Process")
@mock.patch("letsencrypt.client.standalone_authenticator."
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"psutil.Process")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"zope.component.getUtility")
def test_not_listening(self, mock_get_utility, mock_process, mock_net):
from psutil._common import sconn
conns = [
sconn(fd=-1, family=2, type=1, laddr=('0.0.0.0', 30),
raddr=(), status='LISTEN', pid=None),
sconn(fd=3, family=2, type=1, laddr=('192.168.5.10', 32783),
raddr=('20.40.60.80', 22), status='ESTABLISHED', pid=1234),
sconn(fd=-1, family=10, type=1, laddr=('::1', 54321),
raddr=('::1', 111), status='CLOSE_WAIT', pid=None)]
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
raddr=(), status="LISTEN", pid=None),
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
raddr=("::1", 111), status="CLOSE_WAIT", pid=None)]
mock_net.return_value = conns
mock_process.name.return_value = "inetd"
self.assertFalse(self.authenticator.already_listening(17))
self.assertEqual(mock_get_utility.generic_notification.call_count, 0)
self.assertEqual(mock_process.call_count, 0)
@mock.patch("letsencrypt.client.standalone_authenticator.psutil."
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.psutil."
"net_connections")
@mock.patch("letsencrypt.client.standalone_authenticator.psutil.Process")
@mock.patch("letsencrypt.client.standalone_authenticator."
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"psutil.Process")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"zope.component.getUtility")
def test_listening_ipv4(self, mock_get_utility, mock_process, mock_net):
from psutil._common import sconn
conns = [
sconn(fd=-1, family=2, type=1, laddr=('0.0.0.0', 30),
raddr=(), status='LISTEN', pid=None),
sconn(fd=3, family=2, type=1, laddr=('192.168.5.10', 32783),
raddr=('20.40.60.80', 22), status='ESTABLISHED', pid=1234),
sconn(fd=-1, family=10, type=1, laddr=('::1', 54321),
raddr=('::1', 111), status='CLOSE_WAIT', pid=None),
sconn(fd=3, family=2, type=1, laddr=('0.0.0.0', 17),
raddr=(), status='LISTEN', pid=4416)]
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
raddr=(), status="LISTEN", pid=None),
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
raddr=("::1", 111), status="CLOSE_WAIT", pid=None),
sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17),
raddr=(), status="LISTEN", pid=4416)]
mock_net.return_value = conns
mock_process.name.return_value = "inetd"
result = self.authenticator.already_listening(17)
@@ -259,24 +262,25 @@ class AlreadyListeningTest(unittest.TestCase):
self.assertEqual(mock_get_utility.call_count, 1)
mock_process.assert_called_once_with(4416)
@mock.patch("letsencrypt.client.standalone_authenticator.psutil."
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.psutil."
"net_connections")
@mock.patch("letsencrypt.client.standalone_authenticator.psutil.Process")
@mock.patch("letsencrypt.client.standalone_authenticator."
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"psutil.Process")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"zope.component.getUtility")
def test_listening_ipv6(self, mock_get_utility, mock_process, mock_net):
from psutil._common import sconn
conns = [
sconn(fd=-1, family=2, type=1, laddr=('0.0.0.0', 30),
raddr=(), status='LISTEN', pid=None),
sconn(fd=3, family=2, type=1, laddr=('192.168.5.10', 32783),
raddr=('20.40.60.80', 22), status='ESTABLISHED', pid=1234),
sconn(fd=-1, family=10, type=1, laddr=('::1', 54321),
raddr=('::1', 111), status='CLOSE_WAIT', pid=None),
sconn(fd=3, family=10, type=1, laddr=('::', 12345), raddr=(),
status='LISTEN', pid=4420),
sconn(fd=3, family=2, type=1, laddr=('0.0.0.0', 17),
raddr=(), status='LISTEN', pid=4416)]
sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30),
raddr=(), status="LISTEN", pid=None),
sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783),
raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234),
sconn(fd=-1, family=10, type=1, laddr=("::1", 54321),
raddr=("::1", 111), status="CLOSE_WAIT", pid=None),
sconn(fd=3, family=10, type=1, laddr=("::", 12345), raddr=(),
status="LISTEN", pid=4420),
sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17),
raddr=(), status="LISTEN", pid=4416)]
mock_net.return_value = conns
mock_process.name.return_value = "inetd"
result = self.authenticator.already_listening(12345)
@@ -288,12 +292,12 @@ class AlreadyListeningTest(unittest.TestCase):
class PerformTest(unittest.TestCase):
"""Tests for perform() method."""
def setUp(self):
from letsencrypt.client.standalone_authenticator import \
from letsencrypt.client.plugins.standalone.authenticator import \
StandaloneAuthenticator
self.authenticator = StandaloneAuthenticator()
self.authenticator = StandaloneAuthenticator(None)
test_key = pkg_resources.resource_string(
__name__, "testdata/rsa256_key.pem")
"letsencrypt.client.tests", "testdata/rsa256_key.pem")
self.key = le_util.Key("something", test_key)
self.achall1 = achallenges.DVSNI(
@@ -365,13 +369,13 @@ class PerformTest(unittest.TestCase):
class StartListenerTest(unittest.TestCase):
"""Tests for start_listener() method."""
def setUp(self):
from letsencrypt.client.standalone_authenticator import \
from letsencrypt.client.plugins.standalone.authenticator import \
StandaloneAuthenticator
self.authenticator = StandaloneAuthenticator()
self.authenticator = StandaloneAuthenticator(None)
@mock.patch("letsencrypt.client.standalone_authenticator."
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"Crypto.Random.atfork")
@mock.patch("letsencrypt.client.standalone_authenticator.os.fork")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.fork")
def test_start_listener_fork_parent(self, mock_fork, mock_atfork):
self.authenticator.do_parent_process = mock.Mock()
self.authenticator.do_parent_process.return_value = True
@@ -384,9 +388,9 @@ class StartListenerTest(unittest.TestCase):
self.authenticator.do_parent_process.assert_called_once_with(1717)
mock_atfork.assert_called_once_with()
@mock.patch("letsencrypt.client.standalone_authenticator."
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"Crypto.Random.atfork")
@mock.patch("letsencrypt.client.standalone_authenticator.os.fork")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.fork")
def test_start_listener_fork_child(self, mock_fork, mock_atfork):
self.authenticator.do_parent_process = mock.Mock()
self.authenticator.do_child_process = mock.Mock()
@@ -400,12 +404,13 @@ class StartListenerTest(unittest.TestCase):
class DoParentProcessTest(unittest.TestCase):
"""Tests for do_parent_process() method."""
def setUp(self):
from letsencrypt.client.standalone_authenticator import \
from letsencrypt.client.plugins.standalone.authenticator import \
StandaloneAuthenticator
self.authenticator = StandaloneAuthenticator()
self.authenticator = StandaloneAuthenticator(None)
@mock.patch("letsencrypt.client.standalone_authenticator.signal.signal")
@mock.patch("letsencrypt.client.standalone_authenticator."
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"signal.signal")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"zope.component.getUtility")
def test_do_parent_process_ok(self, mock_get_utility, mock_signal):
self.authenticator.subproc_state = "ready"
@@ -414,8 +419,9 @@ class DoParentProcessTest(unittest.TestCase):
self.assertEqual(mock_get_utility.call_count, 1)
self.assertEqual(mock_signal.call_count, 3)
@mock.patch("letsencrypt.client.standalone_authenticator.signal.signal")
@mock.patch("letsencrypt.client.standalone_authenticator."
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"signal.signal")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"zope.component.getUtility")
def test_do_parent_process_inuse(self, mock_get_utility, mock_signal):
self.authenticator.subproc_state = "inuse"
@@ -424,8 +430,9 @@ class DoParentProcessTest(unittest.TestCase):
self.assertEqual(mock_get_utility.call_count, 1)
self.assertEqual(mock_signal.call_count, 3)
@mock.patch("letsencrypt.client.standalone_authenticator.signal.signal")
@mock.patch("letsencrypt.client.standalone_authenticator."
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"signal.signal")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"zope.component.getUtility")
def test_do_parent_process_cantbind(self, mock_get_utility, mock_signal):
self.authenticator.subproc_state = "cantbind"
@@ -434,8 +441,9 @@ class DoParentProcessTest(unittest.TestCase):
self.assertEqual(mock_get_utility.call_count, 1)
self.assertEqual(mock_signal.call_count, 3)
@mock.patch("letsencrypt.client.standalone_authenticator.signal.signal")
@mock.patch("letsencrypt.client.standalone_authenticator."
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"signal.signal")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"zope.component.getUtility")
def test_do_parent_process_timeout(self, mock_get_utility, mock_signal):
# Normally times out in 5 seconds and returns False. We can
@@ -450,11 +458,11 @@ class DoParentProcessTest(unittest.TestCase):
class DoChildProcessTest(unittest.TestCase):
"""Tests for do_child_process() method."""
def setUp(self):
from letsencrypt.client.standalone_authenticator import \
from letsencrypt.client.plugins.standalone.authenticator import \
StandaloneAuthenticator
self.authenticator = StandaloneAuthenticator()
self.authenticator = StandaloneAuthenticator(None)
test_key = pkg_resources.resource_string(
__name__, "testdata/rsa256_key.pem")
"letsencrypt.client.tests", "testdata/rsa256_key.pem")
key = le_util.Key("foo", test_key)
self.key = key
self.cert = achallenges.DVSNI(
@@ -466,9 +474,10 @@ class DoChildProcessTest(unittest.TestCase):
self.authenticator.tasks = {"abcdef.acme.invalid": self.cert}
self.authenticator.parent_pid = 12345
@mock.patch("letsencrypt.client.standalone_authenticator.socket.socket")
@mock.patch("letsencrypt.client.standalone_authenticator.os.kill")
@mock.patch("letsencrypt.client.standalone_authenticator.sys.exit")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"socket.socket")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.kill")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.sys.exit")
def test_do_child_process_cantbind1(
self, mock_exit, mock_kill, mock_socket):
mock_exit.side_effect = IndentationError("subprocess would exit here")
@@ -488,9 +497,10 @@ class DoChildProcessTest(unittest.TestCase):
mock_exit.assert_called_once_with(1)
mock_kill.assert_called_once_with(12345, signal.SIGUSR2)
@mock.patch("letsencrypt.client.standalone_authenticator.socket.socket")
@mock.patch("letsencrypt.client.standalone_authenticator.os.kill")
@mock.patch("letsencrypt.client.standalone_authenticator.sys.exit")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"socket.socket")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.kill")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.sys.exit")
def test_do_child_process_cantbind2(self, mock_exit, mock_kill,
mock_socket):
mock_exit.side_effect = IndentationError("subprocess would exit here")
@@ -504,7 +514,8 @@ class DoChildProcessTest(unittest.TestCase):
mock_exit.assert_called_once_with(1)
mock_kill.assert_called_once_with(12345, signal.SIGUSR1)
@mock.patch("letsencrypt.client.standalone_authenticator.socket.socket")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"socket.socket")
def test_do_child_process_cantbind3(self, mock_socket):
"""Test case where attempt to bind socket results in an unhandled
socket error. (The expected behavior is arguably wrong because it
@@ -517,10 +528,11 @@ class DoChildProcessTest(unittest.TestCase):
self.assertRaises(
socket.error, self.authenticator.do_child_process, 1717, self.key)
@mock.patch("letsencrypt.client.standalone_authenticator."
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"OpenSSL.SSL.Connection")
@mock.patch("letsencrypt.client.standalone_authenticator.socket.socket")
@mock.patch("letsencrypt.client.standalone_authenticator.os.kill")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"socket.socket")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.kill")
def test_do_child_process_success(
self, mock_kill, mock_socket, mock_connection):
sample_socket = mock.MagicMock()
@@ -543,17 +555,18 @@ class DoChildProcessTest(unittest.TestCase):
class CleanupTest(unittest.TestCase):
"""Tests for cleanup() method."""
def setUp(self):
from letsencrypt.client.standalone_authenticator import \
from letsencrypt.client.plugins.standalone.authenticator import \
StandaloneAuthenticator
self.authenticator = StandaloneAuthenticator()
self.authenticator = StandaloneAuthenticator(None)
self.achall = achallenges.DVSNI(
chall=challenges.DVSNI(r="whee", nonce="foononce"),
domain="foo.example.com", key="key")
self.authenticator.tasks = {self.achall.nonce_domain: "stuff"}
self.authenticator.child_pid = 12345
@mock.patch("letsencrypt.client.standalone_authenticator.os.kill")
@mock.patch("letsencrypt.client.standalone_authenticator.time.sleep")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator.os.kill")
@mock.patch("letsencrypt.client.plugins.standalone.authenticator."
"time.sleep")
def test_cleanup(self, mock_sleep, mock_kill):
mock_sleep.return_value = None
mock_kill.return_value = None
@@ -573,9 +586,9 @@ class CleanupTest(unittest.TestCase):
class MoreInfoTest(unittest.TestCase):
"""Tests for more_info() method. (trivially)"""
def setUp(self):
from letsencrypt.client.standalone_authenticator import (
from letsencrypt.client.plugins.standalone.authenticator import (
StandaloneAuthenticator)
self.authenticator = StandaloneAuthenticator()
self.authenticator = StandaloneAuthenticator(None)
def test_more_info(self):
"""Make sure exceptions aren't raised."""
@@ -585,9 +598,9 @@ class MoreInfoTest(unittest.TestCase):
class InitTest(unittest.TestCase):
"""Tests for more_info() method. (trivially)"""
def setUp(self):
from letsencrypt.client.standalone_authenticator import (
from letsencrypt.client.plugins.standalone.authenticator import (
StandaloneAuthenticator)
self.authenticator = StandaloneAuthenticator()
self.authenticator = StandaloneAuthenticator(None)
def test_prepare(self):
"""Make sure exceptions aren't raised.

View File

@@ -83,7 +83,8 @@ class Reverter(object):
def view_config_changes(self):
"""Displays all saved checkpoints.
All checkpoints are printed to the console.
All checkpoints are printed by
:meth:`letsencrypt.client.interfaces.IDisplay.notification`.
.. todo:: Decide on a policy for error handling, OSError IOError...
@@ -130,17 +131,17 @@ class Reverter(object):
os.linesep.join(output), display_util.HEIGHT)
def add_to_temp_checkpoint(self, save_files, save_notes):
"""Add files to temporary checkpoint
"""Add files to temporary checkpoint.
param set save_files: set of filepaths to save
param str save_notes: notes about changes during the save
:param set save_files: set of filepaths to save
:param str save_notes: notes about changes during the save
"""
self._add_to_checkpoint_dir(
self.config.temp_checkpoint_dir, save_files, save_notes)
def add_to_checkpoint(self, save_files, save_notes):
"""Add files to a permanent checkpoint
"""Add files to a permanent checkpoint.
:param set save_files: set of filepaths to save
:param str save_notes: notes about changes during the save
@@ -324,15 +325,18 @@ class Reverter(object):
new_fd.close()
def recovery_routine(self):
"""Revert all previously modified files.
"""Revert configuration to most recent finalized checkpoint.
First, any changes found in IConfig.temp_checkpoint_dir are removed,
then IN_PROGRESS changes are removed The order is important.
IN_PROGRESS is unable to add files that are already added by a TEMP
change. Thus TEMP must be rolled back first because that will be the
'latest' occurrence of the file.
Remove all changes (temporary and permanent) that have not been
finalized. This is useful to protect against crashes and other
execution interruptions.
"""
# First, any changes found in IConfig.temp_checkpoint_dir are removed,
# then IN_PROGRESS changes are removed The order is important.
# IN_PROGRESS is unable to add files that are already added by a TEMP
# change. Thus TEMP must be rolled back first because that will be the
# 'latest' occurrence of the file.
self.revert_temporary_config()
if os.path.isdir(self.config.in_progress_dir):
try:
@@ -385,11 +389,10 @@ class Reverter(object):
return True
def finalize_checkpoint(self, title):
"""Move IN_PROGRESS checkpoint to timestamped checkpoint.
"""Finalize the checkpoint.
Adds title to self.config.in_progress_dir CHANGES_SINCE
Move self.config.in_progress_dir to Backups directory and
rename the directory as a timestamp
Timestamps and permanently saves all changes made through the use
of :func:`~add_to_checkpoint` and :func:`~register_file_creation`
:param str title: Title describing checkpoint
@@ -397,6 +400,9 @@ class Reverter(object):
checkpoint is not able to be finalized.
"""
# Adds title to self.config.in_progress_dir CHANGES_SINCE
# Move self.config.in_progress_dir to Backups directory and
# rename the directory as a timestamp
# Check to make sure an "in progress" directory exists
if not os.path.isdir(self.config.in_progress_dir):
return

View File

@@ -8,8 +8,10 @@ from letsencrypt.acme import challenges
from letsencrypt.acme import jose
KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
"letsencrypt.client.tests", os.path.join("testdata", "rsa256_key.pem")))
KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
pkg_resources.resource_string(
"letsencrypt.client.tests",
os.path.join("testdata", "rsa256_key.pem"))))
# Challenges
SIMPLE_HTTPS = challenges.SimpleHTTPS(
@@ -27,40 +29,40 @@ POP = challenges.ProofOfPossession(
alg="RS256", nonce="xD\xf9\xb9\xdbU\xed\xaa\x17\xf1y|\x81\x88\x99 ",
hints=challenges.ProofOfPossession.Hints(
jwk=jose.JWKRSA(key=KEY.publickey()),
cert_fingerprints=[
cert_fingerprints=(
"93416768eb85e33adc4277f4c9acd63e7418fcfe",
"16d95b7b63f1972b980b14c20291f3c0d1855d95",
"48b46570d9fc6358108af43ad1649484def0debf"
],
certs=[], # TODO
subject_key_identifiers=["d0083162dcc4c8a23ecb8aecbd86120e56fd24e5"],
serial_numbers=[34234239832, 23993939911, 17],
issuers=[
),
certs=(), # TODO
subject_key_identifiers=("d0083162dcc4c8a23ecb8aecbd86120e56fd24e5"),
serial_numbers=(34234239832, 23993939911, 17),
issuers=(
"C=US, O=SuperT LLC, CN=SuperTrustworthy Public CA",
"O=LessTrustworthy CA Inc, CN=LessTrustworthy But StillSecure",
],
authorized_for=["www.example.com", "example.net"],
),
authorized_for=("www.example.com", "example.net"),
)
)
CHALLENGES = [SIMPLE_HTTPS, DVSNI, DNS, RECOVERY_CONTACT, RECOVERY_TOKEN, POP]
DV_CHALLENGES = [chall for chall in CHALLENGES
if isinstance(chall, challenges.DVChallenge)]
CLIENT_CHALLENGES = [chall for chall in CHALLENGES
if isinstance(chall, challenges.ClientChallenge)]
CONT_CHALLENGES = [chall for chall in CHALLENGES
if isinstance(chall, challenges.ContinuityChallenge)]
def gen_combos(challs):
"""Generate natural combinations for challs."""
dv_chall = []
renewal_chall = []
cont_chall = []
for i, chall in enumerate(challs): # pylint: disable=redefined-outer-name
if isinstance(chall, challenges.DVChallenge):
dv_chall.append(i)
else:
renewal_chall.append(i)
cont_chall.append(i)
# Gen combos for 1 of each type
return [[i, j] for i in xrange(len(dv_chall))
for j in xrange(len(renewal_chall))]
# Gen combos for 1 of each type, lowest index first (makes testing easier)
return tuple((i, j) if i < j else (j, i)
for i in dv_chall for j in cont_chall)

View File

@@ -30,17 +30,17 @@ class SatisfyChallengesTest(unittest.TestCase):
from letsencrypt.client.auth_handler import AuthHandler
self.mock_dv_auth = mock.MagicMock(name="ApacheConfigurator")
self.mock_client_auth = mock.MagicMock(name="ClientAuthenticator")
self.mock_cont_auth = mock.MagicMock(name="ContinuityAuthenticator")
self.mock_dv_auth.get_chall_pref.return_value = [challenges.DVSNI]
self.mock_client_auth.get_chall_pref.return_value = [
self.mock_cont_auth.get_chall_pref.return_value = [
challenges.RecoveryToken]
self.mock_client_auth.perform.side_effect = gen_auth_resp
self.mock_cont_auth.perform.side_effect = gen_auth_resp
self.mock_dv_auth.perform.side_effect = gen_auth_resp
self.handler = AuthHandler(
self.mock_dv_auth, self.mock_client_auth, None)
self.mock_dv_auth, self.mock_cont_auth, None)
logging.disable(logging.CRITICAL)
@@ -61,9 +61,9 @@ class SatisfyChallengesTest(unittest.TestCase):
self.assertEqual("DVSNI0", self.handler.responses[dom][0])
self.assertEqual(len(self.handler.dv_c), 1)
self.assertEqual(len(self.handler.client_c), 1)
self.assertEqual(len(self.handler.cont_c), 1)
self.assertEqual(len(self.handler.dv_c[dom]), 1)
self.assertEqual(len(self.handler.client_c[dom]), 0)
self.assertEqual(len(self.handler.cont_c[dom]), 0)
def test_name1_rectok1(self):
dom = "0"
@@ -78,16 +78,16 @@ class SatisfyChallengesTest(unittest.TestCase):
self.assertEqual(len(self.handler.responses[dom]), 1)
# Test if statement for dv_auth perform
self.assertEqual(self.mock_client_auth.perform.call_count, 1)
self.assertEqual(self.mock_cont_auth.perform.call_count, 1)
self.assertEqual(self.mock_dv_auth.perform.call_count, 0)
self.assertEqual("RecoveryToken0", self.handler.responses[dom][0])
# Assert 1 domain
self.assertEqual(len(self.handler.dv_c), 1)
self.assertEqual(len(self.handler.client_c), 1)
self.assertEqual(len(self.handler.cont_c), 1)
# Assert 1 auth challenge, 0 dv
self.assertEqual(len(self.handler.dv_c[dom]), 0)
self.assertEqual(len(self.handler.client_c[dom]), 1)
self.assertEqual(len(self.handler.cont_c[dom]), 1)
def test_name5_dvsni5(self):
for i in xrange(5):
@@ -102,11 +102,11 @@ class SatisfyChallengesTest(unittest.TestCase):
self.assertEqual(len(self.handler.responses), 5)
self.assertEqual(len(self.handler.dv_c), 5)
self.assertEqual(len(self.handler.client_c), 5)
self.assertEqual(len(self.handler.cont_c), 5)
# Each message contains 1 auth, 0 client
# Test proper call count for methods
self.assertEqual(self.mock_client_auth.perform.call_count, 0)
self.assertEqual(self.mock_cont_auth.perform.call_count, 0)
self.assertEqual(self.mock_dv_auth.perform.call_count, 1)
for i in xrange(5):
@@ -114,7 +114,7 @@ class SatisfyChallengesTest(unittest.TestCase):
self.assertEqual(len(self.handler.responses[dom]), 1)
self.assertEqual(self.handler.responses[dom][0], "DVSNI%d" % i)
self.assertEqual(len(self.handler.dv_c[dom]), 1)
self.assertEqual(len(self.handler.client_c[dom]), 0)
self.assertEqual(len(self.handler.cont_c[dom]), 0)
self.assertTrue(isinstance(self.handler.dv_c[dom][0].achall,
achallenges.DVSNI))
@@ -138,10 +138,10 @@ class SatisfyChallengesTest(unittest.TestCase):
self.assertEqual(len(self.handler.responses[dom]),
len(acme_util.DV_CHALLENGES))
self.assertEqual(len(self.handler.dv_c), 1)
self.assertEqual(len(self.handler.client_c), 1)
self.assertEqual(len(self.handler.cont_c), 1)
# Test if statement for client_auth perform
self.assertEqual(self.mock_client_auth.perform.call_count, 0)
# Test if statement for cont_auth perform
self.assertEqual(self.mock_cont_auth.perform.call_count, 0)
self.assertEqual(self.mock_dv_auth.perform.call_count, 1)
self.assertEqual(
@@ -149,7 +149,7 @@ class SatisfyChallengesTest(unittest.TestCase):
self._get_exp_response(dom, path, acme_util.DV_CHALLENGES))
self.assertEqual(len(self.handler.dv_c[dom]), 1)
self.assertEqual(len(self.handler.client_c[dom]), 0)
self.assertEqual(len(self.handler.cont_c[dom]), 0)
self.assertTrue(isinstance(self.handler.dv_c[dom][0].achall,
achallenges.SimpleHTTPS))
@@ -175,16 +175,16 @@ class SatisfyChallengesTest(unittest.TestCase):
self.assertEqual(
len(self.handler.responses[dom]), len(acme_util.CHALLENGES))
self.assertEqual(len(self.handler.dv_c), 1)
self.assertEqual(len(self.handler.client_c), 1)
self.assertEqual(len(self.handler.cont_c), 1)
self.assertEqual(len(self.handler.dv_c[dom]), 1)
self.assertEqual(len(self.handler.client_c[dom]), 1)
self.assertEqual(len(self.handler.cont_c[dom]), 1)
self.assertEqual(
self.handler.responses[dom],
self._get_exp_response(dom, path, acme_util.CHALLENGES))
self.assertTrue(isinstance(self.handler.dv_c[dom][0].achall,
achallenges.SimpleHTTPS))
self.assertTrue(isinstance(self.handler.client_c[dom][0].achall,
self.assertTrue(isinstance(self.handler.cont_c[dom][0].achall,
achallenges.RecoveryToken))
@mock.patch("letsencrypt.client.auth_handler.gen_challenge_path")
@@ -209,7 +209,7 @@ class SatisfyChallengesTest(unittest.TestCase):
self.assertEqual(
len(self.handler.responses[str(i)]), len(acme_util.CHALLENGES))
self.assertEqual(len(self.handler.dv_c), 5)
self.assertEqual(len(self.handler.client_c), 5)
self.assertEqual(len(self.handler.cont_c), 5)
for i in xrange(5):
dom = str(i)
@@ -217,11 +217,11 @@ class SatisfyChallengesTest(unittest.TestCase):
self.handler.responses[dom],
self._get_exp_response(dom, path, acme_util.CHALLENGES))
self.assertEqual(len(self.handler.dv_c[dom]), 1)
self.assertEqual(len(self.handler.client_c[dom]), 1)
self.assertEqual(len(self.handler.cont_c[dom]), 1)
self.assertTrue(isinstance(self.handler.dv_c[dom][0].achall,
achallenges.DVSNI))
self.assertTrue(isinstance(self.handler.client_c[dom][0].achall,
self.assertTrue(isinstance(self.handler.cont_c[dom][0].achall,
achallenges.RecoveryContact))
@mock.patch("letsencrypt.client.auth_handler.gen_challenge_path")
@@ -255,7 +255,7 @@ class SatisfyChallengesTest(unittest.TestCase):
self.assertEqual(len(self.handler.responses), 5)
self.assertEqual(len(self.handler.dv_c), 5)
self.assertEqual(len(self.handler.client_c), 5)
self.assertEqual(len(self.handler.cont_c), 5)
for i in xrange(5):
dom = str(i)
@@ -263,7 +263,7 @@ class SatisfyChallengesTest(unittest.TestCase):
self.assertEqual(self.handler.responses[dom], resp)
self.assertEqual(len(self.handler.dv_c[dom]), 1)
self.assertEqual(
len(self.handler.client_c[dom]), len(chosen_chall[i]) - 1)
len(self.handler.cont_c[dom]), len(chosen_chall[i]) - 1)
self.assertTrue(isinstance(
self.handler.dv_c["0"][0].achall, achallenges.DNS))
@@ -276,10 +276,10 @@ class SatisfyChallengesTest(unittest.TestCase):
self.assertTrue(isinstance(
self.handler.dv_c["4"][0].achall, achallenges.DNS))
self.assertTrue(isinstance(self.handler.client_c["2"][0].achall,
self.assertTrue(isinstance(self.handler.cont_c["2"][0].achall,
achallenges.ProofOfPossession))
self.assertTrue(isinstance(
self.handler.client_c["4"][0].achall, achallenges.RecoveryToken))
self.handler.cont_c["4"][0].achall, achallenges.RecoveryToken))
@mock.patch("letsencrypt.client.auth_handler.gen_challenge_path")
def test_perform_exception_cleanup(self, mock_chall_path):
@@ -309,11 +309,11 @@ class SatisfyChallengesTest(unittest.TestCase):
# Verify cleanup is actually run correctly
self.assertEqual(self.mock_dv_auth.cleanup.call_count, 2)
self.assertEqual(self.mock_client_auth.cleanup.call_count, 2)
self.assertEqual(self.mock_cont_auth.cleanup.call_count, 2)
dv_cleanup_args = self.mock_dv_auth.cleanup.call_args_list
client_cleanup_args = self.mock_client_auth.cleanup.call_args_list
cont_cleanup_args = self.mock_cont_auth.cleanup.call_args_list
# Check DV cleanup
for i in xrange(2):
@@ -325,10 +325,10 @@ class SatisfyChallengesTest(unittest.TestCase):
# Check Auth cleanup
for i in xrange(2):
client_chall_list = client_cleanup_args[i][0][0]
self.assertEqual(len(client_chall_list), 1)
cont_chall_list = cont_cleanup_args[i][0][0]
self.assertEqual(len(cont_chall_list), 1)
self.assertTrue(
isinstance(client_chall_list[0], achallenges.ProofOfPossession))
isinstance(cont_chall_list[0], achallenges.ProofOfPossession))
def _get_exp_response(self, domain, path, challs):
@@ -346,7 +346,7 @@ class GetAuthorizationsTest(unittest.TestCase):
from letsencrypt.client.auth_handler import AuthHandler
self.mock_dv_auth = mock.MagicMock(name="ApacheConfigurator")
self.mock_client_auth = mock.MagicMock(name="ClientAuthenticator")
self.mock_cont_auth = mock.MagicMock(name="ContinuityAuthenticator")
self.mock_sat_chall = mock.MagicMock(name="_satisfy_challenges")
self.mock_acme_auth = mock.MagicMock(name="acme_authorization")
@@ -354,7 +354,7 @@ class GetAuthorizationsTest(unittest.TestCase):
self.iteration = 0
self.handler = AuthHandler(
self.mock_dv_auth, self.mock_client_auth, None)
self.mock_dv_auth, self.mock_cont_auth, None)
self.handler._satisfy_challenges = self.mock_sat_chall
self.handler.acme_authorization = self.mock_acme_auth
@@ -388,7 +388,7 @@ class GetAuthorizationsTest(unittest.TestCase):
# Assignment was > 80 char...
dv_c, c_c = self.handler._challenge_factory(dom, [0])
self.handler.dv_c[dom], self.handler.client_c[dom] = dv_c, c_c
self.handler.dv_c[dom], self.handler.cont_c[dom] = dv_c, c_c
def test_progress_failure(self):
self.handler.add_chall_msg(
@@ -414,7 +414,7 @@ class GetAuthorizationsTest(unittest.TestCase):
self.handler.msgs[dom].challenges)
dv_c, c_c = self.handler._challenge_factory(
dom, self.handler.paths[dom])
self.handler.dv_c[dom], self.handler.client_c[dom] = dv_c, c_c
self.handler.dv_c[dom], self.handler.cont_c[dom] = dv_c, c_c
def test_incremental_progress(self):
for dom, challs in [("0", acme_util.CHALLENGES),
@@ -444,9 +444,9 @@ class GetAuthorizationsTest(unittest.TestCase):
self.handler.paths["1"] = [2]
# This is probably overkill... but set it anyway
dv_c, c_c = self.handler._challenge_factory("0", [1, 3])
self.handler.dv_c["0"], self.handler.client_c["0"] = dv_c, c_c
self.handler.dv_c["0"], self.handler.cont_c["0"] = dv_c, c_c
dv_c, c_c = self.handler._challenge_factory("1", [2])
self.handler.dv_c["1"], self.handler.client_c["1"] = dv_c, c_c
self.handler.dv_c["1"], self.handler.cont_c["1"] = dv_c, c_c
self.iteration += 1
@@ -481,7 +481,7 @@ class PathSatisfiedTest(unittest.TestCase):
self.handler.responses[dom[0]] = [None, "sat", "sat2", None]
self.handler.paths[dom[1]] = [0]
self.handler.responses[dom[1]] = ["sat", None, None, None]
self.handler.responses[dom[1]] = ["sat", None, None, False]
self.handler.paths[dom[2]] = [0]
self.handler.responses[dom[2]] = ["sat"]
@@ -496,7 +496,7 @@ class PathSatisfiedTest(unittest.TestCase):
self.assertTrue(self.handler._path_satisfied(dom[i]))
def test_not_satisfied(self):
dom = ["0", "1", "2"]
dom = ["0", "1", "2", "3"]
self.handler.paths[dom[0]] = [1, 2]
self.handler.responses[dom[0]] = ["sat1", None, "sat2", None]
@@ -506,10 +506,84 @@ class PathSatisfiedTest(unittest.TestCase):
self.handler.paths[dom[2]] = [0]
self.handler.responses[dom[2]] = [None]
self.handler.paths[dom[3]] = [0]
self.handler.responses[dom[3]] = [False]
for i in xrange(3):
self.assertFalse(self.handler._path_satisfied(dom[i]))
class GenChallengePathTest(unittest.TestCase):
"""Tests for letsencrypt.client.auth_handler.gen_challenge_path.
.. todo:: Add more tests for dumb_path... depending on what we want to do.
"""
def setUp(self):
logging.disable(logging.fatal)
def tearDown(self):
logging.disable(logging.NOTSET)
@classmethod
def _call(cls, challs, preferences, combinations):
from letsencrypt.client.auth_handler import gen_challenge_path
return gen_challenge_path(challs, preferences, combinations)
def test_common_case(self):
"""Given DVSNI and SimpleHTTPS with appropriate combos."""
challs = (acme_util.DVSNI, acme_util.SIMPLE_HTTPS)
prefs = [challenges.DVSNI]
combos = ((0,), (1,))
# Smart then trivial dumb path test
self.assertEqual(self._call(challs, prefs, combos), (0,))
self.assertTrue(self._call(challs, prefs, None))
# Rearrange order...
self.assertEqual(self._call(challs[::-1], prefs, combos), (1,))
self.assertTrue(self._call(challs[::-1], prefs, None))
def test_common_case_with_continuity(self):
challs = (acme_util.RECOVERY_TOKEN,
acme_util.RECOVERY_CONTACT,
acme_util.DVSNI,
acme_util.SIMPLE_HTTPS)
prefs = [challenges.RecoveryToken, challenges.DVSNI]
combos = acme_util.gen_combos(challs)
self.assertEqual(self._call(challs, prefs, combos), (0, 2))
# dumb_path() trivial test
self.assertTrue(self._call(challs, prefs, None))
def test_full_cont_server(self):
challs = (acme_util.RECOVERY_TOKEN,
acme_util.RECOVERY_CONTACT,
acme_util.POP,
acme_util.DVSNI,
acme_util.SIMPLE_HTTPS,
acme_util.DNS)
# Typical webserver client that can do everything except DNS
# Attempted to make the order realistic
prefs = [challenges.RecoveryToken,
challenges.ProofOfPossession,
challenges.SimpleHTTPS,
challenges.DVSNI,
challenges.RecoveryContact]
combos = acme_util.gen_combos(challs)
self.assertEqual(self._call(challs, prefs, combos), (0, 4))
# Dumb path trivial test
self.assertTrue(self._call(challs, prefs, None))
def test_not_supported(self):
challs = (acme_util.POP, acme_util.DVSNI)
prefs = [challenges.DVSNI]
combos = ((0, 1),)
self.assertRaises(errors.LetsEncryptAuthHandlerError,
self._call, challs, prefs, combos)
class MutuallyExclusiveTest(unittest.TestCase):
"""Tests for letsencrypt.client.auth_handler.mutually_exclusive."""

View File

@@ -8,8 +8,9 @@ from letsencrypt.client import errors
class DetermineAuthenticatorTest(unittest.TestCase):
def setUp(self):
from letsencrypt.client.apache.configurator import ApacheConfigurator
from letsencrypt.client.standalone_authenticator import (
from letsencrypt.client.plugins.apache.configurator import (
ApacheConfigurator)
from letsencrypt.client.plugins.standalone.authenticator import (
StandaloneAuthenticator)
self.mock_stand = mock.MagicMock(
@@ -65,7 +66,8 @@ class DetermineAuthenticatorTest(unittest.TestCase):
class RollbackTest(unittest.TestCase):
"""Test the rollback function."""
def setUp(self):
from letsencrypt.client.apache.configurator import ApacheConfigurator
from letsencrypt.client.plugins.apache.configurator import (
ApacheConfigurator)
self.m_install = mock.MagicMock(spec=ApacheConfigurator)
@classmethod

View File

@@ -1,4 +1,4 @@
"""Test the ClientAuthenticator dispatcher."""
"""Test the ContinuityAuthenticator dispatcher."""
import unittest
import mock
@@ -13,9 +13,9 @@ class PerformTest(unittest.TestCase):
"""Test client perform function."""
def setUp(self):
from letsencrypt.client.client_authenticator import ClientAuthenticator
from letsencrypt.client.continuity_auth import ContinuityAuthenticator
self.auth = ClientAuthenticator(
self.auth = ContinuityAuthenticator(
mock.MagicMock(server="demo_server.org"))
self.auth.rec_token.perform = mock.MagicMock(
name="rec_token_perform", side_effect=gen_client_resp)
@@ -38,7 +38,7 @@ class PerformTest(unittest.TestCase):
def test_unexpected(self):
self.assertRaises(
errors.LetsEncryptClientAuthError, self.auth.perform, [
errors.LetsEncryptContAuthError, self.auth.perform, [
achallenges.DVSNI(chall=None, domain="0", key="invalid_key")])
def test_chall_pref(self):
@@ -50,9 +50,9 @@ class CleanupTest(unittest.TestCase):
"""Test the Authenticator cleanup function."""
def setUp(self):
from letsencrypt.client.client_authenticator import ClientAuthenticator
from letsencrypt.client.continuity_auth import ContinuityAuthenticator
self.auth = ClientAuthenticator(
self.auth = ContinuityAuthenticator(
mock.MagicMock(server="demo_server.org"))
self.mock_cleanup = mock.MagicMock(name="rec_token_cleanup")
self.auth.rec_token.cleanup = self.mock_cleanup
@@ -70,7 +70,7 @@ class CleanupTest(unittest.TestCase):
token = achallenges.RecoveryToken(chall=None, domain="0")
unexpected = achallenges.DVSNI(chall=None, domain="0", key="dummy_key")
self.assertRaises(errors.LetsEncryptClientAuthError,
self.assertRaises(errors.LetsEncryptContAuthError,
self.auth.cleanup, [token, unexpected])

View File

@@ -0,0 +1,458 @@
"""Tests for letsencrypt.client.network2."""
import datetime
import httplib
import os
import pkg_resources
import unittest
import M2Crypto
import mock
import requests
from letsencrypt.client import errors
from letsencrypt.acme import challenges
from letsencrypt.acme import jose
from letsencrypt.acme import messages2
CERT = jose.ComparableX509(M2Crypto.X509.load_cert_string(
pkg_resources.resource_string(
__name__, os.path.join('testdata/cert.pem'))))
CERT2 = jose.ComparableX509(M2Crypto.X509.load_cert_string(
pkg_resources.resource_string(
__name__, os.path.join('testdata/cert-san.pem'))))
CSR = jose.ComparableX509(M2Crypto.X509.load_request_string(
pkg_resources.resource_string(
__name__, os.path.join('testdata/csr.pem'))))
KEY = jose.JWKRSA.load(pkg_resources.resource_string(
__name__, os.path.join('testdata/rsa512_key.pem')))
KEY2 = jose.JWKRSA.load(pkg_resources.resource_string(
__name__, os.path.join('testdata/rsa256_key.pem')))
class NetworkTest(unittest.TestCase):
"""Tests for letsencrypt.client.network2.Network."""
# pylint: disable=too-many-instance-attributes,too-many-public-methods
def setUp(self):
from letsencrypt.client.network2 import Network
self.net = Network(
new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg',
key=KEY, alg=jose.RS256)
self.response = mock.MagicMock(ok=True, status_code=httplib.OK)
self.response.headers = {}
self.response.links = {}
self.identifier = messages2.Identifier(
typ=messages2.IDENTIFIER_FQDN, value='example.com')
# Registration
self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212')
reg = messages2.Registration(
contact=self.contact, key=KEY.public(), recovery_token='t')
self.regr = messages2.RegistrationResource(
body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1',
new_authzr_uri='https://www.letsencrypt-demo.org/acme/new-reg',
terms_of_service='https://www.letsencrypt-demo.org/tos')
# Authorization
authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1'
challb = messages2.ChallengeBody(
uri=(authzr_uri + '/1'), status=messages2.STATUS_VALID,
chall=challenges.DNS(token='foo'))
self.challr = messages2.ChallengeResource(
body=challb, authzr_uri=authzr_uri)
self.authz = messages2.Authorization(
identifier=messages2.Identifier(
typ=messages2.IDENTIFIER_FQDN, value='example.com'),
challenges=(challb,), combinations=None, key=KEY.public())
self.authzr = messages2.AuthorizationResource(
body=self.authz, uri=authzr_uri,
new_cert_uri='https://www.letsencrypt-demo.org/acme/new-cert')
# Request issuance
self.certr = messages2.CertificateResource(
body=CERT, authzrs=(self.authzr,),
uri='https://www.letsencrypt-demo.org/acme/cert/1',
cert_chain_uri='https://www.letsencrypt-demo.org/ca')
def _mock_post_get(self):
# pylint: disable=protected-access
self.net._post = mock.MagicMock(return_value=self.response)
self.net._get = mock.MagicMock(return_value=self.response)
def test_wrap_in_jws(self):
class MockJSONDeSerializable(jose.JSONDeSerializable):
# pylint: disable=missing-docstring
def __init__(self, value):
self.value = value
def to_json(self):
return self.value
@classmethod
def from_json(cls, value):
return cls(value)
# pylint: disable=protected-access
jws = self.net._wrap_in_jws(MockJSONDeSerializable('foo'))
self.assertEqual(jose.JWS.json_loads(jws).payload, '"foo"')
def test_check_response_not_ok_jobj_no_error(self):
self.response.ok = False
self.response.json.return_value = {}
# pylint: disable=protected-access
self.assertRaises(
errors.NetworkError, self.net._check_response, self.response)
def test_check_response_not_ok_jobj_error(self):
self.response.ok = False
self.response.json.return_value = messages2.Error(detail='foo')
# pylint: disable=protected-access
self.assertRaises(
messages2.Error, self.net._check_response, self.response)
def test_check_response_not_ok_no_jobj(self):
self.response.ok = False
self.response.json.side_effect = ValueError
# pylint: disable=protected-access
self.assertRaises(
errors.NetworkError, self.net._check_response, self.response)
def test_check_response_ok_no_jobj_ct_required(self):
self.response.json.side_effect = ValueError
for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']:
self.response.headers['Content-Type'] = response_ct
# pylint: disable=protected-access
self.assertRaises(
errors.NetworkError, self.net._check_response, self.response,
content_type=self.net.JSON_CONTENT_TYPE)
def test_check_response_ok_no_jobj_no_ct(self):
self.response.json.side_effect = ValueError
for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']:
self.response.headers['Content-Type'] = response_ct
# pylint: disable=protected-access
self.net._check_response(self.response)
def test_check_response_jobj(self):
self.response.json.return_value = {}
for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']:
self.response.headers['Content-Type'] = response_ct
# pylint: disable=protected-access
self.net._check_response(self.response)
@mock.patch('letsencrypt.client.network2.requests')
def test_get_requests_error_passthrough(self, requests_mock):
requests_mock.exceptions = requests.exceptions
requests_mock.get.side_effect = requests.exceptions.RequestException
# pylint: disable=protected-access
self.assertRaises(errors.NetworkError, self.net._get, 'uri')
@mock.patch('letsencrypt.client.network2.requests')
def test_get(self, requests_mock):
# pylint: disable=protected-access
self.net._check_response = mock.MagicMock()
self.net._get('uri', content_type='ct')
self.net._check_response.assert_called_once_with(
requests_mock.get('uri'), content_type='ct')
@mock.patch('letsencrypt.client.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')
@mock.patch('letsencrypt.client.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.net._check_response.assert_called_once_with(
requests_mock.post('uri', 'data'), content_type='ct')
def test_register(self):
self.response.status_code = httplib.CREATED
self.response.json.return_value = self.regr.body.fully_serialize()
self.response.headers['Location'] = self.regr.uri
self.response.links.update({
'next': {'url': self.regr.new_authzr_uri},
'terms-of-service': {'url': self.regr.terms_of_service},
})
self._mock_post_get()
self.assertEqual(self.regr, self.net.register(self.contact))
# TODO: test POST call arguments
# TODO: split here and separate test
reg_wrong_key = self.regr.body.update(key=KEY2.public())
self.response.json.return_value = reg_wrong_key.fully_serialize()
self.assertRaises(
errors.UnexpectedUpdate, self.net.register, self.contact)
def test_register_missing_next(self):
self.response.status_code = httplib.CREATED
self._mock_post_get()
self.assertRaises(
errors.NetworkError, self.net.register, self.regr.body)
def test_update_registration(self):
self.response.headers['Location'] = self.regr.uri
self.response.json.return_value = self.regr.body.fully_serialize()
self._mock_post_get()
self.assertEqual(self.regr, self.net.update_registration(self.regr))
# TODO: split here and separate test
self.response.json.return_value = self.regr.body.update(
contact=()).fully_serialize()
self.assertRaises(
errors.UnexpectedUpdate, self.net.update_registration, self.regr)
def test_request_challenges(self):
self.response.status_code = httplib.CREATED
self.response.headers['Location'] = self.authzr.uri
self.response.json.return_value = self.authz.fully_serialize()
self.response.links = {
'next': {'url': self.authzr.new_cert_uri},
}
self._mock_post_get()
self.net.request_challenges(self.identifier, self.regr)
# TODO: test POST call arguments
# TODO: split here and separate test
authz_wrong_key = self.authz.update(key=KEY2.public())
self.response.json.return_value = authz_wrong_key.fully_serialize()
self.assertRaises(
errors.UnexpectedUpdate, self.net.request_challenges,
self.identifier, self.regr)
def test_request_challenges_missing_next(self):
self.response.status_code = httplib.CREATED
self._mock_post_get()
self.assertRaises(
errors.NetworkError, self.net.request_challenges,
self.identifier, self.regr)
def test_request_domain_challenges(self):
self.net.request_challenges = mock.MagicMock()
self.assertEqual(
self.net.request_challenges(self.identifier),
self.net.request_domain_challenges('example.com', self.regr))
def test_answer_challenge(self):
self.response.links['up'] = {'url': self.challr.authzr_uri}
self.response.json.return_value = self.challr.body.fully_serialize()
chall_response = challenges.DNSResponse()
self._mock_post_get()
self.net.answer_challenge(self.challr.body, chall_response)
# TODO: split here and separate test
self.assertRaises(errors.UnexpectedUpdate, self.net.answer_challenge,
self.challr.body.update(uri='foo'), chall_response)
def test_answer_challenge_missing_next(self):
self._mock_post_get()
self.assertRaises(errors.NetworkError, self.net.answer_challenge,
self.challr.body, challenges.DNSResponse())
def test_answer_challenges(self):
self.net.answer_challenge = mock.MagicMock()
self.assertEqual(
[self.net.answer_challenge(
self.challr.body, challenges.DNSResponse())],
self.net.answer_challenges(
[self.challr.body], [challenges.DNSResponse()]))
def test_retry_after_date(self):
self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT'
self.assertEqual(
datetime.datetime(1999, 12, 31, 23, 59, 59),
self.net.retry_after(response=self.response, default=10))
@mock.patch('letsencrypt.client.network2.datetime')
def test_retry_after_invalid(self, dt_mock):
dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
dt_mock.timedelta = datetime.timedelta
self.response.headers['Retry-After'] = 'foooo'
self.assertEqual(
datetime.datetime(2015, 3, 27, 0, 0, 10),
self.net.retry_after(response=self.response, default=10))
@mock.patch('letsencrypt.client.network2.datetime')
def test_retry_after_seconds(self, dt_mock):
dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
dt_mock.timedelta = datetime.timedelta
self.response.headers['Retry-After'] = '50'
self.assertEqual(
datetime.datetime(2015, 3, 27, 0, 0, 50),
self.net.retry_after(response=self.response, default=10))
@mock.patch('letsencrypt.client.network2.datetime')
def test_retry_after_missing(self, dt_mock):
dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
dt_mock.timedelta = datetime.timedelta
self.assertEqual(
datetime.datetime(2015, 3, 27, 0, 0, 10),
self.net.retry_after(response=self.response, default=10))
def test_poll(self):
self.response.json.return_value = self.authzr.body.fully_serialize()
self._mock_post_get()
self.assertEqual((self.authzr, self.response),
self.net.poll(self.authzr))
def test_request_issuance(self):
self.response.content = CERT.as_der()
self.response.headers['Location'] = self.certr.uri
self.response.links['up'] = {'url': self.certr.cert_chain_uri}
self._mock_post_get()
self.assertEqual(
self.certr, self.net.request_issuance(CSR, (self.authzr,)))
# TODO: check POST args
def test_request_issuance_missing_up(self):
self.response.content = CERT.as_der()
self.response.headers['Location'] = self.certr.uri
self._mock_post_get()
self.assertEqual(
self.certr.update(cert_chain_uri=None),
self.net.request_issuance(CSR, (self.authzr,)))
def test_request_issuance_missing_location(self):
self._mock_post_get()
self.assertRaises(
errors.NetworkError, self.net.request_issuance,
CSR, (self.authzr,))
@mock.patch('letsencrypt.client.network2.datetime')
@mock.patch('letsencrypt.client.network2.time')
def test_poll_and_request_issuance(self, time_mock, dt_mock):
# clock.dt | pylint: disable=no-member
clock = mock.MagicMock(dt=datetime.datetime(2015, 3, 27))
def sleep(seconds):
"""increment clock"""
clock.dt += datetime.timedelta(seconds=seconds)
time_mock.sleep.side_effect = sleep
def now():
"""return current clock value"""
return clock.dt
dt_mock.datetime.now.side_effect = now
dt_mock.timedelta = datetime.timedelta
def poll(authzr): # pylint: disable=missing-docstring
# record poll start time based on the current clock value
authzr.times.append(clock.dt)
# suppose it takes 2 seconds for server to produce the
# result, increment clock
clock.dt += datetime.timedelta(seconds=2)
if not authzr.retries: # no more retries
done = mock.MagicMock(uri=authzr.uri, times=authzr.times)
done.body.status = messages2.STATUS_VALID
return done, []
# response (2nd result tuple element) is reduced to only
# Retry-After header contents represented as integer
# seconds; authzr.retries is a list of Retry-After
# headers, head(retries) is peeled of as a current
# Retry-After header, and tail(retries) is persisted for
# later poll() calls
return (mock.MagicMock(retries=authzr.retries[1:],
uri=authzr.uri + '.', times=authzr.times),
authzr.retries[0])
self.net.poll = mock.MagicMock(side_effect=poll)
mintime = 7
def retry_after(response, default): # pylint: disable=missing-docstring
# check that poll_and_request_issuance correctly passes mintime
self.assertEqual(default, mintime)
return clock.dt + datetime.timedelta(seconds=response)
self.net.retry_after = mock.MagicMock(side_effect=retry_after)
def request_issuance(csr, authzrs): # pylint: disable=missing-docstring
return csr, authzrs
self.net.request_issuance = mock.MagicMock(side_effect=request_issuance)
csr = mock.MagicMock()
authzrs = (
mock.MagicMock(uri='a', times=[], retries=(8, 20, 30)),
mock.MagicMock(uri='b', times=[], retries=(5,)),
)
cert, updated_authzrs = self.net.poll_and_request_issuance(
csr, authzrs, mintime=mintime)
self.assertTrue(cert[0] is csr)
self.assertTrue(cert[1] is updated_authzrs)
self.assertEqual(updated_authzrs[0].uri, 'a...')
self.assertEqual(updated_authzrs[1].uri, 'b.')
self.assertEqual(updated_authzrs[0].times, [
datetime.datetime(2015, 3, 27),
# a is scheduled for 10, but b is polling [9..11), so it
# will be picked up as soon as b is finished, without
# additional sleeping
datetime.datetime(2015, 3, 27, 0, 0, 11),
datetime.datetime(2015, 3, 27, 0, 0, 33),
datetime.datetime(2015, 3, 27, 0, 1, 5),
])
self.assertEqual(updated_authzrs[1].times, [
datetime.datetime(2015, 3, 27, 0, 0, 2),
datetime.datetime(2015, 3, 27, 0, 0, 9),
])
self.assertEqual(clock.dt, datetime.datetime(2015, 3, 27, 0, 1, 7))
def test_check_cert(self):
self.response.headers['Location'] = self.certr.uri
self.response.content = CERT2.as_der()
self._mock_post_get()
self.assertEqual(
self.certr.update(body=CERT2), self.net.check_cert(self.certr))
# TODO: split here and separate test
self.response.headers['Location'] = 'foo'
self.assertRaises(
errors.UnexpectedUpdate, self.net.check_cert, self.certr)
def test_check_cert_missing_location(self):
self.response.content = CERT2.as_der()
self._mock_post_get()
self.assertRaises(errors.NetworkError, self.net.check_cert, self.certr)
def test_refresh(self):
self.net.check_cert = mock.MagicMock()
self.assertEqual(
self.net.check_cert(self.certr), self.net.refresh(self.certr))
def test_fetch_chain(self):
# pylint: disable=protected-access
self.net._get_cert = mock.MagicMock()
self.assertEqual(self.net._get_cert(self.certr.cert_chain_uri),
self.net.fetch_chain(self.certr))
def test_fetch_chain_no_up_link(self):
self.assertTrue(self.net.fetch_chain(self.certr.update(
cert_chain_uri=None)) is None)
def test_revoke(self):
self._mock_post_get()
self.net.revoke(self.certr, when=messages2.Revocation.NOW)
# pylint: disable=protected-access
self.net._post.assert_called_once_with(self.certr.uri, mock.ANY)
def test_revoke_bad_status_raises_error(self):
self.response.status_code = httplib.METHOD_NOT_ALLOWED
self._mock_post_get()
self.assertRaises(errors.NetworkError, self.net.revoke, self.certr)
if __name__ == '__main__':
unittest.main()

View File

@@ -10,7 +10,7 @@ import mock
from letsencrypt.client import errors
from letsencrypt.client import le_util
from letsencrypt.client.apache import configurator
from letsencrypt.client.plugins.apache import configurator
from letsencrypt.client.display import util as display_util

Some files were not shown because too many files have changed in this diff Show More