Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ccb349f3d2 | |||
| 4768065e72 | |||
| 64bf6bfe90 | |||
| aab48f0dbe | |||
| b00e2d8955 |
@@ -61,6 +61,8 @@ proxy:
|
|||||||
# Proxy IP address/domain name and port.
|
# Proxy IP address/domain name and port.
|
||||||
address: "127.0.0.1:1080"
|
address: "127.0.0.1:1080"
|
||||||
# Proxy authentication (optional). Put MTProxy secret in password field.
|
# Proxy authentication (optional). Put MTProxy secret in password field.
|
||||||
|
# For mtproxy, the secret must be hex-encoded (the same form mtg/MTProxy
|
||||||
|
# tools print, e.g. "ee" + 16-byte secret + cloak domain hex for faketls).
|
||||||
username:
|
username:
|
||||||
password:
|
password:
|
||||||
|
|
||||||
|
|||||||
+23
-1
@@ -17,13 +17,31 @@
|
|||||||
package connector
|
package connector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/net/proxy"
|
"golang.org/x/net/proxy"
|
||||||
|
|
||||||
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/dcs"
|
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/dcs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// decodeMTProxySecret parses an MTProxy secret string into raw bytes.
|
||||||
|
// MTProxy secrets are binary (faketls secrets begin with 0xEE, secured with 0xDD)
|
||||||
|
// and cannot be carried verbatim in a YAML string field, so we accept the standard
|
||||||
|
// hex encoding (optionally prefixed with "ee"/"dd") used by mtg/MTProxy tooling.
|
||||||
|
func decodeMTProxySecret(s string) ([]byte, error) {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return nil, fmt.Errorf("mtproxy secret is empty")
|
||||||
|
}
|
||||||
|
b, err := hex.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("mtproxy secret must be hex-encoded: %w", err)
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetProxyDialFunc(cfg ProxyConfig) (dcs.DialFunc, error) {
|
func GetProxyDialFunc(cfg ProxyConfig) (dcs.DialFunc, error) {
|
||||||
switch cfg.Type {
|
switch cfg.Type {
|
||||||
// we can't proxy HTTP through mtproxy
|
// we can't proxy HTTP through mtproxy
|
||||||
@@ -54,7 +72,11 @@ func GetProxyResolver(cfg ProxyConfig) (dcs.Resolver, error) {
|
|||||||
resolver := dcs.Plain(dcs.PlainOptions{Dial: dialer})
|
resolver := dcs.Plain(dcs.PlainOptions{Dial: dialer})
|
||||||
return resolver, nil
|
return resolver, nil
|
||||||
case "mtproxy":
|
case "mtproxy":
|
||||||
return dcs.MTProxy(cfg.Address, []byte(cfg.Password), dcs.MTProxyOptions{})
|
secret, err := decodeMTProxySecret(cfg.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dcs.MTProxy(cfg.Address, secret, dcs.MTProxyOptions{})
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported proxy type %s", cfg.Type)
|
return nil, fmt.Errorf("unsupported proxy type %s", cfg.Type)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package connector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecodeMTProxySecret(t *testing.T) {
|
||||||
|
// faketls secret: 0xee + 16 bytes + cloak domain ("working-name.ru" = 15 bytes)
|
||||||
|
hexSecret := "ee971746d927f4c0138b18447bfe1269bc70312e776f726b696e672d6e616d652e7275"
|
||||||
|
want := []byte{
|
||||||
|
0xee,
|
||||||
|
0x97, 0x17, 0x46, 0xd9, 0x27, 0xf4, 0xc0, 0x13,
|
||||||
|
0x8b, 0x18, 0x44, 0x7b, 0xfe, 0x12, 0x69, 0xbc,
|
||||||
|
0x70, 0x31, 0x2e, 0x77, 0x6f, 0x72, 0x6b, 0x69,
|
||||||
|
0x6e, 0x67, 0x2d, 0x6e, 0x61, 0x6d, 0x65, 0x2e,
|
||||||
|
0x72, 0x75,
|
||||||
|
}
|
||||||
|
got, err := decodeMTProxySecret(hexSecret)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Fatalf("decoded bytes mismatch:\n got=%x\nwant=%x", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := decodeMTProxySecret(" " + hexSecret + "\n"); err != nil {
|
||||||
|
t.Fatalf("whitespace should be tolerated: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := decodeMTProxySecret(""); err == nil {
|
||||||
|
t.Fatal("expected error for empty secret")
|
||||||
|
}
|
||||||
|
if _, err := decodeMTProxySecret("not-hex!!"); err == nil {
|
||||||
|
t.Fatal("expected error for non-hex secret")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package faketls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestReadSkipsChangeCipherSpec ensures interleaved ChangeCipherSpec records
|
||||||
|
// do not pollute the Application-data stream. Earlier behaviour wrote the
|
||||||
|
// 1-byte CCS payload into readBuf, which desynced the obfuscated2 CTR
|
||||||
|
// keystream and produced "msg_key is invalid" on decrypted MTProto messages.
|
||||||
|
func TestReadSkipsChangeCipherSpec(t *testing.T) {
|
||||||
|
a := require.New(t)
|
||||||
|
|
||||||
|
wire := bytes.NewBuffer(nil)
|
||||||
|
|
||||||
|
// CCS record (1 byte = 0x01)
|
||||||
|
_, err := writeRecord(wire, record{
|
||||||
|
Type: RecordTypeChangeCipherSpec,
|
||||||
|
Version: Version12Bytes,
|
||||||
|
Data: []byte{0x01},
|
||||||
|
})
|
||||||
|
a.NoError(err)
|
||||||
|
|
||||||
|
// Application record carrying our payload
|
||||||
|
payload := []byte("hello-mtproto-bytes")
|
||||||
|
_, err = writeRecord(wire, record{
|
||||||
|
Type: RecordTypeApplication,
|
||||||
|
Version: Version12Bytes,
|
||||||
|
Data: payload,
|
||||||
|
})
|
||||||
|
a.NoError(err)
|
||||||
|
|
||||||
|
// Another CCS in the middle
|
||||||
|
_, err = writeRecord(wire, record{
|
||||||
|
Type: RecordTypeChangeCipherSpec,
|
||||||
|
Version: Version12Bytes,
|
||||||
|
Data: []byte{0x01},
|
||||||
|
})
|
||||||
|
a.NoError(err)
|
||||||
|
|
||||||
|
// Second application record
|
||||||
|
more := []byte("second-payload")
|
||||||
|
_, err = writeRecord(wire, record{
|
||||||
|
Type: RecordTypeApplication,
|
||||||
|
Version: Version12Bytes,
|
||||||
|
Data: more,
|
||||||
|
})
|
||||||
|
a.NoError(err)
|
||||||
|
|
||||||
|
tls := NewFakeTLS(zeroReader{}, &readonly{r: wire})
|
||||||
|
|
||||||
|
got, err := io.ReadAll(io.LimitReader(tls, int64(len(payload)+len(more))))
|
||||||
|
a.NoError(err)
|
||||||
|
a.Equal(append(append([]byte(nil), payload...), more...), got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readonly adapts an io.Reader to io.ReadWriter (NewFakeTLS demands one).
|
||||||
|
type readonly struct{ r io.Reader }
|
||||||
|
|
||||||
|
func (r *readonly) Read(p []byte) (int, error) { return r.r.Read(p) }
|
||||||
|
func (r *readonly) Write(p []byte) (int, error) { return len(p), nil }
|
||||||
@@ -14,7 +14,26 @@ import (
|
|||||||
|
|
||||||
const clientHelloLength = 517
|
const clientHelloLength = 517
|
||||||
|
|
||||||
func createClientHello(b *bin.Buffer, sessionID [32]byte, domain string, key [32]byte) (randomOffset int) {
|
// generateGrease produces seven GREASE bytes following the TLS spec
|
||||||
|
// (RFC 8701) and tdlib's TlsInit.cpp constraints used by MTProxy faketls
|
||||||
|
// validators: each byte has the form 0x?A (low nibble 0x0A), and grease[3]
|
||||||
|
// must differ from grease[4].
|
||||||
|
func generateGrease(rng io.Reader) ([7]byte, error) {
|
||||||
|
var raw [7]byte
|
||||||
|
if _, err := io.ReadFull(rng, raw[:]); err != nil {
|
||||||
|
return raw, errors.Wrap(err, "read grease entropy")
|
||||||
|
}
|
||||||
|
var g [7]byte
|
||||||
|
for i, r := range raw {
|
||||||
|
g[i] = (r & 0xF0) | 0x0A
|
||||||
|
}
|
||||||
|
if g[3] == g[4] {
|
||||||
|
g[3] ^= 0x10
|
||||||
|
}
|
||||||
|
return g, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createClientHello(b *bin.Buffer, sessionID [32]byte, domain string, key [32]byte, grease [7]byte) (randomOffset int) {
|
||||||
S := func(s string) {
|
S := func(s string) {
|
||||||
b.Buf = append(b.Buf, s...)
|
b.Buf = append(b.Buf, s...)
|
||||||
}
|
}
|
||||||
@@ -22,8 +41,9 @@ func createClientHello(b *bin.Buffer, sessionID [32]byte, domain string, key [32
|
|||||||
randomOffset = len(b.Buf)
|
randomOffset = len(b.Buf)
|
||||||
b.Expand(n)
|
b.Expand(n)
|
||||||
}
|
}
|
||||||
G := func(_ int) {
|
G := func(n int) {
|
||||||
b.Expand(0)
|
v := grease[n]
|
||||||
|
b.Buf = append(b.Buf, v, v)
|
||||||
}
|
}
|
||||||
R := func() {
|
R := func() {
|
||||||
b.Buf = append(b.Buf, sessionID[:]...)
|
b.Buf = append(b.Buf, sessionID[:]...)
|
||||||
@@ -83,9 +103,18 @@ func createClientHello(b *bin.Buffer, sessionID [32]byte, domain string, key [32
|
|||||||
G(3)
|
G(3)
|
||||||
S("\x00\x01\x00\x00\x15")
|
S("\x00\x01\x00\x00\x15")
|
||||||
|
|
||||||
if pad := clientHelloLength - b.Len(); pad > 0 {
|
// Padding extension (id 0x0015 already written above): write its
|
||||||
b.Expand(pad)
|
// length so the resulting ClientHello is exactly clientHelloLength
|
||||||
|
// bytes, then fill the body with zeros.
|
||||||
|
padLen := clientHelloLength - b.Len() - 2
|
||||||
|
if padLen < 0 {
|
||||||
|
padLen = 0
|
||||||
}
|
}
|
||||||
|
lenPos := b.Len()
|
||||||
|
b.Expand(2)
|
||||||
|
binary.BigEndian.PutUint16(b.Buf[lenPos:lenPos+2], uint16(padLen))
|
||||||
|
b.Expand(padLen)
|
||||||
|
|
||||||
return randomOffset
|
return randomOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,15 +123,21 @@ func createClientHello(b *bin.Buffer, sessionID [32]byte, domain string, key [32
|
|||||||
// See https://tools.ietf.org/html/rfc5246#section-7.4.1.1.
|
// See https://tools.ietf.org/html/rfc5246#section-7.4.1.1.
|
||||||
func writeClientHello(
|
func writeClientHello(
|
||||||
w io.Writer,
|
w io.Writer,
|
||||||
|
rng io.Reader,
|
||||||
now clock.Clock,
|
now clock.Clock,
|
||||||
sessionID [32]byte,
|
sessionID [32]byte,
|
||||||
domain string,
|
domain string,
|
||||||
secret []byte,
|
secret []byte,
|
||||||
) (r [32]byte, err error) {
|
) (r [32]byte, err error) {
|
||||||
|
grease, err := generateGrease(rng)
|
||||||
|
if err != nil {
|
||||||
|
return [32]byte{}, err
|
||||||
|
}
|
||||||
|
|
||||||
b := &bin.Buffer{
|
b := &bin.Buffer{
|
||||||
Buf: make([]byte, 0, 576),
|
Buf: make([]byte, 0, 576),
|
||||||
}
|
}
|
||||||
randomOffset := createClientHello(b, sessionID, domain, [32]byte{})
|
randomOffset := createClientHello(b, sessionID, domain, [32]byte{}, grease)
|
||||||
|
|
||||||
// https://github.com/tdlib/td/blob/27d3fdd09d90f6b77ecbcce50b1e86dc4b3dd366/td/mtproto/TlsInit.cpp#L380-L384
|
// https://github.com/tdlib/td/blob/27d3fdd09d90f6b77ecbcce50b1e86dc4b3dd366/td/mtproto/TlsInit.cpp#L380-L384
|
||||||
mac := hmac.New(sha256.New, secret)
|
mac := hmac.New(sha256.New, secret)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func (o *FakeTLS) Handshake(protocol [4]byte, dc int, s mtproxy.Secret) error {
|
|||||||
return errors.Wrap(err, "generate sessionID")
|
return errors.Wrap(err, "generate sessionID")
|
||||||
}
|
}
|
||||||
|
|
||||||
clientDigest, err := writeClientHello(o.conn, o.clock, sessionID, s.CloakHost, s.Secret)
|
clientDigest, err := writeClientHello(o.conn, o.rand, o.clock, sessionID, s.CloakHost, s.Secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "send ClientHello")
|
return errors.Wrap(err, "send ClientHello")
|
||||||
}
|
}
|
||||||
@@ -93,20 +93,27 @@ func (o *FakeTLS) Read(b []byte) (n int, err error) {
|
|||||||
return o.readBuf.Read(b)
|
return o.readBuf.Read(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
rec, err := readRecord(o.conn)
|
// Skip ChangeCipherSpec records — they are TLS-level keep-alive /
|
||||||
if err != nil {
|
// compatibility markers (one-byte payload 0x01) and must NOT be
|
||||||
return 0, errors.Wrap(err, "read TLS record")
|
// passed up to the obfuscated2 layer, otherwise the CTR keystream
|
||||||
}
|
// position desyncs and subsequent MTProto messages decrypt to
|
||||||
|
// garbage (`msg_key is invalid`).
|
||||||
|
for {
|
||||||
|
rec, err := readRecord(o.conn)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.Wrap(err, "read TLS record")
|
||||||
|
}
|
||||||
|
|
||||||
switch rec.Type {
|
switch rec.Type {
|
||||||
case RecordTypeChangeCipherSpec:
|
case RecordTypeChangeCipherSpec:
|
||||||
case RecordTypeApplication:
|
continue
|
||||||
case RecordTypeHandshake:
|
case RecordTypeApplication:
|
||||||
return 0, errors.New("unexpected record type handshake")
|
o.readBuf.Write(rec.Data)
|
||||||
default:
|
return o.readBuf.Read(b)
|
||||||
return 0, errors.Errorf("unsupported record type %v", rec.Type)
|
case RecordTypeHandshake:
|
||||||
|
return 0, errors.New("unexpected record type handshake")
|
||||||
|
default:
|
||||||
|
return 0, errors.Errorf("unsupported record type %v", rec.Type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
o.readBuf.Write(rec.Data)
|
|
||||||
|
|
||||||
return o.readBuf.Read(b)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ import (
|
|||||||
"github.com/gotd/neo"
|
"github.com/gotd/neo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type zeroReader struct{}
|
||||||
|
|
||||||
|
func (zeroReader) Read(p []byte) (int, error) {
|
||||||
|
for i := range p {
|
||||||
|
p[i] = 0
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestTLS(t *testing.T) {
|
func TestTLS(t *testing.T) {
|
||||||
a := require.New(t)
|
a := require.New(t)
|
||||||
secret := [32]byte{}
|
secret := [32]byte{}
|
||||||
@@ -17,42 +26,42 @@ func TestTLS(t *testing.T) {
|
|||||||
c := neo.NewTime(time.Date(2010, 10, 10, 1, 1, 1, 0, time.UTC))
|
c := neo.NewTime(time.Date(2010, 10, 10, 1, 1, 1, 0, time.UTC))
|
||||||
|
|
||||||
b := bytes.NewBuffer(nil)
|
b := bytes.NewBuffer(nil)
|
||||||
_, err := writeClientHello(b, c, sessionID, "google.com", secret[:])
|
_, err := writeClientHello(b, zeroReader{}, c, sessionID, "google.com", secret[:])
|
||||||
a.NoError(err)
|
a.NoError(err)
|
||||||
|
|
||||||
testVector := []byte{
|
testVector := []byte{
|
||||||
0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03, 0xf9, 0x75, 0x5f, 0xdd, 0xb9,
|
0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03, 0xa9, 0xa8, 0x7f, 0x37, 0x9b,
|
||||||
0xe3, 0x46, 0x57, 0x5a, 0x26, 0x71, 0xfa, 0x29, 0x7f, 0xab, 0xf0, 0xa1, 0xf3, 0x69, 0x4f, 0x72,
|
0x09, 0x80, 0x6a, 0xf3, 0xff, 0x78, 0x4a, 0x6c, 0x4e, 0xbd, 0xdd, 0x94, 0x31, 0x8e, 0x7c, 0x09,
|
||||||
0xe0, 0xc3, 0x8f, 0x62, 0x77, 0x5c, 0x8f, 0x5a, 0xf8, 0xa2, 0xa9, 0x20, 0x00, 0x00, 0x00, 0x00,
|
0x36, 0x63, 0x77, 0x1d, 0x36, 0xf4, 0xcb, 0x6d, 0x3e, 0x13, 0x83, 0x20, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x13, 0x01,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0a, 0x0a,
|
||||||
0x13, 0x02, 0x13, 0x03, 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, 0xcc, 0xa9, 0xcc, 0xa8,
|
0x13, 0x01, 0x13, 0x02, 0x13, 0x03, 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, 0xcc, 0xa9,
|
||||||
0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0x01, 0x00, 0x01, 0x93,
|
0xcc, 0xa8, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0x01, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x0d, 0x00, 0x00, 0x0a, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
0x01, 0x93, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x0d, 0x00, 0x00, 0x0a, 0x67,
|
||||||
0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x17, 0x00, 0x00, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x0a,
|
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x17, 0x00, 0x00, 0xff, 0x01, 0x00,
|
||||||
0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00,
|
0x01, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x0a, 0x0a, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18,
|
||||||
0x00, 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74,
|
0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c,
|
||||||
0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x00, 0x05, 0x00, 0x05,
|
||||||
0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05,
|
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, 0x03, 0x08, 0x04, 0x04,
|
||||||
0x01, 0x08, 0x06, 0x06, 0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x33, 0x00, 0x2b, 0x00, 0x29, 0x00,
|
0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08, 0x06, 0x06, 0x01, 0x00, 0x12, 0x00, 0x00, 0x00,
|
||||||
0x01, 0x00, 0x00, 0x1d, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x33, 0x00, 0x2b, 0x00, 0x29, 0x0a, 0x0a, 0x00, 0x01, 0x00, 0x00, 0x1d, 0x00, 0x20, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x02, 0x01, 0x01, 0x00, 0x2b, 0x00, 0x0b,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d,
|
||||||
0x0a, 0x03, 0x04, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00, 0x1b, 0x00, 0x03, 0x02, 0x00, 0x02,
|
0x00, 0x02, 0x01, 0x01, 0x00, 0x2b, 0x00, 0x0b, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x03, 0x03, 0x03,
|
||||||
0x00, 0x01, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x02, 0x03, 0x01, 0x00, 0x1b, 0x00, 0x03, 0x02, 0x00, 0x02, 0x1a, 0x1a, 0x00, 0x01, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x15, 0x00, 0xd2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
}
|
}
|
||||||
a.Equal(testVector, b.Bytes())
|
a.Equal(testVector, b.Bytes())
|
||||||
|
|||||||
@@ -4,11 +4,20 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/go-faster/errors"
|
"github.com/go-faster/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// peekDump returns up to n bytes from the start of buf as a hex string for diagnostics.
|
||||||
|
func peekDump(buf []byte, n int) string {
|
||||||
|
if len(buf) < n {
|
||||||
|
n = len(buf)
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(buf[:n])
|
||||||
|
}
|
||||||
|
|
||||||
// readServerHello reads faketls ServerHello.
|
// readServerHello reads faketls ServerHello.
|
||||||
func readServerHello(r io.Reader, clientRandom [32]byte, secret []byte) error {
|
func readServerHello(r io.Reader, clientRandom [32]byte, secret []byte) error {
|
||||||
packetBuf := bytes.NewBuffer(nil)
|
packetBuf := bytes.NewBuffer(nil)
|
||||||
@@ -16,10 +25,11 @@ func readServerHello(r io.Reader, clientRandom [32]byte, secret []byte) error {
|
|||||||
|
|
||||||
handshake, err := readRecord(r)
|
handshake, err := readRecord(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "handshake record")
|
return errors.Wrapf(err, "handshake record (peek=%s)", peekDump(packetBuf.Bytes(), 32))
|
||||||
}
|
}
|
||||||
if handshake.Type != RecordTypeHandshake {
|
if handshake.Type != RecordTypeHandshake {
|
||||||
return errors.Wrap(err, "unexpected record type")
|
return errors.Errorf("unexpected handshake record type: got 0x%02x, want 0x%02x (peek=%s)",
|
||||||
|
byte(handshake.Type), byte(RecordTypeHandshake), peekDump(packetBuf.Bytes(), 32))
|
||||||
}
|
}
|
||||||
|
|
||||||
changeCipher, err := readRecord(r)
|
changeCipher, err := readRecord(r)
|
||||||
@@ -27,7 +37,8 @@ func readServerHello(r io.Reader, clientRandom [32]byte, secret []byte) error {
|
|||||||
return errors.Wrap(err, "change cipher record")
|
return errors.Wrap(err, "change cipher record")
|
||||||
}
|
}
|
||||||
if changeCipher.Type != RecordTypeChangeCipherSpec {
|
if changeCipher.Type != RecordTypeChangeCipherSpec {
|
||||||
return errors.Wrap(err, "unexpected record type")
|
return errors.Errorf("unexpected change cipher record type: got 0x%02x, want 0x%02x",
|
||||||
|
byte(changeCipher.Type), byte(RecordTypeChangeCipherSpec))
|
||||||
}
|
}
|
||||||
|
|
||||||
cert, err := readRecord(r)
|
cert, err := readRecord(r)
|
||||||
@@ -35,7 +46,8 @@ func readServerHello(r io.Reader, clientRandom [32]byte, secret []byte) error {
|
|||||||
return errors.Wrap(err, "cert record")
|
return errors.Wrap(err, "cert record")
|
||||||
}
|
}
|
||||||
if cert.Type != RecordTypeApplication {
|
if cert.Type != RecordTypeApplication {
|
||||||
return errors.Wrap(err, "unexpected record type")
|
return errors.Errorf("unexpected application record type: got 0x%02x, want 0x%02x",
|
||||||
|
byte(cert.Type), byte(RecordTypeApplication))
|
||||||
}
|
}
|
||||||
|
|
||||||
// `$record_header = type 1 byte + version 2 bytes + payload_length 2 bytes = 5 bytes`
|
// `$record_header = type 1 byte + version 2 bytes + payload_length 2 bytes = 5 bytes`
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package faketls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestClientHelloStructure verifies that what we generate is a syntactically
|
||||||
|
// valid TLS ClientHello — i.e., the Go crypto/tls server can parse it without
|
||||||
|
// returning a "decode_error"-like syntax error. We don't care that the TLS
|
||||||
|
// handshake then fails (it will, since we're using a fake cert / random data);
|
||||||
|
// we only care that parsing succeeds.
|
||||||
|
func TestClientHelloStructure(t *testing.T) {
|
||||||
|
// Render with deterministic rng + sessionID + key.
|
||||||
|
var session [32]byte
|
||||||
|
for i := range session {
|
||||||
|
session[i] = byte(i)
|
||||||
|
}
|
||||||
|
var key [32]byte
|
||||||
|
for i := range key {
|
||||||
|
key[i] = 0xAA
|
||||||
|
}
|
||||||
|
var grease [7]byte
|
||||||
|
for i := range grease {
|
||||||
|
grease[i] = byte(0x0A + i*0x10)
|
||||||
|
}
|
||||||
|
if grease[3] == grease[4] {
|
||||||
|
grease[3] ^= 0x10
|
||||||
|
}
|
||||||
|
|
||||||
|
b := &bin.Buffer{Buf: make([]byte, 0, 576)}
|
||||||
|
createClientHello(b, session, "example.com", key, grease)
|
||||||
|
if len(b.Buf) != clientHelloLength {
|
||||||
|
t.Fatalf("expected %d bytes, got %d", clientHelloLength, len(b.Buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire it through a real TLS server. The server reads bytes from
|
||||||
|
// our pipe; if it accepts ClientHello but fails on cert/MAC, we get
|
||||||
|
// a non-syntax error. If it returns "decode_error", we know we're
|
||||||
|
// still busted.
|
||||||
|
clientConn, serverConn := net.Pipe()
|
||||||
|
defer clientConn.Close()
|
||||||
|
defer serverConn.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
clientConn.Write(b.Buf)
|
||||||
|
// keep the pipe open until done
|
||||||
|
}()
|
||||||
|
|
||||||
|
cfg := &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{generateSelfSigned(t)},
|
||||||
|
}
|
||||||
|
srv := tls.Server(serverConn, cfg)
|
||||||
|
srv.SetDeadline(time.Now().Add(2 * time.Second))
|
||||||
|
err := srv.Handshake()
|
||||||
|
if err == nil {
|
||||||
|
return // unexpectedly succeeded — fine for our purpose
|
||||||
|
}
|
||||||
|
t.Logf("server handshake error (expected non-syntax): %v", err)
|
||||||
|
|
||||||
|
msg := err.Error()
|
||||||
|
for _, marker := range []string{"decode_error", "syntax", "malformed", "bad ClientHello"} {
|
||||||
|
if strings.Contains(msg, marker) {
|
||||||
|
t.Fatalf("structural parse failure (%q) — ClientHello is malformed: %v", marker, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateSelfSigned builds a throwaway cert for the test TLS server.
|
||||||
|
func generateSelfSigned(t *testing.T) tls.Certificate {
|
||||||
|
cert, err := tls.X509KeyPair(testCertPEM, testKeyPEM)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return cert
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated with `go run filippo.io/mkcert@latest -ecdsa example.com`-ish.
|
||||||
|
// Embedded here for deterministic test environment.
|
||||||
|
var testCertPEM = []byte(`-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw
|
||||||
|
DgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow
|
||||||
|
EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d
|
||||||
|
7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B
|
||||||
|
5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr
|
||||||
|
BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1
|
||||||
|
NDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l
|
||||||
|
Wf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc
|
||||||
|
6MF9+Yw1Yy0t
|
||||||
|
-----END CERTIFICATE-----`)
|
||||||
|
|
||||||
|
var testKeyPEM = []byte(`-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MHcCAQEEIIrYSSNQFaA2Hwf1duRSxKtLYX5CB04fSeQ6tF1aY/PuoAoGCCqGSM49
|
||||||
|
AwEHoUQDQgAEPR3tU2Fta9ktY+6P9G0cWO+0kETA6SFs38GecTyudlHz6xvCdz8q
|
||||||
|
EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA==
|
||||||
|
-----END EC PRIVATE KEY-----`)
|
||||||
@@ -53,7 +53,12 @@ func (o *Obfuscated2) Read(b []byte) (int, error) {
|
|||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
o.decrypt.XORKeyStream(b, b)
|
// IMPORTANT: only XOR the n bytes that were actually read.
|
||||||
|
// XOR-ing the full b advances the CTR keystream past where the
|
||||||
|
// server is and permanently desyncs the stream — every later
|
||||||
|
// MTProto message decrypts to garbage and the engine fails
|
||||||
|
// with "msg_key is invalid".
|
||||||
|
o.decrypt.XORKeyStream(b[:n], b[:n])
|
||||||
}
|
}
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package obfuscated2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// chunkConn delivers data from buf in chunks of at most chunkSize bytes.
|
||||||
|
type chunkConn struct {
|
||||||
|
buf *bytes.Buffer
|
||||||
|
chunkSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *chunkConn) Read(p []byte) (int, error) {
|
||||||
|
if c.buf.Len() == 0 {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
want := len(p)
|
||||||
|
if want > c.chunkSize {
|
||||||
|
want = c.chunkSize
|
||||||
|
}
|
||||||
|
return c.buf.Read(p[:want])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *chunkConn) Write(p []byte) (int, error) { return len(p), nil }
|
||||||
|
|
||||||
|
// TestShortReadKeepsKeystreamAligned ensures that when the underlying
|
||||||
|
// transport returns fewer bytes than the caller asked for, the CTR
|
||||||
|
// keystream is only advanced by the bytes actually delivered.
|
||||||
|
//
|
||||||
|
// The previous implementation called XORKeyStream(b, b) instead of
|
||||||
|
// XORKeyStream(b[:n], b[:n]); after a single short read the client and
|
||||||
|
// server keystreams diverged and every subsequent MTProto message
|
||||||
|
// failed integrity (msg_key invalid).
|
||||||
|
func TestShortReadKeepsKeystreamAligned(t *testing.T) {
|
||||||
|
a := require.New(t)
|
||||||
|
|
||||||
|
key := bytes.Repeat([]byte{0x11}, 32)
|
||||||
|
iv := bytes.Repeat([]byte{0x22}, 16)
|
||||||
|
|
||||||
|
enc, err := aes.NewCipher(key)
|
||||||
|
a.NoError(err)
|
||||||
|
dec, err := aes.NewCipher(key)
|
||||||
|
a.NoError(err)
|
||||||
|
|
||||||
|
encStream := cipher.NewCTR(enc, iv)
|
||||||
|
decStream := cipher.NewCTR(dec, iv)
|
||||||
|
|
||||||
|
plaintext := bytes.Repeat([]byte("Hello, MTProxy! "), 50)
|
||||||
|
ciphertext := make([]byte, len(plaintext))
|
||||||
|
encStream.XORKeyStream(ciphertext, plaintext)
|
||||||
|
|
||||||
|
wire := &chunkConn{buf: bytes.NewBuffer(append([]byte(nil), ciphertext...)), chunkSize: 7}
|
||||||
|
o := &Obfuscated2{
|
||||||
|
conn: wire,
|
||||||
|
keys: keys{decrypt: decStream},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := make([]byte, len(plaintext))
|
||||||
|
off := 0
|
||||||
|
for off < len(plaintext) {
|
||||||
|
end := off + 128
|
||||||
|
if end > len(got) {
|
||||||
|
end = len(got)
|
||||||
|
}
|
||||||
|
n, err := o.Read(got[off:end])
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
t.Fatalf("read at off %d: %v", off, err)
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
t.Fatalf("zero-length read at off %d", off)
|
||||||
|
}
|
||||||
|
off += n
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Equal(plaintext, got, "short reads must not desync the keystream")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user