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
13 changes: 13 additions & 0 deletions app/cmd/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ type clientConfigTLS struct {
CA string `mapstructure:"ca"`
ClientCertificate string `mapstructure:"clientCertificate"`
ClientKey string `mapstructure:"clientKey"`
ECH clientConfigECH `mapstructure:"ech"`
}

type clientConfigECH struct {
ConfigFile string `mapstructure:"configFile"`
}

type clientConfigQUIC struct {
Expand Down Expand Up @@ -321,6 +326,14 @@ func (c *clientConfig) fillTLSConfig(hyConfig *client.Config) error {
return certLoader.GetCertificate(nil)
}
}
// ECH
if c.TLS.ECH.ConfigFile != "" {
echConfigList, err := utils.ParseECHConfigFile(c.TLS.ECH.ConfigFile)
if err != nil {
return configError{Field: "tls.ech.configFile", Err: err}
}
hyConfig.TLSConfig.EncryptedClientHelloConfigList = echConfigList
}
return nil
}

Expand Down
12 changes: 12 additions & 0 deletions app/cmd/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package cmd

import "github.com/spf13/cobra"

var generateCmd = &cobra.Command{
Use: "generate",
Short: "Generate keys and other resources",
}

func init() {
rootCmd.AddCommand(generateCmd)
}
52 changes: 52 additions & 0 deletions app/cmd/generate_ech.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package cmd

import (
"fmt"
"os"

"github.com/spf13/cobra"

"github.com/apernet/hysteria/app/v2/internal/utils"
)

var (
echOutKey string
echOutConfig string
)

var generateECHKeyPairCmd = &cobra.Command{
Use: "ech-keypair <outer_server_name_indication>",
Short: "Generate TLS ECH key pair",
Args: cobra.ExactArgs(1),
Run: runGenerateECHKeyPair,
}

func init() {
generateECHKeyPairCmd.Flags().StringVar(&echOutKey, "outKey", "-", "output file for ECH keys (server), \"-\" for stdout")
generateECHKeyPairCmd.Flags().StringVar(&echOutConfig, "outConfig", "-", "output file for ECH configs (client), \"-\" for stdout")
generateCmd.AddCommand(generateECHKeyPairCmd)
}

func runGenerateECHKeyPair(cmd *cobra.Command, args []string) {
configPem, keyPem, err := utils.ECHKeygen(args[0])
if err != nil {
fmt.Fprintln(os.Stderr, "Error:", err)
os.Exit(1)
}
if err := writeOutput(echOutConfig, configPem); err != nil {
fmt.Fprintln(os.Stderr, "Error writing config:", err)
os.Exit(1)
}
if err := writeOutput(echOutKey, keyPem); err != nil {
fmt.Fprintln(os.Stderr, "Error writing key:", err)
os.Exit(1)
}
}

func writeOutput(path, content string) error {
if path == "-" {
_, err := os.Stdout.WriteString(content)
return err
}
return os.WriteFile(path, []byte(content), 0600)
}
17 changes: 17 additions & 0 deletions app/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type serverConfig struct {
Obfs serverConfigObfs `mapstructure:"obfs"`
TLS *serverConfigTLS `mapstructure:"tls"`
ACME *serverConfigACME `mapstructure:"acme"`
ECH serverConfigECH `mapstructure:"ech"`
QUIC serverConfigQUIC `mapstructure:"quic"`
Bandwidth serverConfigBandwidth `mapstructure:"bandwidth"`
IgnoreClientBandwidth bool `mapstructure:"ignoreClientBandwidth"`
Expand Down Expand Up @@ -90,6 +91,10 @@ type serverConfigTLS struct {
ClientCA string `mapstructure:"clientCA"`
}

type serverConfigECH struct {
KeyFile string `mapstructure:"keyFile"`
}

type serverConfigACME struct {
// Common fields
Domains []string `mapstructure:"domains"`
Expand Down Expand Up @@ -478,6 +483,17 @@ func (c *serverConfig) fillTLSConfig(hyConfig *server.Config) error {
return nil
}

func (c *serverConfig) fillECHConfig(hyConfig *server.Config) error {
if c.ECH.KeyFile != "" {
echKeys, err := utils.ParseECHKeyFile(c.ECH.KeyFile)
if err != nil {
return configError{Field: "ech.keyFile", Err: err}
}
hyConfig.TLSConfig.EncryptedClientHelloKeys = echKeys
}
return nil
}

func genZeroSSLEAB(email string) (*acme.EAB, error) {
req, err := http.NewRequest(
http.MethodPost,
Expand Down Expand Up @@ -911,6 +927,7 @@ func (c *serverConfig) Config() (*server.Config, error) {
fillers := []func(*server.Config) error{
c.fillConn,
c.fillTLSConfig,
c.fillECHConfig,
c.fillQUICConfig,
c.fillRequestHook,
c.fillOutboundConfig,
Expand Down
132 changes: 132 additions & 0 deletions app/internal/utils/ech.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package utils

import (
"crypto/ecdh"
"crypto/rand"
"crypto/tls"
"encoding/pem"
"fmt"
"os"

"golang.org/x/crypto/cryptobyte"
)

// ParseECHConfigFile reads an ECH configs PEM file and returns the raw config list bytes
// for use with tls.Config.EncryptedClientHelloConfigList.
func ParseECHConfigFile(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
block, _ := pem.Decode(data)
if block == nil || block.Type != "ECH CONFIGS" {
return nil, fmt.Errorf("invalid ECH configs PEM")
}
return block.Bytes, nil
}

// ParseECHKeyFile reads an ECH keys PEM file and returns the parsed keys.
func ParseECHKeyFile(path string) ([]tls.EncryptedClientHelloKey, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
block, _ := pem.Decode(data)
if block == nil || block.Type != "ECH KEYS" {
return nil, fmt.Errorf("invalid ECH keys PEM")
}
return unmarshalECHKeys(block.Bytes)
}

// unmarshalECHKeys parses the binary ECH keys format.
// Each key entry is: uint16-length-prefixed private key + uint16-length-prefixed config.
func unmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) {
var keys []tls.EncryptedClientHelloKey
s := cryptobyte.String(raw)
for !s.Empty() {
var key tls.EncryptedClientHelloKey
if !s.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.PrivateKey)) {
return nil, fmt.Errorf("error parsing ECH private key")
}
if !s.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.Config)) {
return nil, fmt.Errorf("error parsing ECH config")
}
keys = append(keys, key)
}
if len(keys) == 0 {
return nil, fmt.Errorf("empty ECH keys")
}
return keys, nil
}

// ECHKeygen generates an ECH key pair for the given public name.
// Returns the config PEM (for clients) and key PEM (for the server).
func ECHKeygen(publicName string) (configPem string, keyPem string, err error) {
echKey, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return
}
echConfig, err := marshalECHConfig(0, echKey.PublicKey().Bytes(), publicName, 0)
if err != nil {
return
}

configBuilder := cryptobyte.NewBuilder(nil)
configBuilder.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(echConfig)
})
configBytes, err := configBuilder.Bytes()
if err != nil {
return
}

