faketls: emit GREASE bytes and a real padding extension
Go / Lint (old) (push) Failing after 4m39s
Go / Lint (latest) (push) Failing after 4m39s
Go / Lint (old) (pull_request) Failing after 4m39s
Go / Lint (latest) (pull_request) Failing after 4m42s

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>
This commit is contained in:
Igor Artamonov
2026-05-01 11:15:03 +03:00
parent aab48f0dbe
commit 64bf6bfe90
4 changed files with 185 additions and 40 deletions
+41 -6
View File
@@ -14,7 +14,26 @@ import (
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) {
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)
b.Expand(n)
}
G := func(_ int) {
b.Expand(0)
G := func(n int) {
v := grease[n]
b.Buf = append(b.Buf, v, v)
}
R := func() {
b.Buf = append(b.Buf, sessionID[:]...)
@@ -83,9 +103,18 @@ func createClientHello(b *bin.Buffer, sessionID [32]byte, domain string, key [32
G(3)
S("\x00\x01\x00\x00\x15")
if pad := clientHelloLength - b.Len(); pad > 0 {
b.Expand(pad)
// 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
}
@@ -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.
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{})
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)
+1 -1
View File
@@ -46,7 +46,7 @@ func (o *FakeTLS) Handshake(protocol [4]byte, dc int, s mtproxy.Secret) error {
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 {
return errors.Wrap(err, "send ClientHello")
}
+42 -33
View File
@@ -10,6 +10,15 @@ import (
"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) {
a := require.New(t)
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))
b := bytes.NewBuffer(nil)
_, err := writeClientHello(b, c, sessionID, "google.com", secret[:])
_, err := writeClientHello(b, zeroReader{}, c, sessionID, "google.com", secret[:])
a.NoError(err)
testVector := []byte{
0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03, 0xf9, 0x75, 0x5f, 0xdd, 0xb9,
0xe3, 0x46, 0x57, 0x5a, 0x26, 0x71, 0xfa, 0x29, 0x7f, 0xab, 0xf0, 0xa1, 0xf3, 0x69, 0x4f, 0x72,
0xe0, 0xc3, 0x8f, 0x62, 0x77, 0x5c, 0x8f, 0x5a, 0xf8, 0xa2, 0xa9, 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, 0x20, 0x13, 0x01,
0x13, 0x02, 0x13, 0x03, 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, 0xcc, 0xa9, 0xcc, 0xa8,
0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0x01, 0x00, 0x01, 0x93,
0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x0d, 0x00, 0x00, 0x0a, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x17, 0x00, 0x00, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x0a,
0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00,
0x00, 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74,
0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05,
0x01, 0x08, 0x06, 0x06, 0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x33, 0x00, 0x2b, 0x00, 0x29, 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, 0x2d, 0x00, 0x02, 0x01, 0x01, 0x00, 0x2b, 0x00, 0x0b,
0x0a, 0x03, 0x04, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00, 0x1b, 0x00, 0x03, 0x02, 0x00, 0x02,
0x00, 0x01, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03, 0xa9, 0xa8, 0x7f, 0x37, 0x9b,
0x09, 0x80, 0x6a, 0xf3, 0xff, 0x78, 0x4a, 0x6c, 0x4e, 0xbd, 0xdd, 0x94, 0x31, 0x8e, 0x7c, 0x09,
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, 0x20, 0x0a, 0x0a,
0x13, 0x01, 0x13, 0x02, 0x13, 0x03, 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, 0xcc, 0xa9,
0xcc, 0xa8, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0x01, 0x00,
0x01, 0x93, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x0d, 0x00, 0x00, 0x0a, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x17, 0x00, 0x00, 0xff, 0x01, 0x00,
0x01, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x0a, 0x0a, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18,
0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c,
0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x00, 0x05, 0x00, 0x05,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, 0x03, 0x08, 0x04, 0x04,
0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08, 0x06, 0x06, 0x01, 0x00, 0x12, 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, 0x2d,
0x00, 0x02, 0x01, 0x01, 0x00, 0x2b, 0x00, 0x0b, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x03, 0x03, 0x03,
0x02, 0x03, 0x01, 0x00, 0x1b, 0x00, 0x03, 0x02, 0x00, 0x02, 0x1a, 0x1a, 0x00, 0x01, 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,
}
a.Equal(testVector, b.Bytes())
+101
View File
@@ -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-----`)