commit 8fd556572a170d06359458a37848d947313984d2 Author: Damian Johnson atagar@torproject.org Date: Thu Dec 1 10:10:43 2011 -0800
Function and testing for cookie authentication
Adding a function for password authentication. This includes checks for the file's existance and that the size is valid (for 4303). --- stem/connection.py | 56 ++++++++++++++++++ test/integ/connection/authentication.py | 94 ++++++++++++++++++++++-------- 2 files changed, 125 insertions(+), 25 deletions(-)
diff --git a/stem/connection.py b/stem/connection.py index 3ea5747..0b654d8 100644 --- a/stem/connection.py +++ b/stem/connection.py @@ -14,7 +14,9 @@ ProtocolInfoResponse - Reply from a PROTOCOLINFO query. +- convert - parses a ControlMessage, turning it into a ProtocolInfoResponse """
+import os import logging +import binascii
import stem.socket import stem.version @@ -38,6 +40,9 @@ LOGGER = logging.getLogger("stem")
AuthMethod = stem.util.enum.Enum("NONE", "PASSWORD", "COOKIE", "UNKNOWN")
+AUTH_COOKIE_MISSING = "Authentication failed: '%s' doesn't exist" +AUTH_COOKIE_WRONG_SIZE = "Authentication failed: authentication cookie '%s' is the wrong size (%i bytes instead of 32)" + def authenticate_none(control_socket): """ Authenticates to an open control socket. All control connections need to @@ -92,6 +97,57 @@ def authenticate_password(control_socket, password): if str(auth_response) != "OK": raise ValueError(str(auth_response))
+def authenticate_cookie(control_socket, cookie_path): + """ + Authenticates to a control socket that uses the contents of an authentication + cookie (generated via the CookieAuthentication torrc option). This does basic + validation that this is a cookie before presenting the contents to the + socket. + + If authentication fails then tor will close the control socket. + + Arguments: + control_socket (stem.socket.ControlSocket) - socket to be authenticated + cookie_path (str) - path of the authentication cookie to send to tor + + Raises: + ValueError if the authentication credentials aren't accepted + OSError if the cookie file doesn't exist or we're unable to read it + stem.socket.ProtocolError the content from the socket is malformed + stem.socket.SocketError if problems arise in using the socket + """ + + if not os.path.exists(cookie_path): + raise OSError(AUTH_COOKIE_MISSING % cookie_path) + + # Abort if the file isn't 32 bytes long. This is to avoid exposing arbitrary + # file content to the port. + # + # Without this a malicious socket could, for instance, claim that + # '~/.bash_history' or '~/.ssh/id_rsa' was its authentication cookie to trick + # us into reading it for them with our current permissions. + # + # https://trac.torproject.org/projects/tor/ticket/4303 + + auth_cookie_size = os.path.getsize(cookie_path) + + if auth_cookie_size != 32: + raise ValueError(AUTH_COOKIE_WRONG_SIZE % (cookie_path, auth_cookie_size)) + + try: + auth_cookie_file = open(cookie_path, "r") + auth_cookie_contents = auth_cookie_file.read() + auth_cookie_file.close() + + control_socket.send("AUTHENTICATE %s" % binascii.b2a_hex(auth_cookie_contents)) + auth_response = control_socket.recv() + + # if we got anything but an OK response then error + if str(auth_response) != "OK": + raise ValueError(str(auth_response)) + except IOError, exc: + raise OSError(exc) + def get_protocolinfo_by_port(control_addr = "127.0.0.1", control_port = 9051, get_socket = False): """ Issues a PROTOCOLINFO query to a control port, getting information about the diff --git a/test/integ/connection/authentication.py b/test/integ/connection/authentication.py index c3759e6..7883c8a 100644 --- a/test/integ/connection/authentication.py +++ b/test/integ/connection/authentication.py @@ -3,6 +3,7 @@ Integration tests for authenticating to the control socket via stem.connection.authenticate_* functions. """
+import os import unittest import functools
@@ -25,17 +26,18 @@ class TestAuthenticate(unittest.TestCase): integ target to exercise the widest range of use cases. """
+ def setUp(self): + connection_type = test.runner.get_runner().get_connection_type() + + # none of these tests apply if there's no control connection + if connection_type == test.runner.TorConnection.NONE: + self.skipTest("(no connection)") + def test_authenticate_none(self): """ Tests the authenticate_none function. """
- runner = test.runner.get_runner() - connection_type = runner.get_connection_type() - - if connection_type == test.runner.TorConnection.NONE: - self.skipTest("(no connection)") - expect_success = self._is_authenticateable(stem.connection.AuthMethod.NONE) self._check_auth(stem.connection.AuthMethod.NONE, None, expect_success)
@@ -44,12 +46,6 @@ class TestAuthenticate(unittest.TestCase): Tests the authenticate_password function. """
- runner = test.runner.get_runner() - connection_type = runner.get_connection_type() - - if connection_type == test.runner.TorConnection.NONE: - self.skipTest("(no connection)") - expect_success = self._is_authenticateable(stem.connection.AuthMethod.PASSWORD) self._check_auth(stem.connection.AuthMethod.PASSWORD, test.runner.CONTROL_PASSWORD, expect_success)
@@ -61,6 +57,42 @@ class TestAuthenticate(unittest.TestCase): self._check_auth(stem.connection.AuthMethod.PASSWORD, "blarg", expect_success) self._check_auth(stem.connection.AuthMethod.PASSWORD, "this has a " in it", expect_success)
+ def test_authenticate_cookie(self): + """ + Tests the authenticate_cookie function. + """ + + test_path = test.runner.get_runner().get_auth_cookie_path() + expect_success = self._is_authenticateable(stem.connection.AuthMethod.COOKIE) + self._check_auth(stem.connection.AuthMethod.COOKIE, test_path, expect_success) + + def test_authenticate_cookie_missing(self): + """ + Tests the authenticate_cookie function with a path that really, really + shouldn't exist. + """ + + test_path = "/if/this/exists/then/they're/asking/for/a/failure" + expected_exc = OSError(stem.connection.AUTH_COOKIE_MISSING % test_path) + self._check_auth(stem.connection.AuthMethod.COOKIE, test_path, False, expected_exc) + + def test_authenticate_cookie_wrong_size(self): + """ + Tests the authenticate_cookie function with our torrc as an auth cookie. + This is to confirm that we won't read arbitrary files to the control + socket. + """ + + test_path = test.runner.get_runner().get_torrc_path() + auth_cookie_size = os.path.getsize(test_path) + + if auth_cookie_size == 32: + # Weird coincidence? Fail so we can pick another file to check against. + self.fail("Our torrc is 32 bytes, preventing the test_authenticate_cookie_wrong_size test from running.") + else: + expected_exc = ValueError(stem.connection.AUTH_COOKIE_WRONG_SIZE % (test_path, auth_cookie_size)) + self._check_auth(stem.connection.AuthMethod.COOKIE, test_path, False, expected_exc) + def _get_socket_auth(self): """ Provides the types of authentication that our current test socket accepts. @@ -99,7 +131,7 @@ class TestAuthenticate(unittest.TestCase): elif auth_type == stem.connection.AuthMethod.COOKIE: return cookie_auth else: return False
- def _check_auth(self, auth_type, auth_value, expect_success): + def _check_auth(self, auth_type, auth_value, expect_success, failure_exc = None): """ Attempts to use the given authentication function against our connection. If this works then checks that we can use the connection. If not then we @@ -111,6 +143,8 @@ class TestAuthenticate(unittest.TestCase): auth_value (str) - value to be provided to the authentication function expect_success (bool) - true if the authentication should succeed, false otherwise + failure_exc (Exception) - exception that we want to assert is raised, if + None then we'll check for an auth mismatch error """
runner = test.runner.get_runner() @@ -124,7 +158,7 @@ class TestAuthenticate(unittest.TestCase): elif auth_type == stem.connection.AuthMethod.PASSWORD: auth_function = stem.connection.authenticate_password elif auth_type == stem.connection.AuthMethod.COOKIE: - auth_function = None # TODO: fill in + auth_function = stem.connection.authenticate_cookie else: raise ValueError("unexpected auth type: %s" % auth_type)
@@ -143,20 +177,30 @@ class TestAuthenticate(unittest.TestCase): self.assertEquals("config-file=%s\nOK" % runner.get_torrc_path(), str(config_file_response)) control_socket.close() else: - if cookie_auth and password_auth: failure_msg = MULTIPLE_AUTH_FAIL - elif cookie_auth: failure_msg = COOKIE_AUTH_FAIL - else: - # if we're attempting to authenticate with a password then it's a - # truncated message - - if auth_type == stem.connection.AuthMethod.PASSWORD: - failure_msg = INCORRECT_PASSWORD_FAIL + # if unset then determine what the general authentication error should + # look like + + if not failure_exc: + if cookie_auth and password_auth: + failure_exc = ValueError(MULTIPLE_AUTH_FAIL) + elif cookie_auth: + failure_exc = ValueError(COOKIE_AUTH_FAIL) else: - failure_msg = PASSWORD_AUTH_FAIL + # if we're attempting to authenticate with a password then it's a + # truncated message + + if auth_type == stem.connection.AuthMethod.PASSWORD: + failure_exc = ValueError(INCORRECT_PASSWORD_FAIL) + else: + failure_exc = ValueError(PASSWORD_AUTH_FAIL)
try: auth_function() self.fail() - except ValueError, exc: - self.assertEqual(failure_msg, str(exc)) + except Exception, exc: + # we can't check exception equality directly because it contains other + # attributes which will fail + + self.assertEqual(type(failure_exc), type(exc)) + self.assertEqual(str(failure_exc), str(exc))
tor-commits@lists.torproject.org