Sfoglia il codice sorgente

Add curve preferences, pinned public key SHA256 and mTLS for TLS options

世界 1 mese fa
parent
commit
7c295acb68

+ 4 - 1
common/tls/reality_server.go

@@ -68,7 +68,10 @@ func NewRealityServer(ctx context.Context, logger log.ContextLogger, options opt
 			return nil, E.New("unknown cipher_suite: ", cipherSuite)
 		}
 	}
-	if len(options.Certificate) > 0 || options.CertificatePath != "" {
+	if len(options.CurvePreferences) > 0 {
+		return nil, E.New("curve preferences is unavailable in reality")
+	}
+	if len(options.Certificate) > 0 || options.CertificatePath != "" || len(options.ClientCertificatePublicKeySHA256) > 0 {
 		return nil, E.New("certificate is unavailable in reality")
 	}
 	if len(options.Key) > 0 || options.KeyPath != "" {

+ 34 - 0
common/tls/std_client.go

@@ -1,9 +1,12 @@
 package tls
 
 import (
+	"bytes"
 	"context"
+	"crypto/sha256"
 	"crypto/tls"
 	"crypto/x509"
+	"encoding/base64"
 	"net"
 	"os"
 	"strings"
@@ -108,6 +111,15 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
 			return err
 		}
 	}
+	if len(options.CertificatePublicKeySHA256) > 0 {
+		if len(options.Certificate) > 0 || options.CertificatePath != "" {
+			return nil, E.New("certificate_public_key_sha256 is conflict with certificate or certificate_path")
+		}
+		tlsConfig.InsecureSkipVerify = true
+		tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+			return verifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts, tlsConfig.Time)
+		}
+	}
 	if len(options.ALPN) > 0 {
 		tlsConfig.NextProtos = options.ALPN
 	}
@@ -137,6 +149,9 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
 			return nil, E.New("unknown cipher_suite: ", cipherSuite)
 		}
 	}
+	for _, curve := range options.CurvePreferences {
+		tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, tls.CurveID(curve))
+	}
 	var certificate []byte
 	if len(options.Certificate) > 0 {
 		certificate = []byte(strings.Join(options.Certificate, "\n"))
@@ -175,3 +190,22 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
 	}
 	return config, nil
 }
+
+func verifyPublicKeySHA256(knownHashValues [][]byte, rawCerts [][]byte, timeFunc func() time.Time) error {
+	leafCertificate, err := x509.ParseCertificate(rawCerts[0])
+	if err != nil {
+		return E.Cause(err, "failed to parse leaf certificate")
+	}
+
+	pubKeyBytes, err := x509.MarshalPKIXPublicKey(leafCertificate.PublicKey)
+	if err != nil {
+		return E.Cause(err, "failed to marshal public key")
+	}
+	hashValue := sha256.Sum256(pubKeyBytes)
+	for _, value := range knownHashValues {
+		if bytes.Equal(value, hashValue[:]) {
+			return nil
+		}
+	}
+	return E.New("unrecognized remote public key: ", base64.StdEncoding.EncodeToString(hashValue[:]))
+}

+ 93 - 20
common/tls/std_server.go

