From 339c63f0c8cd4374f6fa26484498eb6fa91b7bca Mon Sep 17 00:00:00 2001 From: Yawning Angel Date: Sun, 17 Aug 2014 17:11:03 +0000 Subject: Massive cleanup/code reorg. * Changed obfs4proxy to be more like obfsproxy in terms of design, including being an easy framework for developing new TCP/IP style pluggable transports. * Added support for also acting as an obfs2/obfs3 client or bridge as a transition measure (and because the code itself is trivial). * Massively cleaned up the obfs4 and related code to be easier to read, and more idiomatic Go-like in style. * To ease deployment, obfs4proxy will now autogenerate the node-id, curve25519 keypair, and drbg seed if none are specified, and save them to a JSON file in the pt_state directory (Fixes Tor bug #12605). --- transports/base/base.go | 88 +++++ transports/obfs2/obfs2.go | 367 ++++++++++++++++++++ transports/obfs3/obfs3.go | 358 +++++++++++++++++++ transports/obfs4/framing/framing.go | 308 ++++++++++++++++ transports/obfs4/framing/framing_test.go | 169 +++++++++ transports/obfs4/handshake_ntor.go | 426 +++++++++++++++++++++++ transports/obfs4/handshake_ntor_test.go | 222 ++++++++++++ transports/obfs4/obfs4.go | 579 +++++++++++++++++++++++++++++++ transports/obfs4/packet.go | 179 ++++++++++ transports/obfs4/statefile.go | 156 +++++++++ transports/transports.go | 91 +++++ 11 files changed, 2943 insertions(+) create mode 100644 transports/base/base.go create mode 100644 transports/obfs2/obfs2.go create mode 100644 transports/obfs3/obfs3.go create mode 100644 transports/obfs4/framing/framing.go create mode 100644 transports/obfs4/framing/framing_test.go create mode 100644 transports/obfs4/handshake_ntor.go create mode 100644 transports/obfs4/handshake_ntor_test.go create mode 100644 transports/obfs4/obfs4.go create mode 100644 transports/obfs4/packet.go create mode 100644 transports/obfs4/statefile.go create mode 100644 transports/transports.go (limited to 'transports') diff --git a/transports/base/base.go b/transports/base/base.go new file mode 100644 index 0000000..e81ea03 --- /dev/null +++ b/transports/base/base.go @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2014, Yawning Angel + * 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 base provides the common interface that each supported transport +// protocol must implement. +package base + +import ( + "net" + + "git.torproject.org/pluggable-transports/goptlib.git" +) + +// ClientFactory is the interface that defines the factory for creating +// pluggable transport protocol client instances. +type ClientFactory interface { + // Transport returns the Transport instance that this ClientFactory belongs + // to. + Transport() Transport + + // ParseArgs parses the supplied arguments into an internal representation + // for use with WrapConn. This routine is called before the outgoing + // TCP/IP connection is created to allow doing things (like keypair + // generation) to be hidden from third parties. + ParseArgs(args *pt.Args) (interface{}, error) + + // WrapConn wraps the provided net.Conn with a transport protocol + // implementation, and does whatever is required (eg: handshaking) to get + // the connection to a point where it is ready to relay data. + WrapConn(conn net.Conn, args interface{}) (net.Conn, error) +} + +// ServerFactory is the interface that defines the factory for creating +// plugable transport protocol server instances. As the arguments are the +// property of the factory, validation is done at factory creation time. +type ServerFactory interface { + // Transport returns the Transport instance that this ServerFactory belongs + // to. + Transport() Transport + + // Args returns the Args required on the client side to handshake with + // server connections created by this factory. + Args() *pt.Args + + // WrapConn wraps the provided net.Conn with a transport protocol + // implementation, and does whatever is required (eg: handshaking) to get + // the connection to a point where it is ready to relay data. + WrapConn(conn net.Conn) (net.Conn, error) +} + +// Transport is an interface that defines a pluggable transport protocol. +type Transport interface { + // Name returns the name of the transport protocol. It MUST be a valid C + // identifier. + Name() string + + // ClientFactory returns a ClientFactory instance for this transport + // protocol. + ClientFactory(stateDir string) (ClientFactory, error) + + // ServerFactory returns a ServerFactory instance for this transport + // protocol. This can fail if the provided arguments are invalid. + ServerFactory(stateDir string, args *pt.Args) (ServerFactory, error) +} diff --git a/transports/obfs2/obfs2.go b/transports/obfs2/obfs2.go new file mode 100644 index 0000000..3490646 --- /dev/null +++ b/transports/obfs2/obfs2.go @@ -0,0 +1,367 @@ +/* + * Copyright (c) 2014, Yawning Angel + * 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 obfs2 provides an implementation of the Tor Project's obfs2 +// obfuscation protocol. This protocol is considered trivially broken by most +// sophisticated adversaries. +package obfs2 + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "encoding/binary" + "fmt" + "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/transports/base" +) + +const ( + transportName = "obfs2" + sharedSecretArg = "shared-secret" + + clientHandshakeTimeout = time.Duration(30) * time.Second + serverHandshakeTimeout = time.Duration(30) * time.Second + + magicValue = 0x2bf5ca7e + initiatorPadString = "Initiator obfuscation padding" + responderPadString = "Responder obfuscation padding" + initiatorKdfString = "Initiator obfuscated data" + responderKdfString = "Responder obfuscated data" + maxPadding = 8192 + keyLen = 16 + seedLen = 16 + hsLen = 4 + 4 +) + +func validateArgs(args *pt.Args) error { + if _, ok := args.Get(sharedSecretArg); ok { + // "shared-secret" is something no bridges use in practice and is thus + // unimplemented. + return fmt.Errorf("unsupported argument '%s'", sharedSecretArg) + } + return nil +} + +// Transport is the obfs2 implementation of the base.Transport interface. +type Transport struct{} + +// Name returns the name of the obfs2 transport protocol. +func (t *Transport) Name() string { + return transportName +} + +// ClientFactory returns a new obfs2ClientFactory instance. +func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) { + cf := &obfs2ClientFactory{transport: t} + return cf, nil +} + +// ServerFactory returns a new obfs2ServerFactory instance. +func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFactory, error) { + if err := validateArgs(args); err != nil { + return nil, err + } + + sf := &obfs2ServerFactory{t} + return sf, nil +} + +type obfs2ClientFactory struct { + transport base.Transport +} + +func (cf *obfs2ClientFactory) Transport() base.Transport { + return cf.transport +} + +func (cf *obfs2ClientFactory) ParseArgs(args *pt.Args) (interface{}, error) { + return nil, validateArgs(args) +} + +func (cf *obfs2ClientFactory) WrapConn(conn net.Conn, args interface{}) (net.Conn, error) { + return newObfs2ClientConn(conn) +} + +type obfs2ServerFactory struct { + transport base.Transport +} + +func (sf *obfs2ServerFactory) Transport() base.Transport { + return sf.transport +} + +func (sf *obfs2ServerFactory) Args() *pt.Args { + return nil +} + +func (sf *obfs2ServerFactory) WrapConn(conn net.Conn) (net.Conn, error) { + return newObfs2ServerConn(conn) +} + +type obfs2Conn struct { + net.Conn + + isInitiator bool + + rx *cipher.StreamReader + tx *cipher.StreamWriter +} + +func (conn *obfs2Conn) Read(b []byte) (int, error) { + return conn.rx.Read(b) +} + +func (conn *obfs2Conn) Write(b []byte) (int, error) { + return conn.tx.Write(b) +} + +func newObfs2ClientConn(conn net.Conn) (c *obfs2Conn, err error) { + // Initialize a client connection, and start the handshake timeout. + c = &obfs2Conn{conn, true, nil, nil} + deadline := time.Now().Add(clientHandshakeTimeout) + if err = c.SetDeadline(deadline); err != nil { + return nil, err + } + + // Handshake. + if err = c.handshake(); err != nil { + return nil, err + } + + // Disarm the handshake timer. + if err = c.SetDeadline(time.Time{}); err != nil { + return nil, err + } + + return +} + +func newObfs2ServerConn(conn net.Conn) (c *obfs2Conn, err error) { + // Initialize a server connection, and start the handshake timeout. + c = &obfs2Conn{conn, false, nil, nil} + deadline := time.Now().Add(serverHandshakeTimeout) + if err = c.SetDeadline(deadline); err != nil { + return nil, err + } + + // Handshake. + if err = c.handshake(); err != nil { + return nil, err + } + + // Disarm the handshake timer. + if err = c.SetDeadline(time.Time{}); err != nil { + return nil, err + } + + return +} + +func (conn *obfs2Conn) handshake() (err error) { + // Each begins by generating a seed and a padding key as follows. + // The initiator generates: + // + // INIT_SEED = SR(SEED_LENGTH) + // INIT_PAD_KEY = MAC("Initiator obfuscation padding", INIT_SEED)[:KEYLEN] + // + // And the responder generates: + // + // RESP_SEED = SR(SEED_LENGTH) + // RESP_PAD_KEY = MAC("Responder obfuscation padding", INIT_SEED)[:KEYLEN] + // + // Each then generates a random number PADLEN in range from 0 through + // MAX_PADDING (inclusive). + var seed [seedLen]byte + if err = csrand.Bytes(seed[:]); err != nil { + return + } + var padMagic []byte + if conn.isInitiator { + padMagic = []byte(initiatorPadString) + } else { + padMagic = []byte(responderPadString) + } + padKey, padIV := hsKdf(padMagic, seed[:], conn.isInitiator) + padLen := uint32(csrand.IntRange(0, maxPadding)) + + hsBlob := make([]byte, hsLen+padLen) + binary.BigEndian.PutUint32(hsBlob[0:4], magicValue) + binary.BigEndian.PutUint32(hsBlob[4:8], padLen) + if padLen > 0 { + if err = csrand.Bytes(hsBlob[8:]); err != nil { + return + } + } + + // The initiator then sends: + // + // INIT_SEED | E(INIT_PAD_KEY, UINT32(MAGIC_VALUE) | UINT32(PADLEN) | WR(PADLEN)) + // + // and the responder sends: + // + // RESP_SEED | E(RESP_PAD_KEY, UINT32(MAGIC_VALUE) | UINT32(PADLEN) | WR(PADLEN)) + var txBlock cipher.Block + if txBlock, err = aes.NewCipher(padKey); err != nil { + return + } + txStream := cipher.NewCTR(txBlock, padIV) + conn.tx = &cipher.StreamWriter{txStream, conn.Conn, nil} + if _, err = conn.Conn.Write(seed[:]); err != nil { + return + } + if _, err = conn.Write(hsBlob); err != nil { + return + } + + // Upon receiving the SEED from the other party, each party derives + // the other party's padding key value as above, and decrypts the next + // 8 bytes of the key establishment message. + var peerSeed [seedLen]byte + if _, err = io.ReadFull(conn.Conn, peerSeed[:]); err != nil { + return + } + var peerPadMagic []byte + if conn.isInitiator { + peerPadMagic = []byte(responderPadString) + } else { + peerPadMagic = []byte(initiatorPadString) + } + peerKey, peerIV := hsKdf(peerPadMagic, peerSeed[:], !conn.isInitiator) + var rxBlock cipher.Block + if rxBlock, err = aes.NewCipher(peerKey); err != nil { + return + } + rxStream := cipher.NewCTR(rxBlock, peerIV) + conn.rx = &cipher.StreamReader{rxStream, conn.Conn} + hsHdr := make([]byte, hsLen) + if _, err = io.ReadFull(conn, hsHdr[:]); err != nil { + return + } + + // If the MAGIC_VALUE does not match, or the PADLEN value is greater than + // MAX_PADDING, the party receiving it should close the connection + // immediately. + if peerMagic := binary.BigEndian.Uint32(hsHdr[0:4]); peerMagic != magicValue { + err = fmt.Errorf("invalid magic value: %x", peerMagic) + return + } + padLen = binary.BigEndian.Uint32(hsHdr[4:8]) + if padLen > maxPadding { + err = fmt.Errorf("padlen too long: %d", padLen) + return + } + + // Otherwise, it should read the remaining PADLEN bytes of padding data + // and discard them. + tmp := make([]byte, padLen) + if _, err = io.ReadFull(conn.Conn, tmp); err != nil { // Note: Skips AES. + return + } + + // Derive the actual keys. + if err = conn.kdf(seed[:], peerSeed[:]); err != nil { + return + } + + return +} + +func (conn *obfs2Conn) kdf(seed, peerSeed []byte) (err error) { + // Additional keys are then derived as: + // + // INIT_SECRET = MAC("Initiator obfuscated data", INIT_SEED|RESP_SEED) + // RESP_SECRET = MAC("Responder obfuscated data", INIT_SEED|RESP_SEED) + // INIT_KEY = INIT_SECRET[:KEYLEN] + // INIT_IV = INIT_SECRET[KEYLEN:] + // RESP_KEY = RESP_SECRET[:KEYLEN] + // RESP_IV = RESP_SECRET[KEYLEN:] + combSeed := make([]byte, 0, seedLen*2) + if conn.isInitiator { + combSeed = append(combSeed, seed...) + combSeed = append(combSeed, peerSeed...) + } else { + combSeed = append(combSeed, peerSeed...) + combSeed = append(combSeed, seed...) + } + + initKey, initIV := hsKdf([]byte(initiatorKdfString), combSeed, true) + var initBlock cipher.Block + if initBlock, err = aes.NewCipher(initKey); err != nil { + return + } + initStream := cipher.NewCTR(initBlock, initIV) + + respKey, respIV := hsKdf([]byte(responderKdfString), combSeed, false) + var respBlock cipher.Block + if respBlock, err = aes.NewCipher(respKey); err != nil { + return + } + respStream := cipher.NewCTR(respBlock, respIV) + + if conn.isInitiator { + conn.tx.S = initStream + conn.rx.S = respStream + } else { + conn.tx.S = respStream + conn.rx.S = initStream + } + + return +} + +func hsKdf(magic, seed []byte, isInitiator bool) (padKey, padIV []byte) { + // The actual key/IV is derived in the form of: + // m = MAC(magic, seed) + // KEY = m[:KEYLEN] + // IV = m[KEYLEN:] + m := mac(magic, seed) + padKey = m[:keyLen] + padIV = m[keyLen:] + + return +} + +func mac(s, x []byte) []byte { + // H(x) is SHA256 of x. + // MAC(s, x) = H(s | x | s) + h := sha256.New() + h.Write(s) + h.Write(x) + h.Write(s) + return h.Sum(nil) +} + +var _ base.ClientFactory = (*obfs2ClientFactory)(nil) +var _ base.ServerFactory = (*obfs2ServerFactory)(nil) +var _ base.Transport = (*Transport)(nil) +var _ net.Conn = (*obfs2Conn)(nil) diff --git a/transports/obfs3/obfs3.go b/transports/obfs3/obfs3.go new file mode 100644 index 0000000..7844443 --- /dev/null +++ b/transports/obfs3/obfs3.go @@ -0,0 +1,358 @@ +/* + * Copyright (c) 2014, Yawning Angel + * 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 obfs3 provides an implementation of the Tor Project's obfs3 +// obfuscation protocol. +package obfs3 + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/sha256" + "errors" + "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/uniformdh" + "git.torproject.org/pluggable-transports/obfs4.git/transports/base" +) + +const ( + transportName = "obfs3" + + clientHandshakeTimeout = time.Duration(30) * time.Second + serverHandshakeTimeout = time.Duration(30) * time.Second + + initiatorKdfString = "Initiator obfuscated data" + responderKdfString = "Responder obfuscated data" + initiatorMagicString = "Initiator magic" + responderMagicString = "Responder magic" + maxPadding = 8194 + keyLen = 16 +) + +// Transport is the obfs3 implementation of the base.Transport interface. +type Transport struct{} + +// Name returns the name of the obfs3 transport protocol. +func (t *Transport) Name() string { + return transportName +} + +// ClientFactory returns a new obfs3ClientFactory instance. +func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) { + cf := &obfs3ClientFactory{transport: t} + return cf, nil +} + +// ServerFactory returns a new obfs3ServerFactory instance. +func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFactory, error) { + sf := &obfs3ServerFactory{transport: t} + return sf, nil +} + +type obfs3ClientFactory struct { + transport base.Transport +} + +func (cf *obfs3ClientFactory) Transport() base.Transport { + return cf.transport +} + +func (cf *obfs3ClientFactory) ParseArgs(args *pt.Args) (interface{}, error) { + return nil, nil +} + +func (cf *obfs3ClientFactory) WrapConn(conn net.Conn, args interface{}) (net.Conn, error) { + return newObfs3ClientConn(conn) +} + +type obfs3ServerFactory struct { + transport base.Transport +} + +func (sf *obfs3ServerFactory) Transport() base.Transport { + return sf.transport +} + +func (sf *obfs3ServerFactory) Args() *pt.Args { + return nil +} + +func (sf *obfs3ServerFactory) WrapConn(conn net.Conn) (net.Conn, error) { + return newObfs3ServerConn(conn) +} + +type obfs3Conn struct { + net.Conn + + isInitiator bool + rxMagic []byte + txMagic []byte + rxBuf *bytes.Buffer + + rx *cipher.StreamReader + tx *cipher.StreamWriter +} + +func newObfs3ClientConn(conn net.Conn) (c *obfs3Conn, err error) { + // Initialize a client connection, and start the handshake timeout. + c = &obfs3Conn{conn, true, nil, nil, new(bytes.Buffer), nil, nil} + deadline := time.Now().Add(clientHandshakeTimeout) + if err = c.SetDeadline(deadline); err != nil { + return nil, err + } + + // Handshake. + if err = c.handshake(); err != nil { + return nil, err + } + + // Disarm the handshake timer. + if err = c.SetDeadline(time.Time{}); err != nil { + return nil, err + } + + return +} + +func newObfs3ServerConn(conn net.Conn) (c *obfs3Conn, err error) { + // Initialize a server connection, and start the handshake timeout. + c = &obfs3Conn{conn, false, nil, nil, new(bytes.Buffer), nil, nil} + deadline := time.Now().Add(serverHandshakeTimeout) + if err = c.SetDeadline(deadline); err != nil { + return nil, err + } + + // Handshake. + if err = c.handshake(); err != nil { + return nil, err + } + + // Disarm the handshake timer. + if err = c.SetDeadline(time.Time{}); err != nil { + return nil, err + } + + return +} + +func (conn *obfs3Conn) handshake() (err error) { + // The party who opens the connection is the 'initiator'; the one who + // accepts it is the 'responder'. Each begins by generating a + // UniformDH keypair, and a random number PADLEN in [0, MAX_PADDING/2]. + // Both parties then send: + // + // PUB_KEY | WR(PADLEN) + var privateKey *uniformdh.PrivateKey + if privateKey, err = uniformdh.GenerateKey(csrand.Reader); err != nil { + return + } + padLen := csrand.IntRange(0, maxPadding/2) + blob := make([]byte, uniformdh.Size+padLen) + var publicKey []byte + if publicKey, err = privateKey.PublicKey.Bytes(); err != nil { + return + } + copy(blob[0:], publicKey) + if err = csrand.Bytes(blob[uniformdh.Size:]); err != nil { + return + } + if _, err = conn.Conn.Write(blob); err != nil { + return + } + + // Read the public key from the peer. + rawPeerPublicKey := make([]byte, uniformdh.Size) + if _, err = io.ReadFull(conn.Conn, rawPeerPublicKey); err != nil { + return + } + var peerPublicKey uniformdh.PublicKey + if err = peerPublicKey.SetBytes(rawPeerPublicKey); err != nil { + return + } + + // After retrieving the public key of the other end, each party + // completes the DH key exchange and generates a shared-secret for the + // session (named SHARED_SECRET). + var sharedSecret []byte + if sharedSecret, err = uniformdh.Handshake(privateKey, &peerPublicKey); err != nil { + return + } + if err = conn.kdf(sharedSecret); err != nil { + return + } + + return +} + +func (conn *obfs3Conn) kdf(sharedSecret []byte) (err error) { + // Using that shared-secret each party derives its encryption keys as + // follows: + // + // INIT_SECRET = HMAC(SHARED_SECRET, "Initiator obfuscated data") + // RESP_SECRET = HMAC(SHARED_SECRET, "Responder obfuscated data") + // INIT_KEY = INIT_SECRET[:KEYLEN] + // INIT_COUNTER = INIT_SECRET[KEYLEN:] + // RESP_KEY = RESP_SECRET[:KEYLEN] + // RESP_COUNTER = RESP_SECRET[KEYLEN:] + initHmac := hmac.New(sha256.New, sharedSecret) + initHmac.Write([]byte(initiatorKdfString)) + initSecret := initHmac.Sum(nil) + initHmac.Reset() + initHmac.Write([]byte(initiatorMagicString)) + initMagic := initHmac.Sum(nil) + + respHmac := hmac.New(sha256.New, sharedSecret) + respHmac.Write([]byte(responderKdfString)) + respSecret := respHmac.Sum(nil) + respHmac.Reset() + respHmac.Write([]byte(responderMagicString)) + respMagic := respHmac.Sum(nil) + + // The INIT_KEY value keys a block cipher (in CTR mode) used to + // encrypt values from initiator to responder thereafter. The counter + // mode's initial counter value is INIT_COUNTER. The RESP_KEY value + // keys a block cipher (in CTR mode) used to encrypt values from + // responder to initiator thereafter. That counter mode's initial + // counter value is RESP_COUNTER. + // + // Note: To have this be the last place where the shared secret is used, + // also generate the magic value to send/scan for here. + var initBlock cipher.Block + if initBlock, err = aes.NewCipher(initSecret[:keyLen]); err != nil { + return err + } + initStream := cipher.NewCTR(initBlock, initSecret[keyLen:]) + + var respBlock cipher.Block + if respBlock, err = aes.NewCipher(respSecret[:keyLen]); err != nil { + return err + } + respStream := cipher.NewCTR(respBlock, respSecret[keyLen:]) + + if conn.isInitiator { + conn.tx = &cipher.StreamWriter{initStream, conn.Conn, nil} + conn.rx = &cipher.StreamReader{respStream, conn.rxBuf} + conn.txMagic = initMagic + conn.rxMagic = respMagic + } else { + conn.tx = &cipher.StreamWriter{respStream, conn.Conn, nil} + conn.rx = &cipher.StreamReader{initStream, conn.rxBuf} + conn.txMagic = respMagic + conn.rxMagic = initMagic + } + + return +} + +func (conn *obfs3Conn) findPeerMagic() error { + var hsBuf [maxPadding + sha256.Size]byte + for { + n, err := conn.Conn.Read(hsBuf[:]) + if err != nil { + // Yes, Read can return partial data and an error, but continuing + // past that is nonsensical. + return err + } + conn.rxBuf.Write(hsBuf[:n]) + + pos := bytes.Index(conn.rxBuf.Bytes(), conn.rxMagic) + if pos == -1 { + if conn.rxBuf.Len() >= maxPadding+sha256.Size { + return errors.New("failed to find peer magic value") + } + continue + } else if pos > maxPadding { + return errors.New("peer sent too much pre-magic-padding") + } + + // Discard the padding/MAC. + pos += len(conn.rxMagic) + _ = conn.rxBuf.Next(pos) + + return nil + } +} + +func (conn *obfs3Conn) Read(b []byte) (n int, err error) { + // If this is the first time we read data post handshake, scan for the + // magic value. + if conn.rxMagic != nil { + if err = conn.findPeerMagic(); err != nil { + conn.Close() + return + } + conn.rxMagic = nil + } + + // If the handshake receive buffer is still present... + if conn.rxBuf != nil { + // And it is empty... + if conn.rxBuf.Len() == 0 { + // There is no more trailing data left from the handshake process, + // so rewire the cipher.StreamReader to pull data from the network + // instead of the temporary receive buffer. + conn.rx.R = conn.Conn + conn.rxBuf = nil + } + } + + return conn.rx.Read(b) +} + +func (conn *obfs3Conn) Write(b []byte) (n int, err error) { + // If this is the first time we write data post handshake, send the + // padding/magic value. + if conn.txMagic != nil { + padLen := csrand.IntRange(0, maxPadding/2) + blob := make([]byte, padLen+len(conn.txMagic)) + if err = csrand.Bytes(blob[:padLen]); err != nil { + conn.Close() + return + } + copy(blob[padLen:], conn.txMagic) + if _, err = conn.Conn.Write(blob); err != nil { + conn.Close() + return + } + conn.txMagic = nil + } + + return conn.tx.Write(b) +} + + +var _ base.ClientFactory = (*obfs3ClientFactory)(nil) +var _ base.ServerFactory = (*obfs3ServerFactory)(nil) +var _ base.Transport = (*Transport)(nil) +var _ net.Conn = (*obfs3Conn)(nil) diff --git a/transports/obfs4/framing/framing.go b/transports/obfs4/framing/framing.go new file mode 100644 index 0000000..04e788f --- /dev/null +++ b/transports/obfs4/framing/framing.go @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2014, Yawning Angel + * 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 framing implements the obfs4 link framing and cryptography. +// +// The Encoder/Decoder shared secret format is: +// uint8_t[32] NaCl secretbox key +// uint8_t[16] NaCl Nonce prefix +// uint8_t[16] SipHash-2-4 key (used to obfsucate length) +// uint8_t[8] SipHash-2-4 IV +// +// The frame format is: +// uint16_t length (obfsucated, big endian) +// NaCl secretbox (Poly1305/XSalsa20) containing: +// uint8_t[16] tag (Part of the secretbox construct) +// uint8_t[] payload +// +// The length field is length of the NaCl secretbox XORed with the truncated +// SipHash-2-4 digest ran in OFB mode. +// +// Initialize K, IV[0] with values from the shared secret. +// On each packet, IV[n] = H(K, IV[n - 1]) +// mask[n] = IV[n][0:2] +// obfsLen = length ^ mask[n] +// +// The NaCl secretbox (Poly1305/XSalsa20) nonce format is: +// uint8_t[24] prefix (Fixed) +// uint64_t counter (Big endian) +// +// The counter is initialized to 1, and is incremented on each frame. Since +// the protocol is designed to be used over a reliable medium, the nonce is not +// transmitted over the wire as both sides of the conversation know the prefix +// and the initial counter value. It is imperative that the counter does not +// wrap, and sessions MUST terminate before 2^64 frames are sent. +// +package framing + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + + "code.google.com/p/go.crypto/nacl/secretbox" + + "git.torproject.org/pluggable-transports/obfs4.git/common/csrand" + "git.torproject.org/pluggable-transports/obfs4.git/common/drbg" +) + +const ( + // MaximumSegmentLength is the length of the largest possible segment + // including overhead. + MaximumSegmentLength = 1500 - (40 + 12) + + // FrameOverhead is the length of the framing overhead. + FrameOverhead = lengthLength + secretbox.Overhead + + // MaximumFramePayloadLength is the length of the maximum allowed payload + // per frame. + MaximumFramePayloadLength = MaximumSegmentLength - FrameOverhead + + // KeyLength is the length of the Encoder/Decoder secret key. + KeyLength = keyLength + noncePrefixLength + drbg.SeedLength + + maxFrameLength = MaximumSegmentLength - lengthLength + minFrameLength = FrameOverhead - lengthLength + + keyLength = 32 + + noncePrefixLength = 16 + nonceCounterLength = 8 + nonceLength = noncePrefixLength + nonceCounterLength + + lengthLength = 2 +) + +// Error returned when Decoder.Decode() requires more data to continue. +var ErrAgain = errors.New("framing: More data needed to decode") + +// Error returned when Decoder.Decode() failes to authenticate a frame. +var ErrTagMismatch = errors.New("framing: Poly1305 tag mismatch") + +// Error returned when the NaCl secretbox nonce's counter wraps (FATAL). +var ErrNonceCounterWrapped = errors.New("framing: Nonce counter wrapped") + +// InvalidPayloadLengthError is the error returned when Encoder.Encode() +// rejects the payload length. +type InvalidPayloadLengthError int + +func (e InvalidPayloadLengthError) Error() string { + return fmt.Sprintf("framing: Invalid payload length: %d", int(e)) +} + +type boxNonce struct { + prefix [noncePrefixLength]byte + counter uint64 +} + +func (nonce *boxNonce) init(prefix []byte) { + if noncePrefixLength != len(prefix) { + panic(fmt.Sprintf("BUG: Nonce prefix length invalid: %d", len(prefix))) + } + + copy(nonce.prefix[:], prefix) + nonce.counter = 1 +} + +func (nonce boxNonce) bytes(out *[nonceLength]byte) error { + // The security guarantee of Poly1305 is broken if a nonce is ever reused + // for a given key. Detect this by checking for counter wraparound since + // we start each counter at 1. If it ever happens that more than 2^64 - 1 + // frames are transmitted over a given connection, support for rekeying + // will be neccecary, but that's unlikely to happen. + if nonce.counter == 0 { + return ErrNonceCounterWrapped + } + + copy(out[:], nonce.prefix[:]) + binary.BigEndian.PutUint64(out[noncePrefixLength:], nonce.counter) + + return nil +} + +// Encoder is a frame encoder instance. +type Encoder struct { + key [keyLength]byte + nonce boxNonce + drbg *drbg.HashDrbg +} + +// NewEncoder creates a new Encoder instance. It must be supplied a slice +// containing exactly KeyLength bytes of keying material. +func NewEncoder(key []byte) *Encoder { + if len(key) != KeyLength { + panic(fmt.Sprintf("BUG: Invalid encoder key length: %d", len(key))) + } + + encoder := new(Encoder) + copy(encoder.key[:], key[0:keyLength]) + encoder.nonce.init(key[keyLength : keyLength+noncePrefixLength]) + seed, err := drbg.SeedFromBytes(key[keyLength+noncePrefixLength:]) + if err != nil { + panic(fmt.Sprintf("BUG: Failed to initialize DRBG: %s", err)) + } + encoder.drbg, _ = drbg.NewHashDrbg(seed) + + return encoder +} + +// Encode encodes a single frame worth of payload and returns the encoded +// length. InvalidPayloadLengthError is recoverable, all other errors MUST be +// treated as fatal and the session aborted. +func (encoder *Encoder) Encode(frame, payload []byte) (n int, err error) { + payloadLen := len(payload) + if MaximumFramePayloadLength < payloadLen { + return 0, InvalidPayloadLengthError(payloadLen) + } + if len(frame) < payloadLen+FrameOverhead { + return 0, io.ErrShortBuffer + } + + // Generate a new nonce. + var nonce [nonceLength]byte + err = encoder.nonce.bytes(&nonce) + if err != nil { + return 0, err + } + encoder.nonce.counter++ + + // Encrypt and MAC payload. + box := secretbox.Seal(frame[:lengthLength], payload, &nonce, &encoder.key) + + // Obfuscate the length. + length := uint16(len(box) - lengthLength) + lengthMask := encoder.drbg.NextBlock() + length ^= binary.BigEndian.Uint16(lengthMask) + binary.BigEndian.PutUint16(frame[:2], length) + + // Return the frame. + return len(box), nil +} + +// Decoder is a frame decoder instance. +type Decoder struct { + key [keyLength]byte + nonce boxNonce + drbg *drbg.HashDrbg + + nextNonce [nonceLength]byte + nextLength uint16 + nextLengthInvalid bool +} + +// NewDecoder creates a new Decoder instance. It must be supplied a slice +// containing exactly KeyLength bytes of keying material. +func NewDecoder(key []byte) *Decoder { + if len(key) != KeyLength { + panic(fmt.Sprintf("BUG: Invalid decoder key length: %d", len(key))) + } + + decoder := new(Decoder) + copy(decoder.key[:], key[0:keyLength]) + decoder.nonce.init(key[keyLength : keyLength+noncePrefixLength]) + seed, err := drbg.SeedFromBytes(key[keyLength+noncePrefixLength:]) + if err != nil { + panic(fmt.Sprintf("BUG: Failed to initialize DRBG: %s", err)) + } + decoder.drbg, _ = drbg.NewHashDrbg(seed) + + return decoder +} + +// Decode decodes a stream of data and returns the length if any. ErrAgain is +// a temporary failure, all other errors MUST be treated as fatal and the +// session aborted. +func (decoder *Decoder) Decode(data []byte, frames *bytes.Buffer) (int, error) { + // A length of 0 indicates that we do not know how big the next frame is + // going to be. + if decoder.nextLength == 0 { + // Attempt to pull out the next frame length. + if lengthLength > frames.Len() { + return 0, ErrAgain + } + + // Remove the length field from the buffer. + var obfsLen [lengthLength]byte + _, err := io.ReadFull(frames, obfsLen[:]) + if err != nil { + return 0, err + } + + // Derive the nonce the peer used. + err = decoder.nonce.bytes(&decoder.nextNonce) + if err != nil { + return 0, err + } + + // Deobfuscate the length field. + length := binary.BigEndian.Uint16(obfsLen[:]) + lengthMask := decoder.drbg.NextBlock() + length ^= binary.BigEndian.Uint16(lengthMask) + if maxFrameLength < length || minFrameLength > length { + // Per "Plaintext Recovery Attacks Against SSH" by + // Martin R. Albrecht, Kenneth G. Paterson and Gaven J. Watson, + // there are a class of attacks againt protocols that use similar + // sorts of framing schemes. + // + // While obfs4 should not allow plaintext recovery (CBC mode is + // not used), attempt to mitigate out of bound frame length errors + // by pretending that the length was a random valid range as per + // the countermeasure suggested by Denis Bider in section 6 of the + // paper. + + decoder.nextLengthInvalid = true + length = uint16(csrand.IntRange(minFrameLength, maxFrameLength)) + } + decoder.nextLength = length + } + + if int(decoder.nextLength) > frames.Len() { + return 0, ErrAgain + } + + // Unseal the frame. + var box [maxFrameLength]byte + n, err := io.ReadFull(frames, box[:decoder.nextLength]) + if err != nil { + return 0, err + } + out, ok := secretbox.Open(data[:0], box[:n], &decoder.nextNonce, &decoder.key) + if !ok || decoder.nextLengthInvalid { + // When a random length is used (on length error) the tag should always + // mismatch, but be paranoid. + return 0, ErrTagMismatch + } + + // Clean up and prepare for the next frame. + decoder.nextLength = 0 + decoder.nonce.counter++ + + return len(out), nil +} diff --git a/transports/obfs4/framing/framing_test.go b/transports/obfs4/framing/framing_test.go new file mode 100644 index 0000000..03e0d3b --- /dev/null +++ b/transports/obfs4/framing/framing_test.go @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2014, Yawning Angel + * 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 framing + +import ( + "bytes" + "crypto/rand" + "testing" +) + +func generateRandomKey() []byte { + key := make([]byte, KeyLength) + + _, err := rand.Read(key) + if err != nil { + panic(err) + } + + return key +} + +func newEncoder(t *testing.T) *Encoder { + // Generate a key to use. + key := generateRandomKey() + + encoder := NewEncoder(key) + if encoder == nil { + t.Fatalf("NewEncoder returned nil") + } + + return encoder +} + +// TestNewEncoder tests the Encoder ctor. +func TestNewEncoder(t *testing.T) { + encoder := newEncoder(t) + _ = encoder +} + +// TestEncoder_Encode tests Encoder.Encode. +func TestEncoder_Encode(t *testing.T) { + encoder := newEncoder(t) + + buf := make([]byte, MaximumFramePayloadLength) + _, _ = rand.Read(buf) // YOLO + for i := 0; i <= MaximumFramePayloadLength; i++ { + var frame [MaximumSegmentLength]byte + n, err := encoder.Encode(frame[:], buf[0:i]) + if err != nil { + t.Fatalf("Encoder.encode([%d]byte), failed: %s", i, err) + } + if n != i+FrameOverhead { + t.Fatalf("Unexpected encoded framesize: %d, expecting %d", n, i+ + FrameOverhead) + } + } +} + +// TestEncoder_Encode_Oversize tests oversized frame rejection. +func TestEncoder_Encode_Oversize(t *testing.T) { + encoder := newEncoder(t) + + var frame [MaximumSegmentLength]byte + var buf [MaximumFramePayloadLength + 1]byte + _, _ = rand.Read(buf[:]) // YOLO + _, err := encoder.Encode(frame[:], buf[:]) + if _, ok := err.(InvalidPayloadLengthError); !ok { + t.Error("Encoder.encode() returned unexpected error:", err) + } +} + +// TestNewDecoder tests the Decoder ctor. +func TestNewDecoder(t *testing.T) { + key := generateRandomKey() + decoder := NewDecoder(key) + if decoder == nil { + t.Fatalf("NewDecoder returned nil") + } +} + +// TestDecoder_Decode tests Decoder.Decode. +func TestDecoder_Decode(t *testing.T) { + key := generateRandomKey() + + encoder := NewEncoder(key) + decoder := NewDecoder(key) + + var buf [MaximumFramePayloadLength]byte + _, _ = rand.Read(buf[:]) // YOLO + for i := 0; i <= MaximumFramePayloadLength; i++ { + var frame [MaximumSegmentLength]byte + encLen, err := encoder.Encode(frame[:], buf[0:i]) + if err != nil { + t.Fatalf("Encoder.encode([%d]byte), failed: %s", i, err) + } + if encLen != i+FrameOverhead { + t.Fatalf("Unexpected encoded framesize: %d, expecting %d", encLen, + i+FrameOverhead) + } + + var decoded [MaximumFramePayloadLength]byte + + decLen, err := decoder.Decode(decoded[:], bytes.NewBuffer(frame[:encLen])) + if err != nil { + t.Fatalf("Decoder.decode([%d]byte), failed: %s", i, err) + } + if decLen != i { + t.Fatalf("Unexpected decoded framesize: %d, expecting %d", + decLen, i) + } + + if 0 != bytes.Compare(decoded[:decLen], buf[:i]) { + t.Fatalf("Frame %d does not match encoder input", i) + } + } +} + +// BencharkEncoder_Encode benchmarks Encoder.Encode processing 1 MiB +// of payload. +func BenchmarkEncoder_Encode(b *testing.B) { + var chopBuf [MaximumFramePayloadLength]byte + var frame [MaximumSegmentLength]byte + payload := make([]byte, 1024*1024) + encoder := NewEncoder(generateRandomKey()) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + transfered := 0 + buffer := bytes.NewBuffer(payload) + for 0 < buffer.Len() { + n, err := buffer.Read(chopBuf[:]) + if err != nil { + b.Fatal("buffer.Read() failed:", err) + } + + n, err = encoder.Encode(frame[:], chopBuf[:n]) + transfered += n - FrameOverhead + } + if transfered != len(payload) { + b.Fatalf("Transfered length mismatch: %d != %d", transfered, + len(payload)) + } + } +} diff --git a/transports/obfs4/handshake_ntor.go b/transports/obfs4/handshake_ntor.go new file mode 100644 index 0000000..8dcf0c8 --- /dev/null +++ b/transports/obfs4/handshake_ntor.go @@ -0,0 +1,426 @@ +/* + * Copyright (c) 2014, Yawning Angel + * 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 obfs4 + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "hash" + "strconv" + "time" + + "git.torproject.org/pluggable-transports/obfs4.git/common/csrand" + "git.torproject.org/pluggable-transports/obfs4.git/common/ntor" + "git.torproject.org/pluggable-transports/obfs4.git/common/replayfilter" + "git.torproject.org/pluggable-transports/obfs4.git/transports/obfs4/framing" +) + +const ( + maxHandshakeLength = 8192 + + clientMinPadLength = (serverMinHandshakeLength + inlineSeedFrameLength) - + clientMinHandshakeLength + clientMaxPadLength = maxHandshakeLength - clientMinHandshakeLength + clientMinHandshakeLength = ntor.RepresentativeLength + markLength + macLength + + serverMinPadLength = 0 + serverMaxPadLength = maxHandshakeLength - (serverMinHandshakeLength + + inlineSeedFrameLength) + serverMinHandshakeLength = ntor.RepresentativeLength + ntor.AuthLength + + markLength + macLength + + markLength = sha256.Size / 2 + macLength = sha256.Size / 2 + + inlineSeedFrameLength = framing.FrameOverhead + packetOverhead + seedPacketPayloadLength +) + +// ErrMarkNotFoundYet is the error returned when the obfs4 handshake is +// incomplete and requires more data to continue. This error is non-fatal and +// is the equivalent to EAGAIN/EWOULDBLOCK. +var ErrMarkNotFoundYet = errors.New("handshake: M_[C,S] not found yet") + +// ErrInvalidHandshake is the error returned when the obfs4 handshake fails due +// to the peer not sending the correct mark. This error is fatal and the +// connection MUST be dropped. +var ErrInvalidHandshake = errors.New("handshake: Failed to find M_[C,S]") + +// ErrReplayedHandshake is the error returned when the obfs4 handshake fails +// due it being replayed. This error is fatal and the connection MUST be +// dropped. +var ErrReplayedHandshake = errors.New("handshake: Replay detected") + +// ErrNtorFailed is the error returned when the ntor handshake fails. This +// error is fatal and the connection MUST be dropped. +var ErrNtorFailed = errors.New("handshake: ntor handshake failure") + +// InvalidMacError is the error returned when the handshake MACs do not match. +// This error is fatal and the connection MUST be dropped. +type InvalidMacError struct { + Derived []byte + Received []byte +} + +func (e *InvalidMacError) Error() string { + return fmt.Sprintf("handshake: MAC mismatch: Dervied: %s Received: %s.", + hex.EncodeToString(e.Derived), hex.EncodeToString(e.Received)) +} + +// InvalidAuthError is the error returned when the ntor AUTH tags do not match. +// This error is fatal and the connection MUST be dropped. +type InvalidAuthError struct { + Derived *ntor.Auth + Received *ntor.Auth +} + +func (e *InvalidAuthError) Error() string { + return fmt.Sprintf("handshake: ntor AUTH mismatch: Derived: %s Received:%s.", + hex.EncodeToString(e.Derived.Bytes()[:]), + hex.EncodeToString(e.Received.Bytes()[:])) +} + +type clientHandshake struct { + keypair *ntor.Keypair + nodeID *ntor.NodeID + serverIdentity *ntor.PublicKey + epochHour []byte + + padLen int + mac hash.Hash + + serverRepresentative *ntor.Representative + serverAuth *ntor.Auth + serverMark []byte +} + +func newClientHandshake(nodeID *ntor.NodeID, serverIdentity *ntor.PublicKey, sessionKey *ntor.Keypair) *clientHandshake { + hs := new(clientHandshake) + hs.keypair = sessionKey + hs.nodeID = nodeID + hs.serverIdentity = serverIdentity + hs.padLen = csrand.IntRange(clientMinPadLength, clientMaxPadLength) + hs.mac = hmac.New(sha256.New, append(hs.serverIdentity.Bytes()[:], hs.nodeID.Bytes()[:]...)) + + return hs +} + +func (hs *clientHandshake) generateHandshake() ([]byte, error) { + var buf bytes.Buffer + + hs.mac.Reset() + hs.mac.Write(hs.keypair.Representative().Bytes()[:]) + mark := hs.mac.Sum(nil)[:markLength] + + // The client handshake is X | P_C | M_C | MAC(X | P_C | M_C | E) where: + // * X is the client's ephemeral Curve25519 public key representative. + // * P_C is [clientMinPadLength,clientMaxPadLength] bytes of random padding. + // * M_C is HMAC-SHA256-128(serverIdentity | NodeID, X) + // * MAC is HMAC-SHA256-128(serverIdentity | NodeID, X .... E) + // * E is the string representation of the number of hours since the UNIX + // epoch. + + // Generate the padding + pad, err := makePad(hs.padLen) + if err != nil { + return nil, err + } + + // Write X, P_C, M_C. + buf.Write(hs.keypair.Representative().Bytes()[:]) + buf.Write(pad) + buf.Write(mark) + + // Calculate and write the MAC. + hs.mac.Reset() + hs.mac.Write(buf.Bytes()) + hs.epochHour = []byte(strconv.FormatInt(getEpochHour(), 10)) + hs.mac.Write(hs.epochHour) + buf.Write(hs.mac.Sum(nil)[:macLength]) + + return buf.Bytes(), nil +} + +func (hs *clientHandshake) parseServerHandshake(resp []byte) (int, []byte, error) { + // No point in examining the data unless the miminum plausible response has + // been received. + if serverMinHandshakeLength > len(resp) { + return 0, nil, ErrMarkNotFoundYet + } + + if hs.serverRepresentative == nil || hs.serverAuth == nil { + // Pull out the representative/AUTH. (XXX: Add ctors to ntor) + hs.serverRepresentative = new(ntor.Representative) + copy(hs.serverRepresentative.Bytes()[:], resp[0:ntor.RepresentativeLength]) + hs.serverAuth = new(ntor.Auth) + copy(hs.serverAuth.Bytes()[:], resp[ntor.RepresentativeLength:]) + + // Derive the mark. + hs.mac.Reset() + hs.mac.Write(hs.serverRepresentative.Bytes()[:]) + hs.serverMark = hs.mac.Sum(nil)[:markLength] + } + + // Attempt to find the mark + MAC. + pos := findMarkMac(hs.serverMark, resp, ntor.RepresentativeLength+ntor.AuthLength+serverMinPadLength, + maxHandshakeLength, false) + if pos == -1 { + if len(resp) >= maxHandshakeLength { + return 0, nil, ErrInvalidHandshake + } + return 0, nil, ErrMarkNotFoundYet + } + + // Validate the MAC. + hs.mac.Reset() + hs.mac.Write(resp[:pos+markLength]) + hs.mac.Write(hs.epochHour) + macCmp := hs.mac.Sum(nil)[:macLength] + macRx := resp[pos+markLength : pos+markLength+macLength] + if !hmac.Equal(macCmp, macRx) { + return 0, nil, &InvalidMacError{macCmp, macRx} + } + + // Complete the handshake. + serverPublic := hs.serverRepresentative.ToPublic() + ok, seed, auth := ntor.ClientHandshake(hs.keypair, serverPublic, + hs.serverIdentity, hs.nodeID) + if !ok { + return 0, nil, ErrNtorFailed + } + if !ntor.CompareAuth(auth, hs.serverAuth.Bytes()[:]) { + return 0, nil, &InvalidAuthError{auth, hs.serverAuth} + } + + return pos + markLength + macLength, seed.Bytes()[:], nil +} + +type serverHandshake struct { + keypair *ntor.Keypair + nodeID *ntor.NodeID + serverIdentity *ntor.Keypair + epochHour []byte + serverAuth *ntor.Auth + + padLen int + mac hash.Hash + + clientRepresentative *ntor.Representative + clientMark []byte +} + +func newServerHandshake(nodeID *ntor.NodeID, serverIdentity *ntor.Keypair, sessionKey *ntor.Keypair) *serverHandshake { + hs := new(serverHandshake) + hs.keypair = sessionKey + hs.nodeID = nodeID + hs.serverIdentity = serverIdentity + hs.padLen = csrand.IntRange(serverMinPadLength, serverMaxPadLength) + hs.mac = hmac.New(sha256.New, append(hs.serverIdentity.Public().Bytes()[:], hs.nodeID.Bytes()[:]...)) + + return hs +} + +func (hs *serverHandshake) parseClientHandshake(filter *replayfilter.ReplayFilter, resp []byte) ([]byte, error) { + // No point in examining the data unless the miminum plausible response has + // been received. + if clientMinHandshakeLength > len(resp) { + return nil, ErrMarkNotFoundYet + } + + if hs.clientRepresentative == nil { + // Pull out the representative/AUTH. (XXX: Add ctors to ntor) + hs.clientRepresentative = new(ntor.Representative) + copy(hs.clientRepresentative.Bytes()[:], resp[0:ntor.RepresentativeLength]) + + // Derive the mark. + hs.mac.Reset() + hs.mac.Write(hs.clientRepresentative.Bytes()[:]) + hs.clientMark = hs.mac.Sum(nil)[:markLength] + } + + // Attempt to find the mark + MAC. + pos := findMarkMac(hs.clientMark, resp, ntor.RepresentativeLength+clientMinPadLength, + maxHandshakeLength, true) + if pos == -1 { + if len(resp) >= maxHandshakeLength { + return nil, ErrInvalidHandshake + } + return nil, ErrMarkNotFoundYet + } + + // Validate the MAC. + macFound := false + for _, off := range []int64{0, -1, 1} { + // Allow epoch to be off by up to a hour in either direction. + epochHour := []byte(strconv.FormatInt(getEpochHour()+int64(off), 10)) + hs.mac.Reset() + hs.mac.Write(resp[:pos+markLength]) + hs.mac.Write(epochHour) + macCmp := hs.mac.Sum(nil)[:macLength] + macRx := resp[pos+markLength : pos+markLength+macLength] + if hmac.Equal(macCmp, macRx) { + // Ensure that this handshake has not been seen previously. + if filter.TestAndSet(time.Now(), macRx) { + // The client either happened to generate exactly the same + // session key and padding, or someone is replaying a previous + // handshake. In either case, fuck them. + return nil, ErrReplayedHandshake + } + + macFound = true + hs.epochHour = epochHour + + // We could break out here, but in the name of reducing timing + // variation, evaluate all 3 MACs. + } + } + if !macFound { + // This probably should be an InvalidMacError, but conveying the 3 MACS + // that would be accepted is annoying so just return a generic fatal + // failure. + return nil, ErrInvalidHandshake + } + + // Client should never sent trailing garbage. + if len(resp) != pos+markLength+macLength { + return nil, ErrInvalidHandshake + } + + clientPublic := hs.clientRepresentative.ToPublic() + ok, seed, auth := ntor.ServerHandshake(clientPublic, hs.keypair, + hs.serverIdentity, hs.nodeID) + if !ok { + return nil, ErrNtorFailed + } + hs.serverAuth = auth + + return seed.Bytes()[:], nil +} + +func (hs *serverHandshake) generateHandshake() ([]byte, error) { + var buf bytes.Buffer + + hs.mac.Reset() + hs.mac.Write(hs.keypair.Representative().Bytes()[:]) + mark := hs.mac.Sum(nil)[:markLength] + + // The server handshake is Y | AUTH | P_S | M_S | MAC(Y | AUTH | P_S | M_S | E) where: + // * Y is the server's ephemeral Curve25519 public key representative. + // * AUTH is the ntor handshake AUTH value. + // * P_S is [serverMinPadLength,serverMaxPadLength] bytes of random padding. + // * M_S is HMAC-SHA256-128(serverIdentity | NodeID, Y) + // * MAC is HMAC-SHA256-128(serverIdentity | NodeID, Y .... E) + // * E is the string representation of the number of hours since the UNIX + // epoch. + + // Generate the padding + pad, err := makePad(hs.padLen) + if err != nil { + return nil, err + } + + // Write Y, AUTH, P_S, M_S. + buf.Write(hs.keypair.Representative().Bytes()[:]) + buf.Write(hs.serverAuth.Bytes()[:]) + buf.Write(pad) + buf.Write(mark) + + // Calculate and write the MAC. + hs.mac.Reset() + hs.mac.Write(buf.Bytes()) + hs.epochHour = []byte(strconv.FormatInt(getEpochHour(), 10)) + hs.mac.Write(hs.epochHour) + buf.Write(hs.mac.Sum(nil)[:macLength]) + + return buf.Bytes(), nil +} + +// getEpochHour returns the number of hours since the UNIX epoch. +func getEpochHour() int64 { + return time.Now().Unix() / 3600 +} + +func findMarkMac(mark, buf []byte, startPos, maxPos int, fromTail bool) (pos int) { + if len(mark) != markLength { + panic(fmt.Sprintf("BUG: Invalid mark length: %d", len(mark))) + } + + endPos := len(buf) + if startPos > len(buf) { + return -1 + } + if endPos > maxPos { + endPos = maxPos + } + if endPos-startPos < markLength+macLength { + return -1 + } + + if fromTail { + // The server can optimize the search process by only examining the + // tail of the buffer. The client can't send valid data past M_C | + // MAC_C as it does not have the server's public key yet. + pos = endPos - (markLength + macLength) + if !hmac.Equal(buf[pos:pos+markLength], mark) { + return -1 + } + + return + } + + // The client has to actually do a substring search since the server can + // and will send payload trailing the response. + // + // XXX: bytes.Index() uses a naive search, which kind of sucks. + pos = bytes.Index(buf[startPos:endPos], mark) + if pos == -1 { + return -1 + } + + // Ensure that there is enough trailing data for the MAC. + if startPos+pos+markLength+macLength > endPos { + return -1 + } + + // Return the index relative to the start of the slice. + pos += startPos + return +} + +func makePad(padLen int) ([]byte, error) { + pad := make([]byte, padLen) + err := csrand.Bytes(pad) + if err != nil { + return nil, err + } + + return pad, err +} diff --git a/transports/obfs4/handshake_ntor_test.go b/transports/obfs4/handshake_ntor_test.go new file mode 100644 index 0000000..fa03420 --- /dev/null +++ b/transports/obfs4/handshake_ntor_test.go @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2014, Yawning Angel + * 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 obfs4 + +import ( + "bytes" + "testing" + + "git.torproject.org/pluggable-transports/obfs4.git/common/ntor" + "git.torproject.org/pluggable-transports/obfs4.git/common/replayfilter" +) + +func TestHandshakeNtor(t *testing.T) { + // Generate the server node id and id keypair. + nodeID, _ := ntor.NewNodeID([]byte("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13")) + idKeypair, _ := ntor.NewKeypair(false) + serverFilter, _ := replayfilter.New(replayTTL) + + // Test client handshake padding. + for l := clientMinPadLength; l <= clientMaxPadLength; l++ { + // Generate the client state and override the pad length. + clientKeypair, err := ntor.NewKeypair(true) + if err != nil { + t.Fatalf("[%d:0] ntor.NewKeypair failed: %s", l, err) + } + clientHs := newClientHandshake(nodeID, idKeypair.Public(), clientKeypair) + clientHs.padLen = l + + // Generate what the client will send to the server. + clientBlob, err := clientHs.generateHandshake() + if err != nil { + t.Fatalf("[%d:0] clientHandshake.generateHandshake() failed: %s", l, err) + } + if len(clientBlob) > maxHandshakeLength { + t.Fatalf("[%d:0] Generated client body is oversized: %d", l, len(clientBlob)) + } + if len(clientBlob) < clientMinHandshakeLength { + t.Fatalf("[%d:0] Generated client body is undersized: %d", l, len(clientBlob)) + } + if len(clientBlob) != clientMinHandshakeLength+l { + t.Fatalf("[%d:0] Generated client body incorrect size: %d", l, len(clientBlob)) + } + + // Generate the server state and override the pad length. + serverKeypair, err := ntor.NewKeypair(true) + if err != nil { + t.Fatalf("[%d:0] ntor.NewKeypair failed: %s", l, err) + } + serverHs := newServerHandshake(nodeID, idKeypair, serverKeypair) + serverHs.padLen = serverMinPadLength + + // Parse the client handshake message. + serverSeed, err := serverHs.parseClientHandshake(serverFilter, clientBlob) + if err != nil { + t.Fatalf("[%d:0] serverHandshake.parseClientHandshake() failed: %s", l, err) + } + + // Genrate what the server will send to the client. + serverBlob, err := serverHs.generateHandshake() + if err != nil { + t.Fatal("[%d:0]: serverHandshake.generateHandshake() failed: %s", l, err) + } + + // Parse the server handshake message. + clientHs.serverRepresentative = nil + n, clientSeed, err := clientHs.parseServerHandshake(serverBlob) + if err != nil { + t.Fatalf("[%d:0] clientHandshake.parseServerHandshake() failed: %s", l, err) + } + if n != len(serverBlob) { + t.Fatalf("[%d:0] clientHandshake.parseServerHandshake() has bytes remaining: %d", l, n) + } + + // Ensure the derived shared secret is the same. + if 0 != bytes.Compare(clientSeed, serverSeed) { + t.Fatalf("[%d:0] client/server seed mismatch", l) + } + } + + // Test server handshake padding. + for l := serverMinPadLength; l <= serverMaxPadLength+inlineSeedFrameLength; l++ { + // Generate the client state and override the pad length. + clientKeypair, err := ntor.NewKeypair(true) + if err != nil { + t.Fatalf("[%d:0] ntor.NewKeypair failed: %s", l, err) + } + clientHs := newClientHandshake(nodeID, idKeypair.Public(), clientKeypair) + clientHs.padLen = clientMinPadLength + + // Generate what the client will send to the server. + clientBlob, err := clientHs.generateHandshake() + if err != nil { + t.Fatalf("[%d:1] clientHandshake.generateHandshake() failed: %s", l, err) + } + if len(clientBlob) > maxHandshakeLength { + t.Fatalf("[%d:1] Generated client body is oversized: %d", l, len(clientBlob)) + } + + // Generate the server state and override the pad length. + serverKeypair, err := ntor.NewKeypair(true) + if err != nil { + t.Fatalf("[%d:0] ntor.NewKeypair failed: %s", l, err) + } + serverHs := newServerHandshake(nodeID, idKeypair, serverKeypair) + serverHs.padLen = l + + // Parse the client handshake message. + serverSeed, err := serverHs.parseClientHandshake(serverFilter, clientBlob) + if err != nil { + t.Fatalf("[%d:1] serverHandshake.parseClientHandshake() failed: %s", l, err) + } + + // Genrate what the server will send to the client. + serverBlob, err := serverHs.generateHandshake() + if err != nil { + t.Fatal("[%d:1]: serverHandshake.generateHandshake() failed: %s", l, err) + } + + // Parse the server handshake message. + n, clientSeed, err := clientHs.parseServerHandshake(serverBlob) + if err != nil { + t.Fatalf("[%d:1] clientHandshake.parseServerHandshake() failed: %s", l, err) + } + if n != len(serverBlob) { + t.Fatalf("[%d:1] clientHandshake.parseServerHandshake() has bytes remaining: %d", l, n) + } + + // Ensure the derived shared secret is the same. + if 0 != bytes.Compare(clientSeed, serverSeed) { + t.Fatalf("[%d:1] client/server seed mismatch", l) + } + } + + // Test oversized client padding. + clientKeypair, err := ntor.NewKeypair(true) + if err != nil { + t.Fatalf("ntor.NewKeypair failed: %s", err) + } + clientHs := newClientHandshake(nodeID, idKeypair.Public(), clientKeypair) + if err != nil { + t.Fatalf("newClientHandshake failed: %s", err) + } + + clientHs.padLen = clientMaxPadLength + 1 + clientBlob, err := clientHs.generateHandshake() + if err != nil { + t.Fatalf("clientHandshake.generateHandshake() (forced oversize) failed: %s", err) + } + serverKeypair, err := ntor.NewKeypair(true) + if err != nil { + t.Fatalf("ntor.NewKeypair failed: %s", err) + } + serverHs := newServerHandshake(nodeID, idKeypair, serverKeypair) + _, err = serverHs.parseClientHandshake(serverFilter, clientBlob) + if err == nil { + t.Fatalf("serverHandshake.parseClientHandshake() succeded (oversized)") + } + + // Test undersized client padding. + clientHs.padLen = clientMinPadLength - 1 + clientBlob, err = clientHs.generateHandshake() + if err != nil { + t.Fatalf("clientHandshake.generateHandshake() (forced undersize) failed: %s", err) + } + serverHs = newServerHandshake(nodeID, idKeypair, serverKeypair) + _, err = serverHs.parseClientHandshake(serverFilter, clientBlob) + if err == nil { + t.Fatalf("serverHandshake.parseClientHandshake() succeded (undersized)") + } + + // Test oversized server padding. + // + // NB: serverMaxPadLength isn't the real maxPadLength that triggers client + // rejection, because the implementation is written with the asusmption + // that/ the PRNG_SEED is also inlined with the response. Thus the client + // actually accepts longer padding. The server handshake test and this + // test adjust around that. + clientHs.padLen = clientMinPadLength + clientBlob, err = clientHs.generateHandshake() + if err != nil { + t.Fatalf("clientHandshake.generateHandshake() failed: %s", err) + } + serverHs = newServerHandshake(nodeID, idKeypair, serverKeypair) + serverHs.padLen = serverMaxPadLength + inlineSeedFrameLength + 1 + _, err = serverHs.parseClientHandshake(serverFilter, clientBlob) + if err != nil { + t.Fatalf("serverHandshake.parseClientHandshake() failed: %s", err) + } + serverBlob, err := serverHs.generateHandshake() + if err != nil { + t.Fatal("serverHandshake.generateHandshake() (forced oversize) failed: %s", err) + } + _, _, err = clientHs.parseServerHandshake(serverBlob) + if err == nil { + t.Fatalf("clientHandshake.parseServerHandshake() succeded (oversized)") + } +} diff --git a/transports/obfs4/obfs4.go b/transports/obfs4/obfs4.go new file mode 100644 index 0000000..7af7224 --- /dev/null +++ b/transports/obfs4/obfs4.go @@ -0,0 +1,579 @@ +/* + * Copyright (c) 2014, Yawning Angel + * 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 obfs4 provides an implementation of the Tor Project's obfs4 +// obfuscation protocol. +package obfs4 + +import ( + "bytes" + "crypto/sha256" + "fmt" + "math/rand" + "net" + "syscall" + "time" + + "git.torproject.org/pluggable-transports/goptlib.git" + "git.torproject.org/pluggable-transports/obfs4.git/common/drbg" + "git.torproject.org/pluggable-transports/obfs4.git/common/ntor" + "git.torproject.org/pluggable-transports/obfs4.git/common/probdist" + "git.torproject.org/pluggable-transports/obfs4.git/common/replayfilter" + "git.torproject.org/pluggable-transports/obfs4.git/transports/base" + "git.torproject.org/pluggable-transports/obfs4.git/transports/obfs4/framing" +) + +const ( + transportName = "obfs4" + + nodeIDArg = "node-id" + publicKeyArg = "public-key" + privateKeyArg = "private-key" + seedArg = "drbg-seed" + + seedLength = 32 + headerLength = framing.FrameOverhead + packetOverhead + clientHandshakeTimeout = time.Duration(60) * time.Second + serverHandshakeTimeout = time.Duration(30) * time.Second + replayTTL = time.Duration(3) * time.Hour + + // Use a ScrambleSuit style biased probability table. + biasedDist = false + + // Use IAT obfuscation. + iatObfuscation = false + + // Maximum IAT delay (100 usec increments). + maxIATDelay = 100 + + maxCloseDelayBytes = maxHandshakeLength + maxCloseDelay = 60 +) + +type obfs4ClientArgs struct { + nodeID *ntor.NodeID + publicKey *ntor.PublicKey + sessionKey *ntor.Keypair +} + +// Transport is the obfs4 implementation of the base.Transport interface. +type Transport struct{} + +// Name returns the name of the obfs4 transport protocol. +func (t *Transport) Name() string { + return transportName +} + +// ClientFactory returns a new obfs4ClientFactory instance. +func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) { + cf := &obfs4ClientFactory{transport: t} + return cf, nil +} + +// ServerFactory returns a new obfs4ServerFactory instance. +func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFactory, error) { + var err error + + var st *obfs4ServerState + if st, err = serverStateFromArgs(stateDir, args); err != nil { + return nil, err + } + + var iatSeed *drbg.Seed + if iatObfuscation { + iatSeedSrc := sha256.Sum256(st.drbgSeed.Bytes()[:]) + iatSeed, err = drbg.SeedFromBytes(iatSeedSrc[:]) + if err != nil { + return nil, err + } + } + + // Store the arguments that should appear in our descriptor for the clients. + ptArgs := pt.Args{} + ptArgs.Add(nodeIDArg, st.nodeID.Base64()) + ptArgs.Add(publicKeyArg, st.identityKey.Public().Base64()) + + // Initialize the replay filter. + filter, err := replayfilter.New(replayTTL) + if err != nil { + return nil, err + } + + // Initialize the close thresholds for failed connections. + drbg, err := drbg.NewHashDrbg(st.drbgSeed) + if err != nil { + return nil, err + } + rng := rand.New(drbg) + + sf := &obfs4ServerFactory{t, &ptArgs, st.nodeID, st.identityKey, st.drbgSeed, iatSeed, filter, rng.Intn(maxCloseDelayBytes), rng.Intn(maxCloseDelay)} + return sf, nil +} + +type obfs4ClientFactory struct { + transport base.Transport +} + +func (cf *obfs4ClientFactory) Transport() base.Transport { + return cf.transport +} + +func (cf *obfs4ClientFactory) ParseArgs(args *pt.Args) (interface{}, error) { + var err error + + // Handle the arguments. + nodeIDStr, ok := args.Get(nodeIDArg) + if !ok { + return nil, fmt.Errorf("missing argument '%s'", nodeIDArg) + } + var nodeID *ntor.NodeID + if nodeID, err = ntor.NodeIDFromBase64(nodeIDStr); err != nil { + return nil, err + } + + publicKeyStr, ok := args.Get(publicKeyArg) + if !ok { + return nil, fmt.Errorf("missing argument '%s'", publicKeyArg) + } + var publicKey *ntor.PublicKey + if publicKey, err = ntor.PublicKeyFromBase64(publicKeyStr); err != nil { + return nil, err + } + + // Generate the session key pair before connectiong to hide the Elligator2 + // rejection sampling from network observers. + sessionKey, err := ntor.NewKeypair(true) + if err != nil { + return nil, err + } + + return &obfs4ClientArgs{nodeID, publicKey, sessionKey}, nil +} + +func (cf *obfs4ClientFactory) WrapConn(conn net.Conn, args interface{}) (net.Conn, error) { + ca, ok := args.(*obfs4ClientArgs) + if !ok { + return nil, fmt.Errorf("invalid argument type for args") + } + + return newObfs4ClientConn(conn, ca) +} + +type obfs4ServerFactory struct { + transport base.Transport + args *pt.Args + + nodeID *ntor.NodeID + identityKey *ntor.Keypair + lenSeed *drbg.Seed + iatSeed *drbg.Seed + replayFilter *replayfilter.ReplayFilter + + closeDelayBytes int + closeDelay int +} + +func (sf *obfs4ServerFactory) Transport() base.Transport { + return sf.transport +} + +func (sf *obfs4ServerFactory) Args() *pt.Args { + return sf.args +} + +func (sf *obfs4ServerFactory) WrapConn(conn net.Conn) (net.Conn, error) { + // Not much point in having a separate newObfs4ServerConn routine when + // wrapping requires using values from the factory instance. + + // Generate the session keypair *before* consuming data from the peer, to + // attempt to mask the rejection sampling due to use of Elligator2. This + // might be futile, but the timing differential isn't very large on modern + // hardware, and there are far easier statistical attacks that can be + // mounted as a distinguisher. + sessionKey, err := ntor.NewKeypair(true) + if err != nil { + return nil, err + } + + lenDist := probdist.New(sf.lenSeed, 0, framing.MaximumSegmentLength, biasedDist) + var iatDist *probdist.WeightedDist + if sf.iatSeed != nil { + iatDist = probdist.New(sf.iatSeed, 0, maxIATDelay, biasedDist) + } + + c := &obfs4Conn{conn, true, lenDist, iatDist, bytes.NewBuffer(nil), bytes.NewBuffer(nil), nil, nil} + + startTime := time.Now() + + if err = c.serverHandshake(sf, sessionKey); err != nil { + c.closeAfterDelay(sf, startTime) + return nil, err + } + + return c, nil +} + +type obfs4Conn struct { + net.Conn + + isServer bool + + lenDist *probdist.WeightedDist + iatDist *probdist.WeightedDist + + receiveBuffer *bytes.Buffer + receiveDecodedBuffer *bytes.Buffer + + encoder *framing.Encoder + decoder *framing.Decoder +} + +func newObfs4ClientConn(conn net.Conn, args *obfs4ClientArgs) (c *obfs4Conn, err error) { + // Generate the initial protocol polymorphism distribution(s). + var seed *drbg.Seed + if seed, err = drbg.NewSeed(); err != nil { + return + } + lenDist := probdist.New(seed, 0, framing.MaximumSegmentLength, biasedDist) + var iatDist *probdist.WeightedDist + if iatObfuscation { + var iatSeed *drbg.Seed + iatSeedSrc := sha256.Sum256(seed.Bytes()[:]) + if iatSeed, err = drbg.SeedFromBytes(iatSeedSrc[:]); err != nil { + return + } + iatDist = probdist.New(iatSeed, 0, maxIATDelay, biasedDist) + } + + // Allocate the client structure. + c = &obfs4Conn{conn, false, lenDist, iatDist, bytes.NewBuffer(nil), bytes.NewBuffer(nil), nil, nil} + + // Start the handshake timeout. + deadline := time.Now().Add(clientHandshakeTimeout) + if err = conn.SetDeadline(deadline); err != nil { + return nil, err + } + + if err = c.clientHandshake(args.nodeID, args.publicKey, args.sessionKey); err != nil { + return nil, err + } + + // Stop the handshake timeout. + if err = conn.SetDeadline(time.Time{}); err != nil { + return nil, err + } + + return +} + +func (conn *obfs4Conn) clientHandshake(nodeID *ntor.NodeID, peerIdentityKey *ntor.PublicKey, sessionKey *ntor.Keypair) error { + if conn.isServer { + return fmt.Errorf("clientHandshake called on server connection") + } + + // Generate and send the client handshake. + hs := newClientHandshake(nodeID, peerIdentityKey, sessionKey) + blob, err := hs.generateHandshake() + if err != nil { + return err + } + if _, err = conn.Conn.Write(blob); err != nil { + return err + } + + // Consume the server handshake. + var hsBuf [maxHandshakeLength]byte + for { + var n int + if n, err = conn.Conn.Read(hsBuf[:]); err != nil { + // The Read() could have returned data and an error, but there is + // no point in continuing on an EOF or whatever. + return err + } + conn.receiveBuffer.Write(hsBuf[:n]) + + var seed []byte + n, seed, err = hs.parseServerHandshake(conn.receiveBuffer.Bytes()) + if err == ErrMarkNotFoundYet { + continue + } else if err != nil { + return err + } + _ = conn.receiveBuffer.Next(n) + + // Use the derived key material to intialize the link crypto. + okm := ntor.Kdf(seed, framing.KeyLength*2) + conn.encoder = framing.NewEncoder(okm[:framing.KeyLength]) + conn.decoder = framing.NewDecoder(okm[framing.KeyLength:]) + + return nil + } +} + +func (conn *obfs4Conn) serverHandshake(sf *obfs4ServerFactory, sessionKey *ntor.Keypair) (err error) { + if !conn.isServer { + return fmt.Errorf("serverHandshake called on client connection") + } + + // Generate the server handshake, and arm the base timeout. + hs := newServerHandshake(sf.nodeID, sf.identityKey, sessionKey) + if err = conn.Conn.SetDeadline(time.Now().Add(serverHandshakeTimeout)); err != nil { + return + } + + // Consume the client handshake. + var hsBuf [maxHandshakeLength]byte + for { + var n int + if n, err = conn.Conn.Read(hsBuf[:]); err != nil { + // The Read() could have returned data and an error, but there is + // no point in continuing on an EOF or whatever. + return + } + conn.receiveBuffer.Write(hsBuf[:n]) + + var seed []byte + seed, err = hs.parseClientHandshake(sf.replayFilter, conn.receiveBuffer.Bytes()) + if err == ErrMarkNotFoundYet { + continue + } else if err != nil { + return + } + conn.receiveBuffer.Reset() + + if err = conn.Conn.SetDeadline(time.Time{}); err != nil { + return + } + + // Use the derived key material to intialize the link crypto. + okm := ntor.Kdf(seed, framing.KeyLength*2) + conn.encoder = framing.NewEncoder(okm[framing.KeyLength:]) + conn.decoder = framing.NewDecoder(okm[:framing.KeyLength]) + + break + } + + // Since the current and only implementation always sends a PRNG seed for + // the length obfuscation, this makes the amount of data received from the + // server inconsistent with the length sent from the client. + // + // Rebalance this by tweaking the client mimimum padding/server maximum + // padding, and sending the PRNG seed unpadded (As in, treat the PRNG seed + // as part of the server response). See inlineSeedFrameLength in + // handshake_ntor.go. + + // Generate/send the response. + var blob []byte + blob, err = hs.generateHandshake() + if err != nil { + return + } + var frameBuf bytes.Buffer + _, err = frameBuf.Write(blob) + if err != nil { + return + } + + // Send the PRNG seed as the first packet. + if err = conn.makePacket(&frameBuf, packetTypePrngSeed, sf.lenSeed.Bytes()[:], 0); err != nil { + return + } + if _, err = conn.Conn.Write(frameBuf.Bytes()); err != nil { + return + } + + return +} + +func (conn *obfs4Conn) Read(b []byte) (n int, err error) { + // If there is no payload from the previous Read() calls, consume data off + // the network. Not all data received is guaranteed to be usable payload, + // so do this in a loop till data is present or an error occurs. + for conn.receiveDecodedBuffer.Len() == 0 { + err = conn.readPackets() + if err == framing.ErrAgain { + // Don't proagate this back up the call stack if we happen to break + // out of the loop. + err = nil + continue + } else if err != nil { + break + } + } + + // Even if err is set, attempt to do the read anyway so that all decoded + // data gets relayed before the connection is torn down. + if conn.receiveDecodedBuffer.Len() > 0 { + var berr error + n, berr = conn.receiveDecodedBuffer.Read(b) + if err == nil { + // Only propagate berr if there are not more important (fatal) + // errors from the network/crypto/packet processing. + err = berr + } + } + + return +} + +func (conn *obfs4Conn) Write(b []byte) (n int, err error) { + chopBuf := bytes.NewBuffer(b) + var payload [maxPacketPayloadLength]byte + var frameBuf bytes.Buffer + + // Chop the pending data into payload frames. + for chopBuf.Len() > 0 { + // Send maximum sized frames. + rdLen := 0 + rdLen, err = chopBuf.Read(payload[:]) + if err != nil { + return 0, err + } else if rdLen == 0 { + panic(fmt.Sprintf("BUG: Write(), chopping length was 0")) + } + n += rdLen + + err = conn.makePacket(&frameBuf, packetTypePayload, payload[:rdLen], 0) + if err != nil { + return 0, err + } + } + + // Add the length obfuscation padding. In theory, this could be inlined + // with the last chopped packet for certain (most?) payload lenghts, but + // this is simpler. + + if err = conn.padBurst(&frameBuf); err != nil { + return 0, err + } + + // Write the pending data onto the network. Partial writes are fatal, + // because the frame encoder state is advanced, and the code doesn't keep + // frameBuf around. In theory, write timeouts and whatnot could be + // supported if this wasn't the case, but that complicates the code. + + if conn.iatDist != nil { + var iatFrame [framing.MaximumSegmentLength]byte + for frameBuf.Len() > 0 { + iatWrLen := 0 + iatWrLen, err = frameBuf.Read(iatFrame[:]) + if err != nil { + return 0, err + } else if iatWrLen == 0 { + panic(fmt.Sprintf("BUG: Write(), iat length was 0")) + } + + // Calculate the delay. The delay resolution is 100 usec, leading + // to a maximum delay of 10 msec. + iatDelta := time.Duration(conn.iatDist.Sample() * 100) + + // Write then sleep. + _, err = conn.Conn.Write(iatFrame[:iatWrLen]) + if err != nil { + return 0, err + } + time.Sleep(iatDelta * time.Microsecond) + } + } else { + _, err = conn.Conn.Write(frameBuf.Bytes()) + } + + return +} + +func (conn *obfs4Conn) SetDeadline(t time.Time) error { + return syscall.ENOTSUP +} + +func (conn *obfs4Conn) SetWriteDeadline(t time.Time) error { + return syscall.ENOTSUP +} + +func (conn *obfs4Conn) closeAfterDelay(sf *obfs4ServerFactory, startTime time.Time) { + // I-it's not like I w-wanna handshake with you or anything. B-b-baka! + defer conn.Conn.Close() + + delay := time.Duration(sf.closeDelay)*time.Second + serverHandshakeTimeout + deadline := startTime.Add(delay) + if time.Now().After(deadline) { + return + } + + if err := conn.Conn.SetReadDeadline(deadline); err != nil { + return + } + + // Consume and discard data on this connection until either the specified + // interval passes or a certain size has been reached. + discarded := 0 + var buf [framing.MaximumSegmentLength]byte + for discarded < int(sf.closeDelayBytes) { + n, err := conn.Conn.Read(buf[:]) + if err != nil { + return + } + discarded += n + } +} + +func (conn *obfs4Conn) padBurst(burst *bytes.Buffer) (err error) { + tailLen := burst.Len() % framing.MaximumSegmentLength + toPadTo := conn.lenDist.Sample() + + padLen := 0 + if toPadTo >= tailLen { + padLen = toPadTo - tailLen + } else { + padLen = (framing.MaximumSegmentLength - tailLen) + toPadTo + } + + if padLen > headerLength { + err = conn.makePacket(burst, packetTypePayload, []byte{}, + uint16(padLen-headerLength)) + if err != nil { + return + } + } else if padLen > 0 { + err = conn.makePacket(burst, packetTypePayload, []byte{}, + maxPacketPayloadLength) + if err != nil { + return + } + err = conn.makePacket(burst, packetTypePayload, []byte{}, + uint16(padLen)) + if err != nil { + return + } + } + + return +} + +var _ base.ClientFactory = (*obfs4ClientFactory)(nil) +var _ base.ServerFactory = (*obfs4ServerFactory)(nil) +var _ base.Transport = (*Transport)(nil) +var _ net.Conn = (*obfs4Conn)(nil) diff --git a/transports/obfs4/packet.go b/transports/obfs4/packet.go new file mode 100644 index 0000000..9865c82 --- /dev/null +++ b/transports/obfs4/packet.go @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2014, Yawning Angel + * 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 obfs4 + +import ( + "crypto/sha256" + "encoding/binary" + "fmt" + "io" + + "git.torproject.org/pluggable-transports/obfs4.git/common/drbg" + "git.torproject.org/pluggable-transports/obfs4.git/transports/obfs4/framing" +) + +const ( + packetOverhead = 2 + 1 + maxPacketPayloadLength = framing.MaximumFramePayloadLength - packetOverhead + maxPacketPaddingLength = maxPacketPayloadLength + seedPacketPayloadLength = seedLength + + consumeReadSize = framing.MaximumSegmentLength * 16 +) + +const ( + packetTypePayload = iota + packetTypePrngSeed +) + +// InvalidPacketLengthError is the error returned when decodePacket detects a +// invalid packet length/ +type InvalidPacketLengthError int + +func (e InvalidPacketLengthError) Error() string { + return fmt.Sprintf("packet: Invalid packet length: %d", int(e)) +} + +// InvalidPayloadLengthError is the error returned when decodePacket rejects the +// payload length. +type InvalidPayloadLengthError int + +func (e InvalidPayloadLengthError) Error() string { + return fmt.Sprintf("packet: Invalid payload length: %d", int(e)) +} + +var zeroPadBytes [maxPacketPaddingLength]byte + +func (conn *obfs4Conn) makePacket(w io.Writer, pktType uint8, data []byte, padLen uint16) (err error) { + var pkt [framing.MaximumFramePayloadLength]byte + + if len(data)+int(padLen) > maxPacketPayloadLength { + panic(fmt.Sprintf("BUG: makePacket() len(data) + padLen > maxPacketPayloadLength: %d + %d > %d", + len(data), padLen, maxPacketPayloadLength)) + } + + // Packets are: + // uint8_t type packetTypePayload (0x00) + // uint16_t length Length of the payload (Big Endian). + // uint8_t[] payload Data payload. + // uint8_t[] padding Padding. + pkt[0] = pktType + binary.BigEndian.PutUint16(pkt[1:], uint16(len(data))) + if len(data) > 0 { + copy(pkt[3:], data[:]) + } + copy(pkt[3+len(data):], zeroPadBytes[:padLen]) + + pktLen := packetOverhead + len(data) + int(padLen) + + // Encode the packet in an AEAD frame. + var frame [framing.MaximumSegmentLength]byte + frameLen := 0 + frameLen, err = conn.encoder.Encode(frame[:], pkt[:pktLen]) + if err != nil { + // All encoder errors are fatal. + return + } + var wrLen int + wrLen, err = w.Write(frame[:frameLen]) + if err != nil { + return + } else if wrLen < frameLen { + err = io.ErrShortWrite + return + } + + return +} + +func (conn *obfs4Conn) readPackets() (err error) { + // Attempt to read off the network. + var buf [consumeReadSize]byte + rdLen, rdErr := conn.Conn.Read(buf[:]) + conn.receiveBuffer.Write(buf[:rdLen]) + + var decoded [framing.MaximumFramePayloadLength]byte + for conn.receiveBuffer.Len() > 0 { + // Decrypt an AEAD frame. + decLen := 0 + decLen, err = conn.decoder.Decode(decoded[:], conn.receiveBuffer) + if err == framing.ErrAgain { + break + } else if err != nil { + break + } else if decLen < packetOverhead { + err = InvalidPacketLengthError(decLen) + break + } + + // Decode the packet. + pkt := decoded[0:decLen] + pktType := pkt[0] + payloadLen := binary.BigEndian.Uint16(pkt[1:]) + if int(payloadLen) > len(pkt)-packetOverhead { + err = InvalidPayloadLengthError(int(payloadLen)) + break + } + payload := pkt[3 : 3+payloadLen] + + switch pktType { + case packetTypePayload: + if payloadLen > 0 { + conn.receiveDecodedBuffer.Write(payload) + } + case packetTypePrngSeed: + // Only regenerate the distribution if we are the client. + if len(payload) == seedPacketPayloadLength && !conn.isServer { + var seed *drbg.Seed + seed, err = drbg.SeedFromBytes(payload) + if err != nil { + break + } + conn.lenDist.Reset(seed) + if conn.iatDist != nil { + iatSeedSrc := sha256.Sum256(seed.Bytes()[:]) + iatSeed, err := drbg.SeedFromBytes(iatSeedSrc[:]) + if err != nil { + break + } + conn.iatDist.Reset(iatSeed) + } + } + default: + // Ignore unknown packet types. + } + } + + // Read errors (all fatal) take priority over various frame processing + // errors. + if rdErr != nil { + return rdErr + } + + return +} diff --git a/transports/obfs4/statefile.go b/transports/obfs4/statefile.go new file mode 100644 index 0000000..814a545 --- /dev/null +++ b/transports/obfs4/statefile.go @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2014, Yawning Angel + * 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 obfs4 + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path" + + "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/ntor" +) + +const ( + stateFile = "obfs4_state.json" +) + +type jsonServerState struct { + NodeID string `json:"node-id"` + PrivateKey string `json:"private-key"` + PublicKey string `json:"public-key"` + DrbgSeed string `json:"drbgSeed"` +} + +type obfs4ServerState struct { + nodeID *ntor.NodeID + identityKey *ntor.Keypair + drbgSeed *drbg.Seed +} + +func serverStateFromArgs(stateDir string, args *pt.Args) (*obfs4ServerState, error) { + var js jsonServerState + var nodeIDOk, privKeyOk, seedOk bool + + js.NodeID, nodeIDOk = args.Get(nodeIDArg) + js.PrivateKey, privKeyOk = args.Get(privateKeyArg) + js.DrbgSeed, seedOk = args.Get(seedArg) + + if !privKeyOk && !nodeIDOk && !seedOk { + if err := jsonServerStateFromFile(stateDir, &js); err != nil { + return nil, err + } + } else if !privKeyOk { + return nil, fmt.Errorf("missing argument '%s'", privateKeyArg) + } else if !nodeIDOk { + return nil, fmt.Errorf("missing argument '%s'", nodeIDArg) + } else if !seedOk { + return nil, fmt.Errorf("missing argument '%s'", seedArg) + } + + return serverStateFromJSONServerState(&js) +} + +func serverStateFromJSONServerState(js *jsonServerState) (*obfs4ServerState, error) { + var err error + + st := new(obfs4ServerState) + if st.nodeID, err = ntor.NodeIDFromBase64(js.NodeID); err != nil { + return nil, err + } + if st.identityKey, err = ntor.KeypairFromBase64(js.PrivateKey); err != nil { + return nil, err + } + var rawSeed []byte + if rawSeed, err = base64.StdEncoding.DecodeString(js.DrbgSeed); err != nil { + return nil, err + } + if st.drbgSeed, err = drbg.SeedFromBytes(rawSeed); err != nil { + return nil, err + } + + return st, nil +} + +func jsonServerStateFromFile(stateDir string, js *jsonServerState) error { + f, err := ioutil.ReadFile(path.Join(stateDir, stateFile)) + if err != nil { + if os.IsNotExist(err) { + if err = newJSONServerState(stateDir, js); err == nil { + return nil + } + } + return err + } + + if err = json.Unmarshal(f, js); err != nil { + return err + } + + return nil +} + +func newJSONServerState(stateDir string, js *jsonServerState) (err error) { + // Generate everything a server needs, using the cryptographic PRNG. + var st obfs4ServerState + rawID := make([]byte, ntor.NodeIDLength) + if err = csrand.Bytes(rawID); err != nil { + return + } + if st.nodeID, err = ntor.NewNodeID(rawID); err != nil { + return + } + if st.identityKey, err = ntor.NewKeypair(false); err != nil { + return + } + if st.drbgSeed, err = drbg.NewSeed(); err != nil { + return + } + + // Encode it into JSON format and write the state file. + js.NodeID = st.nodeID.Base64() + js.PrivateKey = st.identityKey.Private().Base64() + js.PublicKey = st.identityKey.Public().Base64() + js.DrbgSeed = st.drbgSeed.Base64() + + var encoded []byte + if encoded, err = json.Marshal(js); err != nil { + return + } + + if err = ioutil.WriteFile(path.Join(stateDir, stateFile), encoded, 0600); err != nil { + return err + } + + return nil +} diff --git a/transports/transports.go b/transports/transports.go new file mode 100644 index 0000000..6b80bdc --- /dev/null +++ b/transports/transports.go @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2014, Yawning Angel + * 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 transports provides a interface to query supported pluggable +// transports. +package transports + +import ( + "fmt" + "sync" + + "git.torproject.org/pluggable-transports/obfs4.git/transports/base" + "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" +) + +var transportMapLock sync.Mutex +var transportMap map[string]base.Transport + +// Register registers a transport protocol. +func Register(transport base.Transport) error { + transportMapLock.Lock() + defer transportMapLock.Unlock() + + name := transport.Name() + _, registered := transportMap[name] + if registered { + return fmt.Errorf("transport '%s' already registered", name) + } + transportMap[name] = transport + + return nil +} + +// Transports returns the list of registered transport protocols. +func Transports() []string { + transportMapLock.Lock() + defer transportMapLock.Unlock() + + var ret []string + for name := range transportMap { + ret = append(ret, name) + } + + return ret +} + +// Get returns a transport protocol implementation by name. +func Get(name string) base.Transport { + transportMapLock.Lock() + defer transportMapLock.Unlock() + + t := transportMap[name] + + return t +} + +func init() { + // Initialize the transport list. + transportMap = make(map[string]base.Transport) + + // Register all the currently supported transports. + Register(new(obfs2.Transport)) + Register(new(obfs3.Transport)) + Register(new(obfs4.Transport)) +} -- cgit v1.2.3