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.
merge-requests/3/head
Yawning Angel 10 years ago
parent 5cb3369e20
commit 5bdc376e2a

@ -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

@ -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)

@ -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

@ -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
}

@ -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() {

@ -42,7 +42,7 @@ const (
packetOverhead = 2 + 1
maxPacketPayloadLength = framing.MaximumFramePayloadLength - packetOverhead
maxPacketPaddingLength = maxPacketPayloadLength
seedPacketPayloadLength = drbg.SeedLength
seedPacketPayloadLength = SeedLength
consumeReadSize = framing.MaximumSegmentLength * 16
)

Loading…
Cancel
Save