diff options
author | Yawning Angel <yawning@schwanenlied.me> | 2014-05-09 10:23:58 +0000 |
---|---|---|
committer | Yawning Angel <yawning@schwanenlied.me> | 2014-05-09 10:23:58 +0000 |
commit | ef38b844f9989dff25f553b583aeafc411dd100e (patch) | |
tree | 4162a60fd98f25c8f378523acd13e9adc319df52 |
Initial import.
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | README.md | 54 | ||||
-rw-r--r-- | framing/framing.go | 303 | ||||
-rw-r--r-- | framing/framing_test.go | 178 | ||||
-rw-r--r-- | handshake_ntor.go | 386 | ||||
-rw-r--r-- | handshake_ntor_test.go | 80 | ||||
-rw-r--r-- | ntor/ntor.go | 435 | ||||
-rw-r--r-- | ntor/ntor_test.go | 182 | ||||
-rw-r--r-- | obfs4-client/obfs4-client.go | 183 | ||||
-rw-r--r-- | obfs4-server/obfs4-server.go | 225 | ||||
-rw-r--r-- | obfs4.go | 399 |
11 files changed, 2430 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5eac799 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.swp +*~ + +obfs4-client/obfs4-client +obfs4-server/obfs4-server diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf1d69c --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +## obfs4 - The fourbfuscator +#### 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 +concepts from Philipp Winter's ScrambleSuit protocol. The obfs naming was +chosen primarily because it was shorter, in terms of protocol ancestery obfs4 +is much closer to ScrambleSuit than obfs2/obfs3. + +The notable differences between ScrambleSuit and obfs4: + + * The handshake always does a full key exchange (no such thing as a Session + Ticket Handshake). (TODO: Reconsider this.) + * The handshake uses the Tor Project's ntor handshake with public keys + obfuscated via the Elligator mapping. + * The link layer encryption uses NaCl secret boxes (Poly1305/Salsa20). + +### Why not extend ScrambleSuit? + +It's my protocol and I'll obfuscate if I want to. + +Since a lot of the changes are to the handshaking process, it didn't make sense +to extend ScrambleSuit as writing a server implementation that supported both +handshake variants without being obscenely slow is non-trivial. + +### TODO + + * Packet length obfuscation. + * (Maybe) Make it resilient to transient connection loss. + * (Maybe) Use IP_MTU/TCP_MAXSEG to tweak frame size. + * Write a detailed protocol spec. + * Code cleanups. + * Write more unit tests. + +### 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. + * Adam Langley for his Elligator implementation. + * Philipp Winter for the ScrambleSuit protocol which provided much of the + design. diff --git a/framing/framing.go b/framing/framing.go new file mode 100644 index 0000000..d319207 --- /dev/null +++ b/framing/framing.go @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +// +// Package framing implements the obfs4 link framing and cryptography. +// +// The Encoder/Decoder shared secret format is: +// uint8_t[32] NaCl SecretBox key +// uint8_t[24] NaCl Nonce prefix +// uint8_t[16] SipHash-2-4 key (used to obfsucate length) +// +// The frame format is: +// uint16_t length (obfsucated, big endian) +// NaCl SecretBox (Poly1305/Salsa20) 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 of the previous SecretBox concatenated with the nonce +// used to seal the current SecretBox. +// +// The NaCl SecretBox (Poly1305/Salsa20) 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" + "hash" + + "code.google.com/p/go.crypto/nacl/secretbox" + + "github.com/dchest/siphash" +) + +const ( + // MaximumSegmentLength is the length of the largest possible segment + // including overhead. + MaximumSegmentLength = 1500 - 40 + + // 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 + 16 + + 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)) +} + +// InvalidFrameLengthError is the error returned when Decoder.Decode() +// rejects the payload length. +type InvalidFrameLengthError int + +func (e InvalidFrameLengthError) Error() string { + return fmt.Sprintf("framing: Invalid frame 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 + sip hash.Hash64 + nonce boxNonce +} + +// 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]) + encoder.sip = siphash.New(key[keyLength+noncePrefixLength:]) + + return encoder +} + +// Encode encodes a single frame worth of payload and returns the encoded +// length and the resulting frame. InvalidPayloadLengthError is recoverable, +// all other errors MUST be treated as fatal and the session aborted. +func (encoder *Encoder) Encode(payload []byte) (int, []byte, error) { + payloadLen := len(payload) + if MaximumFramePayloadLength < payloadLen { + return 0, nil, InvalidPayloadLengthError(payloadLen) + } + + // Generate a new nonce. + var nonce [nonceLength]byte + err := encoder.nonce.bytes(&nonce) + if err != nil { + return 0, nil, err + } + encoder.nonce.counter++ + + // Encrypt and MAC payload. + var box []byte + box = secretbox.Seal(nil, payload, &nonce, &encoder.key) + + // Obfuscate the length. + length := uint16(len(box)) + encoder.sip.Write(nonce[:]) + lengthMask := encoder.sip.Sum(nil) + encoder.sip.Reset() + length ^= binary.BigEndian.Uint16(lengthMask) + var obfsLen [lengthLength]byte + binary.BigEndian.PutUint16(obfsLen[:], length) + + // Prepare the next obfsucator. + encoder.sip.Write(box) + + // Return the frame. + return payloadLen + FrameOverhead, append(obfsLen[:], box...), nil +} + +// Decoder is a frame decoder instance. +type Decoder struct { + key [keyLength]byte + nonce boxNonce + sip hash.Hash64 + + nextNonce [nonceLength]byte + nextLength uint16 +} + +// 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]) + decoder.sip = siphash.New(key[keyLength+noncePrefixLength:]) + + return decoder +} + +// Decode decodes a stream of data and returns the length and decoded frame if +// any. ErrAgain is a temporary failure, all other errors MUST be treated as +// fatal and the session aborted. +func (decoder *Decoder) Decode(data *bytes.Buffer) (int, []byte, 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 > data.Len() { + return 0, nil, ErrAgain + } + + // Remove the length field from the buffer. + var obfsLen [lengthLength]byte + n, err := data.Read(obfsLen[:]) + if err != nil { + return 0, nil, err + } else if n != lengthLength { + // Should *NEVER* happen, since at least 2 bytes exist. + panic(fmt.Sprintf("BUG: Failed to read obfuscated length: %d", n)) + } + + // Derive the nonce the peer used. + err = decoder.nonce.bytes(&decoder.nextNonce) + if err != nil { + return 0, nil, err + } + + // Deobfuscate the length field. + length := binary.BigEndian.Uint16(obfsLen[:]) + decoder.sip.Write(decoder.nextNonce[:]) + lengthMask := decoder.sip.Sum(nil) + decoder.sip.Reset() + length ^= binary.BigEndian.Uint16(lengthMask) + if maxFrameLength < length || minFrameLength > length { + return 0, nil, InvalidFrameLengthError(length) + } + decoder.nextLength = length + } + + if int(decoder.nextLength) > data.Len() { + return 0, nil, ErrAgain + } + + // Unseal the frame. + box := make([]byte, decoder.nextLength) + n, err := data.Read(box) + if err != nil { + return 0, nil, err + } else if n != int(decoder.nextLength) { + // Should *NEVER* happen, since at least 2 bytes exist. + panic(fmt.Sprintf("BUG: Failed to read secretbox, got %d, should have %d", n, + decoder.nextLength)) + } + out, ok := secretbox.Open(nil, box, &decoder.nextNonce, &decoder.key) + if !ok { + return 0, nil, ErrTagMismatch + } + decoder.sip.Write(box) + + // Clean up and prepare for the next frame. + decoder.nextLength = 0 + decoder.nonce.counter++ + + return len(out), out, nil +} + +/* vim :set ts=4 sw=4 sts=4 noet : */ diff --git a/framing/framing_test.go b/framing/framing_test.go new file mode 100644 index 0000000..221ea5e --- /dev/null +++ b/framing/framing_test.go @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package 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++ { + n, frame, err := encoder.Encode(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) + } + if len(frame) != n { + t.Fatalf("Encoded frame length/rval mismatch: %d != %d", + len(frame), n) + } + } +} + +// TestEncoder_Encode_Oversize tests oversized frame rejection. +func TestEncoder_Encode_Oversize(t *testing.T) { + encoder := newEncoder(t) + + buf := make([]byte, MaximumFramePayloadLength+1) + _, _ = rand.Read(buf) // YOLO + _, _, err := encoder.Encode(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) + + buf := make([]byte, MaximumFramePayloadLength) + _, _ = rand.Read(buf) // YOLO + for i := 0; i <= MaximumFramePayloadLength; i++ { + encLen, frame, err := encoder.Encode(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) + } + if len(frame) != encLen { + t.Fatalf("Encoded frame length/rval mismatch: %d != %d", + len(frame), encLen) + } + + decLen, decoded, err := decoder.Decode(bytes.NewBuffer(frame)) + 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 len(decoded) != i { + t.Fatalf("Encoded frame length/rval mismatch: %d != %d", + len(decoded), i) + + } + + if 0 != bytes.Compare(decoded, buf[0: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 + 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, frame, err := encoder.Encode(chopBuf[:n]) + transfered += len(frame) - 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 new file mode 100644 index 0000000..44680aa --- /dev/null +++ b/handshake_ntor.go @@ -0,0 +1,386 @@ +/* + * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package obfs4 + +import ( + "bytes" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "hash" + "math/big" + "strconv" + "time" + + "github.com/yawning/obfs4/framing" + "github.com/yawning/obfs4/ntor" +) + +const ( + clientMinPadLength = serverMinHandshakeLength - clientMinHandshakeLength + clientMaxPadLength = framing.MaximumSegmentLength - clientMinHandshakeLength + clientMinHandshakeLength = ntor.RepresentativeLength + markLength + macLength + clientMaxHandshakeLength = framing.MaximumSegmentLength + + serverMinPadLength = 0 + serverMaxPadLength = framing.MaximumSegmentLength - serverMinHandshakeLength + serverMinHandshakeLength = ntor.RepresentativeLength + ntor.AuthLength + + markLength + macLength + serverMaxHandshakeLength = framing.MaximumSegmentLength + + markLength = sha256.Size + macLength = sha256.Size +) + +var ErrMarkNotFoundYet = errors.New("handshake: M_[C,S] not found yet") +var ErrInvalidHandshake = errors.New("handshake: Failed to find M_[C,S]") +var ErrNtorFailed = errors.New("handshake: ntor handshake failure") + +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)) +} + +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 + + mac hash.Hash + + serverRepresentative *ntor.Representative + serverAuth *ntor.Auth + serverMark []byte +} + +func newClientHandshake(nodeID *ntor.NodeID, serverIdentity *ntor.PublicKey) (*clientHandshake, error) { + var err error + + hs := new(clientHandshake) + hs.keypair, err = ntor.NewKeypair(true) + if err != nil { + return nil, err + } + hs.nodeID = nodeID + hs.serverIdentity = serverIdentity + hs.mac = hmac.New(sha256.New, hs.serverIdentity.Bytes()[:]) + + return hs, nil +} + +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) + + // 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 [0,clientMaxPadLength] bytes of random padding. + // * M_C is HMAC-SHA256(serverIdentity, X) + // * MAC is HMAC-SHA256(serverIdentity, X .... E) + // * E is the string representation of the number of hours since the UNIX + // epoch. + + // Generate the padding + pad, err := makePad(clientMinPadLength, clientMaxPadLength) + 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)) + + 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) + } + + // Attempt to find the mark + MAC. + pos := findMark(hs.serverMark, resp, + ntor.RepresentativeLength+ntor.AuthLength, serverMaxHandshakeLength) + if pos == -1 { + if len(resp) >= serverMaxHandshakeLength { + 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) + 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 + + mac hash.Hash + + clientRepresentative *ntor.Representative + clientMark []byte +} + +func newServerHandshake(nodeID *ntor.NodeID, serverIdentity *ntor.Keypair) *serverHandshake { + hs := new(serverHandshake) + hs.nodeID = nodeID + hs.serverIdentity = serverIdentity + hs.mac = hmac.New(sha256.New, hs.serverIdentity.Public().Bytes()[:]) + + return hs +} + +func (hs *serverHandshake) parseClientHandshake(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) + } + + // Attempt to find the mark + MAC. + pos := findMark(hs.clientMark, resp, ntor.RepresentativeLength, + serverMaxHandshakeLength) + if pos == -1 { + if len(resp) >= clientMaxHandshakeLength { + 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) + macRx := resp[pos+markLength : pos+markLength+macLength] + if hmac.Equal(macCmp, macRx) { + macFound = true + hs.epochHour = epochHour + + // In theory, we should always evaluate all 3 MACs, but at this + // point we are reasonably confident that the client knows the + // correct NodeID/Public key, and if this fails, we just ignore the + // client for a random interval and drop the connection anyway. + break + } + } + 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 + } + + // At this point the client knows that we exist, so do the keypair + // generation and complete our side of the handshake. + var err error + hs.keypair, err = ntor.NewKeypair(true) + if err != nil { + return nil, err + } + + 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) + + // 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 [0,serverMaxPadLength] bytes of random padding. + // * M_S is HMAC-SHA256(serverIdentity, Y) + // * MAC is HMAC-SHA256(serverIdentity, Y .... E) + // * E is the string representation of the number of hours since the UNIX + // epoch. + + // Generate the padding + pad, err := makePad(serverMinPadLength, serverMaxPadLength) + 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)) + + return buf.Bytes(), nil +} + +// getEpochHour returns the number of hours since the UNIX epoch. +func getEpochHour() int64 { + return time.Now().Unix() / 3600 +} + +func findMark(mark, buf []byte, startPos, maxPos int) int { + endPos := len(buf) + if endPos > maxPos { + endPos = maxPos + } + + // XXX: bytes.Index() uses a naive search, which kind of sucks. + pos := bytes.Index(buf[startPos:endPos], mark) + if pos == -1 { + return -1 + } + + // Return the index relative to the start of the slice. + return pos + startPos +} + +func makePad(min, max int64) ([]byte, error) { + if max < min { + panic(fmt.Sprintf("makePad: min > max (%d, %d)", min, max)) + } + + padRange := int64((max + 1) - min) + padLen, err := rand.Int(rand.Reader, big.NewInt(padRange)) + if err != nil { + return nil, err + } + pad := make([]byte, padLen.Int64()+min) + _, err = rand.Read(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 new file mode 100644 index 0000000..41780e9 --- /dev/null +++ b/handshake_ntor_test.go @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package obfs4 + +import ( + "bytes" + "testing" + + "github.com/yawning/obfs4/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) + + // Intialize the client and server handshake states + clientHs, err := newClientHandshake(nodeID, idKeypair.Public()) + if err != nil { + t.Fatal("newClientHandshake failed:", err) + } + serverHs := newServerHandshake(nodeID, idKeypair) + + // Generate what the client will send to the server. + cToS, err := clientHs.generateHandshake() + if err != nil { + t.Fatal("clientHandshake.generateHandshake() failed", err) + } + + // Parse the client handshake message. + serverSeed, err := serverHs.parseClientHandshake(cToS) + if err != nil { + t.Fatal("serverHandshake.parseClientHandshake() failed", err) + } + + // Genrate what the server will send to the client. + sToC, err := serverHs.generateHandshake() + if err != nil { + t.Fatal("serverHandshake.generateHandshake() failed", err) + } + + // Parse the server handshake message. + n, clientSeed, err := clientHs.parseServerHandshake(sToC) + if err != nil { + t.Fatal("clientHandshake.parseServerHandshake() failed", err) + } + if n != len(sToC) { + t.Fatalf("clientHandshake.parseServerHandshake() has bytes remaining: %d", n) + } + + // Ensure the derived shared secret is the same. + if 0 != bytes.Compare(clientSeed, serverSeed) { + t.Fatalf("client/server seed mismatch") + } +} diff --git a/ntor/ntor.go b/ntor/ntor.go new file mode 100644 index 0000000..da6dae4 --- /dev/null +++ b/ntor/ntor.go @@ -0,0 +1,435 @@ +/* + * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +// +// Package 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/rand" + "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" +) + +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) +} + +// 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 := rand.Read(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 + } +} + +// LoadKeypair takes an existing Curve25519 private key from a buffer and +// creates a Keypair including the public key. + +// 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) (bool, *KeySeed, *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) (bool, *KeySeed, *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 new file mode 100644 index 0000000..9d7c687 --- /dev/null +++ b/ntor/ntor_test.go @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package 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-client/obfs4-client.go b/obfs4-client/obfs4-client.go new file mode 100644 index 0000000..077fb85 --- /dev/null +++ b/obfs4-client/obfs4-client.go @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * This file is based off goptlib's dummy-client.go file. + */ + +// obfs4 pluggable transport client. Works only as a managed proxy. +// +// Usage (in torrc): +// UseBridges 1 +// Bridge obfs4 X.X.X.X:YYYY public-key=<Base64 Bridge public key> node-id=<Base64 Node ID> +// ClientTransportPlugin obfs4 exec obfs4-client +// +// Becuase the pluggable transport requires arguments, using obfs4-client +// requires tor 0.2.5.x. +package main + +import ( + "io" + "net" + "os" + "os/signal" + "sync" + "syscall" + + "github.com/yawning/obfs4" +) + +import "git.torproject.org/pluggable-transports/goptlib.git" + +var ptInfo pt.ClientInfo + +// When a connection handler starts, +1 is written to this channel; when it +// ends, -1 is written. +var handlerChan = make(chan int) + +func copyLoop(a, b net.Conn) { + var wg sync.WaitGroup + wg.Add(2) + + // TODO: Log errors. + go func() { + io.Copy(b, a) + wg.Done() + }() + go func() { + io.Copy(a, b) + wg.Done() + }() + + wg.Wait() +} + +func handler(conn *pt.SocksConn) error { + // Extract the peer's node ID and public key. + nodeID, ok := conn.Req.Args.Get("node-id") + if !ok { + // TODO: Log something here. + conn.Reject() + } + publicKey, ok := conn.Req.Args.Get("public-key") + if !ok { + // TODO: Log something here. + conn.Reject() + } + + handlerChan <- 1 + defer func() { + handlerChan <- -1 + }() + + defer conn.Close() + remote, err := obfs4.Dial("tcp", conn.Req.Target, nodeID, publicKey) + if err != nil { + conn.Reject() + return err + } + defer remote.Close() + err = conn.Grant(remote.RemoteAddr().(*net.TCPAddr)) + if err != nil { + return err + } + + copyLoop(conn, remote) + + return nil +} + +func acceptLoop(ln *pt.SocksListener) error { + defer ln.Close() + for { + conn, err := ln.AcceptSocks() + if err != nil { + if e, ok := err.(net.Error); ok && !e.Temporary() { + return err + } + continue + } + go handler(conn) + } +} + +func main() { + var err error + + ptInfo, err = pt.ClientSetup([]string{"obfs4"}) + if err != nil { + os.Exit(1) + } + + listeners := make([]net.Listener, 0) + for _, methodName := range ptInfo.MethodNames { + switch methodName { + case "obfs4": + ln, err := pt.ListenSocks("tcp", "127.0.0.1:0") + if err != nil { + pt.CmethodError(methodName, err.Error()) + break + } + go acceptLoop(ln) + pt.Cmethod(methodName, ln.Version(), ln.Addr()) + listeners = append(listeners, ln) + default: + pt.CmethodError(methodName, "no such method") + } + } + pt.CmethodsDone() + + var numHandlers int = 0 + var sig os.Signal + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + // wait for first signal + sig = nil + for sig == nil { + select { + case n := <-handlerChan: + numHandlers += n + case sig = <-sigChan: + } + } + for _, ln := range listeners { + ln.Close() + } + + if sig == syscall.SIGTERM { + return + } + + // wait for second signal or no more handlers + sig = nil + for sig == nil && numHandlers != 0 { + select { + case n := <-handlerChan: + numHandlers += n + case sig = <-sigChan: + } + } +} diff --git a/obfs4-server/obfs4-server.go b/obfs4-server/obfs4-server.go new file mode 100644 index 0000000..aac2351 --- /dev/null +++ b/obfs4-server/obfs4-server.go @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * This file is based off goptlib's dummy-server.go file. + */ + +// obfs4 pluggable transport server. Works only as a managed proxy. +// +// Usage (in torrc): +// BridgeRelay 1 +// ORPort 9001 +// ExtORPort 6669 +// ServerTransportPlugin obfs4 exec obfs4-server +// ServerTransportOptions obfs4 private-key=<Base64 Bridge private key> node-id=<Base64 Node ID> +// +// Becuase the pluggable transport requires arguments, using obfs4-server +// requires tor 0.2.5.x. +package main + +import ( + "encoding/hex" + "flag" + "fmt" + "io" + "net" + "os" + "os/signal" + "sync" + "syscall" + + "github.com/yawning/obfs4" + "github.com/yawning/obfs4/ntor" +) + +import "git.torproject.org/pluggable-transports/goptlib.git" + +var ptInfo pt.ServerInfo + +// When a connection handler starts, +1 is written to this channel; when it +// ends, -1 is written. +var handlerChan = make(chan int) + +func copyLoop(a, b net.Conn) { + var wg sync.WaitGroup + wg.Add(2) + + go func() { + io.Copy(b, a) + wg.Done() + }() + go func() { + io.Copy(a, b) + wg.Done() + }() + + wg.Wait() +} + +func handler(conn net.Conn) error { + defer conn.Close() + + handlerChan <- 1 + defer func() { + handlerChan <- -1 + }() + + or, err := pt.DialOr(&ptInfo, conn.RemoteAddr().String(), "obfs4") + if err != nil { + return err + } + defer or.Close() + + copyLoop(conn, or) + + return nil +} + +func acceptLoop(ln net.Listener) error { + defer ln.Close() + for { + conn, err := ln.Accept() + if err != nil { + if e, ok := err.(net.Error); ok && !e.Temporary() { + return err + } + continue + } + go handler(conn) + } +} + +func generateParams(id string) { + rawID, err := hex.DecodeString(id) + if err != nil { + fmt.Println("Failed to hex decode 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 + } + + fmt.Println("Generated private-key:", keypair.Private().Base64()) + fmt.Println("Generated public-key:", keypair.Public().Base64()) +} + +func main() { + var err error + + // Some command line args. + genParams := flag.String("gen", "", "Generate params given a Node ID.") + flag.Parse() + if *genParams != "" { + generateParams(*genParams) + os.Exit(0) + } + + // Ok, guess we're in PT land. + ptInfo, err = pt.ServerSetup([]string{"obfs4"}) + if err != nil { + os.Exit(1) + } + + listeners := make([]net.Listener, 0) + for _, bindaddr := range ptInfo.Bindaddrs { + switch bindaddr.MethodName { + case "obfs4": + // Handle the mandetory arguments. + privateKey, ok := bindaddr.Options.Get("private-key") + if !ok { + pt.SmethodError(bindaddr.MethodName, "need a private-key option") + break + } + nodeID, ok := bindaddr.Options.Get("node-id") + if !ok { + pt.SmethodError(bindaddr.MethodName, "need a node-id option") + break + } + + ln, err := obfs4.Listen("tcp", bindaddr.Addr.String(), nodeID, + privateKey) + if err != nil { + pt.SmethodError(bindaddr.MethodName, err.Error()) + break + } + + oLn, _ := ln.(*obfs4.Obfs4Listener) + args := pt.Args{} + args.Add("node-id", nodeID) + args.Add("public-key", oLn.PublicKey()) + go acceptLoop(ln) + pt.SmethodArgs(bindaddr.MethodName, ln.Addr(), args) + // TODO: Maybe log the args? + listeners = append(listeners, ln) + default: + pt.SmethodError(bindaddr.MethodName, "no such method") + } + } + pt.SmethodsDone() + + var numHandlers int = 0 + var sig os.Signal + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + // wait for first signal + sig = nil + for sig == nil { + select { + case n := <-handlerChan: + numHandlers += n + case sig = <-sigChan: + } + } + for _, ln := range listeners { + ln.Close() + } + + if sig == syscall.SIGTERM { + return + } + + // wait for second signal or no more handlers + sig = nil + for sig == nil && numHandlers != 0 { + select { + case n := <-handlerChan: + numHandlers += n + case sig = <-sigChan: + } + } +} diff --git a/obfs4.go b/obfs4.go new file mode 100644 index 0000000..afb0716 --- /dev/null +++ b/obfs4.go @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2014, Yawning Angel <yawning at torproject dot org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +// Package obfs4 implements the obfs4 protocol. +package obfs4 + +import ( + "bytes" + "net" + "syscall" + "time" + + "github.com/yawning/obfs4/framing" + "github.com/yawning/obfs4/ntor" +) + +const ( + defaultReadSize = framing.MaximumSegmentLength +) + +// Obfs4Conn is the implementation of the net.Conn interface for obfs4 +// connections. +type Obfs4Conn struct { + conn net.Conn + + encoder *framing.Encoder + decoder *framing.Decoder + + receiveBuffer bytes.Buffer + receiveDecodedBuffer bytes.Buffer + + isOk bool +} + +func (c *Obfs4Conn) clientHandshake(nodeID *ntor.NodeID, publicKey *ntor.PublicKey) error { + // Generate/send the client handshake. + hs, err := newClientHandshake(nodeID, publicKey) + if err != nil { + return err + } + blob, err := hs.generateHandshake() + if err != nil { + return err + } + _, err = c.conn.Write(blob) + if err != nil { + return err + } + + // XXX: Set the response timer. + + // Consume the server handshake. + hsBuf := make([]byte, serverMaxHandshakeLength) + for { + n, err := c.conn.Read(hsBuf) + if err != nil { + return err + } + c.receiveBuffer.Write(hsBuf[:n]) + + n, seed, err := hs.parseServerHandshake(c.receiveBuffer.Bytes()) + if err == ErrMarkNotFoundYet { + continue + } else if err != nil { + return err + } + _ = c.receiveBuffer.Next(n) + + // 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:]) + + // XXX: Kill the response timer. + c.isOk = true + + return nil + } +} + +func (c *Obfs4Conn) serverHandshake(nodeID *ntor.NodeID, keypair *ntor.Keypair) error { + hs := newServerHandshake(nodeID, keypair) + + // XXX: Set the request timer. + + // Consume the client handshake. + hsBuf := make([]byte, clientMaxHandshakeLength) + for { + n, err := c.conn.Read(hsBuf) + if err != nil { + return err + } + c.receiveBuffer.Write(hsBuf[:n]) + + seed, err := hs.parseClientHandshake(c.receiveBuffer.Bytes()) + if err == ErrMarkNotFoundYet { + continue + } else if err != nil { + return err + } + c.receiveBuffer.Reset() + + // 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]) + + // XXX: Kill the request timer. + + break + } + + // Generate/send the response. + blob, err := hs.generateHandshake() + if err != nil { + return err + } + _, err = c.conn.Write(blob) + if err != nil { + return err + } + + c.isOk = true + + return nil +} + +func (c *Obfs4Conn) Read(b []byte) (int, error) { + if !c.isOk { + return 0, syscall.EINVAL + } + + if c.receiveDecodedBuffer.Len() > 0 { + n, err := c.receiveDecodedBuffer.Read(b) + return n, err + } + + // Consume and decode frames off the network. + buf := make([]byte, defaultReadSize) + for c.receiveDecodedBuffer.Len() == 0 { + n, err := c.conn.Read(buf) + if err != nil { + return 0, err + } + c.receiveBuffer.Write(buf[:n]) + + // Decode the data just read. + for c.receiveBuffer.Len() > 0 { + _, frame, err := c.decoder.Decode(&c.receiveBuffer) + if err == framing.ErrAgain { + break + } else if err != nil { + // Any non-timeout frame decoder errors are fatal. + if neterr, ok := err.(net.Error); ok && !neterr.Timeout() { + c.isOk = false + } + return 0, err + } + + // TODO: Support more than raw payload directly in NaCl boxes. + + c.receiveDecodedBuffer.Write(frame) + } + } + + n, err := c.receiveDecodedBuffer.Read(b) + return n, err +} + +func (c *Obfs4Conn) Write(b []byte) (int, error) { + chopBuf := bytes.NewBuffer(b) + buf := make([]byte, framing.MaximumFramePayloadLength) + nSent := 0 + var frameBuf bytes.Buffer + + for chopBuf.Len() > 0 { + // TODO: Support randomly padding frames. + + // Send maximum sized frames. + n, err := chopBuf.Read(buf) + if err != nil { + return nSent, err + } else if n == 0 { + panic("Write(), chopping lenght was 0") + } + + // Encode the frame. + _, frame, err := c.encoder.Encode(buf[:n]) + if err != nil { + c.isOk = false + return nSent, err + } + + _, err = frameBuf.Write(frame) + if err != nil { + c.isOk = false + return nSent, err + } + + nSent += n + } + + // Send the frame. + _, err := c.conn.Write(frameBuf.Bytes()) + if err != nil { + // Non-timeout write errors as fatal. + if neterr, ok := err.(net.Error); ok && !neterr.Timeout() { + c.isOk = false + } + return nSent, err + } + + return nSent, nil +} + +func (c *Obfs4Conn) Close() error { + if c.conn == nil { + return syscall.EINVAL + } + + return c.conn.Close() +} + +func (c *Obfs4Conn) LocalAddr() net.Addr { + if !c.isOk { + return nil + } + + return c.conn.LocalAddr() +} + +func (c *Obfs4Conn) RemoteAddr() net.Addr { + if !c.isOk { + return nil + } + + return c.conn.RemoteAddr() +} + +func (c *Obfs4Conn) SetDeadline(t time.Time) error { + if !c.isOk { + return syscall.EINVAL + } + + return c.conn.SetDeadline(t) +} + +func (c *Obfs4Conn) SetReadDeadline(t time.Time) error { + if !c.isOk { + return syscall.EINVAL + } + + return c.conn.SetReadDeadline(t) +} + +func (c *Obfs4Conn) SetWriteDeadline(t time.Time) error { + if !c.isOk { + return syscall.EINVAL + } + + return c.conn.SetWriteDeadline(t) +} + +func Dial(network, address, nodeID, publicKey string) (net.Conn, 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 + } + + // Connect to the peer. + c := new(Obfs4Conn) + c.conn, err = net.Dial(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 a obfs4 network listener. Clients should use variables of +// type Listener instead of assuming obfs4. +type Obfs4Listener struct { + listener net.Listener + + keyPair *ntor.Keypair + nodeID *ntor.NodeID +} + +type ListenerError struct { + err error +} + +func (e *ListenerError) Error() string { + return e.err.Error() +} + +func (e *ListenerError) Temporary() bool { + return true +} + +func (e *ListenerError) Timeout() bool { + return false +} + +func (l *Obfs4Listener) Accept() (net.Conn, error) { + // Accept a connection. + c, err := l.listener.Accept() + if err != nil { + return nil, err + } + + // Allocate the obfs4 connection state. + cObfs := new(Obfs4Conn) + cObfs.conn = c + + // Complete the handshake. + err = cObfs.serverHandshake(l.nodeID, l.keyPair) + if err != nil { + // XXX: Close after a delay. + c.Close() + return nil, &ListenerError{err} + } + + return cObfs, nil +} + +func (l *Obfs4Listener) Close() error { + return l.listener.Close() +} + +func (l *Obfs4Listener) Addr() net.Addr { + return l.listener.Addr() +} + +func (l *Obfs4Listener) PublicKey() string { + if l.keyPair == nil { + return "" + } + return l.keyPair.Public().Base64() +} + +func Listen(network, laddr, nodeID, privateKey string) (net.Listener, 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 + } + + // Start up the listener. + l.listener, err = net.Listen(network, laddr) + if err != nil { + return nil, err + } + + return l, nil +} |