[tor-commits] [stem/master] Implement Safecookie support in Stem
atagar at torproject.org
atagar at torproject.org
Sun Jun 10 01:27:16 UTC 2012
commit 682cbb4a2b36b0d226ea2a7e8d9bb527fdb2e28e
Author: Ravi Chandra Padmala <neenaoffline at gmail.com>
Date: Tue May 8 01:05:20 2012 +0530
Implement Safecookie support in Stem
---
run_tests.py | 2 +
stem/connection.py | 283 ++++++++++++++++++++++++++----
stem/response/__init__.py | 21 +++-
stem/response/authchallenge.py | 67 ++++++++
stem/response/protocolinfo.py | 6 +-
stem/util/connection.py | 45 +++++
test/integ/connection/authentication.py | 151 ++++++++++++-----
test/integ/response/protocolinfo.py | 1 +
test/unit/connection/authentication.py | 85 ++++++----
test/unit/response/__init__.py | 2 +-
test/unit/response/authchallenge.py | 57 ++++++
11 files changed, 601 insertions(+), 119 deletions(-)
diff --git a/run_tests.py b/run_tests.py
index 22adce3..6a6aa09 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -23,6 +23,7 @@ import test.unit.response.control_line
import test.unit.response.control_message
import test.unit.response.getinfo
import test.unit.response.protocolinfo
+import test.unit.response.authchallenge
import test.unit.util.conf
import test.unit.util.connection
import test.unit.util.enum
@@ -104,6 +105,7 @@ UNIT_TESTS = (
test.unit.response.control_line.TestControlLine,
test.unit.response.getinfo.TestGetInfoResponse,
test.unit.response.protocolinfo.TestProtocolInfoResponse,
+ test.unit.response.authchallenge.TestAuthChallengeResponse,
test.unit.connection.authentication.TestAuthenticate,
)
diff --git a/stem/connection.py b/stem/connection.py
index ce83068..8234441 100644
--- a/stem/connection.py
+++ b/stem/connection.py
@@ -71,7 +71,11 @@ the authentication process. For instance...
| |- CookieAuthRejected - Tor rejected this method of authentication.
| |- IncorrectCookieValue - Authentication cookie was rejected.
| |- IncorrectCookieSize - Size of the cookie file is incorrect.
- | +- UnreadableCookieFile - Unable to read the contents of the auth cookie.
+ | |- UnreadableCookieFile - Unable to read the contents of the auth cookie.
+ | +- AuthChallengeFailed - Failure completing the authchallenge request
+ | |- AuthSecurityFailure - The computer/network may be compromised.
+ | |- InvalidClientNonce - The client nonce is invalid.
+ | +- UnrecognizedAuthChallengeMethod - AUTHCHALLENGE does not support the given methods.
|
+- MissingAuthInfo - Unexpected PROTOCOLINFO response, missing auth info.
|- NoAuthMethods - Missing any methods for authenticating.
@@ -79,6 +83,7 @@ the authentication process. For instance...
"""
import os
+import re
import getpass
import binascii
@@ -88,10 +93,11 @@ import stem.control
import stem.version
import stem.util.enum
import stem.util.system
+import stem.util.connection
import stem.util.log as log
from stem.response.protocolinfo import AuthMethod
-def connect_port(control_addr = "127.0.0.1", control_port = 9051, password = None, chroot_path = None, controller = stem.control.Controller):
+def connect_port(control_addr = "127.0.0.1", control_port = 9051, password = None, chroot_path = None, controller = None):
"""
Convenience function for quickly getting a control connection. This is very
handy for debugging or CLI setup, handling setup and prompting for a password
@@ -224,8 +230,28 @@ def authenticate(controller, password = None, chroot_path = None, protocolinfo_r
Tor allows for authentication by reading it a cookie file, but rejected
the contents of that file.
- * **\***:class:`stem.connection.OpenAuthRejected`
+ * **\***:class:`stem.connection.UnrecognizedAuthChallengeMethod`
+
+ Tor couldn't recognize the AUTHCHALLENGE method Stem sent to it. This
+ shouldn't happen at all.
+
+ * **\***:class:`stem.connection.InvalidClientNonce`
+
+ Tor says that the client nonce provided by Stem during the AUTHCHALLENGE
+ process is invalid.
+
+ * **\***:class:`stem.connection.AuthSecurityFailure`
+
+ Raised when self is a possibility self security having been compromised.
+
+ * **\***:class:`stem.connection.AuthChallengeFailed`
+
+ The AUTHCHALLENGE command has failed (probably because Stem is connecting
+ to an old version of Tor which doesn't support Safe cookie authentication,
+ could also be because of other reasons).
+ * **\***:class:`stem.connection.OpenAuthRejected`
+
Tor says that it allows for authentication without any credentials, but
then rejected our authentication attempt.
@@ -279,16 +305,22 @@ def authenticate(controller, password = None, chroot_path = None, protocolinfo_r
else:
log.debug("Authenticating to a socket with unrecognized auth method%s, ignoring them: %s" % (plural_label, methods_label))
- if AuthMethod.COOKIE in auth_methods and protocolinfo_response.cookie_path is None:
- auth_methods.remove(AuthMethod.COOKIE)
- auth_exceptions.append(NoAuthCookie("our PROTOCOLINFO response did not have the location of our authentication cookie"))
+ if protocolinfo_response.cookie_path is None:
+ for cookie_auth_method in (AuthMethod.COOKIE, AuthMethod.SAFECOOKIE):
+ if cookie_auth_method in auth_methods:
+ try:
+ auth_methods.remove(AuthMethod.COOKIE)
+ except ValueError:
+ pass
+ auth_exceptions.append(NoAuthCookie("our PROTOCOLINFO response did not have the location of our authentication cookie"))
if AuthMethod.PASSWORD in auth_methods and password is None:
auth_methods.remove(AuthMethod.PASSWORD)
auth_exceptions.append(MissingPassword("no passphrase provided"))
# iterating over AuthMethods so we can try them in this order
- for auth_type in (AuthMethod.NONE, AuthMethod.PASSWORD, AuthMethod.COOKIE):
+ for auth_type in (AuthMethod.NONE, AuthMethod.PASSWORD, AuthMethod.SAFECOOKIE,
+ AuthMethod.COOKIE):
if not auth_type in auth_methods: continue
try:
@@ -296,13 +328,16 @@ def authenticate(controller, password = None, chroot_path = None, protocolinfo_r
authenticate_none(controller, False)
elif auth_type == AuthMethod.PASSWORD:
authenticate_password(controller, password, False)
- elif auth_type == AuthMethod.COOKIE:
+ elif auth_type == AuthMethod.COOKIE or auth_type == AuthMethod.SAFECOOKIE:
cookie_path = protocolinfo_response.cookie_path
if chroot_path:
cookie_path = os.path.join(chroot_path, cookie_path.lstrip(os.path.sep))
- authenticate_cookie(controller, cookie_path, False)
+ if auth_type == AuthMethod.SAFECOOKIE:
+ authenticate_safecookie(controller, cookie_path, False)
+ else:
+ authenticate_cookie(controller, cookie_path, False)
return # success!
except OpenAuthRejected, exc:
@@ -314,10 +349,19 @@ def authenticate(controller, password = None, chroot_path = None, protocolinfo_r
# that if PasswordAuthRejected is raised it's being raised in error.
log.debug("The authenticate_password method raised a PasswordAuthRejected when password auth should be available. Stem may need to be corrected to recognize this response: %s" % exc)
auth_exceptions.append(IncorrectPassword(str(exc)))
+ except AuthSecurityFailure, exc:
+ log.info("The authenticate_safecookie method raised an AuthSecurityFailure. Security might have been compromised - attack? (%s)" % exc)
+ auth_exceptions.append(exc)
+ except (InvalidClientNonce, UnrecognizedAuthChallengeMethod,
+ AuthChallengeFailed), exc:
+ auth_exceptions.append(exc)
except (IncorrectCookieSize, UnreadableCookieFile, IncorrectCookieValue), exc:
auth_exceptions.append(exc)
except CookieAuthRejected, exc:
- log.debug("The authenticate_cookie method raised a CookieAuthRejected when cookie auth should be available. Stem may need to be corrected to recognize this response: %s" % exc)
+ auth_func = "authenticate_cookie"
+ if exc.auth_type == AuthMethod.SAFECOOKIE:
+ auth_func = "authenticate_safecookie"
+ log.debug("The %s method raised a CookieAuthRejected when cookie auth should be available. Stem may need to be corrected to recognize this response: %s" % (auth_func, exc))
auth_exceptions.append(IncorrectCookieValue(str(exc), exc.cookie_path))
except stem.socket.ControllerError, exc:
auth_exceptions.append(AuthenticationFailure(str(exc)))
@@ -429,6 +473,47 @@ def authenticate_password(controller, password, suppress_ctl_errors = True):
if not suppress_ctl_errors: raise exc
else: raise PasswordAuthRejected("Socket failed (%s)" % exc)
+def _read_cookie(cookie_path, auth_type):
+ """
+ Provides the contents of a given cookie file. If unable to do so this raises
+ an exception of a given type.
+
+ :param str cookie_path: absolute path of the cookie file
+ :param str auth_type: cookie authentication type (from the AuthMethod Enum)
+
+ :raises:
+ * :class:`stem.connection.UnreadableCookieFile` if the cookie file is unreadable
+ * :class:`stem.connection.IncorrectCookieSize` if the cookie size is incorrect (not 32 bytes)
+ """
+
+ if not os.path.exists(cookie_path):
+ raise UnreadableCookieFile("Authentication failed: '%s' doesn't exist" %
+ cookie_path, cookie_path, auth_type = auth_type)
+
+ # 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:
+ exc_msg = "Authentication failed: authentication cookie '%s' is the wrong \
+size (%i bytes instead of 32)" % (cookie_path, auth_cookie_size)
+ raise IncorrectCookieSize(exc_msg, cookie_path, auth_type = auth_type)
+
+ try:
+ with file(cookie_path, 'rb', 0) as f:
+ cookie_data = f.read()
+ return cookie_data
+ except IOError, exc:
+ raise UnreadableCookieFile("Authentication failed: unable to read '%s' (%s)"
+ % (cookie_path, exc), cookie_path, auth_type = auth_type)
+
def authenticate_cookie(controller, cookie_path, suppress_ctl_errors = True):
"""
Authenticates to a control socket that uses the contents of an authentication
@@ -464,33 +549,10 @@ def authenticate_cookie(controller, cookie_path, suppress_ctl_errors = True):
* :class:`stem.connection.IncorrectCookieValue` if the cookie file's value is rejected
"""
- if not os.path.exists(cookie_path):
- raise UnreadableCookieFile("Authentication failed: '%s' doesn't exist" % cookie_path, 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:
- exc_msg = "Authentication failed: authentication cookie '%s' is the wrong size (%i bytes instead of 32)" % (cookie_path, auth_cookie_size)
- raise IncorrectCookieSize(exc_msg, cookie_path)
-
- try:
- auth_cookie_file = open(cookie_path, "r")
- auth_cookie_contents = auth_cookie_file.read()
- auth_cookie_file.close()
- except IOError, exc:
- raise UnreadableCookieFile("Authentication failed: unable to read '%s' (%s)" % (cookie_path, exc), cookie_path)
+ cookie_data = _read_cookie(cookie_path, AuthMethod.COOKIE)
try:
- msg = "AUTHENTICATE %s" % binascii.b2a_hex(auth_cookie_contents)
+ msg = "AUTHENTICATE %s" % binascii.b2a_hex(cookie_data)
auth_response = _msg(controller, msg)
# if we got anything but an OK response then error
@@ -514,6 +576,111 @@ def authenticate_cookie(controller, cookie_path, suppress_ctl_errors = True):
if not suppress_ctl_errors: raise exc
else: raise CookieAuthRejected("Socket failed (%s)" % exc, cookie_path)
+def authenticate_safecookie(controller, cookie_path, suppress_ctl_errors = True):
+ """
+ Authenticates to a control socket using the safe cookie method, which is
+ enabled by setting the CookieAuthentication torrc option on Tor client's which
+ support it. This uses a two-step process - first, it sends a nonce to the
+ server and receives a challenge from the server of the cookie's contents.
+ Next, it generates a hash digest using the challenge received in the first
+ step and uses it to authenticate to 'controller'.
+
+ The IncorrectCookieSize and UnreadableCookieFile exceptions take
+ precedence over the other exception types.
+
+ The UnrecognizedAuthChallengeMethod, AuthChallengeFailed, InvalidClientNonce
+ and CookieAuthRejected exceptions are next in the order of precedence.
+ Depending on the reason, one of these is raised if the first (AUTHCHALLENGE) step
+ fails.
+
+ In the second (AUTHENTICATE) step, IncorrectCookieValue or
+ CookieAuthRejected maybe raised.
+
+ If authentication fails tor will disconnect and we'll make a best effort
+ attempt to re-establish the connection. This may not succeed, so check
+ is_alive() before using the socket further.
+
+ For general usage use the authenticate() function instead.
+
+ :param controller: tor controller or socket to be authenticated
+ :param str cookie_path: path of the authentication cookie to send to tor
+ :param bool suppress_ctl_errors: reports raised :class:`stem.socket.ControllerError` as authentication rejection if True, otherwise they're re-raised
+
+ :raises:
+ * :class:`stem.connection.IncorrectCookieSize` if the cookie file's size is wrong
+ * :class:`stem.connection.UnreadableCookieFile` if the cookie file doesn't exist or we're unable to read it
+ * :class:`stem.connection.CookieAuthRejected` if cookie authentication is attempted but the socket doesn't accept it
+ * :class:`stem.connection.IncorrectCookieValue` if the cookie file's value is rejected
+ * :class:`stem.connection.UnrecognizedAuthChallengeMethod` if the Tor client fails to recognize the AuthChallenge method
+ * :class:`stem.connection.AuthChallengeFailed` if AUTHCHALLENGE is unimplemented, or if unable to parse AUTHCHALLENGE response
+ * :class:`stem.connection.AuthSecurityFailure` if AUTHCHALLENGE's response looks like a security attack
+ * :class:`stem.connection.InvalidClientNonce` if stem's AUTHCHALLENGE client nonce is rejected for being invalid
+ """
+
+ cookie_data = _read_cookie(cookie_path, AuthMethod.SAFECOOKIE)
+ client_nonce = stem.util.connection.random_bytes(32)
+ client_hash_const = "Tor safe cookie authentication controller-to-server hash"
+ server_hash_const = "Tor safe cookie authentication server-to-controller hash"
+
+ try:
+ challenge_response = _msg(controller, "AUTHCHALLENGE SAFECOOKIE %s" %
+ binascii.b2a_hex(client_nonce))
+
+ if not challenge_response.is_ok():
+ try: controller.connect()
+ except: pass
+
+ challenge_response_str = str(challenge_response)
+ if "AUTHCHALLENGE only supports" in challenge_response_str:
+ raise UnrecognizedAuthChallengeMethod(challenge_response_str, cookie_path, AuthMethod.SAFECOOKIE)
+ elif "Invalid base16 client nonce" in challenge_response_str:
+ raise InvalidClientNonce(challenge_response_str, cookie_path)
+ elif "Authentication required." in challenge_response_str:
+ raise AuthChallengeFailed("SAFECOOKIE Authentication unimplemented", cookie_path)
+ elif "Cookie authentication is disabled." in challenge_response_str:
+ raise CookieAuthRejected(challenge_response_str, cookie_path, auth_type = AuthMethod.SAFECOOKIE)
+ else:
+ raise AuthChallengeFailed(challenge_response, cookie_path)
+ except stem.socket.ControllerError, exc:
+ try: controller.connect()
+ except: pass
+
+ if not suppress_ctl_errors: raise exc
+ else: raise AuthChallengeFailed("Socket failed (%s)" % exc, cookie_path)
+
+ try:
+ stem.response.convert("AUTHCHALLENGE", challenge_response)
+ except stem.socket.ProtocolError, exc:
+ if not suppress_ctl_errors: raise exc
+ else: raise AuthChallengeFailed("Unable to parse AUTHCHALLENGE response: %s" % exc, cookie_path)
+
+ expected_server_hash = stem.util.connection.hmac_sha256(server_hash_const,
+ cookie_data + client_nonce + challenge_response.server_nonce)
+ if not stem.util.connection.cryptovariables_equal(challenge_response.server_hash, expected_server_hash):
+ raise AuthSecurityFailure("Server hash is wrong -- attack?", cookie_path)
+
+ try:
+ client_hash = stem.util.connection.hmac_sha256(client_hash_const,
+ cookie_data + client_nonce + challenge_response.server_nonce)
+ controller.send("AUTHENTICATE %s" % (binascii.b2a_hex(client_hash)))
+ auth_response = controller.recv()
+ except stem.socket.ControllerError, exc:
+ try: controller.connect()
+ except: pass
+
+ if not suppress_ctl_errors: raise exc
+ else: raise CookieAuthRejected("Socket failed (%s)" % exc, cookie_path, auth_response, AuthMethod.SAFECOOKIE)
+
+ # if we got anything but an OK response then err
+ if not auth_response.is_ok():
+ try: controller.connect()
+ except: pass
+
+ if 'Safe cookie response did not match expected value' in auth_response[0]: #Cookie doesn't match
+ raise IncorrectCookieValue(str(auth_response), cookie_path, auth_response, AuthMethod.SAFECOOKIE)
+ else:
+ raise CookieAuthRejected(str(auth_response), cookie_path, auth_response, AuthMethod.SAFECOOKIE)
+
def get_protocolinfo(controller):
"""
Issues a PROTOCOLINFO query to a control socket, getting information about
@@ -665,11 +832,15 @@ class CookieAuthFailed(AuthenticationFailure):
Failure to authenticate with an authentication cookie.
:param str cookie_path: location of the authentication cookie we attempted
+ :param str auth_type: cookie authentication type (from the AuthMethod Enum)
"""
- def __init__(self, message, cookie_path, auth_response = None):
+ def __init__(self, message, cookie_path, auth_response = None, auth_type = AuthMethod.COOKIE):
+ """
+ """
AuthenticationFailure.__init__(self, message, auth_response)
self.cookie_path = cookie_path
+ self.auth_type = auth_type
class CookieAuthRejected(CookieAuthFailed):
"Socket does not support password authentication."
@@ -683,6 +854,39 @@ class IncorrectCookieSize(CookieAuthFailed):
class UnreadableCookieFile(CookieAuthFailed):
"Error arose in reading the authentication cookie."
+class AuthChallengeFailed(CookieAuthFailed):
+ """
+ AUTHCHALLENGE command has failed.
+
+ :param str cookie_path: path to the cookie file
+ :param str auth_type: cookie authentication type (from the AuthMethod Enum)
+ """
+
+ def __init__(self, message, cookie_path, auth_type = AuthMethod.SAFECOOKIE):
+ CookieAuthFailed.__init__(self, message, cookie_path)
+ self.auth_type = auth_type
+
+
+class UnrecognizedAuthChallengeMethod(AuthChallengeFailed):
+ """
+ Tor couldn't recognize our AUTHCHALLENGE method.
+
+ :var str authchallenge_method: AUTHCHALLENGE method that Tor couldn't recognize
+ :var str cookie_path: path to the cookie file
+ :var str auth_type: cookie authentication type (from the AuthMethod Enum)
+ """
+
+ def __init__(self, message, cookie_path, authchallenge_method, auth_type = AuthMethod.SAFECOOKIE):
+ CookieAuthFailed.__init__(self, message, cookie_path)
+ self.authchallenge_method = authchallenge_method
+ self.auth_type = auth_type
+
+class AuthSecurityFailure(AuthChallengeFailed):
+ "AUTHCHALLENGE response is invalid."
+
+class InvalidClientNonce(AuthChallengeFailed):
+ "AUTHCHALLENGE request contains an invalid client nonce."
+
class MissingAuthInfo(AuthenticationFailure):
"""
The PROTOCOLINFO response didn't have enough information to authenticate.
@@ -704,8 +908,11 @@ AUTHENTICATE_EXCEPTIONS = (
IncorrectCookieSize,
UnreadableCookieFile,
IncorrectCookieValue,
+ UnrecognizedAuthChallengeMethod,
+ InvalidClientNonce,
+ AuthSecurityFailure,
+ AuthChallengeFailed,
OpenAuthRejected,
MissingAuthInfo,
- AuthenticationFailure,
+ AuthenticationFailure
)
-
diff --git a/stem/response/__init__.py b/stem/response/__init__.py
index 823cefa..788658b 100644
--- a/stem/response/__init__.py
+++ b/stem/response/__init__.py
@@ -23,7 +23,7 @@ Parses replies from the control socket.
+- pop_mapping - removes and returns the next entry as a KEY=VALUE mapping
"""
-__all__ = ["getinfo", "protocolinfo", "convert", "ControlMessage", "ControlLine"]
+__all__ = ["getinfo", "protocolinfo", "authchallenge", "convert", "ControlMessage", "ControlLine"]
import re
import threading
@@ -48,6 +48,7 @@ def convert(response_type, message):
* GETINFO
* PROTOCOLINFO
+ * AUTHCHALLENGE
If the response_type isn't recognized then this is leaves it alone.
@@ -61,6 +62,7 @@ def convert(response_type, message):
import stem.response.getinfo
import stem.response.protocolinfo
+ import stem.response.authchallenge
if not isinstance(message, ControlMessage):
raise TypeError("Only able to convert stem.response.ControlMessage instances")
@@ -69,6 +71,8 @@ def convert(response_type, message):
response_class = stem.response.getinfo.GetInfoResponse
elif response_type == "PROTOCOLINFO":
response_class = stem.response.protocolinfo.ProtocolInfoResponse
+ elif response_type == "AUTHCHALLENGE":
+ response_class = stem.response.authchallenge.AuthChallengeResponse
else: raise TypeError("Unsupported response type: %s" % response_type)
message.__class__ = response_class
@@ -165,6 +169,20 @@ class ControlMessage:
for _, _, content in self._parsed_content:
yield ControlLine(content)
+
+ def __len__(self):
+ """
+ :returns: Number of ControlLines
+ """
+
+ return len(self._parsed_content)
+
+ def __getitem__(self, index):
+ """
+ :returns: ControlLine at index
+ """
+
+ return ControlLine(self._parsed_content[index][2])
class ControlLine(str):
"""
@@ -302,6 +320,7 @@ class ControlLine(str):
:returns: tuple of the form (key, value)
:raises: ValueError if this isn't a KEY=VALUE mapping or if quoted is True without the value being quoted
+ :raises: IndexError if there's nothing to parse from the line
"""
with self._remainder_lock:
diff --git a/stem/response/authchallenge.py b/stem/response/authchallenge.py
new file mode 100644
index 0000000..61c9ae8
--- /dev/null
+++ b/stem/response/authchallenge.py
@@ -0,0 +1,67 @@
+
+import re
+import binascii
+
+import stem.socket
+import stem.response
+
+class AuthChallengeResponse(stem.response.ControlMessage):
+ """
+ AUTHCHALLENGE query response.
+
+ :var str server_hash: server hash returned by Tor
+ :var str server_nonce: server nonce returned by Tor
+ """
+
+ def _parse_message(self):
+ # Example:
+ # 250 AUTHCHALLENGE SERVERHASH=680A73C9836C4F557314EA1C4EDE54C285DB9DC89C83627401AEF9D7D27A95D5 SERVERNONCE=F8EA4B1F2C8B40EF1AF68860171605B910E3BBCABADF6FC3DB1FA064F4690E85
+
+ _ProtocolError = stem.socket.ProtocolError
+
+ try:
+ line = self[0]
+ except IndexError:
+ raise _ProtocolError("Received empty AUTHCHALLENGE response")
+
+ # sanity check that we're a AUTHCHALLENGE response
+ if not line.pop() == "AUTHCHALLENGE":
+ raise _ProtocolError("Message is not an AUTHCHALLENGE response (%s)" % self)
+
+ if len(self) > 1:
+ raise _ProtocolError("Received multiline AUTHCHALLENGE response (%s)" % line)
+
+ self.server_hash, self.server_nonce = None, None
+
+ try:
+ key, value = line.pop_mapping()
+ except (IndexError, ValueError), exc:
+ raise _ProtocolError(exc.message)
+ if key == "SERVERHASH":
+ if not re.match("^[A-Fa-f0-9]{64}$", value):
+ raise _ProtocolError("SERVERHASH has an invalid value: %s" % value)
+
+ self.server_hash = binascii.a2b_hex(value)
+
+ try:
+ key, value = line.pop_mapping()
+ except (IndexError, ValueError), exc:
+ raise _ProtocolError(exc.message)
+ if key == "SERVERNONCE":
+ if not re.match("^[A-Fa-f0-9]{64}$", value):
+ raise _ProtocolError("SERVERNONCE has an invalid value: %s" % value)
+
+ self.server_nonce = binascii.a2b_hex(value)
+
+ msg = ""
+ if not self.server_hash:
+ msg.append("SERVERHASH")
+ if not self.server_nonce:
+ msg.append("and SERVERNONCE")
+ else:
+ if not self.server_nonce:
+ msg.append("SERVERNONCE")
+
+ if msg:
+ raise _ProtocolError("AUTHCHALLENGE response is missing %s." % msg)
+
diff --git a/stem/response/protocolinfo.py b/stem/response/protocolinfo.py
index 0a9d94d..23bef69 100644
--- a/stem/response/protocolinfo.py
+++ b/stem/response/protocolinfo.py
@@ -16,6 +16,10 @@ methods it will accept in response to PROTOCOLINFO queries.
See tor's CookieAuthentication option. Controllers need to supply the
contents of the cookie file.
+**AuthMethod.SAFECOOKIE**
+ See tor's CookieAuthentication option. Controllers need to reply to a
+ hmac challenge using the contents of the cookie file.
+
**AuthMethod.UNKNOWN**
Tor provided one or more authentication methods that we don't recognize. This
is probably from a new addition to the control protocol.
@@ -27,7 +31,7 @@ import stem.version
import stem.util.enum
import stem.util.log as log
-AuthMethod = stem.util.enum.Enum("NONE", "PASSWORD", "COOKIE", "UNKNOWN")
+AuthMethod = stem.util.enum.Enum("NONE", "PASSWORD", "COOKIE", "SAFECOOKIE", "UNKNOWN")
class ProtocolInfoResponse(stem.response.ControlMessage):
"""
diff --git a/stem/util/connection.py b/stem/util/connection.py
index cbf9856..ed339c9 100644
--- a/stem/util/connection.py
+++ b/stem/util/connection.py
@@ -5,7 +5,11 @@ later to have all of `arm's functions
but for now just moving the parts we need.
"""
+import os
import re
+import hmac
+import random
+import hashlib
def is_valid_ip_address(address):
"""
@@ -78,3 +82,44 @@ def is_valid_port(entry, allow_zero = False):
return entry > 0 and entry < 65536
+
+def hmac_sha256(key, msg):
+ """
+ Generates a sha256 digest using the given key and message.
+
+ :param str key: starting key for the hash
+ :param str msg: message to be hashed
+
+ :returns; A sha256 digest of msg, hashed using the given key.
+ """
+
+ return hmac.new(key, msg, hashlib.sha256).digest()
+
+def random_bytes(length):
+ """
+ Generates and returns a 'length' byte random string.
+
+ :param int length: length of random string to be returned in bytes.
+
+ :returns: A string of length 'length' bytes.
+ """
+
+ return os.urandom(length)
+
+CRYPTOVARIABLE_EQUALITY_COMPARISON_NONCE = random_bytes(32)
+def cryptovariables_equal(x, y):
+ """
+ Compares two strings for equality securely.
+
+ :param str x: string to be compared.
+ :param str y: the other string to be compared.
+
+ :returns: True if both strings are equal, False otherwise.
+ """
+
+ ## Like all too-high-level languages, Python sucks for secure coding.
+ ## I'm not even going to try to compare strings in constant time.
+ ## Fortunately, I have HMAC and a random number generator. -- rransom
+ return (hmac_sha256(CRYPTOVARIABLE_EQUALITY_COMPARISON_NONCE, x) ==
+ hmac_sha256(CRYPTOVARIABLE_EQUALITY_COMPARISON_NONCE, y))
+
diff --git a/test/integ/connection/authentication.py b/test/integ/connection/authentication.py
index 5875a80..aa7d6c1 100644
--- a/test/integ/connection/authentication.py
+++ b/test/integ/connection/authentication.py
@@ -9,17 +9,22 @@ import unittest
import test.runner
import stem.connection
import stem.socket
+from stem.version import Version
+from stem.response.protocolinfo import AuthMethod
# Responses given by tor for various authentication failures. These may change
# in the future and if they do then this test should be updated.
COOKIE_AUTH_FAIL = "Authentication failed: Wrong length on authentication cookie."
+SAFECOOKIE_AUTH_FAIL = "Authentication failed: Wrong length for safe cookie response."
PASSWORD_AUTH_FAIL = "Authentication failed: Password did not match HashedControlPassword value from configuration. Maybe you tried a plain text password? If so, the standard requires that you put it in double quotes."
MULTIPLE_AUTH_FAIL = "Authentication failed: Password did not match HashedControlPassword *or* authentication cookie."
+SAFECOOKIE_AUTHCHALLENGE_FAIL = "Cookie authentication is disabled"
# this only arises in cookie-only or password-only auth when we authenticate
# with the wrong value
INCORRECT_COOKIE_FAIL = "Authentication failed: Authentication cookie did not match expected value."
+INCORRECT_SAFECOOKIE_FAIL = "Authentication failed: Safe cookie response did not match expected value."
INCORRECT_PASSWORD_FAIL = "Authentication failed: Password did not match HashedControlPassword value from configuration"
def _can_authenticate(auth_type):
@@ -36,11 +41,12 @@ def _can_authenticate(auth_type):
tor_options = test.runner.get_runner().get_options()
password_auth = test.runner.Torrc.PASSWORD in tor_options
- cookie_auth = test.runner.Torrc.COOKIE in tor_options
+ safecookie_auth = cookie_auth = test.runner.Torrc.COOKIE in tor_options
if not password_auth and not cookie_auth: return True # open socket
elif auth_type == stem.connection.AuthMethod.PASSWORD: return password_auth
elif auth_type == stem.connection.AuthMethod.COOKIE: return cookie_auth
+ elif auth_type == stem.connection.AuthMethod.SAFECOOKIE: return safecookie_auth
else: return False
def _get_auth_failure_message(auth_type):
@@ -58,27 +64,37 @@ def _get_auth_failure_message(auth_type):
tor_options = test.runner.get_runner().get_options()
password_auth = test.runner.Torrc.PASSWORD in tor_options
- cookie_auth = test.runner.Torrc.COOKIE in tor_options
+ safecookie_auth = cookie_auth = test.runner.Torrc.COOKIE in tor_options
if cookie_auth and password_auth:
return MULTIPLE_AUTH_FAIL
elif cookie_auth:
if auth_type == stem.connection.AuthMethod.COOKIE:
- return INCORRECT_COOKIE_FAIL
+ return INCORRECT_COOKIE_FAIL
+ elif auth_type == stem.connection.AuthMethod.SAFECOOKIE:
+ return INCORRECT_SAFECOOKIE_FAIL
else:
- return COOKIE_AUTH_FAIL
+ return COOKIE_AUTH_FAIL
elif password_auth:
if auth_type == stem.connection.AuthMethod.PASSWORD:
return INCORRECT_PASSWORD_FAIL
else:
return PASSWORD_AUTH_FAIL
else:
- # shouldn't happen, if so then the test has a bug
- raise ValueError("No methods of authentication. If this is an open socket then auth shoulnd't fail.")
+ # shouldn't happen unless safecookie, if so then the test has a bug
+ if auth_type == stem.connection.AuthMethod.SAFECOOKIE:
+ return SAFECOOKIE_AUTHCHALLENGE_FAIL
+ raise ValueError("No methods of authentication. If this is an open socket then auth shouldn't fail.")
class TestAuthenticate(unittest.TestCase):
def setUp(self):
test.runner.require_control(self)
+ self.cookie_auth_methods = [AuthMethod.COOKIE]
+
+ tor_version = test.runner.get_runner().get_tor_version()
+ if tor_version >= Version("0.2.2.36") and tor_version < Version("0.2.3.0") \
+ or tor_version >= Version("0.2.3.13-alpha"):
+ self.cookie_auth_methods.append(AuthMethod.SAFECOOKIE)
def test_authenticate_general_socket(self):
"""
@@ -169,6 +185,23 @@ class TestAuthenticate(unittest.TestCase):
stem.connection.authenticate(control_socket, test.runner.CONTROL_PASSWORD, runner.get_chroot())
test.runner.exercise_controller(self, control_socket)
+ def test_authenticate_general_cookie(self):
+ """
+ Tests the authenticate function's password argument.
+ """
+
+ runner = test.runner.get_runner()
+ tor_options = runner.get_options()
+ is_cookie_only = test.runner.Torrc.COOKIE in tor_options and not test.runner.Torrc.PASSWORD in tor_options
+
+ # test both cookie authentication mechanisms
+ with runner.get_tor_socket(False) as control_socket:
+ if is_cookie_only:
+ for method in self.cookie_auth_methods:
+ protocolinfo_response = stem.connection.get_protocolinfo(control_socket)
+ protocolinfo_response.auth_methods.remove(method)
+ stem.connection.authenticate(control_socket, chroot_path = runner.get_chroot(), protocolinfo_response = protocolinfo_response)
+
def test_authenticate_none(self):
"""
Tests the authenticate_none function.
@@ -213,21 +246,21 @@ class TestAuthenticate(unittest.TestCase):
Tests the authenticate_cookie function.
"""
- auth_type = stem.connection.AuthMethod.COOKIE
- auth_value = test.runner.get_runner().get_auth_cookie_path()
-
- if not os.path.exists(auth_value):
- # If the authentication cookie doesn't exist then we'll be getting an
- # error for that rather than rejection. This will even fail if
- # _can_authenticate is true because we *can* authenticate with cookie
- # auth but the function will short circuit with failure due to the
- # missing file.
+ for auth_type in self.cookie_auth_methods:
+ auth_value = test.runner.get_runner().get_auth_cookie_path()
- self.assertRaises(stem.connection.UnreadableCookieFile, self._check_auth, auth_type, auth_value, False)
- elif _can_authenticate(auth_type):
- self._check_auth(auth_type, auth_value)
- else:
- self.assertRaises(stem.connection.CookieAuthRejected, self._check_auth, auth_type, auth_value)
+ if not os.path.exists(auth_value):
+ # If the authentication cookie doesn't exist then we'll be getting an
+ # error for that rather than rejection. This will even fail if
+ # _can_authenticate is true because we *can* authenticate with cookie
+ # auth but the function will short circuit with failure due to the
+ # missing file.
+
+ self.assertRaises(stem.connection.UnreadableCookieFile, self._check_auth, auth_type, auth_value, False)
+ elif _can_authenticate(auth_type):
+ self._check_auth(auth_type, auth_value)
+ else:
+ self.assertRaises(stem.connection.CookieAuthRejected, self._check_auth, auth_type, auth_value)
def test_authenticate_cookie_invalid(self):
"""
@@ -235,26 +268,34 @@ class TestAuthenticate(unittest.TestCase):
value.
"""
- auth_type = stem.connection.AuthMethod.COOKIE
- auth_value = test.runner.get_runner().get_test_dir("fake_cookie")
-
- # we need to create a 32 byte cookie file to load from
- fake_cookie = open(auth_value, "w")
- fake_cookie.write("0" * 32)
- fake_cookie.close()
-
- if _can_authenticate(stem.connection.AuthMethod.NONE):
- # authentication will work anyway
- self._check_auth(auth_type, auth_value)
- else:
- if _can_authenticate(auth_type):
- exc_type = stem.connection.IncorrectCookieValue
+ for auth_type in self.cookie_auth_methods:
+ auth_value = test.runner.get_runner().get_test_dir("fake_cookie")
+
+ # we need to create a 32 byte cookie file to load from
+ fake_cookie = open(auth_value, "w")
+ fake_cookie.write("0" * 32)
+ fake_cookie.close()
+
+ if _can_authenticate(stem.connection.AuthMethod.NONE):
+ # authentication will work anyway
+ if auth_type == AuthMethod.COOKIE:
+ self._check_auth(auth_type, auth_value)
+ #unless you're trying the safe cookie method
+ elif auth_type == AuthMethod.SAFECOOKIE:
+ exc_type = stem.connection.AuthChallengeFailed
+ self.assertRaises(exc_type, self._check_auth, auth_type, auth_value)
+
else:
- exc_type = stem.connection.CookieAuthRejected
+ if _can_authenticate(auth_type):
+ exc_type = stem.connection.IncorrectCookieValue
+ else:
+ exc_type = stem.connection.CookieAuthRejected
+ if auth_type == AuthMethod.SAFECOOKIE:
+ exc_type = stem.connection.AuthChallengeFailed
+
+ self.assertRaises(exc_type, self._check_auth, auth_type, auth_value)
- self.assertRaises(exc_type, self._check_auth, auth_type, auth_value)
-
- os.remove(auth_value)
+ os.remove(auth_value)
def test_authenticate_cookie_missing(self):
"""
@@ -262,9 +303,9 @@ class TestAuthenticate(unittest.TestCase):
shouldn't exist.
"""
- auth_type = stem.connection.AuthMethod.COOKIE
- auth_value = "/if/this/exists/then/they're/asking/for/a/failure"
- self.assertRaises(stem.connection.UnreadableCookieFile, self._check_auth, auth_type, auth_value, False)
+ for auth_type in self.cookie_auth_methods:
+ auth_value = "/if/this/exists/then/they're/asking/for/a/failure"
+ self.assertRaises(stem.connection.UnreadableCookieFile, self._check_auth, auth_type, auth_value, False)
def test_authenticate_cookie_wrong_size(self):
"""
@@ -273,7 +314,7 @@ class TestAuthenticate(unittest.TestCase):
socket.
"""
- auth_type = stem.connection.AuthMethod.COOKIE
+ auth_type = AuthMethod.COOKIE
auth_value = test.runner.get_runner().get_torrc_path(True)
if os.path.getsize(auth_value) == 32:
@@ -282,6 +323,25 @@ class TestAuthenticate(unittest.TestCase):
else:
self.assertRaises(stem.connection.IncorrectCookieSize, self._check_auth, auth_type, auth_value, False)
+ def test_authenticate_safecookie_wrong_size(self):
+ """
+ Tests the authenticate_safecookie function with our torrc as an auth cookie.
+ This is to confirm that we won't read arbitrary files to the control
+ socket.
+ """
+
+ auth_type = AuthMethod.SAFECOOKIE
+ auth_value = test.runner.get_runner().get_torrc_path(True)
+
+ auth_value = test.runner.get_runner().get_test_dir("fake_cookie")
+
+ # we need to create a 32 byte cookie file to load from
+ fake_cookie = open(auth_value, "w")
+ fake_cookie.write("0" * 48)
+ fake_cookie.close()
+ self.assertRaises(stem.connection.IncorrectCookieSize,
+ stem.connection.authenticate_safecookie, auth_type, auth_value, False)
+
def _check_auth(self, auth_type, auth_arg = None, check_message = True):
"""
Attempts to use the given type of authentication against tor's control
@@ -308,6 +368,8 @@ class TestAuthenticate(unittest.TestCase):
stem.connection.authenticate_password(control_socket, auth_arg)
elif auth_type == stem.connection.AuthMethod.COOKIE:
stem.connection.authenticate_cookie(control_socket, auth_arg)
+ elif auth_type == stem.connection.AuthMethod.SAFECOOKIE:
+ stem.connection.authenticate_safecookie(control_socket, auth_arg)
test.runner.exercise_controller(self, control_socket)
except stem.connection.AuthenticationFailure, exc:
@@ -316,7 +378,10 @@ class TestAuthenticate(unittest.TestCase):
# check that we got the failure message that we'd expect
if check_message:
- failure_msg = _get_auth_failure_message(auth_type)
+ if auth_type != AuthMethod.SAFECOOKIE:
+ failure_msg = _get_auth_failure_message(auth_type)
+ else:
+ failure_msg = _get_auth_failure_message(auth_type)
self.assertEqual(failure_msg, str(exc))
raise exc
diff --git a/test/integ/response/protocolinfo.py b/test/integ/response/protocolinfo.py
index e8e8c12..f9c96b0 100644
--- a/test/integ/response/protocolinfo.py
+++ b/test/integ/response/protocolinfo.py
@@ -120,6 +120,7 @@ class TestProtocolInfo(unittest.TestCase):
if test.runner.Torrc.COOKIE in tor_options:
auth_methods.append(stem.response.protocolinfo.AuthMethod.COOKIE)
+ auth_methods.append(stem.response.protocolinfo.AuthMethod.SAFECOOKIE)
chroot_path = runner.get_chroot()
auth_cookie_path = runner.get_auth_cookie_path()
diff --git a/test/unit/connection/authentication.py b/test/unit/connection/authentication.py
index 831ca62..bd56727 100644
--- a/test/unit/connection/authentication.py
+++ b/test/unit/connection/authentication.py
@@ -12,6 +12,8 @@ various error conditions, and make sure that the right exception is raised.
import unittest
import stem.connection
+import stem.response
+import stem.response.authchallenge
import stem.util.log as log
import test.mocking as mocking
@@ -24,15 +26,17 @@ def _get_all_auth_method_combinations():
for is_none in (False, True):
for is_password in (False, True):
for is_cookie in (False, True):
- for is_unknown in (False, True):
- auth_methods = []
-
- if is_none: auth_methods.append(stem.connection.AuthMethod.NONE)
- if is_password: auth_methods.append(stem.connection.AuthMethod.PASSWORD)
- if is_cookie: auth_methods.append(stem.connection.AuthMethod.COOKIE)
- if is_unknown: auth_methods.append(stem.connection.AuthMethod.UNKNOWN)
-
- yield tuple(auth_methods)
+ for is_safecookie in (False, True):
+ for is_unknown in (False, True):
+ auth_methods = []
+
+ if is_none: auth_methods.append(stem.connection.AuthMethod.NONE)
+ if is_password: auth_methods.append(stem.connection.AuthMethod.PASSWORD)
+ if is_cookie: auth_methods.append(stem.connection.AuthMethod.COOKIE)
+ if is_safecookie: auth_methods.append(stem.connection.AuthMethod.SAFECOOKIE)
+ if is_unknown: auth_methods.append(stem.connection.AuthMethod.UNKNOWN)
+
+ yield tuple(auth_methods)
class TestAuthenticate(unittest.TestCase):
def setUp(self):
@@ -40,6 +44,7 @@ class TestAuthenticate(unittest.TestCase):
mocking.mock(stem.connection.authenticate_none, mocking.no_op())
mocking.mock(stem.connection.authenticate_password, mocking.no_op())
mocking.mock(stem.connection.authenticate_cookie, mocking.no_op())
+ mocking.mock(stem.connection.authenticate_safecookie, mocking.no_op())
def tearDown(self):
mocking.revert_mocking()
@@ -92,6 +97,12 @@ class TestAuthenticate(unittest.TestCase):
stem.connection.CookieAuthRejected(None, None),
stem.connection.IncorrectCookieValue(None, None))
+ all_auth_safecookie_exc = all_auth_cookie_exc + (
+ stem.connection.UnrecognizedAuthChallengeMethod(None, None, None),
+ stem.connection.AuthChallengeFailed(None, None),
+ stem.connection.AuthSecurityFailure(None, None),
+ stem.connection.InvalidClientNonce(None, None))
+
# authentication functions might raise a controller error when
# 'suppress_ctl_errors' is False, so including those
@@ -103,6 +114,7 @@ class TestAuthenticate(unittest.TestCase):
all_auth_none_exc += control_exc
all_auth_password_exc += control_exc
all_auth_cookie_exc += control_exc
+ all_auth_safecookie_exc += control_exc
for protocolinfo_auth_methods in _get_all_auth_method_combinations():
# protocolinfo input for the authenticate() call we'll be making
@@ -114,35 +126,38 @@ class TestAuthenticate(unittest.TestCase):
for auth_none_exc in all_auth_none_exc:
for auth_password_exc in all_auth_password_exc:
for auth_cookie_exc in all_auth_cookie_exc:
- # determine if the authenticate() call will succeed and mock each
- # of the authenticate_* function to raise its given exception
-
- expect_success = False
- auth_mocks = {
- stem.connection.AuthMethod.NONE:
- (stem.connection.authenticate_none, auth_none_exc),
- stem.connection.AuthMethod.PASSWORD:
- (stem.connection.authenticate_password, auth_password_exc),
- stem.connection.AuthMethod.COOKIE:
- (stem.connection.authenticate_cookie, auth_cookie_exc),
- }
-
- for auth_method in auth_mocks:
- auth_function, raised_exc = auth_mocks[auth_method]
+ for auth_safecookie_exc in all_auth_cookie_exc:
+ # determine if the authenticate() call will succeed and mock each
+ # of the authenticate_* function to raise its given exception
+
+ expect_success = False
+ auth_mocks = {
+ stem.connection.AuthMethod.NONE:
+ (stem.connection.authenticate_none, auth_none_exc),
+ stem.connection.AuthMethod.PASSWORD:
+ (stem.connection.authenticate_password, auth_password_exc),
+ stem.connection.AuthMethod.COOKIE:
+ (stem.connection.authenticate_cookie, auth_cookie_exc),
+ stem.connection.AuthMethod.SAFECOOKIE:
+ (stem.connection.authenticate_safecookie, auth_safecookie_exc),
+ }
- if not raised_exc:
- # Mocking this authentication method so it will succeed. If
- # it's among the protocolinfo methods then expect success.
+ for auth_method in auth_mocks:
+ auth_function, raised_exc = auth_mocks[auth_method]
- mocking.mock(auth_function, mocking.no_op())
- expect_success |= auth_method in protocolinfo_auth_methods
+ if not raised_exc:
+ # Mocking this authentication method so it will succeed. If
+ # it's among the protocolinfo methods then expect success.
+
+ mocking.mock(auth_function, mocking.no_op())
+ expect_success |= auth_method in protocolinfo_auth_methods
+ else:
+ mocking.mock(auth_function, mocking.raise_exception(raised_exc))
+
+ if expect_success:
+ stem.connection.authenticate(None, "blah", None, protocolinfo_arg)
else:
- mocking.mock(auth_function, mocking.raise_exception(raised_exc))
-
- if expect_success:
- stem.connection.authenticate(None, "blah", None, protocolinfo_arg)
- else:
- self.assertRaises(stem.connection.AuthenticationFailure, stem.connection.authenticate, None, "blah", None, protocolinfo_arg)
+ self.assertRaises(stem.connection.AuthenticationFailure, stem.connection.authenticate, None, "blah", None, protocolinfo_arg)
# revert logging back to normal
stem_logger.setLevel(log.logging_level(log.TRACE))
diff --git a/test/unit/response/__init__.py b/test/unit/response/__init__.py
index c53d9ef..0e4889c 100644
--- a/test/unit/response/__init__.py
+++ b/test/unit/response/__init__.py
@@ -2,5 +2,5 @@
Unit tests for stem.response.
"""
-__all__ = ["control_message", "control_line", "getinfo", "protocolinfo"]
+__all__ = ["control_message", "control_line", "getinfo", "protocolinfo", "authchallenge"]
diff --git a/test/unit/response/authchallenge.py b/test/unit/response/authchallenge.py
new file mode 100644
index 0000000..593d3ac
--- /dev/null
+++ b/test/unit/response/authchallenge.py
@@ -0,0 +1,57 @@
+"""
+Unit tests for the stem.response.authchallenge.AuthChallengeResponse class.
+"""
+
+import unittest
+
+import stem.socket
+import stem.response
+import stem.response.authchallenge
+import test.mocking as mocking
+
+class TestAuthChallengeResponse(unittest.TestCase):
+ VALID_RESPONSE = "250 AUTHCHALLENGE SERVERHASH=B16F72DACD4B5ED1531F3FCC04B593D46A1E30267E636EA7C7F8DD7A2B7BAA05 SERVERNONCE=653574272ABBB49395BD1060D642D653CFB7A2FCE6A4955BCFED819703A9998C"
+ VALID_HASH = "\xb1or\xda\xcdK^\xd1S\x1f?\xcc\x04\xb5\x93\xd4j\x1e0&~cn\xa7\xc7\xf8\xddz+{\xaa\x05"
+ VALID_NONCE = "e5t'*\xbb\xb4\x93\x95\xbd\x10`\xd6B\xd6S\xcf\xb7\xa2\xfc\xe6\xa4\x95[\xcf\xed\x81\x97\x03\xa9\x99\x8c"
+ INVALID_RESPONSE = "250 AUTHCHALLENGE SERVERHASH=FOOBARB16F72DACD4B5ED1531F3FCC04B593D46A1E30267E636EA7C7F8DD7A2B7BAA05 SERVERNONCE=FOOBAR653574272ABBB49395BD1060D642D653CFB7A2FCE6A4955BCFED819703A9998C"
+
+ def test_valid_response(self):
+ """
+ Parses valid AUTHCHALLENGE responses.
+ """
+
+ control_message = mocking.get_message(self.VALID_RESPONSE)
+ stem.response.convert("AUTHCHALLENGE", control_message)
+
+ # now this should be a AuthChallengeResponse (ControlMessage subclass)
+ self.assertTrue(isinstance(control_message, stem.response.ControlMessage))
+ self.assertTrue(isinstance(control_message, stem.response.authchallenge.AuthChallengeResponse))
+
+ self.assertEqual(self.VALID_HASH, control_message.server_hash)
+ self.assertEqual(self.VALID_NONCE, control_message.server_nonce)
+
+ def test_invalid_responses(self):
+ """
+ Tries to parse various malformed responses and checks it they raise
+ appropriate exceptions.
+ """
+
+ valid_resp = self.VALID_RESPONSE.split()
+
+ control_message = mocking.get_message(' '.join(valid_resp[0:1] + [valid_resp[3]]))
+ self.assertRaises(stem.socket.ProtocolError, stem.response.convert, "AUTHCHALLENGE", control_message)
+
+ control_message = mocking.get_message(' '.join(valid_resp[0:1] + [valid_resp[3], valid_resp[2]]))
+ self.assertRaises(stem.socket.ProtocolError, stem.response.convert, "AUTHCHALLENGE", control_message)
+
+ control_message = mocking.get_message(' '.join(valid_resp[0:2]))
+ self.assertRaises(stem.socket.ProtocolError, stem.response.convert, "AUTHCHALLENGE", control_message)
+
+ for begin in range(4):
+ for end in range(4):
+ try:
+ control_message = mocking.get_message(' '.join(self.VALID_RESPONSE.split()[begin:end]))
+ except stem.socket.ProtocolError:
+ continue
+ self.assertRaises(stem.socket.ProtocolError, stem.response.convert, "AUTHCHALLENGE", control_message)
+
More information about the tor-commits
mailing list