keyBuilder := cryptobyte.NewBuilder(nil)
keyBuilder.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(echKey.Bytes())
})
keyBuilder.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(echConfig)
})
keyBytes, err := keyBuilder.Bytes()
if err != nil {
return
}

configPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBytes}))
keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBytes}))
return
}

// marshalECHConfig builds the binary ECH config structure per the ECH specification.
func marshalECHConfig(id uint8, pubKey []byte, publicName string, maxNameLen uint8) ([]byte, error) {
const (
extensionEncryptedClientHello = 0xfe0d
dhkemX25519HKDFSHA256 = 0x0020
kdfHKDFSHA256 = 0x0001
aeadAES128GCM = 0x0001
aeadAES256GCM = 0x0002
aeadChaCha20Poly1305 = 0x0003
)

builder := cryptobyte.NewBuilder(nil)
builder.AddUint16(extensionEncryptedClientHello)
builder.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddUint8(id)
b.AddUint16(dhkemX25519HKDFSHA256)
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(pubKey)
})
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
for _, aeadID := range []uint16{aeadAES128GCM, aeadAES256GCM, aeadChaCha20Poly1305} {
b.AddUint16(kdfHKDFSHA256)
b.AddUint16(aeadID)
}
})
b.AddUint8(maxNameLen)
b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes([]byte(publicName))
})
b.AddUint16(0) // extensions
})
return builder.Bytes()
}
1 change: 1 addition & 0 deletions core/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ func (c *clientImpl) connect() (*HandshakeInfo, error) {
VerifyPeerCertificate: c.config.TLSConfig.VerifyPeerCertificate,
RootCAs: c.config.TLSConfig.RootCAs,
GetClientCertificate: c.config.TLSConfig.GetClientCertificate,
EncryptedClientHelloConfigList: c.config.TLSConfig.EncryptedClientHelloConfigList,
}
quicConfig := &quic.Config{
InitialStreamReceiveWindow: c.config.QUICConfig.InitialStreamReceiveWindow,
Expand Down
1 change: 1 addition & 0 deletions core/client/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ type TLSConfig struct {
VerifyPeerCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error
RootCAs *x509.CertPool
GetClientCertificate func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
EncryptedClientHelloConfigList []byte
}

// QUICConfig contains the QUIC configuration fields that we want to expose to the user.
Expand Down
1 change: 1 addition & 0 deletions core/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ type TLSConfig struct {
Certificates []tls.Certificate
GetCertificate func(info *tls.ClientHelloInfo) (*tls.Certificate, error)
ClientCAs *x509.CertPool
EncryptedClientHelloKeys []tls.EncryptedClientHelloKey
}

// QUICConfig contains the QUIC configuration fields that we want to expose to the user.
Expand Down
1 change: 1 addition & 0 deletions core/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func convertToStdTLSConfig(config *Config) *tls.Config {
GetCertificate: config.TLSConfig.GetCertificate,
ClientCAs: config.TLSConfig.ClientCAs,
ClientAuth: clientAuth,
EncryptedClientHelloKeys: config.TLSConfig.EncryptedClientHelloKeys,
})
}

Expand Down