Skip to content

Commit c3933dd

Browse files
authored
Add Connection Tag Obfuscation Options (#169)
* add optional methods to obfuscate session hmacID using ECDHE+AES * forgot some relevant changes * forgot to lint * randomize two bits instead of one in elligator encoding
1 parent 2348be1 commit c3933dd

File tree

3 files changed

+337
-5
lines changed

3 files changed

+337
-5
lines changed
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
package transports
2+
3+
import (
4+
"bytes"
5+
"crypto/aes"
6+
"crypto/cipher"
7+
"crypto/rand"
8+
"crypto/sha256"
9+
"errors"
10+
"fmt"
11+
"strconv"
12+
13+
"github.com/refraction-networking/gotapdance/ed25519/extra25519"
14+
"golang.org/x/crypto/curve25519"
15+
)
16+
17+
// Obfuscator provides an interface for obfuscating the tags that are sent by transports in order to
18+
// indicate their knowledge of the shared secret to the station.
19+
type Obfuscator interface {
20+
// Take the plain text and perform an obfuscation to make it distinguishable to the station
21+
Obfuscate(plaintext []byte, stationPubkey []byte) ([]byte, error)
22+
23+
// Take a cipher text and de-obfuscate to make it usable by the station
24+
TryReveal(cipherText []byte, privateKey [32]byte) ([]byte, error)
25+
}
26+
27+
// GCMObfuscator implements the Obfuscator interface using ECDHE and AES GCM. Prevents tag re-use.
28+
type GCMObfuscator struct{}
29+
30+
// TryReveal for GCMObfuscator expects a ciphertext object where the first 32 bytes is an elligator
31+
// encoded public key with which the server can derive an ECDHE shared secret. This secret is then
32+
// used to decrypt and authenticate the remainder of the plaintext using AES GCM.
33+
func (GCMObfuscator) TryReveal(ciphertext []byte, privateKey [32]byte) ([]byte, error) {
34+
if len(ciphertext) < 48 {
35+
return nil, ErrPublicKeyLen
36+
}
37+
38+
var representative, clientPubkey [32]byte
39+
copy(representative[:], ciphertext[:32])
40+
representative[31] &= 0x3F
41+
extra25519.RepresentativeToPublicKey(&clientPubkey, &representative)
42+
43+
sharedSecret, err := curve25519.X25519(privateKey[:], clientPubkey[:])
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
stationPubkeyHash := sha256.Sum256(sharedSecret[:])
49+
aesKey := stationPubkeyHash[:16]
50+
aesIvTag := stationPubkeyHash[16:28]
51+
52+
block, err := aes.NewCipher(aesKey)
53+
if err != nil {
54+
return nil, err
55+
}
56+
// return block.Decrypt(nil, aesIvTag, cipherText[32:])
57+
58+
aesgcm, err := cipher.NewGCM(block)
59+
if err != nil {
60+
return nil, err
61+
}
62+
63+
return aesgcm.Open(nil, aesIvTag, ciphertext[32:], nil)
64+
}
65+
66+
// Obfuscate for GCMObfuscator derives a shared key using ECDHE an then encrypts the plaintext under
67+
// that key using AES GCM. The elligator representative for the clients public key is prepended
68+
// to the returned byte array. This means that the result length will likely be:
69+
//
70+
// 32 + len(plaintext) + 16
71+
//
72+
// [elligator encoded client Pub][Ciphertext + Auth tag]
73+
func (GCMObfuscator) Obfuscate(plainText []byte, stationPubkey []byte) ([]byte, error) {
74+
if len(stationPubkey) != 32 {
75+
return nil, fmt.Errorf("%w, received: %d", ErrPublicKeyLen, len(stationPubkey))
76+
}
77+
var clientPrivate, clientPublic, representative [32]byte
78+
var sharedSecret []byte
79+
for ok := false; !ok; {
80+
var sliceKeyPrivate []byte = clientPrivate[:]
81+
_, err := rand.Read(sliceKeyPrivate)
82+
if err != nil {
83+
return nil, err
84+
}
85+
86+
ok = extra25519.ScalarBaseMult(&clientPublic, &representative, &clientPrivate)
87+
}
88+
89+
sharedSecret, err := curve25519.X25519(clientPrivate[:], stationPubkey)
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
// extra25519.ScalarBaseMult does not randomize most significant bit(sign of y_coord?)
95+
// Other implementations of elligator may have up to 2 non-random bits.
96+
// Here we randomize the bit, expecting it to be flipped back to 0 on station
97+
randByte := make([]byte, 1)
98+
_, err = rand.Read(randByte)
99+
if err != nil {
100+
return nil, err
101+
}
102+
representative[31] |= (0xC0 & randByte[0])
103+
104+
tagBuf := new(bytes.Buffer) // What we have to encrypt with the shared secret using AES
105+
tagBuf.Write(representative[:])
106+
107+
stationPubkeyHash := sha256.Sum256(sharedSecret[:])
108+
aesKey := stationPubkeyHash[:16]
109+
aesIvTag := stationPubkeyHash[16:28] // 12 bytes for plaintext nonce
110+
111+
cipherText, err := aesGcmEncrypt(plainText, aesKey, aesIvTag)
112+
if err != nil {
113+
return nil, err
114+
}
115+
116+
tagBuf.Write(cipherText)
117+
tag := tagBuf.Bytes()
118+
119+
return tag, nil
120+
}
121+
122+
// CTRObfuscator implements the Obfuscator interface using ECDHE and AES CTR. Prevents tag re-use.
123+
type CTRObfuscator struct{}
124+
125+
// TryReveal for CTRObfuscator expects a ciphertext object where the first 32 bytes is an elligator
126+
// encoded public key with which the server can derive an ECDHE shared secret. This secret is then
127+
// used to decrypt the remainder of the plaintext using AES CTR.
128+
func (CTRObfuscator) TryReveal(ciphertext []byte, privateKey [32]byte) ([]byte, error) {
129+
if len(ciphertext) < 32 {
130+
return nil, ErrPublicKeyLen
131+
}
132+
133+
var representative, clientPubkey [32]byte
134+
copy(representative[:], ciphertext[:32])
135+
representative[31] &= 0x3F
136+
extra25519.RepresentativeToPublicKey(&clientPubkey, &representative)
137+
138+
sharedSecret, err := curve25519.X25519(privateKey[:], clientPubkey[:])
139+
if err != nil {
140+
return nil, err
141+
}
142+
143+
stationPubkeyHash := sha256.Sum256(sharedSecret[:])
144+
aesKey := stationPubkeyHash[:16]
145+
aesIvTag := stationPubkeyHash[16:32]
146+
147+
return aesCTR(ciphertext[32:], aesKey, aesIvTag)
148+
}
149+
150+
// Obfuscate for CTRObfuscator derives a shared key using ECDHE an then encrypts the plaintext under
151+
// that key using AES CTR. The elligator representative for the clients public key is prepended
152+
// to the returned byte array. This means that the result length will likely be:
153+
//
154+
// 32 + len(plaintext)
155+
//
156+
// [elligator encoded client Pub][Ciphertext]
157+
func (CTRObfuscator) Obfuscate(plainText []byte, stationPubkey []byte) ([]byte, error) {
158+
if len(stationPubkey) != 32 {
159+
return nil, errors.New("Unexpected station pubkey length. Expected: 32." +
160+
" Received: " + strconv.Itoa(len(stationPubkey)) + ".")
161+
}
162+
var clientPrivate, clientPublic, representative [32]byte
163+
var sharedSecret []byte
164+
for ok := false; !ok; {
165+
var sliceKeyPrivate []byte = clientPrivate[:]
166+
_, err := rand.Read(sliceKeyPrivate)
167+
if err != nil {
168+
return nil, err
169+
}
170+
171+
ok = extra25519.ScalarBaseMult(&clientPublic, &representative, &clientPrivate)
172+
}
173+
174+
sharedSecret, err := curve25519.X25519(clientPrivate[:], stationPubkey)
175+
if err != nil {
176+
return nil, err
177+
}
178+
179+
// extra25519.ScalarBaseMult does not randomize most significant bit(sign of y_coord?)
180+
// Other implementations of elligator may have up to 2 non-random bits.
181+
// Here we randomize the bit, expecting it to be flipped back to 0 on station
182+
randByte := make([]byte, 1)
183+
_, err = rand.Read(randByte)
184+
if err != nil {
185+
return nil, err
186+
}
187+
representative[31] |= (0xC0 & randByte[0])
188+
189+
tagBuf := new(bytes.Buffer) // What we have to encrypt with the shared secret using AES
190+
tagBuf.Write(representative[:])
191+
192+
stationPubkeyHash := sha256.Sum256(sharedSecret[:])
193+
aesKey := stationPubkeyHash[:16]
194+
aesIvTag := stationPubkeyHash[16:32] // 16 bytes for CTR IV
195+
196+
cipherText, err := aesCTR(plainText, aesKey, aesIvTag)
197+
if err != nil {
198+
return nil, err
199+
}
200+
201+
tagBuf.Write(cipherText)
202+
tag := tagBuf.Bytes()
203+
204+
return tag, nil
205+
}
206+
207+
// NilObfuscator implements the Obfuscator interface for no modification the provided tag /
208+
// plaintext / ciphertext. Will NOT prevent tag re-use if a registration is re-used.
209+
type NilObfuscator struct{}
210+
211+
// TryReveal for NilObfuscator just returns the provided ciphertext without modification
212+
func (NilObfuscator) TryReveal(cipherText []byte, privateKey [32]byte) ([]byte, error) {
213+
return cipherText, nil
214+
}
215+
216+
// Obfuscate for NilObfuscator just returns the provided plaintext without modification
217+
func (NilObfuscator) Obfuscate(plainText []byte, stationPubkey []byte) ([]byte, error) {
218+
return plainText, nil
219+
}
220+
221+
// The key argument should be the AES key, either 16 or 32 bytes
222+
// to select AES-128 or AES-256.
223+
func aesGcmEncrypt(plaintext []byte, key []byte, iv []byte) ([]byte, error) {
224+
block, err := aes.NewCipher(key)
225+
if err != nil {
226+
return nil, err
227+
}
228+
229+
aesGcmCipher, err := cipher.NewGCM(block)
230+
if err != nil {
231+
return nil, err
232+
}
233+
return aesGcmCipher.Seal(nil, iv, plaintext, nil), nil
234+
}
235+
236+
func aesCTR(in []byte, key []byte, iv []byte) ([]byte, error) {
237+
block, err := aes.NewCipher(key)
238+
if err != nil {
239+
return nil, err
240+
}
241+
242+
out := make([]byte, len(in))
243+
stream := cipher.NewCTR(block, iv)
244+
stream.XORKeyStream(out, in)
245+
246+
return out, nil
247+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package transports
2+
3+
import (
4+
"bytes"
5+
"crypto/rand"
6+
"testing"
7+
8+
"github.com/refraction-networking/gotapdance/ed25519"
9+
"github.com/refraction-networking/gotapdance/ed25519/extra25519"
10+
"github.com/stretchr/testify/require"
11+
"golang.org/x/crypto/curve25519"
12+
)
13+
14+
func TestObfuscateRevealIntended(t *testing.T) {
15+
_, private, _ := ed25519.GenerateKey(rand.Reader)
16+
17+
var curve25519Public, curve25519Private [32]byte
18+
extra25519.PrivateKeyToCurve25519(&curve25519Private, private)
19+
curve25519.ScalarBaseMult(&curve25519Public, &curve25519Private)
20+
21+
buf := make([]byte, 32)
22+
_, err := rand.Read(buf)
23+
require.Nil(t, err)
24+
25+
for _, Obfsc := range []Obfuscator{GCMObfuscator{}, CTRObfuscator{}, NilObfuscator{}} {
26+
ciphertext, err := Obfsc.Obfuscate(buf, curve25519Public[:])
27+
require.Nil(t, err)
28+
29+
plaintext, err := Obfsc.TryReveal(ciphertext, curve25519Private)
30+
require.Nil(t, err)
31+
32+
require.NotEmpty(t, plaintext)
33+
require.True(t, bytes.Equal(buf, plaintext))
34+
}
35+
}
36+
37+
func TestObfuscateReveal1B(t *testing.T) {
38+
_, private, _ := ed25519.GenerateKey(rand.Reader)
39+
40+
var curve25519Public, curve25519Private [32]byte
41+
extra25519.PrivateKeyToCurve25519(&curve25519Private, private)
42+
curve25519.ScalarBaseMult(&curve25519Public, &curve25519Private)
43+
44+
for _, Obfsc := range []Obfuscator{GCMObfuscator{}, CTRObfuscator{}, NilObfuscator{}} {
45+
ciphertext, err := Obfsc.Obfuscate([]byte{0xff}, curve25519Public[:])
46+
require.Nil(t, err)
47+
48+
plaintext, err := Obfsc.TryReveal(ciphertext, curve25519Private)
49+
require.Nil(t, err)
50+
51+
require.NotEmpty(t, plaintext)
52+
// t.Log(len(ciphertext))
53+
require.True(t, bytes.Equal([]byte{0xff}, plaintext))
54+
}
55+
}
56+
57+
func TestObfuscateReveal100B(t *testing.T) {
58+
_, private, _ := ed25519.GenerateKey(rand.Reader)
59+
60+
var curve25519Public, curve25519Private [32]byte
61+
extra25519.PrivateKeyToCurve25519(&curve25519Private, private)
62+
curve25519.ScalarBaseMult(&curve25519Public, &curve25519Private)
63+
64+
buf := make([]byte, 100)
65+
_, err := rand.Read(buf)
66+
require.Nil(t, err)
67+
68+
for _, Obfsc := range []Obfuscator{GCMObfuscator{}, CTRObfuscator{}, NilObfuscator{}} {
69+
ciphertext, err := Obfsc.Obfuscate(buf, curve25519Public[:])
70+
require.Nil(t, err)
71+
72+
plaintext, err := Obfsc.TryReveal(ciphertext, curve25519Private)
73+
require.Nil(t, err)
74+
75+
require.NotEmpty(t, plaintext)
76+
require.True(t, bytes.Equal(buf, plaintext))
77+
}
78+
}

application/transports/transports.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ import (
1212
)
1313

1414
var (
15-
// ErrTryAgain is returned by transports when
16-
// it is inconclusive with the current amount of data
15+
// ErrTryAgain is returned by transports when it is inconclusive with the current amount of data
1716
// whether the transport exists in the connection.
1817
ErrTryAgain = errors.New("not enough information to determine transport")
1918

@@ -22,11 +21,19 @@ var (
2221
// contain this transport. The caller shouldn't retry
2322
// with this transport.
2423
ErrNotTransport = errors.New("connection does not contain transport")
24+
25+
// ErrTransportNotSupported is returned when a transport is unable to service one or more of the
26+
// required functions because the clientLibVersion is to old and the transport is not backward
27+
// compatible to that version.
28+
ErrTransportNotSupported = errors.New("Transport not supported ")
29+
30+
// ErrPublicKeyLen is returned when the length of the provided public key is incorrect for
31+
// ed25519.
32+
ErrPublicKeyLen = errors.New("Unexpected station pubkey length. Expected: 32B")
2533
)
2634

27-
// PrefixConn allows arbitrary readers to serve as the data source
28-
// of a net.Conn. This allows us to consume data from the socket
29-
// while later making it available again (for things like handshakes).
35+
// PrefixConn allows arbitrary readers to serve as the data source of a net.Conn. This allows us to
36+
// consume data from the socket while later making it available again (for things like handshakes).
3037
type PrefixConn struct {
3138
net.Conn
3239
r io.Reader

0 commit comments

Comments
 (0)