[tor-commits] [stem/master] Function and testing for cookie authentication
atagar at torproject.org
atagar at torproject.org
Thu Dec 1 18:12:00 UTC 2011
commit 8fd556572a170d06359458a37848d947313984d2
Author: Damian Johnson <atagar at 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))
More information about the tor-commits
mailing list