64bf6bfe90
The ClientHello builder used a closure G(n) that was supposed to insert two random GREASE bytes (RFC 8701, 0x?A?A pattern) at known positions but expanded the buffer by zero. Every grease slot was therefore omitted, and the trailing padding extension was written as a bare ext id 0x0015 followed by raw zeros — its length field was never set. Concretely, the old output looked structurally invalid to mtg's faketls validator: the cipher list was off by two, supported_groups declared a list_length larger than its body, and what should have been the padding extension parsed as a stream of empty server_name extensions. mtg responded with a fatal TLS Alert (description 50, decode_error) and shut the connection. Fix: - generate seven distinct GREASE bytes per ClientHello, with the tdlib constraint grease[3] != grease[4] - thread an io.Reader through writeClientHello so generation is deterministic in tests and keyed off the FakeTLS rand source in prod - replace the trailing zero-pad with a proper padding extension whose length field is computed so the ClientHello is exactly 517 bytes Add a regression test (structure_test.go) that feeds the result to crypto/tls.Server: it must not return decode_error / malformed / syntax errors. The previous output failed this; the new output passes. The TestTLS golden vector is regenerated for the new layout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
160 lines
4.0 KiB
Go
160 lines
4.0 KiB
Go
package faketls
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"io"
|
|
|
|
"github.com/go-faster/errors"
|
|
|
|
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
|
|
"go.mau.fi/mautrix-telegram/pkg/gotd/clock"
|
|
)
|
|
|
|
const clientHelloLength = 517
|
|
|
|
// 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) {
|
|
b.Buf = append(b.Buf, s...)
|
|
}
|
|
Z := func(n int) {
|
|
randomOffset = len(b.Buf)
|
|
b.Expand(n)
|
|
}
|
|
G := func(n int) {
|
|
v := grease[n]
|
|
b.Buf = append(b.Buf, v, v)
|
|
}
|
|
R := func() {
|
|
b.Buf = append(b.Buf, sessionID[:]...)
|
|
}
|
|
D := func() {
|
|
b.Buf = append(b.Buf, domain...)
|
|
}
|
|
K := func() {
|
|
b.Buf = append(b.Buf, key[:]...)
|
|
}
|
|
var stack []int
|
|
Open := func() {
|
|
stack = append(stack, b.Len())
|
|
b.Expand(2)
|
|
}
|
|
Close := func() {
|
|
lastIdx := len(stack) - 1
|
|
s := stack[lastIdx]
|
|
stack = stack[:lastIdx]
|
|
|
|
length := b.Len() - (s + 2)
|
|
binary.BigEndian.PutUint16(b.Buf[s:], uint16(length))
|
|
}
|
|
|
|
S("\x16\x03\x01\x02\x00\x01\x00\x01\xfc\x03\x03")
|
|
Z(32)
|
|
S("\x20")
|
|
R()
|
|
S("\x00\x20")
|
|
G(0)
|
|
S("\x13\x01\x13\x02\x13\x03\xc0\x2b\xc0\x2f\xc0\x2c\xc0\x30\xcc\xa9" +
|
|
"\xcc\xa8\xc0\x13\xc0\x14\x00\x9c\x00\x9d\x00\x2f\x00\x35\x01\x00" +
|
|
"\x01\x93")
|
|
G(2)
|
|
S("\x00\x00\x00\x00")
|
|
Open()
|
|
Open()
|
|
S("\x00")
|
|
Open()
|
|
D()
|
|
Close()
|
|
Close()
|
|
Close()
|
|
S("\x00\x17\x00\x00\xff\x01\x00\x01\x00\x00\x0a\x00\x0a\x00\x08")
|
|
G(4)
|
|
S("\x00\x1d\x00\x17\x00\x18\x00\x0b\x00\x02\x01\x00\x00\x23\x00\x00" +
|
|
"\x00\x10\x00\x0e\x00\x0c\x02\x68\x32\x08\x68\x74\x74\x70\x2f\x31" +
|
|
"\x2e\x31\x00\x05\x00\x05\x01\x00\x00\x00\x00\x00\x0d\x00\x12\x00" +
|
|
"\x10\x04\x03\x08\x04\x04\x01\x05\x03\x08\x05\x05\x01\x08\x06\x06" +
|
|
"\x01\x00\x12\x00\x00\x00\x33\x00\x2b\x00\x29")
|
|
G(4)
|
|
S("\x00\x01\x00\x00\x1d\x00\x20")
|
|
K()
|
|
S("\x00\x2d\x00\x02\x01\x01\x00\x2b\x00\x0b\x0a")
|
|
G(6)
|
|
S("\x03\x04\x03\x03\x03\x02\x03\x01\x00\x1b\x00\x03\x02\x00\x02")
|
|
G(3)
|
|
S("\x00\x01\x00\x00\x15")
|
|
|
|
// Padding extension (id 0x0015 already written above): write its
|
|
// 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
|
|
}
|
|
|
|
// writeClientHello writes faketls ClientHello.
|
|
//
|
|
// See https://tools.ietf.org/html/rfc5246#section-7.4.1.1.
|
|
func writeClientHello(
|
|
w io.Writer,
|
|
rng io.Reader,
|
|
now clock.Clock,
|
|
sessionID [32]byte,
|
|
domain string,
|
|
secret []byte,
|
|
) (r [32]byte, err error) {
|
|
grease, err := generateGrease(rng)
|
|
if err != nil {
|
|
return [32]byte{}, err
|
|
}
|
|
|
|
b := &bin.Buffer{
|
|
Buf: make([]byte, 0, 576),
|
|
}
|
|
randomOffset := createClientHello(b, sessionID, domain, [32]byte{}, grease)
|
|
|
|
// https://github.com/tdlib/td/blob/27d3fdd09d90f6b77ecbcce50b1e86dc4b3dd366/td/mtproto/TlsInit.cpp#L380-L384
|
|
mac := hmac.New(sha256.New, secret)
|
|
if _, err := mac.Write(b.Buf); err != nil {
|
|
return [32]byte{}, errors.Wrap(err, "hmac write")
|
|
}
|
|
|
|
s := mac.Sum(nil)
|
|
copy(b.Buf[randomOffset:randomOffset+32], s)
|
|
// Overwrite last 4 bytes using final := original ^ timestamp.
|
|
old := binary.LittleEndian.Uint32(b.Buf[randomOffset+28 : randomOffset+32])
|
|
old ^= uint32(now.Now().Unix())
|
|
binary.LittleEndian.PutUint32(b.Buf[randomOffset+28:randomOffset+32], old)
|
|
|
|
// Copy ClientRandom for later use.
|
|
copy(r[:], b.Buf[randomOffset:randomOffset+32])
|
|
_, err = w.Write(b.Buf)
|
|
return r, err
|
|
}
|