Merge branch 'master' of ssh://github.com/letsencrypt/letsencrypt

This commit is contained in:
Peter Eckersley 2015-10-08 11:32:19 -07:00
commit 53d532cfe3
33 changed files with 324 additions and 166 deletions

View file

@ -5,4 +5,5 @@ include CONTRIBUTING.md
include LICENSE.txt
include linter_plugin.py
include letsencrypt/EULA
recursive-include docs *
recursive-include letsencrypt/tests/testdata *

View file

@ -194,7 +194,7 @@ class JSONDeSerializable(object):
:rtype: str
"""
return self.json_dumps(sort_keys=True, indent=4, separators=(',', ': '))
return self.json_dumps(sort_keys=True, indent=4)
@classmethod
def json_dump_default(cls, python_object):

View file

@ -91,7 +91,7 @@ class JSONDeSerializableTest(unittest.TestCase):
def test_json_dumps_pretty(self):
self.assertEqual(
self.seq.json_dumps_pretty(), '[\n "foo1",\n "foo2"\n]')
self.seq.json_dumps_pretty(), '[\n "foo1", \n "foo2"\n]')
def test_json_dump_default(self):
from acme.jose.interfaces import JSONDeSerializable

View file

@ -1,10 +1,12 @@
"""JSON Web Key."""
import abc
import binascii
import json
import logging
import cryptography.exceptions
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric import rsa
@ -27,6 +29,32 @@ class JWK(json_util.TypedJSONObjectWithFields):
cryptography_key_types = ()
"""Subclasses should override."""
required = NotImplemented
"""Required members of public key's representation as defined by JWK/JWA."""
_thumbprint_json_dumps_params = {
# "no whitespace or line breaks before or after any syntactic
# elements"
'indent': 0,
'separators': (',', ':'),
# "members ordered lexicographically by the Unicode [UNICODE]
# code points of the member names"
'sort_keys': True,
}
def thumbprint(self, hash_function=hashes.SHA256):
"""Compute JWK Thumbprint.
https://tools.ietf.org/html/rfc7638
"""
digest = hashes.Hash(hash_function(), backend=default_backend())
digest.update(json.dumps(
dict((k, v) for k, v in six.iteritems(self.to_json())
if k in self.required),
**self._thumbprint_json_dumps_params).encode())
return digest.finalize()
@abc.abstractmethod
def public_key(self): # pragma: no cover
"""Generate JWK with public key.
@ -60,7 +88,7 @@ class JWK(json_util.TypedJSONObjectWithFields):
exceptions[loader] = error
# no luck
raise errors.Error("Unable to deserialize key: {0}".format(exceptions))
raise errors.Error('Unable to deserialize key: {0}'.format(exceptions))
@classmethod
def load(cls, data, password=None, backend=None):
@ -81,17 +109,17 @@ class JWK(json_util.TypedJSONObjectWithFields):
try:
key = cls._load_cryptography_key(data, password, backend)
except errors.Error as error:
logger.debug("Loading symmetric key, assymentric failed: %s", error)
logger.debug('Loading symmetric key, assymentric failed: %s', error)
return JWKOct(key=data)
if cls.typ is not NotImplemented and not isinstance(
key, cls.cryptography_key_types):
raise errors.Error("Unable to deserialize {0} into {1}".format(
raise errors.Error('Unable to deserialize {0} into {1}'.format(
key.__class__, cls.__class__))
for jwk_cls in six.itervalues(cls.TYPES):
if isinstance(key, jwk_cls.cryptography_key_types):
return jwk_cls(key=key)
raise errors.Error("Unsupported algorithm: {0}".format(key.__class__))
raise errors.Error('Unsupported algorithm: {0}'.format(key.__class__))
@JWK.register
@ -105,6 +133,7 @@ class JWKES(JWK): # pragma: no cover
typ = 'ES'
cryptography_key_types = (
ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey)
required = ('crv', JWK.type_field_name, 'x', 'y')
def fields_to_partial_json(self):
raise NotImplementedError()
@ -122,6 +151,7 @@ class JWKOct(JWK):
"""Symmetric JWK."""
typ = 'oct'
__slots__ = ('key',)
required = ('k', JWK.type_field_name)
def fields_to_partial_json(self):
# TODO: An "alg" member SHOULD also be present to identify the
@ -150,6 +180,7 @@ class JWKRSA(JWK):
typ = 'RSA'
cryptography_key_types = (rsa.RSAPublicKey, rsa.RSAPrivateKey)
__slots__ = ('key',)
required = ('e', JWK.type_field_name, 'n')
def __init__(self, *args, **kwargs):
if 'key' in kwargs and not isinstance(
@ -204,7 +235,7 @@ class JWKRSA(JWK):
jobj.get(x) for x in ('p', 'q', 'dp', 'dq', 'qi'))
if tuple(param for param in all_params if param is None):
raise errors.Error(
"Some private parameters are missing: {0}".format(
'Some private parameters are missing: {0}'.format(
all_params))
p, q, dp, dq, qi = tuple(
cls._decode_param(x) for x in all_params)

View file

@ -25,9 +25,24 @@ class JWKTest(unittest.TestCase):
self.assertRaises(errors.Error, JWKRSA.load, DSA_PEM)
class JWKOctTest(unittest.TestCase):
class JWKTestBaseMixin(object):
"""Mixin test for JWK subclass tests."""
thumbprint = NotImplemented
def test_thumbprint_private(self):
self.assertEqual(self.thumbprint, self.jwk.thumbprint())
def test_thumbprint_public(self):
self.assertEqual(self.thumbprint, self.jwk.public_key().thumbprint())
class JWKOctTest(unittest.TestCase, JWKTestBaseMixin):
"""Tests for acme.jose.jwk.JWKOct."""
thumbprint = (b"=,\xdd;I\x1a+i\x02x\x8a\x12?06IM\xc2\x80"
b"\xe4\xc3\x1a\xfc\x89\xf3)'\xce\xccm\xfd5")
def setUp(self):
from acme.jose.jwk import JWKOct
self.jwk = JWKOct(key=b'foo')
@ -52,10 +67,13 @@ class JWKOctTest(unittest.TestCase):
self.assertTrue(self.jwk.public_key() is self.jwk)
class JWKRSATest(unittest.TestCase):
class JWKRSATest(unittest.TestCase, JWKTestBaseMixin):
"""Tests for acme.jose.jwk.JWKRSA."""
# pylint: disable=too-many-instance-attributes
thumbprint = (b'\x08\xfa1\x87\x1d\x9b6H/*\x1eW\xc2\xe3\xf6P'
b'\xefs\x0cKB\x87\xcf\x85yO\x045\x0e\x91\x80\x0b')
def setUp(self):
from acme.jose.jwk import JWKRSA
self.jwk256 = JWKRSA(key=RSA256_KEY.public_key())
@ -87,6 +105,7 @@ class JWKRSATest(unittest.TestCase):
'dq': 'bHh2u7etM8LKKCF2pY2UdQ',
'qi': 'oi45cEkbVoJjAbnQpFY87Q',
})
self.jwk = self.private
def test_init_auto_comparable(self):
self.assertTrue(isinstance(

View file

@ -10,7 +10,6 @@ install_requires = [
# load_pem_private/public_key (>=0.6)
# rsa_recover_prime_factors (>=0.8)
'cryptography>=0.8',
'mock<1.1.0', # py26
'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304)
'pyasn1', # urllib3 InsecurePlatformWarning (#304)
# Connection.set_tlsext_host_name (>=0.13), X509Req.get_extensions (>=0.15)
@ -25,8 +24,13 @@ install_requires = [
# env markers in extras_require cause problems with older pip: #517
if sys.version_info < (2, 7):
# only some distros recognize stdlib argparse as already satisfying
install_requires.append('argparse')
install_requires.extend([
# only some distros recognize stdlib argparse as already satisfying
'argparse',
'mock<1.1.0',
])
else:
install_requires.append('mock')
testing_extras = [
'nose',

View file

@ -1,2 +1,15 @@
#!/bin/sh
pacman -S git python2 python2-virtualenv gcc dialog augeas openssl libffi ca-certificates
# "python-virtualenv" is Python3, but "python2-virtualenv" provides
# only "virtualenv2" binary, not "virtualenv" necessary in
# ./bootstrap/dev/_common_venv.sh
pacman -S \
git \
python2 \
python-virtualenv \
gcc \
dialog \
augeas \
openssl \
libffi \
ca-certificates \

1
bootstrap/dev/README Normal file
View file

@ -0,0 +1 @@
This directory contains developer setup.

25
bootstrap/dev/_venv_common.sh Executable file
View file

@ -0,0 +1,25 @@
#!/bin/sh -xe
VENV_NAME=${VENV_NAME:-venv}
# .egg-info directories tend to cause bizzaire problems (e.g. `pip -e
# .` might unexpectedly install letshelp-letsencrypt only, in case
# `python letshelp-letsencrypt/setup.py build` has been called
# earlier)
rm -rf *.egg-info
# virtualenv setup is NOT idempotent: shutil.Error:
# `/home/jakub/dev/letsencrypt/letsencrypt/venv/bin/python2` and
# `venv/bin/python2` are the same file
mv $VENV_NAME "$VENV_NAME.$(date +%s).bak" || true
virtualenv --no-site-packages $VENV_NAME $VENV_ARGS
. ./$VENV_NAME/bin/activate
# Separately install setuptools and pip to make sure following
# invocations use latest
pip install -U setuptools
pip install -U pip
pip install "$@"
echo "Please run the following command to activate developer environment:"
echo "source $VENV_NAME/bin/activate"

13
bootstrap/dev/venv.sh Executable file
View file

@ -0,0 +1,13 @@
#!/bin/sh -xe
# Developer virtualenv setup for Let's Encrypt client
export VENV_ARGS="--python python2"
./bootstrap/dev/_venv_common.sh \
-r requirements.txt \
-e acme[testing] \
-e .[dev,docs,testing] \
-e letsencrypt-apache \
-e letsencrypt-nginx \
-e letshelp-letsencrypt \
-e letsencrypt-compatibility-test

8
bootstrap/dev/venv3.sh Executable file
View file

@ -0,0 +1,8 @@
#!/bin/sh -xe
# Developer Python3 virtualenv setup for Let's Encrypt
export VENV_NAME="${VENV_NAME:-venv3}"
export VENV_ARGS="--python python3"
./bootstrap/dev/_venv_common.sh \
-e acme[testing] \

View file

@ -30,7 +30,7 @@ here = os.path.abspath(os.path.dirname(__file__))
# read version number (and other metadata) from package init
init_fn = os.path.join(here, '..', 'letsencrypt', '__init__.py')
with codecs.open(init_fn, encoding='utf8') as fd:
meta = dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", fd.read()))
meta = dict(re.findall(r"""__([a-z]+)__ = '([^']+)""", fd.read()))
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the

View file

@ -7,38 +7,37 @@ Contributing
Hacking
=======
Start by :doc:`installing dependencies and setting up Let's Encrypt
<using>`.
All changes in your pull request **must** have 100% unit test coverage, pass
our `integration`_ tests, **and** be compliant with the
:ref:`coding style <coding-style>`.
When you're done activate the virtualenv:
Bootstrap
---------
Start by :ref:`installing Let's Encrypt prerequisites
<prerequisites>`. Then run:
.. code-block:: shell
source ./venv/bin/activate
./bootstrap/dev/venv.sh
This step should prepend you prompt with ``(venv)`` and save you from
typing ``./venv/bin/...``. It is also required to run some of the
`testing`_ tools. Virtualenv can be disabled at any time by typing
``deactivate``. More information can be found in `virtualenv
Activate the virtualenv:
.. code-block:: shell
source ./$VENV_NAME/bin/activate
This step should prepend you prompt with ``($VENV_NAME)`` and save you
from typing ``./$VENV_NAME/bin/...``. It is also required to run some
of the `testing`_ tools. Virtualenv can be disabled at any time by
typing ``deactivate``. More information can be found in `virtualenv
documentation`_.
Install the development packages:
.. code-block:: shell
pip install -r requirements.txt -e acme -e .[dev,docs,testing] -e letsencrypt-apache -e letsencrypt-nginx -e letshelp-letsencrypt
.. note:: `-e` (short for `--editable`) turns on *editable mode* in
which any source code changes in the current working
directory are "live" and no further `pip install ...`
invocations are necessary while developing.
This is roughly equivalent to `python setup.py develop`. For
more info see `man pip`.
The code base, including your pull requests, **must** have 100% unit
test coverage, pass our `integration`_ tests **and** be compliant with
the :ref:`coding style <coding-style>`.
Note that packages are installed in so called *editable mode*, in
which any source code changes in the current working directory are
"live" and no further ``./bootstrap/dev/venv.sh`` or ``pip install
...`` invocations are necessary while developing.
.. _`virtualenv documentation`: https://virtualenv.pypa.io
@ -67,8 +66,10 @@ The following tools are there to help you:
Integration
~~~~~~~~~~~
Mac OS X users: Run `./tests/mac-bootstrap.sh` instead of `boulder-start.sh` to
install dependencies, configure the environment, and start boulder.
First, install `Go`_ 1.5, libtool-ltdl, mariadb-server and
Otherwise, install `Go`_ 1.5, libtool-ltdl, mariadb-server and
rabbitmq-server and then start Boulder_, an ACME CA server::
./tests/boulder-start.sh

View file

@ -42,6 +42,8 @@ above method instead.
https://github.com/letsencrypt/letsencrypt/archive/master.zip
.. _prerequisites:
Prerequisites
=============
@ -121,11 +123,13 @@ Installation
============
.. "pip install acme" doesn't search for "acme" in cwd, just like "pip
install -e acme" does
install -e acme" does; `-U setuptools pip` necessary for #722
.. code-block:: shell
virtualenv --no-site-packages -p python2 venv
./venv/bin/pip install -U setuptools
./venv/bin/pip install -U pip
./venv/bin/pip install -r requirements.txt acme/ . letsencrypt-apache/ letsencrypt-nginx/
.. warning:: Please do **not** use ``python setup.py install``. Please

View file

@ -1,3 +1,5 @@
import sys
from setuptools import setup
from setuptools import find_packages
@ -7,13 +9,17 @@ version = '0.1.0.dev0'
install_requires = [
'acme=={0}'.format(version),
'letsencrypt=={0}'.format(version),
'mock<1.1.0', # py26
'python-augeas',
'setuptools', # pkg_resources
'zope.component',
'zope.interface',
]
if sys.version_info < (2, 7):
install_requires.append('mock<1.1.0')
else:
install_requires.append('mock')
setup(
name='letsencrypt-apache',
version=version,

View file

@ -1,3 +1,6 @@
include LICENSE.txt
include README.rst
include letsencrypt_compatibility_test/configurators/apache/a2enmod.sh
include letsencrypt_compatibility_test/configurators/apache/a2dismod.sh
include letsencrypt_compatibility_test/configurators/apache/Dockerfile
recursive-include letsencrypt_compatibility_test/testdata *

View file

@ -1,3 +1,5 @@
import sys
from setuptools import setup
from setuptools import find_packages
@ -9,10 +11,14 @@ install_requires = [
'letsencrypt-apache=={0}'.format(version),
'letsencrypt-nginx=={0}'.format(version),
'docker-py',
'mock<1.1.0', # py26
'zope.interface',
]
if sys.version_info < (2, 7):
install_requires.append('mock<1.1.0')
else:
install_requires.append('mock')
setup(
name='letsencrypt-compatibility-test',
version=version,

View file

@ -1,3 +1,5 @@
import sys
from setuptools import setup
from setuptools import find_packages
@ -7,13 +9,17 @@ version = '0.1.0.dev0'
install_requires = [
'acme=={0}'.format(version),
'letsencrypt=={0}'.format(version),
'mock<1.1.0', # py26
'PyOpenSSL',
'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary?
'setuptools', # pkg_resources
'zope.interface',
]
if sys.version_info < (2, 7):
install_requires.append('mock<1.1.0')
else:
install_requires.append('mock')
setup(
name='letsencrypt-nginx',
version=version,

View file

@ -702,8 +702,6 @@ def create_parser(plugins, args):
help=config_help("dvsni_port"))
helpful.add("testing", "--simple-http-port", type=int,
help=config_help("simple_http_port"))
helpful.add("testing", "--no-simple-http-tls", action="store_true",
help=config_help("no_simple_http_tls"))
helpful.add_group(
"security", description="Security parameters & server settings")
@ -729,11 +727,13 @@ def create_parser(plugins, args):
return helpful.parser, helpful.args
# For now unfortunately this constant just needs to match the code below;
# there isn't an elegant way to autogenerate it in time.
VERBS = ["run", "auth", "install", "revoke", "rollback", "config_changes", "plugins"]
HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + VERBS
def _create_subparsers(helpful):
subparsers = helpful.parser.add_subparsers(metavar="SUBCOMMAND")
@ -741,7 +741,7 @@ def _create_subparsers(helpful):
if name == "plugins":
func = plugins_cmd
else:
func = eval(name) # pylint: disable=eval-used
func = eval(name) # pylint: disable=eval-used
h = func.__doc__.splitlines()[0]
subparser = subparsers.add_parser(name, help=h, description=func.__doc__)
subparser.set_defaults(func=func)
@ -762,22 +762,23 @@ def _create_subparsers(helpful):
helpful.add_group("plugins", description="Plugin options")
helpful.add("auth",
"--csr", type=read_file, help="Path to a Certificate Signing Request (CSR) in DER format.")
"--csr", type=read_file,
help="Path to a Certificate Signing Request (CSR) in DER format.")
helpful.add("rollback",
"--checkpoints", type=int, metavar="N",
default=flag_default("rollback_checkpoints"),
help="Revert configuration N number of checkpoints.")
"--checkpoints", type=int, metavar="N",
default=flag_default("rollback_checkpoints"),
help="Revert configuration N number of checkpoints.")
helpful.add("plugins",
"--init", action="store_true", help="Initialize plugins.")
"--init", action="store_true", help="Initialize plugins.")
helpful.add("plugins",
"--prepare", action="store_true", help="Initialize and prepare plugins.")
"--prepare", action="store_true", help="Initialize and prepare plugins.")
helpful.add("plugins",
"--authenticators", action="append_const", dest="ifaces",
const=interfaces.IAuthenticator, help="Limit to authenticator plugins only.")
"--authenticators", action="append_const", dest="ifaces",
const=interfaces.IAuthenticator, help="Limit to authenticator plugins only.")
helpful.add("plugins",
"--installers", action="append_const", dest="ifaces",
const=interfaces.IInstaller, help="Limit to installer plugins only.")
"--installers", action="append_const", dest="ifaces",
const=interfaces.IInstaller, help="Limit to installer plugins only.")
def _paths_parser(helpful):

View file

@ -268,19 +268,15 @@ class Client(object):
:param .RenewableCert cert: Newly issued certificate
"""
if ("autorenew" not in cert.configuration or
cert.configuration.as_bool("autorenew")):
if ("autodeploy" not in cert.configuration or
cert.configuration.as_bool("autodeploy")):
if cert.autorenewal_is_enabled():
if cert.autodeployment_is_enabled():
msg = "Automatic renewal and deployment has "
else:
msg = "Automatic renewal but not automatic deployment has "
elif cert.autodeployment_is_enabled():
msg = "Automatic deployment but not automatic renewal has "
else:
if ("autodeploy" not in cert.configuration or
cert.configuration.as_bool("autodeploy")):
msg = "Automatic deployment but not automatic renewal has "
else:
msg = "Automatic renewal and deployment has not "
msg = "Automatic renewal and deployment has not "
msg += ("been enabled for your certificate. These settings can be "
"configured in the directories under {0}.").format(

View file

@ -4,7 +4,6 @@
is capable of handling the signatures.
"""
import datetime
import logging
import os
@ -258,24 +257,6 @@ def get_sans_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM):
csr, OpenSSL.crypto.load_certificate_request, typ)
def asn1_generalizedtime_to_dt(timestamp):
"""Convert ASN.1 GENERALIZEDTIME to datetime.
Useful for deserialization of `OpenSSL.crypto.X509.get_notAfter` and
`OpenSSL.crypto.X509.get_notAfter` outputs.
.. todo:: This function support only one format: `%Y%m%d%H%M%SZ`.
Implement remaining two.
"""
return datetime.datetime.strptime(timestamp, '%Y%m%d%H%M%SZ')
def pyopenssl_x509_name_as_text(x509name):
"""Convert `OpenSSL.crypto.X509Name` to text."""
return "/".join("{0}={1}" for key, value in x509name.get_components())
def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM):
"""Dump certificate chain into a bundle.

View file

@ -223,8 +223,6 @@ class IConfig(zope.interface.Interface):
"Port number to perform DVSNI challenge. "
"Boulder in testing mode defaults to 5001.")
no_simple_http_tls = zope.interface.Attribute(
"Do not use TLS when solving SimpleHTTP challenges.")
simple_http_port = zope.interface.Attribute(
"Port used in the SimpleHttp challenge.")

