diff --git a/pkg/gotd/mtproxy/faketls/client_hello.go b/pkg/gotd/mtproxy/faketls/client_hello.go index 4efe5ba8..7d51489b 100644 --- a/pkg/gotd/mtproxy/faketls/client_hello.go +++ b/pkg/gotd/mtproxy/faketls/client_hello.go @@ -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) diff --git a/pkg/gotd/mtproxy/faketls/faketls.go b/pkg/gotd/mtproxy/faketls/faketls.go index 2833f4de..feb78569 100644 --- a/pkg/gotd/mtproxy/faketls/faketls.go +++ b/pkg/gotd/mtproxy/faketls/faketls.go @@ -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") } diff --git a/pkg/gotd/mtproxy/faketls/faketls_test.go b/pkg/gotd/mtproxy/faketls/faketls_test.go index 5cee1e5a..9d55f639 100644 --- a/pkg/gotd/mtproxy/faketls/faketls_test.go +++ b/pkg/gotd/mtproxy/faketls/faketls_test.go @@ -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()) diff --git a/pkg/gotd/mtproxy/faketls/structure_test.go b/pkg/gotd/mtproxy/faketls/structure_test.go new file mode 100644 index 00000000..7f0e643f --- /dev/null +++ b/pkg/gotd/mtproxy/faketls/structure_test.go @@ -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-----`)