mirror of
https://gitlab.com/yawning/obfs4.git
synced 2024-11-07 09:20:30 +00:00
Initial import.
This commit is contained in:
commit
ef38b844f9
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
*.swp
|
||||
*~
|
||||
|
||||
obfs4-client/obfs4-client
|
||||
obfs4-server/obfs4-server
|
54
README.md
Normal file
54
README.md
Normal file
@ -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.
|
303
framing/framing.go
Normal file
303
framing/framing.go
Normal file
@ -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 : */
|
178
framing/framing_test.go
Normal file
178
framing/framing_test.go
Normal file
@ -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 : */
|
386
handshake_ntor.go
Normal file
386
handshake_ntor.go
Normal file
@ -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 : */
|
80
handshake_ntor_test.go
Normal file
80
handshake_ntor_test.go
Normal file
@ -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")
|
||||
}
|
||||
}
|
435
ntor/ntor.go
Normal file
435
ntor/ntor.go
Normal file
@ -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 : */
|
182
ntor/ntor_test.go
Normal file
182
ntor/ntor_test.go
Normal file
@ -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 : */
|
183
obfs4-client/obfs4-client.go
Normal file
183
obfs4-client/obfs4-client.go
Normal file
@ -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:
|
||||
}
|
||||
}
|
||||
}
|
225
obfs4-server/obfs4-server.go
Normal file
225
obfs4-server/obfs4-server.go
Normal file
@ -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:
|
||||
}
|
||||
}
|
||||
}
|
399
obfs4.go
Normal file
399
obfs4.go
Normal file
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user