summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDamian Johnson <atagar@torproject.org>2018-08-26 13:47:51 -0700
committerDamian Johnson <atagar@torproject.org>2018-08-26 13:47:51 -0700
commit8a1239ea8e63af3797771b6be12ebcb14e437671 (patch)
tree0b46cf6abc341ff968aafda4626141ac631a6072
parentded189211eec6ac36a139f5b91fbc5441e7669fd (diff)
parentcea4f76279f79a65198743e90e300d7b0e1f6a97 (diff)
Move cell encryption/decription into the RelayCell class
"Less is more." Dave's pull request for ticket #27112 refactored our RelayCell into a BaseRelayCell and RawRelayCell class. Think I see what he's going for, and this was a step forward in some respects. But honestly added a lot of code and complexity without functionally doing anything new. Going with a hybrid of our approaches that integrates the part I think we agree on. Namely, simplifying our Circuit's send() method by moving cell encryption/decription down into the RelayCell class. This is a much, *MUCH* smaller diff than what Dave proposed. Happy to discuss integrating more of the pull request, but lets start with the part I think we agree on to give us a common point to work from.
-rw-r--r--stem/client/__init__.py65
-rw-r--r--stem/client/cell.py111
-rw-r--r--stem/client/datatype.py18
-rw-r--r--stem/util/log.py2
-rw-r--r--stem/util/term.py2
-rw-r--r--test/unit/client/cell.py1
-rw-r--r--test/unit/client/size.py1
-rw-r--r--test/unit/control/controller.py1
8 files changed, 145 insertions, 56 deletions
diff --git a/stem/client/__init__.py b/stem/client/__init__.py
index 24b6de38..aaa62f5b 100644
--- a/stem/client/__init__.py
+++ b/stem/client/__init__.py
@@ -25,7 +25,6 @@ a wrapper for :class:`~stem.socket.RelaySocket`, much the same way as
+- close - closes this circuit
"""
-import copy
import hashlib
import threading
@@ -34,7 +33,7 @@ import stem.client.cell
import stem.socket
import stem.util.connection
-from stem.client.datatype import ZERO, LinkProtocol, Address, Size, KDF, split
+from stem.client.datatype import ZERO, LinkProtocol, Address, KDF, split
__all__ = [
'cell',
@@ -226,7 +225,7 @@ class Circuit(object):
"""
Sends a message over the circuit.
- :param stem.client.RelayCommand command: command to be issued
+ :param stem.client.datatype.RelayCommand command: command to be issued
:param bytes data: message payload
:param int stream_id: specific stream this concerns
@@ -234,54 +233,38 @@ class Circuit(object):
"""
with self.relay._orport_lock:
- orig_digest = self.forward_digest.copy()
- orig_key = copy.copy(self.forward_key)
+ # Encrypt and send the cell. Our digest/key only updates if the cell is
+ # successfully sent.
- # Digests and such are computed using the RELAY cell payload. This
- # doesn't include the initial circuit id and cell type fields.
- # Circuit ids vary in length depending on the protocol version.
+ cell = stem.client.cell.RelayCell(self.id, command, data, stream_id = stream_id)
+ payload, forward_key, forward_digest = cell.encrypt(self.relay.link_protocol, self.forward_key, self.forward_digest)
+ self.relay._orport.send(payload)
- header_size = self.relay.link_protocol.circ_id_size.size + 1
+ self.forward_digest = forward_digest
+ self.forward_key = forward_key
- try:
- cell = stem.client.cell.RelayCell(self.id, command, data, 0, stream_id)
- payload_without_digest = cell.pack(self.relay.link_protocol)[header_size:]
- self.forward_digest.update(payload_without_digest)
+ # Decrypt relay cells received in response. Again, our digest/key only
+ # updates when handled successfully.
- cell = stem.client.cell.RelayCell(self.id, command, data, self.forward_digest, stream_id)
- header, payload = split(cell.pack(self.relay.link_protocol), header_size)
- encrypted_payload = header + self.forward_key.update(payload)
+ reply = self.relay._orport.recv()
+ reply_cells = []
- reply_cells = []
- self.relay._orport.send(encrypted_payload)
- reply = self.relay._orport.recv()
+ if len(reply) % self.relay.link_protocol.fixed_cell_length != 0:
+ raise stem.ProtocolError('Circuit response should be a series of RELAY cells, but received an unexpected size for a response: %i' % len(reply))
- # Check that we got the correct number of bytes for a series of RELAY cells
+ while reply:
+ encrypted_cell, reply = split(reply, self.relay.link_protocol.fixed_cell_length)
+ decrypted_cell, backward_key, backward_digest = stem.client.cell.RelayCell.decrypt(self.relay.link_protocol, encrypted_cell, self.backward_key, self.backward_digest)
- relay_cell_size = header_size + stem.client.cell.FIXED_PAYLOAD_LEN
- relay_cell_cmd = stem.client.cell.RelayCell.VALUE
+ if self.id != decrypted_cell.circ_id:
+ raise stem.ProtocolError('Response should be for circuit id %i, not %i' % (self.id, decrypted_cell.circ_id))
- if len(reply) % relay_cell_size != 0:
- raise stem.ProtocolError('Circuit response should be a series of RELAY cells, but received an unexpected size for a response: %i' % len(reply))
+ self.backward_digest = backward_digest
+ self.backward_key = backward_key
- while reply:
- circ_id, reply = self.relay.link_protocol.circ_id_size.pop(reply)
- command, reply = Size.CHAR.pop(reply)
- payload, reply = split(reply, stem.client.cell.FIXED_PAYLOAD_LEN)
+ reply_cells.append(decrypted_cell)
- if command != relay_cell_cmd:
- raise stem.ProtocolError('RELAY cell responses should be %i but was %i' % (relay_cell_cmd, command))
- elif circ_id != self.id:
- raise stem.ProtocolError('Response should be for circuit id %i, not %i' % (self.id, circ_id))
-
- decrypted = self.backward_key.update(payload)
- reply_cells.append(stem.client.cell.RelayCell._unpack(decrypted, self.id, self.relay.link_protocol))
-
- return reply_cells
- except:
- self.forward_digest = orig_digest
- self.forward_key = orig_key
- raise
+ return reply_cells
def close(self):
with self.relay._orport_lock:
diff --git a/stem/client/cell.py b/stem/client/cell.py
index 12fa994c..91dec143 100644
--- a/stem/client/cell.py
+++ b/stem/client/cell.py
@@ -37,6 +37,7 @@ Messages communicated over a Tor relay's ORPort.
+- pop - decodes cell with remainder
"""
+import copy
import datetime
import inspect
import os
@@ -101,9 +102,9 @@ class Cell(object):
"""
Provides cell attributes by its name.
- :parm str name: cell command to fetch
+ :param str name: cell command to fetch
- :raise: **ValueError** if cell type is invalid
+ :raises: **ValueError** if cell type is invalid
"""
for _, cls in inspect.getmembers(sys.modules[__name__]):
@@ -117,9 +118,9 @@ class Cell(object):
"""
Provides cell attributes by its value.
- :parm int value: cell value to fetch
+ :param int value: cell value to fetch
- :raise: **ValueError** if cell type is invalid
+ :raises: **ValueError** if cell type is invalid
"""
for _, cls in inspect.getmembers(sys.modules[__name__]):
@@ -199,9 +200,9 @@ class Cell(object):
:param bytes payload: cell payload
:param int circ_id: circuit id, if a CircuitCell
- :return: **bytes** with the encoded payload
+ :returns: **bytes** with the encoded payload
- :raise: **ValueError** if cell type invalid or payload makes cell too large
+ :raises: **ValueError** if cell type invalid or payload makes cell too large
"""
if issubclass(cls, CircuitCell):
@@ -324,10 +325,16 @@ class RelayCell(CircuitCell):
"""
Command concerning a relay circuit.
+ Our 'recognized' attribute provides a cheap (but incomplete) check for if our
+ cell payload is encrypted. If non-zero our payload *IS* encrypted, but if
+ zero we're *PROBABLY* fully decrypted. This uncertainty is because encrypted
+ cells have a small chance of coincidently producing zero for this value as
+ well.
+
:var stem.client.RelayCommand command: command to be issued
:var int command_int: integer value of our command
:var bytes data: payload of the cell
- :var int recognized: zero if cell is decrypted, non-zero otherwise
+ :var int recognized: non-zero if payload is encrypted
:var int digest: running digest held with the relay
:var int stream_id: specific stream this concerns
"""
@@ -376,6 +383,96 @@ class RelayCell(CircuitCell):
return RelayCell._pack(link_protocol, bytes(payload), self.unused, self.circ_id)
+ @staticmethod
+ def decrypt(link_protocol, content, key, digest):
+ """
+ Decrypts content as a relay cell addressed to us. This provides back a
+ tuple of the form...
+
+ ::
+
+ (cell (RelayCell), new_key (CipherContext), new_digest (HASH))
+
+ :param int link_protocol: link protocol version
+ :param bytes content: cell content to be decrypted
+ :param cryptography.hazmat.primitives.ciphers.CipherContext key:
+ key established with the relay we received this cell from
+ :param HASH digest: running digest held with the relay
+
+ :returns: **tuple** with our decrypted cell and updated key/digest
+
+ :raises: :class:`stem.ProtocolError` if content doesn't belong to a relay
+ cell
+ """
+
+ new_key = copy.copy(key)
+ new_digest = digest.copy()
+
+ if len(content) != link_protocol.fixed_cell_length:
+ raise stem.ProtocolError('RELAY cells should be %i bytes, but received %i' % (link_protocol.fixed_cell_length, len(content)))
+
+ circ_id, content = link_protocol.circ_id_size.pop(content)
+ command, encrypted_payload = Size.CHAR.pop(content)
+
+ if command != RelayCell.VALUE:
+ raise stem.ProtocolError('Cannot decrypt as a RELAY cell. This had command %i instead.' % command)
+
+ payload = new_key.update(encrypted_payload)
+
+ cell = RelayCell._unpack(payload, circ_id, link_protocol)
+
+ # TODO: Implement our decryption digest. It is used to support relaying
+ # within multi-hop circuits. On first glance this should go something
+ # like...
+ #
+ # # Our updated digest is calculated based on this cell with a blanked
+ # # digest field.
+ #
+ # digest_cell = RelayCell(self.circ_id, self.command, self.data, 0, self.stream_id, self.recognized, self.unused)
+ # new_digest.update(digest_cell.pack(link_protocol))
+ #
+ # is_encrypted == cell.recognized != 0 or self.digest == new_digest
+ #
+ # ... or something like that. Until we attempt to support relaying this is
+ # both moot and difficult to exercise in order to ensure we get it right.
+
+ return cell, new_key, new_digest
+
+ def encrypt(self, link_protocol, key, digest):
+ """
+ Encrypts our cell content to be sent with the given key. This provides back
+ a tuple of the form...
+
+ ::
+
+ (payload (bytes), new_key (CipherContext), new_digest (HASH))
+
+ :param int link_protocol: link protocol version
+ :param cryptography.hazmat.primitives.ciphers.CipherContext key:
+ key established with the relay we're sending this cell to
+ :param HASH digest: running digest held with the relay
+
+ :returns: **tuple** with our encrypted payload and updated key/digest
+ """
+
+ new_key = copy.copy(key)
+ new_digest = digest.copy()
+
+ # Digests are computed from our payload, not including our header's circuit
+ # id (2 or 4 bytes) and command (1 byte).
+
+ header_size = link_protocol.circ_id_size.size + 1
+ payload_without_digest = self.pack(link_protocol)[header_size:]
+ new_digest.update(payload_without_digest)
+
+ # Pack a copy of ourselves with our newly calculated digest, and encrypt
+ # the payload. Header remains plaintext.
+
+ cell = RelayCell(self.circ_id, self.command, self.data, new_digest, self.stream_id, self.recognized, self.unused)
+ header, payload = split(cell.pack(link_protocol), header_size)
+
+ return header + new_key.update(payload), new_key, new_digest
+
@classmethod
def _unpack(cls, content, circ_id, link_protocol):
command, content = Size.CHAR.pop(content)
diff --git a/stem/client/datatype.py b/stem/client/datatype.py
index 5ca4e820..644d4993 100644
--- a/stem/client/datatype.py
+++ b/stem/client/datatype.py
@@ -116,6 +116,7 @@ import collections
import hashlib
import struct
+import stem.client.cell
import stem.prereq
import stem.util
import stem.util.connection
@@ -246,9 +247,11 @@ class LinkProtocol(int):
protocol = int.__new__(cls, version)
protocol.version = version
protocol.circ_id_size = Size.LONG if version > 3 else Size.SHORT
- protocol.fixed_cell_length = 514 if version > 3 else 512
protocol.first_circ_id = 0x80000000 if version > 3 else 0x01
+ cell_header_size = protocol.circ_id_size.size + 1 # circuit id (2 or 4 bytes) + command (1 byte)
+ protocol.fixed_cell_length = cell_header_size + stem.client.cell.FIXED_PAYLOAD_LEN
+
return protocol
def __hash__(self):
@@ -355,10 +358,15 @@ class Size(Field):
raise NotImplementedError("Use our constant's unpack() and pop() instead")
def pack(self, content):
- if not stem.util._is_int(content):
- raise ValueError('Size.pack encodes an integer, but was a %s' % type(content).__name__)
-
- packed = struct.pack(self.format, content)
+ try:
+ packed = struct.pack(self.format, content)
+ except struct.error:
+ if not stem.util._is_int(content):
+ raise ValueError('Size.pack encodes an integer, but was a %s' % type(content).__name__)
+ elif content < 0:
+ raise ValueError('Packed values must be positive (attempted to pack %i as a %s)' % (content, self.name))
+ else:
+ raise # some other struct exception
if self.size != len(packed):
raise ValueError('%s is the wrong size for a %s field' % (repr(packed), self.name))
diff --git a/stem/util/log.py b/stem/util/log.py
index 81fc88a6..0b2de90a 100644
--- a/stem/util/log.py
+++ b/stem/util/log.py
@@ -103,7 +103,7 @@ def get_logger():
"""
Provides the stem logger.
- :return: **logging.Logger** for stem
+ :returns: **logging.Logger** for stem
"""
return LOGGER
diff --git a/stem/util/term.py b/stem/util/term.py
index 3c6fe73a..b4acd61f 100644
--- a/stem/util/term.py
+++ b/stem/util/term.py
@@ -80,7 +80,7 @@ def encoding(*attrs):
:data:`~stem.util.terminal.BgColor`, or :data:`~stem.util.terminal.Attr` to
provide an ecoding for
- :return: **str** of the ANSI escape sequence, **None** no attributes are
+ :returns: **str** of the ANSI escape sequence, **None** no attributes are
recognized
"""
diff --git a/test/unit/client/cell.py b/test/unit/client/cell.py
index ce492638..22843799 100644
--- a/test/unit/client/cell.py
+++ b/test/unit/client/cell.py
@@ -205,6 +205,7 @@ class TestCell(unittest.TestCase):
self.assertEqual(digest, cell.digest)
self.assertEqual(stream_id, cell.stream_id)
self.assertEqual(unused, cell.unused)
+
self.assertEqual(cell_bytes, cell.pack(link_protocol))
digest = hashlib.sha1(b'hi')
diff --git a/test/unit/client/size.py b/test/unit/client/size.py
index eebe3619..dcd5fea3 100644
--- a/test/unit/client/size.py
+++ b/test/unit/client/size.py
@@ -25,6 +25,7 @@ class TestSize(unittest.TestCase):
self.assertEqual(b'\x00\x00\x00\x00\x00\x00\x00\x12', Size.LONG_LONG.pack(18))
self.assertRaisesWith(ValueError, 'Size.pack encodes an integer, but was a str', Size.CHAR.pack, 'hi')
+ self.assertRaisesWith(ValueError, 'Packed values must be positive (attempted to pack -1 as a CHAR)', Size.CHAR.pack, -1)
bad_size = Size('BAD_SIZE', 1, '!H')
self.assertRaisesRegexp(ValueError, re.escape("'\\x00\\x12' is the wrong size for a BAD_SIZE field"), bad_size.pack, 18)
diff --git a/test/unit/control/controller.py b/test/unit/control/controller.py
index 7ee2ab70..ba55208a 100644
--- a/test/unit/control/controller.py
+++ b/test/unit/control/controller.py
@@ -5,7 +5,6 @@ integ tests, but a few bits lend themselves to unit testing.
import datetime
import io
-import time
import unittest
import stem.descriptor.router_status_entry