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). --- README.md | 23 +- common/csrand/csrand.go | 101 ++++ common/drbg/hash_drbg.go | 149 ++++++ common/ntor/ntor.go | 432 +++++++++++++++++ common/ntor/ntor_test.go | 180 +++++++ common/probdist/weighted_dist.go | 220 +++++++++ common/probdist/weighted_dist_test.go | 80 ++++ common/replayfilter/replay_filter.go | 147 ++++++ common/replayfilter/replay_filter_test.go | 95 ++++ common/uniformdh/uniformdh.go | 183 ++++++++ common/uniformdh/uniformdh_test.go | 220 +++++++++ csrand/csrand.go | 102 ---- drbg/hash_drbg.go | 147 ------ framing/framing.go | 310 ------------ framing/framing_test.go | 171 ------- handshake_ntor.go | 427 ----------------- handshake_ntor_test.go | 221 --------- ntor/ntor.go | 437 ----------------- ntor/ntor_test.go | 182 ------- obfs4.go | 758 ------------------------------ obfs4proxy/obfs4proxy.go | 553 ++++++++++------------ obfs4proxy/proxy_extras.go | 51 -- obfs4proxy/proxy_http.go | 3 - obfs4proxy/proxy_socks4.go | 2 - obfs4proxy/pt_extras.go | 23 +- packet.go | 212 --------- replay_filter.go | 145 ------ replay_filter_test.go | 92 ---- 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 ++++ weighted_dist.go | 212 --------- weighted_dist_test.go | 82 ---- 41 files changed, 5014 insertions(+), 3889 deletions(-) create mode 100644 common/csrand/csrand.go create mode 100644 common/drbg/hash_drbg.go create mode 100644 common/ntor/ntor.go create mode 100644 common/ntor/ntor_test.go create mode 100644 common/probdist/weighted_dist.go create mode 100644 common/probdist/weighted_dist_test.go create mode 100644 common/replayfilter/replay_filter.go create mode 100644 common/replayfilter/replay_filter_test.go create mode 100644 common/uniformdh/uniformdh.go create mode 100644 common/uniformdh/uniformdh_test.go delete mode 100644 csrand/csrand.go delete mode 100644 drbg/hash_drbg.go delete mode 100644 framing/framing.go delete mode 100644 framing/framing_test.go delete mode 100644 handshake_ntor.go delete mode 100644 handshake_ntor_test.go delete mode 100644 ntor/ntor.go delete mode 100644 ntor/ntor_test.go delete mode 100644 obfs4.go delete mode 100644 obfs4proxy/proxy_extras.go delete mode 100644 packet.go delete mode 100644 replay_filter.go delete mode 100644 replay_filter_test.go 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 delete mode 100644 weighted_dist.go delete mode 100644 weighted_dist_test.go diff --git a/README.md b/README.md index c97588a..3ee9c0c 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,6 @@ ## obfs4 - The obfourscator #### Yawning Angel (yawning at torproject dot org) -### WARNING - -This is pre-alpha. Don't expect any security or wire protocol stability yet. -If you want to use something like this, you should currently probably be looking -at ScrambleSuit. - ### What? This is a look-like nothing obfuscation protocol that incorporates ideas and @@ -22,6 +16,9 @@ The notable differences between ScrambleSuit and obfs4: obfuscated via the Elligator 2 mapping. * The link layer encryption uses NaCl secret boxes (Poly1305/XSalsa20). +As an added bonus, obfs4proxy also supports acting as an obfs2/3 client and +bridge to ease the transition to the new protocol. + ### Why not extend ScrambleSuit? It's my protocol and I'll obfuscate if I want to. @@ -43,20 +40,6 @@ listed for clarity. * SipHash-2-4 (https://github.com/dchest/siphash) * goptlib (https://git.torproject.org/pluggable-transports/goptlib.git) -### TODO - - * Code cleanups. - * Write more unit tests. - * Optimize further. - -### WON'T DO - - * I do not care that much about standalone mode. Patches *MAY* be accepted, - especially if they are clean and are useful to Tor users. - * Yes, I use a bunch of code from the borg^w^wGoogle. If that bothers you - feel free to write your own implementation. - * I do not care about older versions of the go runtime. - ### Thanks * David Fifield for goptlib. diff --git a/common/csrand/csrand.go b/common/csrand/csrand.go new file mode 100644 index 0000000..45849d3 --- /dev/null +++ b/common/csrand/csrand.go @@ -0,0 +1,101 @@ +/* + * 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 csrand implements the math/rand interface over crypto/rand, along +// with some utility functions for common random number/byte related tasks. +// +// Not all of the convinience routines are replicated, only those that are +// immediately useful. The Rand variable provides access to the full math/rand +// API. +package csrand + +import ( + cryptRand "crypto/rand" + "encoding/binary" + "fmt" + "io" + "math/rand" +) + +var ( + csRandSourceInstance csRandSource + + // Rand is a math/rand instance backed by crypto/rand CSPRNG. + Rand = rand.New(csRandSourceInstance) +) + +type csRandSource struct { + // This does not keep any state as it is backed by crypto/rand. +} + +func (r csRandSource) Int63() int64 { + var src [8]byte + if err := Bytes(src[:]); err != nil { + panic(err) + } + val := binary.BigEndian.Uint64(src[:]) + val &= (1<<63 - 1) + + return int64(val) +} + +func (r csRandSource) Seed(seed int64) { + // No-op. +} + +// Intn returns, as a int, a pseudo random number in [0, n). +func Intn(n int) int { + return Rand.Intn(n) +} + +// Float64 returns, as a float64, a pesudo random number in [0.0,1.0). +func Float64() float64 { + return Rand.Float64() +} + +// IntRange returns a uniformly distributed int [min, max]. +func IntRange(min, max int) int { + if max < min { + panic(fmt.Sprintf("IntRange: min > max (%d, %d)", min, max)) + } + + r := (max + 1) - min + ret := Rand.Intn(r) + return ret + min +} + +// Bytes fills the slice with random data. +func Bytes(buf []byte) error { + if _, err := io.ReadFull(cryptRand.Reader, buf); err != nil { + return err + } + + return nil +} + +// Reader is a alias of rand.Reader. +var Reader = cryptRand.Reader diff --git a/common/drbg/hash_drbg.go b/common/drbg/hash_drbg.go new file mode 100644 index 0000000..5329828 --- /dev/null +++ b/common/drbg/hash_drbg.go @@ -0,0 +1,149 @@ +/* + * 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 drbg implements a minimalistic DRBG based off SipHash-2-4 in OFB +// mode. +package drbg + +import ( + "encoding/base64" + "encoding/binary" + "fmt" + "hash" + + "github.com/dchest/siphash" + + "git.torproject.org/pluggable-transports/obfs4.git/common/csrand" +) + +// Size is the length of the HashDrbg output. +const Size = siphash.Size + +// SeedLength is the length of the HashDrbg seed. +const SeedLength = 16 + Size + +// Seed is the initial state for a HashDrbg. It consists of a SipHash-2-4 +// key, and 8 bytes of initial data. +type Seed [SeedLength]byte + +// Bytes returns a pointer to the raw HashDrbg seed. +func (seed *Seed) Bytes() *[SeedLength]byte { + return (*[SeedLength]byte)(seed) +} + +// Base64 returns the Base64 representation of the seed. +func (seed *Seed) Base64() string { + return base64.StdEncoding.EncodeToString(seed.Bytes()[:]) +} + +// NewSeed returns a Seed initialized with the runtime CSPRNG. +func NewSeed() (seed *Seed, err error) { + seed = new(Seed) + if err = csrand.Bytes(seed.Bytes()[:]); err != nil { + return nil, err + } + + return +} + +// SeedFromBytes creates a Seed from the raw bytes, truncating to SeedLength as +// appropriate. +func SeedFromBytes(src []byte) (seed *Seed, err error) { + if len(src) < SeedLength { + return nil, InvalidSeedLengthError(len(src)) + } + + seed = new(Seed) + copy(seed.Bytes()[:], src) + + return +} + +// SeedFromBase64 creates a Seed from the Base64 representation, truncating to +// SeedLength as appropriate. +func SeedFromBase64(encoded string) (seed *Seed, err error) { + var raw []byte + if raw, err = base64.StdEncoding.DecodeString(encoded); err != nil { + return nil, err + } + + return SeedFromBytes(raw) +} + +// InvalidSeedLengthError is the error returned when the seed provided to the +// DRBG is an invalid length. +type InvalidSeedLengthError int + +func (e InvalidSeedLengthError) Error() string { + return fmt.Sprintf("invalid seed length: %d", int(e)) +} + +// HashDrbg is a CSDRBG based off of SipHash-2-4 in OFB mode. +type HashDrbg struct { + sip hash.Hash64 + ofb [Size]byte +} + +// NewHashDrbg makes a HashDrbg instance based off an optional seed. The seed +// is truncated to SeedLength. +func NewHashDrbg(seed *Seed) (*HashDrbg, error) { + drbg := new(HashDrbg) + if seed == nil { + var err error + if seed, err = NewSeed(); err != nil { + return nil, err + } + } + drbg.sip = siphash.New(seed.Bytes()[:16]) + copy(drbg.ofb[:], seed.Bytes()[16:]) + + return drbg, nil +} + +// Int63 returns a uniformly distributed random integer [0, 1 << 63). +func (drbg *HashDrbg) Int63() int64 { + block := drbg.NextBlock() + ret := binary.BigEndian.Uint64(block) + ret &= (1<<63 - 1) + + return int64(ret) +} + +// Seed does nothing, call NewHashDrbg if you want to reseed. +func (drbg *HashDrbg) Seed(seed int64) { + // No-op. +} + +// NextBlock returns the next 8 byte DRBG block. +func (drbg *HashDrbg) NextBlock() []byte { + drbg.sip.Write(drbg.ofb[:]) + copy(drbg.ofb[:], drbg.sip.Sum(nil)) + + ret := make([]byte, Size) + copy(ret, drbg.ofb[:]) + return ret +} diff --git a/common/ntor/ntor.go b/common/ntor/ntor.go new file mode 100644 index 0000000..37cfe88 --- /dev/null +++ b/common/ntor/ntor.go @@ -0,0 +1,432 @@ +/* + * 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 ntor implements the Tor Project's ntor handshake as defined in +// proposal 216 "Improved circuit-creation key exchange". It also supports +// using Elligator to transform the Curve25519 public keys sent over the wire +// to a form that is indistinguishable from random strings. +// +// Before using this package, it is strongly recommended that the specification +// is read and understood. +package ntor + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "fmt" + "io" + + "code.google.com/p/go.crypto/curve25519" + "code.google.com/p/go.crypto/hkdf" + + "github.com/agl/ed25519/extra25519" + + "git.torproject.org/pluggable-transports/obfs4.git/common/csrand" +) + +const ( + // PublicKeyLength is the length of a Curve25519 public key. + PublicKeyLength = 32 + + // RepresentativeLength is the length of an Elligator representative. + RepresentativeLength = 32 + + // PrivateKeyLength is the length of a Curve25519 private key. + PrivateKeyLength = 32 + + // SharedSecretLength is the length of a Curve25519 shared secret. + SharedSecretLength = 32 + + // NodeIDLength is the length of a ntor node identifier. + NodeIDLength = 20 + + // KeySeedLength is the length of the derived KEY_SEED. + KeySeedLength = sha256.Size + + // AuthLength is the lenght of the derived AUTH. + AuthLength = sha256.Size +) + +var protoID = []byte("ntor-curve25519-sha256-1") +var tMac = append(protoID, []byte(":mac")...) +var tKey = append(protoID, []byte(":key_extract")...) +var tVerify = append(protoID, []byte(":key_verify")...) +var mExpand = append(protoID, []byte(":key_expand")...) + +// PublicKeyLengthError is the error returned when the public key being +// imported is an invalid length. +type PublicKeyLengthError int + +func (e PublicKeyLengthError) Error() string { + return fmt.Sprintf("ntor: Invalid Curve25519 public key length: %d", + int(e)) +} + +// PrivateKeyLengthError is the error returned when the private key being +// imported is an invalid length. +type PrivateKeyLengthError int + +func (e PrivateKeyLengthError) Error() string { + return fmt.Sprintf("ntor: Invalid Curve25519 private key length: %d", + int(e)) +} + +// NodeIDLengthError is the error returned when the node ID being imported is +// an invalid length. +type NodeIDLengthError int + +func (e NodeIDLengthError) Error() string { + return fmt.Sprintf("ntor: Invalid NodeID length: %d", int(e)) +} + +// KeySeed is the key material that results from a handshake (KEY_SEED). +type KeySeed [KeySeedLength]byte + +// Bytes returns a pointer to the raw key material. +func (key_seed *KeySeed) Bytes() *[KeySeedLength]byte { + return (*[KeySeedLength]byte)(key_seed) +} + +// Auth is the verifier that results from a handshake (AUTH). +type Auth [AuthLength]byte + +// Bytes returns a pointer to the raw auth. +func (auth *Auth) Bytes() *[AuthLength]byte { + return (*[AuthLength]byte)(auth) +} + +// NodeID is a ntor node identifier. +type NodeID [NodeIDLength]byte + +// NewNodeID creates a NodeID from the raw bytes. +func NewNodeID(raw []byte) (*NodeID, error) { + if len(raw) != NodeIDLength { + return nil, NodeIDLengthError(len(raw)) + } + + nodeID := new(NodeID) + copy(nodeID[:], raw) + + return nodeID, nil +} + +// NodeIDFromBase64 creates a new NodeID from the Base64 encoded representation. +func NodeIDFromBase64(encoded string) (*NodeID, error) { + raw, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return nil, err + } + + return NewNodeID(raw) +} +// Bytes returns a pointer to the raw NodeID. +func (id *NodeID) Bytes() *[NodeIDLength]byte { + return (*[NodeIDLength]byte)(id) +} + +// Base64 returns the Base64 representation of the NodeID. +func (id *NodeID) Base64() string { + return base64.StdEncoding.EncodeToString(id[:]) +} + +// PublicKey is a Curve25519 public key in little-endian byte order. +type PublicKey [PublicKeyLength]byte + +// Bytes returns a pointer to the raw Curve25519 public key. +func (public *PublicKey) Bytes() *[PublicKeyLength]byte { + return (*[PublicKeyLength]byte)(public) +} + +// Base64 returns the Base64 representation of the Curve25519 public key. +func (public *PublicKey) Base64() string { + return base64.StdEncoding.EncodeToString(public.Bytes()[:]) +} + +// NewPublicKey creates a PublicKey from the raw bytes. +func NewPublicKey(raw []byte) (*PublicKey, error) { + if len(raw) != PublicKeyLength { + return nil, PublicKeyLengthError(len(raw)) + } + + pubKey := new(PublicKey) + copy(pubKey[:], raw) + + return pubKey, nil +} + +// PublicKeyFromBase64 returns a PublicKey from a Base64 representation. +func PublicKeyFromBase64(encoded string) (*PublicKey, error) { + raw, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return nil, err + } + + return NewPublicKey(raw) +} + +// Representative is an Elligator representative of a Curve25519 public key +// in little-endian byte order. +type Representative [RepresentativeLength]byte + +// Bytes returns a pointer to the raw Elligator representative. +func (repr *Representative) Bytes() *[RepresentativeLength]byte { + return (*[RepresentativeLength]byte)(repr) +} + +// ToPublic converts a Elligator representative to a Curve25519 public key. +func (repr *Representative) ToPublic() *PublicKey { + pub := new(PublicKey) + + extra25519.RepresentativeToPublicKey(pub.Bytes(), repr.Bytes()) + return pub +} + +// PrivateKey is a Curve25519 private key in little-endian byte order. +type PrivateKey [PrivateKeyLength]byte + +// Bytes returns a pointer to the raw Curve25519 private key. +func (private *PrivateKey) Bytes() *[PrivateKeyLength]byte { + return (*[PrivateKeyLength]byte)(private) +} + +// Base64 returns the Base64 representation of the Curve25519 private key. +func (private *PrivateKey) Base64() string { + return base64.StdEncoding.EncodeToString(private.Bytes()[:]) +} + +// Keypair is a Curve25519 keypair with an optional Elligator representative. +// As only certain Curve25519 keys can be obfuscated with Elligator, the +// representative must be generated along with the keypair. +type Keypair struct { + public *PublicKey + private *PrivateKey + representative *Representative +} + +// Public returns the Curve25519 public key belonging to the Keypair. +func (keypair *Keypair) Public() *PublicKey { + return keypair.public +} + +// Private returns the Curve25519 private key belonging to the Keypair. +func (keypair *Keypair) Private() *PrivateKey { + return keypair.private +} + +// Representative returns the Elligator representative of the public key +// belonging to the Keypair. +func (keypair *Keypair) Representative() *Representative { + return keypair.representative +} + +// HasElligator returns true if the Keypair has an Elligator representative. +func (keypair *Keypair) HasElligator() bool { + return nil != keypair.representative +} + +// NewKeypair generates a new Curve25519 keypair, and optionally also generates +// an Elligator representative of the public key. +func NewKeypair(elligator bool) (*Keypair, error) { + keypair := new(Keypair) + keypair.private = new(PrivateKey) + keypair.public = new(PublicKey) + if elligator { + keypair.representative = new(Representative) + } + + for { + // Generate a Curve25519 private key. Like everyone who does this, + // run the CSPRNG output through SHA256 for extra tinfoil hattery. + priv := keypair.private.Bytes()[:] + if err := csrand.Bytes(priv); err != nil { + return nil, err + } + digest := sha256.Sum256(priv) + digest[0] &= 248 + digest[31] &= 127 + digest[31] |= 64 + copy(priv, digest[:]) + + if elligator { + // Apply the Elligator transform. This fails ~50% of the time. + if !extra25519.ScalarBaseMult(keypair.public.Bytes(), + keypair.representative.Bytes(), + keypair.private.Bytes()) { + continue + } + } else { + // Generate the corresponding Curve25519 public key. + curve25519.ScalarBaseMult(keypair.public.Bytes(), + keypair.private.Bytes()) + } + + return keypair, nil + } +} + +// KeypairFromBase64 returns a Keypair from a Base64 representation of the +// private key. +func KeypairFromBase64(encoded string) (*Keypair, error) { + raw, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return nil, err + } + + if len(raw) != PrivateKeyLength { + return nil, PrivateKeyLengthError(len(raw)) + } + + keypair := new(Keypair) + keypair.private = new(PrivateKey) + keypair.public = new(PublicKey) + + copy(keypair.private[:], raw) + curve25519.ScalarBaseMult(keypair.public.Bytes(), + keypair.private.Bytes()) + + return keypair, nil +} + +// ServerHandshake does the server side of a ntor handshake and returns status, +// KEY_SEED, and AUTH. If status is not true, the handshake MUST be aborted. +func ServerHandshake(clientPublic *PublicKey, serverKeypair *Keypair, idKeypair *Keypair, id *NodeID) (ok bool, keySeed *KeySeed, auth *Auth) { + var notOk int + var secretInput bytes.Buffer + + // Server side uses EXP(X,y) | EXP(X,b) + var exp [SharedSecretLength]byte + curve25519.ScalarMult(&exp, serverKeypair.private.Bytes(), + clientPublic.Bytes()) + notOk |= constantTimeIsZero(exp[:]) + secretInput.Write(exp[:]) + + curve25519.ScalarMult(&exp, idKeypair.private.Bytes(), + clientPublic.Bytes()) + notOk |= constantTimeIsZero(exp[:]) + secretInput.Write(exp[:]) + + keySeed, auth = ntorCommon(secretInput, id, idKeypair.public, + clientPublic, serverKeypair.public) + return notOk == 0, keySeed, auth +} + +// ClientHandshake does the client side of a ntor handshake and returnes +// status, KEY_SEED, and AUTH. If status is not true or AUTH does not match +// the value recieved from the server, the handshake MUST be aborted. +func ClientHandshake(clientKeypair *Keypair, serverPublic *PublicKey, idPublic *PublicKey, id *NodeID) (ok bool, keySeed *KeySeed, auth *Auth) { + var notOk int + var secretInput bytes.Buffer + + // Client side uses EXP(Y,x) | EXP(B,x) + var exp [SharedSecretLength]byte + curve25519.ScalarMult(&exp, clientKeypair.private.Bytes(), + serverPublic.Bytes()) + notOk |= constantTimeIsZero(exp[:]) + secretInput.Write(exp[:]) + + curve25519.ScalarMult(&exp, clientKeypair.private.Bytes(), + idPublic.Bytes()) + notOk |= constantTimeIsZero(exp[:]) + secretInput.Write(exp[:]) + + keySeed, auth = ntorCommon(secretInput, id, idPublic, + clientKeypair.public, serverPublic) + return notOk == 0, keySeed, auth +} + +// CompareAuth does a constant time compare of a Auth and a byte slice +// (presumably received over a network). +func CompareAuth(auth1 *Auth, auth2 []byte) bool { + auth1Bytes := auth1.Bytes() + return hmac.Equal(auth1Bytes[:], auth2) +} + +func ntorCommon(secretInput bytes.Buffer, id *NodeID, b *PublicKey, x *PublicKey, y *PublicKey) (*KeySeed, *Auth) { + keySeed := new(KeySeed) + auth := new(Auth) + + // secret_input/auth_input use this common bit, build it once. + suffix := bytes.NewBuffer(b.Bytes()[:]) + suffix.Write(b.Bytes()[:]) + suffix.Write(x.Bytes()[:]) + suffix.Write(y.Bytes()[:]) + suffix.Write(protoID) + suffix.Write(id[:]) + + // At this point secret_input has the 2 exponents, concatenated, append the + // client/server common suffix. + secretInput.Write(suffix.Bytes()) + + // KEY_SEED = H(secret_input, t_key) + h := hmac.New(sha256.New, tKey) + h.Write(secretInput.Bytes()) + tmp := h.Sum(nil) + copy(keySeed[:], tmp) + + // verify = H(secret_input, t_verify) + h = hmac.New(sha256.New, tVerify) + h.Write(secretInput.Bytes()) + verify := h.Sum(nil) + + // auth_input = verify | ID | B | Y | X | PROTOID | "Server" + authInput := bytes.NewBuffer(verify) + authInput.Write(suffix.Bytes()) + authInput.Write([]byte("Server")) + h = hmac.New(sha256.New, tMac) + h.Write(authInput.Bytes()) + tmp = h.Sum(nil) + copy(auth[:], tmp) + + return keySeed, auth +} + +func constantTimeIsZero(x []byte) int { + var ret byte + for _, v := range x { + ret |= v + } + + return subtle.ConstantTimeByteEq(ret, 0) +} + +// Kdf extracts and expands KEY_SEED via HKDF-SHA256 and returns `okm_len` bytes +// of key material. +func Kdf(keySeed []byte, okmLen int) []byte { + kdf := hkdf.New(sha256.New, keySeed, tKey, mExpand) + okm := make([]byte, okmLen) + n, err := io.ReadFull(kdf, okm) + if err != nil { + panic(fmt.Sprintf("BUG: Failed HKDF: %s", err.Error())) + } else if n != len(okm) { + panic(fmt.Sprintf("BUG: Got truncated HKDF output: %d", n)) + } + + return okm +} diff --git a/common/ntor/ntor_test.go b/common/ntor/ntor_test.go new file mode 100644 index 0000000..c92c04e --- /dev/null +++ b/common/ntor/ntor_test.go @@ -0,0 +1,180 @@ +/* + * 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 ntor + +import ( + "bytes" + "testing" +) + +// TestNewKeypair tests Curve25519/Elligator keypair generation. +func TestNewKeypair(t *testing.T) { + // Test standard Curve25519 first. + keypair, err := NewKeypair(false) + if err != nil { + t.Fatal("NewKeypair(false) failed:", err) + } + if keypair == nil { + t.Fatal("NewKeypair(false) returned nil") + } + if keypair.HasElligator() { + t.Fatal("NewKeypair(false) has a Elligator representative") + } + + // Test Elligator generation. + keypair, err = NewKeypair(true) + if err != nil { + t.Fatal("NewKeypair(true) failed:", err) + } + if keypair == nil { + t.Fatal("NewKeypair(true) returned nil") + } + if !keypair.HasElligator() { + t.Fatal("NewKeypair(true) mising an Elligator representative") + } +} + +// Test Client/Server handshake. +func TestHandshake(t *testing.T) { + clientKeypair, err := NewKeypair(true) + if err != nil { + t.Fatal("Failed to generate client keypair:", err) + } + if clientKeypair == nil { + t.Fatal("Client keypair is nil") + } + + serverKeypair, err := NewKeypair(true) + if err != nil { + t.Fatal("Failed to generate server keypair:", err) + } + if serverKeypair == nil { + t.Fatal("Server keypair is nil") + } + + idKeypair, err := NewKeypair(false) + if err != nil { + t.Fatal("Failed to generate identity keypair:", err) + } + if idKeypair == nil { + t.Fatal("Identity keypair is nil") + } + + nodeID, err := NewNodeID([]byte("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13")) + if err != nil { + t.Fatal("Failed to load NodeId:", err) + } + + // ServerHandshake + clientPublic := clientKeypair.Representative().ToPublic() + ok, serverSeed, serverAuth := ServerHandshake(clientPublic, + serverKeypair, idKeypair, nodeID) + if !ok { + t.Fatal("ServerHandshake failed") + } + if serverSeed == nil { + t.Fatal("ServerHandshake returned nil KEY_SEED") + } + if serverAuth == nil { + t.Fatal("ServerHandshake returned nil AUTH") + } + + // ClientHandshake + ok, clientSeed, clientAuth := ClientHandshake(clientKeypair, + serverKeypair.Public(), idKeypair.Public(), nodeID) + if !ok { + t.Fatal("ClientHandshake failed") + } + if clientSeed == nil { + t.Fatal("ClientHandshake returned nil KEY_SEED") + } + if clientAuth == nil { + t.Fatal("ClientHandshake returned nil AUTH") + } + + // WARNING: Use a constant time comparison in actual code. + if 0 != bytes.Compare(clientSeed.Bytes()[:], serverSeed.Bytes()[:]) { + t.Fatal("KEY_SEED mismatched between client/server") + } + if 0 != bytes.Compare(clientAuth.Bytes()[:], serverAuth.Bytes()[:]) { + t.Fatal("AUTH mismatched between client/server") + } +} + +// Benchmark Client/Server handshake. The actual time taken that will be +// observed on either the Client or Server is half the reported time per +// operation since the benchmark does both sides. +func BenchmarkHandshake(b *testing.B) { + // Generate the "long lasting" identity key and NodeId. + idKeypair, err := NewKeypair(false) + if err != nil || idKeypair == nil { + b.Fatal("Failed to generate identity keypair") + } + nodeID, err := NewNodeID([]byte("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13")) + if err != nil { + b.Fatal("Failed to load NodeId:", err) + } + b.ResetTimer() + + // Start the actual benchmark. + for i := 0; i < b.N; i++ { + // Generate the keypairs. + serverKeypair, err := NewKeypair(true) + if err != nil || serverKeypair == nil { + b.Fatal("Failed to generate server keypair") + } + + clientKeypair, err := NewKeypair(true) + if err != nil || clientKeypair == nil { + b.Fatal("Failed to generate client keypair") + } + + // Server handshake. + clientPublic := clientKeypair.Representative().ToPublic() + ok, serverSeed, serverAuth := ServerHandshake(clientPublic, + serverKeypair, idKeypair, nodeID) + if !ok || serverSeed == nil || serverAuth == nil { + b.Fatal("ServerHandshake failed") + } + + // Client handshake. + serverPublic := serverKeypair.Representative().ToPublic() + ok, clientSeed, clientAuth := ClientHandshake(clientKeypair, + serverPublic, idKeypair.Public(), nodeID) + if !ok || clientSeed == nil || clientAuth == nil { + b.Fatal("ClientHandshake failed") + } + + // Validate the authenticator. Real code would pass the AUTH read off + // the network as a slice to CompareAuth here. + if !CompareAuth(clientAuth, serverAuth.Bytes()[:]) || + !CompareAuth(serverAuth, clientAuth.Bytes()[:]) { + b.Fatal("AUTH mismatched between client/server") + } + } +} diff --git a/common/probdist/weighted_dist.go b/common/probdist/weighted_dist.go new file mode 100644 index 0000000..2386bbe --- /dev/null +++ b/common/probdist/weighted_dist.go @@ -0,0 +1,220 @@ +/* + * 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 probdist implements a weighted probability distribution suitable for +// protocol parameterization. To allow for easy reproduction of a given +// distribution, the drbg package is used as the random number source. +package probdist + +import ( + "container/list" + "fmt" + "math/rand" + + "git.torproject.org/pluggable-transports/obfs4.git/common/csrand" + "git.torproject.org/pluggable-transports/obfs4.git/common/drbg" +) + +const ( + minValues = 1 + maxValues = 100 +) + +// WeightedDist is a weighted distribution. +type WeightedDist struct { + minValue int + maxValue int + biased bool + values []int + weights []float64 + + alias []int + prob []float64 +} + +// New creates a weighted distribution of values ranging from min to max +// based on a HashDrbg initialized with seed. Optionally, bias the weight +// generation to match the ScrambleSuit non-uniform distribution from +// obfsproxy. +func New(seed *drbg.Seed, min, max int, biased bool) (w *WeightedDist) { + w = &WeightedDist{minValue: min, maxValue: max, biased: biased} + + if max <= min { + panic(fmt.Sprintf("wDist.Reset(): min >= max (%d, %d)", min, max)) + } + + w.Reset(seed) + + return +} + +// genValues creates a slice containing a random number of random values +// that when scaled by adding minValue will fall into [min, max]. +func (w *WeightedDist) genValues(rng *rand.Rand) { + nValues := (w.maxValue + 1) - w.minValue + values := rng.Perm(nValues) + if nValues < minValues { + nValues = minValues + } + if nValues > maxValues { + nValues = maxValues + } + nValues = rng.Intn(nValues) + 1 + w.values = values[:nValues] +} + +// genBiasedWeights generates a non-uniform weight list, similar to the +// ScrambleSuit prob_dist module. +func (w *WeightedDist) genBiasedWeights(rng *rand.Rand) { + w.weights = make([]float64, len(w.values)) + + culmProb := 0.0 + for i := range w.weights { + p := (1.0 - culmProb) * rng.Float64() + w.weights[i] = p + culmProb += p + } +} + +// genUniformWeights generates a uniform weight list. +func (w *WeightedDist) genUniformWeights(rng *rand.Rand) { + w.weights = make([]float64, len(w.values)) + for i := range w.weights { + w.weights[i] = rng.Float64() + } +} + +// genTables calculates the alias and prob tables used for Vose's Alias method. +// Algorithm taken from http://www.keithschwarz.com/darts-dice-coins/ +func (w *WeightedDist) genTables() { + n := len(w.weights) + var sum float64 + for _, weight := range w.weights { + sum += weight + } + + // Create arrays $Alias$ and $Prob$, each of size $n$. + alias := make([]int, n) + prob := make([]float64, n) + + // Create two worklists, $Small$ and $Large$. + small := list.New() + large := list.New() + + scaled := make([]float64, n) + for i, weight := range w.weights { + // Multiply each probability by $n$. + p_i := weight * float64(n) / sum + scaled[i] = p_i + + // For each scaled probability $p_i$: + if scaled[i] < 1.0 { + // If $p_i < 1$, add $i$ to $Small$. + small.PushBack(i) + } else { + // Otherwise ($p_i \ge 1$), add $i$ to $Large$. + large.PushBack(i) + } + } + + // While $Small$ and $Large$ are not empty: ($Large$ might be emptied first) + for small.Len() > 0 && large.Len() > 0 { + // Remove the first element from $Small$; call it $l$. + l := small.Remove(small.Front()).(int) + // Remove the first element from $Large$; call it $g$. + g := large.Remove(large.Front()).(int) + + // Set $Prob[l] = p_l$. + prob[l] = scaled[l] + // Set $Alias[l] = g$. + alias[l] = g + + // Set $p_g := (p_g + p_l) - 1$. (This is a more numerically stable option.) + scaled[g] = (scaled[g] + scaled[l]) - 1.0 + + if scaled[g] < 1.0 { + // If $p_g < 1$, add $g$ to $Small$. + small.PushBack(g) + } else { + // Otherwise ($p_g \ge 1$), add $g$ to $Large$. + large.PushBack(g) + } + } + + // While $Large$ is not empty: + for large.Len() > 0 { + // Remove the first element from $Large$; call it $g$. + g := large.Remove(large.Front()).(int) + // Set $Prob[g] = 1$. + prob[g] = 1.0 + } + + // While $Small$ is not empty: This is only possible due to numerical instability. + for small.Len() > 0 { + // Remove the first element from $Small$; call it $l$. + l := small.Remove(small.Front()).(int) + // Set $Prob[l] = 1$. + prob[l] = 1.0 + } + + w.prob = prob + w.alias = alias +} + +// Reset generates a new distribution with the same min/max based on a new +// seed. +func (w *WeightedDist) Reset(seed *drbg.Seed) { + // Initialize the deterministic random number generator. + drbg, _ := drbg.NewHashDrbg(seed) + rng := rand.New(drbg) + + w.genValues(rng) + if w.biased { + w.genBiasedWeights(rng) + } else { + w.genUniformWeights(rng) + } + w.genTables() +} + +// Sample generates a random value according to the distribution. +func (w *WeightedDist) Sample() int { + var idx int + + // Generate a fair die roll from an $n$-sided die; call the side $i$. + i := csrand.Intn(len(w.values)) + // Flip a biased coin that comes up heads with probability $Prob[i]$. + if csrand.Float64() <= w.prob[i] { + // If the coin comes up "heads," return $i$. + idx = i + } else { + // Otherwise, return $Alias[i]$. + idx = w.alias[i] + } + + return w.minValue + w.values[idx] +} diff --git a/common/probdist/weighted_dist_test.go b/common/probdist/weighted_dist_test.go new file mode 100644 index 0000000..b705add --- /dev/null +++ b/common/probdist/weighted_dist_test.go @@ -0,0 +1,80 @@ +/* + * 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 probdist + +import ( + "fmt" + "testing" + + "git.torproject.org/pluggable-transports/obfs4.git/common/drbg" +) + +const debug = false + +func TestWeightedDist(t *testing.T) { + seed, err := drbg.NewSeed() + if err != nil { + t.Fatal("failed to generate a DRBG seed:", err) + } + + const nrTrials = 1000000 + + hist := make([]int, 1000) + + w := New(seed, 0, 999, true) + if debug { + // Dump a string representation of the probability table. + fmt.Println("Table:") + var sum float64 + for _, weight := range w.weights { + sum += weight + } + for i, weight := range w.weights { + p := weight / sum + if p > 0.000001 { // Filter out tiny values. + fmt.Printf(" [%d]: %f\n", w.minValue+w.values[i], p) + } + } + fmt.Println() + } + + for i := 0; i < nrTrials; i++ { + value := w.Sample() + hist[value]++ + } + + if debug { + fmt.Println("Generated:") + for value, count := range hist { + if count != 0 { + p := float64(count) / float64(nrTrials) + fmt.Printf(" [%d]: %f (%d)\n", value, p, count) + } + } + } +} diff --git a/common/replayfilter/replay_filter.go b/common/replayfilter/replay_filter.go new file mode 100644 index 0000000..95cc5d6 --- /dev/null +++ b/common/replayfilter/replay_filter.go @@ -0,0 +1,147 @@ +/* + * 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 replayfilter implements a generic replay detection filter with a +// caller specifiable time-to-live. It only detects if a given byte sequence +// has been seen before based on the SipHash-2-4 digest of the sequence. +// Collisions are treated as positive matches, though the probability of this +// happening is negligible. +package replayfilter + +import ( + "container/list" + "encoding/binary" + "sync" + "time" + + "github.com/dchest/siphash" + + "git.torproject.org/pluggable-transports/obfs4.git/common/csrand" +) + +// maxFilterSize is the maximum capacity of a replay filter. This value is +// more as a safeguard to prevent runaway filter growth, and is sized to be +// serveral orders of magnitude greater than the number of connections a busy +// bridge sees in one day, so in practice should never be reached. +const maxFilterSize = 100 * 1024 + +type entry struct { + digest uint64 + firstSeen time.Time + element *list.Element +} + +// ReplayFilter is a simple filter designed only to detect if a given byte +// sequence has been seen before. +type ReplayFilter struct { + sync.Mutex + + filter map[uint64]*entry + fifo *list.List + + key [2]uint64 + ttl time.Duration +} + +// New creates a new ReplayFilter instance. +func New(ttl time.Duration) (filter *ReplayFilter, err error) { + // Initialize the SipHash-2-4 instance with a random key. + var key [16]byte + if err = csrand.Bytes(key[:]); err != nil { + return + } + + filter = new(ReplayFilter) + filter.filter = make(map[uint64]*entry) + filter.fifo = list.New() + filter.key[0] = binary.BigEndian.Uint64(key[0:8]) + filter.key[1] = binary.BigEndian.Uint64(key[8:16]) + filter.ttl = ttl + + return +} + +// TestAndSet queries the filter for a given byte sequence, inserts the +// sequence, and returns if it was present before the insertion operation. +func (f *ReplayFilter) TestAndSet(now time.Time, buf []byte) bool { + digest := siphash.Hash(f.key[0], f.key[1], buf) + + f.Lock() + defer f.Unlock() + + f.compactFilter(now) + + if e := f.filter[digest]; e != nil { + // Hit. Just return. + return true + } + + // Miss. Add a new entry. + e := new(entry) + e.digest = digest + e.firstSeen = now + e.element = f.fifo.PushBack(e) + f.filter[digest] = e + + return false +} + +func (f *ReplayFilter) compactFilter(now time.Time) { + e := f.fifo.Front() + for e != nil { + ent, _ := e.Value.(*entry) + + // If the filter is not full, only purge entries that exceed the TTL, + // otherwise purge at least one entry, then revert to TTL based + // compaction. + if f.fifo.Len() < maxFilterSize && f.ttl > 0 { + deltaT := now.Sub(ent.firstSeen) + if deltaT < 0 { + // Aeeeeeee, the system time jumped backwards, potentially by + // a lot. This will eventually self-correct, but "eventually" + // could be a long time. As much as this sucks, jettison the + // entire filter. + f.reset() + return + } else if deltaT < f.ttl { + return + } + } + + // Remove the eldest entry. + eNext := e.Next() + delete(f.filter, ent.digest) + f.fifo.Remove(ent.element) + ent.element = nil + e = eNext + } +} + +func (f *ReplayFilter) reset() { + f.filter = make(map[uint64]*entry) + f.fifo = list.New() +} diff --git a/common/replayfilter/replay_filter_test.go b/common/replayfilter/replay_filter_test.go new file mode 100644 index 0000000..884e4fb --- /dev/null +++ b/common/replayfilter/replay_filter_test.go @@ -0,0 +1,95 @@ +/* + * 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 replayfilter + +import ( + "testing" + "time" +) + +func TestReplayFilter(t *testing.T) { + ttl := 10 * time.Second + + f, err := New(ttl) + if err != nil { + t.Fatal("newReplayFilter failed:", err) + } + + buf := []byte("This is a test of the Emergency Broadcast System.") + now := time.Now() + + // testAndSet into empty filter, returns false (not present). + set := f.TestAndSet(now, buf) + if set { + t.Fatal("TestAndSet empty filter returned true") + } + + // testAndSet into filter containing entry, should return true(present). + set = f.TestAndSet(now, buf) + if !set { + t.Fatal("testAndSet populated filter (replayed) returned false") + } + + buf2 := []byte("This concludes this test of the Emergency Broadcast System.") + now = now.Add(ttl) + + // testAndSet with time advanced. + set = f.TestAndSet(now, buf2) + if set { + t.Fatal("testAndSet populated filter, 2nd entry returned true") + } + set = f.TestAndSet(now, buf2) + if !set { + t.Fatal("testAndSet populated filter, 2nd entry (replayed) returned false") + } + + // Ensure that the first entry has been removed by compact. + set = f.TestAndSet(now, buf) + if set { + t.Fatal("testAndSet populated filter, compact check returned true") + } + + // Ensure that the filter gets reaped if the clock jumps backwards. + now = time.Time{} + set = f.TestAndSet(now, buf) + if set { + t.Fatal("testAndSet populated filter, backward time jump returned true") + } + if len(f.filter) != 1 { + t.Fatal("filter map has a unexpected number of entries:", len(f.filter)) + } + if f.fifo.Len() != 1 { + t.Fatal("filter fifo has a unexpected number of entries:", f.fifo.Len()) + } + + // Ensure that the entry is properly added after reaping. + set = f.TestAndSet(now, buf) + if !set { + t.Fatal("testAndSet populated filter, post-backward clock jump (replayed) returned false") + } +} diff --git a/common/uniformdh/uniformdh.go b/common/uniformdh/uniformdh.go new file mode 100644 index 0000000..ab94a2e --- /dev/null +++ b/common/uniformdh/uniformdh.go @@ -0,0 +1,183 @@ +/* + * 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 uniformdh implements the Tor Project's UniformDH key exchange +// mechanism as defined in the obfs3 protocol specification. This +// implementation is suitable for obfuscation but MUST NOT BE USED when strong +// security is required as it is not constant time. +package uniformdh + +import ( + "fmt" + "io" + "math/big" +) + +const ( + // Size is the size of a UniformDH key or shared secret in bytes. + Size = 1536 / 8 + + // modpStr is the RFC3526 1536-bit MODP Group (Group 5). + modpStr = "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" + + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" + + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" + + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" + + "670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF" + + g = 2 +) + +var modpGroup *big.Int +var gen *big.Int + +// A PrivateKey represents a UniformDH private key. +type PrivateKey struct { + PublicKey + privateKey *big.Int +} + +// A PublicKey represents a UniformDH public key. +type PublicKey struct { + bytes []byte + publicKey *big.Int +} + +// Bytes returns the byte representation of a PublicKey. +func (pub *PublicKey) Bytes() (pubBytes []byte, err error) { + if len(pub.bytes) != Size || pub.bytes == nil { + return nil, fmt.Errorf("public key is not initialized") + } + pubBytes = make([]byte, Size) + copy(pubBytes, pub.bytes) + + return +} + +// SetBytes sets the PublicKey from a byte slice. +func (pub *PublicKey) SetBytes(pubBytes []byte) error { + if len(pubBytes) != Size { + return fmt.Errorf("public key length %d is not %d", len(pubBytes), Size) + } + pub.bytes = make([]byte, Size) + copy(pub.bytes, pubBytes) + pub.publicKey = new(big.Int).SetBytes(pub.bytes) + + return nil +} + +// GenerateKey generates a UniformDH keypair using the random source random. +func GenerateKey(random io.Reader) (priv *PrivateKey, err error) { + privBytes := make([]byte, Size) + if _, err = io.ReadFull(random, privBytes); err != nil { + return + } + priv, err = generateKey(privBytes) + + return +} + +func generateKey(privBytes []byte) (priv *PrivateKey, err error) { + // This function does all of the actual heavy lifting of creating a public + // key from a raw 192 byte private key. It is split so that the KAT tests + // can be written easily, and not exposed since non-ephemeral keys are a + // terrible idea. + + if len(privBytes) != Size { + return nil, fmt.Errorf("invalid private key size %d", len(privBytes)) + } + + // To pick a private UniformDH key, we pick a random 1536-bit number, + // and make it even by setting its low bit to 0 + privBn := new(big.Int).SetBytes(privBytes) + wasEven := privBn.Bit(0) == 0 + privBn = privBn.SetBit(privBn, 0, 0) + + // Let x be that private key, and X = g^x (mod p). + pubBn := new(big.Int).Exp(gen, privBn, modpGroup) + pubAlt := new(big.Int).Sub(modpGroup, pubBn) + + // When someone sends her public key to the other party, she randomly + // decides whether to send X or p-X. Use the lowest most bit of the + // private key here as the random coin flip since it is masked out and not + // used. + // + // Note: The spec doesn't explicitly specify it, but here we prepend zeros + // to the key so that it is always exactly Size bytes. + pubBytes := make([]byte, Size) + if wasEven { + err = prependZeroBytes(pubBytes, pubBn.Bytes()) + } else { + err = prependZeroBytes(pubBytes, pubAlt.Bytes()) + } + if err != nil { + return + } + + priv = new(PrivateKey) + priv.PublicKey.bytes = pubBytes + priv.PublicKey.publicKey = pubBn + priv.privateKey = privBn + + return +} + +// Handshake generates a shared secret given a PrivateKey and PublicKey. +func Handshake(privateKey *PrivateKey, publicKey *PublicKey) (sharedSecret []byte, err error) { + // When a party wants to calculate the shared secret, she raises the + // foreign public key to her private key. + secretBn := new(big.Int).Exp(publicKey.publicKey, privateKey.privateKey, modpGroup) + sharedSecret = make([]byte, Size) + err = prependZeroBytes(sharedSecret, secretBn.Bytes()) + + return +} + +func prependZeroBytes(dst, src []byte) error { + zeros := len(dst) - len(src) + if zeros < 0 { + return fmt.Errorf("src length is greater than destination: %d", zeros) + } + for i := 0; i < zeros; i++ { + dst[i] = 0 + } + copy(dst[zeros:], src) + + return nil +} + +func init() { + // Load the MODP group and the generator. + var ok bool + modpGroup, ok = new(big.Int).SetString(modpStr, 16) + if !ok { + panic("Failed to load the RFC3526 MODP Group") + } + gen = big.NewInt(g) +} diff --git a/common/uniformdh/uniformdh_test.go b/common/uniformdh/uniformdh_test.go new file mode 100644 index 0000000..d705d66 --- /dev/null +++ b/common/uniformdh/uniformdh_test.go @@ -0,0 +1,220 @@ +/* + * 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 uniformdh + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "testing" +) + +const ( + xPrivStr = "6f592d676f536874746f20686e6b776f" + + "20736874206561676574202e6f592d67" + + "6f536874746f20687369742065686720" + + "74612e655920676f532d746f6f686874" + + "6920207368742065656b20796e612064" + + "7567726169646e616f20206668742065" + + "61676574202e61507473202c72707365" + + "6e652c746620747572752c6561206c6c" + + "612065726f20656e6920206e6f592d67" + + "6f536874746f2e68482020656e6b776f" + + "2073687772652065687420656c4f2064" + + "6e4f736562206f72656b74207268756f" + + xPubStr = "76a3d17d5c55b03e865fa3e8267990a7" + + "24baa24b0bdd0cc4af93be8de30be120" + + "d5533c91bf63ef923b02edcb84b74438" + + "3f7de232cca6eb46d07cad83dcaa317f" + + "becbc68ca13e2c4019e6a36531067450" + + "04aecc0be1dff0a78733fb0e7d5cb7c4" + + "97cab77b1331bf347e5f3a7847aa0bc0" + + "f4bc64146b48407fed7b931d16972d25" + + "fb4da5e6dc074ce2a58daa8de7624247" + + "cdf2ebe4e4dfec6d5989aac778c87559" + + "d3213d6040d4111ce3a2acae19f9ee15" + + "32509e037f69b252fdc30243cbbce9d0" + + yPrivStr = "736562206f72656b74207268756f6867" + + "6f2020666c6f2c646120646e77206568" + + "657254206568207968736c61206c7262" + + "6165206b68746f726775206867616961" + + "2e6e482020656e6b776f207368777265" + + "2065685479656820766120657274646f" + + "652072616874732766206569646c2c73" + + "6120646e772065686572542065682079" + + "74736c69206c72746165206468746d65" + + "202c6e612064687720796f6e6f20656e" + + "63206e61622068656c6f206468546d65" + + "61202073685479657420657264610a2e" + + yPubStr = "d04e156e554c37ffd7aba749df662350" + + "1e4ff4466cb12be055617c1a36872237" + + "36d2c3fdce9ee0f9b27774350849112a" + + "a5aeb1f126811c9c2f3a9cb13d2f0c3a" + + "7e6fa2d3bf71baf50d839171534f227e" + + "fbb2ce4227a38c25abdc5ba7fc430111" + + "3a2cb2069c9b305faac4b72bf21fec71" + + "578a9c369bcac84e1a7dcf0754e342f5" + + "bc8fe4917441b88254435e2abaf297e9" + + "3e1e57968672d45bd7d4c8ba1bc3d314" + + "889b5bc3d3e4ea33d4f2dfdd34e5e5a7" + + "2ff24ee46316d4757dad09366a0b66b3" + + ssStr = "78afaf5f457f1fdb832bebc397644a33" + + "038be9dba10ca2ce4a076f327f3a0ce3" + + "151d477b869ee7ac467755292ad8a77d" + + "b9bd87ffbbc39955bcfb03b1583888c8" + + "fd037834ff3f401d463c10f899aa6378" + + "445140b7f8386a7d509e7b9db19b677f" + + "062a7a1a4e1509604d7a0839ccd5da61" + + "73e10afd9eab6dda74539d60493ca37f" + + "a5c98cd9640b409cd8bb3be2bc5136fd" + + "42e764fc3f3c0ddb8db3d87abcf2e659" + + "8d2b101bef7a56f50ebc658f9df1287d" + + "a81359543e77e4a4cfa7598a4152e4c0" +) + +var xPriv, xPub, yPriv, yPub, ss []byte + +// TestGenerateKeyOdd tests creating a UniformDH keypair with a odd private +// key. +func TestGenerateKeyOdd(t *testing.T) { + xX, err := generateKey(xPriv) + if err != nil { + t.Fatal("generateKey(xPriv) failed:", err) + } + + xPubGen, err := xX.PublicKey.Bytes() + if err != nil { + t.Fatal("xX.PublicKey.Bytes() failed:", err) + } + if 0 != bytes.Compare(xPubGen, xPub) { + t.Fatal("Generated public key does not match known answer") + } +} + +// TestGenerateKeyEven tests creating a UniformDH keypair with a even private +// key. +func TestGenerateKeyEven(t *testing.T) { + yY, err := generateKey(yPriv) + if err != nil { + t.Fatal("generateKey(yPriv) failed:", err) + } + + yPubGen, err := yY.PublicKey.Bytes() + if err != nil { + t.Fatal("yY.PublicKey.Bytes() failed:", err) + } + if 0 != bytes.Compare(yPubGen, yPub) { + t.Fatal("Generated public key does not match known answer") + } +} + +// TestHandshake tests conductiong a UniformDH handshake with know values. +func TestHandshake(t *testing.T) { + xX, err := generateKey(xPriv) + if err != nil { + t.Fatal("generateKey(xPriv) failed:", err) + } + yY, err := generateKey(yPriv) + if err != nil { + t.Fatal("generateKey(yPriv) failed:", err) + } + + xY, err := Handshake(xX, &yY.PublicKey) + if err != nil { + t.Fatal("Handshake(xX, yY.PublicKey) failed:", err) + } + yX, err := Handshake(yY, &xX.PublicKey) + if err != nil { + t.Fatal("Handshake(yY, xX.PublicKey) failed:", err) + } + + if 0 != bytes.Compare(xY, yX) { + t.Fatal("Generated shared secrets do not match between peers") + } + if 0 != bytes.Compare(xY, ss) { + t.Fatal("Generated shared secret does not match known value") + } +} + +// Benchmark UniformDH key generation + exchange. THe actual time taken per +// peer is half of the reported time as this does 2 key generation an +// handshake operations. +func BenchmarkHandshake(b *testing.B) { + for i := 0; i < b.N; i++ { + xX, err := GenerateKey(rand.Reader) + if err != nil { + b.Fatal("Failed to generate xX keypair", err) + } + + yY, err := GenerateKey(rand.Reader) + if err != nil { + b.Fatal("Failed to generate yY keypair", err) + } + + xY, err := Handshake(xX, &yY.PublicKey) + if err != nil { + b.Fatal("Handshake(xX, yY.PublicKey) failed:", err) + } + yX, err := Handshake(yY, &xX.PublicKey) + if err != nil { + b.Fatal("Handshake(yY, xX.PublicKey) failed:", err) + } + + _ = xY + _ = yX + } +} + +func init() { + // Load the test vectors into byte slices. + var err error + xPriv, err = hex.DecodeString(xPrivStr) + if err != nil { + panic("hex.DecodeString(xPrivStr) failed") + } + xPub, err = hex.DecodeString(xPubStr) + if err != nil { + panic("hex.DecodeString(xPubStr) failed") + } + yPriv, err = hex.DecodeString(yPrivStr) + if err != nil { + panic("hex.DecodeString(yPrivStr) failed") + } + yPub, err = hex.DecodeString(yPubStr) + if err != nil { + panic("hex.DecodeString(yPubStr) failed") + } + ss, err = hex.DecodeString(ssStr) + if err != nil { + panic("hex.DecodeString(ssStr) failed") + } +} diff --git a/csrand/csrand.go b/csrand/csrand.go deleted file mode 100644 index b059ed0..0000000 --- a/csrand/csrand.go +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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 csrand implements the math/rand interface over crypto/rand, along -// with some utility functions for common random number/byte related tasks. -// -// Not all of the convinience routines are replicated, only those that are -// useful for obfs4. The CsRand variable provides access to the full math/rand -// API. -package csrand - -import ( - cryptRand "crypto/rand" - "encoding/binary" - "fmt" - "io" - "math/rand" -) - -var ( - csRandSourceInstance csRandSource - - // CsRand is a math/rand instance backed by crypto/rand CSPRNG. - CsRand = rand.New(csRandSourceInstance) -) - -type csRandSource struct { - // This does not keep any state as it is backed by crypto/rand. -} - -func (r csRandSource) Int63() int64 { - var src [8]byte - err := Bytes(src[:]) - if err != nil { - panic(err) - } - val := binary.BigEndian.Uint64(src[:]) - val &= (1<<63 - 1) - - return int64(val) -} - -func (r csRandSource) Seed(seed int64) { - // No-op. -} - -// Intn returns, as a int, a pseudo random number in [0, n). -func Intn(n int) int { - return CsRand.Intn(n) -} - -// Float64 returns, as a float64, a pesudo random number in [0.0,1.0). -func Float64() float64 { - return CsRand.Float64() -} - -// IntRange returns a uniformly distributed int [min, max]. -func IntRange(min, max int) int { - if max < min { - panic(fmt.Sprintf("IntRange: min > max (%d, %d)", min, max)) - } - - r := (max + 1) - min - ret := CsRand.Intn(r) - return ret + min -} - -// Bytes fills the slice with random data. -func Bytes(buf []byte) error { - _, err := io.ReadFull(cryptRand.Reader, buf) - if err != nil { - return err - } - - return nil -} - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/drbg/hash_drbg.go b/drbg/hash_drbg.go deleted file mode 100644 index c94902a..0000000 --- a/drbg/hash_drbg.go +++ /dev/null @@ -1,147 +0,0 @@ -/* - * 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 drbg implements a minimalistic DRBG based off SipHash-2-4 in OFB -// mode. -package drbg - -import ( - "encoding/base64" - "encoding/binary" - "fmt" - "hash" - - "github.com/dchest/siphash" - - "git.torproject.org/pluggable-transports/obfs4.git/csrand" -) - -// Size is the length of the HashDrbg output. -const Size = siphash.Size - -// SeedLength is the length of the HashDrbg seed. -const SeedLength = 16 + Size - -// Seed is the initial state for a HashDrbg. It consists of a SipHash-2-4 -// key, and 8 bytes of initial data. -type Seed [SeedLength]byte - -// Bytes returns a pointer to the raw HashDrbg seed. -func (seed *Seed) Bytes() *[SeedLength]byte { - return (*[SeedLength]byte)(seed) -} - -// Base64 returns the Base64 representation of the seed. -func (seed *Seed) Base64() string { - return base64.StdEncoding.EncodeToString(seed.Bytes()[:]) -} - -// NewSeed returns a Seed initialized with the runtime CSPRNG. -func NewSeed() (seed *Seed, err error) { - seed = new(Seed) - err = csrand.Bytes(seed.Bytes()[:]) - if err != nil { - return nil, err - } - - return -} - -// SeedFromBytes creates a Seed from the raw bytes, truncating to SeedLength as -// appropriate. -func SeedFromBytes(src []byte) (seed *Seed, err error) { - if len(src) < SeedLength { - return nil, InvalidSeedLengthError(len(src)) - } - - seed = new(Seed) - copy(seed.Bytes()[:], src) - - return -} - -// SeedFromBase64 creates a Seed from the Base64 representation, truncating to -// SeedLength as appropriate. -func SeedFromBase64(encoded string) (seed *Seed, err error) { - var raw []byte - raw, err = base64.StdEncoding.DecodeString(encoded) - if err != nil { - return nil, err - } - - return SeedFromBytes(raw) -} - -// InvalidSeedLengthError is the error returned when the seed provided to the -// DRBG is an invalid length. -type InvalidSeedLengthError int - -func (e InvalidSeedLengthError) Error() string { - return fmt.Sprintf("invalid seed length: %d", int(e)) -} - -// HashDrbg is a CSDRBG based off of SipHash-2-4 in OFB mode. -type HashDrbg struct { - sip hash.Hash64 - ofb [Size]byte -} - -// NewHashDrbg makes a HashDrbg instance based off an optional seed. The seed -// is truncated to SeedLength. -func NewHashDrbg(seed *Seed) *HashDrbg { - drbg := new(HashDrbg) - drbg.sip = siphash.New(seed.Bytes()[:16]) - copy(drbg.ofb[:], seed.Bytes()[16:]) - - return drbg -} - -// Int63 returns a uniformly distributed random integer [0, 1 << 63). -func (drbg *HashDrbg) Int63() int64 { - block := drbg.NextBlock() - ret := binary.BigEndian.Uint64(block) - ret &= (1<<63 - 1) - - return int64(ret) -} - -// Seed does nothing, call NewHashDrbg if you want to reseed. -func (drbg *HashDrbg) Seed(seed int64) { - // No-op. -} - -// NextBlock returns the next 8 byte DRBG block. -func (drbg *HashDrbg) NextBlock() []byte { - drbg.sip.Write(drbg.ofb[:]) - copy(drbg.ofb[:], drbg.sip.Sum(nil)) - - ret := make([]byte, Size) - copy(ret, drbg.ofb[:]) - return ret -} - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/framing/framing.go b/framing/framing.go deleted file mode 100644 index 725a762..0000000 --- a/framing/framing.go +++ /dev/null @@ -1,310 +0,0 @@ -/* - * 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/csrand" - "git.torproject.org/pluggable-transports/obfs4.git/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 -} - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/framing/framing_test.go b/framing/framing_test.go deleted file mode 100644 index 7df0e28..0000000 --- a/framing/framing_test.go +++ /dev/null @@ -1,171 +0,0 @@ -/* - * 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)) - } - } -} - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/handshake_ntor.go b/handshake_ntor.go deleted file mode 100644 index 8192494..0000000 --- a/handshake_ntor.go +++ /dev/null @@ -1,427 +0,0 @@ -/* - * 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/csrand" - "git.torproject.org/pluggable-transports/obfs4.git/framing" - "git.torproject.org/pluggable-transports/obfs4.git/ntor" -) - -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, 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().Unix(), 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 -} - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/handshake_ntor_test.go b/handshake_ntor_test.go deleted file mode 100644 index 94d2a7f..0000000 --- a/handshake_ntor_test.go +++ /dev/null @@ -1,221 +0,0 @@ -/* - * 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/ntor" -) - -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, _ := newReplayFilter() - - // 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/ntor/ntor.go b/ntor/ntor.go deleted file mode 100644 index b178454..0000000 --- a/ntor/ntor.go +++ /dev/null @@ -1,437 +0,0 @@ -/* - * 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 ntor implements the Tor Project's ntor handshake as defined in -// proposal 216 "Improved circuit-creation key exchange". It also supports -// using Elligator to transform the Curve25519 public keys sent over the wire -// to a form that is indistinguishable from random strings. -// -// Before using this package, it is strongly recommended that the specification -// is read and understood. -// -package ntor - -import ( - "bytes" - "crypto/hmac" - "crypto/sha256" - "crypto/subtle" - "encoding/base64" - "fmt" - "io" - - "code.google.com/p/go.crypto/curve25519" - "code.google.com/p/go.crypto/hkdf" - - "github.com/agl/ed25519/extra25519" - - "git.torproject.org/pluggable-transports/obfs4.git/csrand" -) - -const ( - // PublicKeyLength is the length of a Curve25519 public key. - PublicKeyLength = 32 - - // RepresentativeLength is the length of an Elligator representative. - RepresentativeLength = 32 - - // PrivateKeyLength is the length of a Curve25519 private key. - PrivateKeyLength = 32 - - // SharedSecretLength is the length of a Curve25519 shared secret. - SharedSecretLength = 32 - - // NodeIDLength is the length of a ntor node identifier. - NodeIDLength = 20 - - // KeySeedLength is the length of the derived KEY_SEED. - KeySeedLength = sha256.Size - - // AuthLength is the lenght of the derived AUTH. - AuthLength = sha256.Size -) - -var protoID = []byte("ntor-curve25519-sha256-1") -var tMac = append(protoID, []byte(":mac")...) -var tKey = append(protoID, []byte(":key_extract")...) -var tVerify = append(protoID, []byte(":key_verify")...) -var mExpand = append(protoID, []byte(":key_expand")...) - -// PublicKeyLengthError is the error returned when the public key being -// imported is an invalid length. -type PublicKeyLengthError int - -func (e PublicKeyLengthError) Error() string { - return fmt.Sprintf("ntor: Invalid Curve25519 public key length: %d", - int(e)) -} - -// PrivateKeyLengthError is the error returned when the private key being -// imported is an invalid length. -type PrivateKeyLengthError int - -func (e PrivateKeyLengthError) Error() string { - return fmt.Sprintf("ntor: Invalid Curve25519 private key length: %d", - int(e)) -} - -// NodeIDLengthError is the error returned when the node ID being imported is -// an invalid length. -type NodeIDLengthError int - -func (e NodeIDLengthError) Error() string { - return fmt.Sprintf("ntor: Invalid NodeID length: %d", int(e)) -} - -// KeySeed is the key material that results from a handshake (KEY_SEED). -type KeySeed [KeySeedLength]byte - -// Bytes returns a pointer to the raw key material. -func (key_seed *KeySeed) Bytes() *[KeySeedLength]byte { - return (*[KeySeedLength]byte)(key_seed) -} - -// Auth is the verifier that results from a handshake (AUTH). -type Auth [AuthLength]byte - -// Bytes returns a pointer to the raw auth. -func (auth *Auth) Bytes() *[AuthLength]byte { - return (*[AuthLength]byte)(auth) -} - -// NodeID is a ntor node identifier. -type NodeID [NodeIDLength]byte - -// NewNodeID creates a NodeID from the raw bytes. -func NewNodeID(raw []byte) (*NodeID, error) { - if len(raw) != NodeIDLength { - return nil, NodeIDLengthError(len(raw)) - } - - nodeID := new(NodeID) - copy(nodeID[:], raw) - - return nodeID, nil -} - -// NodeIDFromBase64 creates a new NodeID from the Base64 encoded representation. -func NodeIDFromBase64(encoded string) (*NodeID, error) { - raw, err := base64.StdEncoding.DecodeString(encoded) - if err != nil { - return nil, err - } - - return NewNodeID(raw) -} -// Bytes returns a pointer to the raw NodeID. -func (id *NodeID) Bytes() *[NodeIDLength]byte { - return (*[NodeIDLength]byte)(id) -} - -// Base64 returns the Base64 representation of the NodeID. -func (id *NodeID) Base64() string { - return base64.StdEncoding.EncodeToString(id[:]) -} - -// PublicKey is a Curve25519 public key in little-endian byte order. -type PublicKey [PublicKeyLength]byte - -// Bytes returns a pointer to the raw Curve25519 public key. -func (public *PublicKey) Bytes() *[PublicKeyLength]byte { - return (*[PublicKeyLength]byte)(public) -} - -// Base64 returns the Base64 representation of the Curve25519 public key. -func (public *PublicKey) Base64() string { - return base64.StdEncoding.EncodeToString(public.Bytes()[:]) -} - -// NewPublicKey creates a PublicKey from the raw bytes. -func NewPublicKey(raw []byte) (*PublicKey, error) { - if len(raw) != PublicKeyLength { - return nil, PublicKeyLengthError(len(raw)) - } - - pubKey := new(PublicKey) - copy(pubKey[:], raw) - - return pubKey, nil -} - -// PublicKeyFromBase64 returns a PublicKey from a Base64 representation. -func PublicKeyFromBase64(encoded string) (*PublicKey, error) { - raw, err := base64.StdEncoding.DecodeString(encoded) - if err != nil { - return nil, err - } - - return NewPublicKey(raw) -} - -// Representative is an Elligator representative of a Curve25519 public key -// in little-endian byte order. -type Representative [RepresentativeLength]byte - -// Bytes returns a pointer to the raw Elligator representative. -func (repr *Representative) Bytes() *[RepresentativeLength]byte { - return (*[RepresentativeLength]byte)(repr) -} - -// ToPublic converts a Elligator representative to a Curve25519 public key. -func (repr *Representative) ToPublic() *PublicKey { - pub := new(PublicKey) - - extra25519.RepresentativeToPublicKey(pub.Bytes(), repr.Bytes()) - return pub -} - -// PrivateKey is a Curve25519 private key in little-endian byte order. -type PrivateKey [PrivateKeyLength]byte - -// Bytes returns a pointer to the raw Curve25519 private key. -func (private *PrivateKey) Bytes() *[PrivateKeyLength]byte { - return (*[PrivateKeyLength]byte)(private) -} - -// Base64 returns the Base64 representation of the Curve25519 private key. -func (private *PrivateKey) Base64() string { - return base64.StdEncoding.EncodeToString(private.Bytes()[:]) -} - -// Keypair is a Curve25519 keypair with an optional Elligator representative. -// As only certain Curve25519 keys can be obfuscated with Elligator, the -// representative must be generated along with the keypair. -type Keypair struct { - public *PublicKey - private *PrivateKey - representative *Representative -} - -// Public returns the Curve25519 public key belonging to the Keypair. -func (keypair *Keypair) Public() *PublicKey { - return keypair.public -} - -// Private returns the Curve25519 private key belonging to the Keypair. -func (keypair *Keypair) Private() *PrivateKey { - return keypair.private -} - -// Representative returns the Elligator representative of the public key -// belonging to the Keypair. -func (keypair *Keypair) Representative() *Representative { - return keypair.representative -} - -// HasElligator returns true if the Keypair has an Elligator representative. -func (keypair *Keypair) HasElligator() bool { - return nil != keypair.representative -} - -// NewKeypair generates a new Curve25519 keypair, and optionally also generates -// an Elligator representative of the public key. -func NewKeypair(elligator bool) (*Keypair, error) { - keypair := new(Keypair) - keypair.private = new(PrivateKey) - keypair.public = new(PublicKey) - if elligator { - keypair.representative = new(Representative) - } - - for { - // Generate a Curve25519 private key. Like everyone who does this, - // run the CSPRNG output through SHA256 for extra tinfoil hattery. - priv := keypair.private.Bytes()[:] - err := csrand.Bytes(priv) - if err != nil { - return nil, err - } - digest := sha256.Sum256(priv) - digest[0] &= 248 - digest[31] &= 127 - digest[31] |= 64 - copy(priv, digest[:]) - - if elligator { - // Apply the Elligator transform. This fails ~50% of the time. - if !extra25519.ScalarBaseMult(keypair.public.Bytes(), - keypair.representative.Bytes(), - keypair.private.Bytes()) { - continue - } - } else { - // Generate the corresponding Curve25519 public key. - curve25519.ScalarBaseMult(keypair.public.Bytes(), - keypair.private.Bytes()) - } - - return keypair, nil - } -} - -// KeypairFromBase64 returns a Keypair from a Base64 representation of the -// private key. -func KeypairFromBase64(encoded string) (*Keypair, error) { - raw, err := base64.StdEncoding.DecodeString(encoded) - if err != nil { - return nil, err - } - - if len(raw) != PrivateKeyLength { - return nil, PrivateKeyLengthError(len(raw)) - } - - keypair := new(Keypair) - keypair.private = new(PrivateKey) - keypair.public = new(PublicKey) - - copy(keypair.private[:], raw) - curve25519.ScalarBaseMult(keypair.public.Bytes(), - keypair.private.Bytes()) - - return keypair, nil -} - -// ServerHandshake does the server side of a ntor handshake and returns status, -// KEY_SEED, and AUTH. If status is not true, the handshake MUST be aborted. -func ServerHandshake(clientPublic *PublicKey, serverKeypair *Keypair, idKeypair *Keypair, id *NodeID) (ok bool, keySeed *KeySeed, auth *Auth) { - var notOk int - var secretInput bytes.Buffer - - // Server side uses EXP(X,y) | EXP(X,b) - var exp [SharedSecretLength]byte - curve25519.ScalarMult(&exp, serverKeypair.private.Bytes(), - clientPublic.Bytes()) - notOk |= constantTimeIsZero(exp[:]) - secretInput.Write(exp[:]) - - curve25519.ScalarMult(&exp, idKeypair.private.Bytes(), - clientPublic.Bytes()) - notOk |= constantTimeIsZero(exp[:]) - secretInput.Write(exp[:]) - - keySeed, auth = ntorCommon(secretInput, id, idKeypair.public, - clientPublic, serverKeypair.public) - return notOk == 0, keySeed, auth -} - -// ClientHandshake does the client side of a ntor handshake and returnes -// status, KEY_SEED, and AUTH. If status is not true or AUTH does not match -// the value recieved from the server, the handshake MUST be aborted. -func ClientHandshake(clientKeypair *Keypair, serverPublic *PublicKey, idPublic *PublicKey, id *NodeID) (ok bool, keySeed *KeySeed, auth *Auth) { - var notOk int - var secretInput bytes.Buffer - - // Client side uses EXP(Y,x) | EXP(B,x) - var exp [SharedSecretLength]byte - curve25519.ScalarMult(&exp, clientKeypair.private.Bytes(), - serverPublic.Bytes()) - notOk |= constantTimeIsZero(exp[:]) - secretInput.Write(exp[:]) - - curve25519.ScalarMult(&exp, clientKeypair.private.Bytes(), - idPublic.Bytes()) - notOk |= constantTimeIsZero(exp[:]) - secretInput.Write(exp[:]) - - keySeed, auth = ntorCommon(secretInput, id, idPublic, - clientKeypair.public, serverPublic) - return notOk == 0, keySeed, auth -} - -// CompareAuth does a constant time compare of a Auth and a byte slice -// (presumably received over a network). -func CompareAuth(auth1 *Auth, auth2 []byte) bool { - auth1Bytes := auth1.Bytes() - return hmac.Equal(auth1Bytes[:], auth2) -} - -func ntorCommon(secretInput bytes.Buffer, id *NodeID, b *PublicKey, x *PublicKey, y *PublicKey) (*KeySeed, *Auth) { - keySeed := new(KeySeed) - auth := new(Auth) - - // secret_input/auth_input use this common bit, build it once. - suffix := bytes.NewBuffer(b.Bytes()[:]) - suffix.Write(b.Bytes()[:]) - suffix.Write(x.Bytes()[:]) - suffix.Write(y.Bytes()[:]) - suffix.Write(protoID) - suffix.Write(id[:]) - - // At this point secret_input has the 2 exponents, concatenated, append the - // client/server common suffix. - secretInput.Write(suffix.Bytes()) - - // KEY_SEED = H(secret_input, t_key) - h := hmac.New(sha256.New, tKey) - h.Write(secretInput.Bytes()) - tmp := h.Sum(nil) - copy(keySeed[:], tmp) - - // verify = H(secret_input, t_verify) - h = hmac.New(sha256.New, tVerify) - h.Write(secretInput.Bytes()) - verify := h.Sum(nil) - - // auth_input = verify | ID | B | Y | X | PROTOID | "Server" - authInput := bytes.NewBuffer(verify) - authInput.Write(suffix.Bytes()) - authInput.Write([]byte("Server")) - h = hmac.New(sha256.New, tMac) - h.Write(authInput.Bytes()) - tmp = h.Sum(nil) - copy(auth[:], tmp) - - return keySeed, auth -} - -func constantTimeIsZero(x []byte) int { - var ret byte - for _, v := range x { - ret |= v - } - - return subtle.ConstantTimeByteEq(ret, 0) -} - -// Kdf extracts and expands KEY_SEED via HKDF-SHA256 and returns `okm_len` bytes -// of key material. -func Kdf(keySeed []byte, okmLen int) []byte { - kdf := hkdf.New(sha256.New, keySeed, tKey, mExpand) - okm := make([]byte, okmLen) - n, err := io.ReadFull(kdf, okm) - if err != nil { - panic(fmt.Sprintf("BUG: Failed HKDF: %s", err.Error())) - } else if n != len(okm) { - panic(fmt.Sprintf("BUG: Got truncated HKDF output: %d", n)) - } - - return okm -} - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/ntor/ntor_test.go b/ntor/ntor_test.go deleted file mode 100644 index 9d7c687..0000000 --- a/ntor/ntor_test.go +++ /dev/null @@ -1,182 +0,0 @@ -/* - * 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 ntor - -import ( - "bytes" - "testing" -) - -// TestNewKeypair tests Curve25519/Elligator keypair generation. -func TestNewKeypair(t *testing.T) { - // Test standard Curve25519 first. - keypair, err := NewKeypair(false) - if err != nil { - t.Fatal("NewKeypair(false) failed:", err) - } - if keypair == nil { - t.Fatal("NewKeypair(false) returned nil") - } - if keypair.HasElligator() { - t.Fatal("NewKeypair(false) has a Elligator representative") - } - - // Test Elligator generation. - keypair, err = NewKeypair(true) - if err != nil { - t.Fatal("NewKeypair(true) failed:", err) - } - if keypair == nil { - t.Fatal("NewKeypair(true) returned nil") - } - if !keypair.HasElligator() { - t.Fatal("NewKeypair(true) mising an Elligator representative") - } -} - -// Test Client/Server handshake. -func TestHandshake(t *testing.T) { - clientKeypair, err := NewKeypair(true) - if err != nil { - t.Fatal("Failed to generate client keypair:", err) - } - if clientKeypair == nil { - t.Fatal("Client keypair is nil") - } - - serverKeypair, err := NewKeypair(true) - if err != nil { - t.Fatal("Failed to generate server keypair:", err) - } - if serverKeypair == nil { - t.Fatal("Server keypair is nil") - } - - idKeypair, err := NewKeypair(false) - if err != nil { - t.Fatal("Failed to generate identity keypair:", err) - } - if idKeypair == nil { - t.Fatal("Identity keypair is nil") - } - - nodeID, err := NewNodeID([]byte("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13")) - if err != nil { - t.Fatal("Failed to load NodeId:", err) - } - - // ServerHandshake - clientPublic := clientKeypair.Representative().ToPublic() - ok, serverSeed, serverAuth := ServerHandshake(clientPublic, - serverKeypair, idKeypair, nodeID) - if !ok { - t.Fatal("ServerHandshake failed") - } - if serverSeed == nil { - t.Fatal("ServerHandshake returned nil KEY_SEED") - } - if serverAuth == nil { - t.Fatal("ServerHandshake returned nil AUTH") - } - - // ClientHandshake - ok, clientSeed, clientAuth := ClientHandshake(clientKeypair, - serverKeypair.Public(), idKeypair.Public(), nodeID) - if !ok { - t.Fatal("ClientHandshake failed") - } - if clientSeed == nil { - t.Fatal("ClientHandshake returned nil KEY_SEED") - } - if clientAuth == nil { - t.Fatal("ClientHandshake returned nil AUTH") - } - - // WARNING: Use a constant time comparison in actual code. - if 0 != bytes.Compare(clientSeed.Bytes()[:], serverSeed.Bytes()[:]) { - t.Fatal("KEY_SEED mismatched between client/server") - } - if 0 != bytes.Compare(clientAuth.Bytes()[:], serverAuth.Bytes()[:]) { - t.Fatal("AUTH mismatched between client/server") - } -} - -// Benchmark Client/Server handshake. The actual time taken that will be -// observed on either the Client or Server is half the reported time per -// operation since the benchmark does both sides. -func BenchmarkHandshake(b *testing.B) { - // Generate the "long lasting" identity key and NodeId. - idKeypair, err := NewKeypair(false) - if err != nil || idKeypair == nil { - b.Fatal("Failed to generate identity keypair") - } - nodeID, err := NewNodeID([]byte("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13")) - if err != nil { - b.Fatal("Failed to load NodeId:", err) - } - b.ResetTimer() - - // Start the actual benchmark. - for i := 0; i < b.N; i++ { - // Generate the keypairs. - serverKeypair, err := NewKeypair(true) - if err != nil || serverKeypair == nil { - b.Fatal("Failed to generate server keypair") - } - - clientKeypair, err := NewKeypair(true) - if err != nil || clientKeypair == nil { - b.Fatal("Failed to generate client keypair") - } - - // Server handshake. - clientPublic := clientKeypair.Representative().ToPublic() - ok, serverSeed, serverAuth := ServerHandshake(clientPublic, - serverKeypair, idKeypair, nodeID) - if !ok || serverSeed == nil || serverAuth == nil { - b.Fatal("ServerHandshake failed") - } - - // Client handshake. - serverPublic := serverKeypair.Representative().ToPublic() - ok, clientSeed, clientAuth := ClientHandshake(clientKeypair, - serverPublic, idKeypair.Public(), nodeID) - if !ok || clientSeed == nil || clientAuth == nil { - b.Fatal("ClientHandshake failed") - } - - // Validate the authenticator. Real code would pass the AUTH read off - // the network as a slice to CompareAuth here. - if !CompareAuth(clientAuth, serverAuth.Bytes()[:]) || - !CompareAuth(serverAuth, clientAuth.Bytes()[:]) { - b.Fatal("AUTH mismatched between client/server") - } - } -} - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/obfs4.go b/obfs4.go deleted file mode 100644 index 4cc159c..0000000 --- a/obfs4.go +++ /dev/null @@ -1,758 +0,0 @@ -/* - * 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 implements the obfs4 protocol. For the most part, obfs4 -// connections are exposed via the net.Conn and net.Listener interface, though -// accepting connections as a server requires calling ServerHandshake on the -// conn to finish connection establishment. -package obfs4 - -import ( - "bytes" - "crypto/sha256" - "encoding/base64" - "fmt" - "io" - "math/rand" - "net" - "syscall" - "time" - - "git.torproject.org/pluggable-transports/obfs4.git/drbg" - "git.torproject.org/pluggable-transports/obfs4.git/framing" - "git.torproject.org/pluggable-transports/obfs4.git/ntor" -) - -const ( - // SeedLength is the length of the obfs4 polymorphism seed. - SeedLength = 32 - headerLength = framing.FrameOverhead + packetOverhead - connectionTimeout = time.Duration(30) * time.Second - - maxCloseDelayBytes = maxHandshakeLength - maxCloseDelay = 60 - - maxIatDelay = 100 -) - -type connState int - -const ( - stateInit connState = iota - stateEstablished - stateBroken - stateClosed -) - -// Obfs4Conn is the implementation of the net.Conn interface for obfs4 -// connections. -type Obfs4Conn struct { - conn net.Conn - - sessionKey *ntor.Keypair - - lenProbDist *wDist - iatProbDist *wDist - - encoder *framing.Encoder - decoder *framing.Decoder - - receiveBuffer bytes.Buffer - receiveDecodedBuffer bytes.Buffer - - state connState - isServer bool - - // Server side state. - listener *Obfs4Listener - startTime time.Time -} - -func (c *Obfs4Conn) padBurst(burst *bytes.Buffer) (err error) { - tailLen := burst.Len() % framing.MaximumSegmentLength - toPadTo := c.lenProbDist.sample() - - padLen := 0 - if toPadTo >= tailLen { - padLen = toPadTo - tailLen - } else { - padLen = (framing.MaximumSegmentLength - tailLen) + toPadTo - } - - if padLen > headerLength { - err = c.producePacket(burst, packetTypePayload, []byte{}, - uint16(padLen-headerLength)) - if err != nil { - return - } - } else if padLen > 0 { - err = c.producePacket(burst, packetTypePayload, []byte{}, - maxPacketPayloadLength) - if err != nil { - return - } - err = c.producePacket(burst, packetTypePayload, []byte{}, - uint16(padLen)) - if err != nil { - return - } - } - - return -} - -func (c *Obfs4Conn) closeAfterDelay() { - // I-it's not like I w-wanna handshake with you or anything. B-b-baka! - defer c.conn.Close() - - delay := time.Duration(c.listener.closeDelay)*time.Second + connectionTimeout - deadline := c.startTime.Add(delay) - if time.Now().After(deadline) { - return - } - - err := c.conn.SetReadDeadline(deadline) - if 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(c.listener.closeDelayBytes) { - n, err := c.conn.Read(buf[:]) - if err != nil { - return - } - discarded += n - } -} - -func (c *Obfs4Conn) setBroken() { - c.state = stateBroken -} - -func (c *Obfs4Conn) clientHandshake(nodeID *ntor.NodeID, publicKey *ntor.PublicKey) (err error) { - if c.isServer { - panic(fmt.Sprintf("BUG: clientHandshake() called for server connection")) - } - - defer func() { - c.sessionKey = nil - if err != nil { - c.setBroken() - } - }() - - // Generate/send the client handshake. - var hs *clientHandshake - var blob []byte - hs = newClientHandshake(nodeID, publicKey, c.sessionKey) - blob, err = hs.generateHandshake() - if err != nil { - return - } - - err = c.conn.SetDeadline(time.Now().Add(connectionTimeout * 2)) - if err != nil { - return - } - - _, err = c.conn.Write(blob) - if err != nil { - return - } - - // Consume the server handshake. - var hsBuf [maxHandshakeLength]byte - for { - var n int - n, err = c.conn.Read(hsBuf[:]) - if err != nil { - // Yes, just bail out of handshaking even if the Read could have - // returned data, no point in continuing on EOF/etc. - return - } - c.receiveBuffer.Write(hsBuf[:n]) - - var seed []byte - n, seed, err = hs.parseServerHandshake(c.receiveBuffer.Bytes()) - if err == ErrMarkNotFoundYet { - continue - } else if err != nil { - return - } - _ = c.receiveBuffer.Next(n) - - err = c.conn.SetDeadline(time.Time{}) - if err != nil { - return - } - - // Use the derived key material to intialize the link crypto. - okm := ntor.Kdf(seed, framing.KeyLength*2) - c.encoder = framing.NewEncoder(okm[:framing.KeyLength]) - c.decoder = framing.NewDecoder(okm[framing.KeyLength:]) - - c.state = stateEstablished - - return nil - } -} - -func (c *Obfs4Conn) serverHandshake(nodeID *ntor.NodeID, keypair *ntor.Keypair) (err error) { - if !c.isServer { - panic(fmt.Sprintf("BUG: serverHandshake() called for client connection")) - } - - defer func() { - c.sessionKey = nil - if err != nil { - c.setBroken() - } - }() - - hs := newServerHandshake(nodeID, keypair, c.sessionKey) - err = c.conn.SetDeadline(time.Now().Add(connectionTimeout)) - if err != nil { - return - } - - // Consume the client handshake. - var hsBuf [maxHandshakeLength]byte - for { - var n int - n, err = c.conn.Read(hsBuf[:]) - if err != nil { - // Yes, just bail out of handshaking even if the Read could have - // returned data, no point in continuing on EOF/etc. - return - } - c.receiveBuffer.Write(hsBuf[:n]) - - var seed []byte - seed, err = hs.parseClientHandshake(c.listener.filter, c.receiveBuffer.Bytes()) - if err == ErrMarkNotFoundYet { - continue - } else if err != nil { - return - } - c.receiveBuffer.Reset() - - err = c.conn.SetDeadline(time.Time{}) - if err != nil { - return - } - - // Use the derived key material to intialize the link crypto. - okm := ntor.Kdf(seed, framing.KeyLength*2) - c.encoder = framing.NewEncoder(okm[framing.KeyLength:]) - c.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 - } - c.state = stateEstablished - - // Send the PRNG seed as the first packet. - err = c.producePacket(&frameBuf, packetTypePrngSeed, c.listener.rawSeed, 0) - if err != nil { - return - } - _, err = c.conn.Write(frameBuf.Bytes()) - if err != nil { - return - } - - return -} - -// CanHandshake queries the connection state to see if it is appropriate to -// call ServerHandshake to complete connection establishment. -func (c *Obfs4Conn) CanHandshake() bool { - return c.state == stateInit -} - -// CanReadWrite queries the connection state to see if it is possible to read -// and write data. -func (c *Obfs4Conn) CanReadWrite() bool { - return c.state == stateEstablished -} - -// ServerHandshake completes the server side of the obfs4 handshake. Servers -// are required to call this after accepting a connection. ServerHandshake -// will treat errors encountered during the handshake as fatal and drop the -// connection before returning. -func (c *Obfs4Conn) ServerHandshake() error { - // Handshakes when already established are a no-op. - if c.CanReadWrite() { - return nil - } else if !c.CanHandshake() { - return syscall.EINVAL - } - - if !c.isServer { - panic(fmt.Sprintf("BUG: ServerHandshake() called for client connection")) - } - - // Complete the handshake. - err := c.serverHandshake(c.listener.nodeID, c.listener.keyPair) - if err != nil { - c.closeAfterDelay() - } - c.listener = nil - - return err -} - -// Read implements the net.Conn Read method. -func (c *Obfs4Conn) Read(b []byte) (n int, err error) { - if !c.CanReadWrite() { - return 0, syscall.EINVAL - } - - for c.receiveDecodedBuffer.Len() == 0 { - _, err = c.consumeFramedPackets(nil) - if err == framing.ErrAgain { - continue - } else if err != nil { - return - } - } - - n, err = c.receiveDecodedBuffer.Read(b) - return -} - -// WriteTo implements the io.WriterTo WriteTo method. -func (c *Obfs4Conn) WriteTo(w io.Writer) (n int64, err error) { - if !c.CanReadWrite() { - return 0, syscall.EINVAL - } - - // If there is buffered payload from earlier Read() calls, write. - wrLen := 0 - if c.receiveDecodedBuffer.Len() > 0 { - wrLen, err = w.Write(c.receiveDecodedBuffer.Bytes()) - if err != nil { - c.setBroken() - return int64(wrLen), err - } else if wrLen < int(c.receiveDecodedBuffer.Len()) { - c.setBroken() - return int64(wrLen), io.ErrShortWrite - } - c.receiveDecodedBuffer.Reset() - } - - for { - wrLen, err = c.consumeFramedPackets(w) - n += int64(wrLen) - if err == framing.ErrAgain { - continue - } else if err != nil { - // io.EOF is treated as not an error. - if err == io.EOF { - err = nil - } - break - } - } - - return -} - -// Write implements the net.Conn Write method. The obfs4 lengt obfuscation is -// done based on the amount of data passed to Write (each call to Write results -// in up to 2 frames of padding). Passing excessively short buffers to Write -// will result in significant overhead. -func (c *Obfs4Conn) Write(b []byte) (n int, err error) { - if !c.CanReadWrite() { - return 0, syscall.EINVAL - } - - defer func() { - if err != nil { - c.setBroken() - } - }() - - // TODO: Change this to write directly to c.conn skipping frameBuf. - chopBuf := bytes.NewBuffer(b) - var payload [maxPacketPayloadLength]byte - var frameBuf bytes.Buffer - - 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 = c.producePacket(&frameBuf, packetTypePayload, payload[:rdLen], 0) - if err != nil { - return 0, err - } - } - - // Insert random padding. In theory for some padding lengths, this can be - // inlined with the payload, but doing it this way simplifies the code - // significantly. - err = c.padBurst(&frameBuf) - if err != nil { - return 0, err - } - - // Spit frame(s) onto the network. - // - // Partial writes are fatal because the frame encoder state is advanced - // at this point. It's possible to keep frameBuf around, but fuck it. - // Someone that wants write timeouts can change this. - if c.iatProbDist != 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(c.iatProbDist.sample() * 100) - - // Write then sleep. - _, err = c.conn.Write(iatFrame[:iatWrLen]) - if err != nil { - return 0, err - } - time.Sleep(iatDelta * time.Microsecond) - } - } else { - _, err = c.conn.Write(frameBuf.Bytes()) - if err != nil { - return 0, err - } - } - - return -} - -// Close closes the connection. -func (c *Obfs4Conn) Close() error { - if c.conn == nil { - return syscall.EINVAL - } - - c.state = stateClosed - - return c.conn.Close() -} - -// LocalAddr returns the local network address. -func (c *Obfs4Conn) LocalAddr() net.Addr { - if c.state == stateClosed { - return nil - } - - return c.conn.LocalAddr() -} - -// RemoteAddr returns the remote network address. -func (c *Obfs4Conn) RemoteAddr() net.Addr { - if c.state == stateClosed { - return nil - } - - return c.conn.RemoteAddr() -} - -// SetDeadline is a convoluted way to get syscall.ENOTSUP. -func (c *Obfs4Conn) SetDeadline(t time.Time) error { - return syscall.ENOTSUP -} - -// SetReadDeadline implements the net.Conn SetReadDeadline method. Connections -// must be in the established state (CanReadWrite). -func (c *Obfs4Conn) SetReadDeadline(t time.Time) error { - if !c.CanReadWrite() { - return syscall.EINVAL - } - - return c.conn.SetReadDeadline(t) -} - -// SetWriteDeadline is a convoluted way to get syscall.ENOTSUP. -func (c *Obfs4Conn) SetWriteDeadline(t time.Time) error { - return syscall.ENOTSUP -} - -// DialFn is a function pointer to a dial routine that matches the -// net.Dialer.Dial routine. -type DialFn func(string, string) (net.Conn, error) - -// DialObfs4 connects to the remote address on the network, and handshakes with -// the peer's obfs4 Node ID and Identity Public Key. nodeID and publicKey are -// expected as strings containing the Base64 encoded values. -func DialObfs4(network, address, nodeID, publicKey string, iatObfuscation bool) (*Obfs4Conn, error) { - - return DialObfs4DialFn(net.Dial, network, address, nodeID, publicKey, iatObfuscation) -} - -// DialObfs4DialFn connects to the remote address on the network via DialFn, -// and handshakes with the peers' obfs4 Node ID and Identity Public Key. -func DialObfs4DialFn(dialFn DialFn, network, address, nodeID, publicKey string, iatObfuscation bool) (*Obfs4Conn, error) { - // Decode the node_id/public_key. - pub, err := ntor.PublicKeyFromBase64(publicKey) - if err != nil { - return nil, err - } - id, err := ntor.NodeIDFromBase64(nodeID) - if err != nil { - return nil, err - } - - // Generate the initial length obfuscation distribution. - seed, err := drbg.NewSeed() - if err != nil { - return nil, err - } - - // Generate the Obfs4Conn. - c := new(Obfs4Conn) - c.lenProbDist = newWDist(seed, 0, framing.MaximumSegmentLength) - if iatObfuscation { - iatSeedSrc := sha256.Sum256(seed.Bytes()[:]) - iatSeed, err := drbg.SeedFromBytes(iatSeedSrc[:]) - if err != nil { - return nil, err - } - c.iatProbDist = newWDist(iatSeed, 0, maxIatDelay) - } - - // Generate the session keypair *before* connecting to the remote peer. - c.sessionKey, err = ntor.NewKeypair(true) - if err != nil { - return nil, err - } - - // Connect to the remote peer. - c.conn, err = dialFn(network, address) - if err != nil { - return nil, err - } - - // Handshake. - err = c.clientHandshake(id, pub) - if err != nil { - c.conn.Close() - return nil, err - } - - return c, nil -} - -// Obfs4Listener is the implementation of the net.Listener interface for obfs4 -// connections. -type Obfs4Listener struct { - listener net.Listener - - filter *replayFilter - - keyPair *ntor.Keypair - nodeID *ntor.NodeID - - rawSeed []byte - seed *drbg.Seed - iatSeed *drbg.Seed - iatObfuscation bool - - closeDelayBytes int - closeDelay int -} - -// Accept implements the Accept method of the net.Listener interface; it waits -// for the next call and returns a generic net.Conn. Callers are responsible -// for completing the handshake by calling Obfs4Conn.ServerHandshake(). -func (l *Obfs4Listener) Accept() (net.Conn, error) { - conn, err := l.AcceptObfs4() - if err != nil { - return nil, err - } - - return conn, nil -} - -// AcceptObfs4 accepts the next incoming call and returns a new connection. -// Callers are responsible for completing the handshake by calling -// Obfs4Conn.ServerHandshake(). -func (l *Obfs4Listener) AcceptObfs4() (*Obfs4Conn, error) { - // Accept a connection. - c, err := l.listener.Accept() - if err != nil { - return nil, err - } - - // Allocate the obfs4 connection state. - cObfs := new(Obfs4Conn) - - // Generate the session keypair *before* consuming data from the peer, to - // add more noise to the keypair generation time. The idea is that jitter - // here is masked by network latency (the time it takes for a server to - // accept a socket out of the backlog should not be fixed, and the client - // needs to send the public key). - cObfs.sessionKey, err = ntor.NewKeypair(true) - if err != nil { - return nil, err - } - - cObfs.conn = c - cObfs.isServer = true - cObfs.listener = l - cObfs.lenProbDist = newWDist(l.seed, 0, framing.MaximumSegmentLength) - if l.iatObfuscation { - cObfs.iatProbDist = newWDist(l.iatSeed, 0, maxIatDelay) - } - if err != nil { - c.Close() - return nil, err - } - cObfs.startTime = time.Now() - - return cObfs, nil -} - -// Close stops listening on the Obfs4 endpoint. Already Accepted connections -// are not closed. -func (l *Obfs4Listener) Close() error { - return l.listener.Close() -} - -// Addr returns the listener's network address. -func (l *Obfs4Listener) Addr() net.Addr { - return l.listener.Addr() -} - -// PublicKey returns the listener's Identity Public Key, a Base64 encoded -// obfs4.ntor.PublicKey. -func (l *Obfs4Listener) PublicKey() string { - if l.keyPair == nil { - return "" - } - - return l.keyPair.Public().Base64() -} - -// NodeID returns the listener's NodeID, a Base64 encoded obfs4.ntor.NodeID. -func (l *Obfs4Listener) NodeID() string { - if l.nodeID == nil { - return "" - } - - return l.nodeID.Base64() -} - -// ListenObfs4 annnounces on the network and address, and returns and -// Obfs4Listener. nodeId, privateKey and seed are expected as strings -// containing the Base64 encoded values. -func ListenObfs4(network, laddr, nodeID, privateKey, seed string, iatObfuscation bool) (*Obfs4Listener, error) { - var err error - - // Decode node_id/private_key. - l := new(Obfs4Listener) - l.keyPair, err = ntor.KeypairFromBase64(privateKey) - if err != nil { - return nil, err - } - l.nodeID, err = ntor.NodeIDFromBase64(nodeID) - if err != nil { - return nil, err - } - l.rawSeed, err = base64.StdEncoding.DecodeString(seed) - if err != nil { - return nil, err - } - l.seed, err = drbg.SeedFromBytes(l.rawSeed) - if err != nil { - return nil, err - } - l.iatObfuscation = iatObfuscation - if l.iatObfuscation { - iatSeedSrc := sha256.Sum256(l.seed.Bytes()[:]) - l.iatSeed, err = drbg.SeedFromBytes(iatSeedSrc[:]) - if err != nil { - return nil, err - } - } - - l.filter, err = newReplayFilter() - if err != nil { - return nil, err - } - - rng := rand.New(drbg.NewHashDrbg(l.seed)) - l.closeDelayBytes = rng.Intn(maxCloseDelayBytes) - l.closeDelay = rng.Intn(maxCloseDelay) - - // Start up the listener. - l.listener, err = net.Listen(network, laddr) - if err != nil { - return nil, err - } - - return l, nil -} - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/obfs4proxy/obfs4proxy.go b/obfs4proxy/obfs4proxy.go index c49f3d0..d3490bf 100644 --- a/obfs4proxy/obfs4proxy.go +++ b/obfs4proxy/obfs4proxy.go @@ -23,31 +23,13 @@ * 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. - * - * This file is based off goptlib's dummy-[client,server].go files. */ -// obfs4 pluggable transport. Works only as a managed proxy. -// -// Client usage (in torrc): -// UseBridges 1 -// Bridge obfs4 X.X.X.X:YYYY public-key= node-id= -// ClientTransportPlugin obfs4 exec obfs4proxy -// -// Server usage (in torrc): -// BridgeRelay 1 -// ORPort 9001 -// ExtORPort 6669 -// ServerTransportPlugin obfs4 exec obfs4proxy -// ServerTransportOptions obfs4 private-key= node-id= drbg-seed= -// -// Because the pluggable transport requires arguments, obfs4proxy requires -// tor-0.2.5.x to be useful. +// Go language Tor Pluggable Transport suite. Works only as a managed +// client/server. package main import ( - "encoding/base64" - "encoding/hex" "flag" "fmt" "io" @@ -61,292 +43,307 @@ import ( "sync" "syscall" + "code.google.com/p/go.net/proxy" + "git.torproject.org/pluggable-transports/goptlib.git" - "git.torproject.org/pluggable-transports/obfs4.git" - "git.torproject.org/pluggable-transports/obfs4.git/csrand" - "git.torproject.org/pluggable-transports/obfs4.git/ntor" + "git.torproject.org/pluggable-transports/obfs4.git/transports" + "git.torproject.org/pluggable-transports/obfs4.git/transports/base" ) const ( - obfs4Method = "obfs4" - obfs4LogFile = "obfs4proxy.log" + obfs4proxyLogFile = "obfs4proxy.log" + socksAddr = "127.0.0.1:0" + elidedAddr = "[scrubbed]" ) var enableLogging bool var unsafeLogging bool -var iatObfuscation bool -var ptListeners []net.Listener +var stateDir string +var handlerChan chan int -// When a connection handler starts, +1 is written to this channel; when it -// ends, -1 is written. -var handlerChan = make(chan int) +// DialFn is a function pointer to a function that matches the net.Dialer.Dial +// interface. +type DialFn func(string, string) (net.Conn, error) -func logAndRecover(conn *obfs4.Obfs4Conn) { - if err := recover(); err != nil { - log.Printf("[ERROR] %p: Panic: %s", conn, err) +func elideAddr(addrStr string) string { + if unsafeLogging { + return addrStr } + + if addr, err := resolveAddrStr(addrStr); err == nil { + // Only scrub off the address so that it's slightly easier to follow + // the logs by looking at the port. + return fmt.Sprintf("%s:%d", elidedAddr, addr.Port) + } + + return elidedAddr } -func copyLoop(a net.Conn, b *obfs4.Obfs4Conn) { - var wg sync.WaitGroup - wg.Add(2) +func clientSetup() (launched bool, listeners []net.Listener) { + ptClientInfo, err := pt.ClientSetup(transports.Transports()) + if err != nil { + log.Fatal(err) + } - go func() { - defer logAndRecover(b) - defer wg.Done() - defer b.Close() - defer a.Close() + ptClientProxy, err := ptGetProxy() + if err != nil { + log.Fatal(err) + } else if ptClientProxy != nil { + ptProxyDone() + } - _, err := io.Copy(b, a) - if err != nil { - log.Printf("[WARN] copyLoop: %p: Connection closed: %s", b, err) + // Launch each of the client listeners. + for _, name := range ptClientInfo.MethodNames { + t := transports.Get(name) + if t == nil { + pt.CmethodError(name, "no such transport is supported") + continue } - }() - go func() { - defer logAndRecover(b) - defer wg.Done() - defer a.Close() - defer b.Close() - _, err := io.Copy(a, b) + f, err := t.ClientFactory(stateDir) if err != nil { - log.Printf("[WARN] copyLoop: %p: Connection closed: %s", b, err) + pt.CmethodError(name, "failed to get ClientFactory") + continue } - }() - - wg.Wait() -} - -func serverHandler(conn *obfs4.Obfs4Conn, info *pt.ServerInfo) error { - defer conn.Close() - defer logAndRecover(conn) - - handlerChan <- 1 - defer func() { - handlerChan <- -1 - }() - var addr string - if unsafeLogging { - addr = conn.RemoteAddr().String() - } else { - addr = "[scrubbed]" - } + ln, err := pt.ListenSocks("tcp", socksAddr) + if err != nil { + pt.CmethodError(name, err.Error()) + continue + } - log.Printf("[INFO] server: %p: New connection from %s", conn, addr) + go clientAcceptLoop(f, ln, ptClientProxy) + pt.Cmethod(name, ln.Version(), ln.Addr()) - // Handshake with the client. - err := conn.ServerHandshake() - if err != nil { - log.Printf("[WARN] server: %p: Handshake failed: %s", conn, err) - return err - } + log.Printf("[INFO]: %s - registered listener: %s", name, ln.Addr()) - or, err := pt.DialOr(info, conn.RemoteAddr().String(), obfs4Method) - if err != nil { - log.Printf("[ERROR] server: %p: DialOr failed: %s", conn, err) - return err + listeners = append(listeners, ln) + launched = true } - defer or.Close() - - copyLoop(or, conn) + pt.CmethodsDone() - return nil + return } -func serverAcceptLoop(ln *obfs4.Obfs4Listener, info *pt.ServerInfo) error { +func clientAcceptLoop(f base.ClientFactory, ln *pt.SocksListener, proxyURI *url.URL) error { defer ln.Close() for { - conn, err := ln.AcceptObfs4() + conn, err := ln.AcceptSocks() if err != nil { if e, ok := err.(net.Error); ok && !e.Temporary() { return err } continue } - go serverHandler(conn, info) + go clientHandler(f, conn, proxyURI) } } -func serverSetup() (launched bool) { - // Initialize pt logging. - err := ptInitializeLogging(enableLogging) +func clientHandler(f base.ClientFactory, conn *pt.SocksConn, proxyURI *url.URL) { + defer conn.Close() + handlerChan <- 1 + defer func() { + handlerChan <- -1 + }() + + name := f.Transport().Name() + addrStr := elideAddr(conn.Req.Target) + log.Printf("[INFO]: %s(%s) - new connection", name, addrStr) + + // Deal with arguments. + args, err := f.ParseArgs(&conn.Req.Args) if err != nil { + log.Printf("[ERROR]: %s(%s) - invalid arguments: %s", name, addrStr, err) + conn.Reject() return } - ptServerInfo, err := pt.ServerSetup([]string{obfs4Method}) + // Obtain the proxy dialer if any, and create the outgoing TCP connection. + var dialFn DialFn + if proxyURI == nil { + dialFn = proxy.Direct.Dial + } else { + // This is unlikely to happen as the proxy protocol is verified during + // the configuration phase. + dialer, err := proxy.FromURL(proxyURI, proxy.Direct) + if err != nil { + log.Printf("[ERROR]: %s(%s) - failed to obtain proxy dialer: %s", name, addrStr, err) + conn.Reject() + return + } + dialFn = dialer.Dial + } + remoteConn, err := dialFn("tcp", conn.Req.Target) // XXX: Allow UDP? if err != nil { + // Note: The error message returned from the dialer can include the IP + // address/port of the remote peer. + if unsafeLogging { + log.Printf("[ERROR]: %s(%s) - outgoing connection failed: %s", name, addrStr, err) + } else { + log.Printf("[ERROR]: %s(%s) - outgoing connection failed", name, addrStr) + } + conn.Reject() return } + defer remoteConn.Close() - for _, bindaddr := range ptServerInfo.Bindaddrs { - switch bindaddr.MethodName { - case obfs4Method: - // Handle the mandetory arguments. - privateKey, ok := bindaddr.Options.Get("private-key") - if !ok { - pt.SmethodError(bindaddr.MethodName, "needs a private-key option") - break - } - nodeID, ok := bindaddr.Options.Get("node-id") - if !ok { - pt.SmethodError(bindaddr.MethodName, "needs a node-id option") - break - } - seed, ok := bindaddr.Options.Get("drbg-seed") - if !ok { - pt.SmethodError(bindaddr.MethodName, "needs a drbg-seed option") - break - } - - // Initialize the listener. - ln, err := obfs4.ListenObfs4("tcp", bindaddr.Addr.String(), nodeID, - privateKey, seed, iatObfuscation) - if err != nil { - pt.SmethodError(bindaddr.MethodName, err.Error()) - break - } + // Instantiate the client transport method, handshake, and start pushing + // bytes back and forth. + remote, err := f.WrapConn(remoteConn, args) + if err != nil { + log.Printf("[ERROR]: %s(%s) - handshake failed: %s", name, addrStr, err) + conn.Reject() + return + } + err = conn.Grant(remoteConn.RemoteAddr().(*net.TCPAddr)) + if err != nil { + log.Printf("[ERROR]: %s(%s) - SOCKS grant failed: %s", name, addrStr, err) + return + } - // Report the SMETHOD including the parameters. - args := pt.Args{} - args.Add("node-id", nodeID) - args.Add("public-key", ln.PublicKey()) - go serverAcceptLoop(ln, &ptServerInfo) - pt.SmethodArgs(bindaddr.MethodName, ln.Addr(), args) - ptListeners = append(ptListeners, ln) - launched = true - default: - pt.SmethodError(bindaddr.MethodName, "no such method") - } + err = copyLoop(conn, remote) + if err != nil { + log.Printf("[INFO]: %s(%s) - closed connection: %s", name, addrStr, err) + } else { + log.Printf("[INFO]: %s(%s) - closed connection", name, addrStr) } - pt.SmethodsDone() return } -func clientHandler(conn *pt.SocksConn, proxyURI *url.URL) error { - defer conn.Close() - - var addr string - if unsafeLogging { - addr = conn.Req.Target - } else { - addr = "[scrubbed]" +func serverSetup() (launched bool, listeners []net.Listener) { + ptServerInfo, err := pt.ServerSetup(transports.Transports()) + if err != nil { + log.Fatal(err) } - log.Printf("[INFO] client: New connection to %s", addr) + for _, bindaddr := range ptServerInfo.Bindaddrs { + name := bindaddr.MethodName + t := transports.Get(name) + if t == nil { + pt.SmethodError(name, "no such transport is supported") + continue + } - // Extract the peer's node ID and public key. - nodeID, ok := conn.Req.Args.Get("node-id") - if !ok { - log.Printf("[ERROR] client: missing node-id argument") - conn.Reject() - return nil - } - publicKey, ok := conn.Req.Args.Get("public-key") - if !ok { - log.Printf("[ERROR] client: missing public-key argument") - conn.Reject() - return nil - } + f, err := t.ServerFactory(stateDir, &bindaddr.Options) + if err != nil { + pt.SmethodError(name, err.Error()) + continue + } - handlerChan <- 1 - defer func() { - handlerChan <- -1 - }() + ln, err := net.ListenTCP("tcp", bindaddr.Addr) + if err != nil { + pt.SmethodError(name, err.Error()) + } - defer logAndRecover(nil) - dialFn, err := getProxyDialer(proxyURI) - if err != nil { - log.Printf("[ERROR] client: failed to get proxy dialer: %s", err) - conn.Reject() - return err - } - remote, err := obfs4.DialObfs4DialFn(dialFn, "tcp", conn.Req.Target, nodeID, publicKey, iatObfuscation) - if err != nil { - log.Printf("[ERROR] client: %p: Handshake failed: %s", remote, err) - conn.Reject() - return err - } - defer remote.Close() - err = conn.Grant(remote.RemoteAddr().(*net.TCPAddr)) - if err != nil { - return err - } + go serverAcceptLoop(f, ln, &ptServerInfo) + if args := f.Args(); args != nil { + pt.SmethodArgs(name, ln.Addr(), *args) + } else { + pt.SmethodArgs(name, ln.Addr(), nil) + } - copyLoop(conn, remote) + log.Printf("[INFO]: %s - registered listener: %s", name, elideAddr(ln.Addr().String())) - return nil + listeners = append(listeners, ln) + launched = true + } + pt.SmethodsDone() + + return } -func clientAcceptLoop(ln *pt.SocksListener, proxyURI *url.URL) error { +func serverAcceptLoop(f base.ServerFactory, ln net.Listener, info *pt.ServerInfo) error { defer ln.Close() for { - conn, err := ln.AcceptSocks() + conn, err := ln.Accept() if err != nil { if e, ok := err.(net.Error); ok && !e.Temporary() { return err } continue } - go clientHandler(conn, proxyURI) + go serverHandler(f, conn, info) } } -func clientSetup() (launched bool) { - // Initialize pt logging. - err := ptInitializeLogging(enableLogging) +func serverHandler(f base.ServerFactory, conn net.Conn, info *pt.ServerInfo) { + defer conn.Close() + handlerChan <- 1 + defer func() { + handlerChan <- -1 + }() + + name := f.Transport().Name() + addrStr := elideAddr(conn.RemoteAddr().String()) + log.Printf("[INFO]: %s(%s) - new connection", name, addrStr) + + // Instantiate the server transport method and handshake. + remote, err := f.WrapConn(conn) if err != nil { + log.Printf("[ERROR]: %s(%s) - handshake failed: %s", name, addrStr, err) return } - ptClientInfo, err := pt.ClientSetup([]string{obfs4Method}) + // Connect to the orport. + orConn, err := pt.DialOr(info, conn.RemoteAddr().String(), name) if err != nil { - log.Fatal(err) + log.Printf("[ERROR]: %s(%s) - failed to connect to ORPort: %s", name, addrStr, err) + return } + defer orConn.Close() - ptClientProxy, err := ptGetProxy() + err = copyLoop(orConn, remote) if err != nil { - log.Fatal(err) - } else if ptClientProxy != nil { - ptProxyDone() + log.Printf("[INFO]: %s(%s) - closed connection: %s", name, addrStr, err) + } else { + log.Printf("[INFO]: %s(%s) - closed connection", name, addrStr) } - for _, methodName := range ptClientInfo.MethodNames { - switch methodName { - case obfs4Method: - ln, err := pt.ListenSocks("tcp", "127.0.0.1:0") - if err != nil { - pt.CmethodError(methodName, err.Error()) - break - } - go clientAcceptLoop(ln, ptClientProxy) - pt.Cmethod(methodName, ln.Version(), ln.Addr()) - ptListeners = append(ptListeners, ln) - launched = true - default: - pt.CmethodError(methodName, "no such method") - } + return +} + +func copyLoop(a net.Conn, b net.Conn) error { + // Note: b is always the pt connection. a is the SOCKS/ORPort connection. + errChan := make(chan error, 2) + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + defer b.Close() + defer a.Close() + _, err := io.Copy(b, a) + errChan <- err + }() + go func() { + defer wg.Done() + defer a.Close() + defer b.Close() + _, err := io.Copy(a, b) + errChan <- err + }() + + // Wait for both upstream and downstream to close. Since one side + // terminating closes the other, the second error in the channel will be + // something like EINVAL (though io.Copy() will swallow EOF), so only the + // first error is returned. + wg.Wait() + if len(errChan) > 0 { + return <-errChan } - pt.CmethodsDone() - return + return nil } func ptInitializeLogging(enable bool) error { if enable { - // pt.MakeStateDir will ENV-ERROR for us. - dir, err := pt.MakeStateDir() - if err != nil { - return err - } - // While we could just exit, log an ENV-ERROR so it will propagate to // the tor log. - f, err := os.OpenFile(path.Join(dir, obfs4LogFile), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) + f, err := os.OpenFile(path.Join(stateDir, obfs4proxyLogFile), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) if err != nil { - return ptEnvError(fmt.Sprintf("Failed to open log file: %s\n", err)) + return ptEnvError(fmt.Sprintf("failed to open log file: %s\n", err)) } log.SetOutput(f) } else { @@ -356,122 +353,74 @@ func ptInitializeLogging(enable bool) error { return nil } -func generateServerParams(id string) { - idIsFP := id != "" - var rawID []byte - - if idIsFP { - var err error - rawID, err = hex.DecodeString(id) - if err != nil { - fmt.Println("Failed to hex decode id:", err) - return - } - } else { - rawID = make([]byte, ntor.NodeIDLength) - err := csrand.Bytes(rawID) - if err != nil { - fmt.Println("Failed to generate random node-id:", err) - return - } - } - parsedID, err := ntor.NewNodeID(rawID) - if err != nil { - fmt.Println("Failed to parse id:", err) - return - } - - fmt.Println("Generated node-id:", parsedID.Base64()) - - keypair, err := ntor.NewKeypair(false) - if err != nil { - fmt.Println("Failed to generate keypair:", err) - return - } - - seed := make([]byte, obfs4.SeedLength) - err = csrand.Bytes(seed) - if err != nil { - fmt.Println("Failed to generate DRBG seed:", err) - return - } - seedBase64 := base64.StdEncoding.EncodeToString(seed) - - fmt.Println("Generated private-key:", keypair.Private().Base64()) - fmt.Println("Generated public-key:", keypair.Public().Base64()) - fmt.Println("Generated drbg-seed:", seedBase64) - fmt.Println() - fmt.Println("Client config: ") - if idIsFP { - fmt.Printf(" Bridge obfs4 %s node-id=%s public-key=%s\n", - id, parsedID.Base64(), keypair.Public().Base64()) - } else { - fmt.Printf(" Bridge obfs4 node-id=%s public-key=%s\n", - parsedID.Base64(), keypair.Public().Base64()) - } - fmt.Println() - fmt.Println("Server config:") - fmt.Printf(" ServerTransportOptions obfs4 node-id=%s private-key=%s drbg-seed=%s\n", - parsedID.Base64(), keypair.Private().Base64(), seedBase64) -} - func main() { - // Some command line args. - genParams := flag.Bool("genServerParams", false, "Generate Bridge operator torrc parameters") - genParamsFP := flag.String("genServerParamsFP", "", "Optional bridge fingerprint for genServerParams") - flag.BoolVar(&enableLogging, "enableLogging", false, "Log to TOR_PT_STATE_LOCATION/obfs4proxy.log") - flag.BoolVar(&iatObfuscation, "iatObfuscation", false, "Enable IAT obufscation (EXPENSIVE)") + // Handle the command line arguments. + _, execName := path.Split(os.Args[0]) + flag.BoolVar(&enableLogging, "enableLogging", false, "Log to TOR_PT_STATE_LOCATION/"+obfs4proxyLogFile) flag.BoolVar(&unsafeLogging, "unsafeLogging", false, "Disable the address scrubber") flag.Parse() - if *genParams { - generateServerParams(*genParamsFP) - return - } - // Go through the pt protocol and initialize client or server mode. + // Determine if this is a client or server, initialize logging, and finish + // the pt configuration. + var ptListeners []net.Listener + handlerChan = make(chan int) launched := false isClient, err := ptIsClient() if err != nil { - log.Fatal("[ERROR] obfs4proxy must be run as a managed transport or server") - } else if isClient { - launched = clientSetup() + log.Fatalf("[ERROR]: %s - must be run as a managed transport", execName) + } + if stateDir, err = pt.MakeStateDir(); err != nil { + log.Fatalf("[ERROR]: %s - No state directory: %s", execName, err) + } + if err = ptInitializeLogging(enableLogging); err != nil { + log.Fatalf("[ERROR]: %s - failed to initialize logging", execName) + } + if isClient { + log.Printf("[INFO]: %s - initializing client transport listeners", execName) + launched, ptListeners = clientSetup() } else { - launched = serverSetup() + log.Printf("[INFO]: %s - initializing server transport listeners", execName) + launched, ptListeners = serverSetup() } if !launched { - // Something must have failed in client/server setup, just bail. + // Initialization failed, the client or server setup routines should + // have logged, so just exit here. os.Exit(-1) } - log.Println("[INFO] obfs4proxy - Launched and listening") + log.Printf("[INFO]: %s - launched and accepting connections", execName) defer func() { - log.Println("[INFO] obfs4proxy - Terminated") + log.Printf("[INFO]: %s - terminated", execName) }() - // Handle termination notification. - numHandlers := 0 - var sig os.Signal + // At this point, the pt config protocol is finished, and incoming + // connections will be processed. Per the pt spec, on sane platforms + // termination is signaled via SIGINT (or SIGTERM), so wait on tor to + // request a shutdown of some sort. + sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - // wait for first signal - sig = nil + // Wait for the first SIGINT (close listeners). + var sig os.Signal + numHandlers := 0 for sig == nil { select { case n := <-handlerChan: numHandlers += n case sig = <-sigChan: + if sig == syscall.SIGTERM { + // SIGTERM causes immediate termination. + return + } } } for _, ln := range ptListeners { ln.Close() } - if sig == syscall.SIGTERM { - return - } - - // wait for second signal or no more handlers + // Wait for the 2nd SIGINT (or a SIGTERM), or for all current sessions to + // finish. sig = nil for sig == nil && numHandlers != 0 { select { @@ -481,5 +430,3 @@ func main() { } } } - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/obfs4proxy/proxy_extras.go b/obfs4proxy/proxy_extras.go deleted file mode 100644 index 5ead3b8..0000000 --- a/obfs4proxy/proxy_extras.go +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 main - -import ( - "net/url" - - "code.google.com/p/go.net/proxy" - - "git.torproject.org/pluggable-transports/obfs4.git" -) - -// getProxyDialer is a trival wrapper around the go.net/proxy package to avoid -// having it as a dependency for anything else. -func getProxyDialer(uri *url.URL) (obfs4.DialFn, error) { - if uri == nil { - return proxy.Direct.Dial, nil - } - - dialer, err := proxy.FromURL(uri, proxy.Direct) - if err != nil { - return nil, err - } - - return dialer.Dial, nil -} diff --git a/obfs4proxy/proxy_http.go b/obfs4proxy/proxy_http.go index c7b926a..2db6ca0 100644 --- a/obfs4proxy/proxy_http.go +++ b/obfs4proxy/proxy_http.go @@ -108,7 +108,6 @@ func (s *httpProxy) Dial(network, addr string) (net.Conn, error) { return conn, nil } -// httpConn is the mountain of bullshit we need to do just for staleReader. type httpConn struct { remoteAddr *net.TCPAddr httpConn *httputil.ClientConn @@ -157,5 +156,3 @@ func (c *httpConn) SetWriteDeadline(t time.Time) error { func init() { proxy.RegisterDialerType("http", newHTTP) } - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/obfs4proxy/proxy_socks4.go b/obfs4proxy/proxy_socks4.go index 95cc7b6..9d6bd4d 100644 --- a/obfs4proxy/proxy_socks4.go +++ b/obfs4proxy/proxy_socks4.go @@ -162,5 +162,3 @@ func init() { // Despite the scheme name, this really is SOCKS4. proxy.RegisterDialerType("socks4a", newSOCKS4) } - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/obfs4proxy/pt_extras.go b/obfs4proxy/pt_extras.go index 2d09cc3..9eddd26 100644 --- a/obfs4proxy/pt_extras.go +++ b/obfs4proxy/pt_extras.go @@ -124,7 +124,7 @@ func ptGetProxy() (*url.URL, error) { return nil, ptProxyError(fmt.Sprintf("proxy URI has invalid scheme: %s", spec.Scheme)) } - err = validateAddrStr(spec.Host) + _, err = resolveAddrStr(spec.Host) if err != nil { return nil, ptProxyError(fmt.Sprintf("proxy URI has invalid host: %s", err)) } @@ -135,27 +135,26 @@ func ptGetProxy() (*url.URL, error) { // Sigh, pt.resolveAddr() isn't exported. Include our own getto version that // doesn't work around #7011, because we don't work with pre-0.2.5.x tor, and // all we care about is validation anyway. -func validateAddrStr(addrStr string) error { +func resolveAddrStr(addrStr string) (*net.TCPAddr, error) { ipStr, portStr, err := net.SplitHostPort(addrStr) if err != nil { - return err + return nil, err } if ipStr == "" { - return net.InvalidAddrError(fmt.Sprintf("address string %q lacks a host part", addrStr)) + return nil, net.InvalidAddrError(fmt.Sprintf("address string %q lacks a host part", addrStr)) } if portStr == "" { - return net.InvalidAddrError(fmt.Sprintf("address string %q lacks a port part", addrStr)) + return nil, net.InvalidAddrError(fmt.Sprintf("address string %q lacks a port part", addrStr)) } - if net.ParseIP(ipStr) == nil { - return net.InvalidAddrError(fmt.Sprintf("not an IP string: %q", ipStr)) + ip := net.ParseIP(ipStr) + if ip == nil { + return nil, net.InvalidAddrError(fmt.Sprintf("not an IP string: %q", ipStr)) } - _, err = strconv.ParseUint(portStr, 10, 16) + port, err := strconv.ParseUint(portStr, 10, 16) if err != nil { - return net.InvalidAddrError(fmt.Sprintf("not a Port string: %q", portStr)) + return nil, net.InvalidAddrError(fmt.Sprintf("not a Port string: %q", portStr)) } - return nil + return &net.TCPAddr{IP: ip, Port: int(port), Zone: ""}, nil } - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/packet.go b/packet.go deleted file mode 100644 index 58a5877..0000000 --- a/packet.go +++ /dev/null @@ -1,212 +0,0 @@ -/* - * 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" - "syscall" - - "git.torproject.org/pluggable-transports/obfs4.git/drbg" - "git.torproject.org/pluggable-transports/obfs4.git/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 (c *Obfs4Conn) producePacket(w io.Writer, pktType uint8, data []byte, padLen uint16) (err error) { - var pkt [framing.MaximumFramePayloadLength]byte - - if !c.CanReadWrite() { - return syscall.EINVAL - } - - if len(data)+int(padLen) > maxPacketPayloadLength { - panic(fmt.Sprintf("BUG: makePacket() len(data) + padLen > maxPacketPayloadLength: %d + %d > %d", - len(data), padLen, maxPacketPayloadLength)) - } - - defer func() { - if err != nil { - c.setBroken() - } - }() - - // 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 = c.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 (c *Obfs4Conn) consumeFramedPackets(w io.Writer) (n int, err error) { - if !c.CanReadWrite() { - return n, syscall.EINVAL - } - - var buf [consumeReadSize]byte - rdLen, rdErr := c.conn.Read(buf[:]) - c.receiveBuffer.Write(buf[:rdLen]) - var decoded [framing.MaximumFramePayloadLength]byte - for c.receiveBuffer.Len() > 0 { - // Decrypt an AEAD frame. - decLen := 0 - decLen, err = c.decoder.Decode(decoded[:], &c.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 { - if w != nil { - // c.WriteTo() skips buffering in c.receiveDecodedBuffer - var wrLen int - wrLen, err = w.Write(payload) - n += wrLen - if err != nil { - break - } else if wrLen < int(payloadLen) { - err = io.ErrShortWrite - break - } - } else { - // c.Read() stashes decoded payload in receiveDecodedBuffer - c.receiveDecodedBuffer.Write(payload) - n += int(payloadLen) - } - } - case packetTypePrngSeed: - // Only regenerate the distribution if we are the client. - if len(payload) == seedPacketPayloadLength && !c.isServer { - var seed *drbg.Seed - seed, err = drbg.SeedFromBytes(payload) - if err != nil { - break - } - c.lenProbDist.reset(seed) - if c.iatProbDist != nil { - iatSeedSrc := sha256.Sum256(seed.Bytes()[:]) - iatSeed, err := drbg.SeedFromBytes(iatSeedSrc[:]) - if err != nil { - break - } - c.iatProbDist.reset(iatSeed) - } - } - default: - // Ignore unrecognised packet types. - } - } - - // Read errors and non-framing.ErrAgain errors are all fatal. - if (err != nil && err != framing.ErrAgain) || rdErr != nil { - // Propagate read errors correctly. - if err == nil && rdErr != nil { - err = rdErr - } - c.setBroken() - } - - return -} - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/replay_filter.go b/replay_filter.go deleted file mode 100644 index b8f284a..0000000 --- a/replay_filter.go +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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 ( - "container/list" - "encoding/binary" - "sync" - - "github.com/dchest/siphash" - - "git.torproject.org/pluggable-transports/obfs4.git/csrand" -) - -// maxFilterSize is the maximum capacity of the replay filter. The busiest -// bridge I know about processes something along the order of 3000 connections -// per day. The maximum timespan any entry can live in the filter is 2 hours, -// so this value should be sufficient. -const maxFilterSize = 100 * 1024 - -// replayFilter is a simple filter designed only to answer if it has seen a -// given byte sequence before. It is based around comparing the SipHash-2-4 -// digest of data to match against. Collisions are treated as positive matches -// however, the probability of such occurences is negligible. -type replayFilter struct { - lock sync.Mutex - key [2]uint64 - filter map[uint64]*filterEntry - fifo *list.List -} - -type filterEntry struct { - firstSeen int64 - hash uint64 - element *list.Element -} - -// newReplayFilter creates a new replayFilter instance. -func newReplayFilter() (filter *replayFilter, err error) { - // Initialize the SipHash-2-4 instance with a random key. - var key [16]byte - err = csrand.Bytes(key[:]) - if err != nil { - return - } - - filter = new(replayFilter) - filter.key[0] = binary.BigEndian.Uint64(key[0:8]) - filter.key[1] = binary.BigEndian.Uint64(key[8:16]) - filter.filter = make(map[uint64]*filterEntry) - filter.fifo = list.New() - - return -} - -// testAndSet queries the filter for buf, adds it if it was not present and -// returns if it has added the entry or not. This method is threadsafe. -func (f *replayFilter) testAndSet(now int64, buf []byte) bool { - hash := siphash.Hash(f.key[0], f.key[1], buf) - - f.lock.Lock() - defer f.lock.Unlock() - - f.compactFilter(now) - - entry := f.filter[hash] - if entry != nil { - return true - } - - entry = new(filterEntry) - entry.hash = hash - entry.firstSeen = now - entry.element = f.fifo.PushBack(entry) - f.filter[hash] = entry - - return false -} - -// compactFilter purges entries that are too old to be relevant. If the filter -// is filled to maxFilterCapacity, it will force purge a single entry. This -// method is NOT threadsafe. -func (f *replayFilter) compactFilter(now int64) { - e := f.fifo.Front() - for e != nil { - entry, _ := e.Value.(*filterEntry) - - // If the filter is at max capacity, force purge at least one entry. - if f.fifo.Len() < maxFilterSize { - deltaT := now - entry.firstSeen - if deltaT < 0 { - // Aeeeeeee, the system time jumped backwards, potentially by - // a lot. This will eventually self-correct, but "eventually" - // could be a long time. As much as this sucks, jettison the - // entire filter. - f.reset() - return - } - if deltaT < 3600*2 { - // Why yes, this is 2 hours. The MAC code includes a hour - // resolution timestamp, but to deal with clock skew, it - // accepts time +- 1 hour. - break - } - } - eNext := e.Next() - delete(f.filter, entry.hash) - f.fifo.Remove(entry.element) - entry.element = nil - e = eNext - } -} - -// reset purges the entire filter. This methoid is NOT threadsafe. -func (f *replayFilter) reset() { - f.filter = make(map[uint64]*filterEntry) - f.fifo = list.New() -} - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/replay_filter_test.go b/replay_filter_test.go deleted file mode 100644 index 09337c0..0000000 --- a/replay_filter_test.go +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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 ( - "testing" -) - -func TestReplayFilter(t *testing.T) { - f, err := newReplayFilter() - if err != nil { - t.Fatal("newReplayFilter failed:", err) - } - - buf := []byte("This is a test of the Emergency Broadcast System.") - var now int64 = 3600 - - // testAndSet into empty filter, returns false (not present). - set := f.testAndSet(now, buf) - if set { - t.Fatal("testAndSet empty filter returned true") - } - - // testAndSet into filter containing entry, should return true(present). - set = f.testAndSet(now, buf) - if !set { - t.Fatal("testAndSet populated filter (replayed) returned false") - } - - buf2 := []byte("This concludes this test of the Emergency Broadcast System.") - now += 3600 * 2 - - // testAndSet with time advanced. - set = f.testAndSet(now, buf2) - if set { - t.Fatal("testAndSet populated filter, 2nd entry returned true") - } - set = f.testAndSet(now, buf2) - if !set { - t.Fatal("testAndSet populated filter, 2nd entry (replayed) returned false") - } - - // Ensure that the first entry has been removed by compact. - set = f.testAndSet(now, buf) - if set { - t.Fatal("testAndSet populated filter, compact check returned true") - } - - // Ensure that the filter gets reaped if the clock jumps backwards. - now = 0 - set = f.testAndSet(now, buf) - if set { - t.Fatal("testAndSet populated filter, backward time jump returned true") - } - if len(f.filter) != 1 { - t.Fatal("filter map has a unexpected number of entries:", len(f.filter)) - } - if f.fifo.Len() != 1 { - t.Fatal("filter fifo has a unexpected number of entries:", f.fifo.Len()) - } - - // Ensure that the entry is properly added after reaping. - set = f.testAndSet(now, buf) - if !set { - t.Fatal("testAndSet populated filter, post-backward clock jump (replayed) returned false") - } -} 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)) +} diff --git a/weighted_dist.go b/weighted_dist.go deleted file mode 100644 index 4f1f2a5..0000000 --- a/weighted_dist.go +++ /dev/null @@ -1,212 +0,0 @@ -/* - * 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 ( - "container/list" - "fmt" - "math/rand" - - "git.torproject.org/pluggable-transports/obfs4.git/csrand" - "git.torproject.org/pluggable-transports/obfs4.git/drbg" -) - -const ( - minValues = 1 - maxValues = 100 -) - -// wDist is a weighted distribution. -type wDist struct { - minValue int - maxValue int - values []int - weights []float64 - - alias []int - prob []float64 -} - -// newWDist creates a weighted distribution of values ranging from min to max -// based on a HashDrbg initialized with seed. -func newWDist(seed *drbg.Seed, min, max int) (w *wDist) { - w = &wDist{minValue: min, maxValue: max} - - if max <= min { - panic(fmt.Sprintf("wDist.Reset(): min >= max (%d, %d)", min, max)) - } - - w.reset(seed) - - return -} - -// genValues creates a slice containing a random number of random values -// that when scaled by adding minValue will fall into [min, max]. -func (w *wDist) genValues(rng *rand.Rand) { - nValues := (w.maxValue + 1) - w.minValue - values := rng.Perm(nValues) - if nValues < minValues { - nValues = minValues - } - if nValues > maxValues { - nValues = maxValues - } - nValues = rng.Intn(nValues) + 1 - w.values = values[:nValues] -} - -// genBiasedWeights generates a non-uniform weight list, similar to the -// ScrambleSuit prob_dist module. -func (w *wDist) genBiasedWeights(rng *rand.Rand) { - w.weights = make([]float64, len(w.values)) - - culmProb := 0.0 - for i := range w.values { - p := (1.0 - culmProb) * rng.Float64() - w.weights[i] = p - culmProb += p - } -} - -// genUniformWeights generates a uniform weight list. -func (w *wDist) genUniformWeights(rng *rand.Rand) { - w.weights = make([]float64, len(w.values)) - for i := range w.weights { - w.weights[i] = rng.Float64() - } -} - -// genTables calculates the alias and prob tables used for Vose's Alias method. -// Algorithm taken from http://www.keithschwarz.com/darts-dice-coins/ -func (w *wDist) genTables() { - n := len(w.weights) - var sum float64 - for _, weight := range w.weights { - sum += weight - } - - // Create arrays $Alias$ and $Prob$, each of size $n$. - alias := make([]int, n) - prob := make([]float64, n) - - // Create two worklists, $Small$ and $Large$. - small := list.New() - large := list.New() - - scaled := make([]float64, n) - for i, weight := range w.weights { - // Multiply each probability by $n$. - p_i := weight * float64(n) / sum - scaled[i] = p_i - - // For each scaled probability $p_i$: - if scaled[i] < 1.0 { - // If $p_i < 1$, add $i$ to $Small$. - small.PushBack(i) - } else { - // Otherwise ($p_i \ge 1$), add $i$ to $Large$. - large.PushBack(i) - } - } - - // While $Small$ and $Large$ are not empty: ($Large$ might be emptied first) - for small.Len() > 0 && large.Len() > 0 { - // Remove the first element from $Small$; call it $l$. - l := small.Remove(small.Front()).(int) - // Remove the first element from $Large$; call it $g$. - g := large.Remove(large.Front()).(int) - - // Set $Prob[l] = p_l$. - prob[l] = scaled[l] - // Set $Alias[l] = g$. - alias[l] = g - - // Set $p_g := (p_g + p_l) - 1$. (This is a more numerically stable option.) - scaled[g] = (scaled[g] + scaled[l]) - 1.0 - - if scaled[g] < 1.0 { - // If $p_g < 1$, add $g$ to $Small$. - small.PushBack(g) - } else { - // Otherwise ($p_g \ge 1$), add $g$ to $Large$. - large.PushBack(g) - } - } - - // While $Large$ is not empty: - for large.Len() > 0 { - // Remove the first element from $Large$; call it $g$. - g := large.Remove(large.Front()).(int) - // Set $Prob[g] = 1$. - prob[g] = 1.0 - } - - // While $Small$ is not empty: This is only possible due to numerical instability. - for small.Len() > 0 { - // Remove the first element from $Small$; call it $l$. - l := small.Remove(small.Front()).(int) - // Set $Prob[l] = 1$. - prob[l] = 1.0 - } - - w.prob = prob - w.alias = alias -} - -// reset generates a new distribution with the same min/max based on a new seed. -func (w *wDist) reset(seed *drbg.Seed) { - // Initialize the deterministic random number generator. - drbg := drbg.NewHashDrbg(seed) - rng := rand.New(drbg) - - w.genValues(rng) - //w.genBiasedWeights(rng) - w.genUniformWeights(rng) - w.genTables() -} - -// sample generates a random value according to the distribution. -func (w *wDist) sample() int { - var idx int - - // Generate a fair die roll from an $n$-sided die; call the side $i$. - i := csrand.Intn(len(w.values)) - // Flip a biased coin that comes up heads with probability $Prob[i]$. - if csrand.Float64() <= w.prob[i] { - // If the coin comes up "heads," return $i$. - idx = i - } else { - // Otherwise, return $Alias[i]$. - idx = w.alias[i] - } - - return w.minValue + w.values[idx] -} - -/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/weighted_dist_test.go b/weighted_dist_test.go deleted file mode 100644 index 16b93c4..0000000 --- a/weighted_dist_test.go +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 ( - "fmt" - "testing" - - "git.torproject.org/pluggable-transports/obfs4.git/drbg" -) - -const debug = false - -func TestWeightedDist(t *testing.T) { - seed, err := drbg.NewSeed() - if err != nil { - t.Fatal("failed to generate a DRBG seed:", err) - } - - const nrTrials = 1000000 - - hist := make([]int, 1000) - - w := newWDist(seed, 0, 999) - if debug { - // Dump a string representation of the probability table. - fmt.Println("Table:") - var sum float64 - for _, weight := range w.weights { - sum += weight - } - for i, weight := range w.weights { - p := weight / sum - if p > 0.000001 { // Filter out tiny values. - fmt.Printf(" [%d]: %f\n", w.minValue+w.values[i], p) - } - } - fmt.Println() - } - - for i := 0; i < nrTrials; i++ { - value := w.sample() - hist[value]++ - } - - if debug { - fmt.Println("Generated:") - for value, count := range hist { - if count != 0 { - p := float64(count) / float64(nrTrials) - fmt.Printf(" [%d]: %f (%d)\n", value, p, count) - } - } - } -} - -/* vim :set ts=4 sw=4 sts=4 noet : */ -- cgit v1.2.3