Skip to content

Commit 8dd38fe

Browse files
committed
fix: resolve TLS error decoding message for ECH extension
Replace the placeholder CustomECHExtension with utls.BoringGREASEECH for extension 0xfe0d, and omit it when disableGrease is active. Update TLS 1.3 KeyShareExtension to provide X25519MLKEM768 alongside X25519, matching the order in SupportedCurvesExtension and preventing server HelloRetryRequest failures. Add comprehensive unit tests verifying handshake, ECH extension generation, and curve order. Signed-off-by: Adam Magued <adamismailmageud@gmail.com>
1 parent 9671e04 commit 8dd38fe

2 files changed

Lines changed: 108 additions & 36 deletions

File tree

‎cycletls/extensions.go‎

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -103,38 +103,6 @@ func (c *CustomApplicationSettingsExtension) GetPreservedID() uint16 {
103103
return c.OriginalID
104104
}
105105

106-
// CustomECHExtension preserves the original extension ID for Encrypted Client Hello
107-
type CustomECHExtension struct {
108-
*utls.GenericExtension
109-
OriginalID uint16
110-
}
111-
112-
// NewCustomECHExtension creates a new ECH extension with preserved ID
113-
func NewCustomECHExtension(extID uint16) *CustomECHExtension {
114-
// ECH extension data - using a simple placeholder for now
115-
// In a full implementation, this would contain actual ECH configuration
116-
data := []byte{
117-
0x00, 0x00, // config_id
118-
0x00, 0x00, // kem_id placeholder
119-
0x00, 0x00, // public_key length
120-
// public_key would follow
121-
0x00, 0x00, // cipher_suites length
122-
// cipher_suites would follow
123-
}
124-
125-
return &CustomECHExtension{
126-
GenericExtension: &utls.GenericExtension{
127-
Id: extID,
128-
Data: data,
129-
},
130-
OriginalID: extID,
131-
}
132-
}
133-
134-
// GetPreservedID returns the original extension ID
135-
func (c *CustomECHExtension) GetPreservedID() uint16 {
136-
return c.OriginalID
137-
}
138106

