summaryrefslogtreecommitdiff
path: root/transports
diff options
context:
space:
mode:
authorYawning Angel <yawning@torproject.org>2015-02-17 11:33:29 +0000
committerYawning Angel <yawning@torproject.org>2015-02-17 11:33:29 +0000
commit0066cfc3932c50323fa596981f18ef5b0e862742 (patch)
tree49ac284ec91fe342a1995693b9bc7d3e015eee8e /transports
parent0f038ca4fa4c175d427251838cfee6fb6d6b9e2f (diff)
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.
Diffstat (limited to 'transports')
-rw-r--r--transports/scramblesuit/base.go88
-rw-r--r--transports/scramblesuit/conn.go521
-rw-r--r--transports/scramblesuit/handshake_ticket.go228
-rw-r--r--transports/scramblesuit/handshake_uniformdh.go174
-rw-r--r--transports/scramblesuit/hkdf_expand.go67
-rw-r--r--transports/transports.go2
6 files changed, 1080 insertions, 0 deletions
diff --git a/transports/scramblesuit/base.go b/transports/scramblesuit/base.go
new file mode 100644
index 0000000..711c046
--- /dev/null
+++ b/transports/scramblesuit/base.go
@@ -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)
diff --git a/transports/scramblesuit/conn.go b/transports/scramblesuit/conn.go
new file mode 100644
index 0000000..b77bc5f
--- /dev/null
+++ b/transports/scramblesuit/conn.go
@@ -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
+}
diff --git a/transports/scramblesuit/handshake_ticket.go b/transports/scramblesuit/handshake_ticket.go
new file mode 100644
index 0000000..ad9b4d4
--- /dev/null
+++ b/transports/scramblesuit/handshake_ticket.go
@@ -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
+}
diff --git a/transports/scramblesuit/handshake_uniformdh.go b/transports/scramblesuit/handshake_uniformdh.go
new file mode 100644
index 0000000..4345d65
--- /dev/null
+++ b/transports/scramblesuit/handshake_uniformdh.go
@@ -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
+}
diff --git a/transports/scramblesuit/hkdf_expand.go b/transports/scramblesuit/hkdf_expand.go
new file mode 100644
index 0000000..9626b38
--- /dev/null
+++ b/transports/scramblesuit/hkdf_expand.go
@@ -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
+}
diff --git a/transports/transports.go b/transports/transports.go
index 6b80bdc..ba4e099 100644
--- a/transports/transports.go
+++ b/transports/transports.go
@@ -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))
}