diff --git a/acme/challenges.py b/acme/challenges.py index b856be888..39a927bc5 100644 --- a/acme/challenges.py +++ b/acme/challenges.py @@ -63,6 +63,8 @@ class SimpleHTTPResponse(ChallengeResponse): MAX_PATH_LEN = 25 """Maximum allowed `path` length.""" + CONTENT_TYPE = "text/plain" + @property def good_path(self): """Is `path` good? diff --git a/acme/verify.py b/acme/verify.py index 9945b75a7..c0092d9fb 100644 --- a/acme/verify.py +++ b/acme/verify.py @@ -20,7 +20,7 @@ def simple_http_simple_verify(response, chall, domain): try: http_response = requests.get(uri, verify=False) except requests.exceptions.RequestException as error: - logger.error("Unable to verify %s: %s", uri, error) + logger.error("Unable to reach %s: %s", uri, error) return False logger.debug( 'Received %s. Headers: %s', http_response, http_response.headers) @@ -30,4 +30,8 @@ def simple_http_simple_verify(response, chall, domain): logger.error( "Unable to verify %s! Expected: %r, returned: %r.", uri, chall.token, http_response.text) - return response.good_path and http_response and good_token + # TODO: spec contradicts itself, c.f. + # https://github.com/letsencrypt/acme-spec/pull/156/files#r33136438 + good_ct = response.CONTENT_TYPE == http_response.headers.get( + "Content-Type", response.CONTENT_TYPE) + return response.good_path and good_ct and good_token diff --git a/acme/verify_test.py b/acme/verify_test.py index a76ba959f..908c7ff1b 100644 --- a/acme/verify_test.py +++ b/acme/verify_test.py @@ -14,6 +14,8 @@ class SimpleHTTPSimpleVerifyTest(unittest.TestCase): self.chall = challenges.SimpleHTTP(token="foo") self.resp_http = challenges.SimpleHTTPResponse(path="bar", tls=False) self.resp_https = challenges.SimpleHTTPResponse(path="bar", tls=True) + self.good_headers = { + 'Content-Type': challenges.SimpleHTTPResponse.CONTENT_TYPE} @classmethod def _call(cls, *args, **kwargs): @@ -24,13 +26,20 @@ class SimpleHTTPSimpleVerifyTest(unittest.TestCase): def test_good_token(self, mock_get): for resp in self.resp_http, self.resp_https: mock_get.reset_mock() - mock_get.return_value = mock.MagicMock(text=self.chall.token) + mock_get.return_value = mock.MagicMock( + text=self.chall.token, headers=self.good_headers) self.assertTrue(self._call(resp, self.chall, "local")) mock_get.assert_called_once_with(resp.uri("local"), verify=False) @mock.patch("acme.verify.requests.get") def test_bad_token(self, mock_get): - mock_get().text = self.chall.token + "!" + mock_get.return_value = mock.MagicMock( + text=self.chall.token + "!", headers=self.good_headers) + self.assertFalse(self._call(self.resp_http, self.chall, "local")) + + @mock.patch("acme.verify.requests.get") + def test_bad_content_type(self, mock_get): + mock_get().text = self.chall.token self.assertFalse(self._call(self.resp_http, self.chall, "local")) @mock.patch("acme.verify.requests.get") diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py index 7626b8031..5e964a17e 100644 --- a/letsencrypt/plugins/manual.py +++ b/letsencrypt/plugins/manual.py @@ -30,6 +30,8 @@ Make sure your web server displays the following content at {achall.token} +Content-Type header MUST be set to {ct}. + If you don't have HTTP server configured, you can run the following command on the target server (as root): @@ -40,7 +42,10 @@ command on the target server (as root): mkdir -p {response.URI_ROOT_PATH} echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path} # run only once per server: -python -m SimpleHTTPServer 80""" +python -c "import BaseHTTPServer, SimpleHTTPServer; \\ +SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map = {{'': '{ct}'}}; \\ +s = BaseHTTPServer.HTTPServer(('', 80), SimpleHTTPServer.SimpleHTTPRequestHandler); \\ +s.serve_forever()" """ """Non-TLS command template.""" # https://www.piware.de/2011/01/creating-an-https-server-in-python/ @@ -50,6 +55,7 @@ echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path} # run only once per server: openssl req -new -newkey rsa:4096 -subj "/" -days 1 -nodes -x509 -keyout key.pem -out cert.pem python -c "import BaseHTTPServer, SimpleHTTPServer, ssl; \\ +SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map = {{'': '{ct}'}}; \\ s = BaseHTTPServer.HTTPServer(('', 443), SimpleHTTPServer.SimpleHTTPRequestHandler); \\ s.socket = ssl.wrap_socket(s.socket, keyfile='key.pem', certfile='cert.pem'); \\ s.serve_forever()" """ @@ -99,9 +105,9 @@ binary for temporary key/certificate generation.""".replace("\n", "") assert response.good_path # is encoded os.urandom(18) good? self._notify_and_wait(self.MESSAGE_TEMPLATE.format( - achall=achall, response=response, - uri=response.uri(achall.domain), - command=self.template.format(achall=achall, response=response))) + achall=achall, response=response, uri=response.uri(achall.domain), + ct=response.CONTENT_TYPE, command=self.template.format( + achall=achall, response=response, ct=response.CONTENT_TYPE))) if acme_verify.simple_http_simple_verify( response, achall.challb, achall.domain):