139107
// CustomCompressCertificateExtension preserves the original extension ID
140108
type CustomCompressCertificateExtension struct {
@@ -305,7 +273,7 @@ func CreateExtensionFromID(extID uint16, tlsVersion uint16, components *JA4RComp
305273
// Add post-quantum curves if supported
306274
if tlsVersion == utls.VersionTLS13 {
307275
// Add X25519MLKEM768 if supported by uTLS version
308-
curves = append([]utls.CurveID{utls.X25519}, curves...)
276+
curves = append([]utls.CurveID{utls.X25519MLKEM768}, curves...)
309277
}
310278
return &utls.SupportedCurvesExtension{Curves: curves}
311279
case 0x000b: // EC Point Formats
@@ -396,10 +364,9 @@ func CreateExtensionFromID(extID uint16, tlsVersion uint16, components *JA4RComp
396364
case 0x0033: // Key Share
397365
if tlsVersion == utls.VersionTLS13 {
398366
keyShares := []utls.KeyShare{
367+
{Group: utls.X25519MLKEM768},
399368
{Group: utls.X25519},
400-
{Group: utls.CurveP256},
401369
}
402-
// Add post-quantum key share if supported
403370
return &utls.KeyShareExtension{
404371
KeyShares: keyShares,
405372
}
@@ -412,7 +379,10 @@ func CreateExtensionFromID(extID uint16, tlsVersion uint16, components *JA4RComp
412379
case 0x6399: // X25519Kyber768Draft00 (Post-Quantum) - 25497
413380
return NewCustomPostQuantumExtension(extID, 0x6399)
414381
case 0xfe0d: // Encrypted Client Hello (ECH) - 65037
415-
return NewCustomECHExtension(extID)
382+
if disableGrease {
383+
return nil
384+
}
385+
return utls.BoringGREASEECH()
416386
case 0xff01: // Renegotiation Info - 65281
417387
return &utls.RenegotiationInfoExtension{
418388
Renegotiation: utls.RenegotiateOnceAsClient,
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package unit
2+
3+
import (
4+
"crypto/tls"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/Danny-Dasilva/CycleTLS/cycletls"
10+
utls "github.com/refraction-networking/utls"
11+
)
12+
13+
func TestIssue409_JA4RHandshake(t *testing.T) {
14+
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
15+
w.WriteHeader(http.StatusOK)
16+
_, _ = w.Write([]byte("ok"))
17+
}))
18+
ts.TLS = &tls.Config{
19+
MinVersion: tls.VersionTLS12,
20+
MaxVersion: tls.VersionTLS13,
21+
}
22+
ts.StartTLS()
23+
defer ts.Close()
24+
25+
client := cycletls.Init()
26+
defer client.Close()
27+
28+
ja4r := "t13d1516h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0000,0005,000a,000b,000d,0012,0017,001b,0023,002b,002d,0033,44cd,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601"
29+
30+
resp, err := client.Do(ts.URL, cycletls.Options{
31+
Ja4r: ja4r,
32+
UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
33+
InsecureSkipVerify: true,
34+
}, "GET")
35+
36+
if err != nil {
37+
t.Fatalf("client.Do failed: %v", err)
38+
}
39+
if resp.Status != 200 {
40+
t.Fatalf("expected status 200, got %d, body: %s", resp.Status, resp.Body)
41+
}
42+
}
43+
44+
func TestIssue409_CreateExtensionECH(t *testing.T) {
45+
// When GREASE is enabled, 0xfe0d should return valid BoringGREASEECH extension
46+
ext := cycletls.CreateExtensionFromID(0xfe0d, utls.VersionTLS13, nil, false, "example.com")
47+
if ext == nil {
48+
t.Fatal("expected non-nil extension for 0xfe0d when disableGrease is false")
49+
}
50+
if _, ok := ext.(*utls.GREASEEncryptedClientHelloExtension); !ok {
51+
t.Fatalf("expected *utls.GREASEEncryptedClientHelloExtension, got %T", ext)
52+
}
53+
54+
// When GREASE is disabled, 0xfe0d should return nil
55+
extDisabled := cycletls.CreateExtensionFromID(0xfe0d, utls.VersionTLS13, nil, true, "example.com")
56+
if extDisabled != nil {
57+
t.Fatalf("expected nil extension for 0xfe0d when disableGrease is true, got %T", extDisabled)
58+
}
59+
}
60+
61+
func TestIssue409_SupportedCurvesNoDuplicate(t *testing.T) {
62+
ext := cycletls.CreateExtensionFromID(0x000a, utls.VersionTLS13, nil, false, "example.com")
63+
if ext == nil {
64+
t.Fatal("expected non-nil SupportedCurves extension")
65+
}
66+
curvesExt, ok := ext.(*utls.SupportedCurvesExtension)
67+
if !ok {
68+
t.Fatalf("expected *utls.SupportedCurvesExtension, got %T", ext)
69+
}
70+
71+
seen := make(map[utls.CurveID]bool)
72+
for _, c := range curvesExt.Curves {
73+
if seen[c] {
74+
t.Fatalf("duplicate curve %v found in SupportedCurvesExtension: %v", c, curvesExt.Curves)
75+
}
76+
seen[c] = true
77+
}
78+
79+
if len(curvesExt.Curves) == 0 || curvesExt.Curves[0] != utls.X25519MLKEM768 {
80+
t.Fatalf("expected first curve to be X25519MLKEM768 in TLS 1.3, got %v", curvesExt.Curves)
81+
}
82+
}
83+
84+
func TestIssue409_KeyShare(t *testing.T) {
85+
ext := cycletls.CreateExtensionFromID(0x0033, utls.VersionTLS13, nil, false, "example.com")
86+
if ext == nil {
87+
t.Fatal("expected non-nil KeyShare extension")
88+
}
89+
ksExt, ok := ext.(*utls.KeyShareExtension)
90+
if !ok {
91+
t.Fatalf("expected *utls.KeyShareExtension, got %T", ext)
92+
}
93+
if len(ksExt.KeyShares) < 2 {
94+
t.Fatalf("expected at least 2 key shares, got %d", len(ksExt.KeyShares))
95+
}
96+
if ksExt.KeyShares[0].Group != utls.X25519MLKEM768 {
97+
t.Fatalf("expected first key share to be X25519MLKEM768, got %v", ksExt.KeyShares[0].Group)
98+
}
99+
if ksExt.KeyShares[1].Group != utls.X25519 {
100+
t.Fatalf("expected second key share to be X25519, got %v", ksExt.KeyShares[1].Group)
101+
}
102+
}

0 commit comments

Comments
 (0)