summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYawning Angel <yawning@schwanenlied.me>2014-06-02 17:50:01 +0000
committerYawning Angel <yawning@schwanenlied.me>2014-06-02 17:50:01 +0000
commit5bdc376e2abaf5ac87816b763f5b26e314ee9536 (patch)
tree8746291873e187d7783116a2c9758bab23da5eb1
parent5cb3369e200c72aa23c3f86816cb854c35cc95cb (diff)
Change how the length obfsucation mask is derived.
Instead of using the nonce for the secret box, just use SipHash-2-4 in OFB mode instead. The IV is generated as part of the KDF. This simplifies the code a decent amount and also is better on the off chance that SipHash-2-4 does not avalanche as well as it is currently assumed. While here, also decouple the fact that *this implementation* of obfs4 uses a PRNG with 24 bytes of internal state for protocol polymorphism instead of 32 bytes (that the spec requires). THIS CHANGE BREAKS WIRE PROTCOL COMPATIBILITY.
-rw-r--r--doc/obfs4-spec.txt28
-rw-r--r--drbg/hash_drbg.go12
-rw-r--r--framing/framing.go38
-rw-r--r--obfs4.go12
-rw-r--r--obfs4proxy/obfs4proxy.go11
-rw-r--r--packet.go2
6 files changed, 66 insertions, 37 deletions
diff --git a/doc/obfs4-spec.txt b/doc/obfs4-spec.txt
index 1ac2f30..d2aa859 100644
--- a/doc/obfs4-spec.txt
+++ b/doc/obfs4-spec.txt
@@ -219,7 +219,7 @@
At the point that each side finishes the handshake, they have a 256 bit
shared secret KEY_SEED that is then extracted/expanded via the ntor KDF to
- produce the 128 bytes of keying material used to encrypt/authenticate the
+ produce the 144 bytes of keying material used to encrypt/authenticate the
data.
The keying material is used as follows:
@@ -227,10 +227,12 @@
Bytes 000:031 - Server to Client 256 bit NaCl secretbox key.
Bytes 032:047 - Server to Client 128 bit NaCl secretbox nonce prefix.
Bytes 048:063 - Server to Client 128 bit SipHash-2-4 key.
+ Bytes 064:071 - Server to Client 64 bit SipHash-2-4 OFB IV.
- Bytes 064:095 - Client to Server 256 bit NaCl secretbox key.
- Bytes 096:111 - Client to Server NaCl secretbox nonce prefix.
- Bytes 112:127 - Client to Server 128 bit SipHash-2-4 key.
+ Bytes 072:103 - Client to Server 256 bit NaCl secretbox key.
+ Bytes 104:119 - Client to Server NaCl secretbox nonce prefix.
+ Bytes 120:135 - Client to Server 128 bit SipHash-2-4 key.
+ Bytes 136:143 - Client to Server 64 bit SipHash-2-4 OFB IV.
5. Data Transfer Phase
@@ -246,12 +248,18 @@
The frame length refers to the length of the succeeding secretbox. To
avoid transmitting identifiable length fields in stream, the frame length
- is obfuscated by XORing the value with the SipHash-2-4[4] digest of the
- secretbox nonce, truncated to 2 bytes. As the nonce is deterministic,
- decoding the length is done by deriving the nonce that will be used to
- unseal the next secret box, calculating the truncated SipHash-24 digest,
- and XORing the digest with the obfuscated frame length to obtain the
- length of the secretbox.
+ is obfuscated by XORing a mask derived from SipHash-2-4 in OFB mode.
+
+ K = The SipHash-2-4 key from the KDF.
+ IV[0] = The SipHash-2-4 OFB from the KDF.
+ For each packet:
+ IV[n] = SipHash-2-4(K, IV[n-1])
+ Mask[n] = First 2 bytes of IV[n]
+ obfuscatedLength = length ^ Mask[n]
+
+ As the receiver has the SipHash-2-4 key and IV, decoding the length is done
+ via deriving the mask used to obfsucate the length and XORing the truncated
+ digest to obtain the length of the secretbox.
The payload length refers to the length of the payload portion of the frame
and does not include the padding. It is possible for the payload length to
diff --git a/drbg/hash_drbg.go b/drbg/hash_drbg.go
index 13cc188..7186fd0 100644
--- a/drbg/hash_drbg.go
+++ b/drbg/hash_drbg.go
@@ -44,10 +44,10 @@ import (
const Size = siphash.Size
// SeedLength is the length of the HashDrbg seed.
-const SeedLength = 32
+const SeedLength = 16 + Size
// Seed is the initial state for a HashDrbg. It consists of a SipHash-2-4
-// key, and 16 bytes of initial data.
+// key, and 8 bytes of initial data.
type Seed [SeedLength]byte
// Bytes returns a pointer to the raw HashDrbg seed.
@@ -71,9 +71,10 @@ func NewSeed() (seed *Seed, err error) {
return
}
-// SeedFromBytes creates a Seed from the raw bytes.
+// SeedFromBytes creates a Seed from the raw bytes, truncating to SeedLength as
+// appropriate.
func SeedFromBytes(src []byte) (seed *Seed, err error) {
- if len(src) != SeedLength {
+ if len(src) < SeedLength {
return nil, InvalidSeedLengthError(len(src))
}
@@ -83,7 +84,8 @@ func SeedFromBytes(src []byte) (seed *Seed, err error) {
return
}
-// SeedFromBase64 creates a Seed from the Base64 representation.
+// SeedFromBase64 creates a Seed from the Base64 representation, truncating to
+// SeedLength as appropriate.
func SeedFromBase64(encoded string) (seed *Seed, err error) {
var raw []byte
raw, err = base64.StdEncoding.DecodeString(encoded)
diff --git a/framing/framing.go b/framing/framing.go
index 48d12c3..c053c4c 100644
--- a/framing/framing.go
+++ b/framing/framing.go
@@ -32,6 +32,7 @@
// uint8_t[32] NaCl secretbox key
// uint8_t[16] NaCl Nonce prefix
// uint8_t[16] SipHash-2-4 key (used to obfsucate length)
+// uint8_t[8] SipHash-2-4 IV
//
// The frame format is:
// uint16_t length (obfsucated, big endian)
@@ -40,7 +41,12 @@
// uint8_t[] payload
//
// The length field is length of the NaCl secretbox XORed with the truncated
-// SipHash-2-4 digest of the nonce used to seal/unseal the current secretbox.
+// SipHash-2-4 digest ran in OFB mode.
+//
+// Initialize K, IV[0] with values from the shared secret.
+// On each packet, IV[n] = H(K, IV[n - 1])
+// mask[n] = IV[n][0:2]
+// obfsLen = length ^ mask[n]
//
// The NaCl secretbox (Poly1305/XSalsa20) nonce format is:
// uint8_t[24] prefix (Fixed)
@@ -59,14 +65,12 @@ import (
"encoding/binary"
"errors"
"fmt"
- "hash"
"io"
"code.google.com/p/go.crypto/nacl/secretbox"
- "github.com/dchest/siphash"
-
"github.com/yawning/obfs4/csrand"
+ "github.com/yawning/obfs4/drbg"
)
const (
@@ -82,7 +86,7 @@ const (
MaximumFramePayloadLength = MaximumSegmentLength - FrameOverhead
// KeyLength is the length of the Encoder/Decoder secret key.
- KeyLength = keyLength + noncePrefixLength + 16
+ KeyLength = keyLength + noncePrefixLength + drbg.SeedLength
maxFrameLength = MaximumSegmentLength - lengthLength
minFrameLength = FrameOverhead - lengthLength
@@ -146,8 +150,8 @@ func (nonce boxNonce) bytes(out *[nonceLength]byte) error {
// Encoder is a frame encoder instance.
type Encoder struct {
key [keyLength]byte
- sip hash.Hash64
nonce boxNonce
+ drbg *drbg.HashDrbg
}
// NewEncoder creates a new Encoder instance. It must be supplied a slice
@@ -160,7 +164,11 @@ func NewEncoder(key []byte) *Encoder {
encoder := new(Encoder)
copy(encoder.key[:], key[0:keyLength])
encoder.nonce.init(key[keyLength : keyLength+noncePrefixLength])
- encoder.sip = siphash.New(key[keyLength+noncePrefixLength:])
+ seed, err := drbg.SeedFromBytes(key[keyLength+noncePrefixLength:])
+ if err != nil {
+ panic(fmt.Sprintf("BUG: Failed to initialize DRBG: %s", err))
+ }
+ encoder.drbg = drbg.NewHashDrbg(seed)
return encoder
}
@@ -190,9 +198,7 @@ func (encoder *Encoder) Encode(frame, payload []byte) (n int, err error) {
// Obfuscate the length.
length := uint16(len(box) - lengthLength)
- encoder.sip.Write(nonce[:])
- lengthMask := encoder.sip.Sum(nil)
- encoder.sip.Reset()
+ lengthMask := encoder.drbg.NextBlock()
length ^= binary.BigEndian.Uint16(lengthMask)
binary.BigEndian.PutUint16(frame[:2], length)
@@ -204,7 +210,7 @@ func (encoder *Encoder) Encode(frame, payload []byte) (n int, err error) {
type Decoder struct {
key [keyLength]byte
nonce boxNonce
- sip hash.Hash64
+ drbg *drbg.HashDrbg
nextNonce [nonceLength]byte
nextLength uint16
@@ -221,7 +227,11 @@ func NewDecoder(key []byte) *Decoder {
decoder := new(Decoder)
copy(decoder.key[:], key[0:keyLength])
decoder.nonce.init(key[keyLength : keyLength+noncePrefixLength])
- decoder.sip = siphash.New(key[keyLength+noncePrefixLength:])
+ seed, err := drbg.SeedFromBytes(key[keyLength+noncePrefixLength:])
+ if err != nil {
+ panic(fmt.Sprintf("BUG: Failed to initialize DRBG: %s", err))
+ }
+ decoder.drbg = drbg.NewHashDrbg(seed)
return decoder
}
@@ -253,9 +263,7 @@ func (decoder *Decoder) Decode(data []byte, frames *bytes.Buffer) (int, error) {
// Deobfuscate the length field.
length := binary.BigEndian.Uint16(obfsLen[:])
- decoder.sip.Write(decoder.nextNonce[:])
- lengthMask := decoder.sip.Sum(nil)
- decoder.sip.Reset()
+ lengthMask := decoder.drbg.NextBlock()
length ^= binary.BigEndian.Uint16(lengthMask)
if maxFrameLength < length || minFrameLength > length {
// Per "Plaintext Recovery Attacks Against SSH" by
diff --git a/obfs4.go b/obfs4.go
index ec33fb4..247583b 100644
--- a/obfs4.go
+++ b/obfs4.go
@@ -34,6 +34,7 @@ package obfs4
import (
"bytes"
"crypto/sha256"
+ "encoding/base64"
"fmt"
"io"
"math/rand"
@@ -47,6 +48,8 @@ import (
)
const (
+ // SeedLength is the length of the obfs4 polymorphism seed.
+ SeedLength = 32
headerLength = framing.FrameOverhead + packetOverhead
connectionTimeout = time.Duration(30) * time.Second
@@ -299,7 +302,7 @@ func (c *Obfs4Conn) serverHandshake(nodeID *ntor.NodeID, keypair *ntor.Keypair)
c.state = stateEstablished
// Send the PRNG seed as the first packet.
- err = c.producePacket(&frameBuf, packetTypePrngSeed, c.listener.seed.Bytes()[:], 0)
+ err = c.producePacket(&frameBuf, packetTypePrngSeed, c.listener.rawSeed, 0)
if err != nil {
return
}
@@ -611,6 +614,7 @@ type Obfs4Listener struct {
keyPair *ntor.Keypair
nodeID *ntor.NodeID
+ rawSeed []byte
seed *drbg.Seed
iatSeed *drbg.Seed
iatObfuscation bool
@@ -716,7 +720,11 @@ func ListenObfs4(network, laddr, nodeID, privateKey, seed string, iatObfuscation
if err != nil {
return nil, err
}
- l.seed, err = drbg.SeedFromBase64(seed)
+ l.rawSeed, err = base64.StdEncoding.DecodeString(seed)
+ if err != nil {
+ return nil, err
+ }
+ l.seed, err = drbg.SeedFromBytes(l.rawSeed)
if err != nil {
return nil, err
}
diff --git a/obfs4proxy/obfs4proxy.go b/obfs4proxy/obfs4proxy.go
index b8a3f00..46e562e 100644
--- a/obfs4proxy/obfs4proxy.go
+++ b/obfs4proxy/obfs4proxy.go
@@ -46,6 +46,7 @@
package main
import (
+ "encoding/base64"
"encoding/hex"
"flag"
"fmt"
@@ -62,7 +63,7 @@ import (
"git.torproject.org/pluggable-transports/goptlib.git"
"github.com/yawning/obfs4"
- "github.com/yawning/obfs4/drbg"
+ "github.com/yawning/obfs4/csrand"
"github.com/yawning/obfs4/ntor"
)
@@ -390,15 +391,17 @@ func generateServerParams(id string) {
return
}
- seed, err := drbg.NewSeed()
+ seed := make([]byte, obfs4.SeedLength)
+ err = csrand.Bytes(seed)
if err != nil {
fmt.Println("Failed to generate DRBG seed:", err)
return
}
+ seedBase64 := base64.StdEncoding.EncodeToString(seed)
fmt.Println("Generated private-key:", keypair.Private().Base64())
fmt.Println("Generated public-key:", keypair.Public().Base64())
- fmt.Println("Generated drbg-seed:", seed.Base64())
+ fmt.Println("Generated drbg-seed:", seedBase64)
fmt.Println()
fmt.Println("Client config: ")
fmt.Printf(" Bridge obfs4 <IP Address:Port> %s node-id=%s public-key=%s\n",
@@ -406,7 +409,7 @@ func generateServerParams(id string) {
fmt.Println()
fmt.Println("Server config:")
fmt.Printf(" ServerTransportOptions obfs4 node-id=%s private-key=%s drbg-seed=%s\n",
- parsedID.Base64(), keypair.Private().Base64(), seed.Base64())
+ parsedID.Base64(), keypair.Private().Base64(), seedBase64)
}
func main() {
diff --git a/packet.go b/packet.go
index 3910604..dee5921 100644
--- a/packet.go
+++ b/packet.go
@@ -42,7 +42,7 @@ const (
packetOverhead = 2 + 1
maxPacketPayloadLength = framing.MaximumFramePayloadLength - packetOverhead
maxPacketPaddingLength = maxPacketPayloadLength
- seedPacketPayloadLength = drbg.SeedLength
+ seedPacketPayloadLength = SeedLength
consumeReadSize = framing.MaximumSegmentLength * 16
)