From 3a5f7a026b17ae5423e14c0b4377f1ae25f6a84f Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Nov 2015 12:32:23 +0000 Subject: [PATCH 01/14] Fix old reference to SimpleHTTP --- acme/acme/challenges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index c5855a7ca..57e74144b 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -246,7 +246,7 @@ class HTTP01Response(KeyAuthorizationChallengeResponse): # request URI, if it's standard. if port is not None and port != self.PORT: logger.warning( - "Using non-standard port for SimpleHTTP verification: %s", port) + "Using non-standard port for http-01 verification: %s", port) domain += ":{0}".format(port) uri = chall.uri(domain) From dd92e9529011b5f306799de68dbdb61e7609f96f Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Nov 2015 13:59:02 +0000 Subject: [PATCH 02/14] Remove remaints of simpleHttp from standalone plugin --- letsencrypt/plugins/standalone.py | 26 ++++++++++---------------- letsencrypt/plugins/standalone_test.py | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/letsencrypt/plugins/standalone.py b/letsencrypt/plugins/standalone.py index 5041091e4..3975e9292 100644 --- a/letsencrypt/plugins/standalone.py +++ b/letsencrypt/plugins/standalone.py @@ -28,9 +28,9 @@ class ServerManager(object): Manager for `ACMEServer` and `ACMETLSServer` instances. - `certs` and `simple_http_resources` correspond to + `certs` and `http_01_resources` correspond to `acme.crypto_util.SSLSocket.certs` and - `acme.crypto_util.SSLSocket.simple_http_resources` respectively. All + `acme.crypto_util.SSLSocket.http_01_resources` respectively. All created servers share the same certificates and resources, so if you're running both TLS and non-TLS instances, HTTP01 handlers will serve the same URLs! @@ -38,10 +38,10 @@ class ServerManager(object): """ _Instance = collections.namedtuple("_Instance", "server thread") - def __init__(self, certs, simple_http_resources): + def __init__(self, certs, http_01_resources): self._instances = {} self.certs = certs - self.simple_http_resources = simple_http_resources + self.http_01_resources = http_01_resources def run(self, port, challenge_type): """Run ACME server on specified ``port``. @@ -67,7 +67,7 @@ class ServerManager(object): server = acme_standalone.DVSNIServer(address, self.certs) else: # challenges.HTTP01 server = acme_standalone.HTTP01Server( - address, self.simple_http_resources) + address, self.http_01_resources) except socket.error as error: raise errors.StandaloneBindError(error, port) @@ -150,12 +150,9 @@ class Authenticator(common.Plugin): def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) - # one self-signed key for all DVSNI and HTTP01 certificates + # one self-signed key for all DVSNI certificates self.key = OpenSSL.crypto.PKey() self.key.generate_key(OpenSSL.crypto.TYPE_RSA, bits=2048) - # 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"]) self.served = collections.defaultdict(set) @@ -164,9 +161,9 @@ class Authenticator(common.Plugin): # GIL, the operations are safe, c.f. # https://docs.python.org/2/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe self.certs = {} - self.simple_http_resources = set() + self.http_01_resources = set() - self.servers = ServerManager(self.certs, self.simple_http_resources) + self.servers = ServerManager(self.certs, self.http_01_resources) @classmethod def add_parser_arguments(cls, add): @@ -240,17 +237,14 @@ class Authenticator(common.Plugin): server = self.servers.run( self.config.http01_port, challenges.HTTP01) response, validation = achall.response_and_validation() - self.simple_http_resources.add( + self.http_01_resources.add( acme_standalone.HTTP01RequestHandler.HTTP01Resource( chall=achall.chall, response=response, validation=validation)) - cert = self.simple_http_cert - domain = achall.domain else: # DVSNI server = self.servers.run(self.config.dvsni_port, challenges.DVSNI) response, cert, _ = achall.gen_cert_and_response(self.key) - domain = response.z_domain - self.certs[domain] = (self.key, cert) + self.certs[response.z_domain] = (self.key, cert) self.served[server].add(achall) responses.append(response) diff --git a/letsencrypt/plugins/standalone_test.py b/letsencrypt/plugins/standalone_test.py index 15da04417..c1de52ac8 100644 --- a/letsencrypt/plugins/standalone_test.py +++ b/letsencrypt/plugins/standalone_test.py @@ -24,13 +24,13 @@ class ServerManagerTest(unittest.TestCase): def setUp(self): from letsencrypt.plugins.standalone import ServerManager self.certs = {} - self.simple_http_resources = {} - self.mgr = ServerManager(self.certs, self.simple_http_resources) + self.http_01_resources = {} + self.mgr = ServerManager(self.certs, self.http_01_resources) def test_init(self): self.assertTrue(self.mgr.certs is self.certs) self.assertTrue( - self.mgr.simple_http_resources is self.simple_http_resources) + self.mgr.http_01_resources is self.http_01_resources) def _test_run_stop(self, challenge_type): server = self.mgr.run(port=0, challenge_type=challenge_type) @@ -42,7 +42,7 @@ class ServerManagerTest(unittest.TestCase): def test_run_stop_dvsni(self): self._test_run_stop(challenges.DVSNI) - def test_run_stop_simplehttp(self): + def test_run_stop_http_01(self): self._test_run_stop(challenges.HTTP01) def test_run_idempotent(self): @@ -153,7 +153,7 @@ 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.KeyAuthorizationAnnotatedChallenge( + http_01 = 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) @@ -164,7 +164,7 @@ class AuthenticatorTest(unittest.TestCase): return "server{0}".format(port) self.auth.servers.run.side_effect = _run - responses = self.auth.perform2([simple_http, dvsni]) + responses = self.auth.perform2([http_01, dvsni]) self.assertTrue(isinstance(responses, list)) self.assertEqual(2, len(responses)) @@ -177,11 +177,11 @@ class AuthenticatorTest(unittest.TestCase): ]) self.assertEqual(self.auth.served, { "server1234": set([dvsni]), - "server4321": set([simple_http]), + "server4321": set([http_01]), }) - self.assertEqual(1, len(self.auth.simple_http_resources)) - self.assertEqual(2, len(self.auth.certs)) - self.assertEqual(list(self.auth.simple_http_resources), [ + self.assertEqual(1, len(self.auth.http_01_resources)) + self.assertEqual(1, len(self.auth.certs)) + self.assertEqual(list(self.auth.http_01_resources), [ acme_standalone.HTTP01RequestHandler.HTTP01Resource( acme_util.HTTP01, responses[0], mock.ANY)]) From c805ebc2bf6205085269f00aced226c6863eddb2 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 1 Nov 2015 11:52:54 +0000 Subject: [PATCH 03/14] Use KEY in DVSNI tests --- acme/acme/challenges_test.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 0708f3782..86291d0e8 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -571,8 +571,6 @@ class ProofOfPossessionResponseTest(unittest.TestCase): class DNSTest(unittest.TestCase): def setUp(self): - self.account_key = jose.JWKRSA.load( - test_util.load_vector('rsa512_key.pem')) from acme.challenges import DNS self.msg = DNS(token=jose.b64decode( b'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA')) @@ -594,34 +592,33 @@ class DNSTest(unittest.TestCase): def test_gen_check_validation(self): self.assertTrue(self.msg.check_validation( - self.msg.gen_validation(self.account_key), - self.account_key.public_key())) + self.msg.gen_validation(KEY), KEY.public_key())) def test_gen_check_validation_wrong_key(self): key2 = jose.JWKRSA.load(test_util.load_vector('rsa1024_key.pem')) self.assertFalse(self.msg.check_validation( - self.msg.gen_validation(self.account_key), key2.public_key())) + self.msg.gen_validation(KEY), key2.public_key())) def test_check_validation_wrong_payload(self): validations = tuple( - jose.JWS.sign(payload=payload, alg=jose.RS256, key=self.account_key) + jose.JWS.sign(payload=payload, alg=jose.RS256, key=KEY) for payload in (b'', b'{}') ) for validation in validations: self.assertFalse(self.msg.check_validation( - validation, self.account_key.public_key())) + validation, KEY.public_key())) def test_check_validation_wrong_fields(self): bad_validation = jose.JWS.sign( payload=self.msg.update(token=b'x' * 20).json_dumps().encode('utf-8'), - alg=jose.RS256, key=self.account_key) + alg=jose.RS256, key=KEY) self.assertFalse(self.msg.check_validation( - bad_validation, self.account_key.public_key())) + bad_validation, KEY.public_key())) def test_gen_response(self): with mock.patch('acme.challenges.DNS.gen_validation') as mock_gen: mock_gen.return_value = mock.sentinel.validation - response = self.msg.gen_response(self.account_key) + response = self.msg.gen_response(KEY) from acme.challenges import DNSResponse self.assertTrue(isinstance(response, DNSResponse)) self.assertEqual(response.validation, mock.sentinel.validation) From b864c77b623bf458b9675f1ce5cf31d9015d18d1 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 1 Nov 2015 12:20:38 +0000 Subject: [PATCH 04/14] Add tls-sni-01 to acme --- acme/acme/challenges.py | 130 +++++++++++++++++++++++++++++++-- acme/acme/challenges_test.py | 134 +++++++++++++++++++++++++++++++++++ 2 files changed, 260 insertions(+), 4 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 57e74144b..5f97547ee 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -187,7 +187,7 @@ class KeyAuthorizationChallenge(_TokenDVChallenge): key_authorization=self.key_authorization(account_key)) @abc.abstractmethod - def validation(self, account_key): + def validation(self, account_key, **kwargs): """Generate validation for the challenge. Subclasses must implement this method, but they are likely to @@ -201,7 +201,7 @@ class KeyAuthorizationChallenge(_TokenDVChallenge): """ raise NotImplementedError() # pragma: no cover - def response_and_validation(self, account_key): + def response_and_validation(self, account_key, *args, **kwargs): """Generate response and validation. Convenience function that return results of `response` and @@ -211,7 +211,8 @@ class KeyAuthorizationChallenge(_TokenDVChallenge): :rtype: tuple """ - return (self.response(account_key), self.validation(account_key)) + return (self.response(account_key), + self.validation(account_key, *args, **kwargs)) @ChallengeResponse.register @@ -308,7 +309,7 @@ class HTTP01(KeyAuthorizationChallenge): """ return "http://" + domain + self.path - def validation(self, account_key): + def validation(self, account_key, **unused_kwargs): """Generate validation. :param JWK account_key: @@ -318,6 +319,127 @@ class HTTP01(KeyAuthorizationChallenge): return self.key_authorization(account_key) +@ChallengeResponse.register +class TLSSNI01Response(KeyAuthorizationChallengeResponse): + """ACME tls-sni-01 challenge response.""" + typ = "tls-sni-01" + + DOMAIN_SUFFIX = b".acme.invalid" + """Domain name suffix.""" + + PORT = 443 + + @property + def z(self): + """``z`` value used for verification.""" + return hashlib.sha256( + self.key_authorization.encode("utf-8")).hexdigest().encode() + + @property + def z_domain(self): + """Domain name used for verification, generated from `z`.""" + return self.z[:32] + b'.' + self.z[32:] + self.DOMAIN_SUFFIX + + def gen_cert(self, key=None, bits=2048): + """Generate tls-sni-01 certificate. + + :param OpenSSL.crypto.PKey key: Optional private key used in + certificate generation. If not provided (``None``), then + fresh key will be generated. + :param int bits: Number of bits for newly generated key. + + :rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey` + + """ + if key is None: + key = OpenSSL.crypto.PKey() + key.generate_key(OpenSSL.crypto.TYPE_RSA, bits) + return crypto_util.gen_ss_cert(key, [ + # z_domain is too big to fit into CN, hence first dummy domain + 'dummy', self.z_domain.decode()], force_san=True), key + + def probe_cert(self, domain, **kwargs): + """Probe tls-sni-01 challenge certificate. + + :param unicode domain: + + """ + # TODO: domain is not necessary if host is provided + if "host" not in kwargs: + host = socket.gethostbyname(domain) + logging.debug('%s resolved to %s', domain, host) + kwargs["host"] = host + + kwargs.setdefault("port", self.PORT) + kwargs["name"] = self.z_domain + # TODO: try different methods? + # pylint: disable=protected-access + return crypto_util.probe_sni(**kwargs) + + def verify_cert(self, cert): + """Verify tls-sni-01 challenge certificate.""" + # pylint: disable=protected-access + sans = crypto_util._pyopenssl_cert_or_req_san(cert) + logging.debug('Certificate %s. SANs: %s', cert.digest('sha1'), sans) + return self.z_domain.decode() in sans + + def simple_verify(self, chall, domain, account_public_key, + cert=None, **kwargs): + """Simple verify. + + Verify ``validation`` using ``account_public_key``, optionally + probe tls-sni-01 certificate and check using `verify_cert`. + + :param .challenges.TLSSNI01 chall: Corresponding challenge. + :param str domain: Domain name being validated. + :param JWK account_public_key: + :param OpenSSL.crypto.X509 cert: Optional certificate. If not + provided (``None``) certificate will be retrieved using + `probe_cert`. + + + :returns: ``True`` iff client's control of the domain has been + verified, ``False`` otherwise. + :rtype: bool + + """ + if not self.verify(chall, account_public_key): + logger.debug("Verification of key authorization in response failed") + return False + + if cert is None: + try: + cert = self.probe_cert(domain=domain, **kwargs) + except errors.Error as error: + logger.debug(error, exc_info=True) + return False + + return self.verify_cert(cert) + + +@Challenge.register # pylint: disable=too-many-ancestors +class TLSSNI01(KeyAuthorizationChallenge): + """ACME tls-sni-01 challenge.""" + response_cls = TLSSNI01Response + typ = response_cls.typ + + # boulder#962, ietf-wg-acme#22 + #n = jose.Field("n", encoder=int, decoder=int) + + def validation(self, account_key, **kwargs): + """Generate validation. + + :param JWK account_key: + :param OpenSSL.crypto.PKey cert_key: Optional private key used + in certificate generation. If not provided (``None``), then + fresh key will be generated. + + :rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey` + + """ + return self.response(account_key).gen_cert(key=kwargs.get('cert_key')) + + @Challenge.register # 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 86291d0e8..3fcb01e4d 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -186,6 +186,140 @@ class HTTP01Test(unittest.TestCase): self.msg.update(token=b'..').good_token) +class TLSSNI01ResponseTest(unittest.TestCase): + # pylint: disable=too-many-instance-attributes + + def setUp(self): + from acme.challenges import TLSSNI01 + self.chall = TLSSNI01( + token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e')) + + self.response = self.chall.response(KEY) + self.jmsg = { + 'resource': 'challenge', + 'type': 'tls-sni-01', + 'keyAuthorization': self.response.key_authorization, + } + + # pylint: disable=invalid-name + label1 = b'dc38d9c3fa1a4fdcc3a5501f2d38583f' + label2 = b'b7793728f084394f2a1afd459556bb5c' + self.z = label1 + label2 + self.z_domain = label1 + b'.' + label2 + b'.acme.invalid' + self.domain = 'foo.com' + + def test_z_and_domain(self): + self.assertEqual(self.z, self.response.z) + self.assertEqual(self.z_domain, self.response.z_domain) + + def test_to_partial_json(self): + self.assertEqual(self.jmsg, self.response.to_partial_json()) + + def test_from_json(self): + from acme.challenges import TLSSNI01Response + self.assertEqual(self.response, TLSSNI01Response.from_json(self.jmsg)) + + def test_from_json_hashable(self): + from acme.challenges import TLSSNI01Response + hash(TLSSNI01Response.from_json(self.jmsg)) + + @mock.patch('acme.challenges.socket.gethostbyname') + @mock.patch('acme.challenges.crypto_util.probe_sni') + def test_probe_cert(self, mock_probe_sni, mock_gethostbyname): + mock_gethostbyname.return_value = '127.0.0.1' + self.response.probe_cert('foo.com') + mock_gethostbyname.assert_called_once_with('foo.com') + mock_probe_sni.assert_called_once_with( + host='127.0.0.1', port=self.response.PORT, + name=self.z_domain) + + self.response.probe_cert('foo.com', host='8.8.8.8') + mock_probe_sni.assert_called_with( + host='8.8.8.8', port=mock.ANY, name=mock.ANY) + + self.response.probe_cert('foo.com', port=1234) + mock_probe_sni.assert_called_with( + host=mock.ANY, port=1234, name=mock.ANY) + + self.response.probe_cert('foo.com', bar='baz') + mock_probe_sni.assert_called_with( + host=mock.ANY, port=mock.ANY, name=mock.ANY, bar='baz') + + self.response.probe_cert('foo.com', name=b'xxx') + mock_probe_sni.assert_called_with( + host=mock.ANY, port=mock.ANY, + name=self.z_domain) + + def test_gen_verify_cert(self): + key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem') + cert, key2 = self.response.gen_cert(key1) + self.assertEqual(key1, key2) + self.assertTrue(self.response.verify_cert(cert)) + + def test_gen_verify_cert_gen_key(self): + cert, key = self.response.gen_cert() + self.assertTrue(isinstance(key, OpenSSL.crypto.PKey)) + self.assertTrue(self.response.verify_cert(cert)) + + def test_verify_bad_cert(self): + self.assertFalse(self.response.verify_cert( + test_util.load_cert('cert.pem'))) + + def test_simple_verify_bad_key_authorization(self): + key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) + self.response.simple_verify(self.chall, "local", key2.public_key()) + + @mock.patch('acme.challenges.TLSSNI01Response.verify_cert', autospec=True) + def test_simple_verify(self, mock_verify_cert): + mock_verify_cert.return_value = mock.sentinel.verification + self.assertEqual(mock.sentinel.verification, self.response.simple_verify( + self.chall, self.domain, KEY.public_key(), + cert=mock.sentinel.cert)) + mock_verify_cert.assert_called_once_with(self.response, mock.sentinel.cert) + + @mock.patch('acme.challenges.TLSSNI01Response.probe_cert') + def test_simple_verify_false_on_probe_error(self, mock_probe_cert): + mock_probe_cert.side_effect = errors.Error + self.assertFalse(self.response.simple_verify( + self.chall, self.domain, KEY.public_key())) + + +class TLSSNI01Test(unittest.TestCase): + + def setUp(self): + from acme.challenges import TLSSNI01 + self.msg = TLSSNI01( + token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e')) + self.jmsg = { + 'type': 'tls-sni-01', + 'token': 'a82d5ff8ef740d12881f6d3c2277ab2e', + } + + def test_to_partial_json(self): + self.assertEqual(self.jmsg, self.msg.to_partial_json()) + + def test_from_json(self): + from acme.challenges import TLSSNI01 + self.assertEqual(self.msg, TLSSNI01.from_json(self.jmsg)) + + def test_from_json_hashable(self): + from acme.challenges import TLSSNI01 + hash(TLSSNI01.from_json(self.jmsg)) + + def test_from_json_invalid_token_length(self): + from acme.challenges import TLSSNI01 + self.jmsg['token'] = jose.encode_b64jose(b'abcd') + self.assertRaises( + jose.DeserializationError, TLSSNI01.from_json, self.jmsg) + + @mock.patch('acme.challenges.TLSSNI01Response.gen_cert') + def test_validation(self, mock_gen_cert): + mock_gen_cert.return_value = ('cert', 'key') + self.assertEqual(('cert', 'key'), self.msg.validation( + KEY, cert_key=mock.sentinel.cert_key)) + mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key) + + class DVSNITest(unittest.TestCase): def setUp(self): From 31706a5ef9439bc7950224818234a3efe75354d3 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Nov 2015 14:03:58 +0000 Subject: [PATCH 05/14] tls-sni-01: acme_util and auth_handler --- letsencrypt/tests/acme_util.py | 7 ++-- letsencrypt/tests/auth_handler_test.py | 46 +++++++++++++------------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/letsencrypt/tests/acme_util.py b/letsencrypt/tests/acme_util.py index 300eb453b..de75a2bc9 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 HTTP01 = challenges.HTTP01( token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") +TLSSNI01 = challenges.TLSSNI01( + token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA")) DVSNI = challenges.DVSNI( token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA")) DNS = challenges.DNS(token="17817c66b60ce2e4012dfad92657527a") @@ -41,7 +43,7 @@ POP = challenges.ProofOfPossession( ) ) -CHALLENGES = [HTTP01, DVSNI, DNS, RECOVERY_CONTACT, POP] +CHALLENGES = [HTTP01, TLSSNI01, DNS, RECOVERY_CONTACT, POP] DV_CHALLENGES = [chall for chall in CHALLENGES if isinstance(chall, challenges.DVChallenge)] CONT_CHALLENGES = [chall for chall in CHALLENGES @@ -80,12 +82,13 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name # Pending ChallengeBody objects DVSNI_P = chall_to_challb(DVSNI, messages.STATUS_PENDING) +TLSSNI01_P = chall_to_challb(TLSSNI01, messages.STATUS_PENDING) HTTP01_P = chall_to_challb(HTTP01, messages.STATUS_PENDING) DNS_P = chall_to_challb(DNS, messages.STATUS_PENDING) RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, messages.STATUS_PENDING) POP_P = chall_to_challb(POP, messages.STATUS_PENDING) -CHALLENGES_P = [HTTP01_P, DVSNI_P, DNS_P, RECOVERY_CONTACT_P, POP_P] +CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS_P, RECOVERY_CONTACT_P, POP_P] DV_CHALLENGES_P = [challb for challb in CHALLENGES_P if isinstance(challb.chall, challenges.DVChallenge)] CONT_CHALLENGES_P = [ diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 7be37c91e..be19ab036 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -45,7 +45,7 @@ class ChallengeFactoryTest(unittest.TestCase): self.assertEqual( [achall.chall for achall in cont_c], [acme_util.RECOVERY_CONTACT]) - self.assertEqual([achall.chall for achall in dv_c], [acme_util.DVSNI]) + self.assertEqual([achall.chall for achall in dv_c], [acme_util.TLSSNI01]) def test_unrecognized(self): self.handler.authzr["failure.com"] = acme_util.gen_authzr( @@ -70,7 +70,7 @@ class GetAuthorizationsTest(unittest.TestCase): self.mock_dv_auth = mock.MagicMock(name="ApacheConfigurator") self.mock_cont_auth = mock.MagicMock(name="ContinuityAuthenticator") - self.mock_dv_auth.get_chall_pref.return_value = [challenges.DVSNI] + self.mock_dv_auth.get_chall_pref.return_value = [challenges.TLSSNI01] self.mock_cont_auth.get_chall_pref.return_value = [ challenges.RecoveryContact] @@ -90,7 +90,7 @@ class GetAuthorizationsTest(unittest.TestCase): logging.disable(logging.NOTSET) @mock.patch("letsencrypt.auth_handler.AuthHandler._poll_challenges") - def test_name1_dvsni1(self, mock_poll): + def test_name1_tls_sni_01_1(self, mock_poll): self.mock_net.request_domain_challenges.side_effect = functools.partial( gen_dom_authzr, challs=acme_util.DV_CHALLENGES) @@ -107,14 +107,14 @@ class GetAuthorizationsTest(unittest.TestCase): self.assertEqual(self.mock_dv_auth.cleanup.call_count, 1) self.assertEqual(self.mock_cont_auth.cleanup.call_count, 0) - # Test if list first element is DVSNI, use typ because it is an achall + # Test if list first element is TLSSNI01, use typ because it is an achall self.assertEqual( - self.mock_dv_auth.cleanup.call_args[0][0][0].typ, "dvsni") + self.mock_dv_auth.cleanup.call_args[0][0][0].typ, "tls-sni-01") self.assertEqual(len(authzr), 1) @mock.patch("letsencrypt.auth_handler.AuthHandler._poll_challenges") - def test_name3_dvsni3_rectok_3(self, mock_poll): + def test_name3_tls_sni_01_3_rectok_3(self, mock_poll): self.mock_net.request_domain_challenges.side_effect = functools.partial( gen_dom_authzr, challs=acme_util.CHALLENGES) @@ -309,9 +309,9 @@ class GenChallengePathTest(unittest.TestCase): return gen_challenge_path(challbs, preferences, combinations) def test_common_case(self): - """Given DVSNI and HTTP01 with appropriate combos.""" - challbs = (acme_util.DVSNI_P, acme_util.HTTP01_P) - prefs = [challenges.DVSNI] + """Given TLSSNI01 and HTTP01 with appropriate combos.""" + challbs = (acme_util.TLSSNI01_P, acme_util.HTTP01_P) + prefs = [challenges.TLSSNI01] combos = ((0,), (1,)) # Smart then trivial dumb path test @@ -324,9 +324,9 @@ class GenChallengePathTest(unittest.TestCase): def test_common_case_with_continuity(self): challbs = (acme_util.POP_P, acme_util.RECOVERY_CONTACT_P, - acme_util.DVSNI_P, + acme_util.TLSSNI01_P, acme_util.HTTP01_P) - prefs = [challenges.ProofOfPossession, challenges.DVSNI] + prefs = [challenges.ProofOfPossession, challenges.TLSSNI01] combos = acme_util.gen_combos(challbs) self.assertEqual(self._call(challbs, prefs, combos), (0, 2)) @@ -336,14 +336,14 @@ class GenChallengePathTest(unittest.TestCase): def test_full_cont_server(self): challbs = (acme_util.RECOVERY_CONTACT_P, acme_util.POP_P, - acme_util.DVSNI_P, + acme_util.TLSSNI01_P, acme_util.HTTP01_P, acme_util.DNS_P) # Typical webserver client that can do everything except DNS # Attempted to make the order realistic prefs = [challenges.ProofOfPossession, challenges.HTTP01, - challenges.DVSNI, + challenges.TLSSNI01, challenges.RecoveryContact] combos = acme_util.gen_combos(challbs) self.assertEqual(self._call(challbs, prefs, combos), (1, 3)) @@ -352,8 +352,8 @@ class GenChallengePathTest(unittest.TestCase): self.assertTrue(self._call(challbs, prefs, None)) def test_not_supported(self): - challbs = (acme_util.POP_P, acme_util.DVSNI_P) - prefs = [challenges.DVSNI] + challbs = (acme_util.POP_P, acme_util.TLSSNI01_P) + prefs = [challenges.TLSSNI01] combos = ((0, 1),) self.assertRaises( @@ -411,7 +411,7 @@ class IsPreferredTest(unittest.TestCase): def _call(cls, chall, satisfied): from letsencrypt.auth_handler import is_preferred return is_preferred(chall, satisfied, exclusive_groups=frozenset([ - frozenset([challenges.DVSNI, challenges.HTTP01]), + frozenset([challenges.TLSSNI01, challenges.HTTP01]), frozenset([challenges.DNS, challenges.HTTP01]), ])) @@ -421,11 +421,11 @@ class IsPreferredTest(unittest.TestCase): def test_mutually_exclusvie(self): self.assertFalse( self._call( - acme_util.DVSNI_P, frozenset([acme_util.HTTP01_P]))) + acme_util.TLSSNI01_P, frozenset([acme_util.HTTP01_P]))) def test_mutually_exclusive_same_type(self): self.assertTrue( - self._call(acme_util.DVSNI_P, frozenset([acme_util.DVSNI_P]))) + self._call(acme_util.TLSSNI01_P, frozenset([acme_util.TLSSNI01_P]))) class ReportFailedChallsTest(unittest.TestCase): @@ -446,15 +446,15 @@ class ReportFailedChallsTest(unittest.TestCase): domain="example.com", account_key="key") - kwargs["chall"] = acme_util.DVSNI - self.dvsni_same = achallenges.DVSNI( + kwargs["chall"] = acme_util.TLSSNI01 + self.tls_sni_same = achallenges.KeyAuthorizationAnnotatedChallenge( # pylint: disable=star-args challb=messages.ChallengeBody(**kwargs), domain="example.com", account_key="key") kwargs["error"] = messages.Error(typ="dnssec", detail="detail") - self.dvsni_diff = achallenges.DVSNI( + self.tls_sni_diff = achallenges.KeyAuthorizationAnnotatedChallenge( # pylint: disable=star-args challb=messages.ChallengeBody(**kwargs), domain="foo.bar", @@ -464,7 +464,7 @@ class ReportFailedChallsTest(unittest.TestCase): def test_same_error_and_domain(self, mock_zope): from letsencrypt import auth_handler - auth_handler._report_failed_challs([self.http01, self.dvsni_same]) + auth_handler._report_failed_challs([self.http01, self.tls_sni_same]) call_list = mock_zope().add_message.call_args_list self.assertTrue(len(call_list) == 1) self.assertTrue("Domains: example.com\n" in call_list[0][0][0]) @@ -473,7 +473,7 @@ class ReportFailedChallsTest(unittest.TestCase): def test_different_errors_and_domains(self, mock_zope): from letsencrypt import auth_handler - auth_handler._report_failed_challs([self.http01, self.dvsni_diff]) + auth_handler._report_failed_challs([self.http01, self.tls_sni_diff]) self.assertTrue(mock_zope().add_message.call_count == 2) From 93e69ef7dee7d7315ec44e5567fba1f3bfa145a6 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 1 Nov 2015 12:22:19 +0000 Subject: [PATCH 06/14] tls-sni-01 for standalone --- docs/using.rst | 2 +- examples/cli.ini | 2 +- letsencrypt/achallenges.py | 5 +++-- letsencrypt/plugins/standalone.py | 30 ++++++++++++++------------ letsencrypt/plugins/standalone_test.py | 30 +++++++++++++------------- tests/boulder-integration.sh | 2 +- 6 files changed, 37 insertions(+), 34 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 1bc7fab99..8e8fd132e 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -135,7 +135,7 @@ Plugin A I Notes and status ========== = = ================================================================ standalone Y N Very stable. Uses port 80 (force by ``--standalone-supported-challenges http-01``) or 443 - (force by ``--standalone-supported-challenges dvsni``). + (force by ``--standalone-supported-challenges tls-sni-01``). apache Y Y Alpha. Automates Apache installation, works fairly well but on Debian-based distributions only for now. webroot Y N Works with already running webserver, by writing necessary files diff --git a/examples/cli.ini b/examples/cli.ini index 34fb8ab02..a20764ed8 100644 --- a/examples/cli.ini +++ b/examples/cli.ini @@ -16,7 +16,7 @@ server = https://acme-staging.api.letsencrypt.org/directory # Uncomment to use the standalone authenticator on port 443 # authenticator = standalone -# standalone-supported-challenges = dvsni +# standalone-supported-challenges = tls-sni-01 # Uncomment to use the webroot authenticator. Replace webroot-path with the # path to the public_html / webroot folder being served by your web server. diff --git a/letsencrypt/achallenges.py b/letsencrypt/achallenges.py index f08c6a396..4e46d6af9 100644 --- a/letsencrypt/achallenges.py +++ b/letsencrypt/achallenges.py @@ -49,9 +49,10 @@ class KeyAuthorizationAnnotatedChallenge(AnnotatedChallenge): """Client annotated `KeyAuthorizationChallenge` challenge.""" __slots__ = ('challb', 'domain', 'account_key') - def response_and_validation(self): + def response_and_validation(self, *args, **kwargs): """Generate response and validation.""" - return self.challb.chall.response_and_validation(self.account_key) + return self.challb.chall.response_and_validation( + self.account_key, *args, **kwargs) class DVSNI(AnnotatedChallenge): diff --git a/letsencrypt/plugins/standalone.py b/letsencrypt/plugins/standalone.py index 3975e9292..717434052 100644 --- a/letsencrypt/plugins/standalone.py +++ b/letsencrypt/plugins/standalone.py @@ -11,7 +11,6 @@ import six import zope.interface from acme import challenges -from acme import crypto_util as acme_crypto_util from acme import standalone as acme_standalone from letsencrypt import errors @@ -51,19 +50,19 @@ class ServerManager(object): :param int port: Port to run the server on. :param challenge_type: Subclass of `acme.challenges.Challenge`, - either `acme.challenge.HTTP01` or `acme.challenges.DVSNI`. + either `acme.challenge.HTTP01` or `acme.challenges.TLSSNI01`. :returns: Server instance. :rtype: ACMEServerMixin """ - assert challenge_type in (challenges.DVSNI, challenges.HTTP01) + assert challenge_type in (challenges.TLSSNI01, challenges.HTTP01) if port in self._instances: return self._instances[port].server address = ("", port) try: - if challenge_type is challenges.DVSNI: + if challenge_type is challenges.TLSSNI01: server = acme_standalone.DVSNIServer(address, self.certs) else: # challenges.HTTP01 server = acme_standalone.HTTP01Server( @@ -109,7 +108,7 @@ class ServerManager(object): in six.iteritems(self._instances)) -SUPPORTED_CHALLENGES = set([challenges.DVSNI, challenges.HTTP01]) +SUPPORTED_CHALLENGES = set([challenges.TLSSNI01, challenges.HTTP01]) def supported_challenges_validator(data): @@ -138,7 +137,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 HTTP01 + necessary port in order to respond to incoming tls-sni-01 and http-01 challenges from the certificate authority. Therefore, it does not rely on any existing server program. """ @@ -150,7 +149,7 @@ class Authenticator(common.Plugin): def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) - # one self-signed key for all DVSNI certificates + # one self-signed key for all tls-sni-01 certificates self.key = OpenSSL.crypto.PKey() self.key.generate_key(OpenSSL.crypto.TYPE_RSA, bits=2048) @@ -183,15 +182,16 @@ class Authenticator(common.Plugin): necessary_ports = set() if challenges.HTTP01 in self.supported_challenges: necessary_ports.add(self.config.http01_port) - if challenges.DVSNI in self.supported_challenges: + if challenges.TLSSNI01 in self.supported_challenges: necessary_ports.add(self.config.dvsni_port) return necessary_ports 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 HTTP01 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 " + "tls-sni-01 and http-01 challenges from the certificate " + "authority. Therefore, it does not rely on any existing " + "server program.") def prepare(self): # pylint: disable=missing-docstring pass @@ -241,9 +241,11 @@ class Authenticator(common.Plugin): acme_standalone.HTTP01RequestHandler.HTTP01Resource( chall=achall.chall, response=response, validation=validation)) - else: # DVSNI - server = self.servers.run(self.config.dvsni_port, challenges.DVSNI) - response, cert, _ = achall.gen_cert_and_response(self.key) + else: # tls-sni-01 + server = self.servers.run( + self.config.dvsni_port, challenges.TLSSNI01) + response, (cert, _) = achall.response_and_validation( + cert_key=self.key) self.certs[response.z_domain] = (self.key, cert) self.served[server].add(achall) responses.append(response) diff --git a/letsencrypt/plugins/standalone_test.py b/letsencrypt/plugins/standalone_test.py index c1de52ac8..79718e244 100644 --- a/letsencrypt/plugins/standalone_test.py +++ b/letsencrypt/plugins/standalone_test.py @@ -39,8 +39,8 @@ class ServerManagerTest(unittest.TestCase): self.mgr.stop(port=port) self.assertEqual(self.mgr.running(), {}) - def test_run_stop_dvsni(self): - self._test_run_stop(challenges.DVSNI) + def test_run_stop_tls_sni_01(self): + self._test_run_stop(challenges.TLSSNI01) def test_run_stop_http_01(self): self._test_run_stop(challenges.HTTP01) @@ -73,10 +73,10 @@ class SupportedChallengesValidatorTest(unittest.TestCase): return supported_challenges_validator(data) def test_correct(self): - self.assertEqual("dvsni", self._call("dvsni")) + self.assertEqual("tls-sni-01", self._call("tls-sni-01")) 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")) + self.assertEqual("tls-sni-01,http-01", self._call("tls-sni-01,http-01")) + self.assertEqual("http-01,tls-sni-01", self._call("http-01,tls-sni-01")) def test_unrecognized(self): assert "foo" not in challenges.Challenge.TYPES @@ -93,23 +93,23 @@ class AuthenticatorTest(unittest.TestCase): from letsencrypt.plugins.standalone import Authenticator self.config = mock.MagicMock( dvsni_port=1234, http01_port=4321, - standalone_supported_challenges="dvsni,http-01") + standalone_supported_challenges="tls-sni-01,http-01") self.auth = Authenticator(self.config, name="standalone") def test_supported_challenges(self): self.assertEqual(self.auth.supported_challenges, - set([challenges.DVSNI, challenges.HTTP01])) + set([challenges.TLSSNI01, 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.HTTP01])) + set([challenges.TLSSNI01, challenges.HTTP01])) @mock.patch("letsencrypt.plugins.standalone.util") def test_perform_alredy_listening(self, mock_util): - for chall, port in ((challenges.DVSNI.typ, 1234), + for chall, port in ((challenges.TLSSNI01.typ, 1234), (challenges.HTTP01.typ, 4321)): mock_util.already_listening.return_value = True self.config.standalone_supported_challenges = chall @@ -155,8 +155,8 @@ class AuthenticatorTest(unittest.TestCase): key = jose.JWK.load(test_util.load_vector('rsa512_key.pem')) http_01 = 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) + tls_sni_01 = achallenges.KeyAuthorizationAnnotatedChallenge( + challb=acme_util.TLSSNI01_P, domain=domain, account_key=key) self.auth.servers = mock.MagicMock() @@ -164,19 +164,19 @@ class AuthenticatorTest(unittest.TestCase): return "server{0}".format(port) self.auth.servers.run.side_effect = _run - responses = self.auth.perform2([http_01, dvsni]) + responses = self.auth.perform2([http_01, tls_sni_01]) self.assertTrue(isinstance(responses, list)) self.assertEqual(2, len(responses)) self.assertTrue(isinstance(responses[0], challenges.HTTP01Response)) - self.assertTrue(isinstance(responses[1], challenges.DVSNIResponse)) + self.assertTrue(isinstance(responses[1], challenges.TLSSNI01Response)) self.assertEqual(self.auth.servers.run.mock_calls, [ mock.call(4321, challenges.HTTP01), - mock.call(1234, challenges.DVSNI), + mock.call(1234, challenges.TLSSNI01), ]) self.assertEqual(self.auth.served, { - "server1234": set([dvsni]), + "server1234": set([tls_sni_01]), "server4321": set([http_01]), }) self.assertEqual(1, len(self.auth.http_01_resources)) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 18a996926..d35ecbcff 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -27,7 +27,7 @@ common() { "$@" } -common --domains le1.wtf --standalone-supported-challenges dvsni auth +common --domains le1.wtf --standalone-supported-challenges tls-sni-01 auth common --domains le2.wtf --standalone-supported-challenges http-01 run common -a manual -d le.wtf auth From 2266baf775b3861efa7d1374d4dfbdf5f2f690d1 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Nov 2015 14:15:04 +0000 Subject: [PATCH 07/14] Renames around DVSNIServer --- acme/acme/crypto_util.py | 8 ++++---- acme/acme/standalone.py | 14 +++++++------- acme/acme/standalone_test.py | 24 +++++++++++++----------- letsencrypt/plugins/standalone.py | 2 +- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/acme/acme/crypto_util.py b/acme/acme/crypto_util.py index 5f24e9d9e..72a93141a 100644 --- a/acme/acme/crypto_util.py +++ b/acme/acme/crypto_util.py @@ -13,7 +13,7 @@ from acme import errors logger = logging.getLogger(__name__) -# DVSNI certificate serving and probing is not affected by SSL +# TLSSNI01 certificate serving and probing is not affected by SSL # vulnerabilities: prober needs to check certificate for expected # contents anyway. Working SNI is the only thing that's necessary for # the challenge and thus scoping down SSL/TLS method (version) would @@ -23,7 +23,7 @@ logger = logging.getLogger(__name__) # https://www.openssl.org/docs/ssl/SSLv23_method.html). _serve_sni # should be changed to use "set_options" to disable SSLv2 and SSLv3, # in case it's used for things other than probing/serving! -_DEFAULT_DVSNI_SSL_METHOD = OpenSSL.SSL.SSLv23_METHOD +_DEFAULT_TLSSNI01_SSL_METHOD = OpenSSL.SSL.SSLv23_METHOD class SSLSocket(object): # pylint: disable=too-few-public-methods @@ -35,7 +35,7 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods :ivar method: See `OpenSSL.SSL.Context` for allowed values. """ - def __init__(self, sock, certs, method=_DEFAULT_DVSNI_SSL_METHOD): + def __init__(self, sock, certs, method=_DEFAULT_TLSSNI01_SSL_METHOD): self.sock = sock self.certs = certs self.method = method @@ -103,7 +103,7 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods def probe_sni(name, host, port=443, timeout=300, - method=_DEFAULT_DVSNI_SSL_METHOD, source_address=('0', 0)): + method=_DEFAULT_TLSSNI01_SSL_METHOD, source_address=('0', 0)): """Probe SNI server for SSL certificate. :param bytes name: Byte string to send as the server name in the diff --git a/acme/acme/standalone.py b/acme/acme/standalone.py index 1466671e3..32dd5ae41 100644 --- a/acme/acme/standalone.py +++ b/acme/acme/standalone.py @@ -30,7 +30,7 @@ class TLSServer(socketserver.TCPServer): self.certs = kwargs.pop("certs", {}) self.method = kwargs.pop( # pylint: disable=protected-access - "method", crypto_util._DEFAULT_DVSNI_SSL_METHOD) + "method", crypto_util._DEFAULT_TLSSNI01_SSL_METHOD) self.allow_reuse_address = kwargs.pop("allow_reuse_address", True) socketserver.TCPServer.__init__(self, *args, **kwargs) @@ -50,8 +50,8 @@ class ACMEServerMixin: # pylint: disable=old-style-class allow_reuse_address = True -class DVSNIServer(TLSServer, ACMEServerMixin): - """DVSNI Server.""" +class TLSSNI01Server(TLSServer, ACMEServerMixin): + """TLSSNI01 Server.""" def __init__(self, server_address, certs): TLSServer.__init__( @@ -134,8 +134,8 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): cls, simple_http_resources=simple_http_resources) -def simple_dvsni_server(cli_args, forever=True): - """Run simple standalone DVSNI server.""" +def simple_tls_sni_01_server(cli_args, forever=True): + """Run simple standalone TLSSNI01 server.""" logging.basicConfig(level=logging.DEBUG) parser = argparse.ArgumentParser() @@ -158,7 +158,7 @@ def simple_dvsni_server(cli_args, forever=True): OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, cert_contents)) - server = DVSNIServer(('', int(args.port)), certs=certs) + server = TLSSNI01Server(('', int(args.port)), certs=certs) six.print_("Serving at https://localhost:{0}...".format( server.socket.getsockname()[1])) if forever: # pragma: no cover @@ -168,4 +168,4 @@ def simple_dvsni_server(cli_args, forever=True): if __name__ == "__main__": - sys.exit(simple_dvsni_server(sys.argv)) # pragma: no cover + sys.exit(simple_tls_sni_01_server(sys.argv)) # pragma: no cover diff --git a/acme/acme/standalone_test.py b/acme/acme/standalone_test.py index 85ef6ab14..02b1f69d3 100644 --- a/acme/acme/standalone_test.py +++ b/acme/acme/standalone_test.py @@ -28,8 +28,8 @@ class TLSServerTest(unittest.TestCase): server.server_close() # pylint: disable=no-member -class DVSNIServerTest(unittest.TestCase): - """Test for acme.standalone.DVSNIServer.""" +class TLSSNI01ServerTest(unittest.TestCase): + """Test for acme.standalone.TLSSNI01Server.""" def setUp(self): self.certs = { @@ -37,8 +37,8 @@ class DVSNIServerTest(unittest.TestCase): # pylint: disable=protected-access test_util.load_cert('cert.pem')._wrapped), } - from acme.standalone import DVSNIServer - self.server = DVSNIServer(("", 0), certs=self.certs) + from acme.standalone import TLSSNI01Server + self.server = TLSSNI01Server(("", 0), certs=self.certs) # pylint: disable=no-member self.thread = threading.Thread(target=self.server.serve_forever) self.thread.start() @@ -106,8 +106,8 @@ class HTTP01ServerTest(unittest.TestCase): self.assertFalse(self._test_http01(add=False)) -class TestSimpleDVSNIServer(unittest.TestCase): - """Tests for acme.standalone.simple_dvsni_server.""" +class TestSimpleTLSSNI01Server(unittest.TestCase): + """Tests for acme.standalone.simple_tls_sni_01_server.""" def setUp(self): # mirror ../examples/standalone @@ -118,12 +118,14 @@ class TestSimpleDVSNIServer(unittest.TestCase): shutil.copy(test_util.vector_path('rsa512_key.pem'), os.path.join(localhost_dir, 'key.pem')) - from acme.standalone import simple_dvsni_server + from acme.standalone import simple_tls_sni_01_server self.port = 1234 - self.thread = threading.Thread(target=simple_dvsni_server, kwargs={ - 'cli_args': ('xxx', '--port', str(self.port)), - 'forever': False, - }) + self.thread = threading.Thread( + target=simple_tls_sni_01_server, kwargs={ + 'cli_args': ('xxx', '--port', str(self.port)), + 'forever': False, + }, + ) self.old_cwd = os.getcwd() os.chdir(self.test_cwd) self.thread.start() diff --git a/letsencrypt/plugins/standalone.py b/letsencrypt/plugins/standalone.py index 717434052..afe0b6b39 100644 --- a/letsencrypt/plugins/standalone.py +++ b/letsencrypt/plugins/standalone.py @@ -63,7 +63,7 @@ class ServerManager(object): address = ("", port) try: if challenge_type is challenges.TLSSNI01: - server = acme_standalone.DVSNIServer(address, self.certs) + server = acme_standalone.TLSSNI01Server(address, self.certs) else: # challenges.HTTP01 server = acme_standalone.HTTP01Server( address, self.http_01_resources) From 5e8ed2bbd2707684fd76bc7662e1c835992f2eb8 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Nov 2015 14:21:58 +0000 Subject: [PATCH 08/14] --dvsni-port -> --tls-sni-01-port --- letsencrypt-apache/letsencrypt_apache/dvsni.py | 7 ++++--- letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py | 2 +- letsencrypt-nginx/letsencrypt_nginx/configurator.py | 7 ++++--- letsencrypt-nginx/letsencrypt_nginx/dvsni.py | 2 +- letsencrypt-nginx/letsencrypt_nginx/tests/util.py | 2 +- letsencrypt/cli.py | 5 +++-- letsencrypt/configuration.py | 6 +++--- letsencrypt/constants.py | 4 ++-- letsencrypt/interfaces.py | 4 ++-- letsencrypt/plugins/standalone.py | 4 ++-- letsencrypt/plugins/standalone_test.py | 2 +- letsencrypt/renewer.py | 2 +- letsencrypt/tests/configuration_test.py | 4 ++-- letsencrypt/tests/renewer_test.py | 2 +- tests/integration/_common.sh | 2 +- 15 files changed, 29 insertions(+), 26 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/dvsni.py b/letsencrypt-apache/letsencrypt_apache/dvsni.py index ed88bf8a7..02ae34150 100644 --- a/letsencrypt-apache/letsencrypt_apache/dvsni.py +++ b/letsencrypt-apache/letsencrypt_apache/dvsni.py @@ -62,7 +62,7 @@ class ApacheDvsni(common.Dvsni): # Prepare the server for HTTPS self.configurator.prepare_server_https( - str(self.configurator.config.dvsni_port), True) + str(self.configurator.config.tls_sni_01_port), True) responses = [] @@ -114,14 +114,15 @@ class ApacheDvsni(common.Dvsni): # TODO: Checkout _default_ rules. dvsni_addrs = set() - default_addr = obj.Addr(("*", str(self.configurator.config.dvsni_port))) + default_addr = obj.Addr(("*", str( + self.configurator.config.tls_sni_01_port))) for addr in vhost.addrs: if "_default_" == addr.get_addr(): dvsni_addrs.add(default_addr) else: dvsni_addrs.add( - addr.get_sni_addr(self.configurator.config.dvsni_port)) + addr.get_sni_addr(self.configurator.config.tls_sni_01_port)) return dvsni_addrs diff --git a/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py b/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py index c362d4115..6cb93e0ca 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py @@ -21,7 +21,7 @@ class DvsniPerformTest(util.ApacheTest): config = util.get_apache_configurator( self.config_path, self.config_dir, self.work_dir) - config.config.dvsni_port = 443 + config.config.tls_sni_01_port = 443 from letsencrypt_apache import dvsni self.sni = dvsni.ApacheDvsni(config) diff --git a/letsencrypt-nginx/letsencrypt_nginx/configurator.py b/letsencrypt-nginx/letsencrypt_nginx/configurator.py index 29e69e498..80b4ebc5d 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/configurator.py +++ b/letsencrypt-nginx/letsencrypt_nginx/configurator.py @@ -297,7 +297,7 @@ class NginxConfigurator(common.Plugin): """Make a server SSL. Make a server SSL based on server_name and filename by adding a - ``listen IConfig.dvsni_port ssl`` directive to the server block. + ``listen IConfig.tls_sni_01_port ssl`` directive to the server block. .. todo:: Maybe this should create a new block instead of modifying the existing one? @@ -307,7 +307,7 @@ class NginxConfigurator(common.Plugin): """ snakeoil_cert, snakeoil_key = self._get_snakeoil_paths() - ssl_block = [['listen', '{0} ssl'.format(self.config.dvsni_port)], + ssl_block = [['listen', '{0} ssl'.format(self.config.tls_sni_01_port)], # access and error logs necessary for integration # testing (non-root) ['access_log', os.path.join( @@ -321,7 +321,8 @@ class NginxConfigurator(common.Plugin): vhost.filep, vhost.names, ssl_block) vhost.ssl = True vhost.raw.extend(ssl_block) - vhost.addrs.add(obj.Addr('', str(self.config.dvsni_port), True, False)) + vhost.addrs.add(obj.Addr( + '', str(self.config.tls_sni_01_port), True, False)) def get_all_certs_keys(self): """Find all existing keys, certs from configuration. diff --git a/letsencrypt-nginx/letsencrypt_nginx/dvsni.py b/letsencrypt-nginx/letsencrypt_nginx/dvsni.py index 662f10889..828dac650 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/dvsni.py +++ b/letsencrypt-nginx/letsencrypt_nginx/dvsni.py @@ -48,7 +48,7 @@ class NginxDvsni(common.Dvsni): addresses = [] default_addr = "{0} default_server ssl".format( - self.configurator.config.dvsni_port) + self.configurator.config.tls_sni_01_port) for achall in self.achalls: vhost = self.configurator.choose_vhost(achall.domain) diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/util.py b/letsencrypt-nginx/letsencrypt_nginx/tests/util.py index 953c5d367..cb4e08ddf 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/util.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/util.py @@ -59,7 +59,7 @@ def get_nginx_configurator( temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"), in_progress_dir=os.path.join(backups, "IN_PROGRESS"), server="https://acme-server.org:443/new", - dvsni_port=5001, + tls_sni_01_port=5001, ), name="nginx", version=version) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 5757783cd..c1f3edb70 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -850,8 +850,9 @@ def prepare_and_parse_args(plugins, args): help=config_help("no_verify_ssl"), default=flag_default("no_verify_ssl")) helpful.add( - "testing", "--dvsni-port", type=int, default=flag_default("dvsni_port"), - help=config_help("dvsni_port")) + "testing", "--tls-sni-01-port", type=int, + default=flag_default("tls_sni_01_port"), + help=config_help("tls_sni_01_port")) helpful.add("testing", "--http-01-port", dest="http01_port", type=int, help=config_help("http01_port")) diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index b604651e9..c5a02dfef 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -37,10 +37,10 @@ class NamespaceConfig(object): def __init__(self, namespace): self.namespace = namespace - if self.http01_port == self.dvsni_port: + if self.http01_port == self.tls_sni_01_port: raise errors.Error( - "Trying to run http-01 and DVSNI " - "on the same port ({0})".format(self.dvsni_port)) + "Trying to run http-01 and tls-sni-01 " + "on the same port ({0})".format(self.tls_sni_01_port)) def __getattr__(self, name): return getattr(self.namespace, name) diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index 15f8c53f0..f71bf0329 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -23,7 +23,7 @@ CLI_DEFAULTS = dict( work_dir="/var/lib/letsencrypt", logs_dir="/var/log/letsencrypt", no_verify_ssl=False, - dvsni_port=challenges.DVSNI.PORT, + tls_sni_01_port=challenges.TLSSNI01Response.PORT, auth_cert_path="./cert.pem", auth_chain_path="./chain.pem", @@ -41,7 +41,7 @@ RENEWER_DEFAULTS = dict( EXCLUSIVE_CHALLENGES = frozenset([frozenset([ - challenges.DVSNI, challenges.HTTP01])]) + challenges.TLSSNI01, challenges.HTTP01])]) """Mutually exclusive challenges.""" diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 498b01683..987fdc25e 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -219,8 +219,8 @@ class IConfig(zope.interface.Interface): no_verify_ssl = zope.interface.Attribute( "Disable SSL certificate verification.") - dvsni_port = zope.interface.Attribute( - "Port number to perform DVSNI challenge. " + tls_sni_01_port = zope.interface.Attribute( + "Port number to perform tls-sni-01 challenge. " "Boulder in testing mode defaults to 5001.") http01_port = zope.interface.Attribute( diff --git a/letsencrypt/plugins/standalone.py b/letsencrypt/plugins/standalone.py index afe0b6b39..8b8612fd1 100644 --- a/letsencrypt/plugins/standalone.py +++ b/letsencrypt/plugins/standalone.py @@ -183,7 +183,7 @@ class Authenticator(common.Plugin): if challenges.HTTP01 in self.supported_challenges: necessary_ports.add(self.config.http01_port) if challenges.TLSSNI01 in self.supported_challenges: - necessary_ports.add(self.config.dvsni_port) + necessary_ports.add(self.config.tls_sni_01_port) return necessary_ports def more_info(self): # pylint: disable=missing-docstring @@ -243,7 +243,7 @@ class Authenticator(common.Plugin): validation=validation)) else: # tls-sni-01 server = self.servers.run( - self.config.dvsni_port, challenges.TLSSNI01) + self.config.tls_sni_01_port, challenges.TLSSNI01) response, (cert, _) = achall.response_and_validation( cert_key=self.key) self.certs[response.z_domain] = (self.key, cert) diff --git a/letsencrypt/plugins/standalone_test.py b/letsencrypt/plugins/standalone_test.py index 79718e244..26a040c2e 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, http01_port=4321, + tls_sni_01_port=1234, http01_port=4321, standalone_supported_challenges="tls-sni-01,http-01") self.auth = Authenticator(self.config, name="standalone") diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py index 40e49702a..0a490d447 100644 --- a/letsencrypt/renewer.py +++ b/letsencrypt/renewer.py @@ -75,7 +75,7 @@ def renew(cert, old_version): # XXX: this loses type data (for example, the fact that key_size # was an int, not a str) config.rsa_key_size = int(config.rsa_key_size) - config.dvsni_port = int(config.dvsni_port) + config.tls_sni_01_port = int(config.tls_sni_01_port) config.namespace.http01_port = int(config.namespace.http01_port) zope.component.provideUtility(config) try: diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index c7e227ee5..3a8bf40cf 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -14,12 +14,12 @@ class NamespaceConfigTest(unittest.TestCase): self.namespace = mock.MagicMock( config_dir='/tmp/config', work_dir='/tmp/foo', foo='bar', server='https://acme-server.org:443/new', - dvsni_port=1234, http01_port=4321) + tls_sni_01_port=1234, http01_port=4321) from letsencrypt.configuration import NamespaceConfig self.config = NamespaceConfig(self.namespace) def test_init_same_ports(self): - self.namespace.dvsni_port = 4321 + self.namespace.tls_sni_01_port = 4321 from letsencrypt.configuration import NamespaceConfig self.assertRaises(errors.Error, NamespaceConfig, self.namespace) diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index 05d7e123d..0a39b7987 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -688,7 +688,7 @@ class RenewableCertTests(BaseRenewableCertTest): self.test_rc.configfile["renewalparams"]["rsa_key_size"] = "2048" self.test_rc.configfile["renewalparams"]["server"] = "acme.example.com" self.test_rc.configfile["renewalparams"]["authenticator"] = "fake" - self.test_rc.configfile["renewalparams"]["dvsni_port"] = "4430" + self.test_rc.configfile["renewalparams"]["tls_sni_01_port"] = "4430" self.test_rc.configfile["renewalparams"]["http01_port"] = "1234" self.test_rc.configfile["renewalparams"]["account"] = "abcde" mock_auth = mock.MagicMock() diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index cd894fd10..dbc473728 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -15,7 +15,7 @@ letsencrypt_test () { letsencrypt \ --server "${SERVER:-http://localhost:4000/directory}" \ --no-verify-ssl \ - --dvsni-port 5001 \ + --tls-sni-01-port 5001 \ --http-01-port 5002 \ --manual-test-mode \ $store_flags \ From 937e3edfc19ce014b640ace32887a1df76a7b375 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Nov 2015 18:10:56 +0000 Subject: [PATCH 09/14] tls-sni-01 in apache and nginx plugins --- .../letsencrypt_apache/configurator.py | 12 +++++----- .../letsencrypt_apache/dvsni.py | 12 +++++----- .../tests/configurator_test.py | 12 +++++----- .../letsencrypt_apache/tests/dvsni_test.py | 10 ++++----- .../test_driver.py | 14 ++++++------ .../letsencrypt_nginx/configurator.py | 12 +++++----- letsencrypt-nginx/letsencrypt_nginx/dvsni.py | 4 ++-- .../tests/configurator_test.py | 14 ++++++------ .../letsencrypt_nginx/tests/dvsni_test.py | 22 +++++++++---------- letsencrypt/plugins/common.py | 21 +++++++++--------- letsencrypt/plugins/common_test.py | 22 +++++++++---------- 11 files changed, 76 insertions(+), 79 deletions(-) diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py index d376fe4b6..b8ca05550 100644 --- a/letsencrypt-apache/letsencrypt_apache/configurator.py +++ b/letsencrypt-apache/letsencrypt_apache/configurator.py @@ -13,7 +13,6 @@ import zope.interface from acme import challenges -from letsencrypt import achallenges from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util @@ -1117,7 +1116,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): ########################################################################### def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use """Return list of challenge preferences.""" - return [challenges.DVSNI] + return [challenges.TLSSNI01] def perform(self, achalls): """Perform the configuration related challenge. @@ -1132,11 +1131,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): apache_dvsni = dvsni.ApacheDvsni(self) for i, achall in enumerate(achalls): - if isinstance(achall, achallenges.DVSNI): - # Currently also have dvsni hold associated index - # of the challenge. This helps to put all of the responses back - # together when they are all complete. - apache_dvsni.add_chall(achall, i) + # Currently also have dvsni hold associated index + # of the challenge. This helps to put all of the responses back + # together when they are all complete. + apache_dvsni.add_chall(achall, i) sni_response = apache_dvsni.perform() if sni_response: diff --git a/letsencrypt-apache/letsencrypt_apache/dvsni.py b/letsencrypt-apache/letsencrypt_apache/dvsni.py index 02ae34150..2f9e9ed18 100644 --- a/letsencrypt-apache/letsencrypt_apache/dvsni.py +++ b/letsencrypt-apache/letsencrypt_apache/dvsni.py @@ -7,14 +7,14 @@ from letsencrypt_apache import obj from letsencrypt_apache import parser -class ApacheDvsni(common.Dvsni): +class ApacheDvsni(common.TLSSNI01): """Class performs DVSNI challenges within the Apache configurator. :ivar configurator: ApacheConfigurator object :type configurator: :class:`~apache.configurator.ApacheConfigurator` - :ivar list achalls: Annotated :class:`~letsencrypt.achallenges.DVSNI` - challenges. + :ivar list achalls: Annotated tls-sni-01 + (`.KeyAuthorizationAnnotatedChallenge`) challenges. :param list indices: Meant to hold indices of challenges in a larger array. ApacheDvsni is capable of solving many challenges @@ -145,8 +145,8 @@ class ApacheDvsni(common.Dvsni): def _get_config_text(self, achall, ip_addrs): """Chocolate virtual server configuration text - :param achall: Annotated DVSNI challenge. - :type achall: :class:`letsencrypt.achallenges.DVSNI` + :param .KeyAuthorizationAnnotatedChallenge achall: Annotated + DVSNI challenge. :param list ip_addrs: addresses of challenged domain :class:`list` of type `~.obj.Addr` @@ -165,7 +165,7 @@ class ApacheDvsni(common.Dvsni): # https://docs.python.org/2.7/reference/lexical_analysis.html return self.VHOST_TEMPLATE.format( vhost=ips, - server_name=achall.gen_response(achall.account_key).z_domain, + server_name=achall.response(achall.account_key).z_domain, ssl_options_conf_path=self.configurator.mod_ssl_conf, cert_path=self.get_cert_path(achall), key_path=self.get_key_path(achall), diff --git a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py index 7c2137c45..1fce69969 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/configurator_test.py @@ -382,8 +382,8 @@ class TwoVhost80Test(util.ApacheTest): account_key, achall1, achall2 = self.get_achalls() dvsni_ret_val = [ - achall1.gen_response(account_key), - achall2.gen_response(account_key), + achall1.response(account_key), + achall2.response(account_key), ] mock_dvsni_perform.return_value = dvsni_ret_val @@ -592,15 +592,15 @@ class TwoVhost80Test(util.ApacheTest): def get_achalls(self): """Return testing achallenges.""" account_key = self.rsa512jwk - achall1 = achallenges.DVSNI( + achall1 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb( - challenges.DVSNI( + challenges.TLSSNI01( token="jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q"), "pending"), domain="encryption-example.demo", account_key=account_key) - achall2 = achallenges.DVSNI( + achall2 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb( - challenges.DVSNI( + challenges.TLSSNI01( token="uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU"), "pending"), domain="letsencrypt.demo", account_key=account_key) diff --git a/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py b/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py index 6cb93e0ca..911c2a36b 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py @@ -13,8 +13,8 @@ from letsencrypt_apache.tests import util class DvsniPerformTest(util.ApacheTest): """Test the ApacheDVSNI challenge.""" - auth_key = common_test.DvsniTest.auth_key - achalls = common_test.DvsniTest.achalls + auth_key = common_test.TLSSNI01Test.auth_key + achalls = common_test.TLSSNI01Test.achalls def setUp(self): # pylint: disable=arguments-differ super(DvsniPerformTest, self).setUp() @@ -46,7 +46,7 @@ class DvsniPerformTest(util.ApacheTest): achall = self.achalls[0] self.sni.add_chall(achall) - response = self.achalls[0].gen_response(self.auth_key) + response = self.achalls[0].response(self.auth_key) mock_setup_cert = mock.MagicMock(return_value=response) # pylint: disable=protected-access self.sni._setup_challenge_cert = mock_setup_cert @@ -72,7 +72,7 @@ class DvsniPerformTest(util.ApacheTest): acme_responses = [] for achall in self.achalls: self.sni.add_chall(achall) - acme_responses.append(achall.gen_response(self.auth_key)) + acme_responses.append(achall.response(self.auth_key)) mock_setup_cert = mock.MagicMock(side_effect=acme_responses) # pylint: disable=protected-access @@ -100,7 +100,7 @@ class DvsniPerformTest(util.ApacheTest): z_domains = [] for achall in self.achalls: self.sni.add_chall(achall) - z_domain = achall.gen_response(self.auth_key).z_domain + z_domain = achall.response(self.auth_key).z_domain z_domains.append(set([z_domain])) self.sni._mod_config() # pylint: disable=protected-access diff --git a/letsencrypt-compatibility-test/letsencrypt_compatibility_test/test_driver.py b/letsencrypt-compatibility-test/letsencrypt_compatibility_test/test_driver.py index b91322c3c..5765003b9 100644 --- a/letsencrypt-compatibility-test/letsencrypt_compatibility_test/test_driver.py +++ b/letsencrypt-compatibility-test/letsencrypt_compatibility_test/test_driver.py @@ -60,7 +60,7 @@ def test_authenticator(plugin, config, temp_dir): "Plugin failed to complete %s for %s in %s", type(achalls[i]), achalls[i].domain, config) success = False - elif isinstance(responses[i], challenges.DVSNIResponse): + elif isinstance(responses[i], challenges.TLSSNI01): verify = functools.partial(responses[i].simple_verify, achalls[i], achalls[i].domain, util.JWK.public_key(), @@ -68,10 +68,10 @@ def test_authenticator(plugin, config, temp_dir): port=plugin.https_port) if _try_until_true(verify): logger.info( - "DVSNI verification for %s succeeded", achalls[i].domain) + "tls-sni-01 verification for %s succeeded", achalls[i].domain) else: logger.error( - "DVSNI verification for %s in %s failed", + "tls-sni-01 verification for %s in %s failed", achalls[i].domain, config) success = False @@ -99,12 +99,12 @@ def _create_achalls(plugin): for domain in names: prefs = plugin.get_chall_pref(domain) for chall_type in prefs: - if chall_type == challenges.DVSNI: - chall = challenges.DVSNI( - token=os.urandom(challenges.DVSNI.TOKEN_SIZE)) + if chall_type == challenges.TLSSNI01: + chall = challenges.TLSSNI01( + token=os.urandom(challenges.TLSSNI01.TOKEN_SIZE)) challb = acme_util.chall_to_challb( chall, messages.STATUS_PENDING) - achall = achallenges.DVSNI( + achall = achallenges.KeyAuthorizationAnnotatedChallenge( challb=challb, domain=domain, account_key=util.JWK) achalls.append(achall) diff --git a/letsencrypt-nginx/letsencrypt_nginx/configurator.py b/letsencrypt-nginx/letsencrypt_nginx/configurator.py index 80b4ebc5d..0123ac321 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/configurator.py +++ b/letsencrypt-nginx/letsencrypt_nginx/configurator.py @@ -14,7 +14,6 @@ import zope.interface from acme import challenges from acme import crypto_util as acme_crypto_util -from letsencrypt import achallenges from letsencrypt import constants as core_constants from letsencrypt import crypto_util from letsencrypt import errors @@ -537,7 +536,7 @@ class NginxConfigurator(common.Plugin): ########################################################################### def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use """Return list of challenge preferences.""" - return [challenges.DVSNI] + return [challenges.TLSSNI01] # Entry point in main.py for performing challenges def perform(self, achalls): @@ -553,11 +552,10 @@ class NginxConfigurator(common.Plugin): nginx_dvsni = dvsni.NginxDvsni(self) for i, achall in enumerate(achalls): - if isinstance(achall, achallenges.DVSNI): - # Currently also have dvsni hold associated index - # of the challenge. This helps to put all of the responses back - # together when they are all complete. - nginx_dvsni.add_chall(achall, i) + # Currently also have dvsni hold associated index + # of the challenge. This helps to put all of the responses back + # together when they are all complete. + nginx_dvsni.add_chall(achall, i) sni_response = nginx_dvsni.perform() # Must restart in order to activate the challenges. diff --git a/letsencrypt-nginx/letsencrypt_nginx/dvsni.py b/letsencrypt-nginx/letsencrypt_nginx/dvsni.py index 828dac650..b388c0267 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/dvsni.py +++ b/letsencrypt-nginx/letsencrypt_nginx/dvsni.py @@ -13,7 +13,7 @@ from letsencrypt_nginx import nginxparser logger = logging.getLogger(__name__) -class NginxDvsni(common.Dvsni): +class NginxDvsni(common.TLSSNI01): """Class performs DVSNI challenges within the Nginx configurator. :ivar configurator: NginxConfigurator object @@ -141,7 +141,7 @@ class NginxDvsni(common.Dvsni): block = [['listen', str(addr)] for addr in addrs] block.extend([['server_name', - achall.gen_response(achall.account_key).z_domain], + achall.response(achall.account_key).z_domain], ['include', self.configurator.parser.loc["ssl_options"]], # access and error logs necessary for # integration testing (non-root) diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py index d8bdf8355..7000f85dc 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/configurator_test.py @@ -51,7 +51,7 @@ class NginxConfiguratorTest(util.NginxTest): errors.PluginError, self.config.enhance, 'myhost', 'redirect') def test_get_chall_pref(self): - self.assertEqual([challenges.DVSNI], + self.assertEqual([challenges.TLSSNI01], self.config.get_chall_pref('myhost')) def test_save(self): @@ -210,22 +210,22 @@ class NginxConfiguratorTest(util.NginxTest): def test_perform(self, mock_restart, mock_dvsni_perform): # Only tests functionality specific to configurator.perform # Note: As more challenges are offered this will have to be expanded - achall1 = achallenges.DVSNI( + achall1 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=messages.ChallengeBody( - chall=challenges.DVSNI(token="kNdwjwOeX0I_A8DXt9Msmg"), + chall=challenges.TLSSNI01(token="kNdwjwOeX0I_A8DXt9Msmg"), uri="https://ca.org/chall0_uri", status=messages.Status("pending"), ), domain="localhost", account_key=self.rsa512jwk) - achall2 = achallenges.DVSNI( + achall2 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=messages.ChallengeBody( - chall=challenges.DVSNI(token="m8TdO1qik4JVFtgPPurJmg"), + chall=challenges.TLSSNI01(token="m8TdO1qik4JVFtgPPurJmg"), uri="https://ca.org/chall1_uri", status=messages.Status("pending"), ), domain="example.com", account_key=self.rsa512jwk) dvsni_ret_val = [ - achall1.gen_response(self.rsa512jwk), - achall2.gen_response(self.rsa512jwk), + achall1.response(self.rsa512jwk), + achall2.response(self.rsa512jwk), ] mock_dvsni_perform.return_value = dvsni_ret_val diff --git a/letsencrypt-nginx/letsencrypt_nginx/tests/dvsni_test.py b/letsencrypt-nginx/letsencrypt_nginx/tests/dvsni_test.py index 9fc0a1ad7..d32e3d98f 100644 --- a/letsencrypt-nginx/letsencrypt_nginx/tests/dvsni_test.py +++ b/letsencrypt-nginx/letsencrypt_nginx/tests/dvsni_test.py @@ -19,22 +19,22 @@ from letsencrypt_nginx.tests import util class DvsniPerformTest(util.NginxTest): """Test the NginxDVSNI challenge.""" - account_key = common_test.DvsniTest.auth_key + account_key = common_test.TLSSNI01Test.auth_key achalls = [ - achallenges.DVSNI( + achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb( - challenges.DVSNI(token="kNdwjwOeX0I_A8DXt9Msmg"), "pending"), + challenges.TLSSNI01(token="kNdwjwOeX0I_A8DXt9Msmg"), "pending"), domain="www.example.com", account_key=account_key), - achallenges.DVSNI( + achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb( - challenges.DVSNI( + challenges.TLSSNI01( token="\xba\xa9\xda? Date: Sat, 7 Nov 2015 18:17:24 +0000 Subject: [PATCH 10/14] Kill dvsni in core --- letsencrypt/achallenges.py | 31 --------------------- letsencrypt/auth_handler.py | 7 ++--- letsencrypt/errors.py | 4 +-- letsencrypt/plugins/manual.py | 2 +- letsencrypt/tests/achallenges_test.py | 34 ----------------------- letsencrypt/tests/acme_util.py | 3 -- letsencrypt/tests/continuity_auth_test.py | 4 +-- 7 files changed, 7 insertions(+), 78 deletions(-) delete mode 100644 letsencrypt/tests/achallenges_test.py diff --git a/letsencrypt/achallenges.py b/letsencrypt/achallenges.py index 4e46d6af9..4d85f5d6a 100644 --- a/letsencrypt/achallenges.py +++ b/letsencrypt/achallenges.py @@ -55,37 +55,6 @@ class KeyAuthorizationAnnotatedChallenge(AnnotatedChallenge): self.account_key, *args, **kwargs) -class DVSNI(AnnotatedChallenge): - """Client annotated "dvsni" ACME challenge. - - :ivar .JWK account_key: Authorized Account Key - - """ - __slots__ = ('challb', 'domain', 'account_key') - acme_type = challenges.DVSNI - - def gen_cert_and_response(self, key=None, bits=2048, alg=jose.RS256): - """Generate a DVSNI cert and response. - - :param OpenSSL.crypto.PKey key: Private key used for - certificate generation. If none provided, a fresh key will - be generated. - :param int bits: Number of bits for fresh key generation. - :param .JWAAlgorithm alg: - - :returns: ``(response, cert_pem, key_pem)`` tuple, where - ``response`` is an instance of - `acme.challenges.DVSNIResponse`, ``cert`` is a certificate - (`OpenSSL.crypto.X509`) and ``key`` is a private key - (`OpenSSL.crypto.PKey`). - :rtype: tuple - - """ - response = self.challb.chall.gen_response(self.account_key, alg=alg) - cert, key = response.gen_cert(key=key, bits=bits) - return response, cert, key - - class DNS(AnnotatedChallenge): """Client annotated "dns" ACME challenge.""" __slots__ = ('challb', 'domain') diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 11019daac..027c11158 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -344,8 +344,8 @@ def challb_to_achall(challb, account_key, domain): chall = challb.chall logger.info("%s challenge for %s", chall.typ, domain) - if isinstance(chall, challenges.DVSNI): - return achallenges.DVSNI( + if isinstance(chall, challenges.KeyAuthorizationChallenge): + return achallenges.KeyAuthorizationAnnotatedChallenge( challb=challb, domain=domain, account_key=account_key) elif isinstance(chall, challenges.DNS): return achallenges.DNS(challb=challb, domain=domain) @@ -355,9 +355,6 @@ 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/errors.py b/letsencrypt/errors.py index 3bfed4d23..a785d5e5c 100644 --- a/letsencrypt/errors.py +++ b/letsencrypt/errors.py @@ -57,8 +57,8 @@ class DvAuthError(AuthorizationError): # Authenticator - Challenge specific errors -class DvsniError(DvAuthError): - """Let's Encrypt DVSNI error.""" +class TLSSNI01Error(DvAuthError): + """Let's Encrypt TLSSNI01 error.""" # Plugin Errors diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index a2a2f7f34..07f06ccec 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -31,7 +31,7 @@ class Authenticator(common.Plugin): run as a privileged process. Alternatively shows instructions on how to use Python's built-in HTTP server. - .. todo:: Support for `~.challenges.DVSNI`. + .. todo:: Support for `~.challenges.TLSSNI01`. """ zope.interface.implements(interfaces.IAuthenticator) diff --git a/letsencrypt/tests/achallenges_test.py b/letsencrypt/tests/achallenges_test.py deleted file mode 100644 index 66b1a7ca7..000000000 --- a/letsencrypt/tests/achallenges_test.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Tests for letsencrypt.achallenges.""" -import unittest - -import OpenSSL - -from acme import challenges -from acme import jose - -from letsencrypt.tests import acme_util -from letsencrypt.tests import test_util - - -class DVSNITest(unittest.TestCase): - """Tests for letsencrypt.achallenges.DVSNI.""" - - def setUp(self): - self.challb = acme_util.chall_to_challb(acme_util.DVSNI, "pending") - key = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) - from letsencrypt.achallenges import DVSNI - self.achall = DVSNI( - challb=self.challb, domain="example.com", account_key=key) - - def test_proxy(self): - self.assertEqual(self.challb.token, self.achall.token) - - def test_gen_cert_and_response(self): - response, cert, key = self.achall.gen_cert_and_response() - self.assertTrue(isinstance(response, challenges.DVSNIResponse)) - self.assertTrue(isinstance(cert, OpenSSL.crypto.X509)) - self.assertTrue(isinstance(key, OpenSSL.crypto.PKey)) - - -if __name__ == "__main__": - unittest.main() # pragma: no cover diff --git a/letsencrypt/tests/acme_util.py b/letsencrypt/tests/acme_util.py index de75a2bc9..6b07b840f 100644 --- a/letsencrypt/tests/acme_util.py +++ b/letsencrypt/tests/acme_util.py @@ -16,8 +16,6 @@ HTTP01 = challenges.HTTP01( token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") TLSSNI01 = challenges.TLSSNI01( token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA")) -DVSNI = challenges.DVSNI( - token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA")) DNS = challenges.DNS(token="17817c66b60ce2e4012dfad92657527a") RECOVERY_CONTACT = challenges.RecoveryContact( activation_url="https://example.ca/sendrecovery/a5bd99383fb0", @@ -81,7 +79,6 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name # Pending ChallengeBody objects -DVSNI_P = chall_to_challb(DVSNI, messages.STATUS_PENDING) TLSSNI01_P = chall_to_challb(TLSSNI01, messages.STATUS_PENDING) HTTP01_P = chall_to_challb(HTTP01, messages.STATUS_PENDING) DNS_P = chall_to_challb(DNS, messages.STATUS_PENDING) diff --git a/letsencrypt/tests/continuity_auth_test.py b/letsencrypt/tests/continuity_auth_test.py index d80a1cfb4..70287bd01 100644 --- a/letsencrypt/tests/continuity_auth_test.py +++ b/letsencrypt/tests/continuity_auth_test.py @@ -34,7 +34,7 @@ class PerformTest(unittest.TestCase): def test_unexpected(self): self.assertRaises( errors.ContAuthError, self.auth.perform, [ - achallenges.DVSNI( + achallenges.KeyAuthorizationAnnotatedChallenge( challb=None, domain="0", account_key="invalid_key")]) def test_chall_pref(self): @@ -53,7 +53,7 @@ class CleanupTest(unittest.TestCase): mock.MagicMock(server="demo_server.org"), None) def test_unexpected(self): - unexpected = achallenges.DVSNI( + unexpected = achallenges.KeyAuthorizationAnnotatedChallenge( challb=None, domain="0", account_key="dummy_key") self.assertRaises(errors.ContAuthError, self.auth.cleanup, [unexpected]) From bbb7606fe1914e186443b85bf192f55200400285 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 7 Nov 2015 18:17:35 +0000 Subject: [PATCH 11/14] Kill dvsni in acme --- acme/acme/challenges.py | 162 ----------------------------------- acme/acme/challenges_test.py | 155 --------------------------------- docs/contributing.rst | 4 +- 3 files changed, 2 insertions(+), 319 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 5f97547ee..522e701e7 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -440,168 +440,6 @@ class TLSSNI01(KeyAuthorizationChallenge): return self.response(account_key).gen_cert(key=kwargs.get('cert_key')) -@Challenge.register # pylint: disable=too-many-ancestors -class DVSNI(_TokenDVChallenge): - """ACME "dvsni" challenge. - - :ivar bytes token: Random data, **not** base64-encoded. - - """ - typ = "dvsni" - - PORT = 443 - """Port to perform DVSNI challenge.""" - - def gen_response(self, account_key, alg=jose.RS256, **kwargs): - """Generate response. - - :param .JWK account_key: Private account key. - :rtype: .DVSNIResponse - - """ - return DVSNIResponse(validation=jose.JWS.sign( - payload=self.json_dumps(sort_keys=True).encode('utf-8'), - key=account_key, alg=alg, **kwargs)) - - -@ChallengeResponse.register -class DVSNIResponse(ChallengeResponse): - """ACME "dvsni" challenge response. - - :param bytes s: Random data, **not** base64-encoded. - - """ - typ = "dvsni" - - DOMAIN_SUFFIX = b".acme.invalid" - """Domain name suffix.""" - - PORT = DVSNI.PORT - """Port to perform DVSNI challenge.""" - - validation = jose.Field("validation", decoder=jose.JWS.from_json) - - @property - def z(self): # pylint: disable=invalid-name - """The ``z`` parameter. - - :rtype: bytes - - """ - # Instance of 'Field' has no 'signature' member - # pylint: disable=no-member - return hashlib.sha256(self.validation.signature.encode( - "signature").encode("utf-8")).hexdigest().encode() - - @property - def z_domain(self): - """Domain name for certificate subjectAltName. - - :rtype: bytes - - """ - z = self.z # pylint: disable=invalid-name - return z[:32] + b'.' + z[32:] + self.DOMAIN_SUFFIX - - @property - def chall(self): - """Get challenge encoded in the `validation` payload. - - :rtype: challenges.DVSNI - - """ - # pylint: disable=no-member - return DVSNI.json_loads(self.validation.payload.decode('utf-8')) - - def gen_cert(self, key=None, bits=2048): - """Generate DVSNI certificate. - - :param OpenSSL.crypto.PKey key: Optional private key used in - certificate generation. If not provided (``None``), then - fresh key will be generated. - :param int bits: Number of bits for newly generated key. - - :rtype: `tuple` of `OpenSSL.crypto.X509` and - `OpenSSL.crypto.PKey` - - """ - if key is None: - key = OpenSSL.crypto.PKey() - key.generate_key(OpenSSL.crypto.TYPE_RSA, bits) - return crypto_util.gen_ss_cert(key, [ - # z_domain is too big to fit into CN, hence first dummy domain - 'dummy', self.z_domain.decode()], force_san=True), key - - def probe_cert(self, domain, **kwargs): - """Probe DVSNI challenge certificate. - - :param unicode domain: - - """ - if "host" not in kwargs: - host = socket.gethostbyname(domain) - logging.debug('%s resolved to %s', domain, host) - kwargs["host"] = host - - kwargs.setdefault("port", self.PORT) - kwargs["name"] = self.z_domain - # TODO: try different methods? - # pylint: disable=protected-access - return crypto_util.probe_sni(**kwargs) - - def verify_cert(self, cert): - """Verify DVSNI challenge certificate.""" - # pylint: disable=protected-access - sans = crypto_util._pyopenssl_cert_or_req_san(cert) - logging.debug('Certificate %s. SANs: %s', cert.digest('sha1'), sans) - return self.z_domain.decode() in sans - - def simple_verify(self, chall, domain, account_public_key, - cert=None, **kwargs): - """Simple verify. - - Verify ``validation`` using ``account_public_key``, optionally - probe DVSNI certificate and check using `verify_cert`. - - :param .challenges.DVSNI chall: Corresponding challenge. - :param str domain: Domain name being validated. - :param JWK account_public_key: - :param OpenSSL.crypto.X509 cert: Optional certificate. If not - provided (``None``) certificate will be retrieved using - `probe_cert`. - - - :returns: ``True`` iff client's control of the domain has been - verified, ``False`` otherwise. - :rtype: bool - - """ - # pylint: disable=no-member - if not self.validation.verify(key=account_public_key): - return False - - # TODO: it's not checked that payload has exectly 2 fields! - try: - decoded_chall = self.chall - except jose.DeserializationError as error: - logger.debug(error, exc_info=True) - return False - - if decoded_chall.token != chall.token: - logger.debug("Wrong token: expected %r, found %r", - chall.token, decoded_chall.token) - return False - - if cert is None: - try: - cert = self.probe_cert(domain=domain, **kwargs) - except errors.Error as error: - logger.debug(error, exc_info=True) - return False - - return self.verify_cert(cert) - - @Challenge.register class RecoveryContact(ContinuityChallenge): """ACME "recoveryContact" challenge. diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 3fcb01e4d..c4f3d6c61 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -320,161 +320,6 @@ class TLSSNI01Test(unittest.TestCase): mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key) -class DVSNITest(unittest.TestCase): - - def setUp(self): - from acme.challenges import DVSNI - self.msg = DVSNI( - token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e')) - self.jmsg = { - 'type': 'dvsni', - 'token': 'a82d5ff8ef740d12881f6d3c2277ab2e', - } - - def test_to_partial_json(self): - self.assertEqual(self.jmsg, self.msg.to_partial_json()) - - def test_from_json(self): - from acme.challenges import DVSNI - self.assertEqual(self.msg, DVSNI.from_json(self.jmsg)) - - def test_from_json_hashable(self): - from acme.challenges import DVSNI - hash(DVSNI.from_json(self.jmsg)) - - def test_from_json_invalid_token_length(self): - from acme.challenges import DVSNI - self.jmsg['token'] = jose.encode_b64jose(b'abcd') - self.assertRaises( - jose.DeserializationError, DVSNI.from_json, self.jmsg) - - def test_gen_response(self): - from acme.challenges import DVSNI - self.assertEqual(self.msg, DVSNI.json_loads( - self.msg.gen_response(KEY).validation.payload.decode())) - - -class DVSNIResponseTest(unittest.TestCase): - # pylint: disable=too-many-instance-attributes - - def setUp(self): - from acme.challenges import DVSNI - self.chall = DVSNI( - token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e')) - - from acme.challenges import DVSNIResponse - self.validation = jose.JWS.sign( - payload=self.chall.json_dumps(sort_keys=True).encode(), - key=KEY, alg=jose.RS256) - self.msg = DVSNIResponse(validation=self.validation) - self.jmsg_to = { - 'resource': 'challenge', - 'type': 'dvsni', - 'validation': self.validation, - } - self.jmsg_from = { - 'resource': 'challenge', - 'type': 'dvsni', - 'validation': self.validation.to_json(), - } - - # pylint: disable=invalid-name - label1 = b'e2df3498860637c667fedadc5a8494ec' - label2 = b'09dcc75553c9b3bd73662b50e71b1e42' - self.z = label1 + label2 - self.z_domain = label1 + b'.' + label2 + b'.acme.invalid' - self.domain = 'foo.com' - - def test_z_and_domain(self): - self.assertEqual(self.z, self.msg.z) - self.assertEqual(self.z_domain, self.msg.z_domain) - - def test_to_partial_json(self): - self.assertEqual(self.jmsg_to, self.msg.to_partial_json()) - - def test_from_json(self): - from acme.challenges import DVSNIResponse - self.assertEqual(self.msg, DVSNIResponse.from_json(self.jmsg_from)) - - def test_from_json_hashable(self): - from acme.challenges import DVSNIResponse - hash(DVSNIResponse.from_json(self.jmsg_from)) - - @mock.patch('acme.challenges.socket.gethostbyname') - @mock.patch('acme.challenges.crypto_util.probe_sni') - def test_probe_cert(self, mock_probe_sni, mock_gethostbyname): - mock_gethostbyname.return_value = '127.0.0.1' - self.msg.probe_cert('foo.com') - mock_gethostbyname.assert_called_once_with('foo.com') - mock_probe_sni.assert_called_once_with( - host='127.0.0.1', port=self.msg.PORT, - name=self.z_domain) - - self.msg.probe_cert('foo.com', host='8.8.8.8') - mock_probe_sni.assert_called_with( - host='8.8.8.8', port=mock.ANY, name=mock.ANY) - - self.msg.probe_cert('foo.com', port=1234) - mock_probe_sni.assert_called_with( - host=mock.ANY, port=1234, name=mock.ANY) - - self.msg.probe_cert('foo.com', bar='baz') - mock_probe_sni.assert_called_with( - host=mock.ANY, port=mock.ANY, name=mock.ANY, bar='baz') - - self.msg.probe_cert('foo.com', name=b'xxx') - mock_probe_sni.assert_called_with( - host=mock.ANY, port=mock.ANY, - name=self.z_domain) - - def test_gen_verify_cert(self): - key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem') - cert, key2 = self.msg.gen_cert(key1) - self.assertEqual(key1, key2) - self.assertTrue(self.msg.verify_cert(cert)) - - def test_gen_verify_cert_gen_key(self): - cert, key = self.msg.gen_cert() - self.assertTrue(isinstance(key, OpenSSL.crypto.PKey)) - self.assertTrue(self.msg.verify_cert(cert)) - - def test_verify_bad_cert(self): - self.assertFalse(self.msg.verify_cert(test_util.load_cert('cert.pem'))) - - def test_simple_verify_wrong_account_key(self): - self.assertFalse(self.msg.simple_verify( - self.chall, self.domain, jose.JWKRSA.load( - test_util.load_vector('rsa256_key.pem')).public_key())) - - def test_simple_verify_wrong_payload(self): - for payload in b'', b'{}': - msg = self.msg.update(validation=jose.JWS.sign( - payload=payload, key=KEY, alg=jose.RS256)) - self.assertFalse(msg.simple_verify( - self.chall, self.domain, KEY.public_key())) - - def test_simple_verify_wrong_token(self): - msg = self.msg.update(validation=jose.JWS.sign( - payload=self.chall.update(token=(b'b' * 20)).json_dumps().encode(), - key=KEY, alg=jose.RS256)) - self.assertFalse(msg.simple_verify( - self.chall, self.domain, KEY.public_key())) - - @mock.patch('acme.challenges.DVSNIResponse.verify_cert', autospec=True) - def test_simple_verify(self, mock_verify_cert): - mock_verify_cert.return_value = mock.sentinel.verification - self.assertEqual(mock.sentinel.verification, self.msg.simple_verify( - self.chall, self.domain, KEY.public_key(), - cert=mock.sentinel.cert)) - mock_verify_cert.assert_called_once_with(self.msg, mock.sentinel.cert) - - @mock.patch('acme.challenges.DVSNIResponse.probe_cert') - def test_simple_verify_false_on_probe_error(self, mock_probe_cert): - mock_probe_cert.side_effect = errors.Error - self.assertFalse(self.msg.simple_verify( - self.chall, self.domain, KEY.public_key())) - - class RecoveryContactTest(unittest.TestCase): def setUp(self): diff --git a/docs/contributing.rst b/docs/contributing.rst index efc6c27ae..c71aefeec 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -151,7 +151,7 @@ certificate for some domain name by solving challenges received from the ACME server. From the protocol, there are essentially two different types of challenges. Challenges that must be solved by individual plugins in order to satisfy domain validation (subclasses -of `~.DVChallenge`, i.e. `~.challenges.DVSNI`, +of `~.DVChallenge`, i.e. `~.challenges.TLSSNI01`, `~.challenges.HTTP01`, `~.challenges.DNS`) and continuity specific challenges (subclasses of `~.ContinuityChallenge`, i.e. `~.challenges.RecoveryToken`, `~.challenges.RecoveryContact`, @@ -160,7 +160,7 @@ always handled by the `~.ContinuityAuthenticator`, while plugins are expected to handle `~.DVChallenge` types. Right now, we have two authenticator plugins, the `~.ApacheConfigurator` and the `~.StandaloneAuthenticator`. The Standalone and Apache -authenticators only solve the `~.challenges.DVSNI` challenge currently. +authenticators only solve the `~.challenges.TLSSNI01` challenge currently. (You can set which challenges your authenticator can handle through the :meth:`~.IAuthenticator.get_chall_pref`. From 8208470395d049a54f2c71c5c17e5f055de18f89 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 8 Nov 2015 06:26:22 +0000 Subject: [PATCH 12/14] More docs about ports --- acme/acme/challenges.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 522e701e7..0f25f771b 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -221,6 +221,12 @@ class HTTP01Response(KeyAuthorizationChallengeResponse): typ = "http-01" PORT = 80 + """Verification port as defined by the protocol. + + You can override it (e.g. for testing) by passing ``port`` to + `simple_verify`. + + """ def simple_verify(self, chall, domain, account_public_key, port=None): """Simple verify. @@ -328,6 +334,12 @@ class TLSSNI01Response(KeyAuthorizationChallengeResponse): """Domain name suffix.""" PORT = 443 + """Verification port as defined by the protocol. + + You can override it (e.g. for testing) by passing ``port`` to + `simple_verify`. + + """ @property def z(self): @@ -396,6 +408,7 @@ class TLSSNI01Response(KeyAuthorizationChallengeResponse): :param OpenSSL.crypto.X509 cert: Optional certificate. If not provided (``None``) certificate will be retrieved using `probe_cert`. + :param int port: Port used to probe the certificate. :returns: ``True`` iff client's control of the domain has been From 37574e60e199a7c99d88313594889dbc154b40bc Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 8 Nov 2015 06:29:48 +0000 Subject: [PATCH 13/14] hexdigest lower() --- acme/acme/challenges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 0f25f771b..1b4f6c2ed 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -345,7 +345,7 @@ class TLSSNI01Response(KeyAuthorizationChallengeResponse): def z(self): """``z`` value used for verification.""" return hashlib.sha256( - self.key_authorization.encode("utf-8")).hexdigest().encode() + self.key_authorization.encode("utf-8")).hexdigest().lower().encode() @property def z_domain(self): From c18f0b7073728d01fa012a2ab2e5705c53a14193 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 8 Nov 2015 06:34:28 +0000 Subject: [PATCH 14/14] Add rtype docs --- acme/acme/challenges.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index 1b4f6c2ed..976d7ab12 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -343,13 +343,21 @@ class TLSSNI01Response(KeyAuthorizationChallengeResponse): @property def z(self): - """``z`` value used for verification.""" + """``z`` value used for verification. + + :rtype bytes: + + """ return hashlib.sha256( self.key_authorization.encode("utf-8")).hexdigest().lower().encode() @property def z_domain(self): - """Domain name used for verification, generated from `z`.""" + """Domain name used for verification, generated from `z`. + + :rtype bytes: + + """ return self.z[:32] + b'.' + self.z[32:] + self.DOMAIN_SUFFIX def gen_cert(self, key=None, bits=2048):