[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