[Pkg-privacy-commits] [obfsproxy] 221/353: Import the ScrambleSuit transport protocol.
Ximin Luo
infinity0 at moszumanska.debian.org
Sat Aug 22 13:02:02 UTC 2015
This is an automated email from the git hooks/post-receive script.
infinity0 pushed a commit to branch master
in repository obfsproxy.
commit 6fc79b981e384618b7bba313826df72a6faa3e5c
Author: Philipp Winter <phw at torproject.org>
Date: Tue Feb 4 00:16:09 2014 +0100
Import the ScrambleSuit transport protocol.
ScrambleSuit implements a superset of the obfs3 protocol. Its original
repository is available here:
<https://gitweb.torproject.org/user/phw/scramblesuit.git>
The project web site is available here:
<http://www.cs.kau.se/philwint/scramblesuit/>
---
.../scramblesuit}/__init__.py | 0
obfsproxy/transports/scramblesuit/const.py | 106 ++++
obfsproxy/transports/scramblesuit/fifobuf.py | 121 ++++
obfsproxy/transports/scramblesuit/message.py | 226 ++++++++
obfsproxy/transports/scramblesuit/mycrypto.py | 155 +++++
obfsproxy/transports/scramblesuit/packetmorpher.py | 69 +++
obfsproxy/transports/scramblesuit/probdist.py | 98 ++++
obfsproxy/transports/scramblesuit/replay.py | 85 +++
obfsproxy/transports/scramblesuit/scramblesuit.py | 624 +++++++++++++++++++++
obfsproxy/transports/scramblesuit/state.py | 161 ++++++
obfsproxy/transports/scramblesuit/ticket.py | 391 +++++++++++++
obfsproxy/transports/scramblesuit/uniformdh.py | 185 ++++++
obfsproxy/transports/scramblesuit/util.py | 175 ++++++
obfsproxy/transports/transports.py | 4 +
14 files changed, 2400 insertions(+)
diff --git a/obfsproxy/common/__init__.py b/obfsproxy/transports/scramblesuit/__init__.py
similarity index 100%
copy from obfsproxy/common/__init__.py
copy to obfsproxy/transports/scramblesuit/__init__.py
diff --git a/obfsproxy/transports/scramblesuit/const.py b/obfsproxy/transports/scramblesuit/const.py
new file mode 100644
index 0000000..ca6f8ea
--- /dev/null
+++ b/obfsproxy/transports/scramblesuit/const.py
@@ -0,0 +1,106 @@
+"""
+This module defines constant values for the ScrambleSuit protocol.
+
+While some values can be changed, in general they should not. If you do not
+obey, be at least careful because the protocol could easily break.
+"""
+
+# Length of the key of the HMAC which used to authenticate tickets in bytes.
+TICKET_HMAC_KEY_LENGTH = 32
+
+# Length of the AES key used to encrypt tickets in bytes.
+TICKET_AES_KEY_LENGTH = 16
+
+# Length of the IV for AES-CBC which is used to encrypt tickets in bytes.
+TICKET_AES_CBC_IV_LENGTH = 16
+
+# Directory where long-lived information is stored. It defaults to the current
+# directory but is later set by `setStateLocation()' in util.py.
+STATE_LOCATION = ""
+
+# Divisor (in seconds) for the Unix epoch used to defend against replay
+# attacks.
+EPOCH_GRANULARITY = 3600
+
+# Flags which can be set in a ScrambleSuit protocol message.
+FLAG_PAYLOAD = (1 << 0)
+FLAG_NEW_TICKET = (1 << 1)
+FLAG_PRNG_SEED = (1 << 2)
+
+# Length of ScrambleSuit's header in bytes.
+HDR_LENGTH = 16 + 2 + 2 + 1
+
+# Length of the HMAC-SHA256-128 digest in bytes.
+HMAC_SHA256_128_LENGTH = 16
+
+# Whether or not to use inter-arrival time obfuscation. Disabling this option
+# makes the transported protocol more identifiable but increases throughput a
+# lot.
+USE_IAT_OBFUSCATION = False
+
+# Key rotation time for session ticket keys in seconds.
+KEY_ROTATION_TIME = 60 * 60 * 24 * 7
+
+# Mark used to easily locate the HMAC authenticating handshake messages in
+# bytes.
+MARK_LENGTH = 16
+
+# The master key's length in bytes.
+MASTER_KEY_LENGTH = 32
+
+# Maximum amount of seconds, a packet is delayed due to inter arrival time
+# obfuscation.
+MAX_PACKET_DELAY = 0.01
+
+# The maximum amount of padding to be appended to handshake data.
+MAX_PADDING_LENGTH = 1500
+
+# Length of ScrambleSuit's MTU in bytes. Note that this is *not* the link MTU
+# which is probably 1500.
+MTU = 1448
+
+# Maximum payload unit of a ScrambleSuit message in bytes.
+MPU = MTU - HDR_LENGTH
+
+# The minimum amount of distinct bins for probability distributions.
+MIN_BINS = 1
+
+# The maximum amount of distinct bins for probability distributions.
+MAX_BINS = 100
+
+# Length of a UniformDH public key in bytes.
+PUBLIC_KEY_LENGTH = 192
+
+# Length of the PRNG seed used to generate probability distributions in bytes.
+PRNG_SEED_LENGTH = 32
+
+# File which holds the server's state information.
+SERVER_STATE_FILE = "server_state.cpickle"
+
+# Life time of session tickets in seconds.
+SESSION_TICKET_LIFETIME = KEY_ROTATION_TIME
+
+# SHA256's digest length in bytes.
+SHA256_LENGTH = 32
+
+# The length of the UniformDH shared secret in bytes. It should be a multiple
+# of 5 bytes since outside ScrambleSuit it is encoded in Base32. That way, we
+# can avoid padding which might confuse users.
+SHARED_SECRET_LENGTH = 20
+
+# States which are used for the protocol state machine.
+ST_WAIT_FOR_AUTH = 0
+ST_CONNECTED = 1
+
+# File which holds the client's session tickets.
+CLIENT_TICKET_FILE = "session_ticket.yaml"
+
+# Static validation string embedded in all tickets. Must be a multiple of 16
+# bytes due to AES' block size.
+TICKET_IDENTIFIER = "ScrambleSuitTicket"
+
+# Length of a session ticket in bytes.
+TICKET_LENGTH = 112
+
+# The protocol name which is used in log messages.
+TRANSPORT_NAME = "ScrambleSuit"
diff --git a/obfsproxy/transports/scramblesuit/fifobuf.py b/obfsproxy/transports/scramblesuit/fifobuf.py
new file mode 100644
index 0000000..221a6cb
--- /dev/null
+++ b/obfsproxy/transports/scramblesuit/fifobuf.py
@@ -0,0 +1,121 @@
+"""
+Provides an interface for a fast FIFO buffer.
+
+The interface implements only 'read()', 'write()' and 'len()'. The
+implementation below is a modified version of the code originally written by
+Ben Timby: http://ben.timby.com/?p=139
+"""
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+MAX_BUFFER = 1024**2*4
+
+class Buffer( object ):
+
+ """
+ Implements a fast FIFO buffer.
+
+ Internally, the buffer consists of a list of StringIO objects. New
+ StringIO objects are added and delete as data is written to and read from
+ the FIFO buffer.
+ """
+
+ def __init__( self, max_size=MAX_BUFFER ):
+ """
+ Initialise a Buffer object.
+ """
+
+ self.buffers = []
+ self.max_size = max_size
+ self.read_pos = 0
+ self.write_pos = 0
+
+ def write( self, data ):
+ """
+ Write `data' to the FIFO buffer.
+
+ If necessary, a new internal buffer is created.
+ """
+
+ # Add a StringIO buffer if none exists yet.
+ if not self.buffers:
+ self.buffers.append(StringIO())
+ self.write_pos = 0
+
+ lastBuf = self.buffers[-1]
+ lastBuf.seek(self.write_pos)
+ lastBuf.write(data)
+
+ # If we are over the limit, a new internal buffer is created.
+ if lastBuf.tell() >= self.max_size:
+ lastBuf = StringIO()
+ self.buffers.append(lastBuf)
+
+ self.write_pos = lastBuf.tell()
+
+ def read( self, length=-1 ):
+ """
+ Read `length' elements of the FIFO buffer.
+
+ Drained data is automatically deleted.
+ """
+
+ read_buf = StringIO()
+ remaining = length
+
+ while True:
+
+ if not self.buffers:
+ break
+
+ firstBuf = self.buffers[0]
+ firstBuf.seek(self.read_pos)
+ read_buf.write(firstBuf.read(remaining))
+ self.read_pos = firstBuf.tell()
+
+ if length == -1:
+
+ # We did not limit the read, we exhausted the buffer, so delete
+ # it. Keep reading from the remaining buffers.
+ del self.buffers[0]
+ self.read_pos = 0
+
+ else:
+
+ # We limited the read so either we exhausted the buffer or not.
+ remaining = length - read_buf.tell()
+
+ if remaining > 0:
+ # Exhausted, remove buffer, read more. Keep reading from
+ # remaining buffers.
+ del self.buffers[0]
+ self.read_pos = 0
+ else:
+ # Did not exhaust buffer, but read all that was requested.
+ # Break to stop reading and return data of requested
+ # length.
+ break
+
+ return read_buf.getvalue()
+
+ def __len__(self):
+ """
+ Return the length of the Buffer object.
+ """
+
+ length = 0
+
+ for buf in self.buffers:
+
+ # Jump to the end of the internal buffer.
+ buf.seek(0, 2)
+
+ if buf == self.buffers[0]:
+ length += buf.tell() - self.read_pos
+ else:
+ length += buf.tell()
+
+ return length
diff --git a/obfsproxy/transports/scramblesuit/message.py b/obfsproxy/transports/scramblesuit/message.py
new file mode 100644
index 0000000..ad8d447
--- /dev/null
+++ b/obfsproxy/transports/scramblesuit/message.py
@@ -0,0 +1,226 @@
+"""
+This module provides code to handle ScrambleSuit protocol messages.
+
+The exported classes and functions provide interfaces to handle protocol
+messages, check message headers for validity and create protocol messages out
+of application data.
+"""
+
+import obfsproxy.common.log as logging
+import obfsproxy.common.serialize as pack
+import obfsproxy.transports.base as base
+
+import mycrypto
+import const
+
+log = logging.get_obfslogger()
+
+
+def createProtocolMessages( data, flags=const.FLAG_PAYLOAD ):
+ """
+ Create protocol messages out of the given payload.
+
+ The given `data' is turned into a list of protocol messages with the given
+ `flags' set. The list is then returned. If possible, all messages fill
+ the MTU.
+ """
+
+ messages = []
+
+ while len(data) > const.MPU:
+ messages.append(ProtocolMessage(data[:const.MPU], flags=flags))
+ data = data[const.MPU:]
+
+ messages.append(ProtocolMessage(data, flags=flags))
+
+ log.debug("Created %d protocol messages." % len(messages))
+
+ return messages
+
+
+def getFlagNames( flags ):
+ """
+ Return the flag name encoded in the integer `flags' as string.
+
+ This function is only useful for printing easy-to-read flag names in debug
+ log messages.
+ """
+
+ if flags == 1:
+ return "PAYLOAD"
+
+ elif flags == 2:
+ return "NEW_TICKET"
+
+ elif flags == 4:
+ return "PRNG_SEED"
+
+ else:
+ return "Undefined"
+
+
+def isSane( totalLen, payloadLen, flags ):
+ """
+ Verifies whether the given header fields are sane.
+
+ The values of the fields `totalLen', `payloadLen' and `flags' are checked
+ for their sanity. If they are in the expected range, `True' is returned.
+ If any of these fields has an invalid value, `False' is returned.
+ """
+
+ def isFine( length ):
+ """
+ Check if the given length is fine.
+ """
+
+ return True if (0 <= length <= const.MPU) else False
+
+ log.debug("Message header: totalLen=%d, payloadLen=%d, flags"
+ "=%s" % (totalLen, payloadLen, getFlagNames(flags)))
+
+ validFlags = [
+ const.FLAG_PAYLOAD,
+ const.FLAG_NEW_TICKET,
+ const.FLAG_PRNG_SEED,
+ ]
+
+ return isFine(totalLen) and \
+ isFine(payloadLen) and \
+ totalLen >= payloadLen and \
+ (flags in validFlags)
+
+
+class ProtocolMessage( object ):
+
+ """
+ Represents a ScrambleSuit protocol message.
+
+ This class provides methods to deal with protocol messages. The methods
+ make it possible to add padding as well as to encrypt and authenticate
+ protocol messages.
+ """
+
+ def __init__( self, payload="", paddingLen=0, flags=const.FLAG_PAYLOAD ):
+ """
+ Initialises a ProtocolMessage object.
+ """
+
+ payloadLen = len(payload)
+ if (payloadLen + paddingLen) > const.MPU:
+ raise base.PluggableTransportError("No overly long messages.")
+
+ self.totalLen = payloadLen + paddingLen
+ self.payloadLen = payloadLen
+ self.payload = payload
+ self.flags = flags
+
+ def encryptAndHMAC( self, crypter, hmacKey ):
+ """
+ Encrypt and authenticate this protocol message.
+
+ This protocol message is encrypted using `crypter' and authenticated
+ using `hmacKey'. Finally, the encrypted message prepended by a
+ HMAC-SHA256-128 is returned and ready to be sent over the wire.
+ """
+
+ encrypted = crypter.encrypt(pack.htons(self.totalLen) +
+ pack.htons(self.payloadLen) +
+ chr(self.flags) + self.payload +
+ (self.totalLen - self.payloadLen) * '\0')
+
+ hmac = mycrypto.HMAC_SHA256_128(hmacKey, encrypted)
+
+ return hmac + encrypted
+
+ def addPadding( self, paddingLen ):
+ """
+ Add padding to this protocol message.
+
+ Padding is added to this protocol message. The exact amount is
+ specified by `paddingLen'.
+ """
+
+ # The padding must not exceed the message size.
+ if (self.totalLen + paddingLen) > const.MPU:
+ raise base.PluggableTransportError("Can't pad more than the MTU.")
+
+ if paddingLen == 0:
+ return
+
+ log.debug("Adding %d bytes of padding to %d-byte message." %
+ (paddingLen, const.HDR_LENGTH + self.totalLen))
+ self.totalLen += paddingLen
+
+ def __len__( self ):
+ """
+ Return the length of this protocol message.
+ """
+
+ return const.HDR_LENGTH + self.totalLen
+
+# Alias class name in order to provide a more intuitive API.
+new = ProtocolMessage
+
+class MessageExtractor( object ):
+
+ """
+ Extracts ScrambleSuit protocol messages out of an encrypted stream.
+ """
+
+ def __init__( self ):
+ """
+ Initialise a new MessageExtractor object.
+ """
+
+ self.recvBuf = ""
+ self.totalLen = None
+ self.payloadLen = None
+ self.flags = None
+
+ def extract( self, data, aes, hmacKey ):
+ """
+ Extracts (i.e., decrypts and authenticates) protocol messages.
+
+ The raw `data' coming directly from the wire is decrypted using `aes'
+ and authenticated using `hmacKey'. The payload is then returned as
+ unencrypted protocol messages. In case of invalid headers or HMACs, an
+ exception is raised.
+ """
+
+ self.recvBuf += data
+ msgs = []
+
+ # Keep trying to unpack as long as there is at least a header.
+ while len(self.recvBuf) >= const.HDR_LENGTH:
+
+ # If necessary, extract the header fields.
+ if self.totalLen == self.payloadLen == self.flags == None:
+ self.totalLen = pack.ntohs(aes.decrypt(self.recvBuf[16:18]))
+ self.payloadLen = pack.ntohs(aes.decrypt(self.recvBuf[18:20]))
+ self.flags = ord(aes.decrypt(self.recvBuf[20]))
+
+ if not isSane(self.totalLen, self.payloadLen, self.flags):
+ raise base.PluggableTransportError("Invalid header.")
+
+ # Parts of the message are still on the wire; waiting.
+ if (len(self.recvBuf) - const.HDR_LENGTH) < self.totalLen:
+ break
+
+ rcvdHMAC = self.recvBuf[0:const.HMAC_SHA256_128_LENGTH]
+ vrfyHMAC = mycrypto.HMAC_SHA256_128(hmacKey,
+ self.recvBuf[const.HMAC_SHA256_128_LENGTH:
+ (self.totalLen + const.HDR_LENGTH)])
+
+ if rcvdHMAC != vrfyHMAC:
+ raise base.PluggableTransportError("Invalid message HMAC.")
+
+ # Decrypt the message and remove it from the input buffer.
+ extracted = aes.decrypt(self.recvBuf[const.HDR_LENGTH:
+ (self.totalLen + const.HDR_LENGTH)])[:self.payloadLen]
+ msgs.append(ProtocolMessage(payload=extracted, flags=self.flags))
+ self.recvBuf = self.recvBuf[const.HDR_LENGTH + self.totalLen:]
+
+ # Protocol message processed; now reset length fields.
+ self.totalLen = self.payloadLen = self.flags = None
+
+ return msgs
diff --git a/obfsproxy/transports/scramblesuit/mycrypto.py b/obfsproxy/transports/scramblesuit/mycrypto.py
new file mode 100644
index 0000000..a424795
--- /dev/null
+++ b/obfsproxy/transports/scramblesuit/mycrypto.py
@@ -0,0 +1,155 @@
+"""
+This module provides cryptographic functions not implemented in PyCrypto.
+
+The implemented algorithms include HKDF-SHA256, HMAC-SHA256-128, (CS)PRNGs and
+an interface for encryption and decryption using AES in counter mode.
+"""
+
+import Crypto.Hash.SHA256
+import Crypto.Hash.HMAC
+import Crypto.Util.Counter
+import Crypto.Cipher.AES
+
+import obfsproxy.transports.base as base
+import obfsproxy.common.log as logging
+
+import math
+import os
+
+import const
+
+log = logging.get_obfslogger()
+
+
+class HKDF_SHA256( object ):
+
+ """
+ Implements HKDF using SHA256: https://tools.ietf.org/html/rfc5869
+
+ This class only implements the `expand' but not the `extract' stage since
+ the provided PRK already exhibits strong entropy.
+ """
+
+ def __init__( self, prk, info="", length=32 ):
+ """
+ Initialise a HKDF_SHA256 object.
+ """
+
+ self.hashLen = const.SHA256_LENGTH
+
+ if length > (self.hashLen * 255):
+ raise ValueError("The OKM's length cannot be larger than %d." %
+ (self.hashLen * 255))
+
+ if len(prk) < self.hashLen:
+ raise ValueError("The PRK must be at least %d bytes in length "
+ "(%d given)." % (self.hashLen, len(prk)))
+
+ self.N = math.ceil(float(length) / self.hashLen)
+ self.prk = prk
+ self.info = info
+ self.length = length
+ self.ctr = 1
+ self.T = ""
+
+ def expand( self ):
+ """
+ Return the expanded output key material.
+
+ The output key material is calculated based on the given PRK, info and
+ L.
+ """
+
+ tmp = ""
+
+ # Prevent the accidental re-use of output keying material.
+ if len(self.T) > 0:
+ raise base.PluggableTransportError("HKDF-SHA256 OKM must not "
+ "be re-used by application.")
+
+ while self.length > len(self.T):
+ tmp = Crypto.Hash.HMAC.new(self.prk, tmp + self.info +
+ chr(self.ctr),
+ Crypto.Hash.SHA256).digest()
+ self.T += tmp
+ self.ctr += 1
+
+ return self.T[:self.length]
+
+
+def HMAC_SHA256_128( key, msg ):
+ """
+ Return the HMAC-SHA256-128 of the given `msg' authenticated by `key'.
+ """
+
+ assert(len(key) >= const.SHARED_SECRET_LENGTH)
+
+ h = Crypto.Hash.HMAC.new(key, msg, Crypto.Hash.SHA256)
+
+ # Return HMAC truncated to 128 out of 256 bits.
+ return h.digest()[:16]
+
+
+def strongRandom( size ):
+ """
+ Return `size' bytes of strong randomness suitable for cryptographic use.
+ """
+
+ return os.urandom(size)
+
+
+class PayloadCrypter:
+
+ """
+ Provides methods to encrypt data using AES in counter mode.
+
+ This class provides methods to set a session key as well as an
+ initialisation vector and to encrypt and decrypt data.
+ """
+
+ def __init__( self ):
+ """
+ Initialise a PayloadCrypter object.
+ """
+
+ log.debug("Initialising AES-CTR instance.")
+
+ self.sessionKey = None
+ self.crypter = None
+ self.counter = None
+
+ def setSessionKey( self, key, iv ):
+ """
+ Set AES' session key and the initialisation vector for counter mode.
+
+ The given `key' and `iv' are used as 256-bit AES key and as 128-bit
+ initialisation vector for counter mode. Both, the key as well as the
+ IV must come from a CSPRNG.
+ """
+
+ self.sessionKey = key
+
+ # Our 128-bit counter has the following format:
+ # [ 64-bit static and random IV ] [ 64-bit incrementing counter ]
+ # Counter wrapping is not allowed which makes it possible to transfer
+ # 2^64 * 16 bytes of data while avoiding counter reuse. That amount is
+ # effectively out of reach given today's networking performance.
+ log.debug("Setting IV for AES-CTR.")
+ self.counter = Crypto.Util.Counter.new(64,
+ prefix = iv,
+ initial_value = 1,
+ allow_wraparound = False)
+
+ log.debug("Setting session key for AES-CTR.")
+ self.crypter = Crypto.Cipher.AES.new(key, Crypto.Cipher.AES.MODE_CTR,
+ counter=self.counter)
+
+ def encrypt( self, data ):
+ """
+ Encrypts the given `data' using AES in counter mode.
+ """
+
+ return self.crypter.encrypt(data)
+
+ # Encryption equals decryption in AES-CTR.
+ decrypt = encrypt
diff --git a/obfsproxy/transports/scramblesuit/packetmorpher.py b/obfsproxy/transports/scramblesuit/packetmorpher.py
new file mode 100644
index 0000000..4a72f0b
--- /dev/null
+++ b/obfsproxy/transports/scramblesuit/packetmorpher.py
@@ -0,0 +1,69 @@
+"""
+Provides code to morph a chunk of data to a given probability distribution.
+
+The class provides an interface to morph a network packet's length to a
+previously generated probability distribution. The packet lengths of the
+morphed network data should then match the probability distribution.
+"""
+
+import random
+
+import probdist
+import const
+
+import obfsproxy.common.log as logging
+
+log = logging.get_obfslogger()
+
+class PacketMorpher( object ):
+
+ """
+ Implements methods to morph data to a target probability distribution.
+
+ This class is used to modify ScrambleSuit's packet length distribution on
+ the wire. The class provides a method to determine the padding for packets
+ smaller than the MTU.
+ """
+
+ def __init__( self, dist=None ):
+ """
+ Initialise the packet morpher with the given distribution `dist'.
+
+ If `dist' is `None', a new discrete probability distribution is
+ generated randomly.
+ """
+
+ if dist:
+ self.dist = dist
+ else:
+ self.dist = probdist.new(lambda: random.randint(const.HDR_LENGTH,
+ const.MTU))
+
+ def calcPadding( self, dataLen ):
+ """
+ Based on `dataLen', determines the padding for a network packet.
+
+ ScrambleSuit morphs packets which are smaller than the link's MTU.
+ This method draws a random sample from the probability distribution
+ which is used to determine and return the padding for such packets.
+ This effectively gets rid of Tor's 586-byte signature.
+ """
+
+ # The `is' and `should-be' length of the burst's last packet.
+ dataLen = dataLen % const.MTU
+ sampleLen = self.dist.randomSample()
+
+ # Now determine the padding length which is in {0..MTU-1}.
+ if sampleLen >= dataLen:
+ padLen = sampleLen - dataLen
+ else:
+ padLen = (const.MTU - dataLen) + sampleLen
+
+ log.debug("Morphing the last %d-byte packet to %d bytes by adding %d "
+ "bytes of padding." %
+ (dataLen % const.MTU, sampleLen, padLen))
+
+ return padLen
+
+# Alias class name in order to provide a more intuitive API.
+new = PacketMorpher
diff --git a/obfsproxy/transports/scramblesuit/probdist.py b/obfsproxy/transports/scramblesuit/probdist.py
new file mode 100644
index 0000000..620a662
--- /dev/null
+++ b/obfsproxy/transports/scramblesuit/probdist.py
@@ -0,0 +1,98 @@
+"""
+This module provides code to generate and sample probability distributions.
+
+The class RandProbDist provides an interface to randomly generate probability
+distributions. Random samples can then be drawn from these distributions.
+"""
+
+import random
+
+import const
+
+import obfsproxy.common.log as logging
+
+log = logging.get_obfslogger()
+
+
+class RandProbDist:
+
+ """
+ Provides code to generate, sample and dump probability distributions.
+ """
+
+ def __init__( self, genSingleton, seed=None ):
+ """
+ Initialise a discrete probability distribution.
+
+ The parameter `genSingleton' is expected to be a function which yields
+ singletons for the probability distribution. The optional `seed' can
+ be used to seed the PRNG so that the probability distribution is
+ generated deterministically.
+ """
+
+ self.prng = random if (seed is None) else random.Random(seed)
+
+ self.sampleList = []
+ self.dist = self.genDistribution(genSingleton)
+ self.dumpDistribution()
+
+ def genDistribution( self, genSingleton ):
+ """
+ Generate a discrete probability distribution.
+
+ The parameter `genSingleton' is a function which is used to generate
+ singletons for the probability distribution.
+ """
+
+ dist = {}
+
+ # Amount of distinct bins, i.e., packet lengths or inter arrival times.
+ bins = self.prng.randint(const.MIN_BINS, const.MAX_BINS)
+
+ # Cumulative probability of all bins.
+ cumulProb = 0
+
+ for _ in xrange(bins):
+ prob = self.prng.uniform(0, (1 - cumulProb))
+ cumulProb += prob
+
+ singleton = genSingleton()
+ dist[singleton] = prob
+ self.sampleList.append((cumulProb, singleton,))
+
+ dist[genSingleton()] = (1 - cumulProb)
+
+ return dist
+
+ def dumpDistribution( self ):
+ """
+ Dump the probability distribution using the logging object.
+
+ Only probabilities > 0.01 are dumped.
+ """
+
+ log.debug("Dumping probability distribution.")
+
+ for singleton in self.dist.iterkeys():
+ # We are not interested in tiny probabilities.
+ if self.dist[singleton] > 0.01:
+ log.debug("P(%s) = %.3f" %
+ (str(singleton), self.dist[singleton]))
+
+ def randomSample( self ):
+ """
+ Draw and return a random sample from the probability distribution.
+ """
+
+ assert len(self.sampleList) > 0
+
+ rand = random.random()
+
+ for cumulProb, singleton in self.sampleList:
+ if rand <= cumulProb:
+ return singleton
+
+ return self.sampleList[-1][1]
+
+# Alias class name in order to provide a more intuitive API.
+new = RandProbDist
diff --git a/obfsproxy/transports/scramblesuit/replay.py b/obfsproxy/transports/scramblesuit/replay.py
new file mode 100644
index 0000000..7643229
--- /dev/null
+++ b/obfsproxy/transports/scramblesuit/replay.py
@@ -0,0 +1,85 @@
+"""
+This module implements a mechanism to protect against replay attacks.
+
+The replay protection mechanism is based on a dictionary which caches
+previously observed keys. New keys can be added to the dictionary and existing
+ones can be queried. A pruning mechanism deletes expired keys from the
+dictionary.
+"""
+
+import time
+
+import const
+
+import obfsproxy.common.log as logging
+
+log = logging.get_obfslogger()
+
+
+class Tracker( object ):
+
+ """
+ Implement methods to keep track of replayed keys.
+
+ This class provides methods to add new keys (elements), check whether keys
+ are already present in the dictionary and to prune the lookup table.
+ """
+
+ def __init__( self ):
+ """
+ Initialise a `Tracker' object.
+ """
+
+ self.table = dict()
+
+ def addElement( self, element ):
+ """
+ Add the given `element' to the lookup table.
+ """
+
+ if self.isPresent(element):
+ raise LookupError("Element already present in table.")
+
+ # The key is a HMAC and the value is the current Unix timestamp.
+ self.table[element] = int(time.time())
+
+ def isPresent( self, element ):
+ """
+ Check if the given `element' is already present in the lookup table.
+
+ Return `True' if `element' is already in the lookup table and `False'
+ otherwise.
+ """
+
+ log.debug("Looking for existing element in size-%d lookup table." %
+ len(self.table))
+
+ # Prune the replay table before looking up the given `element'. This
+ # could be done more efficiently, e.g. by pruning every n minutes and
+ # only checking the timestamp of this particular element.
+ self.prune()
+
+ return (element in self.table)
+
+ def prune( self ):
+ """
+ Delete expired elements from the lookup table.
+
+ Keys whose Unix timestamps are older than `const.EPOCH_GRANULARITY' are
+ being removed from the lookup table.
+ """
+
+ log.debug("Pruning the replay table.")
+
+ deleteList = []
+ now = int(time.time())
+
+ for element in self.table.iterkeys():
+ if (now - self.table[element]) > const.EPOCH_GRANULARITY:
+ deleteList.append(element)
+
+ # We can't delete from a dictionary while iterating over it; therefore
+ # this construct.
+ for elem in deleteList:
+ log.debug("Deleting expired element.")
+ del self.table[elem]
diff --git a/obfsproxy/transports/scramblesuit/scramblesuit.py b/obfsproxy/transports/scramblesuit/scramblesuit.py
new file mode 100644
index 0000000..9262b34
--- /dev/null
+++ b/obfsproxy/transports/scramblesuit/scramblesuit.py
@@ -0,0 +1,624 @@
+"""
+The scramblesuit module implements the ScrambleSuit obfuscation protocol.
+
+The paper discussing the design and evaluation of the ScrambleSuit pluggable
+transport protocol is available here:
+http://www.cs.kau.se/philwint/scramblesuit/
+"""
+
+from twisted.internet import reactor
+
+import obfsproxy.transports.base as base
+import obfsproxy.common.log as logging
+
+import random
+import base64
+import yaml
+
+import probdist
+import mycrypto
+import message
+import const
+import util
+import packetmorpher
+import ticket
+import uniformdh
+import state
+import fifobuf
+
+
+log = logging.get_obfslogger()
+
+
+class ScrambleSuitTransport( base.BaseTransport ):
+
+ """
+ Implement the ScrambleSuit protocol.
+
+ The class implements methods which implement the ScrambleSuit protocol. A
+ large part of the protocol's functionality is outsources to different
+ modules.
+ """
+
+ def __init__( self ):
+ """
+ Initialise a ScrambleSuitTransport object.
+ """
+
+ log.error("\n\n################################################\n"
+ "Do NOT rely on ScrambleSuit for strong security!\n"
+ "################################################\n")
+
+ log.debug("Initialising %s." % const.TRANSPORT_NAME)
+
+ super(ScrambleSuitTransport, self).__init__()
+
+ # Load the server's persistent state from file.
+ if self.weAreServer:
+ self.srvState = state.load()
+
+ # Initialise the protocol's state machine.
+ log.debug("Switching to state ST_WAIT_FOR_AUTH.")
+ self.protoState = const.ST_WAIT_FOR_AUTH
+
+ # Buffer for outgoing data.
+ self.sendBuf = ""
+
+ # Buffer for inter-arrival time obfuscation.
+ self.choppingBuf = fifobuf.Buffer()
+
+ # AES instances to decrypt incoming and encrypt outgoing data.
+ self.sendCrypter = mycrypto.PayloadCrypter()
+ self.recvCrypter = mycrypto.PayloadCrypter()
+
+ # Packet morpher to modify the protocol's packet length distribution.
+ self.pktMorpher = packetmorpher.new(self.srvState.pktDist
+ if self.weAreServer else None)
+
+ # Inter-arrival time morpher to obfuscate inter arrival times.
+ self.iatMorpher = self.srvState.iatDist if self.weAreServer else \
+ probdist.new(lambda: random.random() %
+ const.MAX_PACKET_DELAY)
+
+ # Used to extract protocol messages from encrypted data.
+ self.protoMsg = message.MessageExtractor()
+
+ # Used by the server-side: `True' if the ticket is already
+ # decrypted but not yet authenticated.
+ self.decryptedTicket = False
+
+ # If we are in external mode we should already have a shared
+ # secret set up because of validate_external_mode_cli().
+ if self.weAreExternal:
+ assert(self.uniformDHSecret)
+
+ if self.weAreClient and not self.weAreExternal:
+ # As a client in managed mode, we get the shared secret
+ # from callback `handle_socks_args()' per-connection. Set
+ # the shared secret to None for now.
+ self.uniformDHSecret = None
+
+ self.uniformdh = uniformdh.new(self.uniformDHSecret, self.weAreServer)
+
+ @classmethod
+ def setup( cls, transportConfig ):
+ """
+ Called once when obfsproxy starts.
+ """
+
+ util.setStateLocation(transportConfig.getStateLocation())
+
+ cls.weAreClient = transportConfig.weAreClient
+ cls.weAreServer = not cls.weAreClient
+ cls.weAreExternal = transportConfig.weAreExternal
+
+ # If we are server and in managed mode, we should get the
+ # shared secret from the server transport options.
+ if cls.weAreServer and not cls.weAreExternal:
+ cfg = transportConfig.getServerTransportOptions()
+ if cfg and "password" in cfg:
+ cls.uniformDHSecret = base64.b32decode(util.sanitiseBase32(
+ cfg["password"]))
+ cls.uniformDHSecret = cls.uniformDHSecret.strip()
+
+ @classmethod
+ def get_public_server_options( cls, transportOptions ):
+ """
+ Return ScrambleSuit's BridgeDB parameters, i.e., the shared secret.
+
+ As a fallback mechanism, we return an automatically generated password
+ if the bridge operator did not use `ServerTransportOptions'.
+ """
+
+ log.debug("Tor's transport options: %s" % str(transportOptions))
+
+ if not "password" in transportOptions:
+ log.warning("No password found in transport options (use Tor's " \
+ "`ServerTransportOptions' to set your own password)." \
+ " Using automatically generated password instead.")
+ srv = state.load()
+ transportOptions = {"password":
+ base64.b32encode(srv.fallbackPassword)}
+ cls.uniformDHSecret = srv.fallbackPassword
+
+ return transportOptions
+
+ def deriveSecrets( self, masterKey ):
+ """
+ Derive various session keys from the given `masterKey'.
+
+ The argument `masterKey' is used to derive two session keys and nonces
+ for AES-CTR and two HMAC keys. The derivation is done using
+ HKDF-SHA256.
+ """
+
+ assert len(masterKey) == const.MASTER_KEY_LENGTH
+
+ log.debug("Deriving session keys from %d-byte master key." %
+ len(masterKey))
+
+ # We need key material for two symmetric AES-CTR keys, nonces and
+ # HMACs. In total, this equals 144 bytes of key material.
+ hkdf = mycrypto.HKDF_SHA256(masterKey, "", (32 * 4) + (8 * 2))
+ okm = hkdf.expand()
+ assert len(okm) >= ((32 * 4) + (8 * 2))
+
+ # Set AES-CTR keys and nonces for our two AES instances.
+ self.sendCrypter.setSessionKey(okm[0:32], okm[32:40])
+ self.recvCrypter.setSessionKey(okm[40:72], okm[72:80])
+
+ # Set the keys for the two HMACs protecting our data integrity.
+ self.sendHMAC = okm[80:112]
+ self.recvHMAC = okm[112:144]
+
+ if self.weAreServer:
+ self.sendHMAC, self.recvHMAC = self.recvHMAC, self.sendHMAC
+ self.sendCrypter, self.recvCrypter = self.recvCrypter, \
+ self.sendCrypter
+
+ def circuitConnected( self ):
+ """
+ Initiate a ScrambleSuit handshake.
+
+ This method is only relevant for clients since servers never initiate
+ handshakes. If a session ticket is available, it is redeemed.
+ Otherwise, a UniformDH handshake is conducted.
+ """
+
+ # The server handles the handshake passively.
+ if self.weAreServer:
+ return
+
+ # The preferred authentication mechanism is a session ticket.
+ bridge = self.circuit.downstream.transport.getPeer()
+ storedTicket = ticket.findStoredTicket(bridge)
+
+ if storedTicket is not None:
+ log.debug("Redeeming stored session ticket.")
+ (masterKey, rawTicket) = storedTicket
+ self.deriveSecrets(masterKey)
+ self.circuit.downstream.write(ticket.createTicketMessage(rawTicket,
+ self.sendHMAC))
+
+ # We switch to ST_CONNECTED opportunistically since we don't know
+ # yet whether the server accepted the ticket.
+ log.debug("Switching to state ST_CONNECTED.")
+ self.protoState = const.ST_CONNECTED
+
+ self.flushSendBuffer()
+
+ # Conduct an authenticated UniformDH handshake if there's no ticket.
+ else:
+ log.debug("No session ticket to redeem. Running UniformDH.")
+ self.circuit.downstream.write(self.uniformdh.createHandshake())
+
+ def sendRemote( self, data, flags=const.FLAG_PAYLOAD ):
+ """
+ Send data to the remote end after a connection was established.
+
+ The given `data' is first encapsulated in protocol messages. Then, the
+ protocol message(s) are sent over the wire. The argument `flags'
+ specifies the protocol message flags with the default flags signalling
+ payload.
+ """
+
+ log.debug("Processing %d bytes of outgoing data." % len(data))
+
+ # Wrap the application's data in ScrambleSuit protocol messages.
+ messages = message.createProtocolMessages(data, flags=flags)
+
+ # Let the packet morpher tell us how much we should pad.
+ paddingLen = self.pktMorpher.calcPadding(sum([len(msg) for
+ msg in messages]))
+
+ # If padding > header length, a single message will do...
+ if paddingLen > const.HDR_LENGTH:
+ messages.append(message.new("", paddingLen=paddingLen -
+ const.HDR_LENGTH))
+
+ # ...otherwise, we use two padding-only messages.
+ else:
+ messages.append(message.new("", paddingLen=const.MPU -
+ const.HDR_LENGTH))
+ messages.append(message.new("", paddingLen=paddingLen))
+
+ blurb = "".join([msg.encryptAndHMAC(self.sendCrypter,
+ self.sendHMAC) for msg in messages])
+
+ # Flush data chunk for chunk to obfuscate inter arrival times.
+ if const.USE_IAT_OBFUSCATION:
+ if len(self.choppingBuf) == 0:
+ self.choppingBuf.write(blurb)
+ reactor.callLater(self.iatMorpher.randomSample(),
+ self.flushPieces)
+ else:
+ # flushPieces() is still busy processing the chopping buffer.
+ self.choppingBuf.write(blurb)
+ else:
+ self.circuit.downstream.write(blurb)
+
+ def flushPieces( self ):
+ """
+ Write the application data in chunks to the wire.
+
+ The cached data is sent over the wire in chunks. After every write
+ call, control is given back to the Twisted reactor so it has a chance
+ to flush the data. Shortly thereafter, this function is called again
+ to write the next chunk of data. The delays in between subsequent
+ write calls are controlled by the inter-arrival time obfuscator.
+ """
+
+ # Drain and send an MTU-sized chunk from the chopping buffer.
+ if len(self.choppingBuf) > const.MTU:
+
+ self.circuit.downstream.write(self.choppingBuf.read(const.MTU))
+
+ # Drain and send whatever is left in the output buffer.
+ else:
+ self.circuit.downstream.write(self.choppingBuf.read())
+ return
+
+ reactor.callLater(self.iatMorpher.randomSample(), self.flushPieces)
+
+ def processMessages( self, data ):
+ """
+ Acts on extracted protocol messages based on header flags.
+
+ After the incoming `data' is decrypted and authenticated, this method
+ processes the received data based on the header flags. Payload is
+ written to the local application, new tickets are stored, or keys are
+ added to the replay table.
+ """
+
+ if (data is None) or (len(data) == 0):
+ return
+
+ # Try to extract protocol messages from the encrypted blurb.
+ msgs = self.protoMsg.extract(data, self.recvCrypter, self.recvHMAC)
+ if (msgs is None) or (len(msgs) == 0):
+ return
+
+ for msg in msgs:
+ # Forward data to the application.
+ if msg.flags == const.FLAG_PAYLOAD:
+ self.circuit.upstream.write(msg.payload)
+
+ # Store newly received ticket.
+ elif self.weAreClient and (msg.flags == const.FLAG_NEW_TICKET):
+ assert len(msg.payload) == (const.TICKET_LENGTH +
+ const.MASTER_KEY_LENGTH)
+ peer = self.circuit.downstream.transport.getPeer()
+ ticket.storeNewTicket(msg.payload[0:const.MASTER_KEY_LENGTH],
+ msg.payload[const.MASTER_KEY_LENGTH:
+ const.MASTER_KEY_LENGTH +
+ const.TICKET_LENGTH], peer)
+
+ # Use the PRNG seed to generate the same probability distributions
+ # as the server. That's where the polymorphism comes from.
+ elif self.weAreClient and (msg.flags == const.FLAG_PRNG_SEED):
+ assert len(msg.payload) == const.PRNG_SEED_LENGTH
+ log.debug("Obtained PRNG seed.")
+ prng = random.Random(msg.payload)
+ pktDist = probdist.new(lambda: prng.randint(const.HDR_LENGTH,
+ const.MTU),
+ seed=msg.payload)
+ self.pktMorpher = packetmorpher.new(pktDist)
+ self.iatMorpher = probdist.new(lambda: prng.random() %
+ const.MAX_PACKET_DELAY,
+ seed=msg.payload)
+
+ else:
+ log.warning("Invalid message flags: %d." % msg.flags)
+
+ def flushSendBuffer( self ):
+ """
+ Flush the application's queued data.
+
+ The application could have sent data while we were busy authenticating
+ the remote machine. This method flushes the data which could have been
+ queued in the meanwhile in `self.sendBuf'.
+ """
+
+ if len(self.sendBuf) == 0:
+ log.debug("Send buffer is empty; nothing to flush.")
+ return
+
+ # Flush the buffered data, the application is so eager to send.
+ log.debug("Flushing %d bytes of buffered application data." %
+ len(self.sendBuf))
+
+ self.sendRemote(self.sendBuf)
+ self.sendBuf = ""
+
+ def receiveTicket( self, data ):
+ """
+ Extract and verify a potential session ticket.
+
+ The given `data' is treated as a session ticket. The ticket is being
+ decrypted and authenticated (yes, in that order). If all these steps
+ succeed, `True' is returned. Otherwise, `False' is returned.
+ """
+
+ if len(data) < (const.TICKET_LENGTH + const.MARK_LENGTH +
+ const.HMAC_SHA256_128_LENGTH):
+ return False
+
+ potentialTicket = data.peek()
+
+ # Now try to decrypt and parse the ticket. We need the master key
+ # inside to verify the HMAC in the next step.
+ if not self.decryptedTicket:
+ newTicket = ticket.decrypt(potentialTicket[:const.TICKET_LENGTH],
+ self.srvState)
+ if newTicket != None and newTicket.isValid():
+ self.deriveSecrets(newTicket.masterKey)
+ self.decryptedTicket = True
+ else:
+ return False
+
+ # First, find the mark to efficiently locate the HMAC.
+ mark = mycrypto.HMAC_SHA256_128(self.recvHMAC,
+ potentialTicket[:const.TICKET_LENGTH])
+
+ index = util.locateMark(mark, potentialTicket)
+ if not index:
+ return False
+
+ # Now, verify if the HMAC is valid.
+ existingHMAC = potentialTicket[index + const.MARK_LENGTH:
+ index + const.MARK_LENGTH +
+ const.HMAC_SHA256_128_LENGTH]
+ myHMAC = mycrypto.HMAC_SHA256_128(self.recvHMAC,
+ potentialTicket[0:
+ index + const.MARK_LENGTH] +
+ util.getEpoch())
+
+ if not util.isValidHMAC(myHMAC, existingHMAC, self.recvHMAC):
+ log.warning("The HMAC is invalid: `%s' vs. `%s'." %
+ (myHMAC.encode('hex'), existingHMAC.encode('hex')))
+ return False
+
+ # Do nothing if the ticket is replayed. Immediately closing the
+ # connection would be suspicious.
+ if self.srvState.isReplayed(existingHMAC):
+ log.warning("The HMAC was already present in the replay table.")
+ return False
+
+ data.drain(index + const.MARK_LENGTH + const.HMAC_SHA256_128_LENGTH)
+
+ log.debug("Adding the HMAC authenticating the ticket message to the " \
+ "replay table: %s." % existingHMAC.encode('hex'))
+ self.srvState.registerKey(existingHMAC)
+
+ log.debug("Switching to state ST_CONNECTED.")
+ self.protoState = const.ST_CONNECTED
+
+ return True
+
+ def receivedUpstream( self, data ):
+ """
+ Sends data to the remote machine or queues it to be sent later.
+
+ Depending on the current protocol state, the given `data' is either
+ directly sent to the remote machine or queued. The buffer is then
+ flushed once, a connection is established.
+ """
+
+ if self.protoState == const.ST_CONNECTED:
+ self.sendRemote(data.read())
+
+ # Buffer data we are not ready to transmit yet.
+ else:
+ self.sendBuf += data.read()
+ log.debug("Buffered %d bytes of outgoing data." %
+ len(self.sendBuf))
+
+ def sendTicketAndSeed( self ):
+ """
+ Send a session ticket and the PRNG seed to the client.
+
+ This method is only called by the server after successful
+ authentication. Finally, the server's send buffer is flushed.
+ """
+
+ log.debug("Sending a new session ticket and the PRNG seed to the " \
+ "client.")
+
+ self.sendRemote(ticket.issueTicketAndKey(self.srvState),
+ flags=const.FLAG_NEW_TICKET)
+ self.sendRemote(self.srvState.prngSeed,
+ flags=const.FLAG_PRNG_SEED)
+ self.flushSendBuffer()
+
+ def receivedDownstream( self, data ):
+ """
+ Receives and processes data coming from the remote machine.
+
+ The incoming `data' is dispatched depending on the current protocol
+ state and whether we are the client or the server. The data is either
+ payload or authentication data.
+ """
+
+ if self.weAreServer and (self.protoState == const.ST_WAIT_FOR_AUTH):
+
+ # First, try to interpret the incoming data as session ticket.
+ if self.receiveTicket(data):
+ log.debug("Ticket authentication succeeded.")
+
+ self.sendTicketAndSeed()
+
+ # Second, interpret the data as a UniformDH handshake.
+ elif self.uniformdh.receivePublicKey(data, self.deriveSecrets,
+ self.srvState):
+ # Now send the server's UniformDH public key to the client.
+ handshakeMsg = self.uniformdh.createHandshake()
+
+ log.debug("Sending %d bytes of UniformDH handshake and "
+ "session ticket." % len(handshakeMsg))
+
+ self.circuit.downstream.write(handshakeMsg)
+ log.debug("UniformDH authentication succeeded.")
+
+ log.debug("Switching to state ST_CONNECTED.")
+ self.protoState = const.ST_CONNECTED
+
+ self.sendTicketAndSeed()
+
+ else:
+ log.debug("Authentication unsuccessful so far. "
+ "Waiting for more data.")
+ return
+
+ elif self.weAreClient and (self.protoState == const.ST_WAIT_FOR_AUTH):
+
+ if not self.uniformdh.receivePublicKey(data, self.deriveSecrets):
+ log.debug("Unable to finish UniformDH handshake just yet.")
+ return
+
+ log.debug("UniformDH authentication succeeded.")
+
+ log.debug("Switching to state ST_CONNECTED.")
+ self.protoState = const.ST_CONNECTED
+ self.flushSendBuffer()
+
+ if self.protoState == const.ST_CONNECTED:
+
+ self.processMessages(data.read())
+
+ @classmethod
+ def register_external_mode_cli( cls, subparser ):
+ """
+ Register a CLI arguments to pass a secret or ticket to ScrambleSuit.
+
+ Two options are made available over the command line interface: one to
+ specify a ticket file and one to specify a UniformDH shared secret.
+ """
+
+ subparser.add_argument("--password",
+ required=True,
+ type=str,
+ help="Shared secret for UniformDH",
+ dest="uniformDHSecret")
+
+ super(ScrambleSuitTransport, cls).register_external_mode_cli(subparser)
+
+ @classmethod
+ def validate_external_mode_cli( cls, args ):
+ """
+ Assign the given command line arguments to local variables.
+ """
+
+ uniformDHSecret = None
+
+ try:
+ uniformDHSecret = base64.b32decode(util.sanitiseBase32(
+ args.uniformDHSecret))
+ except (TypeError, AttributeError) as error:
+ log.error(error.message)
+ raise base.PluggableTransportError(
+ "UniformDH password '%s' isn't valid base32!"
+ % args.uniformDHSecret)
+
+ parentalApproval = super(
+ ScrambleSuitTransport, cls).validate_external_mode_cli(args)
+ if not parentalApproval:
+ # XXX not very descriptive nor helpful, but the parent class only
+ # returns a boolean without telling us what's wrong.
+ raise base.PluggableTransportError(
+ "Pluggable Transport args invalid: %s" % args )
+
+ if uniformDHSecret:
+ rawLength = len(uniformDHSecret)
+ if rawLength != const.SHARED_SECRET_LENGTH:
+ raise base.PluggableTransportError(
+ "The UniformDH password must be %d bytes in length, ",
+ "but %d bytes are given."
+ % (const.SHARED_SECRET_LENGTH, rawLength))
+ else:
+ cls.uniformDHSecret = uniformDHSecret
+
+ def handle_socks_args( self, args ):
+ """
+ Receive arguments `args' passed over a SOCKS connection.
+
+ The SOCKS authentication mechanism is (ab)used to pass arguments to
+ pluggable transports. This method receives these arguments and parses
+ them. As argument, we only expect a UniformDH shared secret.
+ """
+
+ log.debug("Received the following arguments over SOCKS: %s." % args)
+
+ if len(args) != 1:
+ raise base.SOCKSArgsError("Too many SOCKS arguments "
+ "(expected 1 but got %d)." % len(args))
+
+ # The ScrambleSuit specification defines that the shared secret is
+ # called "password".
+ if not args[0].startswith("password="):
+ raise base.SOCKSArgsError("The SOCKS argument must start with "
+ "`password='.")
+
+ # A shared secret might already be set if obfsproxy is in external
+ # mode.
+ if self.uniformDHSecret:
+ log.warning("A UniformDH password was already specified over "
+ "the command line. Using the SOCKS secret instead.")
+
+ self.uniformDHSecret = base64.b32decode(util.sanitiseBase32(
+ args[0].split('=')[1].strip()))
+
+ rawLength = len(self.uniformDHSecret)
+ if rawLength != const.SHARED_SECRET_LENGTH:
+ raise base.PluggableTransportError("The UniformDH password "
+ "must be %d bytes in length but %d bytes are given." %
+ (const.SHARED_SECRET_LENGTH, rawLength))
+
+ self.uniformdh = uniformdh.new(self.uniformDHSecret, self.weAreServer)
+
+
+class ScrambleSuitClient( ScrambleSuitTransport ):
+
+ """
+ Extend the ScrambleSuit class.
+ """
+
+ def __init__( self ):
+ """
+ Initialise a ScrambleSuitClient object.
+ """
+
+ ScrambleSuitTransport.__init__(self)
+
+
+class ScrambleSuitServer( ScrambleSuitTransport ):
+
+ """
+ Extend the ScrambleSuit class.
+ """
+
+ def __init__( self ):
+ """
+ Initialise a ScrambleSuitServer object.
+ """
+
+ ScrambleSuitTransport.__init__(self)
diff --git a/obfsproxy/transports/scramblesuit/state.py b/obfsproxy/transports/scramblesuit/state.py
new file mode 100644
index 0000000..9d32d0c
--- /dev/null
+++ b/obfsproxy/transports/scramblesuit/state.py
@@ -0,0 +1,161 @@
+"""
+Provide a way to store the server's state information on disk.
+
+The server possesses state information which should persist across runs. This
+includes key material to encrypt and authenticate session tickets, replay
+tables and PRNG seeds. This module provides methods to load, store and
+generate such state information.
+"""
+
+import os
+import sys
+import time
+import cPickle
+import random
+
+import const
+import replay
+import mycrypto
+import probdist
+
+import obfsproxy.common.log as logging
+
+log = logging.get_obfslogger()
+
+def load( ):
+ """
+ Load the server's state object from file.
+
+ The server's state file is loaded and the state object returned. If no
+ state file is found, a new one is created and returned.
+ """
+
+ stateFile = const.STATE_LOCATION + const.SERVER_STATE_FILE
+
+ log.info("Attempting to load the server's state file from `%s'." %
+ stateFile)
+
+ if not os.path.exists(stateFile):
+ log.info("The server's state file does not exist (yet).")
+ state = State()
+ state.genState()
+ return state
+
+ try:
+ with open(stateFile, 'r') as fd:
+ stateObject = cPickle.load(fd)
+ except IOError as err:
+ log.error("Error reading server state file from `%s': %s" %
+ (stateFile, err))
+ sys.exit(1)
+
+ return stateObject
+
+class State( object ):
+
+ """
+ Implement a state class which stores the server's state.
+
+ This class makes it possible to store state information on disk. It
+ provides methods to generate and write state information.
+ """
+
+ def __init__( self ):
+ """
+ Initialise a `State' object.
+ """
+
+ self.prngSeed = None
+ self.keyCreation = None
+ self.hmacKey = None
+ self.aesKey = None
+ self.oldHmacKey = None
+ self.oldAesKey = None
+ self.ticketReplay = None
+ self.uniformDhReplay = None
+ self.pktDist = None
+ self.iatDist = None
+ self.fallbackPassword = None
+
+ def genState( self ):
+ """
+ Populate all the local variables with values.
+ """
+
+ log.info("Generating parameters for the server's state file.")
+
+ # PRNG seed for the client to reproduce the packet and IAT morpher.
+ self.prngSeed = mycrypto.strongRandom(const.PRNG_SEED_LENGTH)
+
+ # HMAC and AES key used to encrypt and authenticate tickets.
+ self.hmacKey = mycrypto.strongRandom(const.TICKET_HMAC_KEY_LENGTH)
+ self.aesKey = mycrypto.strongRandom(const.TICKET_AES_KEY_LENGTH)
+ self.keyCreation = int(time.time())
+
+ # The previous HMAC and AES keys.
+ self.oldHmacKey = None
+ self.oldAesKey = None
+
+ # Replay dictionary for both authentication mechanisms.
+ self.replayTracker = replay.Tracker()
+
+ # Distributions for packet lengths and inter arrival times.
+ prng = random.Random(self.prngSeed)
+ self.pktDist = probdist.new(lambda: prng.randint(const.HDR_LENGTH,
+ const.MTU),
+ seed=self.prngSeed)
+ self.iatDist = probdist.new(lambda: prng.random() %
+ const.MAX_PACKET_DELAY,
+ seed=self.prngSeed)
+
+ # Fallback UniformDH shared secret. Only used if the bridge operator
+ # did not set `ServerTransportOptions'.
+ self.fallbackPassword = os.urandom(const.SHARED_SECRET_LENGTH)
+
+ self.writeState()
+
+ def isReplayed( self, hmac ):
+ """
+ Check if `hmac' is present in the replay table.
+
+ Return `True' if the given `hmac' is present in the replay table and
+ `False' otherwise.
+ """
+
+ assert self.replayTracker is not None
+
+ log.debug("Querying if HMAC is present in the replay table.")
+
+ return self.replayTracker.isPresent(hmac)
+
+ def registerKey( self, hmac ):
+ """
+ Add the given `hmac' to the replay table.
+ """
+
+ assert self.replayTracker is not None
+
+ log.debug("Adding a new HMAC to the replay table.")
+ self.replayTracker.addElement(hmac)
+
+ # We must write the data to disk immediately so that other ScrambleSuit
+ # connections can share the same state.
+ self.writeState()
+
+ def writeState( self ):
+ """
+ Write the state object to a file using the `cPickle' module.
+ """
+
+ stateFile = const.STATE_LOCATION + const.SERVER_STATE_FILE
+
+ log.debug("Writing server's state file to `%s'." %
+ stateFile)
+
+ try:
+ with open(stateFile, 'w') as fd:
+ cPickle.dump(self, fd)
+ except IOError as err:
+ log.error("Error writing state file to `%s': %s" %
+ (stateFile, err))
+ sys.exit(1)
diff --git a/obfsproxy/transports/scramblesuit/ticket.py b/obfsproxy/transports/scramblesuit/ticket.py
new file mode 100644
index 0000000..8599936
--- /dev/null
+++ b/obfsproxy/transports/scramblesuit/ticket.py
@@ -0,0 +1,391 @@
+#!/usr/bin/env python
+
+"""
+This module provides a session ticket mechanism.
+
+The implemented mechanism is a subset of session tickets as proposed for
+TLS in RFC 5077.
+
+The format of a 112-byte ticket is:
+ +------------+------------------+--------------+
+ | 16-byte IV | 64-byte E(state) | 32-byte HMAC |
+ +------------+------------------+--------------+
+
+The 64-byte encrypted state contains:
+ +-------------------+--------------------+--------------------+-------------+
+ | 4-byte issue date | 18-byte identifier | 32-byte master key | 10-byte pad |
+ +-------------------+--------------------+--------------------+-------------+
+"""
+
+import os
+import time
+import const
+import yaml
+import struct
+import random
+import datetime
+
+from Crypto.Cipher import AES
+from Crypto.Hash import HMAC
+from Crypto.Hash import SHA256
+from twisted.internet.address import IPv4Address
+
+import obfsproxy.common.log as logging
+
+import mycrypto
+import util
+import state
+
+log = logging.get_obfslogger()
+
+
+def createTicketMessage( rawTicket, HMACKey ):
+ """
+ Create and return a ready-to-be-sent ticket authentication message.
+
+ Pseudo-random padding and a mark are added to `rawTicket' and the result is
+ then authenticated using `HMACKey' as key for a HMAC. The resulting
+ authentication message is then returned.
+ """
+
+ assert len(rawTicket) == const.TICKET_LENGTH
+ assert len(HMACKey) == const.TICKET_HMAC_KEY_LENGTH
+
+ # Subtract the length of the ticket to make the handshake on
+ # average as long as a UniformDH handshake message.
+ padding = mycrypto.strongRandom(random.randint(0,
+ const.MAX_PADDING_LENGTH -
+ const.TICKET_LENGTH))
+
+ mark = mycrypto.HMAC_SHA256_128(HMACKey, rawTicket)
+
+ hmac = mycrypto.HMAC_SHA256_128(HMACKey, rawTicket + padding +
+ mark + util.getEpoch())
+
+ return rawTicket + padding + mark + hmac
+
+
+def issueTicketAndKey( srvState ):
+ """
+ Issue a new session ticket and append it to the according master key.
+
+ The parameter `srvState' contains the key material and is passed on to
+ `SessionTicket'. The returned ticket and key are ready to be wrapped into
+ a protocol message with the flag FLAG_NEW_TICKET set.
+ """
+
+ log.info("Issuing new session ticket and master key.")
+ masterKey = mycrypto.strongRandom(const.MASTER_KEY_LENGTH)
+ newTicket = (SessionTicket(masterKey, srvState)).issue()
+
+ return masterKey + newTicket
+
+
+def storeNewTicket( masterKey, ticket, bridge ):
+ """
+ Store a new session ticket and the according master key for future use.
+
+ This method is only called by clients. The given data, `masterKey',
+ `ticket' and `bridge', is YAMLed and stored in the global ticket
+ dictionary. If there already is a ticket for the given `bridge', it is
+ overwritten.
+ """
+
+ assert len(masterKey) == const.MASTER_KEY_LENGTH
+ assert len(ticket) == const.TICKET_LENGTH
+
+ ticketFile = const.STATE_LOCATION + const.CLIENT_TICKET_FILE
+
+ log.debug("Storing newly received ticket in `%s'." % ticketFile)
+
+ # Add a new (key, ticket) tuple with the given bridge as hash key.
+ tickets = dict()
+ content = util.readFromFile(ticketFile)
+ if (content is not None) and (len(content) > 0):
+ tickets = yaml.safe_load(content)
+
+ # We also store a timestamp so we later know if our ticket already expired.
+ tickets[str(bridge)] = [int(time.time()), masterKey, ticket]
+ util.writeToFile(yaml.dump(tickets), ticketFile)
+
+
+def findStoredTicket( bridge ):
+ """
+ Retrieve a previously stored ticket from the ticket dictionary.
+
+ The global ticket dictionary is loaded and the given `bridge' is used to
+ look up the ticket and the master key. If the ticket dictionary does not
+ exist (yet) or the ticket data could not be found, `None' is returned.
+ """
+
+ assert bridge
+
+ ticketFile = const.STATE_LOCATION + const.CLIENT_TICKET_FILE
+
+ log.debug("Attempting to read master key and ticket from file `%s'." %
+ ticketFile)
+
+ # Load the ticket hash table from file.
+ yamlBlurb = util.readFromFile(ticketFile)
+ if (yamlBlurb is None) or (len(yamlBlurb) == 0):
+ return None
+ tickets = yaml.safe_load(yamlBlurb)
+
+ try:
+ timestamp, masterKey, ticket = tickets[str(bridge)]
+ except KeyError:
+ log.info("Found no ticket for bridge `%s'." % str(bridge))
+ return None
+
+ # We can remove the ticket now since we are about to redeem it.
+ log.debug("Deleting ticket since it is about to be redeemed.")
+ del tickets[str(bridge)]
+ util.writeToFile(yaml.dump(tickets), ticketFile)
+
+ # If our ticket is expired, we can't redeem it.
+ ticketAge = int(time.time()) - timestamp
+ if ticketAge > const.SESSION_TICKET_LIFETIME:
+ log.warning("We did have a ticket but it already expired %s ago." %
+ str(datetime.timedelta(seconds=
+ (ticketAge - const.SESSION_TICKET_LIFETIME))))
+ return None
+
+ return (masterKey, ticket)
+
+
+def checkKeys( srvState ):
+ """
+ Check whether the key material for session tickets must be rotated.
+
+ The key material (i.e., AES and HMAC keys for session tickets) contained in
+ `srvState' is checked if it needs to be rotated. If so, the old keys are
+ stored and new ones are created.
+ """
+
+ assert (srvState.hmacKey is not None) and \
+ (srvState.aesKey is not None) and \
+ (srvState.keyCreation is not None)
+
+ if (int(time.time()) - srvState.keyCreation) > const.KEY_ROTATION_TIME:
+ log.info("Rotating server key material for session tickets.")
+
+ # Save expired keys to be able to validate old tickets.
+ srvState.oldAesKey = srvState.aesKey
+ srvState.oldHmacKey = srvState.hmacKey
+
+ # Create new key material...
+ srvState.aesKey = mycrypto.strongRandom(const.TICKET_AES_KEY_LENGTH)
+ srvState.hmacKey = mycrypto.strongRandom(const.TICKET_HMAC_KEY_LENGTH)
+ srvState.creationTime = int(time.time())
+
+ # ...and save it to disk.
+ srvState.writeState()
+
+
+def decrypt( ticket, srvState ):
+ """
+ Decrypts, verifies and returns the given `ticket'.
+
+ The key material used to verify the ticket is contained in `srvState'.
+ First, the HMAC over the ticket is verified. If it is valid, the ticket is
+ decrypted. Finally, a `ProtocolState()' object containing the master key
+ and the ticket's issue date is returned. If any of these steps fail,
+ `None' is returned.
+ """
+
+ assert (ticket is not None) and (len(ticket) == const.TICKET_LENGTH)
+ assert (srvState.hmacKey is not None) and (srvState.aesKey is not None)
+
+ log.debug("Attempting to decrypt and verify ticket.")
+
+ checkKeys(srvState)
+
+ # Verify the ticket's authenticity before decrypting.
+ hmac = HMAC.new(srvState.hmacKey, ticket[0:80], digestmod=SHA256).digest()
+ if util.isValidHMAC(hmac, ticket[80:const.TICKET_LENGTH],
+ srvState.hmacKey):
+ aesKey = srvState.aesKey
+ else:
+ if srvState.oldHmacKey is None:
+ return None
+
+ # Was the HMAC created using the rotated key material?
+ oldHmac = HMAC.new(srvState.oldHmacKey, ticket[0:80],
+ digestmod=SHA256).digest()
+ if util.isValidHMAC(oldHmac, ticket[80:const.TICKET_LENGTH],
+ srvState.oldHmacKey):
+ aesKey = srvState.oldAesKey
+ else:
+ return None
+
+ # Decrypt the ticket to extract the state information.
+ aes = AES.new(aesKey, mode=AES.MODE_CBC,
+ IV=ticket[0:const.TICKET_AES_CBC_IV_LENGTH])
+ plainTicket = aes.decrypt(ticket[const.TICKET_AES_CBC_IV_LENGTH:80])
+
+ issueDate = struct.unpack('I', plainTicket[0:4])[0]
+ identifier = plainTicket[4:22]
+ masterKey = plainTicket[22:54]
+
+ if not (identifier == const.TICKET_IDENTIFIER):
+ log.error("The ticket's HMAC is valid but the identifier is invalid. "
+ "The ticket could be corrupt.")
+ return None
+
+ return ProtocolState(masterKey, issueDate=issueDate)
+
+
+class ProtocolState( object ):
+
+ """
+ Defines a ScrambleSuit protocol state contained in a session ticket.
+
+ A protocol state is essentially a master key which can then be used by the
+ server to derive session keys. Besides, a state object contains an issue
+ date which specifies the expiry date of a ticket. This class contains
+ methods to check the expiry status of a ticket and to dump it in its raw
+ form.
+ """
+
+ def __init__( self, masterKey, issueDate=int(time.time()) ):
+ """
+ The constructor of the `ProtocolState' class.
+
+ The four class variables are initialised.
+ """
+
+ self.identifier = const.TICKET_IDENTIFIER
+ self.masterKey = masterKey
+ self.issueDate = issueDate
+ # Pad to multiple of 16 bytes to match AES' block size.
+ self.pad = "\0\0\0\0\0\0\0\0\0\0"
+
+ def isValid( self ):
+ """
+ Verifies the expiry date of the object's issue date.
+
+ If the expiry date is not yet reached and the protocol state is still
+ valid, `True' is returned. If the protocol state has expired, `False'
+ is returned.
+ """
+
+ assert self.issueDate
+
+ lifetime = int(time.time()) - self.issueDate
+ if lifetime > const.SESSION_TICKET_LIFETIME:
+ log.debug("The ticket is invalid and expired %s ago." %
+ str(datetime.timedelta(seconds=
+ (lifetime - const.SESSION_TICKET_LIFETIME))))
+ return False
+
+ log.debug("The ticket is still valid for %s." %
+ str(datetime.timedelta(seconds=
+ (const.SESSION_TICKET_LIFETIME - lifetime))))
+ return True
+
+ def __repr__( self ):
+ """
+ Return a raw string representation of the object's protocol state.
+
+ The length of the returned representation is exactly 64 bytes; a
+ multiple of AES' 16-byte block size. That makes it suitable to be
+ encrypted using AES-CBC.
+ """
+
+ return struct.pack('I', self.issueDate) + self.identifier + \
+ self.masterKey + self.pad
+
+
+class SessionTicket( object ):
+
+ """
+ Encrypts and authenticates an encapsulated `ProtocolState()' object.
+
+ This class implements a session ticket which can be redeemed by clients.
+ The class contains methods to initialise and issue session tickets.
+ """
+
+ def __init__( self, masterKey, srvState ):
+ """
+ The constructor of the `SessionTicket()' class.
+
+ The class variables are initialised and the validity of the symmetric
+ keys for the session tickets is checked.
+ """
+
+ assert (masterKey is not None) and \
+ len(masterKey) == const.MASTER_KEY_LENGTH
+
+ checkKeys(srvState)
+
+ # Initialisation vector for AES-CBC.
+ self.IV = mycrypto.strongRandom(const.TICKET_AES_CBC_IV_LENGTH)
+
+ # The server's (encrypted) protocol state.
+ self.state = ProtocolState(masterKey)
+
+ # AES and HMAC keys to encrypt and authenticate the ticket.
+ self.symmTicketKey = srvState.aesKey
+ self.hmacTicketKey = srvState.hmacKey
+
+ def issue( self ):
+ """
+ Returns a ready-to-use session ticket after prior initialisation.
+
+ After the `SessionTicket()' class was initialised with a master key,
+ this method encrypts and authenticates the protocol state and returns
+ the final result which is ready to be sent over the wire.
+ """
+
+ self.state.issueDate = int(time.time())
+
+ # Encrypt the protocol state.
+ aes = AES.new(self.symmTicketKey, mode=AES.MODE_CBC, IV=self.IV)
+ state = repr(self.state)
+ assert (len(state) % AES.block_size) == 0
+ cryptedState = aes.encrypt(state)
+
+ # Authenticate the encrypted state and the IV.
+ hmac = HMAC.new(self.hmacTicketKey,
+ self.IV + cryptedState, digestmod=SHA256).digest()
+
+ finalTicket = self.IV + cryptedState + hmac
+ log.debug("Returning %d-byte ticket." % len(finalTicket))
+
+ return finalTicket
+
+
+# Alias class name in order to provide a more intuitive API.
+new = SessionTicket
+
+
+# Give ScrambleSuit server operators a way to manually issue new session
+# tickets for out-of-band distribution.
+if __name__ == "__main__":
+
+ import argparse
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("ip_addr", type=str, help="The IPv4 address of the "
+ "%s server." % const.TRANSPORT_NAME)
+ parser.add_argument("tcp_port", type=int, help="The TCP port of the %s "
+ "server." % const.TRANSPORT_NAME)
+ parser.add_argument("ticket_file", type=str, help="The file, the newly "
+ "issued ticket is written to.")
+ args = parser.parse_args()
+
+ print "[+] Loading server state file."
+ serverState = state.load()
+
+ print "[+] Generating new session ticket."
+ masterKey = mycrypto.strongRandom(const.MASTER_KEY_LENGTH)
+ ticket = SessionTicket(masterKey, serverState).issue()
+
+ print "[+] Writing new session ticket to `%s'." % args.ticket_file
+ tickets = dict()
+ server = IPv4Address('TCP', args.ip_addr, args.tcp_port)
+ tickets[str(server)] = [int(time.time()), masterKey, ticket]
+
+ util.writeToFile(yaml.dump(tickets), args.ticket_file)
+
+ print "[+] Success."
diff --git a/obfsproxy/transports/scramblesuit/uniformdh.py b/obfsproxy/transports/scramblesuit/uniformdh.py
new file mode 100644
index 0000000..b070b10
--- /dev/null
+++ b/obfsproxy/transports/scramblesuit/uniformdh.py
@@ -0,0 +1,185 @@
+"""
+This module implements a class to deal with Uniform Diffie-Hellman handshakes.
+
+The class `UniformDH' is used by the server as well as by the client to handle
+the Uniform Diffie-Hellman handshake used by ScrambleSuit.
+"""
+
+import const
+import random
+import binascii
+
+import Crypto.Hash.SHA256
+
+import util
+import mycrypto
+
+import obfsproxy.transports.obfs3_dh as obfs3_dh
+import obfsproxy.transports.base as base
+import obfsproxy.common.log as logging
+
+log = logging.get_obfslogger()
+
+class UniformDH( object ):
+
+ """
+ Provide methods to deal with Uniform Diffie-Hellman handshakes.
+
+ The class provides methods to extract public keys and to generate public
+ keys wrapped in a valid UniformDH handshake.
+ """
+
+ def __init__( self, sharedSecret, weAreServer ):
+ """
+ Initialise a UniformDH object.
+ """
+
+ # `True' if we are the server; `False' otherwise.
+ self.weAreServer = weAreServer
+
+ # The shared UniformDH secret.
+ self.sharedSecret = sharedSecret
+
+ # Cache a UniformDH public key until it's added to the replay table.
+ self.remotePublicKey = None
+
+ # Uniform Diffie-Hellman object (implemented in obfs3_dh.py).
+ self.udh = None
+
+ def getRemotePublicKey( self ):
+ """
+ Return the cached remote UniformDH public key.
+ """
+
+ return self.remotePublicKey
+
+ def receivePublicKey( self, data, callback, srvState=None ):
+ """
+ Extract the public key and invoke a callback with the master secret.
+
+ First, the UniformDH public key is extracted out of `data'. Then, the
+ shared master secret is computed and `callback' is invoked with the
+ master secret as argument. If any of this fails, `False' is returned.
+ """
+
+ # Extract the public key sent by the remote host.
+ remotePublicKey = self.extractPublicKey(data, srvState)
+ if not remotePublicKey:
+ return False
+
+ if self.weAreServer:
+ self.remotePublicKey = remotePublicKey
+ # As server, we need a DH object; as client, we already have one.
+ self.udh = obfs3_dh.UniformDH()
+
+ assert self.udh is not None
+
+ try:
+ uniformDHSecret = self.udh.get_secret(remotePublicKey)
+ except ValueError:
+ raise base.PluggableTransportError("Corrupted public key.")
+
+ # First, hash the 4096-bit UniformDH secret to obtain the master key.
+ masterKey = Crypto.Hash.SHA256.new(uniformDHSecret).digest()
+
+ # Second, session keys are now derived from the master key.
+ callback(masterKey)
+
+ return True
+
+ def extractPublicKey( self, data, srvState=None ):
+ """
+ Extract and return a UniformDH public key out of `data'.
+
+ Before the public key is touched, the HMAC is verified. If the HMAC is
+ invalid or some other error occurs, `False' is returned. Otherwise,
+ the public key is returned. The extracted data is finally drained from
+ the given `data' object.
+ """
+
+ assert self.sharedSecret is not None
+
+ # Do we already have the minimum amount of data?
+ if len(data) < (const.PUBLIC_KEY_LENGTH + const.MARK_LENGTH +
+ const.HMAC_SHA256_128_LENGTH):
+ return False
+
+ log.debug("Attempting to extract the remote machine's UniformDH "
+ "public key out of %d bytes of data." % len(data))
+
+ handshake = data.peek()
+
+ # First, find the mark to efficiently locate the HMAC.
+ publicKey = handshake[:const.PUBLIC_KEY_LENGTH]
+ mark = mycrypto.HMAC_SHA256_128(self.sharedSecret, publicKey)
+
+ index = util.locateMark(mark, handshake)
+ if not index:
+ return False
+
+ # Now that we know where the authenticating HMAC is: verify it.
+ hmacStart = index + const.MARK_LENGTH
+ existingHMAC = handshake[hmacStart:
+ (hmacStart + const.HMAC_SHA256_128_LENGTH)]
+ myHMAC = mycrypto.HMAC_SHA256_128(self.sharedSecret,
+ handshake[0 : hmacStart] +
+ util.getEpoch())
+
+ if not util.isValidHMAC(myHMAC, existingHMAC, self.sharedSecret):
+ log.warning("The HMAC is invalid: `%s' vs. `%s'." %
+ (myHMAC.encode('hex'), existingHMAC.encode('hex')))
+ return False
+
+ # Do nothing if the ticket is replayed. Immediately closing the
+ # connection would be suspicious.
+ if srvState is not None and srvState.isReplayed(existingHMAC):
+ log.warning("The HMAC was already present in the replay table.")
+ return False
+
+ data.drain(index + const.MARK_LENGTH + const.HMAC_SHA256_128_LENGTH)
+
+ if srvState is not None:
+ log.debug("Adding the HMAC authenticating the UniformDH message " \
+ "to the replay table: %s." % existingHMAC.encode('hex'))
+ srvState.registerKey(existingHMAC)
+
+ return handshake[:const.PUBLIC_KEY_LENGTH]
+
+ def createHandshake( self ):
+ """
+ Create and return a ready-to-be-sent UniformDH handshake.
+
+ The returned handshake data includes the public key, pseudo-random
+ padding, the mark and the HMAC. If a UniformDH object has not been
+ initialised yet, a new instance is created.
+ """
+
+ assert self.sharedSecret is not None
+
+ log.debug("Creating UniformDH handshake message.")
+
+ if self.udh is None:
+ self.udh = obfs3_dh.UniformDH()
+ publicKey = self.udh.get_public()
+
+ assert (const.MAX_PADDING_LENGTH - const.PUBLIC_KEY_LENGTH) >= 0
+
+ # Subtract the length of the public key to make the handshake on
+ # average as long as a redeemed ticket. That should thwart statistical
+ # length-based attacks.
+ padding = mycrypto.strongRandom(random.randint(0,
+ const.MAX_PADDING_LENGTH -
+ const.PUBLIC_KEY_LENGTH))
+
+ # Add a mark which enables efficient location of the HMAC.
+ mark = mycrypto.HMAC_SHA256_128(self.sharedSecret, publicKey)
+
+ # Authenticate the handshake including the current approximate epoch.
+ mac = mycrypto.HMAC_SHA256_128(self.sharedSecret,
+ publicKey + padding + mark +
+ util.getEpoch())
+
+ return publicKey + padding + mark + mac
+
+# Alias class name in order to provide a more intuitive API.
+new = UniformDH
diff --git a/obfsproxy/transports/scramblesuit/util.py b/obfsproxy/transports/scramblesuit/util.py
new file mode 100644
index 0000000..bbb6c6a
--- /dev/null
+++ b/obfsproxy/transports/scramblesuit/util.py
@@ -0,0 +1,175 @@
+"""
+This module implements several commonly used utility functions.
+
+The implemented functions can be used to swap variables, write and read data
+from files and to convert a number to raw text.
+"""
+
+import obfsproxy.common.log as logging
+
+import os
+import time
+import const
+
+import mycrypto
+
+log = logging.get_obfslogger()
+
+def setStateLocation( stateLocation ):
+ """
+ Set the constant `STATE_LOCATION' to the given `stateLocation'.
+
+ The variable `stateLocation' determines where persistent information (such
+ as the server's key material) is stored. If `stateLocation' is `None', it
+ remains to be the current directory. In general, however, it should be a
+ subdirectory of Tor's data directory.
+ """
+
+ if stateLocation is None:
+ return
+
+ if not stateLocation.endswith('/'):
+ stateLocation += '/'
+
+ # To be polite, we create a subdirectory inside wherever we are asked to
+ # store data in.
+ stateLocation += (const.TRANSPORT_NAME).lower() + '/'
+
+ # ...and if it does not exist yet, we attempt to create the full
+ # directory path.
+ if not os.path.exists(stateLocation):
+ log.info("Creating directory path `%s'." % stateLocation)
+ os.makedirs(stateLocation)
+
+ log.debug("Setting the state location to `%s'." % stateLocation)
+ const.STATE_LOCATION = stateLocation
+
+
+def isValidHMAC( hmac1, hmac2, key ):
+ """
+ Compares `hmac1' and `hmac2' after HMACing them again using `key'.
+
+ The arguments `hmac1' and `hmac2' are compared. If they are equal, `True'
+ is returned and otherwise `False'. To prevent timing attacks, double HMAC
+ verification is used meaning that the two arguments are HMACed again before
+ (variable-time) string comparison. The idea is taken from:
+ https://www.isecpartners.com/blog/2011/february/double-hmac-verification.aspx
+ """
+
+ assert len(hmac1) == len(hmac2)
+
+ # HMAC the arguments again to prevent timing attacks.
+ doubleHmac1 = mycrypto.HMAC_SHA256_128(key, hmac1)
+ doubleHmac2 = mycrypto.HMAC_SHA256_128(key, hmac2)
+
+ if doubleHmac1 != doubleHmac2:
+ return False
+
+ log.debug("The computed HMAC is valid.")
+
+ return True
+
+
+def locateMark( mark, payload ):
+ """
+ Locate the given `mark' in `payload' and return its index.
+
+ The `mark' is placed before the HMAC of a ScrambleSuit authentication
+ mechanism and makes it possible to efficiently locate the HMAC. If the
+ `mark' could not be found, `None' is returned.
+ """
+
+ index = payload.find(mark)
+ if index < 0:
+ log.debug("Could not find the mark just yet.")
+ return None
+
+ if (len(payload) - index - const.MARK_LENGTH) < \
+ const.HMAC_SHA256_128_LENGTH:
+ log.debug("Found the mark but the HMAC is still incomplete.")
+ return None
+
+ log.debug("Successfully located the mark.")
+
+ return index
+
+
+def getEpoch( ):
+ """
+ Return the Unix epoch divided by a constant as string.
+
+ This function returns a coarse-grained version of the Unix epoch. The
+ seconds passed since the epoch are divided by the constant
+ `EPOCH_GRANULARITY'.
+ """
+
+ return str(int(time.time()) / const.EPOCH_GRANULARITY)
+
+
+def writeToFile( data, fileName ):
+ """
+ Writes the given `data' to the file specified by `fileName'.
+
+ If an error occurs, the function logs an error message but does not throw
+ an exception or return an error code.
+ """
+
+ log.debug("Opening `%s' for writing." % fileName)
+
+ try:
+ with open(fileName, "wb") as desc:
+ desc.write(data)
+
+ except IOError as err:
+ log.error("Error writing to `%s': %s." % (fileName, err))
+
+
+def readFromFile( fileName, length=-1 ):
+ """
+ Read `length' amount of bytes from the given `fileName'
+
+ If `length' equals -1 (the default), the entire file is read and the
+ content returned. If an error occurs, the function logs an error message
+ but does not throw an exception or return an error code.
+ """
+
+ data = None
+
+ if not os.path.exists(fileName):
+ log.debug("File `%s' does not exist (yet?)." % fileName)
+ return None
+
+ log.debug("Opening `%s' for reading." % fileName)
+
+ try:
+ with open(fileName, "rb") as desc:
+ data = desc.read(length)
+
+ except IOError as err:
+ log.error("Error reading from `%s': %s." % (fileName, err))
+
+ return data
+
+
+def sanitiseBase32( data ):
+ """
+ Try to sanitise a Base32 string if it's slightly wrong.
+
+ ScrambleSuit's shared secret might be distributed verbally which could
+ cause mistakes. This function fixes simple mistakes, e.g., when a user
+ noted "1" rather than "I".
+ """
+
+ data = data.upper()
+
+ if "1" in data:
+ log.info("Found a \"1\" in Base32-encoded \"%s\". Assuming " \
+ "it's actually \"I\"." % data)
+ data = data.replace("1", "I")
+
+ if "0" in data:
+ log.info("Found a \"0\" in Base32-encoded \"%s\". Assuming " \
+ "it's actually \"O\"." % data)
+ data = data.replace("0", "O")
+
+ return data
diff --git a/obfsproxy/transports/transports.py b/obfsproxy/transports/transports.py
index 485269b..af1beb9 100644
--- a/obfsproxy/transports/transports.py
+++ b/obfsproxy/transports/transports.py
@@ -3,10 +3,14 @@ import obfsproxy.transports.dummy as dummy
import obfsproxy.transports.b64 as b64
import obfsproxy.transports.obfs2 as obfs2
import obfsproxy.transports.obfs3 as obfs3
+import obfsproxy.transports.scramblesuit.scramblesuit as scramblesuit
transports = { 'dummy' : {'base': dummy.DummyTransport, 'client' : dummy.DummyClient, 'server' : dummy.DummyServer },
'b64' : {'base': b64.B64Transport, 'client' : b64.B64Client, 'server' : b64.B64Server },
'obfs2' : {'base': obfs2.Obfs2Transport, 'client' : obfs2.Obfs2Client, 'server' : obfs2.Obfs2Server },
+ 'scramblesuit' : {'base': scramblesuit.ScrambleSuitTransport,
+ 'client':scramblesuit.ScrambleSuitClient,
+ 'server':scramblesuit.ScrambleSuitServer },
'obfs3' : {'base': obfs3.Obfs3Transport, 'client' : obfs3.Obfs3Client, 'server' : obfs3.Obfs3Server } }
def get_transport_class(name, role):
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-privacy/packages/obfsproxy.git
More information about the Pkg-privacy-commits
mailing list