Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 6 additions & 36 deletions cycletls/extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
}
Expand All @@ -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,
Expand Down
102 changes: 102 additions & 0 deletions cycletls/tests/unit/issue409_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}