summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYawning Angel <yawning@schwanenlied.me>2014-05-09 10:23:58 +0000
committerYawning Angel <yawning@schwanenlied.me>2014-05-09 10:23:58 +0000
commitef38b844f9989dff25f553b583aeafc411dd100e (patch)
tree4162a60fd98f25c8f378523acd13e9adc319df52
Initial import.
-rw-r--r--.gitignore5
-rw-r--r--README.md54
-rw-r--r--framing/framing.go303
-rw-r--r--framing/framing_test.go178
-rw-r--r--handshake_ntor.go386
-rw-r--r--handshake_ntor_test.go80
-rw-r--r--ntor/ntor.go435
-rw-r--r--ntor/ntor_test.go182
-rw-r--r--obfs4-client/obfs4-client.go183
-rw-r--r--obfs4-server/obfs4-server.go225
-rw-r--r--obfs4.go399
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
+}