diff --git a/acme/acme/challenges.py b/acme/acme/challenges.py index c5855a7ca..976d7ab12 100644 --- a/acme/acme/challenges.py +++ b/acme/acme/challenges.py @@ -187,7 +187,7 @@ class KeyAuthorizationChallenge(_TokenDVChallenge): key_authorization=self.key_authorization(account_key)) @abc.abstractmethod - def validation(self, account_key): + def validation(self, account_key, **kwargs): """Generate validation for the challenge. Subclasses must implement this method, but they are likely to @@ -201,7 +201,7 @@ class KeyAuthorizationChallenge(_TokenDVChallenge): """ raise NotImplementedError() # pragma: no cover - def response_and_validation(self, account_key): + def response_and_validation(self, account_key, *args, **kwargs): """Generate response and validation. Convenience function that return results of `response` and @@ -211,7 +211,8 @@ class KeyAuthorizationChallenge(_TokenDVChallenge): :rtype: tuple """ - return (self.response(account_key), self.validation(account_key)) + return (self.response(account_key), + self.validation(account_key, *args, **kwargs)) @ChallengeResponse.register @@ -220,6 +221,12 @@ class HTTP01Response(KeyAuthorizationChallengeResponse): typ = "http-01" PORT = 80 + """Verification port as defined by the protocol. + + You can override it (e.g. for testing) by passing ``port`` to + `simple_verify`. + + """ def simple_verify(self, chall, domain, account_public_key, port=None): """Simple verify. @@ -246,7 +253,7 @@ class HTTP01Response(KeyAuthorizationChallengeResponse): # request URI, if it's standard. if port is not None and port != self.PORT: logger.warning( - "Using non-standard port for SimpleHTTP verification: %s", port) + "Using non-standard port for http-01 verification: %s", port) domain += ":{0}".format(port) uri = chall.uri(domain) @@ -308,7 +315,7 @@ class HTTP01(KeyAuthorizationChallenge): """ return "http://" + domain + self.path - def validation(self, account_key): + def validation(self, account_key, **unused_kwargs): """Generate validation. :param JWK account_key: @@ -318,89 +325,50 @@ class HTTP01(KeyAuthorizationChallenge): return self.key_authorization(account_key) -@Challenge.register # pylint: disable=too-many-ancestors -class DVSNI(_TokenDVChallenge): - """ACME "dvsni" challenge. - - :ivar bytes token: Random data, **not** base64-encoded. - - """ - typ = "dvsni" - - PORT = 443 - """Port to perform DVSNI challenge.""" - - def gen_response(self, account_key, alg=jose.RS256, **kwargs): - """Generate response. - - :param .JWK account_key: Private account key. - :rtype: .DVSNIResponse - - """ - return DVSNIResponse(validation=jose.JWS.sign( - payload=self.json_dumps(sort_keys=True).encode('utf-8'), - key=account_key, alg=alg, **kwargs)) - - @ChallengeResponse.register -class DVSNIResponse(ChallengeResponse): - """ACME "dvsni" challenge response. - - :param bytes s: Random data, **not** base64-encoded. - - """ - typ = "dvsni" +class TLSSNI01Response(KeyAuthorizationChallengeResponse): + """ACME tls-sni-01 challenge response.""" + typ = "tls-sni-01" DOMAIN_SUFFIX = b".acme.invalid" """Domain name suffix.""" - PORT = DVSNI.PORT - """Port to perform DVSNI challenge.""" + PORT = 443 + """Verification port as defined by the protocol. - validation = jose.Field("validation", decoder=jose.JWS.from_json) + You can override it (e.g. for testing) by passing ``port`` to + `simple_verify`. + + """ @property - def z(self): # pylint: disable=invalid-name - """The ``z`` parameter. + def z(self): + """``z`` value used for verification. - :rtype: bytes + :rtype bytes: """ - # Instance of 'Field' has no 'signature' member - # pylint: disable=no-member - return hashlib.sha256(self.validation.signature.encode( - "signature").encode("utf-8")).hexdigest().encode() + return hashlib.sha256( + self.key_authorization.encode("utf-8")).hexdigest().lower().encode() @property def z_domain(self): - """Domain name for certificate subjectAltName. + """Domain name used for verification, generated from `z`. - :rtype: bytes + :rtype bytes: """ - z = self.z # pylint: disable=invalid-name - return z[:32] + b'.' + z[32:] + self.DOMAIN_SUFFIX - - @property - def chall(self): - """Get challenge encoded in the `validation` payload. - - :rtype: challenges.DVSNI - - """ - # pylint: disable=no-member - return DVSNI.json_loads(self.validation.payload.decode('utf-8')) + return self.z[:32] + b'.' + self.z[32:] + self.DOMAIN_SUFFIX def gen_cert(self, key=None, bits=2048): - """Generate DVSNI certificate. + """Generate tls-sni-01 certificate. :param OpenSSL.crypto.PKey key: Optional private key used in certificate generation. If not provided (``None``), then fresh key will be generated. :param int bits: Number of bits for newly generated key. - :rtype: `tuple` of `OpenSSL.crypto.X509` and - `OpenSSL.crypto.PKey` + :rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey` """ if key is None: @@ -411,11 +379,12 @@ class DVSNIResponse(ChallengeResponse): 'dummy', self.z_domain.decode()], force_san=True), key def probe_cert(self, domain, **kwargs): - """Probe DVSNI challenge certificate. + """Probe tls-sni-01 challenge certificate. :param unicode domain: """ + # TODO: domain is not necessary if host is provided if "host" not in kwargs: host = socket.gethostbyname(domain) logging.debug('%s resolved to %s', domain, host) @@ -428,7 +397,7 @@ class DVSNIResponse(ChallengeResponse): return crypto_util.probe_sni(**kwargs) def verify_cert(self, cert): - """Verify DVSNI challenge certificate.""" + """Verify tls-sni-01 challenge certificate.""" # pylint: disable=protected-access sans = crypto_util._pyopenssl_cert_or_req_san(cert) logging.debug('Certificate %s. SANs: %s', cert.digest('sha1'), sans) @@ -439,14 +408,15 @@ class DVSNIResponse(ChallengeResponse): """Simple verify. Verify ``validation`` using ``account_public_key``, optionally - probe DVSNI certificate and check using `verify_cert`. + probe tls-sni-01 certificate and check using `verify_cert`. - :param .challenges.DVSNI chall: Corresponding challenge. + :param .challenges.TLSSNI01 chall: Corresponding challenge. :param str domain: Domain name being validated. :param JWK account_public_key: :param OpenSSL.crypto.X509 cert: Optional certificate. If not provided (``None``) certificate will be retrieved using `probe_cert`. + :param int port: Port used to probe the certificate. :returns: ``True`` iff client's control of the domain has been @@ -454,20 +424,8 @@ class DVSNIResponse(ChallengeResponse): :rtype: bool """ - # pylint: disable=no-member - if not self.validation.verify(key=account_public_key): - return False - - # TODO: it's not checked that payload has exectly 2 fields! - try: - decoded_chall = self.chall - except jose.DeserializationError as error: - logger.debug(error, exc_info=True) - return False - - if decoded_chall.token != chall.token: - logger.debug("Wrong token: expected %r, found %r", - chall.token, decoded_chall.token) + if not self.verify(chall, account_public_key): + logger.debug("Verification of key authorization in response failed") return False if cert is None: @@ -480,6 +438,29 @@ class DVSNIResponse(ChallengeResponse): return self.verify_cert(cert) +@Challenge.register # pylint: disable=too-many-ancestors +class TLSSNI01(KeyAuthorizationChallenge): + """ACME tls-sni-01 challenge.""" + response_cls = TLSSNI01Response + typ = response_cls.typ + + # boulder#962, ietf-wg-acme#22 + #n = jose.Field("n", encoder=int, decoder=int) + + def validation(self, account_key, **kwargs): + """Generate validation. + + :param JWK account_key: + :param OpenSSL.crypto.PKey cert_key: Optional private key used + in certificate generation. If not provided (``None``), then + fresh key will be generated. + + :rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey` + + """ + return self.response(account_key).gen_cert(key=kwargs.get('cert_key')) + + @Challenge.register class RecoveryContact(ContinuityChallenge): """ACME "recoveryContact" challenge. diff --git a/acme/acme/challenges_test.py b/acme/acme/challenges_test.py index 0708f3782..c4f3d6c61 100644 --- a/acme/acme/challenges_test.py +++ b/acme/acme/challenges_test.py @@ -186,14 +186,112 @@ class HTTP01Test(unittest.TestCase): self.msg.update(token=b'..').good_token) -class DVSNITest(unittest.TestCase): +class TLSSNI01ResponseTest(unittest.TestCase): + # pylint: disable=too-many-instance-attributes def setUp(self): - from acme.challenges import DVSNI - self.msg = DVSNI( + from acme.challenges import TLSSNI01 + self.chall = TLSSNI01( + token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e')) + + self.response = self.chall.response(KEY) + self.jmsg = { + 'resource': 'challenge', + 'type': 'tls-sni-01', + 'keyAuthorization': self.response.key_authorization, + } + + # pylint: disable=invalid-name + label1 = b'dc38d9c3fa1a4fdcc3a5501f2d38583f' + label2 = b'b7793728f084394f2a1afd459556bb5c' + self.z = label1 + label2 + self.z_domain = label1 + b'.' + label2 + b'.acme.invalid' + self.domain = 'foo.com' + + def test_z_and_domain(self): + self.assertEqual(self.z, self.response.z) + self.assertEqual(self.z_domain, self.response.z_domain) + + def test_to_partial_json(self): + self.assertEqual(self.jmsg, self.response.to_partial_json()) + + def test_from_json(self): + from acme.challenges import TLSSNI01Response + self.assertEqual(self.response, TLSSNI01Response.from_json(self.jmsg)) + + def test_from_json_hashable(self): + from acme.challenges import TLSSNI01Response + hash(TLSSNI01Response.from_json(self.jmsg)) + + @mock.patch('acme.challenges.socket.gethostbyname') + @mock.patch('acme.challenges.crypto_util.probe_sni') + def test_probe_cert(self, mock_probe_sni, mock_gethostbyname): + mock_gethostbyname.return_value = '127.0.0.1' + self.response.probe_cert('foo.com') + mock_gethostbyname.assert_called_once_with('foo.com') + mock_probe_sni.assert_called_once_with( + host='127.0.0.1', port=self.response.PORT, + name=self.z_domain) + + self.response.probe_cert('foo.com', host='8.8.8.8') + mock_probe_sni.assert_called_with( + host='8.8.8.8', port=mock.ANY, name=mock.ANY) + + self.response.probe_cert('foo.com', port=1234) + mock_probe_sni.assert_called_with( + host=mock.ANY, port=1234, name=mock.ANY) + + self.response.probe_cert('foo.com', bar='baz') + mock_probe_sni.assert_called_with( + host=mock.ANY, port=mock.ANY, name=mock.ANY, bar='baz') + + self.response.probe_cert('foo.com', name=b'xxx') + mock_probe_sni.assert_called_with( + host=mock.ANY, port=mock.ANY, + name=self.z_domain) + + def test_gen_verify_cert(self): + key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem') + cert, key2 = self.response.gen_cert(key1) + self.assertEqual(key1, key2) + self.assertTrue(self.response.verify_cert(cert)) + + def test_gen_verify_cert_gen_key(self): + cert, key = self.response.gen_cert() + self.assertTrue(isinstance(key, OpenSSL.crypto.PKey)) + self.assertTrue(self.response.verify_cert(cert)) + + def test_verify_bad_cert(self): + self.assertFalse(self.response.verify_cert( + test_util.load_cert('cert.pem'))) + + def test_simple_verify_bad_key_authorization(self): + key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) + self.response.simple_verify(self.chall, "local", key2.public_key()) + + @mock.patch('acme.challenges.TLSSNI01Response.verify_cert', autospec=True) + def test_simple_verify(self, mock_verify_cert): + mock_verify_cert.return_value = mock.sentinel.verification + self.assertEqual(mock.sentinel.verification, self.response.simple_verify( + self.chall, self.domain, KEY.public_key(), + cert=mock.sentinel.cert)) + mock_verify_cert.assert_called_once_with(self.response, mock.sentinel.cert) + + @mock.patch('acme.challenges.TLSSNI01Response.probe_cert') + def test_simple_verify_false_on_probe_error(self, mock_probe_cert): + mock_probe_cert.side_effect = errors.Error + self.assertFalse(self.response.simple_verify( + self.chall, self.domain, KEY.public_key())) + + +class TLSSNI01Test(unittest.TestCase): + + def setUp(self): + from acme.challenges import TLSSNI01 + self.msg = TLSSNI01( token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e')) self.jmsg = { - 'type': 'dvsni', + 'type': 'tls-sni-01', 'token': 'a82d5ff8ef740d12881f6d3c2277ab2e', } @@ -201,144 +299,25 @@ class DVSNITest(unittest.TestCase): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): - from acme.challenges import DVSNI - self.assertEqual(self.msg, DVSNI.from_json(self.jmsg)) + from acme.challenges import TLSSNI01 + self.assertEqual(self.msg, TLSSNI01.from_json(self.jmsg)) def test_from_json_hashable(self): - from acme.challenges import DVSNI - hash(DVSNI.from_json(self.jmsg)) + from acme.challenges import TLSSNI01 + hash(TLSSNI01.from_json(self.jmsg)) def test_from_json_invalid_token_length(self): - from acme.challenges import DVSNI + from acme.challenges import TLSSNI01 self.jmsg['token'] = jose.encode_b64jose(b'abcd') self.assertRaises( - jose.DeserializationError, DVSNI.from_json, self.jmsg) + jose.DeserializationError, TLSSNI01.from_json, self.jmsg) - def test_gen_response(self): - from acme.challenges import DVSNI - self.assertEqual(self.msg, DVSNI.json_loads( - self.msg.gen_response(KEY).validation.payload.decode())) - - -class DVSNIResponseTest(unittest.TestCase): - # pylint: disable=too-many-instance-attributes - - def setUp(self): - from acme.challenges import DVSNI - self.chall = DVSNI( - token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e')) - - from acme.challenges import DVSNIResponse - self.validation = jose.JWS.sign( - payload=self.chall.json_dumps(sort_keys=True).encode(), - key=KEY, alg=jose.RS256) - self.msg = DVSNIResponse(validation=self.validation) - self.jmsg_to = { - 'resource': 'challenge', - 'type': 'dvsni', - 'validation': self.validation, - } - self.jmsg_from = { - 'resource': 'challenge', - 'type': 'dvsni', - 'validation': self.validation.to_json(), - } - - # pylint: disable=invalid-name - label1 = b'e2df3498860637c667fedadc5a8494ec' - label2 = b'09dcc75553c9b3bd73662b50e71b1e42' - self.z = label1 + label2 - self.z_domain = label1 + b'.' + label2 + b'.acme.invalid' - self.domain = 'foo.com' - - def test_z_and_domain(self): - self.assertEqual(self.z, self.msg.z) - self.assertEqual(self.z_domain, self.msg.z_domain) - - def test_to_partial_json(self): - self.assertEqual(self.jmsg_to, self.msg.to_partial_json()) - - def test_from_json(self): - from acme.challenges import DVSNIResponse - self.assertEqual(self.msg, DVSNIResponse.from_json(self.jmsg_from)) - - def test_from_json_hashable(self): - from acme.challenges import DVSNIResponse - hash(DVSNIResponse.from_json(self.jmsg_from)) - - @mock.patch('acme.challenges.socket.gethostbyname') - @mock.patch('acme.challenges.crypto_util.probe_sni') - def test_probe_cert(self, mock_probe_sni, mock_gethostbyname): - mock_gethostbyname.return_value = '127.0.0.1' - self.msg.probe_cert('foo.com') - mock_gethostbyname.assert_called_once_with('foo.com') - mock_probe_sni.assert_called_once_with( - host='127.0.0.1', port=self.msg.PORT, - name=self.z_domain) - - self.msg.probe_cert('foo.com', host='8.8.8.8') - mock_probe_sni.assert_called_with( - host='8.8.8.8', port=mock.ANY, name=mock.ANY) - - self.msg.probe_cert('foo.com', port=1234) - mock_probe_sni.assert_called_with( - host=mock.ANY, port=1234, name=mock.ANY) - - self.msg.probe_cert('foo.com', bar='baz') - mock_probe_sni.assert_called_with( - host=mock.ANY, port=mock.ANY, name=mock.ANY, bar='baz') - - self.msg.probe_cert('foo.com', name=b'xxx') - mock_probe_sni.assert_called_with( - host=mock.ANY, port=mock.ANY, - name=self.z_domain) - - def test_gen_verify_cert(self): - key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem') - cert, key2 = self.msg.gen_cert(key1) - self.assertEqual(key1, key2) - self.assertTrue(self.msg.verify_cert(cert)) - - def test_gen_verify_cert_gen_key(self): - cert, key = self.msg.gen_cert() - self.assertTrue(isinstance(key, OpenSSL.crypto.PKey)) - self.assertTrue(self.msg.verify_cert(cert)) - - def test_verify_bad_cert(self): - self.assertFalse(self.msg.verify_cert(test_util.load_cert('cert.pem'))) - - def test_simple_verify_wrong_account_key(self): - self.assertFalse(self.msg.simple_verify( - self.chall, self.domain, jose.JWKRSA.load( - test_util.load_vector('rsa256_key.pem')).public_key())) - - def test_simple_verify_wrong_payload(self): - for payload in b'', b'{}': - msg = self.msg.update(validation=jose.JWS.sign( - payload=payload, key=KEY, alg=jose.RS256)) - self.assertFalse(msg.simple_verify( - self.chall, self.domain, KEY.public_key())) - - def test_simple_verify_wrong_token(self): - msg = self.msg.update(validation=jose.JWS.sign( - payload=self.chall.update(token=(b'b' * 20)).json_dumps().encode(), - key=KEY, alg=jose.RS256)) - self.assertFalse(msg.simple_verify( - self.chall, self.domain, KEY.public_key())) - - @mock.patch('acme.challenges.DVSNIResponse.verify_cert', autospec=True) - def test_simple_verify(self, mock_verify_cert): - mock_verify_cert.return_value = mock.sentinel.verification - self.assertEqual(mock.sentinel.verification, self.msg.simple_verify( - self.chall, self.domain, KEY.public_key(), - cert=mock.sentinel.cert)) - mock_verify_cert.assert_called_once_with(self.msg, mock.sentinel.cert) - - @mock.patch('acme.challenges.DVSNIResponse.probe_cert') - def test_simple_verify_false_on_probe_error(self, mock_probe_cert): - mock_probe_cert.side_effect = errors.Error - self.assertFalse(self.msg.simple_verify( - self.chall, self.domain, KEY.public_key())) + @mock.patch('acme.challenges.TLSSNI01Response.gen_cert') + def test_validation(self, mock_gen_cert): + mock_gen_cert.return_value = ('cert', 'key') + self.assertEqual(('cert', 'key'), self.msg.validation( + KEY, cert_key=mock.sentinel.cert_key)) + mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key) class RecoveryContactTest(unittest.TestCase): @@ -571,8 +550,6 @@ class ProofOfPossessionResponseTest(unittest.TestCase): class DNSTest(unittest.TestCase): def setUp(self): - self.account_key = jose.JWKRSA.load( - test_util.load_vector('rsa512_key.pem')) from acme.challenges import DNS self.msg = DNS(token=jose.b64decode( b'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA')) @@ -594,34 +571,33 @@ class DNSTest(unittest.TestCase): def test_gen_check_validation(self): self.assertTrue(self.msg.check_validation( - self.msg.gen_validation(self.account_key), - self.account_key.public_key())) + self.msg.gen_validation(KEY), KEY.public_key())) def test_gen_check_validation_wrong_key(self): key2 = jose.JWKRSA.load(test_util.load_vector('rsa1024_key.pem')) self.assertFalse(self.msg.check_validation( - self.msg.gen_validation(self.account_key), key2.public_key())) + self.msg.gen_validation(KEY), key2.public_key())) def test_check_validation_wrong_payload(self): validations = tuple( - jose.JWS.sign(payload=payload, alg=jose.RS256, key=self.account_key) + jose.JWS.sign(payload=payload, alg=jose.RS256, key=KEY) for payload in (b'', b'{}') ) for validation in validations: self.assertFalse(self.msg.check_validation( - validation, self.account_key.public_key())) + validation, KEY.public_key())) def test_check_validation_wrong_fields(self): bad_validation = jose.JWS.sign( payload=self.msg.update(token=b'x' * 20).json_dumps().encode('utf-8'), - alg=jose.RS256, key=self.account_key) + alg=jose.RS256, key=KEY) self.assertFalse(self.msg.check_validation( - bad_validation, self.account_key.public_key())) + bad_validation, KEY.public_key())) def test_gen_response(self): with mock.patch('acme.challenges.DNS.gen_validation') as mock_gen: mock_gen.return_value = mock.sentinel.validation - response = self.msg.gen_response(self.account_key) + response = self.msg.gen_response(KEY) from acme.challenges import DNSResponse self.assertTrue(isinstance(response, DNSResponse)) self.assertEqual(response.validation, mock.sentinel.validation) diff --git a/acme/acme/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/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`. 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-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 ed88bf8a7..2f9e9ed18 100644 --- a/letsencrypt-apache/letsencrypt_apache/dvsni.py +++ b/letsencrypt-apache/letsencrypt_apache/dvsni.py @@ -7,14 +7,14 @@ from letsencrypt_apache import obj from letsencrypt_apache import parser -class ApacheDvsni(common.Dvsni): +class ApacheDvsni(common.TLSSNI01): """Class performs DVSNI challenges within the Apache configurator. :ivar configurator: ApacheConfigurator object :type configurator: :class:`~apache.configurator.ApacheConfigurator` - :ivar list achalls: Annotated :class:`~letsencrypt.achallenges.DVSNI` - challenges. + :ivar list achalls: Annotated tls-sni-01 + (`.KeyAuthorizationAnnotatedChallenge`) challenges. :param list indices: Meant to hold indices of challenges in a larger array. ApacheDvsni is capable of solving many challenges @@ -62,7 +62,7 @@ class ApacheDvsni(common.Dvsni): # Prepare the server for HTTPS self.configurator.prepare_server_https( - str(self.configurator.config.dvsni_port), True) + str(self.configurator.config.tls_sni_01_port), True) responses = [] @@ -114,14 +114,15 @@ class ApacheDvsni(common.Dvsni): # TODO: Checkout _default_ rules. dvsni_addrs = set() - default_addr = obj.Addr(("*", str(self.configurator.config.dvsni_port))) + default_addr = obj.Addr(("*", str( + self.configurator.config.tls_sni_01_port))) for addr in vhost.addrs: if "_default_" == addr.get_addr(): dvsni_addrs.add(default_addr) else: dvsni_addrs.add( - addr.get_sni_addr(self.configurator.config.dvsni_port)) + addr.get_sni_addr(self.configurator.config.tls_sni_01_port)) return dvsni_addrs @@ -144,8 +145,8 @@ class ApacheDvsni(common.Dvsni): def _get_config_text(self, achall, ip_addrs): """Chocolate virtual server configuration text - :param achall: Annotated DVSNI challenge. - :type achall: :class:`letsencrypt.achallenges.DVSNI` + :param .KeyAuthorizationAnnotatedChallenge achall: Annotated + DVSNI challenge. :param list ip_addrs: addresses of challenged domain :class:`list` of type `~.obj.Addr` @@ -164,7 +165,7 @@ class ApacheDvsni(common.Dvsni): # https://docs.python.org/2.7/reference/lexical_analysis.html return self.VHOST_TEMPLATE.format( vhost=ips, - server_name=achall.gen_response(achall.account_key).z_domain, + server_name=achall.response(achall.account_key).z_domain, ssl_options_conf_path=self.configurator.mod_ssl_conf, cert_path=self.get_cert_path(achall), key_path=self.get_key_path(achall), diff --git a/letsencrypt-apache/letsencrypt_apache/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 c362d4115..911c2a36b 100644 --- a/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py +++ b/letsencrypt-apache/letsencrypt_apache/tests/dvsni_test.py @@ -13,15 +13,15 @@ from letsencrypt_apache.tests import util class DvsniPerformTest(util.ApacheTest): """Test the ApacheDVSNI challenge.""" - auth_key = common_test.DvsniTest.auth_key - achalls = common_test.DvsniTest.achalls + auth_key = common_test.TLSSNI01Test.auth_key + achalls = common_test.TLSSNI01Test.achalls def setUp(self): # pylint: disable=arguments-differ super(DvsniPerformTest, self).setUp() config = util.get_apache_configurator( self.config_path, self.config_dir, self.work_dir) - config.config.dvsni_port = 443 + config.config.tls_sni_01_port = 443 from letsencrypt_apache import dvsni self.sni = dvsni.ApacheDvsni(config) @@ -46,7 +46,7 @@ class DvsniPerformTest(util.ApacheTest): achall = self.achalls[0] self.sni.add_chall(achall) - response = self.achalls[0].gen_response(self.auth_key) + response = self.achalls[0].response(self.auth_key) mock_setup_cert = mock.MagicMock(return_value=response) # pylint: disable=protected-access self.sni._setup_challenge_cert = mock_setup_cert @@ -72,7 +72,7 @@ class DvsniPerformTest(util.ApacheTest): acme_responses = [] for achall in self.achalls: self.sni.add_chall(achall) - acme_responses.append(achall.gen_response(self.auth_key)) + acme_responses.append(achall.response(self.auth_key)) mock_setup_cert = mock.MagicMock(side_effect=acme_responses) # pylint: disable=protected-access @@ -100,7 +100,7 @@ class DvsniPerformTest(util.ApacheTest): z_domains = [] for achall in self.achalls: self.sni.add_chall(achall) - z_domain = achall.gen_response(self.auth_key).z_domain + z_domain = achall.response(self.auth_key).z_domain z_domains.append(set([z_domain])) self.sni._mod_config() # pylint: disable=protected-access diff --git a/letsencrypt-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 29e69e498..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 @@ -297,7 +296,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 +306,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 +320,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. @@ -536,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): @@ -552,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 662f10889..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 @@ -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) @@ -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?