@@ -3,6 +3,7 @@ package tls
 import (
 	"context"
 	"crypto/tls"
+	"crypto/x509"
 	"net"
 	"os"
 	"strings"
@@ -22,16 +23,17 @@ import (
 var errInsecureUnused = E.New("tls: insecure unused")
 
 type STDServerConfig struct {
-	access          sync.RWMutex
-	config          *tls.Config
-	logger          log.Logger
-	acmeService     adapter.SimpleLifecycle
-	certificate     []byte
-	key             []byte
-	certificatePath string
-	keyPath         string
-	echKeyPath      string
-	watcher         *fswatch.Watcher
+	access                sync.RWMutex
+	config                *tls.Config
+	logger                log.Logger
+	acmeService           adapter.SimpleLifecycle
+	certificate           []byte
+	key                   []byte
+	certificatePath       string
+	keyPath               string
+	clientCertificatePath []string
+	echKeyPath            string
+	watcher               *fswatch.Watcher
 }
 
 func (c *STDServerConfig) ServerName() string {
@@ -111,6 +113,9 @@ func (c *STDServerConfig) startWatcher() error {
 	if c.echKeyPath != "" {
 		watchPath = append(watchPath, c.echKeyPath)
 	}
+	if len(c.clientCertificatePath) > 0 {
+		watchPath = append(watchPath, c.clientCertificatePath...)
+	}
 	if len(watchPath) == 0 {
 		return nil
 	}
@@ -159,6 +164,30 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
 		c.config = config
 		c.access.Unlock()
 		c.logger.Info("reloaded TLS certificate")
+	} else if common.Contains(c.clientCertificatePath, path) {
+		clientCertificateCA := x509.NewCertPool()
+		var reloaded bool
+		for _, certPath := range c.clientCertificatePath {
+			content, err := os.ReadFile(certPath)
+			if err != nil {
+				c.logger.Error(E.Cause(err, "reload certificate from ", c.clientCertificatePath))
+				continue
+			}
+			if !clientCertificateCA.AppendCertsFromPEM(content) {
+				c.logger.Error(E.New("invalid client certificate file: ", certPath))
+				continue
+			}
+			reloaded = true
+		}
+		if !reloaded {
+			return E.New("client certificates is empty")
+		}
+		c.access.Lock()
+		config := c.config.Clone()
+		config.ClientCAs = clientCertificateCA
+		c.config = config
+		c.access.Unlock()
+		c.logger.Info("reloaded client certificates")
 	} else if path == c.echKeyPath {
 		echKey, err := os.ReadFile(c.echKeyPath)
 		if err != nil {
@@ -235,8 +264,14 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
 			return nil, E.New("unknown cipher_suite: ", cipherSuite)
 		}
 	}
-	var certificate []byte
-	var key []byte
+	for _, curveID := range options.CurvePreferences {
+		tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, tls.CurveID(curveID))
+	}
+	tlsConfig.ClientAuth = tls.ClientAuthType(options.ClientAuthentication)
+	var (
+		certificate []byte
+		key         []byte
+	)
 	if acmeService == nil {
 		if len(options.Certificate) > 0 {
 			certificate = []byte(strings.Join(options.Certificate, "\n"))
@@ -278,6 +313,43 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
 			tlsConfig.Certificates = []tls.Certificate{keyPair}
 		}
 	}
+	if len(options.ClientCertificate) > 0 || len(options.ClientCertificatePath) > 0 {
+		if tlsConfig.ClientAuth == tls.NoClientCert {
+			tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
+		}
+	}
+	if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert {
+		if len(options.ClientCertificate) > 0 {
+			clientCertificateCA := x509.NewCertPool()
+			if !clientCertificateCA.AppendCertsFromPEM([]byte(strings.Join(options.ClientCertificate, "\n"))) {
+				return nil, E.New("invalid client certificate strings")
+			}
+			tlsConfig.ClientCAs = clientCertificateCA
+		} else if len(options.ClientCertificatePath) > 0 {
+			clientCertificateCA := x509.NewCertPool()
+			for _, path := range options.ClientCertificatePath {
+				content, err := os.ReadFile(path)
+				if err != nil {
+					return nil, E.Cause(err, "read client certificate from ", path)
+				}
+				if !clientCertificateCA.AppendCertsFromPEM(content) {
+					return nil, E.New("invalid client certificate file: ", path)
+				}
+			}
+			tlsConfig.ClientCAs = clientCertificateCA
+		} else if len(options.ClientCertificatePublicKeySHA256) > 0 {
+			if tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert {
+				tlsConfig.ClientAuth = tls.RequireAnyClientCert
+			} else if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven {
+				tlsConfig.ClientAuth = tls.RequestClientCert
+			}
+			tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+				return verifyPublicKeySHA256(options.ClientCertificatePublicKeySHA256, rawCerts, tlsConfig.Time)
+			}
+		} else {
+			return nil, E.New("missing client_certificate, client_certificate_path or client_certificate_public_key_sha256 for client authentication")
+		}
+	}
 	var echKeyPath string
 	if options.ECH != nil && options.ECH.Enabled {
 		err = parseECHServerConfig(ctx, options, tlsConfig, &echKeyPath)
@@ -286,14 +358,15 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
 		}
 	}
 	serverConfig := &STDServerConfig{
-		config:          tlsConfig,
-		logger:          logger,
-		acmeService:     acmeService,
-		certificate:     certificate,
-		key:             key,
-		certificatePath: options.CertificatePath,
-		keyPath:         options.KeyPath,
-		echKeyPath:      echKeyPath,
+		config:                tlsConfig,
+		logger:                logger,
+		acmeService:           acmeService,
+		certificate:           certificate,
+		key:                   key,
+		certificatePath:       options.CertificatePath,
+		clientCertificatePath: options.ClientCertificatePath,
+		keyPath:               options.KeyPath,
+		echKeyPath:            echKeyPath,
 	}
 	serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
 		serverConfig.access.Lock()

+ 9 - 0
common/tls/utls_client.go

@@ -167,6 +167,15 @@ func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre
 		}
 		tlsConfig.InsecureServerNameToVerify = serverName
 	}
