diff --git a/cycletls/extensions.go b/cycletls/extensions.go index 222ad08d..d47d4975 100644 --- a/cycletls/extensions.go +++ b/cycletls/extensions.go @@ -103,38 +103,6 @@ func (c *CustomApplicationSettingsExtension) GetPreservedID() uint16 { return c.OriginalID } -// CustomECHExtension preserves the original extension ID for Encrypted Client Hello -type CustomECHExtension struct { - *utls.GenericExtension - OriginalID uint16 -} - -// NewCustomECHExtension creates a new ECH extension with preserved ID -func NewCustomECHExtension(extID uint16) *CustomECHExtension { - // ECH extension data - using a simple placeholder for now - // In a full implementation, this would contain actual ECH configuration - data := []byte{ - 0x00, 0x00, // config_id - 0x00, 0x00, // kem_id placeholder - 0x00, 0x00, // public_key length - // public_key would follow - 0x00, 0x00, // cipher_suites length - // cipher_suites would follow - } - - return &CustomECHExtension{ - GenericExtension: &utls.GenericExtension{ - Id: extID, - Data: data, - }, - OriginalID: extID, - } -} - -// GetPreservedID returns the original extension ID -func (c *CustomECHExtension) GetPreservedID() uint16 { - return c.OriginalID -} // CustomCompressCertificateExtension preserves the original extension ID type CustomCompressCertificateExtension struct { @@ -305,7 +273,7 @@ func CreateExtensionFromID(extID uint16, tlsVersion uint16, components *JA4RComp // Add post-quantum curves if supported if tlsVersion == utls.VersionTLS13 { // Add X25519MLKEM768 if supported by uTLS version - curves = append([]utls.CurveID{utls.X25519}, curves...) + curves = append([]utls.CurveID{utls.X25519MLKEM768}, curves...) } return &utls.SupportedCurvesExtension{Curves: curves} case 0x000b: // EC Point Formats @@ -396,10 +364,9 @@ func CreateExtensionFromID(extID uint16, tlsVersion uint16, components *JA4RComp case 0x0033: // Key Share if tlsVersion == utls.VersionTLS13 { keyShares := []utls.KeyShare{ + {Group: utls.X25519MLKEM768}, {Group: utls.X25519}, - {Group: utls.CurveP256}, } - // Add post-quantum key share if supported return &utls.KeyShareExtension{ KeyShares: keyShares, } @@ -412,7 +379,10 @@ func CreateExtensionFromID(extID uint16, tlsVersion uint16, components *JA4RComp case 0x6399: // X25519Kyber768Draft00 (Post-Quantum) - 25497 return NewCustomPostQuantumExtension(extID, 0x6399) case 0xfe0d: // Encrypted Client Hello (ECH) - 65037 - return NewCustomECHExtension(extID) + if disableGrease { + return nil + } + return utls.BoringGREASEECH() case 0xff01: // Renegotiation Info - 65281 return &utls.RenegotiationInfoExtension{ Renegotiation: utls.RenegotiateOnceAsClient, diff --git a/cycletls/tests/unit/issue409_test.go b/cycletls/tests/unit/issue409_test.go new file mode 100644 index 00000000..7975cb59 --- /dev/null +++ b/cycletls/tests/unit/issue409_test.go @@ -0,0 +1,102 @@ +package unit + +import ( + "crypto/tls" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Danny-Dasilva/CycleTLS/cycletls" + utls "github.com/refraction-networking/utls" +) + +func TestIssue409_JA4RHandshake(t *testing.T) { + ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + })) + ts.TLS = &tls.Config{ + MinVersion: tls.VersionTLS12, + MaxVersion: tls.VersionTLS13, + } + ts.StartTLS() + defer ts.Close() + + client := cycletls.Init() + defer client.Close() + + 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" + + resp, err := client.Do(ts.URL, cycletls.Options{ + Ja4r: ja4r, + UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36", + InsecureSkipVerify: true, + }, "GET") + + if err != nil { + t.Fatalf("client.Do failed: %v", err) + } + if resp.Status != 200 { + t.Fatalf("expected status 200, got %d, body: %s", resp.Status, resp.Body) + } +} + +func TestIssue409_CreateExtensionECH(t *testing.T) { + // When GREASE is enabled, 0xfe0d should return valid BoringGREASEECH extension + ext := cycletls.CreateExtensionFromID(0xfe0d, utls.VersionTLS13, nil, false, "example.com") + if ext == nil { + t.Fatal("expected non-nil extension for 0xfe0d when disableGrease is false") + } + if _, ok := ext.(*utls.GREASEEncryptedClientHelloExtension); !ok { + t.Fatalf("expected *utls.GREASEEncryptedClientHelloExtension, got %T", ext) + } + + // When GREASE is disabled, 0xfe0d should return nil + extDisabled := cycletls.CreateExtensionFromID(0xfe0d, utls.VersionTLS13, nil, true, "example.com") + if extDisabled != nil { + t.Fatalf("expected nil extension for 0xfe0d when disableGrease is true, got %T", extDisabled) + } +} + +func TestIssue409_SupportedCurvesNoDuplicate(t *testing.T) { + ext := cycletls.CreateExtensionFromID(0x000a, utls.VersionTLS13, nil, false, "example.com") + if ext == nil { + t.Fatal("expected non-nil SupportedCurves extension") + } + curvesExt, ok := ext.(*utls.SupportedCurvesExtension) + if !ok { + t.Fatalf("expected *utls.SupportedCurvesExtension, got %T", ext) + } + + seen := make(map[utls.CurveID]bool) + for _, c := range curvesExt.Curves { + if seen[c] { + t.Fatalf("duplicate curve %v found in SupportedCurvesExtension: %v", c, curvesExt.Curves) + } + seen[c] = true + } + + if len(curvesExt.Curves) == 0 || curvesExt.Curves[0] != utls.X25519MLKEM768 { + t.Fatalf("expected first curve to be X25519MLKEM768 in TLS 1.3, got %v", curvesExt.Curves) + } +} + +func TestIssue409_KeyShare(t *testing.T) { + ext := cycletls.CreateExtensionFromID(0x0033, utls.VersionTLS13, nil, false, "example.com") + if ext == nil { + t.Fatal("expected non-nil KeyShare extension") + } + ksExt, ok := ext.(*utls.KeyShareExtension) + if !ok { + t.Fatalf("expected *utls.KeyShareExtension, got %T", ext) + } + if len(ksExt.KeyShares) < 2 { + t.Fatalf("expected at least 2 key shares, got %d", len(ksExt.KeyShares)) + } + if ksExt.KeyShares[0].Group != utls.X25519MLKEM768 { + t.Fatalf("expected first key share to be X25519MLKEM768, got %v", ksExt.KeyShares[0].Group) + } + if ksExt.KeyShares[1].Group != utls.X25519 { + t.Fatalf("expected second key share to be X25519, got %v", ksExt.KeyShares[1].Group) + } +}