Add support for acting as a ScrambleSuit client.

This allows obfs4proxy to be used as a ScrambleSuit client that is wire
compatible with the obfs4proxy implementation, including session ticket
support, and length obfuscation.

The current implementation has the following limitations:
 * IAT obfuscation is not supported (and is disabled in all other
   ScrambleSuit implementations by default).
 * The length distribution and probabilites are different from those
   generated by obfsproxy and obfsclient due to a different DRBG.
 * Server support is missing and is unlikely to be implemented.
merge-requests/3/head
Yawning Angel 9 years ago
parent 0f038ca4fa
commit 0066cfc393

@ -1,6 +1,7 @@
Changes in version 0.0.4 - UNRELEASED
- Improve the runtime performance of the obfs4 handshake tests.
- Changed the go.crypto import path to the new location (golang.org/x/crypto).
- Added client only support for ScrambleSuit.
Changes in version 0.0.3 - 2014-10-01
- Change the obfs4 bridge line format to use a "cert" argument instead of the

@ -81,6 +81,9 @@ ServerTransportPlugin obfs4 exec /usr/local/bin/obfs4proxy
`ClientTransportPlugin` and `ServerTransportPlugin` lines in the torrc as
appropriate.
* obfs4proxy can also act as a ScrambleSuit client. Adjust the
`ClientTransportPlugin` line in the torrc as appropriate.
* The autogenerated obfs4 bridge parameters are placed in
`DataDir/pt_state/obfs4_state.json`. To ease deployment, the client side
bridge line is written to `DataDir/pt_state/obfs4_bridgeline.txt`.