+	if len(options.CertificatePublicKeySHA256) > 0 {
+		if len(options.Certificate) > 0 || options.CertificatePath != "" {
+			return nil, E.New("certificate_public_key_sha256 is conflict with certificate or certificate_path")
+		}
+		tlsConfig.InsecureSkipVerify = true
+		tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+			return verifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts, tlsConfig.Time)
+		}
+	}
 	if len(options.ALPN) > 0 {
 		tlsConfig.NextProtos = options.ALPN
 	}

+ 107 - 4
docs/configuration/shared/tls.md

@@ -5,7 +5,13 @@ icon: material/new-box
 !!! quote "Changes in sing-box 1.13.0"
 
     :material-plus: [kernel_tx](#kernel_tx)  
-    :material-plus: [kernel_rx](#kernel_rx)
+    :material-plus: [kernel_rx](#kernel_rx)  
+    :material-plus: [curve_preferences](#curve_preferences)  
+    :material-plus: [certificate_public_key_sha256](#certificate_public_key_sha256)  
+    :material-plus: [client_authentication](#client_authentication)  
+    :material-plus: [client_certificate](#client_certificate)  
+    :material-plus: [client_certificate_path](#client_certificate_path)  
+    :material-plus: [client_certificate_public_key_sha256](#client_certificate_public_key_sha256)
 
 !!! quote "Changes in sing-box 1.12.0"
 
@@ -29,8 +35,13 @@ icon: material/new-box
   "min_version": "",
   "max_version": "",
   "cipher_suites": [],
+  "curve_preferences": [],
   "certificate": [],
   "certificate_path": "",
+  "client_authentication": "",
+  "client_certificate": [],
+  "client_certificate_path": [],
+  "client_certificate_public_key_sha256": [],
   "key": [],
   "key_path": "",
   "kernel_tx": false,
@@ -92,6 +103,7 @@ icon: material/new-box
   "cipher_suites": [],
   "certificate": "",
   "certificate_path": "",
+  "certificate_public_key_sha256": [],
   "fragment": false,
   "fragment_fallback_delay": "",
   "record_fragment": false,
@@ -195,14 +207,29 @@ By default, the maximum version is currently TLS 1.3.
 
 #### cipher_suites
 
-A list of enabled TLS 1.0–1.2 cipher suites. The order of the list is ignored.
+List of enabled TLS 1.0–1.2 cipher suites. The order of the list is ignored.
 Note that TLS 1.3 cipher suites are not configurable.
 
 If empty, a safe default list is used. The default cipher suites might change over time.
 
+#### curve_preferences
+
+!!! question "Since sing-box 1.13.0"
+
+Set of supported key exchange mechanisms. The order of the list is ignored, and key exchange mechanisms are chosen
+from this list using an internal preference order by Golang.
+
+Available values, also the default list:
+
+* `P256`
+* `P384`
+* `P521`
+* `X25519`
+* `X25519MLKEM768`
+
 #### certificate
 
-The server certificate line array, in PEM format.
+Server certificates chain line array, in PEM format.
 
 #### certificate_path
 
@@ -210,7 +237,26 @@ The server certificate line array, in PEM format.
 
     Will be automatically reloaded if file modified.
 
-The path to the server certificate, in PEM format.
+The path to server certificate chain, in PEM format.
+
+
+#### certificate_public_key_sha256
+
+!!! question "Since sing-box 1.13.0"
+
+==Client only==
+
+List of SHA-256 hashes of server certificate public keys, in base64 format.
+
+To generate the SHA-256 hash for a certificate's public key, use the following commands:
+
+```bash
+# For a certificate file
+openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
+
+# For a certificate from a remote server
+echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
+```
 
 #### key
 
@@ -228,6 +274,63 @@ The server private key line array, in PEM format.
 
 The path to the server private key, in PEM format.
 
+#### client_authentication
+
+!!! question "Since sing-box 1.13.0"
+
+==Server only==
+
+The type of client authentication to use.
+
+Available values:
+
+* `no` (default)
+* `request`
+* `require-any`
+* `verify-if-given`
+* `require-and-verify`
+
+One of `client_certificate`, `client_certificate_path`, or `client_certificate_public_key_sha256` is required
+if this option is set to `verify-if-given`, or `require-and-verify`.
+
+#### client_certificate
+
+!!! question "Since sing-box 1.13.0"
+
+==Server only==
+
+Client certificate chain line array, in PEM format.
+
+#### client_certificate_path
+
+!!! question "Since sing-box 1.13.0"
+
+==Server only==
+
+!!! note ""
+
+    Will be automatically reloaded if file modified.
+
+List of path to client certificate chain, in PEM format.
+
+#### client_certificate_public_key_sha256
+
+!!! question "Since sing-box 1.13.0"
+
+==Server only==
+
+List of SHA-256 hashes of client certificate public keys, in base64 format.
+
+To generate the SHA-256 hash for a certificate's public key, use the following commands:
+
+```bash
+# For a certificate file
+openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
+
+# For a certificate from a remote server
+echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
+```
+
 #### kernel_tx
 
 !!! question "Since sing-box 1.13.0"

+ 173 - 66
docs/configuration/shared/tls.zh.md

@@ -1,18 +1,24 @@
 ---
-icon: material/alert-decagram
+icon: material/new-box
 ---
 
 !!! quote "sing-box 1.13.0 中的更改"
 
-    :material-plus: [kernel_tx](#kernel_tx)  
+    :material-plus: [kernel_tx](#kernel_tx)
     :material-plus: [kernel_rx](#kernel_rx)
+    :material-plus: [curve_preferences](#curve_preferences)
+    :material-plus: [certificate_public_key_sha256](#certificate_public_key_sha256)
+    :material-plus: [client_authentication](#client_authentication)
+    :material-plus: [client_certificate](#client_certificate)
+    :material-plus: [client_certificate_path](#client_certificate_path)
+    :material-plus: [client_certificate_public_key_sha256](#client_certificate_public_key_sha256)
 
 !!! quote "sing-box 1.12.0 中的更改"
 
-    :material-plus: [tls_fragment](#tls_fragment)  
-    :material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay)  
-    :material-plus: [tls_record_fragment](#tls_record_fragment)  
-    :material-delete-clock: [ech.pq_signature_schemes_enabled](#pq_signature_schemes_enabled)  
+    :material-plus: [fragment](#fragment)
+    :material-plus: [fragment_fallback_delay](#fragment_fallback_delay)
+    :material-plus: [record_fragment](#record_fragment)
+    :material-delete-clock: [ech.pq_signature_schemes_enabled](#pq_signature_schemes_enabled)
     :material-delete-clock: [ech.dynamic_record_sizing_disabled](#dynamic_record_sizing_disabled)
 
 !!! quote "sing-box 1.10.0 中的更改"
@@ -29,8 +35,13 @@ icon: material/alert-decagram
   "min_version": "",
   "max_version": "",
   "cipher_suites": [],
+  "curve_preferences": [],
   "certificate": [],
   "certificate_path": "",
+  "client_authentication": "",
+  "client_certificate": [],
+  "client_certificate_path": [],
+  "client_certificate_public_key_sha256": [],
   "key": [],
   "key_path": "",
   "kernel_tx": false,
@@ -90,17 +101,20 @@ icon: material/alert-decagram
   "min_version": "",
   "max_version": "",
   "cipher_suites": [],
-  "certificate": [],
+  "certificate": "",
   "certificate_path": "",
+  "certificate_public_key_sha256": [],
   "fragment": false,
   "fragment_fallback_delay": "",
   "record_fragment": false,
   "ech": {
     "enabled": false,
-    "pq_signature_schemes_enabled": false,
-    "dynamic_record_sizing_disabled": false,
     "config": [],
-    "config_path": ""
+    "config_path": "",
+
+    // 废弃的
+    "pq_signature_schemes_enabled": false,
+    "dynamic_record_sizing_disabled": false
   },
   "utls": {
     "enabled": false,
@@ -191,13 +205,27 @@ TLS 版本值:
 
 #### cipher_suites
 
-启用的 TLS 1.0-1.2密码套件的列表。列表的顺序被忽略。请注意,TLS 1.3 的密码套件是不可配置的。
+启用的 TLS 1.0–1.2 密码套件列表。列表的顺序被忽略。请注意,TLS 1.3 的密码套件是不可配置的。
 
 如果为空,则使用安全的默认列表。默认密码套件可能会随着时间的推移而改变。
 
+#### curve_preferences
+
+!!! question "自 sing-box 1.13.0 起"
+
+支持的密钥交换机制集合。列表的顺序被忽略,密钥交换机制通过 Golang 的内部偏好顺序从此列表中选择。
+
+可用值,同时也是默认列表:
+
+* `P256`
+* `P384`
+* `P521`
+* `X25519`
+* `X25519MLKEM768`
+
 #### certificate
 
-服务器 PEM 证书行数组。
+服务器证书行数组,PEM 格式
 
 #### certificate_path
 
@@ -205,7 +233,25 @@ TLS 版本值:
 
     文件更改时将自动重新加载。
 
-服务器 PEM 证书路径。
+服务器证书链路径,PEM 格式。
+
+#### certificate_public_key_sha256
+
+!!! question "自 sing-box 1.13.0 起"
+
+==仅客户端==
+
+服务器证书公钥的 SHA-256 哈希列表,base64 格式。
+
+要生成证书公钥的 SHA-256 哈希,请使用以下命令:
+
+```bash
+# 对于证书文件
+openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
+
+# 对于远程服务器的证书
+echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
+```
 
 #### key
 
@@ -221,7 +267,68 @@ TLS 版本值:
 
 ==仅服务器==
 
-服务器 PEM 私钥路径。
+!!! note ""
+
+    文件更改时将自动重新加载。
+
+服务器私钥路径,PEM 格式。
+
+#### client_authentication
+
+!!! question "自 sing-box 1.13.0 起"
+
+==仅服务器==
+
+要使用的客户端身份验证类型。
+
+可用值:
+
+* `no`(默认)
+* `request`
+* `require-any`
+* `verify-if-given`
+* `require-and-verify`
+
+如果此选项设置为 `verify-if-given` 或 `require-and-verify`,
+则需要 `client_certificate`、`client_certificate_path` 或 `client_certificate_public_key_sha256` 中的一个。
+
+#### client_certificate
+
+!!! question "自 sing-box 1.13.0 起"
+
+==仅服务器==
+
+客户端证书链行数组,PEM 格式。
+
+#### client_certificate_path
+
+!!! question "自 sing-box 1.13.0 起"
+
+==仅服务器==
+
+!!! note ""
+
+    文件更改时将自动重新加载。
+
+客户端证书链路径列表,PEM 格式。
+
+#### client_certificate_public_key_sha256
+
+!!! question "自 sing-box 1.13.0 起"
+
+==仅服务器==
+
+客户端证书公钥的 SHA-256 哈希列表,base64 格式。
+
+要生成证书公钥的 SHA-256 哈希,请使用以下命令:
+
+```bash
+# 对于证书文件
+openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
+
+# 对于远程服务器的证书
+echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
+```
 
 #### kernel_tx
 
@@ -300,18 +407,36 @@ uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻
 
 默认使用 chrome 指纹。
 
-## ECH 字段
+### ECH 字段
+
+ECH (Encrypted Client Hello) 是一个 TLS 扩展,它允许客户端加密其 ClientHello 的第一部分信息。
+
+ECH 密钥和配置可以通过 `sing-box generate ech-keypair` 生成。
+
+#### pq_signature_schemes_enabled
+
+!!! failure "已在 sing-box 1.12.0 废弃"
 
-ECH (Encrypted Client Hello) 是一个 TLS 扩展,它允许客户端加密其 ClientHello 的第一部分
-信息。
+    ECH 支持已在 sing-box 1.12.0 迁移至使用标准库,但标准库不支持后量子对等证书签名方案,因此 `pq_signature_schemes_enabled` 已被弃用且不再工作。
 
-ECH 配置和密钥可以通过 `sing-box generate ech-keypair [--pq-signature-schemes-enabled]` 生成。
+启用对后量子对等证书签名方案的支持。
+
+#### dynamic_record_sizing_disabled
+
+!!! failure "已在 sing-box 1.12.0 废弃"
+
+    `dynamic_record_sizing_disabled` 与 ECH 无关,是错误添加的,现已弃用且不再工作。
+
+禁用 TLS 记录的自适应大小调整。
+
+当为 true 时,总是使用最大可能的 TLS 记录大小。
+当为 false 时,可能会调整 TLS 记录的大小以尝试改善延迟。
 
 #### key
 
 ==仅服务器==
 
-ECH PEM 密钥行数组
+ECH 密钥行数组,PEM 格式。
 
 #### key_path
 
@@ -321,13 +446,13 @@ ECH PEM 密钥行数组
 
     文件更改时将自动重新加载。
 
-ECH PEM 密钥路径
+ECH 密钥路径,PEM 格式。
 
 #### config
 
 ==仅客户端==
 
-ECH PEM 配置行数组
+ECH 配置行数组,PEM 格式。
 
 如果为空,将尝试从 DNS 加载。
 
@@ -335,79 +460,60 @@ ECH PEM 配置行数组
 
 ==仅客户端==
 
-ECH PEM 配置路径
+ECH 配置路径,PEM 格式。
 
 如果为空,将尝试从 DNS 加载。
 
-#### pq_signature_schemes_enabled
-
-!!! failure "已在 sing-box 1.12.0 废弃"
-
-    ECH 支持已在 sing-box 1.12.0 迁移至使用标准库,但标准库不支持后量子对等证书签名方案,因此 `pq_signature_schemes_enabled` 已被弃用且不再工作。
-
-启用对后量子对等证书签名方案的支持。
-
-建议匹配 `sing-box generate ech-keypair` 的参数。
-
-#### dynamic_record_sizing_disabled
-
-!!! failure "已在 sing-box 1.12.0 废弃"
-
-    `dynamic_record_sizing_disabled` 与 ECH 无关,是错误添加的,现已弃用且不再工作。
-
-禁用 TLS 记录的自适应大小调整。
-
-如果为 true,则始终使用最大可能的 TLS 记录大小。
-如果为 false,则可能会调整 TLS 记录的大小以尝试改善延迟。
-
-#### tls_fragment
+#### fragment
 
 !!! question "自 sing-box 1.12.0 起"
 
 ==仅客户端==
 
-通过分段 TLS 握手数据包来绕过防火墙检测
+通过分段 TLS 握手数据包来绕过防火墙。
 
-此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真的审查。
+此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真正的审查。
 
-由于性能不佳,请首先尝试 `tls_record_fragment`,且仅应用于已知被阻止的服务器名称。
+由于性能不佳,请首先尝试 `record_fragment`,且仅应用于已知被阻止的服务器名称。
 
-在 Linux、Apple 平台和需要管理员权限的 Windows 系统上,可自动检测等待时间。
-若无法自动检测,将回退使用 `tls_fragment_fallback_delay` 指定的固定等待时间。
+在 Linux、Apple 平台和(需要管理员权限的)Windows 系统上,
+可以自动检测等待时间。否则,将回退到
+等待 `fragment_fallback_delay` 指定的固定时间。
 
-此外,若实际等待时间小于 20 毫秒,同样会回退至固定等待时间模式,因为此时判定目标处于本地或透明代理之后。
+此外,如果实际等待时间少于 20ms,也会回退到等待固定时间,
+因为目标被认为是本地的或在透明代理后面。
 
-#### tls_fragment_fallback_delay
+#### fragment_fallback_delay
 
 !!! question "自 sing-box 1.12.0 起"
 
 ==仅客户端==
 
-当 TLS 分片功能无法自动判定等待时间时使用的回退值。
+当 TLS 分段无法自动确定等待时间时使用的回退值。
 
 默认使用 `500ms`。
 
-#### tls_record_fragment
-
-==仅客户端==
+#### record_fragment
 
 !!! question "自 sing-box 1.12.0 起"
 
-通过分段 TLS 握手数据包到多个 TLS 记录来绕过防火墙检测。
+==仅客户端==
+
+将 TLS 握手分段为多个 TLS 记录以绕过防火墙。
 
 ### ACME 字段
 
 #### domain
 
-一组域名。
+域名列表
 
-默认禁用 ACME。
+如果为空则禁用 ACME。
 
 #### data_directory
 
-ACME 数据目录。
+ACME 数据存储目录。
 
-默认使用 `$XDG_DATA_HOME/certmagic|$HOME/.local/share/certmagic`。
+如果为空则使用 `$XDG_DATA_HOME/certmagic|$HOME/.local/share/certmagic`。
 
 #### default_server_name
 
@@ -445,12 +551,11 @@ ACME 数据目录。
 
 #### external_account
 
-EAB(外部帐户绑定)包含将 ACME 帐户绑定或映射到其他已知帐户所需的信息由 CA
+EAB(外部帐户绑定)包含将 ACME 帐户绑定或映射到 CA 已知的其他帐户所需的信息。
 
-外部帐户绑定用于将 ACME 帐户与非 ACME 系统中的现有帐户相关联,例如 CA 客户数据库。
+外部帐户绑定"用于将 ACME 帐户与非 ACME 系统中的现有帐户相关联,例如 CA 客户数据库。
 
-为了启用 ACME 帐户绑定,运行 ACME 服务器的 CA 需要向 ACME 客户端提供 MAC 密钥和密钥标识符,使用 ACME 之外的一些机制。
-§7.3.4
+为了启用 ACME 帐户绑定,运行 ACME 服务器的 CA 需要使用 ACME 之外的某种机制向 ACME 客户端提供 MAC 密钥和密钥标识符。§7.3.4
 
 #### external_account.key_id
 
@@ -500,6 +605,8 @@ ACME DNS01 验证字段。如果配置,将禁用其他验证方法。
 
 #### max_time_difference
 
-服务器与和客户端之间允许的最大时间差。
+==仅服务器==
+
+服务器和客户端之间的最大时间差。
 
-默认禁用检查。
+如果为空则禁用检查。

+ 145 - 35
option/tls.go

@@ -1,24 +1,80 @@
 package option
 
-import "github.com/sagernet/sing/common/json/badoption"
+import (
+	"crypto/tls"
+	"encoding/json"
+	"strings"
+
+	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/json/badoption"
+)
 
 type InboundTLSOptions struct {
-	Enabled         bool                       `json:"enabled,omitempty"`
-	ServerName      string                     `json:"server_name,omitempty"`
-	Insecure        bool                       `json:"insecure,omitempty"`
-	ALPN            badoption.Listable[string] `json:"alpn,omitempty"`
-	MinVersion      string                     `json:"min_version,omitempty"`
-	MaxVersion      string                     `json:"max_version,omitempty"`
-	CipherSuites    badoption.Listable[string] `json:"cipher_suites,omitempty"`
-	Certificate     badoption.Listable[string] `json:"certificate,omitempty"`
-	CertificatePath string                     `json:"certificate_path,omitempty"`
-	Key             badoption.Listable[string] `json:"key,omitempty"`
-	KeyPath         string                     `json:"key_path,omitempty"`
-	KernelTx        bool                       `json:"kernel_tx,omitempty"`
-	KernelRx        bool                       `json:"kernel_rx,omitempty"`
-	ACME            *InboundACMEOptions        `json:"acme,omitempty"`
-	ECH             *InboundECHOptions         `json:"ech,omitempty"`
-	Reality         *InboundRealityOptions     `json:"reality,omitempty"`
+	Enabled                          bool                                `json:"enabled,omitempty"`
+	ServerName                       string                              `json:"server_name,omitempty"`
+	Insecure                         bool                                `json:"insecure,omitempty"`
+	ALPN                             badoption.Listable[string]          `json:"alpn,omitempty"`
+	MinVersion                       string                              `json:"min_version,omitempty"`
+	MaxVersion                       string                              `json:"max_version,omitempty"`
+	CipherSuites                     badoption.Listable[string]          `json:"cipher_suites,omitempty"`
+	CurvePreferences                 badoption.Listable[CurvePreference] `json:"curve_preferences,omitempty"`
+	Certificate                      badoption.Listable[string]          `json:"certificate,omitempty"`
+	CertificatePath                  string                              `json:"certificate_path,omitempty"`
+	ClientAuthentication             ClientAuthType                      `json:"client_authentication,omitempty"`
+	ClientCertificate                badoption.Listable[string]          `json:"client_certificate,omitempty"`
+	ClientCertificatePath            badoption.Listable[string]          `json:"client_certificate_path,omitempty"`
+	ClientCertificatePublicKeySHA256 badoption.Listable[[]byte]          `json:"client_certificate_public_key_sha256,omitempty"`
+	Key                              badoption.Listable[string]          `json:"key,omitempty"`
+	KeyPath                          string                              `json:"key_path,omitempty"`
+	KernelTx                         bool                                `json:"kernel_tx,omitempty"`
+	KernelRx                         bool                                `json:"kernel_rx,omitempty"`
+	ACME                             *InboundACMEOptions                 `json:"acme,omitempty"`
+	ECH                              *InboundECHOptions                  `json:"ech,omitempty"`
+	Reality                          *InboundRealityOptions              `json:"reality,omitempty"`
+}
+
+type ClientAuthType tls.ClientAuthType
+
+func (t ClientAuthType) MarshalJSON() ([]byte, error) {
+	var stringValue string
+	switch t {
+	case ClientAuthType(tls.NoClientCert):
+		stringValue = "no"
+	case ClientAuthType(tls.RequestClientCert):
+		stringValue = "request"
+	case ClientAuthType(tls.RequireAnyClientCert):
+		stringValue = "require-any"
+	case ClientAuthType(tls.VerifyClientCertIfGiven):
+		stringValue = "verify-if-given"
+	case ClientAuthType(tls.RequireAndVerifyClientCert):
+		stringValue = "require-and-verify"
+	default:
+		return nil, E.New("unknown client authentication type: ", int(t))
+	}
+	return json.Marshal(stringValue)
+}
+
+func (t *ClientAuthType) UnmarshalJSON(data []byte) error {
+	var stringValue string
+	err := json.Unmarshal(data, &stringValue)
+	if err != nil {
+		return err
+	}
+	switch stringValue {
+	case "no":
+		*t = ClientAuthType(tls.NoClientCert)
+	case "request":
+		*t = ClientAuthType(tls.RequestClientCert)
+	case "require-any":
+		*t = ClientAuthType(tls.RequireAnyClientCert)
+	case "verify-if-given":
+		*t = ClientAuthType(tls.VerifyClientCertIfGiven)
+	case "require-and-verify":
+		*t = ClientAuthType(tls.RequireAndVerifyClientCert)
+	default:
+		return E.New("unknown client authentication type: ", stringValue)
+	}
+	return nil
 }
 
 type InboundTLSOptionsContainer struct {
@@ -39,24 +95,26 @@ func (o *InboundTLSOptionsContainer) ReplaceInboundTLSOptions(options *InboundTL
 }
 
 type OutboundTLSOptions struct {
-	Enabled               bool                       `json:"enabled,omitempty"`
-	DisableSNI            bool                       `json:"disable_sni,omitempty"`
-	ServerName            string                     `json:"server_name,omitempty"`
-	Insecure              bool                       `json:"insecure,omitempty"`
-	ALPN                  badoption.Listable[string] `json:"alpn,omitempty"`
-	MinVersion            string                     `json:"min_version,omitempty"`
-	MaxVersion            string                     `json:"max_version,omitempty"`
-	CipherSuites          badoption.Listable[string] `json:"cipher_suites,omitempty"`
-	Certificate           badoption.Listable[string] `json:"certificate,omitempty"`
-	CertificatePath       string                     `json:"certificate_path,omitempty"`
-	Fragment              bool                       `json:"fragment,omitempty"`
-	FragmentFallbackDelay badoption.Duration         `json:"fragment_fallback_delay,omitempty"`
-	RecordFragment        bool                       `json:"record_fragment,omitempty"`
-	KernelTx              bool                       `json:"kernel_tx,omitempty"`
-	KernelRx              bool                       `json:"kernel_rx,omitempty"`
-	ECH                   *OutboundECHOptions        `json:"ech,omitempty"`
-	UTLS                  *OutboundUTLSOptions       `json:"utls,omitempty"`
-	Reality               *OutboundRealityOptions    `json:"reality,omitempty"`
+	Enabled                    bool                                `json:"enabled,omitempty"`
+	DisableSNI                 bool                                `json:"disable_sni,omitempty"`
+	ServerName                 string                              `json:"server_name,omitempty"`
+	Insecure                   bool                                `json:"insecure,omitempty"`
+	ALPN                       badoption.Listable[string]          `json:"alpn,omitempty"`
+	MinVersion                 string                              `json:"min_version,omitempty"`
+	MaxVersion                 string                              `json:"max_version,omitempty"`
+	CipherSuites               badoption.Listable[string]          `json:"cipher_suites,omitempty"`
+	CurvePreferences           badoption.Listable[CurvePreference] `json:"curve_preferences,omitempty"`
+	Certificate                badoption.Listable[string]          `json:"certificate,omitempty"`
+	CertificatePath            string                              `json:"certificate_path,omitempty"`
+	CertificatePublicKeySHA256 badoption.Listable[[]byte]          `json:"certificate_public_key_sha256,omitempty"`
+	Fragment                   bool                                `json:"fragment,omitempty"`
+	FragmentFallbackDelay      badoption.Duration                  `json:"fragment_fallback_delay,omitempty"`
+	RecordFragment             bool                                `json:"record_fragment,omitempty"`
+	KernelTx                   bool                                `json:"kernel_tx,omitempty"`
+	KernelRx                   bool                                `json:"kernel_rx,omitempty"`
+	ECH                        *OutboundECHOptions                 `json:"ech,omitempty"`
+	UTLS                       *OutboundUTLSOptions                `json:"utls,omitempty"`
+	Reality                    *OutboundRealityOptions             `json:"reality,omitempty"`
 }
 
 type OutboundTLSOptionsContainer struct {
@@ -76,6 +134,58 @@ func (o *OutboundTLSOptionsContainer) ReplaceOutboundTLSOptions(options *Outboun
 	o.TLS = options
 }
 
+type CurvePreference tls.CurveID
+
+const (
+	CurveP256      = 23
+	CurveP384      = 24
+	CurveP521      = 25
+	X25519         = 29
+	X25519MLKEM768 = 4588
+)
+
+func (c CurvePreference) MarshalJSON() ([]byte, error) {
+	var stringValue string
+	switch c {
+	case CurvePreference(CurveP256):
+		stringValue = "P256"
+	case CurvePreference(CurveP384):
+		stringValue = "P384"
+	case CurvePreference(CurveP521):
+		stringValue = "P521"
+	case CurvePreference(X25519):
+		stringValue = "X25519"
+	case CurvePreference(X25519MLKEM768):
+		stringValue = "X25519MLKEM768"
+	default:
+		return nil, E.New("unknown curve id: ", int(c))
+	}
+	return json.Marshal(stringValue)
+}
+
+func (c *CurvePreference) UnmarshalJSON(data []byte) error {
+	var stringValue string
+	err := json.Unmarshal(data, &stringValue)
+	if err != nil {
+		return err
+	}
+	switch strings.ToUpper(stringValue) {
+	case "P256":
+		*c = CurvePreference(CurveP256)
+	case "P384":
+		*c = CurvePreference(CurveP384)
+	case "P521":
+		*c = CurvePreference(CurveP521)
+	case "X25519":
+		*c = CurvePreference(X25519)
+	case "X25519MLKEM768":
+		*c = CurvePreference(X25519MLKEM768)
+	default:
+		return E.New("unknown curve name: ", stringValue)
+	}
+	return nil
+}
+
 type InboundRealityOptions struct {
 	Enabled           bool                           `json:"enabled,omitempty"`
 	Handshake         InboundRealityHandshakeOptions `json:"handshake,omitempty"`