View file

@ -53,7 +53,7 @@ command on the target server (as root):
# served and makes it more obvious that Python command will serve
# anything recursively under the cwd
HTTP_TEMPLATE = """\
CMD_TEMPLATE = """\
mkdir -p {root}/public_html/{response.URI_ROOT_PATH}
cd {root}/public_html
echo -n {validation} > {response.URI_ROOT_PATH}/{encoded_token}
@ -63,33 +63,10 @@ $(command -v python2 || command -v python2.7 || command -v python2.6) -c \\
SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map = {{'': '{ct}'}}; \\
s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\
s.serve_forever()" """
"""Non-TLS command template."""
# https://www.piware.de/2011/01/creating-an-https-server-in-python/
HTTPS_TEMPLATE = """\
mkdir -p {root}/public_html/{response.URI_ROOT_PATH}
cd {root}/public_html
echo -n {validation} > {response.URI_ROOT_PATH}/{encoded_token}
# run only once per server:
openssl req -new -newkey rsa:4096 -subj "/" -days 1 -nodes -x509 -keyout ../key.pem -out ../cert.pem
$(command -v python2 || command -v python2.7 || command -v python2.6) -c \\
"import BaseHTTPServer, SimpleHTTPServer, ssl; \\
SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map = {{'': '{ct}'}}; \\
s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\
s.socket = ssl.wrap_socket(s.socket, keyfile='../key.pem', certfile='../cert.pem'); \\
s.serve_forever()" """
"""TLS command template.
According to the ACME specification, "the ACME server MUST ignore
the certificate provided by the HTTPS server", so the first command
generates temporary self-signed certificate.
"""
"""Command template."""
def __init__(self, *args, **kwargs):
super(Authenticator, self).__init__(*args, **kwargs)
self.template = (self.HTTP_TEMPLATE if self.config.no_simple_http_tls
else self.HTTPS_TEMPLATE)
self._root = (tempfile.mkdtemp() if self.conf("test-mode")
else "/tmp/letsencrypt")
self._httpd = None
@ -97,8 +74,7 @@ s.serve_forever()" """
@classmethod
def add_parser_arguments(cls, add):
add("test-mode", action="store_true",
help="Test mode. Executes the manual command in subprocess. "
"Requires openssl to be installed unless --no-simple-http-tls.")
help="Test mode. Executes the manual command in subprocess.")
def prepare(self): # pylint: disable=missing-docstring,no-self-use
pass # pragma: no cover
@ -142,11 +118,11 @@ binary for temporary key/certificate generation.""".replace("\n", "")
# users, but will not work if multiple domains point at the
# same server: default command doesn't support virtual hosts
response, validation = achall.gen_response_and_validation(
tls=(not self.config.no_simple_http_tls))
tls=False) # SimpleHTTP TLS is dead: ietf-wg-acme/acme#7
port = (response.port if self.config.simple_http_port is None
else int(self.config.simple_http_port))
command = self.template.format(
command = self.CMD_TEMPLATE.format(
root=self._root, achall=achall, response=response,
validation=pipes.quote(validation.json_dumps()),
encoded_token=achall.chall.encode("token"),

View file

@ -23,15 +23,13 @@ class AuthenticatorTest(unittest.TestCase):
def setUp(self):
from letsencrypt.plugins.manual import Authenticator
self.config = mock.MagicMock(
no_simple_http_tls=True, simple_http_port=4430,
manual_test_mode=False)
simple_http_port=8080, manual_test_mode=False)
self.auth = Authenticator(config=self.config, name="manual")
self.achalls = [achallenges.SimpleHTTP(
challb=acme_util.SIMPLE_HTTP_P, domain="foo.com", account_key=KEY)]
config_test_mode = mock.MagicMock(
no_simple_http_tls=True, simple_http_port=4430,
manual_test_mode=True)
simple_http_port=8080, manual_test_mode=True)
self.auth_test_mode = Authenticator(
config=config_test_mode, name="manual")
@ -55,7 +53,7 @@ class AuthenticatorTest(unittest.TestCase):
self.assertEqual([resp], self.auth.perform(self.achalls))
self.assertEqual(1, mock_raw_input.call_count)
mock_verify.assert_called_with(
self.achalls[0].challb.chall, "foo.com", KEY.public_key(), 4430)
self.achalls[0].challb.chall, "foo.com", KEY.public_key(), 8080)
message = mock_stdout.write.mock_calls[0][1][0]
self.assertTrue(self.achalls[0].chall.encode("token") in message)
@ -68,7 +66,7 @@ class AuthenticatorTest(unittest.TestCase):
mock_popen.side_effect = OSError
self.assertEqual([False], self.auth_test_mode.perform(self.achalls))
@mock.patch("letsencrypt.plugins.manual.socket.socket", autospec=True)
@mock.patch("letsencrypt.plugins.manual.socket.socket")
@mock.patch("letsencrypt.plugins.manual.time.sleep", autospec=True)
@mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True)
def test_perform_test_command_run_failure(
@ -78,7 +76,7 @@ class AuthenticatorTest(unittest.TestCase):
self.assertRaises(
errors.Error, self.auth_test_mode.perform, self.achalls)
@mock.patch("letsencrypt.plugins.manual.socket.socket", autospec=True)
@mock.patch("letsencrypt.plugins.manual.socket.socket")
@mock.patch("letsencrypt.plugins.manual.time.sleep", autospec=True)
@mock.patch("acme.challenges.SimpleHTTPResponse.simple_verify",
autospec=True)

View file

@ -129,7 +129,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
self.chain = self.configuration["chain"]
self.fullchain = self.configuration["fullchain"]
def consistent(self):
def _consistent(self):
"""Are the files associated with this lineage self-consistent?
:returns: Whether the files stored in connection with this
@ -187,7 +187,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
# for x in ALL_FOUR))) == 1
return True
def fix(self):
def _fix(self):
"""Attempt to fix defects or inconsistencies in this lineage.
.. todo:: Currently unimplemented.
@ -347,7 +347,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
smallest_current = min(self.current_version(x) for x in ALL_FOUR)
return smallest_current < self.latest_common_version()
def update_link_to(self, kind, version):
def _update_link_to(self, kind, version):
"""Make the specified item point at the specified version.
(Note that this method doesn't verify that the specified version
@ -379,7 +379,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
:param int version: the desired version"""
for kind in ALL_FOUR:
self.update_link_to(kind, version)
self._update_link_to(kind, version)
def _notafterbefore(self, method, version):
"""Internal helper function for finding notbefore/notafter."""
@ -439,6 +439,18 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
with open(target) as f:
return crypto_util.get_sans_from_cert(f.read())
def autodeployment_is_enabled(self):
"""Is automatic deployment enabled for this cert?
If autodeploy is not specified, defaults to True.
:returns: True if automatic deployment is enabled
:rtype: bool
"""
return ("autodeploy" not in self.configuration or
self.configuration.as_bool("autodeploy"))
def should_autodeploy(self):
"""Should this lineage now automatically deploy a newer version?
@ -453,8 +465,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
:rtype: bool
"""
if ("autodeploy" not in self.configuration or
self.configuration.as_bool("autodeploy")):
if self.autodeployment_is_enabled():
if self.has_pending_deployment():
interval = self.configuration.get("deploy_before_expiry",
"5 days")
@ -488,6 +499,18 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
# certificate is not revoked).
return False
def autorenewal_is_enabled(self):
"""Is automatic renewal enabled for this cert?
If autorenew is not specified, defaults to True.
:returns: True if automatic renewal is enabled
:rtype: bool
"""
return ("autorenew" not in self.configuration or
self.configuration.as_bool("autorenew"))
def should_autorenew(self):
"""Should we now try to autorenew the most recent cert version?
@ -504,8 +527,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
:rtype: bool
"""
if ("autorenew" not in self.configuration or
self.configuration.as_bool("autorenew")):
if self.autorenewal_is_enabled():
# Consider whether to attempt to autorenew this cert now
# Renewals on the basis of revocation

View file

@ -57,7 +57,6 @@ class CLITest(unittest.TestCase):
ret = cli.main(args)
return ret, None, stderr, client
def test_no_flags(self):
with mock.patch('letsencrypt.cli.run') as mock_run:
self._call([])
@ -91,7 +90,6 @@ class CLITest(unittest.TestCase):
from letsencrypt import cli
self.assertTrue(cli.USAGE in out)
def test_rollback(self):
_, _, _, client = self._call(['rollback'])
self.assertEqual(1, client.rollback.call_count)

View file

@ -4,14 +4,12 @@ import shutil
import tempfile
import unittest
import configobj
import OpenSSL
import mock
from acme import jose
from letsencrypt import account
from letsencrypt import configuration
from letsencrypt import errors
from letsencrypt import le_util
@ -120,29 +118,28 @@ class ClientTest(unittest.TestCase):
def test_report_renewal_status(self, mock_zope):
# pylint: disable=protected-access
cert = mock.MagicMock()
cert.configuration = configobj.ConfigObj()
cert.cli_config = configuration.RenewerConfiguration(self.config)
cert.cli_config.renewal_configs_dir = "/foo/bar/baz"
cert.configuration["autorenew"] = "True"
cert.configuration["autodeploy"] = "True"
cert.autorenewal_is_enabled.return_value = True
cert.autodeployment_is_enabled.return_value = True
self.client._report_renewal_status(cert)
msg = mock_zope().add_message.call_args[0][0]
self.assertTrue("renewal and deployment has been" in msg)
self.assertTrue(cert.cli_config.renewal_configs_dir in msg)
cert.configuration["autorenew"] = "False"
cert.autorenewal_is_enabled.return_value = False
self.client._report_renewal_status(cert)
msg = mock_zope().add_message.call_args[0][0]
self.assertTrue("deployment but not automatic renewal" in msg)
self.assertTrue(cert.cli_config.renewal_configs_dir in msg)
cert.configuration["autodeploy"] = "False"
cert.autodeployment_is_enabled.return_value = False
self.client._report_renewal_status(cert)
msg = mock_zope().add_message.call_args[0][0]
self.assertTrue("renewal and deployment has not" in msg)
self.assertTrue(cert.cli_config.renewal_configs_dir in msg)
cert.configuration["autorenew"] = "True"
cert.autorenewal_is_enabled.return_value = True
self.client._report_renewal_status(cert)
msg = mock_zope().add_message.call_args[0][0]
self.assertTrue("renewal but not automatic deployment" in msg)

View file

@ -123,46 +123,47 @@ class RenewableCertTests(BaseRenewableCertTest):
self.assertRaises(
errors.CertStorageError, storage.RenewableCert, config, defaults)
def test_consistent(self): # pylint: disable=too-many-statements
def test_consistent(self):
# pylint: disable=too-many-statements,protected-access
oldcert = self.test_rc.cert
self.test_rc.cert = "relative/path"
# Absolute path for item requirement
self.assertFalse(self.test_rc.consistent())
self.assertFalse(self.test_rc._consistent())
self.test_rc.cert = oldcert
# Items must exist requirement
self.assertFalse(self.test_rc.consistent())
self.assertFalse(self.test_rc._consistent())
# Items must be symlinks requirements
fill_with_sample_data(self.test_rc)
self.assertFalse(self.test_rc.consistent())
self.assertFalse(self.test_rc._consistent())
unlink_all(self.test_rc)
# Items must point to desired place if they are relative
for kind in ALL_FOUR:
os.symlink(os.path.join("..", kind + "17.pem"),
getattr(self.test_rc, kind))
self.assertFalse(self.test_rc.consistent())
self.assertFalse(self.test_rc._consistent())
unlink_all(self.test_rc)
# Items must point to desired place if they are absolute
for kind in ALL_FOUR:
os.symlink(os.path.join(self.tempdir, kind + "17.pem"),
getattr(self.test_rc, kind))
self.assertFalse(self.test_rc.consistent())
self.assertFalse(self.test_rc._consistent())
unlink_all(self.test_rc)
# Items must point to things that exist
for kind in ALL_FOUR:
os.symlink(os.path.join("..", "..", "archive", "example.org",
kind + "17.pem"),
getattr(self.test_rc, kind))
self.assertFalse(self.test_rc.consistent())
self.assertFalse(self.test_rc._consistent())
# This version should work
fill_with_sample_data(self.test_rc)
self.assertTrue(self.test_rc.consistent())
self.assertTrue(self.test_rc._consistent())
# Items must point to things that follow the naming convention
os.unlink(self.test_rc.fullchain)
os.symlink(os.path.join("..", "..", "archive", "example.org",
"fullchain_17.pem"), self.test_rc.fullchain)
with open(self.test_rc.fullchain, "w") as f:
f.write("wrongly-named fullchain")
self.assertFalse(self.test_rc.consistent())
self.assertFalse(self.test_rc._consistent())
def test_current_target(self):
# Relative path logic
@ -259,14 +260,15 @@ class RenewableCertTests(BaseRenewableCertTest):
with open(where, "w") as f:
f.write(kind)
self.assertEqual(ver, self.test_rc.current_version(kind))
self.test_rc.update_link_to("cert", 3)
self.test_rc.update_link_to("privkey", 2)
# pylint: disable=protected-access
self.test_rc._update_link_to("cert", 3)
self.test_rc._update_link_to("privkey", 2)
self.assertEqual(3, self.test_rc.current_version("cert"))
self.assertEqual(2, self.test_rc.current_version("privkey"))
self.assertEqual(5, self.test_rc.current_version("chain"))
self.assertEqual(5, self.test_rc.current_version("fullchain"))
# Currently we are allowed to update to a version that doesn't exist
self.test_rc.update_link_to("chain", 3000)
self.test_rc._update_link_to("chain", 3000)
# However, current_version doesn't allow querying the resulting
# version (because it's a broken link).
self.assertEqual(os.path.basename(os.readlink(self.test_rc.chain)),
@ -405,6 +407,14 @@ class RenewableCertTests(BaseRenewableCertTest):
self.assertEqual(self.test_rc.should_autodeploy(), result)
self.assertEqual(self.test_rc.should_autorenew(), result)
def test_autodeployment_is_enabled(self):
self.assertTrue(self.test_rc.autodeployment_is_enabled())
self.test_rc.configuration["autodeploy"] = "1"
self.assertTrue(self.test_rc.autodeployment_is_enabled())
self.test_rc.configuration["autodeploy"] = "0"
self.assertFalse(self.test_rc.autodeployment_is_enabled())
def test_should_autodeploy(self):
"""Test should_autodeploy() on the basis of reasons other than
expiry time window."""
@ -425,6 +435,14 @@ class RenewableCertTests(BaseRenewableCertTest):
f.write(kind)
self.assertFalse(self.test_rc.should_autodeploy())
def test_autorenewal_is_enabled(self):
self.assertTrue(self.test_rc.autorenewal_is_enabled())
self.test_rc.configuration["autorenew"] = "1"
self.assertTrue(self.test_rc.autorenewal_is_enabled())
self.test_rc.configuration["autorenew"] = "0"
self.assertFalse(self.test_rc.autorenewal_is_enabled())
@mock.patch("letsencrypt.storage.RenewableCert.ocsp_revoked")
def test_should_autorenew(self, mock_ocsp):
"""Test should_autorenew on the basis of reasons other than
@ -507,7 +525,8 @@ class RenewableCertTests(BaseRenewableCertTest):
self.defaults, self.cli_config)
# This consistency check tests most relevant properties about the
# newly created cert lineage.
self.assertTrue(result.consistent())
# pylint: disable=protected-access
self.assertTrue(result._consistent())
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.renewal_configs_dir, "the-lineage.com.conf")))
with open(result.fullchain) as f:
@ -578,9 +597,10 @@ class RenewableCertTests(BaseRenewableCertTest):
self.assertRaises(
errors.CertStorageError,
self.test_rc.newest_available_version, "elephant")
# pylint: disable=protected-access
self.assertRaises(
errors.CertStorageError,
self.test_rc.update_link_to, "elephant", 17)
self.test_rc._update_link_to, "elephant", 17)
def test_ocsp_revoked(self):
# XXX: This is currently hardcoded to False due to a lack of an

