diff --git a/common/tls/common.go b/common/tls/common.go index 66cf1223..1b9a746d 100644 --- a/common/tls/common.go +++ b/common/tls/common.go @@ -1,5 +1,12 @@ package tls +import ( + "crypto/x509" + "os" + + E "github.com/sagernet/sing/common/exceptions" +) + const ( VersionTLS10 = 0x0301 VersionTLS11 = 0x0302 @@ -10,3 +17,31 @@ const ( // supported by this package. See golang.org/issue/32716. VersionSSL30 = 0x0300 ) + +func loadCertAsBytes(certificate string, certificatePath string) ([]byte, error) { + if certificate != "" { + return []byte(certificate), nil + } else if certificatePath != "" { + content, err := os.ReadFile(certificatePath) + if err != nil { + return nil, E.Cause(err, "read certificate") + } + return content, nil + } + return nil, nil +} + +func loadCertAsPool(certificate string, certificatePath string) (*x509.CertPool, error) { + certBytes, err := loadCertAsBytes(certificate, certificatePath) + if err != nil { + return nil, err + } + if certBytes == nil { + return nil, nil + } + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(certBytes) { + return nil, E.New("failed to parse certificate:\n\n", certBytes) + } + return certPool, nil +} diff --git a/common/tls/ech_client.go b/common/tls/ech_client.go index 97903caa..8474b212 100644 --- a/common/tls/ech_client.go +++ b/common/tls/ech_client.go @@ -9,12 +9,11 @@ import ( "encoding/base64" "net" "net/netip" - "os" cftls "github.com/sagernet/cloudflare-tls" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing-dns" + dns "github.com/sagernet/sing-dns" E "github.com/sagernet/sing/common/exceptions" mDNS "github.com/miekg/dns" @@ -140,23 +139,32 @@ func NewECHClient(router adapter.Router, serverAddress string, options option.Ou return nil, E.New("unknown cipher_suite: ", cipherSuite) } } - var certificate []byte - if options.Certificate != "" { - certificate = []byte(options.Certificate) - } else if options.CertificatePath != "" { - content, err := os.ReadFile(options.CertificatePath) - if err != nil { - return nil, E.Cause(err, "read certificate") - } - certificate = content + certPool, err := loadCertAsPool(options.Certificate, options.CertificatePath) + if err != nil { + return nil, E.Cause(err, "load certificate") } - if len(certificate) > 0 { - certPool := x509.NewCertPool() - if !certPool.AppendCertsFromPEM(certificate) { - return nil, E.New("failed to parse certificate:\n\n", certificate) - } + if certPool != nil { tlsConfig.RootCAs = certPool } + clientCert, err := loadCertAsBytes(options.ClientCertificate, options.ClientCertificatePath) + if err != nil { + return nil, E.Cause(err, "load client certificate") + } + clientKey, err := loadCertAsBytes(options.ClientKey, options.ClientKeyPath) + if err != nil { + return nil, E.Cause(err, "load client certificate key") + } + if clientCert != nil && clientKey == nil { + return nil, E.New("Client certificate specified without a client key") + } else if clientCert == nil && clientKey != nil { + return nil, E.New("Client key specified without a client certificate") + } else if clientCert != nil && clientKey != nil { + clientKeyPair, err := cftls.X509KeyPair(clientCert, clientKey) + if err != nil { + return nil, E.Cause(err, "parse client certificate/key") + } + tlsConfig.Certificates = []cftls.Certificate{clientKeyPair} + } // ECH Config diff --git a/common/tls/std_client.go b/common/tls/std_client.go index 571602f3..b799aecb 100644 --- a/common/tls/std_client.go +++ b/common/tls/std_client.go @@ -5,7 +5,6 @@ import ( "crypto/x509" "net" "net/netip" - "os" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" @@ -107,22 +106,31 @@ func NewSTDClient(serverAddress string, options option.OutboundTLSOptions) (Conf return nil, E.New("unknown cipher_suite: ", cipherSuite) } } - var certificate []byte - if options.Certificate != "" { - certificate = []byte(options.Certificate) - } else if options.CertificatePath != "" { - content, err := os.ReadFile(options.CertificatePath) - if err != nil { - return nil, E.Cause(err, "read certificate") - } - certificate = content + certPool, err := loadCertAsPool(options.Certificate, options.CertificatePath) + if err != nil { + return nil, E.Cause(err, "load certificate") } - if len(certificate) > 0 { - certPool := x509.NewCertPool() - if !certPool.AppendCertsFromPEM(certificate) { - return nil, E.New("failed to parse certificate:\n\n", certificate) - } + if certPool != nil { tlsConfig.RootCAs = certPool } + clientCert, err := loadCertAsBytes(options.ClientCertificate, options.ClientCertificatePath) + if err != nil { + return nil, E.Cause(err, "load client certificate") + } + clientKey, err := loadCertAsBytes(options.ClientKey, options.ClientKeyPath) + if err != nil { + return nil, E.Cause(err, "load client certificate key") + } + if clientCert != nil && clientKey == nil { + return nil, E.New("Client certificate specified without a client key") + } else if clientCert == nil && clientKey != nil { + return nil, E.New("Client key specified without a client certificate") + } else if clientCert != nil && clientKey != nil { + clientKeyPair, err := tls.X509KeyPair(clientCert, clientKey) + if err != nil { + return nil, E.Cause(err, "parse client certificate/key") + } + tlsConfig.Certificates = []tls.Certificate{clientKeyPair} + } return &STDClientConfig{&tlsConfig}, nil } diff --git a/common/tls/std_server.go b/common/tls/std_server.go index 03730952..df729347 100644 --- a/common/tls/std_server.go +++ b/common/tls/std_server.go @@ -246,6 +246,14 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound tlsConfig.Certificates = []tls.Certificate{keyPair} } } + clientCA, err := loadCertAsPool(options.ClientCA, options.ClientCAPath) + if err != nil { + return nil, E.Cause(err, "load client CA") + } + if clientCA != nil { + tlsConfig.ClientCAs = clientCA + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + } return &STDServerConfig{ config: tlsConfig, logger: logger, diff --git a/common/tls/utls_client.go b/common/tls/utls_client.go index d54dd023..740fa54b 100644 --- a/common/tls/utls_client.go +++ b/common/tls/utls_client.go @@ -4,10 +4,8 @@ package tls import ( "crypto/tls" - "crypto/x509" "net" "net/netip" - "os" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/option" @@ -127,23 +125,32 @@ func NewUTLSClient(router adapter.Router, serverAddress string, options option.O return nil, E.New("unknown cipher_suite: ", cipherSuite) } } - var certificate []byte - if options.Certificate != "" { - certificate = []byte(options.Certificate) - } else if options.CertificatePath != "" { - content, err := os.ReadFile(options.CertificatePath) - if err != nil { - return nil, E.Cause(err, "read certificate") - } - certificate = content + certPool, err := loadCertAsPool(options.Certificate, options.CertificatePath) + if err != nil { + return nil, E.Cause(err, "load certificate") } - if len(certificate) > 0 { - certPool := x509.NewCertPool() - if !certPool.AppendCertsFromPEM(certificate) { - return nil, E.New("failed to parse certificate:\n\n", certificate) - } + if certPool != nil { tlsConfig.RootCAs = certPool } + clientCert, err := loadCertAsBytes(options.ClientCertificate, options.ClientCertificatePath) + if err != nil { + return nil, E.Cause(err, "load client certificate") + } + clientKey, err := loadCertAsBytes(options.ClientKey, options.ClientKeyPath) + if err != nil { + return nil, E.Cause(err, "load client certificate key") + } + if clientCert != nil && clientKey == nil { + return nil, E.New("Client certificate specified without a client key") + } else if clientCert == nil && clientKey != nil { + return nil, E.New("Client key specified without a client certificate") + } else if clientCert != nil && clientKey != nil { + clientKeyPair, err := utls.X509KeyPair(clientCert, clientKey) + if err != nil { + return nil, E.Cause(err, "parse client certificate/key") + } + tlsConfig.Certificates = []utls.Certificate{clientKeyPair} + } var id utls.ClientHelloID switch options.UTLS.Fingerprint { case "chrome", "": diff --git a/option/tls.go b/option/tls.go index 72a74966..685810d3 100644 --- a/option/tls.go +++ b/option/tls.go @@ -12,22 +12,28 @@ type InboundTLSOptions struct { CertificatePath string `json:"certificate_path,omitempty"` Key string `json:"key,omitempty"` KeyPath string `json:"key_path,omitempty"` + ClientCA string `json:"client_ca,omitempty"` + ClientCAPath string `json:"client_ca_path,omitempty"` ACME *InboundACMEOptions `json:"acme,omitempty"` } 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 Listable[string] `json:"alpn,omitempty"` - MinVersion string `json:"min_version,omitempty"` - MaxVersion string `json:"max_version,omitempty"` - CipherSuites Listable[string] `json:"cipher_suites,omitempty"` - Certificate string `json:"certificate,omitempty"` - CertificatePath string `json:"certificate_path,omitempty"` - ECH *OutboundECHOptions `json:"ech,omitempty"` - UTLS *OutboundUTLSOptions `json:"utls,omitempty"` + Enabled bool `json:"enabled,omitempty"` + DisableSNI bool `json:"disable_sni,omitempty"` + ServerName string `json:"server_name,omitempty"` + Insecure bool `json:"insecure,omitempty"` + ALPN Listable[string] `json:"alpn,omitempty"` + MinVersion string `json:"min_version,omitempty"` + MaxVersion string `json:"max_version,omitempty"` + CipherSuites Listable[string] `json:"cipher_suites,omitempty"` + Certificate string `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` + ClientCertificate string `json:"client_certificate,omitempty"` + ClientCertificatePath string `json:"client_certificate_path,omitempty"` + ClientKey string `json:"client_key,omitempty"` + ClientKeyPath string `json:"client_key_path,omitempty"` + ECH *OutboundECHOptions `json:"ech,omitempty"` + UTLS *OutboundUTLSOptions `json:"utls,omitempty"` } type OutboundECHOptions struct {