diff options
author | Peter Eckersley <pde@eff.org> | 2015-10-08 21:32:19 +0300 |
---|---|---|
committer | Peter Eckersley <pde@eff.org> | 2015-10-08 21:32:19 +0300 |
commit | 53d532cfe39873d5fb0700e61822b7c47eb00c85 (patch) | |
tree | 34e99b656442f4e6277ce50f0ff8eab2ebd38869 | |
parent | 0ecfec56e26f40333879d17ec8755ae75b7798f2 (diff) | |
parent | 52a45d158ab2dbc01956b4b772ce5884c1297931 (diff) |
Merge branch 'master' of ssh://github.com/letsencrypt/letsencrypt
33 files changed, 321 insertions, 163 deletions
diff --git a/MANIFEST.in b/MANIFEST.in index 80fd8777e..e421e0cd7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -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 * diff --git a/acme/acme/jose/interfaces.py b/acme/acme/jose/interfaces.py index f841848b3..f85777a30 100644 --- a/acme/acme/jose/interfaces.py +++ b/acme/acme/jose/interfaces.py @@ -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): diff --git a/acme/acme/jose/interfaces_test.py b/acme/acme/jose/interfaces_test.py index 380c3a2a5..91e6f4416 100644 --- a/acme/acme/jose/interfaces_test.py +++ b/acme/acme/jose/interfaces_test.py @@ -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 diff --git a/acme/acme/jose/jwk.py b/acme/acme/jose/jwk.py index 7a976f189..74fa72319 100644 --- a/acme/acme/jose/jwk.py +++ b/acme/acme/jose/jwk.py @@ -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) diff --git a/acme/acme/jose/jwk_test.py b/acme/acme/jose/jwk_test.py index 5462af6b0..d8a7410e8 100644 --- a/acme/acme/jose/jwk_test.py +++ b/acme/acme/jose/jwk_test.py @@ -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( diff --git a/acme/setup.py b/acme/setup.py index 2a3a123c5..6448b7fe9 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -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', diff --git a/bootstrap/archlinux.sh b/bootstrap/archlinux.sh index fbe0987fe..6de7c23d4 100755 --- a/bootstrap/archlinux.sh +++ b/bootstrap/archlinux.sh @@ -1,2 +1,15 @@ #!/bin/sh -pacman -S git python2 python2-virtualenv gcc dialog augeas openssl libffi ca-certificates
\ No newline at end of file + +# "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 \ diff --git a/bootstrap/dev/README b/bootstrap/dev/README new file mode 100644 index 000000000..759496187 --- /dev/null +++ b/bootstrap/dev/README @@ -0,0 +1 @@ +This directory contains developer setup. diff --git a/bootstrap/dev/_venv_common.sh b/bootstrap/dev/_venv_common.sh new file mode 100755 index 000000000..2d84dc39b --- /dev/null +++ b/bootstrap/dev/_venv_common.sh @@ -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" diff --git a/bootstrap/dev/venv.sh b/bootstrap/dev/venv.sh new file mode 100755 index 000000000..d6cf95bb5 --- /dev/null +++ b/bootstrap/dev/venv.sh @@ -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 diff --git a/bootstrap/dev/venv3.sh b/bootstrap/dev/venv3.sh new file mode 100755 index 000000000..ccffffb83 --- /dev/null +++ b/bootstrap/dev/venv3.sh @@ -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] \ diff --git a/docs/conf.py b/docs/conf.py index 2b4b2cd43..e2b360a6e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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 diff --git a/docs/contributing.rst b/docs/contributing.rst index c6443e3b2..3277d321a 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -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: -.. code-block:: shell +Bootstrap +--------- - source ./venv/bin/activate +Start by :ref:`installing Let's Encrypt prerequisites +<prerequisites>`. Then run: -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 -documentation`_. +.. code-block:: shell -Install the development packages: + ./bootstrap/dev/venv.sh -.. code-block:: shell +Activate the virtualenv: - pip install -r requirements.txt -e acme -e .[dev,docs,testing] -e letsencrypt-apache -e letsencrypt-nginx -e letshelp-letsencrypt +.. code-block:: shell -.. 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. + source ./$VENV_NAME/bin/activate - This is roughly equivalent to `python setup.py develop`. For - more info see `man pip`. +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`_. -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 diff --git a/docs/using.rst b/docs/using.rst index cfce29bae..9611f37c0 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -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 diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py index ee1457131..626e700b2 100644 --- a/letsencrypt-apache/setup.py +++ b/letsencrypt-apache/setup.py @@ -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, diff --git a/letsencrypt-compatibility-test/MANIFEST.in b/letsencrypt-compatibility-test/MANIFEST.in index 52bbb3c65..4d346a5d0 100644 --- a/letsencrypt-compatibility-test/MANIFEST.in +++ b/letsencrypt-compatibility-test/MANIFEST.in @@ -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 * diff --git a/letsencrypt-compatibility-test/setup.py b/letsencrypt-compatibility-test/setup.py index 745b49bb5..2e70fd1d7 100644 --- a/letsencrypt-compatibility-test/setup.py +++ b/letsencrypt-compatibility-test/setup.py @@ -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, diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py index 4e770c8cb..a37b8222b 100644 --- a/letsencrypt-nginx/setup.py +++ b/letsencrypt-nginx/setup.py @@ -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, diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 73dd24bdb..64cba508d 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -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): diff --git a/letsencrypt/client.py b/letsencrypt/client.py index c82131af3..7a78add38 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -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( diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index 777b4d006..61aa8b0db 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -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. diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 1f51645ab..5e82d61aa 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -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.") diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index 3f7276725..9d5ef87e9 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -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"), diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py index cfe47b833..8cfff1cc5 100644 --- a/letsencrypt/plugins/manual_test.py +++ b/letsencrypt/plugins/manual_test.py @@ -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) diff --git a/letsencrypt/storage.py b/letsencrypt/storage.py index be270a762..8a0f4829e 100644 --- a/letsencrypt/storage.py +++ b/letsencrypt/storage.py @@ -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 diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 0a92aba62..d0fae370d 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -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) diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 1a232bccb..1e63bdbb6 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -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) diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index e67631605..6f115abf9 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -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 @@ -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 diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index ed877d136..25db8ba6d 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -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 diff --git a/tests/mac-bootstrap.sh b/tests/mac-bootstrap.sh new file mode 100755 index 000000000..66036ce56 --- /dev/null +++ b/tests/mac-bootstrap.sh @@ -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 diff --git a/tools/dev-release.sh b/tools/dev-release.sh index 06f49f0a5..d93a6d21f 100755 --- a/tools/dev-release.sh +++ b/tools/dev-release.sh @@ -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/*/*" diff --git a/tox.cover.sh b/tox.cover.sh index edfd9b81a..8418de9a8 100755 --- a/tox.cover.sh +++ b/tox.cover.sh @@ -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 |