diff --git a/component/ech/echparser/echparser.go b/component/ech/echparser/echparser.go new file mode 100644 index 00000000..4d3b213f --- /dev/null +++ b/component/ech/echparser/echparser.go @@ -0,0 +1,147 @@ +package echparser + +import ( + "errors" + "fmt" + + "golang.org/x/crypto/cryptobyte" +) + +// export from std's crypto/tls/ech.go + +const extensionEncryptedClientHello = 0xfe0d + +type ECHCipher struct { + KDFID uint16 + AEADID uint16 +} + +type ECHExtension struct { + Type uint16 + Data []byte +} + +type ECHConfig struct { + raw []byte + + Version uint16 + Length uint16 + + ConfigID uint8 + KemID uint16 + PublicKey []byte + SymmetricCipherSuite []ECHCipher + + MaxNameLength uint8 + PublicName []byte + Extensions []ECHExtension +} + +var ErrMalformedECHConfigList = errors.New("tls: malformed ECHConfigList") + +type EchConfigErr struct { + field string +} + +func (e *EchConfigErr) Error() string { + if e.field == "" { + return "tls: malformed ECHConfig" + } + return fmt.Sprintf("tls: malformed ECHConfig, invalid %s field", e.field) +} + +func ParseECHConfig(enc []byte) (skip bool, ec ECHConfig, err error) { + s := cryptobyte.String(enc) + ec.raw = []byte(enc) + if !s.ReadUint16(&ec.Version) { + return false, ECHConfig{}, &EchConfigErr{"version"} + } + if !s.ReadUint16(&ec.Length) { + return false, ECHConfig{}, &EchConfigErr{"length"} + } + if len(ec.raw) < int(ec.Length)+4 { + return false, ECHConfig{}, &EchConfigErr{"length"} + } + ec.raw = ec.raw[:ec.Length+4] + if ec.Version != extensionEncryptedClientHello { + s.Skip(int(ec.Length)) + return true, ECHConfig{}, nil + } + if !s.ReadUint8(&ec.ConfigID) { + return false, ECHConfig{}, &EchConfigErr{"config_id"} + } + if !s.ReadUint16(&ec.KemID) { + return false, ECHConfig{}, &EchConfigErr{"kem_id"} + } + if !s.ReadUint16LengthPrefixed((*cryptobyte.String)(&ec.PublicKey)) { + return false, ECHConfig{}, &EchConfigErr{"public_key"} + } + var cipherSuites cryptobyte.String + if !s.ReadUint16LengthPrefixed(&cipherSuites) { + return false, ECHConfig{}, &EchConfigErr{"cipher_suites"} + } + for !cipherSuites.Empty() { + var c ECHCipher + if !cipherSuites.ReadUint16(&c.KDFID) { + return false, ECHConfig{}, &EchConfigErr{"cipher_suites kdf_id"} + } + if !cipherSuites.ReadUint16(&c.AEADID) { + return false, ECHConfig{}, &EchConfigErr{"cipher_suites aead_id"} + } + ec.SymmetricCipherSuite = append(ec.SymmetricCipherSuite, c) + } + if !s.ReadUint8(&ec.MaxNameLength) { + return false, ECHConfig{}, &EchConfigErr{"maximum_name_length"} + } + var publicName cryptobyte.String + if !s.ReadUint8LengthPrefixed(&publicName) { + return false, ECHConfig{}, &EchConfigErr{"public_name"} + } + ec.PublicName = publicName + var extensions cryptobyte.String + if !s.ReadUint16LengthPrefixed(&extensions) { + return false, ECHConfig{}, &EchConfigErr{"extensions"} + } + for !extensions.Empty() { + var e ECHExtension + if !extensions.ReadUint16(&e.Type) { + return false, ECHConfig{}, &EchConfigErr{"extensions type"} + } + if !extensions.ReadUint16LengthPrefixed((*cryptobyte.String)(&e.Data)) { + return false, ECHConfig{}, &EchConfigErr{"extensions data"} + } + ec.Extensions = append(ec.Extensions, e) + } + + return false, ec, nil +} + +// ParseECHConfigList parses a draft-ietf-tls-esni-18 ECHConfigList, returning a +// slice of parsed ECHConfigs, in the same order they were parsed, or an error +// if the list is malformed. +func ParseECHConfigList(data []byte) ([]ECHConfig, error) { + s := cryptobyte.String(data) + var length uint16 + if !s.ReadUint16(&length) { + return nil, ErrMalformedECHConfigList + } + if length != uint16(len(data)-2) { + return nil, ErrMalformedECHConfigList + } + var configs []ECHConfig + for len(s) > 0 { + if len(s) < 4 { + return nil, errors.New("tls: malformed ECHConfig") + } + configLen := uint16(s[2])<<8 | uint16(s[3]) + skip, ec, err := ParseECHConfig(s) + if err != nil { + return nil, err + } + s = s[configLen+4:] + if !skip { + configs = append(configs, ec) + } + } + return configs, nil +} diff --git a/component/ech/key_test.go b/component/ech/key_test.go new file mode 100644 index 00000000..d6097aa0 --- /dev/null +++ b/component/ech/key_test.go @@ -0,0 +1,30 @@ +package ech + +import ( + "encoding/base64" + "testing" + + "github.com/metacubex/mihomo/component/ech/echparser" +) + +func TestGenECHConfig(t *testing.T) { + domain := "www.example.com" + configBase64, _, err := GenECHConfig(domain) + if err != nil { + t.Error(err) + } + echConfigList, err := base64.StdEncoding.DecodeString(configBase64) + if err != nil { + t.Error(err) + } + echConfigs, err := echparser.ParseECHConfigList(echConfigList) + if err != nil { + t.Error(err) + } + if len(echConfigs) == 0 { + t.Error("no ech config") + } + if publicName := string(echConfigs[0].PublicName); publicName != domain { + t.Error("ech config domain error, expect ", domain, " got", publicName) + } +}