diff --git a/.travis.yml b/.travis.yml index 8586c3840..96e28b1b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,12 @@ language: python services: - rabbitmq - - mysql + - mariadb # http://docs.travis-ci.com/user/ci-environment/#CI-environment-OS # gimme has to be kept in sync with Boulder's Go version setting in .travis.yml before_install: + - 'dpkg -s libaugeas0' - '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || eval "$(gimme 1.5.1)"' # using separate envs with different TOXENVs creates 4x1 Travis build @@ -31,13 +32,17 @@ branches: - master - /^test-.*$/ -sudo: false # containers +# container-based infrastructure +sudo: false + addons: # make sure simplehttp simple verification works (custom /etc/hosts) hosts: - le.wtf mariadb: "10.0" apt: + sources: + - augeas packages: # keep in sync with bootstrap/ubuntu.sh and Boulder - python - python-dev diff --git a/Dockerfile b/Dockerfile index b9ea168de..02aa0f0d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,8 @@ WORKDIR /opt/letsencrypt # If doesn't exist, it is created along with all missing # directories in its path. -COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/ + +COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/ubuntu.sh RUN /opt/letsencrypt/src/ubuntu.sh && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* \ @@ -48,7 +49,7 @@ COPY letsencrypt-apache /opt/letsencrypt/src/letsencrypt-apache/ COPY letsencrypt-nginx /opt/letsencrypt/src/letsencrypt-nginx/ -# requirements.txt not installed! +# py26reqs.txt not installed! RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt/venv && \ /opt/letsencrypt/venv/bin/pip install \ -e /opt/letsencrypt/src/acme \ diff --git a/Dockerfile-dev b/Dockerfile-dev index daa8e9af0..838b60e8b 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -22,7 +22,7 @@ WORKDIR /opt/letsencrypt # TODO: Install non-default Python versions for tox. # TODO: Install Apache/Nginx for plugin development. -COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/ +COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/ubuntu.sh RUN /opt/letsencrypt/src/ubuntu.sh && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* \ @@ -32,7 +32,8 @@ RUN /opt/letsencrypt/src/ubuntu.sh && \ # the above is not likely to change, so by putting it further up the # Dockerfile we make sure we cache as much as possible -COPY setup.py README.rst CHANGES.rst MANIFEST.in requirements.txt DISCLAIMER linter_plugin.py tox.cover.sh tox.ini pep8.travis.sh .pep8 .pylintrc /opt/letsencrypt/src/ +# py26reqs.txt not installed! +COPY setup.py README.rst CHANGES.rst MANIFEST.in DISCLAIMER linter_plugin.py tox.cover.sh tox.ini pep8.travis.sh .pep8 .pylintrc /opt/letsencrypt/src/ # all above files are necessary for setup.py, however, package source # code directory has to be copied separately to a subdirectory... @@ -52,7 +53,6 @@ COPY tests /opt/letsencrypt/src/tests/ RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt/venv && \ /opt/letsencrypt/venv/bin/pip install \ - -r /opt/letsencrypt/src/requirements.txt \ -e /opt/letsencrypt/src/acme \ -e /opt/letsencrypt/src \ -e /opt/letsencrypt/src/letsencrypt-apache \ diff --git a/MANIFEST.in b/MANIFEST.in index fa0d16e0d..5d5b0bed4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include requirements.txt +include py26reqs.txt include README.rst include CHANGES.rst include CONTRIBUTING.md diff --git a/README.rst b/README.rst index d3e89c939..ce0d1b686 100644 --- a/README.rst +++ b/README.rst @@ -35,11 +35,11 @@ It's all automated: All you need to do to sign a single domain is:: - user@www:~$ sudo letsencrypt -d www.example.org auth + user@www:~$ sudo letsencrypt -d www.example.org certonly For multiple domains (SAN) use:: - user@www:~$ sudo letsencrypt -d www.example.org -d example.org auth + user@www:~$ sudo letsencrypt -d www.example.org -d example.org certonly and if you have a compatible web server (Apache or Nginx), Let's Encrypt can not only get a new certificate, but also deploy it and configure your diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index c5855a7ca..976d7ab12 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -187,7 +187,7 @@ class KeyAuthorizationChallenge(_TokenDVChallenge): key_authorization=self.key_authorization(account_key)) @abc.abstractmethod - def validation(self, account_key): + def validation(self, account_key, **kwargs): """Generate validation for the challenge. Subclasses must implement this method, but they are likely to @@ -201,7 +201,7 @@ class KeyAuthorizationChallenge(_TokenDVChallenge): """ raise NotImplementedError() # pragma: no cover - def response_and_validation(self, account_key): + def response_and_validation(self, account_key, *args, **kwargs): """Generate response and validation. Convenience function that return results of `response` and @@ -211,7 +211,8 @@ class KeyAuthorizationChallenge(_TokenDVChallenge): :rtype: tuple """ - return (self.response(account_key), self.validation(account_key)) + return (self.response(account_key), + self.validation(account_key, *args, **kwargs)) @ChallengeResponse.register @@ -220,6 +221,12 @@ class HTTP01Response(KeyAuthorizationChallengeResponse): typ = "http-01" PORT = 80 + """Verification port as defined by the protocol. + + You can override it (e.g. for testing) by passing ``port`` to + `simple_verify`. + + """ def simple_verify(self, chall, domain, account_public_key, port=None): """Simple verify. @@ -246,7 +253,7 @@ class HTTP01Response(KeyAuthorizationChallengeResponse): # request URI, if it's standard. if port is not None and port != self.PORT: logger.warning( - "Using non-standard port for SimpleHTTP verification: %s", port) + "Using non-standard port for http-01 verification: %s", port) domain += ":{0}".format(port) uri = chall.uri(domain) @@ -308,7 +315,7 @@ class HTTP01(KeyAuthorizationChallenge): """ return "http://" + domain + self.path - def validation(self, account_key): + def validation(self, account_key, **unused_kwargs): """Generate validation. :param JWK account_key: @@ -318,89 +325,50 @@ class HTTP01(KeyAuthorizationChallenge): return self.key_authorization(account_key) -@Challenge.register # pylint: disable=too-many-ancestors -class DVSNI(_TokenDVChallenge): - """ACME "dvsni" challenge. - - :ivar bytes token: Random data, **not** base64-encoded. - - """ - typ = "dvsni" - - PORT = 443 - """Port to perform DVSNI challenge.""" - - def gen_response(self, account_key, alg=jose.RS256, **kwargs): - """Generate response. - - :param .JWK account_key: Private account key. - :rtype: .DVSNIResponse - - """ - return DVSNIResponse(validation=jose.JWS.sign( - payload=self.json_dumps(sort_keys=True).encode('utf-8'), - key=account_key, alg=alg, **kwargs)) - - @ChallengeResponse.register -class DVSNIResponse(ChallengeResponse): - """ACME "dvsni" challenge response. - - :param bytes s: Random data, **not** base64-encoded. - - """ - typ = "dvsni" +class TLSSNI01Response(KeyAuthorizationChallengeResponse): + """ACME tls-sni-01 challenge response.""" + typ = "tls-sni-01" DOMAIN_SUFFIX = b".acme.invalid" """Domain name suffix.""" - PORT = DVSNI.PORT - """Port to perform DVSNI challenge.""" + PORT = 443 + """Verification port as defined by the protocol. - validation = jose.Field("validation", decoder=jose.JWS.from_json) + You can override it (e.g. for testing) by passing ``port`` to + `simple_verify`. + + """ @property - def z(self): # pylint: disable=invalid-name - """The ``z`` parameter. + def z(self): + """``z`` value used for verification. - :rtype: bytes + :rtype bytes: """ - # Instance of 'Field' has no 'signature' member - # pylint: disable=no-member - return hashlib.sha256(self.validation.signature.encode( - "signature").encode("utf-8")).hexdigest().encode() + return hashlib.sha256( + self.key_authorization.encode("utf-8")).hexdigest().lower().encode() @property def z_domain(self): - """Domain name for certificate subjectAltName. + """Domain name used for verification, generated from `z`. - :rtype: bytes + :rtype bytes: """ - z = self.z # pylint: disable=invalid-name - return z[:32] + b'.' + z[32:] + self.DOMAIN_SUFFIX - - @property - def chall(self): - """Get challenge encoded in the `validation` payload. - - :rtype: challenges.DVSNI - - """ - # pylint: disable=no-member - return DVSNI.json_loads(self.validation.payload.decode('utf-8')) + return self.z[:32] + b'.' + self.z[32:] + self.DOMAIN_SUFFIX def gen_cert(self, key=None, bits=2048): - """Generate DVSNI certificate. + """Generate tls-sni-01 certificate. :param OpenSSL.crypto.PKey key: Optional private key used in certificate generation. If not provided (``None``), then fresh key will be generated. :param int bits: Number of bits for newly generated key. - :rtype: `tuple` of `OpenSSL.crypto.X509` and - `OpenSSL.crypto.PKey` + :rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey` """ if key is None: @@ -411,11 +379,12 @@ class DVSNIResponse(ChallengeResponse): 'dummy', self.z_domain.decode()], force_san=True), key def probe_cert(self, domain, **kwargs): - """Probe DVSNI challenge certificate. + """Probe tls-sni-01 challenge certificate. :param unicode domain: """ + # TODO: domain is not necessary if host is provided if "host" not in kwargs: host = socket.gethostbyname(domain) logging.debug('%s resolved to %s', domain, host) @@ -428,7 +397,7 @@ class DVSNIResponse(ChallengeResponse): return crypto_util.probe_sni(**kwargs) def verify_cert(self, cert): - """Verify DVSNI challenge certificate.""" + """Verify tls-sni-01 challenge certificate.""" # pylint: disable=protected-access sans = crypto_util._pyopenssl_cert_or_req_san(cert) logging.debug('Certificate %s. SANs: %s', cert.digest('sha1'), sans) @@ -439,14 +408,15 @@ class DVSNIResponse(ChallengeResponse): """Simple verify. Verify ``validation`` using ``account_public_key``, optionally - probe DVSNI certificate and check using `verify_cert`. + probe tls-sni-01 certificate and check using `verify_cert`. - :param .challenges.DVSNI chall: Corresponding challenge. + :param .challenges.TLSSNI01 chall: Corresponding challenge. :param str domain: Domain name being validated. :param JWK account_public_key: :param OpenSSL.crypto.X509 cert: Optional certificate. If not provided (``None``) certificate will be retrieved using `probe_cert`. + :param int port: Port used to probe the certificate. :returns: ``True`` iff client's control of the domain has been @@ -454,20 +424,8 @@ class DVSNIResponse(ChallengeResponse): :rtype: bool """ - # pylint: disable=no-member - if not self.validation.verify(key=account_public_key): - return False - - # TODO: it's not checked that payload has exectly 2 fields! - try: - decoded_chall = self.chall - except jose.DeserializationError as error: - logger.debug(error, exc_info=True) - return False - - if decoded_chall.token != chall.token: - logger.debug("Wrong token: expected %r, found %r", - chall.token, decoded_chall.token) + if not self.verify(chall, account_public_key): + logger.debug("Verification of key authorization in response failed") return False if cert is None: @@ -480,6 +438,29 @@ class DVSNIResponse(ChallengeResponse): return self.verify_cert(cert) +@Challenge.register # pylint: disable=too-many-ancestors +class TLSSNI01(KeyAuthorizationChallenge): + """ACME tls-sni-01 challenge.""" + response_cls = TLSSNI01Response + typ = response_cls.typ + + # boulder#962, ietf-wg-acme#22 + #n = jose.Field("n", encoder=int, decoder=int) + + def validation(self, account_key, **kwargs): + """Generate validation. + + :param JWK account_key: + :param OpenSSL.crypto.PKey cert_key: Optional private key used + in certificate generation. If not provided (``None``), then + fresh key will be generated. + + :rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey` + + """ + return self.response(account_key).gen_cert(key=kwargs.get('cert_key')) + + @Challenge.register class RecoveryContact(ContinuityChallenge): """ACME "recoveryContact" challenge. diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 0708f3782..c4f3d6c61 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -186,14 +186,112 @@ class HTTP01Test(unittest.TestCase): self.msg.update(token=b'..').good_token) -class DVSNITest(unittest.TestCase): +class TLSSNI01ResponseTest(unittest.TestCase): + # pylint: disable=too-many-instance-attributes def setUp(self): - from acme.challenges import DVSNI - self.msg = DVSNI( + from acme.challenges import TLSSNI01 + self.chall = TLSSNI01( + token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e')) + + self.response = self.chall.response(KEY) + self.jmsg = { + 'resource': 'challenge', + 'type': 'tls-sni-01', + 'keyAuthorization': self.response.key_authorization, + } + + # pylint: disable=invalid-name + label1 = b'dc38d9c3fa1a4fdcc3a5501f2d38583f' + label2 = b'b7793728f084394f2a1afd459556bb5c' + self.z = label1 + label2 + self.z_domain = label1 + b'.' + label2 + b'.acme.invalid' + self.domain = 'foo.com' + + def test_z_and_domain(self): + self.assertEqual(self.z, self.response.z) + self.assertEqual(self.z_domain, self.response.z_domain) + + def test_to_partial_json(self): + self.assertEqual(self.jmsg, self.response.to_partial_json()) + + def test_from_json(self): + from acme.challenges import TLSSNI01Response + self.assertEqual(self.response, TLSSNI01Response.from_json(self.jmsg)) + + def test_from_json_hashable(self): + from acme.challenges import TLSSNI01Response + hash(TLSSNI01Response.from_json(self.jmsg)) + + @mock.patch('acme.challenges.socket.gethostbyname') + @mock.patch('acme.challenges.crypto_util.probe_sni') + def test_probe_cert(self, mock_probe_sni, mock_gethostbyname): + mock_gethostbyname.return_value = '127.0.0.1' + self.response.probe_cert('foo.com') + mock_gethostbyname.assert_called_once_with('foo.com') + mock_probe_sni.assert_called_once_with( + host='127.0.0.1', port=self.response.PORT, + name=self.z_domain) + + self.response.probe_cert('foo.com', host='8.8.8.8') + mock_probe_sni.assert_called_with( + host='8.8.8.8', port=mock.ANY, name=mock.ANY) + + self.response.probe_cert('foo.com', port=1234) + mock_probe_sni.assert_called_with( + host=mock.ANY, port=1234, name=mock.ANY) + + self.response.probe_cert('foo.com', bar='baz') + mock_probe_sni.assert_called_with( + host=mock.ANY, port=mock.ANY, name=mock.ANY, bar='baz') + + self.response.probe_cert('foo.com', name=b'xxx') + mock_probe_sni.assert_called_with( + host=mock.ANY, port=mock.ANY, + name=self.z_domain) + + def test_gen_verify_cert(self): + key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem') + cert, key2 = self.response.gen_cert(key1) + self.assertEqual(key1, key2) + self.assertTrue(self.response.verify_cert(cert)) + + def test_gen_verify_cert_gen_key(self): + cert, key = self.response.gen_cert() + self.assertTrue(isinstance(key, OpenSSL.crypto.PKey)) + self.assertTrue(self.response.verify_cert(cert)) + + def test_verify_bad_cert(self): + self.assertFalse(self.response.verify_cert( + test_util.load_cert('cert.pem'))) + + def test_simple_verify_bad_key_authorization(self): + key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) + self.response.simple_verify(self.chall, "local", key2.public_key()) + + @mock.patch('acme.challenges.TLSSNI01Response.verify_cert', autospec=True) + def test_simple_verify(self, mock_verify_cert): + mock_verify_cert.return_value = mock.sentinel.verification + self.assertEqual(mock.sentinel.verification, self.response.simple_verify( + self.chall, self.domain, KEY.public_key(), + cert=mock.sentinel.cert)) + mock_verify_cert.assert_called_once_with(self.response, mock.sentinel.cert) + + @mock.patch('acme.challenges.TLSSNI01Response.probe_cert') + def test_simple_verify_false_on_probe_error(self, mock_probe_cert): + mock_probe_cert.side_effect = errors.Error + self.assertFalse(self.response.simple_verify( + self.chall, self.domain, KEY.public_key())) + + +class TLSSNI01Test(unittest.TestCase): + + def setUp(self): + from acme.challenges import TLSSNI01 + self.msg = TLSSNI01( token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e')) self.jmsg = { - 'type': 'dvsni', + 'type': 'tls-sni-01', 'token': 'a82d5ff8ef740d12881f6d3c2277ab2e', } @@ -201,144 +299,25 @@ class DVSNITest(unittest.TestCase): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): - from acme.challenges import DVSNI - self.assertEqual(self.msg, DVSNI.from_json(self.jmsg)) + from acme.challenges import TLSSNI01 + self.assertEqual(self.msg, TLSSNI01.from_json(self.jmsg)) def test_from_json_hashable(self): - from acme.challenges import DVSNI - hash(DVSNI.from_json(self.jmsg)) + from acme.challenges import TLSSNI01 + hash(TLSSNI01.from_json(self.jmsg)) def test_from_json_invalid_token_length(self): - from acme.challenges import DVSNI + from acme.challenges import TLSSNI01 self.jmsg['token'] = jose.encode_b64jose(b'abcd') self.assertRaises( - jose.DeserializationError, DVSNI.from_json, self.jmsg) + jose.DeserializationError, TLSSNI01.from_json, self.jmsg) - def test_gen_response(self): - from acme.challenges import DVSNI - self.assertEqual(self.msg, DVSNI.json_loads( - self.msg.gen_response(KEY).validation.payload.decode())) - - -class DVSNIResponseTest(unittest.TestCase): - # pylint: disable=too-many-instance-attributes - - def setUp(self): - from acme.challenges import DVSNI - self.chall = DVSNI( - token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e')) - - from acme.challenges import DVSNIResponse - self.validation = jose.JWS.sign( - payload=self.chall.json_dumps(sort_keys=True).encode(), - key=KEY, alg=jose.RS256) - self.msg = DVSNIResponse(validation=self.validation) - self.jmsg_to = { - 'resource': 'challenge', - 'type': 'dvsni', - 'validation': self.validation, - } - self.jmsg_from = { - 'resource': 'challenge', - 'type': 'dvsni', - 'validation': self.validation.to_json(), - } - - # pylint: disable=invalid-name - label1 = b'e2df3498860637c667fedadc5a8494ec' - label2 = b'09dcc75553c9b3bd73662b50e71b1e42' - self.z = label1 + label2 - self.z_domain = label1 + b'.' + label2 + b'.acme.invalid' - self.domain = 'foo.com' - - def test_z_and_domain(self): - self.assertEqual(self.z, self.msg.z) - self.assertEqual(self.z_domain, self.msg.z_domain) - - def test_to_partial_json(self): - self.assertEqual(self.jmsg_to, self.msg.to_partial_json()) - - def test_from_json(self): - from acme.challenges import DVSNIResponse - self.assertEqual(self.msg, DVSNIResponse.from_json(self.jmsg_from)) - - def test_from_json_hashable(self): - from acme.challenges import DVSNIResponse - hash(DVSNIResponse.from_json(self.jmsg_from)) - - @mock.patch('acme.challenges.socket.gethostbyname') - @mock.patch('acme.challenges.crypto_util.probe_sni') - def test_probe_cert(self, mock_probe_sni, mock_gethostbyname): - mock_gethostbyname.return_value = '127.0.0.1' - self.msg.probe_cert('foo.com') - mock_gethostbyname.assert_called_once_with('foo.com') - mock_probe_sni.assert_called_once_with( - host='127.0.0.1', port=self.msg.PORT, - name=self.z_domain) - - self.msg.probe_cert('foo.com', host='8.8.8.8') - mock_probe_sni.assert_called_with( - host='8.8.8.8', port=mock.ANY, name=mock.ANY) - - self.msg.probe_cert('foo.com', port=1234) - mock_probe_sni.assert_called_with( - host=mock.ANY, port=1234, name=mock.ANY) - - self.msg.probe_cert('foo.com', bar='baz') - mock_probe_sni.assert_called_with( - host=mock.ANY, port=mock.ANY, name=mock.ANY, bar='baz') - - self.msg.probe_cert('foo.com', name=b'xxx') - mock_probe_sni.assert_called_with( - host=mock.ANY, port=mock.ANY, - name=self.z_domain) - - def test_gen_verify_cert(self): - key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem') - cert, key2 = self.msg.gen_cert(key1) - self.assertEqual(key1, key2) - self.assertTrue(self.msg.verify_cert(cert)) - - def test_gen_verify_cert_gen_key(self): - cert, key = self.msg.gen_cert() - self.assertTrue(isinstance(key, OpenSSL.crypto.PKey)) - self.assertTrue(self.msg.verify_cert(cert)) - - def test_verify_bad_cert(self): - self.assertFalse(self.msg.verify_cert(test_util.load_cert('cert.pem'))) - - def test_simple_verify_wrong_account_key(self): - self.assertFalse(self.msg.simple_verify( - self.chall, self.domain, jose.JWKRSA.load( - test_util.load_vector('rsa256_key.pem')).public_key())) - - def test_simple_verify_wrong_payload(self): - for payload in b'', b'{}': - msg = self.msg.update(validation=jose.JWS.sign( - payload=payload, key=KEY, alg=jose.RS256)) - self.assertFalse(msg.simple_verify( - self.chall, self.domain, KEY.public_key())) - - def test_simple_verify_wrong_token(self): - msg = self.msg.update(validation=jose.JWS.sign( - payload=self.chall.update(token=(b'b' * 20)).json_dumps().encode(), - key=KEY, alg=jose.RS256)) - self.assertFalse(msg.simple_verify( - self.chall, self.domain, KEY.public_key())) - - @mock.patch('acme.challenges.DVSNIResponse.verify_cert', autospec=True) - def test_simple_verify(self, mock_verify_cert): - mock_verify_cert.return_value = mock.sentinel.verification - self.assertEqual(mock.sentinel.verification, self.msg.simple_verify( - self.chall, self.domain, KEY.public_key(), - cert=mock.sentinel.cert)) - mock_verify_cert.assert_called_once_with(self.msg, mock.sentinel.cert) - - @mock.patch('acme.challenges.DVSNIResponse.probe_cert') - def test_simple_verify_false_on_probe_error(self, mock_probe_cert): - mock_probe_cert.side_effect = errors.Error - self.assertFalse(self.msg.simple_verify( - self.chall, self.domain, KEY.public_key())) + @mock.patch('acme.challenges.TLSSNI01Response.gen_cert') + def test_validation(self, mock_gen_cert): + mock_gen_cert.return_value = ('cert', 'key') + self.assertEqual(('cert', 'key'), self.msg.validation( + KEY, cert_key=mock.sentinel.cert_key)) + mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key) class RecoveryContactTest(unittest.TestCase): @@ -571,8 +550,6 @@ class ProofOfPossessionResponseTest(unittest.TestCase): class DNSTest(unittest.TestCase): def setUp(self): - self.account_key = jose.JWKRSA.load( - test_util.load_vector('rsa512_key.pem')) from acme.challenges import DNS self.msg = DNS(token=jose.b64decode( b'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA')) @@ -594,34 +571,33 @@ class DNSTest(unittest.TestCase): def test_gen_check_validation(self): self.assertTrue(self.msg.check_validation( - self.msg.gen_validation(self.account_key), - self.account_key.public_key())) + self.msg.gen_validation(KEY), KEY.public_key())) def test_gen_check_validation_wrong_key(self): key2 = jose.JWKRSA.load(test_util.load_vector('rsa1024_key.pem')) self.assertFalse(self.msg.check_validation( - self.msg.gen_validation(self.account_key), key2.public_key())) + self.msg.gen_validation(KEY), key2.public_key())) def test_check_validation_wrong_payload(self): validations = tuple( - jose.JWS.sign(payload=payload, alg=jose.RS256, key=self.account_key) + jose.JWS.sign(payload=payload, alg=jose.RS256, key=KEY) for payload in (b'', b'{}') ) for validation in validations: self.assertFalse(self.msg.check_validation( - validation, self.account_key.public_key())) + validation, KEY.public_key())) def test_check_validation_wrong_fields(self): bad_validation = jose.JWS.sign( payload=self.msg.update(token=b'x' * 20).json_dumps().encode('utf-8'), - alg=jose.RS256, key=self.account_key) + alg=jose.RS256, key=KEY) self.assertFalse(self.msg.check_validation( - bad_validation, self.account_key.public_key())) + bad_validation, KEY.public_key())) def test_gen_response(self): with mock.patch('acme.challenges.DNS.gen_validation') as mock_gen: mock_gen.return_value = mock.sentinel.validation - response = self.msg.gen_response(self.account_key) + response = self.msg.gen_response(KEY) from acme.challenges import DNSResponse self.assertTrue(isinstance(response, DNSResponse)) self.assertEqual(response.validation, mock.sentinel.validation) diff --git a/acme/acme/client.py b/acme/acme/client.py index 4c89458fb..0e9319f9c 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -481,11 +481,13 @@ class ClientNetwork(object): JSON_ERROR_CONTENT_TYPE = 'application/problem+json' REPLAY_NONCE_HEADER = 'Replay-Nonce' - def __init__(self, key, alg=jose.RS256, verify_ssl=True): + def __init__(self, key, alg=jose.RS256, verify_ssl=True, + user_agent='acme-python'): self.key = key self.alg = alg self.verify_ssl = verify_ssl self._nonces = set() + self.user_agent = user_agent def _wrap_in_jws(self, obj, nonce): """Wrap `JSONDeSerializable` object in JWS. @@ -578,6 +580,8 @@ class ClientNetwork(object): logging.debug('Sending %s request to %s. args: %r, kwargs: %r', method, url, args, kwargs) kwargs['verify'] = self.verify_ssl + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('User-Agent', self.user_agent) response = requests.request(method, url, *args, **kwargs) logging.debug('Received %s. Headers: %s. Content: %r', response, response.headers, response.content) diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index 7e895218c..2df7b5313 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -396,7 +396,8 @@ class ClientNetworkTest(unittest.TestCase): from acme.client import ClientNetwork self.net = ClientNetwork( - key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl) + key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl, + user_agent='acme-python-test') self.response = mock.MagicMock(ok=True, status_code=http_client.OK) self.response.headers = {} @@ -479,7 +480,7 @@ class ClientNetworkTest(unittest.TestCase): self.assertEqual(self.response, self.net._send_request( 'HEAD', 'url', 'foo', bar='baz')) mock_requests.request.assert_called_once_with( - 'HEAD', 'url', 'foo', verify=mock.ANY, bar='baz') + 'HEAD', 'url', 'foo', verify=mock.ANY, bar='baz', headers=mock.ANY) @mock.patch('acme.client.requests') def test_send_request_verify_ssl(self, mock_requests): @@ -492,7 +493,20 @@ class ClientNetworkTest(unittest.TestCase): self.assertEqual( self.response, self.net._send_request('GET', 'url')) mock_requests.request.assert_called_once_with( - 'GET', 'url', verify=verify) + 'GET', 'url', verify=verify, headers=mock.ANY) + + @mock.patch('acme.client.requests') + def test_send_request_user_agent(self, mock_requests): + mock_requests.request.return_value = self.response + # pylint: disable=protected-access + self.net._send_request('GET', 'url', headers={'bar': 'baz'}) + mock_requests.request.assert_called_once_with( + 'GET', 'url', verify=mock.ANY, + headers={'User-Agent': 'acme-python-test', 'bar': 'baz'}) + + self.net._send_request('GET', 'url', headers={'User-Agent': 'foo2'}) + mock_requests.request.assert_called_with( + 'GET', 'url', verify=mock.ANY, headers={'User-Agent': 'foo2'}) @mock.patch('acme.client.requests') def test_requests_error_passthrough(self, mock_requests): diff --git a/acme/acme/crypto_util.py b/acme/acme/crypto_util.py index 5f24e9d9e..72a93141a 100644 --- a/acme/acme/crypto_util.py +++ b/acme/acme/crypto_util.py @@ -13,7 +13,7 @@ from acme import errors logger = logging.getLogger(__name__) -# DVSNI certificate serving and probing is not affected by SSL +# TLSSNI01 certificate serving and probing is not affected by SSL # vulnerabilities: prober needs to check certificate for expected # contents anyway. Working SNI is the only thing that's necessary for # the challenge and thus scoping down SSL/TLS method (version) would @@ -23,7 +23,7 @@ logger = logging.getLogger(__name__) # https://www.openssl.org/docs/ssl/SSLv23_method.html). _serve_sni # should be changed to use "set_options" to disable SSLv2 and SSLv3, # in case it's used for things other than probing/serving! -_DEFAULT_DVSNI_SSL_METHOD = OpenSSL.SSL.SSLv23_METHOD +_DEFAULT_TLSSNI01_SSL_METHOD = OpenSSL.SSL.SSLv23_METHOD class SSLSocket(object): # pylint: disable=too-few-public-methods @@ -35,7 +35,7 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods :ivar method: See `OpenSSL.SSL.Context` for allowed values. """ - def __init__(self, sock, certs, method=_DEFAULT_DVSNI_SSL_METHOD): + def __init__(self, sock, certs, method=_DEFAULT_TLSSNI01_SSL_METHOD): self.sock = sock self.certs = certs self.method = method @@ -103,7 +103,7 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods def probe_sni(name, host, port=443, timeout=300, - method=_DEFAULT_DVSNI_SSL_METHOD, source_address=('0', 0)): + method=_DEFAULT_TLSSNI01_SSL_METHOD, source_address=('0', 0)): """Probe SNI server for SSL certificate. :param bytes name: Byte string to send as the server name in the diff --git a/acme/acme/jose/jwa.py b/acme/acme/jose/jwa.py index 4ce5ca3f5..1853e0107 100644 --- a/acme/acme/jose/jwa.py +++ b/acme/acme/jose/jwa.py @@ -176,5 +176,5 @@ PS384 = JWASignature.register(_JWAPS('PS384', hashes.SHA384)) PS512 = JWASignature.register(_JWAPS('PS512', hashes.SHA512)) ES256 = JWASignature.register(_JWAES('ES256')) -ES256 = JWASignature.register(_JWAES('ES384')) -ES256 = JWASignature.register(_JWAES('ES512')) +ES384 = JWASignature.register(_JWAES('ES384')) +ES512 = JWASignature.register(_JWAES('ES512')) diff --git a/acme/acme/standalone.py b/acme/acme/standalone.py index 1466671e3..3ddb21beb 100644 --- a/acme/acme/standalone.py +++ b/acme/acme/standalone.py @@ -6,7 +6,6 @@ import logging import os import sys -import six from six.moves import BaseHTTPServer # pylint: disable=import-error from six.moves import http_client # pylint: disable=import-error from six.moves import socketserver # pylint: disable=import-error @@ -30,7 +29,7 @@ class TLSServer(socketserver.TCPServer): self.certs = kwargs.pop("certs", {}) self.method = kwargs.pop( # pylint: disable=protected-access - "method", crypto_util._DEFAULT_DVSNI_SSL_METHOD) + "method", crypto_util._DEFAULT_TLSSNI01_SSL_METHOD) self.allow_reuse_address = kwargs.pop("allow_reuse_address", True) socketserver.TCPServer.__init__(self, *args, **kwargs) @@ -50,12 +49,25 @@ class ACMEServerMixin: # pylint: disable=old-style-class allow_reuse_address = True -class DVSNIServer(TLSServer, ACMEServerMixin): - """DVSNI Server.""" +class TLSSNI01Server(TLSServer, ACMEServerMixin): + """TLSSNI01 Server.""" def __init__(self, server_address, certs): TLSServer.__init__( - self, server_address, socketserver.BaseRequestHandler, certs=certs) + self, server_address, BaseRequestHandlerWithLogging, certs=certs) + + +class BaseRequestHandlerWithLogging(socketserver.BaseRequestHandler): + """BaseRequestHandler with logging.""" + + def log_message(self, format, *args): # pylint: disable=redefined-builtin + """Log arbitrary message.""" + logger.debug("%s - - %s", self.client_address[0], format % args) + + def handle(self): + """Handle request.""" + self.log_message("Incoming request") + socketserver.BaseRequestHandler.handle(self) class HTTP01Server(BaseHTTPServer.HTTPServer, ACMEServerMixin): @@ -83,6 +95,15 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): self.simple_http_resources = kwargs.pop("simple_http_resources", set()) socketserver.BaseRequestHandler.__init__(self, *args, **kwargs) + def log_message(self, format, *args): # pylint: disable=redefined-builtin + """Log arbitrary message.""" + logger.debug("%s - - %s", self.client_address[0], format % args) + + def handle(self): + """Handle request.""" + self.log_message("Incoming request") + BaseHTTPServer.BaseHTTPRequestHandler.handle(self) + def do_GET(self): # pylint: disable=invalid-name,missing-docstring if self.path == "/": self.handle_index() @@ -109,17 +130,17 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): """Handle HTTP01 provisioned resources.""" for resource in self.simple_http_resources: if resource.chall.path == self.path: - logger.debug("Serving HTTP01 with token %r", - resource.chall.encode("token")) + self.log_message("Serving HTTP01 with token %r", + resource.chall.encode("token")) self.send_response(http_client.OK) self.send_header("Content-type", resource.chall.CONTENT_TYPE) self.end_headers() self.wfile.write(resource.validation.encode()) return else: # pylint: disable=useless-else-on-loop - logger.debug("No resources to serve") - logger.debug("%s does not correspond to any resource. ignoring", - self.path) + self.log_message("No resources to serve") + self.log_message("%s does not correspond to any resource. ignoring", + self.path) @classmethod def partial_init(cls, simple_http_resources): @@ -134,8 +155,8 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): cls, simple_http_resources=simple_http_resources) -def simple_dvsni_server(cli_args, forever=True): - """Run simple standalone DVSNI server.""" +def simple_tls_sni_01_server(cli_args, forever=True): + """Run simple standalone TLSSNI01 server.""" logging.basicConfig(level=logging.DEBUG) parser = argparse.ArgumentParser() @@ -158,9 +179,8 @@ def simple_dvsni_server(cli_args, forever=True): OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, cert_contents)) - server = DVSNIServer(('', int(args.port)), certs=certs) - six.print_("Serving at https://localhost:{0}...".format( - server.socket.getsockname()[1])) + server = TLSSNI01Server(('', int(args.port)), certs=certs) + logger.info("Serving at https://%s:%s...", *server.socket.getsockname()[:2]) if forever: # pragma: no cover server.serve_forever() else: @@ -168,4 +188,4 @@ def simple_dvsni_server(cli_args, forever=True): if __name__ == "__main__": - sys.exit(simple_dvsni_server(sys.argv)) # pragma: no cover + sys.exit(simple_tls_sni_01_server(sys.argv)) # pragma: no cover diff --git a/acme/acme/standalone_test.py b/acme/acme/standalone_test.py index 85ef6ab14..02b1f69d3 100644 --- a/acme/acme/standalone_test.py +++ b/acme/acme/standalone_test.py @@ -28,8 +28,8 @@ class TLSServerTest(unittest.TestCase): server.server_close() # pylint: disable=no-member -class DVSNIServerTest(unittest.TestCase): - """Test for acme.standalone.DVSNIServer.""" +class TLSSNI01ServerTest(unittest.TestCase): + """Test for acme.standalone.TLSSNI01Server.""" def setUp(self): self.certs = { @@ -37,8 +37,8 @@ class DVSNIServerTest(unittest.TestCase): # pylint: disable=protected-access test_util.load_cert('cert.pem')._wrapped), } - from acme.standalone import DVSNIServer - self.server = DVSNIServer(("", 0), certs=self.certs) + from acme.standalone import TLSSNI01Server + self.server = TLSSNI01Server(("", 0), certs=self.certs) # pylint: disable=no-member self.thread = threading.Thread(target=self.server.serve_forever) self.thread.start() @@ -106,8 +106,8 @@ class HTTP01ServerTest(unittest.TestCase): self.assertFalse(self._test_http01(add=False)) -class TestSimpleDVSNIServer(unittest.TestCase): - """Tests for acme.standalone.simple_dvsni_server.""" +class TestSimpleTLSSNI01Server(unittest.TestCase): + """Tests for acme.standalone.simple_tls_sni_01_server.""" def setUp(self): # mirror ../examples/standalone @@ -118,12 +118,14 @@ class TestSimpleDVSNIServer(unittest.TestCase): shutil.copy(test_util.vector_path('rsa512_key.pem'), os.path.join(localhost_dir, 'key.pem')) - from acme.standalone import simple_dvsni_server + from acme.standalone import simple_tls_sni_01_server self.port = 1234 - self.thread = threading.Thread(target=simple_dvsni_server, kwargs={ - 'cli_args': ('xxx', '--port', str(self.port)), - 'forever': False, - }) + self.thread = threading.Thread( + target=simple_tls_sni_01_server, kwargs={ + 'cli_args': ('xxx', '--port', str(self.port)), + 'forever': False, + }, + ) self.old_cwd = os.getcwd() os.chdir(self.test_cwd) self.thread.start() diff --git a/acme/docs/conf.py b/acme/docs/conf.py index 1448aaea3..55f5eee3f 100644 --- a/acme/docs/conf.py +++ b/acme/docs/conf.py @@ -227,25 +227,25 @@ htmlhelp_basename = 'acme-pythondoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + #'preamble': '', -# Latex figure (float) alignment -#'figure_align': 'htbp', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'acme-python.tex', u'acme-python Documentation', - u'Let\'s Encrypt Project', 'manual'), + (master_doc, 'acme-python.tex', u'acme-python Documentation', + u'Let\'s Encrypt Project', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -289,9 +289,9 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'acme-python', u'acme-python Documentation', - author, 'acme-python', 'One line description of project.', - 'Miscellaneous'), + (master_doc, 'acme-python', u'acme-python Documentation', + author, 'acme-python', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. diff --git a/bootstrap/README b/bootstrap/README index 6a04ac0ba..89fd8b6ba 100644 --- a/bootstrap/README +++ b/bootstrap/README @@ -2,6 +2,6 @@ This directory contains scripts that install necessary OS-specific prerequisite dependencies (see docs/using.rst). General dependencies: -- git-core: requirements.txt git+https://* +- git-core: py26reqs.txt git+https://* - ca-certificates: communication with demo ACMO server at - https://www.letsencrypt-demo.org, requirements.txt git+https://* + https://www.letsencrypt-demo.org, py26reqs.txt git+https://* diff --git a/bootstrap/_deb_common.sh b/bootstrap/_deb_common.sh index 71144ce40..4c6b91a33 100755 --- a/bootstrap/_deb_common.sh +++ b/bootstrap/_deb_common.sh @@ -44,7 +44,7 @@ apt-get install -y --no-install-recommends \ libffi-dev \ ca-certificates \ -if ! which virtualenv > /dev/null ; then +if ! command -v virtualenv > /dev/null ; then echo Failed to install a working \"virtualenv\" command, exiting exit 1 fi diff --git a/bootstrap/_rpm_common.sh b/bootstrap/_rpm_common.sh index 26b91b8c4..5aca13cd4 100755 --- a/bootstrap/_rpm_common.sh +++ b/bootstrap/_rpm_common.sh @@ -1,8 +1,8 @@ #!/bin/sh # Tested with: -# - Fedora 22 (x64) -# - Centos 7 (x64: on AWS EC2 t2.micro, DigitalOcean droplet) +# - Fedora 22, 23 (x64) +# - Centos 7 (x64: onD igitalOcean droplet) if type yum 2>/dev/null then @@ -15,15 +15,33 @@ else exit 1 fi +# Some distros and older versions of current distros use a "python27" +# instead of "python" naming convention. Try both conventions. +if ! $tool install -y \ + python \ + python-devel \ + python-virtualenv +then + if ! $tool install -y \ + python27 \ + python27-devel \ + python27-virtualenv + then + echo "Could not install Python dependencies. Aborting bootstrap!" + exit 1 + fi +fi + # "git-core" seems to be an alias for "git" in CentOS 7 (yum search fails) -$tool install -y \ - git-core \ - python \ - python-devel \ - python-virtualenv \ - gcc \ - dialog \ - augeas-libs \ - openssl-devel \ - libffi-devel \ - ca-certificates \ +if ! $tool install -y \ + git-core \ + gcc \ + dialog \ + augeas-libs \ + openssl-devel \ + libffi-devel \ + ca-certificates +then + echo "Could not install additional dependencies. Aborting bootstrap!" + exit 1 +fi diff --git a/bootstrap/_suse_common.sh b/bootstrap/_suse_common.sh new file mode 100755 index 000000000..46f9d693b --- /dev/null +++ b/bootstrap/_suse_common.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# SLE12 don't have python-virtualenv + +zypper -nq in -l git-core \ + python \ + python-devel \ + python-virtualenv \ + gcc \ + dialog \ + augeas-lenses \ + libopenssl-devel \ + libffi-devel \ + ca-certificates \ diff --git a/bootstrap/dev/venv.sh b/bootstrap/dev/venv.sh index d6cf95bb5..2bd32a89b 100755 --- a/bootstrap/dev/venv.sh +++ b/bootstrap/dev/venv.sh @@ -4,7 +4,7 @@ export VENV_ARGS="--python python2" ./bootstrap/dev/_venv_common.sh \ - -r requirements.txt \ + -r py26reqs.txt \ -e acme[testing] \ -e .[dev,docs,testing] \ -e letsencrypt-apache \ diff --git a/bootstrap/install-deps.sh b/bootstrap/install-deps.sh index 3cb0fc274..e907e7035 100755 --- a/bootstrap/install-deps.sh +++ b/bootstrap/install-deps.sh @@ -29,6 +29,9 @@ elif [ -f /etc/gentoo-release ] ; then elif uname | grep -iq FreeBSD ; then echo "Bootstrapping dependencies for FreeBSD..." $SUDO $BOOTSTRAP/freebsd.sh +elif `grep -qs openSUSE /etc/os-release` ; then + echo "Bootstrapping dependencies for openSUSE.." + $SUDO $BOOTSTRAP/suse.sh elif uname | grep -iq Darwin ; then echo "Bootstrapping dependencies for Mac OS X..." echo "WARNING: Mac support is very experimental at present..." diff --git a/bootstrap/suse.sh b/bootstrap/suse.sh new file mode 120000 index 000000000..fc4c1dee4 --- /dev/null +++ b/bootstrap/suse.sh @@ -0,0 +1 @@ +_suse_common.sh \ No newline at end of file diff --git a/bootstrap/venv.sh b/bootstrap/venv.sh index 6ca082446..ff1a50c6c 100755 --- a/bootstrap/venv.sh +++ b/bootstrap/venv.sh @@ -13,14 +13,14 @@ VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} # later steps, causing "ImportError: cannot import name unpack_url" if [ ! -d $VENV_PATH ] then - virtualenv --no-site-packages --python python2 $VENV_PATH + virtualenv --no-site-packages --python ${LE_PYTHON:-python2} $VENV_PATH fi . $VENV_PATH/bin/activate pip install -U setuptools pip install -U pip -pip install -U letsencrypt letsencrypt-apache # letsencrypt-nginx +pip install -U -r py26reqs.txt letsencrypt letsencrypt-apache # letsencrypt-nginx echo echo "Congratulations, Let's Encrypt has been successfully installed/updated!" diff --git a/docs/ciphers.rst b/docs/ciphers.rst index 12c403d09..49c0824a3 100644 --- a/docs/ciphers.rst +++ b/docs/ciphers.rst @@ -105,7 +105,7 @@ https://wiki.mozilla.org/Security/Server_Side_TLS and the version implemented by the Let's Encrypt client will be the version that was most current as of the release date of each client -version. Mozilla offers three seperate sets of cryptographic options, +version. Mozilla offers three separate sets of cryptographic options, which trade off security and compatibility differently. These are referred to as as the "Modern", "Intermediate", and "Old" configurations (in order from most secure to least secure, and least-backwards compatible diff --git a/docs/conf.py b/docs/conf.py index 62a7cea07..21bcc6817 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -230,25 +230,25 @@ htmlhelp_basename = 'LetsEncryptdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + #'preamble': '', -# Latex figure (float) alignment -#'figure_align': 'htbp', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'LetsEncrypt.tex', u'Let\'s Encrypt Documentation', - u'Let\'s Encrypt Project', 'manual'), + ('index', 'LetsEncrypt.tex', u'Let\'s Encrypt Documentation', + u'Let\'s Encrypt Project', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -295,9 +295,9 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'LetsEncrypt', u'Let\'s Encrypt Documentation', - u'Let\'s Encrypt Project', 'LetsEncrypt', 'One line description of project.', - 'Miscellaneous'), + ('index', 'LetsEncrypt', u'Let\'s Encrypt Documentation', + u'Let\'s Encrypt Project', 'LetsEncrypt', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. diff --git a/docs/contributing.rst b/docs/contributing.rst index a3baa7bc5..c71aefeec 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -151,7 +151,7 @@ certificate for some domain name by solving challenges received from 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`, +of `~.DVChallenge`, i.e. `~.challenges.TLSSNI01`, `~.challenges.HTTP01`, `~.challenges.DNS`) and continuity specific challenges (subclasses of `~.ContinuityChallenge`, i.e. `~.challenges.RecoveryToken`, `~.challenges.RecoveryContact`, @@ -160,7 +160,7 @@ 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. +authenticators only solve the `~.challenges.TLSSNI01` challenge currently. (You can set which challenges your authenticator can handle through the :meth:`~.IAuthenticator.get_chall_pref`. @@ -280,8 +280,14 @@ Steps: 4. Run ``tox -e lint`` to check for pylint errors. Fix any errors. 5. Run ``tox`` to run the entire test suite including coverage. Fix any errors. 6. If your code touches communication with an ACME server/Boulder, you - should run the integration tests, see `integration`_. + should run the integration tests, see `integration`_. See `Known Issues`_ + for some common failures that have nothing to do with your code. 7. Submit the PR. +8. Did your tests pass on Travis? If they didn't, it might not be your fault! + See `Known Issues`_. If it's not a known issue, fix any errors. + +.. _Known Issues: + https://github.com/letsencrypt/letsencrypt/wiki/Known-issues Updating the documentation ========================== diff --git a/docs/make.bat b/docs/make.bat index e83ba8cb5..198e864c3 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,263 +1,263 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - echo. coverage to run coverage check of the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -REM Check if sphinx-build is available and fallback to Python version if any -%SPHINXBUILD% 2> nul -if errorlevel 9009 goto sphinx_python -goto sphinx_ok - -:sphinx_python - -set SPHINXBUILD=python -m sphinx.__init__ -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -:sphinx_ok - - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\LetsEncrypt.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\LetsEncrypt.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "coverage" ( - %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage - if errorlevel 1 exit /b 1 - echo. - echo.Testing of coverage in the sources finished, look at the ^ -results in %BUILDDIR%/coverage/python.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + echo. coverage to run coverage check of the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +REM Check if sphinx-build is available and fallback to Python version if any +%SPHINXBUILD% 2> nul +if errorlevel 9009 goto sphinx_python +goto sphinx_ok + +:sphinx_python + +set SPHINXBUILD=python -m sphinx.__init__ +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +:sphinx_ok + + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\LetsEncrypt.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\LetsEncrypt.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "coverage" ( + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ +results in %BUILDDIR%/coverage/python.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/docs/using.rst b/docs/using.rst index eb8c31595..f6fb82f52 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -42,14 +42,24 @@ To install and run the client you just need to type: ./letsencrypt-auto +.. note:: On RedHat/CentOS 6 you will need to enable the EPEL_ + repository before install. + +.. _EPEL: http://fedoraproject.org/wiki/EPEL + Throughout the documentation, whenever you see references to ``letsencrypt`` script/binary, you can substitute in -``letsencrypt-auto``. For example, to get the help you would type: +``letsencrypt-auto``. For example, to get basic help you would type: .. code-block:: shell ./letsencrypt-auto --help +or for full help, type: + +.. code-block:: shell + + ./letsencrypt-auto --help all Running with Docker ------------------- @@ -86,8 +96,22 @@ in ``/etc/letsencrypt/live`` on the host. .. _`install Docker`: https://docs.docker.com/userguide/ -Distro packages ---------------- +Operating System Packages +-------------------------- + +**FreeBSD** + + * Port: ``cd /usr/ports/security/py-letsencrypt && make install clean`` + * Package: ``pkg install py27-letsencrypt`` + +**Arch Linux** + +.. code-block:: shell + + sudo pacman -S letsencrypt letsencrypt-nginx letsencrypt-apache \ + letshelp-letsencrypt + +**Other Operating Systems** Unfortunately, this is an ongoing effort. If you'd like to package Let's Encrypt client for your distribution of choice please have a @@ -121,32 +145,79 @@ SSL certificates! Plugins ======= -Officially supported plugins: +=========== = = =============================================================== +Plugin A I Notes +=========== = = =============================================================== +apache_ Y Y Automates obtaining and installing a cert with Apache 2.4 on + Debian-based distributions with ``libaugeas0`` 1.0+. +standalone_ Y N Uses a "standalone" webserver to obtain a cert. +webroot_ Y N Obtains a cert using an already running webserver. +manual_ Y N Helps you obtain a cert by giving you instructions to perform + domain validation yourself. +nginx_ Y Y Very experimental and not included in letsencrypt-auto_. +=========== = = =============================================================== -========== = = ================================================================ -Plugin A I Notes and status -========== = = ================================================================ -standalone Y N Very stable. Uses port 80 (force by - ``--standalone-supported-challenges http-01``) or 443 - (force by ``--standalone-supported-challenges dvsni``). -apache Y Y Alpha. Automates Apache installation, works fairly well but on - Debian-based distributions only for now. -webroot Y N Works with already running webserver, by writing necessary files - to the disk (``--webroot-path`` should be pointed to your - ``public_html``). Currently, when multiple domains are specified - (`-d`), they must all use the same web root path. -manual Y N Hidden from standard UI, use with ``-a manual``. Requires to - copy and paste commands into a new terminal session. Allows to - run client on machine different than target webserver, e.g. your - laptop. -nginx Y Y Very experimental. Not included in letsencrypt-auto_. -========== = = ================================================================ +Apache +------ -Third party plugins are listed at -https://github.com/letsencrypt/letsencrypt/wiki/Plugins. If -that's not enough, you can always :ref:`write your own plugin -`. +If you're running Apache 2.4 on a Debian-based OS with version 1.0+ of +the ``libaugeas0`` package available, you can use the Apache plugin. +This automates both obtaining and installing certs on an Apache +webserver. To specify this plugin on the command line, simply include +``--apache``. +Standalone +---------- + +To obtain a cert using a "standalone" webserver, you can use the +standalone plugin by including ``certonly`` and ``--standalone`` +on the command line. This plugin needs to bind to port 80 or 443 in +order to perform domain validation, so you may need to stop your +existing webserver. To control which port the plugin uses, include +one of the options shown below on the command line. + + * ``--standalone-supported-challenges http-01`` to use port 80 + * ``--standalone-supported-challenges tls-sni-01`` to use port 443 + +Webroot +------- + +If you're running a webserver that you don't want to stop to use +standalone, you can use the webroot plugin to obtain a cert by +including ``certonly`` and ``--webroot`` on the command line. In +addition, you'll need to specify ``--webroot-path`` with the root +directory of the files served by your webserver. For example, +``--webroot-path /var/www/html`` or +``--webroot-path /usr/share/nginx/html`` are two common webroot paths. +If multiple domains are specified, they must all use the same path. +Additionally, your server must be configured to serve files from +hidden directories. + +Manual +------ + +If you'd like to obtain a cert running ``letsencrypt`` on a machine +other than your target webserver or perform the steps for domain +validation yourself, you can use the manual plugin. While hidden from +the UI, you can use the plugin to obtain a cert by specifying +``certonly`` and ``--manual`` on the command line. This requires you +to copy and paste commands into another terminal session. + +Nginx +----- + +In the future, if you're running Nginx you can use this plugin to +automatically obtain and install your certificate. The Nginx plugin +is still experimental, however, and is not installed with +letsencrypt-auto_. If installed, you can select this plugin on the +command line by including ``--nginx``. + +Third party plugins +------------------- + +These plugins are listed at +https://github.com/letsencrypt/letsencrypt/wiki/Plugins. If you're +interested, you can also :ref:`write your own plugin `. Renewal ======= @@ -176,7 +247,7 @@ Where are my certificates? ========================== First of all, we encourage you to use Apache or nginx installers, both -which perform the certificate managemant automatically. If, however, +which perform the certificate management automatically. If, however, you prefer to manage everything by hand, this section provides information on where to find necessary files. @@ -197,7 +268,7 @@ The following files are available: .. warning:: This **must be kept secret at all times**! Never share it with anyone, including Let's Encrypt developers. You cannot - put it into safe, however - your server still needs to access + put it into a safe, however - your server still needs to access this file in order for SSL/TLS to work. This is what Apache needs for `SSLCertificateKeyFile diff --git a/examples/cli.ini b/examples/cli.ini index 34fb8ab02..a20764ed8 100644 --- a/examples/cli.ini +++ b/examples/cli.ini @@ -16,7 +16,7 @@ server = https://acme-staging.api.letsencrypt.org/directory # Uncomment to use the standalone authenticator on port 443 # authenticator = standalone -# standalone-supported-challenges = dvsni +# standalone-supported-challenges = tls-sni-01 # Uncomment to use the webroot authenticator. Replace webroot-path with the # path to the public_html / webroot folder being served by your web server. diff --git a/letsencrypt-apache/MANIFEST.in b/letsencrypt-apache/MANIFEST.in index 51c2df858..933cc10ac 100644 --- a/letsencrypt-apache/MANIFEST.in +++ b/letsencrypt-apache/MANIFEST.in @@ -3,3 +3,4 @@ include README.rst recursive-include docs * recursive-include letsencrypt_apache/tests/testdata * include letsencrypt_apache/options-ssl-apache.conf +recursive-include letsencrypt_apache/augeas_lens *.aug diff --git a/letsencrypt-apache/docs/conf.py b/letsencrypt-apache/docs/conf.py index ddbf09262..aa58038cd 100644 --- a/letsencrypt-apache/docs/conf.py +++ b/letsencrypt-apache/docs/conf.py @@ -232,25 +232,25 @@ htmlhelp_basename = 'letsencrypt-apachedoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + #'preamble': '', -# Latex figure (float) alignment -#'figure_align': 'htbp', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'letsencrypt-apache.tex', u'letsencrypt-apache Documentation', - u'Let\'s Encrypt Project', 'manual'), + (master_doc, 'letsencrypt-apache.tex', u'letsencrypt-apache Documentation', + u'Let\'s Encrypt Project', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -293,9 +293,9 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'letsencrypt-apache', u'letsencrypt-apache Documentation', - author, 'letsencrypt-apache', 'One line description of project.', - 'Miscellaneous'), + (master_doc, 'letsencrypt-apache', u'letsencrypt-apache Documentation', + author, 'letsencrypt-apache', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. diff --git a/letsencrypt-apache/letsencrypt_apache/augeas_configurator.py b/letsencrypt-apache/letsencrypt_apache/augeas_configurator.py index 7557a27c6..9e0948f12 100644 --- a/letsencrypt-apache/letsencrypt_apache/augeas_configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/augeas_configurator.py @@ -7,6 +7,8 @@ from letsencrypt import errors from letsencrypt import reverter from letsencrypt.plugins import common +from letsencrypt_apache import constants + logger = logging.getLogger(__name__) @@ -27,10 +29,12 @@ class AugeasConfigurator(common.Plugin): def __init__(self, *args, **kwargs): super(AugeasConfigurator, self).__init__(*args, **kwargs) - # Set Augeas flags to not save backup (we do it ourselves) - # Set Augeas to not load anything by default - my_flags = augeas.Augeas.NONE | augeas.Augeas.NO_MODL_AUTOLOAD - self.aug = augeas.Augeas(flags=my_flags) + self.aug = augeas.Augeas( + # specify a directory to load our preferred lens from + loadpath=constants.AUGEAS_LENS_DIR, + # Do not save backup (we do it ourselves), do not load + # anything by default + flags=(augeas.Augeas.NONE | augeas.Augeas.NO_MODL_AUTOLOAD)) self.save_notes = "" # See if any temporary changes need to be recovered @@ -69,7 +73,8 @@ class AugeasConfigurator(common.Plugin): This function first checks for save errors, if none are found, all configuration changes made will be saved. According to the - function parameters. + function parameters. If an exception is raised, a new checkpoint + was not created. :param str title: The title of the save. If a title is given, the configuration will be saved as a new checkpoint and put in a @@ -78,8 +83,9 @@ class AugeasConfigurator(common.Plugin): :param bool temporary: Indicates whether the changes made will be quickly reversed in the future (ie. challenges) - :raises .errors.PluginError: If there was an error in Augeas, in an - attempt to save the configuration, or an error creating a checkpoint + :raises .errors.PluginError: If there was an error in Augeas, in + an attempt to save the configuration, or an error creating a + checkpoint """ save_state = self.aug.get("/augeas/save") @@ -118,16 +124,16 @@ class AugeasConfigurator(common.Plugin): except errors.ReverterError as err: raise errors.PluginError(str(err)) + self.aug.set("/augeas/save", save_state) + self.save_notes = "" + self.aug.save() + if title and not temporary: try: self.reverter.finalize_checkpoint(title) except errors.ReverterError as err: raise errors.PluginError(str(err)) - self.aug.set("/augeas/save", save_state) - self.save_notes = "" - self.aug.save() - def _log_save_errors(self, ex_errs): """Log errors due to bad Augeas save. diff --git a/letsencrypt-apache/letsencrypt_apache/augeas_lens/README b/letsencrypt-apache/letsencrypt_apache/augeas_lens/README new file mode 100644 index 000000000..fc803a776 --- /dev/null +++ b/letsencrypt-apache/letsencrypt_apache/augeas_lens/README @@ -0,0 +1,2 @@ +Let's Encrypt includes the very latest Augeas lenses in order to ship bug fixes +to Apacche configuration handling bugs as quickly as possible diff --git a/letsencrypt-apache/letsencrypt_apache/augeas_lens/httpd.aug b/letsencrypt-apache/letsencrypt_apache/augeas_lens/httpd.aug new file mode 100644 index 000000000..9b50a8f0e --- /dev/null +++ b/letsencrypt-apache/letsencrypt_apache/augeas_lens/httpd.aug @@ -0,0 +1,112 @@ +(* Apache HTTPD lens for Augeas + +Authors: + David Lutterkort + Francis Giraldeau + Raphael Pinson + +About: Reference + Online Apache configuration manual: http://httpd.apache.org/docs/trunk/ + +About: License + This file is licensed under the LGPL v2+. + +About: Lens Usage + Sample usage of this lens in augtool + + Apache configuration is represented by two main structures, nested sections + and directives. Sections are used as labels, while directives are kept as a + value. Sections and directives can have positional arguments inside values + of "arg" nodes. Arguments of sections must be the firsts child of the + section node. + + This lens doesn't support automatic string quoting. Hence, the string must + be quoted when containing a space. + + Create a new VirtualHost section with one directive: + > clear /files/etc/apache2/sites-available/foo/VirtualHost + > set /files/etc/apache2/sites-available/foo/VirtualHost/arg "172.16.0.1:80" + > set /files/etc/apache2/sites-available/foo/VirtualHost/directive "ServerAdmin" + > set /files/etc/apache2/sites-available/foo/VirtualHost/*[self::directive="ServerAdmin"]/arg "admin@example.com" + +About: Configuration files + This lens applies to files in /etc/httpd and /etc/apache2. See . + +*) + + +module Httpd = + +autoload xfm + +(****************************************************************** + * Utilities lens + *****************************************************************) +let dels (s:string) = del s s + +(* deal with continuation lines *) +let sep_spc = del /([ \t]+|[ \t]*\\\\\r?\n[ \t]*)/ " " + +let sep_osp = Sep.opt_space +let sep_eq = del /[ \t]*=[ \t]*/ "=" + +let nmtoken = /[a-zA-Z:_][a-zA-Z0-9:_.-]*/ +let word = /[a-zA-Z][a-zA-Z0-9._-]*/ + +let comment = Util.comment +let eol = Util.doseol +let empty = Util.empty_dos +let indent = Util.indent + +(* borrowed from shellvars.aug *) +let char_arg_dir = /[^\\ '"\t\r\n]|\\\\"|\\\\'/ +let char_arg_sec = /[^ '"\t\r\n>]|\\\\"|\\\\'/ +let cdot = /\\\\./ +let cl = /\\\\\n/ +let dquot = + let no_dquot = /[^"\\\r\n]/ + in /"/ . (no_dquot|cdot|cl)* . /"/ +let squot = + let no_squot = /[^'\\\r\n]/ + in /'/ . (no_squot|cdot|cl)* . /'/ +let comp = /[<>=]?=/ + +(****************************************************************** + * Attributes + *****************************************************************) + +let arg_dir = [ label "arg" . store (char_arg_dir+|dquot|squot) ] +let arg_sec = [ label "arg" . store (char_arg_sec+|comp|dquot|squot) ] + +let argv (l:lens) = l . (sep_spc . l)* + +let directive = [ indent . label "directive" . store word . + (sep_spc . argv arg_dir)? . eol ] + +let section (body:lens) = + (* opt_eol includes empty lines *) + let opt_eol = del /([ \t]*#?\r?\n)*/ "\n" in + let inner = (sep_spc . argv arg_sec)? . sep_osp . + dels ">" . opt_eol . ((body|comment) . (body|empty|comment)*)? . + indent . dels "" ">" . eol ] + +let rec content = section (content|directive) + +let lns = (content|directive|comment|empty)* + +let filter = (incl "/etc/apache2/apache2.conf") . + (incl "/etc/apache2/httpd.conf") . + (incl "/etc/apache2/ports.conf") . + (incl "/etc/apache2/conf.d/*") . + (incl "/etc/apache2/conf-available/*.conf") . + (incl "/etc/apache2/mods-available/*") . + (incl "/etc/apache2/sites-available/*") . + (incl "/etc/httpd/conf.d/*.conf") . + (incl "/etc/httpd/httpd.conf") . + (incl "/etc/httpd/conf/httpd.conf") . + Util.stdexcl + +let xfm = transform lns filter diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index d376fe4b6..f10f0c241 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -13,7 +13,6 @@ import zope.interface from acme import challenges -from letsencrypt import achallenges from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util @@ -162,7 +161,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # Get all of the available vhosts self.vhosts = self.get_virtual_hosts() - temp_install(self.mod_ssl_conf) + install_ssl_options_conf(self.mod_ssl_conf) def deploy_cert(self, domain, cert_path, key_path, chain_path=None, fullchain_path=None): # pylint: disable=unused-argument @@ -307,6 +306,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): best_points = 0 for vhost in self.vhosts: + if vhost.modmacro is True: + continue if target_name in vhost.get_names(): points = 2 elif any(addr.get_addr() == target_name for addr in vhost.addrs): @@ -326,7 +327,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # No winners here... is there only one reasonable vhost? if best_candidate is None: # reasonable == Not all _default_ addrs - reasonable_vhosts = self._non_default_vhosts() + vhosts = self._non_default_vhosts() + # remove mod_macro hosts from reasonable vhosts + reasonable_vhosts = [vh for vh + in vhosts if vh.modmacro is False] if len(reasonable_vhosts) == 1: best_candidate = reasonable_vhosts[0] @@ -348,8 +352,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ all_names = set() + vhost_macro = [] + for vhost in self.vhosts: all_names.update(vhost.get_names()) + if vhost.modmacro: + vhost_macro.append(vhost.filep) for addr in vhost.addrs: if common.hostname_regex.match(addr.get_addr()): @@ -359,6 +367,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if name: all_names.add(name) + if len(vhost_macro) > 0: + zope.component.getUtility(interfaces.IDisplay).notification( + "Apache mod_macro seems to be in use in file(s):\n{0}" + "\n\nUnfortunately mod_macro is not yet supported".format( + "\n ".join(vhost_macro))) + return all_names def get_name_from_ip(self, addr): # pylint: disable=no-self-use @@ -395,11 +409,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): "ServerAlias", None, start=host.path, exclude=False) for alias in serveralias_match: - host.aliases.add(self.parser.get_arg(alias)) + serveralias = self.parser.get_arg(alias) + if not host.modmacro: + host.aliases.add(serveralias) if servername_match: # Get last ServerName as each overwrites the previous - host.name = self.parser.get_arg(servername_match[-1]) + servername = self.parser.get_arg(servername_match[-1]) + if not host.modmacro: + host.name = servername def _create_vhost(self, path): """Used by get_virtual_hosts to create vhost objects @@ -422,7 +440,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): filename = get_file_path(path) is_enabled = self.is_site_enabled(filename) - vhost = obj.VirtualHost(filename, path, addrs, is_ssl, is_enabled) + macro = False + if "/macro/" in path.lower(): + macro = True + + vhost = obj.VirtualHost(filename, path, addrs, is_ssl, + is_enabled, modmacro=macro) self._add_servernames(vhost) return vhost @@ -1117,7 +1140,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): ########################################################################### def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use """Return list of challenge preferences.""" - return [challenges.DVSNI] + return [challenges.TLSSNI01] def perform(self, achalls): """Perform the configuration related challenge. @@ -1132,11 +1155,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): apache_dvsni = dvsni.ApacheDvsni(self) for i, achall in enumerate(achalls): - if isinstance(achall, achallenges.DVSNI): - # Currently also have dvsni hold associated index - # of the challenge. This helps to put all of the responses back - # together when they are all complete. - apache_dvsni.add_chall(achall, i) + # Currently also have dvsni hold associated index + # of the challenge. This helps to put all of the responses back + # together when they are all complete. + apache_dvsni.add_chall(achall, i) sni_response = apache_dvsni.perform() if sni_response: @@ -1236,7 +1258,7 @@ def get_file_path(vhost_path): avail_fp = vhost_path[6:] # This can be optimized... while True: - # Cast both to lowercase to be case insensitive + # Cast all to lowercase to be case insensitive find_if = avail_fp.lower().find("/ifmodule") if find_if != -1: avail_fp = avail_fp[:find_if] @@ -1245,16 +1267,26 @@ def get_file_path(vhost_path): if find_vh != -1: avail_fp = avail_fp[:find_vh] continue + find_macro = avail_fp.lower().find("/macro") + if find_macro != -1: + avail_fp = avail_fp[:find_macro] + continue break return avail_fp -def temp_install(options_ssl): - """Temporary install for convenience.""" - # WARNING: THIS IS A POTENTIAL SECURITY VULNERABILITY - # THIS SHOULD BE HANDLED BY THE PACKAGE MANAGER - # AND TAKEN OUT BEFORE RELEASE, INSTEAD - # SHOWING A NICE ERROR MESSAGE ABOUT THE PROBLEM. +def install_ssl_options_conf(options_ssl): + """ + Copy Let's Encrypt's SSL options file into the system's config dir if + required. + """ + # XXX if we ever try to enforce a local privilege boundary (eg, running + # letsencrypt for unprivileged users via setuid), this function will need + # to be modified. + + # XXX if the user is in security-autoupdate mode, we should be willing to + # overwrite the options_ssl file at least if it's unmodified: + # https://github.com/letsencrypt/letsencrypt/issues/1123 # Check to make sure options-ssl.conf is installed if not os.path.isfile(options_ssl): diff --git a/letsencrypt-apache/letsencrypt_apache/constants.py b/letsencrypt-apache/letsencrypt_apache/constants.py index b38e898cf..1c17eacc3 100644 --- a/letsencrypt-apache/letsencrypt_apache/constants.py +++ b/letsencrypt-apache/letsencrypt_apache/constants.py @@ -20,6 +20,10 @@ MOD_SSL_CONF_SRC = pkg_resources.resource_filename( """Path to the Apache mod_ssl config file found in the Let's Encrypt distribution.""" +AUGEAS_LENS_DIR = pkg_resources.resource_filename( + "letsencrypt_apache", "augeas_lens") +"""Path to the Augeas lens directory""" + REWRITE_HTTPS_ARGS = [ "^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,QSA,R=permanent]"] """Apache rewrite rule arguments used for redirections to https vhost""" diff --git a/letsencrypt-apache/letsencrypt_apache/dvsni.py b/letsencrypt-apache/letsencrypt_apache/dvsni.py index ed88bf8a7..2f9e9ed18 100644 --- a/letsencrypt-apache/letsencrypt_apache/dvsni.py +++ b/letsencrypt-apache/letsencrypt_apache/dvsni.py @@ -7,14 +7,14 @@ from letsencrypt_apache import obj from letsencrypt_apache import parser -class ApacheDvsni(common.Dvsni): +class ApacheDvsni(common.TLSSNI01): """Class performs DVSNI challenges within the Apache configurator. :ivar configurator: ApacheConfigurator object :type configurator: :class:`~apache.configurator.ApacheConfigurator` - :ivar list achalls: Annotated :class:`~letsencrypt.achallenges.DVSNI` - challenges. + :ivar list achalls: Annotated tls-sni-01 + (`.KeyAuthorizationAnnotatedChallenge`) challenges. :param list indices: Meant to hold indices of challenges in a larger array. ApacheDvsni is capable of solving many challenges @@ -62,7 +62,7 @@ class ApacheDvsni(common.Dvsni): # Prepare the server for HTTPS self.configurator.prepare_server_https( - str(self.configurator.config.dvsni_port), True) + str(self.configurator.config.tls_sni_01_port), True) responses = [] @@ -114,14 +114,15 @@ class ApacheDvsni(common.Dvsni): # TODO: Checkout _default_ rules. dvsni_addrs = set() - default_addr = obj.Addr(("*", str(self.configurator.config.dvsni_port))) + default_addr = obj.Addr(("*", str( + self.configurator.config.tls_sni_01_port))) for addr in vhost.addrs: if "_default_" == addr.get_addr(): dvsni_addrs.add(default_addr) else: dvsni_addrs.add( - addr.get_sni_addr(self.configurator.config.dvsni_port)) + addr.get_sni_addr(self.configurator.config.tls_sni_01_port)) return dvsni_addrs @@ -144,8 +145,8 @@ class ApacheDvsni(common.Dvsni): def _get_config_text(self, achall, ip_addrs): """Chocolate virtual server configuration text - :param achall: Annotated DVSNI challenge. - :type achall: :class:`letsencrypt.achallenges.DVSNI` + :param .KeyAuthorizationAnnotatedChallenge achall: Annotated + DVSNI challenge. :param list ip_addrs: addresses of challenged domain :class:`list` of type `~.obj.Addr` @@ -164,7 +165,7 @@ class ApacheDvsni(common.Dvsni): # https://docs.python.org/2.7/reference/lexical_analysis.html return self.VHOST_TEMPLATE.format( vhost=ips, - server_name=achall.gen_response(achall.account_key).z_domain, + server_name=achall.response(achall.account_key).z_domain, ssl_options_conf_path=self.configurator.mod_ssl_conf, cert_path=self.get_cert_path(achall), key_path=self.get_key_path(achall), diff --git a/letsencrypt-apache/letsencrypt_apache/obj.py b/letsencrypt-apache/letsencrypt_apache/obj.py index 58a6c740e..175ce3f92 100644 --- a/letsencrypt-apache/letsencrypt_apache/obj.py +++ b/letsencrypt-apache/letsencrypt_apache/obj.py @@ -102,6 +102,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods :ivar bool ssl: SSLEngine on in vhost :ivar bool enabled: Virtual host is enabled + :ivar bool modmacro: VirtualHost is using mod_macro https://httpd.apache.org/docs/2.4/vhosts/details.html @@ -112,7 +113,9 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods # ?: is used for not returning enclosed characters strip_name = re.compile(r"^(?:.+://)?([^ :$]*)") - def __init__(self, filep, path, addrs, ssl, enabled, name=None, aliases=None): + def __init__(self, filep, path, addrs, ssl, enabled, name=None, + aliases=None, modmacro=False): + # pylint: disable=too-many-arguments """Initialize a VH.""" self.filep = filep @@ -122,6 +125,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods self.aliases = aliases if aliases is not None else set() self.ssl = ssl self.enabled = enabled + self.modmacro = modmacro def get_names(self): """Return a set of all names.""" @@ -141,21 +145,25 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods "Name: {name}\n" "Aliases: {aliases}\n" "TLS Enabled: {tls}\n" - "Site Enabled: {active}".format( + "Site Enabled: {active}\n" + "mod_macro Vhost: {modmacro}".format( filename=self.filep, vhpath=self.path, addrs=", ".join(str(addr) for addr in self.addrs), name=self.name if self.name is not None else "", aliases=", ".join(name for name in self.aliases), tls="Yes" if self.ssl else "No", - active="Yes" if self.enabled else "No")) + active="Yes" if self.enabled else "No", + modmacro="Yes" if self.modmacro else "No")) def __eq__(self, other): if isinstance(other, self.__class__): return (self.filep == other.filep and self.path == other.path and self.addrs == other.addrs and self.get_names() == other.get_names() and - self.ssl == other.ssl and self.enabled == other.enabled) + self.ssl == other.ssl and + self.enabled == other.enabled and + self.modmacro == other.modmacro) return False diff --git a/letsencrypt-apache/letsencrypt_apache/options-ssl-apache.conf b/letsencrypt-apache/letsencrypt_apache/options-ssl-apache.conf index 8c28c86a5..2a724d7ec 100644 --- a/letsencrypt-apache/letsencrypt_apache/options-ssl-apache.conf +++ b/letsencrypt-apache/letsencrypt_apache/options-ssl-apache.conf @@ -8,12 +8,6 @@ SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA25 SSLHonorCipherOrder on SSLCompression off - -ServerSignature Off -AcceptPathInfo Off -AddOutputFilterByType DEFLATE text/html text/plain text/xml application/pdf -AddDefaultCharset UTF-8 - SSLOptions +StrictRequire # Add vhost name to log entries: diff --git a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py index 7c2137c45..0350a32ec 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py @@ -59,14 +59,20 @@ class TwoVhost80Test(util.ApacheTest): # Weak test.. ApacheConfigurator.add_parser_arguments(mock.MagicMock()) - def test_get_all_names(self): + @mock.patch("zope.component.getUtility") + def test_get_all_names(self, mock_getutility): + mock_getutility.notification = mock.MagicMock(return_value=True) names = self.config.get_all_names() self.assertEqual(names, set( ["letsencrypt.demo", "encryption-example.demo", "ip-172-30-0-17"])) + @mock.patch("zope.component.getUtility") @mock.patch("letsencrypt_apache.configurator.socket.gethostbyaddr") - def test_get_all_names_addrs(self, mock_gethost): + def test_get_all_names_addrs(self, mock_gethost, mock_getutility): mock_gethost.side_effect = [("google.com", "", ""), socket.error] + notification = mock.Mock() + notification.notification = mock.Mock(return_value=True) + mock_getutility.return_value = notification vhost = obj.VirtualHost( "fp", "ap", set([obj.Addr(("8.8.8.8", "443")), @@ -97,7 +103,7 @@ class TwoVhost80Test(util.ApacheTest): """ vhs = self.config.get_virtual_hosts() - self.assertEqual(len(vhs), 4) + self.assertEqual(len(vhs), 5) found = 0 for vhost in vhs: @@ -108,7 +114,7 @@ class TwoVhost80Test(util.ApacheTest): else: raise Exception("Missed: %s" % vhost) # pragma: no cover - self.assertEqual(found, 4) + self.assertEqual(found, 5) @mock.patch("letsencrypt_apache.display_ops.select_vhost") def test_choose_vhost_none_avail(self, mock_select): @@ -174,7 +180,7 @@ class TwoVhost80Test(util.ApacheTest): def test_non_default_vhosts(self): # pylint: disable=protected-access - self.assertEqual(len(self.config._non_default_vhosts()), 3) + self.assertEqual(len(self.config._non_default_vhosts()), 4) def test_is_site_enabled(self): """Test if site is enabled. @@ -345,7 +351,7 @@ class TwoVhost80Test(util.ApacheTest): self.assertEqual(self.config.is_name_vhost(self.vh_truth[0]), self.config.is_name_vhost(ssl_vhost)) - self.assertEqual(len(self.config.vhosts), 5) + self.assertEqual(len(self.config.vhosts), 6) def test_make_vhost_ssl_extra_vhs(self): self.config.aug.match = mock.Mock(return_value=["p1", "p2"]) @@ -382,8 +388,8 @@ class TwoVhost80Test(util.ApacheTest): account_key, achall1, achall2 = self.get_achalls() dvsni_ret_val = [ - achall1.gen_response(account_key), - achall2.gen_response(account_key), + achall1.response(account_key), + achall2.response(account_key), ] mock_dvsni_perform.return_value = dvsni_ret_val @@ -492,10 +498,10 @@ class TwoVhost80Test(util.ApacheTest): def test_get_chall_pref(self): self.assertTrue(isinstance(self.config.get_chall_pref(""), list)) - def test_temp_install(self): - from letsencrypt_apache.configurator import temp_install + def test_install_ssl_options_conf(self): + from letsencrypt_apache.configurator import install_ssl_options_conf path = os.path.join(self.work_dir, "test_it") - temp_install(path) + install_ssl_options_conf(path) self.assertTrue(os.path.isfile(path)) # TEST ENHANCEMENTS @@ -587,20 +593,20 @@ class TwoVhost80Test(util.ApacheTest): self.vh_truth[1].aliases = set(["yes.default.com"]) self.config._enable_redirect(self.vh_truth[1], "") # pylint: disable=protected-access - self.assertEqual(len(self.config.vhosts), 5) + self.assertEqual(len(self.config.vhosts), 6) def get_achalls(self): """Return testing achallenges.""" account_key = self.rsa512jwk - achall1 = achallenges.DVSNI( + achall1 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb( - challenges.DVSNI( + challenges.TLSSNI01( token="jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q"), "pending"), domain="encryption-example.demo", account_key=account_key) - achall2 = achallenges.DVSNI( + achall2 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb( - challenges.DVSNI( + challenges.TLSSNI01( token="uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU"), "pending"), domain="letsencrypt.demo", account_key=account_key) diff --git a/letsencrypt-apache/letsencrypt_apache/tests/display_ops_test.py b/letsencrypt-apache/letsencrypt_apache/tests/display_ops_test.py index d7cfb09b3..6db319d87 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/display_ops_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/display_ops_test.py @@ -57,7 +57,7 @@ class SelectVhostTest(unittest.TestCase): @mock.patch("letsencrypt_apache.display_ops.zope.component.getUtility") def test_multiple_names(self, mock_util): - mock_util().menu.return_value = (display_util.OK, 4) + mock_util().menu.return_value = (display_util.OK, 5) self.vhosts.append( obj.VirtualHost( @@ -65,7 +65,7 @@ class SelectVhostTest(unittest.TestCase): False, False, "wildcard.com", set(["*.wildcard.com"]))) - self.assertEqual(self.vhosts[4], self._call(self.vhosts)) + self.assertEqual(self.vhosts[5], self._call(self.vhosts)) if __name__ == "__main__": diff --git a/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py b/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py index c362d4115..911c2a36b 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py @@ -13,15 +13,15 @@ from letsencrypt_apache.tests import util class DvsniPerformTest(util.ApacheTest): """Test the ApacheDVSNI challenge.""" - auth_key = common_test.DvsniTest.auth_key - achalls = common_test.DvsniTest.achalls + auth_key = common_test.TLSSNI01Test.auth_key + achalls = common_test.TLSSNI01Test.achalls def setUp(self): # pylint: disable=arguments-differ super(DvsniPerformTest, self).setUp() config = util.get_apache_configurator( self.config_path, self.config_dir, self.work_dir) - config.config.dvsni_port = 443 + config.config.tls_sni_01_port = 443 from letsencrypt_apache import dvsni self.sni = dvsni.ApacheDvsni(config) @@ -46,7 +46,7 @@ class DvsniPerformTest(util.ApacheTest): achall = self.achalls[0] self.sni.add_chall(achall) - response = self.achalls[0].gen_response(self.auth_key) + response = self.achalls[0].response(self.auth_key) mock_setup_cert = mock.MagicMock(return_value=response) # pylint: disable=protected-access self.sni._setup_challenge_cert = mock_setup_cert @@ -72,7 +72,7 @@ class DvsniPerformTest(util.ApacheTest): acme_responses = [] for achall in self.achalls: self.sni.add_chall(achall) - acme_responses.append(achall.gen_response(self.auth_key)) + acme_responses.append(achall.response(self.auth_key)) mock_setup_cert = mock.MagicMock(side_effect=acme_responses) # pylint: disable=protected-access @@ -100,7 +100,7 @@ class DvsniPerformTest(util.ApacheTest): z_domains = [] for achall in self.achalls: self.sni.add_chall(achall) - z_domain = achall.gen_response(self.auth_key).z_domain + z_domain = achall.response(self.auth_key).z_domain z_domains.append(set([z_domain])) self.sni._mod_config() # pylint: disable=protected-access diff --git a/letsencrypt-apache/letsencrypt_apache/tests/parser_test.py b/letsencrypt-apache/letsencrypt_apache/tests/parser_test.py index d2e4dec14..bc1f316f9 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/parser_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/parser_test.py @@ -52,7 +52,7 @@ class BasicParserTest(util.ParserTest): test2 = self.parser.find_dir("documentroot") self.assertEqual(len(test), 1) - self.assertEqual(len(test2), 3) + self.assertEqual(len(test2), 4) def test_add_dir(self): aug_default = "/files" + self.parser.loc["default"] diff --git a/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/mod_macro-example.conf b/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/mod_macro-example.conf new file mode 100644 index 000000000..6a6579007 --- /dev/null +++ b/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/mod_macro-example.conf @@ -0,0 +1,15 @@ + + + ServerName $domain + ServerAlias www.$domain + DocumentRoot /var/www/html + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + +Use VHost macro1 test.com +Use VHost macro2 hostname.org +Use VHost macro3 apache.org + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/mod_macro-example.conf b/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/mod_macro-example.conf new file mode 120000 index 000000000..44f254304 --- /dev/null +++ b/letsencrypt-apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/mod_macro-example.conf @@ -0,0 +1 @@ +../sites-available/mod_macro-example.conf \ No newline at end of file diff --git a/letsencrypt-apache/letsencrypt_apache/tests/util.py b/letsencrypt-apache/letsencrypt_apache/tests/util.py index 2594ba773..a8bfe0e4b 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/util.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/util.py @@ -124,6 +124,11 @@ def get_vh_truth(temp_dir, config_name): os.path.join(aug_pre, "letsencrypt.conf/VirtualHost"), set([obj.Addr.fromstring("*:80")]), False, True, "letsencrypt.demo"), + obj.VirtualHost( + os.path.join(prefix, "mod_macro-example.conf"), + os.path.join(aug_pre, + "mod_macro-example.conf/Macro/VirtualHost"), + set([obj.Addr.fromstring("*:80")]), False, True, modmacro=True) ] return vh_truth diff --git a/letsencrypt-auto b/letsencrypt-auto index d163998aa..a3009fe52 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -8,23 +8,103 @@ # without requiring specific versions of its dependencies from the operating # system. +# Note: you can set XDG_DATA_HOME or VENV_PATH before running this script, +# if you want to change where the virtual environment will be installed XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN=${VENV_PATH}/bin +# This script takes the same arguments as the main letsencrypt program, but it +# additionally responds to --verbose (more output) and --debug (allow support +# for experimental platforms) +for arg in "$@" ; do + # This first clause is redundant with the third, but hedging on portability + if [ "$arg" = "-v" ] || [ "$arg" = "--verbose" ] || echo "$arg" | grep -E -- "-v+$" ; then + VERBOSE=1 + elif [ "$arg" = "--debug" ] ; then + DEBUG=1 + fi +done + +# letsencrypt-auto needs root access to bootstrap OS dependencies, and +# letsencrypt itself needs root access for almost all modes of operation +# The "normal" case is that sudo is used for the steps that need root, but +# this script *can* be run as root (not recommended), or fall back to using +# `su` if test "`id -u`" -ne "0" ; then - SUDO=sudo + if command -v sudo 1>/dev/null 2>&1; then + SUDO=sudo + else + echo \"sudo\" is not available, will use \"su\" for installation steps... + # Because the parameters in `su -c` has to be a string, + # we need properly escape it + su_sudo() { + args="" + # This `while` loop iterates over all parameters given to this function. + # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string + # will be wrap in a pair of `'`, then append to `$args` string + # For example, `echo "It's only 1\$\!"` will be escaped to: + # 'echo' 'It'"'"'s only 1$!' + # │ │└┼┘│ + # │ │ │ └── `'s only 1$!'` the literal string + # │ │ └── `\"'\"` is a single quote (as a string) + # │ └── `'It'`, to be concatenated with the strings followed it + # └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself + while [ $# -ne 0 ]; do + args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' " + shift + done + su root -c "$args" + } + SUDO=su_sudo + fi else SUDO= fi -for arg in "$@" ; do - # This first clause is redundant with the third, but hedging on portability - if [ "$arg" = "-v" ] || [ "$arg" = "--verbose" ] || echo "$arg" | grep -E -- "-v+$" ; then - VERBOSE=1 +ExperimentalBootstrap() { + # Arguments: Platform name, boostrap script name, SUDO command (iff needed) + if [ "$DEBUG" = 1 ] ; then + if [ "$2" != "" ] ; then + echo "Bootstrapping dependencies for $1..." + if [ "$3" != "" ] ; then + "$3" "$BOOTSTRAP/$2" + else + "$BOOTSTRAP/$2" + fi + fi + else + echo "WARNING: $1 support is very experimental at present..." + echo "if you would like to work on improving it, please ensure you have backups" + echo "and then run this script again with the --debug flag!" + exit 1 fi -done +} + +DeterminePythonVersion() { + if command -v python2.7 > /dev/null ; then + export LE_PYTHON=${LE_PYTHON:-python2.7} + elif command -v python27 > /dev/null ; then + export LE_PYTHON=${LE_PYTHON:-python27} + elif command -v python2 > /dev/null ; then + export LE_PYTHON=${LE_PYTHON:-python2} + elif command -v python > /dev/null ; then + export LE_PYTHON=${LE_PYTHON:-python} + else + echo "Cannot find any Pythons... please install one!" + fi + + PYVER=`$LE_PYTHON --version 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'` + if [ $PYVER -eq 26 ] ; then + ExperimentalBootstrap "Python 2.6" + elif [ $PYVER -lt 26 ] ; then + echo "You have an ancient version of Python entombed in your operating system..." + echo "This isn't going to work." + exit 1 + fi +} + # virtualenv call is not idempotent: it overwrites pip upgraded in # later steps, causing "ImportError: cannot import name unpack_url" @@ -35,28 +115,29 @@ then echo "Cannot find the letsencrypt bootstrap scripts in $BOOTSTRAP" exit 1 fi + if [ -f /etc/debian_version ] ; then echo "Bootstrapping dependencies for Debian-based OSes..." $SUDO $BOOTSTRAP/_deb_common.sh + elif [ -f /etc/redhat-release ] ; then + echo "Bootstrapping dependencies for RedHat-based OSes..." + $SUDO $BOOTSTRAP/_rpm_common.sh + elif `grep -q openSUSE /etc/os-release` ; then + echo "Bootstrapping dependencies for openSUSE-based OSes..." + $SUDO $BOOTSTRAP/_suse_common.sh elif [ -f /etc/arch-release ] ; then echo "Bootstrapping dependencies for Archlinux..." $SUDO $BOOTSTRAP/archlinux.sh elif [ -f /etc/manjaro-release ] ; then - echo "Bootstrapping dependencies for Manjaro Linux..." - $SUDO $BOOTSTRAP/manjaro.sh - elif [ -f /etc/redhat-release ] ; then - echo "Bootstrapping dependencies for RedHat-based OSes..." - $SUDO $BOOTSTRAP/_rpm_common.sh + ExperimentalBootstrap "Manjaro Linux" manjaro.sh "$SUDO" elif [ -f /etc/gentoo-release ] ; then - echo "Bootstrapping dependencies for Gentoo-based OSes..." - $SUDO $BOOTSTRAP/_gentoo_common.sh + ExperimentalBootstrap "Gentoo" _gentoo_common.sh "$SUDO" elif uname | grep -iq FreeBSD ; then - echo "Bootstrapping dependencies for FreeBSD..." - $SUDO $BOOTSTRAP/freebsd.sh + ExperimentalBootstrap "FreeBSD" freebsd.sh "$SUDO" elif uname | grep -iq Darwin ; then - echo "Bootstrapping dependencies for Mac OS X..." - echo "WARNING: Mac support is very experimental at present..." - $BOOTSTRAP/mac.sh + ExperimentalBootstrap "Mac OS X" mac.sh + elif grep -iq "Amazon Linux" /etc/issue ; then + ExperimentalBootstrap "Amazon Linux" _rpm_common.sh else echo "Sorry, I don't know how to bootstrap Let's Encrypt on your operating system!" echo @@ -65,24 +146,28 @@ then echo "for more info" fi + DeterminePythonVersion echo "Creating virtual environment..." if [ "$VERBOSE" = 1 ] ; then - virtualenv --no-site-packages --python python2 $VENV_PATH + virtualenv --no-site-packages --python $LE_PYTHON $VENV_PATH else - virtualenv --no-site-packages --python python2 $VENV_PATH > /dev/null + virtualenv --no-site-packages --python $LE_PYTHON $VENV_PATH > /dev/null fi +else + DeterminePythonVersion fi + printf "Updating letsencrypt and virtual environment dependencies..." if [ "$VERBOSE" = 1 ] ; then echo $VENV_BIN/pip install -U setuptools $VENV_BIN/pip install -U pip - $VENV_BIN/pip install -U letsencrypt letsencrypt-apache + $VENV_BIN/pip install -r py26reqs.txt -U letsencrypt letsencrypt-apache # nginx is buggy / disabled for now, but upgrade it if the user has # installed it manually if $VENV_BIN/pip freeze | grep -q letsencrypt-nginx ; then - $VENV_BIN/pip install -U letsencrypt letsencrypt-nginx + $VENV_BIN/pip install -U letsencrypt letsencrypt-nginx fi else $VENV_BIN/pip install -U setuptools > /dev/null @@ -90,6 +175,8 @@ else $VENV_BIN/pip install -U pip > /dev/null printf . # nginx is buggy / disabled for now... + $VENV_BIN/pip install -r py26reqs.txt > /dev/null + printf . $VENV_BIN/pip install -U letsencrypt > /dev/null printf . $VENV_BIN/pip install -U letsencrypt-apache > /dev/null diff --git a/letsencrypt-compatibility-test/docs/conf.py b/letsencrypt-compatibility-test/docs/conf.py index 7e9f0d5a4..3ee161efb 100644 --- a/letsencrypt-compatibility-test/docs/conf.py +++ b/letsencrypt-compatibility-test/docs/conf.py @@ -226,25 +226,26 @@ htmlhelp_basename = 'letsencrypt-compatibility-testdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + #'preamble': '', -# Latex figure (float) alignment -#'figure_align': 'htbp', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'letsencrypt-compatibility-test.tex', u'letsencrypt-compatibility-test Documentation', - u'Let\'s Encrypt Project', 'manual'), + (master_doc, 'letsencrypt-compatibility-test.tex', + u'letsencrypt-compatibility-test Documentation', + u'Let\'s Encrypt Project', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -273,7 +274,8 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'letsencrypt-compatibility-test', u'letsencrypt-compatibility-test Documentation', + (master_doc, 'letsencrypt-compatibility-test', + u'letsencrypt-compatibility-test Documentation', [author], 1) ] @@ -287,9 +289,10 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'letsencrypt-compatibility-test', u'letsencrypt-compatibility-test Documentation', - author, 'letsencrypt-compatibility-test', 'One line description of project.', - 'Miscellaneous'), + (master_doc, 'letsencrypt-compatibility-test', + u'letsencrypt-compatibility-test Documentation', + author, 'letsencrypt-compatibility-test', + 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. @@ -309,6 +312,8 @@ intersphinx_mapping = { 'python': ('https://docs.python.org/', None), 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), 'letsencrypt': ('https://letsencrypt.readthedocs.org/en/latest/', None), - 'letsencrypt-apache': ('https://letsencrypt-apache.readthedocs.org/en/latest/', None), - 'letsencrypt-nginx': ('https://letsencrypt-nginx.readthedocs.org/en/latest/', None), + 'letsencrypt-apache': ( + 'https://letsencrypt-apache.readthedocs.org/en/latest/', None), + 'letsencrypt-nginx': ( + 'https://letsencrypt-nginx.readthedocs.org/en/latest/', None), } diff --git a/letsencrypt-compatibility-test/letsencrypt_compatibility_test/test_driver.py b/letsencrypt-compatibility-test/letsencrypt_compatibility_test/test_driver.py index b91322c3c..5765003b9 100644 --- a/letsencrypt-compatibility-test/letsencrypt_compatibility_test/test_driver.py +++ b/letsencrypt-compatibility-test/letsencrypt_compatibility_test/test_driver.py @@ -60,7 +60,7 @@ def test_authenticator(plugin, config, temp_dir): "Plugin failed to complete %s for %s in %s", type(achalls[i]), achalls[i].domain, config) success = False - elif isinstance(responses[i], challenges.DVSNIResponse): + elif isinstance(responses[i], challenges.TLSSNI01): verify = functools.partial(responses[i].simple_verify, achalls[i], achalls[i].domain, util.JWK.public_key(), @@ -68,10 +68,10 @@ def test_authenticator(plugin, config, temp_dir): port=plugin.https_port) if _try_until_true(verify): logger.info( - "DVSNI verification for %s succeeded", achalls[i].domain) + "tls-sni-01 verification for %s succeeded", achalls[i].domain) else: logger.error( - "DVSNI verification for %s in %s failed", + "tls-sni-01 verification for %s in %s failed", achalls[i].domain, config) success = False @@ -99,12 +99,12 @@ def _create_achalls(plugin): for domain in names: prefs = plugin.get_chall_pref(domain) for chall_type in prefs: - if chall_type == challenges.DVSNI: - chall = challenges.DVSNI( - token=os.urandom(challenges.DVSNI.TOKEN_SIZE)) + if chall_type == challenges.TLSSNI01: + chall = challenges.TLSSNI01( + token=os.urandom(challenges.TLSSNI01.TOKEN_SIZE)) challb = acme_util.chall_to_challb( chall, messages.STATUS_PENDING) - achall = achallenges.DVSNI( + achall = achallenges.KeyAuthorizationAnnotatedChallenge( challb=challb, domain=domain, account_key=util.JWK) achalls.append(achall) diff --git a/letsencrypt-nginx/docs/conf.py b/letsencrypt-nginx/docs/conf.py index cdb3490a0..14713a4b2 100644 --- a/letsencrypt-nginx/docs/conf.py +++ b/letsencrypt-nginx/docs/conf.py @@ -225,25 +225,25 @@ htmlhelp_basename = 'letsencrypt-nginxdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + #'preamble': '', -# Latex figure (float) alignment -#'figure_align': 'htbp', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'letsencrypt-nginx.tex', u'letsencrypt-nginx Documentation', - u'Let\'s Encrypt Project', 'manual'), + (master_doc, 'letsencrypt-nginx.tex', u'letsencrypt-nginx Documentation', + u'Let\'s Encrypt Project', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -286,9 +286,9 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'letsencrypt-nginx', u'letsencrypt-nginx Documentation', - author, 'letsencrypt-nginx', 'One line description of project.', - 'Miscellaneous'), + (master_doc, 'letsencrypt-nginx', u'letsencrypt-nginx Documentation', + author, 'letsencrypt-nginx', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. diff --git a/letsencrypt-nginx/letsencrypt_nginx/configurator.py b/letsencrypt-nginx/letsencrypt_nginx/configurator.py index 29e69e498..29445a9d4 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/configurator.py +++ b/letsencrypt-nginx/letsencrypt_nginx/configurator.py @@ -14,7 +14,6 @@ import zope.interface from acme import challenges from acme import crypto_util as acme_crypto_util -from letsencrypt import achallenges from letsencrypt import constants as core_constants from letsencrypt import crypto_util from letsencrypt import errors @@ -94,7 +93,7 @@ class NginxConfigurator(common.Plugin): # These will be set in the prepare function self.parser = None self.version = version - self._enhance_func = {} # TODO: Support at least redirects + self._enhance_func = {"redirect": self._enable_redirect} # Set up reverter self.reverter = reverter.Reverter(self.config) @@ -108,6 +107,10 @@ class NginxConfigurator(common.Plugin): # This is called in determine_authenticator and determine_installer def prepare(self): """Prepare the authenticator/installer.""" + # Verify Nginx is installed + if not le_util.exe_exists(self.conf('ctl')): + raise errors.NoInstallationError + self.parser = parser.NginxParser( self.conf('server-root'), self.mod_ssl_conf) @@ -297,7 +300,7 @@ class NginxConfigurator(common.Plugin): """Make a server SSL. Make a server SSL based on server_name and filename by adding a - ``listen IConfig.dvsni_port ssl`` directive to the server block. + ``listen IConfig.tls_sni_01_port ssl`` directive to the server block. .. todo:: Maybe this should create a new block instead of modifying the existing one? @@ -307,7 +310,7 @@ class NginxConfigurator(common.Plugin): """ snakeoil_cert, snakeoil_key = self._get_snakeoil_paths() - ssl_block = [['listen', '{0} ssl'.format(self.config.dvsni_port)], + ssl_block = [['listen', '{0} ssl'.format(self.config.tls_sni_01_port)], # access and error logs necessary for integration # testing (non-root) ['access_log', os.path.join( @@ -321,7 +324,8 @@ class NginxConfigurator(common.Plugin): vhost.filep, vhost.names, ssl_block) vhost.ssl = True vhost.raw.extend(ssl_block) - vhost.addrs.add(obj.Addr('', str(self.config.dvsni_port), True, False)) + vhost.addrs.add(obj.Addr( + '', str(self.config.tls_sni_01_port), True, False)) def get_all_certs_keys(self): """Find all existing keys, certs from configuration. @@ -340,7 +344,7 @@ class NginxConfigurator(common.Plugin): ################################## def supported_enhancements(self): # pylint: disable=no-self-use """Returns currently supported enhancements.""" - return [] + return ['redirect'] def enhance(self, domain, enhancement, options=None): """Enhance configuration. @@ -362,6 +366,26 @@ class NginxConfigurator(common.Plugin): except errors.PluginError: logger.warn("Failed %s for %s", enhancement, domain) + def _enable_redirect(self, vhost, unused_options): + """Redirect all equivalent HTTP traffic to ssl_vhost. + + Add rewrite directive to non https traffic + + .. note:: This function saves the configuration + + :param vhost: Destination of traffic, an ssl enabled vhost + :type vhost: :class:`~letsencrypt_nginx.obj.VirtualHost` + + :param unused_options: Not currently used + :type unused_options: Not Available + """ + redirect_block = [[['if', '($scheme != "https")'], + [['return', '301 https://$host$request_uri']] + ]] + self.parser.add_server_directives(vhost.filep, vhost.names, + redirect_block) + logger.info("Redirecting all traffic to ssl in %s", vhost.filep) + ###################################### # Nginx server management (IInstaller) ###################################### @@ -536,7 +560,7 @@ class NginxConfigurator(common.Plugin): ########################################################################### def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use """Return list of challenge preferences.""" - return [challenges.DVSNI] + return [challenges.TLSSNI01] # Entry point in main.py for performing challenges def perform(self, achalls): @@ -552,11 +576,10 @@ class NginxConfigurator(common.Plugin): nginx_dvsni = dvsni.NginxDvsni(self) for i, achall in enumerate(achalls): - if isinstance(achall, achallenges.DVSNI): - # Currently also have dvsni hold associated index - # of the challenge. This helps to put all of the responses back - # together when they are all complete. - nginx_dvsni.add_chall(achall, i) + # Currently also have dvsni hold associated index + # of the challenge. This helps to put all of the responses back + # together when they are all complete. + nginx_dvsni.add_chall(achall, i) sni_response = nginx_dvsni.perform() # Must restart in order to activate the challenges. diff --git a/letsencrypt-nginx/letsencrypt_nginx/dvsni.py b/letsencrypt-nginx/letsencrypt_nginx/dvsni.py index 662f10889..8fd705f08 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/dvsni.py +++ b/letsencrypt-nginx/letsencrypt_nginx/dvsni.py @@ -13,7 +13,7 @@ from letsencrypt_nginx import nginxparser logger = logging.getLogger(__name__) -class NginxDvsni(common.Dvsni): +class NginxDvsni(common.TLSSNI01): """Class performs DVSNI challenges within the Nginx configurator. :ivar configurator: NginxConfigurator object @@ -48,7 +48,7 @@ class NginxDvsni(common.Dvsni): addresses = [] default_addr = "{0} default_server ssl".format( - self.configurator.config.dvsni_port) + self.configurator.config.tls_sni_01_port) for achall in self.achalls: vhost = self.configurator.choose_vhost(achall.domain) @@ -99,8 +99,8 @@ class NginxDvsni(common.Dvsni): for key, body in main: if key == ['http']: found_bucket = False - for key, _ in body: - if key == bucket_directive[0]: + for k, _ in body: + if k == bucket_directive[0]: found_bucket = True if not found_bucket: body.insert(0, bucket_directive) @@ -141,7 +141,7 @@ class NginxDvsni(common.Dvsni): block = [['listen', str(addr)] for addr in addrs] block.extend([['server_name', - achall.gen_response(achall.account_key).z_domain], + achall.response(achall.account_key).z_domain], ['include', self.configurator.parser.loc["ssl_options"]], # access and error logs necessary for # integration testing (non-root) diff --git a/letsencrypt-nginx/letsencrypt_nginx/parser.py b/letsencrypt-nginx/letsencrypt_nginx/parser.py index 19483821a..705257c16 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/parser.py +++ b/letsencrypt-nginx/letsencrypt_nginx/parser.py @@ -246,7 +246,7 @@ class NginxParser(object): # Can't be a server block return False - if item[0] == 'server_name': + if len(item) > 0 and item[0] == 'server_name': server_names.update(_get_servernames(item[1])) return server_names == names @@ -257,6 +257,8 @@ class NginxParser(object): ..note :: If replace is True, this raises a misconfiguration error if the directive does not already exist. + ..note :: If replace is False nothing gets added if an identical + block exists already. ..todo :: Doesn't match server blocks whose server_name directives are split across multiple conf files. @@ -411,7 +413,7 @@ def _regex_match(target_name, name): return True else: return False - except re.error: + except re.error: # pragma: no cover # perl-compatible regexes are sometimes not recognized by python return False @@ -425,7 +427,7 @@ def _is_include_directive(entry): """ return (isinstance(entry, list) and - entry[0] == 'include' and len(entry) == 2 and + len(entry) == 2 and entry[0] == 'include' and isinstance(entry[1], str)) @@ -480,7 +482,9 @@ def _add_directives(block, directives, replace=False): if not replace: # We insert new directives at the top of the block, mostly # to work around https://trac.nginx.org/nginx/ticket/810 - block.insert(0, directive) + # Only add directive if its not already in the block + if directive not in block: + block.insert(0, directive) else: changed = False if len(directive) == 0: diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py index d8bdf8355..ff720ea85 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py @@ -1,3 +1,4 @@ +# pylint: disable=too-many-public-methods """Test for letsencrypt_nginx.configurator.""" import os import shutil @@ -29,6 +30,12 @@ class NginxConfiguratorTest(util.NginxTest): shutil.rmtree(self.config_dir) shutil.rmtree(self.work_dir) + @mock.patch("letsencrypt_nginx.configurator.le_util.exe_exists") + def test_prepare_no_install(self, mock_exe_exists): + mock_exe_exists.return_value = False + self.assertRaises( + errors.NoInstallationError, self.config.prepare) + def test_prepare(self): self.assertEquals((1, 6, 2), self.config.version) self.assertEquals(5, len(self.config.parser.parsed)) @@ -44,14 +51,14 @@ class NginxConfiguratorTest(util.NginxTest): "example.*", "www.example.org", "myhost"])) def test_supported_enhancements(self): - self.assertEqual([], self.config.supported_enhancements()) + self.assertEqual(['redirect'], self.config.supported_enhancements()) def test_enhance(self): self.assertRaises( - errors.PluginError, self.config.enhance, 'myhost', 'redirect') + errors.PluginError, self.config.enhance, 'myhost', 'unknown_enhancement') def test_get_chall_pref(self): - self.assertEqual([challenges.DVSNI], + self.assertEqual([challenges.TLSSNI01], self.config.get_chall_pref('myhost')) def test_save(self): @@ -210,22 +217,22 @@ class NginxConfiguratorTest(util.NginxTest): def test_perform(self, mock_restart, mock_dvsni_perform): # Only tests functionality specific to configurator.perform # Note: As more challenges are offered this will have to be expanded - achall1 = achallenges.DVSNI( + achall1 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=messages.ChallengeBody( - chall=challenges.DVSNI(token="kNdwjwOeX0I_A8DXt9Msmg"), + chall=challenges.TLSSNI01(token="kNdwjwOeX0I_A8DXt9Msmg"), uri="https://ca.org/chall0_uri", status=messages.Status("pending"), ), domain="localhost", account_key=self.rsa512jwk) - achall2 = achallenges.DVSNI( + achall2 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=messages.ChallengeBody( - chall=challenges.DVSNI(token="m8TdO1qik4JVFtgPPurJmg"), + chall=challenges.TLSSNI01(token="m8TdO1qik4JVFtgPPurJmg"), uri="https://ca.org/chall1_uri", status=messages.Status("pending"), ), domain="example.com", account_key=self.rsa512jwk) dvsni_ret_val = [ - achall1.gen_response(self.rsa512jwk), - achall2.gen_response(self.rsa512jwk), + achall1.response(self.rsa512jwk), + achall2.response(self.rsa512jwk), ] mock_dvsni_perform.return_value = dvsni_ret_val diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/dvsni_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/dvsni_test.py index 9fc0a1ad7..d32e3d98f 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/dvsni_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/dvsni_test.py @@ -19,22 +19,22 @@ from letsencrypt_nginx.tests import util class DvsniPerformTest(util.NginxTest): """Test the NginxDVSNI challenge.""" - account_key = common_test.DvsniTest.auth_key + account_key = common_test.TLSSNI01Test.auth_key achalls = [ - achallenges.DVSNI( + achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb( - challenges.DVSNI(token="kNdwjwOeX0I_A8DXt9Msmg"), "pending"), + challenges.TLSSNI01(token="kNdwjwOeX0I_A8DXt9Msmg"), "pending"), domain="www.example.com", account_key=account_key), - achallenges.DVSNI( + achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb( - challenges.DVSNI( + challenges.TLSSNI01( token="\xba\xa9\xda? 1 < 7 chars + # first and last char is not "-" + fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? - Header set Content-Type "application/jose+json" + Header set Content-Type "text/plain" @@ -32,7 +32,7 @@ nginx Use the following snippet in your ``server{...}`` stanza:: location ~ /.well-known/acme-challenge/(.*) { - default_type application/jose+json; + default_type text/plain; } and reload your daemon. diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py index 40e49702a..0a490d447 100644 --- a/letsencrypt/renewer.py +++ b/letsencrypt/renewer.py @@ -75,7 +75,7 @@ def renew(cert, old_version): # XXX: this loses type data (for example, the fact that key_size # was an int, not a str) config.rsa_key_size = int(config.rsa_key_size) - config.dvsni_port = int(config.dvsni_port) + config.tls_sni_01_port = int(config.tls_sni_01_port) config.namespace.http01_port = int(config.namespace.http01_port) zope.component.provideUtility(config) try: diff --git a/letsencrypt/tests/achallenges_test.py b/letsencrypt/tests/achallenges_test.py deleted file mode 100644 index 66b1a7ca7..000000000 --- a/letsencrypt/tests/achallenges_test.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Tests for letsencrypt.achallenges.""" -import unittest - -import OpenSSL - -from acme import challenges -from acme import jose - -from letsencrypt.tests import acme_util -from letsencrypt.tests import test_util - - -class DVSNITest(unittest.TestCase): - """Tests for letsencrypt.achallenges.DVSNI.""" - - def setUp(self): - self.challb = acme_util.chall_to_challb(acme_util.DVSNI, "pending") - key = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) - from letsencrypt.achallenges import DVSNI - self.achall = DVSNI( - challb=self.challb, domain="example.com", account_key=key) - - def test_proxy(self): - self.assertEqual(self.challb.token, self.achall.token) - - def test_gen_cert_and_response(self): - response, cert, key = self.achall.gen_cert_and_response() - self.assertTrue(isinstance(response, challenges.DVSNIResponse)) - self.assertTrue(isinstance(cert, OpenSSL.crypto.X509)) - self.assertTrue(isinstance(key, OpenSSL.crypto.PKey)) - - -if __name__ == "__main__": - unittest.main() # pragma: no cover diff --git a/letsencrypt/tests/acme_util.py b/letsencrypt/tests/acme_util.py index 300eb453b..6b07b840f 100644 --- a/letsencrypt/tests/acme_util.py +++ b/letsencrypt/tests/acme_util.py @@ -14,7 +14,7 @@ KEY = test_util.load_rsa_private_key('rsa512_key.pem') # Challenges HTTP01 = challenges.HTTP01( token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") -DVSNI = challenges.DVSNI( +TLSSNI01 = challenges.TLSSNI01( token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA")) DNS = challenges.DNS(token="17817c66b60ce2e4012dfad92657527a") RECOVERY_CONTACT = challenges.RecoveryContact( @@ -41,7 +41,7 @@ POP = challenges.ProofOfPossession( ) ) -CHALLENGES = [HTTP01, DVSNI, DNS, RECOVERY_CONTACT, POP] +CHALLENGES = [HTTP01, TLSSNI01, DNS, RECOVERY_CONTACT, POP] DV_CHALLENGES = [chall for chall in CHALLENGES if isinstance(chall, challenges.DVChallenge)] CONT_CHALLENGES = [chall for chall in CHALLENGES @@ -79,13 +79,13 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name # Pending ChallengeBody objects -DVSNI_P = chall_to_challb(DVSNI, messages.STATUS_PENDING) +TLSSNI01_P = chall_to_challb(TLSSNI01, messages.STATUS_PENDING) HTTP01_P = chall_to_challb(HTTP01, messages.STATUS_PENDING) DNS_P = chall_to_challb(DNS, messages.STATUS_PENDING) RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, messages.STATUS_PENDING) POP_P = chall_to_challb(POP, messages.STATUS_PENDING) -CHALLENGES_P = [HTTP01_P, DVSNI_P, DNS_P, RECOVERY_CONTACT_P, POP_P] +CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS_P, RECOVERY_CONTACT_P, POP_P] DV_CHALLENGES_P = [challb for challb in CHALLENGES_P if isinstance(challb.chall, challenges.DVChallenge)] CONT_CHALLENGES_P = [ diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 7be37c91e..be19ab036 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -45,7 +45,7 @@ class ChallengeFactoryTest(unittest.TestCase): self.assertEqual( [achall.chall for achall in cont_c], [acme_util.RECOVERY_CONTACT]) - self.assertEqual([achall.chall for achall in dv_c], [acme_util.DVSNI]) + self.assertEqual([achall.chall for achall in dv_c], [acme_util.TLSSNI01]) def test_unrecognized(self): self.handler.authzr["failure.com"] = acme_util.gen_authzr( @@ -70,7 +70,7 @@ class GetAuthorizationsTest(unittest.TestCase): self.mock_dv_auth = mock.MagicMock(name="ApacheConfigurator") self.mock_cont_auth = mock.MagicMock(name="ContinuityAuthenticator") - self.mock_dv_auth.get_chall_pref.return_value = [challenges.DVSNI] + self.mock_dv_auth.get_chall_pref.return_value = [challenges.TLSSNI01] self.mock_cont_auth.get_chall_pref.return_value = [ challenges.RecoveryContact] @@ -90,7 +90,7 @@ class GetAuthorizationsTest(unittest.TestCase): logging.disable(logging.NOTSET) @mock.patch("letsencrypt.auth_handler.AuthHandler._poll_challenges") - def test_name1_dvsni1(self, mock_poll): + def test_name1_tls_sni_01_1(self, mock_poll): self.mock_net.request_domain_challenges.side_effect = functools.partial( gen_dom_authzr, challs=acme_util.DV_CHALLENGES) @@ -107,14 +107,14 @@ class GetAuthorizationsTest(unittest.TestCase): self.assertEqual(self.mock_dv_auth.cleanup.call_count, 1) self.assertEqual(self.mock_cont_auth.cleanup.call_count, 0) - # Test if list first element is DVSNI, use typ because it is an achall + # Test if list first element is TLSSNI01, use typ because it is an achall self.assertEqual( - self.mock_dv_auth.cleanup.call_args[0][0][0].typ, "dvsni") + self.mock_dv_auth.cleanup.call_args[0][0][0].typ, "tls-sni-01") self.assertEqual(len(authzr), 1) @mock.patch("letsencrypt.auth_handler.AuthHandler._poll_challenges") - def test_name3_dvsni3_rectok_3(self, mock_poll): + def test_name3_tls_sni_01_3_rectok_3(self, mock_poll): self.mock_net.request_domain_challenges.side_effect = functools.partial( gen_dom_authzr, challs=acme_util.CHALLENGES) @@ -309,9 +309,9 @@ class GenChallengePathTest(unittest.TestCase): return gen_challenge_path(challbs, preferences, combinations) def test_common_case(self): - """Given DVSNI and HTTP01 with appropriate combos.""" - challbs = (acme_util.DVSNI_P, acme_util.HTTP01_P) - prefs = [challenges.DVSNI] + """Given TLSSNI01 and HTTP01 with appropriate combos.""" + challbs = (acme_util.TLSSNI01_P, acme_util.HTTP01_P) + prefs = [challenges.TLSSNI01] combos = ((0,), (1,)) # Smart then trivial dumb path test @@ -324,9 +324,9 @@ class GenChallengePathTest(unittest.TestCase): def test_common_case_with_continuity(self): challbs = (acme_util.POP_P, acme_util.RECOVERY_CONTACT_P, - acme_util.DVSNI_P, + acme_util.TLSSNI01_P, acme_util.HTTP01_P) - prefs = [challenges.ProofOfPossession, challenges.DVSNI] + prefs = [challenges.ProofOfPossession, challenges.TLSSNI01] combos = acme_util.gen_combos(challbs) self.assertEqual(self._call(challbs, prefs, combos), (0, 2)) @@ -336,14 +336,14 @@ class GenChallengePathTest(unittest.TestCase): def test_full_cont_server(self): challbs = (acme_util.RECOVERY_CONTACT_P, acme_util.POP_P, - acme_util.DVSNI_P, + acme_util.TLSSNI01_P, acme_util.HTTP01_P, acme_util.DNS_P) # Typical webserver client that can do everything except DNS # Attempted to make the order realistic prefs = [challenges.ProofOfPossession, challenges.HTTP01, - challenges.DVSNI, + challenges.TLSSNI01, challenges.RecoveryContact] combos = acme_util.gen_combos(challbs) self.assertEqual(self._call(challbs, prefs, combos), (1, 3)) @@ -352,8 +352,8 @@ class GenChallengePathTest(unittest.TestCase): self.assertTrue(self._call(challbs, prefs, None)) def test_not_supported(self): - challbs = (acme_util.POP_P, acme_util.DVSNI_P) - prefs = [challenges.DVSNI] + challbs = (acme_util.POP_P, acme_util.TLSSNI01_P) + prefs = [challenges.TLSSNI01] combos = ((0, 1),) self.assertRaises( @@ -411,7 +411,7 @@ class IsPreferredTest(unittest.TestCase): def _call(cls, chall, satisfied): from letsencrypt.auth_handler import is_preferred return is_preferred(chall, satisfied, exclusive_groups=frozenset([ - frozenset([challenges.DVSNI, challenges.HTTP01]), + frozenset([challenges.TLSSNI01, challenges.HTTP01]), frozenset([challenges.DNS, challenges.HTTP01]), ])) @@ -421,11 +421,11 @@ class IsPreferredTest(unittest.TestCase): def test_mutually_exclusvie(self): self.assertFalse( self._call( - acme_util.DVSNI_P, frozenset([acme_util.HTTP01_P]))) + acme_util.TLSSNI01_P, frozenset([acme_util.HTTP01_P]))) def test_mutually_exclusive_same_type(self): self.assertTrue( - self._call(acme_util.DVSNI_P, frozenset([acme_util.DVSNI_P]))) + self._call(acme_util.TLSSNI01_P, frozenset([acme_util.TLSSNI01_P]))) class ReportFailedChallsTest(unittest.TestCase): @@ -446,15 +446,15 @@ class ReportFailedChallsTest(unittest.TestCase): domain="example.com", account_key="key") - kwargs["chall"] = acme_util.DVSNI - self.dvsni_same = achallenges.DVSNI( + kwargs["chall"] = acme_util.TLSSNI01 + self.tls_sni_same = achallenges.KeyAuthorizationAnnotatedChallenge( # pylint: disable=star-args challb=messages.ChallengeBody(**kwargs), domain="example.com", account_key="key") kwargs["error"] = messages.Error(typ="dnssec", detail="detail") - self.dvsni_diff = achallenges.DVSNI( + self.tls_sni_diff = achallenges.KeyAuthorizationAnnotatedChallenge( # pylint: disable=star-args challb=messages.ChallengeBody(**kwargs), domain="foo.bar", @@ -464,7 +464,7 @@ class ReportFailedChallsTest(unittest.TestCase): def test_same_error_and_domain(self, mock_zope): from letsencrypt import auth_handler - auth_handler._report_failed_challs([self.http01, self.dvsni_same]) + auth_handler._report_failed_challs([self.http01, self.tls_sni_same]) call_list = mock_zope().add_message.call_args_list self.assertTrue(len(call_list) == 1) self.assertTrue("Domains: example.com\n" in call_list[0][0][0]) @@ -473,7 +473,7 @@ class ReportFailedChallsTest(unittest.TestCase): def test_different_errors_and_domains(self, mock_zope): from letsencrypt import auth_handler - auth_handler._report_failed_challs([self.http01, self.dvsni_diff]) + auth_handler._report_failed_challs([self.http01, self.tls_sni_diff]) self.assertTrue(mock_zope().add_message.call_count == 2) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index d6d056777..71b580cf0 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -1,4 +1,5 @@ """Tests for letsencrypt.cli.""" +import argparse import itertools import os import shutil @@ -9,20 +10,28 @@ import unittest import mock +from acme import jose + from letsencrypt import account +from letsencrypt import cli from letsencrypt import configuration +from letsencrypt import crypto_util from letsencrypt import errors +from letsencrypt import le_util from letsencrypt.plugins import disco +from letsencrypt.plugins import manual from letsencrypt.tests import renewer_test from letsencrypt.tests import test_util +CERT = test_util.vector_path('cert.pem') CSR = test_util.vector_path('csr.der') +KEY = test_util.vector_path('rsa256_key.pem') -class CLITest(unittest.TestCase): +class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods """Tests for different commands.""" def setUp(self): @@ -30,33 +39,36 @@ class CLITest(unittest.TestCase): self.config_dir = os.path.join(self.tmp_dir, 'config') self.work_dir = os.path.join(self.tmp_dir, 'work') self.logs_dir = os.path.join(self.tmp_dir, 'logs') + self.standard_args = ['--text', '--config-dir', self.config_dir, + '--work-dir', self.work_dir, '--logs-dir', self.logs_dir, + '--agree-dev-preview'] def tearDown(self): shutil.rmtree(self.tmp_dir) def _call(self, args): - from letsencrypt import cli - args = ['--text', '--config-dir', self.config_dir, - '--work-dir', self.work_dir, '--logs-dir', self.logs_dir, - '--agree-dev-preview'] + args + "Run the cli with output streams and actual client mocked out" + with mock.patch('letsencrypt.cli.client') as client: + ret, stdout, stderr = self._call_no_clientmock(args) + return ret, stdout, stderr, client + + def _call_no_clientmock(self, args): + "Run the client with output streams mocked out" + args = self.standard_args + args with mock.patch('letsencrypt.cli.sys.stdout') as stdout: with mock.patch('letsencrypt.cli.sys.stderr') as stderr: - with mock.patch('letsencrypt.cli.client') as client: - ret = cli.main(args) - return ret, stdout, stderr, client + ret = cli.main(args[:]) # NOTE: parser can alter its args! + return ret, stdout, stderr def _call_stdout(self, args): """ Variant of _call that preserves stdout so that it can be mocked by the caller. """ - from letsencrypt import cli - args = ['--text', '--config-dir', self.config_dir, - '--work-dir', self.work_dir, '--logs-dir', self.logs_dir, - '--agree-dev-preview'] + args + args = self.standard_args + args with mock.patch('letsencrypt.cli.sys.stderr') as stderr: with mock.patch('letsencrypt.cli.client') as client: - ret = cli.main(args) + ret = cli.main(args[:]) # NOTE: parser can alter its args! return ret, None, stderr, client def test_no_flags(self): @@ -112,16 +124,61 @@ class CLITest(unittest.TestCase): self.assertTrue("--key-path" not in out) out = self._help_output(['-h']) - from letsencrypt import cli self.assertTrue(cli.usage_strings(plugins)[0] in out) + @mock.patch('letsencrypt.cli.client.acme_client.Client') + @mock.patch('letsencrypt.cli._determine_account') + @mock.patch('letsencrypt.cli.client.Client.obtain_and_enroll_certificate') + @mock.patch('letsencrypt.cli._auth_from_domains') + def test_user_agent(self, _afd, _obt, det, _client): + # Normally the client is totally mocked out, but here we need more + # arguments to automate it... + args = ["--standalone", "certonly", "-m", "none@none.com", + "-d", "example.com", '--agree-tos'] + self.standard_args + det.return_value = mock.MagicMock(), None + with mock.patch('letsencrypt.cli.client.acme_client.ClientNetwork') as acme_net: + self._call_no_clientmock(args) + os_ver = " ".join(le_util.get_os_info()) + ua = acme_net.call_args[1]["user_agent"] + self.assertTrue(os_ver in ua) + import platform + plat = platform.platform() + if "linux" in plat.lower(): + self.assertTrue(platform.linux_distribution()[0] in ua) + + with mock.patch('letsencrypt.cli.client.acme_client.ClientNetwork') as acme_net: + ua = "bandersnatch" + args += ["--user-agent", ua] + self._call_no_clientmock(args) + acme_net.assert_called_once_with(mock.ANY, verify_ssl=True, user_agent=ua) + + def test_install_abspath(self): + cert = 'cert' + key = 'key' + chain = 'chain' + fullchain = 'fullchain' + + with MockedVerb('install') as mock_install: + self._call(['install', '--cert-path', cert, '--key-path', 'key', + '--chain-path', 'chain', + '--fullchain-path', 'fullchain']) + + args = mock_install.call_args[0][0] + self.assertEqual(args.cert_path, os.path.abspath(cert)) + self.assertEqual(args.key_path, os.path.abspath(key)) + self.assertEqual(args.chain_path, os.path.abspath(chain)) + self.assertEqual(args.fullchain_path, os.path.abspath(fullchain)) + + @mock.patch('letsencrypt.cli.record_chosen_plugins') @mock.patch('letsencrypt.cli.display_ops') - def test_installer_selection(self, mock_display_ops): - self._call(['install', '--domain', 'foo.bar', '--cert-path', 'cert', + def test_installer_selection(self, mock_display_ops, _rec): + self._call(['install', '--domains', 'foo.bar', '--cert-path', 'cert', '--key-path', 'key', '--chain-path', 'chain']) self.assertEqual(mock_display_ops.pick_installer.call_count, 1) - def test_configurator_selection(self): + @mock.patch('letsencrypt.le_util.exe_exists') + def test_configurator_selection(self, mock_exe_exists): + mock_exe_exists.return_value = True real_plugins = disco.PluginsRegistry.find_all() args = ['--agree-dev-preview', '--apache', '--authenticator', 'standalone'] @@ -145,6 +202,16 @@ class CLITest(unittest.TestCase): self.assertTrue("Could not find configuration root" in ret) self.assertTrue("NoInstallationError" in ret) + args = ["certonly", "--webroot"] + ret, _, _, _ = self._call(args) + self.assertTrue("--webroot-path must be set" in ret) + + with mock.patch("letsencrypt.cli._init_le_client") as mock_init: + with mock.patch("letsencrypt.cli._auth_from_domains"): + self._call(["certonly", "--manual", "-d", "foo.bar"]) + auth = mock_init.call_args[0][2] + self.assertTrue(isinstance(auth, manual.Authenticator)) + with MockedVerb("certonly") as mock_certonly: self._call(["auth", "--standalone"]) self.assertEqual(1, mock_certonly.call_count) @@ -168,6 +235,65 @@ class CLITest(unittest.TestCase): for r in xrange(len(flags)))): self._call(['plugins'] + list(args)) + @mock.patch('letsencrypt.cli.plugins_disco') + def test_plugins_no_args(self, mock_disco): + ifaces = [] + plugins = mock_disco.PluginsRegistry.find_all() + + _, stdout, _, _ = self._call(['plugins']) + plugins.visible.assert_called_once_with() + plugins.visible().ifaces.assert_called_once_with(ifaces) + filtered = plugins.visible().ifaces() + stdout.write.called_once_with(str(filtered)) + + @mock.patch('letsencrypt.cli.plugins_disco') + def test_plugins_init(self, mock_disco): + ifaces = [] + plugins = mock_disco.PluginsRegistry.find_all() + + _, stdout, _, _ = self._call(['plugins', '--init']) + plugins.visible.assert_called_once_with() + plugins.visible().ifaces.assert_called_once_with(ifaces) + filtered = plugins.visible().ifaces() + self.assertEqual(filtered.init.call_count, 1) + filtered.verify.assert_called_once_with(ifaces) + verified = filtered.verify() + stdout.write.called_once_with(str(verified)) + + @mock.patch('letsencrypt.cli.plugins_disco') + def test_plugins_prepare(self, mock_disco): + ifaces = [] + plugins = mock_disco.PluginsRegistry.find_all() + + _, stdout, _, _ = self._call(['plugins', '--init', '--prepare']) + plugins.visible.assert_called_once_with() + plugins.visible().ifaces.assert_called_once_with(ifaces) + filtered = plugins.visible().ifaces() + self.assertEqual(filtered.init.call_count, 1) + filtered.verify.assert_called_once_with(ifaces) + verified = filtered.verify() + verified.prepare.assert_called_once_with() + verified.available.assert_called_once_with() + available = verified.available() + stdout.write.called_once_with(str(available)) + + def test_certonly_abspath(self): + cert = 'cert' + key = 'key' + chain = 'chain' + fullchain = 'fullchain' + + with MockedVerb('certonly') as mock_obtaincert: + self._call(['certonly', '--cert-path', cert, '--key-path', 'key', + '--chain-path', 'chain', + '--fullchain-path', 'fullchain']) + + args = mock_obtaincert.call_args[0][0] + self.assertEqual(args.cert_path, os.path.abspath(cert)) + self.assertEqual(args.key_path, os.path.abspath(key)) + self.assertEqual(args.chain_path, os.path.abspath(chain)) + self.assertEqual(args.fullchain_path, os.path.abspath(fullchain)) + def test_certonly_bad_args(self): ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR]) self.assertEqual(ret, '--domains and --csr are mutually exclusive') @@ -175,6 +301,44 @@ class CLITest(unittest.TestCase): ret, _, _, _ = self._call(['-a', 'bad_auth', 'certonly']) self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed') + def test_check_config_sanity_domain(self): + # Punycode + self.assertRaises(errors.ConfigurationError, + self._call, + ['-d', 'this.is.xn--ls8h.tld']) + # FQDN + self.assertRaises(errors.ConfigurationError, + self._call, + ['-d', 'comma,gotwrong.tld']) + # FQDN 2 + self.assertRaises(errors.ConfigurationError, + self._call, + ['-d', 'illegal.character=.tld']) + # Wildcard + self.assertRaises(errors.ConfigurationError, + self._call, + ['-d', '*.wildcard.tld']) + + def test_parse_domains(self): + plugins = disco.PluginsRegistry.find_all() + + short_args = ['-d', 'example.com'] + namespace = cli.prepare_and_parse_args(plugins, short_args) + self.assertEqual(namespace.domains, ['example.com']) + + short_args = ['-d', 'example.com,another.net,third.org,example.com'] + namespace = cli.prepare_and_parse_args(plugins, short_args) + self.assertEqual(namespace.domains, ['example.com', 'another.net', + 'third.org']) + + long_args = ['--domains', 'example.com'] + namespace = cli.prepare_and_parse_args(plugins, long_args) + self.assertEqual(namespace.domains, ['example.com']) + + long_args = ['--domains', 'example.com,another.net,example.com'] + namespace = cli.prepare_and_parse_args(plugins, long_args) + self.assertEqual(namespace.domains, ['example.com', 'another.net']) + @mock.patch('letsencrypt.crypto_util.notAfter') @mock.patch('letsencrypt.cli.zope.component.getUtility') def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter): @@ -235,7 +399,8 @@ class CLITest(unittest.TestCase): @mock.patch('letsencrypt.cli.display_ops.pick_installer') @mock.patch('letsencrypt.cli.zope.component.getUtility') @mock.patch('letsencrypt.cli._init_le_client') - def test_certonly_csr(self, mock_init, mock_get_utility, + @mock.patch('letsencrypt.cli.record_chosen_plugins') + def test_certonly_csr(self, _rec, mock_init, mock_get_utility, mock_pick_installer, mock_notAfter): cert_path = '/etc/letsencrypt/live/blahcert.pem' date = '1970-01-01' @@ -260,11 +425,31 @@ class CLITest(unittest.TestCase): self.assertTrue( date in mock_get_utility().add_message.call_args[0][0]) + @mock.patch('letsencrypt.cli.client.acme_client') + def test_revoke_with_key(self, mock_acme_client): + server = 'foo.bar' + self._call_no_clientmock(['--cert-path', CERT, '--key-path', KEY, + '--server', server, 'revoke']) + with open(KEY) as f: + mock_acme_client.Client.assert_called_once_with( + server, key=jose.JWK.load(f.read()), net=mock.ANY) + with open(CERT) as f: + cert = crypto_util.pyopenssl_load_certificate(f.read())[0] + mock_revoke = mock_acme_client.Client().revoke + mock_revoke.assert_called_once_with(jose.ComparableX509(cert)) + + @mock.patch('letsencrypt.cli._determine_account') + def test_revoke_without_key(self, mock_determine_account): + mock_determine_account.return_value = (mock.MagicMock(), None) + _, _, _, client = self._call(['--cert-path', CERT, 'revoke']) + with open(CERT) as f: + cert = crypto_util.pyopenssl_load_certificate(f.read())[0] + mock_revoke = client.acme_from_config_key().revoke + mock_revoke.assert_called_once_with(jose.ComparableX509(cert)) + @mock.patch('letsencrypt.cli.sys') def test_handle_exception(self, mock_sys): # pylint: disable=protected-access - from letsencrypt import cli - mock_open = mock.mock_open() with mock.patch('letsencrypt.cli.open', mock_open, create=True): exception = Exception('detail') @@ -296,6 +481,19 @@ class CLITest(unittest.TestCase): mock_sys.exit.assert_called_with(''.join( traceback.format_exception_only(KeyboardInterrupt, interrupt))) + def test_read_file(self): + rel_test_path = os.path.relpath(os.path.join(self.tmp_dir, 'foo')) + self.assertRaises( + argparse.ArgumentTypeError, cli.read_file, rel_test_path) + + test_contents = 'bar\n' + with open(rel_test_path, 'w') as f: + f.write(test_contents) + + path, contents = cli.read_file(rel_test_path) + self.assertEqual(path, os.path.abspath(path)) + self.assertEqual(contents, test_contents) + class DetermineAccountTest(unittest.TestCase): """Tests for letsencrypt.cli._determine_account.""" @@ -415,8 +613,6 @@ class MockedVerb(object): """ def __init__(self, verb_name): - from letsencrypt import cli - self.verb_dict = cli.HelpfulArgumentParser.VERBS self.verb_func = None self.verb_name = verb_name diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index b2ebe74f0..d396e25bc 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -70,8 +70,8 @@ class ClientTest(unittest.TestCase): dv_auth=None, installer=None) def test_init_acme_verify_ssl(self): - self.acme_client.assert_called_once_with( - directory=mock.ANY, key=mock.ANY, verify_ssl=True) + net = self.acme_client.call_args[1]["net"] + self.assertTrue(net.verify_ssl) def _mock_obtain_certificate(self): self.client.auth_handler = mock.MagicMock() @@ -148,7 +148,7 @@ class ClientTest(unittest.TestCase): shutil.rmtree(tmp_path) - def test_deploy_certificate(self): + def test_deploy_certificate_success(self): self.assertRaises(errors.Error, self.client.deploy_certificate, ["foo.bar"], "key", "cert", "chain", "fullchain") @@ -166,6 +166,49 @@ class ClientTest(unittest.TestCase): self.assertEqual(installer.save.call_count, 2) installer.restart.assert_called_once_with() + def test_deploy_certificate_failure(self): + installer = mock.MagicMock() + self.client.installer = installer + + installer.deploy_cert.side_effect = errors.PluginError + self.assertRaises(errors.PluginError, self.client.deploy_certificate, + ["foo.bar"], "key", "cert", "chain", "fullchain") + installer.recovery_routine.assert_called_once_with() + + def test_deploy_certificate_save_failure(self): + installer = mock.MagicMock() + self.client.installer = installer + + installer.save.side_effect = errors.PluginError + self.assertRaises(errors.PluginError, self.client.deploy_certificate, + ["foo.bar"], "key", "cert", "chain", "fullchain") + installer.recovery_routine.assert_called_once_with() + + @mock.patch("letsencrypt.client.zope.component.getUtility") + def test_deploy_certificate_restart_failure(self, mock_get_utility): + installer = mock.MagicMock() + installer.restart.side_effect = [errors.PluginError, None] + self.client.installer = installer + + self.assertRaises(errors.PluginError, self.client.deploy_certificate, + ["foo.bar"], "key", "cert", "chain", "fullchain") + self.assertEqual(mock_get_utility().add_message.call_count, 1) + installer.rollback_checkpoints.assert_called_once_with() + self.assertEqual(installer.restart.call_count, 2) + + @mock.patch("letsencrypt.client.zope.component.getUtility") + def test_deploy_certificate_restart_failure2(self, mock_get_utility): + installer = mock.MagicMock() + installer.restart.side_effect = errors.PluginError + installer.rollback_checkpoints.side_effect = errors.ReverterError + self.client.installer = installer + + self.assertRaises(errors.PluginError, self.client.deploy_certificate, + ["foo.bar"], "key", "cert", "chain", "fullchain") + self.assertEqual(mock_get_utility().add_message.call_count, 1) + installer.rollback_checkpoints.assert_called_once_with() + self.assertEqual(installer.restart.call_count, 1) + @mock.patch("letsencrypt.client.enhancements") def test_enhance_config(self, mock_enhancements): self.assertRaises(errors.Error, @@ -180,10 +223,68 @@ class ClientTest(unittest.TestCase): self.assertEqual(installer.save.call_count, 1) installer.restart.assert_called_once_with() + def test_enhance_config_no_installer(self): + self.assertRaises(errors.Error, + self.client.enhance_config, ["foo.bar"]) + + @mock.patch("letsencrypt.client.zope.component.getUtility") + @mock.patch("letsencrypt.client.enhancements") + def test_enhance_config_enhance_failure(self, mock_enhancements, + mock_get_utility): + mock_enhancements.ask.return_value = True + installer = mock.MagicMock() + self.client.installer = installer installer.enhance.side_effect = errors.PluginError + self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], True) installer.recovery_routine.assert_called_once_with() + self.assertEqual(mock_get_utility().add_message.call_count, 1) + + @mock.patch("letsencrypt.client.zope.component.getUtility") + @mock.patch("letsencrypt.client.enhancements") + def test_enhance_config_save_failure(self, mock_enhancements, + mock_get_utility): + mock_enhancements.ask.return_value = True + installer = mock.MagicMock() + self.client.installer = installer + installer.save.side_effect = errors.PluginError + + self.assertRaises(errors.PluginError, + self.client.enhance_config, ["foo.bar"], True) + installer.recovery_routine.assert_called_once_with() + self.assertEqual(mock_get_utility().add_message.call_count, 1) + + @mock.patch("letsencrypt.client.zope.component.getUtility") + @mock.patch("letsencrypt.client.enhancements") + def test_enhance_config_restart_failure(self, mock_enhancements, + mock_get_utility): + mock_enhancements.ask.return_value = True + installer = mock.MagicMock() + self.client.installer = installer + installer.restart.side_effect = [errors.PluginError, None] + + self.assertRaises(errors.PluginError, + self.client.enhance_config, ["foo.bar"], True) + self.assertEqual(mock_get_utility().add_message.call_count, 1) + installer.rollback_checkpoints.assert_called_once_with() + self.assertEqual(installer.restart.call_count, 2) + + @mock.patch("letsencrypt.client.zope.component.getUtility") + @mock.patch("letsencrypt.client.enhancements") + def test_enhance_config_restart_failure2(self, mock_enhancements, + mock_get_utility): + mock_enhancements.ask.return_value = True + installer = mock.MagicMock() + self.client.installer = installer + installer.restart.side_effect = errors.PluginError + installer.rollback_checkpoints.side_effect = errors.ReverterError + + self.assertRaises(errors.PluginError, + self.client.enhance_config, ["foo.bar"], True) + self.assertEqual(mock_get_utility().add_message.call_count, 1) + installer.rollback_checkpoints.assert_called_once_with() + self.assertEqual(installer.restart.call_count, 1) class RollbackTest(unittest.TestCase): diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index c7e227ee5..c42b99081 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -14,12 +14,12 @@ class NamespaceConfigTest(unittest.TestCase): self.namespace = mock.MagicMock( config_dir='/tmp/config', work_dir='/tmp/foo', foo='bar', server='https://acme-server.org:443/new', - dvsni_port=1234, http01_port=4321) + tls_sni_01_port=1234, http01_port=4321) from letsencrypt.configuration import NamespaceConfig self.config = NamespaceConfig(self.namespace) def test_init_same_ports(self): - self.namespace.dvsni_port = 4321 + self.namespace.tls_sni_01_port = 4321 from letsencrypt.configuration import NamespaceConfig self.assertRaises(errors.Error, NamespaceConfig, self.namespace) @@ -59,6 +59,38 @@ class NamespaceConfigTest(unittest.TestCase): self.namespace.http01_port = None self.assertEqual(80, self.config.http01_port) + def test_absolute_paths(self): + from letsencrypt.configuration import NamespaceConfig + + config_base = "foo" + work_base = "bar" + logs_base = "baz" + + mock_namespace = mock.MagicMock(spec=['config_dir', 'work_dir', + 'logs_dir', 'http01_port', + 'tls_sni_01_port', + 'domains', 'server']) + mock_namespace.config_dir = config_base + mock_namespace.work_dir = work_base + mock_namespace.logs_dir = logs_base + config = NamespaceConfig(mock_namespace) + + self.assertTrue(os.path.isabs(config.config_dir)) + self.assertEqual(config.config_dir, + os.path.join(os.getcwd(), config_base)) + self.assertTrue(os.path.isabs(config.work_dir)) + self.assertEqual(config.work_dir, + os.path.join(os.getcwd(), work_base)) + self.assertTrue(os.path.isabs(config.logs_dir)) + self.assertEqual(config.logs_dir, + os.path.join(os.getcwd(), logs_base)) + self.assertTrue(os.path.isabs(config.accounts_dir)) + self.assertTrue(os.path.isabs(config.backup_dir)) + self.assertTrue(os.path.isabs(config.csr_dir)) + self.assertTrue(os.path.isabs(config.in_progress_dir)) + self.assertTrue(os.path.isabs(config.key_dir)) + self.assertTrue(os.path.isabs(config.temp_checkpoint_dir)) + class RenewerConfigurationTest(unittest.TestCase): """Test for letsencrypt.configuration.RenewerConfiguration.""" @@ -81,6 +113,28 @@ class RenewerConfigurationTest(unittest.TestCase): self.config.renewal_configs_dir, '/tmp/config/renewal_configs') self.assertEqual(self.config.renewer_config_file, '/tmp/config/r.conf') + def test_absolute_paths(self): + from letsencrypt.configuration import NamespaceConfig + from letsencrypt.configuration import RenewerConfiguration + + config_base = "foo" + work_base = "bar" + logs_base = "baz" + + mock_namespace = mock.MagicMock(spec=['config_dir', 'work_dir', + 'logs_dir', 'http01_port', + 'tls_sni_01_port', + 'domains', 'server']) + mock_namespace.config_dir = config_base + mock_namespace.work_dir = work_base + mock_namespace.logs_dir = logs_base + config = RenewerConfiguration(NamespaceConfig(mock_namespace)) + + self.assertTrue(os.path.isabs(config.archive_dir)) + self.assertTrue(os.path.isabs(config.live_dir)) + self.assertTrue(os.path.isabs(config.renewal_configs_dir)) + self.assertTrue(os.path.isabs(config.renewer_config_file)) + if __name__ == '__main__': unittest.main() # pragma: no cover diff --git a/letsencrypt/tests/continuity_auth_test.py b/letsencrypt/tests/continuity_auth_test.py index d80a1cfb4..70287bd01 100644 --- a/letsencrypt/tests/continuity_auth_test.py +++ b/letsencrypt/tests/continuity_auth_test.py @@ -34,7 +34,7 @@ class PerformTest(unittest.TestCase): def test_unexpected(self): self.assertRaises( errors.ContAuthError, self.auth.perform, [ - achallenges.DVSNI( + achallenges.KeyAuthorizationAnnotatedChallenge( challb=None, domain="0", account_key="invalid_key")]) def test_chall_pref(self): @@ -53,7 +53,7 @@ class CleanupTest(unittest.TestCase): mock.MagicMock(server="demo_server.org"), None) def test_unexpected(self): - unexpected = achallenges.DVSNI( + unexpected = achallenges.KeyAuthorizationAnnotatedChallenge( challb=None, domain="0", account_key="dummy_key") self.assertRaises(errors.ContAuthError, self.auth.cleanup, [unexpected]) diff --git a/letsencrypt/tests/error_handler_test.py b/letsencrypt/tests/error_handler_test.py index c92f12435..7fbdcffd8 100644 --- a/letsencrypt/tests/error_handler_test.py +++ b/letsencrypt/tests/error_handler_test.py @@ -13,7 +13,11 @@ class ErrorHandlerTest(unittest.TestCase): from letsencrypt import error_handler self.init_func = mock.MagicMock() - self.handler = error_handler.ErrorHandler(self.init_func) + self.init_args = set((42,)) + self.init_kwargs = {'foo': 'bar'} + self.handler = error_handler.ErrorHandler(self.init_func, + *self.init_args, + **self.init_kwargs) # pylint: disable=protected-access self.signals = error_handler._SIGNALS @@ -23,7 +27,8 @@ class ErrorHandlerTest(unittest.TestCase): raise ValueError except ValueError: pass - self.init_func.assert_called_once_with() + self.init_func.assert_called_once_with(*self.init_args, + **self.init_kwargs) @mock.patch('letsencrypt.error_handler.os') @mock.patch('letsencrypt.error_handler.signal') @@ -37,7 +42,8 @@ class ErrorHandlerTest(unittest.TestCase): signum = self.signals[0] signal_handler(signum, None) - self.init_func.assert_called_once_with() + self.init_func.assert_called_once_with(*self.init_args, + **self.init_kwargs) mock_os.kill.assert_called_once_with(mock_os.getpid(), signum) self.handler.reset_signal_handlers() @@ -48,7 +54,8 @@ class ErrorHandlerTest(unittest.TestCase): bad_func = mock.MagicMock(side_effect=[ValueError]) self.handler.register(bad_func) self.handler.call_registered() - self.init_func.assert_called_once_with() + self.init_func.assert_called_once_with(*self.init_args, + **self.init_kwargs) bad_func.assert_called_once_with() def test_sysexit_ignored(self): diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index 05d7e123d..daec9678f 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -688,9 +688,13 @@ class RenewableCertTests(BaseRenewableCertTest): self.test_rc.configfile["renewalparams"]["rsa_key_size"] = "2048" self.test_rc.configfile["renewalparams"]["server"] = "acme.example.com" self.test_rc.configfile["renewalparams"]["authenticator"] = "fake" - self.test_rc.configfile["renewalparams"]["dvsni_port"] = "4430" + self.test_rc.configfile["renewalparams"]["tls_sni_01_port"] = "4430" self.test_rc.configfile["renewalparams"]["http01_port"] = "1234" self.test_rc.configfile["renewalparams"]["account"] = "abcde" + self.test_rc.configfile["renewalparams"]["domains"] = ["example.com"] + self.test_rc.configfile["renewalparams"]["config_dir"] = "config" + self.test_rc.configfile["renewalparams"]["work_dir"] = "work" + self.test_rc.configfile["renewalparams"]["logs_dir"] = "logs" mock_auth = mock.MagicMock() mock_pd.PluginsRegistry.find_all.return_value = {"apache": mock_auth} # Fails because "fake" != "apache" diff --git a/letshelp-letsencrypt/docs/conf.py b/letshelp-letsencrypt/docs/conf.py index 206b0b9e2..a84c4c982 100644 --- a/letshelp-letsencrypt/docs/conf.py +++ b/letshelp-letsencrypt/docs/conf.py @@ -225,25 +225,25 @@ htmlhelp_basename = 'letshelp-letsencryptdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + #'preamble': '', -# Latex figure (float) alignment -#'figure_align': 'htbp', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'letshelp-letsencrypt.tex', u'letshelp-letsencrypt Documentation', - u'Let\'s Encrypt Project', 'manual'), + (master_doc, 'letshelp-letsencrypt.tex', u'letshelp-letsencrypt Documentation', + u'Let\'s Encrypt Project', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -286,9 +286,9 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'letshelp-letsencrypt', u'letshelp-letsencrypt Documentation', - author, 'letshelp-letsencrypt', 'One line description of project.', - 'Miscellaneous'), + (master_doc, 'letshelp-letsencrypt', u'letshelp-letsencrypt Documentation', + author, 'letshelp-letsencrypt', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. diff --git a/py26reqs.txt b/py26reqs.txt new file mode 100644 index 000000000..a94b22c0c --- /dev/null +++ b/py26reqs.txt @@ -0,0 +1,2 @@ +# https://github.com/bw2/ConfigArgParse/issues/17 +git+https://github.com/kuba/ConfigArgParse.git@python2.6-0.9.3#egg=ConfigArgParse diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 972e87eaf..000000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -# https://github.com/bw2/ConfigArgParse/issues/17 -git+https://github.com/kuba/ConfigArgParse.git@python2.6#egg=ConfigArgParse diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 18a996926..53996cd20 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -27,7 +27,7 @@ common() { "$@" } -common --domains le1.wtf --standalone-supported-challenges dvsni auth +common --domains le1.wtf --standalone-supported-challenges tls-sni-01 auth common --domains le2.wtf --standalone-supported-challenges http-01 run common -a manual -d le.wtf auth @@ -40,7 +40,7 @@ common auth --csr "$CSR_PATH" \ openssl x509 -in "${root}/csr/0000_cert.pem" -text openssl x509 -in "${root}/csr/0000_chain.pem" -text -common --domain le3.wtf install \ +common --domains le3.wtf install \ --cert-path "${root}/csr/cert.pem" \ --key-path "${root}/csr/key.pem" diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index cd894fd10..dbc473728 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -15,7 +15,7 @@ letsencrypt_test () { letsencrypt \ --server "${SERVER:-http://localhost:4000/directory}" \ --no-verify-ssl \ - --dvsni-port 5001 \ + --tls-sni-01-port 5001 \ --http-01-port 5002 \ --manual-test-mode \ $store_flags \ diff --git a/tools/dev-release.sh b/tools/dev-release.sh index 6bbc6ced4..ffaf7881c 100755 --- a/tools/dev-release.sh +++ b/tools/dev-release.sh @@ -23,7 +23,7 @@ mv "dist.$version" "dist.$version.$(date +%s).bak" || true git tag --delete "$tag" || true tmpvenv=$(mktemp -d) -virtualenv --no-site-packages $tmpvenv +virtualenv --no-site-packages -p python2 $tmpvenv . $tmpvenv/bin/activate # update setuptools/pip just like in other places in the repo pip install -U setuptools diff --git a/tox.ini b/tox.ini index 8ea3e518f..d1fafe20f 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ # acme and letsencrypt are not yet on pypi, so when Tox invokes # "install *.zip", it will not find deps skipsdist = true -envlist = py26,py27,py33,py34,cover,lint +envlist = py26,py27,py33,py34,py35,cover,lint # nosetest -v => more verbose output, allows to detect busy waiting # loops, especially on Travis @@ -17,7 +17,7 @@ envlist = py26,py27,py33,py34,cover,lint commands = pip install -e acme[testing] nosetests -v acme - pip install -r requirements.txt -e .[testing] + pip install -r py26reqs.txt -e .[testing] nosetests -v letsencrypt pip install -e letsencrypt-apache nosetests -v letsencrypt_apache @@ -41,10 +41,15 @@ commands = pip install -e acme[testing] nosetests -v acme +[testenv:py35] +commands = + pip install -e acme[testing] + nosetests -v acme + [testenv:cover] basepython = python2.7 commands = - pip install -r requirements.txt -e acme -e .[testing] -e letsencrypt-apache -e letsencrypt-nginx -e letshelp-letsencrypt + pip install -e acme -e .[testing] -e letsencrypt-apache -e letsencrypt-nginx -e letshelp-letsencrypt ./tox.cover.sh [testenv:lint] @@ -54,7 +59,7 @@ basepython = python2.7 # duplicate code checking; if one of the commands fails, others will # continue, but tox return code will reflect previous error commands = - pip install -r requirements.txt -e acme -e .[dev] -e letsencrypt-apache -e letsencrypt-nginx -e letsencrypt-compatibility-test -e letshelp-letsencrypt + pip install -e acme -e .[dev] -e letsencrypt-apache -e letsencrypt-nginx -e letsencrypt-compatibility-test -e letshelp-letsencrypt ./pep8.travis.sh pylint --rcfile=.pylintrc letsencrypt pylint --rcfile=.pylintrc acme/acme