diff --git a/common/tls/ech_shared.go b/common/tls/ech_shared.go index f27f0727..1cdfb7e1 100644 --- a/common/tls/ech_shared.go +++ b/common/tls/ech_shared.go @@ -1,14 +1,11 @@ package tls import ( - "bytes" - "encoding/binary" + "crypto/ecdh" + "crypto/rand" "encoding/pem" - E "github.com/sagernet/sing/common/exceptions" - - "github.com/cloudflare/circl/hpke" - "github.com/cloudflare/circl/kem" + "golang.org/x/crypto/cryptobyte" ) type ECHCapableConfig interface { @@ -17,145 +14,68 @@ type ECHCapableConfig interface { SetECHConfigList([]byte) } -func ECHKeygenDefault(serverName string) (configPem string, keyPem string, err error) { - cipherSuites := []echCipherSuite{ - { - kdf: hpke.KDF_HKDF_SHA256, - aead: hpke.AEAD_AES128GCM, - }, { - kdf: hpke.KDF_HKDF_SHA256, - aead: hpke.AEAD_ChaCha20Poly1305, - }, - } - keyConfig := []myECHKeyConfig{ - {id: 0, kem: hpke.KEM_X25519_HKDF_SHA256}, - } - - keyPairs, err := echKeygen(0xfe0d, serverName, keyConfig, cipherSuites) +func ECHKeygenDefault(publicName string) (configPem string, keyPem string, err error) { + echKey, err := ecdh.X25519().GenerateKey(rand.Reader) if err != nil { return } - - var configBuffer bytes.Buffer - var totalLen uint16 - for _, keyPair := range keyPairs { - totalLen += uint16(len(keyPair.rawConf)) + echConfig, err := marshalECHConfig(0, echKey.PublicKey().Bytes(), publicName, 0) + if err != nil { + return } - binary.Write(&configBuffer, binary.BigEndian, totalLen) - for _, keyPair := range keyPairs { - configBuffer.Write(keyPair.rawConf) + configBuilder := cryptobyte.NewBuilder(nil) + configBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { + builder.AddBytes(echConfig) + }) + configBytes, err := configBuilder.Bytes() + if err != nil { + return } - - var keyBuffer bytes.Buffer - for _, keyPair := range keyPairs { - keyBuffer.Write(keyPair.rawKey) + keyBuilder := cryptobyte.NewBuilder(nil) + keyBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { + builder.AddBytes(echKey.Bytes()) + }) + keyBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { + builder.AddBytes(echConfig) + }) + keyBytes, err := keyBuilder.Bytes() + if err != nil { + return } - - configPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBuffer.Bytes()})) - keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBuffer.Bytes()})) + configPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBytes})) + keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBytes})) return } -type echKeyConfigPair struct { - id uint8 - rawKey []byte - conf myECHKeyConfig - rawConf []byte -} +func marshalECHConfig(id uint8, pubKey []byte, publicName string, maxNameLen uint8) ([]byte, error) { + const extensionEncryptedClientHello = 0xfe0d + const DHKEM_X25519_HKDF_SHA256 = 0x0020 + const KDF_HKDF_SHA256 = 0x0001 + builder := cryptobyte.NewBuilder(nil) + builder.AddUint16(extensionEncryptedClientHello) + builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { + builder.AddUint8(id) -type echCipherSuite struct { - kdf hpke.KDF - aead hpke.AEAD -} - -type myECHKeyConfig struct { - id uint8 - kem hpke.KEM - seed []byte -} - -func echKeygen(version uint16, serverName string, conf []myECHKeyConfig, suite []echCipherSuite) ([]echKeyConfigPair, error) { - be := binary.BigEndian - // prepare for future update - if version != 0xfe0d { - return nil, E.New("unsupported ECH version", version) - } - - suiteBuf := make([]byte, 0, len(suite)*4+2) - suiteBuf = be.AppendUint16(suiteBuf, uint16(len(suite))*4) - for _, s := range suite { - if !s.kdf.IsValid() || !s.aead.IsValid() { - return nil, E.New("invalid HPKE cipher suite") - } - suiteBuf = be.AppendUint16(suiteBuf, uint16(s.kdf)) - suiteBuf = be.AppendUint16(suiteBuf, uint16(s.aead)) - } - - pairs := []echKeyConfigPair{} - for _, c := range conf { - pair := echKeyConfigPair{} - pair.id = c.id - pair.conf = c - - if !c.kem.IsValid() { - return nil, E.New("invalid HPKE KEM") - } - - kpGenerator := c.kem.Scheme().GenerateKeyPair - if len(c.seed) > 0 { - kpGenerator = func() (kem.PublicKey, kem.PrivateKey, error) { - pub, sec := c.kem.Scheme().DeriveKeyPair(c.seed) - return pub, sec, nil + builder.AddUint16(DHKEM_X25519_HKDF_SHA256) // The only DHKEM we support + builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { + builder.AddBytes(pubKey) + }) + builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { + const ( + AEAD_AES_128_GCM = 0x0001 + AEAD_AES_256_GCM = 0x0002 + AEAD_ChaCha20Poly1305 = 0x0003 + ) + for _, aeadID := range []uint16{AEAD_AES_128_GCM, AEAD_AES_256_GCM, AEAD_ChaCha20Poly1305} { + builder.AddUint16(KDF_HKDF_SHA256) // The only KDF we support + builder.AddUint16(aeadID) } - if len(c.seed) < c.kem.Scheme().PrivateKeySize() { - return nil, E.New("HPKE KEM seed too short") - } - } - - pub, sec, err := kpGenerator() - if err != nil { - return nil, E.Cause(err, "generate ECH config key pair") - } - b := []byte{} - b = be.AppendUint16(b, version) - b = be.AppendUint16(b, 0) // length field - // contents - // key config - b = append(b, c.id) - b = be.AppendUint16(b, uint16(c.kem)) - pubBuf, err := pub.MarshalBinary() - if err != nil { - return nil, E.Cause(err, "serialize ECH public key") - } - b = be.AppendUint16(b, uint16(len(pubBuf))) - b = append(b, pubBuf...) - - b = append(b, suiteBuf...) - // end key config - // max name len, not supported - b = append(b, 0) - // server name - b = append(b, byte(len(serverName))) - b = append(b, []byte(serverName)...) - // extensions, not supported - b = be.AppendUint16(b, 0) - - be.PutUint16(b[2:], uint16(len(b)-4)) - - pair.rawConf = b - - secBuf, err := sec.MarshalBinary() - if err != nil { - return nil, E.Cause(err, "serialize ECH private key") - } - sk := []byte{} - sk = be.AppendUint16(sk, uint16(len(secBuf))) - sk = append(sk, secBuf...) - sk = be.AppendUint16(sk, uint16(len(b))) - sk = append(sk, b...) - pair.rawKey = sk - - pairs = append(pairs, pair) - } - return pairs, nil + }) + builder.AddUint8(maxNameLen) + builder.AddUint8LengthPrefixed(func(builder *cryptobyte.Builder) { + builder.AddBytes([]byte(publicName)) + }) + builder.AddUint16(0) // extensions + }) + return builder.Bytes() } diff --git a/go.mod b/go.mod index bb824888..1baab2d4 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.23.1 require ( github.com/anytls/sing-anytls v0.0.8 github.com/caddyserver/certmagic v0.23.0 - github.com/cloudflare/circl v1.6.1 github.com/coder/websocket v1.8.13 github.com/cretz/bine v0.2.0 github.com/go-chi/chi/v5 v5.2.2 diff --git a/go.sum b/go.sum index 805e9886..ec44c234 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,6 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= -github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= -github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=