@ -31,9 +31,11 @@
package probdist
import (
"bytes"
"container/list"
"fmt"
"math/rand"
"sync"
"git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
"git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
@ -46,6 +48,8 @@ const (
// WeightedDist is a weighted distribution.
type WeightedDist struct {
sync.Mutex
minValue int
maxValue int
biased bool
@ -192,6 +196,9 @@ func (w *WeightedDist) Reset(seed *drbg.Seed) {
drbg, _ := drbg.NewHashDrbg(seed)
rng := rand.New(drbg)
w.Lock()
defer w.Unlock()
w.genValues(rng)
if w.biased {
w.genBiasedWeights(rng)
@ -205,6 +212,9 @@ func (w *WeightedDist) Reset(seed *drbg.Seed) {
func (w *WeightedDist) Sample() int {
var idx int
w.Lock()
defer w.Unlock()
// Generate a fair die roll from an $n$-sided die; call the side $i$.
i := csrand.Intn(len(w.values))
// Flip a biased coin that comes up heads with probability $Prob[i]$.
@ -218,3 +228,18 @@ func (w *WeightedDist) Sample() int {
return w.minValue + w.values[idx]
}
// String returns a dump of the distribution table.
func (w *WeightedDist) String() string {
var buf bytes.Buffer
buf.WriteString("[ ")
for i, v := range w.values {
p := w.weights[i]
if p > 0.01 { // Squelch tiny probabilities.
buf.WriteString(fmt.Sprintf("%d: %f ", v, p))
}
}
buf.WriteString("]")
return buf.String()
}

@ -1,4 +1,4 @@
.TH OBFS4PROXY 1 "2014-09-24"
.TH OBFS4PROXY 1 "2015-01-20"
.SH NAME
obfs4proxy \- pluggable transport proxy for Tor, implementing obfs4
.SH SYNOPSIS
@ -11,7 +11,8 @@ censors, who usually monitor traffic between the client and the bridge,
will see innocent-looking transformed traffic instead of the actual Tor
traffic.
.PP
obfs4proxy implements the obfuscation protocols obfs2, obfs3 and obfs4.
obfs4proxy implements the obfuscation protocols obfs2, obfs3,
ScrambleSuit (client only) and obfs4.
.PP
obfs4proxy is currently only supported as a managed pluggable transport
spawned as a helper process via the \fBtor\fR daemon.

@ -0,0 +1,88 @@
/*
* Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
// Package scramblesuit provides an implementation of the ScrambleSuit
// obfuscation protocol. The implementation is client only.
package scramblesuit
import (
"fmt"
"net"
"git.torproject.org/pluggable-transports/goptlib.git"
"git.torproject.org/pluggable-transports/obfs4.git/transports/base"
)
const transportName = "scramblesuit"
// Transport is the ScrambleSuit implementation of the base.Transport interface.
type Transport struct{}
// Name returns the name of the ScrambleSuit transport protocol.
func (t *Transport) Name() string {
return transportName
}
// ClientFactory returns a new ssClientFactory instance.
func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) {
tStore, err := loadTicketStore(stateDir)
if err != nil {
return nil, err
}
cf := &ssClientFactory{transport: t, ticketStore: tStore}
return cf, nil
}
// ServerFactory will one day return a new ssServerFactory instance.
func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFactory, error) {
// TODO: Fill this in eventually, though obfs4 is better.
return nil, fmt.Errorf("server not supported")
}
type ssClientFactory struct {
transport base.Transport
ticketStore *ssTicketStore
}
func (cf *ssClientFactory) Transport() base.Transport {
return cf.transport
}
func (cf *ssClientFactory) ParseArgs(args *pt.Args) (interface{}, error) {
return newClientArgs(args)
}
func (cf *ssClientFactory) WrapConn(conn net.Conn, args interface{}) (net.Conn, error) {
ca, ok := args.(*ssClientArgs)
if !ok {
return nil, fmt.Errorf("invalid argument type for args")
}
return newScrambleSuitClientConn(conn, cf.ticketStore, ca)
}
var _ base.ClientFactory = (*ssClientFactory)(nil)
var _ base.Transport = (*Transport)(nil)

@ -0,0 +1,521 @@
/*
* Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package scramblesuit
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha256"
"encoding/base32"
"encoding/binary"
"errors"
"fmt"
"hash"
"io"
"net"
"time"
"git.torproject.org/pluggable-transports/goptlib.git"
"git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
"git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
"git.torproject.org/pluggable-transports/obfs4.git/common/probdist"
"git.torproject.org/pluggable-transports/obfs4.git/common/uniformdh"
)
const (
passwordArg = "password"
maxSegmentLength = 1448
maxPayloadLength = 1427
sharedSecretLength = 160 / 8 // k_B
clientHandshakeTimeout = time.Duration(60) * time.Second
minLenDistLength = 21
maxLenDistLength = maxSegmentLength
keyLength = 32 + 8 + 32
pktPrngSeedLength = 32
pktOverhead = macLength + pktHdrLength
pktHdrLength = 2 + 2 + 1
pktPayload = 1
pktNewTicket = 1 << 1
pktPrngSeed = 1 << 2
)
var (
// ErrNotSupported is the error returned for a unsupported operation.
ErrNotSupported = errors.New("scramblesuit: operation not supported")
// ErrInvalidPacket is the error returned when a invalid packet is received.
ErrInvalidPacket = errors.New("scramblesuit: invalid packet")
zeroPadBytes [maxPayloadLength]byte
)
type ssSharedSecret [sharedSecretLength]byte
type ssClientArgs struct {
kB *ssSharedSecret
sessionKey *uniformdh.PrivateKey
}
func newClientArgs(args *pt.Args) (ca *ssClientArgs, err error) {
ca = &ssClientArgs{}
if ca.kB, err = parsePasswordArg(args); err != nil {
return nil, err
}
// Generate the client keypair before opening a connection since the time
// taken is visible to an adversary. This key might not end up being used
// if a session ticket is present, but this doesn't take that long.
if ca.sessionKey, err = uniformdh.GenerateKey(csrand.Reader); err != nil {
return nil, err
}
return
}
func parsePasswordArg(args *pt.Args) (*ssSharedSecret, error) {
str, ok := args.Get(passwordArg)
if !ok {
return nil, fmt.Errorf("missing argument '%s'", passwordArg)
}
// To match the obfsproxy behavior, 'str' should contain a Base32 encoded
// shared secret (k_B) used for handshaking.
decoded, err := base32.StdEncoding.DecodeString(str)
if err != nil {
return nil, fmt.Errorf("failed to decode password: %s", err)
}
if len(decoded) != sharedSecretLength {
return nil, fmt.Errorf("password length %d is invalid", len(decoded))
}
ss := new(ssSharedSecret)
copy(ss[:], decoded)
return ss, nil
}
type ssCryptoState struct {
s cipher.Stream
mac hash.Hash
}
func newCryptoState(aesKey []byte, ivPrefix []byte, macKey []byte) (*ssCryptoState, error) {
// The ScrambleSuit CTR-AES256 link crypto uses an 8 byte prefix from the
// KDF, and a 64 bit counter initialized to 1 as the IV. The initial value
// of the counter isn't documented in the spec either.
var initialCtr = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}
iv := make([]byte, 0, aes.BlockSize)
iv = append(iv, ivPrefix...)
iv = append(iv, initialCtr...)
b, err := aes.NewCipher(aesKey)
if err != nil {
return nil, err
}
s := cipher.NewCTR(b, iv)
mac := hmac.New(sha256.New, macKey)
return &ssCryptoState{s: s, mac: mac}, nil
}
type ssConn struct {
net.Conn
isServer bool
lenDist *probdist.WeightedDist
receiveBuffer *bytes.Buffer
receiveDecodedBuffer *bytes.Buffer
receiveState ssRxState
txCrypto *ssCryptoState
rxCrypto *ssCryptoState
ticketStore *ssTicketStore
}
type ssRxState struct {
mac []byte
hdr []byte
totalLen int
payloadLen int
}
func (conn *ssConn) Read(b []byte) (n int, err error) {
// If the receive payload buffer is empty, consume data off the network.
for conn.receiveDecodedBuffer.Len() == 0 {
if err = conn.readPackets(); err != nil {
break
}
}
// Service the read request using buffered payload.
if conn.receiveDecodedBuffer.Len() > 0 {
n, _ = conn.receiveDecodedBuffer.Read(b)
}
return
}
func (conn *ssConn) Write(b []byte) (n int, err error) {
var frameBuf bytes.Buffer
p := b
toSend := len(p)
for toSend > 0 {
// Send as much payload as will fit into each frame as possible.
wrLen := len(p)
if wrLen > maxPayloadLength {
wrLen = maxPayloadLength
}
payload := p[:wrLen]
if err = conn.makePacket(&frameBuf, pktPayload, payload, 0); err != nil {
return 0, err
}
toSend -= wrLen
p = p[wrLen:]
n += wrLen
}
// Pad out the burst as appropriate.
if err = conn.padBurst(&frameBuf, conn.lenDist.Sample()); err != nil {
return 0, err
}
// Write and return.
_, err = conn.Conn.Write(frameBuf.Bytes())
return
}
func (conn *ssConn) SetDeadline(t time.Time) error {
return ErrNotSupported
}
func (conn *ssConn) SetReadDeadline(t time.Time) error {
return ErrNotSupported
}
func (conn *ssConn) SetWriteDeadline(t time.Time) error {
return ErrNotSupported
}
func (conn *ssConn) makePacket(w io.Writer, pktType byte, data []byte, padLen int) error {
payloadLen := len(data)
totalLen := payloadLen + padLen
if totalLen > maxPayloadLength {
panic(fmt.Sprintf("BUG: makePacket() len(data) + padLen > maxPayloadLength: %d + %d > %d", len(data), padLen, maxPayloadLength))
}
// Build the packet header (total length, payload length, flags),
// and append the payload and padding.
pkt := make([]byte, pktHdrLength, pktHdrLength+payloadLen+padLen)
binary.BigEndian.PutUint16(pkt[0:], uint16(totalLen))
binary.BigEndian.PutUint16(pkt[2:], uint16(payloadLen))
pkt[4] = pktType
pkt = append(pkt, data...)
pkt = append(pkt, zeroPadBytes[:padLen]...)
// Encrypt the packet, and calculate the MAC.
conn.txCrypto.s.XORKeyStream(pkt, pkt)
conn.txCrypto.mac.Reset()
conn.txCrypto.mac.Write(pkt)
mac := conn.txCrypto.mac.Sum(nil)[:macLength]
// Write out MAC | Packet. Note that this does not go onto the network
// yet, as w is a byte.Buffer (This is done so each call to conn.Write()
// gets padding added).
if _, err := w.Write(mac); err != nil {
return err
}
_, err := w.Write(pkt)
return err
}
func (conn *ssConn) readPackets() error {
// Consume and buffer up to 1 MSS worth of data.
var buf [maxSegmentLength]byte
rdLen, rdErr := conn.Conn.Read(buf[:])
conn.receiveBuffer.Write(buf[:rdLen])
// Process incoming packets incrementally. conn.receiveState stores
// the results of partial processing.
for conn.receiveBuffer.Len() > 0 {
if conn.receiveState.mac == nil {
// Read and store the packet MAC.
if conn.receiveBuffer.Len() < macLength {
break
}
mac := make([]byte, macLength)
conn.receiveBuffer.Read(mac)
conn.receiveState.mac = mac
}
if conn.receiveState.hdr == nil {
// Read and store the packet header.
if conn.receiveBuffer.Len() < pktHdrLength {
break
}
hdr := make([]byte, pktHdrLength)
conn.receiveBuffer.Read(hdr)
// Add the encrypted packet header to the HMAC instance, and then
// decrypt it so that the length of the packet can be determined.
conn.rxCrypto.mac.Reset()
conn.rxCrypto.mac.Write(hdr)
conn.rxCrypto.s.XORKeyStream(hdr, hdr)
// Store the plaintext packet header, and host byte order length
// values.
totalLen := int(binary.BigEndian.Uint16(hdr[0:]))
payloadLen := int(binary.BigEndian.Uint16(hdr[2:]))
if payloadLen > totalLen || totalLen > maxPayloadLength {
return ErrInvalidPacket
}
conn.receiveState.hdr = hdr
conn.receiveState.totalLen = totalLen
conn.receiveState.payloadLen = payloadLen
}
var data []byte
if conn.receiveState.totalLen > 0 {
// If the packet actually has payload (including padding), read,
// digest and decrypt it.
if conn.receiveBuffer.Len() < conn.receiveState.totalLen {
break
}
data = make([]byte, conn.receiveState.totalLen)
conn.receiveBuffer.Read(data)
conn.rxCrypto.mac.Write(data)
conn.rxCrypto.s.XORKeyStream(data, data)
}
// Authenticate the packet, by comparing the received MAC with the one
// calculated over the ciphertext consumed off the network.
cmpMAC := conn.rxCrypto.mac.Sum(nil)[:macLength]
if !hmac.Equal(cmpMAC, conn.receiveState.mac[:]) {
return ErrInvalidPacket
}
// Based on the packet flags, do something useful with the payload.
data = data[:conn.receiveState.payloadLen]
switch conn.receiveState.hdr[4] {
case pktPayload:
// User data, write it into the decoded payload buffer so that Read
// calls can be serviced.
conn.receiveDecodedBuffer.Write(data)
case pktNewTicket:
// New Session Ticket to be used for future handshakes, store it in
// the Session Ticket store.
if conn.isServer || len(data) != ticketKeyLength+ticketLength {
return ErrInvalidPacket
}
conn.ticketStore.storeTicket(conn.RemoteAddr(), data)
case pktPrngSeed:
// New PRNG_SEED for the protocol polymorphism. Regenerate the
// length obfuscation probability distribution.
if conn.isServer || len(data) != pktPrngSeedLength {
return ErrInvalidPacket
}
seed, err := drbg.SeedFromBytes(data)
if err != nil {
return ErrInvalidPacket
}
conn.lenDist.Reset(seed)
default:
return ErrInvalidPacket
}
// Done processing a packet, clear the partial state.
conn.receiveState.mac = nil
conn.receiveState.hdr = nil
conn.receiveState.totalLen = 0
conn.receiveState.payloadLen = 0
}
return rdErr
}
func (conn *ssConn) clientHandshake(kB *ssSharedSecret, sessionKey *uniformdh.PrivateKey) error {
if conn.isServer {
return fmt.Errorf("clientHandshake called on server connection")
}
// Query the Session Ticket store to see if there is a stored session
// ticket.
ticket, err := conn.ticketStore.getTicket(conn.RemoteAddr())
if err != nil {
return err
} else if ticket != nil {
// Ok, there is an existing ticket, so attempt to do a Session Ticket
// handshake. Until we write to the network, failures are non-fatal as
// we can transition gracefully into doing a UniformDH handshake.
// Derive the keys from the prestored master key received with the
// ticket. This is done before the actual handshake since the
// handshake uses the outgoing HMAC-SHA256-128 key for authentication.
if err = conn.initCrypto(ticket.key[:]); err != nil {
goto handshakeUDH
}
// Generate and send the ticket handshake. There is no response, since
// both sides have the keying material.
hs := newTicketClientHandshake(conn.txCrypto.mac, ticket)
blob, err := hs.generateHandshake()
if err != nil {
goto handshakeUDH
}
if _, err = conn.Conn.Write(blob); err != nil {
return err
}
return nil
}
handshakeUDH:
// No session ticket, so take the slow path and do a UniformDH based
// handshake.
// Generate and send the client handshake.
hs := newDHClientHandshake(kB, sessionKey)
blob, err := hs.generateHandshake()
if err != nil {
return err
}
if _, err = conn.Conn.Write(blob); err != nil {
return err
}
// Consume the server handshake. Since we don't actually know the length
// of the respose, we need to consume data off the network till we either
// find the tail marker + MAC digest indicating that a handshake response
// has been received, or the maximum handshake size passes without a valid
// response.
var hsBuf [maxHandshakeLength]byte
for {
var n int
if n, err = conn.Conn.Read(hsBuf[:]); err != nil {
return err
}
conn.receiveBuffer.Write(hsBuf[:n])
// Attempt to process all the data seen so far as a response.
var seed []byte
n, seed, err = hs.parseServerHandshake(conn.receiveBuffer.Bytes())
if err == errMarkNotFoundYet {
// No response found yet, keep trying.
continue
} else if err != nil {
return err
}
// Ok, done processing the handshake, discard the response, and do the
// key derivation based off the calculated shared secret.
_ = conn.receiveBuffer.Next(n)
err = conn.initCrypto(seed)
return err
}
}
func (conn *ssConn) initCrypto(seed []byte) (err error) {
// Use HKDF-SHA256 (Expand only, no Extract) to generate session keys from
// initial keying material.
okm := hkdfExpand(sha256.New, seed, nil, kdfSecretLength)
if conn.txCrypto, err = newCryptoState(okm[0:32], okm[32:40], okm[80:112]); err != nil {
return
}
if conn.rxCrypto, err = newCryptoState(okm[40:72], okm[72:80], okm[112:144]); err != nil {
return
}
return
}
func (conn *ssConn) padBurst(burst *bytes.Buffer, sampleLen int) (err error) {
// Burst contains the fully encrypted+MACed outgoing payload that will be
// written to the network. Pad it out so that the last segment (based on
// the ScrambleSuit MTU) is sampleLen bytes.
dataLen := burst.Len() % maxSegmentLength
padLen := 0
if sampleLen >= dataLen {
padLen = sampleLen - dataLen
} else {
padLen = (maxSegmentLength - dataLen) + sampleLen
}
if padLen < pktOverhead {
// The padLen is less than the MAC + packet header in length, so
// two packets are required.
padLen += maxSegmentLength
}
if padLen == 0 {
return
} else if padLen > maxSegmentLength {
// Note: packetmorpher.py: getPadding is slightly wrong and only
// accounts for one of the two packet headers.
if err = conn.makePacket(burst, pktPayload, nil, 700-pktOverhead); err != nil {
return
}
err = conn.makePacket(burst, pktPayload, nil, padLen-(700+2*pktOverhead))
} else {
err = conn.makePacket(burst, pktPayload, nil, padLen-pktOverhead)
}
return
}
func newScrambleSuitClientConn(conn net.Conn, tStore *ssTicketStore, ca *ssClientArgs) (net.Conn, error) {
// At this point we have kB and our session key, so we can directly
// start handshaking and seeing what happens.
// Seed the initial polymorphism distribution.
seed, err := drbg.NewSeed()
if err != nil {
return nil, err
}
dist := probdist.New(seed, minLenDistLength, maxLenDistLength, true)
// Allocate the client structure.
c := &ssConn{conn, false, dist, bytes.NewBuffer(nil), bytes.NewBuffer(nil), ssRxState{}, nil, nil, tStore}
// Start the handshake timeout.
deadline := time.Now().Add(clientHandshakeTimeout)
if err := conn.SetDeadline(deadline); err != nil {
return nil, err
}
// Attempt to handshake.
if err := c.clientHandshake(ca.kB, ca.sessionKey); err != nil {
return nil, err
}
// Stop the handshake timeout.
if err := conn.SetDeadline(time.Time{}); err != nil {
return nil, err
}
return c, nil
}

@ -0,0 +1,228 @@
/*
* Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package scramblesuit
import (
"bytes"
"encoding/base32"
"encoding/json"
"errors"
"fmt"
"hash"
"io/ioutil"
"net"
"os"
"path"
"strconv"
"sync"
"time"
"git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
)
const (
ticketFile = "scramblesuit_tickets.json"
ticketKeyLength = 32
ticketLength = 112
ticketLifetime = 60 * 60 * 24 * 7
ticketMinPadLength = 0
ticketMaxPadLength = 1388
)
var (
errInvalidTicket = errors.New("scramblesuit: invalid serialized ticket")
)
type ssTicketStore struct {
sync.Mutex
filePath string
store map[string]*ssTicket
}
type ssTicket struct {
key [ticketKeyLength]byte
ticket [ticketLength]byte
issuedAt int64
}
type ssTicketJSON struct {
KeyTicket string `json:"key-ticket"`
IssuedAt int64 `json:"issuedAt"`
}
func (t *ssTicket) isValid() bool {
return t.issuedAt+ticketLifetime > time.Now().Unix()
}
func newTicket(raw []byte) (*ssTicket, error) {
if len(raw) != ticketKeyLength+ticketLength {
return nil, errInvalidTicket
}
t := &ssTicket{issuedAt: time.Now().Unix()}
copy(t.key[:], raw[0:])
copy(t.ticket[:], raw[ticketKeyLength:])
return t, nil
}
func (s *ssTicketStore) storeTicket(addr net.Addr, rawT []byte) {
t, err := newTicket(rawT)
if err != nil {
// Silently ignore ticket store failures.
return
}
s.Lock()
defer s.Unlock()
// Add the ticket to the map, and checkpoint to disk. Serialization errors
// are ignored because the handshake code will just use UniformDH if a
// ticket is not available.
s.store[addr.String()] = t
s.serialize()
}
func (s *ssTicketStore) getTicket(addr net.Addr) (*ssTicket, error) {
aStr := addr.String()
s.Lock()
defer s.Unlock()
t, ok := s.store[aStr]
if ok && t != nil {
// Tickets are one use only, so remove tickets from the map, and
// checkpoint the map to disk.
delete(s.store, aStr)
err := s.serialize()
if !t.isValid() {
// Expired ticket, ignore it.
return nil, err
}
return t, err
}
// No ticket was found, that's fine.
return nil, nil
}
func (s *ssTicketStore) serialize() error {
encMap := make(map[string]*ssTicketJSON)
for k, v := range s.store {
kt := make([]byte, 0, ticketKeyLength+ticketLength)
kt = append(kt, v.key[:]...)
kt = append(kt, v.ticket[:]...)
ktStr := base32.StdEncoding.EncodeToString(kt)
jsonObj := &ssTicketJSON{KeyTicket: ktStr, IssuedAt: v.issuedAt}
encMap[k] = jsonObj
}
jsonStr, err := json.Marshal(encMap)
if err != nil {
return err
}
return ioutil.WriteFile(s.filePath, jsonStr, 0600)
}
func loadTicketStore(stateDir string) (*ssTicketStore, error) {
fPath := path.Join(stateDir, ticketFile)
s := &ssTicketStore{filePath: fPath}
s.store = make(map[string]*ssTicket)
f, err := ioutil.ReadFile(fPath)
if err != nil {
// No ticket store is fine.
if os.IsNotExist(err) {
return s, nil
}
// But a file read error is not.
return nil, err
}
encMap := make(map[string]*ssTicketJSON)
if err = json.Unmarshal(f, &encMap); err != nil {
return nil, fmt.Errorf("failed to load ticket store '%s': '%s'", fPath, err)
}
for k, v := range encMap {
raw, err := base32.StdEncoding.DecodeString(v.KeyTicket)
if err != nil || len(raw) != ticketKeyLength+ticketLength {
// Just silently skip corrupted tickets.
continue
}
t := &ssTicket{issuedAt: v.IssuedAt}
if !t.isValid() {
// Just ignore expired tickets.
continue
}
copy(t.key[:], raw[0:])
copy(t.ticket[:], raw[ticketKeyLength:])
s.store[k] = t
}
return s, nil
}
type ssTicketClientHandshake struct {
mac hash.Hash
ticket *ssTicket
padLen int
}
func (hs *ssTicketClientHandshake) generateHandshake() ([]byte, error) {
var buf bytes.Buffer
hs.mac.Reset()
// The client handshake is T | P | M | MAC(T | P | M | E)
hs.mac.Write(hs.ticket.ticket[:])
m := hs.mac.Sum(nil)[:macLength]
p, err := makePad(hs.padLen)
if err != nil {
return nil, err
}
// Write T, P, M.
buf.Write(hs.ticket.ticket[:])
buf.Write(p)
buf.Write(m)
// Calculate and write the MAC.
e := []byte(strconv.FormatInt(getEpochHour(), 10))
hs.mac.Write(p)
hs.mac.Write(m)
hs.mac.Write(e)
buf.Write(hs.mac.Sum(nil)[:macLength])
hs.mac.Reset()
return buf.Bytes(), nil
}
func newTicketClientHandshake(mac hash.Hash, ticket *ssTicket) *ssTicketClientHandshake {
hs := &ssTicketClientHandshake{mac: mac, ticket: ticket}
hs.padLen = csrand.IntRange(ticketMinPadLength, ticketMaxPadLength)
return hs
}

@ -0,0 +1,174 @@
/*
* Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package scramblesuit
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"errors"
"hash"
"strconv"
"time"
"git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
"git.torproject.org/pluggable-transports/obfs4.git/common/uniformdh"
)
const (
minHandshakeLength = uniformdh.Size + macLength*2
maxHandshakeLength = 1532
dhMinPadLength = 0
dhMaxPadLength = 1308
macLength = 128 / 8 // HMAC-SHA256-128()
kdfSecretLength = keyLength * 2
)
var (
errMarkNotFoundYet = errors.New("mark not found yet")
// ErrInvalidHandshake is the error returned when the handshake fails.
ErrInvalidHandshake = errors.New("invalid handshake")
)
type ssDHClientHandshake struct {
mac hash.Hash
keypair *uniformdh.PrivateKey
epochHour []byte
padLen int
serverPublicKey *uniformdh.PublicKey
serverMark []byte
}
func (hs *ssDHClientHandshake) generateHandshake() ([]byte, error) {
var buf bytes.Buffer
hs.mac.Reset()
// The client handshake is X | P_C | M_C | MAC(X | P_C | M_C | E)
x, err := hs.keypair.PublicKey.Bytes()
if err != nil {
return nil, err
}
hs.mac.Write(x)
mC := hs.mac.Sum(nil)[:macLength]
pC, err := makePad(hs.padLen)
if err != nil {
return nil, err
}
// Write X, P_C, M_C.
buf.Write(x)
buf.Write(pC)
buf.Write(mC)
// Calculate and write the MAC.
hs.epochHour = []byte(strconv.FormatInt(getEpochHour(), 10))
hs.mac.Write(pC)
hs.mac.Write(mC)
hs.mac.Write(hs.epochHour)
buf.Write(hs.mac.Sum(nil)[:macLength])
return buf.Bytes(), nil
}
func (hs *ssDHClientHandshake) parseServerHandshake(resp []byte) (int, []byte, error) {
if len(resp) < minHandshakeLength {
return 0, nil, errMarkNotFoundYet
}
// The server response is Y | P_S | M_S | MAC(Y | P_S | M_S | E).
if hs.serverPublicKey == nil {
y := resp[:uniformdh.Size]
// Pull out the public key, and derive the server mark.
hs.serverPublicKey = &uniformdh.PublicKey{}
err := hs.serverPublicKey.SetBytes(y)
if err != nil {
return 0, nil, err
}
hs.mac.Reset()
hs.mac.Write(y)
hs.serverMark = hs.mac.Sum(nil)[:macLength]
}
// Find the mark+MAC, if it exits.
endPos := len(resp)
if endPos > maxHandshakeLength-macLength {
endPos = maxHandshakeLength - macLength
}
pos := bytes.Index(resp[uniformdh.Size:endPos], hs.serverMark)
if pos == -1 {
if len(resp) >= maxHandshakeLength {
// Couldn't find the mark in a maximum length response.
return 0, nil, ErrInvalidHandshake
}
return 0, nil, errMarkNotFoundYet
} else if len(resp) < pos+2*macLength {
// Didn't receive the full M_S.
return 0, nil, errMarkNotFoundYet
}
pos += uniformdh.Size
// Validate the MAC.
hs.mac.Write(resp[uniformdh.Size : pos+macLength])
hs.mac.Write(hs.epochHour)
macCmp := hs.mac.Sum(nil)[:macLength]
macRx := resp[pos+macLength : pos+2*macLength]
if !hmac.Equal(macCmp, macRx) {
return 0, nil, ErrInvalidHandshake
}
// Derive the shared secret.
ss, err := uniformdh.Handshake(hs.keypair, hs.serverPublicKey)
if err != nil {
return 0, nil, err
}
seed := sha256.Sum256(ss)
return pos + 2*macLength, seed[:], nil
}
func newDHClientHandshake(kB *ssSharedSecret, sessionKey *uniformdh.PrivateKey) *ssDHClientHandshake {
hs := &ssDHClientHandshake{keypair: sessionKey}
hs.mac = hmac.New(sha256.New, kB[:])
hs.padLen = csrand.IntRange(dhMinPadLength, dhMaxPadLength)
return hs
}
func getEpochHour() int64 {
return time.Now().Unix() / 3600
}
func makePad(padLen int) ([]byte, error) {
pad := make([]byte, padLen)
if err := csrand.Bytes(pad); err != nil {
return nil, err
}
return pad, nil
}

@ -0,0 +1,67 @@
/*
* Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package scramblesuit
import (
"crypto/hmac"
"hash"
)
func hkdfExpand(hashFn func() hash.Hash, prk []byte, info []byte, l int) []byte {
// Why, yes. golang.org/x/crypto/hkdf exists, and is a fine
// implementation of HKDF. However it does both the extract
// and expand, while ScrambleSuit only does extract, with no
// way to separate the two steps.
h := hmac.New(hashFn, prk)
digestSz := h.Size()
if l > 255*digestSz {
panic("hkdf: requested OKM length > 255*HashLen")
}
var t []byte
okm := make([]byte, 0, l)
toAppend := l
ctr := byte(1)
for toAppend > 0 {
h.Reset()
h.Write(t)
h.Write(info)
h.Write([]byte{ctr})
t = h.Sum(nil)
ctr++
aLen := digestSz
if toAppend < digestSz {
aLen = toAppend
}
okm = append(okm, t[:aLen]...)
toAppend -= aLen
}
return okm
}

@ -37,6 +37,7 @@ import (
"git.torproject.org/pluggable-transports/obfs4.git/transports/obfs2"
"git.torproject.org/pluggable-transports/obfs4.git/transports/obfs3"
"git.torproject.org/pluggable-transports/obfs4.git/transports/obfs4"
"git.torproject.org/pluggable-transports/obfs4.git/transports/scramblesuit"
)
var transportMapLock sync.Mutex
@ -88,4 +89,5 @@ func init() {
Register(new(obfs2.Transport))
Register(new(obfs3.Transport))
Register(new(obfs4.Transport))
Register(new(scramblesuit.Transport))
}

Loading…
Cancel
Save