Source code for circuit.handshake.ntorfsm

# Copyright 2014, 2015, Nik Kinkel
# See LICENSE for licensing information

'''
.. topic:: Details

    An NTorFSM steps through each state of a circuit handshake. At each step
    an NTorFSM expects to receive a certain kind of cell. Either this cell is
    received, processed, and a response cell is returned or an exception
    is raised (and the circuit is destroyed).

    NTorFSM's also derive key material and add RelayCrypto objects to the
    associated circuit's *crypt_path*.

'''
import logging

from oppy.cell.definitions import (
    CREATED2_CMD,
    DESTROY_CMD,
    RELAY_CMD,
    RELAY_EXTENDED2_CMD,
)
from oppy.cell.fixedlen import Create2Cell
from oppy.cell.relay import RelayExtend2Cell
from oppy.cell.util import LinkSpecifier
from oppy.circuit.handshake.exceptions import (
    BadHandshakeState,
    HandshakeFailed,
    ReceivedDestroyCell,
    UnexpectedCell,
)
from oppy.crypto.exceptions import UnrecognizedCell
from oppy.crypto.ntorhandshake import NTorHandshake
import oppy.crypto.util as crypto
from oppy.util.tools import dispatch, enum


State = enum(
    INIT=0,
    EXPECT_CREATED2=1,
    EXPECT_FIRST_EXTENDED2=2,
    EXPECT_SECOND_EXTENDED2=3,
    DONE=4,
)


