From ef7852c030f5c9f95fdfd771b40a7ebed2114ca1 Mon Sep 17 00:00:00 2001 From: yuhan6665 <1588741+yuhan6665@users.noreply.github.com> Date: Sun, 18 May 2025 17:13:16 -0400 Subject: [PATCH] Clear direct dependencies on other ECH package --- go.mod | 3 +- go.sum | 4 -- main/commands/all/tls/ech.go | 49 +++++++------ transport/internet/tls/ech.go | 127 +++++++++++++++++++++++++++++----- 4 files changed, 139 insertions(+), 44 deletions(-) diff --git a/go.mod b/go.mod index 1abcf8cf..83c8ac07 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,6 @@ module github.com/xtls/xray-core go 1.24 require ( - github.com/OmarTariq612/goech v0.0.1 - github.com/cloudflare/circl v1.6.1 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/golang/mock v1.7.0-rc.1 github.com/google/go-cmp v0.7.0 @@ -36,6 +34,7 @@ require ( require ( github.com/andybalholm/brotli v1.1.0 // indirect + github.com/cloudflare/circl v1.6.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect diff --git a/go.sum b/go.sum index 2e4e6914..745b0789 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/OmarTariq612/goech v0.0.1 h1:/0c918Bk1ik65GXDj2k7SOK78DyZr30Jmq9euy1/HXg= -github.com/OmarTariq612/goech v0.0.1/go.mod h1:FVGavL/QEBQDcBpr3fAojoK17xX5k9bicBphrOpP7uM= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= @@ -76,8 +74,6 @@ github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= -github.com/xtls/reality v0.0.0-20250516070713-4df2ec9a5b47 h1:9aJWkgWBwZ83l3j7+hBh3SurvRKuNfCgsSner5n6BcM= -github.com/xtls/reality v0.0.0-20250516070713-4df2ec9a5b47/go.mod h1:bJdU3ExzfUlY40Xxfibq3THW9IHiE8mHu/tEzud5JWM= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= diff --git a/main/commands/all/tls/ech.go b/main/commands/all/tls/ech.go index 701cf937..4ca1411c 100644 --- a/main/commands/all/tls/ech.go +++ b/main/commands/all/tls/ech.go @@ -1,13 +1,15 @@ package tls import ( + "encoding/base64" "encoding/pem" "os" - "github.com/OmarTariq612/goech" - "github.com/cloudflare/circl/hpke" + "github.com/xtls/reality/hpke" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/main/commands/base" + "github.com/xtls/xray-core/transport/internet/tls" + "golang.org/x/crypto/cryptobyte" ) var cmdECH = &base.Command{ @@ -30,34 +32,37 @@ var input_serverName = cmdECH.Flag.String("serverName", "cloudflare-ech.com", "" var input_pem = cmdECH.Flag.Bool("pem", false, "True == turn on pem output") func executeECH(cmd *base.Command, args []string) { - var kem hpke.KEM + var kem uint16 - if *input_pqSignatureSchemesEnabled { - kem = hpke.KEM_X25519_KYBER768_DRAFT00 - } else { - kem = hpke.KEM_X25519_HKDF_SHA256 - } + // if *input_pqSignatureSchemesEnabled { + // kem = 0x30 // hpke.KEM_X25519_KYBER768_DRAFT00 + // } else { + kem = hpke.DHKEM_X25519_HKDF_SHA256 + // } - echKeySet, err := goech.GenerateECHKeySet(0, *input_serverName, kem, nil) + echKeySet, priv, err := tls.GenerateECHKeySet(0, *input_serverName, kem) common.Must(err) - // Make single key set to a list with only one element - ECHConfigList := make(goech.ECHConfigList, 1) - ECHConfigList[0] = echKeySet.ECHConfig - ECHKeySetList := make(goech.ECHKeySetList, 1) - ECHKeySetList[0] = echKeySet - configBuffer, _ := ECHConfigList.MarshalBinary() - keyBuffer, _ := ECHKeySetList.MarshalBinary() - configStr, _ := ECHConfigList.ToBase64() - keySetStr, _ := ECHKeySetList.ToBase64() + configBytes, _ := tls.MarshalBinary(echKeySet) + var b cryptobyte.Builder + b.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) { + child.AddBytes(configBytes) + }) + configBuffer, _ := b.Bytes() + var b2 cryptobyte.Builder + b2.AddUint16(uint16(len(priv))) + b2.AddBytes(priv) + b2.AddUint16(uint16(len(configBytes))) + b2.AddBytes(configBytes) + keyBuffer, _ := b2.Bytes() - configPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBuffer})) - keyPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBuffer})) if *input_pem { + configPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBuffer})) + keyPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBuffer})) os.Stdout.WriteString(configPEM) os.Stdout.WriteString(keyPEM) } else { - os.Stdout.WriteString("ECH config list: \n" + configStr + "\n") - os.Stdout.WriteString("ECH Key sets: \n" + keySetStr + "\n") + os.Stdout.WriteString("ECH config list: \n" + base64.StdEncoding.EncodeToString(configBuffer) + "\n") + os.Stdout.WriteString("ECH Key sets: \n" + base64.StdEncoding.EncodeToString(keyBuffer) + "\n") } } diff --git a/transport/internet/tls/ech.go b/transport/internet/tls/ech.go index 4f2a597e..94a9225b 100644 --- a/transport/internet/tls/ech.go +++ b/transport/internet/tls/ech.go @@ -3,18 +3,24 @@ package tls import ( "bytes" "context" + "crypto/ecdh" + "crypto/rand" "crypto/tls" + "encoding/base64" + "encoding/binary" "io" "net/http" "strings" "sync" "time" - "github.com/OmarTariq612/goech" "github.com/miekg/dns" + "github.com/xtls/reality" + "github.com/xtls/reality/hpke" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/transport/internet" + "golang.org/x/crypto/cryptobyte" ) func ApplyECH(c *Config, config *tls.Config) error { @@ -48,11 +54,10 @@ func ApplyECH(c *Config, config *tls.Config) error { return err } } else { - ECHConfigList, err := goech.ECHConfigListFromBase64(c.EchConfigList) + ECHConfig, err = base64.StdEncoding.DecodeString(c.EchConfigList) if err != nil { return errors.New("Failed to unmarshal ECHConfigList: ", err) } - ECHConfig, _ = ECHConfigList.MarshalBinary() } config.EncryptedClientHelloConfigList = ECHConfig @@ -60,22 +65,11 @@ func ApplyECH(c *Config, config *tls.Config) error { // for server if len(c.EchKeySets) != 0 { - var keys []tls.EncryptedClientHelloKey - KeySets, err := goech.UnmarshalECHKeySetList(c.EchKeySets) + KeySets, err := ConvertToGoECHKeys(c.EchKeySets) if err != nil { return errors.New("Failed to unmarshal ECHKeySetList: ", err) } - for idx, keySet := range KeySets { - ECHConfig, err := keySet.ECHConfig.MarshalBinary() - ECHPrivateKey, err := keySet.PrivateKey.MarshalBinary() - if err != nil { - return errors.New("Failed to marshal ECHKey in index: ", idx, "with err: ", err) - } - keys = append(keys, tls.EncryptedClientHelloKey{ - Config: ECHConfig, - PrivateKey: ECHPrivateKey}) - } - config.EncryptedClientHelloKeys = keys + config.EncryptedClientHelloKeys = KeySets } return nil @@ -237,3 +231,104 @@ func dnsQuery(server string, domain string) ([]byte, uint32, error) { } return []byte{}, 0, errors.New("no ech record found") } + +// reference github.com/OmarTariq612/goech +func MarshalBinary(ech reality.EchConfig) ([]byte, error) { + var b cryptobyte.Builder + b.AddUint16(ech.Version) + b.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) { + child.AddUint8(ech.ConfigID) + child.AddUint16(ech.KemID) + child.AddUint16(uint16(len(ech.PublicKey))) + child.AddBytes(ech.PublicKey) + child.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) { + for _, cipherSuite := range ech.SymmetricCipherSuite { + child.AddUint16(cipherSuite.KDFID) + child.AddUint16(cipherSuite.AEADID) + } + }) + child.AddUint8(ech.MaxNameLength) + child.AddUint8(uint8(len(ech.PublicName))) + child.AddBytes(ech.PublicName) + child.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) { + for _, extention := range ech.Extensions { + child.AddUint16(extention.Type) + child.AddBytes(extention.Data) + } + }) + }) + return b.Bytes() +} + +var ErrInvalidLen = errors.New("goech: invalid length") + +func ConvertToGoECHKeys(data []byte) ([]tls.EncryptedClientHelloKey, error) { + var keys []tls.EncryptedClientHelloKey + s := cryptobyte.String(data) + for !s.Empty() { + if len(s) < 2 { + return keys, ErrInvalidLen + } + keyLength := int(binary.BigEndian.Uint16(s[:2])) + if len(s) < keyLength+4 { + return keys, ErrInvalidLen + } + configLength := int(binary.BigEndian.Uint16(s[keyLength+2 : keyLength+4])) + if len(s) < 2+keyLength+2+configLength { + return keys, ErrInvalidLen + } + child := cryptobyte.String(s[:2+keyLength+2+configLength]) + var ( + sk, config cryptobyte.String + ) + if !child.ReadUint16LengthPrefixed(&sk) || !child.ReadUint16LengthPrefixed(&config) || !child.Empty() { + return keys, ErrInvalidLen + } + if !s.Skip(2 + keyLength + 2 + configLength) { + return keys, ErrInvalidLen + } + keys = append(keys, tls.EncryptedClientHelloKey{ + Config: config, + PrivateKey: sk, + }) + } + return keys, nil +} + +const ExtensionEncryptedClientHello = 0xfe0d +const KDF_HKDF_SHA384 = 0x0002 +const KDF_HKDF_SHA512 = 0x0003 + +func GenerateECHKeySet(configID uint8, domain string, kem uint16) (reality.EchConfig, []byte, error) { + config := reality.EchConfig{ + Version: ExtensionEncryptedClientHello, + ConfigID: configID, + PublicName: []byte(domain), + KemID: kem, + SymmetricCipherSuite: []reality.EchCipher{ + {KDFID: hpke.KDF_HKDF_SHA256, AEADID: hpke.AEAD_AES_128_GCM}, + {KDFID: hpke.KDF_HKDF_SHA256, AEADID: hpke.AEAD_AES_256_GCM}, + {KDFID: hpke.KDF_HKDF_SHA256, AEADID: hpke.AEAD_ChaCha20Poly1305}, + {KDFID: KDF_HKDF_SHA384, AEADID: hpke.AEAD_AES_128_GCM}, + {KDFID: KDF_HKDF_SHA384, AEADID: hpke.AEAD_AES_256_GCM}, + {KDFID: KDF_HKDF_SHA384, AEADID: hpke.AEAD_ChaCha20Poly1305}, + {KDFID: KDF_HKDF_SHA512, AEADID: hpke.AEAD_AES_128_GCM}, + {KDFID: KDF_HKDF_SHA512, AEADID: hpke.AEAD_AES_256_GCM}, + {KDFID: KDF_HKDF_SHA512, AEADID: hpke.AEAD_ChaCha20Poly1305}, + }, + MaxNameLength: 0, + Extensions: nil, + } + // if kem == hpke.DHKEM_X25519_HKDF_SHA256 { + curve := ecdh.X25519() + priv := make([]byte, 32) //x25519 + _, err := io.ReadFull(rand.Reader, priv) + if err != nil { + return config, nil, err + } + privKey, _ := curve.NewPrivateKey(priv) + config.PublicKey = privKey.PublicKey().Bytes(); + return config, priv, nil + // } + // TODO: add mlkem768 (former kyber768 draft00). The golang mlkem private key is 64 bytes seed? +}