From dc81575527eb680d8101d94a0654c6ee30a211fa Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 19 Oct 2015 20:37:55 +0000 Subject: [PATCH 01/16] Factor out _TokenDVChallenge. --- acme/acme/challenges.py | 42 ++++++++++++++++------------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 31095c04d..fd65b5e0f 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -76,26 +76,21 @@ class UnrecognizedChallenge(Challenge): return cls(jobj) -@Challenge.register -class SimpleHTTP(DVChallenge): - """ACME "simpleHttp" challenge. +class _TokenDVChallenge(DVChallenge): + """DV Challenge with token. :ivar unicode token: """ - typ = "simpleHttp" - TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec """Minimum size of the :attr:`token` in bytes.""" - URI_ROOT_PATH = ".well-known/acme-challenge" - """URI root path for the server provisioned resource.""" - # TODO: acme-spec doesn't specify token as base64-encoded value token = jose.Field( "token", encoder=jose.encode_b64jose, decoder=functools.partial( jose.decode_b64jose, size=TOKEN_SIZE, minimum=True)) + # XXX: rename to ~token_good_for_url @property def good_token(self): # XXX: @token.decoder """Is `token` good? @@ -109,6 +104,15 @@ class SimpleHTTP(DVChallenge): # URI_ROOT_PATH! return b'..' not in self.token and b'/' not in self.token + +@Challenge.register # pylint: disable=too-many-ancestors +class SimpleHTTP(_TokenDVChallenge): + """ACME "simpleHttp" challenge.""" + typ = "simpleHttp" + + URI_ROOT_PATH = ".well-known/acme-challenge" + """URI root path for the server provisioned resource.""" + @property def path(self): """Path (starting with '/') for provisioned resource.""" @@ -274,8 +278,8 @@ class SimpleHTTPProvisionedResource(jose.JSONObjectWithFields): tls = jose.Field("tls", omitempty=False) -@Challenge.register -class DVSNI(DVChallenge): +@Challenge.register # pylint: disable=too-many-ancestors +class DVSNI(_TokenDVChallenge): """ACME "dvsni" challenge. :ivar bytes token: Random data, **not** base64-encoded. @@ -286,13 +290,6 @@ class DVSNI(DVChallenge): PORT = 443 """Port to perform DVSNI challenge.""" - TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec - """Minimum size of the :attr:`token` in bytes.""" - - token = jose.Field( - "token", encoder=jose.encode_b64jose, decoder=functools.partial( - jose.decode_b64jose, size=TOKEN_SIZE, minimum=True)) - def gen_response(self, account_key, alg=jose.RS256, **kwargs): """Generate response. @@ -548,8 +545,8 @@ class ProofOfPossessionResponse(ChallengeResponse): return self.signature.verify(self.nonce) -@Challenge.register -class DNS(DVChallenge): +@Challenge.register # pylint: disable=too-many-ancestors +class DNS(_TokenDVChallenge): """ACME "dns" challenge. :ivar unicode token: @@ -560,13 +557,6 @@ class DNS(DVChallenge): LABEL = "_acme-challenge" """Label clients prepend to the domain name being validated.""" - TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec - """Minimum size of the :attr:`token` in bytes.""" - - token = jose.Field( - "token", encoder=jose.encode_b64jose, decoder=functools.partial( - jose.decode_b64jose, size=TOKEN_SIZE, minimum=True)) - def gen_validation(self, account_key, alg=jose.RS256, **kwargs): """Generate validation. From d2c5b87b953074255fda87a8d25bbde65803a5de Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 28 Oct 2015 19:42:06 +0000 Subject: [PATCH 02/16] Fix documentation for account{,_public}_key docs in acme.challenges. account_key and account_public_key are JWK, not ComparableKey. --- acme/acme/challenges.py | 45 ++++++------------------------------ acme/acme/challenges_test.py | 33 +++++++++++--------------- acme/acme/jose/jws.py | 8 +++---- 3 files changed, 24 insertions(+), 62 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index fd65b5e0f..3ff16e1b3 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -189,13 +189,7 @@ class SimpleHTTPResponse(ChallengeResponse): :param .JWS validation: :param challenges.SimpleHTTP chall: - :type account_public_key: - `~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` - wrapped in `.ComparableKey` + :param .JWK account_public_key: :rtype: bool @@ -221,16 +215,9 @@ class SimpleHTTPResponse(ChallengeResponse): :param challenges.SimpleHTTP chall: Corresponding challenge. :param unicode domain: Domain name being verified. - :param account_public_key: Public key for the key pair + :param JWK account_public_key: Public key for the key pair being authorized. If ``None`` key verification is not performed! - :type account_public_key: - `~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` - wrapped in `.ComparableKey` :param int port: Port used in the validation. :returns: ``True`` iff validation is successful, ``False`` @@ -403,17 +390,12 @@ class DVSNIResponse(ChallengeResponse): :param .challenges.DVSNI chall: Corresponding challenge. :param str domain: Domain name being validated. - :type account_public_key: - `~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` - wrapped in `.ComparableKey` + :param JWK account_public_key: :param OpenSSL.crypto.X509 cert: Optional certificate. If not provided (``None``) certificate will be retrieved using `probe_cert`. + :returns: ``True`` iff client's control of the domain has been verified, ``False`` otherwise. :rtype: bool @@ -488,7 +470,7 @@ class ProofOfPossession(ContinuityChallenge): class Hints(jose.JSONObjectWithFields): """Hints for "proofOfPossession" challenge. - :ivar jwk: JSON Web Key (:class:`acme.jose.JWK`) + :ivar JWK jwk: JSON Web Key :ivar tuple cert_fingerprints: `tuple` of `unicode` :ivar tuple certs: Sequence of :class:`acme.jose.ComparableX509` certificates. @@ -575,14 +557,7 @@ class DNS(_TokenDVChallenge): """Check validation. :param JWS validation: - :type account_public_key: - `~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` - wrapped in `.ComparableKey` - + :param JWK account_public_key: :rtype: bool """ @@ -631,13 +606,7 @@ class DNSResponse(ChallengeResponse): """Check validation. :param challenges.DNS chall: - :type account_public_key: - `~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey` - or - `~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` - wrapped in `.ComparableKey` + :param JWK account_public_key: :rtype: bool diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 2a12b4a64..4a8af2347 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -14,7 +14,7 @@ from acme import test_util CERT = test_util.load_cert('cert.pem') -KEY = test_util.load_rsa_private_key('rsa512_key.pem') +KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem')) class ChallengeTest(unittest.TestCase): @@ -237,18 +237,15 @@ class DVSNITest(unittest.TestCase): jose.DeserializationError, DVSNI.from_json, self.jmsg) def test_gen_response(self): - key = jose.JWKRSA(key=KEY) from acme.challenges import DVSNI self.assertEqual(self.msg, DVSNI.json_loads( - self.msg.gen_response(key).validation.payload.decode())) + self.msg.gen_response(KEY).validation.payload.decode())) class DVSNIResponseTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes def setUp(self): - self.key = jose.JWKRSA(key=KEY) - from acme.challenges import DVSNI self.chall = DVSNI( token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e')) @@ -256,7 +253,7 @@ class DVSNIResponseTest(unittest.TestCase): from acme.challenges import DVSNIResponse self.validation = jose.JWS.sign( payload=self.chall.json_dumps(sort_keys=True).encode(), - key=self.key, alg=jose.RS256) + key=KEY, alg=jose.RS256) self.msg = DVSNIResponse(validation=self.validation) self.jmsg_to = { 'resource': 'challenge', @@ -340,22 +337,22 @@ class DVSNIResponseTest(unittest.TestCase): def test_simple_verify_wrong_payload(self): for payload in b'', b'{}': msg = self.msg.update(validation=jose.JWS.sign( - payload=payload, key=self.key, alg=jose.RS256)) + payload=payload, key=KEY, alg=jose.RS256)) self.assertFalse(msg.simple_verify( - self.chall, self.domain, self.key.public_key())) + 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=self.key, alg=jose.RS256)) + key=KEY, alg=jose.RS256)) self.assertFalse(msg.simple_verify( - self.chall, self.domain, self.key.public_key())) + 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, self.key.public_key(), + self.chall, self.domain, KEY.public_key(), cert=mock.sentinel.cert)) mock_verify_cert.assert_called_once_with(self.msg, mock.sentinel.cert) @@ -363,7 +360,7 @@ class DVSNIResponseTest(unittest.TestCase): 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, self.key.public_key())) + self.chall, self.domain, KEY.public_key())) class RecoveryContactTest(unittest.TestCase): @@ -442,7 +439,7 @@ class RecoveryContactResponseTest(unittest.TestCase): class ProofOfPossessionHintsTest(unittest.TestCase): def setUp(self): - jwk = jose.JWKRSA(key=KEY.public_key()) + jwk = KEY.public_key() issuers = ( 'C=US, O=SuperT LLC, CN=SuperTrustworthy Public CA', 'O=LessTrustworthy CA Inc, CN=LessTrustworthy But StillSecure', @@ -511,7 +508,7 @@ class ProofOfPossessionTest(unittest.TestCase): def setUp(self): from acme.challenges import ProofOfPossession hints = ProofOfPossession.Hints( - jwk=jose.JWKRSA(key=KEY.public_key()), cert_fingerprints=(), + jwk=KEY.public_key(), cert_fingerprints=(), certs=(), serial_numbers=(), subject_key_identifiers=(), issuers=(), authorized_for=()) self.msg = ProofOfPossession( @@ -551,7 +548,7 @@ class ProofOfPossessionResponseTest(unittest.TestCase): # nonce and challenge nonce are the same, don't make the same # mistake here... signature = other.Signature( - alg=jose.RS256, jwk=jose.JWKRSA(key=KEY.public_key()), + alg=jose.RS256, jwk=KEY.public_key(), sig=b'\xa7\xc1\xe7\xe82o\xbc\xcd\xd0\x1e\x010#Z|\xaf\x15\x83' b'\x94\x8f#\x9b\nQo(\x80\x15,\x08\xfcz\x1d\xfd\xfd.\xaap' b'\xfa\x06\xd1\xa2f\x8d8X2>%d\xbd%\xe1T\xdd\xaa0\x18\xde' @@ -659,14 +656,12 @@ class DNSTest(unittest.TestCase): class DNSResponseTest(unittest.TestCase): def setUp(self): - self.key = jose.JWKRSA(key=KEY) - from acme.challenges import DNS self.chall = DNS(token=jose.b64decode( b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA")) self.validation = jose.JWS.sign( payload=self.chall.json_dumps(sort_keys=True).encode(), - key=self.key, alg=jose.RS256) + key=KEY, alg=jose.RS256) from acme.challenges import DNSResponse self.msg = DNSResponse(validation=self.validation) @@ -694,7 +689,7 @@ class DNSResponseTest(unittest.TestCase): def test_check_validation(self): self.assertTrue( - self.msg.check_validation(self.chall, self.key.public_key())) + self.msg.check_validation(self.chall, KEY.public_key())) if __name__ == '__main__': diff --git a/acme/acme/jose/jws.py b/acme/acme/jose/jws.py index 61a3b5aea..1a073e17d 100644 --- a/acme/acme/jose/jws.py +++ b/acme/acme/jose/jws.py @@ -104,7 +104,7 @@ class Header(json_util.JSONObjectWithFields): .. todo:: Supports only "jwk" header parameter lookup. :returns: (Public) key found in the header. - :rtype: :class:`acme.jose.jwk.JWK` + :rtype: .JWK :raises acme.jose.errors.Error: if key could not be found @@ -194,8 +194,7 @@ class Signature(json_util.JSONObjectWithFields): def verify(self, payload, key=None): """Verify. - :param key: Key used for verification. - :type key: :class:`acme.jose.jwk.JWK` + :param JWK key: Key used for verification. """ key = self.combined.find_key() if key is None else key @@ -208,8 +207,7 @@ class Signature(json_util.JSONObjectWithFields): protect=frozenset(), **kwargs): """Sign. - :param key: Key for signature. - :type key: :class:`acme.jose.jwk.JWK` + :param JWK key: Key for signature. """ assert isinstance(key, alg.kty) From 602f0b2dbefd2321e0f7a7da523569d826b2d354 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 28 Oct 2015 20:41:15 +0000 Subject: [PATCH 03/16] Add http-01 to acme --- acme/acme/challenges.py | 223 +++++++++++++++++++++++++++++++++-- acme/acme/challenges_test.py | 147 +++++++++++++++++++++++ acme/acme/jose/jwk.py | 2 + acme/acme/messages_test.py | 2 +- docs/contributing.rst | 2 +- 5 files changed, 367 insertions(+), 9 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 3ff16e1b3..750f2be8d 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -1,9 +1,11 @@ """ACME Identifier Validation Challenges.""" +import abc import functools import hashlib import logging import socket +from cryptography.hazmat.primitives import hashes import OpenSSL import requests @@ -79,7 +81,7 @@ class UnrecognizedChallenge(Challenge): class _TokenDVChallenge(DVChallenge): """DV Challenge with token. - :ivar unicode token: + :ivar bytes token: """ TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec @@ -105,6 +107,217 @@ class _TokenDVChallenge(DVChallenge): return b'..' not in self.token and b'/' not in self.token +class KeyAuthorizationChallengeResponse(ChallengeResponse): + """Response to Challenges based on Key Authorization. + + :param unicode key_authorization: + + """ + key_authorization = jose.Field("keyAuthorization") + thumbprint_hash_function = hashes.SHA256 + + def verify(self, chall, account_public_key): + """Verify the key authorization. + + :param KeyAuthorization chall: Challenge that corresponds to + this response. + :param JWK account_public_key: + + :return: ``True`` iff verification of the key authorization was + successful. + :rtype: bool + + """ + parts = self.key_authorization.split('.') # pylint: disable=no-member + if len(parts) != 2: + logger.debug("Key authorization (%r) is not well formed", + self.key_authorization) + return False + + if parts[0] != chall.encode("token"): + logger.debug("Mismatching token in key authorization: " + "%r instead of %r", parts[0], chall.encode("token")) + return False + + thumbprint = jose.b64encode(account_public_key.thumbprint( + hash_function=self.thumbprint_hash_function)).decode() + if parts[1] != thumbprint: + logger.debug("Mismatching thumbprint in key authorization: " + "%r instead of %r", parts[0], thumbprint) + return False + + return True + + +class KeyAuthorizationChallenge(_TokenDVChallenge): + # pylint: disable=abstract-class-little-used,too-many-ancestors + """Challenge based on Key Authorization. + + :param response_cls: Subclass of `KeyAuthorizationChallengeResponse` + that will be used to generate `response`. + + """ + __metaclass__ = abc.ABCMeta + + response_cls = NotImplemented + thumbprint_hash_function = ( + KeyAuthorizationChallengeResponse.thumbprint_hash_function) + + def key_authorization(self, account_key): + """Generate Key Authorization. + + :param JWK account_key: + :rtype unicode: + + """ + return self.encode("token") + "." + jose.b64encode( + account_key.thumbprint( + hash_function=self.thumbprint_hash_function)).decode() + + def response(self, account_key): + """Generate response to the challenge. + + :param JWK account_key: + + :returns: Response (initialized `response_cls`) to the challenge. + :rtype: KeyAuthorizationChallengeResponse + + """ + return self.response_cls( + key_authorization=self.key_authorization(account_key)) + + @abc.abstractmethod + def validation(self, account_key): + """Generate validation for the challenge. + + Subclasses must implement this method, but they are likely to + return completely different data structures, depending on what's + necessary to complete the challenge. Interepretation of that + return value must be known to the caller. + + :param JWK account_key: + :returns: Challenge-specific validation. + + """ + raise NotImplementedError() # pragma: no cover + + def response_and_validation(self, account_key): + """Generate response and validation. + + Convenience function that return results of `response` and + `validation`. + + :param JWK account_key: + :rtype: tuple + + """ + return (self.response(account_key), self.validation(account_key)) + + +@ChallengeResponse.register +class HTTP01Response(KeyAuthorizationChallengeResponse): + """ACME http-01 challenge response.""" + typ = "http-01" + + PORT = 80 + + def simple_verify(self, chall, domain, account_public_key, port=None): + """Simple verify. + + :param challenges.SimpleHTTP chall: Corresponding challenge. + :param unicode domain: Domain name being verified. + :param account_public_key: Public key for the key pair + being authorized. If ``None`` key verification is not + performed! + :param JWK account_public_key: + :param int port: Port used in the validation. + + :returns: ``True`` iff validation is successful, ``False`` + otherwise. + :rtype: bool + + """ + if not self.verify(chall, account_public_key): + logger.debug("Verification of key authorization in response failed") + return False + + # TODO: ACME specification defines URI template that doesn't + # allow to use a custom port... Make sure port is not in the + # 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) + domain += ":{0}".format(port) + + uri = self.uri(domain, chall) + logger.debug("Verifying %s at %s...", chall.typ, uri) + try: + http_response = requests.get(uri) + except requests.exceptions.RequestException as error: + logger.error("Unable to reach %s: %s", uri, error) + return False + logger.debug("Received %s: %s. Headers: %s", http_response, + http_response.text, http_response.headers) + + found_ct = http_response.headers.get( + "Content-Type", chall.CONTENT_TYPE) + if found_ct != chall.CONTENT_TYPE: + logger.debug("Wrong Content-Type: found %r, expected %r", + found_ct, chall.CONTENT_TYPE) + return False + + if self.key_authorization != http_response.text: + logger.debug("Key authorization from response (%r) doesn't match " + "HTTP response (%r)", self.key_authorization, + http_response.text) + return False + + return True + + +@Challenge.register # pylint: disable=too-many-ancestors +class HTTP01(KeyAuthorizationChallenge): + """ACME http-01 challenge.""" + response_cls = HTTP01Response + typ = response_cls.typ + + CONTENT_TYPE = "text/plain" + """Content-Type header that must be used for provisioned resource.""" + + URI_ROOT_PATH = ".well-known/acme-challenge" + """URI root path for the server provisioned resource.""" + + @property + def path(self): + """Path (starting with '/') for provisioned resource. + + :rtype: string + + """ + return '/' + self.URI_ROOT_PATH + '/' + self.encode('token') + + def uri(self, domain): + """Create an URI to the provisioned resource. + + Forms an URI to the HTTPS server provisioned resource + (containing :attr:`~SimpleHTTP.token`). + + :param unicode domain: Domain name being verified. + :rtype: string + + """ + return "http://" + domain + self.path + + def validation(self, account_key): + """Generate validation. + + :param JWK account_key: + :rtype: unicode + + """ + return self.key_authorization(account_key) + + @Challenge.register # pylint: disable=too-many-ancestors class SimpleHTTP(_TokenDVChallenge): """ACME "simpleHttp" challenge.""" @@ -229,7 +442,7 @@ class SimpleHTTPResponse(ChallengeResponse): # allow to use a custom port... Make sure port is not in the # request URI, if it's standard. if port is not None and port != self.port: - logger.warn( + logger.warning( "Using non-standard port for SimpleHTTP verification: %s", port) domain += ":{0}".format(port) @@ -529,11 +742,7 @@ class ProofOfPossessionResponse(ChallengeResponse): @Challenge.register # pylint: disable=too-many-ancestors class DNS(_TokenDVChallenge): - """ACME "dns" challenge. - - :ivar unicode token: - - """ + """ACME "dns" challenge.""" typ = "dns" LABEL = "_acme-challenge" diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 4a8af2347..53e6b528a 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -43,6 +43,149 @@ class UnrecognizedChallengeTest(unittest.TestCase): self.chall, UnrecognizedChallenge.from_json(self.jobj)) +class KeyAuthorizationChallengeResponseTest(unittest.TestCase): + + def setUp(self): + def _encode(name): + assert name == "token" + return "foo" + self.chall = mock.Mock() + self.chall.encode.side_effect = _encode + + def test_verify_ok(self): + from acme.challenges import KeyAuthorizationChallengeResponse + response = KeyAuthorizationChallengeResponse( + key_authorization='foo.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY') + self.assertTrue(response.verify(self.chall, KEY.public_key())) + + def test_verify_wrong_token(self): + from acme.challenges import KeyAuthorizationChallengeResponse + response = KeyAuthorizationChallengeResponse( + key_authorization='bar.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY') + self.assertFalse(response.verify(self.chall, KEY.public_key())) + + def test_verify_wrong_thumbprint(self): + from acme.challenges import KeyAuthorizationChallengeResponse + response = KeyAuthorizationChallengeResponse( + key_authorization='foo.oKGqedy-b-acd5eoybm2f-NVFxv') + self.assertFalse(response.verify(self.chall, KEY.public_key())) + + def test_verify_wrong_form(self): + from acme.challenges import KeyAuthorizationChallengeResponse + response = KeyAuthorizationChallengeResponse( + key_authorization='.foo.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY') + self.assertFalse(response.verify(self.chall, KEY.public_key())) + + +class HTTP01ResponseTest(unittest.TestCase): + # pylint: disable=too-many-instance-attributes + + def setUp(self): + from acme.challenges import HTTP01Response + self.msg = HTTP01Response(key_authorization=u'foo') + self.jmsg = { + 'resource': 'challenge', + 'type': 'http-01', + 'keyAuthorization': u'foo', + } + + from acme.challenges import HTTP01 + self.chall = HTTP01(token=(b'x' * 16)) + self.response = self.chall.response(KEY) + self.good_headers = {'Content-Type': HTTP01.CONTENT_TYPE} + + def test_to_partial_json(self): + self.assertEqual(self.jmsg, self.msg.to_partial_json()) + + def test_from_json(self): + from acme.challenges import HTTP01Response + self.assertEqual( + self.msg, HTTP01Response.from_json(self.jmsg)) + + def test_from_json_hashable(self): + from acme.challenges import HTTP01Response + hash(HTTP01Response.from_json(self.jmsg)) + + 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.requests.get") + def test_simple_verify_good_validation(self, mock_get): + validation = self.chall.validation(KEY) + mock_get.return_value = mock.MagicMock( + text=validation, headers=self.good_headers) + self.assertTrue(self.response.simple_verify( + self.chall, "local", KEY.public_key())) + mock_get.assert_called_once_with(self.chall.uri("local")) + + @mock.patch("acme.challenges.requests.get") + def test_simple_verify_bad_validation(self, mock_get): + mock_get.return_value = mock.MagicMock( + text="!", headers=self.good_headers) + self.assertFalse(self.response.simple_verify( + self.chall, "local", KEY.public_key())) + + @mock.patch("acme.challenges.requests.get") + def test_simple_verify_bad_content_type(self, mock_get): + mock_get().text = self.chall.token + self.assertFalse(self.response.simple_verify( + self.chall, "local", KEY.public_key())) + + @mock.patch("acme.challenges.requests.get") + def test_simple_verify_connection_error(self, mock_get): + mock_get.side_effect = requests.exceptions.RequestException + self.assertFalse(self.response.simple_verify( + self.chall, "local", KEY.public_key())) + + @mock.patch("acme.challenges.requests.get") + def test_simple_verify_port(self, mock_get): + self.response.simple_verify( + self.chall, domain="local", + account_public_key=KEY.public_key(), port=8080) + self.assertEqual("local:8080", urllib_parse.urlparse( + mock_get.mock_calls[0][1][0]).netloc) + + +class HTTP01Test(unittest.TestCase): + + def setUp(self): + from acme.challenges import HTTP01 + self.msg = HTTP01( + token=jose.decode_b64jose( + 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')) + self.jmsg = { + 'type': 'http-01', + 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', + } + + def test_path(self): + self.assertEqual(self.msg.path, '/.well-known/acme-challenge/' + 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA') + + def test_uri(self): + self.assertEqual( + 'http://example.com/.well-known/acme-challenge/' + 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', + self.msg.uri('example.com')) + + def test_to_partial_json(self): + self.assertEqual(self.jmsg, self.msg.to_partial_json()) + + def test_from_json(self): + from acme.challenges import HTTP01 + self.assertEqual(self.msg, HTTP01.from_json(self.jmsg)) + + def test_from_json_hashable(self): + from acme.challenges import HTTP01 + hash(HTTP01.from_json(self.jmsg)) + + def test_good_token(self): + self.assertTrue(self.msg.good_token) + self.assertFalse( + self.msg.update(token=b'..').good_token) + + class SimpleHTTPTest(unittest.TestCase): def setUp(self): @@ -55,6 +198,10 @@ class SimpleHTTPTest(unittest.TestCase): 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', } + def test_path(self): + self.assertEqual(self.msg.path, '/.well-known/acme-challenge/' + 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA') + def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) diff --git a/acme/acme/jose/jwk.py b/acme/acme/jose/jwk.py index da61b0c4e..4d07229b3 100644 --- a/acme/acme/jose/jwk.py +++ b/acme/acme/jose/jwk.py @@ -47,6 +47,8 @@ class JWK(json_util.TypedJSONObjectWithFields): https://tools.ietf.org/html/rfc7638 + :returns bytes: + """ digest = hashes.Hash(hash_function(), backend=default_backend()) digest.update(json.dumps( diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index d2d859bc5..6c1c4f596 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -278,7 +278,7 @@ class AuthorizationTest(unittest.TestCase): self.challbs = ( ChallengeBody( uri='http://challb1', status=STATUS_VALID, - chall=challenges.SimpleHTTP(token=b'IlirfxKKXAsHtmzK29Pj8A')), + chall=challenges.HTTP01(token=b'IlirfxKKXAsHtmzK29Pj8A')), ChallengeBody(uri='http://challb2', status=STATUS_VALID, chall=challenges.DNS( token=b'DGyRejmCefe7v4NfDGDKfA')), diff --git a/docs/contributing.rst b/docs/contributing.rst index f82d7a583..a3baa7bc5 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -152,7 +152,7 @@ the ACME server. From the protocol, there are essentially two different types of challenges. Challenges that must be solved by individual plugins in order to satisfy domain validation (subclasses of `~.DVChallenge`, i.e. `~.challenges.DVSNI`, -`~.challenges.SimpleHTTPS`, `~.challenges.DNS`) and continuity specific +`~.challenges.HTTP01`, `~.challenges.DNS`) and continuity specific challenges (subclasses of `~.ContinuityChallenge`, i.e. `~.challenges.RecoveryToken`, `~.challenges.RecoveryContact`, `~.challenges.ProofOfPossession`). Continuity challenges are From 01bc073111d139183e24c9d702d5942beb864022 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 1 Nov 2015 10:54:01 +0000 Subject: [PATCH 04/16] Quickfix for misterious abstract-class-little-used --- acme/acme/jose/jwk.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/acme/acme/jose/jwk.py b/acme/acme/jose/jwk.py index 4d07229b3..c82134da9 100644 --- a/acme/acme/jose/jwk.py +++ b/acme/acme/jose/jwk.py @@ -21,6 +21,12 @@ from acme.jose import util logger = logging.getLogger(__name__) +# TODO: bug in pylint? +# ************* Module acme.challenges +# R:153, 0: Abstract class is only referenced 1 times (abstract-class-little-used) +# pylint: disable=abstract-class-little-used + + class JWK(json_util.TypedJSONObjectWithFields): # pylint: disable=too-few-public-methods """JSON Web Key.""" From bb0843b6c6662fd75a78523151ae3543e17e070e Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Oct 2015 21:35:12 +0000 Subject: [PATCH 05/16] acme_util.HTTP01* --- letsencrypt/tests/acme_util.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/letsencrypt/tests/acme_util.py b/letsencrypt/tests/acme_util.py index 235810435..1e3086b18 100644 --- a/letsencrypt/tests/acme_util.py +++ b/letsencrypt/tests/acme_util.py @@ -14,6 +14,8 @@ KEY = test_util.load_rsa_private_key('rsa512_key.pem') # Challenges SIMPLE_HTTP = challenges.SimpleHTTP( token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") +HTTP01 = challenges.HTTP01( + token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") DVSNI = challenges.DVSNI( token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA")) DNS = challenges.DNS(token="17817c66b60ce2e4012dfad92657527a") @@ -81,6 +83,7 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name # Pending ChallengeBody objects DVSNI_P = chall_to_challb(DVSNI, messages.STATUS_PENDING) SIMPLE_HTTP_P = chall_to_challb(SIMPLE_HTTP, 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) From 5ef2507b0c33031e5df25f7cd3b966b325475860 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 28 Oct 2015 20:42:27 +0000 Subject: [PATCH 06/16] KeyAuthorizationAnnotatedChallenge --- letsencrypt/achallenges.py | 9 +++++++++ letsencrypt/auth_handler.py | 4 +++- letsencrypt/tests/auth_handler_test.py | 19 +++++++++++++++++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/letsencrypt/achallenges.py b/letsencrypt/achallenges.py index e86f51a3f..7aa2bd35a 100644 --- a/letsencrypt/achallenges.py +++ b/letsencrypt/achallenges.py @@ -45,6 +45,15 @@ class AnnotatedChallenge(jose.ImmutableMap): return getattr(self.challb, name) +class KeyAuthorizationAnnotatedChallenge(AnnotatedChallenge): + """Client annotated `KeyAuthorizationChallenge` challenge.""" + __slots__ = ('challb', 'domain', 'account_key') + + def response_and_validation(self): + """Generate response and validation.""" + return self.challb.chall.response_and_validation(self.account_key) + + class DVSNI(AnnotatedChallenge): """Client annotated "dvsni" ACME challenge. diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 4f2a6fa3e..f7b6bd507 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -358,7 +358,9 @@ def challb_to_achall(challb, account_key, domain): elif isinstance(chall, challenges.ProofOfPossession): return achallenges.ProofOfPossession( challb=challb, domain=domain) - + elif isinstance(chall, challenges.KeyAuthorizationChallenge): + return achallenges.KeyAuthorizationAnnotatedChallenge( + challb=challb, domain=domain, account_key=account_key) else: raise errors.Error( "Received unsupported challenge of type: %s", chall.typ) diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 18ee56081..247fca4e1 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -9,6 +9,7 @@ from acme import challenges from acme import client as acme_client from acme import messages +from letsencrypt import achallenges from letsencrypt import errors from letsencrypt import le_util @@ -283,6 +284,22 @@ class PollChallengesTest(unittest.TestCase): return (new_authzr, "response") +class ChallbToAchallTest(unittest.TestCase): + """Tests for letsencrypt.auth_handler.challb_to_achall.""" + + def _call(self, challb): + from letsencrypt.auth_handler import challb_to_achall + return challb_to_achall(challb, "account_key", "domain") + + def test_it(self): + self.assertEqual( + self._call(acme_util.HTTP01_P), + achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.HTTP01_P, account_key="account_key", + domain="domain"), + ) + + class GenChallengePathTest(unittest.TestCase): """Tests for letsencrypt.auth_handler.gen_challenge_path. @@ -425,8 +442,6 @@ class ReportFailedChallsTest(unittest.TestCase): # pylint: disable=protected-access def setUp(self): - from letsencrypt import achallenges - kwargs = { "chall": acme_util.SIMPLE_HTTP, "uri": "uri", From fd209eab66f8818796c6be5fed2bc2ace9776d7e Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 28 Oct 2015 20:42:44 +0000 Subject: [PATCH 07/16] http-01 for manual plugin --- letsencrypt/plugins/manual.py | 24 ++++++++++++------------ letsencrypt/plugins/manual_test.py | 10 +++++----- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index c76463f85..ffaad3d99 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -27,7 +27,7 @@ class Authenticator(common.Plugin): """Manual Authenticator. This plugin requires user's manual intervention in setting up a HTTP - server for solving SimpleHTTP challenges and thus does not need to be + server for solving http-01 challenges and thus does not need to be run as a privileged process. Alternatively shows instructions on how to use Python's built-in HTTP server. @@ -68,9 +68,9 @@ Are you OK with your IP being logged? # anything recursively under the cwd CMD_TEMPLATE = """\ -mkdir -p {root}/public_html/{response.URI_ROOT_PATH} +mkdir -p {root}/public_html/{achall.URI_ROOT_PATH} cd {root}/public_html -printf "%s" {validation} > {response.URI_ROOT_PATH}/{encoded_token} +printf "%s" {validation} > {achall.URI_ROOT_PATH}/{encoded_token} # run only once per server: $(command -v python2 || command -v python2.7 || command -v python2.6) -c \\ "import BaseHTTPServer, SimpleHTTPServer; \\ @@ -95,14 +95,14 @@ s.serve_forever()" """ def more_info(self): # pylint: disable=missing-docstring,no-self-use return ("This plugin requires user's manual intervention in setting " - "up an HTTP server for solving SimpleHTTP challenges and thus " + "up an HTTP server for solving http-01 challenges and thus " "does not need to be run as a privileged process. " "Alternatively shows instructions on how to use Python's " "built-in HTTP server.") def get_chall_pref(self, domain): # pylint: disable=missing-docstring,no-self-use,unused-argument - return [challenges.SimpleHTTP] + return [challenges.HTTP01] def perform(self, achalls): # pylint: disable=missing-docstring responses = [] @@ -130,16 +130,16 @@ s.serve_forever()" """ # same path for each challenge response would be easier for # users, but will not work if multiple domains point at the # same server: default command doesn't support virtual hosts - response, validation = achall.gen_response_and_validation( - tls=False) # SimpleHTTP TLS is dead: ietf-wg-acme/acme#7 + response, validation = achall.response_and_validation() port = (response.port if self.config.simple_http_port is None else int(self.config.simple_http_port)) command = self.CMD_TEMPLATE.format( root=self._root, achall=achall, response=response, - validation=pipes.quote(validation.json_dumps()), + # TODO(kuba): pipes still necessary? + validation=pipes.quote(validation), encoded_token=achall.chall.encode("token"), - ct=response.CONTENT_TYPE, port=port) + ct=achall.CONTENT_TYPE, port=port) if self.conf("test-mode"): logger.debug("Test mode. Executing the manual command: %s", command) # sh shipped with OS X does't support echo -n, but supports printf @@ -168,9 +168,9 @@ s.serve_forever()" """ raise errors.PluginError("Must agree to IP logging to proceed") self._notify_and_wait(self.MESSAGE_TEMPLATE.format( - validation=validation.json_dumps(), response=response, - uri=response.uri(achall.domain, achall.challb.chall), - ct=response.CONTENT_TYPE, command=command)) + validation=validation, response=response, + uri=achall.chall.uri(achall.domain), + ct=achall.CONTENT_TYPE, command=command)) if response.simple_verify( achall.chall, achall.domain, diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py index a52129635..431cd07a7 100644 --- a/letsencrypt/plugins/manual_test.py +++ b/letsencrypt/plugins/manual_test.py @@ -25,8 +25,8 @@ class AuthenticatorTest(unittest.TestCase): self.config = mock.MagicMock( simple_http_port=8080, manual_test_mode=False) self.auth = Authenticator(config=self.config, name="manual") - self.achalls = [achallenges.SimpleHTTP( - challb=acme_util.SIMPLE_HTTP_P, domain="foo.com", account_key=KEY)] + self.achalls = [achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)] config_test_mode = mock.MagicMock( simple_http_port=8080, manual_test_mode=True) @@ -45,13 +45,13 @@ class AuthenticatorTest(unittest.TestCase): @mock.patch("letsencrypt.plugins.manual.zope.component.getUtility") @mock.patch("letsencrypt.plugins.manual.sys.stdout") - @mock.patch("acme.challenges.SimpleHTTPResponse.simple_verify") + @mock.patch("acme.challenges.HTTP01Response.simple_verify") @mock.patch("__builtin__.raw_input") def test_perform(self, mock_raw_input, mock_verify, mock_stdout, mock_interaction): mock_verify.return_value = True mock_interaction().yesno.return_value = True - resp = challenges.SimpleHTTPResponse(tls=False) + resp = self.achalls[0].response(KEY) self.assertEqual([resp], self.auth.perform(self.achalls)) self.assertEqual(1, mock_raw_input.call_count) mock_verify.assert_called_with( @@ -89,7 +89,7 @@ class AuthenticatorTest(unittest.TestCase): @mock.patch("letsencrypt.plugins.manual.socket.socket") @mock.patch("letsencrypt.plugins.manual.time.sleep", autospec=True) - @mock.patch("acme.challenges.SimpleHTTPResponse.simple_verify", + @mock.patch("acme.challenges.HTTP01Response.simple_verify", autospec=True) @mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True) def test_perform_test_mode(self, mock_popen, mock_verify, mock_sleep, From ea3611afe6b7e5e605dff6bcd31eb1dbdf235486 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 28 Oct 2015 21:27:04 +0000 Subject: [PATCH 08/16] http-01 for standalone --- acme/acme/challenges.py | 2 +- acme/acme/standalone.py | 26 +++++++++--------- acme/acme/standalone_test.py | 29 ++++++++++---------- docs/using.rst | 2 +- letsencrypt/plugins/standalone.py | 36 ++++++++++++------------- letsencrypt/plugins/standalone_test.py | 37 +++++++++++++------------- tests/boulder-integration.sh | 2 +- 7 files changed, 66 insertions(+), 68 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 750f2be8d..86ff0a902 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -249,7 +249,7 @@ class HTTP01Response(KeyAuthorizationChallengeResponse): "Using non-standard port for SimpleHTTP verification: %s", port) domain += ":{0}".format(port) - uri = self.uri(domain, chall) + uri = chall.uri(domain) logger.debug("Verifying %s at %s...", chall.typ, uri) try: http_response = requests.get(uri) diff --git a/acme/acme/standalone.py b/acme/acme/standalone.py index 310e61995..1466671e3 100644 --- a/acme/acme/standalone.py +++ b/acme/acme/standalone.py @@ -58,26 +58,26 @@ class DVSNIServer(TLSServer, ACMEServerMixin): self, server_address, socketserver.BaseRequestHandler, certs=certs) -class SimpleHTTPServer(BaseHTTPServer.HTTPServer, ACMEServerMixin): - """SimpleHTTP Server.""" +class HTTP01Server(BaseHTTPServer.HTTPServer, ACMEServerMixin): + """HTTP01 Server.""" def __init__(self, server_address, resources): BaseHTTPServer.HTTPServer.__init__( - self, server_address, SimpleHTTPRequestHandler.partial_init( + self, server_address, HTTP01RequestHandler.partial_init( simple_http_resources=resources)) -class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): - """SimpleHTTP challenge handler. +class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + """HTTP01 challenge handler. Adheres to the stdlib's `socketserver.BaseRequestHandler` interface. - :ivar set simple_http_resources: A set of `SimpleHTTPResource` + :ivar set simple_http_resources: A set of `HTTP01Resource` objects. TODO: better name? """ - SimpleHTTPResource = collections.namedtuple( - "SimpleHTTPResource", "chall response validation") + HTTP01Resource = collections.namedtuple( + "HTTP01Resource", "chall response validation") def __init__(self, *args, **kwargs): self.simple_http_resources = kwargs.pop("simple_http_resources", set()) @@ -86,7 +86,7 @@ class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): def do_GET(self): # pylint: disable=invalid-name,missing-docstring if self.path == "/": self.handle_index() - elif self.path.startswith("/" + challenges.SimpleHTTP.URI_ROOT_PATH): + elif self.path.startswith("/" + challenges.HTTP01.URI_ROOT_PATH): self.handle_simple_http_resource() else: self.handle_404() @@ -106,15 +106,15 @@ class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): self.wfile.write(b"404") def handle_simple_http_resource(self): - """Handle SimpleHTTP provisioned resources.""" + """Handle HTTP01 provisioned resources.""" for resource in self.simple_http_resources: if resource.chall.path == self.path: - logger.debug("Serving SimpleHTTP with token %r", + logger.debug("Serving HTTP01 with token %r", resource.chall.encode("token")) self.send_response(http_client.OK) - self.send_header("Content-type", resource.response.CONTENT_TYPE) + self.send_header("Content-type", resource.chall.CONTENT_TYPE) self.end_headers() - self.wfile.write(resource.validation.json_dumps().encode()) + self.wfile.write(resource.validation.encode()) return else: # pylint: disable=useless-else-on-loop logger.debug("No resources to serve") diff --git a/acme/acme/standalone_test.py b/acme/acme/standalone_test.py index ed015b826..85ef6ab14 100644 --- a/acme/acme/standalone_test.py +++ b/acme/acme/standalone_test.py @@ -54,16 +54,16 @@ class DVSNIServerTest(unittest.TestCase): jose.ComparableX509(self.certs[b'localhost'][1])) -class SimpleHTTPServerTest(unittest.TestCase): - """Tests for acme.standalone.SimpleHTTPServer.""" +class HTTP01ServerTest(unittest.TestCase): + """Tests for acme.standalone.HTTP01Server.""" def setUp(self): self.account_key = jose.JWK.load( test_util.load_vector('rsa1024_key.pem')) self.resources = set() - from acme.standalone import SimpleHTTPServer - self.server = SimpleHTTPServer(('', 0), resources=self.resources) + from acme.standalone import HTTP01Server + self.server = HTTP01Server(('', 0), resources=self.resources) # pylint: disable=no-member self.port = self.server.socket.getsockname()[1] @@ -86,25 +86,24 @@ class SimpleHTTPServerTest(unittest.TestCase): 'http://localhost:{0}/foo'.format(self.port), verify=False) self.assertEqual(response.status_code, http_client.NOT_FOUND) - def _test_simple_http(self, add): - chall = challenges.SimpleHTTP(token=(b'x' * 16)) - response = challenges.SimpleHTTPResponse(tls=False) + def _test_http01(self, add): + chall = challenges.HTTP01(token=(b'x' * 16)) + response, validation = chall.response_and_validation(self.account_key) - from acme.standalone import SimpleHTTPRequestHandler - resource = SimpleHTTPRequestHandler.SimpleHTTPResource( - chall=chall, response=response, validation=response.gen_validation( - chall, self.account_key)) + from acme.standalone import HTTP01RequestHandler + resource = HTTP01RequestHandler.HTTP01Resource( + chall=chall, response=response, validation=validation) if add: self.resources.add(resource) return resource.response.simple_verify( resource.chall, 'localhost', self.account_key.public_key(), port=self.port) - def test_simple_http_found(self): - self.assertTrue(self._test_simple_http(add=True)) + def test_http01_found(self): + self.assertTrue(self._test_http01(add=True)) - def test_simple_http_not_found(self): - self.assertFalse(self._test_simple_http(add=False)) + def test_http01_not_found(self): + self.assertFalse(self._test_http01(add=False)) class TestSimpleDVSNIServer(unittest.TestCase): diff --git a/docs/using.rst b/docs/using.rst index 73c3fe140..312e55510 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -127,7 +127,7 @@ Officially supported plugins: Plugin A I Notes and status ========== = = ================================================================ standalone Y N Very stable. Uses port 80 (force by - ``--standalone-supported-challenges simpleHttp``) or 443 + ``--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. diff --git a/letsencrypt/plugins/standalone.py b/letsencrypt/plugins/standalone.py index ccff03319..c45337507 100644 --- a/letsencrypt/plugins/standalone.py +++ b/letsencrypt/plugins/standalone.py @@ -14,7 +14,6 @@ from acme import challenges from acme import crypto_util as acme_crypto_util from acme import standalone as acme_standalone -from letsencrypt import achallenges from letsencrypt import errors from letsencrypt import interfaces @@ -33,7 +32,7 @@ class ServerManager(object): `acme.crypto_util.SSLSocket.certs` and `acme.crypto_util.SSLSocket.simple_http_resources` respectively. All created servers share the same certificates and resources, so if - you're running both TLS and non-TLS instances, SimpleHTTP handlers + you're running both TLS and non-TLS instances, HTTP01 handlers will serve the same URLs! """ @@ -52,13 +51,13 @@ class ServerManager(object): :param int port: Port to run the server on. :param challenge_type: Subclass of `acme.challenges.Challenge`, - either `acme.challenge.SimpleHTTP` or `acme.challenges.DVSNI`. + either `acme.challenge.HTTP01` or `acme.challenges.DVSNI`. :returns: Server instance. :rtype: ACMEServerMixin """ - assert challenge_type in (challenges.DVSNI, challenges.SimpleHTTP) + assert challenge_type in (challenges.DVSNI, challenges.HTTP01) if port in self._instances: return self._instances[port].server @@ -66,8 +65,8 @@ class ServerManager(object): try: if challenge_type is challenges.DVSNI: server = acme_standalone.DVSNIServer(address, self.certs) - else: # challenges.SimpleHTTP - server = acme_standalone.SimpleHTTPServer( + else: # challenges.HTTP01 + server = acme_standalone.HTTP01Server( address, self.simple_http_resources) except socket.error as error: raise errors.StandaloneBindError(error, port) @@ -110,7 +109,7 @@ class ServerManager(object): in six.iteritems(self._instances)) -SUPPORTED_CHALLENGES = set([challenges.DVSNI, challenges.SimpleHTTP]) +SUPPORTED_CHALLENGES = set([challenges.DVSNI, challenges.HTTP01]) def supported_challenges_validator(data): @@ -139,7 +138,7 @@ class Authenticator(common.Plugin): """Standalone Authenticator. This authenticator creates its own ephemeral TCP listener on the - necessary port in order to respond to incoming DVSNI and SimpleHTTP + necessary port in order to respond to incoming DVSNI and HTTP01 challenges from the certificate authority. Therefore, it does not rely on any existing server program. """ @@ -151,10 +150,10 @@ class Authenticator(common.Plugin): def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) - # one self-signed key for all DVSNI and SimpleHTTP certificates + # one self-signed key for all DVSNI and HTTP01 certificates self.key = OpenSSL.crypto.PKey() self.key.generate_key(OpenSSL.crypto.TYPE_RSA, bits=2048) - # TODO: generate only when the first SimpleHTTP challenge is solved + # TODO: generate only when the first HTTP01 challenge is solved self.simple_http_cert = acme_crypto_util.gen_ss_cert( self.key, domains=["temp server"]) @@ -185,7 +184,7 @@ class Authenticator(common.Plugin): @property def _necessary_ports(self): necessary_ports = set() - if challenges.SimpleHTTP in self.supported_challenges: + if challenges.HTTP01 in self.supported_challenges: necessary_ports.add(self.config.simple_http_port) if challenges.DVSNI in self.supported_challenges: necessary_ports.add(self.config.dvsni_port) @@ -193,9 +192,9 @@ class Authenticator(common.Plugin): def more_info(self): # pylint: disable=missing-docstring return("This authenticator creates its own ephemeral TCP listener " - "on the necessary port in order to respond to incoming DVSNI " - "and SimpleHTTP challenges from the certificate authority. " - "Therefore, it does not rely on any existing server program.") + "on the necessary port in order to respond to incoming DVSNI " + "and HTTP01 challenges from the certificate authority. " + "Therefore, it does not rely on any existing server program.") def prepare(self): # pylint: disable=missing-docstring pass @@ -237,13 +236,12 @@ class Authenticator(common.Plugin): responses = [] for achall in achalls: - if isinstance(achall, achallenges.SimpleHTTP): + if isinstance(achall.chall, challenges.HTTP01): server = self.servers.run( - self.config.simple_http_port, challenges.SimpleHTTP) - response, validation = achall.gen_response_and_validation( - tls=False) + self.config.simple_http_port, challenges.HTTP01) + response, validation = achall.response_and_validation() self.simple_http_resources.add( - acme_standalone.SimpleHTTPRequestHandler.SimpleHTTPResource( + acme_standalone.HTTP01RequestHandler.HTTP01Resource( chall=achall.chall, response=response, validation=validation)) cert = self.simple_http_cert diff --git a/letsencrypt/plugins/standalone_test.py b/letsencrypt/plugins/standalone_test.py index 3a6941be8..6bf84cb73 100644 --- a/letsencrypt/plugins/standalone_test.py +++ b/letsencrypt/plugins/standalone_test.py @@ -43,12 +43,12 @@ class ServerManagerTest(unittest.TestCase): self._test_run_stop(challenges.DVSNI) def test_run_stop_simplehttp(self): - self._test_run_stop(challenges.SimpleHTTP) + self._test_run_stop(challenges.HTTP01) def test_run_idempotent(self): - server = self.mgr.run(port=0, challenge_type=challenges.SimpleHTTP) + server = self.mgr.run(port=0, challenge_type=challenges.HTTP01) port = server.socket.getsockname()[1] # pylint: disable=no-member - server2 = self.mgr.run(port=port, challenge_type=challenges.SimpleHTTP) + server2 = self.mgr.run(port=port, challenge_type=challenges.HTTP01) self.assertEqual(self.mgr.running(), {port: server}) self.assertTrue(server is server2) self.mgr.stop(port) @@ -60,7 +60,7 @@ class ServerManagerTest(unittest.TestCase): port = some_server.getsockname()[1] self.assertRaises( errors.StandaloneBindError, self.mgr.run, port, - challenge_type=challenges.SimpleHTTP) + challenge_type=challenges.HTTP01) self.assertEqual(self.mgr.running(), {}) @@ -74,9 +74,9 @@ class SupportedChallengesValidatorTest(unittest.TestCase): def test_correct(self): self.assertEqual("dvsni", self._call("dvsni")) - self.assertEqual("simpleHttp", self._call("simpleHttp")) - self.assertEqual("dvsni,simpleHttp", self._call("dvsni,simpleHttp")) - self.assertEqual("simpleHttp,dvsni", self._call("simpleHttp,dvsni")) + self.assertEqual("http-01", self._call("http-01")) + self.assertEqual("dvsni,http-01", self._call("dvsni,http-01")) + self.assertEqual("http-01,dvsni", self._call("http-01,dvsni")) def test_unrecognized(self): assert "foo" not in challenges.Challenge.TYPES @@ -91,25 +91,26 @@ class AuthenticatorTest(unittest.TestCase): def setUp(self): from letsencrypt.plugins.standalone import Authenticator - self.config = mock.MagicMock(dvsni_port=1234, simple_http_port=4321, - standalone_supported_challenges="dvsni,simpleHttp") + self.config = mock.MagicMock( + dvsni_port=1234, simple_http_port=4321, + standalone_supported_challenges="dvsni,http-01") self.auth = Authenticator(self.config, name="standalone") def test_supported_challenges(self): self.assertEqual(self.auth.supported_challenges, - set([challenges.DVSNI, challenges.SimpleHTTP])) + set([challenges.DVSNI, challenges.HTTP01])) def test_more_info(self): self.assertTrue(isinstance(self.auth.more_info(), six.string_types)) def test_get_chall_pref(self): self.assertEqual(set(self.auth.get_chall_pref(domain=None)), - set([challenges.DVSNI, challenges.SimpleHTTP])) + set([challenges.DVSNI, challenges.HTTP01])) @mock.patch("letsencrypt.plugins.standalone.util") def test_perform_alredy_listening(self, mock_util): for chall, port in ((challenges.DVSNI.typ, 1234), - (challenges.SimpleHTTP.typ, 4321)): + (challenges.HTTP01.typ, 4321)): mock_util.already_listening.return_value = True self.config.standalone_supported_challenges = chall self.assertRaises( @@ -152,8 +153,8 @@ class AuthenticatorTest(unittest.TestCase): def test_perform2(self): domain = b'localhost' key = jose.JWK.load(test_util.load_vector('rsa512_key.pem')) - simple_http = achallenges.SimpleHTTP( - challb=acme_util.SIMPLE_HTTP_P, domain=domain, account_key=key) + simple_http = achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.HTTP01_P, domain=domain, account_key=key) dvsni = achallenges.DVSNI( challb=acme_util.DVSNI_P, domain=domain, account_key=key) @@ -167,11 +168,11 @@ class AuthenticatorTest(unittest.TestCase): self.assertTrue(isinstance(responses, list)) self.assertEqual(2, len(responses)) - self.assertTrue(isinstance(responses[0], challenges.SimpleHTTPResponse)) + self.assertTrue(isinstance(responses[0], challenges.HTTP01Response)) self.assertTrue(isinstance(responses[1], challenges.DVSNIResponse)) self.assertEqual(self.auth.servers.run.mock_calls, [ - mock.call(4321, challenges.SimpleHTTP), + mock.call(4321, challenges.HTTP01), mock.call(1234, challenges.DVSNI), ]) self.assertEqual(self.auth.served, { @@ -181,8 +182,8 @@ class AuthenticatorTest(unittest.TestCase): self.assertEqual(1, len(self.auth.simple_http_resources)) self.assertEqual(2, len(self.auth.certs)) self.assertEqual(list(self.auth.simple_http_resources), [ - acme_standalone.SimpleHTTPRequestHandler.SimpleHTTPResource( - acme_util.SIMPLE_HTTP, responses[0], mock.ANY)]) + acme_standalone.HTTP01RequestHandler.HTTP01Resource( + acme_util.HTTP01, responses[0], mock.ANY)]) def test_cleanup(self): self.auth.servers = mock.Mock() diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 7c0ee8fea..18a996926 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -28,7 +28,7 @@ common() { } common --domains le1.wtf --standalone-supported-challenges dvsni auth -common --domains le2.wtf --standalone-supported-challenges simpleHttp run +common --domains le2.wtf --standalone-supported-challenges http-01 run common -a manual -d le.wtf auth export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \ From e62490c0514a6883d9b91aa5d4d6c6747f0364e1 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Oct 2015 15:02:47 +0000 Subject: [PATCH 09/16] http-01 for webroot --- letsencrypt/plugins/webroot.py | 13 ++++++------- letsencrypt/plugins/webroot_test.py | 13 ++++++++----- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/letsencrypt/plugins/webroot.py b/letsencrypt/plugins/webroot.py index f11325f57..879da2527 100644 --- a/letsencrypt/plugins/webroot.py +++ b/letsencrypt/plugins/webroot.py @@ -23,7 +23,7 @@ class Authenticator(common.Plugin): description = "Webroot Authenticator" MORE_INFO = """\ -Authenticator plugin that performs SimpleHTTP challenge by saving +Authenticator plugin that performs http-01 challenge by saving necessary validation resources to appropriate paths on the file system. It expects that there is some other HTTP server configured to serve all files under specified web root ({0}).""" @@ -37,7 +37,7 @@ to serve all files under specified web root ({0}).""" def get_chall_pref(self, domain): # pragma: no cover # pylint: disable=missing-docstring,no-self-use,unused-argument - return [challenges.SimpleHTTP] + return [challenges.HTTP01] def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) @@ -51,8 +51,7 @@ to serve all files under specified web root ({0}).""" if not os.path.isdir(path): raise errors.PluginError( path + " does not exist or is not a directory") - self.full_root = os.path.join( - path, challenges.SimpleHTTPResponse.URI_ROOT_PATH) + self.full_root = os.path.join(path, challenges.HTTP01.URI_ROOT_PATH) logger.debug("Creating root challenges validation dir at %s", self.full_root) @@ -61,7 +60,7 @@ to serve all files under specified web root ({0}).""" except OSError as exception: if exception.errno != errno.EEXIST: raise errors.PluginError( - "Couldn't create root for SimpleHTTP " + "Couldn't create root for http-01 " "challenge responses: {0}", exception) def perform(self, achalls): # pylint: disable=missing-docstring @@ -72,11 +71,11 @@ to serve all files under specified web root ({0}).""" return os.path.join(self.full_root, achall.chall.encode("token")) def _perform_single(self, achall): - response, validation = achall.gen_response_and_validation(tls=False) + response, validation = achall.response_and_validation() path = self._path_for_achall(achall) logger.debug("Attempting to save validation to %s", path) with open(path, "w") as validation_file: - validation_file.write(validation.json_dumps()) + validation_file.write(validation.encode()) return response def cleanup(self, achalls): # pylint: disable=missing-docstring diff --git a/letsencrypt/plugins/webroot_test.py b/letsencrypt/plugins/webroot_test.py index d8c0e2aa2..aa8f16e38 100644 --- a/letsencrypt/plugins/webroot_test.py +++ b/letsencrypt/plugins/webroot_test.py @@ -6,6 +6,7 @@ import unittest import mock +from acme import challenges from acme import jose from letsencrypt import achallenges @@ -21,8 +22,8 @@ KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) class AuthenticatorTest(unittest.TestCase): """Tests for letsencrypt.plugins.webroot.Authenticator.""" - achall = achallenges.SimpleHTTP( - challb=acme_util.SIMPLE_HTTP_P, domain=None, account_key=KEY) + achall = achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.HTTP01_P, domain=None, account_key=KEY) def setUp(self): from letsencrypt.plugins.webroot import Authenticator @@ -70,9 +71,11 @@ class AuthenticatorTest(unittest.TestCase): self.assertEqual(1, len(responses)) self.assertTrue(os.path.exists(self.validation_path)) with open(self.validation_path) as validation_f: - validation = jose.JWS.json_loads(validation_f.read()) - self.assertTrue(responses[0].check_validation( - validation, self.achall.chall, KEY.public_key())) + validation = validation_f.read() + self.assertTrue( + challenges.KeyAuthorizationChallengeResponse( + key_authorization=validation).verify( + self.achall.chall, KEY.public_key())) self.auth.cleanup([self.achall]) self.assertFalse(os.path.exists(self.validation_path)) From 8d913b42c56f2d5998a7c69a5926687768a74277 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Oct 2015 22:12:17 +0000 Subject: [PATCH 10/16] Remove auth_handler_test.TRANSLATE --- letsencrypt/tests/auth_handler_test.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 247fca4e1..8a4c371f5 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -16,15 +16,6 @@ from letsencrypt import le_util from letsencrypt.tests import acme_util -TRANSLATE = { - "dvsni": "DVSNI", - "simpleHttp": "SimpleHTTP", - "dns": "DNS", - "recoveryContact": "RecoveryContact", - "proofOfPossession": "ProofOfPossession", -} - - class ChallengeFactoryTest(unittest.TestCase): # pylint: disable=protected-access From 581a701cd19c7b7236d018109ace58e5a9ba75c8 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Oct 2015 22:23:57 +0000 Subject: [PATCH 11/16] Kill simpleHttp in core --- letsencrypt/achallenges.py | 25 ------------------------- letsencrypt/auth_handler.py | 3 --- letsencrypt/configuration.py | 4 ++-- letsencrypt/constants.py | 2 +- letsencrypt/tests/acme_util.py | 7 ++----- letsencrypt/tests/auth_handler_test.py | 24 ++++++++++++------------ 6 files changed, 17 insertions(+), 48 deletions(-) diff --git a/letsencrypt/achallenges.py b/letsencrypt/achallenges.py index 7aa2bd35a..f08c6a396 100644 --- a/letsencrypt/achallenges.py +++ b/letsencrypt/achallenges.py @@ -85,31 +85,6 @@ class DVSNI(AnnotatedChallenge): return response, cert, key -class SimpleHTTP(AnnotatedChallenge): - """Client annotated "simpleHttp" ACME challenge.""" - __slots__ = ('challb', 'domain', 'account_key') - acme_type = challenges.SimpleHTTP - - def gen_response_and_validation(self, tls): - """Generates a SimpleHTTP response and validation. - - :param bool tls: True if TLS should be used - - :returns: ``(response, validation)`` tuple, where ``response`` is - an instance of `acme.challenges.SimpleHTTPResponse` and - ``validation`` is an instance of - `acme.challenges.SimpleHTTPProvisionedResource`. - :rtype: tuple - - """ - response = challenges.SimpleHTTPResponse(tls=tls) - - validation = response.gen_validation( - self.challb.chall, self.account_key) - logger.debug("Simple HTTP validation payload: %s", validation.payload) - return response, validation - - class DNS(AnnotatedChallenge): """Client annotated "dns" ACME challenge.""" __slots__ = ('challb', 'domain') diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index f7b6bd507..11019daac 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -347,9 +347,6 @@ def challb_to_achall(challb, account_key, domain): if isinstance(chall, challenges.DVSNI): return achallenges.DVSNI( challb=challb, domain=domain, account_key=account_key) - elif isinstance(chall, challenges.SimpleHTTP): - return achallenges.SimpleHTTP( - challb=challb, domain=domain, account_key=account_key) elif isinstance(chall, challenges.DNS): return achallenges.DNS(challb=challb, domain=domain) elif isinstance(chall, challenges.RecoveryContact): diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index f72005233..4fb417127 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -39,7 +39,7 @@ class NamespaceConfig(object): if self.simple_http_port == self.dvsni_port: raise errors.Error( - "Trying to run SimpleHTTP and DVSNI " + "Trying to run http-01 and DVSNI " "on the same port ({0})".format(self.dvsni_port)) def __getattr__(self, name): @@ -82,7 +82,7 @@ class NamespaceConfig(object): if self.namespace.simple_http_port is not None: return self.namespace.simple_http_port else: - return challenges.SimpleHTTPResponse.PORT + return challenges.HTTP01Response.PORT class RenewerConfiguration(object): diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index 362009ec6..15f8c53f0 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -41,7 +41,7 @@ RENEWER_DEFAULTS = dict( EXCLUSIVE_CHALLENGES = frozenset([frozenset([ - challenges.DVSNI, challenges.SimpleHTTP])]) + challenges.DVSNI, challenges.HTTP01])]) """Mutually exclusive challenges.""" diff --git a/letsencrypt/tests/acme_util.py b/letsencrypt/tests/acme_util.py index 1e3086b18..300eb453b 100644 --- a/letsencrypt/tests/acme_util.py +++ b/letsencrypt/tests/acme_util.py @@ -12,8 +12,6 @@ from letsencrypt.tests import test_util KEY = test_util.load_rsa_private_key('rsa512_key.pem') # Challenges -SIMPLE_HTTP = challenges.SimpleHTTP( - token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") HTTP01 = challenges.HTTP01( token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") DVSNI = challenges.DVSNI( @@ -43,7 +41,7 @@ POP = challenges.ProofOfPossession( ) ) -CHALLENGES = [SIMPLE_HTTP, DVSNI, DNS, RECOVERY_CONTACT, POP] +CHALLENGES = [HTTP01, DVSNI, DNS, RECOVERY_CONTACT, POP] DV_CHALLENGES = [chall for chall in CHALLENGES if isinstance(chall, challenges.DVChallenge)] CONT_CHALLENGES = [chall for chall in CHALLENGES @@ -82,13 +80,12 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name # Pending ChallengeBody objects DVSNI_P = chall_to_challb(DVSNI, messages.STATUS_PENDING) -SIMPLE_HTTP_P = chall_to_challb(SIMPLE_HTTP, 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 = [SIMPLE_HTTP_P, DVSNI_P, DNS_P, RECOVERY_CONTACT_P, POP_P] +CHALLENGES_P = [HTTP01_P, DVSNI_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 8a4c371f5..7be37c91e 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -309,8 +309,8 @@ class GenChallengePathTest(unittest.TestCase): return gen_challenge_path(challbs, preferences, combinations) def test_common_case(self): - """Given DVSNI and SimpleHTTP with appropriate combos.""" - challbs = (acme_util.DVSNI_P, acme_util.SIMPLE_HTTP_P) + """Given DVSNI and HTTP01 with appropriate combos.""" + challbs = (acme_util.DVSNI_P, acme_util.HTTP01_P) prefs = [challenges.DVSNI] combos = ((0,), (1,)) @@ -325,7 +325,7 @@ class GenChallengePathTest(unittest.TestCase): challbs = (acme_util.POP_P, acme_util.RECOVERY_CONTACT_P, acme_util.DVSNI_P, - acme_util.SIMPLE_HTTP_P) + acme_util.HTTP01_P) prefs = [challenges.ProofOfPossession, challenges.DVSNI] combos = acme_util.gen_combos(challbs) self.assertEqual(self._call(challbs, prefs, combos), (0, 2)) @@ -337,12 +337,12 @@ class GenChallengePathTest(unittest.TestCase): challbs = (acme_util.RECOVERY_CONTACT_P, acme_util.POP_P, acme_util.DVSNI_P, - acme_util.SIMPLE_HTTP_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.SimpleHTTP, + challenges.HTTP01, challenges.DVSNI, challenges.RecoveryContact] combos = acme_util.gen_combos(challbs) @@ -411,8 +411,8 @@ class IsPreferredTest(unittest.TestCase): def _call(cls, chall, satisfied): from letsencrypt.auth_handler import is_preferred return is_preferred(chall, satisfied, exclusive_groups=frozenset([ - frozenset([challenges.DVSNI, challenges.SimpleHTTP]), - frozenset([challenges.DNS, challenges.SimpleHTTP]), + frozenset([challenges.DVSNI, challenges.HTTP01]), + frozenset([challenges.DNS, challenges.HTTP01]), ])) def test_empty_satisfied(self): @@ -421,7 +421,7 @@ class IsPreferredTest(unittest.TestCase): def test_mutually_exclusvie(self): self.assertFalse( self._call( - acme_util.DVSNI_P, frozenset([acme_util.SIMPLE_HTTP_P]))) + acme_util.DVSNI_P, frozenset([acme_util.HTTP01_P]))) def test_mutually_exclusive_same_type(self): self.assertTrue( @@ -434,13 +434,13 @@ class ReportFailedChallsTest(unittest.TestCase): def setUp(self): kwargs = { - "chall": acme_util.SIMPLE_HTTP, + "chall": acme_util.HTTP01, "uri": "uri", "status": messages.STATUS_INVALID, "error": messages.Error(typ="tls", detail="detail"), } - self.simple_http = achallenges.SimpleHTTP( + self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge( # pylint: disable=star-args challb=messages.ChallengeBody(**kwargs), domain="example.com", @@ -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.simple_http, self.dvsni_same]) + auth_handler._report_failed_challs([self.http01, self.dvsni_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.simple_http, self.dvsni_diff]) + auth_handler._report_failed_challs([self.http01, self.dvsni_diff]) self.assertTrue(mock_zope().add_message.call_count == 2) From 063544817b9138d7cf7fcb7806977e532b67c580 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 1 Nov 2015 08:46:00 +0000 Subject: [PATCH 12/16] Kill simplehttp apache/nginx --- letsencrypt-apache/letsencrypt_apache/dvsni.py | 2 +- letsencrypt-nginx/letsencrypt_nginx/dvsni.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/dvsni.py b/letsencrypt-apache/letsencrypt_apache/dvsni.py index c6c41dc51..ed88bf8a7 100644 --- a/letsencrypt-apache/letsencrypt_apache/dvsni.py +++ b/letsencrypt-apache/letsencrypt_apache/dvsni.py @@ -20,7 +20,7 @@ class ApacheDvsni(common.Dvsni): larger array. ApacheDvsni is capable of solving many challenges at once which causes an indexing issue within ApacheConfigurator who must return all responses in order. Imagine ApacheConfigurator - maintaining state about where all of the SimpleHTTP Challenges, + maintaining state about where all of the http-01 Challenges, Dvsni Challenges belong in the response array. This is an optional utility. diff --git a/letsencrypt-nginx/letsencrypt_nginx/dvsni.py b/letsencrypt-nginx/letsencrypt_nginx/dvsni.py index 9ac2fcd7c..662f10889 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/dvsni.py +++ b/letsencrypt-nginx/letsencrypt_nginx/dvsni.py @@ -26,7 +26,7 @@ class NginxDvsni(common.Dvsni): larger array. NginxDvsni is capable of solving many challenges at once which causes an indexing issue within NginxConfigurator who must return all responses in order. Imagine NginxConfigurator - maintaining state about where all of the SimpleHTTP Challenges, + maintaining state about where all of the http-01 Challenges, Dvsni Challenges belong in the response array. This is an optional utility. From 1d36a15ab702594072a1b46a379de1e1594b2475 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 1 Nov 2015 10:41:57 +0000 Subject: [PATCH 13/16] Kill simpleHttp in acme --- acme/acme/challenges.py | 160 --------------------------------- acme/acme/challenges_test.py | 169 ----------------------------------- 2 files changed, 329 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 86ff0a902..b424071a1 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -318,166 +318,6 @@ class HTTP01(KeyAuthorizationChallenge): return self.key_authorization(account_key) -@Challenge.register # pylint: disable=too-many-ancestors -class SimpleHTTP(_TokenDVChallenge): - """ACME "simpleHttp" challenge.""" - typ = "simpleHttp" - - URI_ROOT_PATH = ".well-known/acme-challenge" - """URI root path for the server provisioned resource.""" - - @property - def path(self): - """Path (starting with '/') for provisioned resource.""" - return '/' + self.URI_ROOT_PATH + '/' + self.encode('token') - - -@ChallengeResponse.register -class SimpleHTTPResponse(ChallengeResponse): - """ACME "simpleHttp" challenge response. - - :ivar bool tls: - - """ - typ = "simpleHttp" - tls = jose.Field("tls", default=True, omitempty=True) - - URI_ROOT_PATH = SimpleHTTP.URI_ROOT_PATH - _URI_TEMPLATE = "{scheme}://{domain}/" + URI_ROOT_PATH + "/{token}" - - CONTENT_TYPE = "application/jose+json" - PORT = 80 - TLS_PORT = 443 - - @property - def scheme(self): - """URL scheme for the provisioned resource.""" - return "https" if self.tls else "http" - - @property - def port(self): - """Port that the ACME client should be listening for validation.""" - return self.TLS_PORT if self.tls else self.PORT - - def uri(self, domain, chall): - """Create an URI to the provisioned resource. - - Forms an URI to the HTTPS server provisioned resource - (containing :attr:`~SimpleHTTP.token`). - - :param unicode domain: Domain name being verified. - :param challenges.SimpleHTTP chall: - - """ - return self._URI_TEMPLATE.format( - scheme=self.scheme, domain=domain, token=chall.encode("token")) - - def gen_resource(self, chall): - """Generate provisioned resource. - - :param challenges.SimpleHTTP chall: - :rtype: SimpleHTTPProvisionedResource - - """ - return SimpleHTTPProvisionedResource(token=chall.token, tls=self.tls) - - def gen_validation(self, chall, account_key, alg=jose.RS256, **kwargs): - """Generate validation. - - :param challenges.SimpleHTTP chall: - :param .JWK account_key: Private account key. - :param .JWA alg: - - :returns: `.SimpleHTTPProvisionedResource` signed in `.JWS` - :rtype: .JWS - - """ - return jose.JWS.sign( - payload=self.gen_resource(chall).json_dumps( - sort_keys=True).encode('utf-8'), - key=account_key, alg=alg, **kwargs) - - def check_validation(self, validation, chall, account_public_key): - """Check validation. - - :param .JWS validation: - :param challenges.SimpleHTTP chall: - :param .JWK account_public_key: - - :rtype: bool - - """ - if not validation.verify(key=account_public_key): - return False - - try: - resource = SimpleHTTPProvisionedResource.json_loads( - validation.payload.decode('utf-8')) - except jose.DeserializationError as error: - logger.debug(error) - return False - - return resource.token == chall.token and resource.tls == self.tls - - def simple_verify(self, chall, domain, account_public_key, port=None): - """Simple verify. - - According to the ACME specification, "the ACME server MUST - ignore the certificate provided by the HTTPS server", so - ``requests.get`` is called with ``verify=False``. - - :param challenges.SimpleHTTP chall: Corresponding challenge. - :param unicode domain: Domain name being verified. - :param JWK account_public_key: Public key for the key pair - being authorized. If ``None`` key verification is not - performed! - :param int port: Port used in the validation. - - :returns: ``True`` iff validation is successful, ``False`` - otherwise. - :rtype: bool - - """ - # TODO: ACME specification defines URI template that doesn't - # allow to use a custom port... Make sure port is not in the - # 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) - domain += ":{0}".format(port) - - uri = self.uri(domain, chall) - logger.debug("Verifying %s at %s...", chall.typ, uri) - try: - http_response = requests.get(uri, verify=False) - except requests.exceptions.RequestException as error: - logger.error("Unable to reach %s: %s", uri, error) - return False - logger.debug("Received %s: %s. Headers: %s", http_response, - http_response.text, http_response.headers) - - if self.CONTENT_TYPE != http_response.headers.get( - "Content-Type", self.CONTENT_TYPE): - return False - - try: - validation = jose.JWS.json_loads(http_response.text) - except jose.DeserializationError as error: - logger.debug(error) - return False - - return self.check_validation(validation, chall, account_public_key) - - -class SimpleHTTPProvisionedResource(jose.JSONObjectWithFields): - """SimpleHTTP provisioned resource.""" - typ = fields.Fixed("type", SimpleHTTP.typ) - token = SimpleHTTP._fields["token"] - # If the "tls" field is not included in the response, then - # validation object MUST have its "tls" field set to "true". - tls = jose.Field("tls", omitempty=False) - - @Challenge.register # pylint: disable=too-many-ancestors class DVSNI(_TokenDVChallenge): """ACME "dvsni" challenge. diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 53e6b528a..0708f3782 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -186,175 +186,6 @@ class HTTP01Test(unittest.TestCase): self.msg.update(token=b'..').good_token) -class SimpleHTTPTest(unittest.TestCase): - - def setUp(self): - from acme.challenges import SimpleHTTP - self.msg = SimpleHTTP( - token=jose.decode_b64jose( - 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')) - self.jmsg = { - 'type': 'simpleHttp', - 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', - } - - def test_path(self): - self.assertEqual(self.msg.path, '/.well-known/acme-challenge/' - 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA') - - def test_to_partial_json(self): - self.assertEqual(self.jmsg, self.msg.to_partial_json()) - - def test_from_json(self): - from acme.challenges import SimpleHTTP - self.assertEqual(self.msg, SimpleHTTP.from_json(self.jmsg)) - - def test_from_json_hashable(self): - from acme.challenges import SimpleHTTP - hash(SimpleHTTP.from_json(self.jmsg)) - - def test_good_token(self): - self.assertTrue(self.msg.good_token) - self.assertFalse( - self.msg.update(token=b'..').good_token) - - -class SimpleHTTPResponseTest(unittest.TestCase): - # pylint: disable=too-many-instance-attributes - - def setUp(self): - from acme.challenges import SimpleHTTPResponse - self.msg_http = SimpleHTTPResponse(tls=False) - self.msg_https = SimpleHTTPResponse(tls=True) - self.jmsg_http = { - 'resource': 'challenge', - 'type': 'simpleHttp', - 'tls': False, - } - self.jmsg_https = { - 'resource': 'challenge', - 'type': 'simpleHttp', - 'tls': True, - } - - from acme.challenges import SimpleHTTP - self.chall = SimpleHTTP(token=(b"x" * 16)) - self.resp_http = SimpleHTTPResponse(tls=False) - self.resp_https = SimpleHTTPResponse(tls=True) - self.good_headers = {'Content-Type': SimpleHTTPResponse.CONTENT_TYPE} - - def test_to_partial_json(self): - self.assertEqual(self.jmsg_http, self.msg_http.to_partial_json()) - self.assertEqual(self.jmsg_https, self.msg_https.to_partial_json()) - - def test_from_json(self): - from acme.challenges import SimpleHTTPResponse - self.assertEqual( - self.msg_http, SimpleHTTPResponse.from_json(self.jmsg_http)) - self.assertEqual( - self.msg_https, SimpleHTTPResponse.from_json(self.jmsg_https)) - - def test_from_json_hashable(self): - from acme.challenges import SimpleHTTPResponse - hash(SimpleHTTPResponse.from_json(self.jmsg_http)) - hash(SimpleHTTPResponse.from_json(self.jmsg_https)) - - def test_scheme(self): - self.assertEqual('http', self.msg_http.scheme) - self.assertEqual('https', self.msg_https.scheme) - - def test_port(self): - self.assertEqual(80, self.msg_http.port) - self.assertEqual(443, self.msg_https.port) - - def test_uri(self): - self.assertEqual( - 'http://example.com/.well-known/acme-challenge/' - 'eHh4eHh4eHh4eHh4eHh4eA', self.msg_http.uri( - 'example.com', self.chall)) - self.assertEqual( - 'https://example.com/.well-known/acme-challenge/' - 'eHh4eHh4eHh4eHh4eHh4eA', self.msg_https.uri( - 'example.com', self.chall)) - - def test_gen_check_validation(self): - account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) - self.assertTrue(self.resp_http.check_validation( - validation=self.resp_http.gen_validation(self.chall, account_key), - chall=self.chall, account_public_key=account_key.public_key())) - - def test_gen_check_validation_wrong_key(self): - key1 = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) - key2 = jose.JWKRSA.load(test_util.load_vector('rsa1024_key.pem')) - self.assertFalse(self.resp_http.check_validation( - validation=self.resp_http.gen_validation(self.chall, key1), - chall=self.chall, account_public_key=key2.public_key())) - - def test_check_validation_wrong_payload(self): - account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) - validations = tuple( - jose.JWS.sign(payload=payload, alg=jose.RS256, key=account_key) - for payload in (b'', b'{}', self.chall.json_dumps().encode('utf-8'), - self.resp_http.json_dumps().encode('utf-8')) - ) - for validation in validations: - self.assertFalse(self.resp_http.check_validation( - validation=validation, chall=self.chall, - account_public_key=account_key.public_key())) - - def test_check_validation_wrong_fields(self): - resource = self.resp_http.gen_resource(self.chall) - account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) - validations = tuple( - jose.JWS.sign(payload=bad_resource.json_dumps().encode('utf-8'), - alg=jose.RS256, key=account_key) - for bad_resource in (resource.update(tls=True), - resource.update(token=(b'x' * 20))) - ) - for validation in validations: - self.assertFalse(self.resp_http.check_validation( - validation=validation, chall=self.chall, - account_public_key=account_key.public_key())) - - @mock.patch("acme.challenges.requests.get") - def test_simple_verify_good_validation(self, mock_get): - account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) - for resp in self.resp_http, self.resp_https: - mock_get.reset_mock() - validation = resp.gen_validation(self.chall, account_key) - mock_get.return_value = mock.MagicMock( - text=validation.json_dumps(), headers=self.good_headers) - self.assertTrue(resp.simple_verify(self.chall, "local", None)) - mock_get.assert_called_once_with(resp.uri( - "local", self.chall), verify=False) - - @mock.patch("acme.challenges.requests.get") - def test_simple_verify_bad_validation(self, mock_get): - mock_get.return_value = mock.MagicMock( - text="!", headers=self.good_headers) - self.assertFalse(self.resp_http.simple_verify( - self.chall, "local", None)) - - @mock.patch("acme.challenges.requests.get") - def test_simple_verify_bad_content_type(self, mock_get): - mock_get().text = self.chall.token - self.assertFalse(self.resp_http.simple_verify( - self.chall, "local", None)) - - @mock.patch("acme.challenges.requests.get") - def test_simple_verify_connection_error(self, mock_get): - mock_get.side_effect = requests.exceptions.RequestException - self.assertFalse(self.resp_http.simple_verify( - self.chall, "local", None)) - - @mock.patch("acme.challenges.requests.get") - def test_simple_verify_port(self, mock_get): - self.resp_http.simple_verify( - self.chall, domain="local", account_public_key=None, port=4430) - self.assertEqual("local:4430", urllib_parse.urlparse( - mock_get.mock_calls[0][1][0]).netloc) - - class DVSNITest(unittest.TestCase): def setUp(self): From 23d3c3b1e2bc5fbed5fb61c855e1a207ab1a9dda Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 31 Oct 2015 22:04:52 +0000 Subject: [PATCH 14/16] Rename --simple-http-port to --http-01-port --- letsencrypt/cli.py | 4 ++-- letsencrypt/configuration.py | 8 ++++---- letsencrypt/interfaces.py | 2 +- letsencrypt/plugins/manual.py | 6 +++--- letsencrypt/plugins/manual_test.py | 4 ++-- letsencrypt/plugins/standalone.py | 4 ++-- letsencrypt/plugins/standalone_test.py | 2 +- letsencrypt/renewer.py | 2 +- letsencrypt/tests/configuration_test.py | 10 +++++----- letsencrypt/tests/renewer_test.py | 2 +- tests/integration/_common.sh | 2 +- 11 files changed, 23 insertions(+), 23 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index fdfa5bbfb..f8c0da85a 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -854,8 +854,8 @@ def prepare_and_parse_args(plugins, args): helpful.add( "testing", "--dvsni-port", type=int, default=flag_default("dvsni_port"), help=config_help("dvsni_port")) - helpful.add("testing", "--simple-http-port", type=int, - help=config_help("simple_http_port")) + helpful.add("testing", "--http-01-port", dest="http01_port", type=int, + help=config_help("http01_port")) helpful.add_group( "security", description="Security parameters & server settings") diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 4fb417127..b604651e9 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -37,7 +37,7 @@ class NamespaceConfig(object): def __init__(self, namespace): self.namespace = namespace - if self.simple_http_port == self.dvsni_port: + if self.http01_port == self.dvsni_port: raise errors.Error( "Trying to run http-01 and DVSNI " "on the same port ({0})".format(self.dvsni_port)) @@ -78,9 +78,9 @@ class NamespaceConfig(object): self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR) @property - def simple_http_port(self): # pylint: disable=missing-docstring - if self.namespace.simple_http_port is not None: - return self.namespace.simple_http_port + def http01_port(self): # pylint: disable=missing-docstring + if self.namespace.http01_port is not None: + return self.namespace.http01_port else: return challenges.HTTP01Response.PORT diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 8bf714c88..498b01683 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -223,7 +223,7 @@ class IConfig(zope.interface.Interface): "Port number to perform DVSNI challenge. " "Boulder in testing mode defaults to 5001.") - simple_http_port = zope.interface.Attribute( + http01_port = zope.interface.Attribute( "Port used in the SimpleHttp challenge.") diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index ffaad3d99..823de89ec 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -132,8 +132,8 @@ s.serve_forever()" """ # same server: default command doesn't support virtual hosts response, validation = achall.response_and_validation() - port = (response.port if self.config.simple_http_port is None - else int(self.config.simple_http_port)) + port = (response.port if self.config.http01_port is None + else int(self.config.http01_port)) command = self.CMD_TEMPLATE.format( root=self._root, achall=achall, response=response, # TODO(kuba): pipes still necessary? @@ -174,7 +174,7 @@ s.serve_forever()" """ if response.simple_verify( achall.chall, achall.domain, - achall.account_key.public_key(), self.config.simple_http_port): + achall.account_key.public_key(), self.config.http01_port): return response else: logger.error( diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py index 431cd07a7..a9281902f 100644 --- a/letsencrypt/plugins/manual_test.py +++ b/letsencrypt/plugins/manual_test.py @@ -23,13 +23,13 @@ class AuthenticatorTest(unittest.TestCase): def setUp(self): from letsencrypt.plugins.manual import Authenticator self.config = mock.MagicMock( - simple_http_port=8080, manual_test_mode=False) + http01_port=8080, manual_test_mode=False) self.auth = Authenticator(config=self.config, name="manual") self.achalls = [achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)] config_test_mode = mock.MagicMock( - simple_http_port=8080, manual_test_mode=True) + http01_port=8080, manual_test_mode=True) self.auth_test_mode = Authenticator( config=config_test_mode, name="manual") diff --git a/letsencrypt/plugins/standalone.py b/letsencrypt/plugins/standalone.py index c45337507..5041091e4 100644 --- a/letsencrypt/plugins/standalone.py +++ b/letsencrypt/plugins/standalone.py @@ -185,7 +185,7 @@ class Authenticator(common.Plugin): def _necessary_ports(self): necessary_ports = set() if challenges.HTTP01 in self.supported_challenges: - necessary_ports.add(self.config.simple_http_port) + necessary_ports.add(self.config.http01_port) if challenges.DVSNI in self.supported_challenges: necessary_ports.add(self.config.dvsni_port) return necessary_ports @@ -238,7 +238,7 @@ class Authenticator(common.Plugin): for achall in achalls: if isinstance(achall.chall, challenges.HTTP01): server = self.servers.run( - self.config.simple_http_port, challenges.HTTP01) + self.config.http01_port, challenges.HTTP01) response, validation = achall.response_and_validation() self.simple_http_resources.add( acme_standalone.HTTP01RequestHandler.HTTP01Resource( diff --git a/letsencrypt/plugins/standalone_test.py b/letsencrypt/plugins/standalone_test.py index 6bf84cb73..15da04417 100644 --- a/letsencrypt/plugins/standalone_test.py +++ b/letsencrypt/plugins/standalone_test.py @@ -92,7 +92,7 @@ class AuthenticatorTest(unittest.TestCase): def setUp(self): from letsencrypt.plugins.standalone import Authenticator self.config = mock.MagicMock( - dvsni_port=1234, simple_http_port=4321, + dvsni_port=1234, http01_port=4321, standalone_supported_challenges="dvsni,http-01") self.auth = Authenticator(self.config, name="standalone") diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py index c0014f4b2..40e49702a 100644 --- a/letsencrypt/renewer.py +++ b/letsencrypt/renewer.py @@ -76,7 +76,7 @@ def renew(cert, old_version): # was an int, not a str) config.rsa_key_size = int(config.rsa_key_size) config.dvsni_port = int(config.dvsni_port) - config.namespace.simple_http_port = int(config.namespace.simple_http_port) + config.namespace.http01_port = int(config.namespace.http01_port) zope.component.provideUtility(config) try: authenticator = plugins[renewalparams["authenticator"]] diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index 44bccb577..c7e227ee5 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -14,7 +14,7 @@ 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, simple_http_port=4321) + dvsni_port=1234, http01_port=4321) from letsencrypt.configuration import NamespaceConfig self.config = NamespaceConfig(self.namespace) @@ -54,10 +54,10 @@ class NamespaceConfigTest(unittest.TestCase): self.assertEqual(self.config.key_dir, '/tmp/config/keys') self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t') - def test_simple_http_port(self): - self.assertEqual(4321, self.config.simple_http_port) - self.namespace.simple_http_port = None - self.assertEqual(80, self.config.simple_http_port) + def test_http01_port(self): + self.assertEqual(4321, self.config.http01_port) + self.namespace.http01_port = None + self.assertEqual(80, self.config.http01_port) class RenewerConfigurationTest(unittest.TestCase): diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index 2123db367..05d7e123d 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -689,7 +689,7 @@ class RenewableCertTests(BaseRenewableCertTest): 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"]["simple_http_port"] = "1234" + self.test_rc.configfile["renewalparams"]["http01_port"] = "1234" self.test_rc.configfile["renewalparams"]["account"] = "abcde" mock_auth = mock.MagicMock() mock_pd.PluginsRegistry.find_all.return_value = {"apache": mock_auth} diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index 07eca44b2..cd894fd10 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -16,7 +16,7 @@ letsencrypt_test () { --server "${SERVER:-http://localhost:4000/directory}" \ --no-verify-ssl \ --dvsni-port 5001 \ - --simple-http-port 5002 \ + --http-01-port 5002 \ --manual-test-mode \ $store_flags \ --text \ From 99c5c2034fd54aacef1b29794e0551bf6bbf262e Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 1 Nov 2015 11:19:35 +0000 Subject: [PATCH 15/16] Revert "Quickfix for misterious abstract-class-little-used" This reverts commit 01bc073111d139183e24c9d702d5942beb864022. --- acme/acme/jose/jwk.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/acme/acme/jose/jwk.py b/acme/acme/jose/jwk.py index c82134da9..4d07229b3 100644 --- a/acme/acme/jose/jwk.py +++ b/acme/acme/jose/jwk.py @@ -21,12 +21,6 @@ from acme.jose import util logger = logging.getLogger(__name__) -# TODO: bug in pylint? -# ************* Module acme.challenges -# R:153, 0: Abstract class is only referenced 1 times (abstract-class-little-used) -# pylint: disable=abstract-class-little-used - - class JWK(json_util.TypedJSONObjectWithFields): # pylint: disable=too-few-public-methods """JSON Web Key.""" From ee68d5eb8607edfd7628e0426047fc072399c7a7 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 1 Nov 2015 11:23:56 +0000 Subject: [PATCH 16/16] Another attempt at abstract-class-little-used --- .pylintrc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.pylintrc b/.pylintrc index 268d61ec6..f31b77e9a 100644 --- a/.pylintrc +++ b/.pylintrc @@ -38,8 +38,9 @@ load-plugins=linter_plugin # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=fixme,locally-disabled,abstract-class-not-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name -# abstract-class-not-used cannot be disabled locally (at least in pylint 1.4.1) +disable=fixme,locally-disabled,abstract-class-not-used,abstract-class-little-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name +# abstract-class-not-used cannot be disabled locally (at least in +# pylint 1.4.1), same for abstract-class-little-used [REPORTS]