View file

@ -35,7 +35,6 @@ install_requires = [
'ConfigArgParse',
'configobj',
'cryptography>=0.7', # load_pem_x509_certificate
'mock<1.1.0', # py26
'parsedatetime',
'psutil>=2.1.0', # net_connections introduced in 2.1.0
'PyOpenSSL',
@ -50,8 +49,13 @@ install_requires = [
# env markers in extras_require cause problems with older pip: #517
if sys.version_info < (2, 7):
# only some distros recognize stdlib argparse as already satisfying
install_requires.append('argparse')
install_requires.extend([
# only some distros recognize stdlib argparse as already satisfying
'argparse',
'mock<1.1.0',
])
else:
install_requires.append('mock')
dev_extras = [
# Pin astroid==1.3.5, pylint==1.4.2 as a workaround for #289

View file

@ -24,7 +24,6 @@ common() {
common --domains le1.wtf auth
common --domains le2.wtf run
common -a manual -d le.wtf auth
common -a manual -d le.wtf --no-simple-http-tls auth
export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \
OPENSSL_CNF=examples/openssl.cnf

26
tests/mac-bootstrap.sh Executable file
View file

@ -0,0 +1,26 @@
#!/bin/sh
#Check Homebrew
if ! hash brew 2>/dev/null; then
echo "Homebrew Not Installed\nDownloading..."
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
fi
brew install libtool mariadb rabbitmq coreutils go
mysql.server start
rabbit_pid=`ps | grep rabbitmq | grep -v grep | awk '{ print $1}'`
if [ -n "$rabbit_pid" ]; then
echo "RabbitMQ already running"
else
rabbitmq-server &
fi
hosts_entry=`cat /etc/hosts | grep "127.0.0.1 le.wtf"`
if [ -z "$hosts_entry" ]; then
echo "Adding hosts entry for le.wtf..."
sudo sh -c "echo 127.0.0.1 le.wtf >> /etc/hosts"
fi
./tests/boulder-start.sh

View file

@ -70,7 +70,7 @@ echo "Testing packages"
cd "dist.$version"
# start local PyPI
python -m SimpleHTTPServer $PORT &
# cd .. is NOT done on purpose: we make sure that all subpacakges are
# cd .. is NOT done on purpose: we make sure that all subpackages are
# installed from local PyPI rather than current directory (repo root)
virtualenv --no-site-packages ../venv
. ../venv/bin/activate
@ -82,15 +82,16 @@ pip install \
# stop local PyPI
kill $!
# freeze before installing anythin else, so that we know end-user KGS
mkdir kgs
kgs="kgs/$version"
# freeze before installing anything else, so that we know end-user KGS
# make sure "twine upload" doesn't catch "kgs"
mkdir ../kgs
kgs="../kgs/$version"
pip freeze | tee $kgs
pip install nose
# TODO: letsencrypt_apache fails due to symlink, c.f. #838
nosetests letsencrypt $SUBPKGS || true
echo "New root: $root"
echo "KGS is at $root/$kgs"
echo "KGS is at $root/kgs"
echo "In order to upload packages run the following command:"
echo twine upload "$root/dist.$version/*/*"

View file

@ -16,7 +16,7 @@ fi
cover () {
if [ "$1" = "letsencrypt" ]; then
min=97
min=98
elif [ "$1" = "acme" ]; then
min=100
elif [ "$1" = "letsencrypt_apache" ]; then