[docs]class NTorFSM(object): '''Finite state machine to step through an ntor handshake with relays on a circuit's path. ''' _response_map = {} def __init__(self, circuit_id, path, crypt_path): ''' :param int circuit_id: id of the circuit for this ntor fsm :param oppy.path.path.Path path: path for this circuit :param list, oppy.crypto.relaycrypto.RelayCrypto crypt_path: a list (to be filled in by this ntor fsm) of RelayCrypto objects ''' assert len(crypt_path) == 0 msg = "Creating NTorFSM for circuit {}." logging.debug(msg.format(circuit_id)) self.circuit_id = circuit_id self._path = path self._crypt_path = crypt_path self._ntor_handshakes = [] self._state = State.INIT self._ntor_handshakes.append(NTorHandshake(path.entry)) self._ntor_handshakes.append(NTorHandshake(path.middle)) self._ntor_handshakes.append(NTorHandshake(path.exit))
[docs] def recvCell(self, cell): '''Call a handler function to process *cell* based on this ntor fsm's current state. :param cell cell: cell received from this ntor fsm's circuit :returns: **cell, None** may return either the next cell to send to step through handshakes with relays on this path or None if no response is required ''' try: fn = NTorFSM._response_map[self._state].__get__(self, type(self)) except KeyError: msg = "NTorFSM in unknown state: {}.".format(self._state) raise BadHandshakeState(msg) response = fn(cell) return response
[docs] def getInitiatingCell(self): '''Build and return the initiating cell for this ntor fsm - a Create2Cell. Advance this ntor fsm's state. :returns: **oppy.cell.fixedlen.Create2Cell** ''' onion_skin = self._ntor_handshakes[0].createOnionSkin() cell = Create2Cell.make(self.circuit_id, hdata=onion_skin) self._state = State.EXPECT_CREATED2 return cell
@staticmethod def _verifyCellCmd(test_cmd, cmd): '''Verify that *test_cmd* == *cmd*. If not, or if this cmd is a DESTROY_CMD, raise an exception. :param int test_cmd: command to check :param int cmd: expected command value ''' if test_cmd == DESTROY_CMD: msg = "NTorFSM got a DESTROY cell." raise ReceivedDestroyCell(msg) if test_cmd != cmd: msg = "NTorFSM unexpected cell {}, expected {}." msg = msg.format(test_cmd, cmd) raise UnexpectedCell(msg) @dispatch(_response_map, State.EXPECT_CREATED2) def _processCreated2(self, cell): '''Called when this ntor fsm is receives a cell and is expecting an Created2Cell. - verify we did in fact receive a valid Created2Cell - derive crypto keys - create a RelayCrypto object and add to crypt_path - create an Extend2 cell and encrypt it - advance this ntor fsm's state - return the encrypted Extend2 cell Fail if we received an invalid or unexpected cell. :param cell cell: the received cell :returns: **oppy.cell.relay.RelayExtend2Cell** ''' NTorFSM._verifyCellCmd(cell.header.cmd, CREATED2_CMD) entry_crypto = self._ntor_handshakes[0].deriveRelayCrypto(cell) self._crypt_path.append(entry_crypto) relay = self._path.middle lspecs = [LinkSpecifier(relay), LinkSpecifier(relay, legacy=True)] hdata = self._ntor_handshakes[1].createOnionSkin() cell = RelayExtend2Cell.make(self.circuit_id, nspec=len(lspecs), lspecs=lspecs, hdata=hdata) response = crypto.encryptCellToTarget(cell, self._crypt_path, target=0, early=True) self._state = State.EXPECT_FIRST_EXTENDED2 return response @dispatch(_response_map, State.EXPECT_FIRST_EXTENDED2) def _processFstExtended2(self, cell): '''Called when this ntor fsm is receives a cell and is expecting its first Extended2 cell. - decrypt the incoming cell - verify we did in fact receive a valid Extended2 cell - derive crypto keys - create a RelayCrypto object and add to crypt_path - create an Extend2 cell and encrypt it - advance this ntor fsm's state - return the encrypted Extend2 cell Fail if we received an invalid or unexpected cell. :param cell cell: the received cell :returns: **oppy.cell.relay.RelayExtend2Cell** ''' try: cell, origin = crypto.decryptCellUntilRecognized(cell, self._crypt_path, origin=0) except UnrecognizedCell: raise HandshakeFailed() cmd = cell.header.cmd NTorFSM._verifyCellCmd(cmd, RELAY_CMD) rcmd = cell.rheader.cmd NTorFSM._verifyCellCmd(rcmd, RELAY_EXTENDED2_CMD) # Generate crypto material from the received cell. middle_crypto = self._ntor_handshakes[1].deriveRelayCrypto(cell) self._crypt_path.append(middle_crypto) relay = self._path.exit lspecs = [LinkSpecifier(relay), LinkSpecifier(relay, legacy=True)] hdata = self._ntor_handshakes[2].createOnionSkin() cell = RelayExtend2Cell.make(self.circuit_id, nspec=len(lspecs), lspecs=lspecs, hdata=hdata) response = crypto.encryptCellToTarget(cell, self._crypt_path, target=1, early=True) self._state = State.EXPECT_SECOND_EXTENDED2 return response @dispatch(_response_map, State.EXPECT_SECOND_EXTENDED2) def _processSndExtended2(self, cell): '''Called when this ntor fsm is receives a cell and is expecting its second Extend2 cell. - decrypt the incoming cell - verify we did in fact receive a valid Extended2 cell - derive crypto keys - create a RelayCrypto object and add to crypt_path - advance this ntor fsm's state to Done Fail if we received an invalid or unexpected cell. :param cell cell: the received cell ''' # Decrypt and validate received cell. # Decrypt and validate received cell. try: cell, origin = crypto.decryptCellUntilRecognized(cell, self._crypt_path, origin=1) except UnrecognizedCell: raise HandshakeFailed() cmd = cell.header.cmd NTorFSM._verifyCellCmd(cmd, RELAY_CMD) rcmd = cell.rheader.cmd NTorFSM._verifyCellCmd(rcmd, RELAY_EXTENDED2_CMD) # Generate crypto material from the received cell. exit_crypto = self._ntor_handshakes[2].deriveRelayCrypto(cell) self._crypt_path.append(exit_crypto) self._state = State.DONE return None
[docs] def isDone(self): '''Return **True** iff this ntor fsm's state is State.DONE :returns: **bool** **True** if state is State.DONE ''' return self._state == State.DONE